nanye commited on
Commit
24d4e3b
·
1 Parent(s): 82c5de9

- added support for builtin datasets (2D or higher dimensional)

Browse files

- fixed regularization path bug (weight increasing in strength)
- improve plots (use linspace instead of logspace for regularization strength, use average loss)

Files changed (4) hide show
  1. backend/src/logic.py +40 -12
  2. backend/src/manager.py +19 -8
  3. frontends/gradio/main.py +154 -153
  4. usage.md +3 -3
backend/src/logic.py CHANGED
@@ -19,6 +19,37 @@ class Dataset:
19
  x1: list[float]
20
  x2: list[float]
21
  y: list[float]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
 
24
  @dataclass
@@ -189,19 +220,15 @@ def compute_regularization_path(
189
  loss_type: Literal["l1", "l2"],
190
  regularizer_type: Literal["l1", "l2"],
191
  ) -> np.ndarray:
192
- x1 = np.array(dataset.x1)
193
- x2 = np.array(dataset.x2)
194
  y = np.array(dataset.y)
195
-
196
- X = np.stack([x1, x2], axis=1) # (N, 2)
197
-
198
- w = cp.Variable(2)
199
  lambd = cp.Parameter(nonneg=True)
200
 
201
  if loss_type == "l2":
202
- loss_expr = cp.sum_squares(y - X @ w)
203
  elif loss_type == "l1":
204
- loss_expr = cp.norm1(y - X @ w)
205
  else:
206
  raise ValueError(f"Unknown loss type: {loss_type}")
207
 
@@ -216,20 +243,21 @@ def compute_regularization_path(
216
  problem = cp.Problem(objective)
217
 
218
  # todo - user defined reg levels
219
- reg_levels = np.logspace(-4, 4, 100)
220
 
221
  # solve with reg levels in descending order for using warm start
222
  w_solutions = []
223
  for reg_level in sorted(reg_levels, reverse=True):
224
  lambd.value = reg_level
225
  problem.solve(warm_start=True)
 
226
 
227
  if w.value is None:
228
- w_solutions.append(np.array([np.nan, np.nan]))
229
  else:
230
  w_solutions.append(w.value.copy())
231
 
232
- return np.array(w_solutions)
233
 
234
 
235
  def compute_plot_values(
@@ -284,4 +312,4 @@ def compute_suggested_settings(
284
 
285
  reg_levels = [i / 4 * opt_norm for i in range(1, 4)]
286
 
287
- return w1_range, w2_range, reg_levels
 
19
  x1: list[float]
20
  x2: list[float]
21
  y: list[float]
22
+ features: np.ndarray | None = None
23
+ feature_names: list[str] | None = None
24
+
25
+ @property
26
+ def X(self) -> np.ndarray:
27
+ return self.features if self.features is not None else np.column_stack([self.x1, self.x2])
28
+
29
+
30
+ PREDEFINED_DATASETS = ["Diabetes", "Synthetic (2 covariates)", "Synthetic (10 covariates)"]
31
+
32
+
33
+ def load_predefined_dataset(name: str) -> Dataset:
34
+ from sklearn.datasets import load_diabetes, make_regression
35
+ from sklearn.preprocessing import StandardScaler
36
+
37
+ if name == "Diabetes":
38
+ data = load_diabetes()
39
+ X, y = data.data, data.target
40
+ names = list(data.feature_names)
41
+ elif name in PREDEFINED_DATASETS[1:]:
42
+ n_features = 2 if name == "Synthetic (2 covariates)" else 10
43
+ X, y = make_regression(
44
+ n_samples=100, n_features=n_features,
45
+ n_informative=min(5, n_features), noise=10, random_state=42,
46
+ )
47
+ names = [f"x{i + 1}" for i in range(n_features)]
48
+ else:
49
+ raise ValueError(f"Unknown predefined dataset: {name}")
50
+ X = StandardScaler().fit_transform(X)
51
+ y = y - np.mean(y)
52
+ return Dataset(X[:, 0].tolist(), X[:, 1].tolist(), y.tolist(), X, names)
53
 
54
 
55
  @dataclass
 
220
  loss_type: Literal["l1", "l2"],
221
  regularizer_type: Literal["l1", "l2"],
222
  ) -> np.ndarray:
223
+ X = dataset.X
 
224
  y = np.array(dataset.y)
225
+ w = cp.Variable(X.shape[1])
 
 
 
226
  lambd = cp.Parameter(nonneg=True)
227
 
228
  if loss_type == "l2":
229
+ loss_expr = cp.sum_squares(y - X @ w)/(2*len(X))
230
  elif loss_type == "l1":
231
+ loss_expr = cp.norm1(y - X @ w)/(len(X))
232
  else:
233
  raise ValueError(f"Unknown loss type: {loss_type}")
234
 
 
243
  problem = cp.Problem(objective)
244
 
245
  # todo - user defined reg levels
246
+ reg_levels = np.linspace(0.001, 50, 100)
247
 
248
  # solve with reg levels in descending order for using warm start
249
  w_solutions = []
250
  for reg_level in sorted(reg_levels, reverse=True):
251
  lambd.value = reg_level
252
  problem.solve(warm_start=True)
253
+ #print(problem.status)
254
 
255
  if w.value is None:
256
+ w_solutions.append(np.full(X.shape[1], np.nan))
257
  else:
258
  w_solutions.append(w.value.copy())
259
 
260
+ return np.array(w_solutions[::-1])
261
 
262
 
263
  def compute_plot_values(
 
312
 
313
  reg_levels = [i / 4 * opt_norm for i in range(1, 4)]
314
 
315
+ return w1_range, w2_range, reg_levels
backend/src/manager.py CHANGED
@@ -19,6 +19,9 @@ from logic import (
19
  compute_suggested_settings,
20
  generate_dataset,
21
  load_dataset_from_csv,
 
 
 
22
  )
23
 
24
 
@@ -79,6 +82,8 @@ class Manager:
79
  x2_col: int,
80
  y_col: int,
81
  ) -> Dataset:
 
 
82
  if dataset_type == "Generate":
83
  x1, x2 = symbols("x1 x2")
84
  allowed_locals = {
@@ -191,7 +196,7 @@ class Manager:
191
  loss_type: str,
192
  regularizer_type: str,
193
  resolution: int,
194
- ) -> tuple[Manager, Figure, Figure, Figure]:
195
  self.update_dataset(
196
  dataset_type,
197
  function,
@@ -207,6 +212,11 @@ class Manager:
207
  y_col,
208
  )
209
 
 
 
 
 
 
210
  self.compute_plots_data(
211
  loss_type,
212
  regularizer_type,
@@ -218,8 +228,8 @@ class Manager:
218
 
219
  contour_plot = self._generate_contour_plot(self.plots_data)
220
  data_plot = self._generate_data_plot(self.dataset)
221
- strength_plot = self._generate_strength_plot(self.plots_data.path)
222
- return self, contour_plot, data_plot, strength_plot
223
 
224
  @staticmethod
225
  def _generate_contour_plot(plots_data: PlotsData) -> Figure:
@@ -299,16 +309,17 @@ class Manager:
299
  return fig
300
 
301
  @staticmethod
302
- def _generate_strength_plot(path: np.ndarray) -> Figure:
303
- reg_levels = np.logspace(-4, 4, path.shape[0])
304
 
305
  fig, ax = plt.subplots(figsize=(8, 6))
306
  ax.set_xlabel("Regularization Strength")
307
  ax.set_ylabel("Weight")
308
 
309
- ax.plot(reg_levels, path[:, 0], "r-", label="w1")
310
- ax.plot(reg_levels, path[:, 1], "b-", label="w2")
311
- ax.set_xscale("log")
 
312
  ax.legend()
313
  ax.grid(True)
314
  return fig
 
19
  compute_suggested_settings,
20
  generate_dataset,
21
  load_dataset_from_csv,
22
+ load_predefined_dataset,
23
+ PREDEFINED_DATASETS,
24
+ compute_regularization_path,
25
  )
26
 
27
 
 
82
  x2_col: int,
83
  y_col: int,
84
  ) -> Dataset:
85
+ if dataset_type in PREDEFINED_DATASETS:
86
+ return load_predefined_dataset(dataset_type)
87
  if dataset_type == "Generate":
88
  x1, x2 = symbols("x1 x2")
89
  allowed_locals = {
 
196
  loss_type: str,
197
  regularizer_type: str,
198
  resolution: int,
199
+ ) -> tuple[Manager, Figure | None, Figure | None, Figure]:
200
  self.update_dataset(
201
  dataset_type,
202
  function,
 
212
  y_col,
213
  )
214
 
215
+ if self.dataset.X.shape[1] > 2:
216
+ self.plots_data = None
217
+ path = compute_regularization_path(self.dataset, loss_type, regularizer_type)
218
+ return self, None, None, self._generate_path_plot(path, self.dataset.feature_names)
219
+
220
  self.compute_plots_data(
221
  loss_type,
222
  regularizer_type,
 
228
 
229
  contour_plot = self._generate_contour_plot(self.plots_data)
230
  data_plot = self._generate_data_plot(self.dataset)
231
+ path_plot = self._generate_path_plot(self.plots_data.path, self.dataset.feature_names)
232
+ return self, contour_plot, data_plot, path_plot
233
 
234
  @staticmethod
235
  def _generate_contour_plot(plots_data: PlotsData) -> Figure:
 
309
  return fig
310
 
311
  @staticmethod
312
+ def _generate_path_plot(path: np.ndarray, feature_names: list[str] | None = None) -> Figure:
313
+ reg_levels = np.linspace(0.001, 50, path.shape[0])
314
 
315
  fig, ax = plt.subplots(figsize=(8, 6))
316
  ax.set_xlabel("Regularization Strength")
317
  ax.set_ylabel("Weight")
318
 
319
+ for i in range(path.shape[1]):
320
+ label = feature_names[i] if feature_names else f"w{i + 1}"
321
+ ax.plot(reg_levels, path[:, i], label=label)
322
+ #ax.set_xscale("log")
323
  ax.legend()
324
  ax.grid(True)
325
  return fig
frontends/gradio/main.py CHANGED
@@ -1,7 +1,6 @@
1
  from typing import Literal
2
 
3
  import gradio as gr
4
- from matplotlib.figure import Figure
5
 
6
  import sys
7
  from pathlib import Path
@@ -12,6 +11,7 @@ if str(backend_src) not in sys.path:
12
  sys.path.append(str(backend_src))
13
 
14
  from manager import Manager
 
15
 
16
  CSS = """
17
  .hidden-button {
@@ -53,6 +53,7 @@ def handle_dataset_type_change(dataset_type: Literal["Generate", "CSV"]):
53
  def handle_generate_plots(
54
  manager: Manager,
55
  dataset_type: str,
 
56
  function: str,
57
  x1_range_input: str,
58
  x2_range_input: str,
@@ -67,10 +68,10 @@ def handle_generate_plots(
67
  loss_type: str,
68
  regularizer_type: str,
69
  resolution: int,
70
- ) -> tuple[Manager, Figure, Figure, Figure]:
71
  try:
72
- return manager.handle_generate_plots(
73
- dataset_type,
74
  function,
75
  x1_range_input,
76
  x2_range_input,
@@ -86,6 +87,10 @@ def handle_generate_plots(
86
  regularizer_type,
87
  resolution,
88
  )
 
 
 
 
89
  except Exception as e:
90
  raise gr.Error("Error generating plots: " + str(e))
91
 
@@ -111,7 +116,7 @@ def launch():
111
  default_resolution = 100
112
 
113
  manager = Manager()
114
- manager, default_contour_plot, default_data_plot, default_strength_plot = manager.handle_generate_plots(
115
  default_dataset_type,
116
  default_function,
117
  default_x1_range,
@@ -136,116 +141,127 @@ def launch():
136
 
137
  with gr.Row():
138
  with gr.Column(scale=2):
139
- with gr.Tab("Contours"):
140
- main_plot = gr.Plot(value=default_contour_plot)
141
- with gr.Tab("Data"):
142
- data_plot = gr.Plot(value=default_data_plot)
143
- with gr.Tab("Strength"):
144
- strength_plot = gr.Plot(value=default_strength_plot)
 
145
 
146
  with gr.Column(scale=1):
147
  with gr.Tab("Data"):
148
- with gr.Row():
149
- dataset_type = gr.Radio(
150
- label="Dataset type",
151
- choices=["Generate", "CSV"],
152
- value=default_dataset_type,
153
- interactive=True,
154
- )
155
-
156
- with gr.Row():
157
- function = gr.Textbox(
158
- label="Function (in terms of x1 and x2)",
159
- value=default_function,
160
- interactive=True,
161
- )
162
-
163
- with gr.Row():
164
- x1_textbox = gr.Textbox(
165
- label="x1 range",
166
- value=default_x1_range,
167
- interactive=True,
168
- )
169
- x2_textbox = gr.Textbox(
170
- label="x2 range",
171
- value=default_x2_range,
172
- interactive=True,
173
- )
 
 
 
 
 
 
174
 
175
- with gr.Row():
176
- x_selection_method = gr.Radio(
177
- label="How to select x points",
178
- choices=["Grid", "Random"],
179
- value=default_x_selection_method,
180
- interactive=True,
181
- )
182
-
183
- with gr.Row():
184
- sigma = gr.Number(
185
- label="Gaussian noise standard deviation",
186
- value=default_sigma,
187
- interactive=True,
188
- )
189
-
190
- with gr.Row():
191
- nsample = gr.Slider(
192
- label="Number of points",
193
- value=default_num_points,
194
- interactive=True,
195
- minimum=2, # todo - set to 1 after fixing weird cases
196
- maximum=100,
197
- step=1,
198
- )
199
-
200
- with gr.Row():
201
- csv_file = gr.File(
202
- label="Upload CSV file - must have columns: (x1, x2, y)",
203
- file_types=[".csv"],
204
- visible=False,
205
- )
206
-
207
- with gr.Row():
208
- has_header = gr.Checkbox(
209
- label="CSV has header row",
210
- value=default_has_header,
211
- visible=False,
212
- )
213
-
214
- with gr.Row():
215
- x1_col = gr.Number(
216
- label="x1 column index (0-based)",
217
- value=default_x1_col,
218
- visible=False,
219
- )
220
- x2_col = gr.Number(
221
- label="x2 column index (0-based)",
222
- value=default_x2_col,
223
- visible=False,
224
- )
225
 
226
- with gr.Row():
227
- y_col = gr.Number(
228
- label="y column index (0-based)",
229
- value=default_y_col,
230
- visible=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  )
232
-
233
- dataset_type.change(
234
- fn=handle_dataset_type_change,
235
- inputs=[dataset_type],
236
- outputs=[
237
- function,
238
- x1_textbox,
239
- x2_textbox,
240
- x_selection_method,
241
- sigma,
242
- nsample,
243
- csv_file,
244
- has_header,
245
- x1_col,
246
- x2_col,
247
- y_col,
248
- ],
249
  )
250
  regenerate_plots_button1 = gr.Button("Regenerate Plots")
251
 
@@ -278,50 +294,35 @@ def launch():
278
  with open(root_dir / "usage.md", "r") as f:
279
  gr.Markdown(f.read())
280
 
281
- regenerate_plots_button1.click(
282
- fn=handle_generate_plots,
283
- inputs=[
284
- manager_state,
285
- dataset_type,
286
- function,
287
- x1_textbox,
288
- x2_textbox,
289
- x_selection_method,
290
- sigma,
291
- nsample,
292
- csv_file,
293
- has_header,
294
- x1_col,
295
- x2_col,
296
- y_col,
297
- loss_type_dropdown,
298
- regularizer_type_dropdown,
299
- resolution_slider,
300
- ],
301
- outputs=[manager_state, main_plot, data_plot, strength_plot],
302
- )
303
- regenerate_plots_button2.click(
304
- fn=handle_generate_plots,
305
- inputs=[
306
- manager_state,
307
- dataset_type,
308
- function,
309
- x1_textbox,
310
- x2_textbox,
311
- x_selection_method,
312
- sigma,
313
- nsample,
314
- csv_file,
315
- has_header,
316
- x1_col,
317
- x2_col,
318
- y_col,
319
- loss_type_dropdown,
320
- regularizer_type_dropdown,
321
- resolution_slider,
322
- ],
323
- outputs=[manager_state, main_plot, data_plot, strength_plot],
324
- )
325
 
326
 
327
  demo.launch(css=CSS)
 
1
  from typing import Literal
2
 
3
  import gradio as gr
 
4
 
5
  import sys
6
  from pathlib import Path
 
11
  sys.path.append(str(backend_src))
12
 
13
  from manager import Manager
14
+ from logic import PREDEFINED_DATASETS
15
 
16
  CSS = """
17
  .hidden-button {
 
53
  def handle_generate_plots(
54
  manager: Manager,
55
  dataset_type: str,
56
+ dataset_name: str,
57
  function: str,
58
  x1_range_input: str,
59
  x2_range_input: str,
 
68
  loss_type: str,
69
  regularizer_type: str,
70
  resolution: int,
71
+ ):
72
  try:
73
+ result = manager.handle_generate_plots(
74
+ dataset_type if dataset_name == "Custom" else dataset_name,
75
  function,
76
  x1_range_input,
77
  x2_range_input,
 
87
  regularizer_type,
88
  resolution,
89
  )
90
+ show_2d = manager.dataset.X.shape[1] == 2
91
+ return (*result, gr.update(visible=show_2d), gr.update(visible=show_2d),
92
+ gr.update(selected="contours" if show_2d else "path"),
93
+ gr.update(visible=show_2d))
94
  except Exception as e:
95
  raise gr.Error("Error generating plots: " + str(e))
96
 
 
116
  default_resolution = 100
117
 
118
  manager = Manager()
119
+ manager, default_contour_plot, default_data_plot, default_path_plot = manager.handle_generate_plots(
120
  default_dataset_type,
121
  default_function,
122
  default_x1_range,
 
141
 
142
  with gr.Row():
143
  with gr.Column(scale=2):
144
+ with gr.Tabs() as plot_tabs:
145
+ with gr.Tab("Contours", id="contours") as contour_tab:
146
+ main_plot = gr.Plot(value=default_contour_plot)
147
+ with gr.Tab("Data", id="data") as data_tab:
148
+ data_plot = gr.Plot(value=default_data_plot)
149
+ with gr.Tab("Regularization path", id="path"):
150
+ path_plot = gr.Plot(value=default_path_plot)
151
 
152
  with gr.Column(scale=1):
153
  with gr.Tab("Data"):
154
+ dataset_name = gr.Dropdown(
155
+ label="Dataset", choices=["Custom", *PREDEFINED_DATASETS],
156
+ value="Custom", interactive=True,
157
+ )
158
+ gr.Markdown("Predefined datasets use standardized covariates and a centered target.")
159
+ with gr.Group() as custom_controls:
160
+ with gr.Row():
161
+ dataset_type = gr.Radio(
162
+ label="Dataset type",
163
+ choices=["Generate", "CSV"],
164
+ value=default_dataset_type,
165
+ interactive=True,
166
+ )
167
+
168
+ with gr.Row():
169
+ function = gr.Textbox(
170
+ label="Function (in terms of x1 and x2)",
171
+ value=default_function,
172
+ interactive=True,
173
+ )
174
+
175
+ with gr.Row():
176
+ x1_textbox = gr.Textbox(
177
+ label="x1 range",
178
+ value=default_x1_range,
179
+ interactive=True,
180
+ )
181
+ x2_textbox = gr.Textbox(
182
+ label="x2 range",
183
+ value=default_x2_range,
184
+ interactive=True,
185
+ )
186
 
187
+ with gr.Row():
188
+ x_selection_method = gr.Radio(
189
+ label="How to select x points",
190
+ choices=["Grid", "Random"],
191
+ value=default_x_selection_method,
192
+ interactive=True,
193
+ )
194
+
195
+ with gr.Row():
196
+ sigma = gr.Number(
197
+ label="Gaussian noise standard deviation",
198
+ value=default_sigma,
199
+ interactive=True,
200
+ )
201
+
202
+ with gr.Row():
203
+ nsample = gr.Slider(
204
+ label="Number of points",
205
+ value=default_num_points,
206
+ interactive=True,
207
+ minimum=2, # todo - set to 1 after fixing weird cases
208
+ maximum=100,
209
+ step=1,
210
+ )
211
+
212
+ with gr.Row():
213
+ csv_file = gr.File(
214
+ label="Upload CSV file - must have columns: (x1, x2, y)",
215
+ file_types=[".csv"],
216
+ visible=False,
217
+ )
218
+
219
+ with gr.Row():
220
+ has_header = gr.Checkbox(
221
+ label="CSV has header row",
222
+ value=default_has_header,
223
+ visible=False,
224
+ )
225
+
226
+ with gr.Row():
227
+ x1_col = gr.Number(
228
+ label="x1 column index (0-based)",
229
+ value=default_x1_col,
230
+ visible=False,
231
+ )
232
+ x2_col = gr.Number(
233
+ label="x2 column index (0-based)",
234
+ value=default_x2_col,
235
+ visible=False,
236
+ )
237
 
238
+ with gr.Row():
239
+ y_col = gr.Number(
240
+ label="y column index (0-based)",
241
+ value=default_y_col,
242
+ visible=False,
243
+ )
244
+
245
+ dataset_type.change(
246
+ fn=handle_dataset_type_change,
247
+ inputs=[dataset_type],
248
+ outputs=[
249
+ function,
250
+ x1_textbox,
251
+ x2_textbox,
252
+ x_selection_method,
253
+ sigma,
254
+ nsample,
255
+ csv_file,
256
+ has_header,
257
+ x1_col,
258
+ x2_col,
259
+ y_col,
260
+ ],
261
  )
262
+ dataset_name.change(
263
+ fn=lambda name: gr.update(visible=name == "Custom"),
264
+ inputs=[dataset_name], outputs=[custom_controls],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  )
266
  regenerate_plots_button1 = gr.Button("Regenerate Plots")
267
 
 
294
  with open(root_dir / "usage.md", "r") as f:
295
  gr.Markdown(f.read())
296
 
297
+ for event in (
298
+ regenerate_plots_button1.click,
299
+ regenerate_plots_button2.click,
300
+ dataset_name.change,
301
+ ):
302
+ event(
303
+ fn=handle_generate_plots,
304
+ inputs=[
305
+ manager_state,
306
+ dataset_type,
307
+ dataset_name,
308
+ function,
309
+ x1_textbox,
310
+ x2_textbox,
311
+ x_selection_method,
312
+ sigma,
313
+ nsample,
314
+ csv_file,
315
+ has_header,
316
+ x1_col,
317
+ x2_col,
318
+ y_col,
319
+ loss_type_dropdown,
320
+ regularizer_type_dropdown,
321
+ resolution_slider,
322
+ ],
323
+ outputs=[manager_state, main_plot, data_plot, path_plot,
324
+ contour_tab, data_tab, plot_tabs, resolution_slider],
325
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
 
328
  demo.launch(css=CSS)
usage.md CHANGED
@@ -6,7 +6,7 @@ In this visualizer, you visualize the loss and regularization of linear regressi
6
 
7
  ### Data
8
 
9
- In the data tab, generate a dataset using a custom function or upload a csv file. Make sure the function depends on `x1` and `x2`.
10
 
11
 
12
  ### Regularization
@@ -30,7 +30,7 @@ change as the regularization strength moves from 10^{-4} to 10^4 (log scale). Yo
30
  The data plot shows the data points used to compute the loss values.
31
 
32
 
33
- ### Strength Plot
34
 
35
- The strenght plot is a line plot of regularization strength vs weight value for `w1` and `w2`. These match the values shown on the red regularization path in the contour plot.
36
 
 
6
 
7
  ### Data
8
 
9
+ In the data tab, select a predefined regression dataset, or choose **Custom** to generate a dataset using a custom function or upload a CSV file. Predefined datasets have standardized covariates and a centered target. Make sure the function depends on `x1` and `x2`.
10
 
11
 
12
  ### Regularization
 
30
  The data plot shows the data points used to compute the loss values.
31
 
32
 
33
+ ### Regularization path plot
34
 
35
+ The regularization path plot shows regularization strength versus coefficient value for every covariate. Datasets with more than two covariates show only this plot. These match the values shown on the red regularization path in the contour plot.
36