【深度学习】卷积神经网络(CNN)详解

机器学习初学者

共 11970字,需浏览 24分钟

 · 2021-08-11

章节
  • Filter

  • 池化

  • Demo

  • 冷知识

  • 参考


CNN 一共分为输入,卷积,池化,拉直,softmax,输出


卷积由互关运算(用Filter完成)和激活函数


Filter


CNN常用于图像识别,在深度学习中我们不可能直接将图片输入进去,向量是机器学习的通行证,我们将图片转换为像素矩阵再送进去,对于黑白的图片,每一个点只有一个像素值,若为彩色的,每一个点会有三个像素值(RGB)


互关运算其实就是做矩阵点乘运算,用下面的Toy Example说明:其实就是用kernel(filter)来与像素矩阵局部做乘积,如下图,output的第一个阴影值其实是input和kernel的阴影部分进行矩阵乘法所得



接下来引入一个参数(Stride),代表我们每一次滤波器在像素矩阵上移动的步幅,步幅共分为水平步幅和垂直步幅,下图为水平步幅为2,垂直步幅为3的设置



所以filter就不断滑过图片,所到之处做点积,那么,做完点积之后的shape是多少呢?假设input shape是32 * 32,stride 为1,filter shape 为4 * 4,那么结束后的shape为29 * 29,计算公式是((input shape - filter shape) / stride ) + 1,记住在深度学习中务必要掌握每一层的输入输出。


那么,假如stride改为3,那么((32 - 4) / 3) + 1 不是整数,所以这样的设定是错误的,那么,我们可以通过padding的方式填充input shape,用0去填充,这里padding设为1,如下图,填充意味着输入的宽和高都会进行增加2 * 1,那么接下来的out shape 就是 ((32 + 2 * 1 - 4)/3) + 1,即为11 * 11



接下来引入通道(channel),或为深度(depth)的介绍,一张彩色照片的深度为3,每一个像素点由3个值组成,我们的filter的输入通道或者说是深度应该和输入的一致,举例来说,一张照片32 * 32 * 3,filter可以设置为3 * 3 * 3,我们刚开始理解了一维的互关运算,三维无非就是filter拿出每一层和输入的每一层做运算,最后再组成一个深度为3的输出,这里stride设置为1,padding也为1,所以输出的shape为30 * 30 * 3。


卷积的时候是用多个filter完成的,一般经过卷积之后的output shape 的输入通道(深度)为filter的数量,下图为输入深度为2的操作,会发现一个filter的输出最终会相加,将它的深度压为1,而不是一开始的输入通道。这是一个filter,多个filter最后放在一起,最后的深度就是filter的数量了。



Q & A:


1.卷积的意义是什么呢?


其实如果用图片处理上的专业术语,被叫做锐化,卷积其实强调某些特征,然后将特征强化后提取出来,不同的卷积核关注图片上不同的特征,比如有的更关注边缘而有的更关注中心地带等等,如下图:



当完成几个卷积层后(卷积 + 激活函数 + 池化):



可以看出,一开始提取一些比较基础简单的特征,比如边角,后面会越来越关注某个局部比如头部甚至是整体


2.如何使得不同的卷积核关注不同的地方?


设置filter矩阵的值,比如input shape是4 * 4的,filter是2 * 2,filter是以一个一个小区域为单位,如果说我们想要关注每一个小区域的左上角,那么将filter矩阵的第一个值设为1,其他全为0即可


总结来说,就是通过不断改变filter矩阵的值来关注不同的细节,提取不同的特征


3.filter矩阵里的权重参数是怎么来的?


首先会初始化权重参数,然后通过梯度下降不断降低loss来获得最好的权重参数


4.常见参数的默认设置有哪些?


一般filter的数量(output channels)通常可以设置为2的指数次,如32,64,128,512,这里提供一组比较稳定的搭配(具体还得看任务而定),F(kernel_size/filter_size)= 3,stride = 1,padding = 1;F = 5,stride = 1,Padding = 2;F = 1,S = 1,P = 0


5.参数数量?


举例来说,filter的shape为5 * 5 * 3 ,一共6个,stride设置为1,padding设为2,卷积层为(32 * 32 * 6),注意卷积层这里是代表最后的输出shape,输入shape为 32 * 32 * 3,那么所需要的参数数量为 6 * (5 * 5 * 3 + 1),里面 +1 的原因是原因是做完点积运算之后会加偏置(bias),当然这个参数是可以设置为没有的


6.1 x 1 卷积的意义是什么?


filter的shape为1 x 1,stride = 1,padding = 0,假如input为32 * 32 * 3,那么output shape = (32 - 1) / 1 + 1 = 32,换言之,它并没有改变原来的shape,但是filter的数量可以决定输出通道,所以,1 x 1的卷积目的是改变输出通道。可以对输出通道进行升维或者降维,降维之后乘上的参数数量会减少,训练会更快,内存占用会更少。升维或降维的技术在ResNet中同样运用到啦(右图):



另外,其实1 x 1的卷积不过是实现多通道之间的线性叠加,如果你还记得上面多通道的意思,1 x 1 卷积改变卷积核的数量,无非就是使得不同的feature map进行线性叠加而已(feature map指的是最后输出的每一层叠加出来的),因为通道的数量可以随时改变,1 x 1卷积也可以有跨通道信息交流的内涵。


池化


卷积好之后会用RELU进行激活,当然,这并不会改变原来的shape,这样可以增加模型的非线性兼容性,如果模型是线性的,很容易出问题,如XOR问题,接下来进行池化操作(Pooling),常见的是MaxPooling(最大池化),它基本上长得跟filter一样,只不过功能是选出区域内的最大值。假如我们的shape是4 * 4 ,池化矩阵的shape是2 * 2,那么池化后的shape是2 * 2(4 / 2)


那么,池化的意义是什么?池化又可以被成为向下取样(DownSample),经过池化之后shape会减小不少,如果说卷积的意义是提取出特征,那么,池化的意义是在这些特征中取出最有代表性的特征,这样可以降低像素的重复性,使得后续的卷积更有意义,同时可以降低shape,使得计算更为方便


当然,也还有平均池化(AveragePooling),这样做试图包含区域内的所有的特征,那么,如果图片相邻色素重复很多,那么最大池化是不错的,如果说一张图片很多不同的特征需要关注,那么可以考虑平均池化


补充一下,可以给上述池操作加一个Global,这就意味着全局,而不是一个一个的小区域


Demo


!!!我的PyTorch完整Demo在:

https://colab.research.google.com/drive/1XMlSmiZ4FjHohptX-GSHsT_CFs4EoE6f?usp=sharing


进行卷积池化这样一组操作多次之后再全部拉直送入全连接网络,最后输出10个值,然后优化它们与真实标签的交叉熵损失,接下来用PyTorch和TensorFlow实操一下


首先先搭建一个简单的PyTorch网络,这里采用Sequential容器写法,当然也可以按照普遍的self.conv1 = ...,按照Sequential写法更加简洁明了,后面前向传播函数也没有采取x = ...不断更新x,而是直接放进layer,遍历每一层即可,简洁干净


# 导入库import torchfrom torch import nnimport torchvisionfrom torchvision import datasets,transformsimport torch.nn.functional as Fimport matplotlib.pyplot as plt


class Net(nn.Module):   def __init__(self):      super().__init__()      self.layer = nn.Sequential(                   nn.Conv2d(in_channels=1,out_channels=32,kernel_size=3),nn.ReLU(),                   nn.MaxPool2d(kernel_size=2),                   nn.Conv2d(32,64,2),nn.ReLU(),                   nn.MaxPool2d(2,2),                   nn.Flatten(),                   nn.Linear(64 * 6 * 6,10),nn.Softmax(),                   )
def forward(self,x): x = self.layer(x) return x


PyTorch中输入必须为(1,1,28,28),这里比tensorflow多了一个1,原因是Torch中有一个group参数,默认为1,所以可以不设置,如果为N,就会把输入分为N个小部分,每一个部分进行卷积,最后再将结果拼接起来


搭建好网络之后,建议先检验一下网络和优化器参数


# 如果GPU没有就会调到CPUdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = Net().to(device)print(model.parameters)# 训练时还需要优化器(Optimizer)optimizer = torch.optim.Adam(model.parameters())print(optimizer)



接下来定义训练和测试函数,先介绍几个小知识


model.train() # 启用BatchNormalization和Dropoutmodel.eval() # 因为是测试,所以取消两者


i = torch.tensor([    [1,2,3],    [4,5,6]])# 输出最大的值和它的索引print(i.max(1,keepdim=True))# torch.return_types.max(values=tensor([[3],[6]]), indices=tensor([[2],[2]]))# 一般只要索引的话:print(i.max(1,keepdim=True))[1]# tensor([[2],#         [2]])


a = torch.tensor([1,2,3,4])b = torch.tensor([[1],                  [-1],                  [-2],                  2])# 将a转换为与b形状相同a.view_as(b)print(a)# tensor([[1],#        [2],#        [3],#        [4]])
# 相对于numpy的equal函数,判断tensor里每一个值是否相等# 输出为True 或者 Falseprint(b.eq(a.view_as(b)))
# tensor([[ True],# [False],# [False],# [False]])
# 求和用来判断损失和准确率# True --> 1,False --> 0print(b.eq(a.view_as(b)).sum())
# tensor(1)
# 最后将PyTorch的tensor转换为Python中标准值print(b.eq(a.view_as(b)).sum().item())# 1


# 下载训练和测试数据集# transforms函数可以对下载的数据做一些预处理# Compose 指的是将多个transforms操作组合在一起# ToTensor 是将[0,255] 范围 转换为[0,1]# 灰度图片(channel=1),所以每一个括号内只有一个值,前者代表mean,后者std(标准差)# 彩色图片(channel=3),所以每一个括号内有三个值,如# transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))transform = transforms.Compose([    transforms.ToTensor(),    transforms.Normalize((0.5,),(0.5,))])
data_train = datasets.MNIST(root="填自己的主路径", transform=transform, train=True, download=True)
data_test = datasets.MNIST(root="填自己的主路径", transform=transform, train=False)


# 加载数据集# Load Datatrain_loader = torch.utils.data.DataLoader(dataset=data_train,                                                batch_size=64,                                                shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=data_test, batch_size=64, shuffle=True)


每一次新的batch中都需要梯度清零,否则的话梯度就会跨batch


def train(model,device,train_loader,optimizer,epoch):    model.train()    for batch_idx,(data,target) in enumerate(train_loader):        data,target = data.to(device),target.to(device)        optimizer.zero_grad() # 梯度清零        output = model(data)        loss = F.nll_loss(output,target) # negative likelihood loss        loss.backward() # 误差反向传播        optimizer.step() # 参数更新        if (batch_idx + 1) % 200 == 0:            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(                epoch, batch_idx * len(data), len(train_loader.dataset),                100. * batch_idx / len(train_loader), loss.item())) # .item()转换为python值    return loss.item()


因为测试的时候不需要更新参数,所以with torch.no_grad()


# 定义测试函数def test(model, device, test_loader):    model.eval()    test_loss,correct = 0 , 0    with torch.no_grad(): # 不track梯度        for data, target in test_loader:            data, target = data.to(device), target.to(device)            output = model(data)            test_loss += F.nll_loss(output, target, reduction = 'sum') # 将一批的损失相加            pred = output.max(1, keepdim = True)[1] # 找到概率最大的下标            correct += pred.eq(target.view_as(pred)).sum().item() # equals    test_loss /= len(test_loader.dataset)    acc = correct / len(test_loader.dataset)    print("\nTest set: Average loss: {:.4f}, Accuracy: {} ({:.0f}%) \n".format(        test_loss, acc ,        100.* correct / len(test_loader.dataset)            ))
return acc


接下来定义可视化函数


def visualize(lis,epoch,*label):    plt.xlabel("epochs")    plt.ylabel(label)    plt.plot(epoch,lis)    plt.show()


最后进行训练和测试


BATCH_SIZE = 512 # 大概需要2G的显存EPOCHS = 20 # 总共训练批次DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") 


train_ = []test_acc = []for epoch in range(1,EPOCHS+1):    train_loss = train(model,DEVICE,train_loader,optimizer,epoch)    acc = test(model,DEVICE,test_loader)    train_.append(train_loss)    test_acc.append(acc)
visualize(train_,[i for i in range(20)],"loss")visualize(test_acc,[i for i in range(20)],"accuracy")



接下来使用tensorflow-gpu 1.14.0再实操一下


from tensorflow import kerasimport numpy as npimport matplotlib.pyplot as pltimport tensorflow as tfimport matplotlib as mplimport sys 
# solve could not create cudnn handle: CUDNN_STATUS_INTERNAL_ERRORconfig = tf.compat.v1.ConfigProto()config.gpu_options.allow_growth = Truesession = tf.compat.v1.InteractiveSession(config=config)
# 不同库版本,使用此代码块查看print(sys.version_info)for module in mpl,np,tf,keras: print(module.__name__,module.__version__)
'''sys.version_info(major=3, minor=6, micro=9, releaselevel='final', serial=0)matplotlib 3.3.4numpy 1.16.0tensorflow 1.14.0tensorflow.python.keras.api._v1.keras 2.2.4-tf'''# If you get numpy futurewarning,then try numpy 1.16.0
# load train and test(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
# Scale images to the [0, 1] rangex_train = x_train.astype("float32") / 255x_test = x_test.astype("float32") / 255
# 1 Byte = 8 Bits,2^8 -1 = 255。[0,255]代表图上的像素,同时除以一个常数进行归一化。1 就代表全部涂黑。0 就代表没涂
# Make sure images have shape (28, 28, 1)x_train = np.expand_dims(x_train, -1)x_test = np.expand_dims(x_test, -1)
# CNN 的输入方式必须得带上channel,这里扩充一下维度
# convert class vectors to binary class matricesy_train = keras.utils.to_categorical(y_train, 10)y_test = keras.utils.to_categorical(y_test, 10)
# y 属于 [0,9]代表手写数字的标签,这里将它转换为0-1表示,可以类比one-hot,举个例子,如果是2
# [[0,0,1,0,0,0,0,0,0,0]……]
model = keras.Sequential( [ keras.Input(shape=(28, 28, 1)), keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation="relu"), keras.layers.MaxPooling2D(pool_size=(2, 2)), keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation="relu"), keras.layers.MaxPooling2D(pool_size=(2, 2)), keras.layers.Flatten(), keras.layers.Dense(units=10, activation="softmax"), ])
# 注意,Conv2D里面有激活函数不代表在卷积和池化的时候进行。而是在DNN里进行,最后拉直后直接接softmax就行

# kernel_size 代表滤波器的大小,pool_size 代表池化的滤波器的大小
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
model.summary()
history = model.fit(x_train, y_train, batch_size=128, epochs=15, validation_split=0.1) #10层交叉检验score = model.evaluate(x_test, y_test)print("Test loss:", score[0])print("Test accuracy:", score[1])
# Test loss: 0.03664601594209671# Test accuracy: 0.989300012588501
# visualize accuracy and lossdef plot_(history,label): plt.plot(history.history[label]) plt.plot(history.history["val_" + label]) plt.title("model " + label) plt.ylabel(label) plt.xlabel("epoch") plt.legend(["train","test"],loc = "upper left") plt.show()
plot_(history,"acc")plot_(history,"loss")


在机器学习中画精确度和loss的图很有必要,这样可以发现自己的代码中是否存在问题,并且将这个问题可视化




冷知识


We don't minimize total loss to find the best function.


我们采取将数据打乱并分组成一个一个的mini-batch,每个数据所含的数据个数也是可调的。关于epoch



将一个mini-batch中的loss全部加起来,就更新一次参数。一个epoch就等于将所有的mini-batch都遍历一遍,并且经过一个就更新一次参数。


如果epoch设为20,就将上述过程重复20遍


这里再细谈一下batch 和 epoch



由图可知,当batch数目越多,分的越开,每一个epoch的速度理所应当就会**上升**的,当batch_size = 1的时候,1 epoch 就更新参数50000次 和 batch_size = 10的时候,1 epoch就更新5000次,那么如果更新次数相等的话,batch_size = 1会花**166s**;batch_size = 10每个epoch会花**17s**,总的时间就是**17 * 10 = 170s**。其实batch_size = 1不就是[SGD](../optimization/GD.md)。随机化很不稳定,相对而言,batch_size = 10,收敛的会更稳定,时间和等于1的差不多。那么何乐而不为呢?


肯定有人要问了?随机速度快可以理解,看一眼就更新一次参数



为什么batch_size = 10速度和它差不多呢?按照上面来想,应该是一个mini-batch结束再来下一个,这样慢慢进行下去,其实没理由啊。

接下来以batch_size = 2来介绍一下



学过线性代数应该明白,可以将同维度的向量拼成矩阵,来进行矩阵运算,这样每一个mini-batch都在同一时间计算出来,即为平行运算

所有平行运算GPU都能进行加速。

那么,好奇的是到底计算机看到了什么?是一个一个的数字吗?





其实这件事情很反直觉,原以为计算机是看一张一张的图片,可是这个很难看出是单个数字而是数字集,那么我们试试看最大化像素



其实左下角的6其实蛮像的耶。


参考


https://zh-v2.d2l.ai/

https://demo.leemeng.tw/

http://cs231n.stanford.edu/

https://www.youtube.com/watch?v=FrKWiRv254g&list=PLJV_el3uVTsPy9oCRY30oBPNLCo89yu49&index=19




往期精彩回顾




本站qq群851320808,加入微信群请扫码:
浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报