两个实用函数

假如pytorch是一个大型工具箱,我们想查看工具箱中有什么工具,这时就可以使用dir()函数查看,如果我们想知道某一个工具是如何使用的,就可以使用help()函数查看

  • dir():可以查看指定对象包含的全部内容,包括变量、方法、函数和类等。不仅包含可供我们调用的模块成员,还包含所有名称以双下划线“__”开头和结尾的“特殊”命名的私有成员,这些成员是在本模块中使用的,不能在类的外部调用。
  • help():查看指定对象(类型、模块、变量、方法等)的详细使用说明

Pytorch

在学习和使用pytorch时,要经常使用官方文档,里面有详细的使用说明

加载数据集

加载数据方法及label形式

Pytorch中加载数据需要Dataset、Dataloader

  • Dataset提供一种方式去获取每个数据及其对应的label和编号,以及总共有多少个数据
  • Dataloader为后面的网络提供不同的数据形式,可以将数据进行打包

label形式

  • 文件夹名即为label。文件夹中存放若干条数据
  • 一个文件夹存放数据,数据有编号,另一个文件夹存放数据对应编号的说明文本(txt),文本中有label
  • 直接把label写在数据的名称上
from torch.utils.data import Dataset

通过路径加载数据

from PIL import Image
img_path = "数据路径/train/ants/0013035.jpg"
img = Image.open(img_path)
img.show()

python特殊方法补充

Python中有很多特殊方法,这些特殊方法的命名都以双下划线 __开头和结尾,它们是Python中常用的特殊方法。通过定义这些方法,我们可以自定义对象的行为和操作,使得对象能够更好地适应我们的需求

  • __init__(self[, args...]): 构造函数,用于在创建对象时进行初始化。即Java中的构造器
  • __repr__(self): 用于定义对象的字符串表示形式,通常用于调试和记录日志
  • __str__(self): 用于定义对象的字符串表示形式,通常用于显示给终端用户。即Java中的toString
  • __len__(self): 用于返回对象的长度,通常在对像被视为序列或集合时使用
  • __getitem__(self, key): 用于实现索引操作,可以通过索引或切片访问对象中的元素
  • __setitem__(self, key, value): 用于实现索引赋值操作,可以通过索引或切片为对象中的元素赋值
  • __delitem__(self, key): 用于实现删除某个元素的操作,可以通过索引或切片删除对象中的元素
  • __contains__(self, item): 用于检查对象是否包含某个元素,可以通过 in 关键字使用
  • __enter__(self): 用于实现上下文管理器的进入操作,通常与 with 语句一起使用
  • __exit__(self, exc_type, exc_value, traceback): 用于实现上下文管理器的退出操作,通常与 with 语句一起使用
  • __call__(self[, args...]): 用于使对象能够像函数一样被调用,通常在创建可调用的类时使用
  • __eq__(self, other): 用于定义对象相等的比较操作,可以通过 == 运算符使用
  • __lt__(self, other): 用于定义对象小于的比较操作,可以通过 < 运算符使用
  • __gt__(self, other): 用于定义对象大于的比较操作,可以通过 > 运算符使用
  • __add__(self, other): 用于实现对象加法操作,可以通过 + 运算符使用
  • __sub__(self, other): 用于实现对象减法操作,可以通过 - 运算符使用
  • __mul__(self, other): 用于实现对象乘法操作,可以通过 * 运算符使用
  • __truediv__(self, other): 用于实现对象除法操作,可以通过 / 运算符使用

Dataset加载数据示例

from torch.utils.data import Dataset  
from PIL import Image
import os

# 自定义数据集类
class MyData(Dataset):
def __init__(self, root_dir, label_dir):
self.root_dir = root_dir # 记录数据集根目录的路径
self.label_dir = label_dir # 记录数据集标签目录的名称
self.path = os.path.join(self.root_dir, self.label_dir) # os.path.join可将两个字符串拼接成一个完整路径,以获取数据集标签目录的完整路径
self.img_path = os.listdir(self.path) # os.listdir() 函数用于获取指定目录下的所有文件和文件夹的名称列表,以获取数据集标签目录下所有图像文件的路径

def __getitem__(self, idx): # 获取数据集中指定索引位置的数据项
img_name = self.img_path[idx] # 获取图像文件名
img_item_path = os.path.join(self.root_dir, self.label_dir, img_name) # 获取该图像文件的完整路径
img = Image.open(img_item_path) # 打开图像文件
label = self.label_dir # 获取该图像文件所属的标签
return img, label # 返回图像和标签

def __len__(self):
return len(self.img_path)


root_dir = "data/train" # 数据集根目录的路径
ants_label_dir = "ants" # 蚂蚁标签目录的名称
bees_label_dir = "bees" # 蜜蜂标签目录的名称
ants_dataset = MyData(root_dir, ants_label_dir) # 创建蚂蚁数据集对象
bees_dataset = MyData(root_dir, bees_label_dir) # 创建蜜蜂数据集对象
train_dataset = ants_dataset + bees_dataset # 合并蚂蚁和蜜蜂数据集,得到训练集
img, label = train_dataset[200] # 自动调用__getitem__() 方法,获取训练集中第 200 个数据项的图像和标签
print("label:", label) # 查看该数据项的标签
img.show()

TensorBoard

Tensorboad可以用来查看loss是否按照预想的变化,或者查看训练到某一步输出的图像是什么样

Tensorboard使用示例

from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter("logs") # 创建一个 SummaryWriter 对象,指定日志存储目录为 "logs"
for i in range(100):
# 将 y=x 的函数值添加到 TensorBoard 中
writer.add_scalar("y=x", i, i)
writer.close() # 关闭 SummaryWriter 对象

运行完后会在当前目录下创建一个logs文件夹

在终端运行

tensorboard --logdir=logs --port=6007

Transforms

Transforms当成工具箱的话,里面的class就是不同的工具。例如像totensor、resize这些工具。Transforms拿一些特定格式的图片,经过Transforms里面的工具,获得我们想要的结果

transforms.Totensor

Tensor包装了神经网络需要的一些属性,比如反向传播、梯度等属性

from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from PIL import Image

img_path = "/bees/10870992_eebeeb3a12.jpg"
img = Image.open(img_path)

writer = SummaryWriter("logs")

tensor_trans = transforms.ToTensor() # 创建 transforms.ToTensor类 的实例化对象
tensor_img = tensor_trans(img) # 调用 transforms.ToTensor类的__call__方法

writer.add_image("Temsor_img",tensor_img)
writer.close()

transforms.Resize()

调整图像的大小到指定的尺寸

from torchvision import transforms
from PIL import Image

img_path = "data/Images/1.jpg"
img = Image.open(img_path)

# 将图片转为totensor类型
trans_totensor = transforms.ToTensor()
img_tensor = trans_totensor(img)

# resize图片,PIL数据类型的 img -> resize -> PIL数据类型的 img_resize
trans_resize = transforms.Resize((512,512)) # 调整尺寸为512*512
img_resize = trans_resize(img)

# PIL 数据类型的 PIL -> totensor -> img_resize tensor
img_resize = trans_totensor(img_resize)
print(img_resize.size())

transforms.Compose

transforms.Compose 的作用是将多个数据预处理操作组合在一起,方便地对数据进行一次性处理

from torchvision import transforms
from PIL import Image
import cv2

img_path = "data/Images/1.jpg"
img = Image.open(img_path)

tensor_trans = transforms.ToTensor()
img_tensor = tensor_trans(img)

trans_resize_2 = transforms.Resize(512)

# PIL —— resize -> PIL —— totensor -> tensor
trans_compose = transforms.Compose([trans_resize_2, trans_totensor]) # Compose函数中前面一个参数的输出为后面一个参数的输入,即trans_resize_2输出了pil,作为trans_totensor的输入
img_resize_2 = trans_compose(img)
print(img_resize_2.size())

DataLoader

DataLoader可以将数据集进行批量处理

import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

# 准备的测试数据集
test_data = torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor())

# batch_size=4 使得 img0, target0 = dataset[0]、img1, target1 = dataset[1]、img2, target2 = dataset[2]、img3, target3 = dataset[3],然后这四个数据作为Dataloader的一个返回
test_loader = DataLoader(dataset=test_data,batch_size=4,shuffle=True,num_workers=0,drop_last=True)
# 用for循环取出DataLoader打包好的四个数据
writer = SummaryWriter("logs")

for epoch in range(2):
step = 0
for data in test_loader:
imgs, targets = data # 每个data都是由4张图片组成,imgs.size 为 [4,3,32,32],四张32×32图片三通道,targets由四个标签组成
writer.add_images("Epoch:{}".format(epoch), imgs, step) # 注意是images
step = step + 1
writer.close()

神经网络

nn.Module

orch.nn.Module是所有神经网络基本骨架,需要重写__init__和forward(前向传播)函数

import torch
from torch import nn

class Module(nn.Module):
def __init__(self):
super(Module, self).__init__() # 继承

def forward(self, input): # 前向传播
output = input + 1
return output

m = Module()
x = torch.tensor(1.0) # 创建一个值为 1.0 的tensor
output = m(x)
print(output) # 2

卷积函数

convolution卷积,conv2d表示二维

weight卷积核的大小,bias偏置,stride步长,padding填充,group是分组卷积,对不同的通道使用不同的卷积核,言外之意一般是对每个通道使用相同卷积核

import torch
import torchvision
from torch import nn
from torch.nn import Conv2d
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor(),download=True)
dataloader = DataLoader(dataset, batch_size=64)

class Module(nn.Module):
def __init__(self):
super(Module, self).__init__()
self.conv1 = Conv2d(in_channels=3,out_channels=6,kernel_size=3,stride=1,padding=0) # 彩色图像输入为3层,我们想让它的输出为6层,选3 * 3 的卷积

def forward(self,x):
x = self.conv1(x)
return x

m = Module()
for data in dataloader:
imgs, targets = data
output = m(imgs)
print(imgs.shape) # 输入为3通道32×32的64张图片
print(output.shape) # 输出为6通道30×30的64张图片

最大池化

nn.MaxPool2d最大池化(下采样,最常用),nn.MaxUnpool2d(上采样),nn.AdaptiveMaxPool2d(自适应最大池化),ceil_mode=True表示进位,默认为False,写论文会用到公式可以在官网查阅

最大池化的作用:保留输入的特征,同时把数据量减少,比如视频变720P

import torch
import torchvision
from torch import nn
from torch.nn import MaxPool2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor(),download=True)
dataloader = DataLoader(dataset, batch_size=64)

class Module(nn.Module):
def __init__(self):
super(Module, self).__init__()
self.maxpool = MaxPool2d(kernel_size=3, ceil_mode=True)

def forward(self, input):
output = self.maxpool(input)
return output

m = Module() # 即调用forward()
writer = SummaryWriter("logs")
step = 0

for data in dataloader:
imgs, targets = data
writer.add_images("input", imgs, step)
output = m(imgs)
writer.add_images("output", output, step)
step = step + 1

非线性激活

作用:神经网络中引入非线性的特质,才能训练出符合各种特征的模型

nn.ReLU()小于0进行截断,大于0不变,nn.Sigmoid非线性缩放到[0,1],1/1+exp-x,inplace=True表示把原来的值也改变(本来是通过返回值获取值)

线性层及其它层

  • Normalization Layers正则化层:加快神经网络的训练速度
  • Recurrent Layers用于文字识别,特定的网络结构,用的不多
  • Linear Layers:全连接层,用的较多
  • Dropout Layers随机将一些数设为0,防止过拟合
  • Distance Functions计算两个值之间的误差,常用余弦相似度
  • Loss Functions损失函数,常用值nn.MSELoss,nn.CrossEntropyLoss,nn.BCELoss,分布nn.NLLLoss,nn.KLDivLoss
  • torch.flatten()把输入展成一行,与reshape不同

损失函数

  • Loss损失函数一方面计算实际输出和目标之间的差距
  • Loss损失函数另一方面为我们更新输出提供一定的依据(反向传播)

L1loss:差值的绝对值之和,再求平均值

MSE:平方差

梯度下降

梯度下降是一种优化算法,用于最小化损失函数(Loss Function)。神经网络的训练目标是通过不断调整网络的参数(即权重和偏置)来最小化损失函数的值。梯度下降算法通过计算损失函数相对于网络参数的梯度来指导参数更新的方向和步幅。

反向传播

反向传播是一种高效计算梯度的方法,适用于多层神经网络。反向传播算法通过链式法则(Chain Rule)逐层计算损失函数对每个参数的梯度。

反向传播和梯度下降的关系

  1. 目标一致:两者的目标都是通过调整神经网络的参数来最小化损失函数
  2. 互补:反向传播计算损失函数对参数的梯度,而梯度下降利用这些梯度更新参数
  3. 训练过程:在神经网络的训练过程中,反向传播和梯度下降是交替进行的。首先进行前向传播,计算损失并通过反向传播计算梯度,然后使用梯度下降更新参数

优化器

使用优化器时,首先需要optim.zero_grad(),把上一步的梯度清零,否则会累加,然后进行反向传播,再optimizer.step(),对weight参数进行更新

import torch
import torchvision
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
dataloader = DataLoader(dataset, batch_size=64, drop_last=True)


class Module(nn.Module):
def __init__(self):
super(Module, self).__init__()
self.model1 = Sequential(
Conv2d(3, 32, 5, padding=2),
MaxPool2d(2),
Conv2d(32, 32, 5, padding=2),
MaxPool2d(2),
Conv2d(32, 64, 5, padding=2),
MaxPool2d(2),
Flatten(),
Linear(1024, 64),
Linear(64, 10)
)

def forward(self, x):
x = self.model1(x)
return x


loss = nn.CrossEntropyLoss() # 交叉熵
m = Module()
optim = torch.optim.SGD(tudui.parameters(), lr=0.01) # 随机梯度下降优化器 lr为学习率,学习率太大不稳定,太小收敛慢

# 优化20轮
for epoch in range(20):
running_loss = 0.0
for data in dataloader:
imgs, targets = data
outputs = m(imgs)
result_loss = loss(outputs, targets) # 计算实际输出与目标输出的差距
optim.zero_grad() # 梯度清零
result_loss.backward() # 反向传播,计算损失函数的梯度
optim.step() # 根据梯度,对网络的参数进行调优
running_loss = running_loss + result_loss
print(running_loss) # 每轮误差的总和

train()和tudui.eval()方法

分别用于训练步骤和测试步骤,对特定层起作用,最好可以在训练和评估之前加上这个方法

网络模型的保存和读取

使用save方法保存网络模型的结构和参数,load加载时候要把模型定义给复制过来

import torch
import torch.nn as nn

# 定义模型
class SimpleModel(nn.Module):
def __init__(self):
super(SimpleModel, self).__init__()
self.fc = nn.Linear(10, 5)

def forward(self, x):
return self.fc(x)

# 创建实例
model = SimpleModel()

# 保存权重
torch.save(model.state_dict(), 'model_weights.pth')

# 加载权重
loaded_model = SimpleModel()
loaded_model.load_state_dict(torch.load('model_weights.pth'))

# 确保模型和加载的权重一致
loaded_model.eval()

# 可以继续使用加载的模型进行预测等操作

利用GPU训练

在程序之前定义:

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

然后找到

  • 网络模型
  • 数据(输入、标注)
  • 损失函数

后面加上to(device)即可转到GPU训练

完整版实战

import torchvision
import torch
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import time

# 定义训练的设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

class Module(nn.Module):
def __init__(self):
super(Module, self).__init__()
self.model1 = nn.Sequential(
nn.Conv2d(3,32,5,1,2), # 输入通道3,输出通道32,卷积核尺寸5×5,步长1,填充2
nn.MaxPool2d(2),
nn.Conv2d(32,32,5,1,2),
nn.MaxPool2d(2),
nn.Conv2d(32,64,5,1,2),
nn.MaxPool2d(2),
nn.Flatten(), # 展平后变成 64*4*4 了
nn.Linear(64*4*4,64),
nn.Linear(64,10)
)

def forward(self, x):
x = self.model1(x)
return x

# 准备数据集
train_data = torchvision.datasets.CIFAR10("./dataset",train=True,transform=torchvision.transforms.ToTensor(),download=True)
test_data = torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor(),download=True)

# length 长度
train_data_size = len(train_data)
test_data_size = len(test_data)

# 利用 Dataloader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建网络模型
m = Module()
m = m.to(device) # 也可以不赋值,直接m.to(device)

# 损失函数
loss_fn = nn.CrossEntropyLoss() # 交叉熵
loss_fn = loss_fn.to(device)

# 优化器
learning = 0.01
optimizer = torch.optim.SGD(tudui.parameters(),learning) # 随机梯度下降优化器

# 设置网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0

# 训练的轮次
epoch = 3000

# 添加 tensorboard
writer = SummaryWriter("logs")
start_time = time.time()

for i in range(epoch):
print("-----第 {} 轮训练开始-----".format(i+1))

# 训练步骤开始
tudui.train() # 当网络中有dropout层、batchnorm层时,这些层能起作用
for data in train_dataloader:
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
outputs = tudui(imgs)
loss = loss_fn(outputs, targets) # 计算实际输出与目标输出的差距

# 优化器对模型调优
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向传播,计算损失函数的梯度
optimizer.step() # 根据梯度,对网络的参数进行调优

total_train_step = total_train_step + 1
if total_train_step % 100 == 0:
end_time = time.time()
print(end_time - start_time) # 运行训练一百次后的时间间隔
print("训练次数:{},Loss:{}".format(total_train_step,loss.item())) # 方式二:获得loss值
writer.add_scalar("train_loss",loss.item(),total_train_step)

# 测试步骤开始(每一轮训练后都查看在测试数据集上的loss情况)
tudui.eval() # 当网络中有dropout层、batchnorm层时,这些层不能起作用
total_test_loss = 0
total_accuracy = 0
with torch.no_grad(): # 没有梯度了
for data in test_dataloader: # 测试数据集提取数据
imgs, targets = data # 数据放到cuda上
imgs = imgs.to(device) # 也可以不赋值,直接 imgs.to(device)
targets = targets.to(device) # 也可以不赋值,直接 targets.to(device)
outputs = tudui(imgs)
loss = loss_fn(outputs, targets) # 仅data数据在网络模型上的损失
total_test_loss = total_test_loss + loss.item() # 所有loss
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy = total_accuracy + accuracy

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 = total_test_step + 1

torch.save(m, "./model/m_{}.pth".format(i)) # 保存每一轮训练后的结果
#torch.save(m.state_dict(),"m_{}.path".format(i)) # 保存方式二
print("模型已保存")

writer.close()

**with torch.no_grad()**表示在训练数据集的同时进行验证,可以让之后的代码不影响目前的梯度