卷积神经网络之AlexNet经典神经网络,实现手写数字0~9识别
深度学习中较为常见的神经网络模型AlexNet,AlexNet 是一个采用 GPU 训练的深层 CNN,本质是种 LeNet 变体。由特征提取层的5个卷积层两个下采样层和分类器中的三个全连接层构成。
先看原理:
AlexNet网络特点
- 采用 ReLU 激活函数,使训练速度提升 6 倍
- 采用 dropout 层,防止模型过拟合
- 通过平移和翻转的方式对数据进行增强
- 采用 LRN 局部响应归一化,限制数据大小,防止梯度消失和爆炸。但后续证明批量归一化 batch normalization 效果更好。
手写识别0~9项目实现
准备工作:
1、项目文件夹
2、创建4个py文件分别为准备数据模块、模型创建模块、训练模块、测试模块
3、创建两个子文件夹分别命名为data和weights(名字自己定也可以)
1、准备数据部分
首先确保你的python安装了torch和torchvision,我们先导入torchvision里面的数据集模块自带的MNIST包,这里面包含了训练集60000个手写PIL格式的单通道的‘0~9’图片,还有验证集的10000个图片,开始把download参数设置为True,运行下载完成之后数据集即可下载至你的指定目录‘./data’下,同时我们可以查看ds的类型
print(ds[0])
print(len(ds))
print(ds)
from torchvision.datasets import MNIST
#下完了就把download改为False
ds=MNIST(root='./data',train=True,download=False)
print(len(ds))
下载完毕之后我们可以手动设置为False
2、创建模型部分
在AlexNet模块下导入nn包,创建我们的模型类,继承nn.Module
这里我稍微做了修改,由于我们所接受的图片是(N*1*28*28)的单通道灰度图,而我们的模型第一层所接受的输入通道数应该是3,所以为了方便我直接修改模型为单通道输入也就是第一层卷积层:nn.Conv2d(1,96,11,4,2)
然后我们通过特征提取部分:卷积、激活、池化
以及分类器部分:全连接
对张量数据处理
其中我们在进行全连接前还需要把多维数据展平以便全连接,
由于输入图片的初始size需要224*224
通过计算可以得知这里我的最后一层卷积只是刚好把张量数据处理成了(N*9216*1*1)的形状,即使这样还是无法直接全连接,我们依旧需要展平操作Flatten
最后全连接的输出层我们分为10个类别分别对应0~9
import torch
from torch import nn
class AlexNet(nn.Module):
def __init__(self):
super().__init__()
#特征提取
self.features=nn.Sequential(
nn.Conv2d(1,96,11,4,2),
nn.ReLU(),
nn.MaxPool2d(3,stride=2),
nn.Conv2d(96,256,5,padding=2),
nn.ReLU(),
nn.MaxPool2d(3,2),
nn.Conv2d(256,384,3,padding=1),
nn.ReLU(),
nn.Conv2d(384,384,3,padding=1),
nn.ReLU(),
nn.Conv2d(384,256,3,padding=1),
nn.ReLU(),
nn.MaxPool2d(3,2),
nn.Dropout(p=0.5),
nn.Flatten(start_dim=1)
)
#全连接部分(分类)
self.classifier=nn.Sequential(
nn.Linear(9216,4096),
nn.ReLU(),
nn.Linear(4096,4096),
nn.ReLU(),
nn.Dropout(p=0.3),
nn.Linear(4096,10)
)
def forward(self,x):
x=self.features(x)
x=self.classifier(x)
return x
if __name__=='__main__':
model=AlexNet()
x=torch.rand(10,1,224,224)
y=model(x)
print(y.shape)
3、训练模型部分
知识点&步骤:
(1)超参数
(2)准备数据集
(3)数据加载器
(4)模型的引入
(5)优化器(包含L2正则化使得模型具有鲁棒性)
(6)学习率调度器(通过比较测试集的损失改变lr学习率)
(7)训练部分(包含清空梯度,前向传播,清空梯度,计算损失,反向传播,更新梯度)
训练过程中间我们可以打印损失值以便观察梯度下降情况
注意事项:训练样本过大,Epoch博主这里设置2,仅供观察模型结果,最后测试阶段的准确率可能不够,这里可以更改代码里面的cuda内容,使用gpu加速(英伟达cuda加速部分这里不做赘述)
import torch
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torchvision.datasets import MNIST
from torchvision.transforms import Compose,Resize,ToTensor
from torch.utils.data import DataLoader
from torch.optim import Adam
from AlexNet import AlexNet
#我的电脑没有gpu所以没有cuda加速这一步有点多余
device=torch.device('cuda'if torch.cuda.is_available() else 'cpu')
#超参,(我电脑太垃圾了我EPOCH就意思一下写个1)
EPOCH=2
lr =1e-4
batch_size=1000
weight_decay=0.00001
#数据转换器
transformer=Compose([
Resize((244,244),antialias=True),
ToTensor()
])
#用函数设置标签的转换器,也可以用lambda表达式直接设置
def label_transformer(label):
return torch.tensor(label)
#准备数据集
ds=MNIST('./data',train=True,download=False,transform=transformer,target_transform=label_transformer)
#数据加载器DataLoader
#from torch.utils.data import DataLoader导入
#是否打乱顺序我们设置为True
dl=DataLoader(ds,batch_size=batch_size,shuffle=True)
#验证数据集和验证的数据加载器同上
# train设置为False,数据加载器的打乱顺序设置为False即可
val_ds=MNIST('./data',train=False,download=False,transform=transformer,target_transform=label_transformer)
val_dl=DataLoader(val_ds,batch_size=batch_size,shuffle=False)
model=AlexNet()
#调用Gpu设备,我这里没有相当于还是用cpu
model.to(device)
#加载模型
try:
#有模型参数就加载,这里的model用到的方法是继承nn.Module的方法
# 里面的weights_only这个参数是类似于声明本模型我信任,不写有弱警告
model.load_state_dict(torch.load('weights/model_detect_numbers.pt',weights_only=True))
print('模型加载成功')
except:
print('模型加载失败')
#损失函数的准备,由于之前的模型输出层没有激活函数,损失函数用交叉熵损失函数
loss_fn=torch.nn.CrossEntropyLoss()
#优化器添加L2正则化以提高泛化程度
optimizer=Adam(model.parameters(),lr=lr
,weight_decay=weight_decay
)
#学习率调度器,这里是一般的学习率调度器,还有余弦退火调度器
scheduler=ReduceLROnPlateau(
optimizer,
#模式设置为min就是降低学习率
mode='min',
factor=0.5,
#忍耐值为1,就是验证阶段损失函数损失插值小于阈值超过忍耐值就开始降低学习率
#但是前三次的Epoch内不算
patience=1,
#阈值
threshold=1e-3,
#冷静期,简而言之就是大招CD
cooldown=0,
#最小学习率设置
min_lr=2e-5
)
#训练和评估函数
#损失值变量
#总损失浮点型
total_loss=0.
count=0
def train():
#全局变量
global total_loss,count
#训练模式
model.train()
#循环enumerate解包数据并且遍历索引
for i ,(inputs,labels)in enumerate(dl):
# inputs,labels =inputs.to(device),labels.to(device)
#清空梯度
optimizer.zero_grad()
#前向传播
y=model(inputs)
#计算损失
loss=loss_fn(y,labels)
#这里的loss是需要用item转数据类型的
total_loss+=loss.item()
count+=1
#反向传播
loss.backward()
#更新梯度
optimizer.step()
#打印损失,6w条数据每一批次batch_size
if (i+1)%2==0:
print(f'训练损失第【{i}/{len(ds)/batch_size}】次(当前epoch【{epoch+1}/{EPOCH}】)训练损失;{total_loss/count}')
#评估模式,验证损失
val_total_loss=0.
val_count=0
def valid():
global val_total_loss,val_count
#评估模式
model.eval()
#我也不知道with torch.no_grad()什么意思,写就对了
with torch.no_grad():
#评估模式不要清空梯度,反向传播和更新
for i,(inputs,labels)in enumerate(val_dl):
#多余
# inputs,labels=inputs.to(device),labels.to(device)
y=model(inputs)
loss=loss_fn(y,labels)
val_total_loss+=loss.item()
val_count+=1
print(f'评估损失;{val_total_loss / count}')
return val_total_loss/val_count
#开始训练循环
for epoch in range(EPOCH):
#训练循环的循环数
print(f'Epoch进度[{epoch+1}/{EPOCH}]')
#训练开启
train()
#评估开启,且把损失值记录
loss=valid()
#学习率调度器接收评估阶段的损失值得以更新学习率
scheduler.step(loss)
#打印学习率(这部分代码我也不知道什么意思,应该是打印学习率)
print(optimizer.param_groups[0]['lr'])
#保存模型参数
torch.save(model.state_dict(),'weights/model_detect_numbers.pt')
运行之后我们会在weights文件夹内得到模型的参数
这里给出博主第一次Epoch计算的损失以及测试时的准确率:
虽然不高但也达到了90%
4、测试部分
此处我们依然打开MNIST,将train设置为False
开启评估模式之后,对样本预测统计,准确率即(索引与标签匹配个数)/len(ds)
import torch
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision.transforms import Compose, Resize, ToTensor
from AlexNet import AlexNet
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
ds = MNIST(root='./data', train=False, download=False,
transform=Compose([Resize((224, 224), antialias=True), ToTensor()]),
target_transform=lambda label: torch.tensor(label))
dl = DataLoader(ds, batch_size=1000, shuffle=False)
model = AlexNet()
model.to(device)
model.load_state_dict(torch.load('weights/model_detect_numbers.pt', weights_only=True))
model.eval()
# 回答正确的数量
correct_count = 0
for i, (inputs, labels) in enumerate(dl):
# inputs, labels = inputs.to(device), labels.to(device)
with torch.inference_mode():
y = model(inputs)
# 因为模型内没有激活,所以此处激活一下
y = torch.nn.functional.softmax(y, dim=-1)
# 求最大值索引
idx = y.argmax(-1)
correct_count += (idx == labels).short().sum().item()
print(correct_count)
print(f'准确率: {correct_count / len(ds) * 100:.2f}%')