本來是隻用Tenorflow的,但是因為TF有些Numpy特性並不支援,比如對陣列使用列表進行切片,所以只能轉戰Pytorch了(pytorch是支援的)。還好Pytorch比較容易上手,幾乎完美複製了Numpy的特性(但還有一些特性不支援),怪不得熱度上升得這麼快。
模型定義
和TF很像,Pytorch也通過繼承父類來搭建模型,同樣也是實現兩個方法。在TF中是__init__()和call(),在Pytorch中則是__init__()和forward()。功能類似,都分別是初始化模型內部結構和進行推理。其它功能比如計算loss和訓練函式,你也可以繼承在裡面,當然這是可選的。下面搭建一個判別MNIST手寫字的Demo,首先給出模型程式碼:
import numpy as np import matplotlib.pyplot as plt import torch from torch import nn,optim from torchsummary import summary from keras.datasets import mnist from keras.utils import to_categorical device = torch.device('cuda') #——————1—————— class ModelTest(nn.Module): def __init__(self,device): super().__init__() self.layer1 = nn.Sequential(nn.Flatten(),nn.Linear(28*28,512),nn.ReLU())#——————2—————— self.layer2 = nn.Sequential(nn.Linear(512,512),nn.ReLU()) self.layer3 = nn.Sequential(nn.Linear(512,512),nn.ReLU()) self.layer4 = nn.Sequential(nn.Linear(512,10),nn.Softmax()) self.to(device) #——————3—————— self.opt = optim.SGD(self.parameters(),lr=0.01)#——————4—————— def forward(self,inputs): #——————5—————— x = self.layer1(inputs) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) return x def get_loss(self,true_labels,predicts): loss = -true_labels * torch.log(predicts) #——————6—————— loss = torch.mean(loss) return loss def train(self,imgs,labels): predicts = model(imgs) loss = self.get_loss(labels,predicts) self.opt.zero_grad()#——————7—————— loss.backward()#——————8—————— self.opt.step()#——————9—————— model = ModelTest(device) summary(model,(1,28,28),3,device='cuda') #——————10——————
#1:獲取裝置,以方便後面的模型與變數進行記憶體遷移,裝置名只有兩種:'cuda'和'cpu'。通常是在你有GPU的情況下需要這樣顯式進行裝置的設定,從而在需要時,你可以將變數從主存遷移到視訊記憶體中。如果沒有GPU,不獲取也沒事,pytorch會預設將引數都儲存在主存中。
#2:模型中層的定義,可以使用Sequential將想要統一管理的層集中表示為一層。
#3:在初始化中將模型引數遷移到GPU視訊記憶體中,加速運算,當然你也可以在需要時在外部執行model.to(device)進行遷移。
#4:定義模型的優化器,和TF不同,pytorch需要在定義時就將需要梯度下降的引數傳入,也就是其中的self.parameters(),表示當前模型的所有引數。實際上你不用擔心定義優化器和模型引數的順序問題,因為self.parameters()的輸出並不是模型引數的例項,而是整個模型引數物件的指標,所以即使你在定義優化器之後又定義了一個層,它依然能優化到。當然優化器你也可以在外部定義,傳入model.parameters()即可。這裡定義了一個隨機梯度下降。
#5:模型的前向傳播,和TF的call()類似,定義好model()所執行的就是這個函式。
#6:我將獲取loss的函式整合在了模型中,這裡計算的是真實標籤和預測標籤之間的交叉熵。
#7/8/9:在TF中,引數梯度是儲存在梯度帶中的,而在pytorch中,引數梯度是各自整合在對應的引數中的,可以使用tensor.grad來檢視。每次對loss執行backward(),pytorch都會將參與loss計算的所有可訓練引數關於loss的梯度疊加進去(直接相加)。所以如果我們沒有疊加梯度的意願的話,那就要在backward()之前先把之前的梯度刪除。又因為我們前面已經把待訓練的引數都傳入了優化器,所以,對優化器使用zero_grad(),就能把所有待訓練引數中已存在的梯度都清零。那麼梯度疊加什麼時候用到呢?比如批量梯度下降,當記憶體不夠直接計算整個批量的梯度時,我們只能將批量分成一部分一部分來計算,每算一個部分得到loss就backward()一次,從而得到整個批量的梯度。梯度計算好後,再執行優化器的step(),優化器根據可訓練引數的梯度對其執行一步優化。
#10:使用torchsummary函式顯示模型結構。奇怪為什麼不把這個繼承在torch裡面,要重新安裝一個torchsummary庫。
訓練及視覺化
接下來使用模型進行訓練,因為pytorch自帶的MNIST資料集並不好用,所以我使用的是Keras自帶的,定義了一個獲取資料的生成器。下面是完整的訓練及繪圖程式碼(50次迭代記錄一次準確率):
import numpy as np import matplotlib.pyplot as plt import torch from torch import nn,optim from torchsummary import summary from keras.datasets import mnist from keras.utils import to_categorical device = torch.device('cuda') #——————1—————— class ModelTest(nn.Module): def __init__(self,device): super().__init__() self.layer1 = nn.Sequential(nn.Flatten(),nn.Linear(28*28,512),nn.ReLU())#——————2—————— self.layer2 = nn.Sequential(nn.Linear(512,512),nn.ReLU()) self.layer3 = nn.Sequential(nn.Linear(512,512),nn.ReLU()) self.layer4 = nn.Sequential(nn.Linear(512,10),nn.Softmax()) self.to(device) #——————3—————— self.opt = optim.SGD(self.parameters(),lr=0.01)#——————4—————— def forward(self,inputs): #——————5—————— x = self.layer1(inputs) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) return x def get_loss(self,true_labels,predicts): loss = -true_labels * torch.log(predicts) #——————6—————— loss = torch.mean(loss) return loss def train(self,imgs,labels): predicts = model(imgs) loss = self.get_loss(labels,predicts) self.opt.zero_grad()#——————7—————— loss.backward()#——————8—————— self.opt.step()#——————9—————— def get_data(device,is_train = True, batch = 1024, num = 10000): train_data,test_data = mnist.load_data() if is_train: imgs,labels = train_data else: imgs,labels = test_data imgs = (imgs/255*2-1)[:,np.newaxis,...] labels = to_categorical(labels,10) imgs = torch.tensor(imgs,dtype=torch.float32).to(device) labels = torch.tensor(labels,dtype=torch.float32).to(device) i = 0 while(True): i += batch if i > num: i = batch yield imgs[i-batch:i],labels[i-batch:i] train_dg = get_data(device, True,batch=4096,num=60000) test_dg = get_data(device, False,batch=5000,num=10000) model = ModelTest(device) summary(model,(1,28,28),11,device='cuda') ACCs = [] import time start = time.time() for j in range(20000): #訓練 imgs,labels = next(train_dg) model.train(imgs,labels) #驗證 img,label = next(test_dg) predicts = model(img) acc = 1 - torch.count_nonzero(torch.argmax(predicts,axis=1) - torch.argmax(label,axis=1))/label.shape[0] if j % 50 == 0: t = time.time() - start start = time.time() ACCs.append(acc.cpu().numpy()) print(j,t,'ACC: ',acc) #繪圖 x = np.linspace(0,len(ACCs),len(ACCs)) plt.plot(x,ACCs)
準確率變化圖如下:
注意事項
需要注意的是,pytorch的tensor基於numpy的array,它們是共享記憶體的。也就是說,如果你把tensor直接插入一個列表,當你修改這個tensor時,列表中的這個tensor也會被修改;更容易被忽略的是,即使你用tensor.detach.numpy(),先將tensor轉換為array型別,再插入列表,當你修改原本的tensor時,列表中的這個array也依然會被修改。所以如果我們只是想儲存tensor的值而不是整個物件,就要使用np.array(tensor)將tensor的值複製出來。