0603-常用的神经网络层
- 2021 年 4 月 25 日
- 筆記
- torch_long
0603-常用的神经网络层
pytorch完整教程目录://www.cnblogs.com/nickchen121/p/14662511.html
一、图像相关层
图像相关层主要包括卷积层(Conv)、池化层(Pool)等
- 这些层在实际的使用中也分为一维(1D)、二维(2D)和三维(3D)
- 池化方式分为平均池化(AvgPool)、最大值池化(MaxPool)、自适应池化(AdaptiveAvgPool)
- 卷积层除了常用的前向卷积,还有逆卷积(TransposeConv)
import torch as t
from torch import nn
from torch.autograd import Variable as V
from PIL import Image
from torchvision.transforms import ToTensor, ToPILImage
to_tensor = ToTensor() # 把 img 转为 Tensor
to_pil = ToPILImage()
nick = Image.open('img/0802程序输出nick图.jpeg') # 只能在这里牺牲我帅气的面貌
nick
上图是 nick 原图
# 锐化卷积核后的卷积,可以看上一篇文章分享的 06-01 DeepLearning-图像识别 那篇文章里讲到的特征提取
# 输入是一个 batch,batch_size=1
inp = to_tensor(nick).unsqueeze(0)
# 卷积核的构造
kernel = t.ones(3, 3, 3) / -9 # 锐化卷积核,即刻画人物边缘特征,去除掉背景
kernel[:, 1, 1] = 1
conv = nn.Conv2d(
in_channels=3, # 输入通道数
out_channels=1, # 输出通道数,等同于卷积核个数
kernel_size=3, # 核大小
stride=1,
bias=False)
conv.weight.data = kernel.view(1, 3, 3, 3)
"""
conv.weight.size() 输出为 torch.Size([1, 3, 3, 3])
第 1 个 1 代表着卷积核的个数,
第 2 个 3 表示的输入通道数,
后面两个 3 是二维卷积核的尺寸
"""
out = conv(V(inp))
to_pil(out.data.squeeze(0))
# 普通卷积
# 输入是一个 batch,batch_size=1
inp = to_tensor(nick).unsqueeze(0)
# 卷积核的构造
conv = nn.Conv2d(
in_channels=3, # 输入通道数
out_channels=1, # 输出通道数,等同于卷积核个数
kernel_size=3, # 核大小
stride=1,
bias=False)
out = conv(V(inp))
to_pil(out.data.squeeze(0))
上图是卷积后处理的 nick图,可以发现已经进行了一定程度的特征提取,并且通过对卷积核进行特定的处理,对人物边缘进行了一定的刻画,并且去除了背景
pool = nn.AvgPool2d(2, 2)
list(pool.parameters())
[]
out = pool(V(inp))
print(inp.size(), out.size())
to_pil(out.data.squeeze(0))
torch.Size([1, 3, 318, 320]) torch.Size([1, 3, 159, 160])
上图是池化后的 nick 图,由于我们选择 2 个像素池化为 1 个,可以发现池化后的图像大小正好是原图的 0.5 倍
除了卷积核池化,深度学习还经常用到以下几个层:
- Linear:全连接层
- BatchNorm:批规范化层,分为 1D、2D 和 3D。除了标准的 BatchNorm 外,还有在风格迁移中常用到的 InstanceNorm 层
- Dropout:dropout 层,用来防止过拟合,同样分为 1D、2D 和 3D
下面讲解它们的用法。
# 输入 batch_size=2,维度为 3
inp = V(t.randn(2, 3))
linear = nn.Linear(3, 4)
h = linear(inp) # (2,3)*(3,4)=(2,4)
h
tensor([[-0.7140, -0.0469, 1.1187, 2.0739],
[-1.0575, -1.8866, -1.0009, -0.2695]], grad_fn=<AddmmBackward>)
# 4通道,初始化标准差为 4,均值为 0
bn = nn.BatchNorm1d(4)
bn.weight.data = t.ones(4) * 4
bn.bias.data = t.zeros(4)
bn_out = bn(h)
# 注意输出的均值和方差
# 方差是标准差的平方,计算无偏方差分母会减 1
# 使用 unbiased=False,分母不减 1
bn_out.mean(0), bn_out.var(0, unbiased=False)
(tensor([0., 0., 0., 0.], grad_fn=<MeanBackward1>),
tensor([15.9946, 15.9998, 15.9999, 15.9999], grad_fn=<VarBackward1>))
# 每个元素以 0.5 的概率舍弃
dropout = nn.Dropout(0.5)
o = dropout(bn_out)
bn_out, o # 有一半左右的数变为 0
(tensor([[ 3.9993, 4.0000, 4.0000, 4.0000],
[-3.9993, -4.0000, -4.0000, -4.0000]],
grad_fn=<NativeBatchNormBackward>),
tensor([[ 0.0000, 8.0000, 8.0000, 0.0000],
[-7.9986, -8.0000, -0.0000, -8.0000]], grad_fn=<MulBackward0>))
上述的例子中都对 module 的属性也就是 data 进行了直接操作,其中大多都是可学习参数,这些参数在学习中会不断变化。在实际使用中,如非必要,还是不要去修改它们。
二、激活函数
2.1 ReLU 函数
tocrh 实现了常见的激活函数,具体的使用可以去查看官方文档或查看上面分享的文章,看文章的时候需要注意的是激活函数也可以作为独立的 layer 使用。在这里我们就讲讲最常使用的 ReLU 函数,它的数学表达式为:\(ReLU(x) = max(0,x)\),其实就是小于 0 的数置为 0
relu = nn.ReLU(inplace=True)
inp = V(t.randn(2, 3))
inp
tensor([[-1.5703, 0.0868, 1.0811],
[-0.9903, 0.5288, 0.5926]])
output = relu(inp) # 小于 0 的都被置为 0,等价于 inp.camp(min=0)
output
tensor([[0.0000, 0.0868, 1.0811],
[0.0000, 0.5288, 0.5926]])
其中 ReLU 函数有个 inplace 参数,如果设置为 True,它会让输出直接覆盖输入,这样可以节省内存的占用。
在上述例子中,是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络。对于这类网络,如果每次也都写上 forward 函数,就会特别麻烦。因此又两者简化方式——ModuleList 和 Sequential。其中 Sequential 是一个特殊的 Module,包含几个子 module,可以像用 list 一样使用它,但不能直接把输入传给它。
2.2 通过Sequential 构建前馈传播网络
# Sequential 的三种写法
net1 = nn.Sequential()
net1.add_module('conv', nn.Conv2d(3, 3, 3))
net1.add_module('batchnorm', nn.BatchNorm2d(3))
net1.add_module('activation_layer', nn.ReLU())
net2 = nn.Sequential(nn.Conv2d(3, 3, 3), nn.BatchNorm2d(3), nn.ReLU())
from collections import OrderedDict
net3 = nn.Sequential(
OrderedDict([('conv1', nn.Conv2d(3, 3, 3)), ('bn1', nn.BatchNorm2d(3)),
('relu1', nn.ReLU())]))
net1
Sequential(
(conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(activation_layer): ReLU()
)
net2
Sequential(
(0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
net3
Sequential(
(conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu1): ReLU()
)
# 可以根据名字或序号取出子 module
net1.conv, net2[0], net3.conv1
(Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)))
# 通过 Sequential 构建的网络处理数据
inp = V(t.rand(1, 3, 4, 4))
output = net1(inp)
output = net2(inp)
output = net3(inp)
output = net3.relu1(net1.batchnorm(net1.conv(inp)))
2.3 通过 ModuleList 构建前馈传播网络
modellist = nn.ModuleList([nn.Linear(3, 4), nn.ReLU(), nn.Linear(4, 2)])
inp = V(t.rand(1, 3))
for model in modellist:
inp = model(inp)
# output = modellist(inp) # 报错,因为 modellist 没有实现 forward 方法,需要新建一个类定义 forward 方法
在这里使用 ModuleList 而不是使用 list,是因为 ModuleList 是 Module 的子类,在 Module 中使用它时,可以自动识别为子 module,下面我们举例说明。
class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.list = [nn.Linear(3, 4), nn.ReLU()]
self.module_list = nn.ModuleList([nn.Conv2d(3, 3, 3), nn.ReLU()])
def forward(self):
pass
model = MyModule()
model
MyModule(
(module_list): ModuleList(
(0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
(1): ReLU()
)
)
for name, param in model.named_parameters():
print(name, param.size())
module_list.0.weight torch.Size([3, 3, 3, 3])
module_list.0.bias torch.Size([3])
从上面的打印结果可以看出,list 中的子 module 并没有被主 module 识别,而 ModuleList 中的子 module 却被主 module 识别了。这也意味着如果用 list 保存子 module,将无法调整其参数,因为这些参数会加入到 module 的参数中。
三、循环神经网络层
循环神经网络多用于自然语言处理(RNN),torch 中目前实现了最常用的三种 RNN:RNN(vanilla RNN)、LSTM 和 GRU,此外还有对应的三种 RNNCell。
RNN 和 RNNCell 的区别在于前者能够处理整个序列,而后者一次只处理序列中的一个时间点和数据,前者更易于使用,后者的灵活性更高。因此 RNN 一般组合调用 RNNCell 一起使用。
t.manual_seed(1000)
inp = V(t.randn(2, 3, 4)) # 输入:batch_size=3,序列长度为 2,序列中每个元素占 4 维
lstm = nn.LSTM(4, 3, 1) # lstm 输入向量 4 维,3 个隐藏元,1 层
# 初始状态:1 层,batch_size=3,3 个隐藏元
h0 = V(t.randn(1, 3, 3))
c0 = V(t.randn(1, 3, 3))
out, hn = lstm(inp, (h0, c0))
out
tensor([[[-0.3610, -0.1643, 0.1631],
[-0.0613, -0.4937, -0.1642],
[ 0.5080, -0.4175, 0.2502]],
[[-0.0703, -0.0393, -0.0429],
[ 0.2085, -0.3005, -0.2686],
[ 0.1482, -0.4728, 0.1425]]], grad_fn=<StackBackward>)
t.manual_seed(1000)
inp = V(t.randn(2, 3, 4)) # 输入:batch_size=3,序列长度为 2,序列中每个元素占 4 维
# 一个 LSTMCell 对应的层数只能是一层
lstm = nn.LSTMCell(4, 3) # lstm 输入向量 4 维,3 个隐藏元,默认1 层也只能是一层
hx = V(t.randn(3, 3))
cx = V(t.randn(3, 3))
out = []
for i_ in inp:
hx, cx = lstm(i_, (hx, cx))
out.append(hx)
t.stack(out)
tensor([[[-0.3610, -0.1643, 0.1631],
[-0.0613, -0.4937, -0.1642],
[ 0.5080, -0.4175, 0.2502]],
[[-0.0703, -0.0393, -0.0429],
[ 0.2085, -0.3005, -0.2686],
[ 0.1482, -0.4728, 0.1425]]], grad_fn=<StackBackward>)
词向量在自然语言中应用十分广泛,torch 同样也提供了 Embedding 层
embedding = nn.Embedding(4, 5) # 有 4 个词,每个词用 5 维的向量表示
embedding.weight.data = t.arange(0, 20).view(4, 5) # 可以用预训练好的词向量初始化 embedding
with t.no_grad(): # 下面代码的梯度回传有问题,因此去掉梯度
inp = V(t.arange(3, 0, -1)).long()
output = embedding(inp)
print(output)
tensor([[15, 16, 17, 18, 19],
[10, 11, 12, 13, 14],
[ 5, 6, 7, 8, 9]])
四、损失函数
在深度学习中也要用到各种各样的损失函数,这些损失函数可以看作是特殊的 layer,torch 也将这些损失函数实现为 nn.Module 的子类。
然而在实际的应用中,一般把损失函数独立出来,作为独立的一部分。详细的 loss 使用可以参考官方文档,这里仅以最常用的交叉熵损失为例讲解。
# batch_size=3,计算对应每个类别的分数(只有两个类别)
score = V(t.randn(3, 2))
# 三个样本分别属于 1,0,1 类,label 必须是 LongTensor
label = V(t.Tensor([1, 0, 1])).long()
# loss 与普通的 layer 没有什么差别
criterion = nn.CrossEntropyLoss()
loss = criterion(score, label)
loss
tensor([[ 1.0592, 1.4730],
[-0.1558, -0.8712],
[ 0.2548, 0.0817]])
tensor([1, 0, 1])
tensor(0.5630)