ai_pytorch 自用笔记

preliminaries

数据操作

resize 为深拷贝,不改变地址,更改变量,原张量的值也会变。

clone浅拷贝,分配新内存

axis=0[2,2,3] 变为[2,3],keepdim是[1,2,3]

A*B 按元素点乘

微积分

原理

导数→向量:梯度 image-20231127141329842

image-20231127142001522image-20231127142211521image-20231127142333022

矩阵求导,分子布局:

  1. 直观上讲,分子布局,就是分子是列向量形式,分母是行向量形式。分母布局则相反”。
  2. 分子布局的本质分子标量列向量、矩阵向量化后的列向量分母标量列向量转置后的行向量、矩阵的转置矩阵、矩阵向量化后的列向量转置后的行向量。”

image-20231127144223398

example:image-20231127155918180

自动求导

计算一个函数在指定值上的导数,将代码分解为操作子,根据链式法则一步步求导

image-20231127162454590image-20231127162701792image-20231127162941956

import torch

x = torch.arange(4.0)
x.requires_grad_(True)  # 等价于x=torch.arange(4.0,requires_grad=True)
"""
在我们计算y关于x的梯度之前,需要一个地方来存储梯度。 
重要的是,我们不会在每次对一个参数求导时都分配新的内存。 因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。 """
x.grad  # 默认值是None
y = 2 * torch.dot(x, x)  # y=2 * x.T * x
y.backward()  # x是一个长度为4的向量,计算x和x的点积,得到了我们赋值给y的标量输出。 接下来,通过调用反向传播函数来自动计算y关于x每个分量的梯度,并打印这些梯度。
print(x.grad == 4 * x)

# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
y = x.sum()
y.backward()
print(x.grad)

# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
print(x.grad)

# 分离计算
x.grad.zero_()
y = x * x
u = y.detach()  # u不再是关于x的函数,而是常数
z = u * x  # 相当于z = 常数 * x,即z`= u
z.sum().backward()
print(x.grad == u)


# 使用自动微分的一个好处是: 即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),
# 我们仍然可以计算得到的变量的梯度。 在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。
def f(a):
    b = a * 2
    while b.norm() < 1000:
        b = b * 2
    if b.sum() > 0:
        c = b
    else:
        c = 100 * b
    return c


a = torch.randn(size=(), requires_grad=True)
d = f(a)  # 无论a的值,d = 常数  * a
d.backward()
print(a.grad == d / a)

"总结:深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。"

  1. 在运行反向传播函数后,立即再次运行,会发生什么?

通过观察上面的计算图可以发现一个很重要的点:

pytorch在利用计算图求导的过程中根节点都是一个标量,即一个数。当根节点即函数的因变量为一个向量的时候,会构建多个计算图对该向量中的每一个元素分别进行求导,这也就引出了下一节的内容

这里顺带说一下:

pytoch构建的计算图是动态图,为了节约内存,所以每次一轮迭代完也即是进行了一次backward函数计算之后计算图就被在内存释放,因此如果你需要多次backward只需要在第一次反向传播时候添加一个retain_graph=True标识,让计算图不被立即释放。实际上文档中retain_graph和create_graph两个参数作用相同,因为前者是保持计算图不释放,而后者是创建计算图,因此如果我们不想要计算图释放掉,将任意一个参数设置为True都行。

线性神经网络

线性回归

线性模型:

给定一个数据集,我们的目标是寻找模型的权重w和偏置b, 使得根据模型做出的预测大体符合数据里的真实价格。 输出的预测值由输入特征通过线性模型的仿射变换决定,仿射变换由所选权重和偏置确定。 y ^ = w 1 x 1 + . . . + w d x d + b . \hat{y} = w_1 x_1 + ... + w_d x_d + b. y^=w1x1+...+wdxd+b.

点积形式: y ^ = w ⊤ x + b . \hat{y} = \mathbf{w}^\top \mathbf{x} + b. y^=wx+b.

对于数据集 X ∈ R n × d \mathbf{X} \in \mathbb{R}^{n \times d} XRn×d 行是样本,列是特征有: y ^ = X w + b {\hat{\mathbf{y}}} = \mathbf{X} \mathbf{w} + b y^=Xw+b

损失函数:

损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。 回归问题中最常用的损失函数是平方误差函数。

为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和:

L ( w , b ) = 1 n ∑ i = 1 n l ( i ) ( w , b ) = 1 n ∑ i = 1 n 1 2 ( w ⊤ x ( i ) + b − y ( i ) ) 2 . L(\mathbf{w}, b) =\frac{1}{n}\sum_{i=1}^n l^{(i)}(\mathbf{w}, b) =\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2. L(w,b)=n1i=1nl(i)(w,b)=n1i=1n21(wx(i)+by(i))2.

在训练模型时,我们希望寻找一组参数 w ∗ , b ∗ \mathbf{w}^*, b^* w,b

w ∗ , b ∗ = argmin ⁡ w , b   L ( w , b ) . \mathbf{w}^*, b^* = \operatorname*{argmin}_{\mathbf{w}, b}\ L(\mathbf{w}, b). w,b=argminw,b L(w,b).

首先,我们将偏置b合并到参数w中,合并方法是在包含所有参数的矩阵中附加一列。所以有显示解 w ∗ = ( X ⊤ X ) − 1 X ⊤ y . \mathbf{w}^* = (\mathbf X^\top \mathbf X)^{-1}\mathbf X^\top \mathbf{y}. w=(XX)1Xy.

线性回归这样的简单问题存在解析解,但并不是所有的问题都存在解析解。

总结:image-20231129131048593

基础优化方法

image-20231129131359191 n是学习率image-20231129131723874image-20231129132207041

线性回归实现

import math
import random
import torch
import torch
import matplotlib.pyplot as plt

def synthetic_data(w, b, num_examples):  # @save
    """生成y=Xw+b+噪声"""
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape)
    return X, y.reshape((-1, 1))


true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
plt.figure("数据")
plt.plot(features[:, (1)].detach(), labels.detach(), ".")
# plt.show()

"定义一个data_iter函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。 每个小批量包含一组特征和标签"


def data_iter(batch_size, features, labels):
    num_examples = len(features)
    indices = list(range(num_examples))
    # 这些样本是随机读取的,没有特定的顺序
    random.shuffle(indices)
    for i in range(0, num_examples, batch_size):
        batch_indices = torch.tensor(
            indices[i: min(i + batch_size, num_examples)])
        yield features[batch_indices], labels[batch_indices]


batch_size = 10
for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break

"初始化模型参数"
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

"定义模型"


def linreg(X, w, b):  # @save
    """线性回归模型"""
    return torch.matmul(X, w) + b


"""定义loss函数"""


def squared_loss(y_hat, y):  # @save
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2  # **表示平房


"""
定义优化算法,lr为学习率.在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 
接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。
每一步更新的大小由学习速率lr决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。"
"""


def sgd(params, lr, batch_size):  # @save
    """小批量随机梯度下降"""
    with torch.no_grad(): # 用于执行其包裹的代码块时禁用梯度计算。在这个上下文中,任何张量操作都不会被记录用于梯度计算,即不会在计算图中建立梯度信息。
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()


"""训练过程"""
lr = 0.01
num_epochs = 10
net = linreg
loss = squared_loss

for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y)  # X和y的小批量损失
        # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
        # 并以此计算关于[w,b]的梯度
        l.sum().backward()
        sgd([w, b], lr, batch_size)  # 使用参数的梯度更新参数
    with torch.no_grad():
        train_l = loss(net(features, w, b), labels)
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')

间接实现

import numpy as np
import torch
from torch.utils import data


def synthetic_data(w, b, num_examples):  # @save
    """生成y=Xw+b+噪声"""
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape)
    return X, y.reshape((-1, 1))


true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)


# """我们可以调用框架中现有的API来读取数据。
# 我们将features和labels作为API的参数传递,并通过数据迭代器指定batch_size。 此外,布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。"""
def load_array(data_arrays, batch_size, is_train=True):  # @save
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)


batch_size = 10
data_iter = load_array((features, labels), batch_size)
print(next(iter(data_iter)))

# nn是神经网络的缩写
from torch import nn

"""
torch.nn.Sequential 是 PyTorch 中的一个容器模块,用于构建神经网络层的有序序列。它允许按照顺序添加和组织神经网络的各个层。
# 们将两个参数传递到nn.Linear中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。也叫全连接层
# 这一单层被称为全连接层(fully-connected layer), 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。
"""
net = nn.Sequential(nn.Linear(2, 1))

# 初始化模型参数
"""
在使用net之前,我们需要初始化模型参数。 如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 
在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。

正如我们在构造nn.Linear时指定输入和输出尺寸一样, 现在我们能直接访问参数以设定它们的初始值。
 我们通过net[0]选择网络中的第一个图层, 然后使用weight.data和bias.data方法访问参数。 
我们还可以使用替换方法normal_和fill_来重写参数值。
"""
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

print(net[0].weight.data, net[0].bias.data)

# 定义损失函数
"计算均方误差使用的是MSELoss类,也称为L2范数。 默认情况下,它返回所有样本损失的平均值。"
loss = nn.MSELoss()

# 定义优化算法
"""小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim模块中实现了该算法的许多变种。 
当我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。
 小批量随机梯度下降只需要设置lr值,这里设置为0.03。"""
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

# 训练
"""
回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(train_data), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:

通过调用net(X)生成预测并计算损失l(前向传播)。

通过进行反向传播来计算梯度。

通过调用优化器来更新模型参数。

为了更好的衡量训练效果,我们计算每个迭代周期后的损失,并打印它来监控训练过程。
"""
num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        # net(X) 前向传播(Forward Pass): 将输入数据传递给模型,计算模型的预测值。
        l = loss(net(X), y)  # 计算损失(Compute Loss): 将模型的预测值与真实标签进行比较,计算损失值。
        l.backward()  # 反向传播(Backward Pass): 计算相对于模型参数的损失梯度。
        trainer.step()  # 优化器更新(Optimizer Update): 优化器使用损失梯度来更新模型的参数,这一步就是 trainer.step() 的作用。
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

  • 为什么使用平方损失而不是绝对值:最早使用是因为可导

  • 损失为什么要求平均:不管批量大小,梯度的值都差不多.由公式 w t = w t − 1 − η ∂ ℓ ∂ w t − 1 \mathbf{w}_t=\mathbf{w}_{t-1}-\eta\frac{\partial\ell}{\partial\mathbf{w}_{t-1}} wt=wt1ηwt1 不求平均,学习率除就行了

  • batchsize大小:

softmax回归

回归估计一个连续值,分类预测一个离散类别

image-20231129184603862 $$ \mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b} $$
  • 对类别进行有效编码, 独热编码 (one-hot encoding) 。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。在我们的例子中,标签 y y y将是一个三维向量,其中(1,0,0)对应于“猫”、(0,1,0)对应于“鸡”、(0,0,1)对应于“狗”: y ∈ { ( 1 , 0 , 0 ) , ( 0 , 1 , 0 ) , ( 0 , 0 , 1 ) } . y\in\{(1,0,0),(0,1,0),(0,0,1)\} . y{(1,0,0),(0,1,0),(0,0,1)}.
  • 使用均方损失训练
  • 最大值为预测 y ^ = argmax ⁡ o i \hat{y}=\operatorname*{argmax}o_i y^=argmaxoi

image-20231129192135681 y i y_i yi则是概率

  • 交叉熵常用来衡量两个概率的区别 H ( p , q ) = ∑ i − p i log ⁡ ( q i ) H(\mathbf{p},\mathbf{q})=\sum_i-p_i\log(q_i) H(p,q)=ipilog(qi)
  • 将它作为损失函数:

l ( y , y ^ ) = − ∑ i y i log ⁡ y ^ i = − log ⁡ y ^ y l(\mathbf{y},\hat{\mathbf{y}})=-\sum_iy_i\log\hat{y}_i=-\log\hat{y}_y l(y,y^)=iyilogy^i=logy^y

  • 其梯度是真实概率和预测概率的区别

∂ o i l ( y , y ^ ) = s o f t m a x ( o ) i − y i \partial_{o_i}l(\mathbf{y},\hat{\mathbf{y}})=\mathsf{softmax}(\mathbf{o})_i-y_i oil(y,y^)=softmax(o)iyi

l ( y , y ^ ) = − ∑ i y i l o g y i ^ = − ∑ i y i l o g e x p ( o i ) ∑ j e x p ( o j ) = − ∑ i y i ( o i − l o g ( ∑ j e x p ( o j ) ) ) = − ∑ i y i o i + ∑ i y i l o g ( ∑ j e x p ( o j ) ) = − ∑ i y i o i + l o g ( ∑ j e x p ( o j ) ) ∂ l ( y , y ^ ) ∂ o i = − y i + e x p ( o i ) ∑ j e x p ( o j ) = s o f t m a x ( o i ) − y i \begin{equation} \begin{aligned} l(\mathbf{y},\mathbf{\hat{y}})& =-\sum_iy_ilog\hat{y_i} \\ &=-\sum_iy_ilog\frac{exp(o_i)}{\sum_jexp(o_j)} \\ &=-\sum_iy_i(o_i-log(\sum_jexp(o_j))) \\ &=-\sum_iy_io_i+\sum_iy_ilog(\sum_jexp(o_j)) \\ &=-\sum_iy_io_i+log(\sum_jexp(o_j)) \\ \frac{\partial l(\mathbf{y},\mathbf{\hat{y}})}{\partial o_i}& =-y_i+\frac{exp(o_i)}{\sum_jexp(o_j)} \\ &=softmax(o_i)-y_i \end{aligned} \end{equation} l(y,y^)oil(y,y^)=iyilogyi^=iyilogjexp(oj)exp(oi)=iyi(oilog(jexp(oj)))=iyioi+iyilog(jexp(oj))=iyioi+log(jexp(oj))=yi+jexp(oj)exp(oi)=softmax(oi)yi

总结:

  • Softmax 回归是一个多类分类模型

  • 使用 Softmax 操作子得到每个类的预测置信度

  • 使用交叉熵来来衡量预测和标号的区别

损失函数

L2loss norm L2范数 l ( y , y ′ ) = 1 2 ( y − y ′ ) 2 l(y,y^{\prime})=\frac12(y-y^{\prime})^2 l(y,y)=21(yy)2

L2loss l ( y , y ′ ) = ∣ y − y ′ ∣ l(y,y^{\prime})=|y-y^{\prime}| l(y,y)=yy

loss l ( y , y ′ ) = { ∣ y − y ′ ∣ − 1 2 if ∣ y − y ′ ∣ > 1 1 2 ( y − y ′ ) 2 otherwise l(y,y')=\begin{cases}|y-y'|-\dfrac{1}{2}&\text{if}|y-y'|>1\\\frac{1}{2}(y-y')^2&\text{otherwise}\end{cases} l(y,y)= yy2121(yy)2ifyy>1otherwise

softmax从零实现

import matplotlib.pyplot as plt
import torch
from IPython import display
from d2l import torch as d2l

# 加载数据
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# 初始化模型
"和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是28x28的图像。 本节将展平每个图像,把它们看作长度为784的向量"
num_inputs = 784
num_outputs = 10  # 类别

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

# 实现softmax,由公式可得
def softmax(X):
    X_exp = torch.exp(X) # 假设X.shpae:[2,3]
    partition = X_exp.sum(1, keepdim=True) # partition.shape:[2,1]
    return X_exp / partition  # 这里应用了广播机制,[2,3] /[2,1]

# X = torch.normal(0, 1, (2, 5))
# X_prob = softmax(X)
# print(X_prob, X_prob.sum(1, keepdims=True))

# 实现softmax回归模型
" o = x W  + b"
def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

# 定义损失函数
"""
我们创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率,以及它们对应的标签y。 有了y,我们知道在第一个样本中,第一类是正确的预测; 
而在第二个样本中,第三类是正确的预测。 
然后使用y作为y_hat中概率的索引, 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率
y = torch.tensor([0, 2]) # 真实类别
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]]) 
y_hat[[0, 1], y] # 即预测值对真实类别的概率
"""
def cross_entropy(y_hat, y): # 实现交叉熵损失函数
    return - torch.log(y_hat[range(len(y_hat)), y])


# 将预测类别与true比较,精度
"给定预测概率分布y_hat,当我们必须输出硬预测(hard prediction)时, 我们通常选择预测概率最高的类。"
"""当预测与标签分类y一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。 
虽然直接优化精度可能很困难(因为精度的计算不可导), 但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。

为了计算精度,我们执行以下操作。 首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。 
我们使用argmax获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y元素进行比较。 由于等式运算符“==”对数据类型很敏感,
 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。
"""
def accuracy(y_hat, y):  #@save
    """计算预测正确的数量"""
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis=1) # 返回的是a中元素最大值所对应的索引值
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())
# 精度计算则为accuracy(y_hat, y) / len(y)

# 评估模型的精度
def evaluate_accuracy(net, data_iter):  #@save
    """计算在指定数据集上模型的精度"""
    if isinstance(net, torch.nn.Module):
        net.eval()  # 将模型设置为评估模式
    metric = Accumulator(2)  # 迭代器: [正确预测,预测总数]
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

class Accumulator:  #@save
    """在n个变量上累加"""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx): #[]的重载
        return self.data[idx]

# 一个epoch训练
def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """训练模型一个迭代周期(定义见第3章)"""
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module): # isinstance() 函数来判断一个对象是否是一个已知的类型,类似 type()。
        net.train()
    # 训练损失总和、训练准确度总和、样本数
    metric = Accumulator(3)
    for X, y in train_iter:
        # 计算梯度并更新参数
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回训练损失和训练精度
    return metric[0] / metric[2], metric[1] / metric[2]

# 训练函数
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    """训练模型(定义见第3章)"""
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                        legend=['train loss', 'train acc', 'test acc']) # 可视化

    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))

    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc

lr = 0.1

# 优化函数
def updater(batch_size):
    return d2l.sgd([W, b], lr, batch_size)

num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
d2l.plt.show()

import torch
from torch import nn
from d2l import torch as d2l

# 加载数据
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

#初始化模型
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状 把任何一个n-d的tensor 转化为2-d 的tensor,第0维度保留
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

"""定义weight_init函数,并在weight_init中通过判断模块的类型来进行不同的参数初始化定义类型。
model=Net(…) 创建网络结构
model.apply(weight_init),将weight_init初始化方式应用到submodels上"""
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights)

#损失函数
loss = nn.CrossEntropyLoss(reduction='none')

# 优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)

# 训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
d2l.plt.show()

多层感知机

  • 感知机:

image-20231130172922939image-20231130173421983

最多做一个二分类问题,只能产生线性分割面,不能拟合XOR函数

  • 多层感知机

XOR:异或,10=0,11=1,00=1image-20231204155644383

我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。 要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。 我们可以把前L−1层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP

image-20231204155806926 image-20231204160126749

m是隐藏层的大小, 输入时n维, 输出一个分类.

为什么需要非线性激活函数: 若是线性, o仍是关于x的线性函数

s i g m o i d ( x ) = 1 1 + exp ⁡ ( − x ) \mathrm{sigmoid}(x)=\frac1{1+\exp(-x)} sigmoid(x)=1+exp(x)1 是一个软的 σ ( x ) = { 1 if  x > 0 0 otherwise \sigma(x)=\begin{cases}1&\text{if } x>0\\0&\text{otherwise}\end{cases} σ(x)={10if x>0otherwise , 可以求导

tanh ⁡ ( x ) = 1 − exp ⁡ ( − 2 x ) 1 + exp ⁡ ( − 2 x ) \tanh(x)=\frac{1-\exp(-2x)}{1+\exp(-2x)} tanh(x)=1+exp(2x)1exp(2x) 将输入投影到(-1, 1)

R e L U ( x ) = max ⁡ ( x , 0 ) \mathrm{ReLU}(x)=\max(x,0) ReLU(x)=max(x,0)

image-20231204162201136

从零实现

import torch
from torch import nn
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# 初始化
"""
忽略像素之间的空间结构, 我们可以将每个图像视为具有784个输入特征 和10个类的简单分类数据集。 
首先,我们将实现一个具有单隐藏层的多层感知机, 它包含256个隐藏单元。
注意,我们可以将这两个变量都视为超参数。
通常,我们选择2的若干次幂作为层的宽度。 因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。
实现一个具有单隐藏层的多层感知机: 对于每一层我们都要记录一个权重矩阵和一个偏置向量。 跟以前一样,我们要为损失关于这些参数的梯度分配内存。
"""
num_inputs, num_outputs, num_hiddens = 784, 10, 256

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]


# relu函数
def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)

# 模型
def net(X):
    X = X.reshape((-1, num_inputs)) # 平展层
    H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法
    return (H@W2 + b2)
# loss
loss = nn.CrossEntropyLoss(reduction='none')

# 训练
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
d2l.plt.show()

简洁实现

import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(nn.Flatten(),
                    nn.Linear(784, 256),
                    nn.ReLU(),
                    nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);


batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
d2l.plt.show()

模型选择

·训练误差: 模型在训练数据上的误差

·泛化误差: 模型在新数据上的误差
·例子: 根据摸考成绩来预测未来考试分数

·在过去的考试中表现很好 (训练误差) 不代表未来老试一定会好 (泛化误差)
·学生 A 通过背书在摸考中拿到很好成绩
·学生 B 知道答案后面的原因

K-则交叉验证

·在没有足够多数据时使用 (这是常态)
·算法:

·将训练数据分割成 K 块·

Fori=1,…,K

·使用第 i 块作为验证数据集,其余的作为训练· 报告 K个验证集误差的平均

·常用: K=5 或 10

过拟合和欠拟合

image-20231207154124721image-20231207154516603

VC维

统计学习理论的一个核心思想, 对于一个分类模型,VC等于一个最大的数据集的大小,不管如何给定标号,都存在一个模型来对它进行完美分类

2维输入的感知机,VC 维 = 3, 能够分类任何三个点,但不是4个 (xor)

总结:

  • 模型容量需要匹配数据复杂度,否则可能导致欠拟合和过拟合
  • 统计机器学习提供数学工具来衡量模型复杂度
  • 实际中一般靠观察训练误差和验证误差

权重衰退

常用的用来处理过拟合的方法, 通过限制参数值的选择范围来控制模型容量.

硬性限制:

$\min\mathrm{~}\mathit{\left.\ell(\mathbf{w},b)\right.}\ \ \ \ \ \ |\mathbf{w}|^2\leq\theta $

柔性限制:

image-20231207163044630 image-20231207164153375
import torch
from torch import nn
from d2l import torch as d2l

n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)


def train_concise(wd):
    net = nn.Sequential(nn.Linear(num_inputs, 1))
    for param in net.parameters():
        param.data.normal_()
    loss = nn.MSELoss(reduction='none')
    num_epochs, lr = 100, 0.003
    # 偏置参数没有衰减
    trainer = torch.optim.SGD([
        {"params": net[0].weight, 'weight_decay': wd},
        {"params": net[0].bias}], lr=lr)
    animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
                            xlim=[5, num_epochs], legend=['train', 'test'])
    for epoch in range(num_epochs):
        for X, y in train_iter:
            trainer.zero_grad()
            l = loss(net(X), y)
            l.mean().backward()
            trainer.step()
        if (epoch + 1) % 5 == 0:
            animator.add(epoch + 1,
                         (d2l.evaluate_loss(net, train_iter, loss),
                          d2l.evaluate_loss(net, test_iter, loss)))
    # print('w的L2范数:', net[0].weight.norm().item())
    print('w的L2范数:', net[0].weight.norm())


train_concise(3)
d2l.plt.show()

dropout暂退法

一个好的模型需要对输入数据的扰动鲁棒

  • 使用有噪音的数据
  • 丢弃法:在层之间加入噪音

丢弃法: 作用在隐藏全连接层的输出上

image-20231211152738815

正则项只在训练中使用

image-20231211153310849

从零实现

import torch
from torch import nn
from d2l import torch as d2l


def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1
    # 在本情况中,所有元素都被丢弃
    if dropout == 1:
        return torch.zeros_like(X)
    # 在本情况中,所有元素都被保留
    if dropout == 0:
        return X
    mask = (torch.rand(X.shape) > dropout).float()  # 做乘法要快
    return mask * X / (1.0 - dropout)


X = torch.arange(16, dtype=torch.float32).reshape((2, 8))
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5))
print(dropout_layer(X, 1.))

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256


# 定义具有两个隐藏层的多层感知机
# 我们可以将暂退法应用于每个隐藏层的输出(在激活函数之后), 并且可以为每一层分别设置暂退概率:
# 常见的技巧是在靠近输入层的地方设置较低的暂退概率。
# 下面的模型将第一个和第二个隐藏层的暂退概率分别设置为0.2和0.5, 并且暂退法只在训练期间有效。
dropout1, dropout2 = 0.2, 0.5


class Net(nn.Module):
    def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
                 is_training=True):# 区别是否训练
        super(Net, self).__init__()
        self.num_inputs = num_inputs
        self.training = is_training
        self.lin1 = nn.Linear(num_inputs, num_hiddens1)
        self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
        self.lin3 = nn.Linear(num_hiddens2, num_outputs)
        self.relu = nn.ReLU()

    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs)))) # 第一个隐藏层的输出
        # 只有在训练模型时才使用dropout
        if self.training == True:
            # 在第一个全连接层之后添加一个dropout层
            H1 = dropout_layer(H1, dropout1)
        H2 = self.relu(self.lin2(H1))
        if self.training == True:
            # 在第二个全连接层之后添加一个dropout层
            H2 = dropout_layer(H2, dropout2)
        out = self.lin3(H2)
        return out


net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)


num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
d2l.plt.show()

简洁实现

import torch
from torch import nn
from d2l import torch as d2l


def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1
    # 在本情况中,所有元素都被丢弃
    if dropout == 1:
        return torch.zeros_like(X)
    # 在本情况中,所有元素都被保留
    if dropout == 0:
        return X
    mask = (torch.rand(X.shape) > dropout).float()  # 做乘法要快
    return mask * X / (1.0 - dropout)

dropout1, dropout2 = 0.2, 0.5

net = nn.Sequential(nn.Flatten(),
        nn.Linear(784, 256),
        nn.ReLU(),
        # 在第一个全连接层之后添加一个dropout层
        nn.Dropout(dropout1),
        nn.Linear(256, 256),
        nn.ReLU(),
        # 在第二个全连接层之后添加一个dropout层
        nn.Dropout(dropout2),
        nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);


num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
d2l.plt.show()

数值稳定性和模型初始化

梯度爆炸:学习率大小

梯度消失:sigmoid激活函数

两种情况下梯度消失经常出现,一是在深层网络中,二是采用了不合适的损失函数,比如sigmoid。梯度爆炸一般出现在深层网络和权值初始化值太大的情况下,下面分别从这两个角度分析梯度消失和爆炸的原因。

让训练更稳定:

  • 让梯度在合理的范围内

  • 将乘法变加法 resnet,lstm

  • 梯度处理

  • 合理的权重初始化和激活函数

image-20231212143837397

预测kaggle房价

import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l

train_data = pd.read_csv('../data/house-prices-advanced-regression-techniques/train.csv')
test_data = pd.read_csv('../data/house-prices-advanced-regression-techniques/test.csv')
print(train_data.shape)
print(test_data.shape)
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])

# 数据预处理
'去掉id'
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

# 若无法获得测试数据,则可根据训练数据计算均值和标准差
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
    lambda x: (x - x.mean()) / (x.std()))
# 在标准化数据之后,所有均值消失,因此我们可以将缺失值设置为0
all_features[numeric_features] = all_features[numeric_features].fillna(0)

# “Dummy_na=True”将“na”(缺失值)视为有效的特征值,并为其创建指示符特征 ,处理离散值
'''
举例说明,如果原始数据框中有一个名为 "Color" 的列,包含红色、蓝色和绿色这三种类别,经过独热编码后,
可能会生成三列 "Color_Red"、"Color_Blue" 和 "Color_Green",其中每一列对应一种颜色,值为 0 或 1。
参数 dummy_na=True 表示在生成独热编码时,将缺失值(NaN)也视为一种特殊的类别。
'''
all_features = pd.get_dummies(all_features, dummy_na=True)

# 可以看到此转换会将特征的总数量从79个增加到331个。
# 最后,通过values属性,我们可以从pandas格式中提取NumPy格式,并将其转换为张量表示用于训练。
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32)
train_labels = torch.tensor(
    train_data.SalePrice.values.reshape(-1, 1), dtype=torch.float32)

# 训练
loss = nn.MSELoss()
in_features = train_features.shape[1]


def get_net():
    net = nn.Sequential(nn.Linear(in_features, 1))
    return net


# 房价就像股票价格一样,我们关心的是相对数量,而不是绝对数量(否则更贵的房子的权重就会越大)。 因此,我们更关心相对误差
# 解决这个问题的一种方法是用价格预测的对数来衡量差异
# |\log y - \log \hat{y}|
def log_rmse(net, features, labels):
    # 为了在取对数时进一步稳定该值,将小于1的值设置为1
    clipped_preds = torch.clamp(net(features), 1, float('inf'))  # 用于将神经网络的预测值限制在一个范围内
    rmse = torch.sqrt(loss(torch.log(clipped_preds),
                           torch.log(labels)))  # loss 均方误差
    return rmse.item()


# 我们的训练函数将借助Adam优化器 (我们将在后面章节更详细地描述它)。 Adam优化器的主要吸引力在于它对初始学习率不那么敏感。
def train(net, train_features, train_labels, test_features, test_labels,
          num_epochs, learning_rate, weight_decay, batch_size):
    train_ls, test_ls = [], []
    train_iter = d2l.load_array((train_features, train_labels), batch_size)
    # 这里使用的是Adam优化算法
    optimizer = torch.optim.Adam(net.parameters(),
                                 lr=learning_rate,
                                 weight_decay=weight_decay)
    for epoch in range(num_epochs):
        for X, y in train_iter:
            optimizer.zero_grad()
            l = loss(net(X), y)  # 这里
            l.backward()
            optimizer.step()
        train_ls.append(log_rmse(net, train_features, train_labels))
        if test_labels is not None:
            test_ls.append(log_rmse(net, test_features, test_labels))
    return train_ls, test_ls


# K折交叉验证, 它有助于模型选择和超参数调整。 我们首先需要定义一个函数,k折交叉验证过程中返回第i折的数据。
# 具体地说,它选择第i个切片作为验证数据,其余部分作为训练数据。 注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,会有其他解决办法。
def get_k_fold_data(k, i, X, y):
    """
    k: 表示将数据集分成的折数,通常是大于 1 的整数。
    i: 表示当前是第几折,取值范围是 0 到 k-1。
    X: 输入特征的数据集。
    y: 对应的标签。
    """
    assert k > 1
    fold_size = X.shape[0] // k  # //整除
    X_train, y_train = None, None
    for j in range(k):
        idx = slice(j * fold_size, (j + 1) * fold_size)  # 使用切片操作获取当前折的数据索引。
        X_part, y_part = X[idx, :], y[idx]
        if j == i:  # 如果当前折是验证集,将其分配给验证集
            X_valid, y_valid = X_part, y_part
        elif X_train is None:  # 如果当前折是训练集,并且训练集还没有被初始化,将当前折的数据赋给训练集。
            X_train, y_train = X_part, y_part
        else:  # 如果当前折是训练集,并且训练集已经初始化,将当前折的数据拼接到训练集上。
            X_train = torch.cat([X_train, X_part], 0)
            y_train = torch.cat([y_train, y_part], 0)
    return X_train, y_train, X_valid, y_valid


# 当我们在 k折交叉验证中训练k次后,返回训练和验证误差的平均值。
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,
           batch_size):
    train_l_sum, valid_l_sum = 0, 0
    for i in range(k):
        data = get_k_fold_data(k, i, X_train, y_train)
        net = get_net()
        train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
                                   weight_decay, batch_size)
        train_l_sum += train_ls[-1]
        valid_l_sum += valid_ls[-1]
        if i == 0:
            d2l.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],
                     xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],
                     legend=['train', 'valid'], yscale='log')
        print(f'折{i + 1},训练log rmse{float(train_ls[-1]):f}, '
              f'验证log rmse{float(valid_ls[-1]):f}')
    return train_l_sum / k, valid_l_sum / k



k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr,
                          weight_decay, batch_size)
print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, '
      f'平均验证log rmse: {float(valid_l):f}')


def train_and_pred(train_features, test_features, train_labels, test_data,
                   num_epochs, lr, weight_decay, batch_size):
    net = get_net()
    train_ls, _ = train(net, train_features, train_labels, None, None,
                        num_epochs, lr, weight_decay, batch_size)
    d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',
             ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
    print(f'训练log rmse:{float(train_ls[-1]):f}')
    # 将网络应用于测试集。
    preds = net(test_features).detach().numpy()
    # 将其重新格式化以导出到Kaggle
    test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
    submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
    submission.to_csv('submission.csv', index=False)
train_and_pred(train_features, test_features, train_labels, test_data,
               num_epochs, lr, weight_decay, batch_size)

d2l.plt.show()




深度学习计算

模型构造

import torch
from torch import nn
from torch.nn import functional as F

# 块
'nn.Sequential就是一种特殊的Module'
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))


'自定义块'
class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Module的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__()
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))



'''
顺序块, 看看Sequential类是如何工作的
 为了构建我们自己的简化的MySequential, 我们只需要定义两个关键函数:
一种将块逐个追加到列表中的函数;
一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
'''
class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
            # 变量_modules中。_module的类型是OrderedDict
            self._modules[str(idx)] = module

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for block in self._modules.values():
            X = block(X)
        return X

net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(torch.rand(2,20))

"""
然而,并不是所有的架构都是简单的顺序架构。 当需要更强的灵活性时,我们需要定义自己的块.
我们可能希望合并既不是上一层的结果也不是可更新参数的项, 我们称之为常数参数(constant parameter)。 例如,我们需要一个计算函数
在正向传播中执行代码
"""
class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 不计算梯度的随机权重参数。因此其在训练期间保持不变
        self.rand_weight = torch.rand((20, 20), requires_grad=False)
        self.linear = nn.Linear(20, 20)

    def forward(self, X):
        X = self.linear(X)
        # 使用创建的常量参数以及relu和mm函数
        X = F.relu(torch.mm(X, self.rand_weight) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数
        X = self.linear(X)
        # 控制流
        while X.abs().sum() > 1:
            X /= 2
        return X.sum()


net = nn.Sequential(nn.Flatten(0))
net(torch.rand(2,20))

参数管理

import torch
from torch import nn

# 单隐藏层的多层感知机
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)

# 参数访问
print(net[2].state_dict())
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)

# 一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])


# 嵌套参数
def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                         nn.Linear(8, 4), nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        # 在这里嵌套
        net.add_module(f'block {i}', block1())
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
print(rgnet)


# 参数初始化
"""
内置初始化,默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 
这个范围是根据输入和输出维度计算出的。 PyTorch的nn.init模块提供了多种预置初始化方法。
下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0
"""
def init_normal(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
net.apply(init_normal)
print(net)
print(net[0].weight.data[0], net[0].bias.data[0])

def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)
        nn.init.zeros_(m.bias)
net.apply(init_constant)
print(net[0].weight.data[0], net[0].bias.data[0])

"某些块应用不同的初始化方法。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。"
def init_xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)

net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

"自定义初始化"
def my_init(m):
    if type(m) == nn.Linear:
        print("Init", *[(name, param.shape)
                        for name, param in m.named_parameters()])
        nn.init.uniform_(m.weight, -10, 10)
        m.weight.data *= m.weight.data.abs() >= 5

net.apply(my_init)
print(net[0].weight[:2])

"直接设置参数"
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]

#  参数绑定
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

自定义层

import torch
import torch.nn.functional as F
from torch import nn

# 构造一个没有任何参数的自定义层
class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, X):
        return X - X.mean()

layer = CenteredLayer()
print(layer(torch.FloatTensor([1, 2, 3, 4, 5])))

"现在,我们可以将层作为组件合并到更复杂的模型中。"
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
Y = net(torch.rand(4, 8))
Y.mean()

# 带参数的图层, 自定义全连接层
class MyLinear(nn.Module):
    def __init__(self, in_units, units):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(in_units, units))
        self.bias = nn.Parameter(torch.randn(units,))
    def forward(self, X):
        linear = torch.matmul(X, self.weight.data) + self.bias.data
        return F.relu(linear)

linear = MyLinear(5, 3)
print(linear.weight)

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
print(net(torch.rand(2, 64)))

读写文件

import torch
from torch import nn
from torch.nn import functional as F

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(20, 256)
        self.output = nn.Linear(256, 10)

    def forward(self, x):
        return self.output(F.relu(self.hidden(x)))

net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
"模型的定义不好存, 但可以存权重. 模型的参数存储在一个叫做“mlp.params”的文件中"
torch.save(net.state_dict(), 'mlp.params')

"实例化了原始多层感知机模型的一个备份,加载参数"
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
print(clone)

Y_clone = clone(X)
print(Y_clone == Y)

GPU

import torch
from torch import nn

print(torch.cuda.device_count())

def try_gpu(i=0):  #@save
    """如果存在,则返回gpu(i),否则返回cpu()"""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

def try_all_gpus():  #@save
    """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
    devices = [torch.device(f'cuda:{i}')
             for i in range(torch.cuda.device_count())]
    return devices if devices else [torch.device('cpu')]

print(try_gpu(), try_gpu(10), try_all_gpus())

# 查询张量所在的设备。 默认情况下,张量是在CPU上创建的。
x = torch.tensor([1, 2, 3])
print(x.device)

# 存储在GPU上
X = torch.ones(2, 3, device=try_gpu())
print(X)

# 神经网络模型可以指定设备。 下面的代码将模型参数放在GPU上
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())
print(net[0].weight.data)

卷积神经网络

全连接层到卷积

猫狗分类的例子中:假设我们有一个足够充分的照片数据集,数据集中是拥有标注的照片,每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。 即使将隐藏层维度降低到1000,这个全连接层也将有 1 0 9 10^9 109个参数

适合于计算机视觉的神经网络架构。

  1. 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
  2. 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。

卷积层

image-20231218103348253image-20231218103440259

  • 核矩阵的大小是超参数

实现

import torch
from torch import nn
from d2l import torch as d2l

# 自定义卷积核的运算
def corr2d(X, K):  #@save
    """计算二维互相关运算"""
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # 输出维度
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
    return Y

X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
print(corr2d(X, K))

# 实现二维卷积层
"""
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 
所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。 就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。
"""
class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(self, x):
        return corr2d(x, self.weight) + self.bias

# 边缘检测
X = torch.ones((6, 8))
X[:, 2:6] = 0
print(X)
#接下来,我们构造一个高度为1宽度为2的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。
K = torch.tensor([[1.0, -1.0]])
Y = corr2d(X, K)
print(Y)

# 如何学习卷积核

# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2  # 学习率

# 训练10轮
for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.zero_grad()
    l.sum().backward()
    # 迭代卷积核,梯度下降
    conv2d.weight.data[:] -= lr * conv2d.weight.grad
    if (i + 1) % 2 == 0:
        print(f'epoch {i+1}, loss {l.sum():.3f}')
print(conv2d.weight.data)

填充和步幅

填充

image-20231218105833955image-20231218110043711

步幅

image-20231218110333176image-20231218110547485image-20231218110729340

多个输出通道

image-20231218131146045
  • 每个输出通道可以识别特定通道
image-20231218135448397

总结:

·输出通道数是卷积层的超参数

·每个输入通道有独立的二维卷积核,所有通道结果相加得到一个输出通道结果
·每个输出通道有独立的三维卷积核

池化层

image-20231218161900678image-20231218161952114image-20231218162452431image-20231218162622525image-20231218162756187

LeNet

image-20231218192014041

import torch
from torch import nn
from d2l import torch as d2l

#
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10))

X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
net(X)


batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
    """使用GPU计算模型在数据集上的精度"""
    if isinstance(net, nn.Module):
        net.eval()  # 设置为评估模式
        if not device:
            device = next(iter(net.parameters())).device
    # 正确预测的数量,总预测的数量
    metric = d2l.Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(X, list):
                # BERT微调所需的(之后将介绍)
                X = [x.to(device) for x in X]
            else:
                X = X.to(device)
            y = y.to(device)
            metric.add(d2l.accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    """用GPU训练模型(在第六章定义)"""
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    print('training on', device)
    net.to(device) # 参数到GPU
    optimizer = torch.optim.SGD(net.parameters(), lr=lr)
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
    timer, num_batches = d2l.Timer(), len(train_iter)
    for epoch in range(num_epochs):
        # 训练损失之和,训练准确率之和,样本数
        metric = d2l.Accumulator(3)
        net.train()
        for i, (X, y) in enumerate(train_iter):
            timer.start()
            optimizer.zero_grad()
            X, y = X.to(device), y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
            timer.stop()
            train_l = metric[0] / metric[2]
            train_acc = metric[1] / metric[2]
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i + 1) / num_batches,
                             (train_l, train_acc, None))
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
    print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
          f'test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(device)}')

lr, num_epochs = 1, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()
"""
loss 0.467, train acc 0.824, test acc 0.810
52608.6 examples/sec on cuda:0
"""

现代神经网络

AlexNet

更深更大的LeNet

主要改进:

  • 丢弃法
  • ReLu
  • MaxPooling
  • 计算机视觉方法论的改变

image-20231219131731786
2.
image-20231219132054615
3.
image-20231219132453918

细节:

  • 激活函数从 sigmoid变到了 ReLu (减缓梯度消失)
  • 隐藏全连接层后加入了丢弃层
  • 数据增强:裁剪、反转等
import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(
    # 这里使用一个11*11的更大窗口来捕捉对象。
    # 同时,步幅为4,以减少输出的高度和宽度。
    # 另外,输出通道的数目远大于LeNet
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 使用三个连续的卷积层和较小的卷积窗口。
    # 除了最后的卷积层,输出通道的数量进一步增加。
    # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Flatten(),
    # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
    nn.Linear(6400, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    # 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
    nn.Linear(4096, 10))

X = torch.randn(1, 1, 224, 224)
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__, 'output shape:      \t', X.shape)

batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

使用块的网络 VGG

image-20231219151911535 image-20231219152012685
  • VGG使用可重复使用的卷积块来构建深度卷积神经网络
  • 不同的卷积块个数和超参数可以得到不同复杂度的变种
import torch
from torch import nn
from d2l import torch as d2l


def vgg_block(num_convs, in_channels, out_channels):
    layers = []
    for _ in range(num_convs):
        layers.append(nn.Conv2d(in_channels, out_channels,
                                kernel_size=3, padding=1))
        layers.append(nn.ReLU())
        in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
    return nn.Sequential(*layers)


# 原始VGG网络有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。
# 第一个模块有64个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512。
# 由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))


# 定义VGG网络
def vgg(conv_arch):
    conv_blks = []
    in_channels = 1
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
        in_channels = out_channels

    return nn.Sequential(
        *conv_blks, nn.Flatten(),
        # 全连接层部分
        nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 10)
    )


net = vgg(conv_arch)
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
    X = blk(X)
    print(blk.__class__.__name__, 'output shape:\t', X.shape)


ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)
print(net)
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
    X = blk(X)
    print(blk.__class__.__name__, 'output shape:\t', X.shape)

网络中的网络 NiN

image-20231219164619184

参数很多, 占用内存高

image-20231219171731200 image-20231219171844102 image-20231219172014761

总结

  • NiN块使用卷积层加两个1x1卷积层
  • 后者对每个像素增加了非线性性
  • NiN使用全局平均池化层来替代VGG和AlexNet中的全连接层
  • 不容易过拟合,更少的参数个数
import torch
from torch import nn
from d2l import torch as d2l


def nin_block(in_channels, out_channels, kernel_size, strides, padding):
    return nn.Sequential(
        nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
        nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())

# NiN基于AlexNet架构
"""
NiN和AlexNet之间的一个显著区别是NiN完全取消了全连接层。 相反,NiN使用一个NiN块,其输出通道数等于标签类别的数量。
最后放一个全局平均汇聚层(global average pooling layer),生成一个对数几率 (logits)。
NiN设计的一个优点是,它显著减少了模型所需参数的数量。然而,在实践中,这种设计有时会增加训练模型的时间。
"""
net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, strides=4, padding=0),
    nn.MaxPool2d(3, stride=2),
    nin_block(96, 256, kernel_size=5, strides=1, padding=2),
    nn.MaxPool2d(3, stride=2),
    nin_block(256, 384, kernel_size=3, strides=1, padding=1),
    nn.MaxPool2d(3, stride=2),
    nn.Dropout(0.5),
    # 标签类别数是10, NiN块
    nin_block(384, 10, kernel_size=3, strides=1, padding=1),
    nn.AdaptiveAvgPool2d((1, 1)),
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten())

X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

含并行连结的网络 GoogLeNet

Inception块

image-20231220165652887 image-20231220175628915

模型参数小,计算复杂度低

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Inception(nn.Module):
    # c1--c4是每条路径的输出通道数
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        super(Inception, self).__init__(**kwargs)
        # 线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1x1卷积层后接5x5卷积层
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3x3最大汇聚层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return torch.cat((p1, p2, p3, p4), dim=1)


b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.ReLU(),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   nn.AdaptiveAvgPool2d((1,1)),
                   nn.Flatten())

net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))

X = torch.rand(size=(1, 1, 32*4, 128))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()
"""
loss 0.256, train acc 0.903, test acc 0.890
3592.7 examples/sec on cuda:0
"""

批量归一化层

训练神经网络时出现的挑战

1、数据预处理的方式通常会对最终结果产生巨大影响

  • 使用真实数据时,第一步是标准化输入特征(使其均值为0,方差为1),这种标准化可以很好地与优化器配合使用(可以将参数的量级进行统一)

2、对于典型的多层感知机或卷积神经网络,在训练时中间层中的变量可能具有更广的变化范围

  • 不论是沿着从输入到输出的层跨同一层中的单元、或是随着时间的推移,模型参数的随着训练更新变化莫测
  • 归一化假设变量分布中的不规则的偏移可能会阻碍网络的收敛

3、更深层的网络很复杂,容易过拟合

  • 这就意味着正则化变得更加重要
image-20231220185019823

所以提出了假设:能不能在改变底部信息的时候,避免顶部不断的重新训练?(这也是批量归一化所考虑的问题)

核心思想

  • 为什么会变?因为方差和均值整个分布会在不同层之间变化

所以假设将分布固定,假设每一层的输出、梯度都符合某一分布,相对来说就是比较稳定的(具体分布可以做细微的调整,但是整体保持基本一致,这样的话,在学习细微的变动时也比较容易)

image-20231220185254698

其中:

  • 给定一个来自小批量的输入xi,批量归一化对里面的每一个样本减去均值除以标准差,再乘以γ,最后再加上β
  • 上式中的 **μB(样本均值)**和 **σB(样本标准差)**是从数据中计算得到的
  • 拉伸参数(scale)γ偏移参数(shift)β 是可以学习的参数(需要与其他模型参数一起学习),是批量归一化之后学出来的,作用是假设分布在某一均值和方差下不合适,就可以通过学习一个新的均值和方法,使得神经网络输出的分布更好(在训练的过程中,中间层的变化幅度不能过于剧烈,应用批量归一化可以将每一层主动居中,并对β和γ进行限制从而将它们重新调整为给定的平均值和大小,避免变化过于剧烈)

原理

  • 批量归一化可应用于单个可选层,也可应用于所有层
  • 在每次训练迭代中,首先规一化输入(通过减去均值并除以其标准差,其中均值和标准差都是基于当前小批量处理得来的)
  • 然后应用比例系数比例偏移
  • 正是由于是基于批量统计的标准化,所以才有了批量归一化的名称
image-20231220190134429

是一个线性变化,让变化不那么激烈

image-20231220190756883 image-20231220191010351

1、如果对批量大小为1的小批量进行批量归一化,将无法学到任何东西(因为在减去均值之后,每个隐藏层单元将为0)

2、只有使用足够大的小批量,批量规一化才是有效且稳定的

3、在使用批量规一化时,批量大小的选择可能比没有批量归一化时更重要

4、批量归一化层在“训练模式”和“预测模式”中的功能不同(和暂退法一样,批量归一化层在训练模式和预测模式下的行为通常不同):

  • 训练模式:通过小批量统计数据归一化。在训练过程中,由于无法得知使用整个数据集来估计平均值和方差,所以只能根据每个小批次的平均值和方差不断训练模型
  • 预测模式:通过数据集统计归一化。在预测模式下,可以根据整个数据集(不再需要样本均值中的噪声以及在微批次上估计每个小批次产生的样本方差)精确计算批量归一化所需的平均值和方差;可能需要使用模型对逐个样本进行预测(一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出)
import torch
from torch import nn
from d2l import torch as d2l

# 从零实现
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
    if not torch.is_grad_enabled():
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)
        if len(X.shape) == 2:
            # 使用全连接层的情况,计算特征维上的均值和方差
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
            # 这里我们需要保持X的形状以便后面可以做广播运算
            mean = X.mean(dim=(0, 2, 3), keepdim=True)
            var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
        # 训练模式下,用当前的均值和方差做标准化
        X_hat = (X - mean) / torch.sqrt(var + eps)
        # 更新移动平均的均值和方差
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
    Y = gamma * X_hat + beta  # 缩放和移位
    return Y, moving_mean.data, moving_var.data

# 定义BatchNorm层
class BatchNorm(nn.Module):
    # num_features:完全连接层的输出数量或卷积层的输出通道数。
    # num_dims:2表示完全连接层,4表示卷积层
    def __init__(self, num_features, num_dims):
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 非模型参数的变量初始化为0和1
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(shape)

    def forward(self, X):
        # 如果X不在内存上,将moving_mean和moving_var
        # 复制到X所在显存上
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
        # 保存更新过的moving_mean和moving_var
        Y, self.moving_mean, self.moving_var = batch_norm(
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9)
        return Y


net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
    nn.Linear(84, 10))

lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()
"""
loss 0.263, train acc 0.903, test acc 0.889
49370.1 examples/sec on cuda:0
"""

ResNet

问题: 加更多的层总是改进精度吗?

残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一, 引入:

image-20231221111657194 image-20231221112349586
  • 残差块中首先有2个相同输出通道数的3 * 3卷积层,每个卷积层后面接一个批量归一化层和ReLu激活函数;通过跨层数据通路,跳过残差块中的两个卷积运算,将输入直接加在最后的ReLu激活函数前(这种设计要求2个卷积层的输出与输入形状一样,这样才能使第二个卷积层的输出(也就是第二个激活函数的输入)和原始的输入形状相同,才能进行相加)
  • 如果想要改变通道数,就需要引入一个额外的1 * 1的卷积层来将输入变换成需要的形状后再做相加运算(如上图中右侧含1 * 1卷积层的残差块)
img
  • 第一种是高宽减半的ResNet块。第一个卷积层的步幅等于2,使得高宽减半,通道数翻倍(如上图下半部分所示)
  • 第二种是高宽不减半的RexNet块,如上图上半部分所示,重复多次,所有卷积层的步幅等于1

通过ResNet块数量通道数量的不同,可以得到不同的ResNet架构

总结

  • 残差块使得很深的网络更加容易训练(不管网络有多深,因为有跨层数据通路连接的存在,使得始终能够包含小的网络,因为跳转连接的存在,所以会先将下层的小型网络训练好再去训练更深层次的网络),甚至可以训练一千层的网络(只要内存足够,优化算法就能够实现)
  • 学习嵌套函数是神经网络的理想情况,在深层神经网络中,学习另一层作为恒等映射比较容易
  • 残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零
  • 利用残差块可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播
  • 残差网络对随后的深层神经网络的设计产生了深远影响,无论是卷积类网络还是全连接类网络,几乎现在所有的网络都会用到,因为只有这样才能够让网络搭建的更深

image-20231221125121097

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Residual(nn.Module):  #@save
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)

    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)

# input_channels == output_channels
blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
print(Y.shape)

# 在增加输出通道数的同时,减半输出的高和宽。
blk = Residual(3,7, use_1x1conv=True, strides=2)
print(blk(X).shape)

# ResNet的前两层跟之前介绍的GoogLeNet中的一样: 在输出通道数为64、步幅为2的
# 卷积层后,接步幅为2的
# 的最大汇聚层。 不同之处在于ResNet每个卷积层后增加了批量规范化层。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.BatchNorm2d(64), nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

# GoogLeNet在后面接了4个由Inception块组成的模块。 ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。
# 第一个模块的通道数同输入通道数一致。
# 由于之前已经使用了步幅为2的最大汇聚层,所以无须减小高和宽。 之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。
def resnet_block(input_channels, num_channels, num_residuals,
                 first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))
        else:
            blk.append(Residual(num_channels, num_channels))
    return blk


# 定义ResNet18
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))

X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()
"""
loss 0.016, train acc 0.995, test acc 0.923
4869.0 examples/sec on cuda:0
"""



ResNet为什么能训练出1000层的模型

从梯度来看:因为链式法则,乘法,越往下很可能梯度就会变小,resnet让下面的层也能拿到一个大的梯度

计算机视觉

数据增广

  • 增加一个已有数据集,使得有更多的多样性
  • 在语言里面加入各种不同的背景噪音
  • 改变图片的颜色和形状
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

d2l.set_figsize()
img = d2l.Image.open('./img/cat1.jpg')

# 下面定义辅助函数apply。 此函数在输入图像img上多次运行图像增广方法aug并显示所有结果。
def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
    Y = [aug(img) for _ in range(num_rows * num_cols)]
    d2l.show_images(Y, num_rows, num_cols, scale=scale)

# 左右翻转图像通常不会改变对象的类别。
# 这是最早且最广泛使用的图像增广方法之一。 接下来,我们使用transforms模块来创建RandomFlipLeftRight实例,这样就各有50%的几率使图像向左或向右翻转。
apply(img, torchvision.transforms.RandomHorizontalFlip())


#创建一个RandomFlipTopBottom实例,使图像各有50%的几率向上或向下翻
apply(img, torchvision.transforms.RandomVerticalFlip())


# 下面的代码将随机裁剪一个面积为原始面积10%到100%的区域,该区域的宽高比从0.5~2之间随机取值。 然后,区域的宽度和高度都被缩放到200像素。
shape_aug = torchvision.transforms.RandomResizedCrop(
    (200, 200), scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)

# 以随机更改图像的色调
# 创建一个RandomColorJitter实例,并设置如何同时随机更改图像的亮度(brightness)、对比度(contrast)、饱和度(saturation)和色调(hue)
apply(img, torchvision.transforms.ColorJitter(
    brightness=0, contrast=0, saturation=0, hue=0.5))



all_images = torchvision.datasets.CIFAR10(train=True, root="../data",
                                          download=True)

train_augs = torchvision.transforms.Compose([
     torchvision.transforms.RandomHorizontalFlip(),
     torchvision.transforms.ToTensor()])

test_augs = torchvision.transforms.Compose([
     torchvision.transforms.ToTensor()])

def load_cifar10(is_train, augs, batch_size):
    dataset = torchvision.datasets.CIFAR10(root="../data", train=is_train,
                                           transform=augs, download=True)
    dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
                    shuffle=is_train, num_workers=1)
    return dataloader

batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3)

def init_weights(m):
    if type(m) in [nn.Linear, nn.Conv2d]:
        nn.init.xavier_uniform_(m.weight)

net.apply(init_weights)

def train_with_data_aug(train_augs, test_augs, net, lr=0.001):
    train_iter = load_cifar10(True, train_augs, batch_size)
    test_iter = load_cifar10(False, test_augs, batch_size)
    loss = nn.CrossEntropyLoss(reduction="none")
    trainer = torch.optim.Adam(net.parameters(), lr=lr)
    d2l.train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices)

train_with_data_aug(train_augs, test_augs, net)
d2l.plt.show()
"""
loss 0.168, train acc 0.942, test acc 0.837
3277.3 examples/sec on [device(type='cuda', index=0)]
"""

微调

一个神经网络一般可以分成两块

  • 特征抽取将原始像素变成容易线性分割的特征
  • 线性分类器来做分类
image-20231222151204114 image-20231222153859498 image-20231222154458546
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

# @save
d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip',
                          'fba480ffa8aa7e0febbb511d181409f899b9baa5')

data_dir = d2l.download_extract('hotdog')

train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train'))
test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'))

# 使用RGB通道的均值和标准差,以标准化每个通道
normalize = torchvision.transforms.Normalize(
    [0.485, 0.456, 0.406],
    [0.229, 0.224, 0.225])

train_augs = torchvision.transforms.Compose([
    torchvision.transforms.RandomResizedCrop(224),
    torchvision.transforms.RandomHorizontalFlip(),
    torchvision.transforms.ToTensor(),
    normalize])

test_augs = torchvision.transforms.Compose([
    torchvision.transforms.Resize([256, 256]),
    torchvision.transforms.CenterCrop(224),
    torchvision.transforms.ToTensor(),
    normalize])

pretrained_net = torchvision.models.resnet18(pretrained=True)
pretrained_net.fc

finetune_net = torchvision.models.resnet18(pretrained=True)
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
nn.init.xavier_uniform_(finetune_net.fc.weight);


# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5,
                      param_group=True):
    train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'train'), transform=train_augs),
        batch_size=batch_size, shuffle=True)
    test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'test'), transform=test_augs),
        batch_size=batch_size)
    devices = d2l.try_all_gpus()
    loss = nn.CrossEntropyLoss(reduction="none")
    if param_group:
        params_1x = [param for name, param in net.named_parameters()
                     if name not in ["fc.weight", "fc.bias"]]  # 不是最后一层的参数都拿出来
        trainer = torch.optim.SGD([{'params': params_1x}, {'params': net.fc.parameters(), 'lr': learning_rate * 10}],
                                  lr=learning_rate, weight_decay=0.001) # 作用: 让fc以外的层变化更慢
    else:
        trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
                                  weight_decay=0.001)

    d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
                   devices)

# 使用很小的学习率, 只让FC参数变化快
train_fine_tuning(finetune_net, 5e-5)
""""
loss 0.182, train acc 0.929, test acc 0.935
827.6 examples/sec on [device(type='cuda', index=0)]
"""

scratch_net = torchvision.models.resnet18()
scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)
train_fine_tuning(scratch_net, 5e-4, param_group=False)
"""
loss 0.352, train acc 0.850, test acc 0.861
830.8 examples/sec on [device(type='cuda', index=0)]
"""


d2l.plt.show()

锚框

image-20231223152731163 image-20231223153247417 image-20231223154026553 image-20231223154346345
import torch
from d2l import torch as d2l

torch.set_printoptions(2)  # 精简输出精度


#
#@ save
def multibox_prior(data, sizes, ratios):
    """生成以每个像素为中心具有不同形状的锚框"""
    in_height, in_width = data.shape[-2:]
    device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
    boxes_per_pixel = (num_sizes + num_ratios - 1)
    size_tensor = torch.tensor(sizes, device=device)
    ratio_tensor = torch.tensor(ratios, device=device)

    # 为了将锚点移动到像素的中心,需要设置偏移量。
    # 因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5
    offset_h, offset_w = 0.5, 0.5
    steps_h = 1.0 / in_height  # 在y轴上缩放步长
    steps_w = 1.0 / in_width  # 在x轴上缩放步长

    # 生成锚框的所有中心点
    center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
    center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
    shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
    shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)

    # 生成“boxes_per_pixel”个高和宽,
    # 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
    w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
                   sizes[0] * torch.sqrt(ratio_tensor[1:])))\
                   * in_height / in_width  # 处理矩形输入
    h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
                   sizes[0] / torch.sqrt(ratio_tensor[1:])))
    # 除以2来获得半高和半宽
    anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
                                        in_height * in_width, 1) / 2

    # 每个中心点都将有“boxes_per_pixel”个锚框,
    # 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
    out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
                dim=1).repeat_interleave(boxes_per_pixel, dim=0)
    output = out_grid + anchor_manipulations
    return output.unsqueeze(0)

img = d2l.plt.imread('./img/catdog.jpg')
h, w = img.shape[:2]

print(h, w)
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
print(Y.shape)

# 锚框变量Y的形状更改为(图像高度,图像宽度,以同一像素为中心的锚框的数量,4)后,我们可以获得以指定像素的位置为中心的所有锚框。
boxes = Y.reshape(h, w, 5, 4)
boxes[250, 250, 0, :]

d2l.set_figsize()
bbox_scale = torch.tensor((w, h, w, h))
fig = d2l.plt.imshow(img)
d2l.show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
            ['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
             's=0.75, r=0.5'])






ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],
                         [1, 0.55, 0.2, 0.9, 0.88]])
anchors = torch.tensor([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],
                    [0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],
                    [0.57, 0.3, 0.92, 0.9]])
fig = d2l.plt.imshow(img)
d2l.show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
d2l.show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);
d2l.plt.show()

# 我们可以根据狗和猫的真实边界框,标注这些锚框的分类和偏移量。
# 在这个例子中,背景、狗和猫的类索引分别为0、1和2。 下面我们为锚框和真实边界框样本添加一个维度。
# 返回的第一个元素包含了为每个锚框标记的四个偏移值。 请注意,负类锚框的偏移量被标记为零。
# 第三个元素包含标记的输入锚框的类别。
# 返回的第二个元素是掩码(mask)变量,形状为(批量大小,锚框数的四倍)。 掩码变量中的元素与每个锚框的4个偏移量一一对应。
# 由于我们不关心对背景的检测,负类的偏移量不应影响目标函数。 通过元素乘法,掩码变量中的零将在计算目标函数之前过滤掉负类偏移量。
labels = d2l.multibox_target(anchors.unsqueeze(dim=0),
                         ground_truth.unsqueeze(dim=0))
"""
labels[2]
tensor([[0, 1, 2, 0, 2]])

labels[1]
tensor([[0., 0., 0., 0.,    1., 1., 1., 1.,      1., 1., 1., 1.,    0., 0., 0., 0.,     1., 1.,1., 1.]])

"""


# 现在让我们将上述算法应用到一个带有四个锚框的具体示例中。
# 为简单起见,我们假设预测的偏移量都是零,这意味着预测的边界框即是锚框。
# 对于背景、狗和猫其中的每个类,我们还定义了它的预测概率。

# 定义的锚框
anchors = torch.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95],
                      [0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]])

# 预测的结果
offset_preds = torch.tensor([0] * anchors.numel())
cls_probs = torch.tensor([[0] * 4,  # 背景的预测概率
                      [0.9, 0.8, 0.7, 0.1],  # 狗的预测概率
                      [0.1, 0.2, 0.3, 0.9]])  # 猫的预测概率

# NMS
output = d2l.multibox_detection(cls_probs.unsqueeze(dim=0),
                            offset_preds.unsqueeze(dim=0),
                            anchors.unsqueeze(dim=0),
                            nms_threshold=0.5)

# 删除 -1类 (背景)
fig = d2l.plt.imshow(img)
for i in output[0].detach().numpy():
    if i[0] == -1:
        continue
    label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
    d2l.show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)

d2l.plt.show()



物体检测算法

R-CNN 区域卷积神经网络

  • 首先从输入图像中选取若干个提议区域锚框是选取方式的一种),并标注它们的类别边界框(如偏移量)。然后用卷积神经网络来对每个提议区域(锚框)进行前向传播以抽取特征。最后用每个提议区域的特征来预测类别和边界框。

R-CNN 模型的四个步骤:

  1. 对输入图像使用选择性搜索来选取多个高质量的提议区域。这些提议区域通常是在多个尺度下选取的,并具有不同的形状和大小;每个提议区域都将被标注类别和真实边框
  2. 选择一个预训练的卷积神经网络,并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸,并通过前向传播输出抽取的提议区域特征
  3. 将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别
  4. 将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型来预测真实边界框

每次选择的锚框的大小是不同的,在这种情况下,怎样使这些大小不一的锚框变成一个batch?

RoI pooling(兴趣区域池化层)

  • R-CNN 中比较关键的层,作用是将大小不一的锚框变成统一的形状
  • 给定一个锚框,先将其均匀地分割成 n * m 块,然后输出每块里的最大值,这样的话,不管锚框有多大,只要给定了 n 和 m 的值,总是输出 nm 个值,这样的话,不同大小的锚框就都可以变成同样的大小,然后作为一个小批量,之后的处理就比较方便了

image-20231225103446066

小结

  • 尽管 R-CNN 模型通过预训练的卷积神经网络有效地抽取了图像特征,但是速度非常慢(如果从一张图片中选取了上千个提议区域,就需要上千次的卷积神经网络的前向传播来执行目标检测,计算量非常大

Fast R-CNN

R-CNN 的主要性能瓶颈在于,对于每个提议区域,卷积神经网络的前向传播是独立的,没有共享计算(这些提议区域通常有重叠,独立的特征提取会导致重复计算)

image-20231225103945059

  • Faste R-CNN 的改进是:在拿到一张图片之后,首先使用 CNN 对图片进行特征提取(不是对图片中的锚框进行特征提取,而是对整张图片进行特征提取,仅在整张图像上执行卷积神经网络的前向传播),最终会得到一个 7 * 7 或者 14 * 14 的 feature map
  • 抽取完特征之后,再对图片进行锚框的选择(selective search),搜索到原始图片上的锚框之后将其(按照一定的比例)映射到 CNN 的输出上
  • 映射完锚框之后,再使用 RoI pooling 对 CNN 输出的 feature map 上的锚框进行特征抽取,生成固定长度的特征(将 n * m 的矩阵拉伸成为 nm 维的向量),之后再通过一个全连接层(这样就不需要使用SVM一个一个的操作,而是一次性操作了)对每个锚框进行预测:物体的类别和真实的边缘框的偏移
  • 上图中黄色方框的作用就是将原图中生成的锚框变成对应的向量
  • Fast R-CNN 相对于 R-CNN 更快的原因是:Fast R-CNN 中的 CNN 不再对每个锚框抽取特征,而是对整个图片进行特征的提取(这样做的好处是:不同的锚框之间可能会有重叠的部分,如果对每个锚框都进行特征提取的话,可能会对重叠的区域进行多次重复的特征提取操作),然后再在整张图片的feature中找出原图中锚框对应的特征,最后一起做预测

Faster R-CNN

image-20231225104845128

Faster R-CNN 提出将选择性搜索替换为区域提议网络(region proposal network,RPN),模型的其余部分保持不变,从而减少区域的生成数量,并保证目标检测的精度

  • Faster R-CNN 的改进:使用 RPN 神经网络来替代 selective search
  • RoI 的输入是CNN 输出的 feature map生成的锚框
  • RPN 的输入是 CNN 输出的 feature map,输出是一些比较高质量的锚框(可以理解为一个比较小而且比较粗糙的目标检测算法: CNN 的输出进入到 RPN 之后再做一次卷积,然后生成一些锚框(可以是 selective search 或者其他方法来生成初始的锚框),再训练一个二分类问题:预测锚框是否框住了真实的物体以及锚框到真实的边缘框的偏移,最后使用 NMS 进行去重,使得锚框的数量变少)
  • RPN 的作用是生成大量结果很差的锚框,然后进行预测,最终输出比较好的锚框供后面的网络使用(预测出来的比较好的锚框会进入 RoI pooling,后面的操作与 Fast R-CNN 类似)
  • 通常被称为两阶段的目标检测算法:RPN 做小的目标检测(粗糙),整个网络再做一次大的目标检测(精准)
  • Faster R-CNN 目前来说是用的比较多的算法,准确率比较高,但是速度比较慢

值得一提的是,区域提议网络作为Faster R-CNN模型的一部分,是和整个模型一起训练得到的。 换句话说,Faster R-CNN的目标函数不仅包括目标检测中的类别和边界框预测,还包括区域提议网络中锚框的二元类别和边界框预测。 作为端到端训练的结果,区域提议网络能够学习到如何生成高质量的提议区域,从而在减少了从数据中学习的提议区域的数量的情况下,仍保持目标检测的精度。

Mask R-CNN

image-20231225105653515

像素级的精度

总结

  • R-CNN 是最早、也是最有名的一类基于锚框和 CNN 的目标检测算法(R-CNN 可以认为是使用神经网络来做目标检测工作的奠基工作之一),它对图像选取若干提议区域,使用卷积神经网络对每个提议区域执行前向传播以抽取其特征,然后再用这些特征来预测提议区域的类别和边框
  • Fast/Faster R-CNN持续提升性能:Fast R-CNN 只对整个图像做卷积神经网络的前向传播,还引入了兴趣区域汇聚层(RoI pooling),从而为具有不同形状的兴趣区域抽取相同形状的特征;Faster R-CNN 将 Fast R-CNN 中使用的选择性搜索替换为参与训练的区域提议网络,这样可以在减少提议区域数量的情况下仍然保持目标检测的精度;Mask R-CNN 在 Faster R-CNN 的基础上引入了一个全卷积网络,从而借助目标的像素级位置进一步提升目标检测的精度
  • Faster R-CNN 和 Mask R-CNN 是在追求高精度场景下的常用算法(Mask R-CNN 需要有像素级别的标号,所以相对来讲局限性会大一点,在无人车领域使用的比较多)

单发多框检测(SSD)

image-20231225110202821 image-20231225110536991
  • 输入图像之后,首先进入一个基础网络来抽取特征,抽取完特征之后对每个像素生成大量的锚框(每个锚框就是一个样本,然后预测锚框的类别以及到真实边界框的偏移)
  • SSD 在给定锚框之后直接对锚框进行预测,而不需要做两阶段(为什么 Faster RCNN 需要做两次,而 SSD 只需要做一次?SSD 通过做不同分辨率下的预测来提升最终的效果,越到底层的 feature map,就越大,越往上,feature map 越少,因此底层更加有利于小物体的检测,而上层更有利于大物体的检测
  • SSD 不再使用 RPN 网络,而是直接在生成的大量样本(锚框)上做预测,看是否包含目标物体;如果包含目标物体,再预测该样本到真实边缘框的偏移

总结

  • SSD通过单神经网络来检测模型
  • 以每个像素为中心产生多个锚框
  • 多个段的输出上进行多尺度的检测(底层检测小物体,上层检测大物体)

YOLO

  • yolo 也是一个 single-stage 的算法,只有一个单神经网络来做预测
  • yolo 也需要锚框,这点和 SSD 相同,但是 SSD 是对每个像素点生成多个锚框,所以在绝大部分情况下两个相邻像素的所生成的锚框的重叠率是相当高的,这样就会导致很大的重复计算量。
  • yolo 的想法是尽量让锚框不重叠:首先将图片均匀地分成 S * S 块,每一块就是一个锚框,每一个锚框预测 B 个边缘框(考虑到一个锚框中可能包含多个物体),所以最终就会产生 S ^ 2 * B 个样本,因此速度会远远快于 SSD
  • yolo 在后续的版本(V2,V3,V4…)中有持续的改进,但是核心思想没有变,真实的边缘框不会随机的出现,真实的边缘框的比例、大小在每个数据集上的出现是有一定的规律的,在知道有一定的规律的时候就可以使用聚类算法将这个规律找出来(给定一个数据集,先分析数据集中的统计信息,然后找出边缘框出现的规律,这样之后在生成锚框的时候就会有先验知识,从而进一步做出优化)

转置卷积和卷积

1、工作原理

  • 常规卷积是输入跟核进行按元素的乘法并相加,最后得到对应位置的值,通过核在输入上进行滑动从而得到输出
  • 转置卷积是输入的单个元素跟核按元素做乘法但不相加,保持核的大小,然后按元素写回到一个更大的矩阵的对应位置(输入的每个元素会生成与核大小相同的矩阵写入到一个更大的矩阵的对应位置,所以输出的高宽相对于输入来讲是变大的

2、输入输出

  • 常规卷积通过卷积核 “减少” 输入元素
  • 转置卷积通过卷积核 “广播” 输入元素,从而产生大于输入的输出

3、填充

  • 常规卷积中将填充应用于输入(如果将高和宽两侧的填充数指定为 1 时,常规卷积的输入中将增加第一和最后的行和列)
  • 转置卷积中将填充应用于输出(如果将高和宽两侧的填充数指定为 1 时,转置卷积的输出中将删除第一和最后的行和列)

4、步幅

  • 常规卷积中,步幅所指定的是卷积核在输入上的滑动距离
  • 转置卷积中,步幅所指定的是卷积核每次运算结果写回到中间结果(输出)矩阵中对应位置的滑动距离,以下两张图是卷积核为 2 × 2 ,步幅分别为 2 的转置卷积运算
image-20240103100528377

为什么称之为“转置”

对于卷积 Y = X * W

  • " * "代表卷积
  • 可以对 W 构造一个 V (V 是一个比较大的向量),使得卷积等价于矩阵乘法 Y‘ = VX’
  • 这里的 Y‘, X’ 是 Y, X 对应的向量版本(将 Y, X 通过逐行连结拉成向量)
  • 如果 X’ 是一个长为 m 的向量,Y‘ 是一个长为 n 的向量,则 V 就是一个 n×m 的矩阵

转置卷积同样可以对 W 构造一个 V ,则等价于 Y‘ = VTX’

  • 按照上面的假设 VT 就是一个 m×n ,则 X’ 就是一个长为 n 的向量,Y‘ 就是一个长为 m 的向量,X 和 Y 的向量发生了交换
  • 从 V 变成了 VT 所以叫做转置卷积

所以如果卷积将输入从(h,w)变成了(h’,w’),则同样超参数(kernel size, stride, padding)的转置卷积则从(h’,w’)变成(h,w)

注意力机制

image-20240103113941627

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值