卷积神经网络是当前最热门的技术,我想深入地学习这门技术,从他的发展历史开始,了解神经网络算法的兴衰起伏;同时了解他在发展过程中的**里程碑式算法
**,能更好的把握神经网络发展的未来趋势,了解神经网络的特征。
之前的LeNet为以后的神经网络模型打下了一个基础的框架,真正让神经网络模型在外界广泛引起关注的还是AlexNet,在AlexNet之后也出现了相应对他的改进,或多或少会有一些效果。但是ZFNet是在AlexNet上的改进,他的论文对神经网络的各个层级的作用,做了十分详细的阐述,为如何优化模型,怎样“有理有据”地调节参数指出了一个方向,这是他最大的一个贡献。VggNet也是在2014年的ImageNet的定位赛和分类赛上获得了第一名和第二名,他在原来卷积神经网络结构的基础上,大大增加了网络的深度,最后取得了不错的成绩。
VggNet论文原文下载地址
VggNet论文中文翻译
VGG Net由牛津大学的视觉几何组(Visual Geometry Group)和 Google DeepMind公司的研究员一起研发的的深度卷积神经网络,在 ILSVRC 2014 上取得了第二名的成绩,将 Top-5错误率降到7.3%。它主要的贡献是展示出网络的深度(depth)是算法优良性能的关键部分。目前使用比较多的网络结构主要有ResNet(152-1000层),GooleNet(22层),VGGNet(19层),大多数模型都是基于这几个模型上改进,采用新的优化算法,多模型融合等。到目前为止,VGG Net 依然经常被用来提取图像特征。
注:
ImageNet是一个在2009年创建的图像数据集,从2010年开始到2017年举办了七届的ImageNet 挑战赛——ImageNet Large Scale Visual Recognition ChallengeI (LSVRC),在这个挑战赛上诞生了AlexNet、ZFNet、OverFeat、VGG、Inception、ResNet、WideResNet、FractalNet、DenseNet、ResNeXt、DPN、SENet 等经典模型。
VGG 的结构与 AlexNet 类似,区别是深度更深,但形式上更加简单。VGG由5层卷积层、3层全连接层、1层softmax输出层构成,层与层之间使用maxpool(最大化池)分开,所有隐藏层的激活单元都采用ReLU函数。作者在原论文中,根据卷积层不同的子层数量,设计了A、A-LRN、B、C、D、E这6种网络结构。
VGG16总共包含16个子层,第1层卷积层由2个conv3-64组成,第2层卷积层由2个conv3-128组成,第3层卷积层由3个conv3-256组成,第4层卷积层由3个conv3-512组成,第5层卷积层由3个conv3-512组成,然后是2个FC4096,1个FC1000。总共16层,这也就是VGG16名字的由来。
VGG输入图片的尺寸是224x224x3。
第1层卷积层由2个conv3-64组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化。
卷积:输入是224x224x3,使用64个3x3x3的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(224+2 x 1-3)/1+1=224
得到输出是224x224x64。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是224x224x64,使用64个3x3x64的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(224+2 x 1-3)/1+1=224
得到输出是224 x 224 x 64。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2 x 2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(224+2 x 0-2)/2+1=112
每组得到的输出为112 x 112 x 64。
第2层卷积层由2个conv3-128组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化。
卷积:输入是112x112x64,使用128个3x3x64的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(112+2 x 1-3)/1+1=112
得到输出是112x112x128。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是112x112x128,使用128个3x3x128的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(112+2 x 1-3)/1+1=112
得到输出是112x112x128。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2 x 2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(112+2*0-2)/2+1=56
每组得到的输出为56 x 56 x 128。
第3层卷积层由3个conv3-256组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化。
卷积:输入是56x56x128,使用256个3x3x128的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(56+2 x 1-3)/1+1=56
得到输出是56x56x256。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是56 x 56 x 256,使用256个3 x 3 x 256的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(56+2 x 1-3)/1+1=56
得到输出是56x56x256。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2x2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(56+2 x 0-2)/2+1=28
每组得到的输出为28 x 28 x 256。
第4层卷积层由3个conv3-512组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化。
**卷积:**输入是28x28x256,使用512个3x3x256的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(28+2 x 1-3)/1+1=28
得到输出是28x28x512。
**ReLU:**将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是28x28x512,使用512个3x3x512的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(28+2 x 1-3)/1+1=28
得到输出是28x28x512。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2x2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(28+2 x 0-2)/2+1=14
每组得到的输出为14x14x512。
第5层卷积层由3个conv3-512组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化。
卷积:输入是14x14x512,使用512个3x3x512的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(14+2x1-3)/1+1=14
得到输出是14x14x512。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是14x14x512,使用512个3x3x512的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(14+2x1-3)/1+1=14
得到输出是14x14x512。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2x2,stride=2的池化单元进行最大池化操作(max pooling)。
根据公式:(14+2x0-2)/2+1=7
每组得到的输出为7x7x512。
第1层全连接层FC4096由4096个神经元组成。
该层的处理流程是:FC–>ReLU–>Dropout。
**FC:**输入是7x7x512的FeatureMap,展开为77512的一维向量,即77512个神经元,输出为4096个神经元。
**ReLU:**这4096个神经元的运算结果通过ReLU激活函数中。
Dropout:随机的断开全连接层某些神经元的连接,通过不激活某些神经元的方式防止过拟合。
第2层全连接层FC4096由4096个神经元组成。
该层的处理流程是:FC–>ReLU–>Dropout。
**FC:**输入是4096个神经元,输出为4096个神经元。
ReLU:这4096个神经元的运算结果通过ReLU激活函数中。
**Dropout:**随机的断开全连接层某些神经元的连接,通过不激活某些神经元的方式防止过拟合。
第3层全连接层FC1000由1000个神经元组成,对应ImageNet数据集的1000个类别。
该层的处理流程是:FC。
FC:输入是4096个神经元,输出为1000个神经元。
该层的流程为:Softmax
Softmax:这1000个神经元的运算结果通过Softmax函数中,输出1000个类别对应的预测概率值。
VGGNet的结构十分简洁,由5个卷积层、3个全连接层和1个softmax层构成,层与层之间使用最大池化连接,隐藏层之间使用的激活函数全都是ReLU。并且网络的参数也是整齐划一的,赏心悦目。
VGGNet使用含有多个小型的3×3卷积核的卷积层来代替卷积核较大的卷积层。2个3×3的卷积核堆叠的感受野相当于一个5×5的卷积核的感受野,而3个3×3的卷积核堆叠的感受野则相当于一个7×7的卷积核的感受野。因此,采用多个小型卷积核,既能减少参数的数量,又能增强网络的非线性映射从而提升网络的表达能力。
为什么可以增加网络的非线性?我们知道激活函数的作用就是给神经网络增加非线性因素,使其可以拟合任意的函数,每个卷积操作后都会通过ReLU激活,ReLU函数就是一个非线性函数。下图展示了为什么使用2个3x3的卷积核可以代替5×5卷积核。
总结一下,使用多个3×3卷积堆叠的作用有两个:一是在不影响感受野的前提下减少了参数;二是增加了网络的非线性。
与AlexNet相比,VGGNet在池化层全部采用的是2×2的小滤波器。
VGGNet的第一层有64个通道,后面的每一层都对通道进行了翻倍,最多达到了512个通道。由于每个通道都代表着一个feature map,这样就使更多的信息可以被提取出来。
这个特征是体现在VGGNet的测试阶段。在进行网络测试时,将训练阶段的3个全连接层替换为3个卷积层,使测试得到的网络没有全连接的限制,能够接收任意宽和高的输入。如果后面3个层都是全连接层,那么在测试阶段就只能将测试的图像全部缩放到固定尺寸,这样就不便于多尺度测试工作的开展
为什么这样替换之后就可以处理任意尺寸的输入图像了呢?因为1×1卷积一个很重要的作用就是调整通道数。如果下一层输入的特征图需要控制通道数为N,那么设置N个1×1卷积核就可以完成通道数的调整。比如最后需要1000个神经元用于分出1000个类别,那就在最后一层的前面使用1000个1×1的卷积核,这样的到的结果就是(1, 1, 1000)正好可以匹配。
from torchvision import transforms
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import numpy as np
def Myloader(path):
return Image.open(path).convert('RGB')
# get a list of paths and labels.
def init_process(path, lens):
data = []
name = find_label(path)
for i in range(lens[0], lens[1]):
data.append([path % i, name])
return data
class MyDataset(Dataset):
def __init__(self, data, transform, loader):
self.data = data
self.transform = transform
self.loader = loader
def __getitem__(self, item):
img, label = self.data[item]
img = self.loader(img)
img = self.transform(img)
return img, label
def __len__(self):
return len(self.data)
def find_label(str):
"""
Find image tags based on file paths.
:param str: file path
:return: image label
"""
first, last = 0, 0
for i in range(len(str) - 1, -1, -1):
if str[i] == '%' and str[i - 1] == '.':
last = i - 1
if (str[i] == 'c' or str[i] == 'd') and str[i - 1] == '/':
first = i
break
name = str[first:last]
if name == 'dog':
return 1
else:
return 0
def load_data():
print('data processing...')
transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.3),
transforms.RandomVerticalFlip(p=0.3),
transforms.Resize((256, 256)),
transforms.ToTensor(),
transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) # normalization
])
path1 = 'data/training_data/cats/cat.%d.jpg'
data1 = init_process(path1, [0, 500])
path2 = 'data/training_data/dogs/dog.%d.jpg'
data2 = init_process(path2, [0, 500])
path3 = 'data/testing_data/cats/cat.%d.jpg'
data3 = init_process(path3, [1000, 1200])
path4 = 'data/testing_data/dogs/dog.%d.jpg'
data4 = init_process(path4, [1000, 1200])
data = data1 + data2 + data3 + data4 # 1400
# shuffle
np.random.shuffle(data)
# train, val, test = 900 + 200 + 300
train_data, val_data, test_data = data[:900], data[900:1100], data[1100:]
train_data = MyDataset(train_data, transform=transform, loader=Myloader)
Dtr = DataLoader(dataset=train_data, batch_size=50, shuffle=True, num_workers=0)
val_data = MyDataset(val_data, transform=transform, loader=Myloader)
Val = DataLoader(dataset=val_data, batch_size=50, shuffle=True, num_workers=0)
test_data = MyDataset(test_data, transform=transform, loader=Myloader)
Dte = DataLoader(dataset=test_data, batch_size=50, shuffle=True, num_workers=0)
return Dtr, Val, Dte
import copy
import os
import random
import numpy as np
import torch
import torch.nn as nn
from torch import optim
from torch.autograd import Variable
from tqdm import tqdm
from data_process import load_data
import torch.nn.functional as F
from torchvision.models import vgg16
model_vgg = vgg16(pretrained=True)
model_vgg.classifier._modules['6'] = nn.Sequential(nn.Linear(4096, 2))
print(model_vgg)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def setup_seed(seed):
os.environ['PYTHONHASHSEED'] = str(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True
setup_seed(20)
def get_val_loss(model, Val):
model.eval()
criterion = nn.CrossEntropyLoss().to(device)
val_loss = []
for (data, target) in Val:
data, target = Variable(data).to(device), Variable(target.long()).to(device)
output = model(data)
loss = criterion(output, target)
val_loss.append(loss.cpu().item())
return np.mean(val_loss)
def train(model_vgg):
path = "model/vggnet.pkl"
lr = 0.0001
Dtr, Val, Dte = load_data()
model = model_vgg.to(device)
print('train...')
epoch_num = 5
best_model = model
min_epochs = 5
min_val_loss = 5
optimizer = optim.Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss().to(device)
# criterion = nn.BCELoss().to(device)
for epoch in tqdm(range(epoch_num), ascii=True):
train_loss = []
for batch_idx, (data, target) in enumerate(Dtr, 0):
data, target = Variable(data).to(device), Variable(target.long()).to(device)
# target = target.view(target.shape[0], -1)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
train_loss.append(loss.cpu().item())
# validation
val_loss = get_val_loss(model, Val)
model.train()
if epoch + 1 > min_epochs and val_loss < min_val_loss:
min_val_loss = val_loss
best_model = copy.deepcopy(model)
tqdm.write('Epoch {:03d} train_loss {:.5f} val_loss {:.5f}'.format(epoch, np.mean(train_loss), val_loss))
torch.save(best_model.state_dict(), path)
return best_model
def test(model_vgg):
Dtr, Val, Dte = load_data()
path = "model/vggnet.pkl"
model = model_vgg
#device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.load_state_dict(torch.load(path),False)
model = model.to(device)
model.eval()
total = 0
current = 0
for (data, target) in Dte:
data, target = data.to(device), target.to(device)
outputs = model(data)
predicted = torch.max(outputs.data, 1)[1].data
total += target.size(0)
current += (predicted == target).sum()
print('Accuracy:%d%%' % (100 * current / total))
if __name__ == '__main__':
train(model_vgg)
test(model_vgg)
书山有路勤为径,学海无涯苦作舟。
7.1 手撕 CNN 经典网络之 VGGNet(理论篇)
7.2 PyTorch搭建预训练AlexNet、DenseNet、ResNet、VGG实现猫狗图片分类