相較於scikit-learn提供了常用的機器學習、深度學習模型,開發者只要略為理解模型的組成原理,知道如何調整模型的超參數(與模型本身權重、偏差等無關的參數,像是學習率、損失函數之類的參數)就可以使用,在程式庫的定位上,PyTorch經常被視為機器學習、深度學習框架,這意謂著PyTorch的使用者,必須掌握模型組成的更多細節,以及學習的流程。

畢竟,PyTorch是個「框架」,這也表示,它實現了某些主要流程,開發者如果能掌握、認同、遵循其流程典範,才能明瞭PyTorch是否合乎其需求,而不是淪於API的盲目組合,或是與流程典範對抗,而認識PyTorch流程典範最好的方式,就是在沒有使用PyTorch的情況下,試著自行實作出機器學習的處理流程。

機器學習的流程

當然,有不少的文件或書籍可提供這方面的知識,只不過實作過程中,切分出來的各個子流程各不相同。

若是以PyTorch的流程典範來看,可以特意地畫分出四個主要的部份:模型的建立、損失函數的組成、梯度的計算、模型參數的更新。

機器學習所謂的模型,其實就是一個(複雜的)數學函數,函數中的參數值一開始可能是隨意或特意指定,資料的輸入函數後得到的輸出,與真正的值之間會有誤差,誤差會視需求以均方或其他方式,計算出損失值,目的是組合出損失函數。

學習過程的目標就是調整函數中的參數值,令損失函數求得的值可以達到一個最小值。而為了這個目的,必須求得損失函數的梯度,以便知道參數調整的方向──因為梯度可以告訴你,在損失函數的形狀上,哪個方向會是低谷;知道了參數調整的方向,就可以一小步、一小步地向低谷前進,直到接近或抵達損失函數的谷底;一小步、一小步地向低谷前進,意謂著需要一個訓練迴圈,迴圈中每消耗全部的學習資料一次,稱為一個epoch。

NumPy實現線性迴歸

如果開發者有足夠的能力,完全不使用程式庫來實現以上流程是最好的,不過,藉助NumPy可以省些力氣,而且PyTorch的核心資料結構是張量(Tensor),在資料的表現與API外觀上,與NumPy陣列有許多類似之處,NumPy陣列與PyTorch張量彼此之間,也可以互相轉換,甚至共享底層的資料。

雖然PyTorch經常用於深度學習,然而,就理解其主要流程而言,並不需要從複雜的神經網路開始,而是試著實作簡單的線性迴歸模型w*x+b,就可以將流程展現出來,損失使用誤差的均方計算即可。

問題在於梯度的計算部份,由於線性迴歸模型就是個很簡單的數學函數,直接對其損失函數進行各參數的偏微分並不困難,程式實作上也簡單,許多文件或書籍會直接採用此方式求梯度,就像〈NumPy實現線性迴歸(一)〉所言,有個gist裡面的grad_loss函式實作。

不過,如先前專欄〈梯度的局部計算性〉談到的,在神經網路中,透過反向傳播,可以更有效率地求得梯度,也更容易組合各個運算。

雖然對於線性迴歸來說,如此有一點小題大作,然而,確實也能透過反向傳播的方式來求梯度,就像〈NumPy實現線性迴歸(二)〉,當中的grad_loss函式實作,其中分別對每個計算節點求得偏微分公式,然後組成了反向傳播的梯度求解過程。

NumPy實現線性迴歸(一)〉與〈NumPy實現線性迴歸(二)〉的差別,就僅在於grad_loss函式的實作,對損失函數進行偏微分,以及反向傳播,兩者都可用來求得梯度。而我建議採用反向傳播,原因在於,過渡到PyTorch實作時,就可以透過其提供的張量直接進行反向傳播。

逐一轉換至PyTorch

想將NumPy陣列轉至PyTorch張量,我們可以透過torch.from_numpy函式,而像是numpy.where之類的函式,基本上,都有torch.where等API對應,用來建立NumPy陣列的numpy.array,可以改為torch.tensor建立張量,〈NumPy API至PyTorch API〉可以看到例子,幾個簡單的置換動作,就可以從NumPy API轉換至PyTorch API。

而PyTorch張量的神奇之處在於,只要記得建立張量時,將requires_grad設為True,它可以記得自己是怎麼計算而來,而透過backward方法,可以自動求得梯度,因此,就不需要先前的grad_loss實作了,我們在〈PyTorch反向傳播求梯度〉可以看到相關實作,其中,要注意的是,梯度計算預設會被累加,因此,要記得將梯度計算歸零。

另外在更新模型參數時,為了避免修改了模型的計算圖,要在with torch.no_grad()的環境中進行參數更新,記得使用-=運算子,這才會對參數進行原地(in-place)更新操作。如果寫為params=params-lr*params.grad,會建立新張量而預設的requires_grad是False,這會導致無法計算梯度,而在backward方法執行時發生錯誤。

要注意的是,requires_grad設為True的張量,若要轉換至NumPy(像是提供給Matplolib繪圖使用),記得要先detch,解除自動梯度計算的功能,才能透過numpy方法轉為NumPy陣列。

實際上,更新參數的部份params-=lr*params.grad,就是梯度下降法最後求得的參數更新公式,PyTorch提供了torch.optim.SGD,來實現梯度下降法,清除梯度的部份可以透過optimizer.zero_grad(),更新參數的部份可以透過optimizer.step(),關於這部分做法,我們可以在〈使用Optimizer〉中看到如何修改。

損失函數的部份,我們可以使用PyTorch的torch.nn.MSELoss,而原先的model函式,就是個線性模型,PyTorch提供了torch.nn.Linear作為實現,建立時必須指定輸入與輸出的維度,我們可以藉由parameters方法取得模型參數。而在〈PyTorch求線性迴歸〉當中,我們可以看到:線性模型、損失函數、梯度計算、梯度下降等,都使用了PyTorch現成的方案,不需要自行實作相關元件,只要關注學習流程的實現就可以了。

掌握PyTorch流程典範

雖然是個簡單的模型,然而〈PyTorch求線性迴歸〉示範了PyTorch主要的幾個元素,藉由NumPy到PyTorch的過程,我們可以逐漸體會到使用現成實作的方便之處;對於神經網路等更複雜的模型,主要就是小模型與小模型間計算如何連接,這可以透過torch.nn.Sequential來串聯。

從NumPy到PyTorch的實現過程中,我們是否也發現了training_loop的流程幾乎都是固定的呢?你可以繼承nn.Module來實作神經層,定義好forward、backward方法,然後其他流程的接合就交給PyTorch。

如果能夠自行透過NumPy實作一個簡單的模型,代表我們對於數學理論或學習過程等有足夠的認識,若試著將其中的元素逐一用PyTorch取代,就可以清楚地知道PyTorch提供了哪些方便的元件,同時,也可以瞭解到PyTorch支援的流程典範,從這個流程典範延伸來探索其他API,就不至於迷失在許多API之中了。

專欄作者

熱門新聞

Advertisement