快速上手pytorch,以及需要懂的一点点OOP知识
以老py的视角看pytorch,产出一系列对py新手有提升的批注类教程,后续会与jittor进行对比,使得pytorch玩家可以轻松迁移到jittor。
https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html
快速上手pytorch,面向python新手,前提需要知道类是什么,但是可以对OOP不熟悉
数据相关
pytorch通过了两个基类用来处理数据,torch.utils.data.DataLoader
和 torch.utils.data.Dataset
。
torch.utils.data.Dataset
是数据集的基类,用于实现数据集。
OOP:
python中的类是多继承的,用户可以继承基类,重写基类方法实现自己的类。
可以通过继承torch.utils.data.Dataset
实现自定义数据集。
自定义数据集,需要实现 __init__
, __len__
, __getitem__
三个函数。
FashionMNIST数据集
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])
# iloc是pandas中对元素索引的方法,第一个参数是行,第二个参数是列,返回pandas.DataFrame对象
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
OOP:
python类中,
__init__
是入口函数,类实例化的时候,会自动执行__init__
函数,一般在此处初始化类内的各种变量。定义
__len__
方法可以实现 len(对象) 来返回对象的’长度’,此例中返回了图像标签的长度。
__getitem__
方法可以实现直接对类进行索引访问,形如对象[索引]
。本例中实现了通过索引访问数据集中的图片和对应标签。
pytorch也提供了很多数据集库
TorchText, TorchVision, TorchAudio
from torchvision import datasets
# 下载公开的数据集
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor(),
)
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor(),
)
root:下载后保存文件的目录
train:是否是训练集
download:为True时,如果本地没有数据集,会自动从网络下载数据集
transform:要对数据进行操作的转换方法,参照上面__getitem__
中的实现
DataLoader
是数据集访问类(迭代器形式)
定义了很多有用的方法,比如以批量访问数据,采样,随机打乱,多进程数据加载。(supports automatic batching, sampling, shuffling and multiprocess data loading.)
batch_size = 64
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
Dataloader初始化参数:
dataset对象,batch_size(int),shuffer(bool),num_works(多进程加载数据,子进程数),采样器(实现更细粒度的数据控制)等等
OOP
python内置两个魔术方法
__iter__
,__next__
,类中实现这两个方法,可以使得这个类变成迭代器。iter(对象)会返回这个对象的迭代器,next(对象)可以依次取出迭代器中的元素
# 官方示例
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}")
模型相关
创建模型
pytorch提供nn.Module
类用来实现网络模型,一般在__init__
函数中定义网络结构,在forward
函数(前向传播函数)中实现数据的流过模型的形式。
python tricks:
Python 3 中 … 三个点的省略号的作用 - 知乎 (zhihu.com)
OOP:
python类中有一个方法
__call__
,可以使得像访问函数一样对象()
来调用__call__
中的操作。Module类的__call__
方法调用了forward函数,所以执行对象(x)
会自动执行forword函数进行计算。
例子
class Network(nn.Module):
def __init__(self):
# 两个卷积层
# 两个线性层
super().__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
self.fc1 = nn.Linear(in_features=12 * 4 * 4, out_features=120)
self.fc2 = nn.Linear(in_features=120, out_features=60)
self.out = nn.Linear(in_features=60, out_features=10)
def forward(self, t):
# (1) input layer
# 默认是要有一个输入层的
t = t
# (2) hidden conv layer
t = self.conv1(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)
# (3) hidden conv layer
t = self.conv2(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)
# (4) hidden linear layer
t = t.reshape(-1, 12 * 4 * 4)
t = self.fc1(t)
t = F.relu(t)
# (5) hidden linear layer
t = self.fc2(t)
t = F.relu(t)
# (6) output layer
t = self.out(t)
#t = F.softmax(t, dim=1)
return t
模型类中,__init__
一般会初始化网络的各个层。
forward
函数用于实现数据在各个层中如何计算
或者直接使用Sequential
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
Sequential是一个存放网络层的有序容器,可以直接用这种方法定义网络结构。
forward中就对应进行计算即可
优化模型 参数
损失函数(loss)和优化器(optimizer)
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()
# 每走100次批量输出一下损失
if batch % 100 == 0:
loss, current = loss.item(), batch * len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
训练的本质是,输入数据(或者batch),模型进行预测,预测值和预期值进行损失计算,反向传播。
optimizer.zero_grad()
清空以往的计算的梯度
loss.backward()
损失反向传播,计算梯度
optimizer.step()
根据梯度更新网络参数
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")
在模型中,我们通常会加上Dropout层和batch normalization层,在模型预测阶段,我们需要将这些层设置到预测模式,model.eval()就是帮我们一键搞定的,如果在预测的时候忘记使用model.eval(),会导致不一致的预测结果。
Dropout层是随即丢弃神经元,正则化方法
barch normalization 是批量计算时,按批量进行方差均值计算,预测时候是不需要的
一般时候,pytorch是默认会track数据变化,进行梯度计算。预测的时候不进行梯度更新,不需要track张量。
ps: 装饰器 @torch.no_grad()
训练网络
# 设置要训练的轮次
epochs = 5
for t in range(eopchs):
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"))