論算法工程師的自我修養(yǎng)
前言
本文落筆于2021年的1月1日,是我2021年的第一篇文章。本文將根據我的實踐經驗,討論一個算法工程師,如何提升自我修養(yǎng),由菜鳥小白進化為高手。
本文同樣不關注某個具體算法那樣的“術”,也不像《推薦算法的"五環(huán)之歌"》講“算法”之道,而是聚集于“算法工程”之道,即如何將算法從紙面落地于實際項目中。畢竟大家都是工程師出身,做算法的目的,既不是在象牙塔中灌水發(fā)文章,也不是打Kaggle比賽那種"一錘子"買賣。我們做算法的終極目標,是要在復雜的互聯(lián)網工業(yè)環(huán)境中成功落地,能夠取得收益,最重要的,能夠持續(xù)取得收益。否則,掌握再多的NN、FM、Attention,也只不過是紙上談兵罷了。
理論化:扎實的知識基礎雖然講工程, 但還是需要對算法的充分理解做基礎。項目開始前,選擇最可能產生收益的算法來實現,算法遇到問題,找到最可能的原因,這些都需要扎實的算法理論為后盾。否則,如果你只會將別人口中的知名的算法挨個試一遍,在機器已經開始能夠自動調參,能自動搜索最優(yōu)網絡結構的今天,你作為一個工程師的價值在哪里?
體系化+脈絡化
現在講算法的文章不是太少,而是太多,信息爆炸。每年KDD, SIGIR, CIKM上有那么多中外的王婆一起賣瓜,各種各樣的NN、FM、Attention滿天飛,其中不乏實打實的干貨,更不缺乏濕漉漉的灌水文,讓算法新人無所適從。這是因為這些同學孤立地讀論文,結果只能是一葉障目,只見樹木,不見森林。輪到自己的項目,選作者來頭最大的、發(fā)布時間最新的來實現,與“聽小道消息買股票”沒啥區(qū)別。
正確的姿勢應該是,梳理一門學問的脈絡,構建自己的知識體系。只有這樣,才能真正將各篇論文中的觀點融匯貫通。在自己的項目中,采各家之所長,構建最適合自己項目的算法。我已經在《推薦算法的"五環(huán)之歌"》和《無中生有:論推薦算法中的Embedding思想》梳理了推薦算法的發(fā)展脈絡,請感興趣的同學移步閱讀。
重視基本功
現如今,有些人將各種NN+FM+Attention像搭積木一樣拼來拼去,組成一個全新的網絡結構,起個古怪的名字,就能夠在KDD, SIGIR, CIKM成功灌水,讓一眾算法新人趨之若鶩。我勸大家,不要沉迷于奇形怪狀的NN結構,千萬不要以為它們是能夠解決你問題的“銀彈”,更不要相信什么“深度學習使特征工程過時”這樣的外行話。
像我在《負樣本為王:評Facebook的向量化召回算法》一文中的觀點,“排序是特征的藝術,召回是樣本的藝術”,特征工程、樣本選擇這樣的基本功,才值得廣大算法同行重視。否則,喂入的樣本與特征都有問題,garbage in, garbage out,再fancy的NN也不過是沙灘上的城堡。
正確對待細節(jié)
關注細節(jié)
通過面試,我發(fā)現很多人好讀書,但不求甚解。知道很多算法,但一問細節(jié)都不掌握,孰不知,算法成敗的關鍵都在細節(jié)里。隨便舉幾個例子:
- Wide&Deep為什么用了兩個優(yōu)化器分別優(yōu)化Wide側和Deep側?
- DSSM中的負樣本為什么是隨機采樣得到的,而不用“曝光未點擊”當負樣本?
- 在DNN中加入position bias為什么不能和其他特征一樣喂入DNN底層,而必須通過一個淺層網絡接入?
細節(jié)搞清楚了,才算你真正搞明白了一門算法,未來你才能放心使用它。
也不要拘泥于細節(jié)
要從本質上理解算法,而不要形而上學。比如,Youtube召回模型與雙塔召回模型,前者使用sampled softmax loss,后者使用hinge loss或bpr loss,到底有沒有必要在你的推薦系統(tǒng)中都實現一遍?我個人的觀點,盡管二者在細節(jié)上有很多不同,但是它們本質上都屬于Pairwise LTR,優(yōu)化目標都是讓user embedding與正例item embedding足夠近,而與負例item embedding距離足夠遠。因此,沒必要在一個系統(tǒng)中分別實現,而應該合二為一,否則召回結果高度重合,對大盤指標發(fā)揮不了多少作用。
練習不依賴框架實現一遍算法
Keras、PyTorch之類的框架,將實現深度學習算法,變成了基本模塊的拼裝,大大降低了實現難度,但也此同時也使很多同學固步自封,沒有興趣再去了解算法細節(jié)。比如,如果你調用TensorFlow自帶DNNLinearCombinedClassifier來實現Wide&Deep,你能想到如何實現用兩個優(yōu)化器分布優(yōu)化Wide側與Deep側嗎?
掌握算法細節(jié)最徹底的方式,莫過于不借助高級框架,自己動手從頭實現一遍。在實現過程中,除了要保證準確無誤,還要考慮程序的運行效率。
如果時間不允許,看看別人是如何從頭實現的,對提升自己的算法功力,也大有裨益。對Wide&Deep實現細節(jié)感興趣的同學,可以看我的另一遍文章《用NumPy手工打造 Wide & Deep》。對FM感興趣的同學,強烈建議通讀一遍alphaFM的源代碼。
規(guī)范化:寫得一手好代碼算法落地的痛點
鏈條太長
落地一個算法,需要經過“文獻調研→算法選型→收集樣本→特征工程→模型編碼→離線測試→線上AB實驗”這漫長的鏈條,線上AB測試起碼要一周以上,得出的結論才solid。全新算法的落地,花費的時間以月計,牽扯算法、工程多方人員的配合。不到最后一刻,我們都不知道算法的成敗,但是無論哪個環(huán)節(jié)出錯,都可能導致算法落地功虧一簣。
不容易debug
如前所述,算法落地的中間環(huán)節(jié)太多,每個環(huán)節(jié)都可能出問題
- 一開始選擇的算法,可能壓根就不合適你們的場景
- 有臟數據。但是線上數據存在個別臟數據,再正常不過了。況且,在幾T的數據中查找?guī)浊l臟數據,無異于大海撈針
- 算法實現有bug
- 超參不合適。當算法沒效果時,很多算法同學不愿相信數據或模型有問題,覺得只要調調超參就能fix,但是大多數時候,這種想法不過一相情愿罷了。
而最困難的地方在于,你有可能壓根就察覺不出問題。線上AB測試有了正向效果,大家就覺得功德圓滿了,可以開心寫工作總結了。但是孰不知,代碼依然有bug,修正后,線上指標會有更明顯的提升。
解決痛點要靠“防患于未然”
既然算法項目不好debug,就盡量不要出bug。就好比,二戰(zhàn)中德國空軍涌現出各參戰(zhàn)國中最多的王牌飛行員,好幾位的擊落戰(zhàn)績過百,但是二戰(zhàn)德國空軍談不上是最優(yōu)秀的空軍。我個人最欣賞的是以色列空軍,“猶太長臂”追求的是,戰(zhàn)端一開,就把敵人的飛機都擊毀在地面,使其壓根沒有升空的機會。
如何才能做到少出、不出bug?一要靠良好的編程習慣,二要靠小心謹慎的態(tài)度。
良好的編程習慣
良好的編程習慣和你刷過多少leetcode題沒關系。而且就個人感受而言,前者要比后者更重要。因為,在日常算法編程工作中,我們很少用得上leetcode的解題思路,但是良好的編程習慣卻貫穿碼農生涯的始終。
良好的編程習慣,說來也不復雜,無外乎就那么幾條
- 花點心思在命名上,給類、函數、變量起個“見名知義”的好名字。
- 模塊化。不要一個功能,從頭寫到尾,寫成一個幾百行的大函數。而要將其拆分成其他函數或類。
- 注意代碼復用,最忌諱拷貝代碼。
- 開閉原則,代碼模塊應該對擴展開放,而對修改封閉。
- 單一職責原則,每個代碼模塊只負責單一功能,不要將雜七雜八的功能都寫到一個類或函數中。
看似簡單,但是這些習慣能夠使你的代碼清晰、易讀。而易讀的代碼容易發(fā)現問題。但是,這幾條規(guī)則,知易行難,做不到的人,歸根到底還是一個“懶”字。
另外,我強烈建議,限制使用jupyter notebook。可能是受許多公開課的影響,很多人喜歡在notebook中寫程序、調模型,畢竟jupyter notebook能夠邊寫代碼,邊運行查看結果,再加上圖表可視化,功能上的確很有吸引力。
但是,在notebook寫代碼,完全沒有條理,想到哪里,寫到哪里,你不會想到要劃分函數或類。另外,在notebook中非常容易就使用了全局變量,使你的代碼變成緊密耦合的意大利肉醬面。notebook的底層存儲格式,將算法邏輯代碼與頁面渲染代碼,雜揉一處,使版本控制失去了作用。因此,我對jupyter notebook的態(tài)度就是草稿紙,做一些前期的數據探索性分析還可以,實戰(zhàn)代碼免談。
小心謹慎的態(tài)度
我寫代碼,一來寫的時候,就小心遵循著以上那些良好的編程習慣,保持代碼清晰、易讀。小心到什么程度?比如,我將向量命名為user_embedding/doc_embedding,而不是embedding_user/embedding_doc。這是因為現在的IDE都有代碼自動提示功能,如果采用將共同部分作為名字的前綴,我擔心要用到embedding_user的地方,當我輸入embedding前綴時,IDE就自動提示embedding_doc,而我不小心一回車,就張冠李戴,犯下錯誤。
第二,寫完代碼后,不著急吼吼地就接入數據,開始訓練,我起碼要將代碼檢查上兩遍。檢查時,要設想各種corner case,比如各種可能的臟數據。而以前Quora上有一個問題就是“算法工程師在等待模型的訓練結果時做什么?”,而我會利用那段時間,再次檢查我的代碼。
總而言之,就是我們在開發(fā)算法代碼時,要小心、小心、再小心。因為每個環(huán)節(jié)犯下的bug,都有可能導致算法落地失敗,使你和團隊投入的時間精力付之東流。更可怕的是,由于大數據+黑盒模型的雙重影響,你犯下的bug會非常非常難于被發(fā)現。
數字化:構建模型的指標看板現如今,深度學習大行其道,在帶來模型性能提升的同時,也使模型愈發(fā)“黑盒”化,使之不容易調優(yōu),出現問題,也不好定位原因。
面對模型黑盒,打開它是一種思路。學術界提出很多特征解釋算法,但是受限于這些算法的復雜度,在工業(yè)界落地的并不多。另一種思路就是,沒必要打開黑盒,只要對模型的輸入輸出長期監(jiān)控,通過歷史數據的積累,也能對模型的行為建立合理的預期。一旦模型出現異常,我們也能夠很快發(fā)現并定位原因。本小節(jié)就是介紹如何實現這第2種思路。
接下來所說的指標分兩種,輸入模型的的上游數據的指標,和模型的離線評測指標,不包括線上業(yè)務指標。這是因為,第一,線上指標受其他非模型因素(e.g., 節(jié)假日或運營)的影響比較大;第二,線上業(yè)務指標會有專門的人員與工具去分析;第三,線上指標已經是target了,它出了問題只能告訴我們模型可能出了問題,但是對于定位問題還遠遠不夠。
重視離線評測指標
我認識一個有幾年工作經驗的老菜鳥 (這才是最可悲的),團隊分配他去做一個召回模型,很快跑出模型,離線AUC還可以(這就是理論水平不足埋下的隱患,用AUC是照搬排序時的經驗,而召回模型因為沒有真負樣本,很少用AUC評價),加上team leader是個工程出身,對算法一知半解,就批準他上線。接下來幾個星期,他就只知道催工程同學幫他上線模型。終于上線了,第一天的線上指標就不好,他選擇繼續(xù)觀察,同時嘗試不同的超參來撞大運。觀察了一個多月,線上指標不升反降,他又去懷疑是工程實現有問題,又去懷疑是ranker打壓了新召回。最后實在搞不定,找我?guī)兔ΑN夷梦易灾频恼倩胤治瞿_本一跑,召回的東西與用戶興趣與歷史,驢唇不對馬嘴,模型本身就有嚴重問題,換我是他的team leader,壓根就不會批準他上線,浪費大家的時間與精力。
這個案例留給我們的教訓就是,線上AB實驗的代價太高了
- 一方面,現在的模型越來越復雜,很多時候,上線需要花費工程同學的時間與精力,來保證線上服務的實時性能。更不用說,再小的流量也會影響用戶體驗。所以,如果你什么樣的模型都只能靠上線來一試效果,最終耗費的團隊對你的耐心與信任。
- 另一方面,線上AB實驗的耗時太長,為了得到solid的實驗結果,最起碼要進行一周,而且線上指標受非模型因素(e.g.,運營)的影響也比較大。所以,線上實驗對模型效果的反饋慢又有噪聲,一點也不“敏捷”。
所以,我們一定要重視模型的離線指標,盡可能在上線前發(fā)現模型可能存在的問題
- 看論文的時候,指標的具體數值可看可不看,反正都是王婆賣瓜,自賣自夸。但是,一定要看離線實驗的設計,看作者是怎么無偏地收集測試樣本,看作者從哪些維度來評測模型(不是只有準確性,還比如:推薦結果的豐富性、對冷啟是否友好、......等),看作者用了哪些指標來衡量模型在各個維度上的性能,看作者如何設計圖表使模型性能一目了然。
- 另一方面, 指標有時是片面的(比如,召回算法都只召回熱門item, 指標未必很差),這時候人工評測模型,雖然土,但是也能發(fā)揮效果。比如,我就自制了一個工具來幫助我評測召回模型,采樣1000個用戶,把每個用戶的畫像與點擊歷史展示在左邊,把模型召回的item展示在右邊。這個工具能讓我對召回效果具備最直觀的認識,比如:召回結果是不是太偏熱門,而缺乏個性化?召回結果是不是只集中于用戶的個別興趣,對用戶興趣的覆蓋面太窄?
指標監(jiān)控看板
運維有指標看板來監(jiān)控機群性能,算法也需要有自己的指標看板來監(jiān)控模型的性能。
- 我們要監(jiān)控上游的輸入數據,比如:每小時有多少樣本流入模型?樣本中的CTR和平均時長是多少?樣本中有多少user/doc/feature?feature經過hash后的沖突率是多少?……
- 離線評測不是只在上線之前做一次就完事了,而是也需要周期性運行,與同時段的上游輸入數據的指標相對比,才能讓我們對模型建立合理預期,將來定位問題時用得上。常見的監(jiān)控指標包括,auc/precision/recall等,另外還包括“今天的embedding相較于昨天embedding的變化幅度”這樣的對比指標。
舉個例子來說明,指標監(jiān)控在定位模型問題時發(fā)揮的重大作用。我們算法工程師的噩夢之一就是,之前好好的模型,線上指標(突然或緩慢)掉下來了。有一天,我就遇到了這樣的名場面。借助指標看板,
- 首先,我發(fā)現模型的離線指標也掉下來了,說明與線上工程實現或外界環(huán)境無關,應該是模型本身出問題了
- 接著,我發(fā)現輸入模型的樣本中的ctr下降了,說明上游數據出問題了
- 詢問了負責上游數據的同學,發(fā)現他們縮短了等待用戶反饋的時間,導致一部分正樣本由于未能等到用戶反饋被當成負樣本,從而定位了問題
看我的輕描淡寫,是不是感覺我解決這個問題超easy。實際情況恰恰相反,當時我還沒有構建出針對模型的指標看板,模型的離線指標和輸入樣本的CTR都是我遇到問題后再手工回溯的,花費了一兩天才定位問題。吃一塹,長一智,經此一役,我才充分認識到模型指標看板的重要性,現如今它成為了我團隊每個算法工程師的標配。
自動化:優(yōu)秀的工程師造工具算法工作畢竟是實驗科學,所以成功的關鍵是高效做實驗(由于線上容量有限,所以下文所說的實驗,主要指離線實驗)
- 談到高效,我們的第一反應是快。單位時間能做的實驗越多,嘗試的方向(不同特征+不同模型+不同超參)越多,越可能找到最優(yōu)解。為了能夠快速實驗,首先,實驗的代價必須小,為了新實驗要修改代碼是不可接受的,最好是修改一下配置,然后一鍵啟動;第二,能夠并行跑多組實驗,充分發(fā)揮算力,這就要求各實驗之間完全解耦,比如不會向同一個路徑下寫入結果。
- 但是,快只是高效的一個方面,能做的實驗多了,實驗管理就成了問題。
- 每個實驗,我們都要記錄:特征的版本、模型的版本、超參的版本、用哪段時間的數據訓練的、在哪段時間上測試的、各種離線指標、......
- 跑幾十、上百個離線實驗非常常見,這么多實驗數據,如果缺乏管理,也就無從分析比較,無法得到正確結論,實驗等于白做
- 手動管理,比如把實驗數據記錄在excel里,也不是不可以,估計也是大多數算法同行的選擇。但是,一來人懶,特別是模型結果不好的時候,更沒心情做記錄了;二來,實驗數據一般在服務器上,而excel在本地筆記本上,記錄時不得不ctrl-c/ctrl-v,出錯在所難免
- 因此,我們需要一個類似Source Version Control的工具,自動將實驗數據管理起來。
實驗管理工具
這個實驗管理工具,應該具備什么樣的功能?
- 我們下班前配置好要進行的實驗,然后一鍵啟動實驗
- 實驗管理系統(tǒng),自動向后臺機群提交多組實驗,多組實驗并行運行
- 每組實驗結束后,自動記錄實驗版本和結果
- 明早到了辦公室,打開電腦,昨天實驗的各種報表、曲線已經ready了
我在Youtube上看過一個Yelp工程師的介紹,聽說Yelp的實驗管理系統(tǒng)不僅具備以上功能,而且每個實驗結束后,還能向實驗發(fā)起人發(fā)一封郵件,郵件里有完整的實驗報告,包含圖表與曲線。實驗發(fā)起人用新參數回一封郵件,后臺服務器收到郵件后,就會用新超參數開啟下一輪實驗。
網上有一些開源的實驗管理工具,但是
- 一來,浸入式太強,需要我們修改代碼,削足適履地適應那套框架;
- 二來,運維也不可能允許你在公共機群內安裝一大堆亂七八糟的第三方框架
因此,我自己動手寫一個簡單的實驗管理工具
- 實驗是高度配置的,調節(jié)使用特征、調節(jié)網絡結構、調節(jié)超參數、......,都通過配置完成,而無需修改代碼
- 實驗一鍵啟動,各環(huán)節(jié)自動跨平臺銜接(數據準備在Spark集群完成,模型在分布式訓練集群完成訓練與評估),并具備一定容錯能力
- 多組實驗可以并行進行,而一個實驗內部“數據準備”與“訓練模型”異步進行,以減少等待
雖然前期投入時間多一些,但是有了實驗工具的幫助, 我團隊的實驗效率大大提高。目前,我們正在朝著Yelp那套工具的方向邁進,已經隱約看到了“邊吃著火鍋,唱著歌,邊調模型”的美好未來。
利用工具+制造工具
實驗管理工具只是一個例子。事實上,對待工具的態(tài)度,是小白與高手的區(qū)別之一:
- 小白的每個模型都是匠人手工版:為了新實驗,要手動改一堆代碼,要手動ctrl-c/ctrl-v記錄結果,為了等實驗結果不敢下班,......
- 高手善于利用工具,制造工具。為了每天節(jié)省5分鐘,他寧可花兩天時間造個工具。每天到點下班,吃著火鍋,唱著歌,等著實驗結果曲線發(fā)到手機上。
本文講述的,并非某個具體算法的那樣的“技”,而是聚集于“算法工程”之道,從“理論化+規(guī)范化+數字化+自動化”四個方面提出了方法論,根據我的實戰(zhàn)經驗,討論了如何將算法從紙面落地于一個互聯(lián)網級別的實際系統(tǒng)中,從而為你老板和你創(chuàng)造價值。
本文落筆于2021年的1月1日,是我本人在2021年的第一篇文章。請無處不在的互聯(lián)網記錄下我的新年心愿:希望我和我愛的人在2021年平安順遂!
- END -