本文介绍使用 PyTorch 搭建神经网络模型的方法,由于涉及的内容众多,本文将以完成一个神经网络实验的顺序 展开介绍。分别是:数据加载、模型搭建、训练阶段、评估阶段、模型保存与加载 。
本文内容基于 官方 API 文档 及 PyTorch 中文文档 、网上的博客等,实战部分推荐以下教程:
数据加载 PyTorch 中的 torch.utils.data
模块封装了所有关于数据集的操作,可以帮助我们使用一些预加载数据集或者自定义的数据,其中 Dataset
可以储存样本以及对应标签,而 Dataloader
则用于将 Dataset
封装成可迭代对象 ,使得我们能够更加容易地获取样本数据。
预加载 or 自定义 PyTorch 中提供了大量预加载 的数据集(如 MNIST、FashionMNIST),存放在不同的领域库中,分别是:torchvision 、torchtext 、torchaudio 库。
下面以 FashionMNIST 为例介绍加载流程 ,其包含 60000 个训练样本以及 10000 个测试样本,每个样本由一张灰度图、一个类别标签组成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import torchfrom torch.utils.data import Datasetfrom torchvision import datasetsfrom torchvision.transforms import ToTensor training_data = datasets.FashionMNIST( root="data" , train=True , download=True , transform=ToTensor() ) test_data = datasets.FashionMNIST( root="data" , train=False , download=True , transform=ToTensor() )
自定义数据集需要通过面向对象 操作来完成,案例如下:
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 import osimport pandas as pdfrom torchvision.io import read_imagefrom torch.utils.data import Dataset, DataLoaderclass MyDataset (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
数据集迭代访问 获取的数据会以列表的形式存储,特征和标签构成一对元组 。如果这样通过遍历列表的形式来访问数据集,则效率太低,我们希望以小批次 (Mini-batch)为单位读取样本,并且在每一个 epoch 中打乱数据的顺序 以防止模型过拟合,同时利用 Python 的并行处理 (Multi-Processing)功能加速数据处理。
使用 DataLoader
模块就可以实现上述功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from torch.utils.data import DataLoader train_dataloader = DataLoader(training_data, batch_size=64 , shuffle=True , num_workers=4 ) test_dataloader = DataLoader(test_data, batch_size=64 , shuffle=True , num_workers=4 ) train_features, train_labels = next (iter (train_dataloader)) num_epoches = 100 for epoch in range (num_epoches): for img, label in train_dataloader:
其中 num_workers
参数代表在声明时一次性创建的工作进程数量,此时 batch_sampler
会将指定 batch 分配给每个 worker,worker 负责将 batch 载入显存 。当 DataLoader
在某轮迭代需要用到该 batch 时,可以直接使用 。分配的进程数量越多,获取下一 batch 的速度越快,因为它可能早就放在显存中了,但这也加重了 CPU 负担。一般将该值设置为 CPU 核心数量 ,根据性能变化逐步调整。
除了以上四个常用参数,有时还会用到一个 collate_fn
参数,顾名思义,该函数用于对样本进行核对、校勘 。在 Dataset
类中调用 __getitem__
后,每次返回的都是一个样本,最后构成一个 Batch 的样本。如果再这个过程中出现出现什么样本错误 (如存在空值、错误值),或者需要对批次进行特殊处理 (如对齐整个 Batch 样本的长度),则需要在该函数中完成。
collate_fn
默认是等于 default_collate
,这是 PyTorch 自带的处理函数,也可以自己定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def collate_fn (batch ): batch = list (filter (lambda x: x[0 ] is not None , batch)) if len (batch) == 0 : return torch.Tensor() return default_collate(batch)def collate_fn (batch ): batch.sort(key=lambda x: len (x[1 ]), reverse=True ) img, label = zip (*batch) pad_label = [] lens = [] max_len = len (label[0 ]) for i in range (len (label)): temp_label = [0 ] * max_len temp_label[:len (label[i])] = label[i] pad_label.append(temp_label) lens.append(len (label[i])) return img, pad_label, lens
数据预处理 前文中已经出现过 torchvision.transforms
模块,该模块的功能是对图像进行预处理,内置各种数据增强 操作,包括随机裁剪、翻转和旋转、变形和填充、修改属性等。每一个单独的变换都是一个函数 ,输入数据即可得到结果。
1 2 preprocess = transforms.Resize([256 , 256 ]) img_transformed = preprocess(img)
如果要连续进行多种操作,则可以用 transforms.Compose()
组合,参数为操作列表。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import torchvision.transforms as transforms myTransforms = transforms.Compose([ transforms.RandomResizedCrop(224 ), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize([0.485 , 0.456 , 0.406 ], [0.229 , 0.224 , 0.225 ]) ]) data = MyDataset( annotations_file="annotations.csv" , img_dir="data/img" , transform=myTransforms )
该案例中归一化处理传入的两个参数分别代表均值和标准差,这里取的是 ImageNet 数据集的值,这是 CV 领域一种常见的做法,通常只要数据集是自然场景的普通照片 就能使用。如果是特殊场景、特殊风格的数据,则需要重新计算。
模型搭建 网络的定义也是通过面向对象 操作来完成,torch.nn
模块封装了神经网络的大部分操作,案例如下:
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 import torchimport torch.nn as nnimport torch.nn.functional as Fclass MyNet (nn.Module ): def __init__ (self, num_classes=10 ): super ().__init__() self.conv = nn.Conv2d(3 , 6 , 3 ) self.fc = nn.Linear(6 * 5 * 5 , num_classes) def forward (self, x ): x = F.max_pool2d(F.relu(self.conv(x)), 2 ) x = torch.flatten(x, 1 ) x = F.relu(self.fc(x)) return x net = MyNet()input = torch.randn(1 , 3 , 12 , 12 ) out = net(input )
如果这时候 print(net)
,则会打印出类的所有属性,即每个构造函数中的网络部件:
1 2 3 4 MyNet( (conv): Conv2d(3 , 6 , kernel_size=(3 , 3 ), stride=(1 , 1 )) (fc): Linear(in_features=150 , out_features=10 , bias=True ) )
由于继承了父类的方法,调用 net.paprameters()
可以得到参数列表 ,各个层的参数依次排列:
1 2 3 4 5 6 7 8 9 10 11 >>> params = list (net.parameters())>>> print (len (params)) >>> print (params[0 ].size()) >>> print ([(name, param.shape) for name, param in net.named_parameters()]) >>> print (sum (p.numel() for p in net.parameters() if p.requires_grad))
预加载的网络 PyTorch 中自带了搭建好的经典模型,可以直接调用,包括 AlexNet 、VGG 等,详见 官方文档 。可以选择是否使用预训练好的权重、原有的转换 。下面的例子展示了使用预加载网络推理一个样本的过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from torchvision.io import read_imagefrom torchvision.models import resnet50, ResNet50_Weights img = read_iamge("test.jpg" ) weights = ResNet50_Weights.DEFAULT model = resnet50(weights=weights) model.eval () preprocess = weights.transforms() batch = preprocess(img).unsqueeze(0 ) prediction = model(batch).squeeze(0 ).softmax(0 ) class_id = prediction.argmax().item() score = prediction[class_id].item() category_name = weights.meta["categories" ][class_id]print (f"{category_name} : {100 * score:.1 f} %" )
此外,我们还可以对已经定义好的模型进行添加、修改 网络层:
1 2 3 4 5 6 vgg16 = torchvision.models.vgg16(pretrained=True ) vgg16.classifier.add_module("add_linear" , nn.Linear(1000 ,10 )) vgg16.classifier[6 ] = nn.Linear(4096 , 10 )
nn 网络模块 这里介绍一下在构建网络时常见的一些网络模块,即在 __init__
中定义的部分,当然也可以在 nn.Sequential()
中组合,稍后会介绍。
1 2 3 4 5 6 7 ''' in_features: 输入特征张量的大小,可以理解为该层神经元个数,即 (batch_size, in_features) out_features: 输出特征张量的大小,可以理解为下一层神经元个数 bias: 是否启用偏置,除了 weight 数组,还会初始化一个 bias 数组 ''' nn.Flatten() nn.Linear(in_features, out_features, bias=True )
常用的卷积层 ,最终输出的张量维度为 $( , ) $:1 2 3 4 5 6 7 8 9 10 ''' in_channels/out_channels: 输入/输出通道数,四维张量 [N, C, H, W] 中的 C kernel_size: 卷积核大小,可输入一个值或元组 stride: 步幅,默认为 1,可输入一个值或元组 padding: 填充,默认不填充,可输入一个值或元组 padding_mode: 可选 zeros(零) reflect(镜像) replicate(复制) circular(循环) dilation: 是否采用空洞卷积,默认为 1 不采用 卷积核参数量: [out_channels, in_channels, kernel_height, kernel_width] ''' nn.Conv2d(in_channels, out_channels, kernel_size, stride=1 , padding=0 , dilation=1 , bias=True , padding_mode='zeros' )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ''' kernel_size: 池化窗口大小,可输入一个值或元组 stride: 默认为窗口的大小,即每个窗口不重叠,与卷积不同 return_indices: 是否返回最大值位置索引 ceil_mode: 输出的形状取整方向,默认为 False 向下取整 ''' nn.MaxPool2d(kernel_size, stride=None , padding=0 , dilation=1 , return_indices=False , ceil_mode=False )''' count_include_pad: 计算平均值时是否包括零填充,默认包括 divisor_override: 除数大小,默认就是池化窗口大小,但可以自己指定 ''' nn.AvgPool2d(kernel_size, stride=None , padding=0 , ceil_mode=False , count_include_pad=True , divisor_override=None )''' output_size: 固定输出尺寸,即可自适应窗口大小,可输入一个值或元组 ''' nn.MaxPool2d(output_size) nn.AdaptiveAvgPool2d(output_size)
常用激活函数 :也作为层来定义,但都没有参数,默认 inplace=Flase
。1 2 3 4 5 6 7 8 nn.Sigmoid() nn.Tanh() nn.ReLU() nn.LeakyReLU(negative_slope=0.01 ) nn.PReLU(num_parameters=1 ) nn.ELU(alpha=1.0 ) nn.Softplus(beta=1 , threshold=20 ) nn.Softmax(dim=None )
特殊功能层 :注意 \(\textrm{Dropout}\) 只在训练阶段 启用,并且不带参数。而 \(\textrm{BN}\) 层实际是有参数存储 的,包括仿射参数和测试阶段 的移动平均。同样 \(\textrm{LN}\) 层也是有参数存储 的,即仿射参数,但不会跟踪全局的移动平均,所有阶段 都一致。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 ''' p: 张量元素被置为 0 的概率,置为 0 的神经元整个不激活 inplace: 是否进行原地操作,默认为否 ''' nn.Dropout(p=0.5 , inplace=False ) nn.Dropout2d(p=0.5 , inplace=False )''' num_features: 通道数,1d 中代表 [N, L] 中的 L, 2d 中代表 [N, C, H, W] 中的 C eps: 归一化时加到分母(方差)上的小量,防止除零溢出 momentum: 预测模型下进行移动平均的动量 affine: 是否具有可学习的仿射参数 gamma 和 beta,用于拉伸和偏移分布 track_running_stats: 是否跟踪均值和方差,进行移动平均 ''' nn.BatchNorm1d(num_features, eps=1e-05 , momentum=0.1 , affine=True , track_running_stats=True ) nn.BatchNorm2d(num_features, eps=1e-05 , momentum=0.1 , affine=True , track_running_stats=True )''' normalized_shape: 传入一个整数表示最后一维的长度,传入列表表示最后两维的长度 eps: 归一化时加到分母(方差)上的小量,防止除零溢出 elementwise_affine: 是否具有可学习的仿射参数 gamma 和 beta,用于拉伸和偏移分布 ''' nn.LayerNorm(normalized_shape, eps=1e-05 , elementwise_affine=True )
词嵌入层 :在 NLP 任务中经常使用,用来代替 One-Hot 实现降维,并学习词之间的联系。1 2 3 4 5 6 7 8 9 10 11 12 13 14 ''' num_embeddings: 词典的大小,如果有 5000 词,id 就是 0-4999 embedding_dim: 嵌入向量的维度,即用多少维的空间来表示一个词向量 padding_idx: 输入句子长度不一致时,末尾的 PAD 的索引值 ''' nn.Embedding(num_embeddings, embedding_dim, padding_idx=None ) embed = torch.nn.Embedding(num_embeddings, embedding_dim) embed_batch = embed(batch)
如果想观察网络中每层的通道数、数据规模是否符合预期,可以构造一个单独的样本 来前向传播,并实时获取其形状:
1 2 3 4 X = torch.randn(1 , 3 , 224 , 224 )for layer in net: X=layer(X) print (layer.__class__.__name__, '\t\ output shape:\t' , X.shape)
需要注意的是,如果要设计全新的网络模块,除了实现一个运算函数 ,还需要定义可训练的参数 。nn.Parameter()
将一个不可训练 的 Tensor 转换成可训练 的类型 Parameter,并将这个 Parameter 绑定 到模型的参数列表里面。比起参数 requires_grad=True
还多了绑定的过程:
1 2 3 4 5 6 7 8 class Conv2D (nn.Module ): def __init__ (self, kernel_size ): super ().__init__() self.weight = nn.Parameter(torch.rand(kernel_size)) self.bias = nn.Parameter(torch.zeros(1 )) def forward (self, x ): return corr2d(x, self.weight) + self.bias
F 函数式编程 在 PyTorch 的 nn
模块中,我们不需要手动定义网络层的权重和偏置,这就是 PyTorch 比 Tensorflow 简洁的地方。当然,PyTorch 也提供了 nn.Functional
函数式编程的方法,其中的 F.conv2d()
就和 Tensorflow 一样,要预先定义好 卷积核的权重和偏置 nn.Parameter()
,作为形参之一输入。
当然,为了不多此一举,网络中具有可学习参数 的层(如全连接层、卷积层)通常会放在 __init__
中显式定义 ,而不具有参数 的层(如 ReLU、Dropout、BN 层)一般放在 forward
中使用 F
函数调用。
1 2 F.max_pool2d(input , kernel_size, stride=None , padding=0 , dilation=1 , ceil_mode=False , return_indices=False ) F.avg_pool2d(input , kernel_size, stride=None , padding=0 , ceil_mode=False , count_include_pad=True , divisor_override=None )
无参数的激活函数 :以下函数都支持 F.relu_
的原地版本。1 2 3 4 5 6 7 8 F.sigmoid(input ) F.tanh(input ) F.relu(input ) F.leaky_relu(input , negative_slope=0.01 ) F.prelu(input , weight) F.elu(input , alpha=1.0 ) F.softplus(input , beta=1 , threshold=20 ) F.softmax(input )
无参数的特殊功能函数 :包括 Dropout 和一些计算向量距离的函数。1 2 3 4 5 F.dropout(input , p=0.5 , inplace=False ) F.dropout2d(input , p=0.5 , inplace=False ) F.cosine_similarity(x1, x2) F.pairwise_distance(x1, x2, p=2.0 ) F.one_hot(x, num_classes)
注意在 F 函数式编程中,函数名全都为小写 ,与 nn 网络模块中不同!
使用块搭建深层网络 块(Block)比单个层(Layer)大,又比整个模型(Model)小,是实现一个深层网络的必备操作,通常用于封装以重复模式排列的若干层,最早在 \(\textrm{VGG}\) 中得到应用。
从编程角度来看,每个块也需要用类定义,并实现初始化、前向传播 功能。实际中 PyTorch 提供了一个简单的方法 nn.Sequential
,可以将不同神经网络层进行顺序拼接 :
1 2 3 4 5 6 self.net = nn.Sequential( nn.Linear(20 , 256 ), nn.ReLU(), nn.Linear(256 , 10 ) ) output = net(torch.rand(2 , 20 ))
如果希望多个层共享参数 ,只需先实例化一个层,并将其作为网络的多个部分,此时所有层的权重张量共享一个内存空间 。这就相当于 __init__
中的同一层在 forward
中被调用了两次,因此每次调用的 grad 也会相加 。如下:
1 2 3 4 5 sharedLayer = nn.Linear(8 , 8 ) net = nn.Sequential(nn.Linear(4 , 8 ), nn.ReLU(), sharedLayer, nn.ReLU(), sharedLayer, nn.ReLU(), nn.Linear(8 , 1 ))
如果要封装更复杂的块,例如 VGG 中的 VGG_Block
,则可以将其单独用类定义,并设置好参数:
1 2 3 4 5 6 7 8 def VGG_Block (num_convs, in_channels, out_channels ): layers = [] for _ in range (num_convs): layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3 , padding=1 )) layers.append(nn.ReLU()) in_channels = out_channels layers.append(nn.MaxPool2d(kernel_size=2 ,stride=2 )) return nn.Sequential(*layers)
网络初始化 默认情况下,PyTorch 会根据一个范围均匀地初始化 权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。其中nn.init
模块提供了多种预置初始化方法。包括:
1 2 3 4 5 6 7 8 9 10 11 12 nn.init.zeros_(weight) nn.init.ones_(weight) nn.init.eye_(weight) nn.init.orthogonal_(weight) nn.init.constant_(weight, val) nn.init.uniform_(weight, a=0 , b=1 ) nn.init.normal_(weight, mean=0 , std=1 ) nn.init.xavier_uniform_(weight) nn.init.xavier_normal_(weight) nn.init.kaiming_uniform_(weight, a=0 , mode='fan_in' , nonlinearity='leaky_relu' ) nn.init.kaiming_normal_(weight, a=0 , mode='fan_in' , nonlinearity='leaky_relu' )
调用上述内置的初始化器,我们可以实现:
1 2 3 4 5 def init_weights (m ): if type (m) == nn.Linear: nn.init.normal_(m.weight, mean=0 , std=0.01 ) nn.init.zeros_(m.bias) net.apply(init_weights)
注意到上面的方法要求网络类型完全匹配 ,而 PyTorch 中自带有很多相似的层,如 Conv2d
、ConvTranspose2d
等,此时可以获取类名 进行字符串匹配:
1 2 3 4 5 6 7 8 9 def init_weights (m ): classname = m.__class__.__name__ if classname.find('Conv' ) != -1 : nn.init.kaiming_uniform_(m.weight) nn.init.uniform_(weight, a=-1 , b=1 ) elif classname.find('BatchNorm' ) != -1 : nn.init.normal_(m.weight, 1.0 , 0.02 ) nn.init.zeros_(m.bias) net.apply(init_weights)
训练阶段 假设我们已经定义好 MyDataset
类和 MyModel
类,现在正式进入训练阶段。除了实例化数据集和模型,还需要额外定义损失函数和优化器 。下面给出一个完整的训练过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 dataset = MyDataset(file) train_set = DataLoader(dataset, 16 , shuffle=True ) device = torch.device("cuda" if torch.cuda.is_available() else "cpu" ) model = MyModel().to(device) criterion = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), 0.1 ) model.train() for epoch in range (num_epochs): for X, y in train_set: optimizer.zero_grad() X, y = X.to(device), y.to(device) pred = model(X) loss = criterion(pred, y) loss.backward() optimizer.step()
所谓的「训练模式 」,是相对另一个「评估模式 」下的模型传播路径。实现上,模型通过 model.train()
将变量 self.training
置为 True,通过 model.eval()
将 self.training
置为 False,所以即便训练与测试共用一个模型,也能通过该变量来区分现在属于训练还是测试 。
当模型中使用了 Dropout、BatchNorm 等模块,在训练阶段和测试阶段的操作不同 ,此时区分模式就很有必要。此外,如果网络中有一部分层只在训练阶段启用,则可以用 if self.training:
来执行。
损失函数 PyTorch 在 torch.nn
模块中已经定义了大部分常用的 Loss,具体的计算过程详见 手册 :
nn.L1Loss()
绝对值误差损失 回归 nn.MSELoss()
均方误差损失 回归 nn.SmoothL1Loss()
Huber 损失(结合 L1 和 L2) 回归 nn.BCELoss()
二分类交叉熵损失 二分类 nn.BCEWithLogitsLoss()
自带 Sigmoid 的 BCELoss 二分类 nn.CrossEntropyLoss()
交叉熵损失 多分类 nn.NLLLoss()
负对数似然函数损失 多分类 nn.NLLLoss2d()
图像负对数似然函数损失 图像分割 nn.KLDivLoss()
KL 散度损失 分布学习 nn.MarginRankingLoss()
排序相似度的损失 推荐排序 nn.MultiLabelMarginLoss()
多标签分类的损失 多标签分类 nn.SoftMarginLoss()
多标签二分类问题的损失 多标签二分类
大部分损失函数的用法都是先实例化,再输入预测值和标签值 ,预测值形状为 (batch_size, num_class)
,对应每个类的概率 ,标签值形状为 (batch_size, num_class)
或 (batch_size, 1)
,前者代表标签平滑或多标签 ,后者仅仅只有目标类的索引 。具体用法如下:
1 2 criterion = nn.MSELoss() loss = criterion(pred, target)
其中 nn.CrossEntropyLoss()
的 target
输入必须是 LongTensor 类型,可以使用 .long()
进行类型强转。而其他 Loss 则是采用 Float 作为目标输入。
需要注意所有损失函数都会自动对 Loss 取均值 ,原因是默认参数 reduction='mean'
,可以修改成 'sum'
进行求和,或 'none'
保留 (batch_size, 1)
形状。这会影响到梯度的大小,用 mean
时,其学习率大小 应为用 sum
时的 batch_size
倍。有时候自己写的损失函数,也会在调用时加上 .mean()
来对齐结果。
此外,有的损失函数会有 weight
参数,BCEWithLogitsLoss() 有 pos_weight
参数,这些都是用来帮助模型解决样本类别不均衡问题的。需要传入张量参数 orch.from_numpy(np.array([1,15])).float()
。
优化器 PyTorch 在 torch.optim
中定义了十多种优化器,这里介绍常用的几种:
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 ''' params: 待优化的模型参数、或者是模型中参数组的字典(定制不同策略) lr: 学习率起始值,给定常数即可,如 1e-3, 5e-6 momentum: 动量因子,默认为 0,通常设置为 0.9 0.8 weight_decay: 权重衰减(L2惩罚的系数),默认为 0,通常设置为 1e-4 1e-3 再微调 nesterov: 是否采用牛顿动量(NAG),较少设置 ### 变化量(速度) v = mo * v + dx, 参数更新 x -= lr * v ''' optim.SGD(params, lr, momentum=0 , weight_decay=0 , nesterov=False )''' lr: 全局学习率,设定后就不再改变,默认为 0.1 一般不修改(会自适应) ### 根据梯度的大小自适应学习率,梯度大则学习率小,更新 x -= lr * dx / sqrt(dx^2) ''' optim.Adagrad(params, lr=0.01 , weight_decay=0 )''' alpha: 避免学习率下降太快,采用滑动平均的保留比例,默认为 0.99 eps: 为了增加数值计算的稳定性而加到分母里的项,不用理会 ### 缓存 cache = α * cache + (1-α) * dx^2, 更新 x -= lr * dx / sqrt(cache) ''' optim.RMSprop(params, lr=0.01 , alpha=0.99 , eps=1e-08 , weight_decay=0 , momentum=0 )''' betas: 在缓存的基础上再对梯度加上记忆,推荐参数 (0.9, 0.999) ### 记忆 memory = β1 * memory + (1 ‐ β1) * dx ### 缓存 cache = β2 * cache + (1 - β2) * dx^2 ### 更新 x -= lr * memory / sqrt(cache) ''' optim.Adam(params, lr=0.001 , betas=(0.9 , 0.999 ), eps=1e-08 , weight_decay=0 ) optim.Adamax(params, lr=0.002 , betas=(0.9 , 0.999 ), eps=1e-08 , weight_decay=0 ) optim.SparseAdam(params, lr=0.001 , betas=(0.9 ,0.999 ), eps=1e-08 ) optim.AdamW(params, lr=0.001 , betas=(0.9 ,0.999 ), eps=1e-08 , weight_decay=0.01 )
具体用法如下:
1 2 3 4 5 6 7 import torch.optim as optim optimizer = optim.SGD(model.parameters(), lr=0.01 , momentum=0.9 ) optimizer = optim.Adam([var1, var2], lr=0.0001 ) optimizer.zero_grad() optimizer.step()
如果想对网络中不同部分采取不同的优化策略,则需要用到参数组 (param_group),其保存了各个参数及其对应的学习率、动量等,可以通过列表嵌套字典 的形式来设置:
1 2 3 4 5 6 optim.SGD(model.parameters(), lr=1e-2 , momentum=0.9 ) optim.SGD([{'params' : model.base.parameters(), 'weight_decay' : wd}, {'params' : model.classifier.parameters(), 'lr' : 1e-3 }], lr=1e-2 , momentum=0.9 )
调度器 注意到上述优化器参数都有一个固定的 lr
,即使通过不同策略使学习率改变,也会有一个全局 lr
。为了更好地收敛模型,PyTorch 在 torch.optim.lr_scheduler
里封装了主动进行学习率衰减的方法。通过一个调度器(Scheduler)来控制优化器,具体用法如下:
1 2 3 4 5 6 7 8 9 10 11 optimizer = optim.SGD(model.parameters(), lr=lr, weight_decay=1e-4 ) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5 , verbose=True )for epoch in range (num_epochs): for X, y in train_set: optimizer.zero_grad() pred = model(X) loss = criterion(pred, y) loss.backward() optimizer.step() scheduler.step()
当然还有许多不同的调度器,可以有规律地间隔调整 ,或者根据指标的变化情况 调整。参数 verbose=True
表示每次更新都会输出一条信息,适用于所有调度器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1 ) optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1 ) optim.lr_scheduler.ExponentialLR(optimizer, gamma) optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max, eta_min=0 ) optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min' , factor=0.1 , patience=10 ) scheduler.step(val_loss) scheduler.step(val_acc) optimizer = optim.SGD([{'params' : base_params}, {'params' : net.fc.parameters(), 'lr' : 1e-2 }], lr=1e-3 , momentum=0.9 , weight_decay=1e-4 ) lambda1 = lambda epoch: epoch // 3 lambda2 = lambda epoch: 0.95 ** epoch scheduler = LambdaLR(optimizer, lr_lambda=[lambda1, lambda2])
评估阶段 前文提到,「评估模式 」是相对于「训练模式 」的模型传播路径,通常在验证集、测试集 上推理时会开启。开启命令为 model.eval()
,也可以用 model.train(False)
。在该模式下,Dropout、BatchNorm 等模块会有所区别。并且模型不需要进行反向传播,也就无需进行梯度计算,此时使用 torch.is_grad_enabled()
会返回 False。
需要注意的是,为了防止 PyTorch 自动对张量进行梯度计算,要用 with torch.no_grad():
包裹评估阶段的代码。关闭梯度计算后模型的推理速度 也会大大增加。下面介绍具体用法。
验证过程 :通常与训练阶段一起运行,即每个 epoch 训练完都在验证集上验证,并计算 Loss:1 2 3 4 5 6 7 8 9 10 11 12 13 for epoch in range (num_epoch): model.train() for X, y in tr_set: model.eval () with torch.no_grad(): total_loss = 0 for X, y in val_set: X, y = X.to(device), y.to(device) pred = model(X) loss = criterion(pred, y) total_loss += loss.cpu().item() * len (X)
测试阶段 :无需优化、无需计算损失,只需前向推理预测答案即可:1 2 3 4 5 6 7 model.eval () preds = []with torch.no_grad(): for X in test_set: X = X.to(device) pred = model(X) preds.append(pred.cpu())
TensorBoard 可视化 TensorBoard 用于可视化网络的相关信息,包括训练过程的指标、输出输出、网络内部参数等。使用前需要安装 pip install tensorboard
,并在代码中导入 Writer。Writer 会将我们关心的数据保存在一个文件夹中,在显示到浏览器 上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter('./path/to/log' ) writer.close() ''' tag/main_tag: 可视化时图像的标签名,是图的唯一标识 scalar_value: 代码中的变量名,必须是一个标量 tag_scalar_dict: 传入变量字典,key 相当于可视化的 tag,value 是代码中的变量名 global_step: 可视化时的横坐标,传入 epoch 就表示随 epoch 的指标变化 ''' writer.add_scalar(tag, scalar_value, global_step=None , walltime=None ) writer.add_scalars(main_tag, tag_scalar_dict, global_step=None , walltime=None )''' img_tensor: 代码中的变量名,可以传入张量,如输入数据、各层的输入输出 dataformats: 数据格式,如 CHW,HWC,HW ''' writer.add_image(tag, img_tensor, global_step=None , walltime=None , dataformats='CHW' )''' model: 传入模型,必须是 nn.Module 类 input_to_model: 输入给模型的数据 verbose: 是否打印计算图结构信息 ''' writer.add_graph(model, input_to_model=None , verbose=False , use_strict_trace=True )
运行上述代码后,数据将被保存到文件夹中,接下来在终端中输入:
1 tensorboard --logdir=./path/to/log --port 6006
就可以在 localhost:6006
看到可视化结果。如果是在远程服务器中,则需要将服务器的 6006 端口转发到本机的一个端口,在本机浏览器中访问端口。
模型保存与加载 PyTorch 文件后缀为 .pt
、.pth
和 .pkl
,三者在使用上没有区别,都可以保存张量 、模型 (结构、权重参数),张量的保存和加载方法如下:
1 2 3 4 torch.save(t, 'save_tensor.pt' ) t = torch.load('save_tensor.pt' ) t = torch.load('save_tensor.pt' , map_location=torch.device('cpu' )) t = torch.load('save_tensor.pt' , map_location="cuda:0" )
如果有多个张量,可以通过字典的形式进行存储,将字符串映射到张量,这也是模型的保存形式:
1 2 3 tensor_dict = {'x' : x, 'y' : y} torch.save(tensor_dict, 'save_dict.pt' ) tensor_dict2 = torch.load('save_dict.pt' )
模型(Model)分为网络的结构和权重参数 两部分,后者位于 model.stack_dict()
中,以字典 的形式存储,将每一层映射成它的参数张量,通过 .keys()
可以查看所有键。模型的保存和加载方法有两种:
1 2 3 4 5 6 7 8 9 10 11 12 torch.save(model.state_dict(), 'save_param.pt' ) model = Model(*args, **kwargs) model.load_state_dict(torch.load('save_param.pt' , map_location="cuda:0" )) model = model.to(device) model.eval () torch.save(model, 'save_model.pt' ) model = torch.load('save_model.pt' ) model = model.to(device) model.eval ()
注意第二种方法中,网络模型定义的代码 要与 load 代码写在一起,或者从其他文件中 import
,这样才能成功加载。
Checkpoint 在适当的时候保存模型的断点 ,可以在训练意外终止后不必从头开始训练 ,也方便必要的时候回滚 到历史状态。除了模型的参数,还包括 Loss、Epoch 等训练信息。最容易忽略的是优化器的参数 ,也是使用 state_dict()
访问,包括:学习率、动量值、衰减系数等参数。一般使用 tar 文件格式来保存这些检查点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (epoch + 1 ) % checkpoint_interval == 0 : checkpoint = {'epoch' : epoch+1 , 'loss' : loss, 'best_loss' : best_loss, 'model_state_dict' : net.state_dict(), 'optimizer_state_dict' : optimizer.state_dict(), } path_checkpoint = "./history/ckp_{}_epoch.pth.tar" .format (epoch) torch.save(checkpoint, path_checkpoint) model = TheModelClass(*args, **kwargs) optimizer = TheOptimizerClass(*args, **kwargs) checkpoint = torch.load(path_checkpoint) model.load_state_dict(checkpoint['model_state_dict' ]) optimizer.load_state_dict(checkpoint['optimizer_state_dict' ]) epoch = checkpoint['epoch' ] loss = checkpoint['loss' ] best_loss = checkpoint['best_loss' ] model.eval ()
切换设备 load 提供设备重载 的功能,可以在 CPU、GPU 之间切换,也可以在不同 GPU 间切换:
1 2 3 4 5 6 7 8 torch.load('tensors.pt' , map_location=lambda storage, loc: storage) torch.load('tensors.pt' , map_location=torch.device('cpu' )) torch.load('tensors.pt' , map_location=lambda storage, loc: storage.cuda(1 )) torch.load('tensors.pt' , map_location='cuda:1' ) torch.load('tensors.pt' , map_location={'cuda:1' :'cuda:0' })
上述代码只有模型在一个 GPU 上训练时才生效,如果在多个 GPU 中,则需要使用 nn.DataParallel
模块,例如:
1 2 model = nn.DataParalle(model) torch.save(model.module.state_dict(), 'model.pt' )
注意到此时模型的参数名都带上了 module
前缀,如果再想把模型加载到单个 GPU 或 CPU 上,则需要手动将 key 中的前缀去除 :
1 2 3 4 5 6 7 8 9 10 11 12 state_dict = torch.load('model.pt' )from collections import OrderedDict new_state_dict = OrderedDict()for key, value in state_dict.items(): name = key[7 :] new_state_dict[name] = value model.load_state_dict(new_state_dict)
加载部分模型 在迁移学习或者训练新的复杂模型时,加载部分模型 是很常见的。利用经过训练的参数,即使只有少数参数可用,也将有助于预热 训练过程,并且使模型更快收敛。
在加载部分模型参数进行预训练的时候,很可能会碰到键不匹配 的情况(模型权重都是按键值对的形式保存并加载回来的)。因此,无论是缺少键还是多出键的情况,都可以通过在 load_state_dict()
函数中设定 strict
参数为 False
来忽略不匹配的键 。
1 2 3 torch.save(modelA.state_dict(), PATH) modelB = TheModelBClass(*args, **kwargs) modelB.load_state_dict(torch.load(PATH), strict=False )
如果想将某一层的参数加载到其他层,但是有些键不匹配,可以通过强制修改 state_dict
中参数的 key
解决。