原文链接:https://github.com/ShusenTang/Dive-into-DL-PyTorch,将《动手学深度学习》 原书中MXNet代码实现改为PyTorch实现。
torch.Tensor是存储和变换数据的主要工具,它和NumPy的多维数组非常类似,但是Tensor提供GPU计算和自动求梯度等更多的功能。
函数 | 方法 | 创建结果 |
x = torch.empty(5,3) | 创建5x3的未初始化的Tensor | tensor([[4.5001e-39, 4.9592e-39, 5.2347e-39], [4.2246e-39, 1.0286e-38, 1.0653e-38], [1.0194e-38, 8.4490e-39, 1.0469e-38], [9.3674e-39, 9.9184e-39, 8.7245e-39], [9.2755e-39, 8.9082e-39, 9.9184e-39]]) |
x = torch.zeros(5,3,dtype=torch.long) | 创建一个5x3的long型全0的Tensor | tensor([[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]) |
x = torch.tensor([5.5,3]) | 直接根据数据创建 | tensor([5.5000, 3.0000]) |
x = x.new_ones(5,3,dtype=torch.float64) x = torch.randn_like(x,dtype=torch.float) | 通过现有Tensor来创建,默认重用输入Tensor的一些属性 | tensor([[1., 1., 1.], tensor([[ 0.8503, -0.5587, 0.5325], |
x.size() x.shape | 获取Tensor的形状,返回的torch.size是一个tuple,支持所有tuple的操作 | torch.Size([5, 3]) |
Tensor,tensor,ones,zeros,eye,arange, linespace(s,e,step),rand/randn,normal/uniform randperm | 这些创建方法都可以在创建的时候指定数据类型dtype和存放device(cpu/gpu) |
算术操作
在PyTorch中,同一操作可能有多种形式,以加法为例说明
(1)
x = torch.rand(5,3)
y = torch.rand(5,3)
print(x + y)
(2)
result = torch.empty(5,3)
print(torch.add(x, y,out = result))
其中,out是可选项。
(3)
y.add_(x)
print(y)
Pytorch操作的所有inplace版本都有后缀“_”,例如,x.copy_y(y),x.t_()
索引
可以使用类似NumPy的索引操作来访问Tensor的一部分,索引出的结果与原数据共享内存,修改一个,另一个也会修改。这在自己写的时候很容易出现bug。
y = x[0,:]
y += 1
print(y)
print(x[0,:])
函数 | 功能 | 例子 |
torch.index_select() | 在指定维度dim上选择,比如选取某些行、某些列。返回不Tensor不与原Tensor共享内存。 注:在python中,dim=0,表示按行取,dim=1表示按列取 | x = torch.randn(3,4) tensor([[ 0.2899, 0.5337, 0.1933, -0.7244], indicex = torch.LongTensor([0,2]) torch.index_select(x, 0, indicex) tensor([[ 0.2899, 0.5337, 0.1933, -0.7244], torch.index_select(x, 1, indicex) tensor([[ 0.2899, 0.1933], |
masked_select(input, mask) | 例子如上,a[a>0],使用ByteTensor进行选取 | x[x>0] tensor([0.2899, 0.5337, 0.1933, 0.0243, 0.3016, 0.0599]) |
non_zero(input) | 非0元素的下标 | torch.nonzero(x) tensor([[0, 0], |
gather(input, dim, index) | 根据index,在dim维度上选取数据进行聚合 | t = torch.Tensor([[1,2],[3,4]]) torch.gather(t,1,torch.LongTensor([[0,0],[1,0]])) tensor([[1., 1.], |
改变形状
使用view()来改变Tensor形状。
y = x.view(12)
z = x.view(-1,6)
-1表示所指的维度可以根据其他维度的值推算出来。view()返回的新tensor与源tensor共享内存,就是一个tensor,改变其中的一个,另一个也会跟着改变,view仅仅是改变了对这个张量的观察角度。另外还有一个reshape()函数可以改变形状,但是此函数并不能保证返回的是其拷贝,不推荐使用。推荐使用clone创造一个副本,然后使用view。使用clone还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源Tensor。
item()可以将标题Tensor转换成一个Python number。
线性代数
PyTorch支持一些线性函数,如下。具体的使用时,可以查找官方文档,免得用起来的时候自己造轮子。
函数 | 功能 |
trace | 对角线元素之和(矩阵的迹) |
diag | 对角线元素 |
triu/tril | 矩阵的上三角、下三角,可指定偏移量 |
mm/bmm | 矩阵乘法,batch的矩阵乘法 |
addmm/addbmm/addmv/addr/badbmm | 矩阵运算 |
t | 转置 |
dot/cross | 内积,外积 |
inverse | 求逆矩阵 |
svd | 奇异值分解 |
对两个形状不同的Tensor按元素进行运算时,可能会触发广播机制:先适当复制元素使这两个 Tensor形状相同后再按元素运算。
x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y)
索引、view是不会开辟新内存的。使用python自带的id函数,如果两个实例的ID一致,那么它们所对应的内存地址相同,反之则不同。
使用numpy()和from_numpy()可以将Tensor和Numpy中的数组相互转换,但是,这两个函数产生的Tensor和NumPy中的数组共享相同的内存。另外,通过torch.tensor()将NumPy中的array转换成Tensor,会进行数据拷贝,不再共享内存。
用to()可以将Tensor在CPU和GPU之间相互移动。
# 以下代码只有在PyTorch GPU版本上才会执行
if torch.cuda.is_available():
device = torch.device("cuda") # GPU
y = torch.ones_like(x, device=device) # 直接创建一个在GPU上的Tensor
x = x.to(device) # 等价于 .to("cuda")
z = x + y
print(z)
print(z.to("cpu", torch.double)) # to()还可以同时更改数据类型
PyTorch提供的autograd包能够根据输入和前身传播过程自动构建计算图,并执行反向传播。
将tensor的属性.requires_grad设置为True,将追踪在其上的所有操作,并且可以利用链式法则进行梯度传播,完成计算后,可以调用.backward()来完成所有梯度计算。此tensor的梯度将累积到.grad属性中。
注意在y.backward()是,如果y是标题,则不需要为backward传入任何参数,否则要传入一个与y同形的Tensor。
如果不想被继续追踪,可以调用.detach()将其从追踪记录中分离出来,这样就可以防止将来的计算被追踪,梯度就传不过去了。还可以用with torch.no_grad()将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,不需要计算可训练参数的梯度。
Function是另外一个很重要的类。Tensor和Function互相结合就可以构建一个记录整个计算过程的有向无环图(DAG),每个Tensor都有一个.grad_fn属性,该属性即创建该Tensor的Function,就是说该 Tensor是不是通过某些运算得到的,若是,则grad_fn返回一个与这些运算相关的对象,否则是None。
x = torch.ones(2,2,requires_grad=True)#注意x是直接创建的,所以它没有grad_fn,而y是通过一个加法操作创建的,所以它有一个grad_fn。
print(x)
#tensor([[1., 1.],
# [1., 1.]], requires_grad=True)
print(x.grad_fn)
# None
y = x + 2
print(y)
#tensor([[3., 3.],
# [3., 3.]], grad_fn=<AddBackward0>)
print(y.grad_fn)
# <AddBackward0 object at 0x000002334B777148>
# 像x这种直接创建的称为叶子节点,叶子节点对应的grad_fn是None
print(x.is_leaf,y.is_leaf)
#True False
当out是一个标量的时候,在调用backward()时不需要指定求导变量:out.backward()#等价于out.backward(torch.tensor(1))。
注意:grad在反向传播的过程中是累加的,这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。为了避免问题,不允许张量对张量求导,只允许标题对张量求导,求导结果是和自变量同形的张量。所以必要是我们要把张量通过将所有张量的元素加权求和的方式转换成标题。例如,假设y由自变量x计算而来,w是和y同形的张量,则y.backward(w)的含义是:先计算l = torch.sum(y*w),则l是个标题,然后求l对自变量x的导数。
如果需要修改tensor的值,又不希望被autograd记录,不影响反向传播,可以对tensor.data进行操作。
线性回归和softmax回归都是单层神经网络,它们涉及的概念和技术同样适用于大多数的深度学习模型。
它的基本要素包括模型、训练数据、损失函数和优化算法。既可以用神经网络图表示线性回归,又可以用矢量计算表示该模型。
通常,我们用训练数据中所有样本误差的平均来衡量模型预测的质量。
在求数值解的优化算法中,小批量随机梯度下降(mini-batch stochastic gradient descent)在深度学习中被广泛使用。先选取一组模型参数的初始值,如随机选取;接下来对参数进行多次迭代,使用每次迭代都可能降低损失函数的值。在每次迭代中,先随机均匀采样一个由固定数目训练数据样本所组成的小批量(mini-batch),然后求小批量数据样本中的平均损失有关模型参数的导数(梯度),最后用此结果与预先设定的一个正数的乘积作为模型参数在本次迭代的减小量。注意批量大小和学习率是人为设定的,称作超参数(hyperparameter),一般调参是指调节超参数。
常常会同时处理多个数据样本并用到失量计算。我们应该尽可能采用失量进行计算,提升计算效率。
import matplotlib
import torch
from IPython import display
from matplotlib import pyplot as plt
import numpy as np
import random
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.from_numpy(np.random.normal(0, 1, (num_examples, num_inputs)))
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.from_numpy(np.random.normal(0, 0.01, size=labels.size()))
print(features[0], labels[0])
def use_svg_display():
display.set_matplotlib_formats('svg')
def set_figsize(figsize=(3.5, 2.5)):
use_svg_display()
plt.rcParams['figure.figsize'] = figsize
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):
j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)])
yield features.index_select(0, j), labels.index_select(0, j)
def linreg(X, w_l, b):
return torch.mm(X, w_l) + b
def squared_loss(y_hat, y):
return (y_hat - y.view(y_hat.size())) ** 2 /2
def sgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, y)
break
set_figsize()
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1)
# plt.show()
w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, 1)), dtype=torch.double)
b = torch.zeros(1, dtype=torch.double)
w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
lo = loss(net(X, w, b), y).sum()
lo.backward()
sgd([w, b], lr, batch_size)
w.grad.data.zero_()
b.grad.data.zero_()
train_l = loss(net(features, w, b), labels)
print('epoch %d, loss %f' % (epoch + 1, train_l.mean().item()))
print(true_w, '\n', w)
print(true_b, '\n', b)
PyTorch提供了data包来读取数据,由于data常用作变量名,将导入的data模块用Data代替。
PyTorch提供了大量预定义的层,我们只需关注使用哪些层来构造模型。
torch.nn模块,定义了大量神经网络的层,nn的核心数据结构是Module,它既可以表示网络中的某个层,也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,撰写自己的网络层。一个nn.Module实例应该包含一些层以及返回输出的前身传播方法(forward)。
另外,还可以使用nn.sequential来更加方便的搭建网络,Sequential是一个有序容器,网络层按照在传入Sequential的顺序依次被添加到计算图中。
注意,torch.nn仅支持输入一个batch的样本,不支持单个样本输入,如果只有一个样本,可以使用input.unsqueeze(0)来添加一维。
在使用net前,需要初始化模型参数。PyTorch在init模块中提供了多种参数初始化方法,通过init.normal_将权重参数每个元素初始化为随机采样于均值为0、标准差为0.01的正太分布,偏差会初始化为零。
PyTorch在nn模块中提供了各种损失函数,这些损失函数可看作是一种特殊的层,PyTorch将这些损失函数实现为nn.Module的子类。
torch.optim模块提供了很多常用的优化算法如,SGD,Adm和RMSProp等。还可以为不同子网络设置不同的学习率,在finetune时经常用到。
optimizer =optim.SGD([
# 如果对某个参数不指定学习率,就使用最外层的默认学习率
{'params': net.subnet1.parameters()}, # lr=0.03
{'params': net.subnet2.parameters(), 'lr': 0.01}
], lr=0.03)
有时不想让学习率固定为一个常数,主要有两种做法,一种是修改optimizer.para_groups中对应的学习率,另一种推荐使用新建优化器。后者对于使用动量的优化器(Adma)会丢失动量状态信息,可能会造成损失函数的收敛出现震荡等情况。
# 调整学习率
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
(1)使用PyTorch可以简洁地实现模型
(2)torch.utils.data模块提供了有关数据处理的工具,torch.nn模块定义了大量神经网络层,torch.nn.init模块定义了各种初始化方法,torch.optim模块提供了模型参数初始化的各种方法。
import torch
from IPython import display
from matplotlib import pyplot as plt
import numpy as np
import random
import torch.utils.data as Data
import torch.nn as nn
from collections import OrderedDict
from torch.nn import init
import torch.optim as optim
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.tensor(np.random.normal(0, 1, (num_examples, num_inputs)), dtype=torch.float)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)
batch_size = 10
dataset = Data.TensorDataset(features, labels)
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
for X, y in data_iter:
print(X, y)
break;
class LinearNet(nn.Module):
def __init__(self, n_feature):
super(LinearNet, self).__init__()
self.Linear = nn.Linear(n_feature, 1)
def forward(self, x):
y = self.Linear(x)
return y
net = LinearNet(num_inputs)
print(net)
net = nn.Sequential(
nn.Linear(num_inputs, 1)
)
net = nn.Sequential()
net.add_module('Linear', nn.Linear(num_inputs, 1))
net = nn.Sequential(OrderedDict([
('Linear', nn.Linear(num_inputs, 1))
]))
print(net)
print(net[0])
for param in net.parameters():
print(param)
init.normal_(net[0].weight, mean=0, std=0.01)
init.constant_(net[0].weight, val=0)
loss = nn.MSELoss()
optimizer = optim.SGD(net.parameters(), lr=0.03)
print(optimizer)
# optimizer =optim.SGD([
# # 如果对某个参数不指定学习率,就使用最外层的默认学习率
# {'params': net.subnet1.parameters()}, # lr=0.03
# {'params': net.subnet2.parameters(), 'lr': 0.01}
# ], lr=0.03)
num_epochs = 3
for epoch in range(1, num_epochs + 1):
for X, y in data_iter:
output = net(X)
l = loss(output, y.view(-1, 1))
optimizer.zero_grad()
l.backward()
optimizer.step()
print('epoch %d, loss:%f' % (epoch, l.item()))
dense = net[0]
print(true_w, dense.weight)
print(true_b, dense.bias)
softmax回归跟线性回归一样将输入特征与权重做线性叠加,与线性回归的一个主要不同在于,softmax回归的输出值个数等于标签里的类别数。
分类问题需要得到离散的预测输出,一个简单的办法是将输出值当作预测类别是i是置信度。然而,直接使用输出层输出有两个问题。一方面,由于输出层的输出值的范围不确定,我们很难直观上判断这些值的意义。另一方面,由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量。
softmax运算符解决了以上两个问题,它将输出值变换成值为正且和为1的概率分布。softmax可以将输出变成一个合法的类别预测分布,实际上,真实标签也可以用类别分布表达。想要观测分类正在,并不需要预测概率完全等于标签概率,只需要y3比其他两个预测值y1和y2大就行。平方损失过于严格。改善的一个方法使用,使用交叉熵,它更适合衡量两个概率分布差异的测量函数。
torchvisioin主要的组成部分:
1、torchvision.datasets:一些加载数据的函数及常用的数据集接口;
2、torchvision.models:包含常用的模型结构(含预训练模型),例如AlexNet,VGG,ResNet等;
3、torchvision.transforms:常用的图像变换,例如裁剪、旋转等;
4、torchvision.utils:其他的一些有用 的方法。
通过torchvision的torchvision.datasets来下载这个数据集,第一次调用时会自动从网上获取数据。通过参数train来指定训练数据集或测试数据集。指定参数transform = transforms.ToTensor()使所有数据转换为Tensor,如果不进行转换则返回的是PIL图片。transorms.ToTensor()将尺寸为(HxWxC)且数据位于[0,255]的PIL图片或者数据类型为np.uint8的Numpy数组转换为尺寸为(CxHxW)且数据类型为torch.float32且位于[0.0,1.0]的Tensor。需要注意的是,feature的尺寸是CxHxW的,而不是HxWxC的,第一维是通道数,后面两维分别是图像的高和宽。
mnist_train是torch.utils.data.Dataset的子类,可以将其传入torch.utils.data.DataLoader来创建一个读取小批量数据样本的DataLoader实例。在实践中,数据读取经常是训练的性能瓶颈,特别当模型较简单或者计算硬件性能较高时。PyTorch中的DataLoader中一个很方便的功能是允许多进程来加速数据读取,使用参数num_workers来设置4个进程读取数据。
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import time
import sys
import d2lzh_pytorch as d2l
def show_fashiion_mnist(images, labels):
d2l.use_svg_display()
_, figs = plt.subplots(1, len(images), figsize=(12, 12))
for f, img, lbl in zip(figs, images, labels):
f.imshow(img.view((28, 28)).numpy())
f.set_title(lbl)
f.axes.get_xaxis().set_visible(False)
f.axes.get_yaxis().set_visible(False)
plt.show()
def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
mnist_train = torchvision.datasets.FashionMNIST(root='~/Dataset/FashionMNIST', train=True, download=False,
transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='~/Datasets/FashionMNIST', train=False, download=False,
transform=transforms.ToTensor())
print(type(mnist_train))
print(len(mnist_train), len(mnist_test))
feature, label = mnist_train[0]
print(feature.shape, label)
X, y = [], []
for i in range(10):
X.append(mnist_train[i][0])
y.append(mnist_train[i][1])
show_fashiion_mnist(X, get_fashion_mnist_labels(y))
batch_size = 256;
if sys.platform.startswith('win'):
num_workers = 0
else:
num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=True, num_workers=num_workers)
start = time.time()
for X, y in train_iter:
continue
print('%.2f sec' % (time.time() - start))
import torch
import torchvision
import numpy as np
import sys
import d2lzh_pytorch as d2l
def softmax(X):
X_exp = X.exp()
partition = X_exp.sum(dim=1, keepdims=True)
return X_exp / partition
def net(X):
return softmax(torch.mm(X.view((-1,num_ipnuts)), W) + b)
def cross_entropy(y_hat, y):
return - torch.log(y_hat.gather(1, y.view(-1, 1)))
def accuracy(y_hat, y):
return (y_hat.argmax(dim=1) == y).float().mean().item()
def evaluate_accuracy(data_iter, net):
acc_sum, n = 0.0, num_ipnuts
for X, y in data_iter:
acc_sum +=(net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
params=None, lr=None, optimizer=None):
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y).sum()
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
l.backward()
if optimizer is None:
d2l.sgd(params, lr, batch_size)
else:
optimizer.step()
train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
num_ipnuts = 28 * 28
num_outputs = 10
W = torch.tensor(np.random.normal(0, 0.01, (num_ipnuts, num_outputs)), dtype=torch.float)
b = torch.zeros(num_outputs, dtype=torch.float)
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
num_epochs, lr = 5, 0.1
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, params=[W, b], lr=lr)
import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
import d2lzh_pytorch as d2l
from collections import OrderedDict
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
num_inputs = 784
num_outputs = 10
class LinearNet(nn.Module):
def __init__(self, num_inputs, num_outputs):
super(LinearNet, self).__init__()
self.linear = nn.Linear(num_inputs, num_outputs)
def forward(self, x):
y = self.linear(x.view(x.shape[0], -1))
return y
net = LinearNet(num_inputs, num_outputs)
net = nn.Sequential(
OrderedDict([
('flatten', d2l.FlattenLayer()),
('linear', nn.Linear(num_inputs, num_outputs))
])
)
init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0)
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
import torch
import numpy as np
import matplotlib.pylab as plt
import sys
import d2lzh_pytorch as d2l
def xyplot(x_vals, y_vals, name):
d2l.set_figsize(figsize=(5, 2.5))
d2l.plt.plot(x_vals.detach().numpy(), y_vals.detach().numpy())
d2l.plt.xlabel('x')
d2l.plt.ylabel(name + '(x)')
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = x.relu()
y.sum().backward()
xyplot(x, x.grad, 'grad of relu')
d2l.plt.show()
import torch
import numpy as np
import sys
import d2lzh_pytorch as d2l
def relu(X):
return torch.max(input=X, other=torch.tensor(0.0))
def net(X):
X = X.view((-1, num_inputs))
H = relu(torch.matmul(X, W1) + b1)
return torch.matmul(H, W2) + b2
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
num_inputs, num_outputs, num_hiddens = 784, 10, 256
W1 = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=torch.float)
b1 = torch.zeros(num_hiddens, dtype=torch.float)
W2 = torch.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=torch.float)
b2 = torch.zeros(num_outputs, dtype=torch.float)
params = [W1, b1, W2, b2]
for param in params:
param.requires_grad_(requires_grad=True)
loss = torch.nn.CrossEntropyLoss()
num_epochs, lr = 5, 100.0
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
import d2lzh_pytorch as d2l
num_inputs, num_outputs, num_hiddens = 784, 10, 256
net = nn.Sequential(
d2l.FlattenLayer(),
nn.Linear(num_inputs, num_hiddens),
nn.ReLU(),
nn.Linear(num_hiddens, num_outputs),
)
for params in net.parameters():
init.normal_(params, mean=0, std=0.01)
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
loss = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
机器学习模型应该关注降低泛化误差。应对欠拟合和过拟合的一个办法是针对数据集选择合适复杂度的模型。
影响欠拟合和过拟合的另一个重要因素是训练数据集的大小。一般来说,如果训练数据集中样本数过少,特别是比模型的参数数量(按元素计)更少时,过拟合更容易发生。此外,泛化误差不会不会随训练数据集里样本数量增加而增加。因此,在计算资源允许的范围内,我们通常希望训练数据集大一些,特别是在模型复杂度较高时。
权重衰减(weight decay)是一种应对过拟合问题的常用方法,它等价于L2范数正则化。正则化通过为模型损失函数添加惩罚项使学出的模型参数值较小。
weight_decay
超参数来指定。DropOut不改变输入的期望值,在训练模型时起到正则化的作用,并可以用来应对过拟合,在测试模型时,为了拿到更加确定性的结果,一般不使用DropOut。可以分别设置各个层的丢弃概率,通常的建议是把靠近输入层的丢弃概率设得小一点。
在PyTorch中,我们只需要在全连接层后添加Dropout层并指定丢弃概率,在训练模型时,Dropout层将以指定的丢弃概率随机丢弃上一层的输出元素;在测试模型时(即在model.eval()后),dropout层并不发挥作用。
import torch
import torch.nn as nn
import numpy as np
import sys
import d2lzh_pytorch as d2l
def dropout(X, drop_prob):
X = X.float()
assert 0 <= drop_prob <= 1
keep_prob = 1 - drop_prob
if keep_prob == 0:
return torch.zeros_like(X)
mask = (torch.randn(X.shape) < keep_prob).float()
return mask * X / keep_prob
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
W1 = torch.tensor(np.random.normal(0, 0.01, size=(num_inputs, num_hiddens1)), dtype=torch.float, requires_grad=True)
b1 = torch.zeros(num_hiddens1, requires_grad=True)
W2 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens1, num_hiddens2)), dtype=torch.float, requires_grad=True)
b2 = torch.zeros(num_hiddens2, requires_grad=True)
W3 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens2, num_outputs)), dtype=torch.float, requires_grad=True)
b3 = torch.zeros(num_outputs, requires_grad=True)
params = [W1, b1, W2, b2, W3, b3]
drop_prob1, drop_prob2 = 0.2, 0.5
def net(X, is_training=True):
X = X.view(-1, num_inputs)
H1 = (torch.matmul(X, W1) + b1).relu()
if is_training: # 只在训练模型时使用丢弃法
H1 = dropout(H1, drop_prob1) # 在第一层全连接后添加丢弃层
H2 = (torch.matmul(H1, W2) + b2).relu()
if is_training:
H2 = dropout(H2, drop_prob2) # 在第二层全连接后添加丢弃层
return torch.matmul(H2, W3) + b3
def evaluate_accuracy(data_iter, net):
acc_sum, n = 0.0, 0
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval() # 评估模式, 这会关闭dropout
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
net.train() # 改回训练模式
else: # 自定义的模型
if('is_training' in net.__code__.co_varnames): # 如果有is_training这个参数
# 将is_training设置成False
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
num_epochs, lr, batch_size = 5, 100.0, 256
loss = torch.nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
因此,在模型参数初始化完成后,我们交替地进行正向传播和反向传播,并根据反向传播计算的梯度迭代模型参数。既然我们在反向传播中使用了正向传播中计算得到的中间变量来避免重复计算,那么这个复用也导致正向传播结束后不能立即释放中间变量内存。这也是训练要比预测占用更多内存的一个重要原因。另外需要指出的是,这些中间变量的个数大体上与网络层数线性相关,每个变量的大小跟批量大小和输入个数也是线性相关的,它们是导致较深的神经网络使用较大批量训练时更容易超内存的主要原因。
深度模型有关数值稳定性的典型问题是衰减和爆炸。
当神经网络的层数较多时,模型的数值稳定性容易变差。
如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层,在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此,在这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。因此,我们通常将神经网络的模型参数,特别是权重参数进行随机初始化。
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import sys
import d2lzh_pytorch as d2l
print(torch.__version__)
torch.set_default_tensor_type(torch.FloatTensor)
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]])
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())
)
all_features = all_features.fillna(0)
all_features = pd.get_dummies(all_features, dummy_na=True)
print(all_features.shape)
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float)
train_labels = torch.tensor(train_data.SalePrice.values, dtype=torch.float).view(-1, 1)
loss = torch.nn.MSELoss()
def get_net(feature_num):
net = nn.Linear(feature_num, 1)
for param in net.parameters():
nn.init.normal_(param, mean=0, std=0.01)
return net
def log_rmse(net, features, labels):
with torch.no_grad():
# 将小于1的值设成1,使得取对数时数值更稳定
clipped_preds = torch.max(net(features), torch.tensor(1.0))
rmse = torch.sqrt(2 * loss(clipped_preds.log(), labels.log()).mean())
return rmse.item()
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
dataset = torch.utils.data.TensorDataset(train_features, train_labels)
train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)
# 这里使用了Adam优化算法
optimizer = torch.optim.Adam(params=net.parameters(), lr=learning_rate, weight_decay=weight_decay)
net = net.float()
for epoch in range(num_epochs):
for X, y in train_iter:
l = loss(net(X.float()), y.float())
optimizer.zero_grad()
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
def get_k_fold_data(k, i, 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), dim=0)
y_train = torch.cat((y_train, y_part), dim=0)
return X_train, y_train, X_valid, y_valid
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(X_train.shape[1])
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.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse',
range(1, num_epochs + 1), valid_ls,
['train', 'valid'])
print('fold %d, train rmse %f, valid rmse %f' % (i, train_ls[-1], valid_ls[-1]))
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('%d-fold validation: avg train rmse %f, avg valid rmse %f' % (k, train_l, valid_l))
def train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size):
net = get_net(train_features.shape[1])
train_ls, _ = train(net, train_features, train_labels, None, None,
num_epochs, lr, weight_decay, batch_size)
d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse')
print('train rmse %f' % train_ls[-1])
preds = net(test_features).detach().numpy()
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)
基于Module类的模型构造方法,它可以让模型构造更加灵活。
Module类是nn模块里提供的一个模型构造类,是所有神经网络模块的基类。
当模型的前向计算为简单串联各个层的计算时,Sequential类可以通过更加简单的方式定义模型。它可以接收一个子模块的有序字典(OrderedDict)或者一系列子模块作为参数来逐一添加Module实例,而模型的前向计算就是将这些实例按添加的顺序逐一计算。
ModuleList类接收一个子模块的列表作为输入,然后也可以类似List那样进行append和extend操作。
ModuleDict类,接收一个子模块的字典作为输入,然后也可以类似字典那样进行添加访问操作。
直接继承Module类可以极大地拓展模型构造的灵活性。通过get_constant函数创建训练中不被迭代有参数,即常数参数。在前向计算中,除了使用创建的常数参数外,我们还使用Tensor的函数和Python的控制流,并多次调用相同的层。
import torch
from torch import nn
class MLP(nn.Module):
def __init__(self, **kwargs):
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Linear(784, 256)
self.act = nn.ReLU()
self.output = nn.Linear(256, 10)
def forward(self, x):
a = self.act(self.hidden(x))
return self.output(a)
class FancyMLP(nn.Module):
def __init__(self, **kwargs):
super(FancyMLP, self).__init__(**kwargs)
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20)
def forward(self, x):
x = self.linear(x)
x = nn.functional.relu(torch.mm(x, self.rand_weight.data) + 1)
x = self.linear(x) # 复用线性层
while x.norm().item() > 1:
x /= 2
if x.norm().item() < 0.8:
x *= 10
return x.sum()
class NestMLP(nn.Module):
def __init__(self, **kwargs):
super(NestMLP, self).__init__(**kwargs)
self.net = nn.Sequential(nn.Linear(40, 30), nn.ReLU())
def forward(self, x):
return self.net(x)
X = torch.rand(2, 784)
net = MLP()
print(net)
print(net(X))
net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
net.append(nn.Linear(256, 10))
print(net[-1])
print(net)
net = nn.ModuleDict({
'linear': nn.Linear(784, 256),
'act': nn.ReLU(),
})
net['output'] = nn.Linear(256, 10)
net['output2'] = nn.Linear(10, 100);
print(net['linear'])
print(net.output)
print(net)
x = torch.rand(2, 20)
net = FancyMLP()
print(net)
print(net(x))
# 因为FancyMLP 和 Sequential 类都是Module的子类,所以可以嵌套调用它们。
net = nn.Sequential(NestMLP(), nn.Linear(30, 20), FancyMLP())
X = torch.rand(2, 40)
print(net)
print(net(X))
本节将深入讲解如何访问和初始化模型参数,以及如何在多个层之间共享一份模型参数。
init模块,包含了多种模型初始化方法。
对于Sequential实例中含模型参数的层,可能通过Module类的parameters()或者named_parameters方法来访问所有参数,后者除了返回参数Tensor之外,还会返回其名字。
有些情况下,我们希望在多个层之间共享模型参数,如果我们传入Sequential的模块是同一个Module实例的话参数也是共享的。
深度学习的一个魅力在于神经网络中各式各样的层。
import torch
from torch import nn
class CenterdLayer(nn.Module):
def __init__(self, **kwargs):
super(CenterdLayer, self).__init__(**kwargs)
def forward(self, x):
return x - x.mean()
layer = CenterdLayer()
print(layer(torch.tensor([1, 2, 3, 4, 5], dtype=torch.float)))
class MyDense(nn.Module):
def __init__(self):
super(MyDense, self).__init__()
self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])
self.params.append(nn.Parameter(torch.randn(4, 1)))
def forward(self, x):
for i in range(len(self.params)):
x = torch.mm(x, self.params[i])
return x
net = MyDense()
print(net)
class MyDictDense(nn.Module):
def __init__(self):
super(MyDictDense, self).__init__()
self.params = nn.ParameterDict({
'linear1': nn.Parameter(torch.randn(4, 4)),
'linear2': nn.Parameter(torch.randn(4, 1))
})
self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增
def forward(self, x, choice='linear1'):
return torch.mm(x, self.params[choice])
net = MyDictDense()
print(net)
可以直接使用save函数和load函数分别存储和读取Tensor。
在PyTorch中,Module的可学习参数(即权重和偏差),模块包含在参数中(通过model.parameters()访问),state_dict是一个从参数名称映射到参数Tensor的字典对象。
注意,只有具有可学习参数的层(卷积层、线性层)才有state_dict中的条目,优化器(optim)也有一个state_dict,其中包含关于优化器状态以及所使用的超参数的信息。
PyTorch中保存和加载训练模型有两种常见的方法:1、仅保存和加载模型参数(state_dict);2、保存和加载整个模型。
此外,还有一些其他使用场景,例如GPU与CPU之间的模型保存与读取、使用多块GPU的模型的存储等等,使用的时候可以参考官方文档。
存在在不同位置中的数据是不可以直接进行计算的。即存放在CPU上的数据不可以直接与存放在GPU上的数据进行运算,位于不同GPU上的数据也不能直接进行计算的。
通常在卷积层中使用更加直观的互相关运算(cross-correlation)。在二维输入数组和一个二维核(kernel)数组通过互相关运算输出一个二维数组,这个二维核又称为卷积核或过滤器(filter)。卷积核窗口(卷积窗口)的形状取决于卷积核的高和宽。
二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标题偏差。通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。
卷积层可通过重复使用卷积核有效地表征局部空间。
本书中提到的卷积运算均指互相关运算。
import torch
from torch import nn
def corr2d(X, K):
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
class Conv2D(nn.Module):
def __init(self, kernel_size):
super(Conv2D, self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
X = torch.ones(6, 8)
X[:, 2:6] = 0
print(X)
K = torch.tensor([[1.0, -1.0]])
Y = corr2d(X, K)
print(Y)
conv2d = Conv2D(kernel_size=(1, 2))
step = 20
lr = 0.01
for i in range(step):
Y_hat = conv2d(X)
l = ((Y_hat - Y) ** 2).sum()
l.backward()
# 梯度下降
conv2d.weight.data -= lr * conv2d.weight.grad
conv2d.bias.data -= lr * conv2d.bias.grad
# 梯度清0
conv2d.weight.grad.fill_(0)
conv2d.bias.grad.fill_(0)
if (i + 1) % 5 == 0:
print('Step %d, loss %.3f' % (i + 1, l.item()))
卷积层的两个超参数,填充和步幅,可以对给定形状的输入和卷积核改变输出形状。
import torch
from torch import nn
# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
# (1, 1)代表批量大小和通道数(“多输入通道和多输出通道”一节将介绍)均为1
X = X.view((1, 1) + X.shape)
Y = conv2d(X)
return Y.view(Y.shape[2:]) # 排除不关心的前两维:批量和通道
# 注意这里是两侧分别填充1行或列,所以在两侧一共填充2行或列
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)
X = torch.rand(8, 8)
print(comp_conv2d(conv2d, X).shape)
彩色图像在高和宽2个维度外还有RGB 3个颜色通道,将之称为通道维。
当输入数据含有多个通道时,我们需要构造一个输入通道数与输入数据的通道数相同的卷积核。
最后讲座卷积窗口形状为1x1的多通道卷积层。实际上,1x1卷积的主要运算发生在通道维上。输出中的每个元素来自输入中的高和宽上相同位置在不同通道之间的按权重累加。1×1卷积层通常用来调整网络层之间的通道数,并控制模型复杂度。
同一个边缘对应的输出可能出现在卷积输出Y中的不同位置,进而对后面的模式识别造成不便。池化层是为了缓解卷积层对位置的过度敏感性。使用2x2最大池化层时,只要卷积层识别的模式在高和宽上移动不超过一个元素,我们依然可以将它检测出来。池化也可以通过步幅和填充来改变输出形状。
在处理多通道输入数据时,池化层对每个输入通道分别池化,而不将各个通道的输入按通道相加,即池化层的输出通道数与输入通道数相等。
卷积层块里的基本单位是卷积层后接最大池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠构成。
import time
import torch
from torch import nn, optim
import sys
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 6, 5),
nn.Sigmoid(),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5),
nn.Sigmoid(),
nn.MaxPool2d(2, 2)
)
self.fc = nn.Sequential(
nn.Linear(16*4*4, 120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
def evaluate_accuracy(data_iter, net,
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')):
acc_sum, n = 0.0, 0
with torch.no_grad():
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval() # 评估模式, 这会关闭dropout
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
net.train() # 改回训练模式
else: # 自定义的模型, 3.13节之后不会用到, 不考虑GPU
if('is_training' in net.__code__.co_varnames): # 如果有is_training这个参数
# 将is_training设置成False
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
# 本函数已保存在d2lzh_pytorch包中方便以后使用
def train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs):
net = net.to(device)
print("training on ", device)
loss = torch.nn.CrossEntropyLoss()
batch_count = 0
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
optimizer.zero_grad()
l.backward()
optimizer.step()
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec'
% (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))
net = LeNet()
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)
Inception模块的实现,特别是四个并行线路的特征连结,连结就是连结,没有特别的技巧。
class Inception(nn.Module):
# c1 - c4为每条线路里的层的输出通道数
def __init__(self, in_c, c1, c2, c3, c4):
super(Inception, self).__init__()
# 线路1,单1 x 1卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
# 线路2,1 x 1卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3 x 3最大池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_c, 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) # 在通道维上连结输出
通常我们将批量归一化层置于全连接层中的仿射变换和激活函数之间。如果批量归一化无益,理论上,学出的模型可以不使用批量归一化。批量归一化在训练模式和预测模式下的计算结果是不一样的。