張量
數組與張量
PyTorch 作為當前首屈一指的深度學習庫,其將 NumPy 數組的語法盡數吸收,作為自己處理張量的基本語法,且運算速度從使用 CPU 的數組進步到使用 GPU 的張量。
NumPy 和 PyTorch 的基礎語法幾乎一致,具體表現為:
- np 對應 torch;
- 數組 array 對應張量 tensor;
- NumPy 的 n 維數組對應著 PyTorch 的 n 階張量。
數組與張量之間可以互相轉換
- 數組 arr 轉為張量 ts:ts = torch.tensor(arr);
- 張量 ts 轉為數組 arr:arr = np.array(ts)。
從數組到張量
PyTorch 只是少量修改了 NumPy 的函數或方法,現對其中不同的地方進行羅列。
NumPy 的函數 | PyTorch 的函數 | 用法區別 | |
---|---|---|---|
數據類型 | .astype( ) | .type( ) | 無 |
隨機數組 | np.random.random( ) | torch.rand( ) | 無 |
隨機數組 | np.random.randint( ) | torch.randint( ) | 不接納一維張量 |
隨機數組 | np.random.normal( ) | torch.normal( ) | 不接納一維張量 |
隨機數組 | np.random.randn( ) | torch.randn( ) | 無 |
數組切片 | .copy( ) | .clone( ) | 無 |
數組拼接 | np.concatenate( ) | torch.cat( ) | 無 |
數組分裂 | np.split( ) | torch.split( ) | 參數含義優化 |
矩陣乘積 | np.dot( ) | torch.matmul( ) | 無 |
矩陣乘積 | np.dot(v,v) | torch.dot( ) | 無 |
矩陣乘積 | np.dot(m,v) | torch.mv( ) | 無 |
矩陣乘積 | np.dot(m,m) | torch.mm( ) | 無 |
數學函數 | np.exp( ) | torch.exp( ) | 必須傳入張量 |
數學函數 | np.log( ) | torch.log( ) | 必須傳入張量 |
聚合函數 | np.mean( ) | torch.mean( ) | 必須傳入浮點型張量 |
聚合函數 | np.std( ) | torch.std( ) | 必須傳入浮點型張量 |
用GPU存儲張量
默認的張量使用 CPU 存儲,可將其搬至 GPU 上,如示例所示。
import torch
# 默認的張量存儲在 CPU 上
ts1 = torch.randn(3,4)
ts1 #OUT:tensor([[ 2.2716, 1.2107, -0.0582, 0.5885 ],# [-0.5868, -0.6480, -0.2591, 0.1605],# [-1.3968, 0.7999, 0.5180, 1.2214 ]])# 移動到 GPU 上
ts2 = ts1.to('cuda:0') # 第一塊 GPU 是 cuda:0
ts2 #OUT: tensor([[ 2.2716, 1.2107, -0.0582, 0.5885 ],# [-0.5868, -0.6480, -0.2591, 0.1605],# [-1.3968, 0.7999, 0.5180, 1.2214 ]], device='cuda:0')
以上操作可以把數據集搬到 GPU 上,但是神經網絡模型也要搬到 GPU 上才可正常運行,使用下面的代碼即可。
# 搭建神經網絡的類,此處略,詳見第三章
class DNN(torch.nn.Module):
#略
# 根據神經網絡的類創建一個網絡
model = DNN().to('cuda:0') # 把該網絡搬到 GPU 上
想要查看顯卡是否在運作時,在 cmd 中輸入:nvidia-smi,如下圖所示。
DNN原理
神經網絡通過學習大量樣本的輸入與輸出特征之間的關系,以擬合出輸入與輸出之間的方程,學習完成后,只給它輸入特征,它便會可以給出輸出特征。神經網絡可以分為這么幾步:劃分數據集、訓練網絡、測試網絡、使用網絡。
劃分數據集
數據集里每個樣本必須包含輸入與輸出,將數據集按一定的比例劃分為訓練集與測試集,分別用于訓練網絡與測試網絡,如下表所示。
樣本 | 輸入特征 | 輸出特征 | |
---|---|---|---|
訓練集 | 1 | In1 ln2 ln3 | Out1 Out2 Out3 |
訓練集 | 2 | * * * | * * * |
訓練集 | 3 | * * * | * * * |
訓練集 | 4 | * * * | * * * |
訓練集 | 5 | * * * | * * * |
訓練集 | … | * * * | * * * |
訓練集 | 800 | * * * | * * * |
測試集 | 801 | * * * | * * * |
測試集 | 802 | * * * | * * * |
測試集 | … | * * * | * * * |
測試集 | 1000 | * * * | * * * |
考慮到數據集的輸入特征與輸出特征都是 3 列,因此神經網絡的輸入層與輸出層也必須都是 3 個神經元,隱藏層可以自行設計,如下圖所示。
考慮到 Python 列表、NumPy 數組以及 PyTorch 張量都是從索引[0]開始,再加之輸入層沒有內部參數(權重 ω 與偏置 b),所以習慣將輸入層稱之為第 0 層。
訓練網絡
- 神經網絡的訓練過程,就是經過很多次前向傳播與反向傳播的輪回,最終不斷調整其內部參數(權重 ω 與偏置 b),以擬合任意復雜函數的過程。內部參數一開始是隨機的(如 Xavier 初始值、He 初始值),最終會不斷優化到最佳。
- 還有一些訓練網絡前就要設好的外部參數:網絡的層數、每個隱藏層的節點數、每個節點的激活函數類型、學習率、輪回次數、每次輪回的樣本數等等。
- 業界習慣把內部參數稱為參數,外部參數稱為超參數
-
前向傳播
將單個樣本的 3 個輸入特征送入神經網絡的輸入層后,神經網絡會逐層計算到輸出層,最終得到神經網絡預測的 3 個輸出特征。計算過程中所使用的參數就是內部參數,所有的隱藏層與輸出層的神經元都有內部參數,以第 1 層的第 1 個神經元,如下圖所示。
該神經元節點的計算過程為y = ω1x1 + ω2x2 + ω3x3 + b你可以理解為,每一根線就是一個權重 ω,每一個神經元節點也都有它自己的偏置 b。當然,每個神經元節點在計算完后,由于這個方程是線性的,因此必須在外面套一個非線性的函數:y = σ(ω1x1 + ω2x2 + ω3x3 + b),σ被稱為激活函數。如果你不套非線性函數,那么即使 10 層的網絡,也可以用 1 層就擬合出同樣的方程。 -
反向傳播
- 經過前向傳播,網絡會根據當前的內部參數計算出輸出特征的預測值。但是這個預測值與真實值直接肯定有差距,因此需要一個損失函數來計算這個差距。例如,求預測值與真實值之間差的絕對值,就是一個典型的損失函數。
- 損失函數計算好后,逐層退回求梯度,這個過程很復雜,原理不必掌握,大致意思就是,看每一個內部參數是變大還是變小,才會使得損失函數變小。這樣就達到了優化內部參數的目的
- 在這個過程中,有一個外部參數叫學習率。學習率越大,內部參數的優化越快,但過大的學習率可能會使損失函數越過最低點,并在谷底反復橫跳。因此,在網絡的訓練開始之前,選擇一個合適的學習率很重要。
- batch_size
前向傳播與反向傳播一次時,有三種情況:
- 批量梯度下降(Batch Gradient Descent,BGD),把所有樣本一次性輸入進網絡,這種方式計算量開銷很大,速度也很慢。
- 隨機梯度下降(Stochastic Gradient Descent,SGD),每次只把一個樣本輸入進網絡,每計算一個樣本就更新參數。這種方式雖然速度比較快,但是收斂性能差,可能會在最優點附近震蕩,兩次參數的更新也有可能抵消。
- 小批量梯度下降(Mini-Batch Gradient Decent,MBGD)是為了中和上面二者而生,這種辦法把樣本劃分為若干個批,按批來更新參數。
所以,batch_size 即一批中的樣本數,也是一次喂進網絡的樣本數。此外,由于 Batch Normalization 層(用于將每次產生的小批量樣本進行標準化)的存在,batch_size 一般設置為 2 的冪次方,并且不能為 1。
PS:PyTorch 實現時只支持批量與小批量,不支持單個樣本的輸入方式。PyTorch 里的 torch.optim.SGD 只表示梯度下降,批量與小批量見第四、五章
- epochs
1 個 epoch 就是指全部樣本進行 1 次前向傳播與反向傳播。
假設有 10240 個訓練樣本,batch_size 是 1024,epochs 是 5。那么:
- 全部樣本將進行 5 次前向傳播與反向傳播;
- 1 個 epoch,將發生 10 次(10240/1024)前向傳播與反向傳播;
- 一共發生 50 次(10*5)前向傳播和反向傳播。
測試網絡
為了防止訓練的網絡過擬合,因此需要拿出少量的樣本進行測試。過擬合的意思是:網絡優化好的內部參數只能對訓練樣本有效,換成其它就寄。以線性回歸為例,過擬合下圖b所示
當網絡訓練好后,拿出測試集的輸入,進行 1 次前向傳播后,將預測的輸出與測試集的真實輸出進行對比,查看準確率。(測試集就不需要反向傳播了,反向傳播只是為了優化參數)
使用網絡
真正使用網絡進行預測時,樣本只知輸入,不知輸出。直接將樣本的輸入進行 1 次前向傳播,即可得到預測的輸出。
DNN實現
torch.nn 提供了搭建網絡所需的所有組件,nn 即 Neural Network 神經網絡。因此,可以單獨給 torch.nn 一個別名,即 import torch.nn as nn。
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
%matplotlib inline
# 展示高清圖
from matplotlib_inline import backend_inline
backend_inline.set_matplotlib_formats('svg')
制作數據集
在訓練之前,要準備好訓練集的樣本。這里生成 10000 個樣本,設定 3 個輸入特征與 3 個輸出特征,其中:
- 每個輸入特征相互獨立,均服從均勻分布;
- 當(X1+X2+X3)< 1 時,Y1 為 1,否則 Y1 為 0;
- 當 1<(X1+X2+X3)<2 時,Y2 為 1,否則 Y2 為 0;
- 當(X1+X2+X3)>2 時,Y3 為 1,否則 Y3 為 0;
- .float()將布爾型張量轉化為浮點型張量。
# 生成數據集
X1 = torch.rand(10000,1) # 輸入特征 1
X2 = torch.rand(10000,1) # 輸入特征 2
X3 = torch.rand(10000,1) # 輸入特征 3
Y1 = ( (X1+X2+X3)<1 ).float() # 輸出特征 1
Y2 = ( (1<(X1+X2+X3)) & ((X1+X2+X3)<2) ).float() # 輸出特征 2
Y3 = ( (X1+X2+X3)>2 ).float() # 輸出特征 3
Data = torch.cat([X1,X2,X3,Y1,Y2,Y3],axis=1) # 整合數據集
Data = Data.to('cuda:0') # 把數據集搬到 GPU 上
Data.shape #OUT:torch.Size([10000, 6])
事實上,數據的 3 個輸出特征組合起來是一個 One-Hot 編碼(獨熱編碼)。
# 劃分訓練集與測試集
train_size = int(len(Data) * 0.7) # 訓練集的樣本數量
test_size = len(Data) - train_size # 測試集的樣本數量
Data = Data[torch.randperm( Data.size(0)) , : ] # 打亂樣本的順序 防止有些數據具有先后順序
train_Data = Data[ : train_size , : ] # 訓練集樣本
test_Data = Data[ train_size : , : ] # 測試集樣本
train_Data.shape, test_Data.shape #OUT:(torch.Size([7000, 6]), torch.Size([3000, 6]))
以上的代碼屬于通用型代碼,便于我們手動分割訓練集與測試集。
搭建神經網絡
- 搭建神經網絡時,以 nn.Module 作為父類,我們自己的神經網絡可直接繼承父類的方法與屬性,nn.Module 中包含網絡各個層的定義。
- 在定義的神經網絡子類中,通常包含__init__特殊方法與 forward 方法。__init__特殊方法用于構造自己的神經網絡結構,forward 方法用于將輸入數據進行前向傳播。由于張量可以自動計算梯度,所以不需要出現反向傳播方法。
class DNN(nn.Module):def __init__(self):''' 搭建神經網絡各層 '''super(DNN,self).__init__()self.net = nn.Sequential( # 按順序搭建各層nn.Linear(3, 5), nn.ReLU(), # 第 1 層:全連接層nn.Linear(5, 5), nn.ReLU(), # 第 2 層:全連接層nn.Linear(5, 5), nn.ReLU(), # 第 3 層:全連接層nn.Linear(5, 3) # 第 4 層:全連接層)def forward(self, x):''' 前向傳播 '''y = self.net(x) # x 即輸入數據return y # y 即輸出數據
model = DNN().to('cuda:0') # 創建子類的實例,并搬到 GPU 上
model # 查看該實例的各層 #OUT:DNN(# (net): Sequential(# (0): Linear(in_features=3, out_features=5, bias=True)# (1): ReLU()# (2): Linear(in_features=5, out_features=5, bias=True)# (3): ReLU()# (4): Linear(in_features=5, out_features=5, bias=True)# (5): ReLU()# (6): Linear(in_features=5, out_features=3, bias=True)# )# )
在上面的 nn.Sequential()函數中,每一個隱藏層后都使用了 RuLU 激活函數,各層的神經元節點個數分別是:3、5、5、5、3。
PS:輸入層有 3 個神經元、輸出層有 3 個神經元,這不是巧合,是有意而為之。輸入層的神經元數量必須與每個樣本的輸入特征數量一致,輸出層的神經元數量必須與每個樣本的輸出特征數量一致。
網絡的內部參數
神經網絡的內部參數是權重與偏置,內部參數在神經網絡訓練之前會被賦予隨機數,隨著訓練的進行,內部參數會逐漸迭代至最佳值,現對參數進行查看。
# 查看內部參數(非必要)
for name, param in model.named_parameters():print(f"參數:{name}\n 形狀:{param.shape}\n 數值:{param}\n")
代碼一共給了我們 8 個參數,其中參數與形狀的結果如下表所示,考慮到其數值初始狀態時是隨機的(如 Xavier 初始值、He 初始值),此處不討論。
可見,具有權重與偏置的地方只有 net.0、net.2、net.4、net.6,易知這幾個地方其實就是所有的隱藏層與輸出層,這符合理論。
- 首先,net.0.weight 的權重形狀為[5, 3],5 表示它自己的節點數是 5,3 表示與之連接的前一層的節點數為 3
- 其次,由于前面里進行了 model =DNN().to(‘cuda:0’)操作,因此所有的內部參數都自帶device=‘cuda:0’。
- 最后,注意到 requires_grad=True,說明所有需要進行反向傳播的內部參數(即權重與偏置)都打開了張量自帶的梯度計算功能。
網絡的外部參數
外部參數即超參數,這是調參師們關注的重點。搭建網絡時的超參數有:網絡的層數、各隱藏層節點數、各節點激活函數、內部參數的初始值等。訓練網絡的超參數有:如損失函數、學習率、優化算法、batch_size、epochs 等。
- 激活函數
PyTorch 1.12.0 版本進入 https://pytorch.org/docs/1.12/nn.html 搜索 Non-linear Activations,即可查看 torch 內置的所有非線性激活函數(以及各種類型的層)。(網站打開默認為1.12版本,如果你的torch不是1.12,請在網頁左上角自行更改) - 損失函數
進入 https://pytorch.org/docs/1.12/nn.html 搜索 Loss Functions,即可查看 torch
內置的所有損失函數。
# 損失函數的選擇
loss_fn = nn.MSELoss()
- 學習率與優化算法
進入 https://pytorch.org/docs/1.12/optim.html,可查看 torch 的所有優化算法(網站打開默認為1.12版本,如果你的torch不是1.12,請在網頁左上角自行更改)
# 優化算法的選擇
learning_rate = 0.01 # 設置學習率
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
注:PyTorch 實現時只支持 BGD 或 MBGD,不支持單個樣本的輸入方式。這里的 torch.optim.SGD 只表示梯度下降,具體的批量與小批量見第四、五章。
訓練網絡
# 訓練網絡
epochs = 1000 # 所有樣本輪回1000次
losses = [] # 記錄損失函數變化的列表
# 給訓練集劃分輸入與輸出
X = train_Data[ : , :3 ] # 前 3 列為輸入特征
Y = train_Data[ : , -3: ] # 后 3 列為輸出特征
for epoch in range(epochs):Pred = model(X) # 一次前向傳播(批量)loss = loss_fn(Pred, Y) # 計算損失函數losses.append(loss.item()) # 記錄損失函數的變化optimizer.zero_grad() # 清理上一輪滯留的梯度loss.backward() # 一次反向傳播optimizer.step() # 優化內部參數Fig = plt.figure()
plt.plot(range(epochs), losses)
plt.ylabel('loss'), plt.xlabel('epoch')
plt.show()
PS:losses.append(loss.item())中,.append()是指在列表 losses 后再附加 1 個元素,而.item()方法可將 PyTorch 張量退化為普通元素。
測試網絡
測試時,只需讓測試集進行 1 次前向傳播即可,這個過程不需要計算梯度,因此可以在該局部關閉梯度,該操作使用 with torch.no_grad():命令。
考慮到輸出特征是獨熱編碼,而預測的數據一般都是接近 0 或 1 的小數,為了能讓預測數據與真實數據之間進行比較,因此要對預測數據進行規整。例如,使用 Pred[:,torch.argmax(Pred, axis=1)] = 1 命令將每行最大的數置 1,接著再使用Pred[Pred!=1] = 0 將不是 1 的數字置 0,這就使預測數據與真實數據的格式相同。
# 測試網絡
# 給測試集劃分輸入與輸出
X = test_Data[:, :3] # 前 3 列為輸入特征
Y = test_Data[:, -3:] # 后 3 列為輸出特征
with torch.no_grad(): # 該局部關閉梯度計算功能Pred = model(X) # 一次前向傳播(批量)Pred[:,torch.argmax(Pred, axis=1)] = 1Pred[Pred!=1] = 0correct = torch.sum( (Pred == Y).all(1) ) # 預測正確的樣本total = Y.size(0) # 全部的樣本數量print(f'測試集精準度: {100*correct/total} %')
在計算 correct 時需要動點腦筋。
首先,(Pred == Y)計算預測的輸出與真實的輸出的各個元素是否相等,返回一個 3000 行、3 列的布爾型張量;
其次,(Pred == Y).all(1)檢驗該布爾型張量每一行的 3 個數據是否都是 True,對于全是 True 的樣本行,結果就是 True,否則是 False。all(1)中的 1 表示按“行”掃描,最終返回一個形狀為 3000 的一階張量。
最后,torch.sum( (Pred == Y).all(1) )的意思就是看這 3000 個向量相加,True會被當作 1,False 會被當作 0,這樣相加剛好就是預測正確的樣本數。
保存與導入網絡
現在我們要考慮一件大事,那就是有時候訓練一個大網絡需要幾天,那么必須要把整個網絡連同里面的優化好的內部參數給保存下來。
現以本章前面的代碼為例,當網絡訓練好后,將網絡以文件的形式保存下來,并通過文件導入給另一個新網絡,讓新網絡去跑測試集,看看測試集的準確率是否也是 67%。
- 保存網絡
通過“torch.save(模型名, ‘文件名.pth’)”命令,可將該模型完整的保存至Jupyter 的工作路徑下
# 保存網絡
torch.save(model, 'model.pth')
- 導入網絡
通過“新網絡 = torch.load('文件名.pth ')”命令,可將該模型完整的導入給新網絡。
# 把模型賦給新網絡
new_model = torch.load('model.pth')
現在,new_model 就與 model 完全一致,可以直接去跑測試集。
- 用新模型進行測試
# 測試網絡
# 給測試集劃分輸入與輸出
X = test_Data[:, :3] # 前 3 列為輸入特征
Y = test_Data[:, -3:] # 后 3 列為輸出特征
with torch.no_grad(): # 該局部關閉梯度計算功能Pred = new_model(X) # 用新模型進行一次前向傳播Pred[:,torch.argmax(Pred, axis=1)] = 1Pred[Pred!=1] = 0correct = torch.sum( (Pred == Y).all(1) ) # 預測正確的樣本total = Y.size(0) # 全部的樣本數量print(f'測試集精準度: {100*correct/total} %') #OUT:測試集精準度: 67.16666412353516 %
批量梯度下降
本小節將完整、快速地再展示一遍批量梯度下降(BGD)的全過程。
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
%matplotlib inline# 展示高清圖
from matplotlib_inline import backend_inline
backend_inline.set_matplotlib_formats('svg')
制作數據集
這一次的數據集將從 Excel 中導入,需要 Pandas 庫中的 pd.read_csv()函數,這在前一篇文章《Python基礎——Pandas庫》第六章中有詳細的介紹
# 準備數據集
df = pd.read_csv('Data.csv', index_col=0) # 導入數據
arr = df.values # Pandas 對象退化為 NumPy 數組
arr = arr.astype(np.float32) # 轉為 float32 類型數組
ts = torch.tensor(arr) # 數組轉為張量
ts = ts.to('cuda') # 把訓練集搬到 cuda 上
ts.shape #OUT:torch.Size([759, 9])
PS:將 arr 數組轉為了 np.float32 類型這一步必不可少,沒有的話計算過程會出現一些數據類型不兼容的情況。
# 劃分訓練集與測試集
train_size = int(len(ts) * 0.7) # 訓練集的樣本數量
test_size = len(ts) - train_size # 測試集的樣本數量
ts = ts[ torch.randperm( ts.size(0) ) , : ] # 打亂樣本的順序
train_Data = ts[ : train_size , : ] # 訓練集樣本
test_Data = ts[ train_size : , : ] # 測試集樣本
train_Data.shape, test_Data.shape #OUT:(torch.Size([531, 9]), torch.Size([228, 9]))
搭建神經網絡
PS:前面的數據集,輸入有 8 個特征,輸出有 1 個特征,那么神經網絡的輸入層必須有 8 個神經元,輸出層必須有 1 個神經元。
隱藏層的層數、各隱藏層的節點數屬于外部參數(超參數),可以自行設置。
class DNN(nn.Module):def __init__(self):''' 搭建神經網絡各層 '''super(DNN,self).__init__()self.net = nn.Sequential( # 按順序搭建各層nn.Linear(8, 32), nn.Sigmoid(), # 第 1 層:全連接層nn.Linear(32, 8), nn.Sigmoid(), # 第 2 層:全連接層nn.Linear(8, 4), nn.Sigmoid(), # 第 3 層:全連接層nn.Linear(4, 1), nn.Sigmoid() # 第 4 層:全連接層)def forward(self, x):''' 前向傳播 '''y = self.net(x) # x 即輸入數據return y # y 即輸出數據
model = DNN().to('cuda:0') # 創建子類的實例,并搬到 GPU 上
訓練網絡
# 損失函數的選擇
loss_fn = nn.BCELoss(reduction='mean')
# 優化算法的選擇
learning_rate = 0.005 # 設置學習率
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 訓練網絡
epochs = 5000
losses = [] # 記錄損失函數變化的列表
# 給訓練集劃分輸入與輸出
X = train_Data[ : , : -1 ] # 前 8 列為輸入特征
Y = train_Data[ : , -1 ].reshape((-1,1)) # 后 1 列為輸出特征
# 此處的.reshape((-1,1))將一階張量升級為二階張量
for epoch in range(epochs):Pred = model(X) # 一次前向傳播(批量)loss = loss_fn(Pred, Y) # 計算損失函數losses.append(loss.item()) # 記錄損失函數的變化optimizer.zero_grad() # 清理上一輪滯留的梯度loss.backward() # 一次反向傳播optimizer.step() # 優化內部參數Fig = plt.figure()
plt.plot(range(epochs), losses)
plt.ylabel('loss')
plt.xlabel('epoch')
plt.show()
測試網絡
注意,真實的輸出特征都是 0 或 1,因此這里需要對網絡預測的輸出 Pred 進行處理,Pred 大于 0.5 的部分全部置 1,小于 0.5 的部分全部置 0.
# 測試網絡
# 給測試集劃分輸入與輸出
X = test_Data[ : , : -1 ] # 前 8 列為輸入特征
Y = test_Data[ : , -1 ].reshape((-1,1)) # 后 1 列為輸出特征
with torch.no_grad(): # 該局部關閉梯度計算功能Pred = model(X) # 一次前向傳播(批量)Pred[Pred>=0.5] = 1Pred[Pred<0.5] = 0correct = torch.sum( (Pred == Y).all(1) ) # 預測正確的樣本total = Y.size(0) # 全部的樣本數量print(f'測試集精準度: {100*correct/total} %') #OUT:測試集精準度: 71.0526351928711 %
小批量梯度下降
本章將繼續使用第四章中的 Excel 與神經網絡結構,但使用小批量訓練。在使用小批量梯度下降時,必須使用 3 個 PyTorch 內置的實用工具(utils):
- DataSet 用于封裝數據集
- DataLoader 用于加載數據不同的批次
- random_split 用于劃分訓練集與測試集
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torch.utils.data import random_split
import matplotlib.pyplot as plt
%matplotlib inline
# 展示高清圖
from matplotlib_inline import backend_inline
backend_inline.set_matplotlib_formats('svg')
制作數據集
在封裝我們的數據集時,必須繼承實用工具(utils)中的 DataSet 的類,這個過程需要重寫__init__、getitem、__len__三個方法,分別是為了加載數據集、獲取數據索引、獲取數據總量。
# 制作數據集
class MyData(Dataset): # 繼承 Dataset 類def __init__(self, filepath):df = pd.read_csv(filepath, index_col=0) # 導入數據arr = df.values # 對象退化為數組arr = arr.astype(np.float32) # 轉為 float32 類型數組ts = torch.tensor(arr) # 數組轉為張量ts = ts.to('cuda') # 把訓練集搬到 cuda 上self.X = ts[ : , : -1 ] # 前 8 列為輸入特征self.Y = ts[ : , -1 ].reshape((-1,1)) # 后 1 列為輸出特征self.len = ts.shape[0] # 樣本的總數def __getitem__(self, index):return self.X[index], self.Y[index]def __len__(self):return self.len
小批次訓練時,輸入特征與輸出特征的劃分必須寫在上述代碼的子類里面。
# 劃分訓練集與測試集
Data = MyData('Data.csv')
train_size = int(len(Data) * 0.7) # 訓練集的樣本數量
test_size = len(Data) - train_size # 測試集的樣本數量
train_Data, test_Data = random_split(Data, [train_size, test_size])
我們利用實用工具(utils)里的 random_split可輕松實現了訓練集與測試集數據的劃分
# 批次加載器
train_loader = DataLoader(dataset=train_Data, shuffle=True, batch_size=128)
test_loader = DataLoader(dataset=test_Data, shuffle=False, batch_size=64)
實用工具(utils)里的 DataLoader 可以在接下來的訓練中進行小批次的載入數據,shuffle 用于在每一個 epoch 內先洗牌再分批。
搭建神經網絡
class DNN(nn.Module):def __init__(self):''' 搭建神經網絡各層 '''super(DNN,self).__init__()self.net = nn.Sequential( # 按順序搭建各層nn.Linear(8, 32), nn.Sigmoid(), # 第 1 層:全連接層nn.Linear(32, 8), nn.Sigmoid(), # 第 2 層:全連接層nn.Linear(8, 4), nn.Sigmoid(), # 第 3 層:全連接層nn.Linear(4, 1), nn.Sigmoid() # 第 4 層:全連接層)def forward(self, x):''' 前向傳播 '''y = self.net(x) # x 即輸入數據return y # y 即輸出數據
model = DNN().to('cuda:0') # 創建子類的實例,并搬到 GPU 上
訓練網絡
# 損失函數的選擇
loss_fn = nn.BCELoss(reduction='mean')
# 優化算法的選擇
learning_rate = 0.005 # 設置學習率
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 訓練網絡
epochs = 500
losses = [] # 記錄損失函數變化的列表
for epoch in range(epochs):for (x, y) in train_loader: # 獲取小批次的 x 與 yPred = model(x) # 一次前向傳播(小批量)loss = loss_fn(Pred, y) # 計算損失函數losses.append(loss.item()) # 記錄損失函數的變化optimizer.zero_grad() # 清理上一輪滯留的梯度loss.backward() # 一次反向傳播optimizer.step() # 優化內部參數
Fig = plt.figure()
plt.plot(range(len(losses)), losses)
plt.show()
測試網絡
# 測試網絡
correct = 0
total = 0
with torch.no_grad(): # 該局部關閉梯度計算功能for (x, y) in test_loader: # 獲取小批次的 x 與 yPred = model(x) # 一次前向傳播(小批量)Pred[Pred>=0.5] = 1Pred[Pred<0.5] = 0correct += torch.sum( (Pred == y).all(1) )total += y.size(0)
print(f'測試集精準度: {100*correct/total} %') #OUT:測試集精準度: 73.68421173095703 %
手寫數字識別
手寫數字識別數據集(MNIST)是機器學習領域的標準數據集,它被稱為機器學習領域的“Hello World”,只因任何 AI 算法都可以用此標準數據集進行檢驗。MNIST 內的每一個樣本都是一副二維的灰度圖像,如下圖所示。
- 在 MNIST 中,模型的輸入是一副圖像,模型的輸出就是一個與圖像中對應的數字(0 至 9 之間的一個整數,不是獨熱編碼)。
- 我們不用手動將輸出轉換為獨熱編碼,PyTorch 會在整個過程中自動將數據集的輸出轉換為獨熱編碼.只有在最后測試網絡時,我們對比測試集的預測輸出與真實輸出時,才需要注意一下。
- 某一個具體的樣本如下圖所示,每個圖像都是形狀為28*28的二維數組。
在這種多分類問題中,神經網絡的輸出層需要一個 softmax 激活函數,它可以把輸出層的數據歸一化到 0-1 上,且加起來為 1,這樣就模擬出了概率的意思。
制作數據集
這一章我們需要在 torchvision 庫中分別下載訓練集與測試集,因此需要從torchvision 庫中導入 datasets 以下載數據集,下載前需要借助 torchvision 庫中的 transforms 進行圖像轉換,將數據集變為張量,并調整數據集的統計分布。
由于不需要手動構建數據集,因此不導入 utils 中的 Dataset;又由于訓練集與測試集是分開下載的,因此不導入 utils 中的 random_split。
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
import matplotlib.pyplot as plt
%matplotlib inline
# 展示高清圖
from matplotlib_inline import backend_inline
backend_inline.set_matplotlib_formats('svg')
在下載數據集之前,要設定轉換參數:transform,該參數里解決兩個問題:
- ToTensor:將圖像數據轉為張量,且調整三個維度的順序為 CWH;C表示通道數,二維灰度圖像的通道數為 1,三維 RGB 彩圖的通道數為 3。
- Normalize:將神經網絡的輸入數據轉化為標準正態分布,訓練更好;根據統計計算,MNIST 訓練集所有像素的均值是 0.1307、標準差是 0.3081。
# 制作數據集
# 數據集轉換參數
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(0.1307, 0.3081)
])
# 下載訓練集與測試集
train_Data = datasets.MNIST(root = 'D:/Jupyter/dataset/mnist/', # 下載路徑train = True, # 是 train 集download = True, # 如果該路徑沒有該數據集,就下載transform = transform # 數據集轉換參數
)
test_Data = datasets.MNIST(root = 'D:/Jupyter/dataset/mnist/', # 下載路徑train = False, # 是 test 集download = True, # 如果該路徑沒有該數據集,就下載transform = transform # 數據集轉換參數
)
下載輸出下圖所示:
# 批次加載器
train_loader = DataLoader(train_Data, shuffle=True, batch_size=64)
test_loader = DataLoader(test_Data, shuffle=False, batch_size=64)
搭建神經網絡
每個樣本的輸入都是形狀為2828的二維數組,那么對于 DNN 來說,輸入層的神經元節點就要有2828=784個;輸出層使用獨熱編碼,需要 10 個節點。
class DNN(nn.Module):def __init__(self):''' 搭建神經網絡各層 '''super(DNN,self).__init__()self.net = nn.Sequential( # 按順序搭建各層nn.Flatten(), # 把圖像鋪平成一維nn.Linear(784, 512), nn.ReLU(), # 第 1 層:全連接層nn.Linear(512, 256), nn.ReLU(), # 第 2 層:全連接層nn.Linear(256, 128), nn.ReLU(), # 第 3 層:全連接層nn.Linear(128, 64), nn.ReLU(), # 第 4 層:全連接層nn.Linear(64, 10) # 第 5 層:全連接層)def forward(self, x):''' 前向傳播 '''y = self.net(x) # x 即輸入數據return y # y 即輸出數據
model = DNN().to('cuda:0') # 創建子類的實例,并搬到 GPU 上
訓練網絡
# 損失函數的選擇
loss_fn = nn.CrossEntropyLoss() # 自帶 softmax 激活函數
# 優化算法的選擇
learning_rate = 0.01 # 設置學習率
optimizer = torch.optim.SGD(model.parameters(),lr = learning_rate,momentum = 0.5
)
給優化器了一個新參數 momentum(動量),它使梯度下降算法有了力與慣性,該方法給人的感覺就像是小球在地面上滾動一樣。
# 訓練網絡
epochs = 5
losses = [] # 記錄損失函數變化的列表
for epoch in range(epochs):for (x, y) in train_loader: # 獲取小批次的 x 與 yx, y = x.to('cuda:0'), y.to('cuda:0')Pred = model(x) # 一次前向傳播(小批量)loss = loss_fn(Pred, y) # 計算損失函數losses.append(loss.item()) # 記錄損失函數的變化optimizer.zero_grad() # 清理上一輪滯留的梯度loss.backward() # 一次反向傳播optimizer.step() # 優化內部參數
Fig = plt.figure()
plt.plot(range(len(losses)), losses)
plt.show()
PS:由于數據集內部進不去,只能在循環的過程中取出一部分樣本,就立即將之搬到 GPU 上。
測試網絡
# 測試網絡
correct = 0
total = 0
with torch.no_grad(): # 該局部關閉梯度計算功能for (x, y) in test_loader: # 獲取小批次的 x 與 yx, y = x.to('cuda:0'), y.to('cuda:0')Pred = model(x) # 一次前向傳播(小批量)_, predicted = torch.max(Pred.data, dim=1)correct += torch.sum( (predicted == y) )total += y.size(0)
print(f'測試集精準度: {100*correct/total} %') #OUT:測試集精準度: 96.65999603271484 %
a, b = torch.max(Pred.data, dim=1)的意思是,找出 Pred 每一行里的最大值,數值賦給 a,所處位置賦給 b。因此上述代碼里的 predicted 就相當于把獨熱編碼轉換回了普通的阿拉伯數字,這樣一來可以直接與 y 進行比較。
由于此處 predicted 與 y 是一階張量,因此 correct 行的結尾不能加.all(1)。