“让Keras更酷一些!”:层中层与mask
這一篇“讓 Keras 更酷一些!”將和讀者分享兩部分內(nèi)容:第一部分是“層中層”,顧名思義,是在 Keras 中自定義層的時候,重用已有的層,這將大大減少自定義層的代碼量;另外一部分就是應(yīng)讀者所求,介紹一下序列模型中的 mask 原理和方法。
作者丨蘇劍林
單位丨追一科技
研究方向丨NLP,神經(jīng)網(wǎng)絡(luò)
個人主頁丨kexue.fm
層中層
在《“讓Keras更酷一些!”:精巧的層與花式的回調(diào)》[1]?一文中我們已經(jīng)介紹過 Keras 自定義層的基本方法,其核心步驟是定義 build 和 call 兩個函數(shù),其中 build 負責創(chuàng)建可訓練的權(quán)重,而 call 則定義具體的運算。
拒絕重復(fù)勞動
經(jīng)常用到自定義層的讀者可能會感覺到,在自定義層的時候我們經(jīng)常在重復(fù)勞動,比如我們想要增加一個線性變換,那就要在 build 中增加一個 kernel 和 bias 變量(還要自定義變量的初始化、正則化等),然后在 call 里邊用 K.dot 來執(zhí)行,有時候還需要考慮維度對齊的問題,步驟比較繁瑣。
但事實上,一個線性變換其實就是一個不加激活函數(shù)的 Dense 層罷了,如果在自定義層時能重用已有的層,那顯然就可以大大節(jié)省代碼量了。?
事實上,只要你對 Python 面向?qū)ο缶幊瘫容^熟悉,然后仔細研究 Keras 的 Layer 的源代碼,就不難發(fā)現(xiàn)重用已有層的方法了。下面將它整理成比較規(guī)范的流程,供讀者參考調(diào)用。
OurLayer
首先,我們定義一個新的 OurLayer 類:
class?OurLayer(Layer):
????"""定義新的Layer,增加reuse方法,允許在定義Layer時調(diào)用現(xiàn)成的層
????"""
????def?reuse(self,?layer,?*args,?**kwargs):
????????if?not?layer.built:
????????????if?len(args)?>?0:
????????????????inputs?=?args[0]
????????????else:
????????????????inputs?=?kwargs['inputs']
????????????if?isinstance(inputs,?list):
????????????????input_shape?=?[K.int_shape(x)?for?x?in?inputs]
????????????else:
????????????????input_shape?=?K.int_shape(inputs)
????????????layer.build(input_shape)
????????outputs?=?layer.call(*args,?**kwargs)
????????for?w?in?layer.trainable_weights:
????????????if?w?not?in?self._trainable_weights:
????????????????self._trainable_weights.append(w)
????????for?w?in?layer.non_trainable_weights:
????????????if?w?not?in?self._non_trainable_weights:
????????????????self._non_trainable_weights.append(w)
????????return?outputs
這個 OurLayer 類繼承了原來的 Layer 類,為它增加了 reuse 方法,就是通過它我們可以重用已有的層。
下面是一個簡單的例子,定義一個層,運算如下:
這里?f,g 是激活函數(shù),其實就是兩個 Dense 層的復(fù)合,如果按照標準的寫法,我們需要在 build 那里定義好幾個權(quán)重,定義權(quán)重的時候還需要根據(jù)輸入來定義 shape,還要定義初始化等,步驟很多,但事實上這些在 Dense 層不都寫好了嗎,直接調(diào)用就可以了,參考調(diào)用代碼如下:
class?OurDense(OurLayer):
????"""原來是繼承Layer類,現(xiàn)在繼承OurLayer類
????"""
????def?__init__(self,?hidden_dimdim,?output_dim,
?????????????????hidden_activation='linear',
?????????????????output_activation='linear',?**kwargs):
????????super(OurDense,?self).__init__(**kwargs)
????????self.hidden_dim?=?hidden_dim
????????self.output_dim?=?output_dim
????????self.hidden_activation?=?hidden_activation
????????self.output_activation?=?output_activation
????def?build(self,?input_shape):
????????"""在build方法里邊添加需要重用的層,
????????當然也可以像標準寫法一樣條件可訓練的權(quán)重。
????????"""
????????super(OurDense,?self).build(input_shape)
????????self.h_dense?=?Dense(self.hidden_dimdim,
?????????????????????????????activation=self.hidden_activation)
????????self.o_dense?=?Dense(self.output_dim,
?????????????????????????????activation=self.output_activation)
????def?call(self,?inputs):
????????"""直接reuse一下層,等價于o_dense(h_dense(inputs))
????????"""
????????h?=?self.reuse(self.h_dense,?inputs)
????????o?=?self.reuse(self.o_dense,?h)
????????return?o
????def?compute_output_shape(self,?input_shape):
????????return?input_shape[:-1]?+?(self.output_dim,)
是不是特別清爽?
Mask
這一節(jié)我們來討論一下處理變長序列時的 padding 和 mask 問題。?
證明你思考過
近來筆者開源的幾個模型中大量地用到了 mask,不少讀者似乎以前從未遇到過這個東西,各種疑問紛至沓來。本來,對一樣新東西有所疑問是無可厚非的事情,但問題是不經(jīng)思考的提問就顯得很不負責任了。
我一直認為,在向別人提問的時候,需要同時去“證明”自己是思考過的,比如如果你要去解釋關(guān)于 mask 的問題,我會先請你回答:mask 之前的序列大概是怎樣的?mask 之后序列的哪些位置發(fā)生了變化?變成了怎么樣??
這三個問題跟 mask 的原理沒有關(guān)系,只是要你看懂 mask 做了什么運算,在此基礎(chǔ)上,我們才能去討論為什么要這樣運算。如果你連運算本身都看不懂,那只有兩條路可選了,一是放棄這個問題的理解,二是好好學幾個月 Keras 咱們再來討論。?
下面假設(shè)讀者已經(jīng)看懂了 mask 的運算,然后我們來簡單討論一下 mask 的基本原理。
排除padding
mask 是伴隨著 padding 出現(xiàn)的,因為神經(jīng)網(wǎng)絡(luò)的輸入需要一個規(guī)整的張量,而文本通常都是不定長的,這樣一來就需要裁剪或者填充的方式來使得它們變成定長,按照常規(guī)習慣,我們會使用 0 作為 padding 符號。?
這里用簡單的向量來描述 padding 的原理。假設(shè)有一個長度為 5 的向量:
經(jīng)過 padding 變成長度為 8:
當你將這個長度為 8 的向量輸入到模型中時,模型并不知道你這個向量究竟是“長度為 8 的向量”還是“長度為 5 的向量,填充了 3 個無意義的 0”。為了表示出哪些是有意義的,哪些是 padding 的,我們還需要一個 mask 向量(矩陣):
這是一個 0/1 向量(矩陣),用 1 表示有意義的部分,用 0 表示無意義的 padding 部分。
所謂 mask,就是 x 和 m 的運算,來排除 padding 帶來的效應(yīng)。比如我們要求 x 的均值,本來期望的結(jié)果是:
但是由于向量已經(jīng)經(jīng)過 padding,直接算的話就得到:
會帶來偏差。更嚴重的是,對于同一個輸入,每次 padding 的零的數(shù)目可能是不固定的,因此同一個樣本每次可能得到不同的均值,這是很不合理的。有了 mask 向量 m 之后,我們可以重寫求均值的運算:
這里的 ? 是逐位對應(yīng)相乘的意思。這樣一來,分子只對非 padding 部分求和,分母則是對非 padding 部分計數(shù),不管你 padding 多少個零,最終算出來的結(jié)果都是一樣的。
如果要求 x 的最大值呢?我們有 max([1,0,3,4,5])=max([1,0,3,4,5,0,0,0])=5,似乎不用排除 padding 效應(yīng)了?在這個例子中是這樣,但還有可能是:
經(jīng)過 padding 后變成了:
如果直接對 padding 后的 x 求 max,那么得到的是 0,而 0 不在原來的范圍內(nèi)。這時候解決的方法是:讓 padding 部分足夠小,以至于 max(幾乎)不能取到 padding 部分,比如:
正常來說,神經(jīng)網(wǎng)絡(luò)的輸入輸出的數(shù)量級不會很大,所以經(jīng)過后,padding 部分在這個數(shù)量級中上,可以保證取 max 的話不會取到 padding 部分了。
處理 softmax 的 padding 也是如此。在 Attention 或者指針網(wǎng)絡(luò)時,我們就有可能遇到對變長的向量做 softmax,如果直接對 padding 后的向量做 softmax,那么 padding 部分也會平攤一部分概率,導(dǎo)致實際有意義的部分概率之和都不等于 1 了。解決辦法跟 max 時一樣,讓 padding 部分足夠小足夠小,使得足夠接近于 0,以至于可以忽略:
上面幾個算子的 mask 處理算是比較特殊的,其余運算的 mask 處理(除了雙向 RNN),基本上只需要輸出:
就行了,也就是讓 padding 部分保持為 0。
Keras實現(xiàn)要點
Keras 自帶了 mask 功能,但是不建議用,因為自帶的 mask 不夠清晰靈活,而且也不支持所有的層,強烈建議讀者自己實現(xiàn) mask。?
近來開源的好幾個模型都已經(jīng)給出了足夠多的 mask 案例,我相信讀者只要認真去閱讀源碼,一定很容易理解 mask 的實現(xiàn)方式的,這里簡單提一下幾個要點。
一般來說 NLP 模型的輸入是詞 ID 矩陣,形狀為 [batch_size, seq_len],其中我會用 0 作為 padding 的 ID,而 1 作為 UNK 的 ID,剩下的就隨意了,然后我就用一個 Lambda 層生成 mask 矩陣:
#?x是詞ID矩陣
mask?=?Lambda(lambda?x:?K.cast(K.greater(K.expand_dims(x,?2),?0),?'float32'))(x)
這樣生成的 mask 矩陣大小是 [batch_size, seq_len, 1],然后詞 ID 矩陣經(jīng)過 Embedding 層后的大小為 [batch_size, seq_len, word_size],這樣一來就可以用 mask 矩陣對輸出結(jié)果就行處理了。這種寫法只是我的習慣,并非就是唯一的標準。
結(jié)合:雙向RNN
剛才我們的討論排除了雙向 RNN,這是因為 RNN 是遞歸模型,沒辦法簡單地 mask(主要是逆向 RNN 這部分)。所謂雙向 RNN,就是正反各做一次 RNN 然后拼接或者相加之類的。
假如我們要對 [1,0,3,4,5,0,0,0] 做逆向 RNN 運算時,最后輸出的結(jié)果都會包含 padding 部分的 0(因為 padding 部分在一開始就參與了運算)。因此事后是沒法排除的,只有在事前排除。
排除的方案是:要做逆向 RNN,先將 [1,0,3,4,5,0,0,0] 反轉(zhuǎn)為 [5,4,3,0,1,0,0,0],然后做一個正向 RNN,然后再把結(jié)果反轉(zhuǎn)回去,要注意反轉(zhuǎn)的時候只反轉(zhuǎn)非 padding 部分(這樣才能保證遞歸運算時 padding 部分始終不參與,并且保證跟正向 RNN 的結(jié)果對齊),這個 tensorflow 提供了現(xiàn)成的函數(shù) tf.reverse_sequence()。
遺憾的是,Keras 自帶的 Bidirectional 并沒有這個功能,所以我重寫了它,供讀者參考:
class?OurBidirectional(OurLayer):
????"""自己封裝雙向RNN,允許傳入mask,保證對齊
????"""
????def?__init__(self,?layer,?**args):
????????super(OurBidirectional,?self).__init__(**args)
????????self.forward_layer?=?copy.deepcopy(layer)
????????self.backward_layer?=?copy.deepcopy(layer)
????????self.forward_layer.name?=?'forward_'?+?self.forward_layer.name
????????self.backward_layer.name?=?'backward_'?+?self.backward_layer.name
????def?reverse_sequence(self,?x,?mask):
????????"""這里的mask.shape是[batch_size,?seq_len,?1]
????????"""
????????seq_len?=?K.round(K.sum(mask,?1)[:,?0])
????????seq_len?=?K.cast(seq_len,?'int32')
????????return?K.tf.reverse_sequence(x,?seq_len,?seq_dim=1)
????def?call(self,?inputs):
????????x,?mask?=?inputs
????????x_forward?=?self.reuse(self.forward_layer,?x)
????????x_backward?=?self.reverse_sequence(x,?mask)
????????x_backward?=?self.reuse(self.backward_layer,?x_backward)
????????x_backward?=?self.reverse_sequence(x_backward,?mask)
????????x?=?K.concatenate([x_forward,?x_backward],?2)
????????return?x?*?mask
????def?compute_output_shape(self,?input_shape):
????????return?(None,?input_shape[0][1],?self.forward_layer.units?*?2)
使用方法跟自帶的 Bidirectional 基本一樣的,只不過要多傳入 mask 矩陣,比如:
x?=?OurBidirectional(LSTM(128))([x,?x_mask])
小結(jié)
Keras 是一個極其友好、極其靈活的高層深度學習 API 封裝,千萬不要聽信網(wǎng)上流傳的“Keras 對新手很友好,但是欠缺靈活性”的謠言。Keras 對新手很友好,對老手更友好,對需要頻繁自定義模塊的用戶更更友好。
相關(guān)鏈接
[1]?https://kexue.fm/archives/5765
點擊以下標題查看作者其他文章:?
當Bert遇上Keras:這可能是Bert最簡單的打開姿勢
玩轉(zhuǎn)Keras之Seq2Seq自動生成標題 | 附開源代碼
Keras之小眾需求:自定義優(yōu)化器
讓Keras更酷一些:中間變量、權(quán)重滑動和安全生成器
讓Keras更酷一些:分層的學習率和自由的梯度
全新視角:用變分推斷統(tǒng)一理解生成模型
#投 稿 通 道#
?讓你的論文被更多人看到?
如何才能讓更多的優(yōu)質(zhì)內(nèi)容以更短路徑到達讀者群體,縮短讀者尋找優(yōu)質(zhì)內(nèi)容的成本呢?答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術(shù)靈感相互碰撞,迸發(fā)出更多的可能性。
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優(yōu)質(zhì)內(nèi)容,可以是最新論文解讀,也可以是學習心得或技術(shù)干貨。我們的目的只有一個,讓知識真正流動起來。
??來稿標準:
? 稿件確系個人原創(chuàng)作品,來稿需注明作者個人信息(姓名+學校/工作單位+學歷/職位+研究方向)?
? 如果文章并非首發(fā),請在投稿時提醒并附上所有已發(fā)布鏈接?
? PaperWeekly 默認每篇文章都是首發(fā),均會添加“原創(chuàng)”標志
? 投稿郵箱:
? 投稿郵箱:hr@paperweekly.site?
? 所有文章配圖,請單獨在附件中發(fā)送?
? 請留下即時聯(lián)系方式(微信或手機),以便我們在編輯發(fā)布時和作者溝通
?
現(xiàn)在,在「知乎」也能找到我們了
進入知乎首頁搜索「PaperWeekly」
點擊「關(guān)注」訂閱我們的專欄吧
關(guān)于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術(shù)平臺。如果你研究或從事 AI 領(lǐng)域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點擊 |?閱讀原文?| 查看作者博客
總結(jié)
以上是生活随笔為你收集整理的“让Keras更酷一些!”:层中层与mask的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PyTorch必备神器 | 唯快不破:基
- 下一篇: “马踏飞”AI机器人实现方案介绍