這一篇“讓Keras更酷一些!”將和讀者分享兩部分內(nèi)容:第一部分是“層中層”,顧名思義,是在Keras中自定義層的時(shí)候,重用已有的層,這將大大減少自定義層的代碼量;另外一部分就是應(yīng)讀者所求,介紹一下序列模型中的Mask原理和方法。
層中層?#
在《“讓Keras更酷一些!”:精巧的層與花式的回調(diào)》一文中我們已經(jīng)介紹過(guò)Keras自定義層的基本方法,其核心步驟是定義build和call兩個(gè)函數(shù),其中build負(fù)責(zé)創(chuàng)建可訓(xùn)練的權(quán)重,而call則定義具體的運(yùn)算。
拒絕重復(fù)勞動(dòng)?#
經(jīng)常用到自定義層的讀者可能會(huì)感覺(jué)到,在自定義層的時(shí)候我們經(jīng)常在重復(fù)勞動(dòng),比如我們想要增加一個(gè)線性變換,那就要在build中增加一個(gè)kernel和bias變量(還要自定義變量的初始化、正則化等),然后在call里邊用K.dot來(lái)執(zhí)行,有時(shí)候還需要考慮維度對(duì)齊的問(wèn)題,步驟比較繁瑣。但事實(shí)上,一個(gè)線性變換其實(shí)就是一個(gè)不加激活函數(shù)的Dense層罷了,如果在自定義層時(shí)能重用已有的層,那顯然就可以大大節(jié)省代碼量了。
事實(shí)上,只要你對(duì)Python面向?qū)ο缶幊瘫容^熟悉,然后仔細(xì)研究Keras的Layer的源代碼,就不難發(fā)現(xiàn)重用已有層的方法了。下面將它整理成比較規(guī)范的流程,供讀者參考調(diào)用。
OurLayer?#
首先,我們定義一個(gè)新的OurLayer類:
classOurLayer(Layer):"""定義新的Layer,增加reuse方法,允許在定義Layer時(shí)調(diào)用現(xiàn)成的層
? ? """defreuse(self,layer,*args,**kwargs):ifnotlayer.built:iflen(args)>0:inputs=args[0]else:inputs=kwargs['inputs']ifisinstance(inputs,list):input_shape=[K.int_shape(x)forxininputs]else:input_shape=K.int_shape(inputs)layer.build(input_shape)outputs=layer.call(*args,**kwargs)forwinlayer._trainable_weights:ifwnotinself._trainable_weights:self._trainable_weights.append(w)forwinlayer._non_trainable_weights:ifwnotinself._non_trainable_weights:self._non_trainable_weights.append(w)returnoutputs
這個(gè)OurLayer類繼承了原來(lái)的Layer類,為它增加了reuse方法,就是通過(guò)它我們可以重用已有的層。
下面是一個(gè)簡(jiǎn)單的例子,定義一個(gè)層,運(yùn)算如下:
y=g(f(xW1+b1)W2+b2)y=g(f(xW1+b1)W2+b2)
這里f,gf,g是激活函數(shù),其實(shí)就是兩個(gè)Dense層的復(fù)合,如果按照標(biāo)準(zhǔn)的寫(xiě)法,我們需要在build那里定義好幾個(gè)權(quán)重,定義權(quán)重的時(shí)候還需要根據(jù)輸入來(lái)定義shape,還要定義初始化等,步驟很多,但事實(shí)上這些在Dense層不都寫(xiě)好了嗎,直接調(diào)用就可以了,參考調(diào)用代碼如下:
classOurDense(OurLayer):"""原來(lái)是繼承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_activationdefbuild(self,input_shape):"""在build方法里邊添加需要重用的層,
? ? ? ? 當(dāng)然也可以像標(biāo)準(zhǔn)寫(xiě)法一樣條件可訓(xùn)練的權(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)defcall(self,inputs):"""直接reuse一下層,等價(jià)于o_dense(h_dense(inputs))
? ? ? ? """h=self.reuse(self.h_dense,inputs)o=self.reuse(self.o_dense,h)returnodefcompute_output_shape(self,input_shape):returninput_shape[:-1]+(self.output_dim,)
是不是特別清爽?
Mask?#
這一節(jié)我們來(lái)討論一下處理變長(zhǎng)序列時(shí)的padding和mask問(wèn)題。
證明你思考過(guò)?#
近來(lái)筆者開(kāi)源的幾個(gè)模型中大量地用到了mask,不少讀者似乎以前從未遇到過(guò)這個(gè)東西,各種疑問(wèn)紛至沓來(lái)。本來(lái),對(duì)一樣新東西有所疑問(wèn)是無(wú)可厚非的事情,但問(wèn)題是不經(jīng)思考的提問(wèn)就顯得很不負(fù)責(zé)任了。我一直認(rèn)為,在向別人提問(wèn)的時(shí)候,需要同時(shí)去“證明”自己是思考過(guò)的,比如如果你要去解釋關(guān)于mask的問(wèn)題,我會(huì)先請(qǐng)你回答:
mask之前的序列大概是怎樣的?mask之后序列的哪些位置發(fā)生了變化?變成了怎么樣?
這三個(gè)問(wèn)題跟mask的原理沒(méi)有關(guān)系,只是要你看懂mask做了什么運(yùn)算,在此基礎(chǔ)上,我們才能去討論為什么要這樣運(yùn)算。如果你連運(yùn)算本身都看不懂,那只有兩條路可選了,一是放棄這個(gè)問(wèn)題的理解,二是好好學(xué)幾個(gè)月Keras咱們?cè)賮?lái)討論。
下面假設(shè)讀者已經(jīng)看懂了mask的運(yùn)算,然后我們來(lái)簡(jiǎn)單討論一下mask的基本原理。
排除padding?#
mask是伴隨這padding出現(xiàn)的,因?yàn)樯窠?jīng)網(wǎng)絡(luò)的輸入需要一個(gè)規(guī)整的張量,而文本通常都是不定長(zhǎng)的,這樣一來(lái)就需要裁剪或者填充的方式來(lái)使得它們變成定長(zhǎng),按照常規(guī)習(xí)慣,我們會(huì)使用0作為padding符號(hào)。
這里用簡(jiǎn)單的向量來(lái)描述padding的原理。假設(shè)有一個(gè)長(zhǎng)度為5的向量:
x=[1,0,3,4,5]x=[1,0,3,4,5]
經(jīng)過(guò)padding變成長(zhǎng)度為8:
x=[1,0,3,4,5,0,0,0]x=[1,0,3,4,5,0,0,0]
當(dāng)你將這個(gè)長(zhǎng)度為8的向量輸入到模型中時(shí),模型并不知道你這個(gè)向量究竟是“長(zhǎng)度為8的向量”還是“長(zhǎng)度為5的向量,填充了3個(gè)無(wú)意義的0”。為了表示出哪些是有意義的,哪些是padding的,我們還需要一個(gè)mask向量(矩陣):
m=[1,1,1,1,1,0,0,0]m=[1,1,1,1,1,0,0,0]
這是一個(gè)0/1向量(矩陣),用1表示有意義的部分,用0表示無(wú)意義的padding部分。
所謂mask,就是xx和mm的運(yùn)算,來(lái)排除padding帶來(lái)的效應(yīng)。比如我們要求xx的均值,本來(lái)期望的結(jié)果是:
avg(x)=1+0+3+4+55=2.6avg(x)=1+0+3+4+55=2.6
但是由于向量已經(jīng)經(jīng)過(guò)padding,直接算的話就得到:
1+0+3+4+5+0+0+08=1.6251+0+3+4+5+0+0+08=1.625
會(huì)帶來(lái)偏差。更嚴(yán)重的是,對(duì)于同一個(gè)輸入,每次padding的零的數(shù)目可能是不固定的,因此同一個(gè)樣本每次可能得到不同的均值,這是很不合理的。有了mask向量mm之后,我們可以重寫(xiě)求均值的運(yùn)算:
avg(x)=sum(x?m)sum(m)avg(x)=sum(x?m)sum(m)
這里的??是逐位對(duì)應(yīng)相乘的意思。這樣一來(lái),分子只對(duì)非padding部分求和,分母則是對(duì)非padding部分計(jì)數(shù),不管你padding多少個(gè)零,最終算出來(lái)的結(jié)果都是一樣的。
如果要求xx的最大值呢?我們有max([1,0,3,4,5])=max([1,0,3,4,5,0,0,0])=5max([1,0,3,4,5])=max([1,0,3,4,5,0,0,0])=5,似乎不用排除padding效應(yīng)了?在這個(gè)例子中是這樣,但還有可能是:
x=[?1,?2,?3,?4,?5]x=[?1,?2,?3,?4,?5]
經(jīng)過(guò)padding后變成了
x=[?1,?2,?3,?4,?5,0,0,0]x=[?1,?2,?3,?4,?5,0,0,0]
如果直接對(duì)padding后的xx求maxmax,那么得到的是0,而0不在原來(lái)的范圍內(nèi)。這時(shí)候解決的方法是:讓padding部分足夠小,以至于maxmax(幾乎)不能取到padding部分,比如
max(x)=max(x?(1?m)×1010)max(x)=max(x?(1?m)×1010)
正常來(lái)說(shuō),神經(jīng)網(wǎng)絡(luò)的輸入輸出的數(shù)量級(jí)不會(huì)很大,所以經(jīng)過(guò)x?(1?m)×1010x?(1?m)×1010后,padding部分在?1010?1010這個(gè)數(shù)量級(jí)中上,可以保證取maxmax的話不會(huì)取到padding部分了。
處理softmax的padding也是如此。在Attention或者指針網(wǎng)絡(luò)時(shí),我們就有可能遇到對(duì)變長(zhǎng)的向量做softmax,如果直接對(duì)padding后的向量做softmax,那么padding部分也會(huì)平攤一部分概率,導(dǎo)致實(shí)際有意義的部分概率之和都不等于1了。解決辦法跟maxmax時(shí)一樣,讓padding部分足夠小足夠小,使得exex足夠接近于0,以至于可以忽略:
sofmax(x)=max(x?(1?m)×1010)sofmax(x)=max(x?(1?m)×1010)
上面幾個(gè)算子的mask處理算是比較特殊的,其余運(yùn)算的mask處理(除了雙向RNN),基本上只需要輸出
x?mx?m
就行了。
Keras實(shí)現(xiàn)要點(diǎn)?#
Keras自帶了mask功能,但是不建議用,因?yàn)樽詭У膍ask不夠清晰靈活,而且也不支持所有的層,強(qiáng)烈建議讀者自己實(shí)現(xiàn)mask。
近來(lái)開(kāi)源的好幾個(gè)模型都已經(jīng)給出了足夠多的mask案例,我相信讀者只要認(rèn)真去閱讀源碼,一定很容易理解mask的實(shí)現(xiàn)方式的,這里簡(jiǎn)單提一下幾個(gè)要點(diǎn)。一般來(lái)說(shuō)NLP模型的輸入是詞ID矩陣,形狀為[batch_size, seq_len][batch_size, seq_len],其中我會(huì)用0作為padding的ID,而1作為UNK的ID,剩下的就隨意了,然后我就用一個(gè)Lambda層生成mask矩陣:
# x是詞ID矩陣mask=Lambda(lambdax:K.cast(K.greater(K.expand_dims(x,2),0),'float32'))(x)
這樣生成的mask矩陣大小是[batch_size, seq_len, 1][batch_size, seq_len, 1],然后詞ID矩陣經(jīng)過(guò)Embedding層后的大小為[batch_size, seq_len, word_size][batch_size, seq_len, word_size],這樣一來(lái)就可以用mask矩陣對(duì)輸出結(jié)果就行處理了。這種寫(xiě)法只是我的習(xí)慣,并非就是唯一的標(biāo)準(zhǔn)。
結(jié)合:雙向RNN?#
剛才我們的討論排除了雙向RNN,這是因?yàn)镽NN是遞歸模型,沒(méi)辦法簡(jiǎn)單地mask(主要是逆向RNN這部分)。所謂雙向RNN,就是正反各做一次RNN然后拼接或者相加之類的。假如我們要對(duì)[1,0,3,4,5,0,0,0][1,0,3,4,5,0,0,0]做逆向RNN運(yùn)算時(shí),最后輸出的結(jié)果都會(huì)包含padding部分的0(因?yàn)閜adding部分在一開(kāi)始就參與了運(yùn)算)。因此事后是沒(méi)法排除的,只有在事前排除。
排除的方案是:要做逆向RNN,先將[1,0,3,4,5,0,0,0][1,0,3,4,5,0,0,0]反轉(zhuǎn)為[5,4,3,0,1,0,0,0][5,4,3,0,1,0,0,0],然后做一個(gè)正向RNN,然后再把結(jié)果反轉(zhuǎn)回去,要注意反轉(zhuǎn)的時(shí)候只反轉(zhuǎn)非padding部分(這樣才能保證遞歸運(yùn)算時(shí)padding部分始終不參與,并且保證跟正向RNN的結(jié)果對(duì)齊),這個(gè)tensorflow提供了現(xiàn)成的函數(shù)tf.reverse_sequence()。
遺憾的是,Keras自帶的Bidirectional并沒(méi)有這個(gè)功能,所以我重寫(xiě)了它,供讀者參考:
classOurBidirectional(OurLayer):"""自己封裝雙向RNN,允許傳入mask,保證對(duì)齊
? ? """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.namedefreverse_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')returnK.tf.reverse_sequence(x,seq_len,seq_dim=1)defcall(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)returnx*maskdefcompute_output_shape(self,input_shape):return(None,input_shape[0][1],self.forward_layer.units*2)
使用方法跟自帶的Bidirectional基本一樣的,只不過(guò)要多傳入mask矩陣,比如:
x=OurBidirectional(LSTM(128))([x,x_mask])
小結(jié)?#
Keras是一個(gè)極其友好、極其靈活的高層深度學(xué)習(xí)API封裝,千萬(wàn)不要聽(tīng)信網(wǎng)上流傳的“Keras對(duì)新手很友好,但是欠缺靈活性”的謠言~Keras對(duì)新手很友好,對(duì)老手更友好,對(duì)需要頻繁自定義模塊的用戶更更友好。
轉(zhuǎn)載到請(qǐng)包括本文地址:https://kexue.fm/archives/6810
更詳細(xì)的轉(zhuǎn)載事宜請(qǐng)參考:《科學(xué)空間FAQ》