写在前面:根据b站博主霹雳吧啦Wz 学习CNN,作为个人的学习记录。
目录
论文:
模型架构:
模型参数:
模型搭建:
应用:
数据集:
1、公开数据集
2、自己的数据集
训练:
1、寻找可使用的GPU
2、图像预处理
3、找到训练集
4、将类索引写入json文件
5、加载数据集
6、定义网络、训练设备、损失函数、优化器
7、训练
训练结果:
预测:
编辑
预测结果:
数据可视化:
2012年才出现的AlexNet,在ImageNet LSVRC-2010比赛中实现了前1和前5的错误率分别为37.5%和17.0%,在ILSVRC-2012比赛中获得了15.3%的前5的获胜测试错误率。
论文:《ImageNet Classification with Deep Convolutional Neural Networks》
论文贡献:
- 在ILSVRC-2010和ILSVRC-2012比赛中使用的ImageNet子集上训练了迄今为止最大的卷积神经网络之一,并在这些数据集上获得了迄今为止报道过的最好的结果。
- 为了防止过拟合,首次使用ReLUs作为激活函数,并指出快速学习对在大数据集上训练的大模型的性能有很大的影响。
- 首次使用两个GPU进行训练,这将前1和前5的错误率分别降低了1.7%和1.2%,双gpu网络的训练时间比单gpu网络略短。
- 使用局部响应归一化(LRN),有助于快速收敛,增强了模型的泛化能力。(一般是在激活、池化后进行的一种处理方法)。
- 使用重叠池化层,降低了0.4%和0.3%的错误率。避免过拟合。
- 使用数据增强方式:平移和翻转;PCA进行降维。避免过拟合。
- 首次应用DropOut机制,大约使收敛所需的迭代次数翻了一倍。避免过拟合。
翻译:网络架构明确地显示了两个gpu之间的职责描述。一个GPU运行图形顶部的层部件,另一个运行图形底部的层部件。gpu只在特定的层进行通信。网络的输入是150,528维,网络剩余层中的神经元数量由253,440-186,624-64,896-64,896-43,264 - 4096-4096-1000给出。
由于这个模型太大,在这里,所有的卷积核个数和全连接层节点数都减半。
import torch.nn as nn
import torch
class AlexNet(nn.Module):
def __init__(self, num_classes=1000, init_weights=False):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2), # input[3, 224, 224] output[48, 55, 55]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[48, 27, 27]
nn.Conv2d(48, 128, kernel_size=5, padding=2), # output[128, 27, 27]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[128, 13, 13]
nn.Conv2d(128, 192, kernel_size=3, padding=1), # output[192, 13, 13]
nn.ReLU(inplace=True),
nn.Conv2d(192, 192, kernel_size=3, padding=1), # output[192, 13, 13]
nn.ReLU(inplace=True),
nn.Conv2d(192, 128, kernel_size=3, padding=1), # output[128, 13, 13]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[128, 6, 6]
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5), # 随机失活一半的神经元
nn.Linear(128 * 6 * 6, 2048),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(2048, 2048),
nn.ReLU(inplace=True),
nn.Linear(2048, num_classes), # 这里的num_classes是根据数据集内的种类决定
)
if init_weights: # 初始化权重
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, start_dim=1)
x = self.classifier(x)
return x
def _initialize_weights(self): # 初始化权重
for m in self.modules():
if isinstance(m, nn.Conv2d): # 如果在卷积层,那就使用kaiming_normal_方法初始化
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear): # 如果在全连接层,那就正态分布方法初始化
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
使用的是花分类数据集,分别有雏菊、蒲公英、玫瑰、向日葵、郁金香五种类别。首先新建一个名为"flower_data"的新文件夹,然后在浏览器下载该数据集,解压进"flower_data",使用split_data.py程序对数据集进行划分,变成训练集和验证集。
这是所有图片下的文件,一种类别的图片是一个文件夹,文件夹名称为类别名称:
只需要将自己的数据集按类别分开(例如daisy、dandelion、roses、sunflowers、tulips),放在一个文件夹内(例如flower_data)。然后使用split_data.py程序划分训练集和验证集。最后在训练程序和测试程序中找到变量num_classes的值,修改成自己数据集的种类个数。即可
训练程序里:
预测程序里:
split_data.py:
import os
from shutil import copy, rmtree
import random
def mk_file(file_path: str):
if os.path.exists(file_path):
# 如果文件夹存在,则先删除原文件夹在重新创建
rmtree(file_path)
os.makedirs(file_path)
def main():
# 保证随机可复现
random.seed(0)
# 将数据集中10%的数据划分到验证集中
split_rate = 0.1
# 指向你解压后的flower_photos文件夹
cwd = os.getcwd()
data_root = os.path.join(cwd, "flower_data")
origin_flower_path = os.path.join(data_root, "flower_photos")
assert os.path.exists(origin_flower_path), "path '{}' does not exist.".format(origin_flower_path)
flower_class = [cla for cla in os.listdir(origin_flower_path)
if os.path.isdir(os.path.join(origin_flower_path, cla))]
# 建立保存训练集的文件夹
train_root = os.path.join(data_root, "train")
mk_file(train_root)
for cla in flower_class:
# 建立每个类别对应的文件夹
mk_file(os.path.join(train_root, cla))
# 建立保存验证集的文件夹
val_root = os.path.join(data_root, "val")
mk_file(val_root)
for cla in flower_class:
# 建立每个类别对应的文件夹
mk_file(os.path.join(val_root, cla))
for cla in flower_class:
cla_path = os.path.join(origin_flower_path, cla)
images = os.listdir(cla_path)
num = len(images)
# 随机采样验证集的索引
eval_index = random.sample(images, k=int(num*split_rate))
for index, image in enumerate(images):
if image in eval_index:
# 将分配至验证集中的文件复制到相应目录
image_path = os.path.join(cla_path, image)
new_path = os.path.join(val_root, cla)
copy(image_path, new_path)
else:
# 将分配至训练集中的文件复制到相应目录
image_path = os.path.join(cla_path, image)
new_path = os.path.join(train_root, cla)
copy(image_path, new_path)
print("\r[{}] processing [{}/{}]".format(cla, index+1, num), end="") # processing bar
print()
print("processing done!")
if __name__ == '__main__':
main()
如果有,默认使用第一块GPU;如果没有,那就用CPU。
### 1. 电脑如果有GPU的话,就会选择使用GPU,否则就用CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
用transforms方式进行预处理,用compose将各种方法合在一起。
### 2. 图像预处理
data_transform = {
# 训练集图片预处理方式
"train": transforms.Compose([transforms.RandomResizedCrop(224), # 将图像随机裁剪成224*224
transforms.RandomHorizontalFlip(), # 水平翻转
transforms.ToTensor(), # 转换成(C*H*W),并将像素值大小归一化[0,1]
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]), # 将三个通道的像素值标准化[-1,1]
# 验证集图片预处理方式
"val": transforms.Compose([transforms.Resize((224, 224)), # cannot 224, must (224, 224)
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
### 3. 找到训练集
# 返回上一级目录
data_root = os.path.abspath(os.path.join(os.getcwd(), "../..")) # get data root path
# 在上一级目录下寻找data_set文件夹,然后进入其下一级目录flower_data
image_path = os.path.join(data_root, "data_set", "flower_data") # flower data set path
assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
# image_path是指向flower_data文件夹,接着将这个文件夹下的train定义成训练集,然后对训练集的图像根据前面写的data_transform预处理方法处理图像,最后放进train_dataset中。
train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
transform=data_transform["train"])
train_num = len(train_dataset) # 获取训练集数量
### 4. 将类索引写入json文件
# {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
flower_list = train_dataset.class_to_idx # 这里返回一个字典,key是文件夹名,value是顺序值。如上一行所示
cla_dict = dict((val, key) for key, val in flower_list.items()) # 交换键值,主要因为后面的预测值是数字,想输出的时候是花的名称
# write dict into json file 将类索引的字典写入json文件
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
json_file.write(json_str)
生成的json文件如下图所示:
加载训练集
根据找训练集的方式去找到验证集,而且加载进来。
### 5. 加载进训练集和验证集
batch_size = 32 # 设置批次大小
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) # 设置加载数据时的进程数
print('Using {} dataloader workers every process'.format(nw))
# 加载训练集
train_loader = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size, shuffle=True,
num_workers=nw)
# 找到且加载验证集
validate_dataset = datasets.ImageFolder(root=os.path.join(image_path, "val"),
transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
batch_size=4, shuffle=False,
num_workers=nw)
print("using {} images for training, {} images for validation.".format(train_num,
val_num))
net = AlexNet(num_classes=5, init_weights=True) # 定义网络
net.to(device) # 定义训练设备
loss_function = nn.CrossEntropyLoss() # 定义损失函数
# pata = list(net.parameters())
optimizer = optim.Adam(net.parameters(), lr=0.0002) # 定义优化器
### 7. 训练
epochs = 10 # 设置训练轮数
save_path = './AlexNet.pth' # 模型权重保存路径
best_acc = 0.0 # 用于记录最高的准确率
train_steps = len(train_loader) # 训练步数
for epoch in range(epochs):
# train
net.train()
running_loss = 0.0 # 计算平均损失
train_bar = tqdm(train_loader, file=sys.stdout) # 设置训练的进度条
for step, data in enumerate(train_bar):
images, labels = data # 将data分成图像和标签
optimizer.zero_grad() # 梯度清零
outputs = net(images.to(device)) # 正向传播获得输出
loss = loss_function(outputs, labels.to(device)) # 计算损失值
loss.backward() # 损失进行反向传播
optimizer.step() # 更新权重w
# print statistics
running_loss += loss.item() # 累加损失值
# 打印训练进度
train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
epochs,
loss)
# validate
net.eval()
acc = 0.0 # accumulate accurate number / epoch
with torch.no_grad(): # 不计算每个节点的误差梯度
val_bar = tqdm(validate_loader, file=sys.stdout) # 设置进度条
for val_data in val_bar:
val_images, val_labels = val_data # 将val_data分成图像和标签
outputs = net(val_images.to(device)) # 验证集进入网络进行预测,获得所有的预测结果和预测值
predict_y = torch.max(outputs, dim=1)[1] # 找到最大概率的所在索引在哪
acc += torch.eq(predict_y, val_labels.to(device)).sum().item() # 将预测值与真实值进行对比,如果相等,eq(predict_y, val_label)返回为1,否则为0;然后用sum求和求出预测对了多少
val_accurate = acc / val_num # 获得求和数值后再除以总的验证集大小,就是准确率,得到准确率
print('[epoch %d] train_loss: %.3f val_accuracy: %.3f' %
(epoch + 1, running_loss / train_steps, val_accurate)) # 打印这一轮的损失值和准确率
if val_accurate > best_acc: # 为了最终得到的是在验证集上准确率最高的模型参数,然后进行保存
best_acc = val_accurate
torch.save(net.state_dict(), save_path)
print('Finished Training')
import os
import json
import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
from model import AlexNet
def main():
### 1、寻找可使用的GPU,如果没有就用cpu
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
### 2、 定义图像预处理方式
data_transform = transforms.Compose(
[transforms.Resize((224, 224)), # 将图像resize成224*224的
transforms.ToTensor(), # 变成C*H*W格式,并且归一化
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) # 标准化
### 3、加载进需要的预测的图像
img_path = "rose.jpeg"
assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
img = Image.open(img_path)
### 4、图像预处理
plt.imshow(img) # 显示图像
# [N, C, H, W]
img = data_transform(img) # 预处理
# expand batch dimension
img = torch.unsqueeze(img, dim=0) # 拓展成四维,增加一个批次纬度
### 5、获取类别
# 加载json文件获取类别
json_path = './class_indices.json'
assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
with open(json_path, "r") as f:
class_indict = json.load(f)
### 6、定义模型,加载权重,然后预测
# 定义模型
model = AlexNet(num_classes=5).to(device)
# 加载模型权重
weights_path = "./AlexNet.pth"
assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
model.load_state_dict(torch.load(weights_path))
# 和训练那里一样,不计算误差梯度
model.eval()
with torch.no_grad():
# predict class
output = torch.squeeze(model(img.to(device))).cpu() # 通过模型进行正向传输
predict = torch.softmax(output, dim=0) # 将预测结果的概率值变成一个正太分布
predict_cla = torch.argmax(predict).numpy() # 找到最大的预测结果的索引值
print_res = "class: {} prob: {:.3}".format(class_indict[str(predict_cla)],
predict[predict_cla].numpy()) # 在json文件里找到索引对应的种类名称打印出来,而且加上概率值
plt.title(print_res) # 显示的图片名称就是预测的种类名称
for i in range(len(predict)): # 打印出来每种类别的概率
print("class: {:10} prob: {:.3}".format(class_indict[str(i)],
predict[i].numpy()))
plt.show()
if __name__ == '__main__':
main()
预测图片:
准确率和轮数的关系:
损失值和轮数的关系:
网络架构: