1.基本数据:Tensor
Tensor,即张量,是PyTorch中的基本操作对象,可以看做是包含单一数据类型元素的多维矩阵。从使用角度来看,Tensor与NumPy的ndarrays非常类似,相互之间也可以自由转换,只不过Tensor还支持GPU的加速【重点】。
1.1 Tensor的创建
1.2 tensor的操作:
第一部分: 分为FloatTensor,IntTensor,randn,range,zeros,ones,empty
import torch
a = torch.FloatTensor(2,3) # 1.生成两行三列的向量,3代表维度
b = torch.FloatTensor([2,3,4,5]) # 2.生成float类型的四个维度向量
a,b
(tensor([[1.0561e-38, 1.0102e-38, 9.6429e-39],
[8.4490e-39, 9.6429e-39, 9.1837e-39]]),
tensor([2., 3., 4., 5.]))
torch.IntTensor: 用于生成数据类型为整型的Tensor,传递给传递给torch.IntTensor的参数可以是列表,也可以是一个维度值。
import torch
a = torch.IntTensor(2,3)
b = torch.IntTensor([2,3,4,5])
a,b
torch.randn: 用于生成数据类型为浮点数
且维度指定的随机Tensor,随机生成的浮点数的取值满足均值为0,方差为1的正态分布。
import torch
a = torch.randn(2,3)
a
tensor([[-0.0067, -0.0707, -0.6682],
[ 0.8141, 1.1436, 0.5963]])
torch.zeros: torch.zeros用于生成数据类型为浮点型
且维度指定的Tensor
import torch
a = torch.zeros(2,3)
a
tensor([[0., 0., 0.],
[0., 0., 0.]])
torch.abs 、torch.add、torch.clamp、torch.mm()计算:
torch.clamp()的作用
:对tensor中的数据进行裁剪。
torch.mm()的作用
:将参数传递到torch.mm后返回输入参数的求积结果作为输出,不过这个求积的方式和之前的torch.mul运算方式不太一样,torch.mm 运用矩阵之间的乘法规则进行计算 ,所以被传入的参数会被当作矩阵进行处理,参数的维度自然也要满足矩阵乘法的前提条件,即前一个矩阵的行数必须和后一个矩阵列数相等
# 这段代码的目的是展示不同类型的张量的使用方法。下面是加上中文注释后的代码:
import torch
# 1.demo1
a = torch.FloatTensor(2, 3) # 创建一个大小为2x3的浮点型张量
b = torch.FloatTensor([2, 3, 4, 5]) # 创建一个大小为4的浮点型向量
c = torch.Tensor(2, 3) # 创建一个大小为2x3的任意类型张量
# print(a, b) # 输出a和b
# print(c)
# 2.demo2
a = torch.IntTensor(2, 3)
b = torch.IntTensor([2, 3, 4, 5])
# print(a, b)
# 3.demo3:生成数据类型为浮点数且维度指定的随机Tensor,取值满足均值为0,方差为1的正态分布
a = torch.randn(2, 3)
b = torch.range(1, 20, 2) # torch.range用于生成数据类型为浮点型且起始范围和结束范围的Tensor
# print(a)
# print(b)
# 4.demo4: Tensor的计算
a = torch.randn(2, 3)
b = torch.abs(a) # 绝对值的输出
# print(a)
# print(b)
a = torch.randn(2, 3)
b = torch.randn(2, 3)
c = torch.add(a, b)
# print(c)
d = torch.randn(2, 3)
e = torch.add(d, 10) # 增加标量
# print(e)
a = torch.randn(2, 3)
# print(a)
b = torch.clamp(a, -0.1, 0.1) # 卡位操作:限制张量里面每个值都在-0.1到0.1之间,如果<-0.1则设置未-0.1,如果>0.1,则设置未0.1
# print(b)
# 4.demo4:矩阵计算-torch.mm() (2x3)*(3x2)=2x2,如果是矩阵和向量之间的计算,旧用torch.mv()
a = torch.randn(2, 3)
print(a)
b = torch.rand(2, 3)
print(b)
c = torch.mm(a, b.T)
print(c)
torch.mv():
将参数传递到torch.mv后返回输入参数的求积结果作为输出,torch.mv运用矩阵与向量之间的乘法规则进行计算,被传入的第1个参数代表矩阵,第2个参数代表向量,循序不能颠倒。
a = torch.randn(2,3)
a
#我们得到a为:
#tensor([[ 1.0909, -1.1679, 0.3161],
# [-0.8952, -2.1351, -0.9667]])
b = torch.randn(3)
b
#我们得到b为:
#tensor([-1.4689, 1.6197, 0.7209])
#用产生的a,b进行矩阵乘法操作:
c = torch.mv(a,b)
c
#tensor([-3.2663, -2.8402])
2.神经网络工具箱torch.nn
torch.autograd 库虽然实现了自动求导与梯度反向传播。
torch.nn: 该接口构建于 Autograd
之上,提供了网络模组
、优化器
和初始化
策略等一系列功能
nn.Module类: nn.Module
是PyTorch提供的神经网络类,并在类中实现了网络各层的定义及前向计算
与反向传播机制
。在实际使用时,如果想要实现某个神经网络,只需继承nn.Module
,在初始化中定义模型结构与参数,在函数**forward()**中编写网络前向过程即可。
1.nn.Parameter函数
2.forward()函数与反向传播
3.多个Module的嵌套
4.nn.Module与nn.functional库
5.nn.Sequential()模块
import torch.nn as nn
import torch
class MLP(nn.Module):
"""
实现一个具有三层隐藏层的MLP模型。
参数:
- in_dim (int): 输入维度
- hid_dim1 (int): 第一层隐藏层的维度
- hid_dim2 (int): 第二层隐藏层的维度
- out_dim (int): 输出维度
返回:
- 无
"""
def __init__(self, in_dim, hid_dim1, hid_dim2, out_dim):
super(MLP, self).__init__() # 1.调用父类 nn.Module 的初始化方法
# 2.定义一个包含三层线性变换和三层ReLU激活函数的序列模型
self.layers = nn.Sequential(
nn.Linear(in_dim, hid_dim1), # 3.第一层线性变换
nn.ReLU(), # 第一层激活函数
nn.Linear(hid_dim1, hid_dim2), # 第二层线性变换
nn.ReLU(), # 第二层激活函数
nn.Linear(hid_dim2, out_dim) # 第三层线性变换
# 通常在最后一层线性变换后不会再加 ReLU,因为这会影响输出的范围
)
def forward(self, x):
"""
定义网络的前向传播过程。
参数:
- x (Tensor): 输入的特征张量
返回:
- x (Tensor): 经过模型处理后的输出张量
"""
x = self.layers(x) # 输入x依次经过self.layers中定义的各层
return x
# 定义MLP模型输入输出的维度
in_dim = 10
hid_dim1 = 20
hid_dim2 = 15
out_dim = 5
# 实例化MLP模型
model = MLP(in_dim, hid_dim1, hid_dim2, out_dim)
# 得到输入张量x,维度应该和模型model定义的输入张量维度一样的
x = torch.randn(32, in_dim)
# 将输入张量 x 传入模型得到输出的维度,本质就是传递给模型的 forward 方法(或者直接传递给模型实例,因为 forward 方法会被自动调用)
output = model(x)
print(output.shape)
3.搭建简易的神经网络:
3.1下面我们用torch搭一个简易神经网络:
1、【torch.autograd】包的主要功能就是完成神经网络后向传播中的链式求导,手动去写这些求导程序会导致重复造轮子的现象。
2、【自动梯度的功能过程大致为】:先通过输入的Tensor数据类型的变量在神经网络的前向传播过程中生成一张计算图,然后根据这个计算图和输出结果精确计算出每一个参数需要更新的梯度,并通过完成后向传播完成对参数的梯度更新。
import torch
from torch.autograd import Variable
batch_n = 100 # 一个批次输入数据的数量
hidden_layer = 100
input_data = 1000 # 每个数据的特征为1000
output_data = 10
x = Variable(torch.randn(batch_n, input_data), requires_grad=False)
y = Variable(torch.randn(batch_n, output_data), requires_grad=False)
# 用Variable对Tensor数据类型变量进行封装的操作。requires_grad如果是False,表示该变量在进行自动梯度计算的过程中不会保留梯度值。
w1 = Variable(torch.randn(input_data, hidden_layer), requires_grad=True)
w2 = Variable(torch.randn(hidden_layer, output_data), requires_grad=True)
# 学习率和迭代次数
epoch_n = 50
lr = 1e-6
for epoch in range(epoch_n):
h1 = x.mm(w1) # (100,1000)*(1000,100)-->100*100
print(h1.shape)
h1 = h1.clamp(min=0)
y_pred = h1.mm(w2)
# y_pred = x.mm(w1).clamp(min=0).mm(w2)
loss = (y_pred - y).pow(2).sum()
print("epoch:{},loss:{:.4f}".format(epoch, loss.data))
# grad_y_pred = 2*(y_pred-y)
# grad_w2 = h1.t().mm(grad_y_pred)
loss.backward() # 后向传播
# grad_h = grad_y_pred.clone()
# grad_h = grad_h.mm(w2.t())
# grad_h.clamp_(min=0)#将小于0的值全部赋值为0,相当于sigmoid
# grad_w1 = x.t().mm(grad_h)
w1.data -= lr * w1.grad.data
w2.data -= lr * w2.grad.data
w1.grad.data.zero_()
w2.grad.data.zero_()
# w1 = w1 -lr*grad_w1
# w2 = w2 -lr*grad_w2
3.2重写前向传播函数:
该代码并没有继承父类的model,初始化直接利用父类的model,关键点在于重写了前向传播算法
import torch
from torch.autograd import Variable
batch_n = 64#一个批次输入数据的数量
hidden_layer = 100
input_data = 1000#每个数据的特征为1000
output_data = 10
class Model(torch.nn.Module):#完成类继承的操作
def __init__(self):
super(Model,self).__init__()#类的初始化
def forward(self,input,w1,w2):
x = torch.mm(input,w1)
x = torch.clamp(x,min = 0)
x = torch.mm(x,w2)
return x
def backward(self):
pass
model = Model()
x = Variable(torch.randn(batch_n,input_data),requires_grad=False)
y = Variable(torch.randn(batch_n,output_data),requires_grad=False)
#用Variable对Tensor数据类型变量进行封装的操作。requires_grad如果是F,表示该变量在进行自动梯度计算的过程中不会保留梯度值。
w1 = Variable(torch.randn(input_data,hidden_layer),requires_grad=True)
w2 = Variable(torch.randn(hidden_layer,output_data),requires_grad=True)
epoch_n=30
for epoch in range(epoch_n):
y_pred = model(x,w1,w2)
loss = (y_pred-y).pow(2).sum()
print("epoch:{},loss:{:.4f}".format(epoch,loss.data))
loss.backward()
w1.data -= lr*w1.grad.data
w2.data -= lr*w2.grad.data
w1.grad.data.zero_()
w2.grad.data.zero_()
3.3 利用torch.nn.Sequential类(一种序列容器)进行神经网络的搭建:
Sequential
类是torch.nn中的一种序列容器,通过在容器中嵌套各种实现神经网络模型的搭建,最主要的是,参数会按照我们定义好的序列自动传递下去。
Linear(input,hidden)
的作用就是对输入的数据进行线性变换,以下的权重和偏置会在训练过程中通过反向传播算法进行更新。
相关链接:
线性回归
分类的基础学习
线性和非线性
import torch
from torch.autograd import Variable
batch_n = 100 #一个批次输入数据的数量
hidden_layer = 100
input_data = 1000 #每个数据的特征为1000
output_data = 10
x = Variable(torch.randn(batch_n,input_data),requires_grad=False)
y = Variable(torch.randn(batch_n,output_data),requires_grad=False)
#用Variable对Tensor数据类型变量进行封装的操作。requires_grad如果是F,表示该变量在进行自动梯度计算的过程中不会保留梯度值。
models = torch.nn.Sequential(
torch.nn.Linear(input_data,hidden_layer),
torch.nn.ReLU(),
torch.nn.Linear(hidden_layer,output_data)
)
#torch.nn.Sequential括号内就是我们搭建的神经网络模型的具体结构,Linear完成从隐藏层到输出层的线性变换,再用ReLU激活函数激活
#torch.nn.Sequential类是torch.nn中的一种序列容器,通过在容器中嵌套各种实现神经网络模型的搭建,
#最主要的是,参数会按照我们定义好的序列自动传递下去。
4.一些计算损失函数的方法
4.1 MSE均方差
torch.nn.MSELoss类使用均方误差函数对损失值进行计算,定义类的对象时不用传入任何参数,但在使用实例时需要输入两个维度一样的参数方可进行计算。
用于场景: MSE 是预测值和实际值之间差值的平方的平均值。它主要用于回归问题。
import torch
from torch.autograd import Variable
loss_f = torch.nn.MSELoss()
x = Variable(torch.randn(100,100)) # 100行100列的向量
y = Variable(torch.randn(100,100))
loss = loss_f(x,y)
loss.data
#tensor(1.9529)
4.2 L1平均绝对误差
torch.nn.L1Loss类使用平均绝对误差函数对损失值进行计算,定义类的对象时不用传入任何参数,但在使用实例时需要输入两个维度一样的参数方可进行计算。
应用场景: L1 损失是预测值与实际值之间差的绝对值的平均值。它对异常值具有较好的鲁棒性。
import torch
from torch.autograd import Variable
loss_f = torch.nn.L1Loss()
x = Variable(torch.randn(100,100))
y = Variable(torch.randn(100,100))
loss = loss_f(x,y)
loss.data
#tensor(1.1356)
4.3 交叉熵的计算
交叉熵+线性非线性
交叉熵一般就用于多分类的情况,(3,5)表示为3个样本,每个样本为5维的数据,所以我们要给每个样本附上一个标签。
'''
交叉熵:用于多分类任务的损失函数,它需要两个输入:模型的预测输出和目标标签
'''
loss_f = torch.nn.CrossEntropyLoss()
x = Variable(torch.randn(3, 5)) # 创建一个形状为 (3, 5) 的随机张量 x,表示 3 个样本,每个样本有 5 个特征。
y = Variable(torch.LongTensor(3).random_(5)) # 3个0-4的随机数字(表特征),生成了一个包含 3 个随机整数的张量,每个整数的范围在 0 到 4 之间
print(y)
loss = loss_f(x, y)
print(loss.data)
5.使用pytorch搭建神经网络(识别数字)
首先介绍几个常见的包:
5.1 torchvision
torchvision 是PyTorch中专门用来处理图像的库。这个包中有四个大类:
- torchvision.datasets
- torchvision.models
- torchvision.transforms
- torchvision.utils
1.torchvision.datasets
torchvision.datasets可以实现对一些数据集的下载和加载如MNIST可以用torchvision.datasets.MNIST COCO、ImageNet、CIFCAR等都可用这个方法下载和载入。
这里用torchvision.datasets加载MNIST数据集:
data_train = datasets.MNIST(root="./data/",
transform=transform,
train = True,
download = True)
data_test = datasets.MNIST(root="./data/",
transform = transform,
train = False)
2.torchvision.models
torchvision.models 中为我们提供了已经训练好的模型.
torchvision.models模块的 子模块中包含以下模型结构。如:
AlexNet VGG ResNet SqueezeNet DenseNet等
import torchvision.models as models
resnet18 = models.resnet18()
alexnet = models.alexnet()
squeezenet = models.squeezenet1_0()
densenet = models.densenet_161()
5.1.1 如何修改这些模型的参数?
- 比如修改模型的输入层或输出层。
示例: 修改ResNet18模型以适应具有100个输出类别的任务:
import torchvision.models as models
import torch.nn as nn
resnet18 = models.resnet18(pretrained=True) # 加载预训练的模型resnet18
num_ftrs = resnet18.fc.in_features # 获取最后一个全连接层的输入特征数量
resnet18.fc = nn.Linear(num_ftrs, 100) # 修改为100个输出类别
- 冻结参数,并更新全连接层(迁移学习)。
在迁移学习中,你可能希望冻结模型的一部分
,只训练模型的特定层
。这通常通过设置参数的requires_grad
属性来实现。
示例: 冻结DenseNet161模型的所有参数,只训练最后的分类层:
目的:
1. 利用预训练模型的特征提取能力:一个已经在数据集上训练过的模型(如DenseNet161)通过训练已经学会了如何从图像中提取有用的特征,比如边缘、纹理、形状等。这些特征对于图像识别任务是非常有用的。所以我们可以利用它已经学习到的特征提取能力,利用模型已经理解的复杂和通用的视觉特征,这些特征通常对于大多数视觉识别任务都是有效的。
2.只更新模型的最后部分:尽管预训练模型在特征提取方面表现优秀,但它最初是为处理其他类型的任务(如识别ImageNet数据库中的1000个类别)而训练的。如果我们要处理一个新的任务(例如分类100个不同的类别),我们通常需要调整模型的最后一层,使其输出与新任务的类别数量相匹配。这样的原因:模型的前面几层通常负责提取图像的通用特征,而最后几层则更专注于将这些特征转换成特定的预测输出(比如分类)。
3.训练速度和效果:显著减少需要训练的参数数量,因为只有最后一层是新的或被重新训练的。
densenet = models.densenet161(pretrained=True) # 加载了一个预训练的 DenseNet161 模型。这个模型已经在大型的图像数据集(通常是ImageNet)上进行了训练,学习了很多图像特征。
for param in densenet.parameters():
param.requires_grad = False # 遍历 DenseNet161 模型中的所有参数,这些参数在接下来的训练过程中不会被更新(即它们被“冻结”了)
# 仅修改最后的分类层,重新启用梯度
num_ftrs = densenet.classifier.in_features # 获取 DenseNet161 模型中的最后一层全连接层(即为分类层)的输入特征数量
densenet.classifier = nn.Linear(num_ftrs, 100) # 假设有100个类别
densenet.classifier.weight.requires_grad = True # 重新启用了新分类器层权重的梯度计算。
还需要考虑一个点(迁移学习),即为任务的相似性:
如果新任务与原始任务在视觉上有一定的相似性,比如从识别一般物体到识别特定物体(例如从识别动物到识别特定的动物种类),那么原始模型的特征提取层可能仍然非常有效。然而,如果任务从识别日常物品变为识别细粒度的人脸特征,那么可能需要对这些层进行更细致的调整或重新训练,因为这些任务在视觉特征上的需求可能有较大差异。
- 修改架构。
对于一些模型,例如SqueezeNet,你可能需要修改其内部架构,例如改变某些层的激活函数或调整层之间的连接方式。
squeezenet = models.squeezenet1_0(pretrained=True)
# 假设你想修改特定层的属性
squeezenet.features[3].expand3x3.activation = nn.ReLU(inplace=False)
5.1.2 torch.transforms
torch.transforms中有大量数据变换类,如:
5.1.3.1 torchvision.transforms.Resize 用于对载入的图片数据按照我们需求的大小进行缩放。传递的参数可以是一个整型数据,也可以是一个类似于(h,w)的序列。h代表高度,w代表宽度,如果输入的是整型数据那么h和w都等于这个数。
5.1.3.2 torchvision.transforms.Scale 用于对载入的图片数据按照我们需求的大小进行缩放。和Resize类似。
5.1.3.3 torchvision.transforms.CenterCrop 用于对载入的图片以图片中心为参考点,按照我们需要的大小进行裁剪。传递给这个类的参数可以是一个整型数据,也可以是一个类似于(h,w)的序列。
5.1.3.4 torchvision.transforms.RandomCrop 用于对载入的图片按照我们需要的大小进行随机裁剪。传递给这个类的参数可以是一个整型数据,也可以是一个类似于(h,w)的序列。
5.1.3.5 torchvision.transforms.RandomHorizontalFlip 用于对载入的图片按随机概率进行水平翻转。我们通过传递给这个类的自定义随机概率,如果没有定义,则使用默认的概率为0.5
5.1.3.6 torchvision.transforms.RandomVerticalFlip 用于对载入的图片按随机概率进行垂直翻转。我们通过传递给这个类的自定义随机概率,如果没有定义,则使用默认的概率为0.5
5.1.3.7 torchvision.transforms.ToTensor 用于对载入的图片数据进行类型转换,将之前构成PIL图片数据转换为Tensor数据类型的变量,让PyTorch能够对其进行计算和处理。
5.1.3.8 torchvision.transforms.ToPILImage: 用于对Tensor变量的数据转换成PIL图片数据,主要为方便图片显示
#torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;
transform=transforms.Compose(
[transforms.ToTensor(),#将PILImage转换为张量
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))#将[0, 1]归一化到[-1, 1]
#前面的(0.5,0.5,0.5) 是 R G B 三个通道上的均值, 后面(0.5, 0.5, 0.5)是三个通道的标准差
])
#上述代码我们可以将transforms.Compose()看作一种容器,它能够同时对多种数据变换进行组合。
#传入的参数是一个列表,列表中的元素就是对载入数据进行的变换操作。
5.1.4 torch.utils
关于torchvision.utils我们介绍一种用来对数据进行装载的类:torch.utils.data.DataLoader和
dataset 参数指定我们载入的数据集的名称;
batch_size 参数设置每个包中图片的数量; (每个批次的样本数量)
shuffle 设置为True代表在装载的过程会将数据随机打乱顺序并进行打包(True为打乱增强泛化性)。
data_loader_train=torch.utils.data.DataLoader(dataset=data_train,
batch_size=64,#每个batch载入的图片数量,默认为1,这里设置为64
shuffle=True,
#num_workers=2#载入训练数据所需的子任务数
)
data_loader_test=torch.utils.data.DataLoader(dataset=data_test,
batch_size=64,
shuffle=True)
#num_workers=2)
5.1.5 torchvision.utils.make_grid将一个批次的图片构造成网格模式的图片
1、iter和next:获取一个批次的图片数据和其对应的图片标签——>2、再使用torchvision.utils.make_grid:将一个批次的图片构造成网格模式 经过torchvision.utils.make_grid后图片维度变为channel,h,w三维【 因为要用matplotlib将图片显示,我们要使用的数据要是数组且维度为(height,weight,channel)即色彩通道在最后】——> 3、因此我们需要用numpy和transpose完成原始数据类型的转换和数据维度的交换。
#预览
#在尝试过多次之后,发现错误并不是这一句引发的,而是因为图片格式是灰度图只有一个channel,需要变成RGB图才可以,所以将其中一行做了修改:
images,labels = next(iter(data_loader_train))
# dataiter = iter(data_loader_train) #随机从训练数据中取一些数据
# images, labels = dataiter.next()
img = torchvision.utils.make_grid(images)
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img*std+mean
print([labels[i] for i in range(64)])
plt.imshow(img)
5.2 CNN网络卷积层搭建
CNN的学习
ResNet的学习
import math
import torch
import torch.nn as nn
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
#构建卷积层之后的全连接层以及分类器
self.conv1 = nn.Sequential(
# 输入通道数、输出通道数、卷积核大小[3x3]、卷积核移动步长和paddingde值(用于对边界像素的填充)
nn.Conv2d(3,64,kernel_size=3,stride=1,padding=1),
nn.ReLU(),
nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1),
nn.ReLU(),
nn.MaxPool2d(stride=2,kernel_size=2)
)
self.dense = torch.nn.Sequential(
nn.Linear(14*14*128,1024),
nn.ReLU(),
nn.Dropout(p=0.5), # 防止卷积神经网络在训练过程中发生过拟合,原理是以一定的随机概率将卷积神经网络模型的部分参数归零,以达到减少相邻两层神经连接的目的[本质就是减少参数]
nn.Linear(1024,10)
)
def forward(self,x):
x=self.conv1(x)
x=x.view(-1,14*14*128)
x=self.dense(x)
return x
6.完整代码
import torch # 用于深度学习和张量计算
import torch.nn as nn
from torchvision import datasets, transforms, utils # 从Torchvision导入数据集、数据变换工具和utils
import torch.optim as optim
import torchvision # 导入Torchvision库,用于计算机视觉学习任务。
from torchvision import datasets, transforms # 从Torchvision库中导入数据集合数据变换工具
from torch.autograd import Variable # autograd包下自动计算梯度的工具
import numpy as np # 导入NumPy库,用于数值计算
import matplotlib.pyplot as plt # 导入Matplotlib库,用于绘图片
'''
1.检查是否可以使用GPU
'''
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print('设备状态:', device)
'''
2.定义数据变换操作:将图像转换为tensor进行标准化
2.1 x 是一个图像张量,可能是一个单通道(灰度图像)的张量
2.2 第一个维度是通道(channels):从 1 重复到 3,这意味着将单通道图像转换为 RGB 三通道图像。
如果你有一个灰度图像,这样做会创建三个相同副本的通道,形成一个彩色图像,每个通道都有相同的灰度值。
2.3 第二个维度是高度(height):1 表示不改变图像的高度,即不重复。
2.4 第三个维度是宽度(width):同样为 1,表示也不改变图像的宽度,不进行重复。
'''
transform = transforms.Compose([
transforms.ToTensor(), # 将图像转换为tensor
transforms.Lambda(lambda x: x.repeat(3, 1, 1)), # 将单道系统重复成3条道,符合预期输入格式
transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) # 标准化操作,将图像像素值范围调整到[-1, 1]之间
])
'''
3.下载并加载MNIST数据集。
3.1 定义数据集位置、数据变换操作、是否训练。
'''
data_train = datasets.MNIST(root='./data/',
transform=transform,
train=True,
download=True)
data_test = datasets.MNIST(root='./data/',
transform=transform,
train=False,
download=False)
'''
4.定义数据加载器。
'''
data_loader_train = torch.utils.data.DataLoader(dataset=data_train, # 定义训练集位置
batch_size=64, # 每个批次加载64张图片
shuffle=True) # shuffle=True,即打乱数据顺序,增强泛化能力
data_loader_test = torch.utils.data.DataLoader(dataset=data_test,
batch_size=64,
shuffle=True)
'''
5.预览数据
5.1:.transpose(1, 2, 0)操作的意义在于:(而matplotlib在展示图像时,它期望的数据格式遵循(height, width, channels)的规则)
把原始张量的第二维度(高度,height)移动到新张量的第一维度。
把原始张量的第三维度(宽度,width)移动到新张量的第二维度。
把原始张量的第一维度(通道,channels)移动到新张量的第三维度。
5.2:反标准化操作的数学表达式:
假设原始图像数据经过了这样的标准化处理:x_normalized = (x - mean) / std,其中x是原始像素值,x_normalized是标准化后的像素值,mean是平均值,std是标准差。
要从标准化后的数据恢复到原始数据,就需要执行反向操作:x = x_normalized * std + mean。
'''
images, labels = next(iter(data_loader_train)) # 获取一个批次的图像和标签
img = utils.make_grid(images) # 将多个图像images合并为一张大图像,便于可视化
img = img.numpy().transpose(1, 2, 0) # 转换图片维度将其tensor转为numpy形式,以符合matplotlib的要求
std = [0.5, 0.5, 0.5]
mean = [0.5, 0.5, 0.5]
img = img * std + mean # 根据归一化公式反标准化图片,以恢复原始图像的像素值范围,方便展示图像。
print([labels[i] for i in range(64)]) # 打印图片的标签,用于检查数据加载是否正确
plt.imshow(img) # 显示图片
plt.show() # 确保图片显示
'''
6.定义卷积神经网络
'''
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__() # 继承父类nn.Module的初始化方法
'''
6.1定义卷积层。
6.1.1:因为之前数据预览的时候(一批次),图层是8x8=64的,我们的这里卷积核是3x3的,并且步长stride=1,所以要充分读取图层信息,故需要将padding填充设置为1
6.1.2:nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1): 这是第二层卷积层,与第一层类似,但输出通道数增加到128,进一步提取更复杂的特征。
64个滤波器,每个滤波器都会生成一个不同的特征图(feature map),kernel_size=3,这表示每个滤波器的尺寸是3x3
6.1.3:用激活函数Rule:第一是缓解了梯度消失的影响,第二是保证正值不变,负值替换为0,帮助网络更好的学习图像特征(关键信息)。
6.1.4:nn.MaxPool2d(stride=2, kernel_size=2): 这是最大池化层,通常用于降低特征图的空间维度,减少计算量,同时保持重要特征。
'''
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1), # 第一层卷积,输入3通道,输出64通道,步长为1,填充为1
nn.ReLU(), # ReLU激活函数,增加模型的非线性
nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1), # 第二层卷积,输入64通道,输出128通道
nn.ReLU(), # ReLU激活函数
nn.MaxPool2d(stride=2, kernel_size=2) # 最大化池化层,减少特征图尺寸,保持重要特征
)
'''
6.2 定义全连接层
6.2.1:14*14*128:是经过池化层后变为14*14的特征图,然后每个特征有128个通道得到
1024:是全连接层中隐藏单元数
6.2.2:nn.Linear(1024, 10): 这是第二个全连接层,也是模型的输出层。输入维度为1024,
与前一层的输出维度相对应。输出维度为10,对应MNIST数据集的10个类别(0-9的数字)
'''
self.dense = nn.Sequential(
nn.Linear(14 * 14 * 128, 1024), # 全连接层,输入14*14*128,输出1024
nn.ReLU(), # ReLU激活函数
nn.Dropout(p=0.5), # Dropout层,防止过拟合
nn.Linear(1024, 10) # 输出层,10个类别对应MNIST数据集的10个数字
)
'''
6.3 定义前向传播
'''
def forward(self, x):
x = self.conv1(x) # 前向传播:卷积层
x = x.view(-1, 14 * 14 * 128) # 将特征图转为一维张量
x = self.dense(x) # 前向传播:全连接层
return x # 返回输出
model = Model().to(device) # 实例化模型并转移到GPU上。
cost = nn.CrossEntropyLoss() # 定义交叉熵损失函数,用于多分类任务
optimizer = optim.Adam(model.parameters()) # 定义Adam优化器,用于更新模型参数
print(model) # 打印模型结构
'''
7.训练模型
'''
n_epochs = 3
for epoch in range(n_epochs):
running_loss = 0.0
running_correct = 0
print("Epoch {}/{}".format(epoch, n_epochs)) # 打印轮数
print("-" * 10)
for data in data_loader_train:
X_train, y_train = data
print("训练数据X_train,y_train:")
print(X_train, y_train)
X_train, y_train = Variable(X_train).to(device), Variable(y_train).to(device) # 将数据转移到GPU
print("转为GPU后:")
print(X_train, y_train)
outputs = model(X_train) # 前向传播,计算模型输出
_, pred = torch.max(outputs.data, 1) # 获取预测结果
optimizer.zero_grad() # 梯度清零,防止累积
loss = cost(outputs, y_train) # 计算损失
loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新参数
running_loss += loss.data # 累加损失
running_correct += torch.sum(pred == y_train.data) # 计算训练准确率
testing_correct = 0
for data in data_loader_test: # 使用测试集来监控训练进程,有助于及时调整训练策略或提前停止训练,从而节省资源并避免过拟合。
X_test, y_test = data
X_test, y_test = Variable(X_test).to(device), Variable(y_test).to(device) # 将数据转移到GPU
outputs = model(X_test) # 前向传播,计算测试集上的模型输出
_, pred = torch.max(outputs.data, 1) # 获取预测结果
testing_correct += torch.sum(pred == y_test.data) # 计算测试准确率
print("Loss is:{:4f}, Train Accuracy is:{:.4f}%, Test Accuracy is:{:.4f}".format(running_loss / len(data_train),
100 * running_correct / len(
data_train),
100 * testing_correct / len(
data_test))) # 输出训练损失和准确率
'''
8.测试模型:
8.1: torch.max(pred, 1)会返回一个包含这些索引的张量[2, 4],其中_会捕获每行的最大值,
但由于前面使用了下划线(_)作为变量名,这意味着我们【忽略了这些最大值】,【只关心索引】
8.2:batch_size=4说明每次处理4个样本,每个样本10维向量【4x10】,如果有1000个样本,则每个epoch需要处理250批次
8.3:X_test 的维度通常是 (batch_size, channels, height, width):batch_size 是每个批次的样本数。channels 表示图像的通道数(对于灰度图像是1,对于彩色图像是3)。height 和 width 表示图像的高度和宽度。
8.4:y_test 的维度通常是 (batch_size,),包含每个图像对应的类别标签
8.5:pred为预测标签
'''
data_loader_test = torch.utils.data.DataLoader(dataset=data_test,
batch_size=4,
shuffle=True) # 重新定义测试数据加载器,用于测试
X_test, y_test = next(iter(data_loader_test)) # 获取一个批次的测试数据进行预测,X_test是4个样本的张量
inputs = Variable(X_test).to(device) # 将数据转移到GPU
pred = model(inputs) # 前向传播,计算预测结果
_, pred = torch.max(pred, 1) # 获取预测标签
print("预测标签:")
print(pred)
print("Predict Label is:", [i for i in pred.data.cpu()]) # 打印预测标签
print("Real Label is:", [i for i in y_test]) # 打印真实标签
img = torchvision.utils.make_grid(X_test.cpu()) # 将4个测试样本的图像拼接成了一张2x2的网格图,所以你会看到4个数字
img = img.numpy().transpose(1, 2, 0) # 转换图片维度顺序
std = [0.5, 0.5, 0.5]
mean = [0.5, 0.5, 0.5]
img = img * std + mean # 反标准化图片
plt.imshow(img) # 显示测试图片及其标签
plt.show() # 确保图片显示