測試機器學習系統(tǒng):代碼、數(shù)據(jù)和模型

## Intuition 在本課中师抄,將學習如何測試代碼漓柑、數(shù)據(jù)和模型,以構建可以可靠迭代的機器學習系統(tǒng)叨吮。測試是確保某些東西按預期工作的一種方式欺缘。被激勵在開發(fā)周期中盡早實施測試并發(fā)現(xiàn)錯誤來源,以便可以降低[下游成本](https://assets.deepsource.io/39ed384/images/blog/cost-of-fixing-bugs/chart.jpg)和浪費時間挤安。一旦設計了測試,可以在每次更改或添加到代碼庫時自動執(zhí)行它們丧鸯。 > tip > > 強烈建議您在完成之前的課程_后_探索本課程蛤铜,因為主題(和代碼)是迭代開發(fā)的。但是丛肢,確實創(chuàng)建了 [testing-ml](https://github.com/GokuMohandas/testing-ml)存儲庫围肥,可通過交互式note本快速概覽。 ### 測試類型 在開發(fā)周期的不同階段使用了四種主要類型的測試: 1. `Unit tests`:對每個具有[單一職責](https://en.wikipedia.org/wiki/Single-responsibility_principle)的單個組件進行測試(例如過濾列表的功能)蜂怎。 2. `Integration tests`:測試單個組件的組合功能(例如數(shù)據(jù)處理)穆刻。 3. `System tests`:對給定輸入(例如訓練伦乔、推理等)的預期輸出的系統(tǒng)設計進行測試醇坝。 4. `Acceptance tests`:用于驗證是否滿足要求的測試,通常稱為用戶驗收測試 (UAT)或粮。 5. `Regression tests`:基于之前看到的錯誤的測試幽歼,以確保新的更改不會重新引入它們朵锣。 雖然 ML 系統(tǒng)本質上是概率性的,但它們由許多確定性組件組成甸私,可以以與傳統(tǒng)軟件系統(tǒng)類似的方式進行測試诚些。當從測試代碼轉向測試[數(shù)據(jù)](https://franztao.github.io/2022/10/01/Testing//./#data)和[模型](https://franztao.github.io/2022/10/01/Testing//./#models)時,測試 ML 系統(tǒng)之間的區(qū)別就開始了皇型。 ![測試類型](https://upload-images.jianshu.io/upload_images/27840083-744c45174eee9b23.png) > 還有許多其他類型的功能和非功能測試诬烹,例如冒煙測試(快速健康檢查)、性能測試(負載弃鸦、壓力)绞吁、安全測試等,但可以在上面的系統(tǒng)測試中概括所有這些. ### 應該如何測試寡键? 編寫測試時使用的框架是[Arrange Act Assert](http://wiki.c2.com/?ArrangeActAssert)方法掀泳。 - `Arrange`:設置不同的輸入進行測試雪隧。 - `Act`:將輸入應用到要測試的組件上。 - `Assert`:確認收到了預期的輸出员舵。 > `Cleaning`是此方法的非官方第四步脑沿,因為重要的是不要留下可能影響后續(xù)測試的先前測試的殘留物÷砥В可以使用[pytest-randomly](https://github.com/pytest-dev/pytest-randomly)等包通過隨機執(zhí)行測試來測試狀態(tài)依賴性庄拇。 在 Python 中,有許多工具韭邓,例如[unittest](https://docs.python.org/3/library/unittest.html)措近、[pytest](https://docs.pytest.org/en/stable/)等,可以讓在遵守_Arrange Act Assert_框架的同時輕松實現(xiàn)測試女淑。這些工具具有強大的內(nèi)置功能瞭郑,例如參數(shù)化、過濾器等鸭你,可以大規(guī)模測試許多條件屈张。 ### 應該測試什么? 在_安排_輸入和_斷言_預期輸出時袱巨,應該測試輸入和輸出的哪些方面阁谆? - **輸入**:數(shù)據(jù)類型、格式愉老、長度场绿、邊緣情況(最小/最大、小/大等) - **輸出**:數(shù)據(jù)類型嫉入、格式焰盗、異常、中間和最終輸出 > [?? 將在下面介紹與數(shù)據(jù)](https://franztao.github.io/2022/10/01/Testing//./#data)和[模型](https://franztao.github.io/2022/10/01/Testing//./#models)有關的測試內(nèi)容的具體細節(jié)劝贸。 ## 最佳實踐 不管使用什么框架姨谷,將測試與開發(fā)過程緊密結合是很重要的。 - `atomic`:在創(chuàng)建函數(shù)和類時映九,需要確保它們具有[單一的職責](https://en.wikipedia.org/wiki/Single-responsibility_principle)梦湘,以便可以輕松地測試它們。如果沒有件甥,需要將它們拆分成更細粒度的組件捌议。 - `compose`:當創(chuàng)建新組件時,希望編寫測試來驗證它們的功能引有。這是確卑曷可靠性和及早發(fā)現(xiàn)錯誤的好方法。 - `reuse`:應該維護中央存儲庫譬正,其中核心功能在源頭進行測試并在許多項目中重用宫补。這顯著減少了每個新項目代碼庫的測試工作量檬姥。 - `regression`:想解釋回歸測試中遇到的新錯誤,這樣就可以確保將來不會重新引入相同的錯誤粉怕。 - `coverage`:希望確保代碼庫[100% 覆蓋](https://franztao.github.io/2022/10/01/Testing//#coverage)健民。這并不意味著要為每一行代碼編寫測試,而是要考慮每一行代碼贫贝。 - `automate`:如果忘記在提交到存儲庫之前運行測試秉犹,希望在對代碼庫進行更改時自動運行測試。將在后續(xù)課程中學習如何使用[預提交hook在本地執(zhí)行此操作稚晚,并通過](https://franztao.github.io/2022/10/01/Testing//../pre-commit/)[GitHub 操作](https://franztao.github.io/2022/10/01/Testing//../cicd/#github-actions)遠程執(zhí)行此操作崇堵。 ## 測試驅動開發(fā) [測試驅動開發(fā) (TDD)](https://en.wikipedia.org/wiki/Test-driven_development)是在編寫功能之前編寫測試以確保始終編寫測試的過程。這與先編寫功能然后再編寫測試形成對比客燕。以下是對此的查看: - 隨著進步編寫測試很好鸳劳,但這確實意味著 100% 的正確性。 - 在進入代碼或測試之前也搓,最初的時間應該花在設計上棍辕。 如果這些測試沒有意義并且不包含可能的輸入、中間體和輸出的領域还绘,那么完美的覆蓋并不意味著應用程序沒有錯誤。因此栖袋,應該在面臨錯誤時朝著更好的設計和敏捷性努力拍顷,快速解決它們并圍繞它們編寫測試用例以避免下一次。 ## 應用 在[應用程序](https://github.com/GokuMohandas/mlops-course)中塘幅,將測試代碼昔案、數(shù)據(jù)和模型。將首先創(chuàng)建一個`tests`帶有`code`子目錄的單獨目錄來測試`tagifai`腳本电媳。將在下面創(chuàng)建用于測試[數(shù)據(jù)](https://franztao.github.io/2022/10/01/Testing//#??nbsp-data)和[模型](https://franztao.github.io/2022/10/01/Testing//#??nbsp-models)的子目錄踏揣。 ``` mkdir tests cd tests mkdir app config model tagifai touch cd ../ ``` ``` tests/ └── code/ │ ├── test_data.py │ ├── test_evaluate.py │ ├── test_main.py │ ├── test_predict.py │ └── test_utils.py ``` 在學習了本課中的所有概念_后_,請隨意編寫測試并將它們組織在這些腳本中匾乓。建議使用[`tests`](https://github.com/GokuMohandas/mlops-course/tree/main/tests)在 GitHub 上的目錄作為參考捞稿。 > 請注意,`tagifai/train.py`腳本沒有相應的`tests/code/test_train.py`. 一些腳本具有帶有依賴項(例如工件)的大型函數(shù)(例如`train.train()`拼缝、`train.optimize()`娱局、等),通過.`predict.predict()``tests/code/test_main.py` ## ?? Pytest 將使用[pytest](https://docs.pytest.org/en/stable/)作為測試框架咧七,因為它具有強大的內(nèi)置功能衰齐,例如[參數(shù)化](https://franztao.github.io/2022/10/01/Testing//#parametrize)、[固定裝置](https://franztao.github.io/2022/10/01/Testing//#fixtures)继阻、[標記](https://franztao.github.io/2022/10/01/Testing//#markers)等耻涛。 ``` pip install pytest==7.1.2 ``` 由于這個測試包不是核心機器學習操作的組成部分废酷,讓在中創(chuàng)建一個單獨的列表`setup.py`并將其添加到`extras_require`: ``` # setup.py test_packages = [ "pytest==7.1.2", ] # Define our package setup( ... extras_require={ "dev": docs_packages + style_packages + test_packages, "docs": docs_packages, "test": test_packages, }, ) ``` 創(chuàng)建了一個明確的`test`選項,因為用戶只想下載測試包抹缕。[當使用CI/CD 工作流](https://franztao.github.io/2022/10/01/Testing//../cicd/)通過 GitHub Actions 運行測試時澈蟆,將看到這一點。 ### 配置 Pytest 期望測試在`tests`默認情況下組織在一個目錄下歉嗓。但是丰介,也可以添加到現(xiàn)有`pyproject.toml`文件中以配置任何其他測試目錄。進入目錄后鉴分,pytest 會查找以 開頭的 python 腳本哮幢,`tests_*.py`但也可以將其配置為讀取任何其他文件模式。 ``` # Pytest [tool.pytest.ini_options] testpaths = ["tests"] python_files = "test_*.py" ``` ### 斷言 讓看看樣本測試及其結果是什么樣的志珍。假設有一個簡單的函數(shù)來確定水果是否脆: ``` # food/fruits.py def is_crisp(fruit): if fruit: fruit = fruit.lower() if fruit in ["apple", "watermelon", "cherries"]: return True elif fruit in ["orange", "mango", "strawberry"]: return False else: raise ValueError(f"{fruit} not in known list of fruits.") return False ``` 為了測試這個功能橙垢,可以使用[斷言語句](https://docs.pytest.org/en/stable/assert.html)來映射輸入和預期的輸出。單詞后面的語句`assert`必須返回 True伦糯。 ``` # tests/food/test_fruits.py def test_is_crisp(): assert is_crisp(fruit="apple") assert is_crisp(fruit="Apple") assert not is_crisp(fruit="orange") with pytest.raises(ValueError): is_crisp(fruit=None) is_crisp(fruit="pear") ``` > 還可以對[異常](https://docs.pytest.org/en/stable/assert.html#assertions-about-expected-exceptions)進行斷言柜某,就像在第 6-8 行中所做的那樣,其中 with 語句下的所有操作都應該引發(fā)指定的異常敛纲。 > `assert`在項目中使用的例子 > > ``` > # tests/code/test_evaluate.py > def test_get_metrics(): > y_true = np.array([0, 0, 1, 1]) > y_pred = np.array([0, 1, 0, 1]) > classes = ["a", "b"] > performance = evaluate.get_metrics(y_true=y_true, y_pred=y_pred, classes=classes, df=None) > assert performance["overall"]["precision"] == 2/4 > assert performance["overall"]["recall"] == 2/4 > assert performance["class"]["a"]["precision"] == 1/2 > assert performance["class"]["a"]["recall"] == 1/2 > assert performance["class"]["b"]["precision"] == 1/2 > assert performance["class"]["b"]["recall"] == 1/2 > ``` ### 執(zhí)行 可以使用幾個不同的粒度級別執(zhí)行上面的測試: ``` python3 -m pytest # all tests python3 -m pytest tests/food # tests under a directory python3 -m pytest tests/food/test_fruits.py # tests for a single file python3 -m pytest tests/food/test_fruits.py::test_is_crisp # tests for a single function ``` 在上面運行特定測試將產(chǎn)生以下輸出: ``` python3 -m pytest tests/food/test_fruits.py::test_is_crisp ``` ``` tests/food/test_fruits.py::test_is_crisp . [100%] ``` 如果在此測試中的任何斷言失敗喂击,將看到失敗的斷言,以及函數(shù)的預期和實際輸出淤翔。 ``` tests/food/test_fruits.py F [100%] def test_is_crisp(): > assert is_crisp(fruit="orange") E AssertionError: assert False E + where False = is_crisp(fruit='orange') ``` > tip > > 重要的是要測試[上面](https://franztao.github.io/2022/10/01/Testing//#how-should-we-test)概述的各種輸入和預期輸出翰绊,并且**永遠不要假設測試是微不足道的**。在上面的例子中旁壮,如果函數(shù)沒有考慮大小寫监嗜,測試“apple”和“Apple”是很重要的! ### Classes 還可以通過創(chuàng)建測試類來測試類及其各自的功能抡谐。在測試類中裁奇,可以選擇定義在設置或拆除類實例或使用類方法時自動執(zhí)行的[函數(shù)。](https://docs.pytest.org/en/stable/xunit_setup.html) - `setup_class`:為任何類實例設置狀態(tài)麦撵。 - `teardown_class`: 拆除 setup\_class 中創(chuàng)建的狀態(tài)刽肠。 - `setup_method`:在每個方法之前調(diào)用以設置任何狀態(tài)。 - `teardown_method`:在每個方法之后調(diào)用以拆除任何狀態(tài)免胃。 ``` class Fruit(object): def __init__(self, name): self.name = name class TestFruit(object): @classmethod def setup_class(cls): """Set up the state for any class instance.""" pass @classmethod def teardown_class(cls): """Teardown the state created in setup_class.""" pass def setup_method(self): """Called before every method to setup any state.""" self.fruit = Fruit(name="apple") def teardown_method(self): """Called after every method to teardown any state.""" del self.fruit def test_init(self): assert self.fruit.name == "apple" ``` 可以通過指定類名來為類執(zhí)行所有測試: ``` python3 -m pytest tests/food/test_fruits.py::TestFruit ``` ``` tests/food/test_fruits.py::TestFruit . [100%] ``` > `class`在項目中測試 的示例 > > ``` > # tests/code/test_data.py > class TestLabelEncoder: > @classmethod > def setup_class(cls): > """Called before every class initialization.""" > pass > > @classmethod > def teardown_class(cls): > """Called after every class initialization.""" > pass > > def setup_method(self): > """Called before every method.""" > self.label_encoder = data.LabelEncoder() > > def teardown_method(self): > """Called after every method.""" > del self.label_encoder > > def test_empty_init(self): > label_encoder = data.LabelEncoder() > assert label_encoder.index_to_class == {} > assert len(label_encoder.classes) == 0 > > def test_dict_init(self): > class_to_index = {"apple": 0, "banana": 1} > label_encoder = data.LabelEncoder(class_to_index=class_to_index) > assert label_encoder.index_to_class == {0: "apple", 1: "banana"} > assert len(label_encoder.classes) == 2 > > def test_len(self): > assert len(self.label_encoder) == 0 > > def test_save_and_load(self): > with tempfile.TemporaryDirectory() as dp: > fp = Path(dp, "label_encoder.json") > self.label_encoder.save(fp=fp) > label_encoder = data.LabelEncoder.load(fp=fp) > assert len(label_encoder.classes) == 0 > > def test_str(self): > assert str(data.LabelEncoder()) == "" > > def test_fit(self): > label_encoder = data.LabelEncoder() > label_encoder.fit(["apple", "apple", "banana"]) > assert "apple" in label_encoder.class_to_index > assert "banana" in label_encoder.class_to_index > assert len(label_encoder.classes) == 2 > > def test_encode_decode(self): > class_to_index = {"apple": 0, "banana": 1} > y_encoded = [0, 0, 1] > y_decoded = ["apple", "apple", "banana"] > label_encoder = data.LabelEncoder(class_to_index=class_to_index) > label_encoder.fit(["apple", "apple", "banana"]) > assert np.array_equal(label_encoder.encode(y_decoded), np.array(y_encoded)) > assert label_encoder.decode(y_encoded) == y_decoded > ``` ### 參數(shù)化 到目前為止五垮,在測試中,必須創(chuàng)建單獨的斷言語句來驗證輸入和預期輸出的不同組合杜秸。然而放仗,這里有一點冗余,因為輸入總是作為參數(shù)輸入到函數(shù)中撬碟,并且輸出與預期輸出進行比較诞挨。為了消除這種冗余莉撇,pytest 有一個[`@pytest.mark.parametrize`](https://docs.pytest.org/en/stable/parametrize.html)裝飾器,它允許將輸入和輸出表示為參數(shù)惶傻。 ``` @pytest.mark.parametrize( "fruit, crisp", [ ("apple", True), ("Apple", True), ("orange", False), ], ) def test_is_crisp_parametrize(fruit, crisp): assert is_crisp(fruit=fruit) == crisp ``` ``` python3 -m pytest tests/food/test_is_crisp_parametrize.py ... [100%] ``` 1. `[Line 2]`:定義裝飾器下的參數(shù)名稱棍郎,例如∫遥“fruit, crisp”(注意這是一個字符串)涂佃。 2. `[Lines 3-7]`:提供步驟 1 中參數(shù)的值組合列表。 3. `[Line 9]`:將參數(shù)名稱傳遞給測試函數(shù)蜈敢。 4. `[Line 10]`:包括必要的斷言語句辜荠,這些語句將為步驟 2 中列表中的每個組合執(zhí)行。 同樣抓狭,也可以傳入一個異常作為預期結果: ``` @pytest.mark.parametrize( "fruit, exception", [ ("pear", ValueError), ], ) def test_is_crisp_exceptions(fruit, exception): with pytest.raises(exception): is_crisp(fruit=fruit) ``` > `parametrize`項目中的示例 > > ``` > # tests/code/test_data.py > from tagifai import data > @pytest.mark.parametrize( > "text, lower, stem, stopwords, cleaned_text", > [ > ("Hello worlds", False, False, [], "Hello worlds"), > ("Hello worlds", True, False, [], "hello worlds"), > ... > ], > ) > def test_preprocess(text, lower, stem, stopwords, cleaned_text): > assert ( > data.clean_text( > text=text, > lower=lower, > stem=stem, > stopwords=stopwords, > ) > == cleaned_text > ) > ``` ### Fixtures 參數(shù)化允許減少測試函數(shù)內(nèi)部的冗余伯病,但是如何減少不同測試函數(shù)之間的冗余呢?例如否过,假設不同的函數(shù)都有一個數(shù)據(jù)框作為輸入午笛。在這里,可以使用pytest的內(nèi)置[fixture](https://docs.pytest.org/en/stable/fixture.html)苗桂,它是一個在test函數(shù)之前執(zhí)行的函數(shù)药磺。 ``` @pytest.fixture def my_fruit(): fruit = Fruit(name="apple") return fruit def test_fruit(my_fruit): assert my_fruit.name == "apple" ``` > 請注意,fixture 的名稱和 test 函數(shù)的輸入是相同的 ( `my_fruit`)煤伟。 也可以將fixture 應用到類中与涡,當調(diào)用類中的任何方法時都會調(diào)用fixture 函數(shù)。 ``` @pytest.mark.usefixtures("my_fruit") class TestFruit: ... ``` > `fixtures`項目中的示例 > > 在transformers項目中持偏,使用固定裝置有效地將一組輸入(例如 Pandas DataFrame)傳遞給需要它們的不同測試功能(清理、拆分等)氨肌。 > > ``` > # tests/code/test_data.py > @pytest.fixture(scope="module") > def df(): > data = [ > {"title": "a0", "description": "b0", "tag": "c0"}, > {"title": "a1", "description": "b1", "tag": "c1"}, > {"title": "a2", "description": "b2", "tag": "c1"}, > {"title": "a3", "description": "b3", "tag": "c2"}, > {"title": "a4", "description": "b4", "tag": "c2"}, > {"title": "a5", "description": "b5", "tag": "c2"}, > ] > df = pd.DataFrame(data * 10) > return df > > > @pytest.mark.parametrize( > "labels, unique_labels", > [ > ([], ["other"]), # no set of approved labels > (["c3"], ["other"]), # no overlap b/w approved/actual labels > (["c0"], ["c0", "other"]), # partial overlap > (["c0", "c1", "c2"], ["c0", "c1", "c2"]), # complete overlap > ], > ) > def test_replace_oos_labels(df, labels, unique_labels): > replaced_df = data.replace_oos_labels( > df=df.copy(), labels=labels, label_col="tag", oos_label="other" > ) > assert set(replaced_df.tag.unique()) == set(unique_labels) > ``` > 請注意鸿秆,不在參數(shù)化測試函數(shù)`df`中直接使用fixture(傳入)。`df.copy()`如果這樣做了怎囚,那么將`df`在每次參數(shù)化后更改 的值卿叽。 > > > Tip > > > > 在圍繞數(shù)據(jù)集創(chuàng)建固定裝置時,最佳做法是創(chuàng)建一個仍然遵循相同模式的簡化版本恳守。例如考婴,在上面的數(shù)據(jù)框固定裝置中,正在創(chuàng)建一個較小的數(shù)據(jù)框催烘,它仍然具有與實際數(shù)據(jù)框相同的列名沥阱。雖然可以加載transformers實際數(shù)據(jù)集,但隨著transformers數(shù)據(jù)集隨時間變化(新標簽伊群、刪除標簽考杉、非常大的數(shù)據(jù)集等)策精,它可能會導致問題 Fixtures 可以有不同的范圍,這取決于如何使用它們崇棠。例如咽袜,`df`fixture具有模塊范圍,因為不想在每次測試后都重新創(chuàng)建它枕稀,而是希望為模塊中的所有測試只創(chuàng)建一次(`tests/test_data.py`)询刹。 - `function`: 每次測試后,fixture 都會被銷毀萎坷。`[default]` - `class`:fixture在類中的最后一次測試后被銷毀凹联。 - `module`:fixture在模塊(腳本)中的最后一次測試后被銷毀。 - `package`:fixture在包中的最后一次測試后被銷毀食铐。 - `session`:fixture在會話的最后一次測試后被銷毀匕垫。 功能是最低級別的范圍,而[會話](https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session)是最高級別虐呻。首先執(zhí)行最高級別的范圍固定裝置象泵。 > 通常,當在一個特定的測試文件中有許多fixture時斟叼,可以將它們?nèi)拷M織在一個`fixtures.py`腳本中并根據(jù)需要調(diào)用它們偶惠。 ### 標記 已經(jīng)能夠以各種粒度級別(所有測試、腳本朗涩、函數(shù)等)執(zhí)行測試忽孽,但可以使用[標記](https://docs.pytest.org/en/stable/mark.html)創(chuàng)建自定義粒度。已經(jīng)使用了一種類型的標記(參數(shù)化)谢床,但還有其他幾種[內(nèi)置標記](https://docs.pytest.org/en/stable/mark.html#mark)兄一。例如,[`skipif`](https://docs.pytest.org/en/stable/skipping.html#id1)如果滿足條件识腿,標記允許跳過測試的執(zhí)行出革。例如,假設只想在 GPU 可用時測試訓練模型: ``` @pytest.mark.skipif( not torch.cuda.is_available(), reason="Full training tests require a GPU." ) def test_training(): pass ``` [除了一些保留](https://docs.pytest.org/en/stable/reference.html#marks)的標記名稱外渡讼,還可以創(chuàng)建自己的自定義標記骂束。 ``` @pytest.mark.fruits def test_fruit(my_fruit): assert my_fruit.name == "apple" ``` `-m`可以使用需要(區(qū)分大小寫)標記表達式的標志來執(zhí)行它們,如下所示: ``` pytest -m "fruits" # runs all tests marked with `fruits` pytest -m "not fruits" # runs all tests besides those marked with `fruits` ``` > tip > > 使用標記的正確方法是明確列出在[pyproject.toml](https://github.com/GokuMohandas/mlops-course/blob/main/pyproject.toml)文件中創(chuàng)建的標記成箫。在這里展箱,可以指定必須在此文件中使用`--strict-markers`標志定義所有標記,然后在`markers`列表中聲明標記(以及有關它們的一些信息): > > ``` > @pytest.mark.training > def test_train_model(): > assert ... > ``` > ``` > # Pytest > [tool.pytest.ini_options] > testpaths = ["tests"] > python_files = "test_*.py" > addopts = "--strict-markers --disable-pytest-warnings" > markers = [ > "training: tests that involve training", > ] > ``` > 完成此操作后蹬昌,可以通過執(zhí)行查看所有現(xiàn)有的標記列表混驰,`pytest --markers`當嘗試使用此處未定義的新標記時會收到錯誤消息。 ### 覆蓋范圍 當為應用程序的組件開發(fā)測試時,重要的是要知道對代碼庫的覆蓋程度以及知道是否遺漏了任何東西账胧【郝可以使用[Coverage](https://coverage.readthedocs.io/)庫來跟蹤和可視化測試占代碼庫的多少。使用 pytest治泥,由于[pytest-cov](https://pytest-cov.readthedocs.io/)插件筹煮,使用這個包變得更加容易。 ``` pip install pytest-cov==2.10.1 ``` 將把它添加到`setup.py`腳本中: ``` # setup.py test_packages = [ "pytest==7.1.2", "pytest-cov==2.10.1" ] ``` ``` python3 -m pytest --cov tagifai --cov-report html ``` ![pytest](https://upload-images.jianshu.io/upload_images/27840083-28b8d8e511a73d8d.png) 在這里居夹,要求覆蓋 tagifai 和 app 目錄中的所有代碼败潦,并以 HTML 格式生成報告。當運行它時准脂,將看到測試目錄中的測試正在執(zhí)行劫扒,而覆蓋插件正在跟蹤應用程序中的哪些行正在執(zhí)行。測試完成后狸膏,可以查看生成的報告(默認為`htmlcov/index.html`)并單擊各個文件以查看哪些部分未被任何測試覆蓋沟饥。當忘記測試某些條件、異常等時湾戳,這尤其有用贤旷。 ![測試覆蓋率](https://upload-images.jianshu.io/upload_images/27840083-88727a764a09e446.png) > warning > > 雖然有 100% 的覆蓋率,但這并不意味著應用程序是完美的砾脑。覆蓋率只是表示在測試中執(zhí)行的一段代碼幼驶,不一定是它的每一部分都經(jīng)過測試,更不用說徹底測試了韧衣。因此盅藻,覆蓋率**永遠**不應被用作正確性的表示。但是畅铭,將覆蓋率保持在 100% 非常有用氏淑,這樣就可以知道新功能何時尚未測試。在 CI/CD 課程中硕噩,將了解在推送到特定分支時如何使用 GitHub 操作來實現(xiàn) 100% 的覆蓋率假残。 ### 排除項 有時編寫測試來覆蓋應用程序中的每一行是沒有意義的,但仍然希望考慮這些行榴徐,以便可以保持 100% 的覆蓋率。應用排除時匀归,有兩個級別的權限: 1. 通過添加此評論來原諒行`# pragma: no cover, ` ``` if trial: # pragma: no cover, optuna pruning trial.report(val_loss, epoch) if trial.should_prune(): raise optuna.TrialPruned() ``` 2. `pyproject.toml`通過在配置中指定文件來排除文件: ``` # Pytest coverage [tool.coverage.run] omit = ["app/gunicorn.py"] ``` > 重點是能夠通過評論為這些排除項添加理由坑资,以便團隊可以遵循推理。 現(xiàn)在已經(jīng)有了測試傳統(tǒng)軟件的基礎穆端,讓在機器學習系統(tǒng)的背景下深入測試數(shù)據(jù)和模型袱贮。 ## 數(shù)據(jù) 到目前為止,已經(jīng)使用單元測試和集成測試來測試與transformers數(shù)據(jù)交互的功能体啰,但還沒有測試數(shù)據(jù)本身的有效性攒巍。將使用[great expectations](https://github.com/great-expectations/great_expectations)庫來測試transformers數(shù)據(jù)預期的樣子嗽仪。它是一個庫,使能夠以標準化的方式創(chuàng)建關于transformers數(shù)據(jù)應該是什么樣子的期望柒莉。它還提供了與后端數(shù)據(jù)源(如本地文件系統(tǒng)闻坚、S3、數(shù)據(jù)庫等)無縫連接的模塊兢孝。讓通過實現(xiàn)對應用程序所需的期望來探索該庫窿凤。 > ??跟隨交互式note在?[**testing-ml**](https://github.com/GokuMohandas/testing-ml)存儲庫,因為實現(xiàn)了以下概念跨蟹。 ``` pip install great-expectations==0.15.15 ``` 將把它添加到transformers`setup.py`腳本中: ``` # setup.py test_packages = [ "pytest==7.1.2", "pytest-cov==2.10.1", "great-expectations==0.15.15" ] ``` 首先雳殊,將加載想要應用transformers期望的數(shù)據(jù)〈靶可以從各種[來源](https://docs.greatexpectations.io/docs/guides/connecting_to_your_data/connect_to_data_overview)(文件系統(tǒng)夯秃、數(shù)據(jù)庫、云等)加載transformers數(shù)據(jù)痢艺,然后可以將其包裝在一個[數(shù)據(jù)集模塊](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/dataset/index.html)(Pandas/Spark DataFrame仓洼、SQLAlchemy)中。 ``` import great_expectations as ge import json import pandas as pd from urllib.request import urlopen ``` ``` # Load labeled projects projects = pd.read_csv("https://raw.githubusercontent.com/GokuMohandas/Made-With-ML/main/datasets/projects.csv") tags = pd.read_csv("https://raw.githubusercontent.com/GokuMohandas/Made-With-ML/main/datasets/tags.csv") df = ge.dataset.PandasDataset(pd.merge(projects, tags, on="id")) print (f"{len(df)} projects") df.head(5) ``` ``` # Load labeled projects projects = pd.read_csv("https://raw.githubusercontent.com/GokuMohandas/Made-With-ML/main/datasets/projects.csv") tags = pd.read_csv("https://raw.githubusercontent.com/GokuMohandas/Made-With-ML/main/datasets/tags.csv") df = ge.dataset.PandasDataset(pd.merge(projects, tags, on="id")) print (f"{len(df)} projects") df.head(5) ``` | | id | created_on | title | description | tag | | --- | --- | ------------------- | ------------------------------------------------- | --------------------------------------------------- | ---------------------- | | 0 | 6 | 2020-02-20 06:43:18 | Comparison between YOLO and RCNN on real world... | Bringing theory to experiment is cool. We can ... | computer-vision | | 1 | 7 | 2020-02-20 06:47:21 | Show, Infer & Tell: Contextual Inference for C... | The beauty of the work lies in the way it arch... | computer-vision | | 2 | 9 | 2020-02-24 16:24:45 | Awesome Graph Classification | A collection of important graph embedding, cla... | graph-learning | | 3 | 15 | 2020-02-28 23:55:26 | Awesome Monte Carlo Tree Search | A curated list of Monte Carlo tree search papers... | reinforcement-learning | | 4 | 19 | 2020-03-03 13:54:31 | Diffusion to Vector | Reference implementation of Diffusion2Vec (Com... | graph-learning | ### 期望 在對transformers數(shù)據(jù)應該是什么樣子建立期望時腹备,要考慮transformers整個數(shù)據(jù)集和其中的所有特征(列)衬潦。 `# Presence of specific features df.expect_table_columns_to_match_ordered_list( column_list=["id", "created_on", "title", "description", "tag"] )` `# Unique combinations of features (detect data leaks!) df.expect_compound_columns_to_be_unique(column_list=["title", "description"])` `# Missing values df.expect_column_values_to_not_be_null(column="tag")` `# Unique values df.expect_column_values_to_be_unique(column="id")` `# Type adherence df.expect_column_values_to_be_of_type(column="title", type_="str")` `# List (categorical) / range (continuous) of allowed values tags = ["computer-vision", "graph-learning", "reinforcement-learning", "natural-language-processing", "mlops", "time-series"] df.expect_column_values_to_be_in_set(column="tag", value_set=tags)` 這些期望中的每一個都會創(chuàng)建一個輸出,其中包含有關成功或失敗植酥、預期和觀察到的值镀岛、提出的期望等詳細信息。例如友驮,如果成功漂羊,期望將產(chǎn)生以下內(nèi)容:`df.expect_column_values_to_be_of_type(column="title",?type_="str")` { "exception_info": { "raised_exception": false, "exception_traceback": null, "exception_message": null }, "success": true, "meta": {}, "expectation_config": { "kwargs": { "column": "title", "type_": "str", "result_format": "BASIC" }, "meta": {}, "expectation_type": "_expect_column_values_to_be_of_type__map" }, "result": { "element_count": 955, "missing_count": 0, "missing_percent": 0.0, "unexpected_count": 0, "unexpected_percent": 0.0, "unexpected_percent_nonmissing": 0.0, "partial_unexpected_list": [] } } 如果有一個失敗的期望(例如),會收到這個輸出(注意導致失敗的原因的計數(shù)和示例):?`df.expect_column_values_to_be_of_type(column="title",?type_="int")` { "success": false, "exception_info": { "raised_exception": false, "exception_traceback": null, "exception_message": null }, "expectation_config": { "meta": {}, "kwargs": { "column": "title", "type_": "int", "result_format": "BASIC" }, "expectation_type": "_expect_column_values_to_be_of_type__map" }, "result": { "element_count": 955, "missing_count": 0, "missing_percent": 0.0, "unexpected_count": 955, "unexpected_percent": 100.0, "unexpected_percent_nonmissing": 100.0, "partial_unexpected_list": [ "How to Deal with Files in Google Colab: What You Need to Know", "Machine Learning Methods Explained (+ Examples)", "OpenMMLab Computer Vision", "...", ] }, "meta": {} } 可以創(chuàng)造一些不同的期望卸留。一定要探索所有的[期望](https://greatexpectations.io/expectations/)走越,包括[自定義期望](https://docs.greatexpectations.io/docs/guides/expectations/creating_custom_expectations/overview/)。以下是一些與transformers特定數(shù)據(jù)集無關但廣泛適用的其他流行期望: - 特征值與其他特征值的關系 →`expect_column_pair_values_a_to_be_greater_than_b` - 樣本的行數(shù)(精確或范圍)→`expect_table_row_count_to_be_between` - 數(shù)值統(tǒng)計(均值耻瑟、標準差旨指、中位數(shù)、最大值喳整、最小值谆构、總和等)→`expect_column_mean_to_be_between` ### 組織 在組織期望時,建議從表級開始框都,然后轉到各個功能列搬素。 #### Table expectations ``` # columns df.expect_table_columns_to_match_ordered_list( column_list=["id", "created_on", "title", "description", "tag"]) # data leak df.expect_compound_columns_to_be_unique(column_list=["title", "description"]) ``` #### Column期望 ``` # id df.expect_column_values_to_be_unique(column="id") # created_on df.expect_column_values_to_not_be_null(column="created_on") df.expect_column_values_to_match_strftime_format( column="created_on", strftime_format="%Y-%m-%d %H:%M:%S") # title df.expect_column_values_to_not_be_null(column="title") df.expect_column_values_to_be_of_type(column="title", type_="str") # description df.expect_column_values_to_not_be_null(column="description") df.expect_column_values_to_be_of_type(column="description", type_="str") # tag df.expect_column_values_to_not_be_null(column="tag") df.expect_column_values_to_be_of_type(column="tag", type_="str") ``` 可以將所有期望組合在一起以創(chuàng)建一個[Expectation Suite](https://docs.greatexpectations.io/en/latest/reference/core_concepts/expectations/expectations.html#expectation-suites)對象,可以使用它來驗證任何數(shù)據(jù)集模塊闽坡。 ``` # Expectation suite expectation_suite = df.get_expectation_suite(discard_failed_expectations=False) print(df.validate(expectation_suite=expectation_suite, only_return_failures=True)) ``` ``` { "success": true, "results": [], "statistics": { "evaluated_expectations": 11, "successful_expectations": 11, "unsuccessful_expectations": 0, "success_percent": 100.0 }, "evaluation_parameters": {} } ``` ### 項目 到目前為止愚臀,已經(jīng)在臨時腳本/note級別使用了 Great Expectations 庫叉袍,但可以通過創(chuàng)建一個項目來進一步組織transformers期望刁品。 ``` cd tests great_expectations init ``` 這將建立一個`tests/great_expectations`具有以下結構的目錄: ``` tests/great_expectations/ ├── checkpoints/ ├── expectations/ ├── plugins/ ├── uncommitted/ ├── .gitignore └── great_expectations.yml ``` #### 數(shù)據(jù)源 第一步是建立transformers`datasource`掖棉,告訴 Great Expectations transformers數(shù)據(jù)在哪里: ``` great_expectations datasource new ``` ``` What data would you like Great Expectations to connect to? 1. Files on a filesystem (for processing with Pandas or Spark) ?? 2. Relational database (SQL) ``` ``` What are you processing your files with? 1. Pandas ?? 2. PySpark ``` ``` Enter the path of the root directory where the data files are stored: ../data ``` #### Suites 手動咙边、交互或自動創(chuàng)建期望并將它們保存為suite(對特定數(shù)據(jù)assert的一組期望)狐粱。 ``` great_expectations suite new ``` ``` How would you like to create your Expectation Suite? 1. Manually, without interacting with a sample batch of data (default) 2. Interactively, with a sample batch of data ?? 3. Automatically, using a profiler ``` ``` Which data asset (accessible by data connector "default_inferred_data_connector_name") would you like to use? 1. labeled_projects.csv 2. projects.csv ?? 3. tags.csv ``` ``` Name the new Expectation Suite [projects.csv.warning]: projects ``` 這將打開一個交互式note拒啰,可以在其中添加期望皂吮。復制并粘貼下面的期望并運行所有單元格戒傻。`tags.csv`對和重復此步驟`labeled_projects.csv`。 ![寄予厚望的套房](https://upload-images.jianshu.io/upload_images/27840083-7f66bc4773236bf1.png) > Expectations for?`projects.csv` > > Table expectations > > ``` > # Presence of features > validator.expect_table_columns_to_match_ordered_list( > column_list=["id", "created_on", "title", "description"]) > validator.expect_compound_columns_to_be_unique(column_list=["title", "description"]) # data leak > > ``` > Column expectations: > > ``` > # id > validator.expect_column_values_to_be_unique(column="id") > > # create_on > validator.expect_column_values_to_not_be_null(column="created_on") > validator.expect_column_values_to_match_strftime_format( > column="created_on", strftime_format="%Y-%m-%d %H:%M:%S") > > # title > validator.expect_column_values_to_not_be_null(column="title") > validator.expect_column_values_to_be_of_type(column="title", type_="str") > > # description > validator.expect_column_values_to_not_be_null(column="description") > validator.expect_column_values_to_be_of_type(column="description", type_="str") > > ``` > Expectations for?`tags.csv` > > Table expectations > > ``` > # Presence of features > validator.expect_table_columns_to_match_ordered_list(column_list=["id", "tag"]) > > ``` > Column expectations: > > ``` > # id > validator.expect_column_values_to_be_unique(column="id") > > # tag > validator.expect_column_values_to_not_be_null(column="tag") > validator.expect_column_values_to_be_of_type(column="tag", type_="str") > > ``` > Expectations for?`labeled_projects.csv` > > Table expectations > > ``` > # Presence of features > validator.expect_table_columns_to_match_ordered_list( > column_list=["id", "created_on", "title", "description", "tag"]) > validator.expect_compound_columns_to_be_unique(column_list=["title", "description"]) # data leak > > ``` > Column expectations: > > ``` > # id > validator.expect_column_values_to_be_unique(column="id") > > # create_on > validator.expect_column_values_to_not_be_null(column="created_on") > validator.expect_column_values_to_match_strftime_format( > column="created_on", strftime_format="%Y-%m-%d %H:%M:%S") > > # title > validator.expect_column_values_to_not_be_null(column="title") > validator.expect_column_values_to_be_of_type(column="title", type_="str") > > # description > validator.expect_column_values_to_not_be_null(column="description") > validator.expect_column_values_to_be_of_type(column="description", type_="str") > > # tag > validator.expect_column_values_to_not_be_null(column="tag") > validator.expect_column_values_to_be_of_type(column="tag", type_="str") > > ``` 所有這些期望都保存在`great_expectations/expectations`: ``` great_expectations/ ├── expectations/ │ ├── labeled_projects.csv │ ├── projects.csv │ └── tags.csv ``` 還可以列出suite: `great_expectations suite list` ``` Using v3 (Batch Request) API 3 Expectation Suites found: - labeled_projects - projects - tags ``` 要編輯suite蜂筹,可以執(zhí)行以下 CLI 命令: `great_expectations suite edit ` #### 檢查點 創(chuàng)建檢查點需纳,其中將一組期望應用于特定數(shù)據(jù)assert。這是一種以編程方式在現(xiàn)有的和新的數(shù)據(jù)源上應用檢查點的好方法艺挪。 `cd tests great_expectations checkpoint new CHECKPOINT_NAME` 所以對于transformers項目不翩,它將是: ``` great_expectations checkpoint new projects great_expectations checkpoint new tags great_expectations checkpoint new labeled_projects ``` 這些檢查點創(chuàng)建調(diào)用中的每一個都將啟動一個note,可以在其中定義要將此檢查點應用于哪些suite麻裳。必須更改`data_asset_name`(運行檢查點suite的數(shù)據(jù)assert)和`expectation_suite_name`(要使用的suite的名稱)的行口蝠。例如,`projects`檢查點將使用`projects.csv`數(shù)據(jù)assert和`projects`suite津坑。 > 只要架構和驗證適用妙蔗,檢查點就可以共享同一個suite。 ``` my_checkpoint_name = "projects" # This was populated from your CLI command. yaml_config = f""" name: {my_checkpoint_name} config_version: 1.0 class_name: SimpleCheckpoint run_name_template: "%Y%m%d-%H%M%S-my-run-name-template" validations: - batch_request: datasource_name: local_data data_connector_name: default_inferred_data_connector_name data_asset_name: projects.csv data_connector_query: index: -1 expectation_suite_name: projects """ print(yaml_config) ``` > 驗證自動填充 > > 一定要確保`datasource_name`,`data_asset_name`和`expectation_suite_name`都是希望它們成為的樣子(Great Expectations 自動填充那些可能并不總是準確的假設)疆瑰。 `tags`對和檢查點重復這些相同的步驟眉反,`labeled_projects`然后就可以執(zhí)行它們了: ``` great_expectations checkpoint run projects great_expectations checkpoint run tags great_expectations checkpoint run labeled_projects ``` ![寄予厚望的檢查站](https://upload-images.jianshu.io/upload_images/27840083-14165996614fed0a.png) 在本課結束時,將在transformers目標中創(chuàng)建一個`Makefile`運行所有這些測試(代碼穆役、數(shù)據(jù)和模型)的目標寸五,并且將在transformers[預提交課程](https://franztao.github.io/2022/10/26/Pre_commit/)中自動執(zhí)行它們。 > note > > 已經(jīng)對transformers源數(shù)據(jù)集應用了預期耿币,但還有許多其他關鍵領域需要測試數(shù)據(jù)梳杏。例如,清洗淹接、擴充十性、拆分、預處理塑悼、標記化等過程的中間輸出劲适。 ### 文檔 當使用 CLI 應用程序創(chuàng)建期望時,Great Expectations 會自動為transformers測試生成文檔拢肆。它還存儲有關驗證運行及其結果的信息减响。可以使用以下命令啟動生成數(shù)據(jù)文檔:`great_expectations docs build` ![數(shù)據(jù)文檔](https://upload-images.jianshu.io/upload_images/27840083-47f1aa6977b84948.png) > 默認情況下郭怪,Great Expectations 在本地存儲transformers期望支示、結果和指標,但對于生產(chǎn)鄙才,需要設置遠程[元數(shù)據(jù)存儲](https://docs.greatexpectations.io/docs/guides/setup/#metadata-stores)颂鸿。 ### 生產(chǎn) 與孤立的 assert 語句相比,使用諸如 great expectations 之類的庫的優(yōu)勢在于可以: - 減少跨數(shù)據(jù)模式創(chuàng)建測試的冗余工作 - 自動創(chuàng)建測試[檢查點](https://franztao.github.io/2022/10/01/Testing/#checkpoints)以隨著transformers數(shù)據(jù)集增長而執(zhí)行 - 自動生成關于期望的[文檔和運行報告](https://franztao.github.io/2022/10/01/Testing/#documentation) - 輕松連接后端數(shù)據(jù)源攒庵,如本地文件系統(tǒng)嘴纺、S3、數(shù)據(jù)庫等浓冒。 [在transformersDataOps 工作流](https://franztao.github.io/2022/11/10/Orchestration/#dataops)中提取栽渴、加載和轉換數(shù)據(jù)時,將執(zhí)行其中許多期望稳懒。通常闲擦,數(shù)據(jù)將從源([數(shù)據(jù)庫](https://franztao.github.io/2022/11/10/Data_stack/#database)、[API](https://franztao.github.io/2022/10/01/RESTful_API/)等)中提取并加載到數(shù)據(jù)系統(tǒng)(例如[數(shù)據(jù)倉庫](https://franztao.github.io/2022/11/10/Data_stack/#data-warehouse))中场梆,然后在那里進行轉換(例如使用[dbt](https://www.getdbt.com/))以供下游應用程序使用墅冷。在這些任務中,可以運行 Great Expectations 檢查點驗證以確保數(shù)據(jù)的有效性和應用于數(shù)據(jù)的更改或油。[將在編排課程](https://franztao.github.io/2022/11/10/Orchestration/#dataops)中看到一個簡化版本的數(shù)據(jù)驗證何時應該在transformers數(shù)據(jù)工作流中進行寞忿。 ![生產(chǎn)中的 ELT 流水線](https://upload-images.jianshu.io/upload_images/27840083-ab3a8c04aa359258.png) > 如果您不熟悉不同的數(shù)據(jù)系統(tǒng),請在transformers[數(shù)據(jù)堆棧課程](https://franztao.github.io/2022/11/10/Data_stack/)中了解更多信息顶岸。 ## model 測試 ML 系統(tǒng)的最后一個方面涉及在訓練腔彰、評估、推理和部署期間測試模型蜕琴。 ### 訓練 希望在開發(fā)訓練管道時迭代地編寫測試萍桌,以便可以快速發(fā)現(xiàn)錯誤。這一點尤為重要凌简,因為與傳統(tǒng)軟件不同上炎,ML 系統(tǒng)可以運行完成而不會引發(fā)任何異常/錯誤,但可能會產(chǎn)生不正確的系統(tǒng)雏搂。還希望快速捕獲錯誤以節(jié)省時間和計算藕施。 - 檢查模型輸出的形狀和值 ``` assert model(inputs).shape == torch.Size([len(inputs), num_classes]) ``` - 在一批訓練后檢查損失是否減少 ``` assert epoch_loss < prev_epoch_loss ``` - 批量過擬合 ``` accuracy = train(model, inputs=batches[0]) assert accuracy == pytest.approx(0.95, abs=0.05) # 0.95 ± 0.05 ``` - 訓練完成(測試提前停止、保存等) ``` train(model) assert learning_rate >= min_learning_rate assert artifacts ``` - 在不同的設備上 ``` assert train(model, device=torch.device("cpu")) assert train(model, device=torch.device("cuda")) ``` > note > > 您可以使用 pytest 標記標記計算密集型測試凸郑,并且僅在對影響模型的系統(tǒng)進行更改時才執(zhí)行它們裳食。 > > ``` > @pytest.mark.training > def test_train_model(): > ... > > ``` ### 行為測試 行為測試是測試輸入數(shù)據(jù)和預期輸出的過程,同時將模型視為黑盒(與模型無關的評估)芙沥。它們不一定在本質上是對抗性的诲祸,但更多的是在部署模型后可能期望在現(xiàn)實世界中看到的擾動類型浊吏。關于這個主題的具有里程碑意義的論文是[Beyond Accuracy: Behavioral Testing of NLP Models with CheckList](https://arxiv.org/abs/2005.04118),它將行為測試分為三種類型的測試: - `invariance`:更改不應影響輸出救氯。 ``` # INVariance via verb injection (changes should not affect outputs) tokens = ["revolutionized", "disrupted"] texts = [f"Transformers applied to NLP have {token} the ML field." for token in tokens] predict.predict(texts=texts, artifacts=artifacts) ``` ``` ['natural-language-processing', 'natural-language-processing'] ``` - `directional`:變化應該會影響產(chǎn)出找田。 ``` # DIRectional expectations (changes with known outputs) tokens = ["text classification", "image classification"] texts = [f"ML applied to {token}." for token in tokens] predict.predict(texts=texts, artifacts=artifacts) ``` ``` ['natural-language-processing', 'computer-vision'] ``` - `minimum functionality`:輸入和預期輸出的簡單組合。 ``` # Minimum Functionality Tests (simple input/output pairs) tokens = ["natural language processing", "mlops"] texts = [f"{token} is the next big wave in machine learning." for token in tokens] predict.predict(texts=texts, artifacts=artifacts) ``` ``` ['natural-language-processing', 'mlops'] ``` > 對抗性測試 > > 這些類型的測試中的每一種還可以包括對抗性測試着憨,例如使用常見的有偏見的令牌或嘈雜的令牌進行測試等墩衙。 > > ``` > texts = [ > "CNNs for text classification.", # CNNs are typically seen in computer-vision projects > "This should not produce any relevant topics." # should predict `other` label > ] > predict.predict(texts=texts, artifacts=artifacts) > > ``` 可以將這些測試轉換為系統(tǒng)的參數(shù)化測試: ``` mkdir tests/model touch tests/model/test_behavioral.py ``` ``` # tests/model/test_behavioral.py from pathlib import Path import pytest from config import config from tagifai import main, predict @pytest.fixture(scope="module") def artifacts(): run_id = open(Path(config.CONFIG_DIR, "run_id.txt")).read() artifacts = main.load_artifacts(run_id=run_id) return artifacts @pytest.mark.parametrize( "text_a, text_b, tag", [ ( "Transformers applied to NLP have revolutionized machine learning.", "Transformers applied to NLP have disrupted machine learning.", "natural-language-processing", ), ], ) def test_inv(text_a, text_b, tag, artifacts): """INVariance via verb injection (changes should not affect outputs).""" tag_a = predict.predict(texts=[text_a], artifacts=artifacts)[0]["predicted_tag"] tag_b = predict.predict(texts=[text_b], artifacts=artifacts)[0]["predicted_tag"] assert tag_a == tag_b == tag ``` 查看`tests/model/test_behavioral.py` ``` from pathlib import Path import pytest from config import config from tagifai import main, predict @pytest.fixture(scope="module") def artifacts(): run_id = open(Path(config.CONFIG_DIR, "run_id.txt")).read() artifacts = main.load_artifacts(run_id=run_id) return artifacts @pytest.mark.parametrize( "text, tag", [ ( "Transformers applied to NLP have revolutionized machine learning.", "natural-language-processing", ), ( "Transformers applied to NLP have disrupted machine learning.", "natural-language-processing", ), ], ) def test_inv(text, tag, artifacts): """INVariance via verb injection (changes should not affect outputs).""" predicted_tag = predict.predict(texts=[text], artifacts=artifacts)[0]["predicted_tag"] assert tag == predicted_tag @pytest.mark.parametrize( "text, tag", [ ( "ML applied to text classification.", "natural-language-processing", ), ( "ML applied to image classification.", "computer-vision", ), ( "CNNs for text classification.", "natural-language-processing", ) ], ) def test_dir(text, tag, artifacts): """DIRectional expectations (changes with known outputs).""" predicted_tag = predict.predict(texts=[text], artifacts=artifacts)[0]["predicted_tag"] assert tag == predicted_tag @pytest.mark.parametrize( "text, tag", [ ( "Natural language processing is the next big wave in machine learning.", "natural-language-processing", ), ( "MLOps is the next big wave in machine learning.", "mlops", ), ( "This should not produce any relevant topics.", "other", ), ], ) def test_mft(text, tag, artifacts): """Minimum Functionality Tests (simple input/output pairs).""" predicted_tag = predict.predict(texts=[text], artifacts=artifacts)[0]["predicted_tag"] assert tag == predicted_tag ``` ### 推理 部署模型后,大多數(shù)用戶將使用它進行推理(直接/間接)甲抖,因此測試它的各個方面非常重要漆改。 #### 加載artifacts 這是第一次不從內(nèi)存中加載組件,因此希望確保所需的工件(模型權重准谚、編碼器挫剑、配置等)都能夠被加載。 ``` artifacts = main.load_artifacts(run_id=run_id) assert isinstance(artifacts["label_encoder"], data.LabelEncoder) ... ``` #### 預言 一旦加載了工件柱衔,就準備好測試預測管道暮顺。應該只用一個輸入和一批輸入來測試樣本(例如,填充有時會產(chǎn)生意想不到的后果)秀存。 ``` # test our API call directly data = { "texts": [ {"text": "Transfer learning with transformers for text classification."}, {"text": "Generative adversarial networks in both PyTorch and TensorFlow."}, ] } response = client.post("/predict", json=data) assert response.status_code == HTTPStatus.OK assert response.request.method == "POST" assert len(response.json()["data"]["predictions"]) == len(data["texts"]) ... ``` ## 生成文件 讓在其中創(chuàng)建一個目標捶码,`Makefile`這將允許一次調(diào)用執(zhí)行所有測試: ``` # Test .PHONY: test test: pytest -m "not training" cd tests && great_expectations checkpoint run projects cd tests && great_expectations checkpoint run tags cd tests && great_expectations checkpoint run labeled_projects ``` ``` make test ``` ## 測試與監(jiān)控 最后,將討論測試和[監(jiān)控](https://franztao.github.io/2022/10/01/Testing//../monitoring/)之間的相似點和區(qū)別或链。它們都是 ML 開發(fā)管道的組成部分惫恼,并且相互依賴以進行迭代。測試可確保系統(tǒng)(代碼澳盐、數(shù)據(jù)和模型)達到在離線時建立的預期祈纯。鑒于監(jiān)控涉及這些期望繼續(xù)在線傳遞實時生產(chǎn)數(shù)據(jù),同時還通過以下方式確保其數(shù)據(jù)分布[與](https://franztao.github.io/2022/10/01/Testing//../monitoring/#measuring-drift)參考窗口(通常是訓練數(shù)據(jù)的子集)具有可比性噸n. 當這些條件不再成立時叼耙,需要更仔細地檢查(再培訓可能并不總能解決根本問題)腕窥。 對于[監(jiān)控](https://franztao.github.io/2022/10/01/Testing//../monitoring/),在測試期間不必考慮很多不同的問題筛婉,因為它涉及尚未看到的(實時)數(shù)據(jù)簇爆。 - 特征和預測分布(漂移)、類型爽撒、模式不匹配等入蛆。 - 使用間接信號(因為標簽可能不容易獲得)確定模型性能(整體和數(shù)據(jù)切片的滾動和窗口度量)。 - 在大數(shù)據(jù)的情況下硕勿,需要知道要標記哪些數(shù)據(jù)點并進行上采樣以進行訓練哨毁。 - 識別異常和異常值。 > [將在監(jiān)控](https://franztao.github.io/2022/10/01/Testing//../monitoring/)課程中更深入地(和代碼)介紹所有這些概念源武。 ## 資源 - [Great Expectations](https://github.com/great-expectations/great_expectations) - [The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/aad9f93b86b7addfea4c419b9100c6cdd26cacea.pdf) - [Beyond Accuracy: Behavioral Testing of NLP Models with CheckList](https://arxiv.org/abs/2005.04118) - [Robustness Gym: Unifying the NLP Evaluation Landscape](https://arxiv.org/abs/2101.04840) 更多干貨扼褪,第一時間更新在以下微信公眾號: ![](https://raw.githubusercontent.com/franztao/blog_picture/main/marktext/2022-12-03-12-49-27-weixin.png) 您的一點點支持想幻,是我后續(xù)更多的創(chuàng)造和貢獻 ![](https://upload-images.jianshu.io/upload_images/27840083-e458640766afb594.png) 轉載到請包括本文地址 更詳細的轉載事宜請參考[文章如何轉載/引用](https://franztao.github.io/2022/12/04/%E6%96%87%E7%AB%A0%E5%A6%82%E4%BD%95%E8%BD%AC%E8%BD%BD%E5%92%8C%E5%BC%95%E7%94%A8/) 本文主體源自以下鏈接: ``` @article{madewithml, author = {Goku Mohandas}, title = { Made With ML }, howpublished = {\url{https://madewithml.com/}}, year = {2022} } ``` 本文由[mdnice](https://mdnice.com/?platform=6)多平臺發(fā)布
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市话浇,隨后出現(xiàn)的幾起案子举畸,更是在濱河造成了極大的恐慌,老刑警劉巖凳枝,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異跋核,居然都是意外死亡岖瑰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門砂代,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹋订,“玉大人,你說我怎么就攤上這事刻伊÷督洌” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵捶箱,是天一觀的道長智什。 經(jīng)常有香客問我,道長丁屎,這世上最難降的妖魔是什么荠锭? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮晨川,結果婚禮上证九,老公的妹妹穿的比我還像新娘。我一直安慰自己共虑,他們只是感情好愧怜,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著妈拌,像睡著了一般拥坛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上尘分,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天渴逻,我揣著相機與錄音,去河邊找鬼音诫。 笑死惨奕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的竭钝。 我是一名探鬼主播梨撞,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼雹洗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卧波?” 一聲冷哼從身側響起时肿,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎港粱,沒想到半個月后螃成,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡查坪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年寸宏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片偿曙。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡氮凝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出望忆,到底是詐尸還是另有隱情罩阵,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布启摄,位于F島的核電站稿壁,受9級特大地震影響,放射性物質發(fā)生泄漏歉备。R本人自食惡果不足惜常摧,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望威创。 院中可真熱鬧落午,春花似錦、人聲如沸肚豺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吸申。三九已至梗劫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間截碴,已是汗流浹背梳侨。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留日丹,地道東北人走哺。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像哲虾,于是被迫代替她去往敵國和親丙躏。 傳聞我的和親對象是個殘疾皇子择示,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容