为什么要使用卷积

神经网络遇到的问题 1

  • 参数过多内存装不下
    举例:

    • 图像大小 1000*1000
      一层神经元数目为 10^6

    • 全连接参数为 10001000*10^ 6=10^12
      一层就是 1 万亿个参数, 内存装不下

神经网络遇到的问题 2

  • 参数过多容易过拟合
    ◆计算资源不足
    ◆容易过拟合,发生过拟合, 我们就需要更多训练数据, 但是很多时候我们没有更多的数据, 因为获取数据需要成本

为什么我们可以这么做(卷积为什么有作用)呢?

  • 局部连接
    图像的区域性—爱因斯坦的嘴唇附近的色彩等是相似的
  • 参数共享与平移不变性
    图像特征与位置无关—左边是脸, 右边也是脸, 这样无论脸放在什么地方都检查出来,刚好可以解决过拟合的问题(否则脸放到其他地方就检测不出来)

参数共享与平移不变性
可以参考下面链接—重点看
https://blog.csdn.net/weixin_44177568/article/details/102812050?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~first_rank_v2~rank_v25-3-102812050.nonecase&utm_term=%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%81%9A%E5%8D%B7%E7%A7%AF
https://zhuanlan.zhihu.com/p/41696749

卷积操作

具体我们看下如何做卷积, 其实是一个把卷积核和它相对应部分做乘法, 然后再求和

图卷积核重合区域内相对应的每一个像素值乘卷积核 、 内相对应点的权重, 然后求和, 再加上偏置后, 最后得到输出图片中的一个像素值

卷积核的值就是w权重,我们通过训练得到。初始化权值为:nn.init.xavier_normal(**格罗特初始化随机权值)或者init.kaiming.uniform_(self.weight, a=math.sqrt(5))**

总结:!! 卷积输出图像尺寸 1+(n-k)//s(s 是步长, n 是输入尺寸, k 是卷积核)

padding

那有没有操作让大小保持不变呢?采用如下 padding 手法即可:

提取图像的高阶特征:因为padding后图像大小虽然没变,但是提取到的都是图像的高阶特征

卷积神经网络结构

◆卷积神经网络
◆(卷积层+(可选)池化层)N+全连接层M
N>=1,M>=0
卷积层的输入和输出都是矩阵, 全连接层的输入和输出都是向量

矩阵和向量的区分, 一维就是向量, 二维就是矩阵
https://blog.csdn.net/liuxiangxxl/article/details/82253497

在最后一层的卷积上, 把它做一个展平, 这样就可以和全连接层进行运算了为什么卷积要放到前面, 因为展平丧失了维度信息, 因此全连接层后面不能再放卷积层

卷积——多通道(channels)

面的卷积都是单通道的, 多通道, 比如 rgb 三色, 我们相当于有一个厚度, 分别去处理, 然后再加起来, 最后就变为 1 层了

卷积——多个卷积核(channels)

多个卷积核可以从不同角度提取学习图像的高阶特征

池化(pooling)

池化核:没有参数,一般步长是2,移动不到的部分则丢弃。选择核中最大或者均值

池化作用: 池化函数使用某一位置的相邻输出的总体统计特征来代替网络在该位置的输出。本质是 降采样可以大幅减少网络的参数量卷积是获取高阶特征,池化才是降采样

池化技术的本质: 在尽可能保留图片空间信息的前提下, 降低图片的尺寸, 增大卷积核感受视野, 提取高层特征, 同时减少网络参数量, 预防过拟合

简单来说: 等比例缩小图片, 图片的主体内容丢失不多, 依然具有平移, 旋转,尺度的不变性, 简单来说就是图片的主体内容依旧保存着原来大部分的空间信息

最大值池化

能够抑制网络参数误差造成的估计均值偏移的现象

平均值池化

主要用来抑制邻域值之间差别过大,造成的方差过大。

特点

​ ◆ 常使用不重叠(池化核的大小与步长相等) 、 不补零(padding 为 valid)
​ ◆ 没有用于求导的参数(没有需要训练的参数, 参数个数为 0)
​ ◆ 池化层的超参数为步长和池化核大小
​ ◆ 用于减少图像尺寸,从而减少计算量
​ ◆ 一定程度平移鲁棒
​ 比如一只猫移动了一个像素的另外一张图片, 我们先做池化, 再做卷积, 那么最终还是可以识别这个猫
​ ◆ 损失了空间位置精度

代码:

  • 每次图像尺寸变小,需要更多的卷积核来学习高阶特征,所以pool变小一倍以后,卷积核数量翻一倍
  • MaxPool2d(2,2) #池化核大小为(2*2)步长为2
  • 卷积核都为奇数的nxn,没有偶数的
  • 每个卷积核都有一个偏置bias
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
class CNN(nn.Module):
def __init__(self, activation="relu"):
super(CNN, self).__init__()
self.activation = F.relu if activation == "relu" else F.selu
#输入通道数,图片是灰度图,所以是1,图片是彩色图,就是3,输出通道数,就是卷积核的个数(32,1,28,28)
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
#输入x(32,32,28,28)输出x(32,32,28,28)
self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2) #池化核大小为(2*2)步长为2
self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
self.conv4 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1)
self.conv5 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1)
self.conv6 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1)
self.flatten = nn.Flatten()
# input shape is (28, 28, 1) so the fc1 layer in_features is 128 * 3 * 3
self.fc1 = nn.Linear(128 * 3 * 3, 128)
self.fc2 = nn.Linear(128, 10)

self.init_weights()

def init_weights(self):
"""使用 xavier 均匀分布来初始化全连接层、卷积层的权重 W"""
for m in self.modules():
if isinstance(m, (nn.Linear, nn.Conv2d)):
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)


def forward(self, x):
act = self.activation
x = self.pool(act(self.conv2(act(self.conv1(x))))) # 1 * 28 * 28 -> 32 * 14 * 14
x = self.pool(act(self.conv4(act(self.conv3(x))))) # 32 * 14 * 14 -> 64 * 7 * 7
x = self.pool(act(self.conv6(act(self.conv5(x))))) # 64 * 7 * 7 -> 128 * 3 * 3
x = self.flatten(x) # 128 * 3 * 3 ->1152 展平
x = act(self.fc1(x)) # 1152 -> 128
x = self.fc2(x) # 128 -> 10
return x

for idx, (key, value) in enumerate(CNN().named_parameters()):
print(f"{key}\tparamerters num: {np.prod(value.shape)}") # 打印模型的参数信息

参数量计算:

1
2
3
4
5
6
7
8
9
10
11
print(f'conv1 - {1*3*3*32}') # 32个卷积核,每个卷积核大小为1*3*3
print(f'conv2 - {32*3*3*32}') # 32个卷积核,每个卷积核大小为32*3*3
print(f'conv3 - {32*3*3*64}')
print(f'conv4 - {64*3*3*64}')
print(f'conv5 - {64*3*3*128}')
print(f'conv6 - {128*3*3*128}')
print(f'fc1 - {1152*128}')
print(f'fc2 - {128*10}')

#对上面求和,总参数数目为:
1*3*3*32 +32+ 32*3*3*32 +32+ 32*3*3*64 +64+ 64*3*3*64 +64+ 64*3*3*128 +128+ 128*3*3*128 + 128*3*3*128 +128+ 128*10 = 434720
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 训练
def training(
model,
train_loader,
val_loader,
epoch,
loss_fct,
optimizer,
tensorboard_callback=None,
save_ckpt_callback=None,
early_stop_callback=None,
eval_step=500,
):
record_dict = {
"train": [],
"val": []
}

global_step = 0
model.train()
with tqdm(total=epoch * len(train_loader)) as pbar:
for epoch_id in range(epoch):
# training
for datas, labels in train_loader:
datas = datas.to(device)
labels = labels.to(device)
# 梯度清空
optimizer.zero_grad()
# 模型前向计算
logits = model(datas)
# 计算损失
loss = loss_fct(logits, labels)
# 梯度回传
loss.backward()
# 调整优化器,包括学习率的变动等
optimizer.step()
preds = logits.argmax(axis=-1)

acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())
loss = loss.cpu().item()
# record

record_dict["train"].append({
"loss": loss, "acc": acc, "step": global_step
})

# evaluating
if global_step % eval_step == 0:
model.eval()
val_loss, val_acc = evaluating(model, val_loader, loss_fct)
record_dict["val"].append({
"loss": val_loss, "acc": val_acc, "step": global_step
})
model.train()

# 1. 使用 tensorboard 可视化
if tensorboard_callback is not None:
tensorboard_callback(
global_step,
loss=loss, val_loss=val_loss,
acc=acc, val_acc=val_acc,
lr=optimizer.param_groups[0]["lr"],
)

# 2. 保存模型权重 save model checkpoint
if save_ckpt_callback is not None:
save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)

# 3. 早停 Early Stop
if early_stop_callback is not None:
early_stop_callback(val_acc)
if early_stop_callback.early_stop:
print(f"Early stop at epoch {epoch_id} / global_step {global_step}")
return record_dict

# udate step
global_step += 1
pbar.update(1)
pbar.set_postfix({"epoch": epoch_id})

return record_dict


epoch = 20

activation = "relu"
model = CNN(activation)

# 1. 定义损失函数 采用交叉熵损失
loss_fct = nn.CrossEntropyLoss()
# 2. 定义优化器 采用SGD
# Optimizers specified in the torch.optim package
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

# 1. tensorboard 可视化
if not os.path.exists("runs"):
os.mkdir("runs")
tensorboard_callback = TensorBoardCallback(f"runs/cnn-{activation}")
tensorboard_callback.draw_model(model, [1, 1, 28, 28])
# 2. save best
if not os.path.exists("checkpoints"):
os.makedirs("checkpoints")
save_ckpt_callback = SaveCheckpointsCallback(f"checkpoints/cnn-{activation}", save_best_only=True)
# 3. early stop
early_stop_callback = EarlyStopCallback(patience=10)


视野域

经过两层3x3之后一个视野域相当于一个5x5的视野域

一层5x5的卷积核有25个参数,而两层3x3 有 9+9 = 18个参数。同样视野域下参数更少

激活函数

从relu换成selu后,能有好的效果提升,因为selu能在一定程度上缓解梯度消失和梯度爆炸的问题。

10-Monkeys

数据预处理

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
from torchvision import datasets
from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize


from pathlib import Path

DATA_DIR = Path("./archive/")

class MonkeyDataset(datasets.ImageFolder):
def __init__(self, mode, transform=None):
if mode == "train":
root = DATA_DIR / "training"
elif mode == "val":
root = DATA_DIR / "validation"
else:
raise ValueError("mode should be one of the following: train, val, but got {}".format(mode))
super().__init__(root, transform) # 调用父类init方法 root:路径,transform:图片处理方法
self.imgs = self.samples # self.samples里边是图片路径及标签[(path,label),(path,label),...]
self.targets = [s[1] for s in self.samples] # 标签取出来

# 预先设定的图片尺寸
img_h, img_w = 128, 128
transform = Compose([
Resize((img_h, img_w)), # 图片缩放
ToTensor(),
# 预先统计的
Normalize([0.4363, 0.4328, 0.3291], [0.2085, 0.2032, 0.1988]),
ConvertImageDtype(torch.float), # 转换为float类型
]) #数据预处理


train_ds = MonkeyDataset("train", transform=transform)
val_ds = MonkeyDataset("val", transform=transform)

print("load {} images from training dataset".format(len(train_ds)))
print("load {} images from validation dataset".format(len(val_ds)))

import torch.nn as nn
from torch.utils.data.dataloader import DataLoader

batch_size = 64
#从数据集到dataloader,num_workers参数不能加,否则会报错
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)
  • 10-Monkeys:不同类别的图片放在不同的文件夹,继承自datasets.ImageFolder
  • Resize((img_h, img_w)):图片缩放,将图片尺寸进行调整,可能像素会改变(比如用均值拟合),但整体不会改变。尺寸需要统一,不能尺寸变化
  • Tran_ds.classes:[‘n0’,’n1’’n2’,’n3’,’n4’,’n5’,’nó’,’n7’,’n8’,’n9’]
  • Tran_ds.class_to_idx:{..’n2’:2,’n3’:3,’n4’:4,’n5’: 5,’n6’:6,.’n7’:7 …’n9’:9}
  • Normalize([0.4363, 0.4328, 0.3291], [0.2085, 0.2032, 0.1988]):三通道的均值和方差(均值为0,方差为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
31
32
33
34
35
36
37
38
39
40

class CNN(nn.Module):
def __init__(self, num_classes=10, activation="relu"):
super(CNN, self).__init__()
self.activation = F.relu if activation == "relu" else F.selu
self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding="same")
self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, padding="same")
self.pool = nn.MaxPool2d(2, 2)
self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding="same")
self.conv4 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding="same")
self.conv5 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding="same")
self.conv6 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding="same")
self.flatten = nn.Flatten()
# input shape is (3, 128, 128) so the flatten output shape is 128 * 16 * 16
self.fc1 = nn.Linear(128 * 16 * 16, 128)
self.fc2 = nn.Linear(128, num_classes)

self.init_weights()

def init_weights(self):
"""使用 xavier 均匀分布来初始化全连接层、卷积层的权重 W"""
for m in self.modules():
if isinstance(m, (nn.Linear, nn.Conv2d)):
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)

def forward(self, x):
act = self.activation
x = self.pool(act(self.conv2(act(self.conv1(x)))))
x = self.pool(act(self.conv4(act(self.conv3(x)))))
x = self.pool(act(self.conv6(act(self.conv5(x)))))
x = self.flatten(x)
x = act(self.fc1(x))
x = self.fc2(x)
return x


for idx, (key, value) in enumerate(CNN().named_parameters()):
print(f"{key}\tparamerters num: {np.prod(value.shape)}")

  • padding=”same”:保证步长为1的时候,输入输出的维度一致