Complex-value Convolutional Neural Network with KERAS
文章開始前,首先感謝 臺師大光電所 鄭超仁老師、逢甲大學 林立謙老師的指導,也感謝碩班期間和我一起研究的黃彥傑學長、王炫棨同學、劉吉富同學,一同開發出 Complex-Valued Convolutional Neural Network 相關程式。
本文閱讀須知
本文主要目的是分享一些開發經驗,將當時的思考方式跟做法提供各位參考,所以不會去講很難的數學模型、理論推導等讓人摸不著頭緒的東西,盡可能以最簡單的方式進行說明,快速了解一些基本概念後,能夠找到開發程式的方向。
在正文開始前,我會簡單說明深度學習的概念,接著說明整個實驗的動機與背景,也會去說明基本的深度學習觀念。整體內容多是自身觀點去呈現,或許有不少地方說明較模糊,甚至錯誤,還請多包涵,可以的話希望有有人能幫忙校對整個文章的正確性,或是補充一些我沒講到的部份,但還是建議有疑慮直接去看參考文獻的論文,相信能更加深自己的理解。
快速補充深度學習的基本知識
說明何謂是深度學習、如何進行訓練、該用什麼資料去訓練、深度學習的優勢等等觀念。
AI vs ML vs DL
人工智慧 (Artificial Intelligence, AI) 不管在過去還是現在都是非常熱門的主題,其中一項技術是被稱作「機器學習 (Machine Learning, ML」的演算法,此算法通過「資料 (Data)」去歸納出資料「特性 (feature)」,並用這個特性來預測新的資料,到了近幾年「深度學習 (Deep Learning, DL)」這門新的機器學習演算法出現之後,大幅提升在影像、聲音等複雜資料中,找尋特徵的能力。
機器學習與深度學習的差別。
本文就是基於深度學習的理論來開發「複數形式的深度神經網路」,因此整個學習演算法的架構就是依照深度學習的方式來進行。
訓練流程
通常,要訓練一個深度學習模型需要資料、定義模型架構、設定模型權重 (weights)、損失函數 (loss function)、優化演算法 (optimizer) 以及評估函數 (metrics function),整個訓練流程可以參考下圖。
Note: (1) 權重又可以稱做模型參數,權重可以說是影響模型效能好壞最關鍵的元素。 (2) 損失函數用來計算預測結果與標記資料之間的誤差,目標就是讓這個誤差縮小。 (3) 最基本的優化演算法是隨機梯度下降法。
深部學習的訓練流程圖。
圖片取自:IOPLAB & SIPLAB @高揚傑。
從圖中可以看到,我們就是通過不斷在資料中找尋資料的特徵,並運用損失函數來計算誤差,這個誤差將會作為優化權重的基準,重複這個過程直到評估函數的指標達到飽和,數值沒有太大變化時就可以停止。
何謂訓練
而常常聽到的訓練,其實就是優化權重。只要加強權重對特徵的響應,就可以讓機器去辨識資料的特徵。所謂的訓練資料,是指可以通過電腦進行數值分析的資料,只要資料能夠被量化,就有機會進行機器學習的應用。
特徵擷取的過程。
圖片取自論文:L. Xie, T. Ahmad, L. Jin, Y. Liu, S..X Zhang, IEEE TITS, 19(2), p.507-517, 2018
何謂資料
機器學習演算法並沒有限定哪種資料才能訓練,只要能轉成數值,就算是抽象的情感、感覺說不定也有辦法給機器訓練,而目前常見的資料有文字、影像、聲音等等,這些資料是比較容易進行數值量化分析,以影像為例:
一張大小為 28 x 28 ,數字為 5 的灰階影像,可以視為 28 x 28 的矩陣 M,每一個像素 (pixel) 正好對應一個矩陣 M 上的元素,一般影像感測器紀錄的強度資訊是 0 ~ 255 的整數,資料型態為 uint8。上述對於影像的描述,可以發現影像的記錄方式就是採用數值化的方式。
MNIST 的數字 5 及對應的像素值。
對於人類來說,一眼就能明白這張影像是數字 5;但對機器來說不過就是一格一格存在二維陣列中,並無法理解每一格像素都是有關聯性。而讓機器可以自行分析出這些像素的關聯性及重要特徵,正是深度學習最重要的貢獻之一。
正文開始
本文會學到
- 了解 Complex-Valued Convolutional Neural Network
- 通過 KERAS API 寫出 Complex-Valued Layer
- 建構 Complex-Valued Convolutional Neural Network
- 輸入 Complex-Valued Data 來訓練模型
什麼是複數神經網路?
近年出現越來越多 DL 模型,相信會看這篇文章的人,對於 CNN、RNN、GAN 以及物件偵測等專有名詞特別有印象,但這篇文章並不是去討論這些常見模型,而是有別於一般模型的「複數卷積神經網路 (Complex-Valued Convolutional Neural Network)」。
早在 1990 年代就有專家學者嘗試建構「複數神經網路 (Complex Valued ANN)」,因為在某些領域中,複數的資料更能表達資料特性,例如上一節提到的數位全像術,利用傅立葉級數可以更好的表示光傳遞的過程,通過傅立葉轉換,我們可以重建出更完整的光資訊,而這些資訊都是複數形式的資料。
目前深度學習在機器視覺中,展現無與倫比的強大能力,而實現這個技術的正是大名鼎鼎的「卷積神經網路 (Convolutional Neural Network)」,也就是俗稱的「CNN」,而本篇所說的複數卷積神經網路就是基於 CNN 來實現。接下來我會從複數神經網路的源頭 — 複數來進行說明,也會說明這些資料是如何產生的。
快速認識複數
在講複數深度神經網路之前,先來回憶一下什麼是複數!
相信各位都看過 z = a + bi
這個式子,其中:
a
代表實部 (Real Part)。b
代表虛部 (Imaginary Part)。i
表示成\sqrt{-1}
。a
、b
都是實數 (Real Number)。
上面的式子就是「複數 (Complex Value)」,也是應用於本文的資料形式。
複數的另一個表示法 — 極座標
我們可以通過 Euler\’s Formula 將 z = a + bi
這個式子,變成 z = r \cdot e^{i \theta}
。
z = r \cdot e^{i \theta}
我們稱為極座標表示,其中 r
表示為複數的長度,\theta
表示複數角度。
Euler\’s Formula 的數學式:
e^{i \theta} = cos \theta + i \cdot sin \theta
上面兩個式子可以畫成下面這張圖,稱為「複數平面」。這圖解釋了複數的平面座標和極座標的關係。
平面座標與極座標轉換
圖片取自:IOPLAB & SIPLAB @高揚傑
為何是複數形式的資料?
還記得前面說過機器學習是通過資料訓練,藉此優化模型權重,因此資料將會是影響模型訓練好壞至關重要的主角,若是輸入的資料可以更清楚描述資料的特徵,就有機會提高模型準確度。
一般深度學習所用的資料都是實數,例如常見的影像分類都是採用灰階影像或是 RGB 的彩色影像,但是許多數位信號可能不會單單是實數,而是複數形式,例如:正弦波、交流電、電磁波、光波、聲波等訊號都會是複數,只是一般為了計算方便我們只採用振幅來計算,而不考慮波的相位角度。若是今天我們的訊號或是資料,它的特性是反映在相位呢?
用於三維量測的數位全像術
以光波干涉為例,當兩道具高度相關的光波彙整在一起時,會形成亮暗相間的干涉條紋。假設其中一道光穿過樣本,這時所折射出來的光已經發生相位變化,而兩道光之間的差,稱之為「相位差」,相位差將會造成條紋產生形變,通過鄭超仁老師實驗室的「數位全像繞射重建演算法」去計算這個相位變化並重建出樣品的實際高度。事實上,也只有運用相位資訊重建的技術,才能直接量測到樣品高度,因為重要資訊都存在相位之中,因此相位才是能真正反應出資料特性的地方。
Note: 光波干涉最重要的一項應用是數位全像術,這項技術能夠使用一般的影像感測器來測出樣品相位變化。下面影片展示出數位全像檢測的基本架構與資料,最後可以獲得三種型態資料,分別是全像片 (Hologram)、強度影像 (Intensity Image)、相位影像 (Phase Image)。
數位全像顯微鏡的工作原理
影片來源:Lyncée Tec, https://www.lynceetec.com/
影片中,全像片、強度影像、相位影像、三維重建影像的結果圖
圖片來源:Lyncée Tec, https://www.lynceetec.com/
題外話: 說了這麼多,其實只是要認識我們所使用的資料是什麼,唯有搞清楚資料,才能設計出相對應的程式,遇到問題也比較能夠排除。
複數神經網路的難度?
現在的深度學習發展使訓練一個實數 CNN 模型很容易,但要訓練 Complex Value 的資料就不是件容易的事,因為目前的 CNN 所支援的 Layer 都是基於實數,也代表著所有計算也是基於實數,並沒有一個很完整的函式庫是完全支援複數神經網路,也因此我們必須重新建構複數神經網路,這些計算包括卷積層 (Convolutional Layer)、池化層 (Pooling Layer)、仿射層 (Affine Layer)、激活層 (Activation Layer) 等向前傳遞 (Front Propagation) 演算,還有像是計算複數梯度的向後傳播演算 (Backpropagation, BP),向後傳遞演算牽涉到「複數微積分 (Complex Calculus)」的計算,實行上其實相當困難。
我們的複數卷積神經網路
Complex-Valued Convolutional Neural Network
在林立謙老師的指導下,將原本在 CNN 負責特徵擷取的卷積層、池化層、激活層等運算,轉成「複數版的特徵擷取 (Feature extraction based on Complex Value)」。只要能將最重要的複數特徵擷取出來,經過「非線性轉換層 (Nonlinear Layer)」,後面的全連接層及 BP 算法,就可以用一般 CNN 的方式去進行,實現 Complex-Valued Convolutional Neural Network,這也降低研究及實行上的難度。
模型架構的概念
還記得前面說過權重會影響到模型對資料特徵響應,也就是說我們針對實部及虛部都設立權重,藉此來推動模型對兩邊資料的特徵擷取,就能實現前面所提到的「複數特徵擷取」。或許硬生生的文字很難理解,可以用下面概念圖來表達「複數特徵擷取」的概念。
Note: 更直白地說,就是直接將實部、虛部抓出來進行實數卷積運算,藉此模擬複數運算。
Complex-Valued Convolutional Neural Network 的概念圖。以 第一個 Conv 為例,10 個 kernel 中,其實 5 個對應實部,另外 5 個對應虛部。
圖片取自論文:On Complex Valued Convolutional Neural Networks
Complexed Convolution Layer
關於複數的卷積運算,剛剛已經說過就是去設立多一倍的權重來處理實部跟虛部。但是由於這兩者是彼此互相關聯的,因此並不是直接對他們進行捲積而已,而是「實部的 Kernels 也會對虛部造成影響,而虛部的 Kernels 也會對實部造成影響」。
假設有一個 Complex Matrix 與一個 Complex Filter 進行卷積,其中 Complex Matrix 及 Complex Filter 如下:
Complex Matrix: h=x+iy
Complex Filter: W=A+iB
Complex Matrix 及 Complex Filter 的複數卷積運算如下:
W∗h = (A∗x−B∗y) + i(B∗x+A∗y)
上述運算可以等效成以下運算
\begin{bmatrix} Re(Wh)\Im(Wh) \end{bmatrix} = \begin{bmatrix} A&-B\B&A \end{bmatrix} * \begin{bmatrix} x\y \end{bmatrix}
以上面式子為基礎,就可以等效出複數卷積運算了。
而所謂「實部的 Kernels 也會對虛部造成影響,而虛部的 Kernels 也會對實部造成影響」,可以由下圖的運算概念圖中去理解,我就不多家贅述了。
Complex-Valued Convolutional Layer 的運算概念圖。
圖片取自論文:Deep Complex Networks
其他的 Complex Layer
-----------------
其他的運算如 Pooling、Activation 就跟剛剛卷積一樣,一樣分別對實部及虛部進行運算,只是可以不用像卷積一樣考慮之間關係。以激活層為例,直接對實部及虛部使用激活函數 ReLU 來進行運算,就能直接輸出這兩個的特徵圖 (Feature Map)。
其實我參與這項研究工作時,主要都是負責第一版 KERAS 的模組開發以及除錯工作,大多運算都是林老師及他的學生先在 Python 下先行實現。
* * *
> Note: 何謂池化層? 在不影響特徵前提之下,池化層主要用來降低維度,減少資料量,藉此來降低計算成本。通常池化層有幾個常見運算方式,Max Pooling、Averge Pooling 等。
ReLU 的作用? 抑制一些雜訊,只保留正元素,這樣的設計也可以讓導函數計算不會出現梯度消失的問題,能夠讓模型更深,讓模型可以去推論更複雜的任務,同時訓練模型時也可以更快收斂。
除了 ReLU 之外,還有哪些激活函數? 早期神經網路會用 Sigmoid 或是 tanh 作為激活函數,但是這兩個用來計算導術其實很複雜,沒有 ReLU 來的暴力直接,但是在對於其他應用上還是用 Sigmoid 或是 tanh 會比較好,理由是這兩者的輸出都是一個範圍,如 Sigmoid 輸出範圍是 \[0, 1\] 或是 tanh 是 \[-1, 1\],在計算機率上會比 ReLU 好很多。還有許多激活函數可以嘗試,這邊就不多講,網路比我清楚的文章多著呢! **個人理解** ReLU 用來傳遞影像特徵並抑制雜訊;Sigmoid 或是 tanh 用來進行線性轉換。
何謂梯度消失? 假設十層網路,每一層的權重梯度會隨著反向傳播的連鎖率計算而減少,導致後面的 Layer 根本不會進行訓練。
利用 KERAS API 寫出 Complex-Valued Layer
====================================
經過一系列的 Background & Motivation 之後,該來談談程式的部分 — 使用 KERAS API 做出 Custom Layer。
前面有提過,一般機器學習框架不會有 Complex Layer,而是要自己去實現,現在有非常好用的工具可以幫助我們去實現,如 KERAS 高階深度學習框架,就可以用簡單語法建立出自己的 Layer,所以先來認識如何使用 KERAS API 建立自己的 Layer 吧。
Custom Layer with KERAS API
---------------------------
Custom Layer 必須繼承 `tf.keras.layers.Layer` 這個類別,唯有繼承這個類別的自訂層才能在 KERAS 中進行運算。
繼承 `tf.keras.layers.Layer`之後還必須重新定義一些 Custom Layer 的方法,分別是:
### `__init__()`
進行初始化,可以在這裡定義一些參數來控制你的 Custom Layer。
* `super(MyDenseLayer, self).__init__()`:進行父類別 (`tf.keras.layers.Layer`) 的初始化,來獲得所有 `tf.keras.layers.Layer` 的功能。
* `self.num_outputs = num_outputs`:配置 Custom Layer 的類別屬性。也可將 Custom Layer 的輸入參數配置到 Custom Layer 的屬性中,方便在其他的方法中使用,從下面範例可以發現 `num_outputs` 在`build`、`get_config` 皆有被使用。
### `build(self, input_shape)`
配置要訓練的權重,輸入參數為 `input_shape`。
* `self.add_weight()`:利用此方法來定義權重。(注意:放置在這裡需要指定 `Shape`)
### `call(self, inputs, *args, **kwargs)`
設計 Custom Layer 的向前傳播運算。
* `call()` 被執行時, 其實是在執行 `__call__()` 。
* 執行 `__call__()`之前,會先去執行 `build()` 確保權重都配置好了,才會進行向前傳遞。
* 上面兩點說明,為什麼我們一定要在 `build()` 建立權重。
* `inputs`:前一層的輸出結果會自動進到 `inputs` 中。
### `get_config(self)`
配置設定檔。當訓練好模型之後,我們會輸出模型的權重及架構,此時會執行 `get_config()` 來導出 Custom Layer 的參數配置 (`config`)。
### 基礎範例
class MyDenseLayer(tf.keras.layers.Layer): def init(self, num_outputs): # Do initialization super(MyDenseLayer, self).init() # Set arguments to class property self.num_outputs = num_outputs
def build(self, input_shape): # Set weights self.kernel = self.add_weight(“kernel”, shape=[int(input_shape[-1]), self.num_outputs])
def call(self, inputs): # Do forward computation return tf.matmul(inputs, self.kernel)
def get_config(self): # if you want to save model or other operation, you need this. # everything is depend on the config, otherwise you will get error. config = super().get_config().copy() config.update({“num_outputs”: self.num_outputs}) return config
Call custom layer
layer = MyDenseLayer(10)
> Note:KERAS 的所有 Layer 都是類似這樣的方式去實現的。
實現 Complex-Valued Convolutional Layer
-------------------------------------
> 注意: - 此篇不會提及所有 Complex-Valued Layers,僅以 Complex-Valued Convolutional Layer 作為範例。
>
> * 以下範例程式無法在最新版的 tensorflow2 + keras 執行,需降至 tensorflow1 + keras2 底下執行。
> * 所有複數相關程式模組是由林立謙老師的實驗室負責管理、維護及更新,包含執行在 tensorflow2 + keras,本站不提供任何原始碼服務。
### A Step-by-Step Procedure
> 在開始講解之前,先跟各位說一個很重要的觀念,由於我們知道許多運算是改良自 CNN 的程式,再加上 KERAS 是開源函式庫 (Open Source Library),所以可以參考底層的寫法來改良成自己的 Complex-Valued Convolutional Layer。以下複數卷積層就是從底層改出來的。 如果是用 Anaconda 的虛擬環境,你可以從以下路徑去找到原始碼,`你的 Anaconda3路徑\envs\你的虛擬環境名稱\Lib\site-packages`,從中尋找 tensorflow 套件。(如果是裝tensorflow1 + keras2的就是去找keras套件)
1. 先引入 KERAS 模組
""" IOPLAB & SIPLAB @高揚傑 """ from keras import backend as K from keras import activations from keras import initializers from keras.utils import conv_utils from keras.engine.topology import Layer
(2) 建立 class 及初始化 input parameter
基本上都沿用原本的輸入參數,主要是更改呼叫名稱為 `ComplexConv`。
""" IOPLAB & SIPLAB @高揚傑 """ class ComplexConv(Layer):
def __init__(self,
filters,
kernel_size,
strides=(1,1),
padding="valid",
dilation_rate=(1,1),
activation=None,
use_bias=False,
kernel_initializer="glorot_uniform",
bias_initializer="zeros",
**kwargs):
super(ComplexConv, self).__init__(**kwargs)
rank = 2
self.filters = filters
self.kernel_size = conv_utils.normalize_tuple(kernel_size, rank, "kernel_size")
self.strides = conv_utils.normalize_tuple(strides, rank, "strides")
self.padding = conv_utils.normalize_padding(padding)
self.dilation_rate = conv_utils.normalize_tuple(dilation_rate, rank, "dilation_rate")
self.activation = activations.get(activation)
self.use_bias = use_bias
self.kernel_initializer = initializers.get(kernel_initializer)
self.bias_initializer = initializers.get(bias_initializer)
if DEBUG: print("\n---------- Complex Convolution Layers ----------")
(3) build weight
從 input\_shape 來計算需要建立的 kernel size。而這個 kernel 就是依照前面所講的,就立兩種權重去對應實部及虛部。
> 這邊有個小技巧,可以加上 print來印出 data shape,一方面可以確定 data flow,另一方面可以確定資料正確性。
def build(self, input_shape): assert isinstance(input_shape, list shape_a, shape_b = input_shape channel_axis = -1 input_dim = shape_a[channel_axis] kernel_shape = self.kernel_size + (input_dim, self.filters) if DEBUG: print(“build–>”,“kernel_shape:",kernel_shape)
self.kernel_R = self.add_weight(
shape=kernel_shape,
initializer=self.kernel_initializer,
name="kernel_R")
self.kernel_I = self.add_weight(
shape=kernel_shape,
initializer=self.kernel_initializer,
name="kernel_I")
if DEBUG: print("build-->","kernel_R:",K.int_shape(self.kernel_R))
if DEBUG: print("build-->","kernel_I:",K.int_shape(self.kernel_I))
if self.use_bias:
self.bias = self.add_weight(
shape=(self.filters,),
initializer=self.bias_initializer,
name="bias")
else:
self.bias = None
super(ComplexConv, self).build(input_shape)
(4) Complex Convolution 的計算
這裡的 inputs 會直接得到對應實部虛部的 input\_R, input\_I,再將這些 input 與 kernel 按照前面的算式去寫出程式,最後輸出 \[out\_R, out\_I\]。(記得輸出要用 list 回傳,list 對應的語法就是中括號)
"”" IOPLAB & SIPLAB @高揚傑 """ def call(self, inputs): assert isinstance(inputs, list) input_R, input_I = inputs # load real image MRKI = K.conv2d( input_R, self.kernel_I, # use imaginary kernel strides=self.strides, padding=self.padding, dilation_rate=self.dilation_rate) MRKR = K.conv2d( input_R, self.kernel_R, # use real kernel strides=self.strides, padding=self.padding, dilation_rate=self.dilation_rate) # load imaginary image MIKI = K.conv2d( input_I, self.kernel_I, # use imaginary kernel strides=self.strides, padding=self.padding, dilation_rate=self.dilation_rate) MIKR = K.conv2d( input_I, self.kernel_R, # use real kernel strides=self.strides, padding=self.padding, dilation_rate=self.dilation_rate) ## check matrix shape after convolution if DEBUG: print(“call–>”,“MRKI:",K.int_shape(MRKI)) if DEBUG: print(“call–>”,“MRKR:",K.int_shape(MRKR)) if DEBUG: print(“call–>”,“MIKI:",K.int_shape(MIKI)) if DEBUG: print(“call–>”,“MIKR:",K.int_shape(MIKR)) ## complex type relation computation out_I = MRKI+MIKR out_R = MRKR-MIKI if DEBUG: print(“call–>”,“out_I:",K.int_shape(out_I)) if DEBUG: print(“call–>”,“out_R:",K.int_shape(out_R))
if self.use_bias: ## for imaginary
out_I = K.bias_add(
out_I,
self.bias)
outputs = K.concatenate([out_I, out_R], axis=-1) ## combine featuremap of real and imaginary
if DEBUG: print("call-->","outputs:",K.int_shape(outputs))
if self.activation is not None:
# return self.activation(outputs)
return [self.activation(out_R), self.activation(out_I)]
return [out_R, out_I]
(5) 最後是計算輸出的 shape
這裡基本上就是將原本的輸出改成用 list 去傳遞實部及虛部的 shape,因為實部及虛部的特徵圖都是一樣的 shape,所以可以這樣計算。(一樣輸出要用 list 去做回傳)
"”” IOPLAB & SIPLAB @高揚傑 "”” def compute_output_shape(self, input_shape): assert isinstance(input_shape, list) shape_a, shape_b = input_shape ## compute padding size and image size space = shape_a[1:-1] if DEBUG: print(“compute_output_shape–>”,“input image size:",shape_a[1:-1]) new_space = [] for i in range(len(space)): new_dim = conv_utils.conv_output_length( space[i], self.kernel_size[i], padding=self.padding, stride=self.strides[i], dilation=self.dilation_rate[i]) new_space.append(new_dim) if DEBUG: print(“compute_output_shape–>”,“output image size:",tuple(new_space))
return [(shape_a[0],) + tuple(new_space) + (2*self.filters,)]
return [(shape_a[0],) + tuple(new_space) + (self.filters,),
(shape_a[0],) + tuple(new_space) + (self.filters,)]
建構 Complex-Valued Convolutional Neural Network
==============================================
> 原本想直接跑架構圖出來,發現我手邊沒程式了,只好自己畫一個出來........ 這部分如果有誤麻煩大家請多包涵,因為我是憑印象畫的,而且當時丟模型訓練測試的都是其他人,無法保證是否正確。
複數分類模型
------
假設有你有 Complex Layer 的模組,要做出一個基本複數分類模型,其架構圖就類似下面這張圖,至於裡面的參數我就不指定了。從圖中可以發現資料流只有一開始的輸入是兩個,原因是從 ComplexConv 到後面的 Nonlinear 都是傳遞 list。
複數分類模型的架構圖。
圖片取自:IOPLAB & SIPLAB @高揚傑
程式碼 (精簡版)
---------
> 再次提醒!這邊程式是憑印象直接寫得,一樣不設定參數,此程式僅供參考。 建議至林立謙老師的實驗室詢問程式碼的執行方式。
"”” IOPLAB & SIPLAB @高揚傑 "”” from complexlayers import ComplexConv from complexlayers import ComplexMaxPooling from complexlayers import ComplexBatchNorm from complexlayers import Nonlinear from keras.layers import Flatten, Dense, Activation, Input from keras.models import Model
def ComplexCNN(): xr = Input(…, name=“real”) xi = Input(…, name=“imag”) x = [xr, xi] x = ComplexConv(…, name=“cConv”)(x) x = ComplexBatchNorm(…, name=“cNorm”)(x) x = ComplexMaxPooling(…, name=“cPool”)(x) x = Nonlinear(…, name=“nonlinear”)(x) x = Flatten(…, name=“flatten”)(x) x = Dense(…, name=“fc1”)(x) x = Dense(…, name=“fc2”)(x) x_out = Activation(“softmax”, name=“label”)(x) return Model(inputs=[xr,xi], outputs=[x_out])
輸入 Complex-Valued Data 的方法
==========================
終於文章來到了尾聲,最後是說明輸入複數資料的方法,這邊其實就完全是個人經驗了,因為方法太多了。
首先,KERAS 的模型讀資料有三種,直接為 array、list、dict。上面架構用得就是屬於 list,所以我們寫讀資料的模組也要去 follow 這樣的模式,這樣 KERAS 的 API 才會正確讀取。
再來,KERAS 訓練時使用的函數叫做 `fit()`,要輸入資料都會透過它來輸入。而它支援的方式也是很多種,例如:直接丟 NumPy Array、內建的 ImageGenerator、自己寫 Generator、還有 TensorFlow2 的 `tf.data.Dataset` 這個 API,所以讀資料的方式非常多樣,也很容易讓人混淆........。接下來就直接介紹我是如何思考 `tf.data.Dataset` 與 `Model` 之間讀資料的關係。
> 其實 `tf.data.Dataset` 這部分我早就有另外寫文章出來,所以不會講太細,就不附連結了,自己去找吧! 再次提醒!這邊的程式都是我直接打出來,無法執行,只是分享我當時的想法讓各位知道。
(1) 直接輸入 array
--------------
使用 `Dataset.from_tensor_slices()` 直接輸入 NumPy Array,搭配 `Model(inputs=x_in, outputs=x_out)`。
類似下面這種方式:
""" IOPLAB & SIPLAB @高揚傑 """ """ 前略 """ model = Model(inputs=x_in, outputs=x_out)
(x_train, y_train), _ = tf.keras.datasets.mnist() ds = Dataset.from_tensor_slices((x_train, y_train)).batch(32)
(2) 直接輸入 array 的變形版 — 輸入資料路徑:
-----------------------------
一樣使用 `Dataset.from_tensor_slices()`,但搭配 `Model(inputs=[xr, xi], outputs=[x_out])`,而且這次是輸入影像路徑。
類似下面這種方式:
""" IOPLAB & SIPLAB @高揚傑 """ """ 前略 """ model = Model(inputs=[xr, xi], outputs=x_out)
x_real = glob.glob(r"D:\dataset\real_part*.bmp") # 取得資料路徑清單 x_imag = glob.glob(r"D:\dataset\imag_part*.bmp") # 取得資料路徑清單
假設檔名中有對應的類別
y_label = get_label_number_from_filepath(x_real) # 從資料路徑清單中的路徑名稱取得類別號碼
def read_fn(x, y): xr_path, xi_path = x xr = tf.image.decode_bmp() / 255. xi = tf.image.decode_bmp() / 255. y = tf.onehot(y) return [xr, xi], [y]
ds = Dataset.from_tensor_slices((x_real, x_imag),(y_label)).batch(32).map(read_fn)
> 這種做法解決了多種資料 (實部及虛部) 輸入問題,也解決了記憶體不足的問題 (因為一次只讀一部份的資料)。
(3) 直接輸入 array 的變形版 (二) — 使用 dict 輸出
------------------------------------
一樣使用 `Dataset.from_tensor_slices()`,但搭配 `Model(inputs={"real":xr, "imag":xi}, outputs={"label":x_out})`,一樣是輸入影像路徑。
""" IOPLAB & SIPLAB @高揚傑 """ """ 前略 """ model = Model(inputs={“real”:xr, “imag”:xi}, outputs={label:x_out})
x_real = glob.glob(r"D:\dataset\real_part*.bmp") # 取得資料路徑清單 x_imag = glob.glob(r"D:\dataset\imag_part*.bmp") # 取得資料路徑清單
假設檔名中有對應的類別
y_label = get_label_number_from_filepath(x_real) # 從資料路徑清單中的路徑名稱取得類別號碼
def read_fn(x, y): xr_path, xi_path = x xr = tf.image.decode_bmp() / 255. xi = tf.image.decode_bmp() / 255. y = tf.onehot(y) return {“real”:xr, “imag”:xi}, {“label”:y}
ds = Dataset.from_tensor_slices((x_real, x_imag),(y_label)).batch(32).map(read_fn)
> 使用 dict 可以更確定資料是輸入進我們所設定的 Input 中。
(4) 寫一個 generator 並輸入至 `Dataset.from_generator`
-----------------------------------------------
上面使用 `Dataset.from_tensor_slices()` ,已經把 `Model()` 輸入資料的三種方式 (array、list、dict) 講完。
再來說點特別的,如果今天要讀 MATLAB 的 MAT-FILE 呢?
這時候就是輪到 `Dataset.from_generator` 登場,我們可以把非 TensorFlow 的模組寫進一個 generator 之中,例如要讀 MAT-FILE 到 Python 中,要用 scipy 模組
""" IOPLAB & SIPLAB @高揚傑 """ from scipy.io import loadmat
""" 前略 """
只要可以對應到,輸入及輸出都可以自由選擇要用什麼方式
inputs 使用 list;outputs 直接使用 array
model = Model(inputs=[xr,xi], outputs=x_out)
mat_list = glob.glob(r"D:\dataset\real_part*.mat") # 取得MAT-FILE路徑清單 def my_generator(mat_list): for mat in mat_list: data = loadmat(mat) # “XXX.mat” xr = data[“real”] xr = data[“imag”] y = data[“label”] y = tf.onehot(y) yield [xr, xi], y
ds = Dataset.from_generator(my_generator, args=[mat_file]).batch(32)
> 這樣也代表自己開發的一些影像處理程式可以放進來執行。
後記
==
AI 目前應該還是相當熱門的題目,相關的研究也層出不窮,也少不了相關程式教學、書籍和文章,但這些都不足以支撐我在碩班期間的研究工作,至少我那時沒印象國內有複數神經網路的研究,相信在複數神經網路還是一門非常具有前瞻性的研究。
國外本來就有研究複數神經網路,一些參考資料也是看國外的,而且寫這篇文章時發現多了些新東西,像是新的 Review Paper,也有發現網路上也有人釋出類似的複數神經網路程式,代表這還是有一定價值!
* * *
謝謝耐心看到這裡的你們!這篇文章出乎意料寫非常久。相信來看的應該都是鄭老師或是林老師的學生吧!希望這內容對你們能有一點點幫助,一開始就提到,這篇的目的就是純粹分享我當時的研究過程而已,希望藉此降低各位的學習曲線,只是科技發展快速,不知道這文章時效性有多久。即便如此,還是希望能提供微薄之力,各位如果願意,有問題歡迎來信詢問,雖然我不見得能給出答案,但如果能夠聽到一些不同人的想法或許可以找到突破口喔,也祝福各位能有個愉快研究生活,順利畢業^^。
* * *
PS:如果文章有錯可以跟我說,有空我會改正!!!
參考文獻
====
\[1\] [A New CNN-Based Method for Multi-Directional Car License Plate Detection | IEEE Journals & Magazine | IEEE Xplore](https://ieeexplore.ieee.org/document/8253610)
\[2\] [\[1602.09046\] On Complex Valued Convolutional Neural Networks (arxiv.org)](https://arxiv.org/abs/1602.09046)
\[3\] [\[2101.12249\] A Survey of Complex-Valued Neural Networks (arxiv.org)](https://arxiv.org/abs/2101.12249)
\[4\] [\[1705.09792\] Deep Complex Networks (arxiv.org)](https://arxiv.org/abs/1705.09792)
\[5\] [Custom layers | TensorFlow Core](https://www.tensorflow.org/tutorials/customization/custom_layers)
\[6\] [tf.keras.layers.Layer | TensorFlow Core v2.5.0](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer)
\[7\] [tf.data.Dataset | TensorFlow Core v2.5.0](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#from_generator)
* * *
> 版權聲明: 本文內容純粹以學術研究分享之目的,所有標註 "IOPLAB & SIPLAB @高揚傑" 的圖片及程式碼,一律禁止任何形式分享、轉載,本站也不提供任何原始碼服務,未經授權擅自發佈者將違反著作權法[第 91 條](https://law.moj.gov.tw/LawClass/LawSingle.aspx?pcode=J0070017&flno=91)、[第 91-1 條](https://law.moj.gov.tw/LawClass/LawSingle.aspx?pcode=J0070017&flno=91-1)、[第 92 條](https://law.moj.gov.tw/LawClass/LawSingle.aspx?pcode=J0070017&flno=92)。