一、网络结构
AlexNet是一个8层的神经网络,包含5个卷积层和3个全连接层
二、代码实现
首先需要数据集,这里以猫狗二分类为例,文件夹里存放猫猫狗狗的图片
1、model.py
定义AlexNet网络模型
# 导入pytorch库
import torch
# 导入torch.nn模块
from torch import nn
# nn.functional:(一般引入后改名为F)有各种功能组件的函数实现,如:F.conv2d
import torch.nn.functional as F
# 定义AlexNet网络模型
# MyLeNet5(子类)继承nn.Module(父类)
class MyAlexNet(nn.Module):
# 子类继承中重新定义Module类的__init__()和forward()函数
# init():进行初始化,申明模型中各层的定义
def __init__(self):
# super:引入父类的初始化方法给子类进行初始化
super(MyAlexNet, self).__init__()
# 卷积层,输入大小为224*224,输出大小为55*55,输入通道为3,输出为96,卷积核为11,步长为4
self.c1 = nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=2)
# 使用ReLU作为激活函数
self.ReLU = nn.ReLU()
# MaxPool2d:最大池化操作
# 最大池化层,输入大小为55*55,输出大小为27*27,输入通道为96,输出为96,池化核为3,步长为2
self.s1 = nn.MaxPool2d(kernel_size=3, stride=2)
# 卷积层,输入大小为27*27,输出大小为27*27,输入通道为96,输出为256,卷积核为5,扩充边缘为2,步长为1
self.c2 = nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2)
# 最大池化层,输入大小为27*27,输出大小为13*13,输入通道为256,输出为256,池化核为3,步长为2
self.s2 = nn.MaxPool2d(kernel_size=3, stride=2)
# 卷积层,输入大小为13*13,输出大小为13*13,输入通道为256,输出为384,卷积核为3,扩充边缘为1,步长为1
self.c3 = nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1)
# 卷积层,输入大小为13*13,输出大小为13*13,输入通道为384,输出为384,卷积核为3,扩充边缘为1,步长为1
self.c4 = nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1)
# 卷积层,输入大小为13*13,输出大小为13*13,输入通道为384,输出为256,卷积核为3,扩充边缘为1,步长为1
self.c5 = nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1)
# 最大池化层,输入大小为13*13,输出大小为6*6,输入通道为256,输出为256,池化核为3,步长为2
self.s5 = nn.MaxPool2d(kernel_size=3, stride=2)
# Flatten():将张量(多维数组)平坦化处理,神经网络中第0维表示的是batch_size,所以Flatten()默认从第二维开始平坦化
self.flatten = nn.Flatten()
# 全连接层
# Linear(in_features,out_features)
# in_features指的是[batch_size, size]中的size,即样本的大小
# out_features指的是[batch_size,output_size]中的output_size,样本输出的维度大小,也代表了该全连接层的神经元个数
self.f6 = nn.Linear(6 * 6 * 256, 4096)
self.f7 = nn.Linear(4096, 4096)
# 全连接层&softmax
self.f8 = nn.Linear(4096, 1000)
self.f9 = nn.Linear(1000, 2)
# forward():定义前向传播过程,描述了各层之间的连接关系
def forward(self, x):
x = self.ReLU(self.c1(x))
x = self.s1(x)
x = self.ReLU(self.c2(x))
x = self.s2(x)
x = self.ReLU(self.c3(x))
x = self.ReLU(self.c4(x))
x = self.ReLU(self.c5(x))
x = self.s5(x)
x = self.flatten(x)
x = self.f6(x)
# Dropout:随机地将输入中50%的神经元激活设为0,即去掉了一些神经节点,防止过拟合
# “失活的”神经元不再进行前向传播并且不参与反向传播,这个技术减少了复杂的神经元之间的相互影响
x = F.dropout(x, p=0.5)
x = self.f7(x)
x = F.dropout(x, p=0.5)
x = self.f8(x)
x = F.dropout(x, p=0.5)
x = self.f9(x)
return x
# 每个python模块(python文件)都包含内置的变量 __name__,当该模块被直接执行的时候,__name__ 等于文件名(包含后缀 .py )
# 如果该模块 import 到其他模块中,则该模块的 __name__ 等于模块名称(不包含后缀.py)
# “__main__” 始终指当前执行模块的名称(包含后缀.py)
# if确保只有单独运行该模块时,此表达式才成立,才可以进入此判断语法,执行其中的测试代码,反之不行
if __name__ == '__main__':
# rand:返回一个张量,包含了从区间[0, 1)的均匀分布中抽取的一组随机数,此处为四维张量
x = torch.rand([1, 3, 224, 224])
# 模型实例化
model = MyAlexNet()
y = model(x)
2、spilit_data.py
划分给定的数据集为训练集和测试集
import os
from shutil import copy
import random
# 如果file不存在,创建file
def mkfile(file):
if not os.path.exists(file):
os.makedirs(file)
# 获取data文件夹下所有除.txt文件以外所有文件夹名(即需要分类的类名)
# os.listdir():用于返回指定的文件夹包含的文件或文件夹的名字的列表
file_path = 'D:/pycharm/AlexNet/data_name'
pet_class = [cla for cla in os.listdir(file_path) if ".txt" not in cla]
# 创建训练集train文件夹,并由类名在其目录下创建子目录
mkfile('data/train')
for cla in pet_class:
mkfile('data/train/' + cla)
# 创建验证集val文件夹,并由类名在其目录下创建子目录
mkfile('data/val')
for cla in pet_class:
mkfile('data/val/' + cla)
# 划分比例,训练集 : 验证集 = 8 : 2
split_rate = 0.2
# 遍历所有类别的图像并按比例分成训练集和验证集
for cla in pet_class:
# 某一类别的子目录
cla_path = file_path + '/' + cla + '/'
# iamges列表存储了该目录下所有图像的名称
images = os.listdir(cla_path)
num = len(images)
# 从images列表中随机抽取k个图像名称
# random.sample:用于截取列表的指定长度的随机数,返回列表
# eval_index保存验证集val的图像名称
eval_index = random.sample(images, k=int(num * split_rate))
for index, image in enumerate(images):
if image in eval_index:
image_path = cla_path + image
new_path = 'data/val/' + cla
# copy():将源文件的内容复制到目标文件或目录
copy(image_path, new_path)
# 其余图像保存在训练集train中
else:
image_path = cla_path + image
new_path = 'data/train/' + cla
copy(image_path, new_path)
# '\r' 回车,回到当前行的行首,而不会换到下一行,如果接着输出,本行以前的内容会被逐一覆盖
# <模板字符串>.format(<逗号分隔的参数>)
# end="":将print自带的换行用end中指定的str代替
print("\r[{}] processing [{}/{}]".format(cla, index + 1, num), end="")
print()
print("processing done!")
传入数据集地址之后,执行代码,会按比例生成数据集和验证集
3、train.py
加载数据集并训练,计算loss和accuracy,保存训练好的网络参数
import torch
from torch import nn
from model import MyAlexNet
from torch.optim import lr_scheduler
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
import os
import matplotlib.pyplot as plt
# 解决中文显示问题
# 运行配置参数中的字体(font)为黑体(SimHei)
plt.rcParams['font.sans-serif'] = ['simHei']
# 运行配置参数总的轴(axes)正常显示正负号(minus)
plt.rcParams['axes.unicode_minus'] = False
ROOT_TRAIN = 'D:/pycharm/AlexNet/data/train'
ROOT_TEST = 'D:/pycharm/AlexNet/data/val'
# 将图像的像素值归一化到[-1,1]之间
normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# Compose():将多个transforms的操作整合在一起
train_transform = transforms.Compose([
# Resize():把给定的图像随机裁剪到指定尺寸
transforms.Resize((224, 224)),
# RandomVerticalFlip():以0.5的概率竖直翻转给定的PIL图像
transforms.RandomVerticalFlip(),
# ToTensor():数据转化为Tensor格式
transforms.ToTensor(),
normalize])
val_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
normalize])
# 加载训练数据集
# ImageFolder:假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:
# ImageFolder(root, transform=None, target_transform=None, loader=default_loader)
# root:在root指定的路径下寻找图像,transform:对输入的图像进行的转换操作
train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform)
# DataLoader:将读取的数据按照batch size大小封装给训练集
# dataset (Dataset):加载数据的数据集
# batch_size (int, optional):每个batch加载多少个样本(默认: 1)
# shuffle (bool, optional):设置为True时会在每个epoch重新打乱数据(默认: False)
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
# 加载训练数据集
val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
val_dataloader = DataLoader(val_dataset, batch_size=32, shuffle=True)
# 如果有NVIDA显卡,可以转到GPU训练,否则用CPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 模型实例化,将模型转到device
model = MyAlexNet().to(device)
# 定义损失函数(交叉熵损失)
loss_fn = nn.CrossEntropyLoss()
# 定义优化器(随机梯度下降法)
# params(iterable):要训练的参数,一般传入的是model.parameters()
# lr(float):learning_rate学习率,也就是步长
# momentum(float, 可选):动量因子(默认:0),矫正优化率
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# 学习率每隔10轮变为原来的0.5
# StepLR:用于调整学习率,一般情况下会设置随着epoch的增大而逐渐减小学习率从而达到更好的训练效果
# optimizer (Optimizer):更改学习率的优化器
# step_size(int):每训练step_size个epoch,更新一次参数
# gamma(float):更新lr的乘法因子
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
# 定义训练函数
def train(dataloader, model, loss_fn, optimizer):
loss, current, n = 0.0, 0.0, 0
# dataloader: 传入数据(数据包括:训练数据和标签)
# enumerate():用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在for循环当中
# enumerate返回值有两个:一个是序号,一个是数据(包含训练数据和标签)
# x:训练数据(inputs)(tensor类型的),y:标签(labels)(tensor类型)
for batch, (x, y) in enumerate(dataloader):
# 前向传播
image, y = x.to(device), y.to(device)
# 计算训练值
output = model(image)
# 计算观测值(label)与训练值的损失函数
cur_loss = loss_fn(output, y)
# torch.max(input, dim)函数
# input是具体的tensor,dim是max函数索引的维度,0是每列的最大值,1是每行的最大值输出
# 函数会返回两个tensor,第一个tensor是每行的最大值;第二个tensor是每行最大值的索引
_, pred = torch.max(output, axis=1)
# 计算每批次的准确率
# output.shape[0]为该批次的多少,output的一维长度
# torch.sum()对输入的tensor数据的某一维度求和
cur_acc = torch.sum(y == pred) / output.shape[0]
# 反向传播
# 清空过往梯度
optimizer.zero_grad()
# 反向传播,计算当前梯度
cur_loss.backward()
# 根据梯度更新网络参数
optimizer.step()
# item():得到元素张量的元素值
loss += cur_loss.item()
current += cur_acc.item()
n = n + 1
train_loss = loss / n
train_acc = current / n
# 计算训练的错误率
print('train_loss==' + str(train_loss))
# 计算训练的准确率
print('train_acc' + str(train_acc))
return train_loss, train_acc
# 定义验证函数
def val(dataloader, model, loss_fn):
loss, current, n = 0.0, 0.0, 0
# eval():如果模型中有Batch Normalization和Dropout,则不启用,以防改变权值
model.eval()
with torch.no_grad():
for batch, (x, y) in enumerate(dataloader):
# 前向传播
image, y = x.to(device), y.to(device)
output = model(image)
cur_loss = loss_fn(output, y)
_, pred = torch.max(output, axis=1)
cur_acc = torch.sum(y == pred) / output.shape[0]
loss += cur_loss.item()
current += cur_acc.item()
n = n + 1
val_loss = loss / n
val_acc = current / n
# 计算验证的错误率
print('val_loss=' + str(val_loss))
# 计算验证的准确率
print('val_acc=' + str(val_acc))
return val_loss, val_acc
# 定义画图函数
# 错误率
def matplot_loss(train_loss, val_loss):
# 参数label = ''传入字符串类型的值,也就是图例的名称
plt.plot(train_loss, label='train_loss')
plt.plot(val_loss, label='val_loss')
# loc代表了图例在整个坐标轴平面中的位置(一般选取'best'这个参数值)
plt.legend(loc='best')
plt.xlabel('loss')
plt.ylabel('epoch')
plt.title("训练集和验证集的loss值对比图")
plt.show()
# 准确率
def matplot_acc(train_acc, val_acc):
plt.plot(train_acc, label='train_acc')
plt.plot(val_acc, label='val_acc')
plt.legend(loc='best')
plt.xlabel('acc')
plt.ylabel('epoch')
plt.title("训练集和验证集的acc值对比图")
plt.show()
# 开始训练
loss_train = []
acc_train = []
loss_val = []
acc_val = []
# 训练次数
epoch = 20
# 用于判断最佳模型
min_acc = 0
for t in range(epoch):
lr_scheduler.step()
print(f"epoch{t + 1}\n----------")
# 训练模型
train_loss, train_acc = train(train_dataloader, model, loss_fn, optimizer)
# 验证模型
val_loss, val_acc = val(val_dataloader, model, loss_fn)
loss_train.append(train_loss)
acc_train.append(train_acc)
loss_val.append(val_loss)
acc_val.append(val_acc)
# 保存最好的模型权重
if val_acc > min_acc:
folder = 'save_model'
# path.exists:判断括号里的文件是否存在的意思,括号内可以是文件路径,存在为True
if not os.path.exists(folder):
# os.mkdir() 方法用于以数字权限模式创建目录
os.mkdir('save_model')
min_acc = val_acc
print(f"save best model,第{t + 1}轮")
# torch.save(state, dir):保存模型等相关参数,dir表示保存文件的路径+保存文件名
# model.state_dict():返回的是一个OrderedDict,存储了网络结构的名字和对应的参数
torch.save(model.state_dict(), 'save_model/best_model.pth')
# 保存最后一轮权重
if t == epoch - 1:
torch.save(model.state_dict(), 'save_model/best_model.pth')
matplot_loss(loss_train, loss_val)
matplot_acc(acc_train, acc_val)
print('done')
运行代码,训练模型 ,训练结束后会得到一个最好的模型
4、test.py
用自己的数据集进行分类测试
import torch
from model import MyAlexNet
from torch.autograd import Variable
from torchvision import transforms
from torchvision.transforms import ToPILImage
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
ROOT_TRAIN = 'D:/pycharm/AlexNet/data/train'
ROOT_TEST = 'D:/pycharm/AlexNet/data/val'
# 将图像的像素值归一化到[-1,1]之间
normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
val_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
normalize
])
# 加载训练数据集
val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
# 如果有NVIDA显卡,转到GPU训练,否则用CPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 模型实例化,将模型转到device
model = MyAlexNet().to(device)
# 加载train.py里训练好的模型
model.load_state_dict(torch.load(r'D:\pycharm\AlexNet\save_model\best_model.pth'))
# 结果类型
classes = [
"cat",
"dog"
]
# 把Tensor转化为图片,方便可视化
show = ToPILImage()
# 进入验证阶段
model.eval()
for i in range(1):
x, y = val_dataset[i][0], val_dataset[i][1]
# show():显示图片
show(x).show()
# torch.unsqueeze(input, dim),input(Tensor):输入张量,dim (int):插入维度的索引,最终扩展张量维度为4维
x = Variable(torch.unsqueeze(x, dim=0).float(), requires_grad=False).to(device)
with torch.no_grad():
pred = model(x)
# argmax(input):返回指定维度最大值的序号
# 得到预测类别中最高的那一类,再把最高的这一类对应classes中的那一类
predicted, actual = classes[torch.argmax(pred[0])], classes[y]
# 输出预测值与真实值
print(f'predicted:"{predicted}", actual:"{actual}"')
测试出来的猫狗在色彩上会有一些奇怪,是因为像素值进行了归一化,删掉 “normalize”即可恢复正常颜色。