Flink ML API,為實時機器學習設計的演算法介面與迭代引擎

ApacheFlink發表於2022-06-07

摘要:本文整理自阿里巴巴高階技術專家林東、阿里巴巴技術專家高贇(雲騫)在 Flink Forward Asia 2021 核心技術專場的演講。主要內容包括:

  1. 面向實時機器學習的 API
  2. 流批一體的迭代引擎
  3. Flink ML 生態建設

點選檢視直播回放 & 演講PDF

一、面向實時機器學習的 API

img

Flink ML API 指的是提供給使用者使用演算法的介面。通過把所有演算法打包為統一的 API 提供給使用者,讓所有使用者的體驗保持一致,也能降低學習和理解演算法的成本,此外演算法之間也可以更好地互動和相容。

舉個例子,在 Flink ML API 中提供一些基礎的功能類,通過使用這些功能類可以把不同運算元連線組合成一個高階的運算元,可以大大提高了演算法的開發效率。同時,通過使用統一的 Table API,讓所有的資料都以 Table 格式進行傳輸,可以使得不同公司開發的演算法能夠互相相容,降低不同公司重複開發的運算元的成本,提升演算法合作的效率。

img

之前版本的 Flink ML API 還是存在不少痛點。

首先是表達能力方面。之前的 API 的輸入只支援單個 Table 的形式,無法表達一些常見的演算法邏輯。比如有些訓練演算法的輸入表達是一張圖,把資料通過不同的 Table 傳進來,這種情況下單個 Table 輸入的介面就不適用了。再比如有些資料預處理的邏輯需要將多個輸入得到的資料進行融合,用單個 Table 輸入的 API 也不適合。因此我們計劃把演算法介面擴充套件為支援多輸入多輸出。

其次是實時訓練方面。之前的 API 無法原生支援實時機器學習場景。在實時機器學習中,我們希望訓練演算法可以實時產生模型資料,並將模型資料以流的方式實時傳輸到多個前端伺服器中。但是現有的介面只有一次性的訓練和一次性的推理 API,無法表達這種邏輯。

最後是易用性方面。之前採用 toJson() 和 fromJson() 來匯出和載入模型資料,並且允許使用者儲存這些資料。但是有些模型的資料量高達幾個 G,在這種情況下將模型資料以 string 的方式進行儲存,效率會非常低,甚至可能無法實現。當然,存在一些 hacky 方法,可以把模型資料儲存到一個遠端終端,再把相關的 url 通過 toJson() 方法傳匯出來。但是這種情況下會存在易用性的問題,演算法使用者需要自己去解析 URL,並從遠端獲取這些資料。

受限於以上幾個方面的因素,我們決定對 Flink ML API 進行擴充套件。

img

在經過大量討論以及思考之後,我們對新的 API 賦予了以下特性,解決了上面所有問題。

  • 第一,在 API 上增加了獲得模型資料的介面,比如在 model 上增加了 getModelData() 和 setModelData() API,能夠幫助實現實時機器學習場景;
  • 第二,對運算元的介面做了擴充套件,讓運算元可以支援多輸入多輸出,還可以將不同運算元以有向無環圖的方式進行整合,打包成更高階的運算元;
  • 第三,新的 API 是基於 datastream 實現的,可以支援流批一體的特性,能夠同時實現基於有限流和無限流的線上訓練;
  • 第四,對演算法的引數存取 API 做了改進,新的演算法引數的存取 API 更容易使用;
  • 第五,對模型的存取 API 做了改進,新的模型存取 API 採用了 save() 和 load() API,模型資料非常大的情況下使用者也無須考慮這方面的複雜度,只需要呼叫 save() 和 load() 就可以實現相關的功能;
  • 第六,提供了無模型語義的抽象類。

img

上圖是最新的 API 構架圖。最上層有一個 WithParams interface,它提供了存取引數的 API。我們對這個介面做了改進,使用者不再需要表達 isOptional 之類的 field。這個介面之下是一個 stage 介面,它包含了所有演算法模組,並提供了存取模組的 API,即 save() 和 load()。save() 負責把模型資料和引數儲存下來,load() 負責把模型資料和引數讀取出來,還原原先的 stage 例項。使用者不用考慮引數存取的複雜度。

Stage 下分為兩塊,一塊是表達訓練邏輯的 Estimator,另一塊是表達推理邏輯的 AlgoOperator 和 Model 類。Estimator 的核心 API 是 fit()。與之前不同的是現在支援多個 Table 的輸入,可以用來表達需要多個 Table 輸入邏輯,比如特徵的拼接。Estimator::fit() 輸出的是一個 Model。Model 屬於 AlgoOperator。AlgoOperator 表達的是計算邏輯,支援多個 Table 作為輸入和輸出,每個 Table 都是一個資料來源,可以用來表達通用的計算邏輯。

AlgoOperator 之下是 Transformer,可以表達對資料做轉換的邏輯。它與 AlgoOperator 具有相同的 API 格式,但是它們的抽象概念卻有所不同。Transformer 是一個具有模型語義的資料轉換邏輯。在計算中,比如資料預處理,存在一些更通用的將不同資料進行拼接轉換的操作,例如對資料進行過濾,在通用的概念下可能並不適用於 Transformer。因此我們特意增加了 AlgoOperator 類,方便使用者的理解和使用。

Transformer 之下是 model 類。我們增加了 setModelData() 和 getModelData() API。這兩個 API 是為實時機器學習專門設計的,可以讓使用者把模型資料實時匯出到多個遠端終端做線上的推理。

img

上圖是一個比較簡化但經典的實時機器學習場景。

這裡的資料來源主要有兩個,靜態資料來自於 HDFS,動態資料來自於 Kafka。由 AlgoOperator 讀取來自以上兩個資料來源的資料,將它們拼接之後形成一個 Table 輸入到 Estimator 邏輯。Estimator 讀取剛才拼接得到的資料併產生一個 Model,然後可以通過 getModelData() 拿到代表模型資料的 Table。再通過 sink() API 將這些資料傳輸到 Kafka topic。最後在多個前端伺服器上面執行程式,這些程式可以直接建立一個 Model 例項,從 Kafka 中讀出模型資料形成一個 Table,再通過 setModelData() 把這些資料傳遞給 Model,使用得到的 Model 做線上推理。

img

在支援線上訓練和線上推理之後,我們進一步提供了一些基礎元件,方便使用者通過簡單的運算元構建更復雜運算元,這個元件便是 FLIP-175 提供的 GraphBuilder。

假設使用者的輸入也是與上文一致的兩個資料來源,最終輸出到一個資料來源。使用者的核心計算邏輯可以分為兩塊,第一塊是資料預處理,比如特徵拼接,把兩個資料來源的資料讀進來之後做整合,以 Table 的形式輸出到 Estimator,執行第二塊的訓練邏輯。我們希望先執行訓練運算元,得到一個 Model。然後將預處理運算元和 Model 連線,表達線上推理邏輯。

使用者需要做的只是通過 GraphBuilder API 將上述步驟連線進行,不需要專門為線上推理邏輯再寫一遍連線邏輯。GraphBuilder 會自動從前面一個圖生成,並與後面圖中的運算元形成一一對應的關係。AlgoOperator 在訓練圖中的形式是直接轉換為推理圖中的運算元,而 Estimator 在訓練圖中得到的 Model 會成為推理圖中對應的節點,通過將這些節點相連,便得到了最後的 ModelA,最終用作線上推理。

二、流批一體的迭代引擎

img

Flink 是一個基於 Dag 描述執行邏輯的流批一體的處理引擎,但是在許多場景下,尤其是機器學習\圖計算型別的應用中,使用者還需要資料迭代處理的能力。例如,一些演算法的離線訓練、線上訓練以及模型部署後根據結果動態調整模型引數的場景,都需要資料迭代處理。

由於實際的場景同時會涵蓋離線和線上處理的案例,因此需要在迭代這一層能夠同時支援離線和線上處理。

img

前文提到的三個場景的處理邏輯既存在區別,也存在共性。

對於離線訓練,以邏輯迴歸為例,在迭代中可以使用一個節點來快取整個模型,這個節點會將最新的模型傳送給訓練節點,而訓練節點會在迭代開始前預先讀取整個資料集,隨後在每次收到最新的模型後,從資料集中選擇一個mini-batch資料對模型進行更新,並把結果傳送回模型快取節點。

對於線上計算,由於訓練資料是從外部源源不斷到達的,無法預先讀取所有訓練資料,一般的做法是動態讀取一個 mini-batch 的資料,計算完模型的更新後將其傳送給模型快取的節點,等模型的快取節點進一步傳送更新後的模型以後,再讀取下一個 mini-batch 資料,這也就要求訓練節點必須採用優先順序讀的方式對資料進行讀取,從而最終實現逐個處理 mini-batch 的能力。

在這兩種場景下的訓練都存在同步和非同步方式,具體取決於模型快取節點是否要收集到所有更新後再開始下一輪訓練。此外還存在一些模型,在預測的時候會對引數進行動態的更新,處理完每一條資料之後都要立刻評估是否會進行引數更新,如果需要就再發起更新。

img

這幾種場景下的計算邏輯存在一定的共性,首先都需要在作業圖中引入迭代的結構來支援資料的迴圈處理,並且在資料迴圈處理之後需要進行是否終止迭代的判斷。另一方面,計算過程中還需要在每一輪資料接收完成後,接收到相應的通知,觸發特定的計算,比如離線訓練中,接觸完整個模型後就要開始下一輪的計算。

這裡其實存在兩個選擇,一個是在迭代這一層,直接提供將資料集劃分為多個 mini-batch,並且對每個 mini-batch 賦予迭代的能力。它在邏輯上可以接受所有型別的迭代,但是如果想要同時支援逐個 mini-batch 處理與多個 mini-batch 並行處理的邏輯,就必須引入一套新的基於 mini-batch 的流處理介面,並且在實踐層支援這兩種處理邏輯。

另外線上訓練和離線訓練的 mini-batch 產生的方式也不一樣,離線 mini-batch 是通過本地預先讀取所有資料,然後在每一輪處理中進行選取來實現。而線上 mini-batch 通過從外部資料中讀取特定資料量的資料來實現的。因此如果想要相容這兩種情況,會進一步增加介面和實現的複雜度。

最後如果要相容單個 per-record 的處理,還必須引入無限大的 mini-batch,或將每條記錄看作一個單獨的 mini-batch,前者會進一步增加介面的複雜度,而後者需要每一記錄對運算元進行一次通知,會增加執行時的開銷。

考慮到上述情況,我們在迭代中只提供了整個資料集級別的通知,而將劃分 mini-batch 的功能放在了迭代之上。在離線訓練中,使用者可以通過從整個資料集中選取一部分資料來高效實現 mini-batch 選擇的功能。而在線上計算中,使用者可以通過 Flink 自帶的執行集輸入運算元,實現逐個 mini-batch 處理的功能。

img

基於上述思路,整個迭代的設計如上圖所示,主要由 4 部分組成。初始模型這類有回邊的輸入、資料集這類無回邊的只讀輸入、迭代回邊的位置以及最終輸出。其中回邊對應的資料集與有回邊的輸入是一一對應的,從回邊返回的資料與初始資料進行 union 之後,實現資料的迭代處理。

迭代內部為使用者提供了資料集處理完成的通知功能,即進度追蹤的能力。使用者基於這一能力可以實現處理完成資料集的某一輪之後執行特定操作。比如在離線訓練的時候,使用者可以在某個運算元收到模型的更新資料之後,計算模型的下一輪更新。

此外對於沒有回邊的輸入資料,允許使用者指定每一輪是否進行重放。對運算元也提供了每輪新建一個運算元以及通過一個運算元的例項處理所有輪次資料的能力。通過這種方式,使用者無須重建運算元例項就能實現在輪次之間快取資料的能力。使用者也可以通過回放輸入資料並重建運算元來複用迭代外的運算元,比如 Reduce、Join 這種常用運算元,輸入資料進行重放並且運算元會在某一輪進行重建,這種情況下使用者可以直接複用這些迭代外的運算元。

同時,我們提供了兩種終止判斷邏輯,一種是當整個迭代中已經沒有資料在處理的時候,會自然終止迭代。另外一種是在有限流的情況下,也允許使用者指定特定資料集,如果這一資料集在某輪沒有資料產生,使用者可以提前終止整個迭代。

img

DataStream<double[]> initParameters = …
DataStream<Tuple2<double[], Double>> dataset = …
​
DataStreamList resultStreams =
    Iterations.iterate(
          DataStreamList.of(initParameters),
          ReplayableDataStreamList.notReplay(dataset),
          IterationConfig.newBuilder().setOperatorRoundMode(ALL_ROUND).build();
          (variableStreams, dataStreams) -> {
                    DataStream<double[]> modelUpdate = variableStreams.get(0);
                    DataStream<Tuple2<double[], Double>> dataset = dataStreams.get(0);
 
                  
                    DataStream<double[]> newModelUpdate = …
                    DataStream<double[]> modelOutput = …
                    return new IterationBodyResult(
       DataStreamList.of(newModelUpdate), 
       DataStreamList.of(modelOutput)
                });
         
DataStream<double[]> finalModel = resultStreams.get("final_model");

上圖是使用迭代 API 來構建迭代的例子。使用者需要指定有回邊和無回邊的輸入列表、運算元是否需要每輪重建以及迭代體的計算邏輯的等。對於迭代體,使用者需要返回回邊對應的資料集以及迭代的最終輸出。

img

public static class ModelCacheFunction extends ProcessFunction<double[], double[]>
        implements IterationListener<double[]> { 
         
        private final double[] parameters = new double[N_DIM];
 
        public void processElement(double[] update, Context ctx, Collector<O> output) {
            // Suppose we have a util to add the second array to the first.
            ArrayUtils.addWith(parameters, update);
        }
 
        void onEpochWatermarkIncremented(int epochWatermark, Context context, Collector<T> collector) {
            if (epochWatermark < N_EPOCH * N_BATCH_PER_EPOCH) {
                collector.collect(parameters);
            }
        }
 
        public void onIterationEnd(int[] round, Context context) {
            context.output(FINAL_MODEL_OUTPUT_TAG, parameters);
        }
    }

對於迭代內的運算元,如果它實現了 IterationListener 介面,就會在所有資料集處理完某一輪之後,通知迭代的運算元。如果整個迭代都處理終止則會通過 onIterationTerminated 對運算元進行進一步通知,使用者可以在這兩個回撥中實現需要的計算邏輯。

img

在迭代的實現中,對於使用者通過程式碼來建立的迭代處理結構,會增加一部分迭代內部的節點,並對使用者所有的處理節點進行 wrap 操作,從而達到管理運算元生命週期並對資料型別進行轉換的目的。最後, 迭代基於 Colocation 與本地記憶體實現了回邊,這樣在排程器看來整個作業邏輯仍然是一個 DAG,從而可以直接複用現在的排程邏輯。

img

在迭代中插入的專用運算元主要包括 input、output、head 與 tail 運算元,其中 input 和 output 負責資料型別的轉換,外部資料進入迭代內時會為每一條記錄增加一個迭代頭,裡面記錄了該 record 處理的輪次,每個運算元的 wrap 會將迭代頭去掉後交給使用者原始的運算元處理。

head 和 tail 運算元負責實現回邊及計算某一輪迭代是否已經全部處理完成。head 運算元從 input 讀取完整個輸入,並在最後插入一條特殊的 EpochWatermark 事件,來標記第零輪迭代的終止。由於 head 運算元可能會存在多個併發,所以必須等 head 運算元的所有併發都讀取完輸入後,才能傳送第 0 輪終止的事件。

head 運算元通過 Operator Coordinator 來同步所有併發。Operator Coordinator 是一個位於 JobManager 中的全域性元件,它可以與所有 head task 進行雙向通訊,所有 head 運算元併發都收到每一輪處理完成的通知後,就會傳送全域性廣播給所有 head task,告訴他們這一輪的處理已經全部結束。head 傳送 EpochWaterMark 之後,所有迭代中的運算元都會與這一訊息進行對齊。運算元從所有輸入中都讀取到特定輪次的 EpochWatermark 之後,就會認為這一輪處理完成,並呼叫這一輪結束的回撥。當 tail 節點收到某一輪資料或 EpochWatermark 之後,會將輪次加 1,然後通過回邊再次傳送給 head,從而實現資料迴圈處理。

img

最後我們也支援了有迭代情況下的 checkpoint 功能。由於 Flink 預設的 checkpoint 機制無法支援帶環的計算圖,因此我們對 Flink 的 checkpoint 機制進行了擴充套件,實現了帶環的 Chandy-Lamport 演算法,會同時快取來自回邊的訊息。另外 head 運算元在對齊的時候,除了要讀取正常輸入的 barrier 之外,也會等待來自 Operator Coordinator 的特殊的 barrier。每一輪全域性結束的訊息也是來自 Operator Coordinator 廣播,可以保證每個 checkpoint 中所有迭代內的運算元都處在同一輪,從而簡化運算元後續進行併發修改的操作。

另外還有一個開發中的優化,Operator Coordinator 會將收到的 barrier 延遲到下一個全域性對齊訊息之後,再傳送通知,從而使得整個迭代內的運算元的 state 都是恰好處於讀取完某一輪資料之後。許多同步演算法都是先將快取收到的資料儲存在運算元中,直到讀取完一輪所有資料之後再進行統一處理。這一優化可以保證在進行 snapshot 操作的時候,正好清空所有快取,從而使整個 checkpoint 中快取的資料量最小。

img

以上是關於 Flink 流批一體迭代引擎的介紹,這些引擎可以同時在線上和離線場景中使用,並且支援 exactly-once 的容錯。未來我們將進一步支援 batch 的執行模式,並提供更多的上層開發工具。

三、Flink ML 生態建設

img

最近我們已經將 Flink ML 相關程式碼從 Flink 核心程式碼庫中移入一個單獨的 Flink ML 程式碼庫。這樣做的首先是為了方便 Flink ML 的快速迭代,其次也希望通過這個手段減少 Flink 核心程式碼庫的複雜度,避免 Flink 核心程式碼庫過於臃腫。

另外,我們在 Github 上建立了一箇中立的組織 Flink-extended,能夠為所有 Flink 社群的開發者提供平臺來貢獻一些他們希望開源的專案。方便大家分享不帶有特定公司的名字的程式碼,使不同公司的開發人員可以把程式碼貢獻出來,方便 Flink 社群來使用和共建。我們希望藉此促進 Flink 生態的繁榮發展。

目前中立專案中已經有一些比較重要的專案,比如 Deep Learning on Flink,它是由阿里大資料團隊主要開發的一個開源專案,核心作用是可以把 Tensorflow 打包成 Java 運算元在 Flink 中執行,方便將 Flink 的預處理程式與 Tensorflow 深度學習的訓練演算法相結合,形成端到端的訓練以及推理。

最近我們已經在 Flink ML 中新增了若干常見演算法,之後還會繼續提供更多開箱可用的演算法。

img

上圖是我們目前正在進行中的重要工作,其中最核心的工作是將現有的阿里巴巴開源的 Alink 程式碼庫進行改造,使其中的演算法能夠適配新設計的 Flink ML API,並將改造後的演算法貢獻到 Apache 專案,方便 Flink 使用者得到更多開箱可用的演算法。

此外,我們還與 360 一起合作共建 Clink 專案,核心目標是在離線計算中用 Java 去執行某些運算元,得到訓練結果。另一方面,這些運算元需要能夠以非常低的延遲做線上推理。然而低延遲線上推理很難用 Java 實現,通常需要用 C++ 來實現。為了使開發者只寫一遍演算法就能同時應用於 Java 和 C++ 環境,Clink 提供了一些打包的基礎類的功能,方便演算法開發者寫好 C++ 運算元之後,能夠使用 JNI 打包成 Java 運算元,並在 Flink 中使用這些運算元。

最後,我們計劃在 Flink ML 中開發對於 Python 的支援,其中包括允許演算法使用者通過寫 Python 程式將 Flink ML 中的 Java 運算元進行連線和組合使用,希望能提高機器學習開發者的效率和使用體驗。

以上工作基本都已經進入開源專案,其中演算法 API 的設計在 FLIP-173 中 ,迭代引擎的設計主要在 FLIP-176 中 ,FLIP-174 和 FLIP-175 分別提供了演算法引數的 API 以及 GraphBuilder 的 API。Clink 和 Deep Learning on Flink 等專案也已經在 Flink-extended 的組織上,歡迎大家使用。

點選檢視直播回放 & 演講PDF


更多 Flink 相關技術問題,可掃碼加入社群釘釘交流群
第一時間獲取最新技術文章和社群動態,請關注公眾號~

img

活動推薦

阿里雲基於 Apache Flink 構建的企業級產品-實時計算Flink版現開啟活動:
99 元試用 實時計算Flink版(包年包月、10CU)即有機會獲得 Flink 獨家定製衛衣;另包 3 個月及以上還有 85 折優惠!
瞭解活動詳情:https://www.aliyun.com/produc...

image.png

相關文章