“让Keras更酷一些!”:层与模型的重用技巧
作者丨蘇劍林
單位丨追一科技
研究方向丨NLP,神經(jīng)網(wǎng)絡(luò)
個人主頁丨kexue.fm
今天我們繼續(xù)來深挖 Keras,再次體驗(yàn) Keras 那無與倫比的優(yōu)雅設(shè)計(jì)。這一次我們的焦點(diǎn)是“重用”,主要是層與模型的重復(fù)使用。
所謂重用,一般就是奔著兩個目標(biāo)去:一是為了共享權(quán)重,也就是說要兩個層不僅作用一樣,還要共享權(quán)重,同步更新;二是避免重寫代碼,比如我們已經(jīng)搭建好了一個模型,然后我們想拆解這個模型,構(gòu)建一些子模型等。
基礎(chǔ)
事實(shí)上,Keras 已經(jīng)為我們考慮好了很多,所以很多情況下,掌握好基本用法,就已經(jīng)能滿足我們很多需求了。?
層的重用
層的重用是最簡單的,將層初始化好,存起來,然后反復(fù)調(diào)用即可:
x_in?=?Input(shape=(784,)) x?=?x_inlayer?=?Dense(784,?activation='relu')?#?初始化一個層,并存起來x?=?layer(x)?#?第一次調(diào)用 x?=?layer(x)?#?再次調(diào)用 x?=?layer(x)?#?再次調(diào)用要注意的是,必須先初始化好一個層,存為一個變量好再調(diào)用,才能保證重復(fù)調(diào)用的層是共享權(quán)重的。反之,如果是下述形式的代碼,則是非共享權(quán)重的:
x?=?Dense(784,?activation='relu')(x)? x?=?Dense(784,?activation='relu')(x)?#?跟前面的不共享權(quán)重 x?=?Dense(784,?activation='relu')(x)?#?跟前面的不共享權(quán)重模型重用
Keras 的模型有著類似層的表現(xiàn),在調(diào)用時可以用跟層一樣的方式,比如:
x_in?=?Input(shape=(784,)) x?=?x_inx?=?Dense(10,?activation='softmax')(x)model?=?Model(x_in,?x)?#?建立模型x_in?=?Input(shape=(100,)) x?=?x_inx?=?Dense(784,?activation='relu')(x) x?=?model(x)?#?將模型當(dāng)層一樣用model2?=?Model(x_in,?x)讀過 Keras 源碼的朋友就會明白,之所以可以將模型當(dāng)層那樣用,是因?yàn)?Model 本身就是繼承 Layer 類來寫的,所以模型自然也包含了層的一些相同特性。?
模型克隆
模型克隆跟模型重用類似,只不過得到的新模型跟原模型不共享權(quán)重了,也就是說,僅僅保留完全一樣的模型結(jié)構(gòu),兩個模型之間的更新是獨(dú)立的。Keras 提供了模型可用專用的函數(shù),直接調(diào)用即可:
from?keras.models?import?clone_modelmodel2?=?clone_model(model1)注意,clone_model 完全復(fù)制了原模型模型的結(jié)構(gòu),并重新構(gòu)建了一個模型,但沒有復(fù)制原模型的權(quán)重的值。也就是說,對于同樣的輸入,model1.predict 和 model2.predict 的結(jié)果是不一樣的。
如果要把權(quán)重也搬過來,需要手動 set_weights 一下:
model2.set_weights(K.batch_get_value(model1.weights))進(jìn)階
上述談到的是原封不等的調(diào)用原來的層或模型,所以比較簡單,Keras 都準(zhǔn)備好了。下面介紹一些復(fù)雜一些的例子。?
交叉引用
這里的交叉引用是指在定義一個新層的時候,沿用已有的某個層的權(quán)重,注意這個自定義層可能跟舊層的功能完全不一樣,它們之間純粹是共享了某個權(quán)重而已。比如,Bert 在訓(xùn)練 MLM 的時候,最后預(yù)測字詞概率的全連接層,權(quán)重就是跟 Embedding 層共享的。?
參考寫法如下:
class?EmbeddingDense(Layer):"""運(yùn)算跟Dense一致,只不過kernel用Embedding層的embedding矩陣"""def?__init__(self,?embedding_layer,?activation='softmax',?**kwargs):super(EmbeddingDense,?self).__init__(**kwargs)self.kernel?=?K.transpose(embedding_layer.embeddings)self.activation?=?activationself.units?=?K.int_shape(self.kernel)[1]def?build(self,?input_shape):super(EmbeddingDense,?self).build(input_shape)self.bias?=?self.add_weight(name='bias',shape=(self.units,),initializer='zeros')def?call(self,?inputs):outputs?=?K.dot(inputs,?self.kernel)outputs?=?K.bias_add(outputs,?self.bias)outputs?=?Activation(self.activation).call(outputs)return?outputsdef?compute_output_shape(self,?input_shape):return?input_shape[:-1]?+?(self.units,)#?用法 embedding_layer?=?Embedding(10000,?128) x?=?embedding_layer(x)?#?調(diào)用Embedding層 x?=?EmbeddingDense(embedding_layer)(x)?#?調(diào)用EmbeddingDense層提取中間層
有時候我們需要從搭建好的模型中提取中間層的特征,并且構(gòu)建一個新模型,在 Keras 中這同樣是很簡單的操作:
from?keras.applications.resnet50?import?ResNet50 model?=?ResNet50(weights='imagenet')Model(inputs=model.input,outputs=[model.get_layer('res5a_branch1').output,model.get_layer('activation_47').output,] )從中間拆開
最后,來到本文最有難度的地方了,我們要將模型從中間拆開,搞懂之后也可以實(shí)現(xiàn)往已有模型插入或替換新層的操作。這個需求看上去比較奇葩,但是還別說,stackoverflow 上面還有人提問過,說明這確實(shí)是有價(jià)值的。?
https://stackoverflow.com/questions/49492255/how-to-replace-or-insert-intermediate-layer-in-keras-model
假設(shè)我們有一個現(xiàn)成的模型,它可以分解為:
那可能我們需要將 h2 替換成一個新的輸入,然后接上后面的層,來構(gòu)建一個新模型,即新模型的功能是:
如果是 Sequential 類模型,那比較簡單,直接把 model.layers 都遍歷一邊,就可以構(gòu)建新模型了:
x_in?=?Input(shape=(100,)) x?=?x_infor?layer?in?model.layers[2:]:x?=?layer(x)model2?=?Model(x_in,?x)但是,如果模型是比較復(fù)雜的結(jié)構(gòu),比如殘差結(jié)構(gòu)這種不是一條路走到底的,就沒有這么簡單了。事實(shí)上,這個需求本來沒什么難度,該寫的 Keras 本身已經(jīng)寫好了,只不過沒有提供現(xiàn)成的接口罷了。為什么這么說,因?yàn)槲覀兺ㄟ^ model(x) 這樣的代碼調(diào)用已有模型的時候,
實(shí)際上 Keras 就相當(dāng)于把這個已有的這個 model 從頭到尾重新搭建了一遍,既然可以重建整個模型,那搭建“半個”模型原則上也是沒有任技術(shù)難度的,只不過沒有現(xiàn)成的接口。具體可以參考 Keras 源碼的 keras/engine/network.py 的 run_internal_graph 函數(shù):
https://github.com/keras-team/keras/blob/master/keras/engine/network.py
完整重建一個模型的邏輯在 run_internal_graph 函數(shù)里邊,并且可以看到它還不算簡單,所以如無必要我們最好不要重寫這個代碼。但如果不重寫這個代碼,又想調(diào)用這個代碼,實(shí)現(xiàn)從中間層拆解模型的功能,唯一的辦法是“移花接木”了:通過修改已有模型的一些屬性,欺騙一下 run_internal_graph 函數(shù),使得它以為模型的輸入層是中間層,而不是原始的輸入層。有了這個思想,再認(rèn)真讀讀 run_internal_graph 函數(shù)的代碼,就不難得到下述參考代碼:
def?get_outputs_of(model,?start_tensors,?input_layers=None):"""start_tensors為開始拆開的位置"""#?為此操作建立新模型model?=?Model(inputs=model.input,outputs=model.output,name='outputs_of_'?+?model.name)#?適配工作,方便使用if?not?isinstance(start_tensors,?list):start_tensors?=?[start_tensors]if?input_layers?is?None:input_layers?=?[Input(shape=K.int_shape(x)[1:],?dtype=K.dtype(x))for?x?in?start_tensors]elif?not?isinstance(input_layers,?list):input_layers?=?[input_layers]#?核心:覆蓋模型的輸入model.inputs?=?start_tensorsmodel._input_layers?=?[x._keras_history[0]?for?x?in?input_layers]#?適配工作,方便使用if?len(input_layers)?==?1:input_layers?=?input_layers[0]#?整理層,參考自?Model?的?run_internal_graph?函數(shù)layers,?tensor_map?=?[],?set()for?x?in?model.inputs:tensor_map.add(str(id(x)))depth_keys?=?list(model._nodes_by_depth.keys())depth_keys.sort(reverse=True)for?depth?in?depth_keys:nodes?=?model._nodes_by_depth[depth]for?node?in?nodes:n?=?0for?x?in?node.input_tensors:if?str(id(x))?in?tensor_map:n?+=?1if?n?==?len(node.input_tensors):if?node.outbound_layer?not?in?layers:layers.append(node.outbound_layer)for?x?in?node.output_tensors:tensor_map.add(str(id(x)))model._layers?=?layers?#?只保留用到的層#?計(jì)算輸出outputs?=?model(input_layers)return?input_layers,?outputs用法:
from?keras.applications.resnet50?import?ResNet50 model?=?ResNet50(weights='imagenet')x,?y?=?get_outputs_of(model,model.get_layer('add_15').output )model2?=?Model(x,?y)代碼有點(diǎn)長,但其實(shí)邏輯很簡單,真正核心的代碼只有三行:
model.inputs?=?start_tensors model._input_layers?=?[x._keras_history[0]?for?x?in?input_layers] outputs?=?model(input_layers)也就是覆蓋模型的 model.inputs 和 model._input_layers 就可以實(shí)現(xiàn)欺騙模型從中間層開始構(gòu)建的效果了,其余的多數(shù)是適配工作,不是技術(shù)上的,而 model._layers = layers 這一句是只保留了從中間層開始所用到的層,只是為了統(tǒng)計(jì)模型參數(shù)量的準(zhǔn)確性,如果去掉這一部分,模型的參數(shù)量依然是原來整個 model 那么多。
小結(jié)
Keras 是最讓人賞心悅目的深度學(xué)習(xí)框架,至少到目前為止,就模型代碼的可讀性而言,沒有之一。可能讀者會提到 PyTorch,誠然 PyTorch 也有不少可取之處,但就可讀性而言,我認(rèn)為是比不上 Keras 的。
在深究 Keras 的過程中,我不僅驚嘆于 Keras 作者們的深厚而優(yōu)雅的編程功底,甚至感覺自己的編程技能也提高了不少。不錯,我的很多 Python 編程技巧,都是從讀 Keras 源碼中學(xué)習(xí)到的。
點(diǎn)擊以下標(biāo)題查看作者其他文章:?
基于DGCNN和概率圖的輕量級信息抽取模型
#投 稿 通 道#
?讓你的論文被更多人看到?
如何才能讓更多的優(yōu)質(zhì)內(nèi)容以更短路徑到達(dá)讀者群體,縮短讀者尋找優(yōu)質(zhì)內(nèi)容的成本呢?答案就是:你不認(rèn)識的人。
總有一些你不認(rèn)識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學(xué)者和學(xué)術(shù)靈感相互碰撞,迸發(fā)出更多的可能性。
PaperWeekly 鼓勵高校實(shí)驗(yàn)室或個人,在我們的平臺上分享各類優(yōu)質(zhì)內(nèi)容,可以是最新論文解讀,也可以是學(xué)習(xí)心得或技術(shù)干貨。我們的目的只有一個,讓知識真正流動起來。
??來稿標(biāo)準(zhǔn):
? 稿件確系個人原創(chuàng)作品,來稿需注明作者個人信息(姓名+學(xué)校/工作單位+學(xué)歷/職位+研究方向)?
? 如果文章并非首發(fā),請?jiān)谕陡鍟r提醒并附上所有已發(fā)布鏈接?
? PaperWeekly 默認(rèn)每篇文章都是首發(fā),均會添加“原創(chuàng)”標(biāo)志
? 投稿郵箱:
? 投稿郵箱:hr@paperweekly.site?
? 所有文章配圖,請單獨(dú)在附件中發(fā)送?
? 請留下即時聯(lián)系方式(微信或手機(jī)),以便我們在編輯發(fā)布時和作者溝通
?
現(xiàn)在,在「知乎」也能找到我們了
進(jìn)入知乎首頁搜索「PaperWeekly」
點(diǎn)擊「關(guān)注」訂閱我們的專欄吧
關(guān)于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報(bào)道人工智能前沿論文成果的學(xué)術(shù)平臺。如果你研究或從事 AI 領(lǐng)域,歡迎在公眾號后臺點(diǎn)擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點(diǎn)擊 |?閱讀原文?| 查看作者博客
總結(jié)
以上是生活随笔為你收集整理的“让Keras更酷一些!”:层与模型的重用技巧的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑黑屏怎么开机进去u盘启动不了怎么回事
- 下一篇: 节后收心困难?这15篇论文,让你迅速找回