让Keras更酷一些:中间变量、权重滑动和安全生成器
作者丨蘇劍林
單位丨追一科技
研究方向丨NLP,神經網絡
個人主頁丨kexue.fm
繼續“讓Keras更酷一些”之旅。
今天我們會用 Keras 實現靈活地輸出任意中間變量,還有無縫地進行權重滑動平均,最后順便介紹一下生成器的進程安全寫法。
首先是輸出中間變量。在自定義層時,我們可能希望查看中間變量,這些需求有些是比較容易實現的,比如查看中間某個層的輸出,只需要將截止到這個層的部分模型保存為一個新模型即可,但有些需求是比較困難的,比如在使用 Attention 層時我們可能希望查看那個 Attention 矩陣的值,如果用構建新模型的方法則會非常麻煩。而本文則給出一種簡單的方法,徹底滿足這個需求。
接著是權重滑動平均。權重滑動平均是穩定、加速模型訓練甚至提升模型效果的一種有效方法,很多大型模型(尤其是 GAN)幾乎都用到了權重滑動平均。一般來說權重滑動平均是作為優化器的一部分,所以一般需要重寫優化器才能實現它。本文介紹一個權重滑動平均的實現,它可以無縫插入到任意 Keras 模型中,不需要自定義優化器。
至于生成器的進程安全寫法,則是因為 Keras 讀取生成器的時候,用到了多進程,如果生成器本身也包含了一些多進程操作,那么可能就會導致異常,所以需要解決這個這個問題。
輸出中間變量
這一節以基本模型為例,逐步深入地介紹如何獲取 Keras 的中間變量。
x?=?x_in
x?=?Dense(512,?activation='relu')(x)
x?=?Dropout(0.2)(x)
x?=?Dense(256,?activation='relu')(x)
x?=?Dropout(0.2)(x)
x?=?Dense(num_classes,?activation='softmax')(x)
model?=?Model(x_in,?x)
作為一個新模型
假如模型訓練完成后,我想要獲取 x = Dense(256, activation='relu')(x) 對應的輸出,那可以在定義模型的時候,先把對應的變量存起來,然后重新定義一個模型:
x?=?x_in
x?=?Dense(512,?activation='relu')(x)
x?=?Dropout(0.2)(x)
x?=?Dense(256,?activation='relu')(x)
y?=?x
x?=?Dropout(0.2)(x)
x?=?Dense(num_classes,?activation='softmax')(x)
model?=?Model(x_in,?x)
model2?=?Model(x_in,?y)
將 model 訓練完成后,直接用 model2.predict 就可以查看對應的 256 維的輸出了。這樣做的前提是 y 必須是某個層的輸出,不能是隨意一個張量。?
K.function!
有時候我們自定義了一個比較復雜的層,比較典型的就是 Attention 層,我們希望查看層的一些中間變量,比如對應的 Attention 矩陣,這時候就比較麻煩了,如果想要用前面的方式,那么就要把原來的 Attention 層分開為兩個層定義才行。
因為前面已經說了,新定義一個 Keras 模型時輸入輸出都必須是 Keras 層的輸入輸出,不能是隨意一個張量。這樣一來,如果想要分別查看層的多個中間變量,那就要將層不斷地拆開為多個層來定義,顯然是不夠友好的。?
其實 Keras 提供了一個終極的解決方案: K.function !?
介紹 K.function 之前,我們先寫一個簡單示例:
????def?__init__(self,?**kwargs):
????????super(Normal,?self).__init__(**kwargs)
????def?build(self,?input_shape):
????????self.kernel?=?self.add_weight(name='kernel',?
??????????????????????????????????????shape=(1,),
??????????????????????????????????????initializer='zeros',
??????????????????????????????????????trainable=True)
????????self.built?=?True
????def?call(self,?x):
????????self.x_normalized?=?K.l2_normalize(x,?-1)
????????return?self.x_normalized?*?self.kernel
x_in?=?Input(shape=(784,))
x?=?x_in
x?=?Dense(512,?activation='relu')(x)
x?=?Dropout(0.2)(x)
x?=?Dense(256,?activation='relu')(x)
x?=?Dropout(0.2)(x)
normal?=?Normal()
x?=?normal(x)
x?=?Dense(num_classes,?activation='softmax')(x)
model?=?Model(x_in,?x)
在上面的例子中, Normal 定義了一個層,層的輸出是 self.x_normalized * self.kernel ,不過我想在訓練完成后獲取 self.x_normalized 的值,而它是跟輸入有關,并且不是一個層的輸出。這樣一來前面的方法就沒法用了,但用 K.function 就只是一行代碼:
?K.function 的用法跟定義一個新模型類似,要把所有跟 normal.x_normalized?相關的輸入張量都傳進去,但是不要求輸出是一個層的輸出,允許是任意張量!返回的 fn 是一個具有函數功能的對象,所以只需要:
就可以獲取到 x_test 對應的 x_normalized 了!比定義一個新模型簡單通用多了。
事實上 K.function 就是 Keras 底層的基礎函數之一,它直接封裝好了后端的輸入輸出操作,換句話說,你用 Tensorflow 為后端時, fn([x_test]) 就相當于:
所以 K.function 的輸出允許是任意張量,因為它本來就在直接操作后端了。
權重滑動平均
權重滑動平均是提供訓練穩定性的有效方法,通過滑動平均可以幾乎零額外成本地提高解的性能。權重滑動平均一般就是指“Exponential Moving Average”,簡稱 EMA,這是因為一般滑動平均時會使用指數衰減作為權重的比例。它已經被主流模型所接受,尤其是 GAN,在很多 GAN 論文中我們通常會看到類似的描述:?
這就意味著 GAN 模型使用了 EMA。此外,普通模型也會使用,比如 QANet: Combining Local Convolution with Global Self-Attention for Reading Comprehension 就在訓練過程中用了 EMA,衰減率是 0.9999。
滑動平均的格式
滑動平均的格式其實非常簡單,假設每次優化器的更新為:
這里的 Δθn 就是優化器帶來的更新,優化器可以是 SGD、Adam 等任意一種。而滑動平均則是維護一組新的新的變量 Θ:
其中 α 是一個接近于 1 的正常數,稱為“衰減率(decay rate)”。?
注意,盡管在形式上有點相似,但它跟動量加速不一樣:EMA 不改變原來優化器的軌跡,即原來優化器怎么走,現在依然是同樣的走法,只不過它維護一組新變量,來平均原來優化器的軌跡;而動量加速則是改變了原來優化器的軌跡。?
再次強調,權重滑動平均不改變優化器的走向,只不過它降優化器的優化軌跡上的點做了平均后,作為最終的模型權重。?
關于權重滑動平均的原理和效果,可以進一步參考《從動力學角度看優化算法(四):GAN 的第三個階段》一文。?
巧妙的注入實現
實現 EMA 的要點是如何在原來優化器的基礎上引入一組新的平均變量,并且在每次參數更新后執行平均變量的更新。這需要對 Keras 的源碼及其實現邏輯有一定的了解。?
在此給出的參考實現如下:
????"""對模型權重進行指數滑動平均。
????用法:在model.compile之后、第一次訓練之前使用;
????先初始化對象,然后執行inject方法。
????"""
????def?__init__(self,?model,?momentum=0.9999):
????????self.momentum?=?momentum
????????self.model?=?model
????????self.ema_weights?=?[K.zeros(K.shape(w))?for?w?in?model.weights]
????def?inject(self):
????????"""添加更新算子到model.metrics_updates。
????????"""
????????self.initialize()
????????for?w1,?w2?in?zip(self.ema_weights,?self.model.weights):
????????????op?=?K.moving_average_update(w1,?w2,?self.momentum)
????????????self.model.metrics_updates.append(op)
????def?initialize(self):
????????"""ema_weights初始化跟原模型初始化一致。
????????"""
????????self.old_weights?=?K.batch_get_value(self.model.weights)
????????K.batch_set_value(zip(self.ema_weights,?self.old_weights))
????def?apply_ema_weights(self):
????????"""備份原模型權重,然后將平均權重應用到模型上去。
????????"""
????????self.old_weights?=?K.batch_get_value(self.model.weights)
????????ema_weights?=?K.batch_get_value(self.ema_weights)
????????K.batch_set_value(zip(self.model.weights,?ema_weights))
????def?reset_old_weights(self):
????????"""恢復模型到舊權重。
????????"""
????????K.batch_set_value(zip(self.model.weights,?self.old_weights))
使用方法很簡單:
EMAer.inject()?#?在模型compile之后執行
model.fit(x_train,?y_train)?#?訓練模型
訓練完成后:
model.predict(x_test)?#?進行預測、驗證、保存等操作
EMAer.reset_old_weights()?#?繼續訓練之前,要恢復模型舊權重。還是那句話,EMA不影響模型的優化軌跡。
model.fit(x_train,?y_train)?#?繼續訓練
現在翻看實現過程,可以發現主要的一點是引入了 K.moving_average_update 操作,并且插入到 model.metrics_updates 中,在訓練過程中,模型會讀取并執行 model.metrics_updates 的所有算子,從而完成了滑動平均。
進程安全生成器
一般來說,當訓練數據無法全部載入內存,或者需要動態生成訓練數據時,就會用到 generator。一般來說,Keras 模型的 generator 的寫法是:
????while?True:
????????x_train?=?something
????????y_train?=?otherthing
????????yield?x_train,?y_train
但如果 someting 或 otherthing 里邊包含了多進程操作,就可能出問題。這時候有兩種解決方法,一是 fit_generator 時將設置參數use_multiprocessing=False, worker=0 ;另一種方法就是通過繼承 keras.utils.Sequence 類來寫生成器。?
官方參考例子
官方對 keras.utils.Sequence 類的介紹如下:
https://keras.io/utils/#sequence
官方強調:
總之,就是對于多進程來說它是安全的,可以放心用。官方提供的例子如下:
from?skimage.transform?import?resize
import?numpy?as?np
#?Here,?`x_set`?is?list?of?path?to?the?images
#?and?`y_set`?are?the?associated?classes.
class?CIFAR10Sequence(Sequence):
????def?__init__(self,?x_set,?y_set,?batch_size):
????????self.x,?self.y?=?x_set,?y_set
????????self.batch_size?=?batch_size
????def?__len__(self):
????????return?int(np.ceil(len(self.x)?/?float(self.batch_size)))
????def?__getitem__(self,?idx):
????????batch_x?=?self.x[idx?*?self.batch_size:(idx?+?1)?*?self.batch_size]
????????batch_y?=?self.y[idx?*?self.batch_size:(idx?+?1)?*?self.batch_size]
????????return?np.array([
????????????resize(imread(file_name),?(200,?200))
???????????????for?file_name?in?batch_x]),?np.array(batch_y)
就是按格式定義好 __len__ 和 __getitem__ 方法就行了, __getitem__ 方法直接返回一個 batch 的數據。?
bert as service例子
我第一次發現 Sequence 的必要性,是在試驗 bert as service 的時候。bert as service 是肖涵大佬搞的一個快速獲取 bert 編碼向量的服務組件,我曾經想用它獲取字向量,然后傳入到 Keras 中訓練,但發現總會訓練著訓練著就卡住了。?
經過搜索,確認是 Keras 的 fit_generator 所帶的多進程,和 bert-as-service 自帶的多進程沖突問題,具體怎么沖突我也比較模糊,就不深究了。而這里提供了一個參考的解決方案,用的就是繼承 Sequence 類來寫生成器。
https://github.com/hanxiao/bert-as-service/issues/29#issuecomment-442362241
PS:就調用 bert as service 而言,后面肖涵大佬提供了協程版的 ConcurrentBertClient?,可以取代原來的 BertClient?,這樣哪怕在原始生成器也不會有問題了。
清流般的Keras
在我眼里,Keras 就是深度學習框架中的一股清流,就好比 Python 是所有編程語言中的一股清流一樣。用 Keras 實現所需要做的事情,就好比一次次愜意的享受。
點擊以下標題查看作者其他文章:?
變分自編碼器VAE:原來是這么一回事 | 附源碼
再談變分自編碼器VAE:從貝葉斯觀點出發
變分自編碼器VAE:這樣做為什么能成?
簡單修改,讓GAN的判別器秒變編碼器
深度學習中的互信息:無監督提取特征
全新視角:用變分推斷統一理解生成模型
細水長flow之NICE:流模型的基本概念與實現
細水長flow之f-VAEs:Glow與VAEs的聯姻
深度學習中的Lipschitz約束:泛化與生成模型
#投 稿 通 道#
?讓你的論文被更多人看到?
如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢?答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學習心得或技術干貨。我們的目的只有一個,讓知識真正流動起來。
??來稿標準:
? 稿件確系個人原創作品,來稿需注明作者個人信息(姓名+學校/工作單位+學歷/職位+研究方向)?
? 如果文章并非首發,請在投稿時提醒并附上所有已發布鏈接?
? PaperWeekly 默認每篇文章都是首發,均會添加“原創”標志
? 投稿郵箱:
? 投稿郵箱:hr@paperweekly.site?
? 所有文章配圖,請單獨在附件中發送?
? 請留下即時聯系方式(微信或手機),以便我們在編輯發布時和作者溝通
?
現在,在「知乎」也能找到我們了
進入知乎首頁搜索「PaperWeekly」
點擊「關注」訂閱我們的專欄吧
關于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點擊 |?閱讀原文?| 查看作者博客
總結
以上是生活随笔為你收集整理的让Keras更酷一些:中间变量、权重滑动和安全生成器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 清华大学人工智能研究院成立基础理论研究中
- 下一篇: 微软开源项目NeuronBlocks -