Skip to content

Feat/mlp depth loss weight bootstrap - #167

Open
kirilklein wants to merge 5 commits into
mainfrom
feat/mlp-depth-loss-weight-bootstrap
Open

Feat/mlp depth loss weight bootstrap#167
kirilklein wants to merge 5 commits into
mainfrom
feat/mlp-depth-loss-weight-bootstrap

Conversation

@kirilklein

@kirilklein kirilklein commented Mar 2, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added dual sampling modes for cohort selection: resample (no replacement) and bootstrap (with replacement), with input validation and reproducible seeding; sampling method is propagated through sampling flows and logs
    • Configurable neural head depth for flexible multi-layer classifiers
    • Exposure loss weighting and automatic averaging of outcome losses when training with multiple outcomes
  • Tests

    • Added comprehensive tests covering sampling modes, validation, reproducibility, multi-layer head behavior, and gradient flow

kvk-cmd and others added 3 commits February 23, 2026 08:49
MLPHead now accepts num_hidden_layers parameter (default 1, matching
previous architecture). Deeper heads give the outcome model more
capacity to learn the exposure-outcome relationship. Config keys
mlp_num_hidden_layers and mlp_hidden_size_ratio are read from head_config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Outcomes are now averaged instead of summed, preventing exposure from
being dominated when many outcomes are present. exposure_loss_weight
config (default 1.0) controls the balance. PCGrad path applies the
same weighting for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sample_cohort now accepts method parameter: "resample" (default,
without replacement) or "bootstrap" (with replacement). Bootstrap
allows oversampling and enables proper variance estimation for
causal effect estimates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4f0541 and d514b75.

📒 Files selected for processing (1)
  • corebehrt/functional/causal/cohort_sampler.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • corebehrt/functional/causal/cohort_sampler.py

📝 Walkthrough

Walkthrough

Adds a sampling method option ("resample" vs "bootstrap") to cohort sampling and propagates it through simulation; refactors MLPHead to support configurable hidden layers; updates model and trainer to weight exposure loss and average outcome losses; and adds tests for cohort sampling and deeper MLPHead behavior.

Changes

Cohort / File(s) Summary
Cohort Sampling
corebehrt/functional/causal/cohort_sampler.py, corebehrt/main_causal/simulate_with_sampling.py
Adds method parameter (default "resample") with validation; maps to replace flag for RNG sampling ("bootstrap" => with-replacement); propagates method through simulate_with_sampling and updates logs.
MLP Head Implementation
corebehrt/modules/model/causal/heads.py
MLPHead constructor gains num_hidden_layers; replaces fixed two-layer head with LayerNorm followed by configurable repeated Linear→GELU blocks (size reduced by hidden_size_ratio) and final Linear output.
Model Loss & Head Wiring
corebehrt/modules/model/causal/model.py
Introduces exposure_loss_weight; instantiates exposure/outcome heads using mlp_num_hidden_layers and mlp_hidden_size_ratio; outcome head input size adjusted (head_input_size + 1); exposure loss scaled and outcome losses averaged before aggregation.
Trainer PCGrad Integration
corebehrt/modules/trainer/causal/trainer.py
When composing PCGrad task losses, scales exposure loss by model.exposure_loss_weight and normalizes each outcome loss by the number of outcomes before combining.
Cohort Sampler Tests
tests/test_functional/test_causal/test_cohort_sampler.py
Adds tests covering "resample" and "bootstrap" modes, size vs fraction sampling, duplicate behavior, invalid method errors, and seed reproducibility.
MLP Head Tests
tests/test_modules/test_model/test_causal/test_head.py
Adds tests confirming default architecture parity, multi-depth forward pass shapes and module counts, and gradient flow through deeper MLPHead variants.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through code to sample fine,
Resample or bootstrap — choose the line.
Deeper layers stack, then softly yield,
Exposure weighed, outcomes leveled in the field.
A tiny rabbit cheers: fresh tests revealed!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title references multiple distinct features (MLP depth, loss weight, and bootstrap) that are all present in the changeset, making it partially related to the main changes but overly broad without clearly highlighting the primary objective. Consider a more specific title that emphasizes the primary change, such as 'Add bootstrap sampling and configurable MLP depth' or 'Support multiple sampling methods and loss weighting for causal heads'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mlp-depth-loss-weight-bootstrap

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
corebehrt/modules/trainer/causal/trainer.py (1)

149-175: ⚠️ Potential issue | 🟠 Major

PCGrad path currently drops non-task loss terms (e.g., L1 regularization).

When task_losses is populated, backprop only covers exposure/outcome losses. If outputs.loss includes extra terms (like outputs.l1_loss from the model), enabling PCGrad silently changes the optimized objective.

Proposed fix
             # Add outcome losses if available (normalized by count)
             if hasattr(outputs, "outcome_losses") and outputs.outcome_losses:
                 n_outcomes = len(outputs.outcome_losses)
                 for outcome_loss in outputs.outcome_losses.values():
                     if outcome_loss is not None:
                         task_losses.append(outcome_loss / n_outcomes)
+
+            # Preserve regularization terms included in total loss
+            if hasattr(outputs, "l1_loss") and outputs.l1_loss is not None:
+                task_losses.append(outputs.l1_loss)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/modules/trainer/causal/trainer.py` around lines 149 - 175, The
PCGrad branch currently only backprops per-task losses (`task_losses`) and thus
drops any extra terms present in `outputs.loss` (e.g., L1 regularization);
compute the residual extra loss as `residual = outputs.loss - sum(task_losses)`
when `hasattr(outputs, "loss")` and the residual is non-negligible, append that
residual to `task_losses` before scaling (`scaled_losses =
[self.scaler.scale(loss) for loss in task_losses]`) so
`self.optimizer.pc_backward(scaled_losses)` includes those terms, and keep the
existing return logic (`return outputs.loss if hasattr(outputs, "loss") else
sum(task_losses)`) to log the full unscaled objective; reference symbols:
task_losses, outputs.loss, outputs.outcome_losses, self.scaler.scale,
self.optimizer.pc_backward.
🧹 Nitpick comments (1)
corebehrt/modules/model/causal/heads.py (1)

79-91: dropout_prob is currently dead config.

The parameter is exposed but never applied in self.classifier. Either wire nn.Dropout(dropout_prob) into the stack or remove the arg to avoid misleading config behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/modules/model/causal/heads.py` around lines 79 - 91, The parameter
dropout_prob is unused in the constructor building self.classifier; either wire
it into the Sequential or remove it. Fix by inserting nn.Dropout(dropout_prob)
into the layers list (e.g., append nn.Dropout(dropout_prob) immediately after
each nn.GELU() inside the for-loop that builds hidden layers in the class
constructor, and if num_hidden_layers==0 ensure a dropout is added before the
final nn.Linear(current_size, 1) so the config is honored). Alternatively, if
you choose to remove the parameter, delete dropout_prob from the signature and
any references; ensure the unique symbols referenced are dropout_prob,
self.classifier, and the constructor that builds layers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@corebehrt/functional/causal/cohort_sampler.py`:
- Around line 52-54: Add an explicit empty-cohort validation by checking
full_pids (or n_total) before proceeding: if full_pids is empty (n_total == 0)
raise a clear ValueError so callers fail fast; place this check before the
existing lines that compute n_total = len(full_pids) and replace = method ==
"bootstrap" (or immediately after computing n_total) and include a descriptive
message referencing the cohort being empty.

In `@corebehrt/modules/model/causal/heads.py`:
- Around line 85-90: The loop computing next_size = current_size //
hidden_size_ratio can produce zero or invalid widths; add guards in the MLP
builder that validate inputs (ensure current_size and num_hidden_layers are
positive integers and hidden_size_ratio >= 1) and after computing next_size
check if next_size < 1 (or next_size == 0) and raise a clear ValueError
referencing next_size/current_size/hidden_size_ratio, or clamp next_size to at
least 1 before creating nn.Linear; update the code that appends layers (layers
and nn.Linear) to rely on these validated/adjusted sizes so no zero-width Linear
is created.

In `@tests/test_modules/test_model/test_causal/test_head.py`:
- Around line 221-264: The test module (functions like
test_default_matches_original_architecture, test_multi_layer_forward_pass,
test_multi_layer_module_count, test_gradient_flow_deep_head) needs to be
auto-formatted with Ruff to satisfy CI; run `ruff format` on this test file,
review the resulting whitespace/line-break changes, stage the formatted file,
and commit the updates so the CI no longer reports a reformatting error.

---

Outside diff comments:
In `@corebehrt/modules/trainer/causal/trainer.py`:
- Around line 149-175: The PCGrad branch currently only backprops per-task
losses (`task_losses`) and thus drops any extra terms present in `outputs.loss`
(e.g., L1 regularization); compute the residual extra loss as `residual =
outputs.loss - sum(task_losses)` when `hasattr(outputs, "loss")` and the
residual is non-negligible, append that residual to `task_losses` before scaling
(`scaled_losses = [self.scaler.scale(loss) for loss in task_losses]`) so
`self.optimizer.pc_backward(scaled_losses)` includes those terms, and keep the
existing return logic (`return outputs.loss if hasattr(outputs, "loss") else
sum(task_losses)`) to log the full unscaled objective; reference symbols:
task_losses, outputs.loss, outputs.outcome_losses, self.scaler.scale,
self.optimizer.pc_backward.

---

Nitpick comments:
In `@corebehrt/modules/model/causal/heads.py`:
- Around line 79-91: The parameter dropout_prob is unused in the constructor
building self.classifier; either wire it into the Sequential or remove it. Fix
by inserting nn.Dropout(dropout_prob) into the layers list (e.g., append
nn.Dropout(dropout_prob) immediately after each nn.GELU() inside the for-loop
that builds hidden layers in the class constructor, and if num_hidden_layers==0
ensure a dropout is added before the final nn.Linear(current_size, 1) so the
config is honored). Alternatively, if you choose to remove the parameter, delete
dropout_prob from the signature and any references; ensure the unique symbols
referenced are dropout_prob, self.classifier, and the constructor that builds
layers.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8f28ab and 94d346c.

📒 Files selected for processing (7)
  • corebehrt/functional/causal/cohort_sampler.py
  • corebehrt/main_causal/simulate_with_sampling.py
  • corebehrt/modules/model/causal/heads.py
  • corebehrt/modules/model/causal/model.py
  • corebehrt/modules/trainer/causal/trainer.py
  • tests/test_functional/test_causal/test_cohort_sampler.py
  • tests/test_modules/test_model/test_causal/test_head.py

Comment thread corebehrt/functional/causal/cohort_sampler.py
Comment on lines +85 to +90
for _ in range(num_hidden_layers):
next_size = current_size // hidden_size_ratio
layers.append(nn.Linear(current_size, next_size))
layers.append(nn.GELU())
current_size = next_size
layers.append(nn.Linear(current_size, 1, bias=True))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add guardrails for invalid/degenerated MLP dimensions.

next_size = current_size // hidden_size_ratio is unchecked. Bad configs can create zero-width layers or runtime errors.

Proposed fix
     def __init__(
         self,
         input_size: int,
         hidden_size_ratio: int = 2,
         dropout_prob: float = 0.1,
         num_hidden_layers: int = 1,
     ):
         super().__init__()
+        if hidden_size_ratio <= 0:
+            raise ValueError(f"hidden_size_ratio must be > 0, got {hidden_size_ratio}")
+        if num_hidden_layers < 0:
+            raise ValueError(f"num_hidden_layers must be >= 0, got {num_hidden_layers}")
+
         layers = [nn.LayerNorm(input_size)]
         current_size = input_size
-        for _ in range(num_hidden_layers):
+        for layer_idx in range(num_hidden_layers):
             next_size = current_size // hidden_size_ratio
+            if next_size < 1:
+                raise ValueError(
+                    f"Invalid MLP config at hidden layer {layer_idx}: "
+                    f"current_size={current_size}, hidden_size_ratio={hidden_size_ratio}"
+                )
             layers.append(nn.Linear(current_size, next_size))
             layers.append(nn.GELU())
             current_size = next_size
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/modules/model/causal/heads.py` around lines 85 - 90, The loop
computing next_size = current_size // hidden_size_ratio can produce zero or
invalid widths; add guards in the MLP builder that validate inputs (ensure
current_size and num_hidden_layers are positive integers and hidden_size_ratio
>= 1) and after computing next_size check if next_size < 1 (or next_size == 0)
and raise a clear ValueError referencing
next_size/current_size/hidden_size_ratio, or clamp next_size to at least 1
before creating nn.Linear; update the code that appends layers (layers and
nn.Linear) to rely on these validated/adjusted sizes so no zero-width Linear is
created.

Comment thread tests/test_modules/test_model/test_causal/test_head.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_modules/test_model/test_causal/test_head.py (1)

240-249: Consider asserting module order/types, not only count.

Count-only checks can pass even if layer ordering regresses. A light type-pattern assertion would strengthen this test.

Optional test hardening
     def test_multi_layer_module_count(self):
         """Test that deeper heads have more modules."""
         for depth in [1, 2, 3]:
             with self.subTest(depth=depth):
                 head = MLPHead(self.input_size, num_hidden_layers=depth)
                 modules = list(head.classifier)
                 # LayerNorm + (Linear + GELU) * depth + Linear
                 expected = 1 + 2 * depth + 1
                 self.assertEqual(len(modules), expected)
+                self.assertIsInstance(modules[0], torch.nn.LayerNorm)
+                for i in range(depth):
+                    self.assertIsInstance(modules[1 + 2 * i], torch.nn.Linear)
+                    self.assertIsInstance(modules[2 + 2 * i], torch.nn.GELU)
+                self.assertIsInstance(modules[-1], torch.nn.Linear)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_modules/test_model/test_causal/test_head.py` around lines 240 -
249, The test test_multi_layer_module_count only checks the number of modules;
update it to also assert the expected module types and order for MLPHead: verify
head.classifier[0] is LayerNorm, then for i in range(depth) assert the pair
head.classifier[1 + 2*i] is Linear and head.classifier[1 + 2*i + 1] is GELU, and
finally assert the last module is Linear; use isinstance checks against
torch.nn.LayerNorm, torch.nn.Linear, and torch.nn.GELU so the test will fail if
ordering or types regress while still supporting varying depth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_modules/test_model/test_causal/test_head.py`:
- Around line 240-249: The test test_multi_layer_module_count only checks the
number of modules; update it to also assert the expected module types and order
for MLPHead: verify head.classifier[0] is LayerNorm, then for i in range(depth)
assert the pair head.classifier[1 + 2*i] is Linear and head.classifier[1 + 2*i +
1] is GELU, and finally assert the last module is Linear; use isinstance checks
against torch.nn.LayerNorm, torch.nn.Linear, and torch.nn.GELU so the test will
fail if ordering or types regress while still supporting varying depth.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94d346c and e4f0541.

📒 Files selected for processing (2)
  • tests/test_functional/test_causal/test_cohort_sampler.py
  • tests/test_modules/test_model/test_causal/test_head.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_functional/test_causal/test_cohort_sampler.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants