文章目录
1. 张量
1.1 Variable
Variable
是torch.autograd
中的数据类型,主要用于封装Tensor
,进行 自动求导。
- data:被包装的 Tensor
- grad:data 的梯度
- grad_fn :创建 Tensor 的 Function,是自动求导的关键
- requires_grad:指示是否需要梯度
- is_leaf:指示是否是叶子节点
1.2 Tensor
Pytorch 0.4.0 开始,Variable 并入Tensor。
- dtype:张量的数据类型,如
torch.FloatTensor
,torch.cuda.FloatTensor
- shape:张量的形状,如 ( 64 , 3 , 224 , 224 ) (64,3,224,224) (64,3,224,224)
- device :张量所在的设备,GPU/CPU,是加速的关键
1.3 张量的创建
张量创建-参考链接-pytorch中文手册
① 直接创建
- torch.tensor(),从data 创建 tensor
- torch.from_numpy(ndarry),从numpy 创建 tensor,共享内存,当修改一个值时,另一个也将被改动。
② 依据数值创建
- torch.zeros(),依 size 创建全 0 张量
- torch.zeros_like(),依 input 形状创建全 0 张量
- torch.ones()、torch.ones_likes(),依 input 形状创建全 1 张量
- torch.full()、torch.full_likes(),依 input 形状创建 指定数据 的张量
- torch.arange(),创建等差的 1 维张量
- torch.linspace(),创建均分的 1 维张量
- torch.logspace(),创建对数均分的 1 维张量
- torch.eye(),创建单位对角矩阵(2维张量)
③ 依概率分布创建张量
- torch.normal(),生成正太分布
- torch.randn(),生成标准正太分布
- torch.rand(),在区间 [0,1) 上,生成均匀分布
- torch.randint(),生成均匀分布
- torch.randperm(),生成从0~1的随机排列
- torch.bernoulli(),以 input 为概率,生成伯努利分布( 0-1 分布,两点分布)
1.4 张量的操作
张量-索引,切片,连接,换位Indexing, Slicing, Joining, Mutating Ops-参考链接
1.5 计算图与动态图
1.5.1 计算图
计算图是用来 描述运算 的有向无环图,计算图有两个主要元素: 节点(Node)和 边(Edge)。节点 表示 数据,如向量,矩阵,张量;边 表示 运算,如加减乘除卷积等。
用计算图表示 y = ( x + w ) ⋅ ( w + 1 ) y = (x+w)\cdot(w+1) y=(x+w)⋅(w+1),则可表示为 a = x + w , b = w + 1 , y = a ⋅ b a=x+w, b=w+1, y=a\cdot b a=x+w,b=w+1,y=a⋅b
计算图与梯度求导
∂ y ∂ w = ∂ y ∂ a ∂ a ∂ w + ∂ y ∂ b ∂ b ∂ w = b ∗ 1 + a ∗ 1 = b + a = ( w + 1 ) + ( x + w ) = 2 ∗ w + x + 1 = 2 × 1 + 2 + 1 = 5 \begin{aligned} \frac{\partial y}{\partial w} &=\frac{\partial y}{\partial a} \frac{\partial a}{\partial w}+\frac{\partial y}{\partial b} \frac{\partial b}{\partial w} \\ &=b * 1+a * 1 \\ &=b+a \\ &=(w+1)+(x+w) \\ &=2 * w+x+1 \\ &=2\times1+2+1=5 \end{aligned} ∂w∂y=∂a∂y∂w∂a+∂b∂y∂w∂b=b∗1+a∗1=b+a=(w+1)+(x+w)=2∗w+x+1=2×1+2+1=5
叶子节点:用户创建的结点成为叶子节点,如 x x x 与 w w w
设置 叶子节点 主要是为了节省内存,在梯度反向传播结束之后,非叶子结点的梯度会被释放掉。
- grad_fn :记录创建该张量时所用的方法(函数)
w = torch.tensor([1.], requires_grad=True) # 梯度为1
x = torch.tensor([2.], requires_grad=True) # 梯度为2
a = torch.add(w, x) # retain_grad()保留梯度
# a.retain_grad()
b = torch.add(w, 1)
y = torch.mul(a, b)
y.backward()
print(w.grad) # tensor([5.])
# 查看叶子结点
print("is_leaf:\n", w.is_leaf, x.is_leaf, a.is_leaf, b.is_leaf, y.is_leaf)
# True True False False False
# 查看梯度
print('gradient:\n', w.grad, x.grad, a.grad, b.grad, y.grad)
# tensor([5.]) tensor([2.]) None None None
# 查看grad_fn
print("grad_fn:\n", w.grad_fn, x.grad_fn, a.grad_fn, b.grad_fn, y.grad_fn)
# None None <AddBackward0 object at 0x000001ED078C3148>
# <AddBackward0 object at 0x000001ED1E91F108>
# <MulBackward0 object at 0x000001ED1E91F208>
1.6 autograd
1.6.1 torch.autograd.backward()
自动求取梯度
torch.autograd.backward(tensors, # 用于求导的张量,如 loss
grad_tensors=None, # 多梯度权重
retain_grad=None, # 保存计算图
create_graph=False) # 创建导数计算图,用于高阶求导
例子:
w = torch.tensor([1.], requires_grad=True) # 梯度为1
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
# a.retain_grad() # retain_grad()保留梯度
b = torch.add(w, 1)
y0 = torch.mul(a, b) # y0 = (x+w) * (w+1) = 6 dy0/dw = 5 y0对 w求梯度,也可看上面
y1 = torch.add(a, b) # y1 = (x+w) + (w+1) = 5 dy1/dw = 2
loss = torch.cat([y0, y1], dim=0) # tensor([6., 5.])
grad_tenors = torch.tensor([1., 2.]) # 多个梯度中权重的设置,y0对应 1,y1对应 2
# gradient 传入 torch.autograd.backward()中的grad_tensors
loss.backward(gradient=grad_tenors)
print(w.grad) # 9 = 1*5 + 2*2
1.6.2 torch.autograd.gard()
求取梯度
torch.autograd.grad(outputs, # 用于求导的张量,如loss
inputs, # 需要梯度的张量
grad_outputs=None, # 多梯度权重
retain_graph=None, # 保存计算图
create_graph=False) # 创建计算图
例子:
x = torch.tensor([3.], requires_grad=True)
y = torch.pow(x, 2) # y = x**2
grad_1 = torch.autograd.grad(y, x, create_graph=True)
# grad_1 = dy/dx = 2x = 2 * 3 = 6
print(grad_1) # (tensor([6.], grad_fn=<MulBackward0>),)
print(grad_1[0]) # tensor([6.], grad_fn=<MulBackward0>)
grad_2 = torch.autograd.grad(grad_1[0], x) # 求二阶导
# grad_2 = d(dy/dx)/dx = d(2x)/dx = 2
print(grad_2) # (tensor([2.]),)
1.6.3 autograd 三个要点
- 梯度不自动清零
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
for i in range(4):
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
y.backward()
print(w.grad)
w.grad.zero_() # 手动对梯度进行清零,'_':原位操作。
# tensor([5.])
# tensor([5.])
# tensor([5.])
- 依赖于叶子结点的结点,
requires_grad
默认为True
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
print(a.requires_grad, b.requires_grad, y.requires_grad)
# True True True
- 叶子结点不可执行in-place
in-place 原位操作:在原始内存当中去改变这一数据
为什么叶子节点不能进行in-place操作?
在反向传播过程中需要用到叶子结点。而在前向传播时,要记录叶子结点的地址。到反向传播时根据叶子结点的地址去寻找这个数据,进行使用计算。
a = torch.ones((1, ))
print(id(a), a)
# 1970317858920 tensor([1.])
a = a + torch.ones((1, ))
print(id(a), a)
# 1970369990728 tensor([2.])
# 开辟了新的地址,就不是原位操作,
a += torch.ones((1, )) # 原位操作.在原始地址上改变
print(id(a), a)
# 1970369990728 tensor([3.])
另一个例子
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
w.add_(1)
"""
autograd小贴士:
梯度不自动清零
依赖于叶子结点的结点,requires_grad默认为True
叶子结点不可执行in-place
"""
y.backward() # 报错,叶子结点不可执行in-place
2. 数据处理
机器学习训练五大步骤,数据,模型,损失函数,优化器,迭代训练。
2.1 数据读取 Dataloader
torch.utils.data.DataLoader(),数据加载器。组合数据集和采样器,并在数据集上提供单进程或多进程迭代器。参数解释链接
Epoch
:所有训练样本都已输入到模型中,成为一个EpochIteration
:一批样本输入到模型中,称之为一个IterationBatchSize
:批大小,决定一个Epoch有多少个Iteration- 假如有80个样本,设置 BatchSize 为 8,则 1 Epoch = 10 Iteration。
torch.utils.data.Dataset(),Dataset 抽象类,所有自定义的 Dataset 需要继承它,并且复写 __getitem__()
。getitem
:接收一个索引,返回一个样本。对__getitem__()
具体解释可参考链接。
class Dataset(object):
def __getitem__(self, index):
raise NotImplementedError
def __add__(self, other):
return ConcatDataset([self, other])
2.2 数据预处理 transforms
torchvision.transforms
常用的图像预处理方法,数据中心化,数据标准化,缩放,裁剪,旋转,翻转,填充,噪声添加,灰度变换,线性变换,仿射变换,亮度、饱和度及对比度变换。
transforms.Normalize(),逐channel 的对图像进行标准化, o u t p u t = ( i n p u t − m e a n ) / s t d output = (input-mean)/std output=(input−mean)/std
from torchvision import transforms
transforms.Normalize(mean, # 各通道的均值
std, # 各通道的标准差
inplace=False) # 是否原地操作
2.2.1 数据增强 Data Augmentation
数据增强又称为数据增广,数据扩增,它是对训练集进行变换,使训练集更丰富,从而让模型更具泛化能力。
张贤同学 - 二十二种 transforms 图片数据预处理方法 - 参考链接
方法 | 功能 |
---|---|
transforms.CenterCrop() | 裁剪, 从图像中心裁剪图片 |
transforms.RandomCrop() | 裁剪, 从图片中随机裁剪出尺寸为size的图片 |
transforms.RandomResizedCrop() | 裁剪, 随机大小,长宽比裁剪图片 |
transforms.FiveCrop() | 裁剪, 在图像上下左右以及中心裁剪出尺寸为size 的5 张图片 |
transforms.TenCrop() | 裁剪, 对这5 张图片进行水平或者垂直镜像获得10 张图片 |
transforms.RandomHorizontalFlip() | 翻转和旋转, 依概率水平或左右翻转图片 |
transforms.RandomVerticalFlip() | 翻转和旋转, 依概率垂直或上下翻转图片 |
transforms.RandomRotation() | 翻转和旋转, 随机旋转图片 |
transforms.Pad() | 图像变换, 对图片边缘进行填充 |
transforms.ColorJitter() | 图像变换, 调整亮度,对比度,饱和度和色相 |
transforms.Grayscale() | 图像变换, 依概率将图片转为灰度图 |
transforms.RandomGrayscale() | 图像变换, 依概率将图片转为灰度图 |
transforms.RandomAffine() | 图像变换, 对图片进行仿射变换,仿射变换是二维的线性变换,由五种基本原子变换构成,分别是旋转 ,平移 ,缩放 ,错切 和翻转 |
transforms.LinearTransformation() | 图像变换, |
transforms.RandomErasing() | 图像变换, 对图像进行随机遮挡 |
transforms.Lambda() | 图像变换, 用户自定义Lambda方法 |
transforms.Resize() | 图像变换, 给定大小缩放 |
transforms.Totensor() | 图像变换, 转为张量 |
transforms.Normalize() | 图像变换, 标准化,归一化 |
transforms.RandomChoice() | transforms的操作, 从一系列transforms 方法中随机挑一个 |
transforms.RandomApply() | transforms的操作, 依据概率执行一组transforms 操作 |
transforms.RandomOrder() | transforms的操作, 对一组transforms 操作打乱顺序 |
2.2.2 自定义 transforms
自定义 transforms 要素:
- 仅接收一个参数,返回一个参数
- 注意上下游的输出与输入,上一个transform 的输出是下一个 tranform 的输入
可以通过类实现多参数的传入,这里代码相关解释可参考链接
class YourTransforms(object):
def __init__(self, ...):
...
def __call__(self, img):
...
return img
def Compose(object):
def __call__(self, img):
for t in self.transforms:
img = t(img)
return img
椒盐噪声:
椒盐噪声又称为脉冲噪声,是一种随机出现的白点或者黑点,白点称为盐噪声,黑色为椒噪声。
信噪比(Signal-Noise Rate,SNR)是衡量噪声的比例,图像中图像像素的占比。值越大(越接近 1),噪声越小。
定义一个AddPepperNoise
类,作为添加椒盐噪声的 transform
。在构造函数中传入 信噪比 和 概率,在__call__()
函数中执行具体的逻辑,返回的是 image。
# 自定义添加椒盐噪声的 transform
class AddPepperNoise(object):
"""增加椒盐噪声
Args:
snr (float): Signal Noise Rate
p (float): 概率值,依概率执行该操作
"""
def __init__(self, snr, p=0.9):
assert isinstance(snr, float) or (isinstance(p, float))
self.snr = snr
self.p = p
# transform 会调用该方法
def __call__(self, img):
"""
Args:
img (PIL Image): PIL Image
Returns:
PIL Image: PIL image.
"""
# 如果随机概率小于 seld.p,则执行 transform
if random.uniform(0, 1) < self.p:
# 把 image 转为 array
img_ = np.array(img).copy()
# 获得 shape
h, w, c = img_.shape
# 信噪比
signal_pct = self.snr
# 椒盐噪声的比例 = 1 -信噪比
noise_pct = (1 - self.snr)
# 选择的值为 (0, 1, 2),每个取值的概率分别为 [signal_pct, noise_pct/2., noise_pct/2.]
# 椒噪声和盐噪声分别占 noise_pct 的一半
# 1 为盐噪声,2 为 椒噪声
mask = np.random.choice((0, 1, 2), size=(h, w, 1), p=[signal_pct, noise_pct/2., noise_pct/2.])
mask = np.repeat(mask, c, axis=2)
img_[mask == 1] = 255 # 盐噪声
img_[mask == 2] = 0 # 椒噪声
# 再转换为 image
return Image.fromarray(img_.astype('uint8')).convert('RGB')
# 如果随机概率大于 seld.p,则直接返回原图
else:
return img
3. 模型构建
模型创建包括两个要素, 构建子模块 __init__() 和 拼接子模块 forward()。 在 LeNet 中继承nn.Module
,必须实现__init__()
方法和forward()
方法。其中在__init__()
方法里创建子模块,在 forward()
方法里拼接子模块。
在 LeNet 的__init__()
中创建了 5 个子模块,nn.Conv2d()
和nn.Linear()
都是 继承于nn.module
,也就是说一个 module 都是包含多个子 module 的。
class LeNet(nn.Module):
# 子模块创建
def __init__(self, classes): # 父类函数调用
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, classes)
# 子模块拼接
def forward(self, x):
out = F.relu(self.conv1(x))
out = F.avg_pool2d(out, 2)
out = F.relu(self.conv2(out))
out = F.avg_pool2d(out, 2)
out = out.view(out.size(0), -1)
out = F.relu(self.fc1(out))
out = F.relu(self.fc2(out))
out = self.fc3(out)
return out
3.1 nn.Module
pytorch神经网络模块 torch.nn 里包含很多子模块,主要以下面四个展开,其具体方法可参考链接。
nn.Module
主要包含有8 个属性, 都是OrderDict
(有序字典)。在 LeNet 的__init__()
方法中会调用父类nn.Module
的__init__()
方法,创建这 8 个属性。
nn.Module 代码debug 解释部分 - 参考链接
def __init__(self):
"""
Initializes internal Module state, shared by both nn.Module and ScriptModule.
"""
torch._C._log_api_usage_once("python.nn_module")
self.training = True
self._parameters = OrderedDict() # 存储管理nn.Parameter类型的参数
self._buffers = OrderedDict() # 存储管理缓存属性,如BN层中的running_mean
self._non_persistent_buffers_set = set()
self._backward_hooks = OrderedDict() # ***_hooks: 存储管理钩子函数
self._forward_hooks = OrderedDict()
self._forward_pre_hooks = OrderedDict()
self._state_dict_hooks = OrderedDict()
self._load_state_dict_pre_hooks = OrderedDict()
self._modules = OrderedDict() # 存储管理 nn.Module类型的参数
nn.Module
使用方法总结:
- 一个 Module 里可以包含多个子 module。比如 LeNet 是一个Module,里面包括多个卷积层、池化层、全连接层等子Module
- 一个 Module 相当于一个运算,必须实现 forward() 运算
- 每个 Module 都有 8 个字典管理自己的属性
3.2 模型容器 Containers
常见的模型容器Containers
包含如下:
- nn.Sequetial: 按顺序包装多个网络层。 顺序性, 各网络层之间严格按顺序执行,常用于block构建。
- nn.ModuleList:像python的 list 一样包装多个网络层。 迭代性, 常用于大量重复网构建,通过 for 循环实现重复构建。
- nn.ModuleDict:像python的 dict 一样包装多个网络层。 索引性, 常用于可选择的网络层。
3.2.1 nn.Sequetial
在深度学习中,特征工程的概念被弱化了, 特征提取 和 分类器 这两步被融合到了一个神经网络中。在卷积神经网络中,前面的卷积层以及池化层可以认为是特征提取部分,而后面的全连接层可以认为是分类器部分。比如 LeNet 就可以分为特征提取和分类器两部分,这 2 部分都可以分别使用 nn.Sequetial
来包装。
nn.Sequential
是 nn.module
的容器,用于按顺序包装一组网络层,有以下两个特征:
- 顺序性:各网络层之间严格按照顺序构建
- 自带 forward():自带的 forward 里,通过 for 循环依次执行前向传播运算。
class LeNetSequential(nn.Module):
def __init__(self, classes):
super(LeNetSequential, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 6, 5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.classifier = nn.Sequential(
nn.Linear(16*5*5, 120),
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
nn.Linear(84, classes),
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size()[0], -1)
x = self.classifier(x)
return x
初始化时,nn.Sequetial
会调用__init__()
方法,将每一个子 module
添加到 自身的_modules
属性中。这里可以看到,传入的参数可以是一个 list
,或者一个 OrderDict
。如果是一个 OrderDict
,那么则使用 OrderDict
里的 key
,否则使用数字
作为 key
。(下图所示)
# container.py
def __init__(self, *args: Any):
super(Sequential, self).__init__()
if len(args) == 1 and isinstance(args[0], OrderedDict):
for key, module in args[0].items():
self.add_module(key, module)
else:
for idx, module in enumerate(args):
self.add_module(str(idx), module)
网络初始化后,得到两个子Module
:features
和 classifier
。
在进行前向传播时,会进入 LeNet 的forward()
函数,首先调用第一个Sequetial
容器:self.features
,由于self.features
也是一个 module,因此会调用_call_impl(self, *input, **kwargs):
函数,里面调用result = self.forward(*input, **kwargs)
,进入nn.Seuqetial
的forward()
函数,在这里依次调用所有的 module
。具体过程可参考视频链接-模型容器章节。
在nn.Sequetial
中,里面的每个子网络层 module 是使用序号来索引的,即使用数字来作为 key。一旦网络层增多,难以查找特定的网络层,这种情况可以使用 OrderDict (有序字典)。(结果如上图所示)
class LenetSequentialOrderDict(nn.Module):
def __init__(self, classes):
super(LenetSequentialOrderDict, self).__init__()
self.features = nn.Sequential(OrderedDict({
'conv1': nn.Conv2d(3, 6, 5),
'relu1': nn.ReLU(inplace=True),
'pool1': nn.MaxPool2d(kernel_size=2, stride=2),
'conv2': nn.Conv2d(6, 16, 5),
'relu2': nn.ReLU(inplace=True),
'pool2': nn.MaxPool2d(kernel_size=2, stride=2),
}))
self.classifier = nn.Sequential(OrderedDict({
'fc1': nn.Linear(16*5*5, 120),
'relu3': nn.ReLU(),
'fc2': nn.Linear(120, 84),
'relu4': nn.ReLU(inplace=True),
'fc3': nn.Linear(84, classes)
}))
def forward(self, x):
x = self.features(x)
x = x.view(x.size()[0], -1)
x = self.classifier(x)
return x
3.2.2 nn.ModuleList
nn.ModuleList
是 nn.module
的容器,用于包装一组网络层,以 迭代 方式调用网络层,主要方法如下:
- append():在ModoleList 后面 添加网络层
- extend(): 拼接 两个ModuleList
- insert():指定在ModuleList 中位置 插入 网络层
class ModuleList(nn.Module):
def __init__(self):
super(ModuleList, self).__init__()
# 列表生成式,生成20个全连接层,每个全连接层是 10个神经元的网络
self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(20)])
def forward(self, x):
for i, linear in enumerate(self.linears):
x = linear(x)
return x
3.2.3 nn.ModuleDict
nn.ModuleDict
是 nn.module
的容器,用于包装一组网络层,以 索引 方式调用网络层,主要方法如下:
- clear():清空 ModoleDict
- items():返回可迭代的键值对 (Key-value)
- keys():返回字典的键(key)
- values():返回字典的值 (values)
- pop():返回一对键值,并从字典中删除
class ModuleDict(nn.Module):
def __init__(self):
super(ModuleDict, self).__init__()
self.choices = nn.ModuleDict({
'conv': nn.Conv2d(10, 10, 3),
'pool': nn.MaxPool2d(3)
})
self.activations = nn.ModuleDict({
'relu': nn.ReLU(),
'prelu': nn.PReLU() # prelu:有正有负, relu:仅有正
})
def forward(self, x, choice, act):
x = self.choices[choice](x)
x = self.activations[act](x)
return x
3.3 卷积层
对CNN卷积神经网络的描述可参考这篇博文CNN-卷积神经网络
3.3.1 卷积维度
一般情况下,卷积核在几个维度上滑动,就是几维卷积。下面图片引用链接
3.3.2 nn.Conv2d
对多个二维信号进行二维卷积
nn.Conv2d(in_channels= , # 输入通道数
out_channels= , # 输出通道数,等价于卷积核数
kernel_size= , # 卷积核尺寸
stride=1, # 步长
padding=0, # 填充个数
dilation=1, # 空洞卷积大小
groups=1, # 分组卷积设置
bias=True, # 偏置
padding_mode='zeros')
转置卷积 Transpose Convolution
转置卷积(nn.ConvTranspose2d)和部分跨越卷积 (Fractionally-strided Convolution),用于对图像进行上采样 。转置矩阵形状上是一个转置关系,权值完全不一样,则正常矩阵与转置卷积是不可逆的。
详细理解可参考这篇博文一文搞懂反卷积,转置卷积
公式推导细节可参考知乎文章 转置卷积(Transpose Convolution)
3.4 池化层
池化层函数参数具体参考链接
nn.MaxPool2d
是对二维信号(图像)进行最大化池化。
nn.MaxPool2d(kernel_size, # 池化核尺寸
stride=None, # 步长
padding=0, # 填充个数
dilation=1, # 池化核间隔大小
return_indices=False, # 记录池化像素索引。记录最大值像素所在位置的索引,在最大值反池化上采样时使用
ceil_mode=False) # 默认为 False,尺寸向下取整。为 True 时,尺寸向上取整
nn.AvgPool2d
是对二维信号(图像)进行平均值池化。
nn.MaxUnPool2d
是对二维信号(图像)进行最大值池化上采样。