x = torch.arange(12)
x
tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
x.shape
torch.Size([12])
x.numel()
12
X = x.reshape(3, 4)
X
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
不需要通过手动指定每个维度来改变形状。可以通过在希望张量自动推断的维度放置-1来调用此功能
6. 创建一个形状为(2,3,4)的张量,其中所有元素都设置为0
torch.zeros((2, 3, 4))
创建一个形状为(2,3,4)的张量,其中所有元素都设置为1
torch.ones((2, 3, 4))
torch.randn(3, 4)
tensor([[-0.9464, 0.7712, -0.0070, 1.0236],
[-2.1246, -0.7854, -1.9674, -0.1727],
[ 0.0397, -0.0477, -0.0160, -0.0113]])
8.为所需张量中的每个元素赋予确定值
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
tensor([[2, 1, 4, 3],
[1, 2, 3, 4],
[4, 3, 2, 1]])
1.计算
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y # **运算符是求幂运算
torch.exp(x)
2.张量连结(concatenate)
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
(tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]),
tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]]))
2.二元张量
X == Y
tensor([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])
3.张量元素求和,产生只有一个元素的张量
X.sum()
tensor(66.)
1.广播机制(broadcasting mechanism)
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))
由于a和b分别是 3×1 和 1×2 矩阵,如果我们让它们相加,它们的形状不匹配。我们将两个矩阵广播为一个更大的 3×2 矩阵,如下所示:矩阵a将复制列,矩阵b将复制行,然后再按元素相加
a + b
tensor([[0, 1],
[1, 2],
[2, 3]])
和python一样
运行一些操作可能会导致为新结果分配内存。
before = id(Y)
Y = Y + X
id(Y) == before
False
执行原地操作非常简单。我们可以使用切片表示法将操作的结果分配给先前分配的数组 zeros_like来分配一个全 0 的块
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
id(Z): 140272150341696
id(Z): 140272150341696
也可以使用X += Y来减少操作的内存开销。
before = id(X)
X += Y
id(X) == before
True
1.转换后的结果不共享内存
A = X.numpy()
B = torch.tensor(A)
type(A), type(B)
(numpy.ndarray, torch.Tensor)
2.要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数。
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
(tensor([3.5000]), 3.5, 3.5, 3)
具体见 2.2数据预处理.py
A = torch.arange(20).reshape(5, 4)
A
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19]])
矩阵转置 A.T
见上文
A * B
1.求和降维
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和
A.sum(axis=[0, 1]) # Same as `A.sum()`
2.平均值
A.mean(), A.sum() / A.numel()
(tensor(9.5000), tensor(9.5000))
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(tensor([ 8., 9., 10., 11.]), tensor([ 8., 9., 10., 11.])
sum_A = A.sum(axis=1, keepdims=True)
sum_A
tensor([[ 6.],
[22.],
[38.],
[54.],
[70.]])
A / sum_A
tensor([[0.0000, 0.1667, 0.3333, 0.5000],
[0.1818, 0.2273, 0.2727, 0.3182],
[0.2105, 0.2368, 0.2632, 0.2895],
[0.2222, 0.2407, 0.2593, 0.2778],
[0.2286, 0.2429, 0.2571, 0.2714]])
A.cumsum(axis=0)
tensor([[ 0., 1., 2., 3.],
[ 4., 6., 8., 10.],
[12., 15., 18., 21.],
[24., 28., 32., 36.],
[40., 45., 50., 55.]])
y = torch.ones(4, dtype = torch.float32)
x, y, torch.dot(x, y)
(tensor([0., 1., 2., 3.]), tensor([1., 1., 1., 1.]), tensor(6.))
A.shape, x.shape, torch.mv(A, x)
(torch.Size([5, 4]), torch.Size([4]), tensor([ 14., 38., 62., 86., 110.]))
B = torch.ones(4, 3)
torch.mm(A, B)
tensor([[ 6., 6., 6.],
[22., 22., 22.],
[38., 38., 38.],
[54., 54., 54.],
[70., 70., 70.]])
u = torch.tensor([3.0, -4.0])
torch.norm(u)
tensor(5.)
torch.abs(u).sum()
tensor(7.)
torch.norm(torch.ones((4, 9)))
tensor(6.)
通常,目标,或许是深度学习算法最重要的组成部分(除了数据),被表达为范数。
深度学习框架通过自动计算导数,即自动求导(automatic differentiation),来加快这项工作。实际中,根据我们设计的模型,
系统会构建一个计算图(computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动求导使系统能够随后反向传播梯度。
这里,反向传播(backpropagate)只是意味着跟踪整个计算图,填充关于每个参数的偏导数。
import torch
x = torch.arange(4.0)
print(x)
x.requires_grad_(True) # 等价于 `x = torch.arange(4.0, requires_grad=True)`
print(x.grad) # 默认值是None
y = 2 * torch.dot(x, x)
print(y)
y.backward()
print(x.grad)
print(x.grad == 4 * x)
当 y
不是标量时,向量 y
关于向量 X
的导数的最自然解释是一个矩阵。对于高阶和高维的 y
和 x
,求导的结果可以是一个高阶张量
# 对非标量调用`backward`需要传入一个`gradient`参数,该参数指定微分函数关于`self`的梯度。在我们的例子中,我们只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
tensor([0., 2., 4., 6.])
希望将某些计算移动到记录的计算图之外,即视为常数,反向传播时梯度不会从此经过
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
tensor([True, True, True, True])
help()
我们首先定义一个模型变量net,它是一个Sequential类的实例。Sequential类为串联在一起的多个层定义了一个容器。当给定输入数据,Sequential实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。但是由于以后几乎所有的模型都是多层的,在这里使用Sequential会让你熟悉标准的流水线。
在每个迭代周期里,我们将完整遍历一次数据集(rain_data
),不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量,我们会进行以下步骤:
net(X)
生成预测并计算损失 l
(正向传播)。3.3.线性回归.py
独热编码 (one-hot encoding)。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。
全连接层:每个输出取决于全部输入。
在分类器输出0.5的所有样本中,我们希望这些样本有一半实际上属于预测的类。 这个属性叫做校准 (calibration)。
softmax函数:为了将未归一化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未归一化的预测求幂,这样可以确保输出非负。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。
$$
\hat{y}=softmax(o) ,其中, \hat{y_j}=\frac{exp(o_j)}{\sum_k exp(o_k)}
$$
交叉熵损失 (cross-entropy loss)
如果我们不能完全预测每一个事件,那么我们有时可能会感到惊异。当我们赋予一个事件较低的概率时,我们的惊异会更大。克劳德·香农决定用 log 1 P ( j ) = − log P ( j ) \log \frac{1}{P(j)}=-\log P(j) logP(j)1=−logP(j)来量化一个人的 惊异 (surprisal)。在观察一个事件j,并赋予它(主观)概率P(j)。定义的熵是当分配的概率真正匹配数据生成过程时的 预期惊异 (expected surprisal)。
所以,如果熵是知道真实概率的人所经历的惊异程度,那么你可能会想知道,什么是交叉熵? 交叉熵从P到Q,记为H(P,Q),是主观概率为Q的观察者在看到根据概率P实际生成的数据时的预期惊异。当P=Q时,交叉熵达到最低。在这种情况下,从P到Q的交叉熵是 H ( P , P ) = H ( P ) H(P,P)=H(P) H(P,P)=H(P)。
简而言之,我们可以从两方面来考虑交叉熵分类目标:(i)最大化观测数据的似然;(ii)最小化传达标签所需的惊异。
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
loss = nn.CrossEntropyLoss()
在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。这与我们在线性回归例子中的相同,这说明了优化器的普适性。
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
线性整流单元(Rectified linear unit,ReLU)
$$
ReLU(x)=max(x,0)
$$
$$
sigmoid(x)=\frac{1}{1+exp(-x)}
$$
$$
tanch(x)= \frac{1-exp(-2x)}{1+exp(-2x)}
$$
另一个需要牢记的重要因素是数据集的大小。训练数据集中的样本越少,我们就越有可能(且更严重地)遇到过拟合。 随着训练数据量的增加,泛化误差通常会减小。
此外,一般来说,更多的数据不会有什么坏处。 对于固定的任务和数据分布,模型复杂性和数据集大小之间通常存在关系。
给出更多的数据,我们可能会尝试拟合一个更复杂的模型。能够拟合更复杂的模型可能是有益的。如果没有足够的数据,简单的模型可能更有用。
对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。从一定程度上来说,深度学习目前的成功要归功于互联网公司、廉价存储、互联设备以及数字化经济带来的海量数据集。
见4.4.多项式回归.py
dropout在正向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的标准技术。这种方法之所以被称为 dropout ,因为我们从表面上看是在训练过程中丢弃(drop out)一些神经元。 在整个训练过程的每一次迭代中,dropout包括在计算下一层之前将当前层中的一些节点置零。
在标准dropout正则化中,通过按保留(未丢弃)的节点的分数进行归一化来消除每一层的偏差。换言之,每个中间激活值hh以丢弃概率p由随机变量h′替换,如下所示:
$$
h^"=
\begin{cases}
0,概率为P\
\frac{h}{1-p},其他情况
\end{cases}
$$
根据设计,期望值保持不变,即$ E [ h " ] = h E [ h " ] = h E[h"]=hE[h^"]=h E[h"]=hE[h"]=h
通常,Xavier初始化从均值为零,方差 σ 2 = 2 n ( i n ) + n ( o u t ) \sigma^2=\frac{2}{n_(in)+n_(out)} σ2=n(in)+n(out)2的高斯分布中采样权重。
我们也可以利用Xavier的直觉来选择从均匀分布中抽取权重时的方差,注意均匀分布 U ( − a , a ) U(-a,a) U(−a,a)的方差为 a 2 3 \frac{a^2}{3} 3a2,将 a 2 3 \frac{a^2}{3} 3a2代入到 σ 2 \sigma^2 σ2条件中,将得到初始化的建议:
$$
U(-\sqrt{\frac{6}{n_(in)+n_(out)}},\sqrt{\frac{6}{n_(in)+n_(out)}}
$$
在实现我们自定义块之前,我们简要总结一下每个块必须提供的
注意,除非我们实现一个新的运算符,否则我们不必担心反向传播函数或参数初始化,系统将自动生成这些。让我们试一下。
块抽象的一个主要优点是它的多功能性。我们可以子类化块以创建层(如全连接层的类)、整个模型(如上面的MLP
类)或具有中等复杂度的各种组件。
现在我们可以更仔细地看看Sequential
类是如何工作的。回想一下Sequential
的设计是为了把其他模块串起来。为了构建我们自己的简化的MySequential
,我们只需要定义两个关键函数:
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for block in args:
# 这里,`block`是`Module`子类的一个实例。我们把它保存在'Module'类的成员变量
# `_modules` 中。`block`的类型是OrderedDict。
self._modules[block] = block
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
Sequential
类使模型构造变得简单,允许我们组合新的结构,而不必定义自己的类。然而,并不是所有的架构都是简单的顺序结构。当需要更大的灵活性时,我们需要定义自己的块。Sequential
块处理。我们从已有模型中访问参数。当通过Sequential
类定义模型时,我们可以通过索引来访问模型的任意层。这就像模型是一个列表一样。每层的参数都在其属性中。如下所示,我们可以检查第二个全连接层的参数。
print(net[2].state_dict())
注意,每个参数都表示为参数(parameter)类的一个实例。要对参数执行任何操作,首先我们需要访问底层的数值。有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。下面的代码从第二个神经网络层提取偏置,提取后返回的是一个参数类实例,并进一步访问该参数的值。
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([0.1629], requires_grad=True)
tensor([0.1629])
参数是复合的对象,包含值、梯度和额外信息。这就是我们需要显式请求值的原因
除了值之外,我们还可以访问每个参数的梯度。由于我们还没有调用这个网络的反向传播,所以参数的梯度处于初始状态。
net[2].weight.grad == None
True
下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
这为我们提供了另一种访问网络参数的方式,如下所示。
net.state_dict()['2.bias'].data
tensor([0.1629])
可以定义一个生成块的函数(可以说是块工厂),然后将这些块组合到更大的块中。
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)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([-0.0004, 0.0054, 0.0049, 0.0013]), tensor(0.))
我们还可以将所有参数初始化为给定的常数(比如1)
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([1., 1., 1., 1.]), tensor(0.))
我们还可以对某些块应用不同的初始化方法。例如,下面我们使用Xavier初始化方法初始化第一层,然后第二层初始化为常量值42
def 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(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
tensor([-0.1367, -0.2249, 0.4909, -0.6461])
tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
我们实现了一个my_init
函数来应用到net
。
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight[:2]
有时我们希望在多个层间共享参数。
这个例子表明第二层和第三层的参数是绑定的。它们不仅值相等,而且由相同的张量表示。因此,如果我们改变其中一个参数,另一个参数也会改变。
你可能会想,当参数绑定时,梯度会发生什么情况?答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层和第三个隐藏层的梯度会加在一起。
# 我们需要给共享层一个名称,以便可以引用它的参数。
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])
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])
对于单个张量,我们可以直接调用load
和save
函数分别读写它们。这两个函数都要求我们提供一个名称,save
要求将要保存的变量作为输入。
import torch
from torch import nn
from torch.nn import functional as F
x = torch.arange(4)
torch.save(x, 'x-file')
我们现在可以将存储在文件中的数据读回内存.
x2 = torch.load('x-file')
x2
我们可以存储一个张量列表,然后把它们读回内存。
y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)
我们甚至可以写入或读取从字符串映射到张量的字典。当我们要读取或写入模型中的所有权重时,这很方便。
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2
深度学习框架提供了内置函数来保存和加载整个网络。
需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。
让我们从熟悉的多层感知机开始尝试一下。
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()
MLP(
(hidden): Linear(in_features=20, out_features=256, bias=True)
(output): Linear(in_features=256, out_features=10, bias=True)
)
由于两个实例具有相同的模型参数,在输入相同的X
时,两个实例的计算结果应该相同。让我们来验证一下。
Y_clone = clone(X)
Y_clone == Y
tensor([[True, True, True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True, True, True]])
我们可以指定用于存储和计算的设备,如CPU和GPU。默认情况下,张量是在内存中创建的,然后使用CPU计算它。
在PyTorch中,CPU和GPU可以用torch.device('cpu')
和torch.cuda.device('cuda')
表示。应该注意的是,cpu
设备意味着所有物理CPU和内存。这意味着PyTorch的计算将尝试使用所有CPU核心。然而,gpu
设备只代表一个卡和相应的显存。如果有多个GPU,我们使用torch.cuda.device(f'cuda:{i}')
来表示第ii块GPU(ii从0开始)。另外,cuda:0
和cuda
是等价的。
import torch
from torch import nn
torch.device('cpu'), torch.cuda.device('cuda'), torch.cuda.device('cuda:1')
(device(type='cpu'),
<torch.cuda.device at 0x7f0bb457cf40>,
<torch.cuda.device at 0x7f0bb459d550>)
我们可以查询可用gpu的数量
torch.cuda.device_count()
2
默认情况下,张量是在CPU上创建的。我们可以查询张量所在的设备
x = torch.tensor([1, 2, 3])
x.device
device(type='cpu')
需要注意的是,无论何时我们要对多个项进行操作,它们都必须在同一个设备上。例如,如果我们对两个张量求和,我们需要确保两个张量都位于同一个设备上,否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算。
有几种方法可以在GPU上存储张量。例如,我们可以在创建张量时指定存储设备。
接下来,我们在第一个gpu
上创建张量变量X<
。
在GPU上创建的张量只消耗这个GPU的显存。我们可以使用nvidia-smi
命令查看显存使用情况。一般来说,我们需要确保不创建超过GPU显存限制的数据。
X = torch.ones(2, 3, device=try_gpu())
X
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:0')
假设你至少有两个GPU,下面的代码将在第二个GPU上创建一个随机张量
Y = torch.rand(2, 3, device=try_gpu(1))
Y
如果我们要计算X+Y
,我们需要决定在哪里执行这个操作。 不要简单地x
加上Y
, 因为这会导致异常。运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。
由于Y
位于第二个GPU上,所以我们需要将X
移到那里,然后才能执行相加运算。
Z = X.cuda(1)
print(X)
print(Z)
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:0')
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:1')
现在数据在同一个GPU上(Z和Y`都在),我们可以将它们相加。
假设变量Z
已经存在于第二个GPU上。如果我们还是调用Z.cuda(1)
怎么办?它将返回Z
,而不会复制并分配新内存。
Z.cuda(1) is Z
True
类似地,神经网络模型可以指定设备。下面的代码将模型参数放在GPU上。
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())
当输入为GPU上的张量时,模型将在同一GPU上计算结果。
模型参数存储在同一个GPU上
ndarray
中)时,将触发全局解释器锁,从而使所有GPU阻塞。最好是为GPU内部的日志分配内存,并且只移动较大的日志。X
生成 Y
的卷积核。 我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较 Y
与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。在计算互相关时,卷积窗口从输入张量的左上角开始,向下和向右滑动。 在前面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
我们将每次滑动元素的数量称为 步幅 (stride)
通常,当垂直步幅为 s h s_h sh 、水平步幅为 s w s_w sw 时,输出形状为:
$$
[(n_h -k_h +p_h+s_h)/s_h]* [(n_w -k_w+p_w+s_w)/s_w]
$$
深度学习对计算资源要求很高,训练可能需要数百个迭代周期,每次迭代都需要通过代价高昂的许多线性代数层传递数据。这也是为什么在20世纪90年代至21世纪初,优化凸目标的简单算法是研究人员的首选。然而,用GPU训练神经网络改变了这一格局。图形处理器 (Graphics Processing Unit,GPU)早年用来加速图形处理,使电脑游戏玩家受益。GPU可优化高吞吐量的 4×4 矩阵和向量乘法,从而服务于基本的图形任务。幸运的是,这些数学运算与卷积层的计算惊人地相似。由此,英伟达(NVIDIA)和ATI已经开始为通用计算操作优化gpu,甚至把它们作为 通用GPU(general-purpose GPUs,GPGPU)来销售。
那么GPU比CPU强在哪里呢?
首先,我们深度理解一下中央处理器(Central Processing Unit,CPU)的核心。 CPU的每个核心都拥有高时钟频率的运行能力,和高达数MB的三级缓存(L3 Cache)。 它们非常适合执行各种指令,具有分支预测器、深层流水线和其他使CPU能够运行各种程序的功能。 然而,这种明显的优势也是它的致命弱点:通用核心的制造成本非常高。 它们需要大量的芯片面积、复杂的支持结构(内存接口、内核之间的缓存逻辑、高速互连等等),而且它们在任何单个任务上的性能都相对较差。 现代笔记本电脑最多有4核,即使是高端服务器也很少超过64核,因为它们的性价比不高。
相比于CPU,GPU由 100∼1000 个小的处理单元组成(NVIDIA、ATI、ARM和其他芯片供应商之间的细节稍有不同),通常被分成更大的组(NVIDIA称之为warps)。 虽然每个GPU核心都相对较弱,有时甚至以低于1GHz的时钟频率运行,但庞大的核心数量使GPU比CPU快几个数量级。 例如,NVIDIA最近一代的Ampere GPU架构为每个芯片提供了高达312 TFlops的浮点性能,而CPU的浮点性能到目前为止还没有超过1 TFlops。 之所以有如此大的差距,原因其实很简单:首先,功耗往往会随时钟频率呈二次方增长。 对于一个CPU核心,假设它的运行速度比GPU快4倍,你可以使用16个GPU内核取代,那么GPU的综合性能就是CPU的 16×1/4=4 倍。 其次,GPU内核要简单得多,这使得它们更节能。 此外,深度学习中的许多操作需要相对较高的内存带宽,而GPU拥有10倍于CPU的带宽。
AlexNet和LeNet的设计理念非常相似,但也存在显著差异。 首先,AlexNet比相对较小的LeNet5要深得多。 AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。 其次,AlexNet使用ReLU而不是sigmoid作为其激活函数。 下面,让我们深入研究AlexNet的细节。
在AlexNet的第一层,卷积窗口的形状是 11×11 。 由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。 第二层中的卷积窗口形状被缩减为 5×5 ,然后是 3×3 。 此外,在第一层、第二层和第五层卷积层之后,加入窗口形状为 3×3 、步幅为2的最大汇聚层。 而且,AlexNet的卷积通道数目是LeNet的10倍。
在最后一个卷积层后有两个全连接层,分别有4096个输出。
此外,AlexNet将sigmoid激活函数改为更简单的ReLU激活函数。 一方面,ReLU激活函数的计算更简单,它不需要如sigmoid激活函数那般复杂的求幂运算。 另一方面,当使用不同的参数初始化方法时,ReLU激活函数使训练模型更加容易。 当sigmoid激活函数的输出非常接近于0或1时,这些区域的梯度几乎为0,因此反向传播无法继续更新一些模型参数。 相反,ReLU激活函数在正区间的梯度总是1。 因此,如果模型参数没有正确初始化,sigmoid函数可能在正区间内得到几乎为0的梯度,从而使模型无法得到有效的训练。
AlexNet通过dropout( 4.6节 )控制全连接层的模型复杂度,而LeNet只使用了权重衰减。 为了进一步扩充数据,AlexNet在训练时增加了大量的图像增强数据,如翻转、裁切和变色。 这使得模型更健壮,更大的样本量有效地减少了过拟合。