返回文章列表

資料集管理服務設計與實作

本文探討資料集管理服務的設計與實作,包含資料集擷取 API 設計、版本控制、資料儲存結構、不同資料集型別處理以及強型別資料結構的優勢。服務提供 `PrepareTrainingDataset` 與 `FetchTrainingDataset` 兩個

資料科學 系統設計

資料集管理服務在機器學習流程中扮演著關鍵角色,負責資料集的儲存、版本控制和提供訓練資料。為了滿足不同資料科學家的需求,服務設計了彈性的資料集擷取方式和多樣化的資料集型別支援。PrepareTrainingDataset API 允許使用者提交資料準備請求,並傳回一個版本雜湊值,而 FetchTrainingDataset API 則允許使用者使用版本雜湊值查詢和取得訓練資料。這種非同步的設計模式有效地提升了服務的效率和穩定性,避免了長時間的資料準備阻塞請求。同時,服務內部採用了版本控制機制,確保資料集的可追溯性和一致性,方便資料科學家進行實驗和模型訓練。

2.2 資料集管理服務總覽

在資料科學領域中,資料集的管理對於模型的訓練至關重要。Julia 作為一名資料科學家,可以透過呼叫 FetchTrainingDataset API 來取得訓練資料,如果背景執行緒已經完成資料準備。

資料集擷取方法的定義

首先,讓我們檢視 gRPC 服務方法的定義(位於 grpc-contract/src/main/proto/data_management.proto),該定義描述了兩個用於擷取資料集的方法:PrepareTrainingDatasetFetchTrainingDataset

rpc PrepareTrainingDataset (DatasetQuery) returns (DatasetVersionHash);
rpc FetchTrainingDataset (VersionHashQuery) returns (VersionHashDataset);

message DatasetQuery {
  string dataset_id = 1;
  string commit_id = 2;
  repeated Tag tags = 3;
}

message VersionHashQuery {
  string dataset_id = 1;
  string version_hash = 2;
}

清單 2.9 訓練資料集擷取服務定義

圖 2.10 展示了資料集擷取層中的兩個方法:PrepareTrainingDatasetFetchTrainingDataset,這兩個方法支援資料集的擷取。

為何需要兩個 API 來擷取資料集

如果只提供一個 API 用於取得訓練資料,呼叫者需要等待 API 呼叫直到後端資料準備完成才能獲得最終的訓練資料。如果資料準備需要很長時間,這個請求可能會逾時。

圖 2.11 對資料集建置請求的八個步驟進行了高階概述

當資料集管理服務(DM)收到資料集準備請求(圖 2.11,步驟 1)時,它會執行三個動作:

  • 試圖在儲存中找到指定資料集 ID 的資料集。
  • 套用給定的資料過濾器來選擇資料集中的提交。
  • 建立一個 versionedSnapshot 物件來追蹤其內部儲存中的訓練資料(versionHashRegistry)。versionedSnapshot 物件的 ID 是根據所選提交的 ID 清單生成的雜湊字串。

清單 2.10 準備訓練資料請求 API 的程式碼實作

public void prepareTrainingDataset(DatasetQuery request) {
  // 步驟 1,接收資料集準備請求
  Dataset dataset = store.datasets.get(datasetId);
  String commitId;
  // ...
}

內容解密:

  1. prepareTrainingDataset 方法:此方法負責處理 DatasetQuery 請求,首先根據 datasetId 從儲存中取得對應的 Dataset 物件。
  2. DatasetQuery 物件:包含了 dataset_idcommit_idtags 等資訊,用於指定要準備的訓練資料集及其過濾條件。
  3. versionedSnapshot 物件的建立:根據所選提交的 ID 清單生成一個唯一的雜湊字串作為 versionedSnapshot 的 ID,用於追蹤和管理訓練資料。
  4. 背景執行緒處理:在建立 versionedSnapshot 後,DM 會啟動背景執行緒來實際建置訓練資料集,包括下載、聚合和壓縮所選提交的檔案,並將結果上傳至 MinIO 伺服器。

為何使用兩個步驟來擷取資料集

使用兩個 API 可以避免因資料準備時間過長導致的請求逾時問題。第一個 API 用於提交資料準備請求,第二個 API 用於查詢資料準備狀態並取得最終的訓練資料。這種設計使得資料集擷取 API 的效能保持一致,不受資料集大小的影響。

資料模式的討論

在資料集管理服務中,原始的 ingested 資料(提交)和生成的訓練資料(versionedSnapshot)以不同的資料格式儲存。資料合併操作(圖 2.11,步驟 6 和 7)將原始的 ingested 資料聚合並轉換成特定格式的訓練資料。關於資料模式的詳細討論將在第 2.2.6 節進行。

資料集管理服務的核心運作機制

在資料科學的領域中,資料集管理服務扮演著至關重要的角色。該服務不僅需要有效地管理資料集,還要確保資料的正確性與可重複使用性。以下將探討資料集管理服務的內部運作機制。

準備訓練資料集的請求處理

當資料集管理服務(DM Service)接收到準備訓練資料集的請求時,它會啟動一個後台任務來構建訓練資料,並傳回一個version_hash字串以供追蹤。使用者可以利用FetchTrainingDataset API和version_hash字串來查詢資料集構建的進度,並最終取得訓練資料集。

關鍵實作步驟

  1. 選擇資料提交: 根據標籤過濾器選擇特定的資料提交。

    BitSet pickedCommits = new BitSet();
    List<DatasetPart> parts = Lists.newArrayList();
    List<CommitInfo> commitInfoList = Lists.newLinkedList();
    for (int i = 1; i <= Integer.parseInt(commitId); i++) {
        CommitInfo commit = dataset.commits.get(Integer.toString(i));
        boolean matched = true;
        for (Tag tag : request.getTagsList()) {
            matched &= commit.getTagsList().stream().anyMatch(k -> k.equals(tag));
        }
        if (!matched) {
            continue;
        }
        pickedCommits.set(i);
        commitInfoList.add(commit);
        // ...
    }
    

    內容解密:

    • 使用BitSet來記錄被選中的提交。
    • 遍歷所有提交,並檢查是否匹配請求中的標籤。
    • 如果提交的標籤與請求標籤不匹配,則跳過該提交。
    • 將匹配的提交新增到commitInfoList中。
  2. 生成版本雜湊: 從選定的提交列表中生成版本雜湊。

    String versionHash = String.format("hash%s",
        Base64.getEncoder().encodeToString(pickedCommits.toByteArray()));
    

    內容解密:

    • pickedCommits轉換為位元組陣列並進行Base64編碼。
    • 生成以"hash"為字首的版本雜湊字串。
  3. 啟動後台任務: 在後台執行緒中聚合資料並構建訓練資料集。

    threadPool.submit(
        new DatasetCompressor(minioClient, store, datasetId,
            dataset.getDatasetType(), parts, versionHash,
            config.minioBucketName));
    

    內容解密:

    • 提交一個DatasetCompressor任務到執行緒池。
    • 該任務負責將選定的提交壓縮成訓練資料集。

取得訓練資料集

使用者可以透過FetchTrainingDataset API和version_hash字串來查詢訓練資料集的準備狀態。

實作細節

public void fetchTrainingDataset(VersionQuery request) {
    String datasetId = request.getDatasetId();
    Dataset dataset = store.datasets.get(datasetId);
    if (dataset.versionHashRegistry.containsKey(
        request.getVersionHash())) {
        responseObserver.onNext(
            dataset.versionHashRegistry.get(
                request.getVersionHash()));
        responseObserver.onCompleted();
        // ...
    }
}

內容解密:

  • 根據請求中的datasetId取得對應的資料集。
  • 檢查versionHashRegistry中是否存在請求中的versionHash
  • 如果存在,則傳回對應的versionedSnapshot物件。

內部資料集儲存結構

資料集管理服務內部儲存結構設計為能夠同時處理動態和靜態資料。動態資料指的是不斷被新資料更新的提交,而靜態資料則是指由這些提交生成的、可用於訓練的版本化快照。

資料儲存概覽

  • 提交(Commits): 代表動態被吸收的原始資料,具有標籤和註解訊息。
  • 版本化快照(Versioned Snapshots): 代表靜態的訓練資料,由選定的提交轉換而來,每個快照都有一個版本號。

此圖示展示了資料集內部儲存結構,包含用於原始資料的提交和用於訓練資料的版本化快照。

綜上所述,資料集管理服務透過精心設計的內部機制,能夠有效地管理資料集並確保訓練資料的可重複使用性,為資料科學家提供了強大的支援。

資料集管理服務深度解析

在探討資料集管理(Dataset Management, DM)服務的實作細節之前,我們首先需要了解資料集的儲存概念以及如何有效地管理和操作不同型別的資料集。資料集管理服務旨在提供一個統一的API集合,以處理不同型別的資料集,包括圖片、音訊和文字等。

統一的API與儲存結構

為了實作跨不同資料集型別的統一操作,我們設計了一個統一的API集合。這使得我們能夠使用相同的儲存結構來儲存和管理不同型別的資料集。在我們的儲存實作中,實際的檔案(如提交資料和快照資料)被儲存在雲端物件儲存服務(如Amazon S3)中,而資料集的後設資料則儲存在我們的DM系統中。

資料集的寫入與讀取

那麼,實際的資料集寫入和讀取是如何工作的?我們如何為不同型別的資料集(如GENERIC和TEXT_INTENT)序列化提交和快照?在儲存後端的實作中,我們採用了一個簡單的繼承概念來處理不同資料集型別的檔案操作。我們定義了一個DatasetTransformer介面,其中包括ingest()函式和compress()函式。ingest()函式將輸入資料儲存為內部儲存中的一個提交,而compress()函式則將選定的提交資料合併成一個版本快照(訓練資料)。

程式碼範例:DatasetTransformer介面

public interface DatasetTransformer {
    void ingest(String data);
    void compress(List<String> commitIds);
}

內容解密:

  1. DatasetTransformer介面定義了兩個關鍵方法:ingest()compress(),分別用於處理資料的輸入和壓縮合併。
  2. 對於TEXT_INTENT型別的資料集,我們使用IntentTextTransformer來應用強型別的檔案結構於檔案轉換。
  3. 對於GENERIC型別的資料集,我們使用GenericTransformer來儲存原始格式的資料,無需任何檢查或格式轉換。

資料集後設資料

我們將資料集後設資料定義為除實際資料檔案以外的所有資訊,例如資料集ID、資料擁有者、變更歷史(稽核)、訓練快照、提交、資料統計等。為了示範目的,我們將資料集的後設資料儲存在記憶體字典中,並將所有資料檔案儲存在MinIO伺服器中。但您可以擴充套件它以使用資料函式庫或NoSQL資料函式庫來儲存資料集的後設資料。

資料結構定義

對於每種強型別的資料集,例如TEXT_INTENT資料集,我們定義了兩個資料結構:一個用於資料攝取,另一個用於訓練資料擷取。這兩個不同的資料結構是DM服務提供給我們的兩種不同使用者(Jianguo和Julia)的資料契約。

TEXT_INTENT資料集攝取資料結構

<text utterance>, <label>,<label>,<label>, ...

TEXT_INTENT訓練資料結構

包含兩個檔案:examples.csv和labels.csv。Labels.csv定義了標籤名稱到標籤ID的對映,而examples.csv定義了訓練文字(utterance)到標籤ID的對映。

程式碼範例:TEXT_INTENT訓練資料結構

# examples.csv
"I am still waiting on my credit card", 0;1
"I couldn’t purchase gas in Costco", 2

# labels.csv
0, activate_my_card
1, card_arrival
2, card_not_working

內容解密:

  1. TEXT_INTENT訓練資料結構被設計為一個zip檔案,包含兩個關鍵檔案:examples.csv和labels.csv。
  2. examples.csv檔案中的每一行代表一個訓練樣本,包含文字表達和對應的標籤ID。
  3. labels.csv檔案定義了標籤ID和標籤名稱之間的對映關係。

為什麼使用自定義的資料結構

我們為TEXT_INTENT建立了自定義的資料結構,而不是使用PyTorch或Tensorflow資料集格式(如TFRecordDataset),以從模型訓練框架中抽象出來。這種設計使得我們的DM服務能夠靈活地支援不同的模型訓練框架,並且能夠根據具體需求進行調整和最佳化。

在資料集管理服務中擁有兩種強型別資料結構的優勢

在資料集管理服務(DM)中擁有兩種強型別資料結構,並讓DM負責將攝入資料格式轉換為訓練資料格式,能夠平行化資料收集開發和訓練程式碼開發。例如,當Jianguo想要為TEXT_INTENT資料集新增一個名為「text language」的新特徵時,他可以與DM服務開發人員合作,更新資料攝入結構以新增一個資料欄位。

此舉不會影響Julia,因為訓練資料結構並未改變。Julia可以在稍後有頻寬處理新特徵時,再來更新訓練資料結構。重點是,Jianguo和Julia無需同步工作即可引入新的資料集增強功能;他們可以獨立作業。

使用CSV檔案的限制

為了簡化與展示目的,我們選擇使用CSV檔案儲存資料。然而,使用純CSV檔案的問題在於缺乏向後相容性支援和資料型別驗證支援。在生產環境中,我們建議使用Parquet、Google Protobuf或Avro來定義資料結構和儲存資料。它們帶有一套用於資料驗證、資料序列化和結構向後相容支援的函式庫。

一個通用資料集:無結構的資料集

儘管我們在多處強調定義強型別資料集結構是資料集管理服務的基礎,但我們將在此處做一個例外,新增一個自由格式的資料集型別——GENERIC資料集。與強型別的TEXT_INTENT資料集不同,GENERIC型別的資料集沒有資料結構驗證。我們的服務將按原樣儲存任何原始輸入資料,並在建立訓練資料時,將所有原始資料以其原始格式封裝到訓練資料集中。

GENERIC 資料集型別的適用場景

GENERIC資料集型別可能看起來不是一個好主意,因為我們基本上是將從上游資料來源接收到的任何資料傳遞給下游訓練應用程式,這很容易破壞訓練程式碼中的資料解析邏輯。這絕對不是生產環境中的選擇,但它為實驗性專案提供了必要的敏捷性。

程式碼範例:使用GENERIC 資料集

import pandas as pd

# 假設我們有一個 GENERIC 資料集
generic_data = pd.read_csv('generic_data.csv')

# 將原始資料封裝到訓練資料集中
training_data = generic_data.to_dict(orient='records')

#### 內容解密:
# 1. 使用 pandas 讀取 GENERIC 資料集的 CSV 檔案。
# 2. 將讀取到的 DataFrame 轉換為字典列表,以便於進一步處理。
# 3. 這種方式保留了原始資料的格式和內容,適用於實驗性專案。

新增新的資料集型別(IMAGE_CLASS)

讓我們設想有一天Julia要求我們(平台開發人員)將她的實驗性影像分類別專案提升為正式專案。Julia和她的團隊正在使用GENERIC資料集開發影像分類別模型,由於效果良好,他們現在希望定義一個強型別的資料集(IMAGE_CLASS)來穩定原始資料收集和訓練資料使用的資料結構,以保護訓練程式碼免受未來資料集更新的影響。

新增IMAGE_CLASS 資料集型別的步驟

  1. 定義訓練資料格式:與Julia討論後,我們決定由FetchTrainingDataset API產生的訓練資料將是一個zip檔案,包含以下三個檔案:

    • examples.csv<image filename>,<label id>
    • labels.csv<label id>,<label name>
    • examples/ 資料夾:包含實際的影像檔案。
  2. 定義攝入資料格式:與負責收集影像和標籤的資料工程師Jianguo討論攝入資料結構。我們同意每個CreateDatasetUpdateDataset請求的有效載荷也是一個zip檔案;其目錄結構如下:zip檔案應該是一個只有子目錄的資料夾。每個根目錄下的子目錄代表一個標籤,其下的影像屬於該標籤。子目錄中只應包含影像,而不應有巢狀目錄。

IMAGE_CLASS 資料集結構

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 資料集管理服務設計與實作

package "資料集管理服務" {
    package "API 設計" {
        component [PrepareTrainingDataset] as prepare
        component [FetchTrainingDataset] as fetch
        component [gRPC 定義] as grpc
    }

    package "版本控制" {
        component [版本雜湊值] as hash
        component [版本快照] as snapshot
        component [資料追溯] as trace
    }

    package "資料儲存" {
        component [資料集儲存] as storage
        component [非同步處理] as async
        component [資料過濾器] as filter
    }
}

prepare --> hash : 產生版本
fetch --> snapshot : 查詢資料
async --> storage : 背景執行

note bottom of hash
  確保資料
  可追溯性
end note

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

此圖示展示了IMAGE_CLASS資料集的zip檔案結構,每個子目錄代表一個類別標籤。

程式碼範例:處理IMAGE_CLASS 資料集

import zipfile

# 處理攝入的zip檔案
with zipfile.ZipFile('image_class_dataset.zip', 'r') as zip_ref:
    zip_ref.extractall('extracted_dataset')

#### 內容解密:
# 1. 使用 zipfile 模組讀取上傳的 zip 檔案。
# 2. 將 zip 檔案內容解壓到指定目錄。
# 3. 這種方式確保了目錄結構按照預期進行組織,便於後續處理。