23暑假深度学习week2

本文最后更新于:2023年7月29日 晚上

卷积神经网络的结构

卷积

image-20230208210343866

卷积核大小定义了卷积操作的感受野(Receptive Field),即所有可能影响像 元 x 前向计算的输入区域。步幅定义了卷积核窗口滑动时的单位距离长度;填充 即在图像边缘添加额外的元素(通常为 0),可以使得输入矩阵和输出矩阵具有相 同的大小。

卷积操作

3D 卷积

二维卷积(2D-CNN)能够充分提取图像数据的特征,并用于分类、识别等任务,而对于具有时间维度的视频分析、行为识别等问题,但二维卷积很难捕获数据中的时序信息。因此三维卷积(3D-CNN)操作被提出,并最早应用于行为识别任务,卷积神经网络通过三维的卷积核同时提取数据的时间和空间特征。3D 卷积比 2D 卷积多一个深度维度信息,可以将 2D 卷积看作是深度为 1 的 3D 卷积。

3D卷积

池化层

通过卷积操作,虽然已经对输入特征进行了提取和降维,但是维度仍旧很高,容易在训练中造成过拟合的问题。因此引入池化操作,对卷积后的图像特征进行分块,特征图被分为不相交的多个块,然后计算块内部的最大值或者平均值,从而得到池化后的图像。池化层不包含需要神经网络学习的参数,进行池化操作也不会改变通道数。因此,经过池化层处理的特征图通道数在处理前后相等。此外,采用池化层还可以使神经网络对一些图像局部细节的形态改变保持不变性,其感受野相比运算之前往往更大。

最大值池化(Max Pooling)与均值池化(Mean Pooling)是两种常见的池化操作方式。

池化

全连接层

全连接层(Full Connected Layer)通常处于神经网络的末尾,可以对神经网络所提取的特征进行汇总,将局部特征重新通过权值矩阵组装成完整的图,最终将学习到的“分布式特征表示”映射到样本标记空间。通常使用 softmax 函数将输出结果变换为满足大小为正、和为一的概率分布,以得到最终的分类或回归结果。

MNIST 数据集分类

构建简单的CNN对mnist数据集进行分类。同时,还会在实验中学习池化与卷积操作的基本作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy

# 一个函数,用来计算模型中有多少参数
def get_n_params(model):
np=0
for p in list(model.parameters()):
np += p.nelement()
return np

# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

1. 加载数据 (MNIST)

PyTorch里包含了 MNISTCIFAR10 等常用数据集,调用 torchvision.datasets 即可把这些数据由远程下载到本地,下面给出MNIST的使用方法:

torchvision.datasets.MNIST(root, train=True, transform=None, target_transform=None, download=False)

  • root 为数据集下载到本地后的根目录,包括 training.pt 和 test.pt 文件
  • train,如果设置为True,从training.pt创建数据集,否则从test.pt创建。
  • download,如果设置为True, 从互联网下载数据并放到root文件夹下
  • transform, 一种函数或变换,输入PIL图片,返回变换之后的数据。
  • target_transform 一种函数或变换,输入目标,进行变换。

另外值得注意的是,DataLoader是一个比较重要的类,提供的常用操作有:batch_size(每个batch的大小), shuffle(是否进行随机打乱顺序的操作), num_workers(加载数据的时候使用几个子进程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
input_size  = 28*28   # MNIST上的图像尺寸是 28x28
output_size = 10 # 类别为 0 到 9 的数字,因此为十类

train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=True, download=True,
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=64, shuffle=True)

test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=1000, shuffle=True)

显示数据集中的部分图像

1
2
3
4
5
6
plt.figure(figsize=(8, 5))
for i in range(20):
plt.subplot(4, 5, i + 1)
image, _ = train_loader.dataset.__getitem__(i)
plt.imshow(image.squeeze().numpy(),'gray')
plt.axis('off');

输出

2. 创建网络

定义网络时,需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在构造函数**init**中。

只要在nn.Module的子类中定义了forward函数,backward函数就会自动被实现(利用autograd)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class FC2Layer(nn.Module):
def __init__(self, input_size, n_hidden, output_size):
# nn.Module子类的函数必须在构造函数中执行父类的构造函数
# 下式等价于nn.Module.__init__(self)
super(FC2Layer, self).__init__()
self.input_size = input_size
# 这里直接用 Sequential 就定义了网络,注意要和下面 CNN 的代码区分开
self.network = nn.Sequential(
nn.Linear(input_size, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, output_size),
nn.LogSoftmax(dim=1)
)
def forward(self, x):
# view一般出现在model类的forward函数中,用于改变输入或输出的形状
# x.view(-1, self.input_size) 的意思是多维的数据展成二维
# 代码指定二维数据的列数为 input_size=784,行数 -1 表示我们不想算,电脑会自己计算对应的数字
# 在 DataLoader 部分,我们可以看到 batch_size 是64,所以得到 x 的行数是64
# 大家可以加一行代码:print(x.cpu().numpy().shape)
# 训练过程中,就会看到 (64, 784) 的输出,和我们的预期是一致的

# forward 函数的作用是,指定网络的运行过程,这个全连接网络可能看不啥意义,
# 下面的CNN网络可以看出 forward 的作用。
x = x.view(-1, self.input_size)
return self.network(x)



class CNN(nn.Module):
def __init__(self, input_size, n_feature, output_size):
# 执行父类的构造函数,所有的网络都要这么写
super(CNN, self).__init__()
# 下面是网络里典型结构的一些定义,一般就是卷积和全连接
# 池化、ReLU一类的不用在这里定义
self.n_feature = n_feature
self.conv1 = nn.Conv2d(in_channels=1, out_channels=n_feature, kernel_size=5)
self.conv2 = nn.Conv2d(n_feature, n_feature, kernel_size=5)
self.fc1 = nn.Linear(n_feature*4*4, 50)
self.fc2 = nn.Linear(50, 10)

# 下面的 forward 函数,定义了网络的结构,按照一定顺序,把上面构建的一些结构组织起来
# 意思就是,conv1, conv2 等等的,可以多次重用
def forward(self, x, verbose=False):
x = self.conv1(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = x.view(-1, self.n_feature*4*4)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x

定义训练和测试函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 训练函数
def train(model):
model.train()
# 主里从train_loader里,64个样本一个batch为单位提取样本进行训练
for batch_idx, (data, target) in enumerate(train_loader):
# 把数据送到GPU中
data, target = data.to(device), target.to(device)

optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print('Train: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))


def test(model):
model.eval()
test_loss = 0
correct = 0
for data, target in test_loader:
# 把数据送到GPU中
data, target = data.to(device), target.to(device)
# 把数据送入模型,得到预测结果
output = model(data)
# 计算本次batch的损失,并加到 test_loss 中
test_loss += F.nll_loss(output, target, reduction='sum').item()
# get the index of the max log-probability,最后一层输出10个数,
# 值最大的那个即对应着分类结果,然后把分类结果保存在 pred 里
pred = output.data.max(1, keepdim=True)[1]
# 将 pred 与 target 相比,得到正确预测结果的数量,并加到 correct 中
# 这里需要注意一下 view_as ,意思是把 target 变成维度和 pred 一样的意思
correct += pred.eq(target.data.view_as(pred)).cpu().sum().item()

test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
accuracy))

3. 在小型全连接网络上训练(Fully-connected network)

1
2
3
4
5
6
7
8
9
n_hidden = 8 # number of hidden units

model_fnn = FC2Layer(input_size, n_hidden, output_size)
model_fnn.to(device)
optimizer = optim.SGD(model_fnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_fnn)))

train(model_fnn)
test(model_fnn)

训练结果

3. 在卷积神经网络上训练

1
2
3
4
5
6
7
8
9
10
# Training settings
n_features = 6 # number of feature maps

model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))

train(model_cnn)
test(model_cnn)

输出结果

通过上面的测试结果,可以发现,含有相同参数的 CNN 效果要明显优于 简单的全连接网络,是因为 CNN 能够更好的挖掘图像中的信息,主要通过两个手段:

  • 卷积:Locality and stationarity in images
  • 池化:Builds in some translation invariance

5. 打乱像素顺序再次在两个网络上训练与测试

考虑到CNN在卷积与池化上的优良特性,如果我们把图像中的像素打乱顺序,这样 卷积 和 池化 就难以发挥作用了,为了验证这个想法,我们把图像中的像素打乱顺序再试试。

首先下面代码展示随机打乱像素顺序后,图像的形态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 这里解释一下 torch.randperm 函数,给定参数n,返回一个从0到n-1的随机整数排列
perm = torch.randperm(784)
plt.figure(figsize=(8, 4))
for i in range(10):
image, _ = train_loader.dataset.__getitem__(i)
# permute pixels
image_perm = image.view(-1, 28*28).clone()
image_perm = image_perm[:, perm]
image_perm = image_perm.view(-1, 1, 28, 28)
plt.subplot(4, 5, i + 1)
plt.imshow(image.squeeze().numpy(), 'gray')
plt.axis('off')
plt.subplot(4, 5, i + 11)
plt.imshow(image_perm.squeeze().numpy(), 'gray')
plt.axis('off')

打乱像素顺序的图像

重新定义训练与测试函数,我们写了两个函数 train_permtest_perm,分别对应着加入像素打乱顺序的训练函数与测试函数。

与之前的训练与测试函数基本上完全相同,只是对 data 加入了打乱顺序操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 对每个 batch 里的数据,打乱像素顺序的函数
def perm_pixel(data, perm):
# 转化为二维矩阵
data_new = data.view(-1, 28*28)
# 打乱像素顺序
data_new = data_new[:, perm]
# 恢复为原来4维的 tensor
data_new = data_new.view(-1, 1, 28, 28)
return data_new

# 训练函数
def train_perm(model, perm):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
# 像素打乱顺序
data = perm_pixel(data, perm)

optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print('Train: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))

# 测试函数
def test_perm(model, perm):
model.eval()
test_loss = 0
correct = 0
for data, target in test_loader:
data, target = data.to(device), target.to(device)

# 像素打乱顺序
data = perm_pixel(data, perm)

output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item()
pred = output.data.max(1, keepdim=True)[1]
correct += pred.eq(target.data.view_as(pred)).cpu().sum().item()

test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
accuracy))

在全连接网络上训练与测试:

1
2
3
4
5
6
7
8
9
10
perm = torch.randperm(784)
n_hidden = 8 # number of hidden units

model_fnn = FC2Layer(input_size, n_hidden, output_size)
model_fnn.to(device)
optimizer = optim.SGD(model_fnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_fnn)))

train_perm(model_fnn, perm)
test_perm(model_fnn, perm)

测试结果

在卷积神经网络上训练与测试:

1
2
3
4
5
6
7
8
9
10
perm = torch.randperm(784)
n_features = 6 # number of feature maps

model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))

train_perm(model_cnn, perm)
test_perm(model_cnn, perm)

测试结果

从打乱像素顺序的实验结果来看,全连接网络的性能基本上没有发生变化,但是 卷积神经网络的性能明显下降。

思考

卷积神经网络(Convolutional Neural Network,CNN)在处理图像、语音、文本等数据时,利用了数据的局部关系。在图像中,像素之间的相邻关系是非常重要的,因为相邻的像素通常具有相似的颜色、亮度、纹理等特征。在卷积操作中,卷积核会在输入数据上进行滑动,并对每个位置上的像素进行卷积计算,从而捕捉到局部特征。

然而,当像素的顺序被打乱后,像素之间的相邻关系也被破坏了,这会影响到卷积操作的准确性,从而影响到整个卷积神经网络的性能。因为卷积神经网络是通过学习数据中的特征来进行分类、识别等任务的,如果数据中的特征信息被破坏了,网络的性能就会受到影响。

相比之下,全连接神经网络(Fully Connected Neural Network,FCN)在处理数据时并不依赖于像素的顺序,因为所有的神经元都是相互连接的。所以,即使像素的顺序被打乱,全连接神经网络的性能并不会受到太大的影响。

总之,卷积神经网络对于像素的局部关系比较敏感,而全连接神经网络对于数据的组织形式不太敏感,这也是它们在处理打乱像素顺序的实验中出现不同性能表现的原因。

CIFAR10 数据集分类

下面将使用CIFAR10数据集,它包含十个类别:‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。CIFAR-10 中的图像尺寸为3x32x32,也就是RGB的3层颜色通道,每层通道内的尺寸为32*32。

CIFAR10

首先,加载并归一化 CIFAR10 使用 torchvision 。torchvision 数据集的输出是范围在[0,1]之间的 PILImage,我们将他们转换成归一化范围为[-1,1]之间的张量 Tensors。

大家肯定好奇,下面代码中说的是 0.5,怎么就变化到[-1,1]之间了?PyTorch源码中是这么写的:

1
input[channel] = (input[channel] - mean[channel]) / std[channel]

这样就是:((0,1)-0.5)/0.5=(-1,1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# 注意下面代码中:训练的 shuffle 是 True,测试的 shuffle 是 false
# 训练时可以打乱顺序增加多样性,测试是没有必要
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,
shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=8,
shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

下面展示 CIFAR10 里面的一些图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def imshow(img):
plt.figure(figsize=(8,8))
img = img / 2 + 0.5 # 转换到 [0,1] 之间
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()

# 得到一组图像
images, labels = next(iter(trainloader))
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示第一行图像的标签
for j in range(8):
print(classes[labels[j]])

样例

1
2
3
4
5
6
7
8
frog
cat
plane
dog
cat
cat
horse
ship

接下来定义网络,损失函数和优化器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
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, 10)

def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

# 网络放到GPU上
net = Net().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

训练网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for epoch in range(10):  # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))

print('Finished Training')

现在我们从测试集中取出8张图片:

1
2
3
4
5
6
7
# 得到一组图像
images, labels = next(iter(testloader))
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示图像的标签
for j in range(8):
print(classes[labels[j]])

1
2
3
4
5
6
7
8
cat
ship
ship
plane
frog
frog
car
frog

我们把图片输入模型,看看CNN把这些图片识别成什么:

1
2
3
4
5
6
outputs = net(images.to(device))
_, predicted = torch.max(outputs, 1)

# 展示预测的结果
for j in range(8):
print(classes[predicted[j]])
1
2
3
4
5
6
7
8
cat
ship
ship
plane
frog
frog
car
frog

让我们看看网络在整个数据集上的表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
correct = 0
total = 0

for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))

Accuracy of the network on the 10000 test images: 61 %

使用 VGG16 对 CIFAR10 分类

VGG是由Simonyan 和Zisserman在文献《Very Deep Convolutional Networks for Large Scale Image Recognition》中提出卷积神经网络模型,其名称来源于作者所在的牛津大学视觉几何组(Visual Geometry Group)的缩写。

该模型参加2014年的 ImageNet图像分类与定位挑战赛,取得了优异成绩:在分类任务上排名第二,在定位任务上排名第一。

VGG16的网络结构如下图所示:

VGG16

16层网络的结节信息如下:

  • 01: Convolution using 64 filters
  • 02: Convolution using 64 filters + Max pooling
  • 03: Convolution using 128 filters
  • 04: Convolution using 128 filters + Max pooling
  • 05: Convolution using 256 filters
  • 06: Convolution using 256 filters
  • 07: Convolution using 256 filters + Max pooling
  • 08: Convolution using 512 filters
  • 09: Convolution using 512 filters
  • 10: Convolution using 512 filters + Max pooling
  • 11: Convolution using 512 filters
  • 12: Convolution using 512 filters
  • 13: Convolution using 512 filters + Max pooling
  • 14: Fully connected with 4096 nodes
  • 15: Fully connected with 4096 nodes
  • 16: Softmax

1. 定义 dataloader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])

transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)

trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

2. VGG 网络定义

下面定义VGG网络,现在的结构基本上是:

64 conv, maxpooling,

128 conv, maxpooling,

256 conv, 256 conv, maxpooling,

512 conv, 512 conv, maxpooling,

512 conv, 512 conv, maxpooling,

softmax

下面是模型的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
self.cfg = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']
self.features = self._make_layers(self.cfg)
self.classifier = nn.Linear(512, 10)

def forward(self, x):
out = self.features(x)
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out

def _make_layers(self, cfg):
layers = []
in_channels = 3
for x in cfg:
if x == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
nn.BatchNorm2d(x),
nn.ReLU(inplace=True)]
in_channels = x
layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
return nn.Sequential(*layers)

初始化网络,根据实际需要,修改分类层。因为 tiny-imagenet 是对200类图像分类,这里把输出修改为200。

1
2
3
4
# 网络放到GPU上
net = VGG().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

3. 网络训练

训练的代码和以前是完全一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for epoch in range(10):  # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))

print('Finished Training')

4. 测试验证准确率:

测试的代码和之前也是完全一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
correct = 0
total = 0

for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %.2f %%' % (
100 * correct / total))

准确率

可以看到,使用一个简化版的 VGG 网络,就能够显著地将准确率由 61%,提升到 83.56 %

问题总结

1. dataloader 里面 shuffle 取不同值有什么区别?

Dataloader中的shuffle参数用于指定是否在每个epoch开始时对数据进行重新洗牌。当shuffle=True时,数据将在每个epoch开始时按照随机顺序进行重新排列,这有助于增加模型的泛化能力。当shuffle=False时,数据将按照原始的顺序进行处理,这可能会在某些情况下提高模型的准确性,特别是对于那些依赖于输入数据顺序的模型。

另外,当shuffle=True时,Dataloader会在每个epoch开始时重新生成一个随机数种子,以确保每次运行时得到相同的结果。这对于需要在多个设备上并行训练时非常有用,因为它可以确保每个设备上的数据顺序是一致的。

2. transform 里,取了不同值,这个有什么区别?

在数据转换(transform)中,不同的取值可以导致不同的数据转换结果,这会对模型的训练和性能产生不同的影响。以下是一些常见的transform取值及其可能的影响:

  1. 随机旋转(Random Rotation):如果在transform中设置了随机旋转,当取不同值时,每次训练过程中数据集中的图像会被随机旋转一定的角度。这可以增加模型的鲁棒性,使其能够适应不同角度的图像。然而,如果旋转角度过大,可能会导致模型过拟合,因为模型需要学习更多的变化模式。
  2. 随机裁剪(Random Cropping):如果在transform中设置了随机裁剪,当取不同值时,每次训练过程中数据集中的图像会被随机裁剪掉一部分。这可以帮助模型捕捉到图像的局部特征,并提高模型的泛化能力。然而,如果裁剪大小过小,可能会导致模型失去一些全局信息,从而影响准确性。
  3. 数据增强(Data augmentation):如果在transform中设置了数据增强,当取不同值时,每次训练过程中数据集中的图像会被进行一些修改,例如随机翻转、旋转、缩放等。这可以大大增加模型的泛化能力,因为它能够让模型看到更多的变化模式。然而,如果增强操作过于剧烈,可能会导致模型失去对原始图像的记忆,从而影响准确性。
  4. 归一化(Normalization):如果在transform中设置了归一化操作,当取不同值时,数据集中的特征值会被映射到不同的范围内。这可以帮助模型更快地收敛并提高训练的稳定性。然而,如果归一化的范围过大或过小,可能会导致模型无法正确学习到数据的分布特征,从而影响准确性。

3. epoch 和 batch 的区别?

在深度学习中,epoch和batch是两个重要的概念,它们都与模型的训练过程相关,但具有不同的含义和作用。

  1. epoch:epoch指的是一个完整的训练过程,即使用整个训练集进行一次完整的训练。在每个epoch中,模型将从训练集的起始点开始,依次遍历每个样本,并对其进行前向传播和反向传播,以更新模型的参数。一个epoch结束后,模型将重新开始遍历整个训练集,重复这个过程,直到达到预设的迭代次数或满足收敛条件。
  2. batch:batch指的是将整个训练集分成若干个小块(即批量),每次训练时只使用其中一个批量数据进行训练。在每个batch中,模型将使用当前批量数据进行一次前向传播和反向传播,并更新模型的参数。然后,模型将移动到下一个batch,继续进行训练。在每个epoch中,模型通常会使用多个batch进行训练,以提高训练效率。

4. 1x1的卷积和 FC 有什么区别?主要起什么作用?

1x1的卷积层是一种特殊的卷积层,其卷积核的大小为1x1。它对输入数据进行线性的变换,并通过激活函数对结果进行非线性映射。1x1的卷积层在深度神经网络中的作用包括:

  1. 降维/升维:1x1的卷积层可以用于改变输入数据的通道数,从而实现降维或升维的功能。通过将通道数减少,可以减少网络的参数数量,提高计算效率;通过将通道数增加,可以增加网络的表达能力,使网络能够适应更复杂的输入数据。
  2. 跨通道信息交互:1x1的卷积层可以用于跨通道的信息交互。通过将不同通道的输入数据进行线性组合和变换,可以在不同通道之间传递信息,增强网络对输入数据的理解和表达能力。
  3. 参数共享:1x1的卷积层可以实现参数共享,即不同的神经元可以共享同一组参数。这可以有效减少网络的参数量,提高网络的泛化能力。

全连接层(FC)是一种常见的层类型,它通过将输入数据与一个权重矩阵进行乘积,并将结果进行加权求和得到输出。全连接层在深度神经网络中的作用包括:

  1. 分类/回归:全连接层通常用于网络的最后一层,用于进行分类或回归任务。它可以将网络的输出映射到类别标签或连续的数值上。
  2. 特征组合:全连接层可以用于对输入数据进行特征组合。通过将不同的特征进行线性组合和变换,可以生成新的特征表示,提高网络对输入数据的理解和表达能力。
  3. 参数调整:全连接层可以通过调整权重矩阵和偏置向量来调整网络的输出结果。这使得全连接层可以用于优化网络的性能,例如提高准确率或降低损失值。

5. residual leanring 为什么能够提升准确率?

残差学习(Residual Learning)是一种在深度神经网络中提高训练效果的方法,它通过在神经网络中引入残差块(residual block),使得网络可以学习到残差映射,即从输入到输出的差异。残差学习的主要思想是在网络中加入短路连接(shortcut connection),使得网络能够直接学习到输入和输出之间的残差映射,而不用学习复杂的非线性变换。

残差学习能够提升准确率的原因有以下几点:

  1. 缓解了梯度消失问题:在深度神经网络中,当网络层数增加时,梯度可能会逐渐消失,导致网络难以训练。残差学习通过引入短路连接,使得每一层的梯度可以直接从前一层传递到后一层,缓解了梯度消失的问题,提高了网络的训练效果。
  2. 增加了网络的非线性能力:残差学习中的残差块引入了非线性变换,使得网络能够学习更加复杂的映射关系。通过学习残差映射,网络能够更好地适应输入和输出之间的复杂关系,提高了网络的非线性能力,从而提高了网络的准确率。
  3. 增加了网络的表达能力:残差块中的参数可以通过训练进行优化,从而使得网络能够更好地适应不同的数据分布和任务需求。通过引入残差块,网络能够更好地捕捉数据的变化趋势和规律,提高了网络的表达能力,从而提高了网络的准确率。

6. 代码练习二里,网络和1989年 Lecun 提出的 LeNet 有什么区别?

  • LeNet1仅在最后一层使用了全连接层,而代码二使用了3层全连接层
  • LeNet1使用的是平均值池化,而代码二使用的是最大值池化
  • LeNet1未使用优化器,而代码二使用了Adam优化器

7. 卷积以后feature map 尺寸会变小,如何应用 Residual Learning?

  • 使用扩张卷积(dilated convolution)来保持特征图的尺寸。扩张卷积是一种在卷积操作中使用不同的扩张率(dilated rate)来扩大卷积核的间隔,从而在保持特征图尺寸的同时增加特征图的感受野大小。
  • 使用1×1的卷积核

8. 有什么方法可以进一步提升准确率?

  • 通过数据增强(data augmentation)技术生成更多的训练样本,例如随机旋转、翻转、缩放等操作。
  • 使用更深、更宽的网络结构
  • 使用非线性模块,例如批归一化(Batch Normalization)、残差连接(Residual Connection)
  • 使用注意力机制 [1706.03762] Attention Is All You Need (arxiv.org)
  • 利用预训练模型

23暑假深度学习week2
https://jialiangz.github.io/2023/07/18/23暑假深度学习week2/
作者
爱吃菠萝
发布于
2023年7月18日
更新于
2023年7月29日
许可协议