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
# 梯度清零
conv2d.weight.grad.zero_()
conv2d.bias.grad.zero_()
if (i + 1) % 5 == 0:
print('Step %d, loss %.3f' % (i + 1, l.item()))
.data
仍保留,但建议使用 .detach()
, 区别在于 .data
返回和 x 的相同数据 tensor, 但不会加入到x的计算历史里,且require s_grad = False, 这样有些时候是不安全的, 因为 x.data
不能被 autograd 追踪求微分 。 .detach()
返回相同数据的 tensor ,且 requires_grad=False ,但能通过 in-place 操作报告给 autograd 在进行反向传播的时候.
在以上这段代码中,参数更新部分并不需要追踪梯度,所以需要使用.data
来进行参数更新。
二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。影响特征图中元素 x x x的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做 x x x的感受野(receptive field)。
卷积层的两个超参数,即填充和步幅,它们可以对给定形状的输入和卷积核改变输出形状。
**填充(padding)**是指在输入高和宽的两侧填充元素(通常是0元素)。
如果原输入的高和宽是 n h n_h nh和 n w n_w nw,卷积核的高和宽是 k h k_h kh和 k w k_w kw,在高的两侧一共填充 p h p_h ph行,在宽的两侧一共填充 p w p_w pw列,则输出形状为:
( n h + p h − k h + 1 ) × ( n w + p w − k w + 1 ) \left(n_{h}+p_{h}-k_{h}+1\right) \times\left(n_{w}+p_{w}-k_{w}+1\right) (nh+ph−kh+1)×(nw+pw−kw+1)
我们在卷积神经网络中使用奇数高宽的核,比如 3 × 3 3 \times 3 3×3, 5 × 5 5 \times 5 5×5的卷积核,对于高度(或宽度)为大小为 2 k + 1 2 k + 1 2k+1的核,令步幅为1,在高(或宽)两侧选择大小为 k k k的填充,便可保持输入与输出尺寸相同。
在互相关运算中,卷积核在输入数组上滑动,每次滑动的行数与列数即是步幅(stride)。
如果原输入的高和宽是 n h n_h nh和 n w n_w nw,卷积核的高和宽是 k h k_h kh和 k w k_w kw,在高的两侧一共填充 p h p_h ph行,在宽的两侧一共填充 p w p_w pw列,则输出形状为:
⌊ ( n h + p h − k h + s h ) / s h ⌋ × ⌊ ( n w + p w − k w + s w ) / s w ⌋ \left\lfloor\left(n_{h}+p_{h}-k_{h}+s_{h}\right) / s_{h}\right\rfloor \times\left\lfloor\left(n_{w}+p_{w}-k_{w}+s_{w}\right) / s_{w}\right\rfloor ⌊(nh+ph−kh+sh)/sh⌋×⌊(nw+pw−kw+sw)/sw⌋
我们在卷积神经网络中使用奇数高宽的核,比如 3 × 3 3 \times 3 3×3, 5 × 5 5 \times 5 5×5的卷积核,对于高度(或宽度)为大小为 2 k + 1 2 k + 1 2k+1的核,令步幅为1,在高(或宽)两侧选择大小为 k k k的填充,便可保持输入与输出尺寸相同。
二维卷积层经常用于处理图像,与此前的全连接层相比,它主要有两个优势:
一是全连接层把图像展平成一个向量,在输入图像上相邻的元素可能因为展平操作不再相邻,网络难以捕捉局部信息。而卷积层的设计,天然地具有提取局部信息的能力。
我们使用Pytorch中的nn.Conv2d
类来实现二维卷积层,主要关注以下几个构造函数参数:
in_channels
(python:int) – Number of channels in the input imagout_channels
(python:int) – Number of channels produced by the convolutionkernel_size
(python:int or tuple) – Size of the convolving kernelstride
(python:int or tuple, optional) – Stride of the convolution. Default: 1padding
(python:int or tuple, optional) – Zero-padding added to both sides of the input. padding指的是上下两侧同时扩充的层数,总的扩展层数为padding*2.bias
(bool, optional) – If True, adds a learnable bias to the output. Default: True以上参数凡是涉及到类型为int整数型或者tuple元组型都对应着两种情况:一是卷积核的长宽、宽度和高度上的步幅、行方向和列方向填充数相等,则为int整数型;二是以上参数不相等,则为tuple元组型。
forward
函数的参数为一个四维张量,形状为 ( N , C i n , H i n , W i n ) (N, C_{in}, H_{in}, W_{in}) (N,Cin,Hin,Win),返回值也是一个四维张量,形状为 ( N , C o u t , H o u t , W o u t ) (N, C_{out}, H_{out}, W_{out}) (N,Cout,Hout,Wout),其中 N N N是批量大小, C , H , W C, H, W C,H,W分别表示通道数、高度、宽度。
池化层主要用于缓解卷积层对位置的过度敏感性。同卷积层一样,池化层每次对输入数据的一个固定形状窗口(又称池化窗口)中的元素计算输出,池化层直接计算池化窗口内元素的最大值或者平均值,该运算也分别叫做最大池化或平均池化。
池化层也可以在输入的高和宽两侧填充并调整窗口的移动步幅来改变输出形状。池化层填充和步幅与卷积层填充和步幅的工作机制一样。
在处理多通道输入数据时,池化层对每个输入通道分别池化,但不会像卷积层那样将各通道的结果按通道相加。这意味着池化层的输出通道数与输入通道数相等。
我们使用Pytorch中的nn.MaxPool2d
实现最大池化层,关注以下构造函数参数:
kernel_size
– the size of the window to take a max overstride
– the stride of the window. Default value is kernel_sizepadding
– implicit zero padding to be added on both sidesforward
函数的参数为一个四维张量,形状为 ( N , C , H i n , W i n ) (N, C, H_{in}, W_{in}) (N,C,Hin,Win),返回值也是一个四维张量,形状为 ( N , C , H o u t , W o u t ) (N, C, H_{out}, W_{out}) (N,C,Hout,Wout),其中 N N N是批量大小, C , H , W C, H, W C,H,W分别表示通道数、高度、宽度。
LeNet分为卷积层块和全连接层块。
卷积层块里的基本单位是卷积层后接平均池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的平均池化层则用来降低卷积层对位置的敏感性。
Sequential类实现LeNet模型
#net
class Flatten(torch.nn.Module): #展平操作
def forward(self, x):
return x.view(x.shape[0], -1)
class Reshape(torch.nn.Module): #将图像大小重定型
def forward(self, x):
return x.view(-1,1,28,28) #(B x C x H x W)
net = torch.nn.Sequential( #Lelet
Reshape(),
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), #b*1*28*28 =>b*6*28*28
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*6*28*28 =>b*6*14*14
nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5), #b*6*14*14 =>b*16*10*10
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*16*10*10 => b*16*5*5
Flatten(), #b*16*5*5 => b*400
nn.Linear(in_features=16*5*5, out_features=120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
因为卷积神经网络计算比多层感知机要复杂,建议使用GPU来加速计算。我们查看看是否可以用GPU,如果成功则使用cuda:0
,否则仍然使用cpu
。
# This function has been saved in the d2l package for future use
#use GPU
def try_gpu():
"""If GPU is available, return torch.device as cuda:0; else return torch.device as cpu."""
if torch.cuda.is_available():
device = torch.device('cuda:0')
else:
device = torch.device('cpu')
return device
device = try_gpu()
device
注:
机器学习的特征提取:手工定义的特征提取函数
神经网络的特征提取:通过学习得到数据的多级表征,并逐级表⽰越来越抽象的概念或模式。
AlexNet首次证明了学习到的特征可以超越⼿⼯设计的特征,从而⼀举打破计算机视觉研究的前状。
特征:
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
nn.ReLU(),
nn.MaxPool2d(3, 2), # kernel_size, stride
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(3, 2),
# 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
# 前两个卷积层后不使用池化层来减小输入的高和宽
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)
# 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 4096),
nn.ReLU(),
nn.Dropout(0.5),
#由于使用CPU镜像,精简网络,若为GPU镜像可添加该层
#nn.Linear(4096, 4096),
#nn.ReLU(),
#nn.Dropout(0.5),
# 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10),
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
VGG:通过重复使⽤简单的基础块来构建深度模型。
Block:数个相同的填充为1、窗口形状为 3 × 3 3\times 3 3×3的卷积层,接上一个步幅为2、窗口形状为 2 × 2 2\times 2 2×2的最大池化层。
卷积层保持输入的高和宽不变,而池化层则对其减半。
VGG的简单实现
def vgg_block(num_convs, in_channels, out_channels): #卷积层个数,输入通道数,输出通道数
blk = []
for i in range(num_convs):
if i == 0:
blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
else:
blk.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
blk.append(nn.ReLU())
blk.append(nn.MaxPool2d(kernel_size=2, stride=2)) # 这里会使宽高减半
return nn.Sequential(*blk)
LeNet、AlexNet和VGG:先以由卷积层构成的模块充分抽取 空间特征,再以由全连接层构成的模块来输出分类结果。
NiN:串联多个由卷积层和“全连接”层构成的小⽹络来构建⼀个深层⽹络。
⽤了输出通道数等于标签类别数的NiN块,然后使⽤全局平均池化层对每个通道中所有元素求平均并直接⽤于分类。
1×1卷积核作用
1.放缩通道数:通过控制卷积核的数量达到通道数的放缩。
2.增加非线性。1×1卷积核的卷积过程相当于全连接层的计算过程,并且还加入了非线性激活函数,从而可以增加网络的非线性。
3.计算参数少
def nin_block(in_channels, out_channels, kernel_size, stride, padding):
blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, 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())
return blk
NiN重复使⽤由卷积层和代替全连接层的1×1卷积层构成的NiN块来构建深层⽹络。
NiN去除了容易造成过拟合的全连接输出层,而是将其替换成输出通道数等于标签类别数 的NiN块和全局平均池化层。
NiN的以上设计思想影响了后⾯⼀系列卷积神经⽹络的设计。
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) # 在通道维上连结输出
基于inception块构建GoogLeNet如图:
注:
与 seq2seq模型相似,Transformer同样基于编码器-解码器架构,其区别主要在于以下三点:
(1) Transformer blocks:将seq2seq模型重的循环网络替换为了Transformer Blocks,该模块包含一个多头注意力层(Multi-head Attention Layers)以及两个position-wise feed-forward (2)networks(FFN)。对于解码器来说,另一个多头注意力层被用于接受编码器的隐藏状态。
(3)Add and norm:多头注意力层和前馈网络的输出被送到两个“add and norm”层进行处理,该层包含残差结构以及层归一化。
Position encoding:由于自注意力层并没有区分元素的顺序,所以一个位置编码层被用于向序列元素里添加位置信息。
在我们讨论多头注意力层之前,先来迅速理解以下自注意力(self-attention)的结构。自注意力模型是一个正规的注意力模型,序列的每一个元素对应的key,value,query是完全一致的。如图10.3.2 自注意力输出了一个与输入长度相同的表征序列,与循环神经网络相比,自注意力对每个元素输出的计算是并行的,所以我们可以高效的实现这个模块。
F i g . 10.3.2 自 注 意 力 结 构 Fig.10.3.2\ 自注意力结构 Fig.10.3.2 自注意力结构
多头注意力层包含 h h h个并行的自注意力层,每一个这种层被成为一个head。对每个头来说,在进行注意力计算之前,我们会将query、key和value用三个现行层进行映射,这 h h h个注意力头的输出将会被拼接之后输入最后一个线性层进行整合。
F i g . 10.3.3 多 头 注 意 力 Fig.10.3.3\ 多头注意力 Fig.10.3.3 多头注意力
假设query,key和value的维度分别是 d q d_q dq、 d k d_k dk和 d v d_v dv。那么对于每一个头 i = 1 , … , h i=1,\ldots,h i=1,…,h,我们可以训练相应的模型权重 W q ( i ) ∈ R p q × d q W_q^{(i)} \in \mathbb{R}^{p_q\times d_q} Wq(i)∈Rpq×dq、 W k ( i ) ∈ R p k × d k W_k^{(i)} \in \mathbb{R}^{p_k\times d_k} Wk(i)∈Rpk×dk和 W v ( i ) ∈ R p v × d v W_v^{(i)} \in \mathbb{R}^{p_v\times d_v} Wv(i)∈Rpv×dv,以得到每个头的输出:
o ( i ) = a t t e n t i o n ( W q ( i ) q , W k ( i ) k , W v ( i ) v ) o^{(i)} = attention(W_q^{(i)}q, W_k^{(i)}k, W_v^{(i)}v) o(i)=attention(Wq(i)q,Wk(i)k,Wv(i)v)
这里的attention可以是任意的attention function,比如前一节介绍的dot-product attention以及MLP attention。之后我们将所有head对应的输出拼接起来,送入最后一个线性层进行整合,这个层的权重可以表示为 W o ∈ R d 0 × h p v W_o\in \mathbb{R}^{d_0 \times hp_v} Wo∈Rd0×hpv
o = W o [ o ( 1 ) , … , o ( h ) ] o = W_o[o^{(1)}, \ldots, o^{(h)}] o=Wo[o(1),…,o(h)]
接下来我们就可以来实现多头注意力了,假设我们有h个头,隐藏层权重 h i d d e n _ s i z e = p q = p k = p v hidden\_size = p_q = p_k = p_v hidden_size=pq=pk=pv 与query,key,value的维度一致。除此之外,因为多头注意力层保持输入与输出张量的维度不变,所以输出feature的维度也设置为 d 0 = h i d d e n _ s i z e d_0 = hidden\_size d0=hidden_size。
Transformer 模块另一个非常重要的部分就是基于位置的前馈网络(FFN),它接受一个形状为(batch_size,seq_length, feature_size)的三维张量。Position-wise FFN由两个全连接层组成,他们作用在最后一维上。因为序列的每个位置的状态都会被单独地更新,所以我们称他为position-wise,这等效于一个1x1的卷积。
除了上面两个模块之外,Transformer还有一个重要的相加归一化层,它可以平滑地整合输入和其他层的输出,因此我们在每个多头注意力层和FFN层后面都添加一个含残差连接的Layer Norm层。这里 Layer Norm 与7.5小节的Batch Norm很相似,唯一的区别在于Batch Norm是对于batch size这个维度进行计算均值和方差的,而Layer Norm则是对最后一维进行计算。层归一化可以防止层内的数值变化过大,从而有利于加快训练速度并且提高泛化性能。 (ref)
这一层并不会改变输入张量的维度。
与循环神经网络不同,无论是多头注意力网络还是前馈神经网络都是独立地对每个位置的元素进行更新,这种特性帮助我们实现了高效的并行,却丢失了重要的序列顺序的信息。为了更好的捕捉序列信息,Transformer模型引入了位置编码去保持输入序列元素的位置。
假设输入序列的嵌入表示 X ∈ R l × d X\in \mathbb{R}^{l\times d} X∈Rl×d, 序列长度为 l l l嵌入向量维度为 d d d,则其位置编码为 P ∈ R l × d P \in \mathbb{R}^{l\times d} P∈Rl×d ,输出的向量就是二者相加 X + P X + P X+P。
位置编码是一个二维的矩阵,i对应着序列中的顺序,j对应其embedding vector内部的维度索引。我们可以通过以下等式计算位置编码:
P i , 2 j = s i n ( i / 1000 0 2 j / d ) P_{i,2j} = sin(i/10000^{2j/d}) Pi,2j=sin(i/100002j/d)
P i , 2 j + 1 = c o s ( i / 1000 0 2 j / d ) P_{i,2j+1} = cos(i/10000^{2j/d}) Pi,2j+1=cos(i/100002j/d)
f o r i = 0 , … , l − 1 a n d j = 0 , … , ⌊ ( d − 1 ) / 2 ⌋ for\ i=0,\ldots, l-1\ and\ j=0,\ldots,\lfloor (d-1)/2 \rfloor for i=0,…,l−1 and j=0,…,⌊(d−1)/2⌋
F i g . 10.3.4 位 置 编 码 Fig. 10.3.4\ 位置编码 Fig.10.3.4 位置编码
编码器包含一个多头注意力层,一个position-wise FFN,和两个 Add and Norm层。对于attention模型以及FFN模型,我们的输出维度都是与embedding维度一致的,这也是由于残差连接天生的特性导致的,因为我们要将前一层的输出与原始输入相加并归一化。
class EncoderBlock(nn.Module):
def __init__(self, embedding_size, ffn_hidden_size, num_heads,
dropout, **kwargs):
super(EncoderBlock, self).__init__(**kwargs)
self.attention = MultiHeadAttention(embedding_size, embedding_size, num_heads, dropout)
self.addnorm_1 = AddNorm(embedding_size, dropout)
self.ffn = PositionWiseFFN(embedding_size, ffn_hidden_size, embedding_size)
self.addnorm_2 = AddNorm(embedding_size, dropout)
def forward(self, X, valid_length):
Y = self.addnorm_1(X, self.attention(X, X, X, valid_length))
return self.addnorm_2(Y, self.ffn(Y))
整个编码器由n个刚刚定义的Encoder Block堆叠而成,因为残差连接的缘故,中间状态的维度始终与嵌入向量的维度d一致;同时注意到我们把嵌入向量乘以 d \sqrt{d} d 以防止其值过小。
class TransformerEncoder(d2l.Encoder):
def __init__(self, vocab_size, embedding_size, ffn_hidden_size,
num_heads, num_layers, dropout, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.embedding_size = embedding_size
self.embed = nn.Embedding(vocab_size, embedding_size)
self.pos_encoding = PositionalEncoding(embedding_size, dropout)
self.blks = nn.ModuleList()
for i in range(num_layers):
self.blks.append(
EncoderBlock(embedding_size, ffn_hidden_size,
num_heads, dropout))
def forward(self, X, valid_length, *args):
X = self.pos_encoding(self.embed(X) * math.sqrt(self.embedding_size))
for blk in self.blks:
X = blk(X, valid_length)
return X
Transformer 模型的解码器与编码器结构类似,然而,除了之前介绍的几个模块之外,编码器部分有另一个子模块。该模块也是多头注意力层,接受编码器的输出作为key和value,decoder的状态作为query。与编码器部分相类似,解码器同样是使用了add and norm机制,用残差和层归一化将各个子层的输出相连。
仔细来讲,在第t个时间步,当前输入 x t x_t xt是query,那么self attention接受了第t步以及前t-1步的所有输入 x 1 , … , x t − 1 x_1,\ldots, x_{t-1} x1,…,xt−1。在训练时,由于第t位置的输入可以观测到全部的序列,这与预测阶段的情形项矛盾,所以我们要通过将第t个时间步所对应的可观测长度设置为t,以消除不需要看到的未来的信息。
对于Transformer解码器来说,构造方式与编码器一样,除了最后一层添加一个dense layer以获得输出的置信度分数。下面让我们来实现一下Transformer Decoder,除了常规的超参数例如vocab_size embedding_size 之外,解码器还需要编码器的输出 enc_outputs 和句子有效长度 enc_valid_length。