PyTorch笔记

前言

简介: PyTorch是一个基于Python的科学计算库,它主要提供了两个高级功能:一是支持张量计算,类似于NumPy,但是可以在GPU上运行;二是支持构建和训练深度神经网络。

在PyTorch中,张量(Tensor)是最基本的数据结构,类似于NumPy中的多维数组,但是可以在GPU上进行高效的计算。PyTorch支持多种张量操作,包括张量加法、减法、乘法、除法、矩阵乘法等,同时还支持各种元素级函数和聚合函数。

PyTorch还提供了一种灵活的、动态的计算图机制,称为Autograd。在PyTorch中,每个张量都可以被看作是计算图中的一个节点,每个节点都有一个对应的梯度节点,用于计算该节点对应的变量的梯度。当我们对计算图中的某个节点进行操作时,PyTorch会自动构建该节点的梯度计算图,并通过反向传播算法计算出该节点对应的变量的梯度。

PyTorch还提供了一种方便的、高层次的API,用于构建和训练深度神经网络。这个API称为torch.nn模块,它提供了各种层和损失函数的实现,可以方便地构建各种类型的深度神经网络,例如卷积神经网络、循环神经网络、自编码器等。同时,PyTorch还提供了torch.optim模块,用于实现各种优化算法,例如SGD、Adam等,以便训练深度神经网络。

总之,PyTorch是一个功能强大、易于使用的深度学习框架,适用于各种类型的深度学习任务,包括图像处理、自然语言处理、语音识别等。同时,PyTorch还具有优秀的可扩展性和灵活性,可以方便地与其他Python库和工具集成使用。

视频链接: PyTorch深度学习快速入门教程(绝对通俗易懂!)【小土堆】

1 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
        # os.path.join():将两个路径用“\\”连接
        self.path = os.path.join(self.root_dir, self.label_dir)
        # 返回指定的文件夹包含的文件或文件夹的名字的列表
        self.img_path = os.listdir(self.path)

    # 对象索引函数
    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)


train_root_dir = "data/images/dataset/train"
ant_label_dir = "ants_image"
bee_label_dir = "bees_image"
ant_dataset = MyData(train_root_dir, ant_label_dir)
bee_dataset = MyData(train_root_dir, bee_label_dir)

# __getitem__() 调用方法
ant_img, ant_label = ant_dataset[0]
print(ant_dataset[0])

train_dataset = ant_dataset + bee_dataset  # 按加的顺序组合
train_img, train_label = train_dataset[124]
print(len(ant_dataset), len(bee_dataset), sep="\n")  # 124 121
train_img.show()

2 TensorBoard

  • TensorBoard是一个用于可视化TensorFlow模型和训练结果的工具。它可以帮助你更好地理解和调试你的模型,比如可视化模型的结构、训练和评估指标、数据分布、嵌入向量、图像和视频等。通过使用TensorBoard,你可以更直观地了解模型的表现,找到模型中的问题并进行优化。同时,它也可以用来比较不同模型的性能,跟踪训练过程中的进展,并与他人分享你的研究成果。TensorBoard是一个非常有用的工具,可以帮助你更高效地进行深度学习研究和开发。
from torch.utils.tensorboard import SummaryWriter


writer = SummaryWriter("logs")

# writer.add_image()

for i in range(100):
    # tag:标题;scalar_value:y轴;global_step:x轴
    writer.add_scalar(tag="y = x", scalar_value=i * i, global_step=i)

writer.close()

运行上面代码之后,在下方终端输入:

tensorboard --logdir=logs

之后打开网页:

 http://localhost:6006/ 

端口也可以自己改(6007可以随便改):

tensorboard --logdir=logs --port=6007

3 Transforms

  • 在PyTorch中,Transforms是一种数据预处理的方式。它可以用来对输入数据进行各种变换,例如调整大小、裁剪、旋转、翻转、标准化等等。这些变换可以帮助我们增加训练数据的多样性,减少过拟合,提高模型的泛化能力。
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms


writer = SummaryWriter("logs")
img = Image.open("data/images/SSSS.Dynazenon.jpg")

'''
toTensor:
Convert a ``PIL Image`` or ``numpy.ndarray`` to tensor
PIL Image 和 numpy.ndarray 分别对应PIL和opencv的图片读取
'''
trans_toTensor = transforms.ToTensor()
img_tensor = trans_toTensor(img)
writer.add_image("ToTensor", img_tensor)

'''
normalize:归一化
计算方法:output[channel] = (input[channel] - mean[channel]) / std[channel]
防止数据集中有过大或者过小的值,影响训练
'''
trans_norm = transforms.Normalize([5, 0.5, 0.5], [0.5, 0.5, 0.5])  # 三个通道的均值和标准差
img_norm = trans_norm(img_tensor)
writer.add_image("norm img", img_norm)

'''
resize:
Resize the input image to the given size.
resize不会改变图片的数据类型
'''
trans_resize = transforms.Resize((512, 512))
img_resize = trans_resize(img_tensor)
writer.add_image("resize img", img_resize)

'''
compose:
compose就是一个指定一个transform操作序列,定义了一条加工流水线
'''
trans_resize_2 = transforms.Resize(512)  # 将短边缩到 512
# 先 toTensor 再 resize,前一个的输出对应后一个输入
trans_compose = transforms.Compose([trans_toTensor, trans_resize_2])
img_resize_2 = trans_compose(img)
writer.add_image("resize_2 img", img_resize_2)

'''
random crop:
随机剪一个指定尺寸的新图片
'''
trans_random = transforms.RandomCrop(500)  # 指定裁剪尺寸
trans_compose_2 = transforms.Compose([trans_random, trans_toTensor])
for i in range(10):
    img_crop = trans_compose_2(img)
    writer.add_image("RandomCrop", img_crop, i)

writer.close()

4 运用Datasets以及Transform

import torchvision
from torch.utils.tensorboard import SummaryWriter


# transforms操作序列
dataset_transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor()
])

# 训练集测试集初始化
train_set = torchvision.datasets.CIFAR10(root="./dataset", train=True, transform=dataset_transform, download=True)
test_set = torchvision.datasets.CIFAR10(root="./dataset", train=False, transform=dataset_transform, download=True)

# 将测试集前 10 张在 TensorBoard 显示
writer = SummaryWriter("logs2")
for i in range(10):
    img, target = test_set[i]
    writer.add_image("test_set", img, i)

writer.close()

5 DataLoader

  • 在PyTorch中,DataLoader是一个用于加载和处理数据的工具。它可以帮助我们对数据进行批量读取、异步加载、并行处理等操作,从而提高训练的效率。

  • 通常情况下,我们需要将数据集转化为一个Dataset对象,并通过DataLoader将其加载到模型中进行训练。Dataset对象可以是PyTorch内置的数据集,也可以是用户自定义的数据集。每个数据集可以包含多个样本,每个样本包含一个输入和一个标签。在DataLoader中,我们可以指定批量大小、数据加载顺序、多线程等参数,从而对数据进行处理和加载。

  • 在训练时,我们可以通过迭代DataLoader对象来获取数据。每个迭代返回一个批量的数据,包含了输入和对应的标签。这样,我们就可以在模型上进行批量的训练和推理操作,从而提高训练的效率和准确率。

  • 总之,DataLoader是PyTorch中一个非常重要的工具,可以帮助我们更加高效地加载和处理数据,从而使得模型训练更加简单和快速。

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

test = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor())

'''
batch size:loader能每次装弹4枚进入枪膛,或者理解每次抓四张牌
shuffle:每次epoch是否打乱原来的顺序,就像打完一轮牌后,洗不洗牌
num_workers:进程数
drop last:最后的打他不够一个 batch 还要不要了
'''
test_loader = DataLoader(dataset=test, batch_size=4, shuffle=True, num_workers=0, drop_last=False)

# 使用 board 可视化
writer = SummaryWriter("logs_dataloader")
step = 0
for data in test_loader:
    imgs, targets = data

    # 这里如果你用add image会报错,因为这个方法只能一次写一个图片,你必须换add images方法来写入带有批处理的图片
    # writer.add_image("test set loader", imgs, step)
    writer.add_images("test set loader", imgs, step)
    step += 1

writer.close()

6 nn_module

  • nn 是 Neural Network(神经网络)的缩写
import torch
import torch.nn as nn


# 神经网络类的构造
class Module(nn.Module):
    def __init__(self):
        super().__init__()

    # nn.Module 中的 call 方法会调用 forward
    def forward(self, input):
        output = input + 1
        return output


mdl = Module()
x = torch.tensor(1.0)
output = mdl(x)
print(output)

7 nn_conv(卷积)

  • conv 是 Convolution(卷积)的缩写

  • 下面是一个简单的示意图,展示了一个 \(3\times 3\) 的输入数据和一个 \(2\times 2\) 的卷积核进行卷积操作的过程。

    输入数据:      卷积核:
    1 2 3           1 0
    4 5 6           0 1
    7 8 9      
    
    卷积操作后的输出:
    6  8
    12 14
    

    我们将卷积核从输入数据的左上角开始扫描,并逐步向右下方移动。对于每一个位置,我们将卷积核对应的元素与输入数据中对应位置的元素相乘,并将所有乘积相加,得到一个标量值作为输出数据 \(y\) 中对应位置的值。

    例如,在扫描到输入数据的左上角时,卷积核与输入数据中对应位置的值进行乘法运算,得到的结果为:

    1*1 + 0*2 + 0*4 + 1*5 = 6
    

    具体的卷积操作可以用以下公式表示:

    \[y_{i,j}=\sum_{m=1}^{M}\sum_{n=1}^{N}w_{m,n}x_{i+m-1,j+n-1} \]

    其中 \(x\) 是输入数据,\(w\) 是卷积核,\(y\) 是输出数据,\(M\)\(N\) 是卷积核的大小,\(i\)\(j\) 是输出数据中的位置坐标。

  • 卷积运算示意图:1

import torch
import torch.nn.functional as F


input = torch.tensor([[1, 2, 0, 3, 1],
                      [0, 1, 2, 3, 1],
                      [1, 2, 1, 0, 0],
                      [5, 2, 3, 1, 1],
                      [2, 1, 0, 1, 1]])

kernel = torch.tensor([[1, 2, 1],
                       [0, 1, 0],
                       [2, 1, 0]])

# conv2d需要输入的tensor是四维的(batch, c,h,w),但是现在的input kernel是二维
# (要变换的矩阵, (批次大小(batch size), 通道数(channel), 矩阵高度, 矩阵宽度))
input = torch.reshape(input, (1, 1, 5, 5))
kernel = torch.reshape(kernel, (1, 1, 3, 3))

# stride:步长,padding:如果步长是 1,又想保持输入输出高宽不变,就把 padding 设置 1
output1 = F.conv2d(input, kernel, stride=1)
print(output1)

# padding=1 会在输入矩阵外圈填充一圈 0,在卷积时,如果 stride=1,输入输出尺寸会不变
output2 = F.conv2d(input, kernel, stride=1, padding=1)
print(output2)

8 nn_conv2d(卷积)

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


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


class Module(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = Conv2d(in_channels=3, out_channels=6,
                            kernel_size=(3, 3), stride=(1, 1), padding=0)

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


mdl = Module()
print(mdl)

writer = SummaryWriter("logs_nn_conv2d")

step = 0
for data in dataloader:
    img, target = data
    output = mdl(img)
    # output 的 size 是 torch.Size([64, 6, 30, 30])
    writer.add_images("before conv2d", img, step)
    # output 是 6 通道,board不知道该怎么写入图片了,所以要 reshape
    output = torch.reshape(output, (-1, 3, 30, 30))  # batch size不知道可以填 -1
    writer.add_images("after conv2d", output, step)

    step += 1

writer.close()

9 nn_maxpool(最大池化)

  • 池化(Pooling)是神经网络中另一种常用的操作,通常紧随卷积层之后,用于减小特征图的尺寸,提高模型的计算效率和泛化能力。

  • 最大池化(Max Pooling)是一种常用的池化操作,其原理是在输入数据的每个局部区域中选取最大的值作为输出。下面我来详细解释一下最大池化运算的过程,并举一个简单的例子来说明。

    假设我们有一个 \(4\times 4\) 的输入数据 \(x\),如下所示:

    1 2 2 1
    2 2 1 1
    1 1 1 2
    2 1 2 1
    

    我们希望对这个输入数据进行最大池化操作,设置池化窗口的大小为 \(2\times 2\),即将输入数据分割成若干个 \(2\times 2\) 的局部区域。对于每个局部区域,我们选取其中的最大值作为输出。

    具体地,我们从输入数据的左上角开始,将池化窗口逐步向右下方移动。对于每个池化窗口,我们选取其中的最大值作为输出。例如,在扫描到输入数据的左上角时,池化窗口覆盖的局部区域为:

    1 2
    2 2
    

    其中最大的值为 \(2\),因此将 \(2\) 作为输出。接着,将池化窗口向右移动池化窗口尺寸大小,重复上述操作,得到第一行第二个位置的输出:

    2 1
    1 1
    

    其中最大的值为 \(2\),因此将 \(2\) 作为输出。以此类推,可以得到输入数据的所有局部区域的最大值,从而得到最终的输出数据 \(y\)

    2 2
    2 2
    

    这个输出数据的尺寸是原输入数据的一半,因为我们使用了 \(2\times 2\) 的池化窗口,将输入数据的尺寸减小了一半。

  • 最大池化操作可以减小特征图的尺寸,同时还可以提取输入数据的局部不变特征,增强模型对图像、语音等数据的抗干扰能力。在卷积神经网络中,通常将最大池化操作和卷积操作交替出现,构成卷积池化层组合,用于提取输入数据的高层特征。

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


# dtype:设置矩阵元素的数据类型
input = torch.tensor([[1, 2, 0, 3, 1],
                      [0, 1, 2, 3, 1],
                      [1, 2, 1, 0, 0],
                      [5, 2, 3, 1, 1],
                      [2, 1, 0, 1, 1]], dtype=torch.float)

input = torch.reshape(input, (-1, 1, 5, 5))

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


# ceil mode:池化核走出input时还要不要里边的最大值 默认不要
class Module(nn.Module):
    def __init__(self):
        super(Module, self).__init__()
        self.max_pool1 = MaxPool2d(kernel_size=(3, 3), ceil_mode=True)

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


mdl = Module()
output = mdl(input)
print(output)

writer = SummaryWriter("logs_maxpool")

step = 0
for data in dataloader:
    img, target = data
    writer.add_images("input", img, step)
    output = mdl(img)
    writer.add_images("output", output, step)

    step += 1

writer.close()

10 nn_relu(非线性激活)

  • 非线性激活函数是神经网络中一种常用的激活函数,它的作用是将神经元的输入信号进行非线性变换,增强神经网络的非线性表示能力。常见的非线性激活函数包括sigmoid函数、tanh函数、ReLU函数、Leaky ReLU函数、ELU函数等。

    其中,sigmoid函数将输入映射到[0,1]的范围内,tanh函数将输入映射到[-1,1]的范围内。ReLU函数在输入为正时输出等于输入,输入为负时输出为0,Leaky ReLU函数在输入为负时输出一个小的负数,ELU函数在输入为负时输出一个接近于0的小值,比ReLU函数更平滑。

    在神经网络中,非线性激活函数的作用是引入非线性变换,使得神经网络可以学习非线性关系,从而提高模型的表达能力和性能。

  • ReLU(Rectified Linear Unit)函数是一种常用的非线性激活函数,它将输入信号直接输出作为输出信号,即:

    f(x) = max(0, x)

    其中,x是输入信号,f(x)是输出信号。

    ReLU函数的特点是在输入为正时直接输出,而在输入为负时输出为0,因此它具有计算简单、速度快的优点。此外,ReLU函数还具有一定的生物学合理性,因为在生物神经元中,当输入信号超过一定阈值时才会产生输出信号,而ReLU函数也具有这种类似的特点。

    在深度学习中,ReLU函数已经成为了一种标配的激活函数,并且在很多神经网络模型中都被广泛使用。它的应用除了可以提高模型的表达能力外,还可以缓解神经网络中的梯度消失问题,从而加速模型的训练和收敛。

  • Sigmoid函数是一种常用的非线性激活函数,它将输入信号压缩到[0,1]的范围内,具有平滑的S形曲线,定义如下:

    f(x) = 1 / (1 + exp(-x))

    其中,x是输入信号,f(x)是输出信号。

    Sigmoid函数的输出值在[0,1]之间,并且随着输入值的变化呈现出平滑的曲线变化,因此在一些需要将输出值限制在一定范围内的任务中,Sigmoid函数是一种比较常用的选择。此外,Sigmoid函数在二分类问题中也经常被用作输出层的激活函数,可以将输出值解释为概率。

    然而,Sigmoid函数也存在一些问题,其中一个主要问题是在输入值较大或较小时,函数的梯度会趋近于0,从而导致反向传播过程中的梯度消失问题。此外,Sigmoid函数的输出值不是以0为中心对称的,这可能导致一些神经元被“杀死”,即输出恒为0,从而导致模型的性能下降。

    在实际应用中,由于Sigmoid函数存在上述问题,它已经逐渐被ReLU函数等其他激活函数所取代。

import torch
import torchvision
from torch import nn
from torch.nn import ReLU, Sigmoid
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter


input = torch.tensor([[1, -0.5],
                     [-1, 3]])

input = torch.reshape(input, (-1, 1, 2, 2))

dataset = torchvision.datasets.CIFAR10("./dataset", False, torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, 64)


class Module(nn.Module):
    def __init__(self):
        super(Module, self).__init__()
        # inplace=True:ReLU会返回一个output,input的值不变,可以保留原始数据,一般情况inplace=False
        self.relu1 = ReLU(inplace=False)  # 默认为 False
        self.sigmoid = Sigmoid()

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


mdl = Module()

writer = SummaryWriter("logs_relu")
step = 0

for data in dataloader:
    img, target = data
    writer.add_images("input", img, step)
    output = mdl(img)
    writer.add_images("output", img, step)

    step += 1

writer.close()

11 nn_linear(线性层)

把图像每行首尾相接,并成一行

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


dataset = torchvision.datasets.CIFAR10("./dataset", False, torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, 64)


class Module(nn.Module):
    def __init__(self):
        super(Module, self).__init__()
        self.linear1 = Linear(196608, 10)

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


mdl = Module()

for data in dataloader:
    img, target = data
    print(img.shape)
    output = torch.reshape(img, (1, 1, 1, -1))
    print(output.shape)
    output = mdl(output)
    print(output.shape)

或者(常用):

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


dataset = torchvision.datasets.CIFAR10("./dataset", False, torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, 64)

for data in dataloader:
    img, target = data
    print(img.shape)
    output = torch.flatten(img)
    print(output.shape)

12 nn_seq(Sequential)

torch.nn.Sequential 是 PyTorch 中的一个模型容器,它允许用户按照顺序组合多个神经网络层以构建神经网络模型。通过 Sequential,可以方便地将多个层组合成一个整体,并将其作为一个单独的层或模块来使用。

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

dataset = torchvision.datasets.CIFAR10("./dataset", False, torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, 64)


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

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


mdl = Module()
print(mdl)
# 用 1 填充矩阵;(批次大小(batch size), 通道数(channel), 矩阵高度, 矩阵宽度)
input = torch.ones((64, 3, 32, 32))
output = mdl(input)
print(output.shape)

13 nn_loss(损失函数)

  • L1 Loss和MSE Loss都是在深度学习中用于计算预测值和真实值之间差异的损失函数。
    • L1 Loss,也叫做Mean Absolute Error(MAE)损失,是预测值和真实值之差的绝对值之和的平均数。它的公式是: L1 Loss = |预测值 - 真实值|
    • L1 Loss相对于MSE Loss来说,更加注重outliers。当存在很多离群点(outliers)或数据分布不均匀的情况,使用L1 Loss可能会更加合适。
  • MSE Loss,也称为Mean Squared Error(MSE)的损失,计算预测值和真实值之间差的平方的平均数。它的公式是: MSE Loss = (预测值 - 真实值)²,MSE损失是最常用的损失函数之一。当预测值和真实值之间的差异很小,使用MSE Loss可以帮助模型更快地收敛。
  • CrossEntropyLoss是一种常用于机器学习和深度学习分类任务的损失函数。它通常用于神经网络中,例如图像分类、语音识别和自然语言处理等任务。CrossEntropyLoss测量预测概率分布与实际概率分布之间的差异。它会为错误的预测分配更高的损失,为正确的预测分配较低的损失。目标是最小化损失,使得预测的概率尽可能与实际概率匹配。CrossEntropyLoss的公式如下: L = -1/N * sum(ylog(p) + (1-y)log(1-p)) 其中,y是真实标签(0或1),p是预测概率,N是样本数,log是自然对数。
import torch
from torch.nn import L1Loss, MSELoss, CrossEntropyLoss

inputs = torch.tensor([1, 2, 3], dtype=torch.float32)
outputs = torch.tensor([1, 2, 5], dtype=torch.float32)

inputs = torch.reshape(inputs, (1, 1, 1, 3))
outputs = torch.reshape(outputs, (1, 1, 1, 3))

loss = L1Loss(reduction="sum")
result = loss(inputs, outputs)

loss_mse = MSELoss()
result_mse = loss_mse(inputs, outputs)

print(result)
print(result_mse)

x = torch.tensor([0.1, 0.2, 0.3])
y = torch.tensor([1])
x = torch.reshape(x, (1, 3))
loss_cross = CrossEntropyLoss()
result_cross = loss_cross(x, y)
print(result_cross)

14 nn_optim(优化器)

  • SGD是常用的优化算法之一,用于训练神经网络。在SGD中,每次迭代时,我们从数据集中随机选择一个小批量的样本,计算这些样本的损失函数,并更新网络参数以尽量减小损失。更新的方向是梯度的相反方向,更新的大小由学习率控制,通常采用动态调整学习率的方法来提高性能。
  • 具体来说,SGD的更新公式为: $$ w_{t+1} = w_t - \alpha\nabla L(w_t;x_t,y_t) $$ 其中,\(w\)是待更新的参数向量,\(\alpha\)是学习率,\(L\)是损失函数,\(\nabla L(w_t;x_t,y_t)\)是损失函数对参数的梯度。在每次迭代中,我们从数据集中随机选择一个小批量的样本\((x_t,y_t)\),然后根据当前参数\(w_t\)计算出该样本的损失函数的梯度,并根据上述公式更新参数\(w\),直到达到预定的迭代次数或者损失函数收敛为止。 需要注意的是,SGD很容易陷入局部最优解,因此需要采用一些正则化技术来减少过拟合的风险,如L1/L2正则化、dropout等。
  • CrossEntropyLoss() 是PyTorch中用于多分类任务的损失函数之一,常用于分类问题中。它是交叉熵损失函数的一种特殊形式,可以用于衡量模型预测的概率分布与真实标签的差距。 具体来说,对于一个大小为 N 的样本集合,其损失函数的计算方式为: $$ \text{loss} = -\frac{1}{N}\sum_{i=1}{N}\sum_{j=1}y_{ij}\log(p_{ij}) $$ 其中,\(y_{ij}\)表示第\(i\)个样本的第\(j\)个标签是否为正样本,当\(y_{ij}=1\)时表示正样本,否则为负样本。\(p_{ij}\)表示模型预测第\(i\)个样本的第\(j\)个类别的概率,它是模型的输出结果。 对于每个样本,该损失函数计算了它所有标签的损失,并对所有样本的损失求平均。当模型的预测与真实标签完全一致时,损失函数为0,否则损失函数会大于0。 在PyTorch中,可以使用 nn.CrossEntropyLoss() 来构造一个交叉熵损失函数。它会自动地对模型的输出进行softmax归一化,并计算交叉熵损失。
import torch
import torchvision
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.tensorboard import SummaryWriter
from torch.optim.lr_scheduler import StepLR
from torch.utils.data import DataLoader

"""
demo1:在模型训练中加入损失函数和优化器
"""
dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=1)


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


mld = Module()
# 损失函数 优化器
loss = nn.CrossEntropyLoss()
# 调用 mld.parameters() 可以获取到 Module 中的所有可训练参数;lr是训练速率
optim = torch.optim.SGD(mld.parameters(), lr=0.01)

for i in range(20):
    running_loss = 0.0
    for data in dataloader:
        imgs, targets = data
        outputs = mld(imgs)
        result_loss = loss(outputs, targets)
        # 注意 清零--》反向传播算梯度--》更新参数
        optim.zero_grad()
        result_loss.backward()
        optim.step()
        running_loss = running_loss + result_loss
    print(running_loss)

# running_loss:
# tensor(18726.5977, grad_fn=<AddBackward0>)
# tensor(16132.8926, grad_fn=<AddBackward0>)
# tensor(15426.6357, grad_fn=<AddBackward0>)

15 model_pretrained(现有网络模型使用与修改)

import torch
import torchvision
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential

"""
demo1:加载vgg训练好的模型,并在里边加入一个线性层
"""
# ImageNet数据集太大了,100多G,还是用CIFAR10吧
# train_data = torchvision.datasets.ImageNet("../data_image_net", split='train', download=True,
#                                         transform=torchvision.transforms.ToTensor())
train_data = torchvision.datasets.CIFAR10('../data', train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
# 加载现有的vgg模型
vgg16_not_pretrain = torchvision.models.vgg16(pretrained=False)
vgg16_pretrained = torchvision.models.vgg16(pretrained=True)

# 修改方法1:加入一个线性层,编号7
vgg16_pretrained.add_module("7", nn.Linear(1000, 10))
print(vgg16_pretrained)

# 修改方法2:修改原来的第六个线性层
vgg16_not_pretrain.classifier[6] = nn.Linear(4096, 10)
print(vgg16_not_pretrain)

16 model_save(模型的保存与加载)

import torch
import torchvision
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential

train_data = torchvision.datasets.CIFAR10('../data', train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
# 加载现有的vgg模型
vgg16_not_pretrain = torchvision.models.vgg16(pretrained=False)
vgg16_pretrained = torchvision.models.vgg16(pretrained=True)

"""
demo2:保存/加载模型
两种保存方法 对应两种加载方法
保存模型都是用torch.save,加载模型都是用torch.load,一起保存的时候save整个模型,加载时直接torch.load加载
保存时只保存参数的,需要先向model vgg加载结构,再用model vgg.load state dict加载参数,加载参数还是要torc.load方法
保存方法1的‘陷阱’:
在使用方法1保存现有模型时,不会出错,代码更少,但是使用方法1保存自己的模型时,必须要引入这个模型的定义才可以
"""
# 保存东西需要现有东西保存
vgg16 = torchvision.models.vgg16(pretrained=False)
# 保存方式1:模型结构+参数一起保存
torch.save(vgg16, "vgg16_pretrained_save_method1.pth")
# 多保存一个vgg16_pretrained,后面 完整模型测试讨论会用到
torch.save(vgg16_pretrained, "vgg16_pretrained_save_method1.pth")
# 加载方式1
model1 = torch.load("vgg16_save_method1.pth")


# 保存方式2:只保存模型参数(推荐)
torch.save(vgg16.state_dict(), "vgg16_save_method2.pth")
# 加载方式2
# 先加载模型结构
model_vgg = torchvision.models.vgg16(pretrained=False)
# 再加载模型参数
model_vgg.load_state_dict(torch.load("vgg16_save_method2.pth"))


# 保存方法1的‘陷阱’
"""先保存tudui模型
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, 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


tudui = Tudui()
torch.save(tudui, "tudui_save_method1.pth")
"""

"""
直接加载tudui模型会报错
tudui = torch.load("tudui_save_method1.pth")
报错:
AttributeError: Can't get attribute 'Tudui' ...>
"""

17 完整训练流程

  • model.py
import torch
from torch import nn
from torch.nn import Sequential, Conv2d, ReLU, MaxPool2d, Flatten, Linear


# 创建网络模型
class Module(nn.Module):
    def __init__(self):
        super(Module, self).__init__()
        self.model1 = Sequential(
            Conv2d(3, 32, (5, 5), (1, 1), 2),
            ReLU(),
            MaxPool2d(2),
            Conv2d(32, 32, (5, 5), (1, 1), 2),
            ReLU(),
            MaxPool2d(2),
            Conv2d(32, 64, (5, 5), (1, 1), 2),
            ReLU(),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )

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


if __name__ == '__main__':
    mdl = Module()
    inputs = torch.ones((64, 3, 32, 32))
    outputs = mdl(inputs)
    print(outputs.shape)

  • trian.py
import os
import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

from model import Module

# 定义训练的设备
# cuda:0 是独显,因为集显没有cuda
# cuda:0 可以替换成 cuda,对于单显卡的 pc 没有区别
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

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)

train_data_size = len(train_data)
test_data_size = len(test_data)

print(f"训练数据集长度:{train_data_size}")
print(f"测试数据集长度:{test_data_size}")

# 使用dataloader加载数据
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建模型
mdl = Module()
mdl = mdl.to(device)

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

# 优化器
learning_rate = 0.01
optimizer = torch.optim.SGD(mdl.parameters(), lr=learning_rate)

# 设置控制训练次数的参数
# 记录训练 测试次数
total_train_step = 0
total_test_step = 0
# 训练轮数
epoch = 20
# 记录最佳准确率
best_accuracy = 0
# 写入board
writer = SummaryWriter("logs_train")

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

    # 训练步骤开始
    mdl.train()
    for data in train_dataloader:
        imgs, targets = data
        imgs = imgs.to(device)
        targets = targets.to(device)

        outputs = mdl(imgs)
        loss = loss_fn(outputs, targets)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_train_step += 1
        writer.add_scalar("train loss", loss.item(), total_train_step)

        if total_train_step % 100 == 0:
            print("训练次数:{}, loss:{}".format(total_train_step, loss.item()))

    # 测试步骤开始
    mdl.eval()
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            imgs = imgs.to(device)
            targets = targets.to(device)
            outputs = mdl(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss += loss
            accuracy = (outputs.argmax(1) == targets).sum()
            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)

    now_accuracy = total_accuracy / test_data_size
    writer.add_scalar("test accuracy", total_accuracy / test_data_size, total_test_step)
    total_test_step += 1

    model_folder = "./model"
    if not os.path.exists(model_folder):
        os.makedirs(model_folder)

    # 保存准确率最高的模型
    if now_accuracy > best_accuracy:
        best_accuracy = now_accuracy
        torch.save(mdl.state_dict(), f"./model/best.pth")
    # 保存最新的模型
    if i == epoch - 1:
        torch.save(mdl.state_dict(), f"./model/last.pth")

writer.close()

18 完整推理流程

  • 找一张狗的图片,放在./data/images/dog.jpg
import torch
import torchvision
from PIL import Image

from model import Module

# 加载数据集
dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=False)
# 获取分类列表
class_list = list(dataset.class_to_idx.keys())

# 加载图片
image_path = "./data/images/dog.jpg"
image = Image.open(image_path)

'''
image = image.convert('RGB')
由于图片有png jpg的不同格式,而png图片是四通道的 多一个透明度通道,jpg是三通道的 只有三个颜色通道
这一行代码可以让png jpg都只有三个颜色通道,增强了代码的适应性
'''
image = image.convert('RGB')
transform = torchvision.transforms.Compose([torchvision.transforms.Resize((32, 32)),
                                            torchvision.transforms.ToTensor()])
image = transform(image)
print(image.shape)

# 加载模型
model = Module()
model.load_state_dict(torch.load("./model/best.pth", map_location=torch.device('cuda')))
print(model)
image = torch.reshape(image, (1, 3, 32, 32))

# 查看模型输出
model.eval()
with torch.no_grad():
    output = model(image)
# 所有类别的概率
print(output)

# 找到最大的概率值的位置 查看数字对应类别在debug datasets.CIFAR10的class to idx
print(output.argmax(1))
# 输出预测结果
print(class_list[output.argmax(1)])

posted @ 2023-05-17 12:33  修凡  阅读(54)  评论(0编辑  收藏  举报