近期一直在做实验室的项目,比较忙。最近总算是暂时告一段落了,于是决定把之前的笔记整理整理,发到博客上。

这篇文章是之前学习 PyTorch 记录的内容,主要来自于 Microsoft Learn 上面的 PyTorch 教程。

1. 什么是张量

张量是一种特殊的数据结构,和矩阵很相似。在 PyTorch 中用张量对模型的输入输出和模型参数进行编码。张量类似于 NumPy 的 ndarray,不同之处在于张量可以在 GPU 或其他硬件加速器上运行。

1.1 张量的初始化

张量初始化:

import torch
import numpy as np

# 第一种方法:数组转换
data = [[1, 2], [3, 4]]
data_tensor = torch.tensor(data)

# 第二种方法:从 NumPy 的 ndarray 得到
np_array = np.array(data)
np_array_tensor = torch.tensor(np_array)

# 第三种方法:从 tensor 转换
ones_tensor = torch.ones_like(data_tensor)
rand_tensor = torch.rand_like(data_tensor, dtype = torch.float)

# 第四种方法:从 shape 形状元组转换
shape = (2, 3, )
ones_tensor = torch.ones(shape)
rand_tensor = torch.rand(shape)
zeros_tensor = torch.zeros(shape)

1.2 张量的属性

张量的属性:

# 形状:
tensor_shape = tensor.shape
# 数据类型:
tensor_dtype = tensor.dtype
# 设备:
tensor_device = tensor.device

1.3 张量的运算

张量的运算:这里记录了 100 多种张量运算方法。张量初始化都是在 CPU 中,需要用 to 方法转移到 GPU 中。

# 转移 tensor
if torch.cuda.is_available():
    tensor = tensor.to('cuda')

基本运算:

tensor = tensor.ones(4, 4)
tensor_first_row = tensor[0]
tensor_first_column = tensor[:, 0]
tensor_last_column = tensor[..., -1]

# 第一列全部设为 0
tensor[:, 1] = 0

# 以指定维度连接张量
new_tensor = torch.cat([tensor, tensor, tensor], dim = 1)

矩阵乘法:

# 第一种方法:@ 运算符
matmul1 = tensor @ tensor.T

# 第二种方法:torch.tensor.matmul 方法
matmul2 = tensor.matmul(tensor.T)

# 第三种方法:torch.matmaul 方法
matmul3 = torch.zeros(tensor)
torch.matmul(tensor, tensor.T, out = matmul3)

元素乘法:

# 第一种方法:* 运算符
mul1 = tensor * tensor

# 第二种方法:tensor.torch.mul 运算符
mul2 = tensor.mul(tensor)

# 第三种方法:torch.mul 运算符
mul3 = torch.zeros(tensor)
torch.mul(tensor, tensor, out = mul3)

求和和转换为 python 值:

agg = tensor.sum()

# 需要用 item 方法转换为 python 类型
agg_python_item = agg.item()

# agg_python_item 为 12.0. dtype 为 float

NumPy 和 Tensor 的桥接:改变一个将改变另一个

# tensor 到 numpy,直接用 tensor 的 numpy 方法就可以
numpy1 = tensor1.numpy()

# numpy 到 tensor,需要用到 torch 的 from_numpy 方法
tensor2 = torch.from_numpy(numpy2)

2. 数据集和数据加载

PyTorch 提供了两个数据原语:torch.utils.data.DataLoader 和 torch.utils.data.Dataset,

2.1 加载数据集

加载数据集:

import torch
from torch.utils.data import Dataset
# 这里用到的是图像数据集,所以要导入 torchvision。文本数据集是 torchtext,语音数据集是 torchaudio
from torchvision import datasets
# ToTensor 将数据转换为 tensor 类型
from torchvision.transforms import ToTensor, Lambda
import matplotlib.pyplot as plt

training_data = datasets.FashionMNIST(
    root = "data".
    train = True,
    download = True,
    transform = ToTensor()
)

test_data = datasets.FashionMNIST(
    root = "data",
    train = False,
    download = True,
    transform = ToTensor()
)

迭代和可视化:

labels_map = {
    0: "T-Shirt",
    1: "Trouser",
    2: "Pullover",
    3: "Dress",
    4: "Coat",
    5: "Sandal",
    6: "Shirt",
    7: "Sneaker",
    8: "Bag",
    9: "Ankle Boot",
}

# 用 Matplotlib 绘图
figure = plt.figure(figsize = (8, 8))
cols, rows = 3, 3
for i in range(1, cols * rows + 1):
    # 随机从数据集中采样,每次采样一个,然后将数据转换为 python 数
    sample_idx = torch.randint(len(training_data), size = (1,)).item()
    img, label = training_data[sample_idx]
    # 在图像中加入该图
    figure.add_subplot(rows, cols, i)
    # 用 matplotlib.pyplot 设置每个子图绘制的要素
    plt.title(labels_map[label])
    plt.axis("off")
    # 压缩并用灰度图
    plt.imshow(img.squeeze(), cmap = "gray")
plt.show()

2.2 自定义数据集

自定义数据集需要提供三个功能:__init__, __len____getitem__

import os
import pandas as pd
import torchvision.io as tvio

class CustomImageDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
        self.img_labels = pd.read_csv(annotations_file)
        self.img_dir = img_dir
        self.transform = transform
        self.target_transform = target_transform

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

    def __getitem__(self, idx):
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
        image = tvio.read_image(img_path)
        label = self.img_labels.iloc[idx, 1]
        if self.transform:
            image = self.transform(image)
        if self.target_transform:
            label = self.target_transform(label)
        sample = {"image": image, "label": label}
        return sample

数据集样例:csv 文件需要是这样的。

    tshirt1.jpg, 0
    tshirt2.jpg, 0
    ......
    ankleboot999.jpg, 9

2.3 数据迭代器

数据迭代器:DataLoader 是一个迭代器。训练模型时,希望用 batch 的形式传递样本,通过抽取数据减少拟合,同时通过 python multiprocessing 加快数据处理。DataLoader 可以简单的方式提供这些功能。

from torch.utils.data import DataLoader

# 构建数据迭代器
train_dataloader = DataLoader(training_data, batch_size = 64, shuffle = True)
test_dataloader = DataLoader(testing_data, batch_size = 64, shuffle = True)

数据迭代:DataLoader 每次都会得到一批 train_features 和 train_labels。由于设置了 shuffle = True,遍历完所有批次后,数据会被打乱。

train_features, train_labels = next(iter(train_dataloader))
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap = "gray")
plt.show()

3. 数据集转换

数据集转换用于将提供的数据转换成容易训练的形式。

3.1 数据集转换

数据集转换:torchvision.transform 提供了一些类似的转换。以数据集 FashionMNIST 为例,提供的 features 为 PIL 图像,标签为整数。需要将标签转换为归一化张量,标签转换为独热编码张量。对此,可以使用 ToTensor 方法和 Lambda 函数实现。

from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda

ds = datasets.FashionMNIST(
    root = "data",
    train = True,
    download = True,
    transform = ToTensor(),
    target_transform = Lambda(lambda y: torch.zeros(10, dtype = torch.float).scatter_(0, torch.tensor(y), value = 1))
)

4. 构建模型层

构建神经网络时,模型由 torch.nn 提供的模型层构成,同时模型应该以 toch.nn.Module 为参数。

4.1 构建神经网络模型

构建神经网络模型:通过子类化定义神经网络 nn.Module,在 __init__ 方法中

import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

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

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28 * 28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
            nn.ReLU()
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

4.2 应用模型

应用模型:

# 将模型提交给设备
model = NeuralNetwork().to(device)
print(model)

# 生成特征
x = torch.rand(1, 28, 28, device = device)
# 得到结果
logits = model(x)
# 预测层采用 Softmax
predict = nn.Softmax(dim = 1)(logits)
# 获取预测结果
y = predict.argmax(1)
print(y)

image-20211121204310892

4.3 各层分析

各层分析:为了分析每一层的效果,构建如下实验。

# 构建 3 * 28 * 28 的图像特征(可以用 size 方法查看形状)
input_image = torch.rand(3, 28, 28)

# 展平层,将 3 * 28 * 28 转换为 3 * 784 的连续数组,保持批量维度不变,也就是 3 不变
flatten = nn.Flatten()
flat_image = flatten(input_image)

# 线性层,存储权重和偏置,对输入进行线性变换
layer1 = nn.Linear(in_features = 28 * 28, out_features = 20)
hidden1 = layer1(flat_image)

# 非线性激活,在模型的输入和输出之间构建复杂映射,在线性后引入非线性变换,帮助神经网络学习各种现象。
hidden1 = nn.ReLU()(hidden1)

# 顺序容器,构建一个快速网络
seq_modules = nn.Sequential(
    flatten,
    layer1,
    nn.ReLU(),
    nn.Linear(20, 10)
)
input_image = torch.rand(3, 28, 28)
logits = seq_modules(input_image)

# Softmax 激活函数,将 logits 缩放到 [0, 1],代表模型对每个类别的预测密度。dim 指的是缩放的维度,比如 [2,3,4] 这样的张量,nn.Softmax(dim = 1),就是 3 对应的维度。
softmax = nn.Softmax(dim = 1)
predict = softmax(logits)
y = argmax(predict)

# 打印模型结构
print("Model structure: ", model, "\n\n")

for name, param in model.named_parameters():
    print(f"Layer: {name} | Size: {param.size()} | Values : {param[:2]} \n")

image-20211122112355299

  • 图:softmax 的结果

5. 自动微分

训练神经网络时,最常用的算法是反向传播。参数(模型权重)根据损失函数,根据给定参数的梯度进行调整。torch.autograd 是内置微分引擎,支持任何计算图的梯度自动计算。

5.1 自动微分计算

自动微分计算:PyTorch 提供方法自动前向计算函数和反向传播计算导数。grad_fn 属性存储了反向传播函数。

import torch

x = torch.ones(5)
y = torch.zeros(3)
w = torch.randn(5, 3, requires_grad = True)
b = torch.randn(3, requires_grad = True)
z = torch.matmul(x, w) + b
loss = torch.nn.functional.binary_cross_with_logits(z, y)

# z 和 loss 的反向传播函数存储在 z.grad_fn 和 loss.grad_fn 中。

image-20211122152352667

  • 图:计算图。其中 w 和 b 是要优化的参数,因此设置了 requires_grad 属性。可以在创建时设置 requires_grad,也可以随后使用 x.requires_grad(True) 进行设置。

计算梯度:为了优化神经网络中参数的权重,需要计算损失函数关于参数的导数。为了计算导数,要调用 loss.backward() 方法,然后从 w.gradb.grad 中获取导数。backward 方法一次只能调用一次。

loss.backward()
print(w.grad)
print(b.grad)

禁用梯度:如果模型完成了训练,只需要预测,可以设置 torch.no_grad() 包裹代码块实现单纯预测。另一种方法是用 detach() 方法。

z = torch.matmul(x, w) + b

# 第一种方法:torch.no_grad() 包裹
with torch.no_grad():
    z = torch.matmul(x, w) + b

# 第二种方法:detach() 方法
z_detach = z.detach()

6. 优化模型参数

有了模型和数据,就可以通过优化数据来训练、验证和测试模型了。训练模型在每次迭代(epoch)中对模型进行预测,计算其中误差(loss),然后反向传播计算导数,并优化梯度下降的参数。

之前的代码:

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda

training_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)

test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28 * 28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
            nn.ReLU()
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

model = NeuralNetwork()

6.1 设置超参数

设置超参数:超参数是可调节的参数,用于控制模型优化过程。设置如下超参数:迭代数据集的次数(epoch num),模型在每个 epoch 中看到的数据样本数量(batch size),每个批次 batch 和迭代 epoch 中更新模型的程度(learning rate)。较小的学习率会导致学习过慢,较大的会让学习过程中出现不可预测行为。

learning_rate = 1e-3
batch_size = 64
epoch = 5

6.2 设置优化循环

设置优化循环:设置完超参数就可以进行优化循环来训练和优化模型。优化循环的每一轮是一个 epoch,每个 epoch 包括两步:训练循环和验证测试循环。

添加损失函数:损失函数衡量模型得到的结果和目标值的不相似程度。常见的损失函数包括回归任务的 nn.MSELoss(均方误差),分类任务的 nn.NLLLoss(负对数似然)和 nn.CrossEntropyLoss(交叉熵损失)(交叉熵损失 nn. CrossEntropyLoss 融合了 nn.LogSoftmaxnn.NLLLoss)。将模型的输出传递给损失函数,从而标准化并预测误差。

loss_fn = nn.CrossEntropyLoss()

添加优化器:优化是在每个训练步骤中调整模型参数,从而降低模型损失的过程。优化算法定义了这一步是如何进行的。所有优化都在 optimizer 对象中。训练过程中,优化包括三步:

  • 调用 optimizer.zero_grad() 重置模型参数的梯度,默认情况下渐变相加,为了避免重复计算,每次迭代 epoch 时明确归零。
  • 调用反向传播预测损失 loss.backwards(),获取并存储梯度。
  • 获取梯度后,调用 optimizer.step() 通过梯度进行参数调整。
optimizer = torch.optim.SGD(model.parameters, lr = learning_rate)

优化循环:

def train_loop(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    for batch, (x, y) in enumerate(dataloader):
        predict = model(x)

        # 计算损失
        loss = loss_fn(predict, y)

        # 优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(x)
            print(f"loss: {loss:>7f} [{current:>5d}]/{size:>5d}")

测试循环:

def test_loop(dataloader, model, loss_fn):
    # 计算 batch_size
    size = len(dataloader.dataset)
    test_loss, correct = 0, 0

    with torch.no_grad():
        for x, y in dataloader:
            predict = model(x)

            # 因为要算数了,所以后面跟个 item()
            test_loss += loss_fn(predict, y).item()
            # 计算正确预测个数
            correct += (predict.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

6.3 开始学习

开始学习:完成模型定义、超参数设置、优化循环设置后,就可以开始学习了。

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), ln = learning_rate)

epochs = 10
for t in range(epochs):
    print(f"Epoch {t + 1}")
    train_loop(train_dataloader, model, loss_fn, optimizer)
    test_loop(test_dataloader, model, loss_fn)
print("Done.")

7. 保存和加载模型

保存和加载模型。

import torch
import torch.onnx as onnx
import torchvision.models as models

7.1 保存和加载模型权重

保存和加载模型权重:可以用 torch.save(model.state_dict(), path) 存储模型权重。model.state_dict() 保存了权重。可以用 torch.load_state_dict(torch.load(path)) 导入模型权重。PyTorch 的模型将学习到的参数存储到了内部字典 state_dict 中,可以通过 model.state_dict() 获取。

# 获取模型
model = models.vgg16(pretrained = True)
# 保存模型权重
torch.save(model.state_dict(), 'data/model_weights.pth')

# 获取没有权重的模型
model = models.vgg16()
# 导入模型权重
model.load_state_dict(torch.load('data/model_weights.pth'))

7.2 保存和加载带有形状的模型

保存和加载带有形状的模型:保存模型和读取模型就更简单了。直接 torch.save(model, path) 和 torch.load(path) 就行了。

torch.save(model, 'data/model_weights.pth')
model = torch.load('data/model_weights.pth')

8. 完整模型构建

8.1 数据处理

数据处理:

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda, Compose
import matplotlib.pyplolt as plt

training_data = datasets.FashionMNIST(
    root = 'data',
    train = True,
    download = True,
    transform = ToTensor(),
)
test_data = datasets.FashionMNIST(
    root = 'data',
    train = False,
    download = True,
    transform = ToTensor(),
)

batch_size = 64

train_dataloader = DataLoader(training_data, batch_size = batch_size)
test_dataloader = DataLoader(test_data, batch_size = batch_size)

8.2 创建模型

创建模型:

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Using {} device'.format(device))

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28 * 28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
            nn.ReLU(),
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

8.3 优化模型参数

优化模型参数:

loss_fn = nn.CrossEntropyLoss()
learning_rate = 1e-3
optimizer = torch.optim.SGD(model.parameters(), lr = learning_rate)

def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    for batch, (x, y) in enumerate(dataloader):
        x, y = x.to(device), y.to(device)

        predict = model(x)
        loss = loss_fn(predict, y)

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

        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(x)
            print(f'loss: {loss:>7f} [{current:>5d}/{size:>5d}]')

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    model.eval()
    test_loss, corrent = 0, 0
    with torch.no_grad():
        for x, y in dataloader:
            x, y = x.to(device), y.to(device)

            predict = model(x)
            test_loss += loss_fn(predict, y).item()
            correct += (predict.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= size
    correct /= size
    print(f'Test Error: \n Accuracy: {(100 * correct):>0.1f}%, Avg loss: {test_loss:>8f} \n')

8.4 训练和测试模型

训练和测试模型:

epochs = 15
for t in range(epochs):
    print(f'Epoch {t + 1}\n')
    train(training_data, model, loss_fn, optimizer)
    test(test_data, model, loss_fn)
print('Done!')

8.5 保存和加载模型

保存和加载模型:

torch.save(model.state_dict(), 'data/model.pth')
print('Saved.')

model = NeuralNetwork()
model.load_state_dict(torch.load('data/model.pth'))

torch.save('data/model.pth')
model = torch.load('data/model.pth')