本文作为初识pytorch的笔记。
## tensor
tensor汉译为张量,是数组的高维扩张(数组是一维的,矩阵是二维的,张量是高维的)。当然,高维也可以退化成为低维,只要不赋值即可。
初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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) print (f"Ones Tensor: \n {x_ones} \n" )x_rand = torch.rand_like(x_data, dtype=torch.float ) print (f"Random Tensor: \n {x_rand} \n" )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.shape
)、数据类型(tensor.dtype
)、存储设备(tensor.device
)
操作 tensor的操作很多,包括算数、矩阵、抽样等等。比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 tensor = torch.ones(4 , 4 ) print (f"First row: {tensor[0 ]} " )print (f"First column: {tensor[:, 0 ]} " )print (f"Last column: {tensor[..., -1 ]} " )tensor[:,1 ] = 0 print (tensor)y1 = tensor @ tensor.T y2 = tensor.matmul(tensor.T) y3 = torch.rand_like(y1) torch.matmul(tensor, tensor.T, out=y3) z1 = tensor * tensor z2 = tensor.mul(tensor) z3 = torch.rand_like(tensor) torch.mul(tensor, tensor, out=z3) print (f"{tensor} \n" )tensor.add_(5 ) print (tensor)
cpu上的tensor和nparray可以共享底层内存存储,更改其中一个将会更改另一个,有点类似于引用。实现两者的转化有
1 2 3 4 t = torch.ones(5 ) n = t.numpy() n = np.ones(5 ) t = torch.from_numpy(n)
dataset and dataLoader
处理数据样本的代码可能会变得杂乱无章,难以维护;我们希望我们的数据集代码与我们的模型训练代码分离,以提高可读性和模块化。 PyTorch 提供了两个数据基类: torch.utils.data.DataLoader
和 torch.utils.data.Dataset
。允许你使用预加载的数据集以及你自己的数据集。 Dataset
存储样本和它们相应的标签,DataLoader
在 Dataset
基础上添加了一个迭代器,迭代器可以迭代数据集,以便能够轻松地访问 Dataset
中的样本。
也就是说,在pytorch中,dataset用来存数据,dataloader用来操作dataset中的data。
PyTorch 领域库提供了一些预加载的数据集(如FashionMNIST),这些数据集是 torch.utils.data.Dataset
的子类,并实现特定数据的功能。它们可以被用来为你的模型制作原型和基准。
加载一个数据集 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import torchfrom torch.utils.data import Datasetfrom torchvision import datasetsfrom torchvision.transforms import ToTensorimport matplotlib.pyplot as plttraining_data = datasets.FashionMNIST( root="data" , train=True , download=True , transform=ToTensor() ) test_data = datasets.FashionMNIST( root="data" , train=False , download=True , transform=ToTensor() )
可视化数据集 Dataset
支持索引访问,我们可以使用matplotlib
来可视化训练数据中的样本,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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__
:从数据集中给定的索引处加载并返回一个样本。
一个样例是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import osimport pandas as pdfrom torchvision.io import read_imageclass 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
DataLoader
在训练一个模型时,我们通常希望以 “小批量” 的方式传递样本,在每个训练周期重新打乱数据以减少模型的过拟合,并使用 Python 的 multiprocessing
来加快数据的加载速度。
以上需求可以抽象成为一个可迭代对象,每次迭代的时候返回一定量的、随机抽取的样本。DataLoader
允许我们简单地获得这种可迭代对象:
1 2 3 4 from torch.utils.data import DataLoadertrain_dataloader=DataLoader(training_data,batch_size=64 ,shuffle=True ) test_dataloader=DataLoader(training_data,batch_size=64 ,shuffle=True )
遍历数据 使用iter()
函数可以实现对象迭代。用next可以对元组遍历。
1 2 3 4 5 6 7 8 9 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} " )
transforms
数据并不总是以训练机器学习算法所需的最终处理形式出现。我们使用变换来对数据进行一些处理,使其适合训练。 所有的 TorchVision 数据集都有两个参数: transform 用于修改特征和 target_transform 用于修改标签,它们接受包含转换逻辑的 callables。torchvision.transforms 模块提供了几个常用的转换算法,开箱即用。
这里的特征指的就是图片,不过在学习过程中,我们将输入称为特征,因为系统需要从图片中提取特征,进行学习。
比如FashionMNIST 的特征是 PIL 图像格式,而标签是整数。对于训练,我们需要将特征作为归一化的tensor,将标签作为独特编码的tensor。 为了进行这些转换,我们使用 ToTensor 和 Lambda。
1 2 3 4 5 6 7 8 9 10 11 import torchfrom torchvision import datasetsfrom torchvision.transforms import ToTensor, Lambdads = 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:ToTensor 将 PIL 图像或 NumPy 的 ndarray 转换为 FloatTensor。图像的像素强度值在 [0., 1.] 范围内缩放。
Lambda transforms:Lambda transforms 应用任何用户定义的 lambda 函数。在这里,我们定义了一个函数来把整数变成一个独热(one-hot)编码的tensor。 它首先创建一个大小为10(我们数据集中的标签数量)的零tensor,然后传递参数 value=1 在标签 y 所给的索引上调用 scatter_ 。 这里的标签形式为one-hot形式,因为数据集中就只有10种标签所以大小为10,然后转化即可。
创建神经网络
神经网络由在数据上进行操作的层/模块构成。torch.nn 命名空间提供了所有你用来构建你自己的神经网络所需的组件。PyTorch 中每个模块都是 nn.Module 的子类。一个由其他模块(层)组成的神经网络自身也是一个模块。这种嵌套的结构让构建和管理复杂的结构更轻松。
这里提到的组件,就是各种神经网络会使用到的基础类,比如卷积层(nn.Conv2d)、线性层(nn.Linear)、激活函数(nn.ReLU)、损失函数等。我们可以将多个模块组合起来形成一个完整的神经网络。
比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import torchimport torch.nn as nnlinear = nn.Linear(in_features=10 , out_features=5 ) class SimpleNet (nn.Module): def __init__ (self ): super (SimpleNet, self ).__init__() self .layer1 = nn.Linear(10 , 50 ) self .layer2 = nn.ReLU() self .layer3 = nn.Linear(50 , 5 ) def forward (self, x ): x = self .layer1(x) x = self .layer2(x) x = self .layer3(x) return x net = SimpleNet()
上述过程犹显麻烦,我们可以用一个例子来展示如何快速构建模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class NeuralNetwork (nn.Module): def __init__ (self ): super ().__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
模型参数 nn.Module
子类会自动追踪所有定义在模型对象中的字段,并通过parameters()
,named_parameters()
方法访问所有参数。
比如
1 2 3 4 print (f"Model structure: {model} \n\n" )for name, param in model.named_parameters(): print (f"Layer: {name} | Size: {param.size()} | Values : {param[:2 ]} \n" )
损失函数梯度计算 pytorch内置了一个微分运算引擎叫做torch.autograd
,支持对任何计算图自动计算梯度。
一个例子:
1 2 3 4 5 6 7 8 import torchx = 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_entropy_with_logits(z, y)
为了计算梯度,我们只需要调用
1 2 3 loss.backward() print (w.grad)print (b.grad)
这将会计算设置为true的叶子节点的grad,其他的grad是无法得到的。
优化模型参数 有了梯度,我们就可以开始训练模型了。
首先我们交代好要用的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import torchfrom torch import nnfrom torch.utils.data import DataLoaderfrom torchvision import datasetsfrom torchvision.transforms import ToTensortraining_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 ), ) def forward (self, x ): x = self .flatten(x) logits = self .linear_relu_stack(x) return logits model = NeuralNetwork()
控制训练的参数称为超参数,因为这部分内容不是机器能决定的,而是人决定的。
常见的超参数有
迭代数据集的次数
数据样本规模
学习率(更新模型参数的幅度)
我们设定超参数如下所示:
1 2 3 learning_rate=1e-3 batch_size=64 epochs=5
每一次迭代(epoch)由两个主要部分组成:
训练循环:用训练数据集进行训练,修改参数。
验证循环:测试模型效果是否提升。
上述循环都需要 损失函数 的参与。常见的损失函数包括给回归任务用的 nn.MSELoss(Mean Square Error, 均方误差)、给分类任务使用的 nn.NLLLoss(Negative Log Likelihood, 负对数似然)、nn.CrossEntropyLoss(交叉熵损失函数)。并结合了 nn.LogSoftmax 和 nn.NLLLoss.
除此以外,我们还需要在损失函数梯度计算完毕之后,进行优化,这个行为我们交给 优化器 来完成。优化方法有很多,专门交给一个对象来决定完成是不错的选择。最常见的优化算法是SGD,pytorch已经帮我们写好了(还包括一些其他的)。
调用优化器很简单,只需要
1 optimizer = torch.optim.SGD(model.parameters(),lr=learning_rate)
即可,然后用的时候,先获得梯度(用loss.backward()),然后再调用optimizer.step()走一步。最后再清除积累的梯度(optimizer.zero_grad())。
我们用下面的循环来进行训练:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 def train_loop (dataloader, model, loss_fn, optimizer ): size = len (dataloader.dataset) model.train() for batch, (X, y) in enumerate (dataloader): pred = model(X) loss = loss_fn(pred, y) loss.backward() optimizer.step() optimizer.zero_grad() if batch % 100 == 0 : loss, current = loss.item(), (batch + 1 ) * len (X) print (f"loss: {loss:>7f} [{current:>5d} /{size:>5d} ]" ) def test_loop (dataloader, model, loss_fn ): model.eval () size = len (dataloader.dataset) num_batches = len (dataloader) test_loss, correct = 0 , 0 with torch.no_grad(): for X, y in dataloader: 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.1 f} %, Avg loss: {test_loss:>8f} \n" )
保存/调用我们的模型 我们最终肯定是要留下我们训练成功的模型进行部署的。
pytorch 将模型学习到的参数存储在一个内部状态字典中,称为state_dict,可以通过torch.save来持久化。
需要用到的新模块是 torch.models
。
例如
1 2 3 4 model = models.vgg16(weights='IMAGENET1K_V1' ) torch.save(model.state_dict(), 'model_weights.pth' )
如果要加载模型权重,那么我们需要一个和需要加载权重的模型结构完全相同的模型,然后使用 load_state_dict()
方法加载参数,比如
1 2 3 model=models.vgg16() model.load_state_dict(torch.load('model_weights.pth' )) model.eval ()
当然,既然模型权重加载需要和模型绑定,那么最好将模型也存储到一个文件中,这样就可以实现很好的封装。pytorch支持这样的封装,也就是说,我们可以用
1 torch.save(model,'model.pth' )
的方式来保存模型,然后用
1 model=torch.load('model.pth' )
的方式来载入模型。
一个小小的总结: 深度学习的流程大致是这样:
分析问题类型,设计框架,收集训练数据和测试数据,训练、测试、评估(往往这是一个循环的过程),最后保存训练成果、部署。