机器学习第二十七周周报
摘要
《ImageNet Classification with Deep Convolutional Neural Networks》的主要贡献是构建了一个深层神经网络架构,该架构具有几点创新之处。第一,通过减少参数量来加速训练;第二,提出了几种避免过拟合的措施;第三,使用ReLU激活函数取代了tanh和softmax。另外,我还深入学习了CNN的原理。CNN通过卷积和池化等操作,逐步减小图像尺寸,从而大大减少了参数量。
Abstract
The main contribution of “ImageNet Classification with Deep Convolutional Neural Networks” is constructing a deep neural network architecture that has several innovations. First, it reduces the number of parameters to accelerate training. Second, it proposes several measures to prevent overfitting. Third, it uses the ReLU activation function instead of tanh and softmax. In addition, I studied the principles of CNN in depth. CNN gradually reduces image size through operations like convolution and pooling, thus greatly reducing the number of parameters.
一、文献阅读
1.题目
ImageNet Classification with Deep Convolutional Neural Networks
期刊:Communications of the ACM
论文链接:https://dl.acm.org/doi/10.1145/3065386
2.ABSTRACT
This article utilized a deep convolutional neural network for image classification and achieved excellent results. The deep convolutional network consists of 60 million parameters, 650,000 neurons, five convolutional layers, and three fully connected layers. GPU acceleration was employed to speed up the training process. Dropout, a regularization technique, was used to reduce overfitting.
这篇文章利用了一个深度卷积神经网络来进行图片分类,取得了一个非常好的效果。深度卷积网络由60million个参数,65w个神经元,以及五个卷积层和三个全连接层组成。为了加快训练,用到了GPU加速实现。用了dropout这个正则化方法来减少过拟合。
3.网络架构
网络架构分为八个层,其中有五个卷积层和三个全连接层。
输入为224×224×3的图像
卷积层1的卷积核为11×11×3,strde=4,每个GPU内输出55×55×48,响应规范化,池化
卷积层2:256个卷积核,大小为5×5×48;响应规范化,池化
卷积层3,4:384个卷积核,大小为3×3×256;无池化
卷积层5:256个卷积核,大小为3×3×192
全连接层6,7:每个GPU内有2048个神经元,共4096个
全连接层8:输出与1000个softmax相连
输出:关于1000个类的分布
4.文献解读
(1)introduce
文章主要任务就是利用深度卷积神经网络进行分类任务,作者首先确定使用CNN的架构,这样做减少了神经层之间的连接以及参数量,便于训练,且性能相比标准的具有相似尺寸大小的网络层的前馈神经网络只是轻微的下降。之后作者在框架中引入了几个设计,如ReLU的使用,数据集的扩增等等,提高了CNN网络的性能,还有就是GPU的使用硬性保证了大规模网络能够train起来。
(2)创新点
使用ReLU函数替代softmax和tanh函数
实线为用ReLU函数的误差下降率,虚线为tanh函数的误差下降率,从图中可以看出,用ReLU函数误差下降会比tanh函数快好多倍。
在多个GPU上训练
从网络框架图中我们可以看出,整个网络被分为两个来进行训练,然后把切完之后的网络用两个GPU来训练。
局部响应归一化
在ReLU层之前我们应用了normalization得到了一个更好的效果,文章中说,使用局部响应归一化比没有使用局部响应归一化的错误率要低%2左右。
局部响应归一化的好处:它的作用是对每个神经元的输出进行归一化,使得较大的响应值相对较小,较小的响应值相对较大。这样做的好处是可以抑制较大的响应,使得其他神经元的响应更加突出,从而增强网络对不同特征的敏感性。
重叠池化
一般来说两个pooling是不重叠的,但是这里采用了一种对传统的pooling改进的方式。文章中通过重叠池化,得出top-1和top-5错误率分别降低0.4%和0.3%。
(3)实验过程
数据集
本文使用的数据集是ILSVRC-2010,LSVRC使用ImageNet的一个子集,1000个类别中的每个类别大约有1000个图像。总共大约有120万个训练图像、50000个验证图像和150000个测试图像。
由于实验数据中的图像是由可变分辨率的图像组成,所以我们在输入数据之前要先固定分辨率,所以我们就把所有的图像裁剪成256256的面片。具体裁剪方法:先对原始图片进行缩放,将短边变成256的大小,另一个长边在这一步操作中也会根据长宽比进行调整,然后第二步从图片中心对长边进行两侧的裁剪,得到256256的尺寸大小。
评估指标
ImageNet通常用两个指标来代表错误率:top-1和top-5。top-5表示正确标签不在网络模型认为的最有可能的五个标签内的比例。
实验数据
ILSVRC-2010测试集的结果比较
从图中可以看出,本文所用的模型CNN的表现结果要比之前最优的模型的表现结果还要好。
ILSVRC-2012验证集和测试集的错误率比较
1 CNN是本文描述的模型,5 CNNs是五个相似CNN的预测平均值,1 CNNs是在1CNN基础上,在最后一个池化层后接了一个新的卷积层(第六层卷积层),用整个ImageNet Fall 2011 release训练(15M images 22K categories)然后在ILSVRC-2012上进行"fine-tuning"的结果,7 CNNs* 两个在Fall 2011上与训练的模型和5CNNs的预测平均值。
超参数的设定
我们使用随机梯度下降训练我们的模型,批量大小为128个示例,动量为0.9,权重衰减为0.0005。
初始化参数:用均值为0 ,方差为0.01的高斯随机变量去初始化了权重参数
学习率:我们在所有层上使用相同的学习率,设为0.01。但验证误差不降的时候我们就手动的乘以0.1,也就是降低十倍。也有自动的方法,例如Resnet,训练120轮epoch,初始学习率也是设为0.01,每30轮降低十倍,本文是训练了90个epoch,每一次是120w张图片。
(4)结论
一个大型的深度CNN能够使用纯粹的监督学习在一个具有高度挑战性的数据集上实现破纪录的结果。值得注意的是,如果删除单个卷积层,我们的网络性能会下降。例如,移除任何中间层导致网络的前1个性能损失约2%。
二、代码复现
1.alexnet模型
import torch
from torch import nn
#N = (input_size − conv_size + 2*Padding )/Stride+1
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
#nn.Sequential()是一个PyTorch中用于封装序列的函数,通常用于将多个层串联起来形成一个神经网络。
self.net = nn.Sequential(
# 为了和论文的图像输入尺寸保持一致以及下一层的55对应,这里对图像进行了padding
#输入通道3,输出通道96,卷积核大小11*11,步长为4,padding设置为2
nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=2),
#ReLU
nn.ReLU(),
#局部响应归一化,size:用于归一化的相邻通道的数量;alpha:乘法因子,默认值0.0001;beta:指数,默认值0.75;k:附加因子,默认为1
nn.LocalResponseNorm(size=5, alpha=10e-4, beta=0.75, k=2),
#最大池化,卷积核大小为3*#,步长设置为2
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, padding=2),
nn.ReLU(),
nn.LocalResponseNorm(size=5, alpha=10e-4, beta=0.75, k=2),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
#它用来将输入张量展平为一维张量,它可以被用来将二维卷积层的输出偏扁为一维传到全连接层里
nn.Flatten(),
#在 training 模式下,基于伯努利分布抽样,以概率 p 对张量 input 的值随机置0;
nn.Dropout(p=0.5),
#nn.Linear()是PyTorch中的一个线性变换层,常用来在神经网络中构建一个线性分类器
nn.Linear(in_features=256 * 6 * 6, out_features=4096),
nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(in_features=4096, out_features=4096),
nn.ReLU(),
nn.Linear(in_features=4096, out_features=num_classes)
)
self.init_weights()
def init_weights(self):
for layer in self.net:
# 先一致初始化
if isinstance(layer, nn.Conv2d):
nn.init.kaiming_normal_(layer.weight, mode='fan_out', nonlinearity='relu')
# nn.init.normal_(layer.weight, mean=0, std=0.01) # 论文权重初始化策略
nn.init.constant_(layer.bias, 0)
elif isinstance(layer, nn.Linear):
nn.init.normal_(layer.weight, mean=0, std=0.01)
nn.init.constant_(layer.bias, 1)
# 单独对论文网络中的2、4、5卷积层的偏置进行初始化
nn.init.constant_(self.net[4].bias, 1)
nn.init.constant_(self.net[10].bias, 1)
nn.init.constant_(self.net[12].bias, 1)
def forward(self, x):
return self.net(x)
def test_output_shape(self):
test_img = torch.rand(size=(1, 3, 227, 227), dtype=torch.float32)
for layer in self.net:
test_img = layer(test_img)
print(layer.__class__.__name__, 'output shape: \t', test_img.shape)
2.数据集划分
import os
from shutil import copy, rmtree
import random
def make_dir(file_path):
if os.path.exists(file_path):
# 如果文件夹存在,则先删除原文件夹再创建
rmtree(file_path)
os.makedirs(file_path)
def split_data(input_file_path, output_file_path, split_rate, seed='random'):
if seed == 'fixed':
random.seed(0)
else:
random.seed()
# 获取当前文件路径
cwd = os.getcwd()
input_dataset_path = os.path.join(cwd, input_file_path)
output_dataset_path = os.path.join(cwd, output_file_path)
assert os.path.exists(input_dataset_path), f"path '{input_dataset_path}' does not exist."
# ===================#######################################
# os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表
# os.path.isdir() 方法用于判断某一路径是否为目录
# 先遍历dataset_path获得文件\文件夹名称列表,再判断名称是否为目录
############################################################
dataset_classes = [dataset_class for dataset_class in os.listdir(input_dataset_path) if
os.path.isdir(os.path.join(input_dataset_path, dataset_class))]
# 训练集
train_path = os.path.join(output_dataset_path, 'train')
make_dir(train_path)
for dataset_class in dataset_classes:
make_dir(os.path.join(train_path, dataset_class))
# 验证集
val_path = os.path.join(output_dataset_path, 'val')
make_dir(val_path)
for dataset_class in dataset_classes:
make_dir(os.path.join(val_path, dataset_class))
for dataset_class in dataset_classes:
input_dataset_class_path = os.path.join(input_dataset_path, dataset_class)
images = os.listdir(input_dataset_class_path)
images_num = len(images)
# 随机选取验证集
val_images = random.sample(images, k=int(images_num * split_rate))
for index, image in enumerate(images):
# 获取图像路径
image_path = os.path.join(input_dataset_class_path, image)
if image in val_images:
# 将图像文件copy到验证集对应路径
copy(image_path, os.path.join(val_path, dataset_class))
else:
copy(image_path, os.path.join(train_path, dataset_class))
print(f'[{dataset_class}] is processing: {index + 1}/{images_num}')
print('process finished.')
if __name__ == '__main__':
original_data_file_path = r'E:\研究生学习\project\alexnet\flower_photos\flower_photos'
spilit_data_file_path = 'data'
split_rate = 0.1
split_data(original_data_file_path, spilit_data_file_path, split_rate)
3.训练
import os
import json
import torch
from torch import nn
from torch import optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
from tqdm import tqdm
from alexnet import AlexNet
BATCH_SIZE = 64 # 论文128
LR = 0.0001 # 论文 0.01
WEIGHT_DECAY = 0.0005
MOMENTUM = 0.9
EPOCHS = 10 # 论文90
DATASET_PATH = 'data'
MODEL = 'AlexNet.pth'
def train_device(device='cpu'):
# 只考虑单卡训练
if device == 'gpu':
cuda_num = torch.cuda.device_count()
if cuda_num >= 1:
print('device:gpu')
return torch.device(f'cuda:{0}')
else:
print('device:cpu')
return torch.device('cpu')
def dataset_loader(dataset_path):
dataset_path = os.path.join(os.getcwd(), dataset_path)
assert os.path.exists(dataset_path), f'[{dataset_path}] does not exist.'
train_dataset_path = os.path.join(dataset_path, 'train')
val_dataset_path = os.path.join(dataset_path, 'val')
# 训练集图片随机裁剪224x224区域,以0.5的概率水平翻转
# 由于torchvision没有封装PCA jitter,所以用Corlor jitter模拟RGB通道强度的变化(不够严谨...)
# alexnet中训练样本分布为零均值分布,这里采用了常用的均值为0方差为1的标准正态分布
data_transform = {
'train': transforms.Compose([transforms.RandomResizedCrop(size=224),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
'val': transforms.Compose([transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
train_dataset = datasets.ImageFolder(root=train_dataset_path, transform=data_transform['train'])
val_dataset = datasets.ImageFolder(root=val_dataset_path, transform=data_transform['val'])
return train_dataset, val_dataset
def idx2class_json(train_dataset):
class2idx_dic = train_dataset.class_to_idx
idx2class_dic = dict((val, key) for key, val in class2idx_dic.items())
# json.dumps()把python对象转换成json格式的字符串
json_str = json.dumps(idx2class_dic)
with open('class_idx.json', 'w') as json_file:
json_file.write(json_str)
print('write class_idx.json complete.')
def evaluate_val_accuracy(net, val_dataset_loader, val_dataset_num, device=torch.device('cpu')):
if isinstance(net, nn.Module):
net.eval()
val_correct_num = 0
for i, (val_img, val_label) in enumerate(val_dataset_loader):
val_img, val_label = val_img.to(device), val_label.to(device)
output = net(val_img)
_, idx = torch.max(output.data, dim=1)
val_correct_num += torch.sum(idx == val_label)
val_correct_rate = val_correct_num / val_dataset_num
return val_correct_rate
def train(net, train_dataset, val_dataset, device=torch.device('cpu')):
train_dataset_loader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True)
val_dataset_loader = DataLoader(dataset=val_dataset, batch_size=BATCH_SIZE)
print(f'[{len(train_dataset)}] images for training, [{len(val_dataset)}] images for validation.')
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.SGD(params=net.parameters(), lr=LR, momentum=MOMENTUM, weight_decay=WEIGHT_DECAY) # 论文使用的优化器
lr_scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer, mode='max', factor=0.1, patience=1,
min_lr=0.00000001)
# 在训练的过程中会根据验证集的最佳准确率保存模型
best_val_correct_rate = 0.0
for epoch in range(EPOCHS):
net.train()
# 可视化训练进度条
train_bar = tqdm(train_dataset_loader)
# 计算每个epoch的loss总和
loss_sum = 0.0
for i, (train_img, train_label) in enumerate(train_bar):
optimizer.zero_grad()
train_img, train_label = train_img.to(device), train_label.to(device)
output = net(train_img)
loss = loss_function(output, train_label)
loss.backward()
optimizer.step()
loss_sum += loss.item()
train_bar.desc = f'train epoch:[{epoch + 1}/{EPOCHS}], loss:{loss:.5f}'
# 测试验证集准确率
val_correct_rate = evaluate_val_accuracy(net, val_dataset_loader, len(val_dataset), device)
# 根据验证集准确率更新学习率
lr_scheduler.step(val_correct_rate)
print(
f'epoch:{epoch + 1}, '
f'train loss:{(loss_sum / len(train_dataset_loader)):.5f}, '
f'val correct rate:{val_correct_rate:.5f}')
if val_correct_rate > best_val_correct_rate:
best_val_correct_rate = val_correct_rate
# 保存模型
torch.save(net.state_dict(), MODEL)
print('train finished.')
if __name__ == '__main__':
# 这里数据集只有5类
alexnet = AlexNet(num_classes=5)
device = train_device('gpu')
train_dataset, val_dataset = dataset_loader(DATASET_PATH)
# 保存类别对应索引的json文件,预测用
idx2class_json(train_dataset)
train(alexnet, train_dataset, val_dataset, device)
4.测试
import os
import json
import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
from alexnet import AlexNet
IMG_PATH = r'E:\研究生学习\project\alexnet\04.png'
JSON_PATH = 'class_idx.json'
WEIGHT_PATH = 'AlexNet.pth'
def predict(net, img, json_label):
data_transform = transforms.Compose(
[transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
original_img=img
img = data_transform(img) # 3,224,224
img = torch.unsqueeze(img, dim=0) # 1,3,224,224
assert os.path.exists(WEIGHT_PATH), f'file {WEIGHT_PATH} does not exist.'
net.load_state_dict(torch.load(WEIGHT_PATH))
net.eval()
with torch.no_grad():
output = torch.squeeze(net(img)) # net(img)的size为1,5,经过squeeze后变为5
predict = torch.softmax(output, dim=0)
predict_label_idx=int(torch.argmax(predict))
predict_label=json_label[str(predict_label_idx)]
predict_probability=predict[predict_label_idx]
predict_result=f'class:{predict_label}, probability:{predict_probability:.3f}'
plt.imshow(original_img)
plt.title(predict_result)
print(predict_result)
plt.show()
def read_json(json_path):
assert os.path.exists(json_path), f'{json_path} does not exist.'
with open(json_path, 'r') as json_file:
idx2class = json.load(json_file)
return idx2class
if __name__ == '__main__':
net = AlexNet(num_classes=5)
img = Image.open(IMG_PATH)
idx2class = read_json(JSON_PATH)
predict(net, img, idx2class)