跳至主要內容

    實驗版本、資料版本與可重現性

    從同一個 seed 找不到同一個結果,拆解一次實驗到底需要留下什麼

    sunny 桑尼· · 47 MIN READ · 243 次瀏覽

    實驗版本、資料版本與可重現性
    本文目錄

    「我把 seed 設成 17 了。」這句話仍然沒有告訴同事,你用哪一份資料、哪些訓練列、哪支程式與哪個環境。他拿到同名的 data.csv,跑出另一個分數,也許根本不是隨機性造成的。

    可重現性的第一步,是讓一個結果有可以追溯的來路。資料、配方、切分與程式要能找回;輸出的數字與模型也要能對上那次執行。否則兩個看似相近的分數,可能來自不同測試集,連比較的前提都不相同。

    本篇先用 24 列人工資料,把這些差別做成可執行的例子。你會看到同 seed 改資料、同資料改排列與改 split seed 分別留下什麼痕跡,再理解實驗追蹤與資料版本工具可以接住哪一部分。

    你說的「同一個實驗」,到底要同到哪裡?

    考慮一次簡單的線性回歸。你固定 seed,用資料的一部分估斜率與截距,再在剩下的資料算 MSE。下一次重做時,至少有六種東西可能變:

    要找回的東西只記一個名稱會漏掉什麼

    原始資料
    同名 CSV 的某列可能被修過
    處理規則
    相同文字可被不同型別、缺失值或排序規則解讀
    訓練/測試列
    seed 一樣,但輸入列順序可能不同
    程式與參數
    修改前處理或 loss,分數仍沿用舊檔名
    執行環境
    Python、套件、硬體與數值運算行為可能不同
    結果與產物
    複製錯一個模型檔,紀錄就不再對應這個分數

    重現也有不同層次。檔案 bytes 一樣、同環境得到完全相同結果、數值在容許誤差內一致,和換一批資料仍支持同一個研究結論,是不同的要求。本文實際檢查前面幾個具體環節,不把一次 deterministic 小算例當成整個研究結論已可重現。

    先看這些紀錄在工具裡會長什麼樣。MLflow 把一次執行稱作 run,讓使用者從名稱或 ID 找到該次結果。下面來源圖只有一筆 run,作用是辨識一份紀錄,不是已展示多次實驗比較。

    圖 1|官方 MLflow Quickstart 的單一 run 列表,藍色箭頭原本就在來源圖中,指向可開啟的 run。主圖保留完整介面,手機版只放大 run 名稱與建立時間的原圖局部;不是新增另一個 run。來源:MLflow project contributors,Apache-2.0;主圖原檔未改,手機裁取原像素,封面為完整主圖等比白底。這是來源 MLflow 3.5.0 範例,非本站已啟動的服務或 2023 年歷史介面。

    接下來先不依賴工具,用一個能算清楚的小實驗,決定每個 run 究竟該留下什麼。

    檔名、raw hash 與內容正規化,為什麼要分開?

    檔名是方便找東西的位置。Hash 則可以比較拿到的 bytes 是否跟可信紀錄一致:本例用 SHA256,將原 CSV 的全部 bytes 算成一個指紋。

    只要其中一列的 target 改了,即使檔名還叫 data.csv,raw 指紋也會不同。可是反過來說,raw 不同不一定代表統計內容不同。把同一份 CSV 的所有列倒序,檔案 bytes 已不同;若後續處理本來就先依 ID 排序,模型可能仍看到相同資料與相同切分。

    因此本例另外保存「依明訂規則解讀後」的指紋:把 id 讀成整數、x/y 讀成有限浮點,拒絕重複 ID,再按唯一 ID 排序,最後以固定 key 順序與緊湊 JSON 編碼。這個 normalized rows hash 只回答這份 schema 和規則下的內容是否相同,不是普遍的語意相等判定。

    例如換一種小數文字寫法,浮點解析可能得到同值;換成另一種單位,數字或許也很接近,卻已不是相同現象。規則必須先清楚,不能看兩個 hash 一樣就替資料語意背書。

    同樣地,hash 的基準得可信。若原檔與紀錄都被一起換掉,單純比較兩者不會自動揭穿來源;它不是驗簽、資料品質證書或惡意程式安全證明。

    一個 24 列實驗:把切分與計算配方一起留下來

    本例的人工資料有 id=0 到 23:

    x_i = i − 12
    y_i = 2.5 x_i + 4 + 0.2 × (((7i) mod 5) − 2)
    

    程式先按 ID 排序,再以 seed 17 shuffle ID。前六個作 test,剩下十八個作 train,並保存實際列 ID。只在 train 用含截距的一維最小平方法估參數:

    slope = Σ_train (x−mean_x)(y−mean_y) / Σ_train (x−mean_x)²
    intercept = mean_y − slope × mean_x
    

    seed 只控制這份明確演算法的一個隨機步驟。保存 train/test IDs,才能直接檢查哪些列真正參與 fit,而不必靠回憶當時資料有沒有先排序。

    本次實際 test IDs 是 1、2、10、12、19、22;其餘十八列 fit 得到:

    項目實際結果

    slope
    2.5
    intercept
    3.9333333333333336
    test MSE
    .13999999999999943

    這是人工配方的單次 holdout。它沒有調參,也不代表真實問題的品質。這裡刻意把模型算得很小,讓注意力放在結果與輸入如何對應。

    程式最後產出三個檔案:data.csv 保留原始資料;result.json 保存模型參數、每筆 test 預測與 MSE;receipt.json 記資料與程式指紋、配方、切分列、部分環境,以及結果的指紋。

    receipt 不是把所有資料塞進一個 JSON,而是一份尋找與比對的線索。程式指紋也只是此單檔原始碼的 bytes,不會自動涵蓋外部檔案、整個 Git repository 或它沒有記錄的依賴。

    三個受控變動,找出「看起來一樣」藏著的差別

    程式真的在同一次執行裡重做相同輸入,再測三個變動,沒有重新抽新資料來湊結論。

    變動指紋/結果發生什麼事本例三項 checker 的反應
    一列已知 train 的 target 加 1raw 與 normalized 內容改變,模型結果也改變拒絕 raw 不一致
    所有列倒序raw 改變;按 ID 排序後的內容、切分與結果相同仍拒絕 raw 不一致
    只把 split seed 17 改 18這次檢查直接指出 seed 與舊紀錄不同拒絕 seed 不一致

    第一種變動改的是 ID 0,原本已在 train。相同 seed 並沒有把模型鎖住;資料改了,模型仍會改。

    第二種最容易誤讀。Checker 的拒絕是「你沒有拿到完全相同的原 bytes」,不等於它證明統計結果一定不同。程式另外驗證,正規化後的 rows 與結果完全一致;這恰好說明 bytes 身分與本例處理後的內容身分是兩件事。

    相同輸入與環境重做兩次,程式將 receipt 與 result 轉成 canonical JSON,兩次 bytes 相同。Canonical 在這裡明訂為 key 排序、無多餘空白、禁止 NaN;保存到檔案時再加一個換行。這項證據只限本次執行與這份流程,不能推成跨平台或任意演算法的 deterministic 保證。

    存下來,不等於每一項都驗過

    這個範例裡最重要的界線,藏在 check_inputs 函式。它實際只比三項:

    目前 raw 資料的 SHA,是否等於 receipt 記錄?
    目前這支程式的 SHA,是否等於 receipt 記錄?
    傳入的 split seed,是否等於 receipt 記錄?
    

    它沒有驗整份 receipt,也沒有讀 result.json 重新核對結果。train/test IDs、環境與 result hash 是保存供追溯與另行比較的紀錄,不是這個函式自動保證全部正確的欄位。

    獨立原型審查實際測過反例:只把 receipt 裡的 train_ids 換成空陣列,或把 result_sha256 改成別的字串,這個三項 checker 仍接受。這符合它的程式範圍,也指出它不能被叫做完整的實驗紀錄驗證器。

    如果你的任務要求驗證保存的結果,就需要真的重新讀結果檔、按同規則算 hash 再比較;若要求重建訓練,也要檢查完整配方、切分、環境與依賴。要求來源不可否認或避免惡意竄改,還會牽涉信任與驗簽。不要只多存幾個欄位,就把尚未實作的檢查算成已完成。

    MLflow 與 DVC 接住哪一部分?

    當實驗變多,手動收集結果與檔案會越來越難。MLflow Tracking 以 run 組織一次執行的 parameters、metrics 與 artifacts,experiment 再把多個 run 放在同一個任務脈絡裡。這讓你有地方找回某次設定與產物,但紀錄什麼仍然需要設計,分數對應哪份資料也不能省略。

    圖 2|同一來源 run 的詳情把 metrics、parameters 與 run metadata 放在一起。畫面 .975 是 training_score/training_accuracy_score,不是本文 OLS 的 test 成績,也不能直接當泛化品質。random_state=8888 屬來源分類器設定,不是本例 split seed 17。主圖完整,手機版裁取原 metrics/parameters 區域。來源:MLflow project contributors,Apache-2.0;主圖原檔未改,手機只裁原像素。

    指標不能只留一個 value,還要知道它算在哪份資料、哪個階段。畫面列名的 training 就提醒我們,訓練成績和保留測試的任務不同。本文自己的 .14 則來自六筆 test MSE,模型、資料與指標都不同,不能跟 .975 排成一個好壞比較。

    圖 3|來源介面另列 logged model,讓使用者從一次 run 找到保存的模型產物。Ready 是這張介面的狀態,並不等於通過本站驗收或已上線;主圖的 Registered models 欄為「—」,不能把 logged 一律讀成已 registered。手機版只裁模型名稱與狀態的原圖局部,藍箭頭沿用來源。來源:MLflow project contributors,Apache-2.0;主圖原檔未改、手機原像素裁切。

    模型產物與 run 紀錄需要能彼此對上。若只保留分數,拿不到當時的模型就無法交接;若只留下模型檔,卻不知哪份資料與配方產生它,也難以解釋這個產物。保存與登錄是可區分的步驟,不因為畫面上有模型名稱就推定它已經完成發布流程。

    DVC 的 .dvc 文件 則提供追蹤資料檔案的 metadata,可跟 Git 一起保存,連到實際資料內容。這裡的用意是分清小型版本紀錄與資料本體,而不是把大型原始資料全塞進程式碼提交。

    除了追蹤資料,DVC 也能在 dvc.yaml 宣告 stage 的命令、輸入依賴與輸出。當一個 stage 的輸出成為另一個 stage 的輸入,它們就有先後依賴;執行關係來自這些連結,不是 YAML 列出的上下順序。

    圖 4|官方文件原有 dvc dag 文字區塊的瀏覽器擷取:prepare 連到 featurize;featurize 同時供 train 與 evaluate 使用,train 也供 evaluate 使用,因此不是四個步驟依序排成單一鏈。這是來源文件範例,非本站執行 DVC 的輸出,也不是本文 OLS 程式的依賴圖。來源:DVC documentation project/contributors,CC BY 4.0;直接擷取原文字區塊,未重打內容、未新增邊線。

    例如資料處理結果變了,訓練與評估哪些部分需要重做,要看真實的檔案/參數依賴。若程式偷讀了沒有宣告的外部檔案,圖也不會神奇知道。因此這類工具需要正確的依賴定義,和「紀錄已存下來」一樣,都不能取代對流程本身的理解。

    兩者都不能替你決定「這次比較是否公平」。即使每個 run 都完整保存,若測試集已被反覆看答案選設定,追蹤工具只會更清楚地保存這個過程,不會自動修正它。

    這裡只介紹跟本篇任務有關的基本角色;引用的是目前官方概念頁,不將頁面上的較新產品功能當成 2023 年以前的歷史敘述。本例沒有安裝或執行 MLflow、DVC、remote 或 tracking server,也沒有把下面的 receipt 當作這些工具的等價實作。

    完整可執行程式:先把一份紀錄做對

    程式只使用 Python 標準函式庫。把完整內容存成 versioned_experiment.py,在同目錄執行:

    python3 versioned_experiment.py > stdout.json
    

    本次環境為 CPython 3.10.12、Linux、x86_64。成功時 exit 0、stdout 可解析成 JSON,三種預期拒絕都列在 rejected_inputs,並在本例 artifacts 目錄重建 data.csv、receipt.json、result.json。這不是保留歷次執行的完整 registry;再次執行會覆寫這三個本例檔案,需要保留舊 run 時應另存各次紀錄。

    輸出的環境資訊只有 Python 版本、implementation、system 與 machine,沒有完整 OS、套件或 container lock。標準庫小例能把來路說清楚,不能代替更複雜模型需要的依賴鎖定與數值容許誤差設計。

    """Original stdlib-only local provenance exercise, not an MLflow/DVC implementation."""
    import csv
    import hashlib
    import io
    import json
    import math
    import platform
    import random
    from pathlib import Path
    
    BASE = Path(__file__).resolve().parent
    
    
    def digest(data):
        return hashlib.sha256(data).hexdigest()
    
    
    def canonical(value):
        return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
    
    
    def csv_bytes(rows):
        stream = io.StringIO(newline="")
        writer = csv.writer(stream, lineterminator="\n")
        writer.writerow(["id", "x", "y"])
        writer.writerows(rows)
        return stream.getvalue().encode()
    
    
    def parse(raw):
        records = list(csv.DictReader(io.StringIO(raw.decode())))
        rows = [{"id": int(r["id"]), "x": float(r["x"]), "y": float(r["y"])} for r in records]
        if len({r["id"] for r in rows}) != len(rows):
            raise ValueError("Duplicate row ID")
        if not all(math.isfinite(r[k]) for r in rows for k in ("x", "y")):
            raise ValueError("Nonfinite data")
        return sorted(rows, key=lambda r: r["id"])
    
    
    def experiment(raw, seed=17):
        rows = parse(raw)
        shuffled_ids = [r["id"] for r in rows]
        random.Random(seed).shuffle(shuffled_ids)
        test_ids = sorted(shuffled_ids[:6])
        train_ids = sorted(shuffled_ids[6:])
        train = [r for r in rows if r["id"] in train_ids]
        test = [r for r in rows if r["id"] in test_ids]
        # Closed-form univariate OLS with intercept, fit on training rows only.
        mean_x = math.fsum(r["x"] for r in train) / len(train)
        mean_y = math.fsum(r["y"] for r in train) / len(train)
        denominator = math.fsum((r["x"] - mean_x)**2 for r in train)
        if denominator == 0:
            raise ValueError("Training x has zero variance")
        slope = math.fsum((r["x"] - mean_x) * (r["y"] - mean_y) for r in train) / denominator
        intercept = mean_y - slope * mean_x
        predictions = [{"id": r["id"], "prediction": slope * r["x"] + intercept} for r in test]
        mse = math.fsum((p["prediction"] - r["y"])**2 for p, r in zip(predictions, test)) / len(test)
        result = {"model": {"slope": slope, "intercept": intercept},
                  "test_predictions": predictions, "test_mse": mse}
        receipt = {"raw_data_sha256": digest(raw), "normalized_rows_sha256": digest(canonical(rows)),
            "code_sha256": digest(Path(__file__).read_bytes()),
            "recipe": {"model": "univariate OLS with intercept; train-only closed form",
                       "split_seed": seed, "test_count": 6, "row_policy": "sort unique integer IDs before shuffle"},
            "train_ids": train_ids, "test_ids": test_ids,
            "environment": {"python": platform.python_version(), "implementation": platform.python_implementation(),
                            "platform": platform.system(), "machine": platform.machine()},
            "result_sha256": digest(canonical(result))}
        return receipt, result
    
    
    def check_inputs(raw, receipt, seed):
        checks = {"raw_data": digest(raw) == receipt["raw_data_sha256"],
                  "code": digest(Path(__file__).read_bytes()) == receipt["code_sha256"],
                  "split_seed": seed == receipt["recipe"]["split_seed"]}
        # An integrity check against a trusted saved receipt, not proof of authorship/security.
        if not all(checks.values()):
            raise ValueError("Input mismatch: " + ",".join(k for k, same in checks.items() if not same))
    
    
    def main():
        rows = [(i, i - 12, 2.5 * (i - 12) + 4 + ((i * 7) % 5 - 2) * .2) for i in range(24)]
        raw = csv_bytes(rows)
        receipt, result = experiment(raw)
        check_inputs(raw, receipt, 17)
        replay_receipt, replay_result = experiment(raw)
        assert canonical(receipt) == canonical(replay_receipt)
        assert canonical(result) == canonical(replay_result)
        reordered = csv_bytes(list(reversed(rows)))
        reorder_receipt, reorder_result = experiment(reordered)
        assert reorder_receipt["raw_data_sha256"] != receipt["raw_data_sha256"]
        assert reorder_receipt["normalized_rows_sha256"] == receipt["normalized_rows_sha256"]
        assert canonical(reorder_result) == canonical(result)
        # Change one known training target; same filename and seed would hide this without a receipt.
        changed_id = receipt["train_ids"][0]
        changed_rows = [(i, x, y + 1 if i == changed_id else y) for i, x, y in rows]
        changed = csv_bytes(changed_rows)
        _, changed_result = experiment(changed)
        assert canonical(changed_result) != canonical(result)
        mismatches = []
        for name, candidate, seed in [("changed_training_target", changed, 17),
                                       ("same_rows_reordered", reordered, 17),
                                       ("changed_split_seed", raw, 18)]:
            try:
                check_inputs(candidate, receipt, seed)
            except ValueError as error:
                mismatches.append({"case": name, "rejected": str(error)})
            else:
                raise AssertionError("Expected input mismatch")
        out = BASE / "artifacts"
        out.mkdir(exist_ok=True)
        (out / "data.csv").write_bytes(raw)
        (out / "receipt.json").write_bytes(canonical(receipt) + b"\n")
        (out / "result.json").write_bytes(canonical(result) + b"\n")
        print(json.dumps({"scope": "Original 24-row synthetic local OLS provenance exercise; one existing environment",
            "receipt": receipt, "result": result,
            "checks": {"same_environment_replay_receipt_and_result_byte_exact": True,
                "reordered_raw_bytes_differ": True, "reordered_normalized_rows_and_result_same": True,
                "changed_training_target_id": changed_id, "changed_target_changes_result": True},
            "rejected_inputs": mismatches,
            "limits": ["Hashes check bytes against a trusted receipt, not source trust or data quality.",
                "Normalization is this explicit schema/ID policy, not a universal semantic equivalence test.",
                "Environment metadata is partial, not an OS/library/container lock or cross-platform guarantee.",
                "A seed alone does not identify data, code, split IDs, environment or numeric behavior.",
                "Toy held-out MSE is not production quality, fairness or leakage assurance.",
                "No MLflow server, DVC remote, cloud registry or external service was installed or tested."]},
            indent=2, allow_nan=False))
    
    
    if __name__ == "__main__":
        main()
    

    一份可用的實驗紀錄,應讓接手的人知道先找哪個資料版本、哪段程式、哪組切分,再對哪個結果。遇到差異時,也能指出是 bytes、處理規則、配方還是執行環境先變了。把這些問題拆開,才有辦法從「我記得有跑過」走到「我們能檢查這次究竟跑了什麼」。

    來源與延伸閱讀:

    圖像來源、改作與完整授權副本:

    DS70 image sources and modifications
    
    Figures 1–3 and cover: MLflow project contributors. Source: MLflow Tracking Quickstart, https://mlflow.org/docs/latest/ml/getting-started/quickstart/ . Pinned repository: https://github.com/mlflow/mlflow/tree/c7027358d33437df5b9bea99058ea7b0ef884bde . Original files: quickstart-ui-run-list.png, quickstart-our-run.png, quickstart-ui-logged-models.png under docs/static/images/tutorials/introductory/quickstart-tracking/. Repository license: Apache-2.0; full original LICENSE.txt follows, including the Databricks copyright notice. The selected original screenshots are copied without changes; the three mobile views crop original pixel rectangles without retyping or rearranging labels/data. The cover fits the full run-list screenshot proportionally on a white canvas. These are the source tutorial's MLflow 3.5.0 UI examples and training records, not this project's OLS results or a historical 2023 UI. The source run detail shows training metrics, not this project's test scores. Product names/logos are used descriptively; no endorsement is implied.
    
    Figure 4: DVC documentation project/contributors. Source: https://doc.dvc.org/command-reference/dag . Pinned source and grant: https://github.com/treeverse/dvc.org/blob/21fabba8d66bf81e3cfcbcb9e061d4a5363399ad/content/docs/command-reference/dag.md and https://github.com/treeverse/dvc.org/blob/21fabba8d66bf81e3cfcbcb9e061d4a5363399ad/README.md#copyright . Documentation and images are licensed CC BY 4.0, https://creativecommons.org/licenses/by/4.0/ , except where otherwise noted. This is a browser screenshot of the official documentation's existing dvc dag command/output block; no output, label, edge or value was retyped. It is not a separate upstream bitmap, a local DVC execution, or the graph of this project's original OLS script. No additional diagram content was drawn.
    
    MLflow original Apache-2.0 license (verbatim)
    Copyright 2018 Databricks, Inc.  All rights reserved.
    
    				Apache License
                               Version 2.0, January 2004
                            http://www.apache.org/licenses/
    
       TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    
       1. Definitions.
    
          "License" shall mean the terms and conditions for use, reproduction,
          and distribution as defined by Sections 1 through 9 of this document.
    
          "Licensor" shall mean the copyright owner or entity authorized by
          the copyright owner that is granting the License.
    
          "Legal Entity" shall mean the union of the acting entity and all
          other entities that control, are controlled by, or are under common
          control with that entity. For the purposes of this definition,
          "control" means (i) the power, direct or indirect, to cause the
          direction or management of such entity, whether by contract or
          otherwise, or (ii) ownership of fifty percent (50%) or more of the
          outstanding shares, or (iii) beneficial ownership of such entity.
    
          "You" (or "Your") shall mean an individual or Legal Entity
          exercising permissions granted by this License.
    
          "Source" form shall mean the preferred form for making modifications,
          including but not limited to software source code, documentation
          source, and configuration files.
    
          "Object" form shall mean any form resulting from mechanical
          transformation or translation of a Source form, including but
          not limited to compiled object code, generated documentation,
          and conversions to other media types.
    
          "Work" shall mean the work of authorship, whether in Source or
          Object form, made available under the License, as indicated by a
          copyright notice that is included in or attached to the work
          (an example is provided in the Appendix below).
    
          "Derivative Works" shall mean any work, whether in Source or Object
          form, that is based on (or derived from) the Work and for which the
          editorial revisions, annotations, elaborations, or other modifications
          represent, as a whole, an original work of authorship. For the purposes
          of this License, Derivative Works shall not include works that remain
          separable from, or merely link (or bind by name) to the interfaces of,
          the Work and Derivative Works thereof.
    
          "Contribution" shall mean any work of authorship, including
          the original version of the Work and any modifications or additions
          to that Work or Derivative Works thereof, that is intentionally
          submitted to Licensor for inclusion in the Work by the copyright owner
          or by an individual or Legal Entity authorized to submit on behalf of
          the copyright owner. For the purposes of this definition, "submitted"
          means any form of electronic, verbal, or written communication sent
          to the Licensor or its representatives, including but not limited to
          communication on electronic mailing lists, source code control systems,
          and issue tracking systems that are managed by, or on behalf of, the
          Licensor for the purpose of discussing and improving the Work, but
          excluding communication that is conspicuously marked or otherwise
          designated in writing by the copyright owner as "Not a Contribution."
    
          "Contributor" shall mean Licensor and any individual or Legal Entity
          on behalf of whom a Contribution has been received by Licensor and
          subsequently incorporated within the Work.
    
       2. Grant of Copyright License. Subject to the terms and conditions of
          this License, each Contributor hereby grants to You a perpetual,
          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
          copyright license to reproduce, prepare Derivative Works of,
          publicly display, publicly perform, sublicense, and distribute the
          Work and such Derivative Works in Source or Object form.
    
       3. Grant of Patent License. Subject to the terms and conditions of
          this License, each Contributor hereby grants to You a perpetual,
          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
          (except as stated in this section) patent license to make, have made,
          use, offer to sell, sell, import, and otherwise transfer the Work,
          where such license applies only to those patent claims licensable
          by such Contributor that are necessarily infringed by their
          Contribution(s) alone or by combination of their Contribution(s)
          with the Work to which such Contribution(s) was submitted. If You
          institute patent litigation against any entity (including a
          cross-claim or counterclaim in a lawsuit) alleging that the Work
          or a Contribution incorporated within the Work constitutes direct
          or contributory patent infringement, then any patent licenses
          granted to You under this License for that Work shall terminate
          as of the date such litigation is filed.
    
       4. Redistribution. You may reproduce and distribute copies of the
          Work or Derivative Works thereof in any medium, with or without
          modifications, and in Source or Object form, provided that You
          meet the following conditions:
    
          (a) You must give any other recipients of the Work or
              Derivative Works a copy of this License; and
    
          (b) You must cause any modified files to carry prominent notices
              stating that You changed the files; and
    
          (c) You must retain, in the Source form of any Derivative Works
              that You distribute, all copyright, patent, trademark, and
              attribution notices from the Source form of the Work,
              excluding those notices that do not pertain to any part of
              the Derivative Works; and
    
          (d) If the Work includes a "NOTICE" text file as part of its
              distribution, then any Derivative Works that You distribute must
              include a readable copy of the attribution notices contained
              within such NOTICE file, excluding those notices that do not
              pertain to any part of the Derivative Works, in at least one
              of the following places: within a NOTICE text file distributed
              as part of the Derivative Works; within the Source form or
              documentation, if provided along with the Derivative Works; or,
              within a display generated by the Derivative Works, if and
              wherever such third-party notices normally appear. The contents
              of the NOTICE file are for informational purposes only and
              do not modify the License. You may add Your own attribution
              notices within Derivative Works that You distribute, alongside
              or as an addendum to the NOTICE text from the Work, provided
              that such additional attribution notices cannot be construed
              as modifying the License.
    
          You may add Your own copyright statement to Your modifications and
          may provide additional or different license terms and conditions
          for use, reproduction, or distribution of Your modifications, or
          for any such Derivative Works as a whole, provided Your use,
          reproduction, and distribution of the Work otherwise complies with
          the conditions stated in this License.
    
       5. Submission of Contributions. Unless You explicitly state otherwise,
          any Contribution intentionally submitted for inclusion in the Work
          by You to the Licensor shall be under the terms and conditions of
          this License, without any additional terms or conditions.
          Notwithstanding the above, nothing herein shall supersede or modify
          the terms of any separate license agreement you may have executed
          with Licensor regarding such Contributions.
    
       6. Trademarks. This License does not grant permission to use the trade
          names, trademarks, service marks, or product names of the Licensor,
          except as required for reasonable and customary use in describing the
          origin of the Work and reproducing the content of the NOTICE file.
    
       7. Disclaimer of Warranty. Unless required by applicable law or
          agreed to in writing, Licensor provides the Work (and each
          Contributor provides its Contributions) on an "AS IS" BASIS,
          WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
          implied, including, without limitation, any warranties or conditions
          of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
          PARTICULAR PURPOSE. You are solely responsible for determining the
          appropriateness of using or redistributing the Work and assume any
          risks associated with Your exercise of permissions under this License.
    
       8. Limitation of Liability. In no event and under no legal theory,
          whether in tort (including negligence), contract, or otherwise,
          unless required by applicable law (such as deliberate and grossly
          negligent acts) or agreed to in writing, shall any Contributor be
          liable to You for damages, including any direct, indirect, special,
          incidental, or consequential damages of any character arising as a
          result of this License or out of the use or inability to use the
          Work (including but not limited to damages for loss of goodwill,
          work stoppage, computer failure or malfunction, or any and all
          other commercial damages or losses), even if such Contributor
          has been advised of the possibility of such damages.
    
       9. Accepting Warranty or Additional Liability. While redistributing
          the Work or Derivative Works thereof, You may choose to offer,
          and charge a fee for, acceptance of support, warranty, indemnity,
          or other liability obligations and/or rights consistent with this
          License. However, in accepting such obligations, You may act only
          on Your own behalf and on Your sole responsibility, not on behalf
          of any other Contributor, and only if You agree to indemnify,
          defend, and hold each Contributor harmless for any liability
          incurred by, or claims asserted against, such Contributor by reason
          of your accepting any such warranty or additional liability.
    
       END OF TERMS AND CONDITIONS
       APPENDIX: How to apply the Apache License to your work.
    
          To apply the Apache License to your work, attach the following
          boilerplate notice, with the fields enclosed by brackets "[]"
          replaced with your own identifying information. (Don't include
          the brackets!)  The text should be enclosed in the appropriate
          comment syntax for the file format. We also recommend that a
          file or class name and description of purpose be included on the
          same "printed page" as the copyright notice for easier
          identification within third-party archives.
    
       Copyright [yyyy] [name of copyright owner]
    
       Licensed under the Apache License, Version 2.0 (the "License");
       you may not use this file except in compliance with the License.
       You may obtain a copy of the License at
    
           http://www.apache.org/licenses/LICENSE-2.0
    
       Unless required by applicable law or agreed to in writing, software
       distributed under the License is distributed on an "AS IS" BASIS,
       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       See the License for the specific language governing permissions and
       limitations under the License.
    

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →