Trao đổi về dự án
Trí Tuệ Nhân Tạo

Huấn luyện bộ phân loại GED giữa các site với FedAvg và FedProx

Năm site dùng cùng schema định tuyến nhưng có từ vựng và tần suất lớp khác nhau. Hướng dẫn này dùng trình mô phỏng cục bộ để so sánh mô hình theo site, FedAvg và FedProx với các chỉ số toàn cục và theo site.

Parcours pédagogique : comprendre le cas, construire, mesurer puis vérifier.

Năm site dùng cùng một schema định tuyến nhưng có từ vựng và tần suất lớp khác nhau. Một mô hình cục bộ có thể thiếu dữ liệu và phép gộp đồng đều có thể đánh trọng số quá cao cho site nhỏ. Chia sẻ trọng số tự nó không phải là bảo đảm về quyền riêng tư.

Một trình mô phỏng cục bộ so sánh mô hình theo site, FedAvg và FedProx, với chỉ số toàn cục và theo site. Các thông điệp trao đổi có thể kiểm tra; không tài liệu thật nào được dùng và không tuyên bố triển khai liên kết an toàn nào.

Tải dự án hoàn chỉnh, bản sửa đổi r002

Hành trình của hướng dẫn, từ trường hợp cụ thể đến bước kiểm chứng.

Các quần thể

Các site A/B/C/D/E có 200/300/400/500/600 tài liệu, tổng cộng 2000. Mỗi family có năm tài liệu. 400 family nằm trong một site và một split: 70% train, 15% validation, 15% test. Điều đó cho 1400/300/300 tài liệu, gồm 140/210/280/350/420 train theo site. Số lượng lớp được ghi lại và mọi lớp đều có mặt trong mỗi tập test.

Bốn lớp tổng hợp mô tả bảo trì, chất lượng, mua hàng và kỹ thuật. Phong cách và tỷ lệ thay đổi theo site. Các cặp fragment mẫu và định danh family không giao nhau, nhưng các mệnh đề ngữ nghĩa của lớp lặp lại giữa site/split. Kết quả không đo khả năng khái quát hóa sang GED thật hay các tác vụ chưa biết.

E5-small cố định tại 614241f622f53c4eeff9890bdc4f31cfecc418b3 mã hóa mỗi văn bản với query:, trung bình có mặt nạ và chuẩn hóa L2. Các vector 384 vẫn ở trong thư mục khách hàng. Đầu vào vượt 512 token bị từ chối. Không gold nào dùng để mã hóa; suy luận cuối không có label.

Mô hình cục bộ

Head CPU FP32 là 384→128, ReLU, rồi 4 logits. Nó không có BatchNorm hay buffer nguyên. Các client bắt đầu một vòng với cùng tham số toàn cục. Mỗi client thực hiện ba epoch SGD, learning_rate 0,1, momentum 0,9, batch 32; optimizer được khởi tạo lại ở mỗi vòng. Mỗi client giữ bộ sinh hoán vị của mình giữa các vòng.

FedAvg dùng cross-entropy cục bộ. FedProx thêm mu/2 somme(||w−wt||²), với mu 0,1 và wt đã detach. Trong mọi cập nhật cục bộ, penalty giữ điểm tham chiếu từ đầu vòng. Với mu bằng 0, cùng dữ liệu, batch và RNG, cập nhật FedAvg được tái tạo chính xác; kiểm thử thực sự huấn luyện head nhỏ để xác minh đồng nhất này.

Vai trò giới hạn của server

Một client gửi tham số, định danh, round_id, hash mô hình ban đầu và số ví dụ train duy nhất. Các byte là FP32 little-endian, mã hóa base64 JSON với dtype, shape, độ dài và SHA-256. Bộ giải mã từ chối dữ liệu không hữu hạn, khóa/kích thước/dtype sai và dấu vết sai. Nó không tải pickle nào.

Server gán trọng số nk/somme(nj) cho các người tham gia hiện diện. Trọng số không phụ thuộc số batch. Việc cộng dồn dùng FP64 theo thứ tự cố định rồi trở về FP32; định dạng trao đổi vẫn là FP32. Tham chiếu vô hướng dùng 1 và 3 với số lượng 1 và 3, cho 2,5. Nếu B vắng mặt, A nhận toàn bộ trọng số; không cập nhật rỗng nào thay thế một người vắng mặt.

Dịch vụ server chỉ mount exchange. Nó không đọc văn bản, feature hay label của client. Điều phối viên host vẫn đáng tin cậy; các client ảo dùng chung một máy có thể quản trị. Sự tách biệt chức năng này không bảo vệ trước quản trị viên và không phải aggregation an toàn hay quyền riêng tư vi sai. Cập nhật có thể chứa thông tin về dữ liệu; không cuộc tấn công hay bảo đảm quyền riêng tư nào được đo ở đây.

Giao thức và tiếp tục

Hai mươi vòng với năm site, ba epoch cục bộ mỗi vòng. FedAvg và FedProx có cùng tham số ban đầu, RNG hoán vị và lịch. Tham số cuối được cố định ở vòng 20; validation/test không chọn checkpoint. Các cập nhật stale, trùng lặp, không hữu hạn hoặc có sai số lượng bị từ chối. Phản hồi server đã ghi không bị ghi đè.

Luồng chạy dừng sau tám vòng, thực hiện riêng hai mươi vòng liên tục rồi tiếp tục mười hai vòng còn lại từ trạng thái 8 đã lưu. So sánh xét mô hình server, toàn bộ yêu cầu, trạng thái RNG client, lịch và loss cục bộ. Việc tiếp tục ở ranh giới vòng; nó không hứa khôi phục giữa một lần ghi hoặc mini-batch.

Năm tham chiếu cục bộ học sáu mươi epoch trên site riêng của chúng. Tham chiếu tập trung học sáu mươi epoch trên 1400 tài liệu hư cấu, ngoài kịch bản liên kết. Momentum của chúng được giữ trong sáu mươi epoch này; client liên kết khởi tạo lại nó mỗi vòng. Ngân sách lượt qua ví dụ trùng cho lịch đầy đủ, nhưng quỹ đạo và số cập nhật toàn cục không giống nhau. So sánh kiểm soát chính là FedAvg/FedProx.

Đọc chỉ số và chi phí

Với mỗi site, macro-F1 lấy trung bình bốn F1 theo lớp. Sau đó hiển thị trung bình không trọng số của năm site và trung bình của các macro-F1 có trọng số theo số lượng. Đây là hai cách gộp khác nhau; không cách nào thay việc đọc site tệ nhất. Khoảng tốt nhất−tệ nhất và ma trận nhầm lẫn vẫn hiển thị số lượng. Tham chiếu cục bộ được đánh giá bằng mô hình riêng của từng site.

Loss theo vòng là trung bình có trọng số của CE quan sát trong các quá trình huấn luyện cục bộ. Nó không phải loss của mô hình toàn cục được đánh giá trên tập kiểm soát. Byte tham số lên/xuống được tính từ tensor truyền đi; kích thước thật của yêu cầu JSON được tách riêng vì chúng gồm metadata, base64 và mô hình ban đầu. Trình mô phỏng này không đo thông lượng mạng. Thời gian mã hóa E5 tách biệt với thời gian của head và khởi chạy container.

Bài tập: năm lần E vắng mặt

Loại E ở các vòng 3, 6, 9, 12 và 15, trước mọi kết quả, cho cả FedAvg lẫn FedProx. Khởi động lại với cùng tham số và RNG ban đầu. Trọng số của người hiện diện được chuẩn hóa lại; RNG của client vắng mặt không dùng hoán vị. Dữ liệu của nó không được tái tạo bằng hạng proximal. So sánh điểm của E và bốn site khác, không yêu cầu một phương pháp phải thắng.

Bài tập giảm số lượt qua dữ liệu E cho cả hai phương pháp; vì thế ngân sách tổng khác lịch đầy đủ. So sánh FedAvg/FedProx vẫn dùng cùng lịch. Kết quả chính được giữ bằng SHA-256. Không bản gốc nào được truyền tới GED, không quyết định tài liệu nào được áp dụng.

Bước 01 — Tạo năm phân phối cục bộ không rò rỉ family

Non-IID và các phân vùng được nhóm

Lỗi: Cùng mẫu nghiệp vụ xuất hiện ở nhiều site và split

Khắc phục: Phân vùng family/mẫu trước khi tạo văn bản; kiểm tra hash.

Tệp src/ged_apprentissage_federe/data.py

import hashlib
from .config import save_json

SIZES = [200, 300, 400, 500, 600]
STYLES = [
    "Workshop note:",
    "Laboratory file:",
    "Procurement-facing register:",
    "Field operations memo:",
    "Engineering service report:",
]
CONTENTS = [
    [
        "Replace the damaged seal and restore pump operation.",
        "Lubricate the bearings and repair the actuator.",
        "Inspect the vibration source then restore the mechanical assembly.",
    ],
    [
        "Measure the sample against the tolerance and record conformity.",
        "Verify calibration evidence and audit the inspection results.",
        "Review the nonconformity record and confirm the measurement protocol.",
    ],
    [
        "Request a supplier quotation and place an equipment purchase order.",
        "Compare offers and record supplier delivery conditions.",
        "Prepare the replacement parts order and procurement approval.",
    ],
    [
        "Design a revised assembly and validate the prototype geometry.",
        "Document a new control algorithm and its engineering test plan.",
        "Revise the technical drawing and assess the design alternatives.",
    ],
]
OPEN = [
    "A recorded item",
    "The submitted note",
    "A service record",
    "The current memorandum",
    "This technical entry",
    "A pending file",
    "An operational message",
    "The reviewed page",
    "A local dispatch",
    "The archived request",
    "An incoming record",
    "A reference note",
    "A newly received item",
    "The working copy",
    "A documented request",
    "An indexed message",
    "The consolidated entry",
    "A dated report",
    "The shared note",
    "A formal record",
]
CLOSE = [
    "has supporting context",
    "retains the initial details",
    "keeps a local reference",
    "contains the required description",
    "preserves the source wording",
    "has a recorded owner",
    "includes the attached context",
    "keeps its revision history",
    "carries a local annotation",
    "remains a proposed assignment",
    "needs a routing suggestion",
    "is available for review",
    "has a traceable origin",
    "keeps the technical wording",
    "contains no routing label",
    "records the site terminology",
    "uses the local writing style",
    "preserves the original entry",
    "remains a fictitious example",
    "belongs to the demo inventory",
]


def prepare(config):
    rows = []
    gold = []
    templates = {}
    counts = {}
    family_global = 0
    for site_index, (site, size) in enumerate(zip(config["sites"], SIZES)):
        families = size // 5
        train = int(0.7 * families)
        validation = int(0.15 * families)
        site_rows = []
        site_gold = []
        for family in range(families):
            split = (
                "train"
                if family < train
                else "validation" if family < train + validation else "test"
            )
            template = OPEN[family_global // 20] + " " + CLOSE[family_global % 20] + "."
            template_id = f"F{family_global:03d}"
            templates[template_id] = {"site": site, "split": split, "text": template}
            for j in range(5):
                label = site_index % 4 if j < 3 else (family + site_index + j - 3) % 4
                identifier = f"{site}-{family:03d}-{j}"
                content = f"{STYLES[site_index]} {template} {CONTENTS[label][(family+j)%3]} Equipment ticket {site}{family:03d}{j}. Please preserve the original source."
                row = {
                    "document_id": identifier,
                    "site": site,
                    "family": template_id,
                    "split": split,
                    "text": content,
                    "content_sha256": hashlib.sha256(content.encode()).hexdigest(),
                }
                g = {
                    "document_id": identifier,
                    "site": site,
                    "family": template_id,
                    "split": split,
                    "label": label,
                }
                rows.append(row)
                gold.append(g)
                site_rows.append(row)
                site_gold.append(g)
            family_global += 1
        save_json(f"data/generated/clients/{site}/input.json", site_rows)
        save_json(
            f"data/generated/clients/{site}/train-labels.json",
            [g for g in site_gold if g["split"] == "train"],
        )
        counts[site] = {
            split: {
                "documents": sum(g["split"] == split for g in site_gold),
                "classes": [
                    sum(g["split"] == split and g["label"] == c for g in site_gold)
                    for c in range(4)
                ],
            }
            for split in ["train", "validation", "test"]
        }
    save_json("data/generated/input/pages.json", rows)
    save_json("data/generated/input/templates.json", templates)
    save_json("data/generated/labels/gold.json", gold)
    save_json(
        "data/generated/labels/train.json", [g for g in gold if g["split"] == "train"]
    )
    save_json(
        "data/generated/manifest.json",
        {
            "documents": 2000,
            "families": 400,
            "sites": counts,
            "train": 1400,
            "validation": 300,
            "test": 300,
            "simulation_only": True,
            "privacy_guarantee": False,
            "limits": "Class-specific semantic clauses repeat across families/sites; template pairs and document identities are disjoint. Site style and imbalance are deliberate.",
        },
    )

Bước 02 — Đo các mô hình cục bộ và feature chung

Encoder cố định và cùng feature

Lỗi: Server nhận feature cùng trọng số

Khắc phục: Giới hạn giao thức thông điệp ở tham số, số lượng và chỉ số đã quy định.

Tệp src/ged_apprentissage_federe/baseline.py

import torch
from torch.nn import functional as F
from .client import load_train
from .model import initial_model
from .weights import pack, fingerprint
from .config import save_json


def fit(x, y, ids, config):
    model = initial_model(config["seed"])
    optimizer = torch.optim.SGD(
        model.parameters(), lr=config["learning_rate"], momentum=config["momentum"]
    )
    g = torch.Generator().manual_seed(config["seed"])
    history = []
    steps = 0
    for epoch in range(config["baseline_epochs"]):
        order = torch.randperm(len(x), generator=g)
        loss_sum = 0.0
        for start in range(0, len(x), config["batch_size"]):
            selected = order[start : start + config["batch_size"]]
            optimizer.zero_grad(set_to_none=True)
            loss = F.cross_entropy(model(x[selected]), y[selected])
            loss.backward()
            optimizer.step()
            steps += 1
            loss_sum += float(loss.detach()) * len(selected)
        history.append(
            {"epoch": epoch + 1, "CE": loss_sum / len(x), "examples": len(x)}
        )
    weights = pack(model.state_dict())
    return {
        "parameters": weights,
        "sha256": fingerprint(weights),
        "train_ids": ids,
        "train_examples": len(x),
        "epochs": config["baseline_epochs"],
        "steps": steps,
        "history": history,
        "optimizer_reset": "once at baseline start; not federation rounds",
    }


def baseline(config):
    all_x = []
    all_y = []
    all_ids = []
    for site in config["sites"]:
        x, y, ids = load_train(site)
        save_json(f"outputs/baselines/local-{site}.json", fit(x, y, ids, config))
        all_x.append(x)
        all_y.append(y)
        all_ids.extend(ids)
    save_json(
        "outputs/baselines/central.json",
        fit(torch.cat(all_x), torch.cat(all_y), all_ids, config),
    )
    save_json(
        "outputs/baselines/NOTICE.json",
        {
            "centralization": "Separate reference on original fictitious data only, not a federated deployment",
            "privacy_guarantee": False,
            "baseline_budget": "60epochs, continuous SGD momentum; federated clients reset momentum per round",
        },
    )

Bước 03 — Viết phép gộp có trọng số có ý nghĩa

FedAvg và các tham số được đặt tên

Lỗi: Trung bình đồng đều dù số lượng rất khác nhau

Khắc phục: Dùng n_k duy nhất; công bố trung bình site tách biệt với trọng số huấn luyện.

Tệp src/ged_apprentissage_federe/server.py

"""Server reads public parameter states and updates only, never client features."""

import argparse, json
from pathlib import Path
import torch
from .model import initial_model
from .weights import pack, unpack, fingerprint


def aggregate(base, updates, round_id, participants, train_counts):
    if not participants or len(set(participants)) != len(participants):
        raise ValueError("Empty or duplicate participation")
    if len(updates) != len(participants) or {u["client"] for u in updates} != set(
        participants
    ):
        raise ValueError("Missing or duplicate client update")
    expected = initial_model().state_dict()
    old = unpack(base, expected)
    digest = fingerprint(base)
    decoded = {}
    for update in updates:
        client = update["client"]
        if update["round_id"] != round_id or update["base_sha256"] != digest:
            raise ValueError("Stale round or initial model")
        if (
            type(update["train_examples"]) is not int
            or update["train_examples"] != train_counts[client]
            or update["train_examples"] <= 0
        ):
            raise ValueError("Wrong unique training count")
        decoded[client] = unpack(update["parameters"], expected)
    total = sum(train_counts[c] for c in participants)
    coefficients = {c: train_counts[c] / total for c in sorted(participants)}
    # FP64 accumulation then one FP32 cast, order fixed; wire tensors stay FP32.
    state = {
        name: sum(
            decoded[c][name].double() * coefficients[c] for c in sorted(participants)
        ).float()
        for name in old
    }
    result = pack(state)
    return {
        "round_id": round_id,
        "parameters": result,
        "sha256": fingerprint(result),
        "participants": sorted(participants),
        "coefficients": coefficients,
        "unique_train_examples": total,
        "parameter_bytes": sum(v["bytes"] for v in result.values()),
        "server_reads_client_data": False,
    }


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--request", required=True)
    p.add_argument("--output", required=True)
    args = p.parse_args()
    request = json.loads(Path(args.request).read_text())
    output = Path(args.output)
    if output.exists():
        raise ValueError("Refuse duplicate committed server round")
    result = aggregate(
        request["base"],
        request["updates"],
        request["round_id"],
        request["participants"],
        request["train_counts"],
    )
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(json.dumps(result, sort_keys=True, allow_nan=False) + "\n")


if __name__ == "__main__":
    main()

Bước 04 — Thực hiện các vòng có và không có hạng proximal

Tối ưu cục bộ và FedProx

Lỗi: w_t giữ gradient và di chuyển trong penalty

Khắc phục: Clone/detach tham chiếu đầu vòng và kiểm tra nó bất biến.

Tệp src/ged_apprentissage_federe/client.py

import base64, math
from pathlib import Path
import torch
from torch.nn import functional as F
from .config import read_json, save_json
from .model import initial_model, proximal_loss
from .weights import pack, unpack, fingerprint

TRAIN_COUNTS = {"A": 140, "B": 210, "C": 280, "D": 350, "E": 420}


def generator_state(g):
    return base64.b64encode(g.get_state().numpy().tobytes()).decode()


def generator_from_state(state):
    g = torch.Generator(device="cpu")
    g.set_state(
        torch.tensor(list(base64.b64decode(state, validate=True)), dtype=torch.uint8)
    )
    return g


def load_train(site):
    rows = read_json(f"outputs/clients/{site}/features.json")
    labels = {
        r["document_id"]: r["label"]
        for r in read_json(f"data/generated/clients/{site}/train-labels.json")
    }
    selected = [r for r in rows if r["split"] == "train"]
    if (
        len(selected) != TRAIN_COUNTS[site]
        or {r["document_id"] for r in selected} != labels.keys()
    ):
        raise ValueError("Client training identity/count mismatch")
    return (
        torch.tensor([r["vector"] for r in selected], dtype=torch.float32),
        torch.tensor([labels[r["document_id"]] for r in selected]),
        [r["document_id"] for r in selected],
    )


def train_update(site, base, round_id, mu, state, config):
    x, y, ids = load_train(site)
    model = initial_model()
    anchor = unpack(base, model.state_dict())
    model.load_state_dict(anchor, strict=True)
    g = generator_from_state(state)
    optimizer = torch.optim.SGD(
        model.parameters(), lr=config["learning_rate"], momentum=config["momentum"]
    )
    epochs = []
    steps = 0
    for epoch in range(config["local_epochs"]):
        order = torch.randperm(len(x), generator=g)
        ce_total = prox_total = 0.0
        for start in range(0, len(x), config["batch_size"]):
            selected = order[start : start + config["batch_size"]]
            optimizer.zero_grad(set_to_none=True)
            ce = F.cross_entropy(model(x[selected]), y[selected])
            prox = proximal_loss(model, anchor, mu)
            loss = ce + prox
            if not torch.isfinite(loss):
                raise ValueError("Non-finite local loss")
            loss.backward()
            optimizer.step()
            steps += 1
            ce_total += float(ce.detach()) * len(selected)
            prox_total += float(prox.detach()) * len(selected)
        epochs.append(
            {
                "epoch": epoch + 1,
                "CE": ce_total / len(x),
                "proximal": prox_total / len(x),
                "objective": (ce_total + prox_total) / len(x),
                "examples": len(x),
            }
        )
    parameters = pack(model.state_dict())
    return {
        "client": site,
        "round_id": round_id,
        "base_sha256": fingerprint(base),
        "train_examples": len(ids),
        "train_ids": ids,
        "mu": mu,
        "parameters": parameters,
        "parameter_bytes": sum(v["bytes"] for v in parameters.values()),
        "steps": steps,
        "epochs": epochs,
        "sampler_before": state,
        "sampler_after": generator_state(g),
        "optimizer_reset": True,
        "device": "cpu",
    }


def initialize(config):
    base = pack(initial_model(config["seed"]).state_dict())
    states = {
        site: generator_state(torch.Generator().manual_seed(config["seed"] + 100 * i))
        for i, site in enumerate(config["sites"])
    }
    save_json(
        "outputs/shared/initial.json",
        {
            "parameters": base,
            "sha256": fingerprint(base),
            "sampler_states": states,
            "round": 0,
            "train_counts": TRAIN_COUNTS,
            "model": "384->128ReLU->4 FP32 no buffers",
        },
    )


def request_round(config, variant, mode, round_id, exercise=False):
    if variant not in ["fedavg", "fedprox"] or mode not in [
        "interrupt",
        "continuous",
        "resume",
        "exercise",
    ]:
        raise ValueError("Unknown run")
    initial = read_json("outputs/shared/initial.json")
    root = Path(f"outputs/exchange/{variant}/{mode}")
    root.mkdir(parents=True, exist_ok=True)
    target = root / f"round_{round_id:03d}.request.json"
    if target.exists():
        raise ValueError("Refuse duplicate client round")
    if round_id == 1:
        base = initial["parameters"]
        states = initial["sampler_states"]
    else:
        previous_mode = (
            "interrupt"
            if mode == "resume" and round_id == config["interrupt_round"] + 1
            else mode
        )
        previous = Path(f"outputs/exchange/{variant}/{previous_mode}")
        server = read_json(previous / f"round_{round_id-1:03d}.server.json")
        request = read_json(previous / f"round_{round_id-1:03d}.request.json")
        base = server["parameters"]
        states = request["sampler_states_after"]
        if server["round_id"] != round_id - 1:
            raise ValueError("Previous round mismatch")
    participants = list(config["sites"])
    if exercise and round_id in config["exercise_absent_rounds"]:
        participants.remove(config["exercise_absent_site"])
    mu = 0.0 if variant == "fedavg" else config["mu"]
    updates = [
        train_update(site, base, round_id, mu, states[site], config)
        for site in participants
    ]
    after = dict(states)
    for update in updates:
        after[update["client"]] = update["sampler_after"]
    save_json(
        target,
        {
            "base": base,
            "updates": updates,
            "round_id": round_id,
            "participants": participants,
            "train_counts": TRAIN_COUNTS,
            "sampler_states_before": states,
            "sampler_states_after": after,
            "variant": variant,
            "mode": mode,
            "exercise": exercise,
        },
    )


def verify_resume(config, variant):
    full = Path(f"outputs/exchange/{variant}/continuous")
    resume = Path(f"outputs/exchange/{variant}/resume")
    checks = []
    for i in range(config["interrupt_round"] + 1, config["rounds"] + 1):
        a = read_json(full / f"round_{i:03d}.server.json")
        b = read_json(resume / f"round_{i:03d}.server.json")
        ra = read_json(full / f"round_{i:03d}.request.json")
        rb = read_json(resume / f"round_{i:03d}.request.json")
        ra.pop("mode")
        rb.pop("mode")
        checks.append(a == b and ra == rb)
    if not all(checks):
        raise AssertionError("Federated full state resume mismatch")
    save_json(
        f"outputs/resume-{variant}.json",
        {
            "status": "EXACT",
            "rounds_checked": list(
                range(config["interrupt_round"] + 1, config["rounds"] + 1)
            ),
            "server_parameters": True,
            "client_sampler_states": True,
            "updates_losses_calendar": True,
            "optimizer_policy": "reset per client round, boundary resume only",
        },
    )

Bước 05 — Đọc người thắng và thua theo site

Tính không đồng nhất và đánh giá không gộp

Lỗi: Điểm của site lớn nhất che một suy giảm ở site nhỏ nhất

Khắc phục: Công bố hai cách gộp và hỗ trợ mà không ép một kết luận có lợi.

Tệp src/ged_apprentissage_federe/evaluate.py

from pathlib import Path
import torch
from .config import read_json, save_json, sha
from .weights import unpack, fingerprint
from .model import initial_model


def probabilities_from_logits(logits):
    """Export FP64 probabilities while keeping learned head/logits FP32."""
    if logits.dtype != torch.float32 or logits.ndim != 2 or logits.shape[1] != 4:
        raise ValueError("Expected a batch of four FP32 class logits")
    if not torch.isfinite(logits).all():
        raise ValueError("Nonfinite classification logits")
    return logits.to(torch.float64).softmax(-1)


def classification_metrics(rows, gold):
    confusion = [[0] * 4 for _ in range(4)]
    for row in rows:
        confusion[gold[row["document_id"]]][row["predicted"]] += 1
    classes = []
    for c in range(4):
        tp = confusion[c][c]
        fp = sum(confusion[r][c] for r in range(4) if r != c)
        fn = sum(confusion[c][j] for j in range(4) if j != c)
        classes.append(
            {
                "class": c,
                "support": sum(confusion[c]),
                "tp": tp,
                "fp": fp,
                "fn": fn,
                "F1": 2 * tp / (2 * tp + fp + fn) if 2 * tp + fp + fn else 0.0,
            }
        )
    return {
        "documents": len(rows),
        "macro_F1": sum(c["F1"] for c in classes) / 4,
        "accuracy": sum(confusion[i][i] for i in range(4)) / len(rows),
        "confusion": confusion,
        "per_class": classes,
    }


def predict(config, exercise=False):
    rows = []
    models = {}
    variants = (
        ["exercise-fedavg", "exercise-fedprox"]
        if exercise
        else ["local", "central", "fedavg", "fedprox"]
    )
    for variant in variants:
        for site in config["sites"]:
            if variant == "local":
                path = f"outputs/baselines/local-{site}.json"
            elif variant == "central":
                path = "outputs/baselines/central.json"
            else:
                method = variant.removeprefix("exercise-")
                mode = "exercise" if exercise else "resume"
                path = f'outputs/exchange/{method}/{mode}/round_{config["rounds"]:03d}.server.json'
            state = read_json(path)
            model = initial_model()
            model.load_state_dict(unpack(state["parameters"], model.state_dict()))
            model.eval()
            models[path] = state["sha256"]
            features = [
                f
                for f in read_json(f"outputs/clients/{site}/features.json")
                if f["split"] != "train"
            ]
            x = torch.tensor([f["vector"] for f in features], dtype=torch.float32)
            with torch.inference_mode():
                logits = model(x)
                probabilities = probabilities_from_logits(logits)
            for f, z, prob in zip(features, logits.tolist(), probabilities.tolist()):
                rows.append(
                    {
                        "document_id": f["document_id"],
                        "site": site,
                        "split": f["split"],
                        "variant": variant,
                        "logits": z,
                        "probabilities": prob,
                        "predicted": max(range(4), key=lambda c: prob[c]),
                        "model_path": path,
                        "model_sha256": state["sha256"],
                    }
                )
    save_json(
        "outputs/predictions-exercise.json" if exercise else "outputs/predictions.json",
        {
            "rows": rows,
            "models": models,
            "labels_visible": False,
            "device": "cpu",
            "logits_dtype": "float32",
            "probability_dtype": "float64",
        },
    )


def evaluate(config, exercise=False):
    rows = read_json(
        "outputs/predictions-exercise.json" if exercise else "outputs/predictions.json"
    )["rows"]
    gold = {
        g["document_id"]: g["label"]
        for g in read_json("data/generated/labels/gold.json")
    }
    result = {}
    for variant in sorted({r["variant"] for r in rows}):
        for split in ["validation", "test"]:
            per_site = {
                site: classification_metrics(
                    [
                        r
                        for r in rows
                        if r["variant"] == variant
                        and r["site"] == site
                        and r["split"] == split
                    ],
                    gold,
                )
                for site in config["sites"]
            }
            values = [m["macro_F1"] for m in per_site.values()]
            total = sum(m["documents"] for m in per_site.values())
            result[variant + "/" + split] = {
                "sites": per_site,
                "unweighted_site_macro_F1": sum(values) / 5,
                "size_weighted_site_macro_F1": sum(
                    m["documents"] * m["macro_F1"] for m in per_site.values()
                )
                / total,
                "best_minus_worst_site": max(values) - min(values),
                "documents": total,
            }
    communication = {}
    for variant in ["fedavg", "fedprox"]:
        mode = "exercise" if exercise else "continuous"
        rounds = []
        for i in range(1, config["rounds"] + 1):
            path = Path(f"outputs/exchange/{variant}/{mode}/round_{i:03d}.request.json")
            request = read_json(path)
            server = read_json(path.with_name(f"round_{i:03d}.server.json"))
            total = server["unique_train_examples"]
            weighted = (
                sum(
                    u["train_examples"]
                    * sum(e["CE"] for e in u["epochs"])
                    / len(u["epochs"])
                    for u in request["updates"]
                )
                / total
            )
            rounds.append(
                {
                    "round": i,
                    "participants": server["participants"],
                    "coefficients": server["coefficients"],
                    "local_steps": sum(u["steps"] for u in request["updates"]),
                    "uploaded_parameter_bytes": sum(
                        u["parameter_bytes"] for u in request["updates"]
                    ),
                    "downloaded_parameter_bytes": server["parameter_bytes"]
                    * len(request["updates"]),
                    "request_json_file_bytes": path.stat().st_size,
                    "mean_local_training_CE": weighted,
                    "CE_interpretation": "sample-weighted mean along local training trajectories, not held-out/global-model loss",
                }
            )
        communication[variant] = rounds
    save_json(
        "outputs/evaluation-exercise.json" if exercise else "outputs/evaluation.json",
        {
            "metrics": result,
            "communication": communication,
            "privacy_guarantee": False,
            "one_host_sequential_simulation": True,
            "seed": config["seed"],
        },
    )


def protect_exercise(config):
    save_json(
        "outputs/exercise-protected.json",
        {
            p: sha(p)
            for p in [
                "outputs/evaluation.json",
                "outputs/predictions.json",
                "outputs/resume-fedavg.json",
                "outputs/resume-fedprox.json",
            ]
        },
    )


def finish_exercise(config):
    protected = read_json("outputs/exercise-protected.json")
    if not all(sha(p) == v for p, v in protected.items()):
        raise AssertionError("Main outputs changed")
    main = read_json("outputs/evaluation.json")["metrics"]
    other = read_json("outputs/evaluation-exercise.json")["metrics"]
    deltas = {
        variant: {
            site: other["exercise-" + variant + "/test"]["sites"][site]["macro_F1"]
            - main[variant + "/test"]["sites"][site]["macro_F1"]
            for site in config["sites"]
        }
        for variant in ["fedavg", "fedprox"]
    }
    save_json(
        "outputs/exercise.json",
        {
            "absent_site": config["exercise_absent_site"],
            "absent_rounds": config["exercise_absent_rounds"],
            "site_macro_F1_deltas": deltas,
            "protected_sha256": protected,
            "main_unchanged": True,
            "privacy_guarantee": False,
        },
    )

Bước 06 — Mô phỏng sự tham gia không đều

Lịch client và tiếp tục

Lỗi: Mô phỏng được mô tả là GED phân tán an toàn

Khắc phục: Nhắc lại phạm vi cục bộ, thông điệp rõ và không có kiểm toán quyền riêng tư.

Tệp src/ged_apprentissage_federe/evaluate.py

from pathlib import Path
import torch
from .config import read_json, save_json, sha
from .weights import unpack, fingerprint
from .model import initial_model


def probabilities_from_logits(logits):
    """Export FP64 probabilities while keeping learned head/logits FP32."""
    if logits.dtype != torch.float32 or logits.ndim != 2 or logits.shape[1] != 4:
        raise ValueError("Expected a batch of four FP32 class logits")
    if not torch.isfinite(logits).all():
        raise ValueError("Nonfinite classification logits")
    return logits.to(torch.float64).softmax(-1)


def classification_metrics(rows, gold):
    confusion = [[0] * 4 for _ in range(4)]
    for row in rows:
        confusion[gold[row["document_id"]]][row["predicted"]] += 1
    classes = []
    for c in range(4):
        tp = confusion[c][c]
        fp = sum(confusion[r][c] for r in range(4) if r != c)
        fn = sum(confusion[c][j] for j in range(4) if j != c)
        classes.append(
            {
                "class": c,
                "support": sum(confusion[c]),
                "tp": tp,
                "fp": fp,
                "fn": fn,
                "F1": 2 * tp / (2 * tp + fp + fn) if 2 * tp + fp + fn else 0.0,
            }
        )
    return {
        "documents": len(rows),
        "macro_F1": sum(c["F1"] for c in classes) / 4,
        "accuracy": sum(confusion[i][i] for i in range(4)) / len(rows),
        "confusion": confusion,
        "per_class": classes,
    }


def predict(config, exercise=False):
    rows = []
    models = {}
    variants = (
        ["exercise-fedavg", "exercise-fedprox"]
        if exercise
        else ["local", "central", "fedavg", "fedprox"]
    )
    for variant in variants:
        for site in config["sites"]:
            if variant == "local":
                path = f"outputs/baselines/local-{site}.json"
            elif variant == "central":
                path = "outputs/baselines/central.json"
            else:
                method = variant.removeprefix("exercise-")
                mode = "exercise" if exercise else "resume"
                path = f'outputs/exchange/{method}/{mode}/round_{config["rounds"]:03d}.server.json'
            state = read_json(path)
            model = initial_model()
            model.load_state_dict(unpack(state["parameters"], model.state_dict()))
            model.eval()
            models[path] = state["sha256"]
            features = [
                f
                for f in read_json(f"outputs/clients/{site}/features.json")
                if f["split"] != "train"
            ]
            x = torch.tensor([f["vector"] for f in features], dtype=torch.float32)
            with torch.inference_mode():
                logits = model(x)
                probabilities = probabilities_from_logits(logits)
            for f, z, prob in zip(features, logits.tolist(), probabilities.tolist()):
                rows.append(
                    {
                        "document_id": f["document_id"],
                        "site": site,
                        "split": f["split"],
                        "variant": variant,
                        "logits": z,
                        "probabilities": prob,
                        "predicted": max(range(4), key=lambda c: prob[c]),
                        "model_path": path,
                        "model_sha256": state["sha256"],
                    }
                )
    save_json(
        "outputs/predictions-exercise.json" if exercise else "outputs/predictions.json",
        {
            "rows": rows,
            "models": models,
            "labels_visible": False,
            "device": "cpu",
            "logits_dtype": "float32",
            "probability_dtype": "float64",
        },
    )


def evaluate(config, exercise=False):
    rows = read_json(
        "outputs/predictions-exercise.json" if exercise else "outputs/predictions.json"
    )["rows"]
    gold = {
        g["document_id"]: g["label"]
        for g in read_json("data/generated/labels/gold.json")
    }
    result = {}
    for variant in sorted({r["variant"] for r in rows}):
        for split in ["validation", "test"]:
            per_site = {
                site: classification_metrics(
                    [
                        r
                        for r in rows
                        if r["variant"] == variant
                        and r["site"] == site
                        and r["split"] == split
                    ],
                    gold,
                )
                for site in config["sites"]
            }
            values = [m["macro_F1"] for m in per_site.values()]
            total = sum(m["documents"] for m in per_site.values())
            result[variant + "/" + split] = {
                "sites": per_site,
                "unweighted_site_macro_F1": sum(values) / 5,
                "size_weighted_site_macro_F1": sum(
                    m["documents"] * m["macro_F1"] for m in per_site.values()
                )
                / total,
                "best_minus_worst_site": max(values) - min(values),
                "documents": total,
            }
    communication = {}
    for variant in ["fedavg", "fedprox"]:
        mode = "exercise" if exercise else "continuous"
        rounds = []
        for i in range(1, config["rounds"] + 1):
            path = Path(f"outputs/exchange/{variant}/{mode}/round_{i:03d}.request.json")
            request = read_json(path)
            server = read_json(path.with_name(f"round_{i:03d}.server.json"))
            total = server["unique_train_examples"]
            weighted = (
                sum(
                    u["train_examples"]
                    * sum(e["CE"] for e in u["epochs"])
                    / len(u["epochs"])
                    for u in request["updates"]
                )
                / total
            )
            rounds.append(
                {
                    "round": i,
                    "participants": server["participants"],
                    "coefficients": server["coefficients"],
                    "local_steps": sum(u["steps"] for u in request["updates"]),
                    "uploaded_parameter_bytes": sum(
                        u["parameter_bytes"] for u in request["updates"]
                    ),
                    "downloaded_parameter_bytes": server["parameter_bytes"]
                    * len(request["updates"]),
                    "request_json_file_bytes": path.stat().st_size,
                    "mean_local_training_CE": weighted,
                    "CE_interpretation": "sample-weighted mean along local training trajectories, not held-out/global-model loss",
                }
            )
        communication[variant] = rounds
    save_json(
        "outputs/evaluation-exercise.json" if exercise else "outputs/evaluation.json",
        {
            "metrics": result,
            "communication": communication,
            "privacy_guarantee": False,
            "one_host_sequential_simulation": True,
            "seed": config["seed"],
        },
    )


def protect_exercise(config):
    save_json(
        "outputs/exercise-protected.json",
        {
            p: sha(p)
            for p in [
                "outputs/evaluation.json",
                "outputs/predictions.json",
                "outputs/resume-fedavg.json",
                "outputs/resume-fedprox.json",
            ]
        },
    )


def finish_exercise(config):
    protected = read_json("outputs/exercise-protected.json")
    if not all(sha(p) == v for p, v in protected.items()):
        raise AssertionError("Main outputs changed")
    main = read_json("outputs/evaluation.json")["metrics"]
    other = read_json("outputs/evaluation-exercise.json")["metrics"]
    deltas = {
        variant: {
            site: other["exercise-" + variant + "/test"]["sites"][site]["macro_F1"]
            - main[variant + "/test"]["sites"][site]["macro_F1"]
            for site in config["sites"]
        }
        for variant in ["fedavg", "fedprox"]
    }
    save_json(
        "outputs/exercise.json",
        {
            "absent_site": config["exercise_absent_site"],
            "absent_rounds": config["exercise_absent_rounds"],
            "site_macro_F1_deltas": deltas,
            "protected_sha256": protected,
            "main_unchanged": True,
            "privacy_guarantee": False,
        },
    )

Nguồn

McMahan và cộng sự, 2017 giới thiệu FedAvg. Li và cộng sự, FedProx nghiên cứu tối ưu liên kết không đồng nhất; bài thực hành này không tái tạo benchmark của họ. Deep Leakage from Gradients cho thấy cần thận trọng với nội dung cập nhật; không cuộc tấn công nào được thực hiện ở đây. E5 cố định.

Độ chính xác của các xác suất xuất

Head, tham số, quá trình học và logits của nó vẫn ở FP32. Softmax dùng để xuất xác suất được tính ở FP64 từ các logits này: so sánh độc lập dùng cùng độ chính xác. r001 quan sát sai lệch tối đa 1,80 × 10⁻⁷ giữa softmax CPU FP32 và tham chiếu FP64, cao hơn ngưỡng cố định 10⁻⁷. Ngưỡng này không thay đổi. Việc sửa biểu diễn số này không phải cải thiện phân loại hay thay đổi FedAvg/FedProx.

Tham chiếu kỹ thuật: triển khai CPU của softmax trong PyTorch 2.8.0. Đường FP32 có các phép làm tròn riêng, một phép giảm vector hóa và nhân với tổng nghịch đảo; nó không phải đánh giá bằng số học chính xác.

Kết quả quan sát được và giới hạn của thí nghiệm này

Bốn phương pháp — mô hình cục bộ, tập trung, FedAvg và FedProx — đều đạt macro-F1 test bằng 1 trên mỗi trong năm site. 300 tài liệu test được phân bổ 30/45/60/75/90 tài liệu. Việc E vắng mặt ở các vòng 3, 6, 9, 12 và 15 cho biến thiên macro-F1 bằng không với cả hai phương pháp liên kết trên năm site. Vì vậy biểu đồ chênh lệch vẫn ở không: không phải thiếu kết quả. Các mẫu ngữ nghĩa dùng chung làm kho này dễ; các điểm này không chứng minh lợi ích của liên kết, FedProx hay khả năng chịu lỗi thực.

Việc tiếp tục của hai phương pháp tái tạo chính xác tham số và trạng thái client của các nhánh liên tục. Mỗi head có 49 796 tham số FP32, tức 199 184 byte logic. Hai mươi vòng với năm client đại diện 19 918 400 byte gửi lên cho mỗi phương pháp; các số này không phải kích thước JSON hay thông lượng mạng. Bài thực hành mô phỏng các site tuần tự trên một máy: nó không cung cấp aggregation an toàn, quyền riêng tư vi sai hay bảo đảm quyền riêng tư.

Các mô hình cục bộ và tập trung giữ momentum trong 60 epoch; optimizer liên kết được khởi tạo lại ở mỗi vòng ba epoch. Do đó, các so sánh không chỉ thay đổi nơi đặt dữ liệu. Một seed duy nhất và kho hư cấu này không cho phép kết luận nghiệp vụ.

r002 giữ chính xác mô hình, logits FP32 và lớp của r001. Chỉ softmax xuất được tính ở FP64; ngưỡng kiểm toán độc lập 10⁻⁷ không đổi. Việc sửa số học này không cải thiện phân loại.

Quan sát trong quá trình phát triển

Các giá trị được tính lại từ đầu ra thô của lượt chạy này. Chúng không phải ngưỡng cải thiện; lượt chạy từ ZIP sẽ tính lại chúng trên cùng các tập dữ liệu.

{
  "status": "PASS",
  "documents": 2000,
  "train": 1400,
  "validation": 300,
  "test": 300,
  "sites": 5,
  "parameters": 49796,
  "rounds_per_method": 20,
  "resume": "EXACT",
  "privacy_guarantee": false,
  "test_metrics": {
    "central/test": {
      "sites": {
        "A": {
          "documents": 30,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              21,
              0,
              0,
              0
            ],
            [
              0,
              2,
              0,
              0
            ],
            [
              0,
              0,
              3,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 21,
              "tp": 21,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 2,
              "tp": 2,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 3,
              "tp": 3,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "B": {
          "documents": 45,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              5,
              0,
              0,
              0
            ],
            [
              0,
              32,
              0,
              0
            ],
            [
              0,
              0,
              4,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 5,
              "tp": 5,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 32,
              "tp": 32,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "C": {
          "documents": 60,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              6,
              0,
              0,
              0
            ],
            [
              0,
              6,
              0,
              0
            ],
            [
              0,
              0,
              42,
              0
            ],
            [
              0,
              0,
              0,
              6
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 42,
              "tp": 42,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "D": {
          "documents": 75,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              7,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              8,
              0
            ],
            [
              0,
              0,
              0,
              52
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 7,
              "tp": 7,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 52,
              "tp": 52,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "E": {
          "documents": 90,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              63,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              9,
              0
            ],
            [
              0,
              0,
              0,
              10
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 63,
              "tp": 63,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 9,
              "tp": 9,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 10,
              "tp": 10,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        }
      },
      "unweighted_site_macro_F1": 1.0,
      "size_weighted_site_macro_F1": 1.0,
      "best_minus_worst_site": 0.0,
      "documents": 300
    },
    "fedavg/test": {
      "sites": {
        "A": {
          "documents": 30,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              21,
              0,
              0,
              0
            ],
            [
              0,
              2,
              0,
              0
            ],
            [
              0,
              0,
              3,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 21,
              "tp": 21,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 2,
              "tp": 2,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 3,
              "tp": 3,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "B": {
          "documents": 45,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              5,
              0,
              0,
              0
            ],
            [
              0,
              32,
              0,
              0
            ],
            [
              0,
              0,
              4,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 5,
              "tp": 5,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 32,
              "tp": 32,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "C": {
          "documents": 60,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              6,
              0,
              0,
              0
            ],
            [
              0,
              6,
              0,
              0
            ],
            [
              0,
              0,
              42,
              0
            ],
            [
              0,
              0,
              0,
              6
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 42,
              "tp": 42,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "D": {
          "documents": 75,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              7,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              8,
              0
            ],
            [
              0,
              0,
              0,
              52
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 7,
              "tp": 7,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 52,
              "tp": 52,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "E": {
          "documents": 90,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              63,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              9,
              0
            ],
            [
              0,
              0,
              0,
              10
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 63,
              "tp": 63,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 9,
              "tp": 9,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 10,
              "tp": 10,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        }
      },
      "unweighted_site_macro_F1": 1.0,
      "size_weighted_site_macro_F1": 1.0,
      "best_minus_worst_site": 0.0,
      "documents": 300
    },
    "fedprox/test": {
      "sites": {
        "A": {
          "documents": 30,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              21,
              0,
              0,
              0
            ],
            [
              0,
              2,
              0,
              0
            ],
            [
              0,
              0,
              3,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 21,
              "tp": 21,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 2,
              "tp": 2,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 3,
              "tp": 3,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "B": {
          "documents": 45,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              5,
              0,
              0,
              0
            ],
            [
              0,
              32,
              0,
              0
            ],
            [
              0,
              0,
              4,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 5,
              "tp": 5,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 32,
              "tp": 32,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "C": {
          "documents": 60,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              6,
              0,
              0,
              0
            ],
            [
              0,
              6,
              0,
              0
            ],
            [
              0,
              0,
              42,
              0
            ],
            [
              0,
              0,
              0,
              6
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 42,
              "tp": 42,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "D": {
          "documents": 75,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              7,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              8,
              0
            ],
            [
              0,
              0,
              0,
              52
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 7,
              "tp": 7,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 52,
              "tp": 52,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "E": {
          "documents": 90,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              63,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              9,
              0
            ],
            [
              0,
              0,
              0,
              10
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 63,
              "tp": 63,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 9,
              "tp": 9,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 10,
              "tp": 10,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        }
      },
      "unweighted_site_macro_F1": 1.0,
      "size_weighted_site_macro_F1": 1.0,
      "best_minus_worst_site": 0.0,
      "documents": 300
    },
    "local/test": {
      "sites": {
        "A": {
          "documents": 30,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              21,
              0,
              0,
              0
            ],
            [
              0,
              2,
              0,
              0
            ],
            [
              0,
              0,
              3,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 21,
              "tp": 21,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 2,
              "tp": 2,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 3,
              "tp": 3,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "B": {
          "documents": 45,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              5,
              0,
              0,
              0
            ],
            [
              0,
              32,
              0,
              0
            ],
            [
              0,
              0,
              4,
              0
            ],
            [
              0,
              0,
              0,
              4
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 5,
              "tp": 5,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 32,
              "tp": 32,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 4,
              "tp": 4,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "C": {
          "documents": 60,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              6,
              0,
              0,
              0
            ],
            [
              0,
              6,
              0,
              0
            ],
            [
              0,
              0,
              42,
              0
            ],
            [
              0,
              0,
              0,
              6
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 42,
              "tp": 42,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 6,
              "tp": 6,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "D": {
          "documents": 75,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              7,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              8,
              0
            ],
            [
              0,
              0,
              0,
              52
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 7,
              "tp": 7,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 52,
              "tp": 52,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        },
        "E": {
          "documents": 90,
          "macro_F1": 1.0,
          "accuracy": 1.0,
          "confusion": [
            [
              63,
              0,
              0,
              0
            ],
            [
              0,
              8,
              0,
              0
            ],
            [
              0,
              0,
              9,
              0
            ],
            [
              0,
              0,
              0,
              10
            ]
          ],
          "per_class": [
            {
              "class": 0,
              "support": 63,
              "tp": 63,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 1,
              "support": 8,
              "tp": 8,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 2,
              "support": 9,
              "tp": 9,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            },
            {
              "class": 3,
              "support": 10,
              "tp": 10,
              "fp": 0,
              "fn": 0,
              "F1": 1.0
            }
          ]
        }
      },
      "unweighted_site_macro_F1": 1.0,
      "size_weighted_site_macro_F1": 1.0,
      "best_minus_worst_site": 0.0,
      "documents": 300
    }
  },
  "exercise_deltas": {
    "fedavg": {
      "A": 0.0,
      "B": 0.0,
      "C": 0.0,
      "D": 0.0,
      "E": 0.0
    },
    "fedprox": {
      "A": 0.0,
      "B": 0.0,
      "C": 0.0,
      "D": 0.0,
      "E": 0.0
    }
  }
}

Các kết quả quan sát được, phạm vi của chúng và thí nghiệm được đề xuất.

Chia sẻ bài viết