Add higher-capacity last-four-layer fusion

This commit is contained in:
Xin Wang
2026-08-26 13:06:18 +08:00
parent 43e0e14cf5
commit f45ea9cbc6
9 changed files with 397 additions and 17 deletions

View File

@@ -79,6 +79,74 @@ The raw 8-second console output, machine-readable comparison, and detailed
interpretation are archived in interpretation are archived in
[`results/easycom_open_stream_8s_vs_20s_2026-08-23/`](results/easycom_open_stream_8s_vs_20s_2026-08-23/README.md). [`results/easycom_open_stream_8s_vs_20s_2026-08-23/`](results/easycom_open_stream_8s_vs_20s_2026-08-23/README.md).
### Last-4 query-layer learned mixture
A frozen Qwen2.5-Omni-3B experiment now reads the query states from the final
four Thinker layers and learns four global softmax mixture weights before the
same `10 x 3` anticipation head. With an 8-second open-stream context, 2 FPS
video, 5 Hz outputs, and 25 head-training epochs, it reaches **73.45085** mAP
at 0.2 seconds and **60.53423** mean mAP on official test Sessions 3, 2, and 1.
Relative to the recorded single-final-layer 8-second run, this is **+1.45317**
at 0.2 seconds and **+2.01486** mean mAP, with positive gains at every horizon.
The final oldest-to-newest layer weights are
`[0.246384, 0.248327, 0.241774, 0.263515]`. Because they remain almost uniform,
the result currently demonstrates the value of multi-layer query readout, not
yet the value of learning the mixture. A fixed uniform last-4 mean is the next
required control. The complete table, machine-readable metrics, provenance,
and naming caveat are archived in
[`results/easycom_frozen_3b_8s_last4_learned_mix_2026-08-26/`](results/easycom_frozen_3b_8s_last4_learned_mix_2026-08-26/README.md).
#### Higher-capacity last-4 concat-MLP ablation
`train_turn_head.py` also supports a higher-capacity fusion control. It
flattens each `[4, 2048]` last-layer stack to `8192` dimensions, applies
`LayerNorm -> Linear(8192, 2048) -> GELU`, and then uses the unchanged
anticipation MLP. This adds about 16.8 million fusion parameters, compared
with four logits in the scalar-mixture experiment. Keep the seed and training
recipe fixed for a clean capacity ablation:
```bash
CUDA_VISIBLE_DEVICES=0 \
OMP_NUM_THREADS=4 \
MKL_NUM_THREADS=4 \
OPENBLAS_NUM_THREADS=4 \
python train_turn_head.py \
--feature-root /data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_8s_last4_stack \
--target-root /data1/yfzx/wangx/Datasets/easycom/easycom_target_frame \
--output checkpoints/easycom_frozen_3b_8s_last4_concat_mlp_25ep.pth \
--sessions 4 5 6 7 8 9 10 11 12 \
--epochs 25 \
--batch-size 512 \
--learning-rate 7e-5 \
--weight-decay 5e-5 \
--warmup-epochs 10 \
--warmup-factor 0.3 \
--dropout 0.2 \
--loss lstr-mce \
--layer-fusion concat-mlp \
--expected-mode open-stream-replay \
--seed 0 \
--device cuda:0
```
Evaluate with the same dense stacked test features:
```bash
CUDA_VISIBLE_DEVICES=0 python infer_turn_head.py \
--feature-root /data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_8s_last4_stack \
--target-root /data1/yfzx/wangx/Datasets/easycom/easycom_target_frame \
--checkpoint checkpoints/easycom_frozen_3b_8s_last4_concat_mlp_25ep.pth \
--output-dir results/easycom_frozen_3b_8s_last4_concat_mlp_25ep \
--expected-mode open-stream-replay \
--batch-size 1024 \
--device cuda:0
```
Treat this as a capacity ablation. If it improves, compare it against a
parameter-matched single-layer MLP and use stronger regularization or a clean
development session before attributing the gain to cross-layer fusion.
## Current Ego4D context-length results ## Current Ego4D context-length results
The following single-seed snapshot was recorded on 2026-08-22. Both variants The following single-seed snapshot was recorded on 2026-08-22. Both variants

View File

@@ -34,6 +34,12 @@ STEP_SECONDS = 0.2
NUM_HORIZONS = 10 NUM_HORIZONS = 10
NUM_CLASSES = 3 NUM_CLASSES = 3
DATASET_NAMES = ("easycom", "ego4d") DATASET_NAMES = ("easycom", "ego4d")
LAYER_FUSION_MODES = (
"single",
"fixed-mean",
"scalar-mixture",
"concat-mlp",
)
DatasetName = Literal["easycom", "ego4d"] DatasetName = Literal["easycom", "ego4d"]
@@ -48,30 +54,55 @@ class TurnPredictionHead(nn.Module):
dropout: float = 0.1, dropout: float = 0.1,
num_input_layers: int = 1, num_input_layers: int = 1,
learnable_layer_mixture: bool = False, learnable_layer_mixture: bool = False,
layer_fusion: str | None = None,
) -> None: ) -> None:
super().__init__() super().__init__()
if num_input_layers <= 0: if num_input_layers <= 0:
raise ValueError("num_input_layers must be positive") raise ValueError("num_input_layers must be positive")
if learnable_layer_mixture and num_input_layers < 2: if layer_fusion is None:
raise ValueError( layer_fusion = (
"learnable_layer_mixture requires at least two input layers" "scalar-mixture" if learnable_layer_mixture else "single"
) )
if not learnable_layer_mixture and num_input_layers != 1: if layer_fusion not in LAYER_FUSION_MODES:
raise ValueError( raise ValueError(
"Multiple input layers require learnable_layer_mixture=True" f"Unknown layer_fusion={layer_fusion!r}; expected one of "
f"{LAYER_FUSION_MODES}"
)
if learnable_layer_mixture and layer_fusion != "scalar-mixture":
raise ValueError(
"learnable_layer_mixture=True is only compatible with "
"layer_fusion='scalar-mixture'"
)
if layer_fusion != "single" and num_input_layers < 2:
raise ValueError(
f"layer_fusion={layer_fusion!r} requires at least two input layers"
)
if layer_fusion == "single" and num_input_layers != 1:
raise ValueError(
"Multiple input layers require a non-single layer_fusion"
) )
self.hidden_size = hidden_size self.hidden_size = hidden_size
self.num_horizons = num_horizons self.num_horizons = num_horizons
self.num_classes = num_classes self.num_classes = num_classes
self.dropout = dropout self.dropout = dropout
self.num_input_layers = num_input_layers self.num_input_layers = num_input_layers
self.learnable_layer_mixture = learnable_layer_mixture self.layer_fusion = layer_fusion
if learnable_layer_mixture: self.learnable_layer_mixture = layer_fusion == "scalar-mixture"
if self.learnable_layer_mixture:
# Zero initialization exactly reproduces a uniform layer mean at # Zero initialization exactly reproduces a uniform layer mean at
# step zero while allowing training to select useful query layers. # step zero while allowing training to select useful query layers.
self.layer_logits = nn.Parameter(torch.zeros(num_input_layers)) self.layer_logits = nn.Parameter(torch.zeros(num_input_layers))
else: else:
self.register_parameter("layer_logits", None) self.register_parameter("layer_logits", None)
if layer_fusion == "concat-mlp":
fusion_input_size = num_input_layers * hidden_size
self.layer_fusion_net: nn.Module | None = nn.Sequential(
nn.LayerNorm(fusion_input_size),
nn.Linear(fusion_input_size, hidden_size),
nn.GELU(),
)
else:
self.layer_fusion_net = None
self.net = nn.Sequential( self.net = nn.Sequential(
nn.LayerNorm(hidden_size), nn.LayerNorm(hidden_size),
nn.Linear(hidden_size, hidden_size // 2), nn.Linear(hidden_size, hidden_size // 2),
@@ -85,13 +116,16 @@ class TurnPredictionHead(nn.Module):
raise ValueError( raise ValueError(
f"Expected hidden size {self.hidden_size}, got {states.shape[-1]}" f"Expected hidden size {self.hidden_size}, got {states.shape[-1]}"
) )
if self.learnable_layer_mixture: if self.layer_fusion != "single":
if states.ndim < 3 or states.shape[-2] != self.num_input_layers: if states.ndim < 3 or states.shape[-2] != self.num_input_layers:
raise ValueError( raise ValueError(
"Layer-mixture input must end in " "Multi-layer input must end in "
f"[{self.num_input_layers},{self.hidden_size}], got " f"[{self.num_input_layers},{self.hidden_size}], got "
f"{tuple(states.shape)}" f"{tuple(states.shape)}"
) )
if self.layer_fusion == "fixed-mean":
states = states.mean(dim=-2)
elif self.layer_fusion == "scalar-mixture":
assert self.layer_logits is not None assert self.layer_logits is not None
weights = self.layer_logits.softmax(dim=0).to(states.dtype) weights = self.layer_logits.softmax(dim=0).to(states.dtype)
view_shape = (1,) * (states.ndim - 2) + ( view_shape = (1,) * (states.ndim - 2) + (
@@ -99,6 +133,10 @@ class TurnPredictionHead(nn.Module):
1, 1,
) )
states = (states * weights.view(view_shape)).sum(dim=-2) states = (states * weights.view(view_shape)).sum(dim=-2)
elif self.layer_fusion == "concat-mlp":
assert self.layer_fusion_net is not None
states = states.flatten(start_dim=-2)
states = self.layer_fusion_net(states)
logits = self.net(states) logits = self.net(states)
return logits.view( return logits.view(
*states.shape[:-1], self.num_horizons, self.num_classes *states.shape[:-1], self.num_horizons, self.num_classes
@@ -406,6 +444,14 @@ def checkpoint_head_config(payload: dict[str, Any]) -> dict[str, Any]:
config.setdefault("dropout", 0.1) config.setdefault("dropout", 0.1)
config.setdefault("num_input_layers", 1) config.setdefault("num_input_layers", 1)
config.setdefault("learnable_layer_mixture", False) config.setdefault("learnable_layer_mixture", False)
config.setdefault(
"layer_fusion",
(
"scalar-mixture"
if config["learnable_layer_mixture"]
else "single"
),
)
return config return config

View File

@@ -328,6 +328,7 @@ def main() -> None:
"checkpoint_epoch": checkpoint_payload.get("epoch"), "checkpoint_epoch": checkpoint_payload.get("epoch"),
"checkpoint_train_sessions": checkpoint_payload.get("train_sessions"), "checkpoint_train_sessions": checkpoint_payload.get("train_sessions"),
"checkpoint_dev_sessions": checkpoint_payload.get("dev_sessions"), "checkpoint_dev_sessions": checkpoint_payload.get("dev_sessions"),
"layer_fusion": head.layer_fusion,
"layer_mixture_weights_oldest_to_newest": ( "layer_mixture_weights_oldest_to_newest": (
head.layer_mixture_weights().detach().float().cpu().tolist() head.layer_mixture_weights().detach().float().cpu().tolist()
if head.layer_mixture_weights() is not None if head.layer_mixture_weights() is not None

View File

@@ -0,0 +1,111 @@
# EasyCom frozen Qwen2.5-Omni-3B last-4 learned mixture
Recorded on **2026-08-26** from the user-reported remote training and
`infer_turn_head.py` output. This is a single-run diagnostic result, not yet a
multi-seed paper number.
## Experiment contract
- Dataset: EasyCom.
- Test split: confirmed as EgoSpeak official Sessions 3, 2, and 1. Training is
recorded as the script's default official Sessions 4--12; verify this against
the remote checkpoint JSON because the supplied excerpt starts at epoch 24.
- Backbone: frozen Qwen2.5-Omni-3B Thinker.
- Streaming mode: open-stream replay with an 8-second causal-memory budget.
- Input sampling: 2 FPS video and raw audio.
- Prediction rate: 5 Hz, with ten future horizons from 0.2 to 2.0 seconds.
- Readout: the query states from the final four Thinker layers, ordered oldest
to newest, followed by a learned global softmax layer mixture and the same
`10 x 3` anticipation MLP.
- Training: head and four scalar layer logits only; Thinker remains frozen.
- Evaluator: EgoSpeak full-timeline per-frame AP/mAP.
The remote artifact is named
`easycom_frozen_3b_8s_last4_learned_mix_50ep.pth`, but the supplied log and the
user report both show that the actual run ended at **epoch 25/25**. The
`50ep` suffix is therefore a naming error and must not be used as provenance.
## Summary
All values below are percentages. The comparison baseline is the previously
recorded frozen 3B, 8-second, open-stream, single-final-layer run evaluated on
the same official test sessions.
| Readout | 0.2s full mAP | Full mean mAP | Delta at 0.2s | Delta mean |
|---|---:|---:|---:|---:|
| Single final-layer query | 71.99768 | 58.51937 | -- | -- |
| **Last-4 learned mixture** | **73.45085** | **60.53423** | **+1.45317** | **+2.01486** |
The last-4 readout improves every evaluated horizon. The largest gain is
`+2.41084` at 1.0 seconds; the smallest is still a positive `+1.45317` at
0.2 seconds.
## Per-horizon full-timeline results
| Horizon | Last-4 mAP | Single-layer mAP | Delta | Background AP | Wearer AP | Others AP |
|---:|---:|---:|---:|---:|---:|---:|
| 0.2s | 73.45085 | 71.99768 | +1.45317 | 56.69385 | 78.35111 | 85.30760 |
| 0.4s | 68.58645 | 66.74289 | +1.84356 | 49.77891 | 73.48058 | 82.49988 |
| 0.6s | 64.44213 | 62.38972 | +2.05241 | 44.51563 | 68.39317 | 80.41758 |
| 0.8s | 61.34852 | 59.12229 | +2.22623 | 40.98328 | 64.20310 | 78.85917 |
| 1.0s | 59.12464 | 56.71380 | +2.41084 | 38.99833 | 60.75541 | 77.62018 |
| 1.2s | 57.53667 | 55.36300 | +2.17367 | 37.76815 | 58.15110 | 76.69076 |
| 1.4s | 56.43710 | 54.21297 | +2.22413 | 37.22701 | 56.12642 | 75.95786 |
| 1.6s | 55.42035 | 53.43221 | +1.98814 | 36.72606 | 54.14045 | 75.39454 |
| 1.8s | 54.76334 | 52.84703 | +1.91631 | 36.45931 | 52.80226 | 75.02845 |
| 2.0s | 54.23227 | 52.37207 | +1.86020 | 36.45292 | 51.56488 | 74.67902 |
| **Mean** | **60.53423** | **58.51937** | **+2.01486** | **41.56035** | **61.79685** | **78.24550** |
Averaged over horizons, the AP gains relative to the single-layer baseline are
`+1.92941` for background, `+2.88478` for camera-wearer speaking, and
`+1.23041` for other-people speaking. The wearer class contributes the largest
improvement, which is encouraging for the short-horizon turn-taking objective.
## Learned layer mixture
| Layer order | Weight |
|---|---:|
| Oldest of selected four | 0.246384 |
| Second | 0.248327 |
| Third | 0.241774 |
| Final layer | 0.263515 |
The weights remain close to uniform: normalized entropy is `0.999619`, the L1
distance from `[0.25, 0.25, 0.25, 0.25]` is only `0.02703`, and the largest
weight exceeds the smallest by only `0.021741`. Consequently, this experiment
supports **multi-layer query readout**, but does not yet show that learning the
mixture is better than a fixed last-4 mean.
## Interpretation and next controls
1. This is the strongest recorded frozen-Omni EasyCom result in the project so
far: `73.45085` at 0.2 seconds and `60.53423` mean mAP.
2. The gain occurs at all horizons and all three classes, rather than coming
from a single AP outlier.
3. Because the learned weights are nearly uniform, the first required ablation
is a **fixed uniform last-4 mean** with the same feature files, head recipe,
epoch count, and seed.
4. Then compare each of the four individual query layers. This separates layer
ensembling from access to a specifically better intermediate layer.
5. Repeat the single-layer, fixed-mean, and learned-mixture variants with at
least three identical seeds before using the gain as a paper result.
6. The supplied output contains no strict causal-valid table. Archive the
remote `metrics.json` or rerun the evaluator before the publication table.
7. The single-layer comparison came from an earlier run; its random seed and
head-training duration must be matched in the fixed-mean ablation.
## Provenance
- Remote feature root:
`/data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_8s_last4_stack`
- Remote checkpoint:
`checkpoints/easycom_frozen_3b_8s_last4_learned_mix_50ep.pth`
- Remote result directory:
`results/easycom_frozen_3b_8s_last4_learned_mix_50ep`
- Final training loss: `0.822849`.
- Raw console excerpt: [`raw_console.txt`](raw_console.txt).
- Machine-readable values: [`summary.json`](summary.json) and
[`metrics.csv`](metrics.csv).
- Raw remote `metrics.json` archived locally: no.
- Exact training-session list verified from the remote checkpoint JSON: no.
- Multi-seed: no.

View File

@@ -0,0 +1,12 @@
horizon_seconds,last4_map,single_final_layer_map,delta,background_ap,camera_wearer_ap,other_people_ap
0.2,73.45085,71.99768,1.45317,56.69385,78.35111,85.30760
0.4,68.58645,66.74289,1.84356,49.77891,73.48058,82.49988
0.6,64.44213,62.38972,2.05241,44.51563,68.39317,80.41758
0.8,61.34852,59.12229,2.22623,40.98328,64.20310,78.85917
1.0,59.12464,56.71380,2.41084,38.99833,60.75541,77.62018
1.2,57.53667,55.36300,2.17367,37.76815,58.15110,76.69076
1.4,56.43710,54.21297,2.22413,37.22701,56.12642,75.95786
1.6,55.42035,53.43221,1.98814,36.72606,54.14045,75.39454
1.8,54.76334,52.84703,1.91631,36.45931,52.80226,75.02845
2.0,54.23227,52.37207,1.86020,36.45292,51.56488,74.67902
mean,60.53423,58.51937,2.01486,41.56035,61.79685,78.24550
1 horizon_seconds last4_map single_final_layer_map delta background_ap camera_wearer_ap other_people_ap
2 0.2 73.45085 71.99768 1.45317 56.69385 78.35111 85.30760
3 0.4 68.58645 66.74289 1.84356 49.77891 73.48058 82.49988
4 0.6 64.44213 62.38972 2.05241 44.51563 68.39317 80.41758
5 0.8 61.34852 59.12229 2.22623 40.98328 64.20310 78.85917
6 1.0 59.12464 56.71380 2.41084 38.99833 60.75541 77.62018
7 1.2 57.53667 55.36300 2.17367 37.76815 58.15110 76.69076
8 1.4 56.43710 54.21297 2.22413 37.22701 56.12642 75.95786
9 1.6 55.42035 53.43221 1.98814 36.72606 54.14045 75.39454
10 1.8 54.76334 52.84703 1.91631 36.45931 52.80226 75.02845
11 2.0 54.23227 52.37207 1.86020 36.45292 51.56488 74.67902
12 mean 60.53423 58.51937 2.01486 41.56035 61.79685 78.24550

View File

@@ -0,0 +1,33 @@
head epoch 24/25: 100% | 142/142 | 137.53 batch/s
epoch 24: train_loss=0.822925, layer_weights=[0.2464,0.2483,0.2418,0.2635]
head epoch 25/25: 100% | 142/142 | 139.49 batch/s, lr=0
epoch 25: train_loss=0.822849, layer_weights=[0.2464,0.2483,0.2418,0.2635]
checkpoint: checkpoints/easycom_frozen_3b_8s_last4_learned_mix_50ep.pth
training summary: checkpoints/easycom_frozen_3b_8s_last4_learned_mix_50ep.pth.json
saved layer mixture weights (oldest->newest): 0.246384, 0.248327, 0.241774, 0.263515
feature root: /data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_8s_last4_stack
test sessions: Session_3, Session_2, Session_1
egospeak_full_timeline:
Action anticipation (0.20s) perframe mAP: 73.45085
AP: background=56.69385, camera_wearer_speaking=78.35111, other_people_speaking=85.30760; n=23048
Action anticipation (0.40s) perframe mAP: 68.58645
AP: background=49.77891, camera_wearer_speaking=73.48058, other_people_speaking=82.49988; n=23048
Action anticipation (0.60s) perframe mAP: 64.44213
AP: background=44.51563, camera_wearer_speaking=68.39317, other_people_speaking=80.41758; n=23048
Action anticipation (0.80s) perframe mAP: 61.34852
AP: background=40.98328, camera_wearer_speaking=64.20310, other_people_speaking=78.85917; n=23048
Action anticipation (1.00s) perframe mAP: 59.12464
AP: background=38.99833, camera_wearer_speaking=60.75541, other_people_speaking=77.62018; n=23048
Action anticipation (1.20s) perframe mAP: 57.53667
AP: background=37.76815, camera_wearer_speaking=58.15110, other_people_speaking=76.69076; n=23048
Action anticipation (1.40s) perframe mAP: 56.43710
AP: background=37.22701, camera_wearer_speaking=56.12642, other_people_speaking=75.95786; n=23048
Action anticipation (1.60s) perframe mAP: 55.42035
AP: background=36.72606, camera_wearer_speaking=54.14045, other_people_speaking=75.39454; n=23048
Action anticipation (1.80s) perframe mAP: 54.76334
AP: background=36.45931, camera_wearer_speaking=52.80226, other_people_speaking=75.02845; n=23048
Action anticipation (2.00s) perframe mAP: 54.23227
AP: background=36.45292, camera_wearer_speaking=51.56488, other_people_speaking=74.67902; n=23048
Action anticipation (mean) perframe mAP: 60.53423

View File

@@ -0,0 +1,66 @@
{
"schema_version": 1,
"recorded_date": "2026-08-26",
"status": "single_run_diagnostic_snapshot",
"dataset": "EasyCom",
"split": {
"train_sessions": [4, 5, 6, 7, 8, 9, 10, 11, 12],
"train_sessions_source": "train_turn_head.py default; not visible in supplied epoch-24 excerpt",
"test_sessions": [3, 2, 1],
"test_sessions_confirmed_in_console": true
},
"model": {
"backbone": "Qwen2.5-Omni-3B",
"thinker_frozen": true,
"streaming_mode": "open-stream-replay",
"memory_seconds": 8,
"video_fps": 2,
"prediction_hz": 5
},
"readout": {
"selected_layers": "last_4_query_states",
"layer_order": "oldest_to_newest",
"fusion": "learned_global_softmax",
"weights": [0.246384, 0.248327, 0.241774, 0.263515],
"normalized_entropy": 0.999619394230947,
"l1_distance_from_uniform": 0.02703,
"max_min_weight_gap": 0.021741
},
"training": {
"actual_epochs": 25,
"final_train_loss": 0.822849,
"artifact_name": "easycom_frozen_3b_8s_last4_learned_mix_50ep.pth",
"artifact_epoch_suffix_is_incorrect": true
},
"metrics_percent": {
"protocol": "egospeak_full_timeline",
"horizon_seconds": [0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0],
"map": [73.45085, 68.58645, 64.44213, 61.34852, 59.12464, 57.53667, 56.43710, 55.42035, 54.76334, 54.23227],
"background_ap": [56.69385, 49.77891, 44.51563, 40.98328, 38.99833, 37.76815, 37.22701, 36.72606, 36.45931, 36.45292],
"camera_wearer_ap": [78.35111, 73.48058, 68.39317, 64.20310, 60.75541, 58.15110, 56.12642, 54.14045, 52.80226, 51.56488],
"other_people_ap": [85.30760, 82.49988, 80.41758, 78.85917, 77.62018, 76.69076, 75.95786, 75.39454, 75.02845, 74.67902],
"mean_map": 60.53423,
"strict_causal_valid_reported": false
},
"comparison_to_single_final_layer_8s_percent": {
"baseline_mean_map": 58.51937,
"baseline_horizon_0.2_map": 71.99768,
"mean_map_delta": 2.01486,
"horizon_0.2_map_delta": 1.45317,
"horizon_map_delta": [1.45317, 1.84356, 2.05241, 2.22623, 2.41084, 2.17367, 2.22413, 1.98814, 1.91631, 1.86020],
"mean_class_ap_delta": {
"background": 1.92941,
"camera_wearer": 2.88478,
"other_people": 1.23041
}
},
"provenance": {
"source": "user-reported remote console output",
"remote_feature_root": "/data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_8s_last4_stack",
"remote_checkpoint": "checkpoints/easycom_frozen_3b_8s_last4_learned_mix_50ep.pth",
"remote_result_dir": "results/easycom_frozen_3b_8s_last4_learned_mix_50ep",
"raw_metrics_json_archived": false,
"remote_checkpoint_json_archived": false,
"multi_seed": false
}
}

View File

@@ -979,6 +979,9 @@ def _save_weight_checkpoint(
"num_horizons": NUM_HORIZONS, "num_horizons": NUM_HORIZONS,
"num_classes": NUM_CLASSES, "num_classes": NUM_CLASSES,
"dropout": raw_head.dropout, "dropout": raw_head.dropout,
"num_input_layers": raw_head.num_input_layers,
"layer_fusion": raw_head.layer_fusion,
"learnable_layer_mixture": raw_head.learnable_layer_mixture,
}, },
"initial_head_checkpoint": ( "initial_head_checkpoint": (
str(args.head_checkpoint.resolve()) if args.head_checkpoint else None str(args.head_checkpoint.resolve()) if args.head_checkpoint else None

View File

@@ -23,6 +23,7 @@ from tqdm.auto import tqdm
from easycom_head import ( from easycom_head import (
CLASS_NAMES, CLASS_NAMES,
DATASET_NAMES, DATASET_NAMES,
LAYER_FUSION_MODES,
NUM_CLASSES, NUM_CLASSES,
NUM_HORIZONS, NUM_HORIZONS,
OFFICIAL_TRAIN_SESSIONS, OFFICIAL_TRAIN_SESSIONS,
@@ -95,8 +96,17 @@ def parse_args() -> argparse.Namespace:
"--learnable-layer-mixture", "--learnable-layer-mixture",
action="store_true", action="store_true",
help=( help=(
"Learn softmax weights over stacked [T,L,D] query features before " "Legacy alias for --layer-fusion scalar-mixture."
"the anticipation head." ),
)
parser.add_argument(
"--layer-fusion",
choices=tuple(mode for mode in LAYER_FUSION_MODES if mode != "single"),
default=None,
help=(
"Fusion for stacked [T,L,D] query features. fixed-mean has no "
"fusion parameters; scalar-mixture learns L global weights; "
"concat-mlp flattens L*D and learns a full projection to D."
), ),
) )
parser.add_argument( parser.add_argument(
@@ -135,6 +145,14 @@ def _validate_args(args: argparse.Namespace) -> None:
raise ValueError("--num-workers cannot be negative") raise ValueError("--num-workers cannot be negative")
if args.dataset == "ego4d" and args.manifest is None: if args.dataset == "ego4d" and args.manifest is None:
raise ValueError("--manifest is required when --dataset ego4d") raise ValueError("--manifest is required when --dataset ego4d")
if (
args.learnable_layer_mixture
and args.layer_fusion not in {None, "scalar-mixture"}
):
raise ValueError(
"--learnable-layer-mixture is an alias for "
"--layer-fusion scalar-mixture and cannot select another fusion"
)
def _resolve_device(name: str) -> torch.device: def _resolve_device(name: str) -> torch.device:
@@ -348,6 +366,7 @@ def _checkpoint(
"num_classes": NUM_CLASSES, "num_classes": NUM_CLASSES,
"dropout": args.dropout, "dropout": args.dropout,
"num_input_layers": num_input_layers, "num_input_layers": num_input_layers,
"layer_fusion": args.layer_fusion,
"learnable_layer_mixture": args.learnable_layer_mixture, "learnable_layer_mixture": args.learnable_layer_mixture,
}, },
"alignment": { "alignment": {
@@ -383,7 +402,8 @@ def _checkpoint(
"scheduler": "linear_warmup_times_cosine", "scheduler": "linear_warmup_times_cosine",
"seed": args.seed, "seed": args.seed,
"expected_mode": args.expected_mode, "expected_mode": args.expected_mode,
"learnable_layer_mixture": args.learnable_layer_mixture, "layer_fusion": args.layer_fusion,
"learnable_layer_mixture": args.layer_fusion == "scalar-mixture",
"supervision_policy": ( "supervision_policy": (
"transition-sparse-manifest" "transition-sparse-manifest"
if args.sparse_anchor_cache_dir if args.sparse_anchor_cache_dir
@@ -401,6 +421,11 @@ def _checkpoint(
def main() -> None: def main() -> None:
args = parse_args() args = parse_args()
_validate_args(args) _validate_args(args)
if args.layer_fusion is None:
args.layer_fusion = (
"scalar-mixture" if args.learnable_layer_mixture else "single"
)
args.learnable_layer_mixture = args.layer_fusion == "scalar-mixture"
_seed_everything(args.seed) _seed_everything(args.seed)
device = _resolve_device(args.device) device = _resolve_device(args.device)
expected_mode = None if args.expected_mode == "any" else args.expected_mode expected_mode = None if args.expected_mode == "any" else args.expected_mode
@@ -473,14 +498,15 @@ def main() -> None:
f"training examples: {len(states)}, feature_dim={hidden_size}, " f"training examples: {len(states)}, feature_dim={hidden_size}, "
f"input_layers={num_input_layers}" f"input_layers={num_input_layers}"
) )
if num_input_layers > 1 and not args.learnable_layer_mixture: if num_input_layers > 1 and args.layer_fusion == "single":
raise ValueError( raise ValueError(
f"Features contain {num_input_layers} stacked layers; pass " f"Features contain {num_input_layers} stacked layers; pass "
"--learnable-layer-mixture" "--layer-fusion fixed-mean, scalar-mixture, or concat-mlp"
) )
if num_input_layers == 1 and args.learnable_layer_mixture: if num_input_layers == 1 and args.layer_fusion != "single":
raise ValueError( raise ValueError(
"--learnable-layer-mixture requires stacked [T,L,D] features" f"--layer-fusion {args.layer_fusion} requires stacked [T,L,D] "
"features"
) )
dev_states: torch.Tensor | None = None dev_states: torch.Tensor | None = None
dev_targets: torch.Tensor | None = None dev_targets: torch.Tensor | None = None
@@ -529,7 +555,21 @@ def main() -> None:
dropout=args.dropout, dropout=args.dropout,
num_input_layers=num_input_layers, num_input_layers=num_input_layers,
learnable_layer_mixture=args.learnable_layer_mixture, learnable_layer_mixture=args.learnable_layer_mixture,
layer_fusion=args.layer_fusion,
).to(device) ).to(device)
trainable_parameters = sum(
parameter.numel() for parameter in head.parameters() if parameter.requires_grad
)
fusion_parameters = (
sum(parameter.numel() for parameter in head.layer_fusion_net.parameters())
if head.layer_fusion_net is not None
else (head.layer_logits.numel() if head.layer_logits is not None else 0)
)
print(
f"layer_fusion={args.layer_fusion}, "
f"fusion_parameters={fusion_parameters:,}, "
f"head_trainable_parameters={trainable_parameters:,}"
)
if head.layer_logits is None: if head.layer_logits is None:
optimizer_parameters: Any = head.parameters() optimizer_parameters: Any = head.parameters()
else: else:
@@ -698,7 +738,7 @@ def main() -> None:
summary = {key: value for key, value in payload.items() if key != "head"} summary = {key: value for key, value in payload.items() if key != "head"}
summary_path.write_text(json.dumps(summary, indent=2) + "\n") summary_path.write_text(json.dumps(summary, indent=2) + "\n")
print(f"training summary: {summary_path}") print(f"training summary: {summary_path}")
if args.learnable_layer_mixture: if payload["head_config"].get("layer_fusion") == "scalar-mixture":
saved_logits = payload["head"]["layer_logits"].float() saved_logits = payload["head"]["layer_logits"].float()
saved_weights = saved_logits.softmax(dim=0).tolist() saved_weights = saved_logits.softmax(dim=0).tolist()
print( print(