模型訓練和模型服務是機器學習中兩個不同的階段,使用相同的演算法但處於不同的執行模式。模型佈署和模型釋出指的是將訓練好的模型佈署到生產環境,通常是將模型檔案載入預測服務中。常見的模型服務策略有三種:直接模型嵌入、模型服務和模型伺服器。直接模型嵌入將模型載入到應用程式程式中執行預測,但存在跨語言實作、資源消耗和版本管理等問題。模型服務為每個模型建立專用網頁服務,優點是簡單易用,但多模型管理成本高。模型伺服器以黑盒方式處理多種型別的模型,使用統一 API,降低維護成本,但實作較複雜。

模型服務設計

在機器學習領域中,模型訓練(training)和模型服務(serving)是兩個不同的階段,它們使用相同的機器學習演算法(相同的神經網路),但處於不同的執行模式。

 模型佈署(model deployment)和模型釋出(model release)是可互換的術語,它們指將訓練好的模型(檔案)佈署或複製到生產環境中,以便業務可以執行並讓客戶受益於這個新模型。通常,這意味著將模型檔案載入預測服務中。

常見的模型服務策略

在第6.3節中,我們將檢視具體的模型服務案例和預測服務設計。在此之前,讓我們先看看三種常見的模型服務策略:直接模型嵌入(direct model embedding)、模型服務(model service)和模型伺服器(model server)。無論您需要為特定的使用案例做什麼,通常可以採用以下三種方法之一來建立您的預測服務。

直接模型嵌入

直接模型嵌入意味著在使用者應用程式的程式中載入模型並執行模型預測。例如,花卉身份驗證的行動應用程式可以直接在其本地程式中載入影像分類別模型,並從給定的照片中預測植物身份。整個模型的載入和服務都在本地(在手機上)進行,無需與其他程式或遠端伺服器通訊。

大多數使用者應用程式,如行動應用程式,都是用強型別語言編寫的,例如Go、Java和C#,但大多數深度學習建模程式碼都是用Python編寫的。因此,很難將模型程式碼嵌入應用程式碼中,即使您這樣做了,這個過程也可能需要一段時間。為了促進跨非Python程式的模型預測,像PyTorch和TensorFlow這樣的深度學習框架提供了C++函式庫。此外,TensorFlow還提供了Java(https://github.com/tensorflow/java)和JavaScript(https://github.com/tensorflow/tfjs)函式庫,用於直接從Java或JavaScript應用程式中載入和執行TensorFlow模型。

# 載入 TensorFlow 模型的範例程式碼
import tensorflow as tf

# 載入模型
model = tf.saved_model.load("model_path")

# 使用模型進行預測
predictions = model(input_data)

內容解密:

上述程式碼展示瞭如何使用TensorFlow載入一個儲存的模型並進行預測。首先,我們匯入了TensorFlow函式庫。接著,使用tf.saved_model.load方法載入儲存的模型。最後,我們可以使用載入的模型對輸入資料進行預測。

直接嵌入的另一個缺點是資源消耗。如果模型在客戶端裝置上執行,沒有高階裝置的使用者可能無法獲得良好的體驗。執行大型深度學習模型需要大量的計算,這可能會導致應用程式變慢。

最後,直接嵌入涉及將模型服務程式碼與應用程式業務邏輯混合,這對向後相容性提出了挑戰。因此,由於它很少被使用,我們只對其進行簡要描述。

模型服務

模型服務指的是在伺服器端執行模型服務。對於每個模型、每個版本的模型或每種型別的模型,我們為其構建一個專用的網頁服務。此網頁服務透過HTTP或gRPC介面公開模型的預測API。

# 使用 Flask 建立模型服務的範例程式碼
from flask import Flask, request
import tensorflow as tf

app = Flask(__name__)

# 載入模型
model = tf.saved_model.load("model_path")

@app.route('/predict', methods=['POST'])
def predict():
    # 取得輸入資料
    input_data = request.get_json()
    
    # 使用模型進行預測
    predictions = model(input_data)
    
    # 傳回預測結果
    return predictions

if __name__ == '__main__':
    app.run()

內容解密:

上述程式碼展示瞭如何使用Flask框架建立一個簡單的模型服務。首先,我們匯入了必要的函式庫並建立了一個Flask應用程式。接著,載入了儲存的TensorFlow模型。在/predict端點上,當接收到POST請求時,我們從請求中取得輸入資料,使用載入的模型進行預測,並將預測結果傳回給客戶端。

模型服務的最大優勢是簡單性。我們可以輕易地將模型的訓練容器轉換為模型服務容器,因為本質上,模型的預測執行涉及到執行訓練好的模型神經網路。透過新增HTTP或gRPC介面並將神經網路設定為評估模式,模型的訓練程式碼可以快速轉變為預測網頁服務。

由於模型服務是特定於模型演算法的,因此我們需要為不同的模型型別或版本構建單獨的服務。如果您有多個不同的模型需要服務,這種一服務對一模型的方法可能會產生許多服務,而維護這些服務(如補丁、佈署和監控)可能會非常耗費人力。如果您面臨這種情況,模型伺服器方法是正確的選擇。

模型伺服器

模型伺服器方法旨在以黑盒方式處理多種型別的模型。無論模型的演算法和版本如何,模型伺服器都可以使用統一的網頁預測API操作這些模型。對於新的型號或新版本的型號,我們不再需要更改程式碼或佈署新的服務。這節省了許多在模型服務方法中重複的開發和維護工作。

然而,相比於模型服務方法,實施和管理統一處理各種型號的模型的伺服器要複雜得多。在一個服務和統一的API中處理不同型號的模型的服務是一項挑戰。模型的演算法和資料不同,其預測功能也不同。例如,影像分類別模型可以使用CNN網路進行訓練,而文字分類別模型可以使用LSTM網路進行訓練。它們的輸入資料不同(文字vs.影像),其演算法也不同(CNN vs. LSTM)。

package "資料處理" {
    component [資料收集] as collect
    component [資料清洗] as clean
    component [特徵工程] as feature
}

package "模型訓練" {
    component [模型選擇] as select
    component [超參數調優] as tune
    component [交叉驗證] as cv
}

package "評估部署" {
    component [模型評估] as eval
    component [模型部署] as deploy
    component [監控維護] as monitor
}

}

collect –> clean : 原始資料 clean –> feature : 乾淨資料 feature –> select : 特徵向量 select –> tune : 基礎模型 tune –> cv : 最佳參數 cv –> eval : 訓練模型 eval –> deploy : 驗證模型 deploy –> monitor : 生產模型

note right of feature 特徵工程包含:

  • 特徵選擇
  • 特徵轉換
  • 降維處理 end note

note right of eval 評估指標:

  • 準確率/召回率
  • F1 Score
  • AUC-ROC end note

@enduml


此圖示展示了客戶端如何透過HTTP或gRPC請求與模型伺服器互動,伺服器載入並執行適當的模型以生成預測結果,並將結果傳回給客戶端。圖表清晰地呈現了客戶端、模型伺服器以及預測結果之間的邏輯關係。

## 設計預測服務

在軟體系統設計中,一個常見的錯誤是試圖建立一個萬能的系統,而不考慮具體的使用者場景。過度設計會將我們的注意力從立即的客戶需求轉移到未來可能有用的功能上。因此,系統要麼需要不必要地花很長時間來構建,要麼難以使用。對於模型服務來說,這種情況尤其如此。

深度學習是一個昂貴的業務,無論是在人力還是計算資源方面。我們應該只構建必要的東西,以便盡快將模型投入生產,並盡量減少營運成本。為此,我們需要從使用者場景開始。

在本文中,我們將介紹三種典型的模型服務場景,從簡單到複雜。對於每個使用案例,我們將解釋場景並闡述一個合適的高階設計。透過依次閱讀以下三個小節,您將看到當使用案例變得越來越複雜時,預測服務的設計是如何演變的。

### 單一模型應用

設想建立一個手機應用程式,可以在兩張圖片之間交換人們的臉部。消費者期望應用程式的UI上傳照片,選擇來源和目標圖片,並執行深度偽造模型(deepfake model)來交換兩個選定影像之間的臉部。對於只需要與一個模型一起工作的應用程式,服務方法可以是模型服務(model service,6.2.2)或直接模型嵌入(direct model embedding,6.2.1)。

#### 模型服務方法

根據6.2.2節的討論,模型服務方法涉及為每個模型建立一個網頁服務。因此,我們可以使用以下三個元件來構建人臉交換模型應用程式:一個在手機上執行的前端UI應用程式(元件A);一個處理使用者操作的應用程式後端(元件B);以及一個後端服務或預測器(元件C),用於託管深度偽造模型並公開網頁API以執行每個面部交換請求的模型。

當使用者上傳來源影像和目標影像並點選手機應用程式上的面部交換按鈕時,行動裝置後端應用程式將接收請求並呼叫預測器的網頁API進行面部交換。然後,預測器對使用者請求資料(影像)進行前處理,執行模型演算法,並對模型輸出(影像)進行後處理,以傳回給應用程式後端。最終,手機應用程式將顯示具有交換臉部的來源和目標影像。圖6.4說明瞭一個適合面部交換使用案例的一般設計。

#### 直接模型嵌入方法

另一種設計方法是將模型執行程式碼與應用程式的使用者邏輯程式碼結合起來。沒有伺服器後端,因此一切都在使用者本地的電腦或手機上發生。以人臉交換應用程式為例,深度偽造模型檔案位於應用程式的佈署套件中,當應用程式啟動時,模型被載入到應用程式的行程空間中。圖6.5說明瞭這個概念。

### 設計比較

兩種設計方法各有優缺點。模型服務方法提供了一個獨立的服務,可以輕鬆地擴充套件和維護,但需要額外的網路請求。直接模型嵌入方法減少了一個網路跳躍,提高了服務的可除錯性,但需要在應用程式中整合模型服務邏輯。

### 圖表說明

    package "資料處理" {
        component [資料收集] as collect
        component [資料清洗] as clean
        component [特徵工程] as feature
    }

    package "模型訓練" {
        component [模型選擇] as select
        component [超參數調優] as tune
        component [交叉驗證] as cv
    }

    package "評估部署" {
        component [模型評估] as eval
        component [模型部署] as deploy
        component [監控維護] as monitor
    }
}

collect --> clean : 原始資料
clean --> feature : 乾淨資料
feature --> select : 特徵向量
select --> tune : 基礎模型
tune --> cv : 最佳參數
cv --> eval : 訓練模型
eval --> deploy : 驗證模型
deploy --> monitor : 生產模型

note right of feature
  特徵工程包含:
  - 特徵選擇
  - 特徵轉換
  - 降維處理
end note

note right of eval
  評估指標:
  - 準確率/召回率
  - F1 Score
  - AUC-ROC
end note

@enduml

此圖示展示了客戶端應用程式、應用程式後端和預測器服務之間的互動流程。

內容解密:

  • 圖表展示了單一模型預測服務的設計流程,包括客戶端應用程式上傳圖片、應用程式後端呼叫預測器服務、預測器服務執行模型演算法並傳回結果。
  • 預測器服務可以獨立佈署和擴充套件,提高了系統的可維護性和可擴充套件性。
  • 直接模型嵌入方法可以減少網路請求,提高服務的可除錯性,但需要在應用程式中整合模型服務邏輯。

為何模型服務(Model Service)方法更受歡迎?

雖然直接將模型嵌入應用程式的方法看似簡單且節省了一個網路跳躍(network hop),但在實際建置模型服務時,這種方法並未成為主流選擇。主要有以下四個原因:

1. 模型演算法需要以不同語言重新實作

模型的演算法和執行程式碼通常以 Python 編寫。如果選擇模型服務方法,將模型服務實作為 Web 服務(如圖 6.4 中的預測器),我們可以重複使用大部分的訓練程式碼並快速建置。但若選擇將模型服務嵌入非 Python 應用程式,就必須在該應用程式的語言(如 Java 或 C++)中重新實作模型的載入、執行和資料處理邏輯。這項工作並非微不足道,且並非所有開發者都具備足夠的知識來重寫訓練演算法。

2. 所有權界限變得模糊

當將模型嵌入應用程式時,業務邏輯程式碼可能會與服務程式碼混雜在一起。隨著程式碼函式庫變得複雜,很難在服務程式碼(由資料科學家擁有)和其他應用程式碼(由開發者擁有)之間劃清界限。當資料科學家和開發者來自不同的團隊但在同一個程式碼倉函式庫中工作時,由於跨團隊的程式碼審查和佈署需要比平常更長的時間,交付速度將會顯著下降。

3. 使用者端裝置可能出現效能問題

通常,應用程式執行在客戶的行動裝置、平板電腦電腦或低階筆電上。在這些裝置上,從原始使用者資料中擷取特徵,然後進行模型輸入資料的預處理和執行模型預測,可能會導致 CPU 使用率飆升、應用程式變慢和記憶體使用率高等效能問題。

4. 記憶體洩漏問題容易發生

例如,在 Java 中執行 TensorFlow 模型時,演算法執行和輸入/輸出引數物件都建立在本機空間。這些物件不會被 Java 的垃圾回收(GC)自動回收,我們必須手動釋放資源。很容易忽略回收模型所佔用的本機資源,而且由於本機物件的記憶體分配不會被 Java 堆積疊追蹤,它們的記憶體使用情況難以觀察和衡量。因此,記憶體洩漏很容易發生且難以修復。

使用 Jemalloc 排查本機記憶體洩漏

要排查本機記憶體洩漏,Jemalloc 是一個非常有用的工具。您可以檢視我的部落格文章「Fix Memory Issues in Your Java Apps」(http://mng.bz/lJ8o)以瞭解更多詳情。

根據上述原因,我們強烈建議對於單一模型應用場景採用模型服務方法。

多租戶應用場景

我們將以一個聊天機器人應用為例來說明多租戶場景。首先,讓我們設定背景。一個租戶是指使用聊天機器人應用與其客戶溝通的公司或組織(如學校或零售店)。這些租戶使用相同的軟體/服務——聊天機器人應用,但擁有獨立的帳戶並且資料是隔離的。聊天使用者是指租戶的客戶,他們使用聊天機器人與租戶進行業務往來。

多租戶設計

在設計上,聊天機器人應用依賴於意圖分類別模型來識別使用者對話中的意圖,然後將使用者請求重新導向到租戶的正確服務部門。目前,這個聊天機器人採用單一模型應用方法,即對每位使用者和租戶使用單一的意圖分類別模型。

現在,由於來自租戶的客戶反饋表明單一意圖分類別模型的預測準確度較低,我們決定允許租戶使用我們的訓練演算法,以他們自己的資料集建立自己的模型。這樣,模型可以更好地適應每個租戶的業務情況。對於模型服務,我們將允許租戶使用他們自己的模型進行意圖分類別預測請求。當聊天使用者現在與聊天機器人應用互動時,應用將找到該租戶特定的模型來回答使用者的問題。因此,聊天機器人變成了多租戶應用。

在這個聊天機器人的多租戶場景中,雖然不同租戶擁有的模型是由不同資料集訓練而成,但它們是同一型別的模型。由於這些模型是使用相同的演算法訓練的,它們的演算法和預測函式都是相同的。我們可以擴充套件圖 6.4 中的模型服務設計,透過新增一個模型快取來支援多租戶。在記憶體中快取模型圖及其依賴資料,可以在一個服務中執行多租戶模型的服務。圖 6.6 說明瞭這個概念。

圖 6.6 多租戶模型服務設計

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 模型服務設計策略

package "直接模型嵌入" {
    component [客戶端應用] as client_app
    component [本地模型載入] as local_load
    component [TensorFlow/PyTorch\nC++/Java 函式庫] as lib
}

package "模型服務 (Model Service)" {
    component [Flask/FastAPI] as flask
    component [HTTP/gRPC API] as api
    component [單一模型服務] as single_svc
}

package "模型伺服器 (Model Server)" {
    component [統一 API 端點] as unified_api
    component [路由元件] as router
    component [模型 A (CNN)] as model_a
    component [模型 B (LSTM)] as model_b
    component [模型 C (Transformer)] as model_c
}

package "多租戶平台" {
    component [預測服務] as pred_svc
    component [租戶 A] as tenant_a
    component [租戶 B] as tenant_b
}

client_app --> local_load : 載入模型
local_load --> lib : 跨語言支援

flask --> api : 暴露端點
api --> single_svc : 處理請求

unified_api --> router : 請求分發
router --> model_a : CNN 請求
router --> model_b : LSTM 請求
router --> model_c : Transformer 請求

pred_svc --> tenant_a : 客戶 A 模型
pred_svc --> tenant_b : 客戶 B 模型

note right of single_svc
  模型服務優點:
  - 簡單易實作
  - 快速轉換訓練容器
end note

note right of router
  模型伺服器優點:
  - 統一管理多模型
  - 降低維護成本
end note

@enduml
package "資料處理" {
    component [資料收集] as collect
    component [資料清洗] as clean
    component [特徵工程] as feature
}

package "模型訓練" {
    component [模型選擇] as select
    component [超參數調優] as tune
    component [交叉驗證] as cv
}

package "評估部署" {
    component [模型評估] as eval
    component [模型部署] as deploy
    component [監控維護] as monitor
}

}

collect –> clean : 原始資料 clean –> feature : 乾淨資料 feature –> select : 特徵向量 select –> tune : 基礎模型 tune –> cv : 最佳參數 cv –> eval : 訓練模型 eval –> deploy : 驗證模型 deploy –> monitor : 生產模型

note right of feature 特徵工程包含:

  • 特徵選擇
  • 特徵轉換
  • 降維處理 end note

note right of eval 評估指標:

  • 準確率/召回率
  • F1 Score
  • AUC-ROC end note

@enduml


此圖示展示了多租戶模型的服務架構,其中包含了一個用於存放不同模型的**模型快取**(元件 A)以及一個用於儲存和管理模型檔案的**模型檔案伺服器**(元件 B)。

#### 內容解密:
1. **模型快取**:用於在記憶體中存放多個模型的圖結構及其相關資料,以支援多租戶模型的載入與執行。
2. **預測服務**:負責接收客戶端的預測請求,並根據請求中的租戶資訊,從**模型快取**中提取對應的模型進行預測。
3. **模型檔案伺服器**:用於儲存各個租戶的模型檔案,供**預測服務**動態載入至**模型快取**中使用。

相比於圖 6.4 中的單一模型服務設計,圖 6.6 的設計新增了**模型快取**(元件 A)和**模型檔案伺服器**(元件 B)。由於我們希望在一個服務中支援多個模型的執行,因此需要在記憶體中使用**模型快取**來存放和執行不同的模型。同時,透過**模型檔案伺服器**來集中管理各個模型的檔案,以便動態載入至**模型快取**中。

要建立一個良好的**模型快取**,需要考慮**快取管理**和**記憶體資源管理**。對於**模型快取**,我們需要為每個快取中的模型分配一個唯一的`model ID`作為快取鍵,以便識別每個模型的身份。例如,可以使用模型的訓練執行 ID 作為`model ID`;這樣的好處是,對於快取中的每個模型,我們都可以追蹤到是哪一次訓練執行產生了它。另一種更靈活的`model ID`建構方法是結合自定義的`model name`和`model version`。無論採用哪種`model ID`策略,它都必須是唯一的,並且需要在每次預測請求中提供對應的`model ID`。

與單一模型的場景類別似,多租戶模型的載入與管理也需要高效且穩定的機制,以確保各個租戶能夠順暢地使用各自的模型進行預測。同時,這種設計也使得系統具備了良好的擴充套件性,能夠支援更多租戶及模型的擴充需求。

#### 程式碼範例:動態載入多租戶模型的範例
```python
# 載入指定 model_id 的模型
def load_model(model_id):
    model_path = fetch_model_path_from_server(model_id)
    model = tf.saved_model.load(model_path)
    model_cache[model_id] = model
    return model

# 從快取或檔案伺服器取得對應 model_id 的模型
def get_model(model_id):
    if model_id in model_cache:
        return model_cache[model_id]
    else:
        return load_model(model_id)

# 處理客戶端的預測請求
def handle_prediction_request(request):
    model_id = request['model_id']
    input_data = request['input_data']
    model = get_model(model_id)
    prediction_result = model(input_data)
    return prediction_result

內容解密:

  1. load_model函式: 當指定的model_id不在快取中時,會從模型檔案伺服器取得對應的model_path並載入該模型至記憶體,然後放入model_cache中。
  2. get_model函式: 用於從model_cache中取得已載入的模型;如果未命中,則呼叫load_model動態載入。
  3. handle_prediction_request函式: 處理客戶端的預測請求,從請求中提取出model_id及輸入資料,並利用對應的模型進行預測,最後傳回結果。

這樣的設計不僅能夠有效支援多租戶場景下的不同業務需求,還能靈活地管理和更新各個租戶所使用的 AI 模型,使得整體系統具備良好的可擴充套件性和維護性。

設計預測服務的挑戰與解決方案

在模型服務設計中,預測服務的設計是一個關鍵環節。由於每個伺服器的記憶體和GPU資源有限,我們無法將所有所需的模型都載入記憶體。因此,我們需要建立模型交換邏輯到模型快取中。當資源容量達到上限時,例如,行程即將耗盡記憶體,則需要從模型快取中驅逐一些模型,以釋放資源供新的模型預測請求使用。

模型快取設計的挑戰

為了減少快取遺失率(請求模型不在快取中)並使模型交換不那麼具有破壞性,可以採用一些方法,如LRU(最近最少使用)演算法和跨不同例項的模型分割。不過,當面臨多種模型型別時,是否能夠擴充套件模型快取設計以支援多種模型型別呢?

多種模型型別的支援問題

我們不建議將模型快取設計擴充套件到多種模型型別。因為不同模型型別的輸入/輸出資料格式和資料處理邏輯差異很大,例如影像分類別模型和意圖分類別模型,所以很難在同一個模型快取中託管和執行不同型別的模型。這樣做需要為每種型別的模型建立獨立的網頁介面和獨立的資料前處理和後處理程式碼。

一服務一模型型別的方法

對於每種型別的模型,建立獨立的預測服務是一個比較可行的方案,每個服務都有自己的網頁介面和資料處理邏輯,並管理自己的模型快取。例如,可以為影像分類別和意圖分類別這兩種不同的模型型別分別建立獨立的預測服務。

可擴充套件性問題

雖然一服務一模型型別的方法在只有幾種模型型別時運作良好,但當有20多種模型型別時,這種方法就難以擴充套件。建立和維護網頁服務(如設定CI/CD管道、網路和佈署)是非常昂貴的。此外,監控服務的工作量也是不容忽視的;需要建立監控和警示機制,以確保服務24/7不間斷執行。

預測平台的設計

當有多個應用程式需要模型服務支援時,建立一個集中的預測服務(預測平台)是更好的選擇。預測平台採用模型伺服器方法(6.2.3節),並在一個系統中處理所有型別的模型服務。這是支援多個應用程式最具成本效益的方法,因為模型上線和維護成本僅限於一個系統。

預測平台的元件

  1. 統一的網頁API:為了支援任意模型,需要設計通用的API。無論呼叫哪個模型,API的規範(如預測請求和回應的有效載荷架構)都應該足夠通用,以滿足模型的演算法需求。

  2. 路由元件:由於每種型別的服務後端通常只能處理幾種型別的模型,因此需要不同的服務後端(如TensorFlow Serving後端用於TensorFlow模型,TorchServe後端用於PyTorch模型)。路由元件負責將預測請求路由到正確的後端推斷伺服器。

此圖示說明瞭預測平台的整體架構,包括應用程式如何透過統一的網頁API傳送預測請求,以及路由元件如何將請求轉發到適當的推斷伺服器。