- Federated learning
- Surgical video AI
- Appendicitis grading
- Domain shift
- LoRA
- Challenge statistics
- PyTorch
A surgeon at a hospital near Dresden removes an inflamed appendix through three small incisions, and the laparoscope records every second. Those recordings could teach an AI system to judge how severe the inflammation is, a call that shapes antibiotics, drains and how long a patient stays. But the video cannot leave the hospital. Privacy law and ethics approvals keep it on local servers, and the three other hospitals in the region face the same wall.
Federated learning promises a way around it. The model travels to each hospital, learns there, and only its weights come back. The FedSurg challenge at MICCAI 2024 was the first international test of that promise on surgical video, using recordings of adults and children undergoing laparoscopic appendectomy for suspected appendicitis at four German hospitals. The results, now published in Medical Image Analysis, are sobering and unusually honest about what a small benchmark can and cannot prove.
Key points
- FedSurg asked teams to grade appendicitis on a six level scale from laparoscopic video, training across three hospitals without sharing data and testing on a fourth hospital they never saw.
- The one submission that modeled time across frames, a frozen video transformer, generalized best, with 23.03 percent macro F1 and 12.41 percent expected cost on the unseen hospital.
- Absolute performance is far from clinical use. Even a model trained on all data pooled centrally reached only 26.31 percent macro F1 on the unseen hospital.
- Several methods fell below simple chance references, and two of three submissions collapsed onto a single grade for almost every patient at the unseen center.
- With 70 test videos, only large differences are statistically resolvable, and the adaptation ranking changes under resampling. The paper says so directly.
Medical disclaimer. This article explains published research on artificial intelligence for surgical video. It is not medical advice, diagnosis or treatment, and none of the systems described are approved or suitable for clinical use. Decisions about appendicitis and its treatment belong to qualified clinicians. If you have symptoms or questions about your own health, consult a qualified healthcare professional.
Why surgical AI keeps running into a data wall
Surgical data science has made real progress on recognizing instruments, anatomy and phases of an operation. The harder and more clinically useful questions concern the patient. How advanced is the disease? Will this patient develop a complication? Those questions need many patients from many places, because a single hospital sees a narrow slice of cases, uses one brand of laparoscope, one lighting setup and one surgical culture.
The field knows its datasets are small. A systematic review of 188 surgical scene understanding studies by Carstens and colleagues, cited in the FedSurg paper, found a median of 65 videos and 40 patients per study, with 70.7 percent of studies relying on a single institution and 59.0 percent on one procedure, laparoscopic cholecystectomy. Pooling data across hospitals would fix much of this, but patient video is protected by regulations such as GDPR in Europe and HIPAA in the United States, and the paperwork alone can stall a multicenter project for years.
Federated learning, introduced by McMahan and colleagues in 2017, flips the direction of travel. Each hospital trains a copy of the model on its own data. A server collects the updated weights, averages them, and sends the new global model back out. In the standard Federated Averaging algorithm, the server combines client models in proportion to how much data each holds.
The raw video never leaves the hospital. What leaves is a set of numbers, and that trade has made federated learning a favorite idea for medical AI. Our earlier piece on sequential versus parallel federated training covers the theory of how heterogeneous clients pull a shared model in different directions. FedSurg is a rare chance to see that theory collide with real surgical video.
What FedSurg actually asked
The challenge ran as part of the Endoscopic Vision challenge series at MICCAI 2024 in Marrakesh, organized by teams from the National Center for Tumor Diseases in Dresden, TUD Dresden University of Technology and Purdue University. The paper, Federated Learning for surgical vision in appendicitis classification, results of the FedSurg EndoVis 2024 challenge, is led by Max Kirchner with Stefanie Speidel as last author, and includes all participating teams as coauthors.
The data
The dataset is a preliminary subset of Appendix300, a multicenter collection of laparoscopic appendectomy recordings described by Kolbinger and colleagues. FedSurg used 223 recordings from four German hospitals, spanning university and community settings, including a university pediatric surgery department in Dresden and hospitals in Dresden, Radeberg and Ravensburg. From each video, up to 200 frames were extracted at two frames per second from a 100 second window around the moment the operating surgeon marked the appendix as fully visible, before dissection began.
Each recording carries an inflammation grade from 0 to 5 assigned by the operating surgeon, following the laparoscopic grading system of Gomes and colleagues. Grade 0 is a normal appendix, grade 1 shows redness and swelling, grade 2 fibrin, grade 3 any necrosis, grade 4 a regional abscess or peritonitis, and grade 5 generalized peritonitis. Finer subgrades 3A and 3B, and 4A and 4B, were merged.
| Center | Training videos | Testing videos | Total |
|---|---|---|---|
| Center 1 | 40 | 10 | 50 |
| Center 2 | 33 | 9 | 42 |
| Center 3 | 80 | 22 | 102 |
| Center 4 (held out) | 0 | 29 | 29 |
| Total | 153 | 70 | 223 |
Table 1 of the paper. Center 4 contributes no training data and is used only to test generalization.
The grade distribution is lopsided and differs by center. Center 3 alone contributed 80 of the 153 training videos, and mid level grades dominate everywhere. Participants saw only a public quarter of the training data. The organizers ran final training on the full data inside a secure federated setup, executing each team’s Docker container on a cluster with up to eight NVIDIA V100 GPUs, so that no team could quietly centralize the data.
Two tasks, two kinds of success
Task 1 tested generalization. Teams trained a global model across Centers 1 to 3, and it was evaluated on Center 4, a hospital it had never seen. Task 2 tested adaptation. The same global model was fine tuned separately at each training center and then tested on that center’s own held out videos. These two goals pull against each other, and much of the paper is about that tension.
How scores were measured
Two metrics decided the ranking. Macro F1 averages the F1 score over all six grades. The second, Expected Cost, respects the fact that grades are ordered. Calling a grade 4 patient grade 3 is a smaller mistake than calling them grade 0, so each error is weighted by distance.
Rankings on each metric were averaged within each task, and the two tasks were averaged into a final rank. Stability was checked with 10,000 bootstrap resamples of the test set, and after peer review the organizers added paired permutation tests over the actual test cases, which is where some of the most important findings come from.
Three teams, three strategies
Twenty four teams registered, four submitted, and three submissions ran to completion. Each took a different path.
Team Santhi, a single participant, used a Video Vision Transformer pretrained on Kinetics-400 with the backbone frozen and only the classification layer trained. It sampled 32 frames per video, two thirds from a narrow window around the center of the clip and the rest spread across the video with a bias toward the center. Federated Averaging combined the clients over 20 rounds of five local epochs. Readers who want the architecture in more depth can find it in our review of video vision transformers.
Team Elbflorenz from NCT Dresden used EndoViT, a vision transformer pretrained on the Endo700k collection of endoscopic images, again frozen with a trained head. It classified 100 evenly spaced frames individually and took a majority vote. On the clients it used adaptive federated sharpness aware minimization, FedSAM, which seeks flat minima that tend to generalize across heterogeneous data, and on the server it used FedOpt, which applies an adaptive optimizer to the averaged update.
Team Camma from IHU Strasbourg took a metric learning route. A Siamese ResNet50 mapped frames into a 256 dimensional normalized embedding trained with a cosine triplet loss, so that videos of the same grade cluster together. Classification compared test embeddings with class prototypes or with every stored example. Switchable normalization was meant to absorb differences between centers, and Federated Median, which takes the coordinate wise median of client updates, provided robustness to outliers.
| Team | Backbone | Prediction | Aggregation | Rounds × local |
|---|---|---|---|---|
| Santhi | Frozen ViViT (Kinetics-400) | Video level, 32 frames | FedAvg | 20 × 5 |
| Elbflorenz | Frozen EndoViT (Endo700k) | Frame level, majority vote | FedSAM and FedOpt | 50 × 2 |
| Camma | Siamese ResNet50 | Embedding and prototypes | FedMedian | 10 × 5 |
Condensed from Table 2 of the paper.
The organizers added four baselines for context, outside the official ranking. A centralized model built on SurgTempoNet, a ConvNeXt Base backbone pretrained on ImageNet-22k feeding a single layer LSTM with 160 hidden units, trained on pooled data. A Swarm Learning version of the same model trained peer to peer without a central server. And two parameter efficient baselines using LoRA, low rank adapters of rank 8 on the attention projections of a frozen backbone, one on frame level EndoViT and one on a video ViViT with 16 frame clips. Each LoRA baseline trained fewer than a million parameters.
Generalization to an unseen hospital
| Method | Task 1 EC ↓ | Task 1 F1 ↑ | Task 2 avg EC ↓ | Task 2 avg F1 ↑ |
|---|---|---|---|---|
| Santhi (submission) | 12.41 | 23.03 | 21.03 | 13.40 |
| Elbflorenz (submission) | 24.14 | 7.83 | 21.42 | 12.21 |
| Camma (submission) | 57.24 | 4.76 | 22.23 | 18.79 |
| Centralized baseline | 10.34 | 26.31 | 19.82 | 23.51 |
| Swarm Learning baseline | 13.10 | 19.00 | 20.41 | 17.77 |
| EndoViT+LoRA | 17.24 | 9.24 | 20.13 | 8.70 |
| ViViT+LoRA | 22.76 | 15.93 | 17.60 | 21.47 |
| Constant grade 3 (reference) | 13.79 | 9.17 | 23.54 | 6.26 |
| Random by training prevalence (reference) | 25.52 | 14.30 | 27.85 | 14.65 |
Percent values from Tables 3, 4, 6 and H.25 of the paper. Baselines and references are contextual and were not part of the challenge ranking.
Team Santhi generalized best by a wide margin, and the bootstrap put it in first place in 99.23 percent of resamples on F1 and 99.79 percent on EC. Its confusion matrix on Center 4 hugs the diagonal, with most errors landing on adjacent grades. It correctly identified 8 of 11 grade 3 cases and struggled with the adjacent grades 2 and 4 and with rare grades.
The other two submissions collapsed. Camma’s global model predicted grade 0 for 26 of the 29 patients at the new hospital, even though the test set there contained no grade 0 cases at all. Elbflorenz predicted grade 2 for 28 of 29. Both learned something useful at the training centers, as Task 2 shows, but whatever they learned did not survive the move to a hospital with different cameras, lighting and patients.
Paired permutation tests over the 29 cases confirm the picture. Camma and Elbflorenz each differ significantly from Santhi on both metrics, and Camma differs from Elbflorenz on EC, but not on F1, where the p value is 0.592. The ordering of the top three methods overall, Santhi, the centralized baseline and the Swarm baseline, is not resolved by any test.
The approach that looked at how the scene changes over time, rather than judging individual frames, was the only submission that transferred to a new hospital. Frame level voting and frame based embeddings both fell apart under domain shift.
The chance references change the story
Here is where the paper does something most challenge reports skip. Absolute scores on a six class problem are hard to read, so the authors built four reference classifiers from training labels alone. TrivialGlobal always predicts the most common pooled training grade, which is grade 3. TrivialLocal predicts each center’s own most common training grade. RandomUniform guesses all six grades with equal probability. RandomPrior guesses according to training prevalence.
Against these floors, the results look harder. On Task 1, always answering grade 3 gives an expected cost of 13.79 percent. Two of the three submissions fail to beat that, and so do both parameter efficient baselines. Camma’s 57.24 percent is worse than uniform random guessing at 33.55 percent. At Center 1, always predicting that center’s usual grade gives 20.00 percent expected cost, better than five of the seven methods, including the centralized baseline. At Center 3, no submission beats the constant classifier’s 18.18 percent. Across all test sets and metrics, the authors count twenty cases in which a method falls below a chance reference, covering eleven distinct method, test set and metric combinations.
Macro F1 has its own quirk. A grade that never appears in a test set contributes zero to the average, but the denominator stays at six. Center 4 has no grade 0 patients, so the best possible macro F1 there is 83.33 percent, not 100. The authors argue that Expected Cost, which respects ordinal distance and is not distorted by missing classes, should be treated as the primary indicator. That is a sensible recommendation for any ordinal grading task with small, imbalanced test sets.
We rebuilt these references from the label counts printed in the paper and reproduced every constant reference value exactly, along with the Task 1 scores of all three submissions from their published confusion matrices. The code is at the end of this article. That kind of check is only possible because the authors published enough detail, which deserves credit on its own.
Adapting to each hospital
Task 2 fine tuned the global model at each training center. Absolute scores here are not comparable with Task 1, because each center’s test set has a different grade mix.
| Team | Center 1 EC / F1 | Center 2 EC / F1 | Center 3 EC / F1 | Average EC / F1 |
|---|---|---|---|---|
| Camma | 28.00 / 3.33 | 17.78 / 30.28 | 20.91 / 22.76 | 22.23 / 18.79 |
| Elbflorenz | 18.00 / 17.42 | 24.44 / 3.70 | 21.82 / 15.51 | 21.42 / 12.21 |
| Santhi | 24.00 / 14.81 | 20.00 / 13.33 | 19.09 / 12.04 | 21.03 / 13.40 |
Table 4 of the paper, percent values.
No team won everywhere. Santhi had the most consistent results and the lowest average expected cost, but was never best on F1 at a single center. Camma swung from 30.28 percent F1 at Center 2 to 3.33 percent at Center 1. Elbflorenz’s models at Centers 1 and 2 put 7 of 10 and 8 of 9 cases into grade 2.
The statistics are blunt about what this means. No Task 2 comparison reaches significance under the permutation test on either metric. The EC ranking is close to uniform, with first place probabilities of 0.40, 0.36 and 0.24 for Santhi, Elbflorenz and Camma. Following the criterion of Maier-Hein and colleagues for interpreting challenge rankings, the authors state that the Task 2 EC ordering carries essentially no information about which submission is better. Santhi and the Swarm baseline differ by 0.62 percentage points in average expected cost with a permutation p value of 0.844. Among the baselines, ViViT+LoRA reached the lowest Task 2 average EC of any method at 17.60 percent, and 29.57 percent F1 at Center 3, close to the centralized model’s 32.67 percent at that center.
“We consider the reporting of weak absolute performance on a clinically meaningful task, measured against chance and constant references, more useful to realistic expectations of clinical AI performance than stronger numbers on tasks of limited clinical relevance would be.”Kirchner and colleagues, Medical Image Analysis, 2027
Why temporal modeling mattered
The paper’s clearest finding is that video level models transferred better than frame level ones. The authors’ explanation is persuasive. Within a single laparoscopic video, individual frames vary enormously. The camera moves, smoke drifts, instruments block the view, and the appendix appears at different angles and distances. Any single frame is an unreliable witness to the patient’s grade. A model that reads 32 frames jointly can pick up on what persists across the clip and ignore the noise. A frame classifier with majority voting inherits every frame’s confusion, and a frame embedding tied to the look of the training hospitals has nothing to anchor it when the look changes.
The parameter efficient baselines offer a partial controlled test. EndoViT+LoRA and ViViT+LoRA share the aggregation, optimizer, schedule, rounds and head, differing only in the frozen backbone. The temporal version reached 15.93 percent Task 1 F1 against 9.24 percent for the frame version. But the two backbones also differ in pretraining, endoscopic images versus general video, so the gap cannot be pinned on temporal modeling alone. And on Task 1 expected cost the ordering flips, 17.24 percent for EndoViT+LoRA against 22.76 percent for ViViT+LoRA, because collapsing to a central grade is cheap under a linear ordinal cost. The authors are careful to call temporal modeling the dominant factor on the submissions, where the evidence is consistent, rather than a proven cause.
That the domain specific EndoViT backbone did worse than general video pretraining is interesting in its own right. It suggests the fine grained visual distinctions in this task are not what current endoscopic pretraining captures. For comparison, our coverage of ST3-Former for gastrointestinal endoscopy shows how much work goes into simply cleaning up endoscopic frames before any grading is attempted.
Is federation to blame for the low scores?
This is the question a hospital administrator would ask first. The centralized baseline gives a partial answer. Even with all data pooled on one server, it reached only 26.31 percent macro F1 on the unseen center. The task itself is hard, regardless of how the model is trained.
The authors compare the centralized and Swarm baselines against chance references more closely. Under macro F1, the centralized model sits 2.14 standard deviations above the prevalence matched chance level, a value reached by 3.19 percent of random draws. The Swarm model sits 0.80 standard deviations above it and is matched or exceeded by 18.25 percent of draws. Under expected cost, the two improve on the constant classifier by 3.45 and 0.69 percentage points. The two baselines are not statistically separable from one another at this sample size, so the paper makes no claim of a measured decentralization penalty. What it does say is that the federated result sits much closer to the chance threshold.
The authors point to data scale as the binding constraint. With 153 training videos split across three centers, each federated node has too few examples, especially of rare grades, to learn robust representations. A related study cited in the paper, Saldanha and colleagues in NEJM AI in 2026, applied a similar decentralized pipeline to a larger multinational appendectomy cohort and reports performance comparable to centralized training. Different cohort, center count and task definition, so it is not a controlled comparison, but it points the same way.
Communication cost
Federated learning trades data transfer for model transfer, and models are not small. For server based federation, total traffic is twice the number of clients times the number of rounds times the model size, the factor of two covering upload and download. Swarm Learning replaces rounds with synchronization events and counts only the other peers.
| Setup | Parameters | Model size | Sync events | Total traffic |
|---|---|---|---|---|
| Swarm Learning baseline | 89.35M | 340.85 MB | 100 | 133.14 GB |
| Elbflorenz | 87.21M | 332.69 MB | 50 | 97.47 GB |
| Santhi | 88.65M | 338.18 MB | 20 | 39.63 GB |
| Camma | 24.09M | 91.88 MB | 10 | 5.38 GB |
Table 8 of the paper. Sizes use binary units, so 39.63 GB here means 39.63 × 230 bytes.
Camma’s compact model and sparse synchronization cut traffic by 95.9 percent relative to the Swarm baseline. The LoRA baselines point to an even bigger saving that was not used. They still transmitted full model snapshots, but their trainable adapters are only about 940 to 969 thousand parameters, roughly 3.8 MB. Sending only the adapters would reduce per round transmission to under 2 percent of the costs in the table. For hospitals with limited network bandwidth, that is not a small detail.
Clinical translation gap
It is worth being plain about the distance between these results and a hospital. The best macro F1 on an unseen hospital, 26.31 percent for a centralized model, is far from anything a surgeon could rely on. The paper itself states that absolute performance remains far from clinical viability and that no clinically acceptable threshold for intraoperative appendicitis grading has been established, so there is no target against which to declare success.
The labels themselves cap what any model can achieve. On the complete Appendix300 dataset, an independent second reading of all recordings reached a weighted Cohen’s kappa of 0.614 against the original annotations. Exact agreement ranged from 0.618 at grade 0 to none at all at grade 5, and 9 percent of cases showed disagreements of three or more grades and needed a third reader. These figures come from the full dataset, not the challenge labels, but they show that expert surgeons do not agree on this grading as often as one might assume. A model cannot reliably beat the consistency of its own training labels.
The failure modes also carry clinical weight. The authors note that collapse toward a low grade systematically undercalls severity, while collapse toward a high grade overcalls it. Neither shows up in an aggregate F1 score, and both could mislead a clinician in different directions. Camma’s global model, predicting grade 0 for patients who in fact had fibrin, necrosis or abscesses, is exactly the failure a safety review would need to catch before any deployment.
Regulatory questions have not been reached yet. A system like this would count as software influencing clinical decisions, and would need prospective validation on patients and hospitals it has never seen, along with evidence of consistent performance across demographic groups. The challenge’s use of four German hospitals, all within one national health system, leaves those questions open. The ethics side was handled properly for research use. The study was prospectively reviewed by the TUD Dresden institutional review board and registered in the German Clinical Trials Register under DRKS00030874.
Clinical and methodological limitations
Sample size. The evaluation rests on 153 training and 70 test videos. The unseen center has 29 cases, and the Task 2 test sets have 10, 9 and 22. Bootstrap confidence intervals on individual scores span roughly ten percentage points. Permutation tests only detect large differences, on the order of 11 to 47 percentage points in expected cost. The paper asks readers to treat reported numbers as indicative, not as reliable estimates of method quality.
Dataset bias. All four centers are in Germany, and the grade distribution favors mid level inflammation, with few mild or extreme cases. Center 3 contributed more than half the training videos, so global models lean toward its data. Grade 5 appears in only three training videos in total. A model trained on this mix learns little about the rarest and most dangerous presentations.
Generalization. Generalization was measured to exactly one unseen hospital. Whether the ranking would hold at a second or third new center, or outside Germany, is unknown.
Simulated federation. The federated setup was simulated on a single cluster. Real deployments face network latency, nodes dropping out and different hardware at each site, none of which were tested.
Confounded designs. The three submissions differed in backbone, frame sampling, loss and aggregation all at once, so the contribution of the federated strategy cannot be separated from the architecture. The baselines are post hoc organizer analyses, not competing entries.
Participation. Only three complete submissions arrived from 24 registrations. The authors attribute this to the burden of building a containerized end to end federated pipeline, the rarity of combined expertise in federated optimization and surgical video, and a public data subset that was too small for conventional development. They also report candidly that one Center 1 test case was initially skipped for the submissions by an evaluation harness that filtered inputs by expected frame count. Predictions were recovered for all three teams, every reported result uses the full 70 test cases, and the ranking did not change.
FedSurg does not show that federated learning works for surgical grading, and it does not show that it fails. It shows what can be resolved with 70 test videos, which is only large differences, and it gives future challenges a protocol, references and failure modes to build on.
Where we would push back, gently
The paper already criticizes itself thoroughly, which leaves less to add than usual. Two points are worth making.
The Task 2 protocol fine tunes on small local data without any constraint, then attributes collapse partly to that design choice. Since the organizers ran every submission, it would have been informative to add one controlled comparison, the same model with and without imbalance aware local objectives such as a weighted or focal loss. The paper recommends such methods for future work, and a single experiment here would have tested the recommendation rather than stating it.
The headline recommendation, structured personalized federated learning with a frozen temporal backbone plus parameter efficient local heads, rests on one submission and one baseline pair whose backbones differ in two ways at once. The authors call it a hypothesis for future evaluation rather than a validated recommendation, and that framing is right. It should be read as a reasonable next experiment, not as settled advice.
Who should care
For researchers planning federated medical AI studies, FedSurg is a useful template. Its protocol separates generalization and adaptation, reports constant and chance references alongside every score, uses expected cost for ordinal tasks, and pairs bootstrap stability with actual hypothesis tests over test cases. Other medical challenges could adopt the same reporting habits. Our analysis of KongNet and the MONKEY pathology challenge shows how much depends on how a leaderboard is read.
For hospitals, the practical lessons are about data and design. Temporal models with frozen backbones transferred best. Personalization needs protection against small, imbalanced local datasets. Sending only small adapter weights could make federation cheap on bandwidth. And none of it matters until datasets grow well beyond a few hundred videos. For more on privacy and security in clinical AI systems, see our review of healthcare IoT security and privacy, and for broader surgical AI, the magnetic navigation system for compression anastomosis. The federated learning and AI privacy archive collects related work in one place.
Reference implementation in PyTorch
FedSurg is a challenge report, so there is no single proposed network to reproduce. The code below implements the benchmark itself, so that the protocol, the three submitted strategies and the organizer baselines can be rerun on data laid out the same way. It contains macro F1 with a fixed denominator of six and expected cost with linear ordinal weights, the four reference classifiers, bootstrap rank stability with shared resampling indices, paired permutation tests for both tasks, and the communication cost formulas for server based and swarm federation.
It also includes four model heads on a frozen frame encoder. A temporal transformer mirrors Team Santhi’s video level approach, and its LoRA variant mirrors the ViViT+LoRA baseline. A frame classifier with majority voting mirrors Team Elbflorenz, a cosine triplet embedding with prototype inference and switchable normalization mirrors Team Camma, and an LSTM head mirrors the SurgTempoNet baseline. The federated layer implements FedAvg, FedMedian, server side FedOpt and client side adaptive SAM, together with the frame sampling strategies, federated feature standardization computed from summed client statistics, and the Task 1 and Task 2 protocols.
The smoke test does three things. It reproduces the paper’s constant reference values and Task 1 scores from the label counts and confusion matrices printed in the paper. It reruns bootstrap and permutation statistics on those reconstructed cases. Because the pairing of predictions across methods within a grade is not published, those p values are approximate. Finally, it runs every strategy end to end on synthetic videos that follow the paper’s per center label distribution. The frozen encoder is a small random CNN standing in for ViViT, EndoViT or ResNet50, so swap in a pretrained backbone for real use. The organizers’ own evaluation code is available on GitLab.
"""
FedSurg toolkit. Federated learning for patient level appendicitis grading from surgical video.
Reference PyTorch implementation of the benchmark described in
Kirchner et al. Federated Learning for surgical vision in appendicitis classification,
Results of the FedSurg EndoVis 2024 challenge. Medical Image Analysis 115 (2027) 104290.
https://doi.org/10.1016/j.media.2026.104290
The paper is a challenge report, so there is no single proposed network. This file implements
the whole benchmark so that the protocol, the three submitted strategies and the organizer
baselines can be rerun on any data laid out the same way.
1. Metrics macro F1 with a fixed denominator of six grades (Eq. 1-2),
Expected Cost with linear ordinal weights (Eq. 3-4)
2. References TrivialGlobal, TrivialLocal, RandomUniform, RandomPrior (Sec. 3.4)
3. Statistics 10,000 replicate bootstrap rank stability with shared indices,
paired permutation tests over test cases (Sec. 2.4.1)
4. Communication Eq. 5-7 for server based FL and peer to peer swarm learning
5. Models a frozen frame encoder (stand in for ViViT, EndoViT, ResNet-50)
with four heads
TemporalHead video level transformer over sampled frames (Team Santhi)
FrameHead per frame classifier plus majority vote (Team Elbflorenz)
MetricHead cosine triplet embedding plus prototypes (Team Camma)
LSTMHead frame features into one LSTM layer (SurgTempoNet baseline)
plus LoRA adapters for the parameter efficient baselines
6. Federation FedAvg, FedMedian, FedOpt (server Adam) and client side FedSAM
7. Tasks Task 1 generalization to held out Center 4,
Task 2 adaptation by fine tuning on each training center
Honest notes
* The real backbones (ViViT on Kinetics-400, EndoViT on Endo700k, ResNet-50) are replaced by
a small frozen CNN so the file runs on a CPU. Swap `FrameEncoder` for a pretrained model.
* The smoke test reproduces the paper's reference classifier values and Task 1 scores exactly
from label counts and confusion matrices printed in the paper, then runs the federated
pipeline end to end on synthetic videos that follow the paper's label distribution.
* Loss weights, the frame window width and FedSAM rho are not all reported; values marked
"our choice" are ours.
"""
import copy
import math
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
C = 6 # inflammation grades 0 to 5
# ---------------------------------------------------------------------------
# 1. Metrics
# ---------------------------------------------------------------------------
def confusion(y_true, y_pred, c=C):
m = np.zeros((c, c), dtype=np.int64)
np.add.at(m, (np.asarray(y_true), np.asarray(y_pred)), 1)
return m
def macro_f1(y_true, y_pred, c=C):
"""Eq. 1-2. Grades absent from both truth and prediction score zero but still count in the
denominator of six, so the ceiling drops below 100 percent when a grade is missing."""
m = confusion(y_true, y_pred, c)
tp = np.diag(m).astype(float)
fp, fn = m.sum(0) - tp, m.sum(1) - tp
denom = 2 * tp + fp + fn
f1 = np.divide(2 * tp, denom, out=np.zeros(c), where=denom > 0)
return f1.mean()
def expected_cost(y_true, y_pred, c=C):
"""Eq. 3-4. Linear ordinal cost |i - j| / (C - 1), averaged over cases."""
y_true, y_pred = np.asarray(y_true), np.asarray(y_pred)
return (np.abs(y_true - y_pred) / (c - 1)).mean()
def f1_ceiling(y_true, c=C):
return len(np.unique(y_true)) / c
# ---------------------------------------------------------------------------
# 2. Reference classifiers (use training labels only)
# ---------------------------------------------------------------------------
def mode(labels):
return int(np.bincount(labels, minlength=C).argmax())
def trivial_constant(y_true, grade):
pred = np.full(len(y_true), grade)
return macro_f1(y_true, pred), expected_cost(y_true, pred)
def random_reference(y_true, probs, draws=10000, seed=0):
"""RandomUniform (probs = 1/6 each) or RandomPrior (probs = training prevalence).
Returns the simulation mean and 95 percent percentile interval for F1 and EC."""
rng = np.random.default_rng(seed)
f1s, ecs = np.empty(draws), np.empty(draws)
for d in range(draws):
pred = rng.choice(C, size=len(y_true), p=probs)
f1s[d], ecs[d] = macro_f1(y_true, pred), expected_cost(y_true, pred)
q = lambda a: (a.mean(), np.percentile(a, 2.5), np.percentile(a, 97.5))
return q(f1s), q(ecs)
# ---------------------------------------------------------------------------
# 3. Bootstrap rank stability and paired permutation tests
# ---------------------------------------------------------------------------
def bootstrap_ranks(y_true, preds: dict, metric="f1", iters=10000, seed=0):
"""Shared resampling indices for all methods, predictions aligned by case.
Returns rank frequency per method and pairwise win probabilities."""
rng = np.random.default_rng(seed)
names = list(preds)
n, k = len(y_true), len(names)
fn = macro_f1 if metric == "f1" else expected_cost
higher_better = metric == "f1"
ranks = np.zeros((k, k))
wins = np.zeros((k, k))
y_true = np.asarray(y_true)
for _ in range(iters):
idx = rng.integers(0, n, n)
vals = np.array([fn(y_true[idx], np.asarray(preds[m])[idx]) for m in names])
order = np.argsort(-vals if higher_better else vals, kind="stable")
for r, j in enumerate(order):
ranks[j, r] += 1
better = vals[:, None] > vals[None] if higher_better else vals[:, None] < vals[None]
wins += better
return names, ranks / iters, wins / iters
def paired_permutation(y_true, pred_a, pred_b, metric="f1", perms=10000, seed=0):
"""Two sided test. Each case's two predictions are swapped with probability 0.5."""
rng = np.random.default_rng(seed)
fn = macro_f1 if metric == "f1" else expected_cost
y, a, b = map(np.asarray, (y_true, pred_a, pred_b))
obs = fn(y, a) - fn(y, b)
count = 0
for _ in range(perms):
swap = rng.random(len(y)) < 0.5
pa, pb = np.where(swap, b, a), np.where(swap, a, b)
count += abs(fn(y, pa) - fn(y, pb)) >= abs(obs) - 1e-12
return obs, max(count, 1) / perms
def paired_permutation_task2(centers, metric="ec", perms=10000, seed=0):
"""Task 2 version. `centers` is a list of (y_true, pred_a, pred_b) per center. The swap is
done within each center and the per center metrics are averaged with equal weight."""
rng = np.random.default_rng(seed)
fn = macro_f1 if metric == "f1" else expected_cost
avg = lambda triples: np.mean([fn(y, a) - fn(y, b) for y, a, b in triples])
data = [tuple(map(np.asarray, t)) for t in centers]
obs = avg(data)
count = 0
for _ in range(perms):
perm = []
for y, a, b in data:
s = rng.random(len(y)) < 0.5
perm.append((y, np.where(s, b, a), np.where(s, a, b)))
count += abs(avg(perm)) >= abs(obs) - 1e-12
return obs, max(count, 1) / perms
# ---------------------------------------------------------------------------
# 4. Communication cost (Eq. 5-7)
# ---------------------------------------------------------------------------
def fl_cost(bytes_per_model, clients, rounds):
return 2 * clients * rounds * bytes_per_model
def swarm_sync_events(epochs, batches_per_center, f_sync):
return sum(epochs * b // f_sync for b in batches_per_center) + 1
def swarm_cost(bytes_per_model, clients, sync_events):
return 2 * (clients - 1) * sync_events * bytes_per_model
# ---------------------------------------------------------------------------
# 5. Models
# ---------------------------------------------------------------------------
class FrameEncoder(nn.Module):
"""Frozen stand in for a pretrained image or video backbone. Maps (B, T, 3, H, W) to (B, T, D)."""
def __init__(self, dim=64):
super().__init__()
self.net = nn.Sequential(nn.Conv2d(3, 32, 3, 2, 1), nn.GELU(), nn.Conv2d(32, 32, 3, 1, 1), nn.GELU(),
nn.AdaptiveAvgPool2d(4), nn.Flatten(), nn.Linear(32 * 16, dim))
self.requires_grad_(False)
self.dim = dim
def forward(self, x):
b, t = x.shape[:2]
x = (x.flatten(0, 1) - 0.5) / 0.25 # ImageNet style input normalization
return self.net(x).view(b, t, -1)
class LoRALinear(nn.Module):
"""y = W x + (alpha / r) B A x with W frozen. Rank 8, alpha 16 in the paper's PEFT baselines."""
def __init__(self, base: nn.Linear, r=8, alpha=16):
super().__init__()
self.base = base
self.base.requires_grad_(False)
self.A = nn.Parameter(torch.randn(r, base.in_features) / math.sqrt(base.in_features))
self.B = nn.Parameter(torch.zeros(base.out_features, r))
self.scale = alpha / r
def forward(self, x):
return self.base(x) + self.scale * (x @ self.A.T @ self.B.T)
class LoRAAttention(nn.Module):
"""Multihead self attention with LoRA on the query, key, value and output projections."""
def __init__(self, dim, heads=4, lora=False, r=8, alpha=16):
super().__init__()
self.h = heads
mk = lambda: nn.Linear(dim, dim)
self.q, self.k, self.v, self.o = mk(), mk(), mk(), mk()
if lora:
self.q, self.k, self.v, self.o = (LoRALinear(m, r, alpha) for m in (self.q, self.k, self.v, self.o))
def forward(self, x):
b, t, d = x.shape
sh = lambda z: z.view(b, t, self.h, d // self.h).transpose(1, 2)
a = F.scaled_dot_product_attention(sh(self.q(x)), sh(self.k(x)), sh(self.v(x)))
return self.o(a.transpose(1, 2).reshape(b, t, d))
class TemporalHead(nn.Module):
"""Video level classification over sampled frames (Team Santhi's ViViT idea; LoRA variant = ViViT+LoRA)."""
def __init__(self, dim=64, layers=2, lora=False, max_t=256):
super().__init__()
self.cls = nn.Parameter(torch.zeros(1, 1, dim))
self.pos = nn.Parameter(torch.randn(1, max_t + 1, dim) * 0.02)
self.blocks = nn.ModuleList()
for _ in range(layers):
self.blocks.append(nn.ModuleDict(dict(n1=nn.LayerNorm(dim), att=LoRAAttention(dim, lora=lora), n2=nn.LayerNorm(dim),
mlp=nn.Sequential(nn.Linear(dim, 2 * dim), nn.GELU(), nn.Linear(2 * dim, dim)))))
if lora: # freeze everything in the temporal encoder except LoRA factors
for blk in self.blocks:
for name, p in blk.named_parameters():
p.requires_grad_(name.endswith(".A") or name.endswith(".B"))
self.cls.requires_grad_(False)
self.pos.requires_grad_(False)
self.head = nn.Sequential(nn.LayerNorm(dim), nn.Linear(dim, dim), nn.GELU(), nn.Linear(dim, dim), nn.GELU(), nn.Linear(dim, C))
def forward(self, f):
x = torch.cat([self.cls.expand(len(f), -1, -1), f], 1) + self.pos[:, : f.shape[1] + 1]
for blk in self.blocks:
x = x + blk["att"](blk["n1"](x))
x = x + blk["mlp"](blk["n2"](x))
return self.head(x[:, 0])
class FrameHead(nn.Module):
"""Per frame classifier; the video label is a majority vote, ties broken by mean confidence
(Team Elbflorenz). With `lora` it becomes the EndoViT+LoRA style frame level baseline."""
def __init__(self, dim=64):
super().__init__()
self.head = nn.Sequential(nn.LayerNorm(dim), nn.Linear(dim, dim), nn.GELU(), nn.Linear(dim, C))
def forward(self, f): # frame logits (B, T, C)
return self.head(f)
@torch.no_grad()
def vote(self, f):
logits = self.forward(f)
probs = logits.softmax(-1)
votes = F.one_hot(logits.argmax(-1), C).sum(1).float() # (B, C)
top = votes == votes.max(-1, keepdim=True).values
conf = probs.mean(1).masked_fill(~top, -1) # tie break by average confidence
return conf.argmax(-1)
class SwitchNorm1d(nn.Module):
"""Switchable normalization for feature vectors, softmax gated mix of batch and layer statistics."""
def __init__(self, dim, eps=1e-5):
super().__init__()
self.w = nn.Parameter(torch.ones(dim))
self.b = nn.Parameter(torch.zeros(dim))
self.mix_mean = nn.Parameter(torch.ones(2))
self.mix_var = nn.Parameter(torch.ones(2))
self.eps = eps
def forward(self, x):
mb, vb = x.mean(0, keepdim=True), x.var(0, unbiased=False, keepdim=True) if len(x) > 1 else torch.ones_like(x[:1])
ml, vl = x.mean(1, keepdim=True), x.var(1, unbiased=False, keepdim=True)
wm, wv = self.mix_mean.softmax(0), self.mix_var.softmax(0)
mean, var = wm[0] * mb + wm[1] * ml, wv[0] * vb + wv[1] * vl
return (x - mean) / torch.sqrt(var + self.eps) * self.w + self.b
class MetricHead(nn.Module):
"""Siamese embedding into a 256 dimensional L2 normalized space trained with a cosine triplet
margin loss; inference compares to class prototypes or to all support embeddings (Team Camma)."""
def __init__(self, dim=64, emb=256):
super().__init__()
self.proj = nn.Sequential(nn.Linear(dim, emb), SwitchNorm1d(emb), nn.GELU(), nn.Linear(emb, emb))
def forward(self, f): # f (B, T, D) of selected frames
return F.normalize(self.proj(f.mean(1)), dim=-1)
@staticmethod
def triplet_loss(z, y, margin=0.2):
sim = z @ z.T
same = y[:, None] == y[None]
eye = torch.eye(len(y), dtype=torch.bool)
pos = sim.masked_fill(~same | eye, 2).min(1).values # hardest positive
neg = sim.masked_fill(same, -2).max(1).values # hardest negative
valid = (same & ~eye).any(1) & (~same).any(1)
if valid.sum() == 0:
return z.sum() * 0
return F.relu(neg - pos + margin)[valid].mean()
@torch.no_grad()
def predict(self, z, z_support, y_support, mode="prototype"):
if mode == "prototype":
protos = torch.stack([F.normalize(z_support[y_support == c].mean(0), dim=0) if (y_support == c).any()
else torch.full((z.shape[1],), float("nan")) for c in range(C)])
sim = z @ protos.nan_to_num(-10).T
sim[:, torch.isnan(protos[:, 0])] = -10
return sim.argmax(-1)
sims = z @ z_support.T # per sample, mean similarity per class
scores = torch.stack([sims[:, y_support == c].mean(1) if (y_support == c).any() else torch.full((len(z),), -10.0)
for c in range(C)], 1)
return scores.argmax(-1)
class LSTMHead(nn.Module):
"""SurgTempoNet style, frame features into a single LSTM layer with hidden size 160."""
def __init__(self, dim=64, hidden=160):
super().__init__()
self.lstm = nn.LSTM(dim, hidden, batch_first=True)
self.fc = nn.Linear(hidden, C)
def forward(self, f):
out, _ = self.lstm(f)
return self.fc(out[:, -1])
# ---------------------------------------------------------------------------
# 6. Frame sampling
# ---------------------------------------------------------------------------
def sample_frames_santhi(n_total, k=32, center=100, half_window=16, p_center=0.6, rng=None):
"""Two thirds from a narrow window around the centre frame, the rest from the whole video
with probability 0.6 of landing near the centre. Window width is our choice."""
rng = rng or np.random.default_rng()
center = min(center, n_total // 2)
lo, hi = max(0, center - half_window), min(n_total, center + half_window)
k_win = (2 * k) // 3
idx = list(rng.choice(np.arange(lo, hi), size=k_win, replace=hi - lo < k_win))
for _ in range(k - k_win):
if rng.random() < p_center:
idx.append(int(rng.integers(max(0, center - 2 * half_window), min(n_total, center + 2 * half_window))))
else:
idx.append(int(rng.integers(0, n_total)))
return np.sort(np.array(idx))
def federated_standardizer(client_feats):
"""Per dimension mean and std from summed client statistics (count, sum, sum of squares), so no
raw features leave a center. Returns a function that standardizes (B, T, D) features."""
n = sum(f.shape[0] * f.shape[1] for f in client_feats)
s1 = sum(f.sum((0, 1)) for f in client_feats)
s2 = sum((f ** 2).sum((0, 1)) for f in client_feats)
mu = s1 / n
sd = (s2 / n - mu ** 2).clamp(min=1e-12).sqrt()
return lambda f: (f - mu) / sd
def sample_frames_equidistant(n_total, k=100):
return np.linspace(0, n_total - 1, min(k, n_total)).round().astype(int)
# ---------------------------------------------------------------------------
# 7. Federated strategies
# ---------------------------------------------------------------------------
def trainable_state(model):
return {n: p.detach().clone() for n, p in model.named_parameters() if p.requires_grad}
def load_trainable(model, state):
with torch.no_grad():
for n, p in model.named_parameters():
if n in state:
p.copy_(state[n])
def fedavg(states, weights):
w = torch.tensor(weights, dtype=torch.float32) / sum(weights)
return {k: sum(wi * s[k] for wi, s in zip(w, states)) for k in states[0]}
def fedmedian(states, weights=None):
return {k: torch.stack([s[k] for s in states]).median(0).values for k in states[0]}
class FedOptServer:
"""Server side Adam on the pseudo gradient (global minus average client model), Reddi et al."""
def __init__(self, lr=1e-2, b1=0.9, b2=0.99, eps=1e-3):
self.lr, self.b1, self.b2, self.eps = lr, b1, b2, eps
self.m, self.v = None, None
def __call__(self, global_state, states, weights):
avg = fedavg(states, weights)
delta = {k: avg[k] - global_state[k] for k in avg}
if self.m is None:
self.m = {k: torch.zeros_like(d) for k, d in delta.items()}
self.v = {k: torch.zeros_like(d) for k, d in delta.items()}
out = {}
for k, d in delta.items():
self.m[k] = self.b1 * self.m[k] + (1 - self.b1) * d
self.v[k] = self.b2 * self.v[k] + (1 - self.b2) * d * d
out[k] = global_state[k] + self.lr * self.m[k] / (self.v[k].sqrt() + self.eps)
return out
def sam_step(model, loss_fn, opt, rho=0.05, adaptive=True):
"""One (A)SAM update. Perturb weights toward higher loss, take the gradient there, restore, step."""
loss = loss_fn()
opt.zero_grad()
loss.backward()
params = [p for p in model.parameters() if p.requires_grad and p.grad is not None]
scale = [(p.abs() if adaptive else torch.ones_like(p)) for p in params]
norm = torch.norm(torch.stack([(s * p.grad).norm() for s, p in zip(scale, params)])) + 1e-12
eps = []
with torch.no_grad():
for s, p in zip(scale, params):
e = rho * s * s * p.grad / norm if adaptive else rho * p.grad / norm
p.add_(e)
eps.append(e)
opt.zero_grad()
loss_fn().backward()
with torch.no_grad():
for p, e in zip(params, eps):
p.sub_(e)
opt.step()
return loss.item()
# ---------------------------------------------------------------------------
# 8. Wrapping heads into trainable clients
# ---------------------------------------------------------------------------
def class_weights(labels):
counts = np.bincount(labels, minlength=C).astype(float)
w = np.where(counts > 0, counts.sum() / np.maximum(counts, 1), 0.0)
return torch.tensor(w / w[w > 0].mean(), dtype=torch.float32)
class Method:
"""Bundles an encoder, a head and how to train and predict. kind in
{'temporal', 'temporal_lora', 'frame', 'frame_lora', 'metric', 'lstm'}."""
def __init__(self, kind, dim=64, seed=0):
torch.manual_seed(seed)
self.kind = kind
self.encoder = FrameEncoder(dim)
if kind.startswith("temporal"):
self.head = TemporalHead(dim, lora=kind.endswith("lora"))
elif kind.startswith("frame"):
self.head = FrameHead(dim)
elif kind == "metric":
self.head = MetricHead(dim)
else:
self.head = LSTMHead(dim)
self.support = None
def features(self, videos):
with torch.no_grad():
return self.encoder(videos)
def loss(self, f, y, cw):
if self.kind.startswith("frame"):
logits = self.head(f)
return F.cross_entropy(logits.flatten(0, 1), y.repeat_interleave(f.shape[1]), weight=cw)
if self.kind == "metric":
return MetricHead.triplet_loss(self.head(f), y)
return F.cross_entropy(self.head(f), y, weight=cw)
@torch.no_grad()
def predict(self, f):
if self.kind.startswith("frame"):
return self.head.vote(f)
if self.kind == "metric":
zs, ys = self.support
return self.head.predict(self.head(f), zs, ys)
return self.head(f).argmax(-1)
@torch.no_grad()
def set_support(self, f, y):
self.support = (self.head(f), torch.as_tensor(y))
def local_train(method, f, y, epochs, lr, bs, use_sam=False, weighted=True):
params = [p for p in method.head.parameters() if p.requires_grad]
opt = torch.optim.SGD(params, lr=lr, momentum=0.9) if use_sam else torch.optim.Adam(params, lr=lr)
cw = class_weights(y.numpy()) if weighted else None
method.head.train()
for _ in range(epochs):
perm = torch.randperm(len(y))
for i in range(0, len(y), bs):
idx = perm[i:i + bs]
fn = lambda: method.loss(f[idx], y[idx], cw)
if use_sam:
sam_step(method.head, fn, opt)
else:
opt.zero_grad(); fn().backward(); opt.step()
method.head.eval()
def federated_train(kind, clients, rounds, local_epochs, lr, bs, aggregator="fedavg", use_sam=False, seed=0):
"""clients is a list of (features, labels). Returns the global Method."""
global_m = Method(kind, seed=seed)
gstate = trainable_state(global_m.head)
server = FedOptServer() if aggregator == "fedopt" else None
for _ in range(rounds):
states, sizes = [], []
for f, y in clients:
m = copy.deepcopy(global_m)
load_trainable(m.head, gstate)
local_train(m, f, y, local_epochs, lr, bs, use_sam)
states.append(trainable_state(m.head))
sizes.append(len(y))
if aggregator == "fedavg":
gstate = fedavg(states, sizes)
elif aggregator == "fedmedian":
gstate = fedmedian(states)
else:
gstate = server(gstate, states, sizes)
load_trainable(global_m.head, gstate)
if kind == "metric":
global_m.set_support(torch.cat([f for f, _ in clients]), torch.cat([y for _, y in clients]))
return global_m
def task1(global_m, f_test, y_test):
pred = global_m.predict(f_test).numpy()
return pred, macro_f1(y_test, pred), expected_cost(y_test, pred)
def task2(global_m, clients, tests, epochs, lr, bs, use_sam=False):
"""Fine tune a copy of the global model on each center, test on that center, average metrics."""
out = []
for (f, y), (ft, yt) in zip(clients, tests):
m = copy.deepcopy(global_m)
local_train(m, f, y, epochs, lr, bs, use_sam)
if m.kind == "metric":
m.set_support(f, y)
pred = m.predict(ft).numpy()
out.append((pred, macro_f1(yt, pred), expected_cost(yt, pred)))
return out, np.mean([o[1] for o in out]), np.mean([o[2] for o in out])
# ---------------------------------------------------------------------------
# 9. Smoke test
# ---------------------------------------------------------------------------
# Label counts per grade 0..5, read from Fig. 2 of the paper
TRAIN_COUNTS = {1: [9, 5, 17, 3, 6, 0], 2: [0, 7, 3, 10, 12, 1], 3: [0, 13, 13, 36, 16, 2], 4: [0] * 6}
TEST_COUNTS = {1: [2, 1, 4, 1, 2, 0], 2: [1, 2, 1, 2, 3, 0], 3: [1, 4, 3, 9, 4, 1], 4: [0, 1, 5, 11, 11, 1]}
# Task 1 confusion matrices on Center 4, rows true grade, columns predicted grade (Fig. 4)
CM_TASK1 = {
"Santhi": [[0] * 6, [0, 0, 0, 1, 0, 0], [0, 1, 1, 3, 0, 0], [0, 1, 2, 8, 0, 0], [0, 0, 1, 5, 5, 0], [0, 0, 0, 0, 1, 0]],
"Camma": [[0] * 6, [1, 0, 0, 0, 0, 0], [5, 0, 0, 0, 0, 0], [10, 0, 0, 0, 1, 0], [9, 0, 0, 0, 2, 0], [1, 0, 0, 0, 0, 0]],
"Elbflorenz": [[0] * 6, [0, 0, 1, 0, 0, 0], [0, 0, 5, 0, 0, 0], [0, 0, 11, 0, 0, 0], [0, 0, 10, 0, 1, 0], [0, 0, 1, 0, 0, 0]],
}
def labels_from_counts(counts):
return np.repeat(np.arange(C), counts)
def cases_from_cm(cm):
"""Expand a confusion matrix into aligned (true, pred) case lists, true grade ascending."""
y, p = [], []
for i, row in enumerate(cm):
for j, n in enumerate(row):
y += [i] * n
p += [j] * n
return np.array(y), np.array(p)
def synthetic_videos(labels, center, n_frames=40, size=16, seed=0):
"""Grade shows up as a reddish, textured patch that is strongest near the middle frames.
Each center has its own color cast and brightness, and Center 4 differs most."""
g = torch.Generator().manual_seed(seed + 97 * center)
cast = {1: (0.00, 0.00), 2: (0.06, -0.04), 3: (-0.05, 0.05), 4: (0.12, -0.10)}[center]
vids = []
for y in labels:
base = 0.35 + 0.05 * torch.randn(n_frames, 3, size, size, generator=g)
t = torch.linspace(-1, 1, n_frames)
strength = torch.exp(-4 * t ** 2)[:, None, None]
patch = (0.06 * y + 0.02 * torch.randn(1, generator=g)) * strength
base[:, 0, 4:12, 4:12] += patch
base[:, 1, 4:12, 4:12] -= 0.5 * patch
base[:, 0] += cast[0]
base += cast[1]
vids.append(base.clamp(0, 1))
return torch.stack(vids)
if __name__ == "__main__":
np.set_printoptions(precision=4, suppress=True)
# (a) Reference classifiers from the published label counts (compare with Table H.25)
print("== Reference classifiers, Task 1 Center 4 ==")
y4 = labels_from_counts(TEST_COUNTS[4])
pooled = np.concatenate([labels_from_counts(TRAIN_COUNTS[c]) for c in (1, 2, 3)])
g_mode = mode(pooled)
f1, ec = trivial_constant(y4, g_mode)
print(f"TrivialGlobal (grade {g_mode}) F1 {100 * f1:.2f} EC {100 * ec:.2f} paper 9.17 / 13.79")
prior = np.bincount(pooled, minlength=C) / len(pooled)
(fu, fl, fh), (eu, el, eh) = random_reference(y4, np.full(C, 1 / C), draws=10000)
print(f"RandomUniform F1 {100 * fu:.2f} [{100 * fl:.1f} to {100 * fh:.1f}] EC {100 * eu:.2f} paper 11.91 / 33.55")
(fu, fl, fh), (eu, el, eh) = random_reference(y4, prior, draws=10000)
print(f"RandomPrior F1 {100 * fu:.2f} [{100 * fl:.1f} to {100 * fh:.1f}] EC {100 * eu:.2f} paper 14.30 / 25.52")
print(f"macro F1 ceiling on Center 4 (grade 0 absent): {100 * f1_ceiling(y4):.2f}")
for c in (1, 2, 3):
yc = labels_from_counts(TEST_COUNTS[c])
lm = mode(labels_from_counts(TRAIN_COUNTS[c]))
f1, ec = trivial_constant(yc, lm)
print(f"TrivialLocal Center {c} (grade {lm}) F1 {100 * f1:.2f} EC {100 * ec:.2f}")
# (b) Task 1 scores recomputed from the published confusion matrices (compare with Table 3)
print("\n== Task 1 scores from Fig. 4 confusion matrices ==")
preds = {}
for team, cm in CM_TASK1.items():
y, p = cases_from_cm(cm)
assert (y == y4).all()
preds[team] = p
print(f"{team:11s} F1 {100 * macro_f1(y, p):.2f} EC {100 * expected_cost(y, p):.2f}")
print("paper Santhi 23.03 / 12.41 Camma 4.76 / 57.24 Elbflorenz 7.83 / 24.14")
# (c) Bootstrap rank stability and permutation tests on those reconstructed cases.
# Predictions are aligned by true grade only, so cross method pairing within a grade is arbitrary.
names, rk, wins = bootstrap_ranks(y4, preds, "ec", iters=2000)
print("\nBootstrap EC rank frequencies (rank 1, 2, 3):")
for n, r in zip(names, rk):
print(f" {n:11s} {r}")
for a, b in [("Camma", "Santhi"), ("Elbflorenz", "Santhi"), ("Camma", "Elbflorenz")]:
for met in ("ec", "f1"):
obs, p = paired_permutation(y4, preds[a], preds[b], met, perms=5000)
print(f" permutation {a} vs {b} {met.upper():2s} diff {100 * obs:+6.2f} pp p = {p:.4f}")
# (d) Communication costs (compare with Table 8)
print("\n== Communication cost ==")
for team, nbytes, rounds in [("Santhi", 354_604_056, 20), ("Elbflorenz", 348_852_152, 50), ("Camma", 96_344_056, 10)]:
print(f"{team:11s} {fl_cost(nbytes, 3, rounds) / 2**30:8.2f} GiB")
print(f"{'SL baseline':11s} {swarm_cost(357_405_880, 3, 100) / 2**30:8.2f} GiB (the paper labels these binary units GB)")
lora_bytes = 969_000 * 4
print(f"LoRA only exchange for ViViT+LoRA over 25 rounds {fl_cost(lora_bytes, 3, 25) / 2**30:.3f} GiB, "
f"{100 * lora_bytes / 354_604_056:.2f} percent of a full model per round")
# (e) End to end federated run on synthetic videos with the paper's label distribution
print("\n== Synthetic federated run ==")
torch.manual_seed(0)
train = {c: (synthetic_videos(labels_from_counts(TRAIN_COUNTS[c]), c, seed=1), labels_from_counts(TRAIN_COUNTS[c])) for c in (1, 2, 3)}
test = {c: (synthetic_videos(labels_from_counts(TEST_COUNTS[c]), c, seed=2), labels_from_counts(TEST_COUNTS[c])) for c in (1, 2, 3, 4)}
rng = np.random.default_rng(0)
configs = [("temporal", "fedavg", False, 1e-3, 4), ("temporal_lora", "fedavg", False, 3e-3, 4),
("frame", "fedopt", True, 1e-2, 16), ("metric", "fedmedian", False, 1e-3, 16), ("lstm", "fedavg", False, 1e-3, 4)]
for kind, agg, sam, lr, bs in configs:
enc = Method(kind).encoder
if kind.startswith("frame"):
sel = sample_frames_equidistant(40, 20)
else:
sel = sample_frames_santhi(40, k=12, center=20, half_window=6, rng=rng)
raw = lambda v: enc(v[:, sel])
std = federated_standardizer([raw(train[c][0]) for c in (1, 2, 3)])
feat = lambda v: std(raw(v))
clients = [(feat(train[c][0]), torch.as_tensor(train[c][1])) for c in (1, 2, 3)]
tests = [(feat(test[c][0]), test[c][1]) for c in (1, 2, 3)]
gm = federated_train(kind, clients, rounds=10, local_epochs=2, lr=lr, bs=bs, aggregator=agg, use_sam=sam)
gm.encoder = enc
_, f1_1, ec_1 = task1(gm, feat(test[4][0]), test[4][1])
_, f1_2, ec_2 = task2(gm, clients, tests, epochs=2, lr=lr, bs=bs, use_sam=sam)
n_train = sum(p.numel() for p in gm.head.parameters() if p.requires_grad)
print(f"{kind:13s} {agg:9s} trainable {n_train:7d} Task 1 F1 {100 * f1_1:5.2f} EC {100 * ec_1:5.2f} Task 2 avg F1 {100 * f1_2:5.2f} EC {100 * ec_2:5.2f}")
Running the file on a laptop CPU takes under a minute and prints the following. The reference classifier and Task 1 values match the paper exactly, and the communication costs reproduce Table 8. The stochastic references land within a few tenths of a point of the paper’s simulation means, as expected from independent random draws. The synthetic federated run only shows that every component trains and connects. Its numbers say nothing about real surgical video.
Conclusion
FedSurg set out to test whether hospitals could jointly train an AI system to grade appendicitis from surgical video without sharing a single frame. The honest answer from this first edition is that the question cannot yet be settled. The best submission, a frozen video transformer reading 32 frames at a time, generalized to an unseen hospital far better than its rivals, but even a model trained on all the data centrally reached only about a quarter on macro F1 there. Several methods did no better than always guessing the most common grade.
The conceptual contributions are less about any model and more about how to evaluate one. Separating generalization from adaptation exposes a real tension that averaged scores hide. Expected cost treats an ordinal grading task as ordinal. Constant and random references turn an unreadable 12 percent into a clear statement about whether a model has learned anything. Permutation tests over actual cases, rather than p values computed over bootstrap replicates, show which differences the data can support.
Those habits carry over to other clinical AI benchmarks. Ordinal grading appears across medicine, from tumor staging to retinopathy severity to burn depth, and small multicenter test sets are the norm rather than the exception. Any challenge on such a task would benefit from the same references and statistics. The finding that temporal context helps under domain shift also deserves testing on other surgical procedures, where frame level variability is just as severe.
The limits are clear, and the authors name them. The evaluation rests on 70 test videos from four German hospitals. The label agreement ceiling, a weighted kappa of 0.614 on the full dataset, bounds what any model can learn. The submissions differed in too many ways at once to isolate the effect of federation. The simulated setup did not face the network and hardware realities of a real deployment, and no clinical threshold exists yet to judge success against.
The path forward runs through scale, more hospitals, more countries and more videos, together with personalized federated methods that protect shared representations from small, skewed local data, and pretraining built for fine surgical distinctions. What FedSurg gives the field is a measured starting point. Its most useful result may be how carefully it reports its own uncertainty, and that is exactly the standard clinical AI needs.
Frequently asked questions
What was the FedSurg challenge?
FedSurg was the first international challenge on federated learning for surgical video, held at MICCAI 2024 as part of the Endoscopic Vision series. Teams built models to grade appendicitis severity from laparoscopic appendectomy videos from four German hospitals, training across three hospitals without sharing data and testing on a fourth.
How well did federated learning grade appendicitis from surgical video?
Not well enough for clinical use. The best submission reached 23.03 percent macro F1 on the unseen hospital, and a model trained on all data centrally reached 26.31 percent. Several methods did no better than always predicting the most common grade.
Why did the video transformer generalize better than the other methods?
It analyzed 32 frames together instead of judging frames one at a time. Individual laparoscopic frames vary heavily with camera motion, smoke, instruments and lighting, so a model that reads a sequence can focus on what stays consistent. Frame level models collapsed onto a single grade at the new hospital.
What is Expected Cost and why use it instead of accuracy?
Expected Cost weights each error by how far the predicted grade is from the true grade, divided by the largest possible distance. Because appendicitis grades are ordered, calling a grade 4 case grade 3 is a smaller mistake than calling it grade 0. It is also not distorted by grades that are missing from a small test set.
Is federated learning worse than centralized training for surgical AI?
The FedSurg paper cannot say. The centralized and Swarm Learning baselines were not statistically separable at this sample size, although the federated result sat closer to chance level. The authors identify the small dataset of 153 training videos, rather than federation itself, as the main constraint.
Is AI ready to grade appendicitis during surgery?
No. The FedSurg results are far below clinical viability, no clinically acceptable threshold for this grading task has been established, and even expert surgeons show only moderate agreement on the grades. Treatment decisions for appendicitis should be made by qualified clinicians.
Read the full paper
The article is open access under a Creative Commons Attribution license in Medical Image Analysis, with full confusion matrices, bootstrap tables, permutation tests and reference classifier values in the appendices.
Kirchner, M., Hoffmann, H., Jenke, A. C., Saldanha, O. L., Pfeiffer, K., Kanjo, W., Alekseenko, J., de Boer, C., Kolamuri, S. R., Mazza, L., Padoy, N., Bano, S., Reinke, A., Maier-Hein, L., Stoyanov, D., Kather, J. N., Kolbinger, F. R., Bodenstedt, S., and Speidel, S. Federated Learning for surgical vision in appendicitis classification, results of the FedSurg EndoVis 2024 challenge. Medical Image Analysis 115 (2027) 104290. DOI 10.1016/j.media.2026.104290.
This analysis is based on the published paper and an independent evaluation of its claims.
