PyTorch-模型
簡介
在前一篇文章提到了關于數據的一系列操作,數據讀入之后就是將數據“喂”給深度模型,所以構建一個深度學習模型是很重要的,本文主要講解PyTorch中模型的相關操作,包括模型的定義、模型參數的初始化、模型的保存及加載。
模型構建
在PyTorch中,想要讓PyTorch的后續接口認為這是一個模型的關鍵需要滿足下面三個條件。
- 自定義的模型以類的形式存在,且該類必須繼承自torch.nn.Module,該繼承操作可以讓PyTorch識別該類為一個模型。
- 在自定義的類中的__init__方法中聲明模型使用到的組件,一般是封裝好的張量操作,如卷積。池化、批量標準化、全連接等。
- 在forward方法中使用__init__方法中的組件進行組裝,構成網絡的前向傳播邏輯。
該模型的前向運算通過調用模型的call方法即可,通過實例化的對象加括號即可默認調用該方法,如net(input_tensor)。包括很多nn中定義的張量運算也可以通過該方法進行運算。
下面的代碼演示了一個比較簡單的卷積神經網絡分類器,其中涉及到的張量操作如卷積等均為計算機視覺中的基本知識,參數也比較好理解,不多贅述,可以查看nn模塊的文檔。**這里需要注意,PyTorch不同于TensorFlow,PyTorch期待圖片的張量格式輸入是(batch, c, h, w ),這點需要注意,當然,如果按照上篇文章中PyTorch的數據接口讀入數據,那不需要自行控制。**下面的代碼輸出如期待的是[batch, 101]的維度,通過softmax激活后就是常見的分類器輸出。
import torch import torch.nn as nn import torch.nn.functional as Fclass Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=(3, 3))self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3)self.pool2 = nn.MaxPool2d(2, 2)self.fc1 = nn.Linear(64*54*54, 256)self.fc2 = nn.Linear(256, 128)self.fc3 = nn.Linear(128, 101)def forward(self, x):x = self.pool1(F.relu(self.conv1(x)))x = self.pool2(F.relu(self.conv2(x)))x = x.view(-1, 64*54*54)x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))x = self.fc3(x)return xif __name__ == '__main__':net = Net()data = torch.randn((32, 3, 224, 224))label = torch.zeros((32, ))pred = net(data)print(pred.shape)實際上,真正設計模型的時候,模型都是比較復雜的,遠不是一個模塊就能完成的,這時候就需要組合多個模型,也需要組合多個張量運算,將多個張量運算堆疊或者多個模型堆疊需要使用一個容器—torch.nn.Sequential,該容器將一系列操作按照前后順序堆疊起來,如下面的例子,該容器接受一系列的張量操作或者模型操作為參數。
nn.Sequential(nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False),nn.BatchNorm2d(out_channels),nn.ReLU(inplace=True),nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False),nn.BatchNorm2d(out_channels))下面的例子就演示了構建較為復雜的深度模型的方法,該模型為ResNet34,參考了這個思路。
import torch from torch import nn from torch.nn import functional as Fclass ResidualBlock(nn.Module):"""殘差模塊"""def __init__(self, in_channels, out_channels, stride=1, shortcut=None):super(ResidualBlock, self).__init__()self.left = nn.Sequential(nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False),nn.BatchNorm2d(out_channels),nn.ReLU(inplace=True),nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False),nn.BatchNorm2d(out_channels))self.right = shortcutdef forward(self, x):out = self.left(x)residual = x if self.right is None else self.right(x)out += residualreturn F.relu(out)class ResNet34(nn.Module):def __init__(self, num_classes=101):super(ResNet34, self).__init__()self.model_name = 'ResNet34'self.pre = nn.Sequential(nn.Conv2d(3, 64, 7, 2, 3, bias=False),nn.BatchNorm2d(64),nn.ReLU(inplace=True),nn.MaxPool2d(3, 2, 1))self.layer1 = self.make_layer(64, 128, 3)self.layer2 = self.make_layer(128, 256, 4, stride=2)self.layer3 = self.make_layer(256, 512, 6, stride=2)self.layer4 = self.make_layer(512, 512, 3, stride=2)self.fc = nn.Linear(512, num_classes)def make_layer(self, in_channels, out_channels, block_num, stride=1):shortcut = nn.Sequential(nn.Conv2d(in_channels, out_channels, 1, stride, bias=False),nn.BatchNorm2d(out_channels))layers = list()layers.append(ResidualBlock(in_channels, out_channels, stride, shortcut))for i in range(1, block_num):layers.append(ResidualBlock(out_channels, out_channels))return nn.Sequential(*layers)def forward(self, x):x = self.pre(x)x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)x = F.avg_pool2d(x, 7)x = x.view(x.size(0), -1)return self.fc(x)if __name__ == '__main__':resnet = ResNet34(num_classes=101)data = torch.randn((32, 3, 224, 224))label = torch.zeros((32,))pred = resnet(data)print(pred.shape)權重初始化
在上一節的介紹中,詳細敘述了模型的構建細節,但是有一個很重要的點沒有提及,就是權重初始化,如上面案例中的conv2d和Linear,這些運算都有可訓練的參數需要初始化(當然,不設定也有默認初始化方法),這些參數權重也是模型訓練的目的,參數初始化的效果在深度學習中尤為重要,它會直接影響模型的收斂效果。
當然,一般需要對不同的運算層采用不同的初始化方法,在PyTorch中初始化方法均在torch.nn.init中封裝。在上一節自定義網絡的類中,添加一個權重初始化方法如下(針對不同的結構采用不同的初始化方法,如Xavier、Kaiming正太分布等),修改后的代碼如下。
import torch import torch.nn as nn import torch.nn.functional as Fclass Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=(3, 3))self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3)self.pool2 = nn.MaxPool2d(2, 2)self.fc1 = nn.Linear(64*54*54, 256)self.fc2 = nn.Linear(256, 128)self.fc3 = nn.Linear(128, 101)def forward(self, x):x = self.pool1(F.relu(self.conv1(x)))x = self.pool2(F.relu(self.conv2(x)))x = x.view(-1, 64*54*54)x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))x = self.fc3(x)return xdef init_weights(self):for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.xavier_normal_(m.weight.data)if m.bias is not None:m.bias.data.zero_()elif isinstance(m, nn.BatchNorm2d):m.weight.data.fill_(1)m.bias.data.zero_()elif isinstance(m, nn.Linear):nn.init.normal_(m.weight.data, 0, 0.01)m.bias.data.zero_()if __name__ == '__main__':net = Net()net.init_weights()data = torch.randn((32, 3, 224, 224))label = torch.zeros((32,))pred = net(data)print(pred.shape)初始化方法
在上一節,主要講述如何對模型各層進行初始化,事實上常用的初始化方法PyTorch都進行了封裝于torch.nn.init模塊下,有Xavier初始化和Kaiming初始化這兩種效果比較顯著的初始化方法,也有一些比較傳統的方法。
Xavier初始化
這是依據“方差一致性”推導得到的初始化方法,有均勻分布和正態分布兩種。
- Xavier均勻分布
- nn.init.xavier_uniform_(tensor, gain=1)
- Xavier初始化方法服從均勻分布(-a, a),其中a=gain*sqrt(6/fan_in+fan_out),其中gain是依據激活函數類型設定的。
- Xavier正態分布
- nn.init.xavier_normal_(tensor, gain=1)
- Xavier初始化方法服從正太分布,mean=0, std=gain*sqrt(2/fan_in+fan_out)。
Kaiming初始化
同樣根據“方差一致性”推導得到,針對xavier初始化方法在relu這一類激活函數上效果不佳而提出的。
- Kaiming均勻分布
- nn.init.kaiming_uniform_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
- 服從(-b, b)的均勻分布,其中b=sqrt(6/(1+a^2)*fan_in),其中a為激活函數負半軸的斜率,relu對應的a為0。
- mode參數為fan_in或者fan_out,前者使正向傳播時方差一致,后者使反向傳播時方差一致。
- nonlinearity參數表示是否非線性,relu表示線性,leaky_relu表示非線性。
- Kaiming正態分布
- nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
- 服從(0, std)的正態分布,其中std=sqrt(2/(1+a^2)*fan_in)。
- 參數同上。
其他初始化
- 均勻分布初始化
- nn.init.uniform_(tensor, a=0, b=1)
- 服從U(a,b)的均勻分布。
- 正態分布初始化
- nn.init.normal_(tensor, mean=0, std=1)
- 服從N(mean,std)的正態分布。
- 常數初始化
- nn.init.constant_(tensor, val)
- 使值為常數val的初始化方法。
- 單位矩陣初始化
- nn.init.eye_(tensor)
- 將二維Tensor初始化為單位矩陣。
- 正交初始化
- nn.init.orthogonal_(tensor, gain=1)
- 使得tensor是正交的。
- 稀疏初始化
- nn.init.sparse_(tensor, sparsity, std=0.01)
- 從正態分布N(0,std)中進行稀疏化,使得每一列有一部分為0,其中sparsity控制列中為0的比例。
- 增益計算
- nn.init.calculate_gain(nonlinearity, param=None)
- 用于計算不同激活函數的gain值。
遷移學習
上一節,介紹了很多權重初始化的方法,可以知道良好的初始化可以加速模型收斂、獲得更好的精度,實際使用中,通常采用一個預訓練的模型的權重作為模型的初始化參數,這個方法稱為Finetune,更廣泛的就是指遷移學習,Finetune技術本質上就是使得模型具有更好的權重初始化。
一般,需要如下三個步驟。
上述過程有兩種模型的保存方法,一種是保存整個模型,另一種是保存模型的參數,推薦后者。
下面的代碼演示如何進行Finetune的初始化,當然,實際進行遷移學習時對于不同的層需要采用不同的學習速率,一般希望前面的層學習率低,后層(如全連接層)學習率高一些,需要對不同的層設置不同的學習率,這里不多提及了。
net = Net() # 假設訓練完成了 # 保存模型參數 torch.save(net.state_dict(), 'net_weights.pkl') # 加載模型參數 pretrained_params = torch.load('net_weights.pkl') # 構建模型,預訓練參數初始化 net = Net() net_state_dict = net.state_dict() pre_dict = {k: v for k, v in pretrained_params.items() if k in net_state_dict} net_state_dict.update(pre_dict)補充說明
本文介紹了PyTorch中模型構建的方法以及權重初始化技巧,進一步提到了遷移學習,這包括模型的保存與加載、使用預訓練模型的參數作為網絡的初始化參數(finetune技巧)。本文的所有代碼均開源于我的Github,歡迎star或者fork。
總結
以上是生活随笔為你收集整理的PyTorch-模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PyTorch-数据准备
- 下一篇: PyTorch-训练