lester1027 commited on
Commit
2437075
·
1 Parent(s): 99d6b6e

Discrete labels -> continuous labels

Browse files
Files changed (1) hide show
  1. app.py +104 -116
app.py CHANGED
@@ -18,13 +18,6 @@ COLUMN_COLORS = {
18
  "mae": "rgb(255, 245, 230)",
19
  }
20
 
21
- BEHAVIORS = ["Behavior A", "Behavior B", "Behavior C"]
22
- BEHAVIOR_COLORS = {
23
- "Behavior A": "#636EFA",
24
- "Behavior B": "#EF553B",
25
- "Behavior C": "#00CC96",
26
- }
27
-
28
  METRIC_DIRECTION = {
29
  "r2": "high_is_good",
30
  "rmse": "low_is_good",
@@ -34,6 +27,12 @@ METRIC_DIRECTION = {
34
  # Universal axis limits for better comparison across panels
35
  XYZ_RANGE: Tuple[float, float] = (-25.0, 25.0)
36
 
 
 
 
 
 
 
37
 
38
  def build_toy_benchmark() -> Dict[str, pd.DataFrame]:
39
  return {
@@ -103,49 +102,51 @@ def generate_mock_embedding(
103
  model_name: str,
104
  *,
105
  noise_level: float,
106
- n_points_per_behavior: int = 250,
107
- # Behavior geometry knobs (at noise=0)
108
- base_center_spread: float = 7.5,
109
- base_noise_scale: float = 1.0,
110
- transform_strength: float = 0.55,
111
- shift_strength: float = 2.0,
112
- warp_strength: float = 0.35,
113
- # Noise injection knobs
 
114
  max_replace_frac: float = 0.60,
115
- background_scale: float = 9.0,
116
  ) -> pd.DataFrame:
117
  """
118
- Mock 3D embedding with a "noise spike" control.
 
 
 
 
119
 
120
  noise_level in [0,1]:
121
- - reduces inter-behavior separation (centers collapse)
122
- - increases within-cluster spread
123
- - replaces a fraction of points with background noise points
124
  """
125
  nl = float(np.clip(noise_level, 0.0, 1.0))
126
 
127
  seed = abs(hash((dataset_name, model_name))) % (2**32)
128
  rng = np.random.default_rng(seed)
129
 
130
- # Base behavior centers in a canonical triangle in 3D
131
- base_centers = {
132
- "Behavior A": np.array([0.0, 0.0, 0.0]),
133
- "Behavior B": np.array([1.0, 0.2, 0.1]),
134
- "Behavior C": np.array([0.5, 1.1, -0.2]),
135
- }
 
 
 
136
 
137
- # As noise increases, pull centers toward a common centroid
138
- centroid = sum(base_centers.values()) / len(base_centers)
139
- center_spread = base_center_spread * (1.0 - 0.80 * nl)
140
- centers = {}
141
- for beh, c0 in base_centers.items():
142
- c_collapsed = (1.0 - 0.85 * nl) * c0 + (0.85 * nl) * centroid
143
- centers[beh] = c_collapsed * center_spread
144
 
145
- # Within-cluster spread increases with noise
146
- noise_scale = base_noise_scale * (1.0 + 2.25 * nl)
 
147
 
148
- # Stronger per-panel transforms so different (dataset, model) look different
149
  angles = rng.uniform(low=-np.pi, high=np.pi, size=3)
150
  cx, cy, cz = np.cos(angles)
151
  sx, sy, sz = np.sin(angles)
@@ -157,53 +158,44 @@ def generate_mock_embedding(
157
 
158
  scales = np.diag(1.0 + transform_strength * rng.normal(size=3))
159
  shear = np.eye(3)
160
- shear[0, 1] = 0.25 * transform_strength * rng.normal()
161
- shear[0, 2] = 0.25 * transform_strength * rng.normal()
162
- shear[1, 2] = 0.25 * transform_strength * rng.normal()
163
 
164
- A = (R @ scales @ shear)
165
  b = shift_strength * rng.normal(size=(3,))
166
 
167
- # How many points get replaced by background "noise spikes"
168
- replace_frac = max_replace_frac * nl
169
 
170
- rows: List[dict] = []
171
- for beh in BEHAVIORS:
172
- X = centers[beh] + noise_scale * rng.normal(size=(n_points_per_behavior, 3))
173
-
174
- # Apply affine transform
175
- X = X @ A.T + b
176
-
177
- # Nonlinear warp (twist + bend); stays even with noise
178
- theta = warp_strength * (0.15 * X[:, 0] + 0.10 * X[:, 1])
179
- ct = np.cos(theta)
180
- st = np.sin(theta)
181
- x_new = ct * X[:, 0] - st * X[:, 1]
182
- y_new = st * X[:, 0] + ct * X[:, 1]
183
- z_new = X[:, 2] + warp_strength * np.tanh(0.25 * X[:, 0]) * 2.0
184
- X = np.stack([x_new, y_new, z_new], axis=1)
185
-
186
- # Replace a fraction with background noise points
187
- n_replace = int(round(replace_frac * n_points_per_behavior))
188
- if n_replace > 0:
189
- idx_replace = rng.choice(n_points_per_behavior, size=n_replace, replace=False)
190
-
191
- # Background noise: heavy-ish tails + broad scale, centered near origin
192
- # This creates global "salt-and-pepper" points that mix behaviors.
193
- noise_bg = background_scale * rng.standard_t(df=3, size=(n_replace, 3))
194
- X[idx_replace] = noise_bg
195
-
196
- for i in range(n_points_per_behavior):
197
- rows.append(
198
- {
199
- "x": float(X[i, 0]),
200
- "y": float(X[i, 1]),
201
- "z": float(X[i, 2]),
202
- "behavior": beh,
203
- }
204
- )
205
 
206
- return pd.DataFrame(rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
 
209
  def _parse_rgb(rgb: str) -> tuple[int, int, int]:
@@ -261,9 +253,9 @@ def make_column_gradient_styles(
261
  low = col_min + (col_max - col_min) * (i / n_bins)
262
  high = col_min + (col_max - col_min) * ((i + 1) / n_bins)
263
 
264
- t = (i + 1) / n_bins
265
  if direction == "low_is_good":
266
- t = 1.0 - t
267
 
268
  styles.append(
269
  {
@@ -271,7 +263,7 @@ def make_column_gradient_styles(
271
  "column_id": col,
272
  "filter_query": f"{{{col}}} >= {low} && {{{col}}} < {high}",
273
  },
274
- "backgroundColor": _blend_rgb(white, dark_base, t),
275
  }
276
  )
277
 
@@ -295,9 +287,6 @@ def make_column_gradient_styles(
295
 
296
 
297
  def _apply_universal_scene_ranges(fig: go.Figure, *, n_scenes: int) -> None:
298
- """
299
- Apply the same x/y/z ranges to every 3D scene in a subplot grid.
300
- """
301
  for idx in range(n_scenes):
302
  scene_id = "scene" if idx == 0 else f"scene{idx + 1}"
303
  fig.update_layout(
@@ -340,31 +329,39 @@ def _embedding_grid_figure(
340
  md = model_for_each_panel[idx]
341
  df_emb = generate_mock_embedding(ds, md, noise_level=noise_level)
342
 
343
- for beh in BEHAVIORS:
344
- df_b = df_emb[df_emb["behavior"] == beh]
345
- fig.add_trace(
346
- go.Scatter3d(
347
- x=df_b["x"],
348
- y=df_b["y"],
349
- z=df_b["z"],
350
- mode="markers",
351
- name=beh,
352
- legendgroup=beh,
353
- showlegend=(idx == 0),
354
- marker={
355
- "size": 2.5,
356
- "opacity": 0.75,
357
- "color": BEHAVIOR_COLORS[beh],
358
- },
359
- hovertemplate=(
360
- "behavior=%{text}<br>"
361
- "x=%{x:.3f}<br>y=%{y:.3f}<br>z=%{z:.3f}<extra></extra>"
 
 
 
 
362
  ),
363
- text=[beh] * len(df_b),
364
  ),
365
- row=r,
366
- col=c,
367
- )
 
 
 
 
 
368
 
369
  _apply_universal_scene_ranges(fig, n_scenes=n_panels)
370
 
@@ -384,7 +381,6 @@ app = Dash(__name__)
384
  app.layout = html.Div(
385
  [
386
  html.H3("Neural Decoder Benchmarking"),
387
-
388
  html.Div(
389
  [
390
  html.Div("Mode"),
@@ -400,7 +396,6 @@ app.layout = html.Div(
400
  ],
401
  style={"marginBottom": "12px"},
402
  ),
403
-
404
  html.Div(
405
  [
406
  html.Div(
@@ -445,7 +440,6 @@ app.layout = html.Div(
445
  ],
446
  style={"display": "flex", "gap": "16px", "alignItems": "flex-end"},
447
  ),
448
-
449
  dash_table.DataTable(
450
  id="metrics-table",
451
  columns=[],
@@ -466,7 +460,6 @@ app.layout = html.Div(
466
  style_data_conditional=[],
467
  sort_by=[{"column_id": "score", "direction": "desc"}],
468
  ),
469
-
470
  html.Div(
471
  [
472
  html.Div("Noise level (fraction of spikes replaced / corrupted)"),
@@ -482,7 +475,6 @@ app.layout = html.Div(
482
  ],
483
  style={"maxWidth": "1100px", "marginTop": "10px", "marginBottom": "10px"},
484
  ),
485
-
486
  dcc.Graph(
487
  id="embeddings-grid",
488
  style={"height": "800px"},
@@ -528,7 +520,6 @@ def update_outputs(mode: str, dataset_name: str, model_name: str, noise_level: f
528
  metric_direction_for_colors["score"] = "high_is_good"
529
 
530
  if mode == "by_model":
531
- # Rows = datasets (fixed model)
532
  rows = []
533
  for ds_name, df_ds in TOY_STORE.items():
534
  df_row = df_ds[df_ds["model"] == model_name]
@@ -559,9 +550,7 @@ def update_outputs(mode: str, dataset_name: str, model_name: str, noise_level: f
559
  noise_level=float(noise_level),
560
  n_cols=3,
561
  )
562
-
563
  else:
564
- # Rows = models (fixed dataset)
565
  df = TOY_STORE[dataset_name].copy()
566
 
567
  df["score"] = compute_mock_score(
@@ -583,7 +572,6 @@ def update_outputs(mode: str, dataset_name: str, model_name: str, noise_level: f
583
  n_cols=3,
584
  )
585
 
586
- # DataTable columns + trailing zeros formatting
587
  num_cols = [c for c in df.columns if c != id_col and pd.api.types.is_numeric_dtype(df[c])]
588
 
589
  columns = [{"name": id_col, "id": id_col}]
 
18
  "mae": "rgb(255, 245, 230)",
19
  }
20
 
 
 
 
 
 
 
 
21
  METRIC_DIRECTION = {
22
  "r2": "high_is_good",
23
  "rmse": "low_is_good",
 
27
  # Universal axis limits for better comparison across panels
28
  XYZ_RANGE: Tuple[float, float] = (-25.0, 25.0)
29
 
30
+ # Universal behavior-value range for color normalization (keeps colors comparable)
31
+ BEHAVIOR_RANGE: Tuple[float, float] = (0.0, 1.0)
32
+
33
+ # Continuous colorscale for regression target (Plotly built-in)
34
+ CONTINUOUS_COLORSCALE = "Viridis"
35
+
36
 
37
  def build_toy_benchmark() -> Dict[str, pd.DataFrame]:
38
  return {
 
102
  model_name: str,
103
  *,
104
  noise_level: float,
105
+ n_points: int = 750,
106
+ # Manifold geometry (at noise=0)
107
+ base_radius: float = 9.0,
108
+ base_thickness: float = 0.8,
109
+ # Per-panel transforms
110
+ transform_strength: float = 0.70,
111
+ shift_strength: float = 2.5,
112
+ warp_strength: float = 0.45,
113
+ # Noise injection (spike replacement)
114
  max_replace_frac: float = 0.60,
115
+ background_scale: float = 10.0,
116
  ) -> pd.DataFrame:
117
  """
118
+ Continuous target embedding.
119
+
120
+ Returns columns:
121
+ - x, y, z
122
+ - behavior_value in [0,1] (regression-style target)
123
 
124
  noise_level in [0,1]:
125
+ - increases thickness/noise around the manifold
126
+ - replaces a fraction of points with background noise spikes
 
127
  """
128
  nl = float(np.clip(noise_level, 0.0, 1.0))
129
 
130
  seed = abs(hash((dataset_name, model_name))) % (2**32)
131
  rng = np.random.default_rng(seed)
132
 
133
+ # Continuous behavior value (regression target)
134
+ t = rng.uniform(low=BEHAVIOR_RANGE[0], high=BEHAVIOR_RANGE[1], size=n_points)
135
+
136
+ # Base 1D manifold (a warped helix / loop) parameterized by t
137
+ # You can interpret t as the "behavioral state" / continuous label.
138
+ phase = 2.0 * np.pi * t
139
+ x0 = base_radius * (2.0 * t - 1.0)
140
+ y0 = base_radius * np.sin(phase)
141
+ z0 = 0.6 * base_radius * np.cos(phase)
142
 
143
+ X = np.stack([x0, y0, z0], axis=1)
 
 
 
 
 
 
144
 
145
+ # Thickness around the manifold increases with noise_level
146
+ thickness = base_thickness * (1.0 + 2.5 * nl)
147
+ X = X + thickness * rng.normal(size=(n_points, 3))
148
 
149
+ # Strong per-panel affine transform
150
  angles = rng.uniform(low=-np.pi, high=np.pi, size=3)
151
  cx, cy, cz = np.cos(angles)
152
  sx, sy, sz = np.sin(angles)
 
158
 
159
  scales = np.diag(1.0 + transform_strength * rng.normal(size=3))
160
  shear = np.eye(3)
161
+ shear[0, 1] = 0.30 * transform_strength * rng.normal()
162
+ shear[0, 2] = 0.30 * transform_strength * rng.normal()
163
+ shear[1, 2] = 0.30 * transform_strength * rng.normal()
164
 
165
+ A = R @ scales @ shear
166
  b = shift_strength * rng.normal(size=(3,))
167
 
168
+ X = X @ A.T + b
 
169
 
170
+ # Nonlinear warp (keeps panels visually distinct)
171
+ theta = warp_strength * (0.10 * X[:, 0] + 0.08 * X[:, 1])
172
+ ct = np.cos(theta)
173
+ st = np.sin(theta)
174
+ x_new = ct * X[:, 0] - st * X[:, 1]
175
+ y_new = st * X[:, 0] + ct * X[:, 1]
176
+ z_new = X[:, 2] + warp_strength * np.tanh(0.20 * X[:, 0]) * 2.2
177
+ X = np.stack([x_new, y_new, z_new], axis=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
+ # Replace a fraction with background noise spikes (increases mixing)
180
+ replace_frac = max_replace_frac * nl
181
+ n_replace = int(round(replace_frac * n_points))
182
+ if n_replace > 0:
183
+ idx_replace = rng.choice(n_points, size=n_replace, replace=False)
184
+ noise_bg = background_scale * rng.standard_t(df=3, size=(n_replace, 3))
185
+ X[idx_replace] = noise_bg
186
+
187
+ # Optional: also partially destroy label structure for replaced points
188
+ # (push their behavior_value toward random)
189
+ t[idx_replace] = rng.uniform(low=BEHAVIOR_RANGE[0], high=BEHAVIOR_RANGE[1], size=n_replace)
190
+
191
+ return pd.DataFrame(
192
+ {
193
+ "x": X[:, 0].astype(float),
194
+ "y": X[:, 1].astype(float),
195
+ "z": X[:, 2].astype(float),
196
+ "behavior_value": t.astype(float),
197
+ }
198
+ )
199
 
200
 
201
  def _parse_rgb(rgb: str) -> tuple[int, int, int]:
 
253
  low = col_min + (col_max - col_min) * (i / n_bins)
254
  high = col_min + (col_max - col_min) * ((i + 1) / n_bins)
255
 
256
+ intensity = (i + 1) / n_bins
257
  if direction == "low_is_good":
258
+ intensity = 1.0 - intensity
259
 
260
  styles.append(
261
  {
 
263
  "column_id": col,
264
  "filter_query": f"{{{col}}} >= {low} && {{{col}}} < {high}",
265
  },
266
+ "backgroundColor": _blend_rgb(white, dark_base, intensity),
267
  }
268
  )
269
 
 
287
 
288
 
289
  def _apply_universal_scene_ranges(fig: go.Figure, *, n_scenes: int) -> None:
 
 
 
290
  for idx in range(n_scenes):
291
  scene_id = "scene" if idx == 0 else f"scene{idx + 1}"
292
  fig.update_layout(
 
329
  md = model_for_each_panel[idx]
330
  df_emb = generate_mock_embedding(ds, md, noise_level=noise_level)
331
 
332
+ # Single trace: continuous color encodes regression target
333
+ # Use cmin/cmax so all panels share the same color normalization. :contentReference[oaicite:2]{index=2}
334
+ show_scale = (idx == 0)
335
+
336
+ fig.add_trace(
337
+ go.Scatter3d(
338
+ x=df_emb["x"],
339
+ y=df_emb["y"],
340
+ z=df_emb["z"],
341
+ mode="markers",
342
+ showlegend=False,
343
+ marker=dict(
344
+ size=2.5,
345
+ opacity=0.75,
346
+ color=df_emb["behavior_value"],
347
+ colorscale=CONTINUOUS_COLORSCALE,
348
+ cmin=float(BEHAVIOR_RANGE[0]),
349
+ cmax=float(BEHAVIOR_RANGE[1]),
350
+ showscale=show_scale,
351
+ colorbar=dict( # colorbar config supported via marker.colorbar :contentReference[oaicite:3]{index=3}
352
+ title=dict(text="behavior"),
353
+ tickformat=".2f",
354
+ len=0.70,
355
  ),
 
356
  ),
357
+ hovertemplate=(
358
+ "behavior=%{marker.color:.3f}<br>"
359
+ "x=%{x:.3f}<br>y=%{y:.3f}<br>z=%{z:.3f}<extra></extra>"
360
+ ),
361
+ ),
362
+ row=r,
363
+ col=c,
364
+ )
365
 
366
  _apply_universal_scene_ranges(fig, n_scenes=n_panels)
367
 
 
381
  app.layout = html.Div(
382
  [
383
  html.H3("Neural Decoder Benchmarking"),
 
384
  html.Div(
385
  [
386
  html.Div("Mode"),
 
396
  ],
397
  style={"marginBottom": "12px"},
398
  ),
 
399
  html.Div(
400
  [
401
  html.Div(
 
440
  ],
441
  style={"display": "flex", "gap": "16px", "alignItems": "flex-end"},
442
  ),
 
443
  dash_table.DataTable(
444
  id="metrics-table",
445
  columns=[],
 
460
  style_data_conditional=[],
461
  sort_by=[{"column_id": "score", "direction": "desc"}],
462
  ),
 
463
  html.Div(
464
  [
465
  html.Div("Noise level (fraction of spikes replaced / corrupted)"),
 
475
  ],
476
  style={"maxWidth": "1100px", "marginTop": "10px", "marginBottom": "10px"},
477
  ),
 
478
  dcc.Graph(
479
  id="embeddings-grid",
480
  style={"height": "800px"},
 
520
  metric_direction_for_colors["score"] = "high_is_good"
521
 
522
  if mode == "by_model":
 
523
  rows = []
524
  for ds_name, df_ds in TOY_STORE.items():
525
  df_row = df_ds[df_ds["model"] == model_name]
 
550
  noise_level=float(noise_level),
551
  n_cols=3,
552
  )
 
553
  else:
 
554
  df = TOY_STORE[dataset_name].copy()
555
 
556
  df["score"] = compute_mock_score(
 
572
  n_cols=3,
573
  )
574
 
 
575
  num_cols = [c for c in df.columns if c != id_col and pd.api.types.is_numeric_dtype(df[c])]
576
 
577
  columns = [{"name": id_col, "id": id_col}]