课前的话:
大多数机器学习工作流程包括处理数据、创建模型、优化模型参数以及保存训练过的模型。
以FashionMNIST数据集为例来训练一个神经网络,它可以预测输入图像是否属于以下类别之一:t恤/上衣、Trouser、套头衫、连衣裙、外套、凉鞋、衬衫、Sneaker、Bag或踝靴。
训练可以在云端进行,也可以在本地进行
pytorch有初始数据集, torch.utils.data.DataLoader、torch.utils.data.Dataset。Dataset存放样本和他们相对应的标签,Dataloader在Dataset周围包装了一个可以迭代的对象。
导入的相关代码如下:
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.pyplot as plt
pytorch提供了特定领域的库,比如说TorchText,TorchVision和TorchAudio,他们都包含数据集。
torchvision.datasets模块包含Dataset对象,包含真实视觉的数据例如CIFAR,COCO等数据(详见官方文档)以其中一个FashionMNIST数据集进行。每一个数据集有两个对象,transform和target_transform来更好鉴别样本和标签。
我们来用dataset里面的FashionMNIST来进行实验:
# Download training data from open datasets.
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor(),
)
# Download test data from open datasets.
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor(),
)
但是官网下载会很慢,甚至报错,所以就需要自行下载数据集进行,很多博客都有这样的数据集,下载以后更改root里面的路径就好了
我们将Dataset作为参数传递给DataLoader。这在我们的数据集上包装了一个迭代器,并支持自动批处理、采样、打乱和多进程数据加载。这里我们定义了批处理大小为64,也就是说,dataloader iterable中的每个元素将返回一批64个特性和标签。
加载数据的代码块如下
batch_size = 64
# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)
for X, y in test_dataloader:
print("Shape of X [N, C, H, W]: ", X.shape)
print("Shape of y: ", y.shape, y.dtype)
break
为了建立一个神经网络模型, 我们建立了一个类里面包含了nn.Module。我们定义神经网络的每一个层数,并且在forward函数里面指定如何通过网络,如果GPU可以使用,我们就把它部署到GPU上面进行
代码块如下:
# Get cpu or gpu device for training.
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")
# Define model
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)
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
model = NeuralNetwork().to(device)
print(model)
为了更好训练网络,我们需要一个损失函数和一个优化值,代码块如下:
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
在单一的训练循环中,模型对训练数据集进行预测(分批反馈给它),并将预测误差反向传播以调整模型的参数。
训练数据的代码如下:
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
model.train()
for batch, (X, y) in enumerate(dataloader):
X, y = X.to(device), y.to(device)
# Compute prediction error
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
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)
num_batches = len(dataloader)
model.eval()
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
训练过程是在很多个迭代的过程里面进行的(epochs), 在每一次训练里面,明星学习参数来更好进行预测, 我们在每一次训练里面学习,并且打印出精确度和误差,我们可以通过代码来看一下每一个遍历情况下的精确度和误差:
epochs = 5
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train(train_dataloader, model, loss_fn, optimizer)
test(test_dataloader, model, loss_fn)
print("Done!")
保存模型的代码很简单,就这一个代码,然后保存到相应路径之下即可
torch.save(model.state_dict(), "model.pth")
print("Saved PyTorch Model State to model.pth")
我们已经训练好一个模型了,接下来就是去加载它,加载模型的过程包括重新创建模型结构和将状态字典加载到模型中。
代码如下:
model = NeuralNetwork()
model.load_state_dict(torch.load("model.pth"))
这个模型可以用来预测:
classes = [
"T-shirt/top",
"Trouser",
"Pullover",
"Dress",
"Coat",
"Sandal",
"Shirt",
"Sneaker",
"Bag",
"Ankle boot",
]
model.eval()
x, y = test_data[0][0], test_data[0][1]
with torch.no_grad():
pred = model(x)
predicted, actual = classes[pred[0].argmax(0)], classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')
张量是一种专门的数据结构,与数组和矩阵非常相似。在PyTorch中,我们使用张量来编码模型的输入和输出,以及模型的参数。
张量类似于numpy里面的ndarray,张量可以在GPU或其它硬件加速器上面运行,事实上,张量和NumPy数组通常可以共享相同的底层内存,从而消除了复制数据的需要(参见使用NumPy的桥接)。张量还针对自动微分进行了优化(我们将在稍后的Autograd部分中看到更多相关内容)。
首要操作代码:
import torch
import numpy as np
张量初始化有很多种方法,有很多样例:
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
新张量保留了参数张量的属性(形状、数据类型),除非显式重写。
x_ones = torch.ones_like(x_data) # retains the properties of x_data
print(f"Ones Tensor: \n {x_ones} \n")
x_rand = torch.rand_like(x_data, dtype=torch.float) # overrides the datatype of x_data
print(f"Random Tensor: \n {x_rand} \n")
Shape是张量维数的元组。在下面的函数中,它决定了输出张量的维数。
代码如下:
shape = (2,3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)
print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")
张量属性描述了它们的形状、数据类型和存储它们的设备。
代码如下:
tensor = torch.rand(3,4)
print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")
由上面运行结果可以知道,但是tensor的操作是在CPU上面的,很多张量的操作可以完成很多数学运算。超过100个张量操作,包括算术,线性代数,矩阵操作(转置,索引,切片),采样和更多的全面描述这里。
但是这些张量计算可以在GPU上面跑,但是tensor默认在CPU上面跑,我们需要把它移动到GPU上面跑,方法见以下代码,规模大的tensor运算成本高昂,时间成本和空间成本,所以说需要在GPU上面跑,操作如下:
如果你的电脑上面有GPU,可以如下操作:
# We move our tensor to the GPU if available
if torch.cuda.is_available():
tensor = tensor.to('cuda')
pytorch可以进行很多tensor运算,就像numpy一样,记住numpy的用法就和tensor差不多了。
操作方法见下:
tensor = torch.ones(4, 4)
print('First row: ', tensor[0])
print('First column: ', tensor[:, 0])
print('Last column:', tensor[..., -1])
tensor[:,1] = 0
print(tensor)
张量的连接,用torch.cat方法连接张量,将沿给定维数的张量序列串联起来。另一个操作是torch.stack方法,和torch.cat连接方法不一样。
见下:
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)
# This computes the matrix multiplication between two tensors. y1, y2, y3 will have the same value
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)
y3 = torch.rand_like(tensor)
torch.matmul(tensor, tensor.T, out=y3)
# This computes the element-wise product. z1, z2, z3 will have the same value
z1 = tensor * tensor
z2 = tensor.mul(tensor)
z3 = torch.rand_like(tensor)
torch.mul(tensor, tensor, out=z3)
如果你有一个单元素张量,例如通过将一个张量的所有值聚合为一个值,你可以使用以下方法将它转换为Python数值:item()
agg = tensor.sum()
agg_item = agg.item()
print(agg_item, type(agg_item))
就地操作,当打就是把结果存储到操作数中的操作,他们结构就像这样:加了下划线_,x.copy_(y),
x.t_(),可以改变x的值
print(tensor, "\n")
tensor.add_(5)
print(tensor)
但是用的不是很频繁,因为就地操作节省了一些内存,但是在计算导数时可能会出现问题,因为会立即丢失历史记录。
tensor在CPU上可以和numpy数组共享存储位置,并且改变一个就会改变另一个。
tensor到numpy的改变:
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")
tensor改变了值,numpy数组值也改变了
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")
numpy数组转tensor:
n = np.ones(5)
t = torch.from_numpy(n)
numpy数组值改变,tensor的值也会改变
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")
处理数据样本的代码可能会变得混乱和难以维护;理想情况下,我们希望我们的数据集代码与我们的模型训练代码解耦,以获得更好的可读性和模块化。
pytorch提供了两个方法进行(在开篇讲过),这样的数据集允许你预加载数据和你自己的数据,Dataset存放样本和标签,Dataloader在Dataset周围包装了一个可以迭代的对象,意键更方便接触这些样本。
pytorch下有这些样本,他们有图片,文本和音频,可以帮我们训练样本来进行我们的研究。
我们看第一个样例,从TorchVision加载Fashion-MNIST数据集的示例。Fashion-MNIST是Zalando文章图片的数据集,包含60,000个训练示例和10,000个测试示例。每个示例包含一个28×28灰度图像和来自10个类之一的相关标签。
下面标签作用是:
root是数据集的路径,train的区分数据集还是验证集,是的话为true,否则为false,download:下载来自路径的数据(如果数据不在路径的话),transform:专门进行特征和标签转换。
标签和可视化:
可以进行index方法在tensor里面。
代码如下:
labels_map = {
0: "T-Shirt",
1: "Trouser",
2: "Pullover",
3: "Dress",
4: "Coat",
5: "Sandal",
6: "Shirt",
7: "Sneaker",
8: "Bag",
9: "Ankle Boot",
}
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
for i in range(1, cols * rows + 1):
sample_idx = torch.randint(len(training_data), size=(1,)).item()
img, label = training_data[sample_idx]
figure.add_subplot(rows, cols, i)
plt.title(labels_map[label])
plt.axis("off")
plt.imshow(img.squeeze(), cmap="gray")
plt.show()
数据集里面必须包含下面三个函数:__init,____len,和getitem方法,数据集存放于img_dir,其他标签数据集存放在分开的文件夹annotations_file,样例代码如下:
import os
import pandas as pd
from torchvision.io import read_image
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 = 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)
return image, label
__init__函数在实例化Dataset对象时运行一次。我们初始化包含图像、注释文件和这两种转换的目录(下一节将详细介绍)。
__len__函数返回数据集中样本的数量。
__getitem__函数从给定索引idx的数据集中加载并返回一个样本。根据索引确定图像在磁盘上的位置,使用read_image将其转换为一个张量,并从self中的csv数据中检索相应的标签。调用它们的变换函数(如果适用),并以元组的形式返回张量图像和相应的标签。
Dataset检索我们的数据集的特性,并每次标记一个样本。在训练模型时,我们通常希望以“小批量”方式传递样本,在每个epoch重新编排数据以减少模型的过拟合,并使用Python的多处理来加速数据检索。
DataLoader是一个可迭代对象,它在一个简单的API中为我们抽象了这种复杂性。
代码如下:
from torch.utils.data import DataLoader
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
我们已经将该数据集加载到DataLoader中,并可以根据需要迭代该数据集。下面的每次迭代返回一批train_features和train_labels(分别包含batch_size=64个feature和标签)。因为我们指定了shuffle=True,所以在遍历所有批之后,数据会被打乱
# Display image and label.
train_features, train_labels = next(iter(train_dataloader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap="gray")
plt.show()
print(f"Label: {label}")
数据并不总是以训练机器学习算法所需的最终处理形式出现。我们使用转换来对数据进行一些操作,使其适合于训练。
所有TorchVision数据集都有两个参数——transform用于修改特性,target_transform用于修改标签——它们接受包含转换逻辑的可调用对象。torchvision。Transforms模块提供了几种常用的开箱即用转换。
FashionMNIST特性采用PIL Image格式,标签为整数。在训练中,我们需要将特征作为归一化张量,而将标签作为单热点编码张量。为了进行这些变换,我们使用了ToTensor和Lambda。
方法见下面:
import torch
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))
)
ToTensor将PIL图像或NumPy ndarray转换为FloatTensor。在[0]范围内缩放图像的像素强度值。
Lambda变换应用任何用户定义的Lambda函数。在这里,我们定义了一个函数,将整数转换为一个单热的编码张量。
它首先创建一个大小为10(数据集中标签的数量)的零张量,并调用scatter_,它在标签y给出的索引上赋值=1。
target_transform = Lambda(lambda y: torch.zeros(
10, dtype=torch.float).scatter_(dim=0, index=torch.tensor(y), value=1))
神经网络由对数据进行操作的层/模块组成。torch.nn命名空间提供了构建自己的神经网络所需的所有构建块。PyTorch中的每个模块都是nn.Module的子类。神经网络本身就是由其他模块(层)组成的模块。这种嵌套结构允许轻松地构建和管理复杂的体系结构。
建立方法如下:
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
我们都希望用GPU来加速训练,所以我们就使用以下代码来教案是否有GPU:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Using {device} device')
由于神经网络在类nn.Module之下,我们通过子类化nn来定义我们的神经网络。模块,并在__init__中初始化神经网络层。每一个神经网络。模块子类实现了forward方法中对输入数据的操作。
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),
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
然后定义模型,跑到GPU底下运行
model = NeuralNetwork().to(device)
print(model)
要使用模型,我们将输入数据传递给它。这将执行模型的正向操作,以及一些后台操作。不要直接调用model.forward()
在输入上调用模型将返回一个10维张量,其中包含每个类的原始预测值。我们通过将它传递给神经网络的一个实例来得到预测概率。用Softmax模块。
X = torch.rand(1, 28, 28, device=device)
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")
input_image = torch.rand(3,28,28)
print(input_image.size())
我们初始化nn.Flatten层,将每个2D 28x28图像转换为一个784像素值的连续数组(保持小批量维度(在dim=0))。
flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size())
线性层是一个模块,它使用其存储的权重和偏差对输入进行线性变换。
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())
非线性激活是在模型的输入和输出之间创建复杂映射的原因。它们在线性变换后被用于引入非线性,帮助神经网络学习各种各样的现象。
在这个模型中,我们使用nn.ReLU在我们的线性层之间,但还有其他激活可以在您的模型中引入非线性。
print(f"Before ReLU: {hidden1}\n\n")
hidden1 = nn.ReLU()(hidden1)
print(f"After ReLU: {hidden1}")
nn.Sequential是模块的有序容器。数据按照定义的相同顺序通过所有模块。您可以使用顺序容器来构建一个像seq_modules这样的快速网络。
seq_modules = nn.Sequential(
flatten,
layer1,
nn.ReLU(),
nn.Linear(20, 10)
)
input_image = torch.rand(3,28,28)
logits = seq_modules(input_image)
神经网络的最后一个线性层返回logits ([-infty, infty]中的原始值),它被传递给nn.Softmax模块。对数被缩放到代表模型对每个类别的预测概率的值[0,1]。Dim参数指示值之和为1的维度。
softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)
神经网络中的许多层都是参数化的,即在训练过程中优化了相关的权重和偏差。子类化神经网络。模块自动跟踪模型对象中定义的所有字段,并使用模型的parameters()或named_parameters()方法访问所有参数。
print("Model structure: ", model, "\n\n")
for name, param in model.named_parameters():
print(f"Layer: {name} | Size: {param.size()} | Values : {param[:2]} \n")
现在我们已经有了一个模型和数据,现在是时候通过优化数据上的参数来训练、验证和测试我们的模型了。训练模型是一个迭代的过程;在每次迭代(称为epoch)中,模型对输出进行猜测,计算其猜测中的误差(损失),收集误差对其参数的导数(如我们在前一节中看到的),并使用梯度下降优化这些参数。(可以看吴恩达视频加以理解)
代码前面就有过了
对以下东西有一个定义:
超参数是可调节的参数,让您控制模型优化过程。不同的超参数值会影响模型的训练和收敛速度
我们定义了以下训练超参数:
epochs——遍历数据集的次数
batch_size-参数更新之前通过网络传播的数据样本的数量
learning_rate-在每批/历期更新多少模型参数。值越小,学习速度越慢,而值越大,训练过程中行为不可预测。
给个样例:
learning_rate = 1e-3
batch_size = 64
epochs = 5
一旦我们设置了超参数,我们就可以用一个优化循环来训练和优化我们的模型。优化循环的每次迭代被称为一个epoch。
每个epoch由两个主要部分组成:
Train Loop—迭代训练数据集并尝试收敛到最优参数。
The Validation/Test Loop——遍历测试数据集,检查模型性能是否提高。
常见的损失函数包括nn.MSELoss(均方误差)用于回归任务,nn.NLLLoss(负对数似然值)用于分类。 nn.CrossEntropyLoss 包含 nn.LogSoftmax 和 nn.NLLLoss.
# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()
优化是在每个训练步骤中调整模型参数以减少模型误差的过程。优化算法定义了如何执行这个过程(在本例中,我们使用随机梯度下降)。所有优化逻辑都封装在优化器对象中。方法吴恩达讲过
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
应用看前面
前面就介绍过了,这里就不赘述了