大佬希望神经网络是门科学,但实际上是工程,反过来讲50%是艺术。
# 序
未来。
基于PyTorch# 数据操作与预处理
在深度学习中,多维数组 ndarray 是最主要的承载信息的数据结构之一。其中:
- 0-d :标量 scalar 。表示类别等。
- 1-d :向量 vector 。可以表示一个样本的所有特征值。
- 2-d :矩阵 matrix 。可以表示一些样本的所有特征值。
- 3-d :图片通过找到每个维度的坐标进而定位某个属性值在三维空间上的位置。可以表示一张图片,这时,三个维度分别是宽*高*通道(RGB)。
- 4-d :图片批量。在深度学习神经网络训练时,通常输入一个批量 Batch 的图片作为训练数据,如每次读128张图片。批量大小*宽*高*通道。
- 5-d :视频批量。批量大小*时间*宽*高*通道。
Tensor
中文名为张量,是深度学习中的基本数据类型。
创建并访问张量的基本属性:
# 创建张量 x = torch.arange(12) # 通过逻辑创建张量 x = tensor_x == tensor_y # 查看张量的形状 print(x.shape) # 查看张量的元素总数,类似于len() print(x.numel()) # 改变张量维度 x = x.reshape(3, 4)
涉及剃度下降求解全局最优参数时,可能需要创建一些特定形状的张量以承载参数。
创建全0或全1张量,或使用特定数值创建张量:
# 创建全0张量 x = torch.zeros((3, 4, 5)) # 创建全1张量 x = torch.ones((3, 4, 5)) # 像numpy.array()以一样通过列表新建张量 x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
创建张量时可以指定数据类型,且张量之间可以按照维度拼接。
指定数据类型与拼接:
x = torch.tensor([1, 2, 3], dtype=torch.int16) y = torch.tensor([1., 2., 3.], dtype=torch.float16) # 按照dim(维度)拼接 a = torch.cat([x, y], dim=0)
张量与多维数组在数据类型的转换方面十分方便:
nd_array = x_tensor.numpy()
ndarray_to_x_tensor = torch.tensor(nd_array)
# 若tensor内仅含一个标量,则可以转为Python的数据类型
a = float(torch.tensor([1]))
Tensor运算
在 +-×÷ 基础运算方面,张量和 numpy 的多维数组在语法上来看区别不大。甚至在广播机制上也有一致的使用方法。然而二者本身的概念有显著区别,并且在基础运算结果方面可能也有细微区别,如在求和方面,前者返回的以就是张量,而后者返回的是常量。
按维度求和并保留轴数不变的意义在于,方便利用广播机制:
sum_a = a.sum(axis=1, keepdim=True) # 利用广播机制 a / sum_a # 保留了数轴后,发现数轴上没有元素 a = a.reshape((2, 3, 5)) sum_a = a.sum(axis=1, keepdim=True) print(sum_a) tensor([[[15, 18, 21, 24, 27]], [[60, 63, 66, 69, 72]]])
在张量的乘法:
点积:对应的元素相乘然后加起来
# 返回的是仅含一个标量元素的张量
a = torch.dot(x, y)
mv:矩阵乘向量:
a = torch.mv(A, x)
mm:矩阵乘矩阵:
a = torch.mm(A, B)
特征缩放
如果都为很大的正数,则先取[latex]\log[/latex],再做均值为〇方差为一的归一化。
# 线性代数
terminology
- [latex]\textbf{L1范数}[/latex]:向量元素的绝对值之和。 torch.abs(a_tensor).sum()
- [latex]\textbf{L2范数}[/latex]:向量的长度。 torch.norm(a_tensor)
- [latex]\textbf{F-范数}[/latex]:Frobenius Norm,是矩阵的范数。将每个元素平方后求和,再开根号得到。 torch.norm(a_tensor)
- [latex]\textbf{哈达玛积}[/latex]:两个同阶矩阵相乘得到的结果为两个矩阵的哈达玛积。
自动求导
[latex]\textbf{自动求导}[/latex]旨在计算一个函数在指定值上的导数,有别于提供了解析的导数表达式并适用于数学推导和证明的[latex]\textbf{符号求导}[/latex],以及不依赖于函数的解析表达式而是通过对函数在一些离散点上进行有限差分的计算来估计导数值的[latex]\textbf{数值求导}[/latex]。terminology
根据链式法则,自动求导具有两种求导方式,即[latex]\textbf{正向累积}[/latex]与[latex]\textbf{反向传递}[/latex]。前者执行计算图,存储中间结果;后者从相反方向执行计算图,去除不需要的枝。这有异于神经网络。
此外,[latex]\textbf{计算图}[/latex]用于描述复杂的数学运算或计算模型,尤其在深度学习中被广泛应用。在深度学习中,神经网络模型可以被表示为一个计算图,其中每个节点表示一个神经元或神经网络层,边表示数据或梯度的流动方向。
计算图既可以被显式构造,又可以被隐式构造。前者定义出数学公式的每一个符号,被 Tensorflow 所采用;后者是执行计算时自动产生的节点或连接,被 PyTorch 所采用。
自动求导代码
# 仅有浮点数才能要求存储梯度,也可以通过dtype=来设置数据类型
x = torch.arange(12.0)
# 存梯度
x.requires_grad_(True)
# 定义函数
y = 2 * torch.dot(x, x)
# 执行反向传播,并存储每个分量上的梯度
y.backward()
# 输出每个分量上的梯度
print(x.grad)
在默认情况下, PyTorch 会累积梯度,所以在进行下一次自动求导之前,需要清除之前记录的梯度值:
# 清除之前累积的梯度
x.grad.zero_()
.detach() 多用于固定神经网络的参数。将某些计算移动到记录的计算图之外:
y = x * x
# 把y当作一个常数,而不是关于x的函数。这是.detach()的作用
u = y.detach()
z = u * x
z.sum().backward()
# 线性回归
线性模型
给定[latex]n[/latex]维输入[latex]x=\lbrack x_1,x_2,\dots ,x_n\rbrack^T[/latex],那么线性模型将有一个维度是[latex]n[/latex]的权重[latex]w=\lbrack w_1,w_2,\dots ,w_n\rbrack^T[/latex]以及一个标量偏差 b 。
输出是输入的加权和:
[latex]\textbf{向量版本}[/latex]:[latex]y=<w,x>+b[/latex]。通过 平方损失 ,比较真实值与预测值,能够较好衡量预测的质量:
线性模型可以被看作是单层神经网络
单层神经网络指带权重层数为1的神经网络。
关于线性回归的详细概念,见此处。
线性回归的显示解
类似于此处介绍的正规方程解法,能够使用方程或式子表示最佳答案,这种方式被称为显示解
梯度下降 解法不是显示解。梯度下降很少在实践中被直接采用,深度学习通常采用 小批量随机梯度下降
采用梯度下降计算梯度将多次遍历全体样本,这种办法会消耗相当的时间。因此在深度学习实践中一般采用小批量随机梯度下降,即随机采用 b 个样本来近似地计算损失。该被指定的样本数量,即批量 b ,是一个超参数(学习率是另一个超参数)。
批量不能太小,因为基于GPU的并行计算不适合小批量;也不能太大,因为会消耗大量内存。
获取小批量样本数据
非常有用的生成随机正态分布数据的算法:
def synthetic_data(w, b, n):
# 生成y = wX + b + 噪声
# 0为均值,1为标准差,(n, len(w))分别指n个样本,列数为len(w)
X = torch.normal(0, 1, (n, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, .01, y.shape)
return X, y.reshape((-1, 1))
true_w = torch.tensor([2., -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 100)
fig, ax = pyplot.subplots(figsize=(8, 8))
print(features)
# 在部分版本中,需要先detach()才能转多维数组。不转也行
ax.scatter(features[:, 0].detach().numpy(), labels.detach().numpy())
pyplot.show()
基于上述数据,生成小批量样本:
def data_iter(batch_size, features, labels):
# 共有n个样本
n = len(features)
# 为这n个样本生成index
indices = list(range(n))
# 打乱index
random.shuffle(indices)
# 0为start,n为stop,batch_size为步长
for i in range(0, n, batch_size):
# min()的意义在于,当i+batch_size超过样本量时,选择样本量
batch_indices = torch.tensor(indices[i:min(i + batch_size, n)])
yield features[batch_indices], labels[batch_indices]
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
代码中迭代器的相关概念,详见此处。
[latex]\textit{batch_size}[/latex]越大,之后随机梯度下降使用的样本越多,占用的显存越多。基于PyTorch实现线性回归
通过深度学习框架方便地实现线性回归模型、生成数据集。
from torch.utils import data
import torch
from torch import nn
def load_array(data_arrays, batch_size, is_train=True):
"""PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
## 初始化参数
true_w = torch.tensor([2., -3.4])
true_b = 4.2
## 超参数部分
batch_size = 10
lr = 0.03
## 数据部分
# 这里在生成随机数据。实践中使用真实数据
features, labels = synthetic_data(true_w, true_b, 1000)
## 选取小样本
# 创建数据迭代器实例
data_iter = load_array((features, labels), batch_size)
## 模型部分
# 构造单线性层神经网络。其中输入维度为2,输出维度为1
net = nn.Sequential(nn.Linear(2, 1)) # Sequential()是一个承载神经网络各层的容器,各layer按顺序排列
# 初始化模型参数,这一步非必须
net[0].weight.data.normal_(0, 0.01) # 访问并修改weight的data,并使用均值为0标准差为0.01的数据进行替换
net[0].bias.data.fill_(0) # 访问并修改bias的data,并使用0来进行替换
# 定义使用何种误差,此处使用均方误差
loss = nn.MSELoss()
# 实例化SGD,其中.parameters()指拿出所有参数
trainer = torch.optim.SGD(net.parameters(), lr=lr)
## 训练部分
# 设定梯度下降次数
num_epochs = 3
# 进行梯度下降
for epoch in range(num_epochs):
for X, y in data_iter:
# net(X)相当于.predict(X)。实际上是net.__call__(X),然而该函数被映射到了net.forward()
l = loss(net(X), y)
# 将梯度清零
trainer.zero_grad()
# 进行反向传播
l.backward()
# 更新参数的值
trainer.step()
# 得到上一次迭代后的总损失值
l = loss(net(features), labels)
print(l)
代码中的 load_array() 与此处作用一致,但实现过程基于 PyTorch ,而不是手动实现。该函数返回的是一个迭代器。
随机梯度下降收敛的前提是学习率不断减小,还要设置 lr_scheduler
详见此处。
QA
样本大小不是批量数的整数倍怎么办?
三种解决方法:1.将批量大小调小;2.把最后一部分丢掉;3.从其他地方随机采样以补充。
b 过小可能对模型收敛有益,但大批量不行。因为随机梯度下降实质上为模型训练带来了噪音,而批量越小噪音越多。这样的噪音对神经网络的训练有益。不过批量也不能太小,因为太小的批量不适合并行计算。Batchsize是否影响模型结果?
Epoch与Batch?
一个 Epoch 是指一轮训练,整个过程是遍历所有的 Batch 并更新参数。而在每一轮 Epoch 里,有多个 Batch 。通过每一份 Batch 进行参数更新,叫做迭代 iteration 。
# Softmax回归
该方法处理分类任务。
其概念与公式详见此处。
图像分类数据集
MNIST 是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。因此,使用类似但更复杂的 Fashion-MNIST 更加合适。
读取数据
import time
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
def get_dataloader_worker():
"""一般而言数据放在硬盘上,此处指定使用3个进程来读取数据"""
return 3
## 通过.ToTensor()将图像数据从PIL类型转变为32位浮点数格式,同时对数据进行归一化处理
trans = transforms.ToTensor()
# 下载数据,并实例化。其中,train=True意为下载的是训练集;transform=trans意为拿到数据后转为Tensor;download=True意味从网上下载。
mnist_train = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=False, transform=trans, download=True)
# 查看第一类物体第一张图片的shape。可以看到只有1个channel,说明是黑白图片,长宽具有28个像素。
print(mnist_test[0][0].shape)
batch_size = 256
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_worker())
# 查看数据读取时间
start = time.time()
for X, y in train_iter:
continue
end = time.time()
print(f'time cost: {end - start:.3f}')
使用 matplotlib 展示图像数据
def get_fashion_mnist_labels(labels):
"""返回数据集的文本标签"""
# 实际上,Fashion_mnist的数据集标签按顺序排列
text_labels = ['t-shirt', 'trousers', 'pullover', 'dress', 'coat', 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
"""画出一些图片"""
figsize = (num_cols * scale, num_rows * scale)
_, axes = pyplot.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
ax.imshow(img.numpy())
ax.set_title(titles[i])
else:
ax.imshow(img)
ax.set_title(titles[i])
pyplot.show()
## 通过.ToTensor()将图像数据从PIL类型转变为32位浮点数格式,同时对数据进行归一化处理
trans = transforms.ToTensor()
# 下载数据,并实例化。其中,train=True意为下载的是训练集;transform=trans意为拿到数据后转为Tensor;download=True意味从网上下载。
mnist_train = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=False, transform=trans, download=True)
# 查看第一类物体第一张图片的shape。可以看到只有1个channel,说明是黑白图片,长宽具有28个像素。
print(mnist_test[0][0].shape)
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y))
为重复使用上述函数,可以将其封装:
def load_data_fashion_mnist(batch_size, resize=None):
"""下载Fashion_mnist数据集并加载至内存"""
trans = [transforms.ToTensor()]
# 如果要resize图像
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(root='./dir', train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root='./dir', train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, num_workers=get_dataloader_worker()),
data.DataLoader(mnist_test, batch_size, num_workers=get_dataloader_worker()))
从零实现\(\textit{Softmax}\)
该方法要求输入是向量,因此需要将形如 (channel, length, width) 的图片拉为向量。拉成向量会丢失空间信息,使用卷积神经网络可以较好弥补这一点。
根据事物类别数量确定模型输出维度。
基于PyTorch实现\(\textit{Softmax}\)
def init_weights(layer):
if type(layer) == nn.Linear:
# 设定参数的标准差为0.01。在深度神经网络中有讲究
nn.init.normal_(layer.weight, std=0.01)
## 通过.ToTensor()将图像数据从PIL类型转变为32位浮点数格式,同时对数据进行归一化处理
trans = transforms.ToTensor()
# 下载数据,并实例化。其中,train=True意为下载的是训练集;transform=trans意为拿到数据后转为Tensor;download=True意味从网上下载。
mnist_train = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=False, transform=trans, download=True)
batch_size = 256
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_worker())
test_iter = data.DataLoader(mnist_test, batch_size, num_workers=get_dataloader_worker())
# PyTorch不会自动调整输入数据的形状,因此定义一个展平层,以在线性层之前调整输入数据的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) # 长宽28像素,共784,输出10类
# 为线性层初始化参数
net.apply(init_weights)
# 在交叉熵损失函数中传递未归一化的预测,并同时计算softmax及其对数
loss = nn.CrossEntropyLoss()
# 使用学习率为0.1的小批量随机梯度下降作为优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
# 开学
X_test, y_test = next(iter(test_iter))
num_epoch = 15
net.train()
for epoch in range(num_epoch):
for X, y in train_iter:
l = loss(net(X), y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(X_test), y_test)
print(l)
注:最后一层为[latex]\textit{nn.Linear()}[/latex],按理来说最后一层应是[latex]\textit{nn.Softmax()}[/latex],然而由于 nn.CrossEntropyLoss() 已经包括[latex]\textit{nn.Softmax()}[/latex]层了,因此即使是分类任务通常下网络最后不写[latex]\textit{nn.Softmax()}[/latex]层。
但是预测时要求最后输出的是概率,需要结合 torch.max() 或 torch.argmax() 转换为类别。详见此处。
需要注意代码中关于初始化模型参数的部分。权重初始化参考此处。
QA
\(\textit{Softlabel}\)策略
在[latex]\textit{Softmax}[/latex]中,针对某个物类,[latex]y\_label[/latex]是[latex]\lbrack 0,0,1,0\rbrack[/latex],其中的[latex]1[/latex]和[latex]0[/latex]不利于计算损失中的指数运算。而[latex]\textit{Softlabel}[/latex]策略则是把原本的向量中的[latex]1[/latex]和[latex]0[/latex]分别变小和变大,如把[latex]1[/latex]变成[latex]0.9[/latex]并把[latex]0[/latex]变为[latex]0.1[/latex],即[latex]\lbrack 0.1,0.1,0.9,0.1\rbrack[/latex]。这是图片分类中的常用技巧。
net.eval()的作用
告诉模型这是在计算模型精度,就不需要计算梯度等东西了,进而提升效率。
# 感知机Perceptron
人工智能最早的模型。输出与线性回归不同,线性回归输出实数,而感知机输出离散类别;与[latex]\textit{Softmax}[/latex]不同,[latex]\textit{Softmax}[/latex]输出不同类别的概率,可用于多分类,而感知机输出两个离散类别。
感知机训练过程:
\begin{align}& \textbf{initialize}\,w=0\text{ and }b=0\\& \textbf{repeat}\\& \quad \textbf{if}\,y_i[<w,x_i>+b]\leq0\,\textbf{then}\\& \qquad w\gets w+y_1x_i\text{ and }b\gets b+y_i\\& \quad \textbf{end if}\\& \textbf{until}\text{ all classified correctly}\end{align}
与[latex]\textit{Softmax}[/latex]不同,感知机将参数 w, b 都初始化为0。
训练感知机的优化函数等价于 batchsize=1 的梯度下降,损失函数为:
$$l(y, x, w)=\text{max}(0,-y<w,x>)$$
感知机无法拟合XOR函数
感知机只能产生线性分割面,甚至无法处理XOR问题,进而导致第一次人工智能寒冬。十几年后的多层感知机终于让人看到了希望,能处理XOR问题了。
多层感知机
多层感知机初具神经网络雏形,分为三层,即输入层、隐藏层和输出层。其中隐藏层的大小(层数和每层的单元个数)为超参数。且具有非线性激活函数。最常用的激活函数为:
[latex]Sigmoid[/latex]激活函数[latex]\text{sigmoid}(x)=\frac{1}{1+\text{exp}(-x)}[/latex]将输入投影到[latex](0, 1)[/latex]
该函数可以看作一个软的[latex]\sigma(x)=\begin{cases}1,\,\text{if $x>0$}\\0,\,\text{otherwise}\end{cases}[/latex]。
[latex]Tanh[/latex]激活函数[latex]\text{tanh}(x)=\frac{1-\text{exp}(-2x)}{1+\text{exp}(-2x)}[/latex]将输入投影到[latex](-1, 1)[/latex]
和[latex]Sigmoid[/latex]和像,但是区间不同。
[latex]\text{ReLU}=\text{max}(x, 0)[/latex]ReLU激活函数:rectified linear unit
非常常用的激活函数,没什么其他想法就用它。
激活函数的作用是避免层数塌陷,输出层不需要激活函数。
多层感知机与[latex]\textit{Softmax}[/latex]的区别是多了隐藏层。它是使用隐藏层和激活函数得到的非线性模型,然后基于[latex]\textit{Softmax}[/latex]来处理多分类。
基于PyTorch实现多层感知机
def init_weights(layer):
if type(layer) == nn.Linear:
# 设定参数的标准差为0.01。在深度神经网络中有讲究
nn.init.normal_(layer.weight, std=0.01)
# 超参数定义
batch_size = 256
lr = 0.1
num_epochs = 10
## 数据获取
mnist_train = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=True, transform=transforms.ToTensor(), download=True)
mnist_test = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=False, transform=transforms.ToTensor(), download=True)
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=4)
test_iter = data.DataLoader(mnist_test, batch_size, num_workers=4)
## 多层感知机构造
# 第一层是Flatten(),将三维的数据变为二维;第二层为线性层,输入是784,输出是256。这里加入ReLU激活函数;第三层为线性层,输入为256,输出为10
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
# 初始化参数
net.apply(init_weights)
# 定义损失函数
loss = nn.CrossEntropyLoss()
# 定义优化器
trainer = torch.optim.SGD(net.parameters(), lr=lr)
# 开学
X_test, y_test = next(iter(test_iter))
num_epoch = 15
net.train()
for epoch in range(num_epoch):
for X, y in train_iter:
l = loss(net(X), y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(X_test), y_test)
print(l)
注意最后输出结果的输出层为线性层。
QA
感知机层数与大小?
复杂度相近的情况下,层数越少、单元越多的多层感知机与层数多、每层单元较少的多层感知机相比,后者比较方便训练。并称后者为深度学习;前者为浅度学习,容易过拟合。所以,模型深一点会好点。
激活函数与隐藏层数量、大小
激活函数没特殊要求都可以选[latex]ReLU[/latex],因为激活函数对结果的影响远不及隐藏层的大小、数量。
范式?
如输入为128,则第一步为不使用隐藏层,[latex]128\to2[/latex];第二步为加一层含有16个单元的隐藏层,再试32,再试64,再试128。即[latex]128\to32\to2[/latex];第三步为看上一步多少个单元合适,基于合适的单元个数,如32的效果较好,然后添加第二隐藏层,再试8,再试16。即[latex]128\to32\to16\to2[/latex]。从简单开始慢慢变复杂。写一个[latex]for loop[/latex]。
多层感知机与CNN、RNN?
理论上,添加一层隐藏层便能使得多层感知机拟合任何函数,然而这样的模型大概率训练不出来。因此提出[latex]CNN[/latex]和[latex]RNN[/latex]这样的结构来帮助实现。如[latex]CNN[/latex]告诉模型所训练的数据存在空间信息;[latex]RNN[/latex]告诉模型所训练的数据存在时序信息。
# 模型选择
在吴恩达老师的课程中,一般把全体数据按照[latex]6:2:2[/latex]分为训练集、交叉验证集以及测试集。将交叉验证集用于模型选择,评判超参数的好坏。
然而,在小样本的情况下,按照上述规则分割全体数据可能效果欠佳。因此引入 K折交叉验证 改善上述境况。即,按照[latex]7:3[/latex]分为训练数据与测试数据,并在训练数据中做K折交叉验证,这指的是把训练数据中的[latex]\frac{1}{K}[/latex]拿出来做交叉验证。
在传统机器学习中,K折交叉验证被广泛运用;然而在深度学习中使用得并不多,因为深度学习数据量一般较大。
K折交叉验证
算法流程:
\begin{align}& \centerdot \text{将训练数据分割成K块}\\& \centerdot \text{For i=1, …, K}\\& \quad \centerdot \text{使用第i块作为验证数据集,其余的作为训练数据集}\\& \centerdot \text{报告K个验证集误差的平均}\\& \textbf{常用:K=5或10}\end{align}
K折,即进行K次训练取平均验证误差,当数据量较小时可以取较大值,反之取较小值。
K折交叉验证代码实现
def get_k_fold_data(k, i, X, y): """ :param k: 指定K折 :param i: 第几折 :param X: 特征数据集 :param 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 == 1: 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], 0) y_train = torch.cat([y_train, y_part], 0) return X_train, y_train, X_valid, y_valid ## 开训,并进行K折交叉验证 data_iter = load_array((train_features, train_labels), 64) # 数据迭代器,虽然使用Adam,但该小批量还是小批量 optimizer = torch.optim.Adam(net.parameters(), lr=5, weight_decay=0.001) # Adam可以看作是平滑的SGD,对学习率不敏感,因此lr设置了5 # K(5)折交叉验证 train_l_sum, valid_l_sum = 0, 0 k = 5 num_epoch = 100 for i in range(k): data = get_k_fold_data(k, i, train_features, train_labels) train_ls, valid_ls = [], [] for epoch in range(num_epoch): for X, y in data_iter: optimizer.zero_grad() l = loss(net(X), y) l.backward() optimizer.step() train_ls.append(log_rmse(net, train_features, train_labels)) if data[3] is not None: valid_ls.append(log_rmse(net, data[2], data[3])) train_l_sum += train_ls[-1] valid_l_sum += valid_ls[-1] print(f'train loss: {train_l_sum / k}\nvalid loss: {valid_l_sum / k}')
上述代码仅进行一次五折交叉验证,如果要多次进行交叉验证,建议将交叉验证封装为方法。
过拟合、欠拟合以及模型容量
实践中可能需要让模型承受一定程度过拟合,以降低模型泛化误差。在深度学习中,首先要保证模型够大,再调参控制其容量,并得到足够低的泛化误差。
不同种类算法之间难以比较模型容量。在同种模型种类,有两个主要因素控制模型容量,即参数的个数和参数值的选择范围。
模型容量需要匹配数据复杂度,否则导致欠拟合或过拟合。
QA
SVM的缺点?神经网络的优点?
a1、不支持大数据。可能最多支持十万左右的数据量。a2、能调整的部分较少,如核的种类和宽度,但效果不大,比较平滑。b1、神经网络是语言。b2、神经网络集特征提取与分类于一体,SVM仅做分类。
二分类问题中,正负样本比例差异巨大,怎么选取交叉验证集?
如果是小样本,则保证交叉验证集的正负比例[latex]1:1[/latex]。如果是大样本,则无所谓了。
拿到偏斜数据怎么办(续上一个问题)
首先看真实世界分布是否如此,符合真实情况则尽量做好模型训练就行。如因为采样导致得到偏斜数据,且不符合真实情况,则考虑提升所需数据的权重,如复制几遍之类。
# 权重衰减
在一年前的帖子中,我跟着Andrew Ng将其称为正则化,实际上它的术语是权重衰减 Weight Decay ,是一种常见的处理过拟合的方法。
主要的控制模型容量的方法有两个,即控制模型大小(参数个数)与控制每个参数可选值的范围。如采用均方范数作为硬性限制:
$$\text{min}\; l ( \textbf{w},b ) \quad \textbf{subject to}\quad\lVert w\rVert^2\leq\theta$$
其中:通常不限制截距项;小的[latex]\theta[/latex]意味着更强的正则项。
然而实际上,通过上述方法限制参数可选值范围不方便。通常采用均方范数作为柔性限制,即对每个[latex]\theta[/latex],都可以找到[latex]\lambda[/latex]使得之前的目标函数等价于:
$$\text{min}\; l(\textbf{w},b)+\frac{\lambda}{2}\lVert w \rVert^2$$
其中:超参数[latex]\lambda[/latex]控制了正则项的重要程度,即当[latex]\lambda=0[/latex]时无作用;当[latex]\lambda\to\infty[/latex]时使得[latex]w\to0[/latex]。
带上正则项后参数更新法则
\begin{align}& \centerdot\textbf{计算梯度}\\& \qquad\frac{\partial}{\partial w}\left( l(\textbf{w},b)+\frac{\lambda}{2}\lVert\textbf{w}\rVert^2 \right)=\frac{\partial l(\textbf{w},b)}{\partial w}+\lambda w\\& \centerdot\textbf{时间t更新参数}\\& \qquad \textbf{w}_{t+1}=\left(1-\eta\lambda\right)\textbf{w}_t-\eta\frac{\partial l(\textbf{w}_t,b_t)}{\partial \textbf{w}_t}\\& \centerdot\textbf{通常}\eta\lambda<1\textbf{,且这在深度学习中被称为权重衰减}\end{align}
权重衰减通过 L2 正则项使得模型参数不会过大,从而控制模型复杂度。正则项的权重 λ 是控制模型复杂度的超参数。
基于PyTorch实现权重衰减
net = nn.Sequential(nn.Linear(input_features_num, output_features_num))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss()
num_epochs, lr = 100, .003
trainer = torch.optim.SGD(
[
{
'params':net[0].weight,
'weight_decay':0.001
},
{
'params':net[0].bias
}
],
lr=lr
)
注意:权重衰减既可以在损失函数中定义,也可以在 trainer 中定义,上述代码采用后者;截距项不需要正则化,因此定义 trainer 时使用了列表和字典分别定义参数。
QA
λ 一般取多大
一般取[latex]1e-1[/latex]、[latex]1e-2[/latex]、[latex]1e-3[/latex],多试试,一般取[latex]1e-3[/latex]。此外,权重衰减在控制模型复杂度方面效果也就那样,不要太指望。对于多层感知机来说, drop-out 可能更有用。
一种说法
模型学习了数据中的大量噪音,因此难以到达一个所谓的“最优解”,因此一个合适的正则项能把求得的解往“最优解”拉。但如果模型没有过拟合,往回拉(正则项)实际上没什么用。
# 丢弃法
丢弃法的目的是增强模型的鲁棒性,在避免过拟合方面效果优于权重衰减。该方法仅适用于全连接层。
使用有噪音的数据等价于 Tikhonov 正则,而丢弃法是在层之间加入噪音,具体而言:
\begin{align}& \large \textbf{无偏差地加入噪音}\\& \large \centerdot\text{对x加入噪音得到}x’\text{,我们希望}\\& \huge \qquad E\lbrack x’\rbrack=x\\& \large \centerdot \text{丢弃法对每个元素进行如下扰动}\\& \large \qquad x_i’=\begin{cases}0 &\text{with probability } p \\ \frac{x_i}{1-p} &\text{otherwise}\end{cases}\end{align}
其中,上述式子中的元素通常是隐藏全连接层的输出,即丢弃法通常作用在隐藏全连接层的神经元输出上:
\begin{align}\\ h&=\sigma \left(W_1x+b_1\right)\\ h’&=\text{dropout}(h)\\ o&=W_2h’+b_2\\ y&=\text{softmax}(o)\end{align}
基于PyTorch实现丢弃法
在丢弃法正则化的加持下,将模型设置复杂点,隐藏层设大点,再把丢弃率设置大一点,效果较好。
# 构造一个含有两个隐藏层的多层感知机
net = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 256), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(256, 256), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(256, 10)
)
net.apply(init_weights) # 该函数见上文
QA
总结一下
丢弃法常被作用于多层感知机隐藏层的输出上,很少用在CNN之类的模型上;丢弃的概率[latex]p,\; p\in [0,1][/latex]是控制模型复杂度的超参数,通常取[latex]0.5, 0.9, 0.1[/latex];效果比权重衰减好,但不如权重衰减普遍。
每迭代一次,丢弃法就要丢一次吗?
每一层调用前向传播时就要随机丢弃一次。
使用BN时,还有必要使用丢弃法吗?
BN作用于卷积层,丢弃法作用于全连接层,二者没有太大关系。
丢弃法与权重衰减?
尽管丢弃法比权重衰减效果更好,然而丢弃法更加适配全连接层,而权重衰减不仅适用于全连接层,还适用于卷积层、[latex]Transformer[/latex]等。因此权重衰减更常用。
丢弃法效果好体现在,更直观更方便调参。不过丢弃法会使模型收敛变慢。
# 数值稳定性、模型初始化与激活函数
当神经网络规模较大时,数值非常容易变得不稳定。具体原因如下:
\begin{align}& \centerdot \textbf{神经网络的梯度}\\& \large \quad h^t=f_t(h^{t-1})\;\text{and}\;y=l\circ f_d\circ \dots\circ f_1(x)\\& \centerdot \text{计算损失}l\text{关于参数}W_t\text{的梯度}\\& \large \quad \frac{\partial l}{\partial W^t}=\frac{\partial l}{\partial h^d} \frac{\partial h_d}{\partial h^{d-1}} \cdots \frac{\partial h_{t+1}}{\partial h^{t}} \frac{\partial h_{t}}{\partial W^{t}}\end{align}
其中,数值不稳定主要来自最后[latex]d-t[/latex]次的矩阵乘法。进一步将数值稳定性问题分为:
\begin{align}& \large \star \text{梯度爆炸}\\& \text{值超出值域(infinity)}\\& \quad \text{对于16位浮点数影响尤为严重(数值区间6e-5至6e-4)}\\& \text{对学习率敏感}\\& \quad \text{如果学习率太大 -> 大参数值 -> 更大的梯度}\\& \quad \text{如果学习率太小 -> 训练无进展}\\& \quad \text{我们可能需要在训练过程不断调整学习率}\\& \text{举例:} 1.5^{100}\approx 4\times 10^{17}\end{align}
\begin{align}& \large \star \text{梯度消失}\\& \text{梯度值变成0}\\& \quad \text{对16位浮点数影响尤为严重}\\& \text{训练没有进展}\\& \quad \text{无论如何选择学习率}\\& \text{对底部层影响尤为严重}\\& \quad \text{仅仅顶部层(后面的层)训练得较好}\\& \quad \text{无法让神经网络更深,因为底部层都是〇}\\& \text{举例:}0.8^{100}\approx 2\times 10^{-10}\end{align}
数值过大或过小都会导致数值问题,数值问题常发生在深度模型中,因为其会对[latex]n[/latex]个数累乘。其中,梯度爆炸的出现使得对学习率的准确设置成为调参过程中更加复杂的挑战;梯度消失使得反向传播失效。
让训练更稳定
因此,亟需使梯度值在合理的区间内,如[latex][1e-6,1e-3][/latex]。具体有以下方法:
\begin{align}& \large \centerdot \text{将乘法变加法}\\& \quad \text{如ResNet, LSTM采用这种方法}\\& \large \centerdot \text{归一化}\\& \quad \text{梯度归一化,梯度裁剪}\\& \large \centerdot \text{合理的权重初始化和激活函数}\end{align}
合理的权重初始化
让每一层的前向输出和梯度的方差和均值都保持一致。即:
\begin{align}& \large \textbf{让每层的方差是一个常数}\\& \centerdot \text{将每层的前向输出和梯度都看作随机变量}\\& \centerdot \text{让它们的均值和方差都保持一致}\\& \quad \text{正向}\qquad\qquad\qquad\text{反向}\\& \begin{matrix}\mathbb{E}[h_i^t]=0\\\text{Var}[h_i^t]=a\end{matrix}\qquad\mathbb{E}\left\lbrack\frac{\partial l}{\partial h_i^t}\right\rbrack=0\quad\text{Var}\left\lbrack\frac{\partial l}{\partial h_i^t}\right\rbrack=b\;\;\forall i,t\end{align}
其中[latex]a[/latex]和[latex]b[/latex]都是常数。
在训练刚开始时更容易发生数值不稳定,若离最优解太远损失函数表面可能很复杂,若离最优解太近则表面很平滑。因此需要在合适的区间内随机初始参数。
Xavier初始
除非输入维度刚好等于输出,否则上述两个条件很难同时满足,因此需要进行权衡 trade-off 。通常使用 Xavier 初始化进行上述权衡,具体如下:
\begin{align}& \large \centerdot \text{由于难以满足}n_{t-1}\gamma_t=1\text{和}n_t\gamma_t=1\\& \large \centerdot \text{Xavier使得}\gamma_t(n_{t-1}\gamma_t)/2=1\;\;\to\;\;\gamma_t=2/(n_{t-1}+n_t)\\& \large \quad\text{有两种初始化方式:}\\& \quad \text{正态分布}N\left(0,\sqrt(2/(n_{t-1}n_t))\right)\\& \quad \text{均匀分布}\mu\left(-\sqrt{6/(n_{t-1}+n_t)},\sqrt{6/(n_{t-1}+n_t)}\right)\\& \large \centerdot \text{该方法使得权重初始化时方差根据输入和输出维度变换}\end{align}
当网络变化较大时,根据输入和输出维度,将输出和梯度的方差限定在一个范围里。
激活函数
如果希望梯度的期望为〇,方差为固定常数。那么激活函数必须为[latex]f(x)=x[/latex]。需要选取合适的激活函数使得数值稳定。
检查本帖之前提到的激活函数发现:
$$\large \text{使用泰勒展开}$$
\begin{align}\text{sigmoid}(x)&=\frac{1}{2}+\frac{x}{4}-\frac{x^3}{48}+O(x^5)\\\text{tanh}(x)&=0+x-\frac{x^3}{3}+O(x^5)\\\text{relu}(x)&=0+x \;\;\text{for}\;\;x\geq 0\end{align}
可见,除 sigmoid 外。其余两个函数在0点附近可近似为[latex]f(x)=x[/latex] 恒等函数 ,而 sigmoid 不过原点,不满足该条件,因此需要调整为[latex]\text{sigmoid}(x)=4\times \text{sigmoid}(x)-2[/latex]。
tanh 和 ReLU 作为激活函数表现良好的原因可以从数值稳定性这一点来解释,且调整后的 sigmoid 函数基本上能解决之前遇到的问题。QA
总结一下
需要进行合适的权重初始化,并选择合适的激活函数,来使得数值稳定。具体而言,使每一层的前向输出和梯度成为均值为〇且方差为固定常数的随机变量。权重初始化建议选 Xavier ,激活函数建议选 ReLU 或 tanh 。若选 sigmoid ,则需要进行上述变化。
在上述实例中使用[latex]N(0,0.01)[/latex]初始化权重?
这样初始化仅适用于小网络,不能保证在深度神经网络上有效。
nan 可能是除以0导致的。 inf 因为数值太大而产生,可能学习率太大,可能没合理初始化权重。解决方法为合理初始化权重(方差调小),选择正确的激活函数,调小学习率。nan和inf是怎么产生的,怎么解决?
训练两个epochs后,验证集准确率突然变为50%左右,之后稳定在50%左右,为什么?
数值稳定性出了问题。可以尝试调小学习率,若不能解决问题,那么是模型稳定性较差,权重跑歪了。之后的ResNet和LSTM被提出来也是为了避免这些情况。
训练中,隐藏层中间的输出突然变为nan,是发生梯度爆炸了吗?
是的。
fp16与fp32?
fp16(半精度浮点数)是十六位浮点数,具体指计算到小数点后十六位。使用该数据结构承载数据,在计算中比fp32快一倍,比fp64快三倍。目前而言,越来越多的程序考虑使用bfloat16或其它的数据结构,避免梯度爆炸等问题。
为什么正态分布有利于学习?
实际上并不是正态分布利于训练,而是希望输出值在合理的区间内。业内使用正态分布和均匀分布是因为这二者计算方便。除此二者外,只要能保证均值为〇方差为固定值,任意分布均可。
使输出和梯度的期望为〇方差为固定常数,是否会降低模型准确率?
没有因果关系,本章的内容旨在满足硬件处理数据的需求,使得硬件处理数据不容易发生梯度爆炸和梯度消失。
# Kaggle:预测房价
点击此处查看主体代码。
提交结果
# drop Id列之前获取Id的信息
test_Id = test_data['Id'].values.flatten()
## 预测并输出结果
preds = net(test_features).detach().numpy()
submission = pandas.DataFrame({
'Id': test_Id,
'SalePrice': preds.flatten()
})
submission.to_csv('./house_price/submission.csv', index=False)
QA
独热编码时OOM?
0太多,考虑使用稀疏矩阵。但实际上,当独热编码生成太多太多变量时,应考虑任务类型采用其他方法,如在NLP方面考虑切词方法等等。
训练时抖动很厉害,为什么?
可以抖动,但整体而言一定要下降;光抖不下降就不行了。可能的原因:1.学习率太大;2.数据多样性较大,随机采样看到很不一样的样本。建议把曲线做平滑;3. batch_size 太小,扩大它。
# 神经网络基础
nn.Sequential 定义了一种特殊的[latex]\text{Module}[/latex]。任何层、神经网络都是[latex]Module[/latex]的子类。模型构造
自定义块
这里自定义了一个MLP
class MLP(nn.Module):
def __init__(self):
super().__init__() # 获取父类的内容,通常在子类的构造方法中使用,以便在添加子类特有的功能之前,初始化父类中定义的属性和执行其他必要的设置。比如之后初始化权重等。
self.hidden = nn.Linear(20, 256) # 定义一个全连接层
self.out = nn.Linear(256, 10) # 定义一个输出层
# 定义前向传播函数
def forward(self, X):
X = self.hidden(X)
X = functional.relu(X)
X = self.out(X)
return X
# 等同于 return self.out(functional.relu(self.hidden(X))) # 返回的是,先经过第一个隐藏层,再经过输出层的值
net = MLP() # 实例化
# 上述代码等价于
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
自定义顺序块
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for block in args:
self._modules[block] = block
def forward(self, X):
for block in self._modules.values():
X = block(X)
return X
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
# 上述代码等价于
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
在初始化和正向传播函数中执行其它计算
class modifiedMLP(nn.Module):
def __init__(self):
super().__init__()
self.rand_weight = torch.rand((20, 20), requires_grad=False) # 生成一个随机权重,这一步不参与梯度计算
self.hidden = nn.Linear(20, 20)
def forward(self, X):
X = self.hidden(X) # 先调用一个线性类
X = functional.relu(torch.mm(X, self.rand_weight) + 1) # 先把X乘一个初始化中生成的随机权重,再加个偏移,然后丢进relu函数
X = self.hidden(X) # 再调用一个线性类
# 再写个循环
while X.abs().sum() > 1:
X /= 2
return X.sum()
net = modifiedMLP()
嵌套使用,混合搭配各种组合块的方法
class nestedMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU())
self.linear = nn.Linear(32, 16)
def forward(self, X):
X = self.net(X)
X = self.linear(X)
return X
net = nestedMLP()
# 超级加倍
nestednestedMLP = nn.Sequential(nestedMLP(), nn.Linear(16, 20), modifiedMLP())
参数管理
一个多层感知机内设有控制不同内容的参数,如权重、偏差等。
参数访问
# 定义一个多层感知机
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
## 参数访问
# 访问nn.Linear(8, 1)的参数,包括8个权重和1个偏移
net[2].state_dict()
# 也可以直接访问某一个具体参数
net[2].bias.data # 访问bias的值
net[2].weight.data # 访问第三块的权重
net[2].weight.grad # 访问第三块的梯度
# 访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
# 通过名字获取参数
net.state_dict()['2.bias'].data
初始化权重
def init_normal(m):
# 如果是线性类,全连接层,才进行如下参数初始化
if type(m) == nn.Linear:
# normal_()指直接替换,相当于inplace=True
nn.init.normal_(m.weight, mean=0, std=0.01)
# 将偏差设为0
nn.init.zeros_(m.bias)
def xavier_uni(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def xavier_normal(m):
if type(m) == nn.Linear:
nn.init.xavier_normal_(m.weight)
# 初始化为常数,但从算法角度来讲没人会这么做
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net.apply(init_normal)
参数绑定
如果希望在不同层之间共享参数(权重等),可以参考参数绑定:
# 首先构造共享层。构造时会申请这些想要贡献的参数。
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1))
此时,更改多层感知机第三层的参数,第五层的参数将随之变化。
这也是在不同网络之间共享权重的方法。
自定义层
class customLayer(nn.Module):
def __init__(self):
super().__init__()
def forward(self, X):
X = X - X.mean()
return X
myLayer = customLayer()
net = nn.Sequential(nn.Linear(8, 128), customLayer())
net = nn.Sequential(nn.Linear(8, 128), myLayer)
还可以设定权重偏差等:
class customLayer(nn.Module):
def __init__(self, in_units, out_units):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, out_units))
self.bias = nn.Parameter(torch.randn(out_units,))
def forward(self, X):
X = torch.matmul(X, self.weight.data) + self.bias
X = functional.relu(X)
return X
myLayer = customLayer(5, 3)
net = nn.Sequential(nn.Linear(8, 128), customLayer(128, 3))
读写文件
存储张量
x = torch.arange(42)
torch.save(x, './x-file')
x2 = torch.load('./x-file')
# 作为数组来存
torch.save([x, y], 'x-files')
x, y = torch.load('x-files')
# 作为字典来存
myDict = {
'x': x,
'y': y
}
torch.save(myDict, './myDict-file')
myDict2 = torch.load('./myDict-file')
PyTorch 本身无法存储模型的定义,只能存模型的权重。因此,将存储的参数复制到新模型之前,需要先克隆模型。加载和保存模型参数
# 将模型参数存储为mlp.params
net = nn.Sequential(nn.Linear(8, 64), nn.ReLU(), nn.Linear(64,2))
torch.save(net.state_dict(), './mlp.params')
clone_net = nn.Sequential(nn.Linear(8, 64), nn.ReLU(), nn.Linear(64,2))
clone_net.load_state_dict('./mlp.params')
QA
自定义的激活函数如果不可导,auto-grad是否可以求出导数?
很多函数只是不能处处可导,但在这里的数值运算中,刚好遇到断点的概率特别低,遇到了就随便给个0。几乎没有不能导的函数,只是有些点不好导。
调参得到的结果,已知在验证集上表现不错,怎么推测在测试集上的结果?
恰好得到较好的超参数后,向上或向下再调一调参数,看结果是否发生巨大变化,是否敏感。如果发生巨大变化,则说明这个点周围并不平滑,泛化性可能不好。
# Graphics Processing Unit
没有N卡的鼠鼠开始难过了。
查看GPU信息
!nvidia-smi
由于深度学习框架默认基于CPU进行运算,因此有必要在 PyTorch 中指定使用GPU计算:
# 采用CPU
torch.device('cpu')
# 采用第一块GPU
torch.device('cuda')
# 采用第二块GPU
torch.device('cuda:1')
# 查看可用GPU的数量
torch.cuda.device_count()
这里采用字符串 cuda 表示GPU,是出于历史原因将错就错。
在GPU上存储张量
x = torch.tensor([1, 2, 3])
# 查看张量存在哪里
x.device
# 在第一块GPU上存张量
x = torch.ones(2, 3, device=torch.device('cuda'))
# 在第二块GPU上存张量
y = torch.ones(2, 3, device=torch.device('cuda:1'))
# 将存放在第1个GPU上的张量移动到第二个GPU上
x = x.cuda(1)
x = x.to(device='cuda:1')
为保证张量计算发生在GPU上,应使参与运算的张量均在同一块GPU上。即,张量在同一块GPU上时,相关运算就会被GPU加速。不在同一块GPU上运算时 PyTorch 会报错。然而实际上在不同设备上的张量可以进行运算,只是性能会大大降低,且不方便排错。
在GPU上存储模型
# 先定义模型
net = nn.Sequential(nn.Linear(3, 1))
# 再将模型移动到第一块GPU上
net = net.to(device=torch.device('cuda'))
# 确保数据和模型都在同一块GPU上
net(X)
# 确认模型参数存储在同一块GPU上
net[0].weight.data.device
查看GPU信息
# 查看GPU是否可用 torch.cuda.is_available() # 查看CUDA版本 torch.version.cuda # 查看GPU性能的简要信息 torch.cuda.get_device_capability() # 查看GPU性能详细信息 device = torch.device('cuda') torch.cuda.get_device_properties(device) # 查看Torch版本 torch.__version__ # 查看cuDNN版本 torch.backends.cudnn.version()
QA
GPU购买建议?
显存越大越好,CUDA越多越好。
显存不够时,如果把 batch_size 调小,满足显存限制后但 CUDA 占用很低,怎么办?
把模型变小。
使用GPU训练时,数据最好在哪一步挪到GPU?
一般而言在最后,训练网络之前。因为GPU不一定很好支持数据预处理。但对于GPU能很好支持的数据预处理任务,如图像处理,那么可以尽早将数据挪到GPU做预处理。
# 卷积神经网络
介绍卷积层及其填充和步幅、多输入多输出通道、池化以及经典和深度卷积神经网络。
卷积层
采用全连接层执行图像处理任务时,大量待处理像素要求巨大的输入层,经过神经元处理进一步使参数膨胀。这使梯度爆炸的概率显著提升。因此提出卷积层规避此类问题。
图片识别的两个基本原则
\(\textbf{平移不变性:无论对象位于何处,都会输出类似结果。}\)
\(\textbf{局部性:像素与近邻更相关。}\)
二维交叉相关
$$\require{color} \huge \overset{\textbf{Input}}{\begin{array}{|c|c|c|}\hline \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{1} & 2 \\ \hline \colorbox{#87ceeb}{3} & \colorbox{#87ceeb}{4} & 5 \\ \hline 6 & 7 & 8 \\ \hline \end{array}} \times \overset{\textbf{Kernel}}{\colorbox{#87ceeb}{$\begin{array}{|c|c|}\hline 0 & 1 \\ \hline 2 & 3 \\ \hline \end{array}$}}=\overset{\textbf{Output}}{\begin{array}{|c|c|}\hline \colorbox{#87ceeb}{19} & 25 \\ \hline 37 & 43 \\ \hline \end{array}}$$ $$\begin{align}0\times 0+1\times 1+3\times 2+4\times 3=19\\1\times 0+2\times 1+4\times 2+5\times 3=25\\3\times 0+4\times 1+6\times 2+7\times 3=37\\4\times 0+5\times 1+7\times 2+8\times 3=43\end{align}$$
$$\begin{align}& \hline \large \centerdot \text{输入}X:n_h\times n_w\\& \large \centerdot \text{核}W:k_h\times k_w\\& \large \centerdot \text{偏差}b\in \mathbb{R}\\& \large \centerdot \text{输出}Y:(n_h-k_h+1)\times (n_w-k_w+1)\\& \large \quad Y=X\star W+b\\& \centerdot \large \text{W和b是可学习的参数}\\ \hline\end{align}$$
交叉相关[latex]vs[/latex]卷积
$$\begin{align}& \large \text{二维交叉相关}\\& \large \quad y_{i,j}=\displaystyle\sum_{a=1}^{h}\sum_{b=1}^{w}w_{a,b}x_{i+a,j+b}\\& \large \text{二维卷积}\\& \large y_{i,j}=\displaystyle\sum_{a=1}^{h}\sum_{b=1}^{w}w_{-a,-b}x_{i+a,j+b}\\& \large \text{由于对称性,在实际使用中没有区别}\end{align}$$
一维交叉相关
$$\begin{align}& \large \text{一维}\\& \large \quad y_i=\displaystyle\sum_{a=1}^{h}w_ax_{i+a}\\& \large \text{文本}\\& \large \text{语言}\\& \large \text{时序序列}\end{align}$$
三维交叉相关
$$\begin{align}& \large \text{三维}\\& \large y_{i,j,k}=\displaystyle\sum_{a=1}^{h}\sum_{b=1}^{w}\sum_{c=1}^{d}w_{a,b,c}x_{i+a,j+b,k+c}\\& \large \text{视频}\\& \large \text{医学图像}\\& \large \text{气象地图}\end{align}$$
阶段总结与代码
卷积层将输入和核矩阵进行交叉相关,再加上偏移得到输出;核矩阵和偏移是学习得到的参数;核矩阵的大小是超参数。
import torch
from torch import nn
# 定义数据
X = torch.ones((6, 8))
X[:, 2:6] = 0
X = X.reshape((1, 1, 6, 8))
Y = torch.zeros((6, 7))
Y[:, 1] = 1
Y[:, -2] = -1
Y = Y.reshape((1, 1, 6, 7))
# 实例化一个二维卷积层
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
epoch_num = 20
for epoch in range(epoch_num):
l = (conv2d(X) - Y)**2
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
if (epoch + 1) % 2 == 0:
print(f'batch {epoch + 1}, loss {l}')
print(conv2d.weight.data)
卷积层的填充 [latex]\textbf{\(\textit{Padding}\)}[/latex]
如在[latex](32\times 32)[/latex]图像上应用[latex](5\times 5)[/latex]的卷积核,第一层得到大小为[latex](28\times 28)[/latex]的输出,第七层得到大小为[latex](4\times 4)[/latex]的输出。且更大的卷积核会更快地减小输出大小。即,在没有填充且步幅默认为[latex]1[/latex]的情况下,大小为[latex](k_h\times k_w)[/latex]的卷积核作用在大小为[latex](n_h\times n_w)[/latex]的输入,每卷积一次,输出的大小减少到[latex](n_h-k_h+1,n_w-k_w+1)[/latex]。
鉴此,较小的输入与较大的卷积核使输出大小骤减,卷积核无法作用于多次卷积后得到的输入,进而使得模型深度受限。因此,有必要采用填充 padding 避免此类情况。
在输入[latex]\text{Input}[/latex]周围添加额外的行/列:
$$\require{color}\large \overset{\textbf{Input}}{\begin{array}{|c|c|c|c|c|}\hline \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{0} & 0 & 0 & 0 \\ \hline \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{0} & 1 & 2 & 0 \\ \hline 0 & 3 & 4 & 5 & 0 \\ \hline 0 & 6 & 7 & 8 & 0\\ \hline 0 & 0 & 0 & 0 & 0 \\ \hline \end{array}} \times \overset{\textbf{Kernel}}{\colorbox{#87ceeb}{$\begin{array}{|c|c|}\hline 0 & 1 \\ \hline 2 & 3 \\ \hline \end{array}$}}=\overset{\textbf{Output}}{\begin{array}{|c|c|c|c|}\hline \colorbox{#87ceeb}{0} & 3 & 8 & 4 \\ \hline 9 & 19 & 25 & 10 \\ \hline 21 & 37 & 43& 16 \\ \hline 6 & 7 & 8 & 0 \\ \hline \end{array}}$$ $$\large 0\times 0+0\times 1+0\times 2+0\times 3=0$$
输出大小与填充的关系
$$\large \begin{align}& \text{填充}p_h\text{行和}p_w\text{列,输出形状为}\\ & (n_h-k_h+p_h+1)\times (n_w-k_w+p_w+1)\\ & \text{通常取}p_h=k_h-1,\;\;p_w=k_w-1\\& \centerdot \text{当}k_h\text{为奇数:在上下两侧填充}p_h/2\\& \text{当}k_h\text{为偶数:在上侧填充}\lceil p_h/2\rceil \text{,在下侧填充}\lceil p_h/2\rceil\end{align}$$
卷积层的步幅 [latex]\textbf{\(\textit{Stride}\)}[/latex]
将一个常见的[latex](3\times 3)[/latex]或[latex](5\times 5)[/latex]的卷积核作用在较大的图片上时,需要非常多卷积层才能得到较小输出,大量卷积层使计算变得巨大且复杂。因此,有必要采用步幅 stride 避免此类情况。
步幅[latex]\text{Stride}[/latex]指行/列的滑动步长,如高度[latex]3[/latex]宽度[latex]2[/latex]的步幅:
$$\require{color}\large \overset{\textbf{Input}}{\begin{array}{|c|c|c|c|c|}\hline 0 & 0 & \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{0} & 0 \\ \hline 0 & 0 & \colorbox{#87ceeb}{1} & \colorbox{#87ceeb}{2} & 0 \\ \hline 0 & 3 & 4 & 5 & 0 \\ \hline \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{6} & 7 & 8 & 0\\ \hline \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{0} & 0 & 0 & 0 \\ \hline \end{array}} \times \overset{\textbf{Kernel}}{\colorbox{#87ceeb}{$\begin{array}{|c|c|}\hline 0 & 1 \\ \hline 2 & 3 \\ \hline \end{array}$}}=\overset{\textbf{Output}}{\begin{array}{|c|c|}\hline 0 & \colorbox{#87ceeb}{8} \\ \hline \colorbox{#87ceeb}{6} & 8 \\ \hline \end{array}}$$ $$\begin{align}\large 0\times 0+0\times 1+1\times 2+2\times 3=8\\ \large 0\times 0+6\times 1+0\times 2+0\times 3=6\end{align}$$
输出大小与步幅的关系
$$\large \begin{align}& \text{给定高度}s_h\text{和高度}s_w\text{的步幅,输出形状是}\\& \qquad \lfloor (n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor (n_w-k_w+p_w+s_w)/s_w\rfloor\\& \text{如果}p_h=k_h-1,\;\;p_w=k_w-1\text{,输出形状则为}\\& \qquad \lfloor (n_h+s_h-1)/s_h\rfloor \times \lfloor (n_w+s_w-1)/s_w\rfloor\\& \text{如果输入高度和宽度可以被步幅整除,那么输出形状为}\\& \qquad (n_h/s_h)\times (n_w/s_w) \end{align}$$
其中,[latex]p_h=k_h-1,\;\;p_w=k_w-1[/latex]是通常选择。
填充与步幅代码
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
阶段总结
填充和步幅都是卷积层的超参数;填充在输入周围添加额外的行/列,以控制输出形状的减少量;步幅是每次滑动核窗口的行/列的步长,可以成倍的减少输出形状。
[latex]p_h=k_h-1,\;\;p_w=k_w-1[/latex]是通常选择。步幅选[latex]1[/latex]最好,但如果计算量太大,则扩大步幅。QA
为什么核矩阵不是越大越好?
就像,一个很浅很宽的全连接层没有较深较窄的全连接层效果好那般,较少卷积层较大核不如较多卷积层较小核。
超参数的重要性排序?
核的大小最重要,填充一般取[latex]p_h=k_h-1,\;\;p_w=k_w-1[/latex],步幅取决于想要的模型复杂度。
为什么卷积核的边长一般选奇数?
卷积核一般选[latex]3\times 3[/latex],主要是计算得快,且结合填充后对称一点。
手动设计卷积核的情况多吗?还是直接套用经典网络结构?
一般套用[latex]\text{ResNet}[/latex],或者在经典架构上稍微调整。自己设计的神经网络架构往往效果不如经典款好。然而神经网络架构并没有那么重要,数据预处理和训练的细节通常会产生很大影响。
为什么核通常取[latex]3\times 3[/latex],这样感受野不会很小吗
当神经网络很深的时候,感受野就大起来了。
通过多层卷积最后输出和输入形状相同,特征会丢失吗?
机器学习是压缩信息的一系列算法,过程中会丢失大量信息。把原始的计算机能理解的像素和字符串文字信息,压缩到人能理解的语义空间里。
多层卷积是否可以理解为理解图像的多种不同纹理特征?
一个特定卷积层可以匹配特定纹理,可以这样理解。
多输入通道
通道数是卷积神经网络中非常重要的超参数。在图像识别中通常使用彩色图片,含有[latex]\text{RGB}[/latex]三个通道。帖中之前用到的 Fashion-MNIST 是灰度图片,仅含一个通道。转灰度丢失信息,如果图片内容很简单,那么学习效果可能还不错。[latex]\text{每个输入通道都有一个卷积核,结果是所有通道卷积结果的和:}[/latex]
$$(1\times 1+2\times 2+4\times 3+5\times 4)+(0\times 0+1\times 1+3\times 2+4\times 3)=56$$
可见,输入的通道数为[latex]2[/latex]且每个通道内维度为[latex](3,3)[/latex],仅有[latex]1[/latex]个输入通道数为[latex]2[/latex]且每个通道内维度为[latex](2,2)[/latex]的卷积核。因为只有[latex]1[/latex]个卷积核,所以输出通道数为[latex]1[/latex]。
多输入通道×单输出通道
$$\large \begin{align}& \text{输入}X:c_i\times n_h\times n_w\\& \text{核}W:c_i\times k_h\times k_w\\& \text{输出}Y:m_h\times m_w\\& \qquad Y=\displaystyle\sum_{i=1}^{c_i}X_{i,:,:}\star w_{i,:,:}\end{align}$$
然而单输出通道不足以高效处理图像任务,因此提出多输出通道。
多输出通道
$$\large \begin{align}& \text{无论有多少输入通道,截至上一步只有单输出通道}\\& \text{可以有多个三维卷积核,每个核生成一个输出通道}\\& \quad \text{输入}X:c_i\times n_h\times n_w\\& \quad \text{核}W:c_o\times c_i\times k_h\times k_w\\& \quad \text{输出}Y:c_o\times m_h\times m_w\\& \qquad Y_{i,:,:}=X\star W_{i,:,:,:} \qquad \text{for \(i=1,\dots ,c_o\)}\end{align}$$
其中[latex]c_o[/latex]指不同[latex]\text{channel}[/latex]的[latex]\text{output}[/latex]。
可以认为每个输出通道识别一个特定的\(\text{Pattern}\),如绿色通道某个点、红色通道某个点、边[latex]Edge[/latex]以及特定角度的边缘纹理等。每个通道识别[latex]1[/latex]个模式,识别[latex]6[/latex]个模式,那么输出通道的个数则也为[latex]6[/latex]。多输入通道×多输出通道
多输入通道得到前一层多输出通道输出的[latex]\textit{Pattern}[/latex],再加权得到组合的模式识别。
对于一个深度神经网络,下面的层识别局部的、底层的各种角度边边角角纹理,越往上层将局部纹理组合起来,倒数几层各输出通道可能分别识别猫眼、猫耳,最后一层组合成猫。
[latex]1\times 1[/latex]卷积层
[latex]k_h=k_w=1[/latex]是一个受欢迎的卷积核,它不识别空间模式,只融合通道信息。即只抽取通道信息,不抽取空间信息。输入形状为[latex]n_hn_w\times c_i[/latex],权重为[latex]c_o\times c_i[/latex]的全连接层。如上图所示,将三个通道融合为两个通道输出。等价于把输入 .flatten() 成向量,然后做一个 nn.Linear(3, 2) 。
注:这里输入通道数为[latex]3[/latex]且每个通道内维度为[latex](3,3)[/latex],有[latex]2[/latex]个输入通道数为[latex]3[/latex]且每个通道维度为[latex](1,1)[/latex]的卷积核。因为有[latex]2[/latex]个卷积核,所以输出通道数为[latex]2[/latex]。[latex]\gets[/latex]这里的[latex]2[/latex]个输出通道决定了下一个卷积层的输入通道个数为[latex]2[/latex]。二维卷积层
$$\large \begin{align}& \text{输入}X:c_i\times n_h\times n_w\\& \text{核}W:c_o\times c_i\times k_h\times k_w\\& \text{偏差}B:c_o\times c_i\\& \text{输出}Y:c_o\times m_h\times m_w\\& \qquad Y=X\star W+B\\& \normalsize \text{计算复杂度(浮点计算数FLOP)}O(c_ic_ok_hk_wm_hm_w)\\& \normalsize \quad c_i=c_0=100\\ & \normalsize \quad k_h=k_w=5\qquad \to \qquad \text{1GFLOP}\\& \normalsize \quad m_h=m_w=64\\& \normalsize \text{10层,1M样本,10PFlops}\\& \normalsize \text{(CPU: 0.15TF=18h, GPU: 12TF=15min)}\end{align}$$
基于PyTorch实现多通道输入输出
# in_channels和out_channels分别控制输入输出通道
conv2d = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=(1, 2), bias=False)
阶段总结
输出通道是当前卷积层的超参数;每个输入通道有独立的二维卷积核,所有通道结果相加得到一个输出通道结果;每个输出通道有独立的三维卷积核,所以卷积核被当作四维的张量。
QA
网络越深,填充[latex]0[/latex]越多,是否影响性能?
会影响一点计算性能,不怎么影响模型性能。
每个通道的卷积核一样吗?
每个通道不同卷积层的卷积核不一样,不同通道之间对应某卷积层的卷积核大小是一样的。
计算卷积时,Bias的存在影响大吗?
Bias有一些用,但作用会变得越来越地。如之后的 Batch normalization 就会使它作用变低。之后会做大量归一化操作,所以几乎不会对结果产生什么影响。默认加上偏移就好。
卷积能获取位置信息吗?
能。之后的池化会使位置不敏感。
池化层 [latex]\textbf{\(\textit{Pooling Layer}\)}[/latex]
卷积层对空间信息过于敏感,为缓解其负面影响,池化层得以提出。
池化层有多种类型,其中最常见的是[latex]\text{二维最大池化}[/latex]和[latex]\text{平均池化}[/latex]:前者输出最强的信号;后者输出平均信号,即把信号磨平。
二维最大池化
$$\require{color} \huge \overset{\textbf{Input}}{\begin{array}{|c|c|c|}\hline \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{1} & 2 \\ \hline \colorbox{#87ceeb}{3} & \colorbox{#87ceeb}{4} & 5 \\ \hline 6 & 7 & 8 \\ \hline \end{array}} \qquad \large \boxed{2\times 2 \;\; \text{Max Pooling}}\qquad \huge \overset{\textbf{Output}}{\begin{array}{|c|c|}\hline \colorbox{#87ceeb}{4} & 5 \\ \hline 7 & 8 \\ \hline \end{array}}$$ $$\large \text{max}(0,1,3,4)=4$$
$$\large \overset{\textbf{垂直边缘检测}}{\begin{array}{ccccc} [[1. & 1. & 0. & 0. & 0.\\ \;[1. & 1. & 0. & 0. & 0.\\ [1. & 1. & 0. & 0. & 0.\\ [1. & 1. & 0. & 0. & 0.\\ \end{array}} \qquad \overset{\textbf{卷积输出}}{\begin{array}{cccc} [[0. & 1. & 0. & 0.\\ \;[0. & 1. & 0. & 0.\\ \;[0. & 1. & 0. & 0.\\ \;[0. & 1. & 0. & 0.\\ \end{array}} \qquad \overset{\textbf{2\(\times\)2最大池化(无填充)}}{\begin{array}{ccc} [[1. & 1. & 0.\\ \;[1. & 1. & 0.\\ \;[1. & 1. & 0.\\ \;[1. & 1. & 0.\\\end{array}}$$
平均池化
$$\require{color} \huge \overset{\textbf{Input}}{\begin{array}{|c|c|c|}\hline \colorbox{#87ceeb}{0} & \colorbox{#87ceeb}{1} & 2 \\ \hline \colorbox{#87ceeb}{3} & \colorbox{#87ceeb}{4} & 5 \\ \hline 6 & 7 & 8 \\ \hline \end{array}} \qquad \large \boxed{2\times 2 \;\; \text{Average Pooling}}\qquad \huge \overset{\textbf{Output}}{\begin{array}{|c|c|}\hline \colorbox{#87ceeb}{2} & 3 \\ \hline 5 & 6 \\ \hline \end{array}}$$ $$\large \text{mean}(0,1,3,4)=2$$
$$\large \overset{\textbf{垂直边缘检测}}{\begin{array}{ccccc} [[1. & 1. & 0. & 0. & 0.\\ \;[1. & 1. & 0. & 0. & 0.\\ [1. & 1. & 0. & 0. & 0.\\ [1. & 1. & 0. & 0. & 0.\\ \end{array}} \qquad \overset{\textbf{卷积输出}}{\begin{array}{cccc} [[0. & 1. & 0. & 0.\\ \;[0. & 1. & 0. & 0.\\ \;[0. & 1. & 0. & 0.\\ \;[0. & 1. & 0. & 0.\\ \end{array}} \qquad \overset{\textbf{2\(\times\)2平均池化(无填充)}}{\begin{array}{ccc} [[0.5 & 0.5 & 0.\\ \;[0.5. & 0.5 & 0.\\ \;[0.5 & 0.5 & 0.\\ \;[0.5 & 0.5 & 0.\\\end{array}}$$
基于PyTorch实现池化层
pool2d = nn.AvgPool2d((3, 3), padding=1, stride=2)
pool2d = nn.AvgPool2d((2, 3), padding=(1, 1), stride=(2, 3))
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
print(pool2d(X))
# 窗口大小为2,stride=2
nn.MaxPool2d(2)
QA
池化层放在卷积层后面吗?
为使卷积层对输出不敏感,通常放在卷积层后。
池化时窗口有重叠与没重叠有什么不同?
没有太大不同。但不知道未来会不会不同,因为现在池化用得越来越少。
池化后计算量是否会变小?
取决于核的大小、填充和步幅。
为什么池化层用得越来越少了?
池化有两个作用,具体是:降低卷积层对位置的敏感程度;设置合适的窗口大小、填充和步幅可以减少之后的计算量。然而,数据增强中,通常会使用旋转、缩放等操作改变原始图像,这样会降低卷积层过拟合的可能性。这降低了池化层的第一点作用;卷积层的步幅也可以减少之后的计算量。
但一般在最后会用一个池化层。
# LeNet
卷积神经网络中最有名的网络。[latex]\textbf{\(\textit{楽Net}\)}[/latex]于上个世纪八十年代提出,随之一并问世的还有同样著名的[latex]\textbf{MNIST}[/latex]数字图像数据集。
注:特征图[latex]\textit{feature map}[/latex]是一个很 fancy 的叫法,实际上就是卷积层的输出。最后一个全连接层实际上是[latex]\textit{Gauss Layer}[/latex],可以看作[latex]\textit{Softmax}[/latex],现在几乎不用高斯层了。
[latex]\text{Lenet}[/latex]是早期成功的神经网络,先使用卷积层学习图片空间信息,再使用池化层降低其对空间信息的敏感度,最后使用全连接层转换为每一目标类别对应的概率。基于PyTorch实现\(\text{LeNet}\)
除了按照上图的架构构造 LeNet 之外,还需构造参数初始化函数、数据迭代器等。
\(\textit{xavier}\)初始化参数
def xavier_uni(m):
# 如果是线性类,全连接层,才进行如下参数初始化
if type(m) is nn.Linear or type(m) is nn.Conv2d:
# uniform_()指直接替换,相当于inplace=True
nn.init.xavier_uniform_(m.weight)
# 将偏差设为0
nn.init.zeros_(m.bias)
\(\text{Reshape}\)函数
目的是将输入数据的批量改为自适应,视频中构造了该函数,然而正式出版的书籍中没有该函数。可能新版[latex]\text{PyTorch}[/latex]作出了修正。
class Reshape(nn.Module):
def forward(self, x):
# fashion-mnist为单通道,因此第二个参数为1,每张图片为28*28。第一个参数控制批量大小
return x.reshape(-1, 1, 28, 28)
设定超参数与构造数据迭代器
batch_size = 256
epoch_num = 10
lr = 0.9
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=False, transform=trans, download=True)
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True)
test_iter = data.DataLoader(mnist_test, batch_size, shuffle=True)
构造\(\text{LeNet}\),存储在显卡上,并初始化相应层的参数
net = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10)
)
net.to(device='cuda:0')
net.apply(xavier_uni)
其中[latex]\text{Sigmoid}[/latex]可以替换为[latex]\text{ReLU}[/latex]或其它。卷积层转全连接层之前,需要调用 .flatten() 拉直。
\(\textit{(Optional)}\)查看每一层的形状,用于确定正确的通道数
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32) for lyr in net: X = lyr(X) print(lyr.__class__.__name__, 'output shape: \t', X.shape)
选取合适的优化器与损失函数,然后开训。训练时获取训练准确率以及测试准确率
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
for epoch in range(epoch_num):
# 转变为训练形态!
net.train()
for X, y in train_iter: # 来笔数据做iteration!
X, y = X.to(device='cuda:0'), y.to(device='cuda:0') # 把这笔数据丢显卡上
optimizer.zero_grad() # 清空梯度
l = loss(net(X), y) # 计算损失
l.backward() # 反向传播
optimizer.step() # 同步更新参数
with torch.no_grad():
acc_train = (net(X).argmax(dim=-1) == y).float().mean()
train_acc.append(acc_train.to('cpu'))
l = l.to('cpu')
l_list.append(l.detach().numpy())
net.eval()
X_test, y_test = next(iter(valid_iter))
X_test, y_test = X_test.cuda(0), y_test.cuda(0)
with torch.no_grad():
acc_test = (net(X_test).argmax(dim=1) == y_test).float().mean()
test_acc.append(acc_test.to('cpu'))
print(f"test accuracy: {100. * acc_test:.2f}%")
注:在获取测试集准确率、验证集准确率时,一定不要记录梯度。
通过全连接层输出的概率得到对应的类别
由于最后一层为\(\text{nn.Linear()}\)线性层,输出的是各类物品的概率,因此需要\(\text{torch.max()}\)或\(\text{torch.argmax()}\)函数将最大概率转换为对应的类别。
# torch.max()实现
_, predicted = torch.max(y_hat, dim=1) # 将nn.Linear()输出的概率结果转变为类别。.max()第一个返回的是最大值,第二个返回的是最大值的index
correct_item = [True if predicted[i] == y_test[i] else False for i in range(len(y_hat))
print(correct_item.count(True) / len(y_test))
# torch.argmax()实现
y_hat = net(X_train).argmax(dim=1)
correct_item = [True if y_hat[i] == y_test[i] else False for i in range(len(y_hat))]
# 直接计算准确率
acc_train = (net(X).argmax(dim=-1) == y).float().mean()
QA
高宽减半时将通道翻倍,这也被成为一个\(\textit{Stage}\)因为最后是把大量像素压缩到几百或几千的输出。可能最后的一个像素就需要代表一个类别。所以不能说是信息被放大了。\(\text{LeNet}\)的第二个卷积层通道数增加到\(16\),这意味信息被放大了吗?
输出通道增多,这些增加的信息是什么?
输出通道可以理解为[latex]\text{匹配的特定模式}[/latex],如手写数字的某个弯、横线、竖线、纹理、颜色、圈等。越下层的输出通道匹配的更多是简单的模式,如小纹理等;越上层的输出通道匹配的模式更复杂,如一个圈,一段特定内容等。
池化层一般用最大池化还是平均池化?用最大池化会不会损失很多信息?
用最大池化,是说,只关心最大的信号,如图片里有没有狗。如果出现了狗,把这个最大信息拎出来就行了。最大池化不见得比平均池化更损失信息,其实最大池化比平均池化训练起来效果更好,因为数值更大。
卷积层到底学了什么?
可以搜索[latex]\text{CNN Explainer}[/latex],有图例详解。
在模型跑得动的情况下,中间层输出通道应尽量大吗?
不一定,太大容易过拟合。中间层的大小应和数据复杂度相关。
深度学习是否一定需要大样本数据?有没有适用于小样本的深度学习?
在真实的场景中,通常使用迁移学习,这样就不要大量数据集了,也能很好地作用于自己的小样本数据。
深度学习是通过[latex]\text{结构化的模型}[/latex]去抽取[latex]\text{非结构化数据}[/latex]的语义信息,如图片、文本、音频等。
# 现代卷积神经网络
[latex]\text{AlexNet}[/latex]([latex]2012[/latex]年)之前的计算机视觉研究中,模型选择重要性程度位于次要,重要的是特征提取。此外,从时间上来看,由于数据量的大小与计算单元的能力尚可,神经网络在上个世纪八九十年代流行了一段时间;到[latex]2012[/latex]年之前,随着数据量增加,若使用神经网络,那么计算单元的性能无法较好处理这些数据,因此这段时间流行的是核方法;[latex]2012[/latex]年至今,得益于[latex]\text{AlexNet}[/latex]的提出和数据量与基于[latex]\text{GPU}[/latex]的计算单元性能得到巨大提升,神经网络因此又被广泛运用在方方面面。可见,模型选择受限于数据量和芯片能力。当计算能力超过数据量时,能深度挖掘数据信息的神经网络更受欢迎,反之核方法更受欢迎。并且,核方法的另一个优势是具有理论解释。
# AlexNet\(\text{ (Deep Convolutional Neural Networks)}\)
具体而言,此前计算机视觉的研究中最受关注的部分是人工提取图片中的特征,专家将对问题的理解转化为标准机器学习算法能够理解的数值,再通过 SVM 做识别;而[latex]\text{AlexNet}[/latex]通过 CNN 自动学习特征([latex]\textit{Patterns}[/latex]),再通过[latex]\textit{Softmax}[/latex]输出结果。在此过程中,深度学习网络学习到的特征很可能是[latex]\textit{Softmax}[/latex]想要的结果,这种一体化流程式的训练避免了大量专业计算机视觉方面的知识,如人工选取特征等。
[latex]\text{AlexNet}[/latex]是更深更大的[latex]\text{LeNet}[/latex],具有后者的[latex]10[/latex]倍参数个数,[latex]260[/latex]倍计算复杂度;新增加了丢弃法、[latex]\text{ReLU}[/latex]、池化层和数据增强等小技巧;[latex]\text{AlexNet}[/latex]标志着新的神经网络热潮开始。阶段总结
基于PyTorch实现\(\text{AlexNet}\)
这里仅给出网络与更改输入图片大小部分的代码:
\(\text{AlexNet}\)
net = nn.Sequential(
# 这里使用一个11*11的更大窗口来捕捉对象。
# 同时,步幅为4,以减少输出的高度和宽度。
# 另外,输出通道的数目远大于LeNet
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 使用三个连续的卷积层和较小的卷积窗口。
# 除了最后的卷积层,输出通道的数量进一步增加。
# 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
nn.Linear(6400, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10))
更改输入图片的像素大小
# trans = [transforms.ToTensor()]
# trans.insert(0, transforms.Resize(224)) # 用下面的表达
trans = transforms.Compose([transforms.Resize(224), transforms.ToTensor()])
mnist_train = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root='./fashion_mnist', train=False, transform=trans, download=True)
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True)
test_iter = data.DataLoader(mnist_test, batch_size, shuffle=True)
QA
\(\text{ImageNet}\)数据集是否成为历史了?
依旧是在用的数据集。论文中为证明训练的卷积神经网络效果较好,会使用它来给出结果。
\(\text{AlexNet}\)让机器自己寻找特征,所得到的特征与专家选取的特征一致吗?如果不一致,怎么解释?
不一致。优化目标是选取特征使最后一层能分类,不保证人读懂。[latex]\text{AlexNet}[/latex]怎么知道人存在这个事情。虽然神经网络一开始是模拟人的大脑工作,但现在不提这一套了。
一个效果会差。\(\text{Dense(4096)}\)是非常厉害的模型。因为前面的卷积特征抽得不够好不够深,所以靠这两个巨大的全连接层来补。为什么最后有两个\(\text{Dense(4096)}\)全连接层,一个行吗?
数据增强后,效果反而更差了,为什么?
数据增强可以看作是超参数,调了之后效果变差也很正常。
为什么\(\text{LeNet}\)不属于深度卷积神经网络?
当然属于。在[latex]\text{transformer}[/latex]和[latex]\text{Attention}[/latex]之前,神经网络鲜有新事物。因此需要把之前的东西用 fashion 的词汇包装,其中 deep 就是这帮搞深度学习的人想出来的很好的词。
# Networks Using Blocks\(\text{ (Visual Geometry Group, VGG)}\)
更深、更大、数据更多使神经网络效果更好。[latex]\text{VGG}[/latex]提出一种使网络变得更深更大的设计思想,即将卷积层组合成块,再一块一块地垒砌成神经网络。
可见,[latex]\text{VGG块}[/latex]由[latex]3\times 3[/latex]且\(\textit{Padding=}1\)的卷积层与\(2\times 2\)且\(\textit{Stride}=2\)的最大池化层组成。同样计算开销下网络深且窄效果更好,因此卷积层不用[latex]5\times 5[/latex]。其中,可设置的超参数为卷积层的层数\(n\)以及通道数\(m\)。
从图中还可发现,多个\(\text{VGG块}\)后面接全连接层。组合不同次数的重复块可得到不同的架构 VGG-16 、 VGG-19 。注:其中的[latex]16[/latex]包括最后的三个全连接层。
[latex]\text{VGG}[/latex]使用可重复使用的卷积块来构建深度卷积神经网络,不同的卷积块个数及其超参数可以得到不同复杂度的变种。之后出的很多网络无论在速度还是准确率上均优于\(\text{VGG}\),然而\(\text{VGG}\)作为一个经典思想,会在之后被大量使用。基于PyTorch实现\(\text{VGG}\)
\(\text{VGG块}\)函数实现
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
def vgg(conv_arch):
conv_blks = []
in_channels = 1
# 卷积层部分
for (num_convs, out_channels) in conv_arch:
conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
# 全连接层部分
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 10))
构建\(\text{VGG}\)
# (1, 64)指的是有1层卷积层,64个通道。这是经典架构,高宽减半时通道翻倍
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)) # 这里有5块是因为224 // 2^5 = 7,7为质数除不动了
net = vgg(conv_arch)
# (optional)基于上面的代码快速得到更小的网络
ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)
每一个\(\text{VGG块}\)将图片高宽减半,然后将通道翻倍,这是经典操作。
# 网络中的网络 \(\text{ (Network in Network, NiN)}\)
该网络现已不常用,但其提出的概念很重要,并融入了之后的网络。
[latex]\large \begin{align} & \textbf{全连接层的问题}\\& \centerdot \text{卷积层需要较少的参数}\\& \quad c_i\times c_o\times k^2\\& \centerdot \text{但卷积层后的第一个全连接层的参数:}\\& \quad \text{LeNet}\;\;16\times 5\times 5\times 120=48k\\& \quad \text{AlexNet}\;\;256\times 5\times 5\times 4096=26M\\& \quad \text{VGG}\;\;512\times 7\times 7\times 4096=102M\end{align}[/latex]
大量参数意味着:占用更多内存、计算带宽;收敛快但更容易过拟合,因此需要更多的正则化。
为解决上述问题,[latex]\text{NiN}[/latex]提出完全不要全连接层的方案。即[latex]\text{NiN块}[/latex]:
[latex]\large \begin{align}& \textbf{NiN块}\\& \centerdot \text{一个卷积层后跟两个\((1\times 1)\)卷积层}\\& \quad \text{步幅1,无填充,输出形状跟卷积层输出一致}\\& \quad \text{起到全连接层的作用}\end{align}[/latex]
如此以来,[latex]\text{NiN架构}[/latex]就不含全连接层了;它交替使用[latex]\text{NiN块}[/latex]和[latex]Stride=2[/latex]的最大池化层,以此逐步减少高宽和增大通道数;最后使用[latex]\text{输入通道数=类别数}[/latex]的全局平均池化层得到输出结果。
[latex]\text{NiN}[/latex]的核心思想汇聚于[latex]\text{NiN块}[/latex]。该块在卷积层后加两个[latex](1\times 1)[/latex]卷积层,后者通过[latex]\textit{nn.ReLU()}[/latex]为每个像素增加了非线性性;[latex]\text{NiN}[/latex]使用\(\textbf{全局平均池化层}\)来代替[latex]\text{VGG}[/latex]和[latex]\text{AlexNet}[/latex]中的全连接层,这样不容易过拟合,且降低了模型复杂度,因为要求更少的参数。阶段总结
基于PyTorch实现\(\text{NIN}\)
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, 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())
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10,所以输出通道降成了10
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
# 将四维的输出转成二维的输出,其形状为(批量大小,10)
nn.Flatten())
注:在最后,输出通道猛地降低至输出类别数之前,有一层[latex]\textit{nn.Dropout(0.5)}[/latex]。
QA
很宽的单隐藏层为什么效果差?
很容易过拟合。
哪些东西占内存?
无论[latex]\text{batch_size}[/latex]是多大,都要存模型的参数、梯度等。并且[latex]\text{batch_size}[/latex]越大,占的空间也越大;越深的网络对内存、显存的要求也越高。在实装 AMD 的 Linux 上使用 rocm-smi 可查看占用情况。
全局池化层带来的影响大吗?
首先,全局平均池化层并未替代[latex]\textit{nn.Softmax()}[/latex],没在 net 中写出来的原因是交叉损失函数里有[latex]\textit{Softmax()}[/latex]层;全局池化层非常重要,它的作用是把输入变小,且没有可学习的参数,此举降低了模型复杂度。它还能提升泛化性,不过这样会降低收敛速度。
[latex]\text{AlexNet}[/latex]收敛得快是因为最后的超大全连接层太强了,太容易拟合数据了。# Multi-Branch Networks\(\text{ – GoogLeNet}\)
前文中的[latex]\text{NiN}[/latex]已不常见,但却严重影响了该网络的构建。鉴于良好的表现,[latex]\text{GoogLeNet}[/latex]的后续版本目前还很常见。
前文提到的[latex]\text{LeNet}[/latex]、[latex]\text{AlexNet}[/latex]、[latex]\text{VGG}[/latex]、[latex]\text{NiN}[/latex]各自有独特的设计排列卷积层,然而哪种设计效果最好却不得而知。
[latex]\text{GoogLeNet}[/latex]提出的[latex]\text{Inception块}[/latex]说小学生才做选择题,于是另辟蹊径在一个块中给出了[latex]4[/latex]个路径,通过这些路径从不同层面抽取信息,然后在输出通道维合并,所以输出通道特别多。而输入与输出的高宽不变。如下图所示:在通道数分配方面,可以更多地把通道留给效果更好的路径,如[latex]3\times 3[/latex]卷积层的路径。从下表可见,和单[latex]3\times 3[/latex]或[latex]5\times 5[/latex]卷积层比,[latex]\text{Inception块}[/latex]具有更少的参数个数和计算复杂度。标成白色的卷积层用来变化通道数,要么改变输入要么改变输出;蓝色的层用来抽取信息,其中\(1\times 1\)的层不抽取空间信息只抽取通道信息。其它大小的抽取空间信息。最大池化层也抽空间信息,可以被认为起卷积层的作用,使得模型更加鲁棒。
$$ \large \begin{array}{|c|c|c|}\hline & \text{#parameters} & \text{FLOPS} \\ \hline \text{Inception} & 0.16\text{M} & 128\text{M} \\ \hline 3\times 3\text{ Conv} & 0.44\text{M} & 346\text{M} \\ \hline 5\times 5\text{ Conv} & 1.22\text{M} & 963\text{M} \\ \hline\end{array}$$
[latex]\text{GoogLeNet}[/latex]被分为[latex]5[/latex]个[latex]\textit{stage}[/latex],一共使用[latex]9[/latex]个[latex]\text{Inception块}[/latex]和全局平均汇聚层的堆叠来生成其估计值。[latex]\text{Inception块}[/latex]之间的最大汇聚层可降低维度。 第一个模块类似于[latex]\text{AlexNet}[/latex]和[latex]\text{LeNet}[/latex],[latex]\text{Inception块}[/latex]的组合从[latex]\text{VGG}[/latex]继承,全局平均汇聚层避免了在最后使用全连接层。
其中,[latex]\text{Inception块}[/latex]输出时不降低输入的高宽。而每个[latex]\textit{Stage}[/latex]降低一半的高宽,这通过[latex]Stride=2[/latex]且[latex]Padding=1[/latex]的最大池化层实现。
第五模块的后面紧跟输出层,该模块同[latex]\text{NiN}[/latex]一样使用全局平均汇聚层,将每个通道的高和宽变成[latex]1[/latex],得到长为通道数的向量。 这里不强求通道数=类别数。原因见下面的阶段。
最后将输出变成二维数组,再接上一个输出个数为标签类别数的全连接层。
[latex]\textbf{\(\textit{图源D2L}\)}[/latex]
注:在[latex]\textit{Stage 1&2}[/latex]中,和[latex]\text{AlexNet}[/latex]相对比,使用了更小的卷积窗口,并且更缓慢地减少输入高宽。
这五个阶段做的事都是\(\textbf{降低高宽,增加通道}\)。
[latex]\text{Inception-BN(v2)}[/latex]使用了 Batch normalization ;[latex]\text{Inception-V3}[/latex]修改了[latex]\text{Inception块}[/latex],如替换[latex]5\times 5[/latex]为多个[latex]3\times 3[/latex]卷积层、替换[latex]5\times 5[/latex]为[latex]1\times 7[/latex]和[latex]7\times 1[/latex]卷积层、替换[latex]3\times 3[/latex]为[latex]1\times 3[/latex]和[latex]3\times 1[/latex]卷积层,并将网络做得更深;[latex]\text{Inception-V4}[/latex]使用了残差连接。\(\text{Inception}\)后续变种
[latex]\text{Inception块}[/latex]用四条具有不同超参数的卷积层和池化层的路径来抽取不同信息,主要优点是模型参数小,计算复杂度低;[latex]\text{GoogLeNet}[/latex]使用[latex]9[/latex]个[latex]\text{Inception块}[/latex],是第一个含上百个卷积层的网络,且后续有一系列改进。注:[latex]\text{ResNet}[/latex]是第一个有上百层深卷积层的网络。阶段总结
基于PyTorch实现\(\text{GoogLeNet}\)
由于 PyTorch 提出了 nn.LazyConv2d() 方法,避免了计算输入通道数的麻烦,因此后续代码尽量使用该新方法。注:该新方法仅需给出输出通道数;截至2024年5月依旧是新特性,可能随时变换接口。
\(\text{Inception块}\)
class Inception(nn.Module):
# c1--c4 are the number of output channels for each branch
def __init__(self, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# Branch 1
self.b1_1 = nn.LazyConv2d(c1, kernel_size=1)
# Branch 2
self.b2_1 = nn.LazyConv2d(c2[0], kernel_size=1)
self.b2_2 = nn.LazyConv2d(c2[1], kernel_size=3, padding=1)
# Branch 3
self.b3_1 = nn.LazyConv2d(c3[0], kernel_size=1)
self.b3_2 = nn.LazyConv2d(c3[1], kernel_size=5, padding=2)
# Branch 4
self.b4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.b4_2 = nn.LazyConv2d(c4, kernel_size=1)
def forward(self, x):
b1 = F.relu(self.b1_1(x))
b2 = F.relu(self.b2_2(F.relu(self.b2_1(x))))
b3 = F.relu(self.b3_2(F.relu(self.b3_1(x))))
b4 = F.relu(self.b4_2(self.b4_1(x)))
return torch.cat((b1, b2, b3, b4), dim=1)
构造原文中的五个模块
b1 = nn.Sequential(
nn.LazyConv2d(64, kernel_size=7, stride=2, padding=3),
nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b2 = nn.Sequential(
nn.LazyConv2d(64, kernel_size=1), nn.ReLU(),
nn.LazyConv2d(192, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b3 = nn.Sequential(Inception(64, (96, 128), (16, 32), 32),
Inception(128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b4 = nn.Sequential(Inception(192, (96, 208), (16, 48), 64),
Inception(160, (112, 224), (24, 64), 64),
Inception(128, (128, 256), (24, 64), 64),
Inception(112, (144, 288), (32, 64), 64),
Inception(256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b5 = nn.Sequential(Inception(256, (160, 320), (32, 128), 128),
Inception(384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1,1)), nn.Flatten())
构造网络并测试每层的输入输出
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
QA
正常使用深度学习时直接套用别的模型吗?
如果不是专家,尽量先套用经典模型,且不要更改架构;可以更改经典模型的输入输出通道。
替换\(3\times 3\)为\(1\times 3\)和\(3\times 1\)卷积层的好处是什么?
降低计算量。坏处是效果没有原来那么的好。
Linear、Dense和Flatten的区别?
Linear和Dense是一个东西,都指全连接。Flatten是保持批量大小维的情况下把其他所有维的东西拉成向量。
在图像识别中低成本调参?
可以用[latex]\text{ImageNet}[/latex]的子集上调参。
为什么通道数越来越多?
识别足够的\(\textit{Patterns}\)。对于\(100\)万张图片的[latex]\text{ImageNet}[/latex]来说,最后有\(1024\)个通道差不多了。注:和类别有一定关系,但不是完全对应的关系。即使有\(2000\)类、\(3000\)类,\(1024\)个输出通道还是不错的选择。但更大的话可以适当调大输出通道数。
# 批量规范化
在深度神经网络中,每一个[latex]\textit{Batch}[/latex]内样本的分布可能差异较大,且每一层输入的分布随着网络的加深而发生变化。为使模型正常训练,则要求较小的学习率、正确初始化网络参数并严格控制网络深度,导致模型收敛地十分缓慢。
为加速模型训练,提出[latex]\text{批量规范化}[/latex]使输入在每一层都服从类似的分布。批量规范化是很深的神经网络不可避免的层。
\(\large \begin{align}& \centerdot \text{固定小批量样本的均值和方差}\\& \quad \mu_B=\frac{1}{\lvert B\rvert}\displaystyle\sum_{i\in B}x_i \text{ and } \sigma_{B}^{2}=\frac{1}{\lvert B\rvert}\displaystyle\sum_{i\in B}\left(x_i -\mu_B\right)^2+\epsilon\\& \text{然后再做额外的调整(这是可学习的参数)}\\& \quad x_{i+1}=\gamma\frac{x_i-\mu_B}{\sigma_B}+\beta \end{align}\)
\(\large \begin{align}& \centerdot \text{可学习的参数为}\gamma\text{和}\beta\\& \centerdot \text{作用在}\\& \quad \centerdot \text{全连接层和卷积层输出上,激活函数前}\\& \quad \centerdot \text{全连接层和卷积层输入上}\\& \centerdot \text{对于全连接层,作用在特征维}\\& \centerdot \text{对于卷积层,作用在通道维} \end{align}\)
其中:[latex]B[/latex]指小批量;[latex]\epsilon[/latex]为避免分母为〇的辅助常数,常取[latex]1e-5[/latex]、[latex]1e-6[/latex];[latex]\gamma[/latex]为学习到的方差;[latex]\beta[/latex]是学习到的均值。推理时用全局平均与方差,训练时用小批量的均值与方差。
有论文指出,[latex]\text{批量规范化}[/latex]可能通过在每个小批量加入噪音以控制模型复杂度,因此没必要和丢弃法混合使用。
阶段总结:[latex]\text{批量规范化}[/latex]固定小批量中的均值和方差,然后学习出合适的偏移和缩放;可以加速收敛速度,但一般不改变模型精度;加速收敛后把学习率扩大\(10\)倍!;输出层加不加[latex]\text{批量规范化层}[/latex]都无所谓。
基于PyTorch实现\(\text{Batch Normalization}\)
net = nn.Sequential(
nn.LazyConv2d(6, kernel_size=5), nn.LazyBatchNorm2d(),
nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2),
nn.LazyConv2d(16, kernel_size=5), nn.LazyBatchNorm2d(),
nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(), nn.LazyLinear(120), nn.LazyBatchNorm1d(),
nn.Sigmoid(), nn.LazyLinear(84), nn.LazyBatchNorm1d(),
nn.Sigmoid(), nn.LazyLinear(num_classes))
QA
参数初始化的\(\textit{normalization}\)和这里的\(\textit{normalization}\)有什么区别?
从目的上来看,没太大区别。前者使模型初始比较稳定,不能保证之后;这里的[latex]\textit{normalization}[/latex]让整个模型都比较稳定。核心都是让数值稳定。
\(\textit{normalization}\)能用在全连接多层感知机上吗?
可以。但它对深度神经网络更有用,对[latex]\text{LeNet}[/latex]之类的浅层网络没太多用。
\(\textit{batch_size}\)、\(\textit{learning_rate}\)、\(\textit{architecture}\)和\(\textit{epoch}\)怎么权衡?
在实践中,首先看\(\textit{batch_size}\),不能太大也不能太小。调一个能占用较高\(\text{GPU}\)的值,如利用率\(90\%\)。最好调到\(\text{num_examples}\)不再提升的时候;\(\textit{epoch}\)尽量大,训练中途可以停,再调;\(\textit{architecture}\)一般不会换,大家都习惯用熟悉的框架。
\(\text{Batch Normalization}\)能否用在激活函数之后?
一般不用在激活函数之后,因为它是对输入输出做变换的方法。
# 残差网络\(\textit{ – ResNet}\)
提问:在卷积神经网络中,加深网络一定会提升模型准确度吗?答案是否定的。如下图所示,左子图为不使用残差块的网络,可能加深至[latex]\textit{\(F_6\)}[/latex]后网络学偏了,离最优点最小距离可能还大于最初的[latex]\textit{\(F_1\)}[/latex];右子图利用残差块,加深的网络块学习残差,会不断逼近最优点。该思想类似于梯度提升树。
[latex]\textbf{残差块}[/latex]如右图所示
一个正常块(左子图)和一个残差块(右子图)
串联一个层改变函数类,希望扩大函数类。
残差块加入快速通道(右图)来得到[latex]f(x)=x+g(x)[/latex]的结构。
[latex]\textbf{\(\textit{图源D2L}\)}[/latex][latex]\textbf{残差块}[/latex]细节
下图中二者都为残差块。一般而言,最大池化后先跟右子图残差块,将输入高宽减半通道数翻倍,其中的\(1\times 1\)卷积层用于变换通道。然后跟多个不改变高宽的左子图的残差块。
[latex]\textbf{ResNet}[/latex]架构
类似于[latex]\textbf{ VGG }[/latex]和[latex]\textbf{ GoogLeNet }[/latex]的总体架构,先[latex]7\times 7[/latex]卷积然后[latex]3\times 3[/latex]最大池化,后面替换为自己想要的块。这里使用[latex]\textbf{ 残差块 }[/latex]。这是 ResNet-18 。
阶段总结
之后几乎所有新的网络,无论是卷积类网络还是全连接类网络,学习残差的思想(残差块)都会被用到;残差块使很深的网络训练更加容易,因此可以把网络加深到千层往上。
基于PyTorch实现\(\text{残差块}\)
构造高宽减半通道增加和不改变高宽的残差块
class Residual(nn.Module): #@save
"""The Residual block of ResNet models."""
def __init__(self, num_channels, use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1,
stride=strides)
self.conv2 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.LazyConv2d(num_channels, kernel_size=1,
stride=strides)
else:
self.conv3 = None
self.bn1 = nn.LazyBatchNorm2d()
self.bn2 = nn.LazyBatchNorm2d()
def forward(self, X):
Y = nn.functional.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return nn.functional.relu(Y)
# 高宽减半通道加倍的残差块
blk = Residual(6, use_1x1conv=True, strides=2)
# 不改变高宽的残差块
blk = Residual(3)
构建\(\text{ResNet}\)模型
def resnet_block(num_channels, num_residuals, first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(num_channels, use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels))
return blk
b1 = nn.Sequential(nn.LazyConv2d(64, kernel_size=7, stride=2, padding=3),
nn.LazyBatchNorm2d(), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b2 = nn.Sequential(*resnet_block(64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(128, 2))
b4 = nn.Sequential(*resnet_block(256, 2))
b5 = nn.Sequential(*resnet_block(512, 2))
net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.LazyLinear(10))
QA
大\(\textit{batch_size}\)为什么影响收敛速度?
当\(\textit{batch_size}\)较大时,大部分图片很相似,重复的图片意味重复计算,因此影响收敛进度。
为什么\(\text{ResNet}\)可以做到上千层
把乘法变成加法了,极大降低了梯度爆炸和梯度消失的发生概率。
\(\text{ResNet}\)可以用于文本吗?
不行。但卷积可以用于文本。
# Kaggle:树叶分类
详见此处。
其中[latex]\textbf{加载自定义数据集的方法}[/latex]、[latex]\textbf{分割训练集验证集的方法}[/latex]、[latex]\textbf{给字符串标签编号的方法}[/latex]以及[latex]\textbf{调用\(\textit{torchvision}\)自带模型的方法}[/latex]都很值得参考。其中需要利用 pillow 、 scikit-learn 。
# 深度学习硬件
提升\(\textit{CPU}\)利用率I
可以考虑从时间和空间的内存本地性以提升其利用率。具体而言,时间的内存本地性指重用数据使它们保持在缓存里;空间的内存本地性指按序读写数据使数据可以被预读取。
提升\(\textit{CPU}\)利用率II
可以考虑提高其频率、并行利用所有的核来提升其利用率。
提升\(\textit{GPU}\)利用率II
并行:使用数千个线程。不过这也取决于数据维度等。;内存本地性:缓存更小,\(\textit{GPU}\)架构更简单;少用控制语句:架构支持有限,同步开销很大。
不要频繁在\(\textit{CPU}\)与\(\textit{GPU}\)之间搬数据
二者传输的带宽有限;具有同步开销,即搬数据时要停止计算。
\(\textit{CPU}\)与\(\textit{GPU}\)小结
前者可以处理通用计算,在性能优化方面考虑数据读写速率与多线程;后者使用更多小核和更好的内存带宽,适合能大规模并行的计算任务。