超詳細:完整的推薦系統架構設計

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"架構設計概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"架構設計是一個很大的話題,這裏只討論和推薦系統相關的部分。更具體地說,我們主要關注的是算法以及其他相關邏輯在時間和空間上的關係——這樣一種邏輯上的架構關係。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在前面的章節中我們講到了很多種算法,每種算法都是用來解決整個推薦系統流程中的某個問題的。我們的最終目標是將這些算法以合理的方式組合起來,形成一整套系統。在這個過程中,可用的組合方式有很多,每種方式都有舍有得,但每種組合方式都可被看作一種架構。這裏要介紹的就是一些經過實踐檢驗的架構層面的最佳實踐,以及對這些最佳實踐在不同應用場景下的分析。除此之外,還希望能夠通過把各種推薦算法放在架構的視角和場景下重新審視,讓讀者對算法間的關係有更深入的理解,從全局的角度看待推薦系統,而不是隻看到一個個孤立的算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"架構設計的本質之一是平衡和妥協。一個推薦系統在不同的時期、不同的數據環境、不同的應用場景下會選擇不同的架構,在選擇時本質上是在平衡一些重要的點。下面介紹幾個常用的平衡點。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1. 個性化vs複雜度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"個性化是推薦系統作爲一個智能信息過濾系統的安身立命之本,從最早的熱榜,到後來的公式規則,再到著名的協同過濾算法,最後到今天的大量使用機器學習算法,其主線之一就是爲用戶提供個性化程度越來越高的體驗,讓每個人看到的東西都儘量差異化,並且符合個人的喜好。爲了達到這一目的,系統的整體複雜度越來越高,具體表現爲使用的算法越來越多、算法使用的數據量和數據維度越來越多、機器學習模型使用的特徵越來越多,等等。同時,爲了更好地支持這些高複雜度算法的開發、迭代和調試,又衍生出了一系列對應的配套系統,進一步增加了整個系統的複雜度。可以說整個推薦邏輯鏈條上的每一步都被不斷地細化分析和優化,這些不同維度的優化橫縱交織,構造出了一個整體複雜度非常高的系統。從機器學習理論的角度來類比,如果把推薦系統整體看作一個巨大的以區分用戶爲目標的機器學習模型,則可以認爲複雜度的增加對應着模型中特徵維度的增加,這使得模型的VC維不斷升高,對應着可分的用戶數不斷增加,進而提高了整個空間中用戶的個性化程度。這條通過不斷提高系統複雜度來提升用戶個性化體驗的路線,也是近年來推薦系統發展的主線之一。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2. 時效性vs計算量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦系統中的時效性概念體現在實時服務的響應速度、實時數據的處理速度以及離線作業的運行速度等幾個方面。這幾個速度從時效性角度影響着推薦系統的效果,整體上講,運行速度越快,耗時越少,得到的效果越好。這是因爲響應速度越快,意味着對用戶行爲、物品信息變化的感知越快,感知後的處理速度越快,處理後結果的反饋就越快,最終體現到用戶體驗上,就是系統更懂用戶,更快地對用戶行爲做出了反應,從而產生了更好的用戶體驗。但這些時效性的優化,帶來的是更大的計算量,計算量又對應着複雜的實現邏輯和更多的計算資源。在設計得當的前提下,這樣的付出通常是值得的。如同前面章節中介紹過的,時效性優化是推薦系統中非常重要的一類優化方法和優化思路,但由此帶來的計算壓力和系統設計的複雜度也是必須要面對的。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3. 時間vs空間"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時間和空間之間的平衡關係可以說是計算機系統中最爲本質的關係之一,在推薦系統中也不例外。時間和空間這一對矛盾關係在推薦系統中的典型表現,主要體現在對緩存的使用上。緩存通常用來存儲一些計算代價較高以及相對靜態變化較少的數據,例如用戶的一些畫像標籤以及離線計算的相關性結果等。但是隨着越來越多的實時計算的引入,緩存的使用也越來越廣泛,常常在生產者和消費者之間起到緩衝的作用,使得二者可以解耦,各自異步進行。例如實時用戶興趣計算這一邏輯,如果沒有將之前計算的興趣緩存起來,那麼在每次需要用戶興趣時都要實時計算一次,並要求在較短的時間內返回結果,這對計算性能提出了較高的要求。但如果中間有一層緩存作爲緩衝,則需求方可以直接從緩存中取來結果使用。這在結果的實時性和新鮮度上雖然做了一定的妥協,但卻能給性能提升帶來極大的幫助。這樣就將生產和消費隔離開來,生產者可以根據具體情況選擇生產的方式和速度。當然,仍然可以努力提高生產速度,生產速度越快,緩存給時效性帶來的損失就越小,消費者不做任何改動就可以享受到這一提升效果。所以說,這種利用緩存來解耦系統,帶來性能上的提升以及開發的便利,也是在推薦系統架構設計中需要掌握的一種通用的思路。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面介紹的一些基本性原則貫穿着推薦系統架構設計的方方面面,是一些具有較高通用性的思路,掌握這些思路,可以產生出很多具體的設計和方法;反過來,每一種設計技巧或方法,也都可以映射到一個或幾個這樣的高層次抽象原則上來。這種自頂向下的思維學習方法對於推薦系統的架構設計是非常重要的,並且可以推廣到很多其他系統的設計中。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"系統邊界和外部依賴"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"架構設計的第一步是確定系統的邊界。所謂邊界,就是區分什麼是這個系統要負責的,也就是邊界內的部分,以及什麼是這個模型要依賴的,也就是邊界外的部分。劃分清楚邊界,意味着確定了功能的邊界以及團隊的邊界,能夠讓後期的工作都專注於核心功能的設計和實現。反之,如果系統邊界沒有清晰的定義,可能會在開發過程中無意識地侵入其他系統中,形成冗餘甚至矛盾,或者默認某些功能別人會開發而將其忽略掉。無論哪種情況,都會影響系統的開發乃至最終的運轉。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"系統邊界的確定,簡單來說,就是在輸入方面確定需要別人給我提供什麼,而在輸出方面確定我要給別人提供什麼。在輸入方面,就是判斷什麼輸入是需要別人提供給我的,要把握的主要原則包括:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個數據或服務是否與我的業務強相關。在推薦業務中用到的每個東西,並不是都與推薦業務強相關,例如電商推薦系統中的商品信息,只有與推薦業務強相關的服務才應該被納入推薦系統的邊界中。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個數據或服務除了我的業務在使用,是否還有其他業務也在使用。例如上面說到的商品信息服務,除了推薦系統在使用,其他子系統也在廣泛使用,那麼顯然它應該是一個外部依賴。也有例外情況,例如推薦系統要用到一些其他系統都用不到的商品信息,這時候,雖然理論上應該升級商品信息服務來支持推薦系統,但由於其他地方都用不到這些信息,因此很多時候可能需要推薦系統的負責團隊來實現這樣一個定製化服務。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依照此原則,圖11-1展示了推薦系統的主要外部依賴。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d4/d4b50471ddcae939dbc21628567a6f96.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖11-1 推薦系統的主要外部依賴"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1. 數據依賴"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦系統作爲一個典型的數據算法系統,數據是其最重要的依賴。這裏面主要包括用戶行爲數據和物品數據兩大類,前面介紹的各種算法幾乎都是以這兩種數據作爲輸入進行計算的。這些數據除了爲推薦系統所用,它們也是搜索、展示等其他重要系統的輸入數據,所以作爲通用的公共數據和服務,顯然不應該在推薦系統的邊界內部,而應該是外部依賴。需要特別指出的是,雖然有專門的團隊負責行爲數據的收集,但是收集到的數據是否符合推薦系統的期望卻不是一件可以想當然的事情。例如,對於結果展示的定義,數據收集團隊認爲前端請求到了結果就是展示,但對於推薦系統來說,只有用戶真正看見了纔是真實的展示。其中的原因在於數據收集團隊並不直接使用數據,那麼他們就無法保證數據的正確性,這時就需要具體使用數據的業務方,在這裏是推薦團隊,來和他們一起確認數據收集的邏輯是正確的。如果數據收集的邏輯不正確,後面的算法邏輯就是在做無用功。花在確保數據正確上的精力和資源,幾乎總是有收益的。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2. 平臺工具依賴"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦系統是一個計算密集型的系統,需要對各種形態的數據做各種計算處理,在此過程中,需要一整套計算平臺工具的支持,典型的如機器學習平臺、實時計算平臺、離線計算平臺、其他平臺工具等。在一個較爲理想的環境中,這些平臺工具都是由專門的團隊來構建和維護的。而在一些場景下,推薦系統可能是整個組織中最早使用這些技術的系統,推薦業務也還沒有重要和龐大到需要老闆專門配備一個平臺團隊爲之服務的程度,在這種情況下,其中的一些平臺工具就需要推薦系統的團隊自己負責來構建和維護了。爲了簡化邏輯,下面我們假設這些平臺工具都是獨立於推薦系統存在的,屬於推薦系統的外部依賴。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在對外輸出方面,系統邊界的劃定會根據公司組織的不同有所差異。例如,在一些公司中,推薦團隊負責的是與推薦相關的整個系統,在輸出方面的體現就是從算法邏輯到結果展示,這時候系統的邊界就要延伸到最終的結果展示。而在另外一些公司中,前端展示是由一個大團隊統一負責的,這時候推薦系統只需要給出要展示的物品ID和相關展示信息即可,前端團隊會負責統一展示這些物品信息。這兩種模式沒有絕對的好壞之分,重要的是要與整個技術團隊的規劃和架構相統一。在本書中,爲了敘述簡便,我們不討論前端展示涉及的內容,只專注於推薦結果的生產邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦系統的效果和性能在一定程度上取決於這些依賴系統,所以在尋求推薦系統的優化目標時,目光不能只看到推薦系統本身,很多時候這些依賴系統也是重要的效果提升來源。例如,物品信息的變更如果能被更快地通知到推薦系統,那麼推薦系統的時效性就會更好,給到用戶的結果也就會更好;再如,用戶行爲數據收集的準確性能有所提高的話,對應的相關性算法的準確性也會隨之提高。在有些情況下,外部系統升級會比優化算法有更大的效果提升。當然,推薦系統的問題也可能來自這些外部的依賴系統。例如,前端渲染展示速度的延遲會導致用戶點擊率的顯著下降,因爲這會讓用戶失去耐心。所以,當推薦系統指標出現下降時,不光要從內部找問題,也要把思路拓展到系統外部,從全局的角度去找問題。綜合來講,外部依賴的存在啓發我們要從全鏈條、全系統的角度來看問題,找問題,以及設計優化方法。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"離線層、在線層和近線層架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"架構設計有很多不同的切入方式,最簡單也是最常用的一種方式就是先決定某個模塊或邏輯是運行在離線層、在線層還是近線層。這三層的對比如表11-1所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/da/daae1b046e6ed75e54462a3f0bb130be.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任何使用非實時數據、提供非實時服務的邏輯模塊,都可以被定義爲離線模塊。其典型代表是離線的協同過濾算法,以及一些離線的標籤挖掘類算法。離線層通常用來進行大數據量的計算,由於計算是離線進行的,因此用到的數據也都是非實時數據,最終會產出一份非實時的離線數據,供下游進一步處理使用。與離線層相對的是在線層,也常被稱爲服務層,這一層的核心功能是對外提供服務,實時處理調用方的請求。這一層的典型代表是推薦系統的對外服務接口,接受實時調用並返回結果。在線層提供的服務是實時的,但用到的數據卻不一定侷限於實時數據,也可以使用離線計算好的各種數據,例如相關性數據或標籤數據等,但前提是這些數據已經以對實時友好的形態被存儲起來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"近線層則處於離線層和在線層的中間位置,是一個比較奇妙的層。這一層的典型特點就是:使用實時數據(也會使用非實時數據),但不提供實時服務,而是提供一種近實時的服務。所謂近實時指的是越快越好,但並不強求像在線層一樣在幾十毫秒內給出結果,因爲通常在近線層計算的結果會寫入緩存系統,供在線層讀取,做了一層隔離,因此對時效性無強要求。其典型代表是我們前面講過的實時協同過濾算法,該算法通過用戶的實時行爲計算最新的相關性結果,但這些計算結果並不是實時提供給用戶的,而是要等到用戶發起請求時纔會把最新的結果提供給他使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面詳細介紹每一層的特點、案例和具體分析。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"離線層架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"離線層是推薦系統中承擔最大計算量的一個部分,很大一部分的相關性計算、標籤挖掘以及用戶畫像挖掘工作都是在這一層進行的。這一層的任務具有的普遍特點是使用大量數據以及較爲複雜的算法進行計算和挖掘。所謂大量數據,通常指的是可以使用較長時間段的用戶行爲數據和全量的物品數據;而在算法方面,可以使用較爲複雜的模型或算法,對性能的壓力相對較小。對應地,離線層的任務也有缺點,就是在時間上存在滯後性。由於離線任務通常是按天級別運行的,用戶行爲或物品信息的變更也要等一天甚至更久才能夠被反映到計算結果中。在離線層雖然進行的是離線作業,但其生產出來的數據通常是被實時使用的,因此離線數據在生產出來之後還需要同步到方便在線層讀取的地方,例如數據庫、在線緩存等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在具體實踐中,經常放在離線層執行的任務主要包括:協同過濾等行爲類相關性算法計算、用戶標籤挖掘、物品標籤挖掘、用戶長期興趣挖掘、機器學習模型排序等。仔細分析這些任務,會發現它們都符合上面提到的特點。這些任務的具體流程各不相同,但大體上都遵循一個共同的邏輯流程,如圖11-2所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3b/3bf16588fef95500eaed554467862eba.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":" 圖11-2 離線層邏輯架構圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個邏輯架構圖中,離線算法的數據來源主要有兩大類:一類是HDFS/Hive這樣的分佈式文件系統,通常用來存儲收集到的用戶行爲日誌以及其他服務器日誌;另一類是RDBMS這樣的關係數據庫,通常用來存儲商品等物品信息。離線算法會從輸入數據源獲取原始數據並進行預處理,例如,協同過濾算法會先把數據處理成兩個倒排表,LDA算法會先對物品文本做分詞處理,等等,我們將預處理後的數據統一稱爲訓練數據(雖然有些離線算法並不是機器學習算法)。預處理這一步值得單獨拿出來講,這是因爲很多算法用到的預處理是高度類似的,例如,文本標籤類算法需要先對原始文本進行分詞或詞性標註,行爲類相關性算法需要先將行爲數據按用戶聚合,點擊率模型需要先將數據按照點擊/展示進行聚合整理,等等。所以在設計離線挖掘的整體架構時,有必要有針對性地將數據預處理流程單獨提煉出來,以方便後面的流程使用,做到更好的可擴展性和可複用性。下一步是各種推薦算法或機器學習模型基於各自的訓練數據進行挖掘計算,得到挖掘結果。離線計算用到的工具通常包括Hadoop、Spark等,結果可能是一份協同過濾相關性數據,可能是物品的文本主題特徵,也可能是結果排序模型。接下來,爲了讓挖掘結果能夠被後面的流程所使用,需要將挖掘結果同步到不同的存儲系統中。一般來說,如果挖掘結果要被用作下游離線流程的輸入,是一份中間結果,那麼通常它會被再次同步到Hive或HDFS這樣的分佈式文件系統中;如果挖掘結果要被最終的推薦服務在線實時使用,那麼它就需要被同步到Redis或RDBMS這樣對實時訪問更爲友好的存儲系統中。至此,一個完整的離線挖掘流程就完成了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面講到離線任務通常以天爲單位來執行,但是在很多情況下,提高作業的運行頻率以及對應的數據同步頻率,例如從一天一次提升到一天多次,都會對推薦系統的效果有提升作用,因爲這些都可以被理解爲在做時效性方面的優化。一種極限的思想是,當我們把作業的運行頻率提高到極致時,例如每分鐘甚至每幾秒鐘運行一次作業,離線任務就變成了近線任務。當然,在這種情況下就需要對離線算法做相應的修改以適應近線計算的要求,例如前面介紹過的實時協同過濾算法就是對原始協同過濾算法的修改,以及將機器學習的模型訓練過程從離線改爲在線。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,雖然我們會把某些任務放到離線層來執行,但並不代表這些任務就只能是離線任務。我們要深入理解爲什麼將這些任務放在離線層來執行,在什麼情況下可以提高其運行頻率,甚至變爲近線任務,以及這樣做的好處和代價是什麼。只有做到這一點,才能夠做到融會貫通,不被當前的表象迷住眼睛。一種典型的情況是,當實時計算或流計算平臺資源不足,或者開發人力資源不足時,我們傾向於把更多的任務放到離線層來執行,因爲離線計算對時效性要求較低,出錯之後影響也較小。綜合來說,就是容錯度較高,適合在整體資源受限的情況下優先選擇。而隨着平臺的不斷完善,以及人力資源的不斷補充,就可以把一些對時效敏感的任務放到近線層來執行,以獲得更好的收益。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"近線層架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了上面的鋪墊,近線層的存在理由和價值就比較明確了,從生產力發展的角度來看,可以認爲它是實時計算平臺工具發展到一定程度對離線計算的自然改造;而從推薦系統需求的角度來看,它是各種推薦算法追求實時化效果提升的一種自然選擇。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"近線層和離線層最大的差異在於,它可以獲取到實時數據,並有能力對實時數據進行實時或近實時的計算。也正是由於這個特點,近線層適合用來執行對時效比較敏感的計算任務,例如實時的數據統計等,以及實時執行能夠獲得較大效果提升的任務,例如一些實時的相關性算法計算或標籤提取算法計算。近線層在計算時可使用實時數據,也可使用離線生成的數據,在提供服務時,由於無須直接響應用戶請求,因此也不用提供實時服務,而是通常會將數據寫入對實時服務友好的在線緩存中,方便實時服務讀取,同時也會同步到離線端做備份使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常放在近線層執行的任務包括實時指標統計、用戶的實時興趣計算、實時相關性算法計算、物品的實時標籤挖掘、推薦結果的去重、機器學習模型統計類特徵的實時更新、機器學習模型的在線更新等,這些任務通常會以如下兩種方式進行計算。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"個體實時:所謂個體實時,指的是每個實時數據點到來時都會觸發一次計算,做到真正意義上的實時。典型的工具代表是Storm和Flink。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"批量實時:很多時候並不需要到來一個實時數據點就計算一次,因爲這會帶來大量的計算和I/O,而是可以將一定的時間窗口或一定數量的數據收集起來,以小批次爲單位進行計算,這可以有效減少I/O量。這種妥協對於很多應用來說,只要時間窗口不太大,就不會帶來效果的顯著下降。典型的工具代表是Spark Streaming。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖11-3展示了典型的近線層計算架構圖。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cf/cfd5017ac78a5d5e7be53ace97d0808d.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖11-3 典型的近線層計算架構圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從數據源接入的角度來看,近線層主要使用實時數據進行計算,這就引出了近線層和離線層的一個主要區別:近線層的計算通常是事件觸發的,而離線層的計算通常是時間觸發的。事件觸發意味着對計算擁有更多的主動權和選擇權,但時間觸發則無法主動做出選擇。事件觸發意味着每個事件發生之後都會得到通知,但是否要計算以及計算什麼是可以自己選擇的。例如,可以選擇只捕捉滿足某種條件的事件,或者等事件累積到一定程度時再計算,等等。所以,當某個任務的觸發條件是某個事件發生之後進行計算,那麼這個任務就很適合放在近線層來執行。例如推薦結果的去重,需要在用戶瀏覽過該物品之後將其加入一個去重集合中,這就是一個典型的事件觸發的計算任務。此外,近線層的計算是可以使用離線數據的,但前提是需要提前將這些數據同步到對實時計算友好的存儲系統中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在近線層中執行的典型任務包括但不限於:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"特徵的實時更新。例如,根據用戶的實時點擊行爲實時更新各維度的點擊率特徵。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶實時興趣的計算。根據用戶實時的喜歡和不喜歡行爲計算其當下實時興趣的變化。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"物品實時標籤的計算。例如,在第6章用戶畫像系統中介紹過的實時提取標籤的流程。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"算法模型的在線更新。通過實時消息隊列接收和拼接實時樣本,採用FTRL等在線更新算法來更新模型,並將更新後的模型推送到線上。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦結果的去重。用戶兩次請求之間是有時間間隔的,所以無須在處理實時請求時進行去重,而是可以將這個信息通過消息隊列發送給一個專門的服務,在近線層中處理。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時相關性算法計算。典型的如實時協同過濾算法,按照其原理,也可以把隨機遊走等行爲類算法改寫爲實時計算,放到近線層中執行。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總結起來,凡是可以和實時請求解耦,但需要實時或近實時計算結果的任務,都可以放到近線層中執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"近線層的實時計算雖然沒有響應時間的要求,但卻存在數據堆積的壓力。具體來說,近線層計算用到的數據大部分是通過Kafka這樣的消息隊列實時發送過來的,在接收到每一個消息或消息窗口之後,如果對消息或消息窗口的計算速度不夠快,就會導致後面的消息堆積。這就像大家都在排隊辦理業務,如果一個業務辦理得太慢,那麼排的隊就會越來越長,長到一定程度就會出問題。所以,近線層的計算邏輯不宜過於複雜,而且近線層讀取的外部數據,例如離線同步好的Redis中的數據,也不宜過多,還有I/O次數不宜過多。這就要求近線層的計算邏輯和用到的數據結構都要經過精心的設計,共同保證近線層的計算效率,以免造成數據堆積。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了純數據統計類型的任務,以及結果去重這樣的無數據產出的任務,近線層的大多數任務在離線層都有對應的部分,二者有着明顯的優勢和劣勢,因此應該結合起來使用。典型的如實時協同過濾算法,由於引入了實時性,使得它在一些新物品和新用戶上的效果比原始的協同過濾算法的效果好;但由於它只使用實時數據,所以在稀疏性和不穩定性方面的問題也是比較大的,要使用離線版本的協同過濾算法作爲補充,才能形成更全面的覆蓋。再比如在近線層執行的用戶實時興趣預測,能夠捕捉到用戶最新鮮的興趣,準確率會比較高;但由於短期興趣易受展示等各種因素影響發生較大的波動,如果完全根據短期興趣來進行推薦的話,則很有可能會陷入局部的信息繭房,產生高度同質的結果,影響用戶的整體體驗。而如果將離線計算的長期興趣和短期興趣相結合,就可以有效避免這個問題,既能利用實時數據取得高相關性,又能利用長期數據取得穩定性和多樣性。從這些例子可以看出,離線層和近線層之間並沒有不可逾越的鴻溝,二者更多的是在效率、效果、穩定性、稀疏性等多個因素之間進行權衡得到的不同選擇,一個優秀的工程師應該做到“碼中有層,心中無層”,纔算是對算法和架構做到了融會貫通。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面講到離線層的任務在一定條件下可以放到近線層來執行,那麼類似地,近線層的任務是否可以放到在線層來執行呢?這個問題其實涉及離線層、近線層這兩層作爲整體和在線層的關係。如果把推薦系統比作一支打仗的軍隊,那麼在線層就是在前方衝鋒陷陣的士兵,直接面對敵人的攻擊,而離線層和近線層就是提供支持的支援部門,離線層就像是生產糧食和軍火的大後方,近線層就像是搭橋修路的前方支援部門,二者的本質都是讓前線士兵能夠最高效、最猛烈地打擊敵人,但其業務本質導致它們無法到前線去殺敵。離線層和近線層是推薦系統的生產者,在線層是推薦系統的消費者(也會承擔一定的生產責任),它們有着截然不同的分工和定位,是無法互換的。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"在線層架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在線層與離線層、近線層最大的差異在於,它是直接面對用戶的,所有的用戶請求都會發送到在線層,而在線層需要快速給出結果。如果抽離掉其他所有細節,這就是在線層最本質的東西。在線層最本質的東西並不是在線計算部分,因爲在極端情況下,在接收到用戶請求之後,在線層可以直接從緩存或數據庫中取出結果,返回給用戶,而不做任何額外計算。而事實上,早年還沒有引入機器學習等複雜的算法技術時,絕大多數計算都是在離線層進行的,在線層就起到一個數據傳遞的作用,很多推薦系統基本都是這麼做的,甚至時至今日,這種做法仍然是一種極端情況下的降級方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦系統發展到現在,尤其是各種機器學習算法的引入,使得我們可以使用的信息越來越多,可用的算法也越來越複雜,給用戶的推薦結果通常是融合了多種召回策略,並且又加了重排序之後的結果,而融合和重排序現在通常是在在線層做的。那麼問題來了:這些複雜計算一定要放到在線層做嗎?爲了回答這個問題,不妨假設:如果將所有計算都放在離線層做,在線層只負責按照用戶ID查詢返回結果,是否可行?如果將所有計算都放在離線層做,由於不知道明天會有哪些用戶來訪問系統,所以就需要爲每個用戶都計算出推薦結果,這要求我們計算出全平臺所有用戶的推薦結果,而對於那些明天沒有來訪問系統的用戶,今天的計算就浪費掉了。但這仍然不夠,因爲明天還會有新來的用戶,這些用戶的信息在當前計算時是拿不到的,所以,即使今天離線計算出了所有當前用戶的推薦結果,明天也還會有大量覆蓋不到的用戶。這就是將上面提到的複雜計算一定要放在在線層做的第一個主要原因:只有按需實時計算才能覆蓋到所有用戶,並且不會產生計算的浪費。從另一個角度來看,如果今天就把用戶的推薦結果完全計算出來,若用戶明天的實時行爲表達出來的興趣和今天的不相符,或者機器學習模型中一些關鍵特徵的取值發生了變化,那麼推薦結果就會不準確,並且無法及時調整。例如,用戶昨天看的是手機,今天打算買衣服,但我們昨天計算出的推薦結果是以手機爲主的,那麼用戶今天的需求是無法滿足的。這就是需要在在線層做複雜計算的第二個主要原因:只有在線實時計算,才能夠充分利用用戶的實時信息,包括實時興趣、實時特徵以及其他近線層計算的結果等。除此以外,還有其他原因,比如實時處理可以快速應對實時發生的業務請求等。以上這些原因共同決定了在線層存在的意義。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從目前的趨勢來看,在線層承擔的工作越來越多,因爲大家希望利用的信息越來越多地來自實時計算結果。如果說離線層和近線層是廚房裏的小工,負責一切食材和配料的前期準備工作,那麼在線層就是最後掌勺的大廚,它需要將大家準備好的材料進行組合裝配,最終形成一盤菜。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在線層的典型形態是一個RESTful API,對外提供服務。調用方傳入的參數在不同公司的設計中差異較大,但基本都會包含訪問用戶的ID標識和推薦場景這兩個核心信息,其他信息推薦系統都可以通過這兩個信息從其他地方獲取到。在線層接收到請求後會啓動一套流程,將離線層和近線層生成的數據進行串聯,在毫秒級響應時間內返回給調用方。這套流程的典型步驟包括:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AB實驗分流。根據用戶ID或請求ID,決定當前用戶要執行的策略版本。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取用戶畫像。根據傳入的用戶ID信息和場景信息,從Redis等緩存中獲取用戶的畫像信息,用在後面的流程中。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相關性候選集召回。包括行爲相關性、內容相關性、上下文相關性、冷啓動物品等多維度候選集的召回。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"候選集融合排序。將上面流程得到的候選集進行融合,再進一步進行機器學習模型排序,最後得到在算法上效果最優的結果列表。在當今推薦系統大量使用機器學習算法的背景下,這一部分的邏輯通常會比較複雜。而爲了將機器學習模型預測這一越來越通用的邏輯和推薦主邏輯相剝離,通常也會爲機器學習專門搭建一套在線系統,用來提供預測功能,包括對推薦結果的點擊、轉化預測。這樣做的好處是機器學習模型的升級改造不會干擾到推薦系統本身,有利於模塊化維護。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"業務邏輯干預。在完成算法邏輯之前或之後,還需要加入一些業務邏輯,例如去除或減少某些類別的物品,或者出於業務考慮插入一些在算法上非最優的結果,等等。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拼接展示信息。在一些推薦系統中,推薦服務要負責將展示所需的所有信息集成到一起,這樣調用方拿到結果後就可以直接展示了,而不需要再去獲取其他內容。這看起來是一個負擔,但從某些角度來看也是好事,因爲我們可以做一些展示層面的個性化,典型的如根據不同的用戶展示不同的圖片或標題,要知道展示層對於用戶是否對物品感興趣是起着非常重要的作用的,畢竟這是一個處處看臉的時代。Netflix就做過劇集封面個性化的嘗試,相比給所有人展示同樣的封面,個性化封面使得在用戶點擊方面獲得了顯著的提升。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這套流程中,本書前面介紹過的相關性算法的結果、用戶畫像的結果、用戶興趣模型的結果等都會被串聯起來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這套流程對應的在線層服務架構圖如圖11-4所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/dc/dc01f3f0eb5e0e3183f0cd28024c7e14.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖11-4 在線層服務架構圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在圖11-4中不僅呈現了在線服務層的流程架構,而且還把它所依賴的數據和服務也一併呈現出來,這樣可以最直接地體現在線層“主廚”的串聯作用。最上面一層在線服務層的流程體現了上面介紹的在線層的典型計算流程。下面所依賴的數據平臺,包含了推薦服務用到的所有數據,如相關性數據、用戶畫像數據、用戶興趣數據,以及與機器學習相關的模型和特徵數據等。這些數據又是通過下面的計算平臺這一層生成的,包括離線層的計算平臺和近線層的計算平臺。這些計算平臺所使用的數據構成了整個推薦系統的數據源,主要包括:物品數據源、行爲數據源和外部數據源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個架構圖從數據和計算的角度對推薦系統做了分割,跟之前講的離線層和近線層的分割方法是兩種不同的視角,相互正交。經常從不同的視角去抽象、剝離一個系統,有助於我們更全面、更深刻地認識系統。在複雜系統面前,我們的認識過程就像盲人摸象,需要不斷地從新的視角去看待理解它,才能得到更全面的認識。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"架構層級對比"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在介紹完離線層、近線層和在線層的架構之後,我們通過表11-2對它們進行更全面的對比。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/95/951ead5874d5914d812497dc3523be04.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在表11-2中基本上列出了推薦系統的所有主要模塊在架構中的位置,建議讀者從架構的視角對其算法進行回顧,以加深對它們的理解。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"系統和架構演進原則"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面介紹了推薦系統各個組成部分在架構中的合理位置,但就像羅馬不是一天建成的,這個樣子的推薦系統也不是一天構建起來的,一定是沿着一條路徑逐漸演進而來的。每個企業、每個團隊在系統演進方面都有不同的路徑,但也有一些共同的特點,遵循一些共同的原則。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"從簡單到複雜"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任何一個系統都是從簡單到複雜不斷演進的,這不僅對應着事物發展的一般規律,而且和系統背後的業務發展相契合。對於推薦系統來說,簡單和複雜常常體現在如下一些維度上。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1. 產品形態的數量和複雜度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以電商網站爲例,在一個較成熟的網站中,推薦產品會存在於轉化流程的各個環節中,例如主頁、商品詳情頁、購物車頁、訂單完成頁等。但在系統開發初期,只需要在一兩個最主要的場景下開發推薦模塊就可以了。不僅因爲這樣更能集中資源快速開發,而且因爲這個時候技術和業務層面的基礎能力還不夠全面、深入,盲目擴大地盤帶來的損失可能要大於收益,會使整個團隊陷入修復bug和滿足各種不重要需求的低效率循環中。就像發佈一個半成品到市場上,會收到大量的外部客戶投訴和內部升級要求,還不如等產品相對成熟後再大面積推廣。由於不同的推薦模塊之間存在可複用部分,因此,當一兩個推薦模塊開發相對完整之後,就可以以較低的代價將其可複用部分應用到更多的模塊中。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2. 推薦算法的數量和複雜度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家知道,更豐富、更全面的推薦算法組合,能夠更全面地覆蓋相關性的維度,達到更好的推薦效果。但是在實際應用中,建議大家不要貪多,只考慮算法的數量,而是要把經典算法逐個喫透,讓其在線上充分發揮作用。快速堆算法上去雖然能夠在短期內起到一定的效果提升作用,但是卻容易矇蔽住工程師的眼睛,讓其忽視了對算法內核本質的探求,以及對數據特點的探索和理解,把算法當作帶有魔法的黑盒子來用,更加不利於對這些算法的持續優化。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3. 數據源的數量和質量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"算法和數據是推薦系統的兩條腿,與算法類似,在數據方面也存在由淺到深的演進過程。這中間不僅包括數據源的數量,也包括對數據的清洗和預處理程度。互聯網上的數據,尤其是用戶行爲產生的數據,可以說沒有百分之百乾淨的,多多少少總是混有雜質,或者說對算法造成干擾。典型的如網絡爬蟲訪問產生的數據、作弊設備訪問產生的數據,以及非作弊但是訪問行爲明顯異於普通用戶的所謂異常用戶產生的行爲數據,這些數據都會對算法效果產生不同程度的影響。在系統開發初期,注意力通常在算法和服務上,沒有太多精力關注到這麼細緻層面的數據質量問題。但隨着系統的向前發展,初期的算法紅利被快速消化掉,這時就需要花更多的精力在數據質量上,通過不斷細化數據質量來獲得進一步的效果提升。數據質量的優化還有一個特點,就是它是一個需要持續進行的工作,甚至帶有一些對抗性質。典型的如作弊數據,當你發現作弊規律並對作弊數據進行相應的處理之後,作弊者很快就會發現作弊數據被處理了,於是他就會想到用別的方法來生成作弊數據。例如,協同過濾算法比較容易受到用戶行爲的干擾,如果作弊者生成大量賬號全部用來訪問某個物品,那麼這個物品就會和很多其他物品產生關聯,出現在它們的協同過濾算法結果中,影響算法的公正性。當這樣的作弊行爲被發現後,作弊者可能又會找出其他方法來達到目的。像這樣帶有對抗性質的數據質量清洗和提升,也是由淺入深貫穿於推薦系統的發展歷程的。在具體操作時,可能會是如下這樣的演進路線。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(1)不對數據做任何清洗。大部分推薦算法,只要是在一個正常數據遠多於異常數據的環境下,就可以發揮作用,所以,即使不對數據做任何清洗,一般結果也是有效的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(2)清洗爬蟲數據。爬蟲數據是垃圾數據中量最大、相對最容易識別的一類數據。去掉大量的爬蟲數據,不僅推薦效果會有所提升,而且計算量也會有所減小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(3)清洗明顯的作弊數據。所謂明顯的作弊數據,指的是介於正常訪問和爬蟲訪問中間的一類行爲數據,其特點是訪問量大、訪問時間有規律、訪問類目有規律等。和爬蟲數據不同,這類作弊數據的目的通常可能是刷榜、刷單或者攻擊推薦算法,以達到非正常提升某些物品流量的目的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(4)建立對數據進行持續清洗和優化的機制。在完成上面幾步的整體性清洗之後,需要建立一種日常持續的數據質量優化機制,在這種機制下,數據質量的優化通常和日常開發並行進行,或者融入日常開發過程中,例如週期性地處理促銷期間產生的,與日常數據有着顯著差異的數據。到了這個階段,問題通常不會那麼明顯,這時就需要工程師對數據有高度的敏感性,並且要多花時間去觀察數據,做到對數據以及數據背後用戶的熟悉,才能夠發現正常數據中的異常點。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"4. 排序模型的複雜度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前,基於機器學習的排序模型已經是推薦系統中必不可少的一個部分,各個團隊在這方面的投入也在不斷加大。和其他算法類技術一樣,常用的簡單模型能夠解決80%的問題,更加複雜的模型本質上是在解決80%以上的問題。由於排序模型位於推薦算法流程的最後一個環節,因此其對推薦系統整體效果起到的作用是受前面環節制約的。所以,在模型應用選擇方面,也應遵循循序漸進的原則,保持排序模型和召回算法同步前進,最大程度地減小上游對下游造成的瓶頸效應,讓整個系統協調發展。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"從離線到在線"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面講過,推薦系統效果優化的一類重要方法就是把離線邏輯實時化,實現一系列實時的計算邏輯,例如實時的協同過濾算法、實時的用戶興趣模型、實時的排序模型更新等。從架構層面來看,這意味着把一大批任務從離線層遷移到近線層。毫無疑問,這樣做是有好處的,但通常也會帶來更大的維護成本。例如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在線任務容錯度低。如果離線任務一次執行失敗,則可以重新執行,或者用上一個週期的數據替代,沒有很強的時間要求。但在線任務一旦執行失敗,數據就會產生堆積,需要儘快處理,對處理速度要求較高。如果在處理過程中出現問題,則會再次影響線上數據。因此,整體來講,在線任務對錯誤的容忍度較低,需要更多的以及水平更高的工程師來確保其穩定性。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在線任務不易調試。當發生和預期不符的問題時,對於離線任務,可以很容易把數據現場固定下來,一步步縮小範圍找到問題。而對在線的流式任務的處理則要困難一些,要想達到理想的調試效果,需要搭建配套的測試環境,這無疑增加了成本。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據一致性難以保障。最常出現一致性問題的場景之一就是服務重啓時,這時有的計算平臺上可能會出現已經消費過的數據被重新處理的情況,造成數據不一致,進而影響推薦的效果。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"監控機制更加複雜。對離線數據的監控統計可以通過一套離線作業來實現,實現邏輯較爲直觀,而實時作業則需要將離線監控作業改寫爲一套實時計算的監控作業,增加了統計的複雜度。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,在實際開發中,建議所有的算法都先實現其離線版本,拿到80%的效果,並在此過程中深刻理解其運行原理,在此基礎上,等有了充足的機器和人力資源時,再將其遷移改寫爲實時算法,以獲取更高的收益。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"從統一到拆分"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從一個角度來看,推薦系統架構研發的進程,就是從統一到拆分的演進過程。在系統開發初期,一般所有的在線服務功能都在一個模塊中,包括召回、排序、過濾和業務干預等。但隨着業務和算法的發展,這個模塊開始變得臃腫,難以擴展、維護,此時就需要按照功能做縱向拆分,把一些核心的通用功能拆分爲獨立的服務,以增強其獨立性和可維護性。模塊拆分增加了功能的複雜度和通用性,使得該模塊以一種更加獨立的形態存在,方便對其進行升級和維護。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"系統拆分一定是有代價的,主要包括邏輯解耦的額外工作量、子系統之間數據通信的額外代價等。在整體邏輯不夠複雜的情況下,如果從架構的角度出發,設計一個和當前業務需求不符的多模塊架構,則會造成付出的代價大於收益,得不償失。拆分重構,其背後的本質並不全是技術,還有業務的增長。甚至可以說,基於業務發展驅動的架構重構,或者至少有業務發展驅動成分在裏面的架構重構,纔是有生命力的重構,才能夠站得住腳。因爲在這種情況下可以明確地知道爲什麼要重構、重構的目標是什麼、邊界在哪裏,更重要的是,知道收益是什麼。其實在大多數技術工作中,如何解決問題並不是最難的,最難的是確定要不要解決某問題,或者說要解決什麼問題。如果完全基於所謂的技術理想來驅動架構的拆分演進,則很有可能得不到明確的收益,重構的邊界也無法確定,就像跳進一個火坑,最終以爛尾告終。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,推薦系統架構的研發,要本着以業務發展和技術進步爲雙主線的原則,根據確定性目標來進行架構的抽象和拆分,減少無用功。技術人員要追求技術,但也千萬不能被技術矇住了眼睛,把一切判斷都建立在純技術的基礎上,置業務於不顧。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基於領域特定語言的架構設計"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實際的推薦系統開發中,常常會遇到這樣的情況:不同位置的推薦模塊具有不同的推薦邏輯,但底層調用的是同樣的一批數據和算法邏輯。例如,在商品詳情頁和購物車頁分別有一個推薦模塊,其邏輯差異主要在於召回邏輯的不同和排序模型的不同。在召回邏輯方面,商品詳情頁的推薦模塊會融合瀏覽的協同過濾和購買的協同過濾,而購物車頁的推薦模塊則主要使用了購買的協同過濾。在排序模型方面,兩個推薦模塊用同樣的算法各自訓練了一個模型,分別在線上調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在常規的做法中,會爲兩種推薦主邏輯分別寫一套代碼,一套代碼描述商品詳情頁的推薦邏輯,包括它調用的召回數據以及調用的排序模型等;另一套代碼用類似的方法描述購物車頁對應的推薦邏輯。這樣做在功能實現上是沒有問題的,但會存在如下一些缺點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出現大量冗餘邏輯。不同推薦模塊之間會存在較多類似的甚至相同的邏輯,例如業務過濾、候選集召回等,可能會出現兩個推薦模塊的代碼大量是一樣的,只有少量存在差異的情況。這無疑造成了代碼的高冗餘和低複用。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"邏輯變更復雜。推薦系統是一個重算法的系統,經常需要對各種邏輯進行調整。如果將邏輯都用代碼寫死,則意味着每次調整邏輯都需要上線,拉長了實驗週期,降低了實驗效率。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於存在以上一些缺點,越來越多的推薦系統開始採用領域特定語言來進行邏輯架構的構建描述。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"領域特定語言(Domain Specific Language,DSL),指的是特定使用於某個領域的語言。所謂領域特定,指的是專門用來處理某一特定領域的問題,與DSL相對的是Java、Python等這些可用來解決各種通用問題的語言。大家最熟悉的DSL應該就是SQL了,它是特定用於處理關係數據或者說集合數據的一門語言,其核心的select、project、join操作都是針對關係數據庫設計的,無法完成對關係數據以外的數據的操作。類似的還有HTML,其專門用來描述網頁結構和內容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與傳統的代碼編寫方式相比,基於DSL的開發方式增加了兩個部分,一個是邏輯描述部分,一個是邏輯翻譯部分。邏輯描述部分,也就是DSL外在的表現形式,用來描述想要實現的邏輯,例如可以指定使用哪些召回源、使用哪個排序模型、使用哪些過濾規則等;邏輯翻譯部分,負責將邏輯描述的DSL代碼翻譯成通用的計算機語言,進而執行。與SQL相比,DSL的邏輯描述部分就像工程師寫的SQL語句,用來描述想要選擇哪些列、條件是什麼等,而邏輯翻譯部分就像SQL的編譯器,將SQL語句翻譯成更底層的執行邏輯,最終執行的是翻譯後的結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DSL執行流程圖如圖11-5所示。首先用DSL來描述要執行的推薦邏輯,例如使用哪些召回源、使用哪個排序模型等。編寫好邏輯描述代碼之後,在服務啓動時調用邏輯翻譯代碼,將邏輯描述代碼中描述的邏輯翻譯爲最終可執行的邏輯執行代碼。其中邏輯描述代碼可以是自定義的DSL代碼,有自己的語法;邏輯翻譯代碼和邏輯執行代碼是平臺的原生代碼,例如Java、Go或C++代碼等。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/40/40825fc2529dcc632856ab72aa46e68a.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖11-5 DSL執行流程圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面看一個簡單的例子。這是一段自定義的DSL代碼,用來描述一個簡單的推薦模塊。"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"module_name = ShoppingCart\nrecallers = ItemCFRecaller + UserCFRecaller + RandomWalkRecaller\nrank_model = LR\nrank_model_host = 10.1.1.1:8888\nfilters = TopSaleFilter + BizCategoryFilter"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段DSL代碼描述的邏輯非常清晰,首先說明這個推薦模塊的名字是ShoppingCart,也就是購物車推薦,然後指定使用ItemCF、UserCF和RandomWalk三個召回源,接下來指定排序模型的類型和模型服務的調用地址,最後指定使用熱銷商品過濾器和某個業務指定類別的過濾器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段DSL代碼隨後會被一個DSL翻譯解析程序所讀取,構造成一段可執行代碼。比如在選擇Java語言作爲DSL解析語言的情況下,可以在DSL代碼中指定推薦主流程中所使用的召回器,然後用Java語言來解析這段DSL代碼,就可以通過Java語言的反射機制將DSL代碼中指定的召回器實例化爲一個具體的召回器對象,如ItemCFRecaller。推薦主流程按照上面的方法使用DSL代碼完成初始化之後,就可以在接收到用戶請求時,按照DSL代碼中指定的邏輯執行了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種基於DSL的推薦系統架構設計,至少具有如下一些好處。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大量減少代碼,減少冗餘代碼,提高代碼的複用性。在這種模式下,如果不新增算法,只是新增推薦模塊,那麼只需要寫一段描述邏輯的DSL代碼即可,代碼量遠少於寫一套Java代碼的代碼量。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"邏輯表達與執行解耦,便於邏輯變更實驗。由於執行程序是由解析程序解析DSL代碼自動生成的,要更改邏輯,只需要更改DSL代碼,然後讓執行程序重新加載即可,秒級別實現邏輯線上生效。上線流程大大縮短,再配合ABTest模塊,實驗迭代效率得到大幅度提升。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表達清晰明瞭,減少bug數量。代碼越少,邏輯越清晰,bug就越少。在DSL代碼中只保留了與推薦邏輯描述相關的核心代碼,其他實現層面的細節全部隱去,使得開發者可以專注於推薦邏輯本身,代碼的易讀性得到顯著提升。這對於組內新來人員熟悉工作以及工作交接都有好處。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"算法實現和調用分離,減少開發成本。如果實現了一種新算法,例如新的召回算法,那麼只需要按照接口規範實現算法並註冊,然後在DSL代碼中直接使用即可,減少了新算法上線的成本。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在機器學習廣泛使用的TensorFlow也可以被理解爲一種DSL,它提供了對機器學習業務更簡單的表達方式。用戶在寫代碼時,只需要關注和網絡結構、特徵處理等要解決的業務問題緊密相關的內容,而機器學習層面的技術細節被DSL所屏蔽,例如特徵分桶是如何實現的、反向傳播求導是如何實現的,等等。這使得機器學習的使用面被大幅度擴展,用戶不再需要很深厚的機器學習基礎就可以上手應用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實現推薦系統的DSL代碼時,可以選擇自己定義一套簡單的語法邏輯,就像上面的例子中那樣。後期如果要實現邏輯較複雜的DSL代碼,則可以藉助Python、Groovy、Scala等表達能力強的通用語言,網上也有這方面的資料,讀者可以找來參考。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本章我們從架構設計的角度回顧和討論了推薦系統的一些核心算法模塊,重點從離線層、近線層和在線層三個架構層面討論了這些算法。本章雖然沒有講解一些具體推薦模塊的架構設計,如時下熱門的feed流推薦、商品詳情頁的推薦等,但無論什麼推薦模塊,其邏輯經過拆解後都可以映射到本章介紹的這套架構體系中,做到觸類旁通,舉一反三。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過本章的討論,希望工程師在設計和實現算法時,腦子裏除了有算法和數據,還應多一個架構的維度,能夠從架構工程的角度來考慮算法,做到心中有系統,而不只是一些零散推薦算法的實現,這樣才能構建好一個推薦系統。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,本章還介紹了推薦系統架構的發展規律,以及在發展過程中可能遇到的問題,希望能爲工程師的具體實踐提供參考。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文節選自《"},{"type":"text","marks":[{"type":"strong"}],"text":"從零開始構建企業級推薦系統"},{"type":"text","text":"》一書"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"張相於 著"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"電子工業出版社出版"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"貨真價實的老司機泛場景無縫商業落地"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從零門檻到萬人斬一步到位的駕駛指南"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/71/71a41862f3a3adfa89d1af113c1ad494.jpeg","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"https://u.jd.com/jTHmKE","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章