隨著機器學習系統日益複雜,確保其穩定性和可靠性變得至關重要。本文將探討如何有效地測試機器學習系統,涵蓋從程式碼單元到佈署後全流程的測試策略,並介紹一些實用的測試方法和技巧。特別是針對機器學習系統中常見的長時間訓練流程,本文將介紹如何利用訓練煙霧測試快速驗證程式碼變更的有效性,避免長時間等待後才發現錯誤。此外,本文也將深入探討 API 測試的重要性,以及如何利用整體斷言確保 API 的穩定性和可靠性,同時也將討論佈署後測試的必要性和實施方法,以確保系統在真實環境中的正常運作。
單元測試
單元測試是軟體測試中的一種基本型別,主要用於測試程式碼中的個別單元,例如函式或方法。透過單元測試,開發人員可以確保每個單元的功能正確,並能夠在早期階段發現和修復錯誤。
不良測試的例子
下面的例子展示了一個不良的測試:
def test_convert_keys_to_snake_case():
result = convert_keys_to_snake_case({"Job Description": "Wizard", "Work Address": "Hogwarts Castle", "Current_title": "Headmaster"})
assert result["job_description"] == "Wizard"
assert result["work_address"] is not None
這個測試存在幾個問題:測試名稱過於簡單,沒有明確指出測試的目的;斷言不完整,沒有涵蓋所有需要測試的場景;斷言過於模糊,沒有明確指出預期的結果。
良好的測試的例子
下面的例子展示了一個良好的測試:
def test_convert_keys_to_snake_case_with_spaces_and_punctuation():
input_data = {"Job Description": "Wizard", "Work Address": "Hogwarts Castle", "Current_title": "Headmaster"}
expected_output = {"job_description": "Wizard", "work_address": "Hogwarts Castle", "current_title": "Headmaster"}
result = convert_keys_to_snake_case(input_data)
assert result == expected_output
這個測試具有清晰的結構和明確的目的,斷言完整且明確。
軟體測試的型別
軟體測試可以分為幾種型別,包括:
- 單元測試:用於測試程式碼中的個別單元。
- 訓練煙霧測試:用於測試模型的訓練過程。
- API 測試:用於測試 API 的功能和效能。
- 佈署後測試:用於測試模型在佈署後的效能和功能。
單元測試的重要性
單元測試是確保個別程式模組正確性的關鍵步驟。它們幫助我們明確定義函式的預期行為,並確保這些預期在每次程式碼變更時仍然成立。經過數十年的軟體工程實踐,我們已經明白,單元測試比手動測試更可靠、更具擴充套件性。
在機器學習(ML)系統中,單元測試尤其重要,因為軟體邏輯佔據了大部分的程式碼。例如,特徵工程是一個重要的部分,而這些資料轉換基本上是純函式和邏輯轉換。若不測試這些邏輯,我們就會為錯誤和除錯留下空間。
如何設計單元測試的程式碼
要開始撰寫單元測試,首先需要確保程式碼是可測試的。然而,如果程式碼全部都在一個長的Python指令碼或筆記本中,沒有可呼叫的函式,這將很難測試。幸好,有技巧可以重構程式碼,使其更模組化、合理和可測試。
一種方法是使用函式式核心、指令式外殼的設計模式。函式式核心是一組純函式的集合,例如資料處理、特徵工程和資料轉換。這些純函式是確定性的和等冪的,且更容易測試。指令式外殼是一組小的函式,執行副作用,例如載入資料或儲存檔案到磁碟或遠端儲存桶。透過分離這些部分,我們可以使函式式核心更容易和更快地測試。
函式式核心和指令式外殼設計模式
函式式核心是指一組純函式的集合,例如:
def process_data(data):
# 處理資料
return processed_data
def feature_engineering(data):
# 特徵工程
return engineered_features
指令式外殼是指一組小的函式,執行副作用,例如:
def load_data():
# 載入資料
return data
def save_data(data):
# 儲存資料
pass
透過分離這些部分,我們可以使函式式核心更容易和更快地測試。
單元測試的重要性
單元測試是軟體開發中的一個重要步驟,它可以幫助我們確保每個功能單元的正確性和可靠性。在本節中,我們將探討如何撰寫單元測試,並瞭解其重要性。
撰寫單元測試的步驟
要撰寫單元測試,需要遵循以下步驟:
- 指定輸入: 指定要測試的功能單元的輸入引數。
- 指定預期結果: 指定預期的輸出結果或副作用。
- 執行測試: 執行測試並比較實際結果與預期結果。
單元測試的結構
單元測試通常具有以下結構:
- 可讀的測試名稱: 測試的名稱應該清晰地描述測試的目的。
- AAA 結構: 測試應該遵循 Arrange-Act-Assert 的結構,分別代表設定、執行和斷言。
- 整體斷言: 測試應該包含整體斷言,以確保測試的結果是正確的。
單元測試的例子
以下是兩個單元測試的例子:
範例 1: 測試字典轉換函式
def test_convert_keys_to_snake_case_replaces_title_cased_keys():
result = convert_keys_to_snake_case({"Job Description": "Wizard", "Work_Address": "Hogwarts Castle"})
assert result == {"job_description": "Wizard", "work_address": "Hogwarts Castle"}
範例 2: 測試資料轉換函式
from pandas._testing import assert_frame_equal
def test_normalize_columns_returns_a_dataframe_with_values_between_0_and_1():
loan_applications = pd.DataFrame({"income": [10, 100, 10000]})
result = normalize_columns(loan_applications)
expected = pd.DataFrame({"income": [0, 0.009, 1]})
assert_frame_equal(expected, result)
單元測試的好處
單元測試可以幫助我們:
- 確保功能單元的正確性和可靠性
- 減少 bug 的出現
- 提高程式碼的可維護性和可擴充套件性
- 提高開發速度和效率
機器學習訓練煙霧測試
機器學習(ML)訓練管線是另一個被認為難以測試的元件。訓練煙霧測試(Training Smoke Tests)可以提供快速的反饋,告訴我們程式碼變更是否按預期工作,或者是否引起任何問題。
何謂訓練煙霧測試?
訓練煙霧測試是一種快速的測試,旨在驗證機器學習訓練管線是否正常工作。它使用最小的資料集,通常只有10個樣本,就足以快速檢測出程式碼變更是否引起任何問題。這種測試可以在一分鐘內完成,從而大大減少了反饋迴圈的時間。
實作訓練煙霧測試
實作訓練煙霧測試的方法可能會根據您使用的機器學習框架而有所不同,但一般的方法是相同的。您需要在本地呼叫機器學習訓練管線,使用一個非常小的資料集。以下是訓練煙霧測試的範例:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
def test_training_smoke_test():
# 載入資料
data = pd.read_csv("/code/data/train.csv", encoding="utf-8", low_memory=False)
# 將資料分組並取出10個樣本
test_data = data.groupby('DEFAULT').apply(lambda df: df.head(10)).reset_index(drop=True)
# 訓練模型
pipeline = train_model(test_data)
# 預測結果
predictions = pipeline.predict(test_data)
# 驗證預測結果
valid_predictions = {0, 1}
assert valid_predictions.issubset(predictions)
# 執行訓練煙霧測試
test_training_smoke_test()
執行訓練煙霧測試
您可以在終端機中執行訓練煙霧測試。這個測試將快速地告訴您程式碼變更是否正常工作,或者是否引起任何問題。如果測試透過,您就可以更有信心地進行全面的機器學習訓練。
圖表翻譯:
graph LR A[載入資料] --> B[分組並取出10個樣本] B --> C[訓練模型] C --> D[預測結果] D --> E[驗證預測結果] E --> F[透過/失敗]
這個圖表展示了訓練煙霧測試的流程,從載入資料到驗證預測結果。
機器學習模型訓練流程最佳化
在機器學習模型的訓練過程中,效率和可靠性至關重要。過去,我們曾經有個機器學習模型訓練流程需要花費三個小時。這個流程是在雲端(使用Metaflow)執行的,機器學習從業人員有時候需要等待兩個小時,才發現最終步驟出現了錯誤。為瞭解決這個問題,我們設計了一個模型訓練的冒煙測試(smoke test)。
訓練冒煙測試
這個訓練冒煙測試也可以作為預提交鉤子(pre-commit hook)執行,這意味著如果我們即將提交一個錯誤的程式碼,該測試可以在兩分鐘內就發現錯誤,而不是等待三個小時。無論您使用什麼機器學習管道工具或平臺,都應該探索如何使用它來建立一個訓練冒煙測試。如果沒有這樣的功能,且您經常花費大量時間等待,那麼該平臺或工具可能正在阻礙您的進度,而不是提供幫助。
API 測試
現在,您可以快速獲得對典型慢速和長時間執行的機器學習系統元件的反饋。讓我們轉換一下思路,來看看如何測試為了佈署模型而撰寫的軟體。
如果您將訓練好的模型封裝並作為網路API佈署,您可以像測試其他網路API一樣測試和啟動它。這裡的測試物件是一個網路API應用程式。作為機器學習模型的生產者,您很可能會有下游元件依賴於您,例如前端應用程式或其他API。這些下游消費者最終會依賴於您的API的行為(例如,請求和回應結構)。如果您的行為發生變化,並以破壞下游消費者的方式違反了契約或承諾,您將會導致下游消費者出現問題,從而給所有人帶來很多頭痛。
寫API測試
API測試在這裡非常有用。它們可以作為輕量級的契約測試,驗證您是否仍然履行著對外部世界的承諾(即契約)。如果您的程式碼變化會破壞下游系統,您寧願讓測試在本地告訴您,而不是在變化被佈署之前或缺陷被提交之前。API測試失敗也會提示您思考API版本控制和管理API結構變化。測試失敗總比晚上收到其他團隊的意外訊息要好。
您可以按照以下三個步驟寫這些測試:
- 考慮您對給定請求的預期行為。
- 找到有關如何為您正在使用的API函式庫編寫API測試的檔案(例如,如果您使用FastAPI,則使用FastAPI TestClient)。
- 編寫並執行測試!
以下是模型API測試的示例:
from fastapi.testclient import TestClient
from precisely import assert_that, is_mapping, greater_than_or_equal_to, less_than_or_equal_to
from api.app import app
client = TestClient(app)
內容解密:
在上述程式碼中,我們使用FastAPI的TestClient來建立一個測試客戶端。這個客戶端允許我們對API進行請求並驗證回應。assert_that
函式用於斷言回應符合預期。這個測試確保了API的行為是正確的,並且符合下游消費者的需求。
圖表翻譯:
graph LR A[API請求] --> B[API處理] B --> C[API回應] C --> D[斷言回應] D --> E[驗證結果]
在這個圖表中,我們展示了API測試的流程。首先,我們傳送API請求,然後API進行處理,生成回應。接下來,我們使用斷言函式驗證回應是否符合預期。最後,驗證結果會告訴我們測試是否透過或失敗。
測試驅動開發:整體斷言的重要性
在開發API的過程中,測試是一個非常重要的步驟。好的測試可以幫助我們確保API的正確性和可靠性。在這篇文章中,我們將討論測試驅動開發中的一個重要概念:整體斷言。
測試驅動開發的流程
測試驅動開發的流程通常包括以下三個步驟:
- Arrange:準備測試資料和環境。
- Act:執行被測試的程式碼。
- Assert:驗證結果是否符合預期。
整體斷言的優點
整體斷言是指在測試中斷言整個結果物件,而不是斷言結果物件的個別部分。這種方法有兩個主要優點:
- 可讀性:整體斷言可以使測試程式碼更容易閱讀和理解。透過斷言整個結果物件,我們可以清晰地看到API的回應結構和內容。
- 全面性:整體斷言可以幫助我們確保測試的全面性。透過斷言整個結果物件,我們可以確保API的回應符合預期的結構和內容。
不良的測試實踐:部分斷言
部分斷言是指在測試中斷言結果物件的個別部分,而不是斷言整個結果物件。這種方法有以下缺點:
- 可讀性差:部分斷言可以使測試程式碼更難以閱讀和理解。
- 全面性差:部分斷言可能導致測試的全面性不足。透過斷言結果物件的個別部分,我們可能會忽略其他重要的部分。
範例:整體斷言 vs. 部分斷言
以下是兩個測試範例,展示了整體斷言和部分斷言的差異:
# 部分斷言
response = client.post("/predict/", json=valid_request_payload)
assert response.json()["prediction"] == 3
assert response.json()["message"] == "OK"
# 整體斷言
response = client.post("/predict/", json=valid_request_payload)
assert_that(response.json(), is_mapping({"prediction": 3, "message": "OK"}))
在整體斷言的範例中,我們斷言了整個結果物件,包括prediction
和message
兩個部分。這種方法可以使測試程式碼更容易閱讀和理解,並且可以確保測試的全面性。
內容解密:
在這篇文章中,我們討論了測試驅動開發中的一個重要概念:整體斷言。整體斷言是指在測試中斷言整個結果物件,而不是斷言結果物件的個別部分。這種方法可以使測試程式碼更容易閱讀和理解,並且可以確保測試的全面性。透過使用整體斷言,我們可以使測試程式碼更強大和可靠。
圖表翻譯:
graph LR A[測試驅動開發] --> B[整體斷言] B --> C[可讀性] B --> D[全面性] C --> E[測試程式碼更容易閱讀] D --> F[測試的全面性] F --> G[確保測試的正確性]
在這個圖表中,我們展示了測試驅動開發、整體斷言、可讀性和全面性的關係。透過使用整體斷言,我們可以使測試程式碼更容易閱讀和理解,並且可以確保測試的全面性。
全面API測試策略
在軟體開發中,測試是確保系統穩定性和可靠性的關鍵步驟。對於API來說,測試尤其重要,因為它們通常作為不同系統之間的橋樑,承擔著資料交換和業務邏輯的重任。這篇文章將探討如何對API進行全面測試,包括區域性測試和佈署後測試。
區域性測試
區域性測試是指在API佈署之前,在本地環境中對其進行測試。這種測試方式可以幫助我們發現API程式碼中的缺陷和錯誤。然而,傳統的斷言方式可能無法滿足我們的需求,尤其是在處理非確定性值的情況下。
寫入整體斷言
為瞭解決這個問題,我們可以使用Python函式庫,如precisely
,來寫入整體斷言。這種斷言方式允許我們對API的回應模式進行測試,而不是特定的值。以下是一個例子:
from precisely import assert_that, is_mapping, any_of, equal_to, is_instance
actual_response = {
"prediction": 1,
"status": "OK",
"user_name": "Harry"
}
assert_that(actual_response, is_mapping({
"prediction": any_of(equal_to(0), equal_to(1), equal_to(2)),
"status": "OK",
"user_name": is_instance(str)
}))
在這個例子中,我們使用is_mapping
來指定我們預期的回應是一個Python字典。然後,我們使用any_of
來指定prediction
欄位可以是0、1或2中的任意一個值。同時,我們使用is_instance
來指定user_name
欄位必須是一個字串。
佈署後測試
佈署後測試是指在API佈署到真實環境(如預生產或生產環境)後對其進行測試。這種測試方式可以幫助我們確保API可以成功處理請求,並根據我們的期望傳回正確的回應。
測試API的依賴關係
如果API有任何依賴關係(如資料函式庫、遠端儲存桶或外部服務),這些測試也可以幫助我們確保API可以正確地與這些依賴關係進行互動。
測試策略
在進行佈署後測試時,我們需要考慮以下幾個方面:
- 測試覆寫率:確保我們的測試涵蓋了API的所有功能和場景。
- 測試資料:準備足夠的測試資料,以模擬真實的使用場景。
- 測試環境:確保測試環境與生產環境一致,以避免環境差異導致的測試失敗。
實施測試
在實施測試時,我們可以使用如requests
和unittest
等Python函式庫來傳送HTTP請求和驗證回應。以下是一個簡單的例子:
import requests
import unittest
class TestAPI(unittest.TestCase):
def test_get_user(self):
response = requests.get('https://example.com/api/user')
self.assertEqual(response.status_code, 200)
self.assertIn('user_name', response.json())
if __name__ == '__main__':
unittest.main()
在這個例子中,我們使用requests
來傳送GET請求到API的/user
端點。然後,我們使用unittest
來驗證回應的狀態碼和內容。
後佈署測試:確保系統在真實環境中的正確性
後佈署測試是一種重要的測試型別,旨在驗證系統在真實環境中的正確性,尤其是在佈署到預生產環境或生產環境後。這類測試關注於確認系統各個元件(如 API、機器學習模型、資料函式庫)之間的介面是否正常運作。
為什麼後佈署測試如此重要?
後佈署測試可以幫助我們在佈署變更到預生產環境或生產環境後,立即發現潛在的錯誤或問題。這樣可以節省手動測試的時間,減少生產環境中的缺陷,從而減少了維護和解決生產環境問題的時間和壓力。
如何撰寫後佈署測試?
撰寫後佈署測試時,應避免重複 API 測試中的邏輯,以免維護和更新兩套測試。API 測試已經涵蓋了所有程式碼路徑和邊緣案例,如果 API 有其他依賴(如資料函式庫),API 測試可以包含模擬和存根邏輯來模擬錯誤,但是在後佈署測試中,這些邏輯無法實作。
在後佈署測試中,簡單地向被測試的物件(即已佈署到環境中的 API)傳送請求,並驗證是否收到預期的回應。
範例:使用 Python 和 Requests 實作後佈署測試
import requests
class TestPostDeployment:
def test_root(self):
response = requests.get(self.endpoint_url)
assert response.status_code == 200
def test_predict_should_return_a_prediction_when_given_a_valid_payload(self):
valid_request_payload = {
"Changed_Credit_Limit": 0,
"Annual_Income": 0,
"Monthly_Inhand_Salary": 0,
"Age": 0,
}
response = requests.post(
f"{self.endpoint_url}/predict/",
headers={"Content-Type": "application/json"},
json=valid_request_payload,
)
# 驗證回應
assert response.status_code == 200
# 進一步驗證回應內容
自動化測試的重要性
在機器學習(ML)系統的開發中,自動化測試是一個至關重要的環節。它能夠幫助開發團隊快速、安全、可靠地迭代專案。透過自動化測試,開發人員可以在早期階段捕捉到錯誤和缺陷,從而避免了手動測試的時間和精力。
隨著機器學習技術的普及和應用場景的擴充套件,對高品質、可靠的機器學習系統的需求日益增長。本文深入探討了機器學習系統測試的各個方面,涵蓋了單元測試、訓練煙霧測試、API 測試以及佈署後測試,並闡述瞭如何設計和編寫有效的測試程式碼,以及測試驅動開發和整體斷言的重要性。多維比較分析顯示,相比傳統的手動測試,自動化測試在效率、可靠性和可維護性方面都具有顯著優勢。技術限制深析指出,儘管自動化測試能夠有效提升軟體品質,但仍需關注測試覆寫率和測試使用案例設計的合理性,才能最大限度地發揮其作用。玄貓認為,全面且自動化的測試策略是構建穩定可靠機器學習系統的基本,對於提升開發效率和確保產品品質至關重要。隨著DevOps 和 MLOps 理念的普及,自動化測試將在機器學習系統的生命週期中扮演越來越重要的角色,值得技術團隊持續投入和關注。