工作職位推薦系統的演算法與架構

OReillyData發表於2016-12-19

Indeed.com 每個月有兩億不同的訪客,有每天處理數億次請求的推薦引擎。在這篇文章裡,我們將描述我們的推薦引擎是如何演化的,如何從最初的基於Apache Mahout建立的最簡化可用行產品,到一個線上離線混合的成熟產品管道。我們將探索這些變化對產品效能指標的影響,以及我們是如何通過使用演算法、架構和模型格式的增量修改來解決這些挑戰的。進一步,我們將回顧在系統設計中的一些相關經驗,相信可以適用於任何高流量的機器學習應用中。

從搜尋引擎到推薦

Indeed的產品執行在世界各地的許多資料中心上。來自每個資料中心的點選流資料以及其他應用事件被複制到我們在奧斯丁資料中心的一箇中心化的HDFS資料倉儲中。我們在這個資料倉儲上進行計算分析並且構建我們的機器學習模型。
我們的職位搜尋引擎是簡單而直觀的,只有兩個輸入:關鍵字和地點。搜尋結果頁面展示了一個匹配的職位列表,並基於相關性進行了排序。搜尋引擎是我們的使用者發現職位的主要方式。
我們決定在搜尋引擎之上加入職位推薦作為一個新的互動模式是基於以下幾個關鍵原因:

  • Indeed上所有搜尋的25%只指定了一個區域,並沒有搜尋關鍵字。有許多的求職者並不確定在他們的搜尋中應該用什麼關鍵字

  • 當我們傳送有針對性的推薦時,求職者的體驗是個性化的

  • 推薦可以幫助甚至最複雜的使用者來發現額外的未被搜尋所涵蓋的職位

  • 推薦為亞馬遜帶來了額外35%的銷量為Netflix75%的內容閱覽。很顯然,推薦能提供額外的價值

推薦職位與推薦產品或者是電影有很顯著的區別。這裡列舉了一些我們在構建推薦引擎時仔細考慮過的因素:

  • (職位)庫存快速增長:我們每天要聚合計算幾百萬新的職位。可以推薦的職位的集合在不斷變化

  • 新使用者:每天有幾百萬新的求職者訪問我們的網站並開始他們的職位搜尋。我們想要能夠根據有限的使用者資料生成推薦。

  • 流動性:在indeed上一個職位的平均生命週期是30天左右。內容的更新十分重要,因為老的職位的需要非常可能已經被滿足了

  • 有限的供給關係:一個釋出的職位通常只招一個的應聘者。這和書籍或者電影很不一樣,並不是只要有庫存就可以同時推薦給很多使用者。如果我們過度推薦一個職位,我們可能會讓僱主收到到上千個應聘申請的轟炸。

如何理解推薦演算法

推薦是一個匹配問題。給定一個使用者集合和一個物品集合,我們想要將使用者匹配到他們喜歡的物品上。有兩種高層次的方法可以達成這類匹配:基於內容的和基於行為的。它們各有優缺點,此外也有將兩者結合起來從而發揮各自優勢的方法。

基於內容的方法使用比如使用者偏好設定、被推薦物品的特性之類的資料來決定最佳匹配。對於職位推薦來說,通過職位的描述中的關鍵字來匹配使用者上傳的簡歷中的關鍵字是一種基於內容的推薦方法。通過一個職位的關鍵字來找到其他看上去相似的職位是另外一種基於內容的推薦的實現方法。

基於行為的方法利用了使用者的行為來生成推薦。這類方法是領域無關的,這意味著同樣的演算法如果對於音樂或者電影有效的話,也可以被應用在求職領域。基於行為的方法會遇到所謂的冷啟動問題。如果你只有很少的使用者活動資料,就很難去生成高質量的推薦。

Mahout協同過濾

我們從建立一個基於行為的推薦引擎開始,因為我們想要利用我們已有的求職使用者和他們的點選資料,我們的首次個性化推薦嘗試是基於Apache Mahout提供的基於使用者間的協同過濾演算法的實現。我們將點選流資料餵給一個執行在Hadoop叢集上的Mahout構建器併產生一個使用者到推薦職位的對映。我們建立一個新的服務使得可以執行時訪問這個模型,且多個客戶端應用可以同時訪問這個服務來獲取推薦的職位。

產品原型的結果和障礙

作為一個產品原型,基於行為的推薦引擎向我們展示了一步步迭代前進的重要性。通過快速建立起這個系統並將其展示給使用者,我們發現這種推薦對於求職者來說是有用的。然而,我們很快就遇到了一些在我們的資料流上使用Mahout的問題:

  • 模型構建器需要花大約18個小時的時間來處理Indeed網站2013年的點選流資料,這個資料量要比今日的資料小了三倍。

  • 我們只能一天執行一個模型構造器,這意味著每天新加入的使用者直到第二天為止看不到任何推薦。

  • 幾百萬新彙總的職位在模型構造器再次執行之前是不能作為可見的推薦的。

  • 我們產生的模型是一個很大的對映,大約佔了50吉位元組的空間,需要花費數個小時將其通過廣域網從其生成的資料中心拷貝到全球各地的資料中心。

  • Mahout的實現的提供露了一些可配置引數,比如相似度閾值。我們可以調節演算法的引數,但是我們想要測試整個不同的演算法這樣的靈活性。

為推薦實現最小雜湊

我們先解決最重要的問題:構建器太慢了。我們發現在Mahout中的使用者間相似度是通過在n^2複雜度下的使用者間兩兩比較的來實現的。僅對於美國的網站使用者來說(五千萬的訪問量),這個比較的數量將達到15 * 10^15次,這是難以接受的。而且這一計算本身也是批量處理的,新加一個使用者或者一個新的點選事件就要求重新計算所有的相似度。

我們意識到推薦是一個非精確匹配問題。我們是在尋求方法來發現給定使用者最相近的使用者們,但是我們並不需要100%準確。我們找了一些估算相似度的方法,不需要準確的計算。

主要貢獻者戴夫格里菲思從一篇谷歌新聞學術論文上看到了最小雜湊方法。最小雜湊(或者最小獨立序列)允許近似計算傑卡德相似度。將這一方法應用到兩個使用者都點選過的職位上,我們發現兩個使用者有更多共同的職位點選,那麼他們的傑卡徳相似度就越高。為所有的使用者對計算傑卡徳相似度的複雜度是O(n^2),而有了最小雜湊後,我們可以將複雜度降到O(n)。

給定一個雜湊函式h1, 一個專案集合的最小雜湊被定義為將集合中所有項用該雜湊函式分別雜湊,然後取其中最小的值。由於方差太大,單個雜湊函式不足以計算近似傑卡徳相似度。我們必須使用一個雜湊函式族來合理地計算近似的傑卡徳距離。通過使用一個雜湊函式族,最小雜湊可被用來實現可調節傑卡徳相似度閾值的個性化推薦。

挖掘海量資料集”,一個最近的由史丹佛大學教授萊斯科維克、拉賈羅曼和厄爾曼講解的Coursera課程,非常詳細的解釋了最小雜湊。他們書的第三章——“挖掘海量資料集”,解釋了最小雜湊背後的數學證明原理。

我們針對推薦的最小雜湊的實現涉及到以下三個階段:

1. 簽名計算和叢集分配

每一個求職者被對映到一個h個叢集的集合,對應了一個雜湊函式族H(下面的虛擬碼展示了這一過程):
H = {H1, H2, ..H20}

for user in Users

       for hash in H

             minhash[hash] = min{x∈Itemsi| hash(x)}

這裡H是一個包含h個雜湊函式的族。這一步的最後,每一個使用者被一個簽名集合所代表,也即一個由h個值組成的叢集。

2. 叢集擴充套件

共享相同簽名的使用者被認為是相似的,他們的職位將為被互相推薦。因此我們從每一個在叢集中的使用者開始,用其所有的職位來擴充套件每個叢集。

3.生成推薦

為了給一個使用者生成推薦,我們將h個使用者所在的叢集中的職位並起來。然後我們除去任何使用者已經訪問過的職位,從而得到最後的職位推薦。

為新使用者推薦職位

最小雜湊的數學屬性使得我們可以解決為新使用者推薦職位的問題,並且可以向所有的使用者推薦新的職位。當新的點選資料到來的時候,我們增量地更新最小雜湊簽名。我們也在記憶體中維護新職位和他們的最小雜湊族的對映。通過在記憶體中保留這兩部分資料,我們就可以在新使用者點選了一些職位後為他們推薦職位。只要任何新發布的職位被點選訪問過,它就可以被推薦給使用者。

在轉移到最小雜湊方法後,我們有了一個混合的推薦模型,由一個在Hadoop上的構建的離線每日被更新的元件和一個由當日的點選資料組成、在記憶體快取中實現的線上元件。這兩個模型被整合起來用以計算每一個使用者的最終推薦集合。在我們做了這些改變之後,每一個使用者的推薦變得更加得動態,因為它們將隨著使用者點選感興趣的職位的同時發生變化。

這些改變中我們認識到,我們可以在效能和準確度上做出權衡,用小的誤差增加來換大的效能提升。我們也認識到可以將較慢的離線模型通過線上的模型進行補充,從而得到更新的推薦結果。

工程基礎設施的影響

包含每一個使用者到他們的推薦的對映的推薦模型是一個相當龐大的檔案。由於職位對於每個國家來說都是本地的,我們首先嚐試將我們的資料基於大約的地理邊界進行分片。我們在每一塊區域執行模型構造器,而不是在整個世界執行一個。每一個地區都有多個國家組成。舉個例子,東亞地區包括為中國、日本、韓國、香港、臺灣以及印度的推薦。

但是在分片之後,一些區域產生的資料仍然十分龐大,需要花好幾個小時才能通過廣域網從歐洲資料中心拷貝到奧斯丁的Hadoop叢集。為了解決這個問題,我們決定增量的傳輸推薦資料,而不是一天一次。我們重用了連續先寫日誌(sequential write ahead logs)的方法,通過日誌結構合併樹來實現。這一方法已經在Indeed的其他大型產品應用中得到驗證,比如我們的文件服務。

我們修改了我們的模型構建器,將推薦資料儲存成小的分段,而不是產生一個單獨龐大的模型檔案。每一個段檔案使用順序輸入輸出,並且為快速複製做了優化。這些分段在遠端資料中心執行推薦服務時將被重新組裝成一個日誌結構合併樹

這一基礎架構的改變使得使用者可以比之前快數個小時在遠端的資料中心上看到他們新的推薦。從我們的A/B測試的結果來看,由使用者更快的得到新職位推薦帶來了30%的點選量提升。

這一改進展示了工程基礎設施的改進與演算法的改進帶來的影響不相上下。

改進A/B測試的速度

建立起計算和更新推薦的管道只是一個開始。為了改進覆蓋率和推薦質量,我們需要加快我們的A/B測試的速度。我們為了調整最後的推薦集合做了很多決策。這些決策包括,相似度閾值,每一個使用者的推薦包含的職位數量,以及各種不同的用來過濾掉低質量推薦的方法。我們想要調節和優化計算推薦的各個方面,但是這需要逐個對演算法的每個改進都構建新的模型。測試多個改進想法意味著處理使用者請求的伺服器上多倍的磁碟以記憶體佔用。

我們開始通過傳輸計算推薦的單獨元件而不是最終的結果來改進我們的A/B測試。我們改變了推薦服務來執行最後的計算,即通過將碎片整合起來,而不是簡單的讀取模型返回結果。重要的推薦子元件是每個使用者的叢集分配,從每一個叢集到這個叢集中的職位的對映以及一個對於每個使用者來說包含不應該推薦給他們的職位的黑名單。我們修改了我們的構建器,來產生這些元件,並且修改了我們的服務,將他們在收到請求時組合起來從而返回最終的推薦列表。

通過實現這種架構上的改變,我們只傳輸那些在每一個A/B測試中改變的子元件。比如說,如果測試只調節了什麼樣的職位會從一個使用者的推薦中除去,那麼我們只需要傳輸這個測試組的黑名單。

這一改變改進了A/B測試的速度的數量級。我們能夠在很短的時間內測試並驗證多個可以改進推薦質量和覆蓋率的想法。之前,由於設定環境的開銷較大,我們平均每個季度只能測試一個模型中的改進,

我們的經驗表明A/B測試的速度應該在設計大型機器學習系統時就被考慮進去。你能越快地將你的想法放在使用者面前,那麼你就能越快地在上面迭代。

這篇文章總結了我們在構建我們的推薦引擎時做出的一系列演算法和架構上的改變。我們迭代地構建軟體,我們從一個最簡單原型開始,從中獲取經驗,並不斷改進。這些改變帶來的成果是職位推薦的點選增加從2014年初最簡原型時的3%增長到了14%。

我們將繼續精煉我們的推薦引擎。我們正在使用Apache Spark建立一個原型模型,建立一個模型整合組合,並精煉我們的優化引數來應對流行偏見問題。

640?wx_fmt=jpeg

Preetha Appan

Preetha Appan是Indeed的高階軟體工程師,是高效能推薦搜尋分散式系統的專家。她對Indeed的職位和簡歷搜尋引擎的貢獻包括,文字切分改進、查詢擴充套件特性以及其他重要的基礎設施和效能改進。她樂於並享受解決機器學習和資訊檢索方面的挑戰性的問題。

640?wx_fmt=png

相關文章