2024年4月2日 星期二

深度學習框架 DeZero 學習筆記 (一) : 變數與函數

昨天從母校圖書館借到下面這本齋藤康毅的好書 :



Source : 博客來


這本書上回借時看完前兩章, 這次我打算邊看邊測編寫筆記, 這樣才能紮實地吸收這本書的精華. 這本書的目標是只使用少數必要函式庫 (Numpy & Matplotlib) 從零打造精簡版的深度學習框架 DeZero, 與 PyTorch, TensorFlow 一樣具有 Define-by-Run 功能 (定義好運算圖連結即可執行), 不僅可在 Colab 雲端跑, 也可以在手機上跑. 

DeZero 已在 PyPi 上架, 如果要直接使用 DeZero 可用 pip install 安裝 (先不用) :

pip install dezero  

打造 DeZero 需要用到 Numpy 與 Matplotlib : 

pip install numpy  
pip install  matplotlib  

此書將打造 DeZero 框架的 60 個步驟分成五個階段 (一章就是一個步驟) :
  • 步驟 1~10 : 建立 DeZero 的基礎 (自動微分)
  • 步驟 11~24  : 使用 Pythonized 語法撰寫 DeZero
  • 步驟 25~36 : 計算二階微分以進行反向傳播
  • 步驟 37~51 : 用 DeZero 建立類神經網路
  • 步驟 52~60 : 用 DeZero 搭建 CNN 與 RNN 模型 
總之, 透過這種徒手打造深度學習框架的過程, 可以深刻理解深度學習的原理與框架結構. 

以下是 Ch1~2 關於建立變數與函數的筆記. 


1. 建立變數 : 

變數就是資料的容器 (箱子), 在 DeZero 中使用名為 Variable 的類別來作為存放變數的容器 :

class Variable:
    def __init__(self, data):
        self.data=data

這是標準的 Python 類別用法, 類別名稱首字元大寫符合 Python PEP8 原則, 函式 __init__() 用來對物件 (類別的實體) 進行初始化, 傳入參數 self 代表物件本身, 後面跟著的是使用者參數, 此處 data 為一個 Numpy 陣列 (ndarray 物件), 所以一個 Variable 物件就是用來儲存 ndarray 物件用的容器 :

>>> class Variable:  
    def __init__(self, data):  
        self.data=data  
        
先建立一個 Numpy 陣列 : 

>>> import numpy as np    
>>> data=np.array(1.0)  
>>> data   
array(1.0)    
>>> type(data)     
<class 'numpy.ndarray'>   

將陣列傳給 Variable() 建構式, 它會呼叫 __init()__ 初始化物件 : 

>>> x=Variable(data)  
>>> x.data   
array(1.0) 
 
可見 Numpy 陣列 data 已存入 x 物件的 data 屬性中.

 
1. 建立函數 : 

在數學上, 函數用來描述兩組變數之間的對應關係, 在 DeZero 框架裡, 則是定義一個名為 Function 的類別來實作函數, 它會定義一個 __call__() 函式, 接受一個 Variable 物件變數輸入, 然後將演算結果 (另一個 Variable 變數) 傳回作為輸出, 例如下面的平方函數 :

class Function:
    def __call__(self, input):
        x=input.data
        y=x ** 2
        output=Variable(y)
        return output

__call__() 為 Python 特殊函式, 定義此方法的類別可以把它的實體名稱當作函式直接呼叫, 例如 : 

>>> x=Variable(10)    # 建立 Variable 變數物件
>>> f=Function()        # 建立 Function 函數物件
>>> type(f)                  
<class '__main__.Function'>   
>>> y=f(x)                   # 呼叫函式物件並傳入資料 
>>> type(y)                 # 傳回 Variable 物件
<class '__main__.Variable'>
>>> y.data                   
100
>>> 

不過上面這個 Function 類別被固定做平方運算, 應該將特定運算 (例如平方) 交給 Function 的子類別 Square 的函式 forward() 來執行, 這樣 Function 就可泛化為可執行任何函數的類別, 改寫如下 : 

class Function:
    def __call__(self, input):
        x=input.data
        y=self.forward(x) 
        output=Variable(y)
        return output
    def forward(self, x):   # 讓子類別覆蓋的空方法
        raise NotImplementedError()   # 預防子類別沒有實作此方法時出現例外

class Square(Function):   # 繼承 Function 並覆蓋其 forward() 方法
    def forward(self, x):
        return x ** 2

此處將計算函數交由 Function 的子類別 Square 的方法 forward() 來做, 在 Function 類別則添加空的 forward() 方法 (讓子類別繼承時覆蓋掉), 然後修改 __call__ 裡的 輸出 y, 改為呼叫 forward() 來求值,  :

>>> x=Variable(10)   
>>> f=Square()         # 建立函數的類別
>>> y=f(x)                 # 建立函數類別的實體並傳入 x 求值
>>> type(f)   
<class '__main__.Square'>     # f 是 Square 物件
>>> type(y)      
<class '__main__.Variable'>   # f(x) 傳回值為 Variable 物件 
>>> y.data     
100

結果相同, 但此處的 Function 比上面更一般化了, 我們可以定義任何函數類別掛接到 Function 來求值, 例如求立方的函數類別 : 

>>> class Cube(Function):   # 繼承 Function 並覆蓋其 forward() 方法
    def forward(self, x):    
        return x ** 3   
    
>>> x=Variable(10)   
>>> f=Cube()   
>>> y=f(x)    
>>> y.data    
1000 

沒有留言 :