继续跟着小土堆去学习机器学习
文章目录
- Flatten
- 1. `Flatten` 的作用
- 2. 何时使用 `Flatten`
- 3. PyTorch 中的 `Flatten`
- Sequentia
- 优化器
- 模型的保存与加载
- 模型的完整训练
Flatten
在神经网络中,Flatten
操作是将高维的输入(如二维图像或三维特征图)转换为一维向量的过程。这个步骤通常出现在卷积神经网络(CNN)的中后部分,用于连接卷积层或池化层的输出与全连接层(Fully Connected Layer, FC)。
1. Flatten
的作用
在卷积神经网络中,输入通常是多维数据,比如图像会表示为三维张量(例如,RGB图片的格式为 [Height, Width, Channels])。卷积层或池化层处理之后,输出的仍然是高维的特征图。为了能够与全连接层相连,需要将这些高维特征图转换为一维向量,这就是 Flatten
的作用。
示例:
假设一个卷积层的输出形状是 (batch_size, channels, height, width)
,比如 (64, 6, 30, 30)
,在使用 Flatten
后,会将这个输出转换为 (batch_size, 6 * 30 * 30)
,即 (64, 5400)
,这样可以输入到全连接层中。
2. 何时使用 Flatten
Flatten
通常在以下场景中使用:
- 卷积神经网络的最后阶段:在卷积层或池化层处理完特征图后,通常会在全连接层之前使用
Flatten
,将特征图展开为一维向量。 - 序列或分类问题:全连接层一般用于序列生成或分类任务,输入要求是向量形式,而不是多维的张量。
3. PyTorch 中的 Flatten
在 PyTorch 中,Flatten
操作可以使用 torch.flatten()
或者直接在模型中使用 nn.Flatten()
层。
代码示例:
import torch
import torch.nn as nn
# 定义一个简单的卷积神经网络
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
# 定义一个卷积层
self.conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=1)
# 定义一个最大池化层
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
# 定义一个Flatten层
self.flatten = nn.Flatten()
# 定义一个全连接层
self.fc = nn.Linear(6 * 16 * 16, 10) # 假设输入图像是32x32,经过池化后尺寸为16x16
def forward(self, x):
x = self.conv1(x) # 卷积层
x = self.pool(x) # 池化层
x = self.flatten(x) # 展平特征图
x = self.fc(x) # 全连接层
return x
# 创建网络实例
model = SimpleCNN()
# 随机生成一个输入张量,模拟输入32x32的RGB图像
input_tensor = torch.randn(1, 3, 32, 32)
# 前向传播
output = model(input_tensor)
print(output.shape) # 输出的张量形状
在这个例子中,Flatten
将卷积层的输出从 (batch_size, channels, height, width)
转换为一维向量 (batch_size, flattened_size)
。其中 flattened_size = channels * height * width
,从而可以输入到全连接层中。
Sequentia
import torch
from torch import nn # 导入PyTorch中的神经网络模块
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential # 导入必要的网络层
from torch.utils.tensorboard import SummaryWriter # 导入用于TensorBoard可视化的工具
# 定义神经网络模型类,继承自nn.Module
class Tudui(nn.Module):
def __init__(self):
# 调用父类的初始化方法
super(Tudui, self).__init__()
# 使用Sequential容器将多个层按顺序构建
self.model1 = Sequential(
# 第一个卷积层:输入通道3(对应RGB图像),输出通道32,卷积核大小5x5,填充为2(保持图像大小)
Conv2d(3, 32, 5, padding=2),
# 第一个池化层:使用2x2的最大池化层来缩小图像尺寸
MaxPool2d(2),
# 第二个卷积层:输入通道32,输出通道仍为32,卷积核大小5x5,填充为2
Conv2d(32, 32, 5, padding=2),
# 第二个池化层:继续使用2x2的最大池化
MaxPool2d(2),
# 第三个卷积层:输入通道32,输出通道64,卷积核大小5x5,填充为2
Conv2d(32, 64, 5, padding=2),
# 第三个池化层:再次使用2x2的最大池化
MaxPool2d(2),
# 展平层:将3D的特征图展开为1D的向量,便于传入全连接层
Flatten(),
# 全连接层:将展平后的特征图(大小为1024)映射到64维向量
Linear(1024, 64),
# 全连接层:将64维向量映射到最终的10类(假设是10分类任务)
Linear(64, 10)
)
# 定义前向传播的过程
def forward(self, x):
x = self.model1(x) # 将输入x依次传入self.model1中的各层
return x # 返回最终的输出
# 实例化网络模型
tudui = Tudui()
# 打印模型结构,方便检查各层的参数与连接情况
print(tudui)
# 创建一个大小为[64, 3, 32, 32]的输入张量,表示批量大小为64,3通道的32x32图像
input = torch.ones((64, 3, 32, 32))
# 将输入张量传入模型,获取输出
output = tudui(input)
# 打印输出的形状,通常是[64, 10],即64个样本,每个样本对应10类的输出
print(output.shape)
# 创建TensorBoard的SummaryWriter实例,指定日志保存的路径
writer = SummaryWriter("../logs_seq")
# 将模型图添加到TensorBoard中进行可视化
writer.add_graph(tudui, input)
# 关闭writer,释放资源
writer.close()
优化器
优化器就是更新参数和学习率
import torch # 导入PyTorch库
import torchvision # 导入用于计算机视觉任务的工具包
from torch import nn # 导入神经网络模块
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear # 导入需要使用的网络层
from torch.optim.lr_scheduler import StepLR # 导入学习率调度器
from torch.utils.data import DataLoader # 导入数据加载器
# 下载并加载CIFAR-10数据集,将图像转换为张量格式
dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
# 使用DataLoader来按批次加载数据
dataloader = DataLoader(dataset, batch_size=1) # 设置batch_size为1,意味着一次加载一张图片
# 定义一个卷积神经网络模型类
class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
# 使用Sequential容器将多个层按顺序连接
self.model1 = Sequential(
Conv2d(3, 32, 5, padding=2), # 第一个卷积层:输入3通道(RGB),输出32通道,卷积核5x5,填充2
MaxPool2d(2), # 第一个最大池化层:2x2池化
Conv2d(32, 32, 5, padding=2), # 第二个卷积层:输入32通道,输出32通道,卷积核5x5,填充2
MaxPool2d(2), # 第二个最大池化层
Conv2d(32, 64, 5, padding=2), # 第三个卷积层:输入32通道,输出64通道
MaxPool2d(2), # 第三个最大池化层
Flatten(), # 展平:将多维特征图展开为一维向量
Linear(1024, 64), # 全连接层:将1024维的特征向量映射到64维
Linear(64, 10) # 全连接层:将64维向量映射到10类(CIFAR-10数据集的类别数)
)
# 前向传播过程
def forward(self, x):
x = self.model1(x) # 将输入x依次传入网络
return x # 返回输出
# 定义损失函数为交叉熵损失,用于分类任务
loss = nn.CrossEntropyLoss()
# 实例化神经网络模型
tudui = Tudui()
# 定义优化器,使用随机梯度下降(SGD),学习率设为0.01
optim = torch.optim.SGD(tudui.parameters(), lr=0.005)#经常会出现nan 和loss变化为0的情况,这时候就得要调参了。感觉0.01,0.001,0.1,都不太好,神经网络经常发神经
# 定义学习率调度器,优化器,使用StepLR,每5个epoch将学习率乘以0.1
scheduler = StepLR(optim, step_size=5, gamma=0.1)
# 训练过程,设定20个epoch
for epoch in range(20):
running_loss = 0.0 # 初始化每个epoch的累计损失,每次计算都要loss清零
for data in dataloader: # 遍历每一批数据
imgs, targets = data # imgs是输入图像,targets是对应的标签
outputs = tudui(imgs) # 将图像输入网络,得到预测输出
result_loss = loss(outputs, targets) # 计算损失
optim.zero_grad() # 优化器梯度清零,防止累加
result_loss.backward() # 反向传播,计算梯度
optim.step() # 小土堆源码没有,注意 更新权重
running_loss += result_loss.item() # 累加损失值
scheduler.step() # 那小土堆的改到epoch的代码,更新学习率
print(running_loss) # 打印每个epoch的累计损失
运行结果也还行
Files already downloaded and verified
18864.705932210665
15439.860634332174
13744.55594523397
12428.718186999993
11342.776699185897
7056.810089337026
5552.8082057812735
4661.32626584378
3898.5063211931256
3208.6409183643354
2436.161548842425
2299.006631286251
2211.327144692005
2136.3464579411434
2067.7032547439658
1984.4474980983596
1975.8219145699131
1968.4251595352762
1961.4812723635232
1954.759079959732
感觉后面学习有点吃力,就把gamma值改成0.5,确实感觉效果更好。调参技能get点
Files already downloaded and verified
18972.406568901613
15460.233340543578
13756.254361514979
12417.585550802492
11310.643081888893
7457.094932298466
5994.618627047323
5507.483447367285
5794.649616663532
4877.494456583252
2490.7070572731336
955.5687210418287
371.26659479515604
169.87884318090371
72.07394882918663
42.53886234954232
33.56396412941994
28.734524085854517
25.233990153099555
22.519621285593622
好奇0.3会不会更好点,果然还是差点,前面速度还不错,后面效果有点拉跨。
Files already downloaded and verified
18824.60679809982
15556.305155576905
13735.934430592446
12342.659838442378
11285.891235672563
6900.019034815649
5026.226464926721
3598.409451258059
2544.912955761785
2417.9225108390256
1447.369545274334
549.7065285773949
312.4407196212337
210.27363093647688
156.48747144329707
126.68451174328214
117.18196939325574
109.68678552139573
103.18362671615421
97.42433589926554
模型的保存与加载
# 导入 PyTorch 和 torchvision 库
import torch
import torchvision
from torch import nn
# 加载 VGG16 模型结构,设置 pretrained=False 表示不使用预训练权重
vgg16 = torchvision.models.vgg16(pretrained=False)
# 保存方式1: 保存整个模型(包括结构和参数)这种方法简单,但在加载时需要使用相同的代码来重新定义模型结构。
torch.save(vgg16, "vgg16_method1.pth")
# 保存方式2: 只保存模型的参数(推荐使用此方法),参数保存成字典形式,,空间更小这是官方推荐的方式,因为它更灵活且易于管理。
torch.save(vgg16.state_dict(), "vgg16_method2.pth")
# 定义一个简单的自定义模型类 Tudui
class Tudui(nn.Module):
def __init__(self):
# 调用父类的构造函数
super(Tudui, self).__init__()
# 定义一个卷积层,输入通道数为 3,输出通道数为 64,卷积核大小为 3
self.conv1 = nn.Conv2d(3, 64, kernel_size=3)
def forward(self, x):
# 在前向传播中应用卷积层
x = self.conv1(x)
return x
# 实例化 Tudui 模型
tudui = Tudui()
# 保存方式1: 保存整个自定义模型(包括结构和参数)
torch.save(tudui, "tudui_method1.pth")
model=torch.load('tudui_method1.pth')
print(model)
虽然保存整个模型方便,但在模型结构发生变化时会导致加载失败。使用 state_dict 保存参数是更为推荐的做法,因为它只保存模型的参数,不依赖于模型的代码结构。
模型的完整训练
with 是 Python 中的一个关键字,用于简化资源管理和异常处理。
进入 with 块时,torch.no_grad() 会自动调用其 enter 方法,禁用梯度计算;当退出块时,会调用 exit 方法,恢复梯度计算状态。在模型推理阶段(例如,验证或测试模型时),通常不需要计算梯度,因为不需要进行反向传播。禁用梯度计算可以提高推理的效率,减少内存占用。
示例 1:文件操作
使用 with 语句打开文件,可以确保在处理完文件后自动关闭文件,而不需要显式调用 close() 方法。
不使用GPU,大概是10分钟,使用GPU,速度加快
import torchvision # 用于数据集和图像处理
from torch.utils.tensorboard import SummaryWriter # 用于可视化训练过程。*:这个符号表示导入该模块中的所有公开对象(例如类、函数、变量等)。这意味着,你可以直接使用 model.py 中定义的任何内容,而无需指定模块名。
from torch import nn # 导入 PyTorch 的神经网络模块
import torch
from torch.utils.data import DataLoader # 导入数据加载器
import time
train_data = torchvision.datasets.CIFAR10(
root="../data",
train=True,
transform=torchvision.transforms.ToTensor(), # 将图像转换为 Tensor
download=True # 如果数据集未下载,则自动下载
)
test_data = torchvision.datasets.CIFAR10(
root="../data",
train=False,
transform=torchvision.transforms.ToTensor(), # 将图像转换为 Tensor
download=True # 如果数据集未下载,则自动下载
)
# 获取数据集长度,图片张数
train_data_size = len(train_data) # 训练数据集的大小
test_data_size = len(test_data) # 测试数据集的大小
# 打印数据集长度
print("训练数据集的长度为:{}".format(train_data_size))#将大括号内的内容替换为train_data_size
print("测试数据集的长度为:{}".format(test_data_size))
# 利用 DataLoader 加载数据集,设置批大小为 64
train_dataloader = DataLoader(train_data, batch_size=64, shuffle=True) # 随机打乱训练数据
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=False) # 测试数据一般不打乱
# 定义一个名为 Tudui 的类,继承自 nn.Module
class Tudui(nn.Module):
def __init__(self):
# 初始化父类 nn.Module
super(Tudui, self).__init__()
# 定义一个顺序模型,包含多个层
self.model = nn.Sequential(
# 第一个卷积层:接收 3 通道的输入(RGB图像),输出 32 通道,卷积核大小为 5,步长为 1,填充为 2
nn.Conv2d(3, 32, 5, 1, 2),
# 第一个最大池化层:池化窗口大小为 2,减小特征图的尺寸
nn.MaxPool2d(2),
# 第二个卷积层:接收 32 通道的输入,输出 32 通道,卷积核大小为 5,步长为 1,填充为 2
nn.Conv2d(32, 32, 5, 1, 2),
# 第二个最大池化层
nn.MaxPool2d(2),
# 第三个卷积层:接收 32 通道的输入,输出 64 通道,卷积核大小为 5,步长为 1,填充为 2
nn.Conv2d(32, 64, 5, 1, 2),
# 第三个最大池化层
nn.MaxPool2d(2),
# 将多维输入展平为一维,以便输入全连接层
nn.Flatten(),
# 第一个全连接层:输入为展平后的特征,输出 64 个特征
nn.Linear(64 * 4 * 4, 64),
# 第二个全连接层:输出 10 个特征(适用于10类分类任务,如CIFAR-10)
nn.Linear(64, 10)
)
# 定义前向传播的方法
def forward(self, x):
# 将输入 x 传递通过模型,得到输出
x = self.model(x)
# 返回输出
return x
# 主程序入口
if __name__ == '__main__':
# 实例化自定义模型 Tudui
tudui = Tudui()
# 创建一个输入张量,大小为 (64, 3, 32, 32)
# 这里表示 64 张 RGB 图像,每张图像的尺寸为 32x32 像素
input = torch.ones((64, 3, 32, 32))
# 将输入张量传递给模型,得到输出
output = tudui(input)
# 打印输出的形状,应该是 (64, 10),表示每张输入图像对应 10 个类的预测
print(output.shape) # 输出形状应为 torch.Size([64, 10])
# 创建网络模型
tudui = Tudui() # 实例化自定义模型 Tudui
tudui=tudui.cuda()
# 定义损失函数,使用交叉熵损失
loss_fn = nn.CrossEntropyLoss()
loss_fn=loss_fn.cuda()
# 定义优化器,使用 SGD 优化器,学习率设置为 0.01
learning_rate = 1e-2
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate)
# 设置训练网络的一些参数
total_train_step = 0 # 记录训练的次数
total_test_step = 0 # 记录测试的次数
epoch = 20 # 训练的轮数
start_time=time.time()
# 添加 TensorBoard 记录器
writer = SummaryWriter("../logs_train") # 指定日志目录
# 训练过程
for i in range(epoch): # 遍历每一个训练轮次
print("-------第 {} 轮训练开始-------".format(i + 1))
# 训练步骤开始
tudui.train() # 设置模型为训练模式,启用 Dropout 和 BatchNorm
for data in train_dataloader: # 遍历训练数据
imgs, targets = data # 拆分输入图像和目标标签
imgs=imgs.cuda()
targets=targets.cuda()
outputs = tudui(imgs) # 前向传播,获取模型输出
loss = loss_fn(outputs, targets) # 计算损失
# 优化器优化模型
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新模型参数
total_train_step += 1 # 增加训练次数计数
if total_train_step % 100 == 0: # 每 100 次训练打印一次损失
end_time=time.time()
print(end_time-start_time)
print("训练次数:{}, Loss: {}".format(total_train_step, loss.item())) # 打印当前训练步数和损失值
writer.add_scalar("train_loss", loss.item(), total_train_step) # 记录训练损失到 TensorBoard
# 测试步骤开始
tudui.eval() # 设置模型为评估模式,禁用 Dropout 和 BatchNorm
total_test_loss = 0 # 初始化测试损失
total_accuracy = 0 # 初始化正确率计数
with torch.no_grad(): # 禁用梯度计算,减少内存使用
for data in test_dataloader: # 遍历测试数据
imgs, targets = data # 拆分输入图像和目标标签
imgs=imgs.cuda()
targets=targets.cuda()
outputs = tudui(imgs) # 前向传播,获取模型输出
loss = loss_fn(outputs, targets) # 计算损失
total_test_loss += loss.item() # 累加测试损失
accuracy = (outputs.argmax(1) == targets).sum() # 计算正确预测的数量
total_accuracy += accuracy.item() # 累加正确预测数量
# 打印测试结果
print("整体测试集上的Loss: {}".format(total_test_loss)) # 打印测试损失
print("整体测试集上的正确率: {}".format(total_accuracy / test_data_size)) # 计算并打印准确率
writer.add_scalar("test_loss", total_test_loss, total_test_step) # 记录测试损失
writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step) # 记录测试准确率
total_test_step += 1 # 增加测试次数计数
# 保存模型
torch.save(tudui, "tudui_{}.pth".format(i)) # 每个 epoch 保存一次模型
print("模型已保存")
writer.close() # 关闭 TensorBoard 记录器
还是学到了很多,收获颇丰。建议学习机器学习,先学《深度学习入门》,再学小土堆。