一、网络的亮点
1、首次利用GPU进行网络加速训练
2、使用了ReLU激活函数,而不是传统的Sigmoid激活函数以及Tanh激活函数
3、使用了LRN局部相应归一化
4、在全连接层的前两层使用了Dropout随机失活神经元操作,以减少过拟合
二、Sigmoid、Tanh、ReLU激活函数介绍
1、Sigmoid激活函数
Sigmoid激活函数是一种常用的非线性激活函数,它将实数映射到区间(0, 1)上。其公式为
其中 e 是自然对数的底数。
Sigmoid函数在深度学习中常用于二分类问题的输出层,也可以用于多分类问题的多个输出层。它的输出值介于0和1之间,可以被解释为概率值,使得模型输出更符合概率分布的特性。Sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似。如果我们将输出视为二分类概率问题时, Sigmoid仍然被⼴泛⽤作输出单元上的激活函数。
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-10, 10, 1000)
y = 1 / (1 + np.exp(-x))
plt.figure(figsize=(8, 6))
plt.plot(x, y)
plt.title("Sigmoid Activation Function")
plt.xlabel("Input")
plt.ylabel("Output")
plt.grid(True)
plt.show()
2、tanh激活函数
tanh激活函数是一种非线性激活函数,它使用双曲正切函数将实数值压缩到-1到1的区间内。公式为:
Sigmoid函数类似,tanh函数也可以用于预测概率的输出层,但它更适用于隐藏层。
import numpy as np
import matplotlib.pyplot as plt
# 生成x的取值范围
x = np.linspace(-10, 10, 1000)
# 计算tanh函数的值
y = np.tanh(x)
# 绘制图像
plt.plot(x, y)
plt.title("tanh activation function")
plt.xlabel("x")
plt.ylabel("tanh(x)")
plt.grid(True)
plt.show()
3、ReLU激活函数
ReLU(Rectified Linear Unit)是一种常用的激活函数,其主要特点是对输入的小于0的数值进行截断,保留大于0的数值。具体来说,ReLU函数的数学表达式为:
这意味着,当输入x大于0时,ReLU函数的输出就是输入x;而当输入x小于0时,ReLU函数的输出为0。
ReLU激活函数具有以下优点:
(1)计算效率高:因为ReLU函数在大于0的区域内是线性的,所以计算相对简单快速。
(2)缓解了梯度消失问题:相比Sigmoid和Tanh等激活函数,ReLU函数的导数在大于0的区域内始终为1,不会随着输入值变大而趋近于0,这在一定程度上缓解了深度神经网络中常见的梯度消失问题。
(3)有利于模型学习:ReLU激活函数使得神经网络更倾向于学习那些在训练集中出现过的、大于0的输入,这可能有助于模型更好地捕捉训练数据中的一些特性。
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-10, 10, 1000)
y = np.maximum(0, x)
plt.plot(x, y)
plt.title("ReLU Activation Function")
plt.xlabel("Input")
plt.ylabel("Output")
plt.show()
三、什么是过拟合?
过拟合的根本原因是特征维度太多,模型假设过于复杂,参数过多,训练数据过少,噪声过多,导致拟合的函数完美的预测了训练集,但对于新数据的预测结果相对来说比较差。过度的拟合了训练数据,而没有考虑到模型的泛化能力。
四、网络层结构
layer_name | kernel_size | kernels | padding | stride |
Conv1 | 11 | 96 | [1,2] | 4 |
Maxpool1 | 3 | None | 0 | 2 |
Conv2 | 5 | 256 | [2,2] | 1 |
Maxpool2 | 3 | None | 0 | 2 |
Conv3 | 3 | 384 | [1,1] | 1 |
Conv4 | 3 | 384 | [1,1] | 1 |
COnv5 | 3 | 256 | [1,1] | 1 |
Maxpool3 | 3 | None | 0 | 2 |
FC | 2048 | None | None | None |
FC | 2048 | None | None | None |
FC | 1000 | None | None | None |
五、模型代码解读(Module)
模型的代码(含注释)如下:
import torch.nn as nn # 导入Pytorch的神经网络模块
import torch # 导入Pytorch库
# 定义AlexNet类,继承于nn.Module这个父类
class AlexNet(nn.Module):
# 定义构造函数,接收两个参数:num_classes表示分类的类别数,默认为1000;init_weights表示是否初始化权重,默认为False。
def __init__(self, num_classes=1000, init_weights=False):
super(AlexNet, self).__init__() # 调用父类的构造函数
# 定义特征提取部分,使用nn.Sequential容器来保存一系列的层
self.features = nn.Sequential(
# 卷积层,输入通道数为3,输入图像大小224×224,输出通道数为48,卷积核大小为11x11,步长为4,填充为2。输出特征图大小为55x55
nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True), # ReLU激活函数
# MaxPooling池化层,池化窗口大小为3x3,步长为2。输出特征图大小为27x27
nn.MaxPool2d(kernel_size=3, stride=2),
# 卷积层,输入通道数为48,输出通道数为128,卷积核大小为5x5,填充为2。输出特征图大小为27x27
nn.Conv2d(48, 128, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
# MaxPooling池化层,输出特征图大小为13x13
nn.MaxPool2d(kernel_size=3, stride=2),
# 卷积层,输入通道数为128,输出通道数为192,卷积核大小为3x3,填充为1。输出特征图大小为13x13
nn.Conv2d(128, 192, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
# 卷积层,输入通道数为192,输出通道数为192,卷积核大小为3x3,填充为1。输出特征图大小为13x13
nn.Conv2d(192, 192, kernel_size=3, padding=1), # output[192, 13, 13]
nn.ReLU(inplace=True),
# 卷积层,输入通道数为192,输出通道数为128,卷积核大小为3x3,填充为1。输出特征图大小为13x13
nn.Conv2d(192, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
# MaxPooling池化层,输出特征图大小为6x6
nn.MaxPool2d(kernel_size=3, stride=2),
)
# 定义分类器部分,使用nn.Sequential容器来保存一系列的层
self.classifier = nn.Sequential(
nn.Dropout(p=0.5), # Dropout层,保留概率为0.5
nn.Linear(128 * 6 * 6, 2048), # 全连接层,输入特征维度为128*6*6,输出特征维度为2048
nn.ReLU(inplace=True), # ReLU激活函数
nn.Dropout(p=0.5), # Dropout层,保留概率为0.5
nn.Linear(2048, 2048), # 全连接层,输入特征维度为2048,输出特征维度为2048
nn.ReLU(inplace=True), # ReLU激活函数
nn.Linear(2048, num_classes), # 全连接层,输入特征维度为2048,输出特征维度为num_classes(即分类的类别数)
)
# 如果参数init_weights为True,则初始化权重
if init_weights:
self._initialize_weights() # 调用自定义的函数
# 定义forward方法,模型的正向传播过程
def forward(self, x):
x = self.features(x) # 输入数据x进行特征提取层的处理
x = torch.flatten(x, start_dim=1) # 将特征提取处理过后的x进行展平操作
x = self.classifier(x) # 通过分类器进行分类预测
return x
# 定义_initialize_weights方法,用来初始化模型权重
def _initialize_weights(self):
# 遍历模型的所有模块
for m in self.modules():
# 如果当前模块是2D卷积,则用kaiming正常分布来初始化权重
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
# 如果偏置存在,则将偏置初始化为0
if m.bias is not None:
nn.init.constant_(m.bias, 0)
# 如果模块是线性层(nn.Linear)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01) # 使用标准差为0.01的正态分布来初始化权重
nn.init.constant_(m.bias, 0) # 将偏置初始化为0
六、训练代码解读(train)
import os # 导入操作系统相关的功能
import sys # 导入系统相关的功能
import json # 导入json相关的功能
import torch # 导入Pytorch库,用于深度学习
import torch.nn as nn # 导入Pytorch的神经网络模块
from torchvision import transforms, datasets, utils # 导入torchvision库,用于图像处理和数据加载
import matplotlib.pyplot as plt # 导入matplotlib库,用于图像可视化
import numpy as np # 导入numpy库,用于数学计算
import torch.optim as optim # 导入pytorch的优化器
from tqdm import tqdm # 导入进度条库,用于显示训练进度
from model import AlexNet # 导入自定义的AlexNet模型
# 定义主函数
def main():
# 选择设备,如果有可用的GPU则使用GPU,如果没有GPU则使用CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
# 定义数据预处理操作
data_transform = {
# 定义训练集的一系列转换操作
"train": transforms.Compose([transforms.RandomResizedCrop(224), # 随机裁剪,并缩放到224×224大小
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ToTensor(), # 转换为tensor张量
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]), # 将像素值归一化
# 定义验证集的一系列操作
"val": transforms.Compose([transforms.Resize((224, 224)), # 将图像调整为224×224
transforms.ToTensor(), # 转换为张量
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])} # 像素值归一化
# 获得数据集根目录所在路径
data_root = os.path.abspath(os.path.join(os.getcwd(), "F:\deep-learning-for-image-processing-master"))
# 获取flower数据集的路径
image_path = os.path.join(data_root, "data_set", "flower")
# 判断路径是否存在
assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
# 使用ImageFolder加载训练集,传入两个参数,路径和预处理的操作
train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
transform=data_transform["train"])
# 获取数据集的数量
train_num = len(train_dataset)
# {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
# 获取类别到索引的映射字典
flower_list = train_dataset.class_to_idx
# 将映射翻转,索引到类别的映射
cla_dict = dict((val, key) for key, val in flower_list.items())
# 将字典转换成接送字符串,并缩进4个空格方便阅读
json_str = json.dumps(cla_dict, indent=4)
# 以写入模式打开文件,将json字符串写入文件
with open('class_indices.json', 'w') as json_file:
json_file.write(json_str)
batch_size = 16 # 设置每一个批次多少样本
# 获取可用处理器的数量
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))
# 创建验证集数据迭代器
test_data_iter = iter(validate_loader)
# 获取一个批次的验证集样本数据和标签
test_image, test_label = test_data_iter.next()
# 显示图像函数
def imshow(img):
img = img / 2 + 0.5 # 将图像进行反标准化操作(逆归一化)
npimg = img.numpy() # 将图像转换为numpy数组
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 打印测试集中前四个样本的类别标签,以便检查数据加载是否正确
print(' '.join('%5s' % cla_dict[test_label[j].item()] for j in range(4)))
# 显示测试图像:将测试集中的图像使用make_grid函数组合成一个大图
imshow(utils.make_grid(test_image))
# 创建一个AlexNet网络模型,设置类别数量为5,同时初始化权重
net = AlexNet(num_classes=5, init_weights=True)
# 将模型指认到设备上,有GPU指认GPU
net.to(device)
# 定义损失函数,使用交叉熵损失函数作为优化目标
loss_function = nn.CrossEntropyLoss()
# 使用Adam优化器来优化网络参数,设置学习率为0.0002
optimizer = optim.Adam(net.parameters(), lr=0.0002)
epochs = 10 # 设置训练轮数
save_path = './AlexNet.pth' # 制定模型保存的路径
best_acc = 0.0 # 定义最佳准确率,用来保存性能最优的参数
train_steps = len(train_loader)
# 训练循环
for epoch in range(epochs):
net.train() # 将网络设置为训练模式train
running_loss = 0.0 # 初始化训练损失为0
# 遍历训练集,使用train_loader提供的数据
train_bar = tqdm(train_loader, file=sys.stdout)
for step, data in enumerate(train_bar):
images, labels = data # 从data数据中解包出images和labels
optimizer.zero_grad() # 清零优化器的梯度,以准备计算新的梯度
outputs = net(images.to(device)) # 将输入图像传入网络模型,并在指定设备上计算
loss = loss_function(outputs, labels.to(device)) # 使用交叉熵损失函数loss_function计算模型输出与真实标签的损失
loss.backward() # 根据损失计算参数的梯度,进行反向传播
optimizer.step() # 通过计算得到的梯度,通过优化器更新网络模型的参数
# 累计当前批次的损失值,用于统计训练损失
running_loss += loss.item()
# 更新进度条train_bar的描述,显示当前训练周期、总周期数以及当前批次的损失值
train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
epochs,
loss)
net.eval() # 将网络设置为验证模式
acc = 0.0 # 定义一个变量acc,用于累计当前训练周期内正确分类的数量
# 关闭梯度计算
with torch.no_grad():
# 创建一个进度条val_bar,用于可视化验证集的遍历进度
val_bar = tqdm(validate_loader, file=sys.stdout)
# 遍历验证数据集迭代器
for val_data in val_bar:
val_images, val_labels = val_data # 从验证数据集中解包出验证图像和验证标签
outputs = net(val_images.to(device)) # 将验证图像传入网络,并得到模型输出
predict_y = torch.max(outputs, dim=1)[1] # 使用torch.max函数在模型输出中获取每个样本最大值的索引,即预测的类别
# 将预测的类别与真实标签进行比较,统计正确分类的数量,累加到acc中
acc += torch.eq(predict_y, val_labels.to(device)).sum().item()
# 计算验证准确率,用正确分类的数量除以验证集的总数
val_accurate = acc / val_num
# 打印训练周期结束后的训练损失和验证准确率
print('[epoch %d] train_loss: %.3f val_accuracy: %.3f' %
(epoch + 1, running_loss / train_steps, val_accurate))
# 如果当前验证的准确率大于历史最佳准确率,则更新best_acc并保存模型参数到指定的路径save_path
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(net.state_dict(), save_path)
print('Finished Training')
if __name__ == '__main__':
main()
名词解释:
1、反标准化(逆归一化)
反标准化(De-normalization)操作是在对数据进行归一化(标准化)后,将其恢复到原始数据的操作。在机器学习和深度学习中,数据标准化是一种常见的预处理步骤,用于将数据的特征值缩放到一个较小的范围,以帮助模型的训练和优化。
反标准化操作通常在预测阶段或结果展示阶段进行,以便将标准化后的结果转换回原始数据的范围。这是因为模型在训练过程中使用标准化后的数据进行训练,但最终的预测或展示需要在原始数据的尺度上进行。
在图像处理中,像素值通常在0到255之间,而在训练神经网络时,可以将像素值标准化到[-1, 1]或[0, 1]的范围,以便更好地进行训练。在进行预测时,为了正确地展示图像或计算一些指标,可能需要将预测结果进行反标准化,使其回到原始像素值的范围。
反标准化的具体方法取决于标准化时使用的方法,例如,如果在标准化时使用了均值和标准差来缩放数据,那么在反标准化时就需要使用相应的均值和标准差来还原数据。
2、张量
在计算机编程和数学领域,张量(Tensor)是一个广义的矩阵概念,它是一个多维数组,可以是一个标量(零维数组,即单个值)、向量(一维数组)、矩阵(二维数组)以及更高维度的数组。张量是一个非常通用的数据结构,它可以用来表示各种数据,例如图像、音频、文本和其他复杂的结构化数据。
在深度学习和机器学习中,张量是非常重要的数据类型,因为神经网络模型的输入、输出和参数通常都是张量。PyTorch 和 TensorFlow 等深度学习框架中都有张量的概念,用于存储和处理模型的数据。
以下是一些不同维度的张量示例:
(1)标量(0维张量):单个值,如一个数字。
(2)向量(1维张量):一列值,类似于列表或数组。
(3)矩阵(2维张量):二维数组,如表格数据。
(4)3维张量:例如彩色图像,具有高度、宽度和通道维度。
(5)更高维度的张量:在深度学习中,常常处理具有多个特征维度的数据,如序列数据(时间序列、文本序列等)。
在PyTorch中,张量是 torch.Tensor
类的实例,可以使用这个类创建、操作和处理张量。张量不仅可以包含数据,还可以包含用于自动求导(反向传播)的梯度信息。
七、预测代码解读(predict)
import os # 导入操作系统相关的模块
import json # 导入json模块,用于处理json格式的数据
import torch # 导入torch模块,实现深度学习算法和计算
from PIL import Image # 导入PIL模块中的Image模块,用于图像处理
from torchvision import transforms # 导入torchvision模块中的transforms模块,用于图像的预处理
import matplotlib.pyplot as plt # 导入matplotlib模块的pyplot模块,用于数据可视化
from model import AlexNet # 从model中导入AlexNet模块,图像分类
def main():
# 判断是否有可用的GPU,如果有则将设备设置为GPU,如果没有则设置为CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 创建一个transform序列,这个序列包含以下三个操作
# 1、将图像大小调整为224×244
# 2、将图像数据转换为张量
# 3、对图像的每一个通道进行归一化,将每一个通道的值从[0,1]缩放到[0.5,0.5]
data_transform = transforms.Compose(
[transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 定义图像的路径,假设图像位于程序运行的当前目录的上一级目录中
img_path = "../tulip.jpg"
# 检查文件是否存在,如果不存在则抛出错误
assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
# 使用PIL的Image模块打开图像
img = Image.open(img_path)
# 使用matplotlib的pyplot模块显示原始图像
plt.imshow(img)
# 将图像数据转换为张量并且进行归一化处理
# [N, C, H, W]是张量的维度,N为批量大小(这里只有一个样本),C是通道数,H是高度,W是宽度
img = data_transform(img)
# 在张量的维度上增加一个维度,以使其可以作为网络的输入
img = torch.unsqueeze(img, dim=0)
# 读取类别索引的json文件,在这里假设已经位于当前运行目录中
json_path = './class_indices.json'
# 判断json文件是否存在
assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
# 打开json文件并加载其中的数据
with open(json_path, "r") as f:
class_indict = json.load(f)
# 创建一个AlexNet模型,设置类别为5,并设置在指定的设备上运行
model = AlexNet(num_classes=5).to(device)
# 加载预训练权重
weights_path = "./AlexNet.pth"
# 判断文件是否存在
assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
# 加载模型的权重到模型的state_dict中
model.load_state_dict(torch.load(weights_path))
# 设置模型为验证模式
model.eval()
# 不计算梯度,因为我们不训练,只是预测
with torch.no_grad():
# 将输入图像img转移到指定的设备(这里为CPU)并传递给模型model进行前向传播
output = torch.squeeze(model(img.to(device))).cpu()
# 使用torch.softmax函数将输出向量转换为概率分布,表示每个类别的概率
predict = torch.softmax(output, dim=0)
# 使用torch.argmax函数找到概率最高的类别索引,并将其转换为numpy数组
predict_cla = torch.argmax(predict).numpy()
print_res = "class: {} prob: {:.3}".format(class_indict[str(predict_cla)],
predict[predict_cla].numpy())
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()