不要】重復(fù)自己*——如何為現(xiàn)代機(jī)器學(xué)習(xí)設(shè)計(jì)開源庫
?不要重復(fù)自己*
如何為現(xiàn)代機(jī)器學(xué)習(xí)設(shè)計(jì)開源庫
?? Transformers 設(shè)計(jì)理念
“不要重復(fù)自己 (Don’t Repeat Yourself)” ,或 DRY,是廣為人知的軟件開發(fā)原則。該原則出自《程序員修煉之道: 從小工到專家》 (英文名為 The pragmatic programmer),這是代碼設(shè)計(jì)領(lǐng)域迄今為止閱讀量最大的一本書。該原則言簡(jiǎn)意賅,即: 重用而不要重寫其他地方已有的邏輯。這可以確保代碼保持同步,使其更易于維護(hù)且更健壯。該做法使得對(duì)公共代碼邏輯的任何更改都會(huì)統(tǒng)一地影響所有依賴該公共代碼邏輯的代碼。
乍一看,Hugging Face transformers 庫的設(shè)計(jì)與 DRY 原則背道而馳。注意力機(jī)制的代碼被復(fù)制到不同的模型文件里不下 50 次。有時(shí)整個(gè) BERT 模型的代碼都會(huì)被復(fù)制到其他模型文件中。貢獻(xiàn)者在添加新模型時(shí),如果新模型用到了現(xiàn)有的某個(gè)模型,我們經(jīng)常強(qiáng)制要求他們把該現(xiàn)有模型的所有代碼復(fù)制到新模型代碼中,連一個(gè)小小的邏輯調(diào)整也不例外。我們?yōu)槭裁匆@么做?是因?yàn)槲覀兲珣幸只蚴且驗(yàn)槲覀儫o力承擔(dān)將所有公共邏輯集中到一個(gè)地方所帶來的工作量?
不,我們并不懶 —— 不在 transformers 庫中使用 DRY 原則是有意之舉。我們決定采用一種與 DRY 不同的設(shè)計(jì)原則,我們稱之為 單模型文件 策略 (single model file policy)。 單一模型文件 策略要求,任何模型的所有代碼都只應(yīng)該放在一個(gè)文件中,這個(gè)文件就是該模型自己的模型文件。如果讀者想了解 BERT 如何是進(jìn)行推理的,他/她只需要閱讀 BERT 的 modeling_bert.py
文件即可。通常情況下,我們拒絕任何將不同模型的相同子模塊抽象并集中到一個(gè)新文件中的嘗試。我們不想要一個(gè)包含所有可能的注意力機(jī)制的 attention_layer.py
。
我們?yōu)楹巫鞒鲞@樣的設(shè)計(jì)呢?我們將原因概括如下:
1. Transformers 生于開源,服務(wù)開源
2. 我們的產(chǎn)品是模型,我們的客戶是那些閱讀或修改模型代碼的用戶。
3. 機(jī)器學(xué)習(xí)領(lǐng)域發(fā)展極其迅速。
4. 機(jī)器學(xué)習(xí)模型是靜態(tài)的。
1. 生于開源,服務(wù)開源
Transformers 積極鼓勵(lì)來自外部的貢獻(xiàn)。貢獻(xiàn)一般有錯(cuò)誤修復(fù)和新模型添加兩類。如果有人發(fā)現(xiàn)了某個(gè)模型文件中的錯(cuò)誤,我們希望他/她很容易就能修復(fù)它。沒有什么比修復(fù)了一個(gè) bug 卻發(fā)現(xiàn)它導(dǎo)致了其他模型上的 100 個(gè) bug 更令人沮喪的了。
因?yàn)槊總€(gè)模型代碼相互獨(dú)立,所以對(duì)于只了解他/她正在用的那個(gè)模型的人來說,修復(fù)它會(huì)輕松很多。同樣,如果只添加一個(gè)新的模型文件,添加新的模型代碼以及 review 相應(yīng)的 PR 會(huì)更容易。貢獻(xiàn)者不必弄清楚如何在不破壞現(xiàn)有模型的情況下向公共的注意力機(jī)制代碼添加新功能,代碼評(píng)審者也缺省地知道這個(gè) PR 不會(huì)破壞任何一個(gè)現(xiàn)有模型。
2. 模型代碼即產(chǎn)品
我們假設(shè) transformers 庫的很多用戶不僅會(huì)閱讀文檔,而且會(huì)查看實(shí)際模型代碼并有可能對(duì)其進(jìn)行修改。鑒于 transformers 庫被 fork 了 1 萬多次,我們的 transformers 論文被引用了 1 千多次,這個(gè)假設(shè)應(yīng)該是站得住腳的。
因此,最重要的是讓第一次閱讀 transformers 模型代碼的人能夠輕松理解并修改它。在單個(gè)模型文件中囊括該模型的所有必要邏輯組件有助于提高可讀性和可修改性。處于同樣的目的,我們也非常關(guān)注變量及方法命名的合理性,我們更喜歡表達(dá)力強(qiáng)/可讀性強(qiáng)的代碼,而不盲目追求短代碼。
3. 機(jī)器學(xué)習(xí)正以驚人的速度發(fā)展
機(jī)器學(xué)習(xí)領(lǐng)域,尤其是神經(jīng)網(wǎng)絡(luò)領(lǐng)域的研究發(fā)展非常迅速。一年前最先進(jìn)的模型今天可能已經(jīng)過時(shí)了。我們甚至不知道明年會(huì)流行哪一種注意力機(jī)制、位置嵌入或架構(gòu)。因此,我們無法定義適用于所有模型的標(biāo)準(zhǔn)模板。
例如,兩年前,人們可能將 BERT 的自注意力層定義為所有 transformer 模型的標(biāo)準(zhǔn)注意力層。從邏輯上講,“標(biāo)準(zhǔn)”注意力函數(shù)可以移到一個(gè)集中性的 attention.py
文件中。但是隨后出現(xiàn)了在每層中添加相對(duì)位置嵌入的注意力層 (如 T5),多種不同形式的分塊注意力層 (Reformer,Longformer,BigBird),以及將位置嵌入和詞嵌入分離的注意力機(jī)制 (DeBERTa) …… 每當(dāng)發(fā)生這類事情時(shí),我們都不得不問自己是否應(yīng)該調(diào)整“標(biāo)準(zhǔn)”注意力函數(shù),還是說向 attention.py
添加一個(gè)新的注意力函數(shù)更好。但如果要添加新的注意力函數(shù),我們?cè)撊绾蚊兀?attention_with_positional_embd
, reformer_attention
還有 deberta_attention
?
給機(jī)器學(xué)習(xí)模型的組件起通用的名字是危險(xiǎn)的,因?yàn)殛P(guān)于名字意義的解釋可能會(huì)很快改變或過時(shí)。例如,分塊注意力指的是 GPTNeo 的分塊注意力,還是 Reformer 的分塊注意力,抑或是 BigBird 的分塊注意力?注意層是自注意層、交叉注意層,還是兩者都包含?如果我們最終決定用模型名稱來命名注意力層,我們何不直接把這個(gè)注意力函數(shù)放在相應(yīng)的模型文件中?
4. 機(jī)器學(xué)習(xí)模型是靜態(tài)的
Transformers 庫是不同研究團(tuán)隊(duì)創(chuàng)建的統(tǒng)一且完善的機(jī)器學(xué)習(xí)模型的集合。每個(gè)機(jī)器學(xué)習(xí)模型通常都對(duì)應(yīng)一篇論文及其官方 GitHub 存儲(chǔ)庫。機(jī)器學(xué)習(xí)模型一旦發(fā)布,后面就很少會(huì)對(duì)其進(jìn)行調(diào)整或更改。
相反,研究團(tuán)隊(duì)傾向于發(fā)布基于之前模型構(gòu)建的新模型,而很少對(duì)已發(fā)布的代碼進(jìn)行重大更改。在決定 transformers 庫的設(shè)計(jì)原則時(shí),這是一個(gè)重要的認(rèn)知。這意味著一旦將模型架構(gòu)添加到 transformers 中,模型的基本組件就不會(huì)再改變。有可能會(huì)發(fā)現(xiàn)并修復(fù)一些錯(cuò)誤,有可能會(huì)重命名方法或變量,也有可能對(duì)模型的輸出或輸入格式進(jìn)行微調(diào),但一般不會(huì)改動(dòng)模型的核心組件。因此,對(duì) transformers 中的所有模型進(jìn)行大的全局性改動(dòng)的需求大大減少,這使得每個(gè)邏輯模塊只存在一次這件事情變得不那么重要,因?yàn)槲覀兒苌俑膭?dòng)它。
第二個(gè)認(rèn)知是模型之間 不 存在雙向依賴。新發(fā)布的模型可能依賴于現(xiàn)存模型,但很明顯,現(xiàn)存模型在邏輯上并不依賴于其前面的模型。例如,T5 部分建立在 BERT 之上,因此 T5 的模型代碼在邏輯上可能依賴于 BERT 的模型代碼,但 BERT 在邏輯上絕不可能依賴于 T5。因此,重構(gòu) BERT 的注意力功能以使其滿足 T5 的要求這件事在邏輯上不合理 —— 閱讀 BERT 的注意力層代碼的人不需要對(duì) T5 有任何了解。同樣,這也促使我們不要將注意力層等組件集中到所有模型都可以訪問的公共模塊中。
另一方面,新模型的代碼在邏輯上可能對(duì)其前面的模型有一定的依賴性。例如,DeBERTa-v2 的代碼確實(shí)在某種程度上依賴于 DeBERTa 的代碼。通過確保 DeBERTa-v2 的模型代碼與 DeBERTa 的保持同步,可以顯著提高可維護(hù)性。理論上來講,修復(fù) DeBERTa 中的 bug 的同時(shí)也應(yīng)該修復(fù) DeBERTa-v2 中的相同 bug。我們?nèi)绾卧诖_保新模型與其依賴的模型保持同步的同時(shí)維持 單模型文件 策略?
現(xiàn)在,我們解釋一下為什么我們?cè)?“重復(fù)自己” 之后加上星號(hào)*。我們不會(huì)無腦復(fù)制粘貼現(xiàn)有模型的相應(yīng)代碼,即使看上去我們好像就是這么做的。 Transformers 的核心維護(hù)者之一 Sylvain Gugger 發(fā)現(xiàn)了一種既尊重 單文件策略 又將可維護(hù)性成本控制在一定范圍內(nèi)的好機(jī)制。該機(jī)制,我們暫且稱其為 “復(fù)制機(jī)制” ,允許我們使用 #Copied from <predecessor_model>.<function>
語句標(biāo)記某些邏輯組件 (如注意力層函數(shù)),從而強(qiáng)制被標(biāo)記的當(dāng)前代碼與 <predecessor_model>
的 <function>
相同。例如,DeBERTa-v2 類 里的這行代碼強(qiáng)制整個(gè) DebertAV2Layer
類除了類名前綴 DeBERTAV2
之外須與 DebertaLayer 類 相同。如此可以看到,復(fù)制機(jī)制使模型代碼非常容易理解,同時(shí)又顯著減少了維護(hù)成本。如果有人改動(dòng)了某個(gè)模型的某個(gè)函數(shù),則我們可以使用一個(gè)自動(dòng)化工具來更正依賴于這個(gè)模型的這個(gè)函數(shù)的所有其他模型的相應(yīng)代碼。
缺點(diǎn)
顯然,單文件策略也有缺點(diǎn),我們?cè)谶@里簡(jiǎn)單提兩個(gè)。
Transformers 的一個(gè)主要目標(biāo)是為所有模型的推理和訓(xùn)練提供統(tǒng)一的 API,以便用戶可以在不同模型之間快速切換。但是,如果不允許模型文件使用抽象這一設(shè)計(jì)模式,則確??缒P偷慕y(tǒng)一 API 會(huì)困難得多。我們通過運(yùn)行 大量 測(cè)試 (截至本文撰寫時(shí),每天需要運(yùn)行大約 2 萬次測(cè)試) 來解決這個(gè)問題,以確保模型遵循一致的 API。在這種情況下,單文件策略要求我們?cè)谠u(píng)審新模型和新測(cè)例時(shí)非常嚴(yán)格。
其次,有很多研究?jī)H針對(duì)機(jī)器學(xué)習(xí)模型的單個(gè)組件。 例如 ,有研究團(tuán)隊(duì)會(huì)致力于研究一種適用于所有現(xiàn)有預(yù)訓(xùn)練模型的注意力機(jī)制的新形式,如 Rethinking Attention with Performers 一文所做的。我們應(yīng)該如何將此類研究納入 transformers 庫?確實(shí)不好弄。我們應(yīng)該改變所有現(xiàn)有模型嗎?這將違背上文中的第 3 點(diǎn)和第 4 點(diǎn)。還是我們應(yīng)該添加 100 多個(gè)新的模型文件,每個(gè)文件都以 Performer...
為前綴?這也很荒謬。遺憾的是,對(duì)此類情況我們還沒有好的解決方案,我們只能選擇不將該論文的成果集成到 transformers 中。等這篇論文獲得更多關(guān)注并有了性能強(qiáng)大的預(yù)訓(xùn)練 checkpoint,我們可能會(huì)為其中最重要的模型添加一個(gè)新的模型文件,例如目前我們已有 modeling_performer_bert.py
。
總結(jié)
總而言之,在 ?? Hugging Face,我們堅(jiān)信 單文件策略 是適合 transformers 的代碼設(shè)計(jì)理念。
你的想法如何?我們很想聽聽你的意見!如果你有話要說,歡迎留言。
英文原文:https://hf.co/blog/transformers-design-philosophy
原文作者: Patrick von Platen
譯者: Matrix Yao (姚偉峰),英特爾深度學(xué)習(xí)工程師,工作方向?yàn)?transformer-family 模型在各模態(tài)數(shù)據(jù)上的應(yīng)用及大規(guī)模模型的訓(xùn)練推理。
審校/排版: zhongdongy (阿東)