Uber機器學習平臺Michelangelo是如何使用Spark模型的?

Michelangelo是Uber的機器學習(ML)平臺,可以訓練並服務於整個公司範圍內生產環境中的數千種模型。該平臺被設計成了一個端到端的工作流,目前支持經典的機器學習、時間序列預測和深度學習模型,可以涵蓋大量的用例,從生成市場預測、響應客戶支持工單到準確計算預計到達時間(EAT)以及使用自然語言處理(NLP)模型在駕駛員App中提供一鍵式聊天功能。

本文最初發佈於Uber工程博客,由InfoQ中文站翻譯並分享。

Michelangelo是Uber的機器學習(ML)平臺,可以訓練並服務於整個公司範圍內生產環境中的數千種模型。該平臺被設計成了一個端到端的工作流,目前支持經典的機器學習、時間序列預測和深度學習模型,可以涵蓋大量的用例,從生成市場預測響應客戶支持工單準確計算預計到達時間(EAT)以及使用自然語言處理(NLP)模型在駕駛員App中提供一鍵式聊天功能

大多數Michelangelo模型都是基於Apache Spark MLlib的,這是一個可伸縮的Apache Spark機器學習庫。爲了處理高QPS的在線服務,Michelangelo最初僅通過內部定製的模型序列化和表示支持Spark MLlib模型的一個子集,這使得客戶無法靈活地試驗任意複雜的模型管道,限制了Michelangelo的擴展速度。爲了解決這些問題,我們改進了Michelangelo對Spark MLlib的使用,特別是在模型表示、持久性和在線服務方面。

在Michelangelo中改進Spark MLlib用法的動機

我們最初開發Michelangelo是爲了爲生產環境提供可擴展的機器學習模型。它對基於Spark的數據攝取調度、模型訓練和評估以及批量和在線模型服務部署提供端到端的支持,在Uber獲得了廣泛的認可。

圖1 爲了將Spark用於數據預處理和低延遲服務,並使用GPU進行分佈式深度學習訓練,Michelangelo針對深度學習用例使用了一致的Spark管道架構。

最近,Michelangelo已經發展到可以處理更多的用例,包括提供在覈心Michelangelo之外訓練過的模型。例如,深度學習模型的擴展和端到端加速訓練需要在不同環境中執行操作步驟,以便利用Spark的分佈式計算轉換、Spark Pipelines在CPU上的低延遲服務以及使用Horovod(Uber的分佈深度學習框架)在GPU集羣上的分佈式深度學習訓練。爲了使這些需求更容易實現,並保證訓練和服務的一致性,有一個一致的架構和模型表示很重要,它可以利用我們現有的低延遲的基於JVM的模型服務基礎設施,同時提供正確的抽象來封裝這些需求。

圖2 具有活動Spark會話的外部環境可以無縫地反序列化來自Michelangelo的經過訓練的管道模型,並序列化供Michelangelo生態系統的其餘部分使用的模型。Apache Spark是Apache軟件基金會在美國和/或其他國家的註冊商標。使用這個標識並不意味着Apache軟件基金會的認可。

另一個動機是使數據科學家能夠使用PySpark在熟悉的Jupyter筆記本(數據科學工作臺)中構建和試驗任意複雜的模型,同時仍然能夠利用Michelangelo生態系統可靠地進行分佈式訓練、部署和服務。這也爲集成學習多任務學習技術所需的更復雜的模型結構提供了可能性,同時讓用戶可以動態地實現數據操作和自定義評估。

因此,我們回顧了Michelangelo對Spark MLlib和Spark ML管道的使用,概括了其模型持久性和在線服務機制,目的是在不影響可伸縮性的情況下實現可擴展性和互操作性。

Michelangelo最初推出的是一個統一的架構,它負責管理緊耦合的工作流和用於訓練和服務的Spark作業。Michelangelo對每種支持的模型類型都有特定的管道模型定義,並使用內部定製的protobuf表示經過訓練的服務模型。離線服務通過Spark處理;在線服務使用添加到Spark內部版本的自定義API來處理,以實現高效的單行預測。

圖3 向本地Spark序列化和反序列化轉變在模型持久化管道階段(Transformer/Estimator)層面上實現了靈活性和跨環境兼容性。Apache Spark是Apache軟件基金會在美國和/或其他國家的註冊商標。使用這個標識並不意味着Apache軟件基金會的認可。

最初的架構支持通用機器學習模型(如廣義線性模型GLM和梯度增強決策樹模型GBDT)的大規模訓練和服務,但是自定義的protobuf表示使添加對新Spark轉換器的支持變得困難,並排除了在Michelangelo之外訓練的模型。當新版本的Spark可用時,Spark的自定義內部版本也使得每次的升級迭代變得複雜。爲了提高對新轉換器的支持速度,並允許客戶將自己的模型引入Michelangelo,我們考慮瞭如何改進模型表示並更加順暢地添加在線服務接口。

Michelangelo的架構和模型表示法的演變

圖4 Michelangelo的架構必須處理由不同功能需求帶來的複雜性,並保持訓練和服務環境之間的一致性。

Uber的機器學習工作流通常很複雜,涉及不同的團隊、庫、數據格式和語言;爲了使模型表示和在線服務接口可以恰當地演化,我們需要考慮所有這些維度。

圖5 部署並提供機器學習管道模型服務包括模型前面的所有轉換和操作步驟。

要部署用於服務的機器學習模型,需要部署整個管道,包括模型前面的轉換工作流。通常還需要打包數據轉換、特徵提取、預處理甚至預測後轉換。原始預測通常需要解釋或轉換回標籤,在某些情況下需要轉換到不同的維度空間,比如日誌空間,以便下游服務使用。它也可以用於通過額外的數據加強原始預測,如它們的置信區間,通過概率校準來校準概率。我們想要一個能夠反映Spark MLlib模型固有管道步驟的模型表示,並允許Michelangelo與外部工具進行無縫交互。

選擇一種最新的模型表示

在評估可供選擇的模型表示時,我們評估了不同的需求,包括:

  • 表示廣義轉換序列的能力(必需)
  • 處理在線用例的輕量級服務的可擴展性(必需)
  • 支持使用非Michelangelo原生Spark工具替換存儲在Michelangelo中的模型(必需)
  • 訓練時和服務時的模型解釋偏差風險低(非常想要)
  • Spark更新速度要快,並且易於編寫新的估計器/轉換器(非常想要)

我們考慮的一種方法是使用MLeap,這是一個獨立的庫,它通過一個專用的運行時來執行管道,提供了管道和模型序列化(到Bundle.ML)和反序列化支持。MLeap具有所需的表達能力和對輕量級在線服務的支持。但是,它有自己專有的持久化格式,這限制了與序列化和反序列化普通Spark MLlib模型的工具集的互操作性。

MLeap還引入了服務時行爲偏離訓練時評估的風險,因爲在技術上,正在提供服務的模型加載時的格式與訓練時內存中的格式不同。MLeap還降低了Spark更新的速度,因爲除了Spark MLlib本地使用的方法之外,還必須爲每個轉換器和估計器添加單獨的MLeap保存/加載方法。Databricks的ML模型導出dbml-local提供了類似的方法。

我們考慮的另一種方法就是將訓練模型導出到一個標準的格式,如預測模型標記語言(PMML)或可移植分析格式(PFA),它們都具備我們需要的表達能力並且可以和Spark交互,Spark直接提供了PMML支持,而aardpfark可以將Spark模型導出爲PFA。然而,這些表示還是存在服務時行爲偏離方面的風險,我們認爲這一風險高於MLeap,因爲一般標準在特定的實現中通常會有不同的解釋。這些標準在Spark更新速度方面也帶來了更大的阻礙,因爲根據Spark變化的性質,標準本身可能需要更新。

我們發現,最直接的方法是使用標準的Spark ML管道序列化來表示模型。Spark ML管道展示了我們想要的表達能力,允許與Michelangelo之外的Spark工具集交互,展現出了低風險的模型解釋偏差,對Spark的更新速度影響較小。它還有助於編寫自定義的轉換器和估計器。

我們看到,使用Spark管道序列化的主要挑戰是它與在線服務需求的不兼容性(Nick Pentreath在2018年的Spark AI峯會上也討論了這一點)。這種方法會啓動一個本地Spark會話,並用它來加載一個訓練好的Spark MLlib模型,這相當於在單臺主機上運行一個小型集羣,內存開銷和延遲都很大,使它不適合許多在線服務場景要求的p99毫秒級延遲。雖然現有的用於提供服務的Spark API集在性能上還不足以滿足Uber的用例,但我們發現,我們可以在這種開箱即用的體驗中進行許多直接的更改,以滿足我們的需求。

爲了提供用於在線服務的輕量級接口,我們將anOnlineTransformer添加到可以提供在線服務的轉換器中,包括利用低級Spark預測方法的單個方法和小型方法列表。我們還調整了模型加載的性能,以滿足我們的目標開銷要求。

使用增強型轉換器和估計器的管道

爲了實現一個可以由Michelangelo在線訓練並提供服務的Transformer或Estimator,我們構建了一個OnlineTransformer接口,它擴展了開箱即用的Spark Transformer接口,並執行兩個方法:1)Transform(instance: Dataset[Any]) ;2)ScoreInstance(instance: Map[String, Any])。

Transform(instance: Dataset[Object])作爲分佈式批處理服務的入口,提供了開箱即用的基於數據集的執行模型。ScoreInstance(instance: Map[String, Object]): Map[String, Object]作爲較爲輕量級的API,用於低延遲、實時服務場景中出現的針對單一特徵值映射集的單行預測請求。ScoreInstance背後的動機是提供一個更輕量級的API,它可以繞過依賴於Spark SQL Engine的Catalyst Optimizer的Dataset所帶來的巨大開銷,從而對每個請求進行查詢規劃和優化。如上所述,這對於實時服務場景(如市場營銷和欺詐檢測)非常重要,其中p99延遲的SLA通常是毫秒級的。

當加載Spark PipelineModel時,任何具有相似類(包含OnlineTransformer特性)的Transformer都會映射到該類。這使得現有的訓練好的Spark模型(由支持的轉換器組成)獲得了提供在線服務的能力,而且沒有任何額外的工作。注意,OnlineTransformer還實現了Spark的MLWritable和MLReadable接口,這爲Spark免費提供了對序列化和反序列化的本地支持。

保持在線和離線服務一致性

轉向標準的PipelineModel驅動的架構,通過消除PipelineModel之外的任何自定義預評分和後評分實現,進一步加強了在線和離線服務準確性之間的一致性。在每個管道階段,實現自定義評分方法時的標準實踐是首先實現一個公共評分函數。在離線轉換中,它可以作爲DataFrame輸入上的一組Spark用戶定義函數(UDF)運行,相同的評分函數也可以應用於在線scoreInstance和scoreInstances方法。在線和離線評分一致性將通過單元測試和端到端集成測試進一步保證。

性能優化

我們最初的度量結果顯示,與我們自定義的protobuf表示的加載延遲相比,原生Spark管道加載延遲非常高,如下表所示:

這種序列化模型加載時間上的性能差異對於在線服務場景是不可接受的。模型實際上被分片到每個在線預測服務實例中,並在每個服務實例啓動時、新模型部署期間或接收到針對特定模型的預測請求時加載。在我們的多租戶模型服務設置中,過長的加載時間會影響服務器資源的敏捷性和健康狀況監控。我們分析了加載延遲的來源,並進行了一些調優更改。

影響所有轉換器加載時間的一個開銷來源是Spark本地使用sc.textFile讀取轉換器元數據;從一個小的單行文件生成一個字符串RDD非常慢。對於本地文件,用Java I/O替換這段代碼要快得多:

[loadMetadata in src/main/scala/org/apache/spark/ml/util/ReadWrite.scala]

在我們的用例中(例如,LogisticRegression、StringIndexer和LinearRegression),影響許多轉換器的另一個開銷來源是對與轉換器相關的少量數據使用Spark分佈式read/select命令。對於這些情況,我們使用ParquetUtil.read代替sparkSession.read.parquet;直接進行Parquet read/getRecord大大降低了轉換器的加載時間。

樹集成(Tree ensemble)轉換器有一些特殊的調優機會。加載樹集成模型需要將序列化到磁盤的模型元數據文件讀取到磁盤,這會觸發小文件的groupByKey、sortByKey以及Spark的分佈式read/select/sort/collect操作,這些操作非常慢。我們直接用更快的Parquet read/getRecord代替了它們。在樹集成模型保存方面,我們合併了樹集成節點和元數據權重DataFrame,以避免寫入大量讀起來很慢的小文件。

通過這些調優工作,我們能夠將基準示例的本地Spark模型加載時間從8到44倍減少到僅比從自定義protobuf加載慢2到3倍,這相當於比Spark原生模型快4到15倍。考慮到使用標準表示的好處,這種水平的開銷是可以接受的。

需要注意的是,Michelangelo在線服務創建了一個本地的SparkContext來處理任何未加密的轉換器的負載,因此,在線服務不需要SparkContext。我們發現,當沒有模型加載處於活動狀態時,讓SparkContext繼續運行會對性能產生負面影響並導致延遲,例如,通過SparkContext清理器的操作。爲了避免這種影響,我們在沒有運行負載時停止SparkContext。

可服務管道的靈活結構

使用管道模型作爲Michelangelo的模型表示,用戶可以靈活地組合和擴展可服務組件單元,使它們在線和離線服務時保持一致。然而,這並沒有完全封裝管道模型在機器學習工作流的各個階段使用時的操作需求差異。有些操作步驟或概念本質上與機器學習工作流的特定階段相關,而與其他階段完全無關。例如,當用戶對模型進行評估和迭代時,常常需要進行超參數優化、交叉驗證以及生成模型解釋及評估所需的特定元數據等操作。這些步驟允許用戶幫助生成模型、與模型交互以及評估管道模型,但是一旦準備好進行產品化,就不應該將這些步驟合併到模型服務中。

同時,在機器學習工作流不同階段的需求差異,促使我們開發了一個基於通用編排引擎的工作流和操作員框架。除了組合自定義可服務管道模型的靈活性之外,這還允許用戶以有向圖或工作流的形式組合和編排自定義操作的執行,以實現最終的可服務管道模型,如圖6所示:

圖6 Michelangelo基於Operator Framework的工作流提供了另一種程度的靈活性,通過優化的執行計劃來方便地定製操作,從而生成可服務的、序列化的Michelangelo管道模型以及有用的工件。Apache Spark是Apache軟件基金會在美國和/或其他國家的註冊商標。使用這個標記並不意味着Apache軟件基金會的認可。Docker和Docker標識是Docker公司在美國和/或其他國家的商標或註冊商標。Docker公司及其他各方也可以在本協議中使用的其他條款中享有商標權。本標記的使用並不意味着Docker的認可。TensorFlow、TensorFlow標識及任何相關標識均爲谷歌公司的商標。

未來展望

在這一點上,Spark原生模型表示已經在Michelangelo生產環境中運行了一年多,作爲一種健壯且可擴展的方法,在我們公司範圍內提供ML模型。

得益於這種演變和Michelangelo平臺的其他更新,Uber的技術棧可以支持新的用例(如靈活的試驗和在Uber的數據科學工作臺中訓練模型)、Jupyter筆記本環境以及使用TFTransformers的端到端深度學習。爲了介紹我們的經驗並幫助其他人擴展他們自己的ML建模解決方案,我們在2019年4月的Spark AI峯會上討論了這些更新,並提交了SPIP和JIRA,把我們對Spark MLlib的更改開源。

原文鏈接:

Evolving Michelangelo Model Representation for Flexibility at Scale

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章