darabos commited on
Commit
c7da3a5
·
2 Parent(s): 8d6b9c3 a4e8413

Merge pull request #114 from biggraph/darabos-repeats

Browse files
.github/workflows/test.yaml CHANGED
@@ -81,6 +81,7 @@ jobs:
81
  - name: Run Playwright tests
82
  run: |
83
  cd lynxkite-app/web
 
84
  npm run test
85
 
86
  - uses: actions/upload-artifact@v4
 
81
  - name: Run Playwright tests
82
  run: |
83
  cd lynxkite-app/web
84
+ npm run build
85
  npm run test
86
 
87
  - uses: actions/upload-artifact@v4
examples/Model definition CHANGED
@@ -1,171 +1,278 @@
1
  {
2
  "edges": [
3
  {
4
- "id": "Input: embedding 1 Linear 1",
5
- "source": "Input: embedding 1",
6
- "sourceHandle": "x",
7
- "target": "Linear 1",
 
 
 
 
 
 
 
8
  "targetHandle": "x"
9
  },
10
  {
11
- "id": "Input: label 1 MSE loss 1",
12
- "source": "Input: label 1",
13
- "sourceHandle": "y",
14
- "target": "MSE loss 1",
15
  "targetHandle": "y"
16
  },
17
  {
18
- "id": "Linear 1 Activation 2",
19
- "source": "Linear 1",
 
 
 
 
 
 
 
20
  "sourceHandle": "x",
21
- "target": "Activation 2",
22
  "targetHandle": "x"
23
  },
24
  {
25
- "id": "Activation 2 MSE loss 1",
26
- "source": "Activation 2",
27
- "sourceHandle": "x",
28
- "target": "MSE loss 1",
29
  "targetHandle": "x"
30
  },
31
  {
32
- "id": "MSE loss 1 Optimizer 2",
33
- "source": "MSE loss 1",
34
- "sourceHandle": "loss",
35
- "target": "Optimizer 2",
36
- "targetHandle": "loss"
37
  }
38
  ],
39
  "env": "PyTorch model",
40
  "nodes": [
41
  {
42
  "data": {
 
 
43
  "display": null,
44
  "error": null,
 
45
  "meta": {
46
- "inputs": {},
47
- "name": "Input: embedding",
48
- "outputs": {
49
- "x": {
50
- "name": "x",
51
- "position": "top",
52
  "type": {
53
  "type": "tensor"
54
  }
55
  }
56
  },
57
- "params": {},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  "type": "basic"
59
  },
60
- "params": {},
 
 
 
61
  "status": "planned",
62
- "title": "Input: embedding"
63
  },
64
  "dragHandle": ".bg-primary",
65
- "height": 200.0,
66
- "id": "Input: embedding 1",
67
  "position": {
68
- "x": 91.0,
69
- "y": 266.0
70
  },
71
  "type": "basic",
72
- "width": 200.0
73
  },
74
  {
75
  "data": {
 
 
76
  "display": null,
77
  "error": null,
 
78
  "meta": {
79
  "inputs": {
80
  "x": {
81
  "name": "x",
82
  "position": "bottom",
83
  "type": {
84
- "type": "tensor"
85
  }
86
  }
87
  },
88
- "name": "Linear",
89
  "outputs": {
90
- "x": {
91
- "name": "x",
92
  "position": "top",
93
  "type": {
94
- "type": "tensor"
95
  }
96
  }
97
  },
98
  "params": {
99
- "output_dim": {
100
- "default": "same",
101
- "name": "output_dim",
102
  "type": {
103
- "type": "<class 'str'>"
 
 
 
 
 
104
  }
105
  }
106
  },
 
 
 
 
107
  "type": "basic"
108
  },
109
  "params": {
110
- "output_dim": "same"
111
  },
112
  "status": "planned",
113
- "title": "Linear"
114
  },
115
  "dragHandle": ".bg-primary",
116
  "height": 200.0,
117
- "id": "Linear 1",
118
  "position": {
119
- "x": 86.0,
120
- "y": 33.0
121
  },
122
  "type": "basic",
123
  "width": 200.0
124
  },
125
  {
126
  "data": {
 
 
127
  "display": null,
128
  "error": null,
 
129
  "meta": {
130
- "inputs": {
 
 
131
  "x": {
132
  "name": "x",
133
- "position": "bottom",
134
  "type": {
135
  "type": "tensor"
136
  }
137
- },
138
- "y": {
139
- "name": "y",
140
- "position": "bottom",
 
 
141
  "type": {
142
- "type": "tensor"
143
  }
144
  }
145
  },
146
- "name": "MSE loss",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  "outputs": {
148
- "loss": {
149
- "name": "loss",
150
  "position": "top",
151
  "type": {
152
  "type": "tensor"
153
  }
154
  }
155
  },
156
- "params": {},
 
 
 
 
 
 
 
 
 
 
 
 
157
  "type": "basic"
158
  },
159
- "params": {},
 
 
160
  "status": "planned",
161
- "title": "MSE loss"
162
  },
163
  "dragHandle": ".bg-primary",
164
  "height": 200.0,
165
- "id": "MSE loss 1",
166
  "position": {
167
- "x": 315.0,
168
- "y": -510.0
169
  },
170
  "type": "basic",
171
  "width": 200.0
@@ -174,31 +281,51 @@
174
  "data": {
175
  "display": null,
176
  "error": null,
 
177
  "meta": {
178
- "inputs": {},
179
- "name": "Input: label",
180
- "outputs": {
 
 
 
 
 
181
  "y": {
182
  "name": "y",
 
 
 
 
 
 
 
 
 
 
183
  "position": "top",
184
  "type": {
185
- "type": "tensor"
186
  }
187
  }
188
  },
189
  "params": {},
 
 
 
 
190
  "type": "basic"
191
  },
192
  "params": {},
193
  "status": "planned",
194
- "title": "Input: label"
195
  },
196
  "dragHandle": ".bg-primary",
197
  "height": 200.0,
198
- "id": "Input: label 1",
199
  "position": {
200
- "x": 615.0,
201
- "y": -165.0
202
  },
203
  "type": "basic",
204
  "width": 200.0
@@ -209,58 +336,62 @@
209
  "collapsed": null,
210
  "display": null,
211
  "error": null,
 
212
  "meta": {
213
  "inputs": {
214
- "x": {
215
- "name": "x",
216
- "position": "bottom",
217
  "type": {
218
  "type": "tensor"
219
  }
220
  }
221
  },
222
- "name": "Activation",
223
  "outputs": {
224
- "x": {
225
- "name": "x",
226
- "position": "top",
227
  "type": {
228
  "type": "tensor"
229
  }
230
  }
231
  },
232
  "params": {
233
- "type": {
234
- "default": "ReLU",
235
- "name": "type",
236
  "type": {
237
- "enum": [
238
- "ReLU",
239
- "Leaky ReLU",
240
- "Tanh",
241
- "Mish"
242
- ]
 
 
243
  }
244
  }
245
  },
246
  "position": {
247
- "x": 419.0,
248
- "y": 396.0
249
  },
250
  "type": "basic"
251
  },
252
  "params": {
253
- "type": "Leaky ReLU"
 
254
  },
255
  "status": "planned",
256
- "title": "Activation"
257
  },
258
  "dragHandle": ".bg-primary",
259
  "height": 200.0,
260
- "id": "Activation 2",
261
  "position": {
262
- "x": 93.61643829835265,
263
- "y": -229.04087132886406
264
  },
265
  "type": "basic",
266
  "width": 200.0
@@ -271,61 +402,54 @@
271
  "collapsed": null,
272
  "display": null,
273
  "error": null,
 
274
  "meta": {
275
  "inputs": {
276
- "loss": {
277
- "name": "loss",
278
  "position": "bottom",
279
  "type": {
280
- "type": "tensor"
281
  }
282
  }
283
  },
284
- "name": "Optimizer",
285
- "outputs": {},
286
- "params": {
287
- "lr": {
288
- "default": 0.001,
289
- "name": "lr",
290
  "type": {
291
- "type": "<class 'float'>"
292
  }
293
- },
294
- "type": {
295
- "default": "AdamW",
296
- "name": "type",
 
 
297
  "type": {
298
- "enum": [
299
- "AdamW",
300
- "Adafactor",
301
- "Adagrad",
302
- "SGD",
303
- "Lion",
304
- "Paged AdamW",
305
- "Galore AdamW"
306
- ]
307
  }
308
  }
309
  },
310
  "position": {
311
- "x": 526.0,
312
- "y": 116.0
313
  },
314
  "type": "basic"
315
  },
316
  "params": {
317
- "lr": "0.1",
318
- "type": "SGD"
319
  },
320
  "status": "planned",
321
- "title": "Optimizer"
322
  },
323
  "dragHandle": ".bg-primary",
324
  "height": 200.0,
325
- "id": "Optimizer 2",
326
  "position": {
327
- "x": 305.6132943499785,
328
- "y": -804.0094318451224
329
  },
330
  "type": "basic",
331
  "width": 200.0
 
1
  {
2
  "edges": [
3
  {
4
+ "id": "MSE loss 2 Optimizer 2",
5
+ "source": "MSE loss 2",
6
+ "sourceHandle": "output",
7
+ "target": "Optimizer 2",
8
+ "targetHandle": "loss"
9
+ },
10
+ {
11
+ "id": "Activation 1 MSE loss 2",
12
+ "source": "Activation 1",
13
+ "sourceHandle": "output",
14
+ "target": "MSE loss 2",
15
  "targetHandle": "x"
16
  },
17
  {
18
+ "id": "Input: tensor 3 MSE loss 2",
19
+ "source": "Input: tensor 3",
20
+ "sourceHandle": "x",
21
+ "target": "MSE loss 2",
22
  "targetHandle": "y"
23
  },
24
  {
25
+ "id": "Activation 1 Repeat 1",
26
+ "source": "Activation 1",
27
+ "sourceHandle": "output",
28
+ "target": "Repeat 1",
29
+ "targetHandle": "input"
30
+ },
31
+ {
32
+ "id": "Input: tensor 1 Linear 1",
33
+ "source": "Input: tensor 1",
34
  "sourceHandle": "x",
35
+ "target": "Linear 1",
36
  "targetHandle": "x"
37
  },
38
  {
39
+ "id": "Linear 1 Activation 1",
40
+ "source": "Linear 1",
41
+ "sourceHandle": "output",
42
+ "target": "Activation 1",
43
  "targetHandle": "x"
44
  },
45
  {
46
+ "id": "Repeat 1 Linear 1",
47
+ "source": "Repeat 1",
48
+ "sourceHandle": "output",
49
+ "target": "Linear 1",
50
+ "targetHandle": "x"
51
  }
52
  ],
53
  "env": "PyTorch model",
54
  "nodes": [
55
  {
56
  "data": {
57
+ "__execution_delay": 0.0,
58
+ "collapsed": null,
59
  "display": null,
60
  "error": null,
61
+ "input_metadata": null,
62
  "meta": {
63
+ "inputs": {
64
+ "loss": {
65
+ "name": "loss",
66
+ "position": "bottom",
 
 
67
  "type": {
68
  "type": "tensor"
69
  }
70
  }
71
  },
72
+ "name": "Optimizer",
73
+ "outputs": {},
74
+ "params": {
75
+ "lr": {
76
+ "default": 0.001,
77
+ "name": "lr",
78
+ "type": {
79
+ "type": "<class 'float'>"
80
+ }
81
+ },
82
+ "type": {
83
+ "default": "AdamW",
84
+ "name": "type",
85
+ "type": {
86
+ "enum": [
87
+ "AdamW",
88
+ "Adafactor",
89
+ "Adagrad",
90
+ "SGD",
91
+ "Lion",
92
+ "Paged AdamW",
93
+ "Galore AdamW"
94
+ ]
95
+ }
96
+ }
97
+ },
98
  "type": "basic"
99
  },
100
+ "params": {
101
+ "lr": "0.1",
102
+ "type": "SGD"
103
+ },
104
  "status": "planned",
105
+ "title": "Optimizer"
106
  },
107
  "dragHandle": ".bg-primary",
108
+ "height": 250.0,
109
+ "id": "Optimizer 2",
110
  "position": {
111
+ "x": 292.3983313429414,
112
+ "y": -853.8015246037802
113
  },
114
  "type": "basic",
115
+ "width": 232.0
116
  },
117
  {
118
  "data": {
119
+ "__execution_delay": 0.0,
120
+ "collapsed": null,
121
  "display": null,
122
  "error": null,
123
+ "input_metadata": null,
124
  "meta": {
125
  "inputs": {
126
  "x": {
127
  "name": "x",
128
  "position": "bottom",
129
  "type": {
130
+ "type": "<class 'inspect._empty'>"
131
  }
132
  }
133
  },
134
+ "name": "Activation",
135
  "outputs": {
136
+ "output": {
137
+ "name": "output",
138
  "position": "top",
139
  "type": {
140
+ "type": "None"
141
  }
142
  }
143
  },
144
  "params": {
145
+ "type": {
146
+ "default": "ReLU",
147
+ "name": "type",
148
  "type": {
149
+ "enum": [
150
+ "ReLU",
151
+ "Leaky_ReLU",
152
+ "Tanh",
153
+ "Mish"
154
+ ]
155
  }
156
  }
157
  },
158
+ "position": {
159
+ "x": 344.0,
160
+ "y": 384.0
161
+ },
162
  "type": "basic"
163
  },
164
  "params": {
165
+ "type": "Leaky_ReLU"
166
  },
167
  "status": "planned",
168
+ "title": "Activation"
169
  },
170
  "dragHandle": ".bg-primary",
171
  "height": 200.0,
172
+ "id": "Activation 1",
173
  "position": {
174
+ "x": 99.77615018185415,
175
+ "y": -249.43925929074078
176
  },
177
  "type": "basic",
178
  "width": 200.0
179
  },
180
  {
181
  "data": {
182
+ "__execution_delay": 0.0,
183
+ "collapsed": null,
184
  "display": null,
185
  "error": null,
186
+ "input_metadata": null,
187
  "meta": {
188
+ "inputs": {},
189
+ "name": "Input: tensor",
190
+ "outputs": {
191
  "x": {
192
  "name": "x",
193
+ "position": "top",
194
  "type": {
195
  "type": "tensor"
196
  }
197
+ }
198
+ },
199
+ "params": {
200
+ "name": {
201
+ "default": null,
202
+ "name": "name",
203
  "type": {
204
+ "type": "None"
205
  }
206
  }
207
  },
208
+ "position": {
209
+ "x": 258.0,
210
+ "y": 397.0
211
+ },
212
+ "type": "basic"
213
+ },
214
+ "params": {
215
+ "name": "X"
216
+ },
217
+ "status": "planned",
218
+ "title": "Input: tensor"
219
+ },
220
+ "dragHandle": ".bg-primary",
221
+ "height": 200.0,
222
+ "id": "Input: tensor 1",
223
+ "position": {
224
+ "x": 85.83561484252238,
225
+ "y": 293.6278596776366
226
+ },
227
+ "type": "basic",
228
+ "width": 200.0
229
+ },
230
+ {
231
+ "data": {
232
+ "__execution_delay": 0.0,
233
+ "collapsed": null,
234
+ "display": null,
235
+ "error": null,
236
+ "input_metadata": null,
237
+ "meta": {
238
+ "inputs": {},
239
+ "name": "Input: tensor",
240
  "outputs": {
241
+ "x": {
242
+ "name": "x",
243
  "position": "top",
244
  "type": {
245
  "type": "tensor"
246
  }
247
  }
248
  },
249
+ "params": {
250
+ "name": {
251
+ "default": null,
252
+ "name": "name",
253
+ "type": {
254
+ "type": "None"
255
+ }
256
+ }
257
+ },
258
+ "position": {
259
+ "x": 1169.0,
260
+ "y": 340.0
261
+ },
262
  "type": "basic"
263
  },
264
+ "params": {
265
+ "name": "Y"
266
+ },
267
  "status": "planned",
268
+ "title": "Input: tensor"
269
  },
270
  "dragHandle": ".bg-primary",
271
  "height": 200.0,
272
+ "id": "Input: tensor 3",
273
  "position": {
274
+ "x": 485.8840220312055,
275
+ "y": -149.86223034126274
276
  },
277
  "type": "basic",
278
  "width": 200.0
 
281
  "data": {
282
  "display": null,
283
  "error": null,
284
+ "input_metadata": null,
285
  "meta": {
286
+ "inputs": {
287
+ "x": {
288
+ "name": "x",
289
+ "position": "bottom",
290
+ "type": {
291
+ "type": "<class 'inspect._empty'>"
292
+ }
293
+ },
294
  "y": {
295
  "name": "y",
296
+ "position": "bottom",
297
+ "type": {
298
+ "type": "<class 'inspect._empty'>"
299
+ }
300
+ }
301
+ },
302
+ "name": "MSE loss",
303
+ "outputs": {
304
+ "output": {
305
+ "name": "output",
306
  "position": "top",
307
  "type": {
308
+ "type": "None"
309
  }
310
  }
311
  },
312
  "params": {},
313
+ "position": {
314
+ "x": 937.0,
315
+ "y": 270.0
316
+ },
317
  "type": "basic"
318
  },
319
  "params": {},
320
  "status": "planned",
321
+ "title": "MSE loss"
322
  },
323
  "dragHandle": ".bg-primary",
324
  "height": 200.0,
325
+ "id": "MSE loss 2",
326
  "position": {
327
+ "x": 309.4422414664647,
328
+ "y": -552.1056805642488
329
  },
330
  "type": "basic",
331
  "width": 200.0
 
336
  "collapsed": null,
337
  "display": null,
338
  "error": null,
339
+ "input_metadata": null,
340
  "meta": {
341
  "inputs": {
342
+ "input": {
343
+ "name": "input",
344
+ "position": "top",
345
  "type": {
346
  "type": "tensor"
347
  }
348
  }
349
  },
350
+ "name": "Repeat",
351
  "outputs": {
352
+ "output": {
353
+ "name": "output",
354
+ "position": "bottom",
355
  "type": {
356
  "type": "tensor"
357
  }
358
  }
359
  },
360
  "params": {
361
+ "same_weights": {
362
+ "default": false,
363
+ "name": "same_weights",
364
  "type": {
365
+ "type": "<class 'bool'>"
366
+ }
367
+ },
368
+ "times": {
369
+ "default": 1.0,
370
+ "name": "times",
371
+ "type": {
372
+ "type": "<class 'int'>"
373
  }
374
  }
375
  },
376
  "position": {
377
+ "x": 487.0,
378
+ "y": 443.0
379
  },
380
  "type": "basic"
381
  },
382
  "params": {
383
+ "same_weights": false,
384
+ "times": "2"
385
  },
386
  "status": "planned",
387
+ "title": "Repeat"
388
  },
389
  "dragHandle": ".bg-primary",
390
  "height": 200.0,
391
+ "id": "Repeat 1",
392
  "position": {
393
+ "x": -210.0,
394
+ "y": -135.0
395
  },
396
  "type": "basic",
397
  "width": 200.0
 
402
  "collapsed": null,
403
  "display": null,
404
  "error": null,
405
+ "input_metadata": null,
406
  "meta": {
407
  "inputs": {
408
+ "x": {
409
+ "name": "x",
410
  "position": "bottom",
411
  "type": {
412
+ "type": "<class 'inspect._empty'>"
413
  }
414
  }
415
  },
416
+ "name": "Linear",
417
+ "outputs": {
418
+ "output": {
419
+ "name": "output",
420
+ "position": "top",
 
421
  "type": {
422
+ "type": "None"
423
  }
424
+ }
425
+ },
426
+ "params": {
427
+ "output_dim": {
428
+ "default": 1024.0,
429
+ "name": "output_dim",
430
  "type": {
431
+ "type": "<class 'int'>"
 
 
 
 
 
 
 
 
432
  }
433
  }
434
  },
435
  "position": {
436
+ "x": 359.0,
437
+ "y": 310.0
438
  },
439
  "type": "basic"
440
  },
441
  "params": {
442
+ "output_dim": "4"
 
443
  },
444
  "status": "planned",
445
+ "title": "Linear"
446
  },
447
  "dragHandle": ".bg-primary",
448
  "height": 200.0,
449
+ "id": "Linear 1",
450
  "position": {
451
+ "x": 88.83370222907377,
452
+ "y": 48.642890099180136
453
  },
454
  "type": "basic",
455
  "width": 200.0
examples/Model use CHANGED
@@ -575,58 +575,58 @@
575
  "columns": [
576
  "x",
577
  "y",
578
- "predicted"
579
  ],
580
  "data": [
581
  [
582
- "[0.49691743 0.61873293 0.90698647 0.94486356]",
583
- "[1.49691749 1.61873293 1.90698647 1.94486356]",
584
- "[1.4993021488189697, 1.6404846906661987, 1.923316240310669, 1.9422152042388916]"
585
  ],
586
  [
587
- "[0.56922203 0.98222166 0.76851749 0.28615737]",
588
- "[1.56922197 1.9822216 1.76851749 1.28615737]",
589
- "[1.5835213661193848, 1.9884355068206787, 1.7694181203842163, 1.2917503118515015]"
590
  ],
591
  [
592
- "[0.90817457 0.89270043 0.38583666 0.66566533]",
593
- "[1.90817451 1.89270043 1.3858366 1.66566539]",
594
- "[1.9053494930267334, 1.9083378314971924, 1.3998609781265259, 1.6636812686920166]"
595
  ],
596
  [
597
- "[0.72795159 0.79317838 0.27832931 0.96576637]",
598
- "[1.72795153 1.79317832 1.27832937 1.96576643]",
599
- "[1.734963297843933, 1.8026459217071533, 1.2926064729690552, 1.9596911668777466]"
600
  ],
601
  [
602
- "[0.04508126 0.76880038 0.80721325 0.62542385]",
603
- "[1.04508126 1.76880038 1.80721331 1.62542391]",
604
- "[1.0830243825912476, 1.7584562301635742, 1.8005754947662354, 1.6277496814727783]"
605
  ],
606
  [
607
- "[0.6032477 0.83361369 0.18538666 0.19108021]",
608
- "[1.60324764 1.83361363 1.18538666 1.19108021]",
609
- "[1.6177492141723633, 1.8144152164459229, 1.1718573570251465, 1.1950569152832031]"
610
  ],
611
  [
612
- "[0.15064228 0.03198934 0.25754827 0.51484001]",
613
- "[1.15064228 1.03198934 1.25754833 1.51484001]",
614
- "[1.1556042432785034, 0.9955940246582031, 1.2316606044769287, 1.5150485038757324]"
615
  ],
616
  [
617
- "[0.48959708 0.48549271 0.32688856 0.356677 ]",
618
- "[1.48959708 1.48549271 1.32688856 1.35667706]",
619
- "[1.4930214881896973, 1.467790961265564, 1.3132573366165161, 1.3589863777160645]"
620
  ],
621
  [
622
- "[0.08107251 0.2602725 0.18861133 0.44833237]",
623
- "[1.08107257 1.2602725 1.18861127 1.44833231]",
624
- "[1.102121114730835, 1.2180893421173096, 1.160165548324585, 1.4495322704315186]"
625
  ],
626
  [
627
- "[0.68094063 0.45189077 0.22661722 0.37354094]",
628
- "[1.68094063 1.45189071 1.22661722 1.37354088]",
629
- "[1.6725687980651855, 1.4393560886383057, 1.2169336080551147, 1.3746893405914307]"
630
  ]
631
  ]
632
  },
@@ -688,13 +688,17 @@
688
  "[0.11693293 0.49860179 0.55020827 0.88832849]",
689
  "[1.11693287 1.49860179 1.55020833 1.88832855]"
690
  ],
 
 
 
 
691
  [
692
  "[0.50272274 0.54912758 0.17663097 0.79070699]",
693
  "[1.50272274 1.54912758 1.17663097 1.79070699]"
694
  ],
695
  [
696
- "[0.19908059 0.17570406 0.51475513 0.1893943 ]",
697
- "[1.19908059 1.175704 1.51475513 1.18939424]"
698
  ],
699
  [
700
  "[0.40167677 0.25953674 0.9407078 0.76308483]",
@@ -712,10 +716,18 @@
712
  "[0.62569475 0.9881897 0.83639616 0.9828859 ]",
713
  "[1.62569475 1.9881897 1.83639622 1.98288584]"
714
  ],
 
 
 
 
715
  [
716
  "[0.88776821 0.51636773 0.30333066 0.32230979]",
717
  "[1.88776827 1.51636767 1.30333066 1.32230973]"
718
  ],
 
 
 
 
719
  [
720
  "[0.48507756 0.80808765 0.77162558 0.47834778]",
721
  "[1.48507762 1.80808759 1.77162552 1.47834778]"
@@ -724,10 +736,6 @@
724
  "[0.68062544 0.98093534 0.14778823 0.53244978]",
725
  "[1.68062544 1.98093534 1.14778829 1.53244972]"
726
  ],
727
- [
728
- "[0.31518555 0.49643308 0.11509258 0.95458382]",
729
- "[1.31518555 1.49643302 1.11509252 1.95458388]"
730
- ],
731
  [
732
  "[0.79121011 0.54161114 0.69369799 0.1520769 ]",
733
  "[1.79121017 1.54161119 1.69369793 1.15207696]"
@@ -744,10 +752,6 @@
744
  "[0.94516498 0.08422136 0.5608117 0.07652664]",
745
  "[1.94516492 1.08422136 1.56081176 1.07652664]"
746
  ],
747
- [
748
- "[0.26661873 0.45946234 0.13510543 0.81294441]",
749
- "[1.26661873 1.4594624 1.13510537 1.81294441]"
750
- ],
751
  [
752
  "[0.30754459 0.77694583 0.09278506 0.38326019]",
753
  "[1.30754459 1.77694583 1.09278512 1.38326025]"
@@ -804,10 +808,6 @@
804
  "[0.73217702 0.65233225 0.44077861 0.33837909]",
805
  "[1.73217702 1.65233231 1.44077861 1.33837914]"
806
  ],
807
- [
808
- "[0.34084332 0.73018837 0.54168713 0.91440833]",
809
- "[1.34084332 1.73018837 1.54168713 1.91440833]"
810
- ],
811
  [
812
  "[0.60110539 0.3618983 0.32342511 0.98672163]",
813
  "[1.60110545 1.3618983 1.32342505 1.98672163]"
@@ -816,6 +816,10 @@
816
  "[0.77427191 0.21829212 0.12769502 0.74303615]",
817
  "[1.77427197 1.21829212 1.12769508 1.74303615]"
818
  ],
 
 
 
 
819
  [
820
  "[0.59812403 0.78395379 0.0291847 0.81814629]",
821
  "[1.59812403 1.78395379 1.0291847 1.81814623]"
@@ -840,18 +844,6 @@
840
  "[0.95928186 0.84273899 0.71514636 0.38619852]",
841
  "[1.95928192 1.84273899 1.7151463 1.38619852]"
842
  ],
843
- [
844
- "[0.32565445 0.90939188 0.07488042 0.13730896]",
845
- "[1.32565451 1.90939188 1.07488036 1.13730896]"
846
- ],
847
- [
848
- "[0.9829582 0.59269661 0.40120947 0.95487177]",
849
- "[1.9829582 1.59269667 1.40120947 1.95487177]"
850
- ],
851
- [
852
- "[0.79905868 0.89367443 0.75429088 0.3190186 ]",
853
- "[1.79905868 1.89367437 1.75429082 1.3190186 ]"
854
- ],
855
  [
856
  "[0.54914117 0.03810108 0.87531954 0.73044223]",
857
  "[1.54914117 1.03810108 1.87531948 1.73044229]"
@@ -876,10 +868,6 @@
876
  "[0.60075855 0.12234765 0.00614399 0.30560958]",
877
  "[1.60075855 1.12234759 1.00614405 1.30560958]"
878
  ],
879
- [
880
- "[0.39147133 0.29854035 0.84663737 0.58175623]",
881
- "[1.39147139 1.29854035 1.84663737 1.58175623]"
882
- ],
883
  [
884
  "[0.02162331 0.81861657 0.92468154 0.07808572]",
885
  "[1.02162337 1.81861663 1.92468154 1.07808566]"
@@ -924,6 +912,10 @@
924
  "[0.59492421 0.90274489 0.38069052 0.46101224]",
925
  "[1.59492421 1.90274489 1.38069057 1.46101224]"
926
  ],
 
 
 
 
927
  [
928
  "[0.12024075 0.21342516 0.56858408 0.58644271]",
929
  "[1.12024069 1.21342516 1.56858408 1.58644271]"
@@ -932,6 +924,14 @@
932
  "[0.91730917 0.22574073 0.09591609 0.33056474]",
933
  "[1.91730917 1.22574067 1.09591603 1.33056474]"
934
  ],
 
 
 
 
 
 
 
 
935
  [
936
  "[0.63235509 0.70352674 0.96188956 0.46240485]",
937
  "[1.63235509 1.70352674 1.96188951 1.46240485]"
@@ -948,10 +948,6 @@
948
  "[0.40234613 0.54987347 0.49542785 0.54153186]",
949
  "[1.40234613 1.54987347 1.49542785 1.5415318 ]"
950
  ],
951
- [
952
- "[0.80893755 0.92237449 0.88346356 0.93164903]",
953
- "[1.80893755 1.92237449 1.88346362 1.93164897]"
954
- ],
955
  [
956
  "[0.12858278 0.09930819 0.83222693 0.72485673]",
957
  "[1.12858272 1.09930825 1.83222699 1.72485673]"
@@ -981,13 +977,17 @@
981
  "[1.28942847 1.05601001 1.33039129 1.27781558]"
982
  ],
983
  [
984
- "[0.43681622 0.74680805 0.83598751 0.12414402]",
985
- "[1.43681622 1.74680805 1.83598757 1.12414408]"
986
  ],
987
  [
988
  "[0.47870928 0.17129105 0.27300501 0.20634609]",
989
  "[1.47870922 1.17129111 1.27300501 1.20634604]"
990
  ],
 
 
 
 
991
  [
992
  "[0.87608397 0.93200487 0.80169648 0.37758952]",
993
  "[1.87608397 1.93200493 1.80169654 1.37758946]"
@@ -1000,7 +1000,7 @@
1000
  }
1001
  },
1002
  "other": {
1003
- "model": "ModelConfig(model=Sequential(\n (0) - Linear(in_features=4, out_features=4, bias=True): Input__embedding_1_x -> Linear_1_x\n (1) - <function leaky_relu at 0x719e0ce23a60>: Linear_1_x -> Activation_2_x\n (2) - Identity(): Activation_2_x -> Activation_2_x\n), model_inputs=['Input__embedding_1_x'], model_outputs=['Activation_2_x'], loss_inputs=['Input__label_1_y', 'Activation_2_x'], loss=Sequential(\n (0) - <function mse_loss at 0x719e0ce2d580>: Activation_2_x, Input__label_1_y -> MSE_loss_1_loss\n (1) - Identity(): MSE_loss_1_loss -> loss\n), optimizer=SGD (\nParameter Group 0\n dampening: 0\n differentiable: False\n foreach: None\n fused: None\n lr: 0.1\n maximize: False\n momentum: 0\n nesterov: False\n weight_decay: 0\n), source_workspace=None, trained=True)"
1004
  },
1005
  "relations": []
1006
  },
@@ -1016,7 +1016,7 @@
1016
  },
1017
  "df_test": {
1018
  "columns": [
1019
- "predicted",
1020
  "x",
1021
  "y"
1022
  ]
@@ -1032,14 +1032,14 @@
1032
  "model": {
1033
  "model": {
1034
  "inputs": [
1035
- "Input__embedding_1_x"
1036
  ],
1037
  "loss_inputs": [
1038
- "Input__label_1_y",
1039
- "Activation_2_x"
1040
  ],
1041
  "outputs": [
1042
- "Activation_2_x"
1043
  ],
1044
  "trained": true
1045
  },
@@ -1207,14 +1207,14 @@
1207
  "model": {
1208
  "model": {
1209
  "inputs": [
1210
- "Input__embedding_1_x"
1211
  ],
1212
  "loss_inputs": [
1213
- "Input__label_1_y",
1214
- "Activation_2_x"
1215
  ],
1216
  "outputs": [
1217
- "Activation_2_x"
1218
  ],
1219
  "trained": false
1220
  },
@@ -1270,8 +1270,8 @@
1270
  "type": "basic"
1271
  },
1272
  "params": {
1273
- "epochs": "1001",
1274
- "input_mapping": "{\"map\":{\"Input__embedding_1_x\":{\"column\":\"x\",\"df\":\"df_train\"},\"Input__label_1_y\":{\"column\":\"y\",\"df\":\"df_train\"}}}",
1275
  "model_name": "model"
1276
  },
1277
  "status": "done",
@@ -1304,7 +1304,6 @@
1304
  },
1305
  "df_test": {
1306
  "columns": [
1307
- "predicted",
1308
  "x",
1309
  "y"
1310
  ]
@@ -1320,14 +1319,14 @@
1320
  "model": {
1321
  "model": {
1322
  "inputs": [
1323
- "Input__embedding_1_x"
1324
  ],
1325
  "loss_inputs": [
1326
- "Input__label_1_y",
1327
- "Activation_2_x"
1328
  ],
1329
  "outputs": [
1330
- "Activation_2_x"
1331
  ],
1332
  "trained": true
1333
  },
@@ -1383,15 +1382,15 @@
1383
  "type": "basic"
1384
  },
1385
  "params": {
1386
- "input_mapping": "{\"map\":{\"Input__embedding_1_x\":{\"column\":\"x\",\"df\":\"df_test\"}}}",
1387
  "model_name": "model",
1388
- "output_mapping": "{\"map\":{\"Activation_2_x\":{\"column\":\"predicted\",\"df\":\"df_test\"}}}"
1389
  },
1390
  "status": "done",
1391
  "title": "Model inference"
1392
  },
1393
  "dragHandle": ".bg-primary",
1394
- "height": 893.0,
1395
  "id": "Model inference 1",
1396
  "position": {
1397
  "x": 2181.718373860645,
 
575
  "columns": [
576
  "x",
577
  "y",
578
+ "pred"
579
  ],
580
  "data": [
581
  [
582
+ "[0.19908059 0.17570406 0.51475513 0.1893943 ]",
583
+ "[1.19908059 1.175704 1.51475513 1.18939424]",
584
+ "[1.560641884803772, 1.5941988229751587, 1.5775359869003296, 1.4935821294784546]"
585
  ],
586
  [
587
+ "[0.43681622 0.74680805 0.83598751 0.12414402]",
588
+ "[1.43681622 1.74680805 1.83598757 1.12414408]",
589
+ "[1.5766589641571045, 1.7117265462875366, 1.7645087242126465, 1.3384637832641602]"
590
  ],
591
  [
592
+ "[0.9829582 0.59269661 0.40120947 0.95487177]",
593
+ "[1.9829582 1.59269667 1.40120947 1.95487177]",
594
+ "[1.5375217199325562, 1.4159281253814697, 1.2972962856292725, 1.7269455194473267]"
595
  ],
596
  [
597
+ "[0.32565445 0.90939188 0.07488042 0.13730896]",
598
+ "[1.32565451 1.90939188 1.07488036 1.13730896]",
599
+ "[1.562728762626648, 1.6061222553253174, 1.597141146659851, 1.4772177934646606]"
600
  ],
601
  [
602
+ "[0.31518555 0.49643308 0.11509258 0.95458382]",
603
+ "[1.31518555 1.49643302 1.11509252 1.95458388]",
604
+ "[1.528311848640442, 1.3380011320114136, 1.171952247619629, 1.8305948972702026]"
605
  ],
606
  [
607
+ "[0.79905868 0.89367443 0.75429088 0.3190186 ]",
608
+ "[1.79905868 1.89367437 1.75429082 1.3190186 ]",
609
+ "[1.5757312774658203, 1.7105278968811035, 1.7636661529541016, 1.3394038677215576]"
610
  ],
611
  [
612
+ "[0.80893755 0.92237449 0.88346356 0.93164903]",
613
+ "[1.80893755 1.92237449 1.88346362 1.93164897]",
614
+ "[1.562132716178894, 1.6031286716461182, 1.593322992324829, 1.4810831546783447]"
615
  ],
616
  [
617
+ "[0.26661873 0.45946234 0.13510543 0.81294441]",
618
+ "[1.26661873 1.4594624 1.13510537 1.81294441]",
619
+ "[1.533058762550354, 1.3753284215927124, 1.230975866317749, 1.7815138101577759]"
620
  ],
621
  [
622
+ "[0.39147133 0.29854035 0.84663737 0.58175623]",
623
+ "[1.39147139 1.29854035 1.84663737 1.58175623]",
624
+ "[1.5607244968414307, 1.5942375659942627, 1.5779708623886108, 1.4935153722763062]"
625
  ],
626
  [
627
+ "[0.34084332 0.73018837 0.54168713 0.91440833]",
628
+ "[1.34084332 1.73018837 1.54168713 1.91440833]",
629
+ "[1.5488454103469849, 1.4963982105255127, 1.422922968864441, 1.622254490852356]"
630
  ]
631
  ]
632
  },
 
688
  "[0.11693293 0.49860179 0.55020827 0.88832849]",
689
  "[1.11693287 1.49860179 1.55020833 1.88832855]"
690
  ],
691
+ [
692
+ "[0.48959708 0.48549271 0.32688856 0.356677 ]",
693
+ "[1.48959708 1.48549271 1.32688856 1.35667706]"
694
+ ],
695
  [
696
  "[0.50272274 0.54912758 0.17663097 0.79070699]",
697
  "[1.50272274 1.54912758 1.17663097 1.79070699]"
698
  ],
699
  [
700
+ "[0.04508126 0.76880038 0.80721325 0.62542385]",
701
+ "[1.04508126 1.76880038 1.80721331 1.62542391]"
702
  ],
703
  [
704
  "[0.40167677 0.25953674 0.9407078 0.76308483]",
 
716
  "[0.62569475 0.9881897 0.83639616 0.9828859 ]",
717
  "[1.62569475 1.9881897 1.83639622 1.98288584]"
718
  ],
719
+ [
720
+ "[0.56922203 0.98222166 0.76851749 0.28615737]",
721
+ "[1.56922197 1.9822216 1.76851749 1.28615737]"
722
+ ],
723
  [
724
  "[0.88776821 0.51636773 0.30333066 0.32230979]",
725
  "[1.88776827 1.51636767 1.30333066 1.32230973]"
726
  ],
727
+ [
728
+ "[0.90817457 0.89270043 0.38583666 0.66566533]",
729
+ "[1.90817451 1.89270043 1.3858366 1.66566539]"
730
+ ],
731
  [
732
  "[0.48507756 0.80808765 0.77162558 0.47834778]",
733
  "[1.48507762 1.80808759 1.77162552 1.47834778]"
 
736
  "[0.68062544 0.98093534 0.14778823 0.53244978]",
737
  "[1.68062544 1.98093534 1.14778829 1.53244972]"
738
  ],
 
 
 
 
739
  [
740
  "[0.79121011 0.54161114 0.69369799 0.1520769 ]",
741
  "[1.79121017 1.54161119 1.69369793 1.15207696]"
 
752
  "[0.94516498 0.08422136 0.5608117 0.07652664]",
753
  "[1.94516492 1.08422136 1.56081176 1.07652664]"
754
  ],
 
 
 
 
755
  [
756
  "[0.30754459 0.77694583 0.09278506 0.38326019]",
757
  "[1.30754459 1.77694583 1.09278512 1.38326025]"
 
808
  "[0.73217702 0.65233225 0.44077861 0.33837909]",
809
  "[1.73217702 1.65233231 1.44077861 1.33837914]"
810
  ],
 
 
 
 
811
  [
812
  "[0.60110539 0.3618983 0.32342511 0.98672163]",
813
  "[1.60110545 1.3618983 1.32342505 1.98672163]"
 
816
  "[0.77427191 0.21829212 0.12769502 0.74303615]",
817
  "[1.77427197 1.21829212 1.12769508 1.74303615]"
818
  ],
819
+ [
820
+ "[0.08107251 0.2602725 0.18861133 0.44833237]",
821
+ "[1.08107257 1.2602725 1.18861127 1.44833231]"
822
+ ],
823
  [
824
  "[0.59812403 0.78395379 0.0291847 0.81814629]",
825
  "[1.59812403 1.78395379 1.0291847 1.81814623]"
 
844
  "[0.95928186 0.84273899 0.71514636 0.38619852]",
845
  "[1.95928192 1.84273899 1.7151463 1.38619852]"
846
  ],
 
 
 
 
 
 
 
 
 
 
 
 
847
  [
848
  "[0.54914117 0.03810108 0.87531954 0.73044223]",
849
  "[1.54914117 1.03810108 1.87531948 1.73044229]"
 
868
  "[0.60075855 0.12234765 0.00614399 0.30560958]",
869
  "[1.60075855 1.12234759 1.00614405 1.30560958]"
870
  ],
 
 
 
 
871
  [
872
  "[0.02162331 0.81861657 0.92468154 0.07808572]",
873
  "[1.02162337 1.81861663 1.92468154 1.07808566]"
 
912
  "[0.59492421 0.90274489 0.38069052 0.46101224]",
913
  "[1.59492421 1.90274489 1.38069057 1.46101224]"
914
  ],
915
+ [
916
+ "[0.15064228 0.03198934 0.25754827 0.51484001]",
917
+ "[1.15064228 1.03198934 1.25754833 1.51484001]"
918
+ ],
919
  [
920
  "[0.12024075 0.21342516 0.56858408 0.58644271]",
921
  "[1.12024069 1.21342516 1.56858408 1.58644271]"
 
924
  "[0.91730917 0.22574073 0.09591609 0.33056474]",
925
  "[1.91730917 1.22574067 1.09591603 1.33056474]"
926
  ],
927
+ [
928
+ "[0.49691743 0.61873293 0.90698647 0.94486356]",
929
+ "[1.49691749 1.61873293 1.90698647 1.94486356]"
930
+ ],
931
+ [
932
+ "[0.6032477 0.83361369 0.18538666 0.19108021]",
933
+ "[1.60324764 1.83361363 1.18538666 1.19108021]"
934
+ ],
935
  [
936
  "[0.63235509 0.70352674 0.96188956 0.46240485]",
937
  "[1.63235509 1.70352674 1.96188951 1.46240485]"
 
948
  "[0.40234613 0.54987347 0.49542785 0.54153186]",
949
  "[1.40234613 1.54987347 1.49542785 1.5415318 ]"
950
  ],
 
 
 
 
951
  [
952
  "[0.12858278 0.09930819 0.83222693 0.72485673]",
953
  "[1.12858272 1.09930825 1.83222699 1.72485673]"
 
977
  "[1.28942847 1.05601001 1.33039129 1.27781558]"
978
  ],
979
  [
980
+ "[0.68094063 0.45189077 0.22661722 0.37354094]",
981
+ "[1.68094063 1.45189071 1.22661722 1.37354088]"
982
  ],
983
  [
984
  "[0.47870928 0.17129105 0.27300501 0.20634609]",
985
  "[1.47870922 1.17129111 1.27300501 1.20634604]"
986
  ],
987
+ [
988
+ "[0.72795159 0.79317838 0.27832931 0.96576637]",
989
+ "[1.72795153 1.79317832 1.27832937 1.96576643]"
990
+ ],
991
  [
992
  "[0.87608397 0.93200487 0.80169648 0.37758952]",
993
  "[1.87608397 1.93200493 1.80169654 1.37758946]"
 
1000
  }
1001
  },
1002
  "other": {
1003
+ "model": "ModelConfig(model=Sequential(\n (0) - Identity(): Input__tensor_1_x -> START_Repeat_1_output\n (1) - Linear(in_features=4, out_features=4, bias=True): START_Repeat_1_output -> Linear_1_output\n (2) - <function leaky_relu at 0x762d1f82c680>: Linear_1_output -> Activation_1_output\n (3) - Identity(): Activation_1_output -> START_Repeat_1_output\n (4) - Linear(in_features=4, out_features=4, bias=True): START_Repeat_1_output -> Linear_1_output\n (5) - <function leaky_relu at 0x762d1f82c680>: Linear_1_output -> Activation_1_output\n (6) - Identity(): Activation_1_output -> END_Repeat_1_output\n (7) - Identity(): END_Repeat_1_output -> END_Repeat_1_output\n), model_inputs=['Input__tensor_1_x'], model_outputs=['END_Repeat_1_output'], loss_inputs=['END_Repeat_1_output', 'Input__tensor_3_x'], loss=Sequential(\n (0) - <function mse_loss at 0x762d1f82e160>: END_Repeat_1_output, Input__tensor_3_x -> MSE_loss_2_output\n (1) - Identity(): MSE_loss_2_output -> loss\n), optimizer_parameters={'lr': 0.1, 'type': <OptionsFor_type.SGD: 4>}, optimizer=SGD (\nParameter Group 0\n dampening: 0\n differentiable: False\n foreach: None\n fused: None\n lr: 0.1\n maximize: False\n momentum: 0\n nesterov: False\n weight_decay: 0\n), source_workspace='Model definition', trained=True)"
1004
  },
1005
  "relations": []
1006
  },
 
1016
  },
1017
  "df_test": {
1018
  "columns": [
1019
+ "pred",
1020
  "x",
1021
  "y"
1022
  ]
 
1032
  "model": {
1033
  "model": {
1034
  "inputs": [
1035
+ "Input__tensor_1_x"
1036
  ],
1037
  "loss_inputs": [
1038
+ "END_Repeat_1_output",
1039
+ "Input__tensor_3_x"
1040
  ],
1041
  "outputs": [
1042
+ "END_Repeat_1_output"
1043
  ],
1044
  "trained": true
1045
  },
 
1207
  "model": {
1208
  "model": {
1209
  "inputs": [
1210
+ "Input__tensor_1_x"
1211
  ],
1212
  "loss_inputs": [
1213
+ "END_Repeat_1_output",
1214
+ "Input__tensor_3_x"
1215
  ],
1216
  "outputs": [
1217
+ "END_Repeat_1_output"
1218
  ],
1219
  "trained": false
1220
  },
 
1270
  "type": "basic"
1271
  },
1272
  "params": {
1273
+ "epochs": "1003",
1274
+ "input_mapping": "{\"map\":{\"Input__tensor_1_x\":{\"df\":\"df_train\",\"column\":\"x\"},\"Input__tensor_3_x\":{\"df\":\"df_train\",\"column\":\"y\"}}}",
1275
  "model_name": "model"
1276
  },
1277
  "status": "done",
 
1304
  },
1305
  "df_test": {
1306
  "columns": [
 
1307
  "x",
1308
  "y"
1309
  ]
 
1319
  "model": {
1320
  "model": {
1321
  "inputs": [
1322
+ "Input__tensor_1_x"
1323
  ],
1324
  "loss_inputs": [
1325
+ "END_Repeat_1_output",
1326
+ "Input__tensor_3_x"
1327
  ],
1328
  "outputs": [
1329
+ "END_Repeat_1_output"
1330
  ],
1331
  "trained": true
1332
  },
 
1382
  "type": "basic"
1383
  },
1384
  "params": {
1385
+ "input_mapping": "{\"map\":{\"Input__tensor_1_x\":{\"df\":\"df_test\",\"column\":\"x\"}}}",
1386
  "model_name": "model",
1387
+ "output_mapping": "{\"map\":{\"END_Repeat_1_output\":{\"df\":\"df_test\",\"column\":\"pred\"}}}"
1388
  },
1389
  "status": "done",
1390
  "title": "Model inference"
1391
  },
1392
  "dragHandle": ".bg-primary",
1393
+ "height": 650.0,
1394
  "id": "Model inference 1",
1395
  "position": {
1396
  "x": 2181.718373860645,
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  // @ts-ignore
2
  import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
3
 
@@ -14,13 +15,16 @@ function ParamName({ name }: { name: string }) {
14
  function Input({
15
  value,
16
  onChange,
 
17
  }: {
18
  value: string;
19
  onChange: (value: string, options?: { delay: number }) => void;
 
20
  }) {
21
  return (
22
  <input
23
  className="input input-bordered w-full"
 
24
  value={value || ""}
25
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
26
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
@@ -29,6 +33,13 @@ function Input({
29
  );
30
  }
31
 
 
 
 
 
 
 
 
32
  function getModelBindings(
33
  data: any,
34
  variant: "training input" | "inference input" | "output",
@@ -71,11 +82,16 @@ function parseJsonOrEmpty(json: string): object {
71
  }
72
 
73
  function ModelMapping({ value, onChange, data, variant }: any) {
 
 
 
 
74
  const v: any = parseJsonOrEmpty(value);
75
  v.map ??= {};
76
  const dfs: { [df: string]: string[] } = {};
77
  const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
78
  for (const input of inputs) {
 
79
  const dataframes = input.dataframes as {
80
  [df: string]: { columns: string[] };
81
  };
@@ -84,6 +100,17 @@ function ModelMapping({ value, onChange, data, variant }: any) {
84
  }
85
  }
86
  const bindings = getModelBindings(data, variant);
 
 
 
 
 
 
 
 
 
 
 
87
  return (
88
  <table className="model-mapping-param">
89
  <tbody>
@@ -98,21 +125,10 @@ function ModelMapping({ value, onChange, data, variant }: any) {
98
  <select
99
  className="select select-ghost"
100
  value={v.map?.[binding]?.df}
101
- onChange={(evt) => {
102
- const df = evt.currentTarget.value;
103
- if (df === "") {
104
- const map = { ...v.map, [binding]: undefined };
105
- onChange(JSON.stringify({ map }));
106
- } else {
107
- const columnSpec = {
108
- column: dfs[df][0],
109
- ...(v.map?.[binding] || {}),
110
- df,
111
- };
112
- const map = { ...v.map, [binding]: columnSpec };
113
- onChange(JSON.stringify({ map }));
114
- }
115
  }}
 
116
  >
117
  <option key="" value="" />
118
  {Object.keys(dfs).map((df: string) => (
@@ -125,13 +141,16 @@ function ModelMapping({ value, onChange, data, variant }: any) {
125
  <td>
126
  {variant === "output" ? (
127
  <Input
 
 
 
128
  value={v.map?.[binding]?.column}
129
  onChange={(column, options) => {
130
- const columnSpec = {
131
- ...(v.map?.[binding] || {}),
132
- column,
133
- };
134
- const map = { ...v.map, [binding]: columnSpec };
135
  onChange(JSON.stringify({ map }), options);
136
  }}
137
  />
@@ -139,16 +158,12 @@ function ModelMapping({ value, onChange, data, variant }: any) {
139
  <select
140
  className="select select-ghost"
141
  value={v.map?.[binding]?.column}
142
- onChange={(evt) => {
143
- const column = evt.currentTarget.value;
144
- const columnSpec = {
145
- ...(v.map?.[binding] || {}),
146
- column,
147
- };
148
- const map = { ...v.map, [binding]: columnSpec };
149
- onChange(JSON.stringify({ map }));
150
  }}
 
151
  >
 
152
  {dfs[v.map?.[binding]?.df]?.map((col: string) => (
153
  <option key={col} value={col}>
154
  {col}
 
1
+ import { useRef } from "react";
2
  // @ts-ignore
3
  import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
4
 
 
15
  function Input({
16
  value,
17
  onChange,
18
+ inputRef,
19
  }: {
20
  value: string;
21
  onChange: (value: string, options?: { delay: number }) => void;
22
+ inputRef?: React.Ref<HTMLInputElement>;
23
  }) {
24
  return (
25
  <input
26
  className="input input-bordered w-full"
27
+ ref={inputRef}
28
  value={value || ""}
29
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
30
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
 
33
  );
34
  }
35
 
36
+ type Bindings = {
37
+ [key: string]: {
38
+ df: string;
39
+ column: string;
40
+ };
41
+ };
42
+
43
  function getModelBindings(
44
  data: any,
45
  variant: "training input" | "inference input" | "output",
 
82
  }
83
 
84
  function ModelMapping({ value, onChange, data, variant }: any) {
85
+ const dfsRef = useRef({} as { [binding: string]: HTMLSelectElement | null });
86
+ const columnsRef = useRef(
87
+ {} as { [binding: string]: HTMLSelectElement | HTMLInputElement | null },
88
+ );
89
  const v: any = parseJsonOrEmpty(value);
90
  v.map ??= {};
91
  const dfs: { [df: string]: string[] } = {};
92
  const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
93
  for (const input of inputs) {
94
+ if (!input.dataframes) continue;
95
  const dataframes = input.dataframes as {
96
  [df: string]: { columns: string[] };
97
  };
 
100
  }
101
  }
102
  const bindings = getModelBindings(data, variant);
103
+ function getMap() {
104
+ const map: Bindings = {};
105
+ for (const binding of bindings) {
106
+ const df = dfsRef.current[binding]?.value ?? "";
107
+ const column = columnsRef.current[binding]?.value ?? "";
108
+ if (df.length || column.length) {
109
+ map[binding] = { df, column };
110
+ }
111
+ }
112
+ return map;
113
+ }
114
  return (
115
  <table className="model-mapping-param">
116
  <tbody>
 
125
  <select
126
  className="select select-ghost"
127
  value={v.map?.[binding]?.df}
128
+ ref={(el) => {
129
+ dfsRef.current[binding] = el;
 
 
 
 
 
 
 
 
 
 
 
 
130
  }}
131
+ onChange={() => onChange(JSON.stringify({ map: getMap() }))}
132
  >
133
  <option key="" value="" />
134
  {Object.keys(dfs).map((df: string) => (
 
141
  <td>
142
  {variant === "output" ? (
143
  <Input
144
+ inputRef={(el) => {
145
+ columnsRef.current[binding] = el;
146
+ }}
147
  value={v.map?.[binding]?.column}
148
  onChange={(column, options) => {
149
+ const map = getMap();
150
+ // At this point the <input> has not been updated yet. We use the value from the event.
151
+ const df = dfsRef.current[binding]?.value ?? "";
152
+ map[binding] ??= { df, column };
153
+ map[binding].column = column;
154
  onChange(JSON.stringify({ map }), options);
155
  }}
156
  />
 
158
  <select
159
  className="select select-ghost"
160
  value={v.map?.[binding]?.column}
161
+ ref={(el) => {
162
+ columnsRef.current[binding] = el;
 
 
 
 
 
 
163
  }}
164
+ onChange={() => onChange(JSON.stringify({ map: getMap() }))}
165
  >
166
+ <option key="" value="" />
167
  {dfs[v.map?.[binding]?.df]?.map((col: string) => (
168
  <option key={col} value={col}>
169
  {col}
lynxkite-core/src/lynxkite/core/ops.py CHANGED
@@ -1,6 +1,7 @@
1
  """API for implementing LynxKite operations."""
2
 
3
  from __future__ import annotations
 
4
  import enum
5
  import functools
6
  import inspect
@@ -13,7 +14,7 @@ from typing_extensions import Annotated
13
  if typing.TYPE_CHECKING:
14
  from . import workspace
15
 
16
- CATALOGS = {}
17
  EXECUTORS = {}
18
 
19
  typeof = type # We have some arguments called "type".
@@ -297,3 +298,13 @@ def op_registration(env: str):
297
  def passive_op_registration(env: str):
298
  """Returns a function that can be used to register operations without associated code."""
299
  return functools.partial(register_passive_op, env)
 
 
 
 
 
 
 
 
 
 
 
1
  """API for implementing LynxKite operations."""
2
 
3
  from __future__ import annotations
4
+ import asyncio
5
  import enum
6
  import functools
7
  import inspect
 
14
  if typing.TYPE_CHECKING:
15
  from . import workspace
16
 
17
+ CATALOGS: dict[str, dict[str, "Op"]] = {}
18
  EXECUTORS = {}
19
 
20
  typeof = type # We have some arguments called "type".
 
298
  def passive_op_registration(env: str):
299
  """Returns a function that can be used to register operations without associated code."""
300
  return functools.partial(register_passive_op, env)
301
+
302
+
303
+ def slow(func):
304
+ """Decorator for slow, blocking operations. Turns them into separate threads."""
305
+
306
+ @functools.wraps(func)
307
+ async def wrapper(*args, **kwargs):
308
+ return await asyncio.to_thread(func, *args, **kwargs)
309
+
310
+ return wrapper
lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py CHANGED
@@ -1,5 +1,6 @@
1
  """Graph analytics executor and data types."""
2
 
 
3
  import os
4
  from lynxkite.core import ops, workspace
5
  import dataclasses
@@ -177,10 +178,16 @@ async def execute(ws: workspace.Workspace):
177
  # All inputs for this node are ready, we can compute the output.
178
  todo.remove(id)
179
  progress = True
180
- _execute_node(node, ws, catalog, outputs)
181
 
182
 
183
- def _execute_node(node, ws, catalog, outputs):
 
 
 
 
 
 
184
  params = {**node.data.params}
185
  op = catalog.get(node.data.title)
186
  if not op:
@@ -214,6 +221,7 @@ def _execute_node(node, ws, catalog, outputs):
214
  # Execute op.
215
  try:
216
  result = op(*inputs, **params)
 
217
  except Exception as e:
218
  if os.environ.get("LYNXKITE_LOG_OP_ERRORS"):
219
  traceback.print_exc()
 
1
  """Graph analytics executor and data types."""
2
 
3
+ import inspect
4
  import os
5
  from lynxkite.core import ops, workspace
6
  import dataclasses
 
178
  # All inputs for this node are ready, we can compute the output.
179
  todo.remove(id)
180
  progress = True
181
+ await _execute_node(node, ws, catalog, outputs)
182
 
183
 
184
+ async def await_if_needed(obj):
185
+ if inspect.isawaitable(obj):
186
+ obj = await obj
187
+ return obj
188
+
189
+
190
+ async def _execute_node(node, ws, catalog, outputs):
191
  params = {**node.data.params}
192
  op = catalog.get(node.data.title)
193
  if not op:
 
221
  # Execute op.
222
  try:
223
  result = op(*inputs, **params)
224
+ result.output = await await_if_needed(result.output)
225
  except Exception as e:
226
  if os.environ.get("LYNXKITE_LOG_OP_ERRORS"):
227
  traceback.print_exc()
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py CHANGED
@@ -347,7 +347,7 @@ def define_model(
347
  assert model_workspace, "Model workspace is unset."
348
  ws = load_ws(model_workspace)
349
  # Build the model without inputs, to get its interface.
350
- m = pytorch_model_ops.build_model(ws, {})
351
  m.source_workspace = model_workspace
352
  bundle = bundle.copy()
353
  bundle.other[save_as] = m
@@ -369,6 +369,7 @@ class ModelOutputMapping(pytorch_model_ops.ModelMapping):
369
 
370
 
371
  @op("Train model")
 
372
  def train_model(
373
  bundle: core.Bundle,
374
  *,
@@ -379,14 +380,12 @@ def train_model(
379
  """Trains the selected model on the selected dataset. Most training parameters are set in the model definition."""
380
  m = bundle.other[model_name].copy()
381
  inputs = pytorch_model_ops.to_tensors(bundle, input_mapping)
382
- if not m.trained and m.source_workspace:
383
- # Rebuild the model for the correct inputs.
384
- ws = load_ws(m.source_workspace)
385
- m = pytorch_model_ops.build_model(ws, inputs)
386
  t = tqdm(range(epochs), desc="Training model")
 
387
  for _ in t:
388
  loss = m.train(inputs)
389
  t.set_postfix({"loss": loss})
 
390
  m.trained = True
391
  bundle = bundle.copy()
392
  bundle.other[model_name] = m
@@ -394,6 +393,7 @@ def train_model(
394
 
395
 
396
  @op("Model inference")
 
397
  def model_inference(
398
  bundle: core.Bundle,
399
  *,
@@ -409,7 +409,13 @@ def model_inference(
409
  inputs = pytorch_model_ops.to_tensors(bundle, input_mapping)
410
  outputs = m.inference(inputs)
411
  bundle = bundle.copy()
 
412
  for k, v in output_mapping.map.items():
 
 
 
 
 
413
  bundle.dfs[v.df][v.column] = outputs[k].detach().numpy().tolist()
414
  return bundle
415
 
 
347
  assert model_workspace, "Model workspace is unset."
348
  ws = load_ws(model_workspace)
349
  # Build the model without inputs, to get its interface.
350
+ m = pytorch_model_ops.build_model(ws)
351
  m.source_workspace = model_workspace
352
  bundle = bundle.copy()
353
  bundle.other[save_as] = m
 
369
 
370
 
371
  @op("Train model")
372
+ @ops.slow
373
  def train_model(
374
  bundle: core.Bundle,
375
  *,
 
380
  """Trains the selected model on the selected dataset. Most training parameters are set in the model definition."""
381
  m = bundle.other[model_name].copy()
382
  inputs = pytorch_model_ops.to_tensors(bundle, input_mapping)
 
 
 
 
383
  t = tqdm(range(epochs), desc="Training model")
384
+ losses = []
385
  for _ in t:
386
  loss = m.train(inputs)
387
  t.set_postfix({"loss": loss})
388
+ losses.append(loss)
389
  m.trained = True
390
  bundle = bundle.copy()
391
  bundle.other[model_name] = m
 
393
 
394
 
395
  @op("Model inference")
396
+ @ops.slow
397
  def model_inference(
398
  bundle: core.Bundle,
399
  *,
 
409
  inputs = pytorch_model_ops.to_tensors(bundle, input_mapping)
410
  outputs = m.inference(inputs)
411
  bundle = bundle.copy()
412
+ copied = set()
413
  for k, v in output_mapping.map.items():
414
+ if not v.df or not v.column:
415
+ continue
416
+ if v.df not in copied:
417
+ bundle.dfs[v.df] = bundle.dfs[v.df].copy()
418
+ copied.add(v.df)
419
  bundle.dfs[v.df][v.column] = outputs[k].detach().numpy().tolist()
420
  return bundle
421
 
lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch_model_ops.py CHANGED
@@ -1,20 +1,35 @@
1
  """Boxes for defining PyTorch models."""
2
 
3
  import copy
 
4
  import graphlib
5
- import types
6
 
7
  import pydantic
8
  from lynxkite.core import ops, workspace
9
  from lynxkite.core.ops import Parameter as P
10
  import torch
11
- import torch_geometric as pyg
12
  import dataclasses
13
  from . import core
14
 
15
  ENV = "PyTorch model"
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  def reg(name, inputs=[], outputs=None, params=[]):
19
  if outputs is None:
20
  outputs = inputs
@@ -27,13 +42,9 @@ def reg(name, inputs=[], outputs=None, params=[]):
27
  )
28
 
29
 
30
- reg("Input: embedding", outputs=["x"])
31
  reg("Input: graph edges", outputs=["edges"])
32
- reg("Input: label", outputs=["y"])
33
- reg("Input: positive sample", outputs=["x_pos"])
34
- reg("Input: negative sample", outputs=["x_neg"])
35
  reg("Input: sequential", outputs=["y"])
36
- reg("Input: zeros", outputs=["x"])
37
 
38
  reg("LSTM", inputs=["x", "h"], outputs=["x", "h"])
39
  reg(
@@ -59,10 +70,35 @@ reg(
59
  ),
60
  ],
61
  )
 
 
62
  reg("Attention", inputs=["q", "k", "v"], outputs=["x", "weights"])
63
  reg("LayerNorm", inputs=["x"])
64
  reg("Dropout", inputs=["x"], params=[P.basic("p", 0.5)])
65
- reg("Linear", inputs=["x"], params=[P.basic("output_dim", "same")])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  reg("Softmax", inputs=["x"])
67
  reg(
68
  "Graph conv",
@@ -70,16 +106,10 @@ reg(
70
  outputs=["x"],
71
  params=[P.options("type", ["GCNConv", "GATConv", "GATv2Conv", "SAGEConv"])],
72
  )
73
- reg(
74
- "Activation",
75
- inputs=["x"],
76
- params=[P.options("type", ["ReLU", "Leaky ReLU", "Tanh", "Mish"])],
77
- )
78
  reg("Concatenate", inputs=["a", "b"], outputs=["x"])
79
  reg("Add", inputs=["a", "b"], outputs=["x"])
80
  reg("Subtract", inputs=["a", "b"], outputs=["x"])
81
  reg("Multiply", inputs=["a", "b"], outputs=["x"])
82
- reg("MSE loss", inputs=["x", "y"], outputs=["loss"])
83
  reg("Triplet margin loss", inputs=["x", "x_pos", "x_neg"], outputs=["loss"])
84
  reg("Cross-entropy loss", inputs=["x", "y"], outputs=["loss"])
85
  reg(
@@ -110,7 +140,7 @@ ops.register_passive_op(
110
  outputs=[ops.Output(name="output", position="bottom", type="tensor")],
111
  params=[
112
  ops.Parameter.basic("times", 1, int),
113
- ops.Parameter.basic("same_weights", True, bool),
114
  ],
115
  )
116
 
@@ -128,6 +158,21 @@ def _to_id(*strings: str) -> str:
128
  return "_".join("".join(c if c.isalnum() else "_" for c in s) for s in strings)
129
 
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  class ColumnSpec(pydantic.BaseModel):
132
  df: str
133
  column: str
@@ -144,10 +189,17 @@ class ModelConfig:
144
  model_outputs: list[str]
145
  loss_inputs: list[str]
146
  loss: torch.nn.Module
147
- optimizer: torch.optim.Optimizer
 
148
  source_workspace: str | None = None
149
  trained: bool = False
150
 
 
 
 
 
 
 
151
  def _forward(self, inputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
152
  model_inputs = [inputs[i] for i in self.model_inputs]
153
  output = self.model(*model_inputs)
@@ -174,10 +226,20 @@ class ModelConfig:
174
  self.optimizer.step()
175
  return loss.item()
176
 
 
 
 
 
 
 
177
  def copy(self):
178
  """Returns a copy of the model."""
179
- c = dataclasses.replace(self)
180
- c.model = copy.deepcopy(self.model)
 
 
 
 
181
  return c
182
 
183
  def metadata(self):
@@ -192,113 +254,219 @@ class ModelConfig:
192
  }
193
 
194
 
195
- def build_model(ws: workspace.Workspace, inputs: dict[str, torch.Tensor]) -> ModelConfig:
196
  """Builds the model described in the workspace."""
197
- catalog = ops.CATALOGS[ENV]
198
- optimizers = []
199
- nodes = {}
200
- for node in ws.nodes:
201
- nodes[node.id] = node
202
- if node.data.title == "Optimizer":
203
- optimizers.append(node.id)
204
- assert optimizers, "No optimizer found."
205
- assert len(optimizers) == 1, f"More than one optimizer found: {optimizers}"
206
- [optimizer] = optimizers
207
- dependencies = {n.id: [] for n in ws.nodes}
208
- in_edges = {}
209
- out_edges = {}
210
- # TODO: Dissolve repeat boxes here.
211
- for e in ws.edges:
212
- dependencies[e.target].append(e.source)
213
- in_edges.setdefault(e.target, {}).setdefault(e.targetHandle, []).append(
214
- (e.source, e.sourceHandle)
215
- )
216
- out_edges.setdefault(e.source, {}).setdefault(e.sourceHandle, []).append(
217
- (e.target, e.targetHandle)
218
- )
219
- sizes = {}
220
- for k, i in inputs.items():
221
- sizes[k] = i.shape[-1]
222
- ts = graphlib.TopologicalSorter(dependencies)
223
- layers = []
224
- loss_layers = []
225
- in_loss = set()
226
- cfg = {}
227
- used_in_model = set()
228
- made_in_model = set()
229
- used_in_loss = set()
230
- made_in_loss = set()
231
- for node_id in ts.static_order():
232
- node = nodes[node_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  t = node.data.title
234
- op = catalog[t]
235
  p = op.convert_params(node.data.params)
236
- for b in dependencies[node_id]:
237
- if b in in_loss:
238
- in_loss.add(node_id)
239
- if "loss" in t:
240
- in_loss.add(node_id)
241
- inputs = {}
242
- for n in in_edges.get(node_id, []):
243
- for b, h in in_edges[node_id][n]:
244
- i = _to_id(b, h)
245
- inputs[n] = i
246
- if node_id in in_loss:
247
- used_in_loss.add(i)
248
- else:
249
- used_in_model.add(i)
250
- outputs = {}
251
- for out in out_edges.get(node_id, []):
252
- i = _to_id(node_id, out)
253
- outputs[out] = i
254
- if inputs: # Nodes with no inputs are input nodes. Their outputs are not "made" by us.
255
- if node_id in in_loss:
256
- made_in_loss.add(i)
257
- else:
258
- made_in_model.add(i)
259
- inputs = types.SimpleNamespace(**inputs)
260
- outputs = types.SimpleNamespace(**outputs)
261
- ls = loss_layers if node_id in in_loss else layers
262
  match t:
263
- case "Linear":
264
- isize = sizes.get(inputs.x, 1)
265
- osize = isize if p["output_dim"] == "same" else int(p["output_dim"])
266
- ls.append((torch.nn.Linear(isize, osize), f"{inputs.x} -> {outputs.x}"))
267
- sizes[outputs.x] = osize
268
- case "Activation":
269
- f = getattr(torch.nn.functional, p["type"].name.lower().replace(" ", "_"))
270
- ls.append((f, f"{inputs.x} -> {outputs.x}"))
271
- sizes[outputs.x] = sizes.get(inputs.x, 1)
272
- case "MSE loss":
273
- ls.append(
274
- (
275
- torch.nn.functional.mse_loss,
276
- f"{inputs.x}, {inputs.y} -> {outputs.loss}",
277
  )
278
- )
279
- cfg["model_inputs"] = list(used_in_model - made_in_model)
280
- cfg["model_outputs"] = list(made_in_model & used_in_loss)
281
- cfg["loss_inputs"] = list(used_in_loss - made_in_loss)
282
- # Make sure the trained output is output from the last model layer.
283
- outputs = ", ".join(cfg["model_outputs"])
284
- layers.append((torch.nn.Identity(), f"{outputs} -> {outputs}"))
285
- # Create model.
286
- cfg["model"] = pyg.nn.Sequential(", ".join(cfg["model_inputs"]), layers)
287
- # Make sure the loss is output from the last loss layer.
288
- [(lossb, lossh)] = in_edges[optimizer]["loss"]
289
- lossi = _to_id(lossb, lossh)
290
- loss_layers.append((torch.nn.Identity(), f"{lossi} -> loss"))
291
- # Create loss function.
292
- cfg["loss"] = pyg.nn.Sequential(", ".join(cfg["loss_inputs"]), loss_layers)
293
- assert not list(cfg["loss"].parameters()), (
294
- f"loss should have no parameters: {list(cfg['loss'].parameters())}"
295
- )
296
- # Create optimizer.
297
- op = catalog["Optimizer"]
298
- p = op.convert_params(nodes[optimizer].data.params)
299
- o = getattr(torch.optim, p["type"].name)
300
- cfg["optimizer"] = o(cfg["model"].parameters(), lr=p["lr"])
301
- return ModelConfig(**cfg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
 
304
  def to_tensors(b: core.Bundle, m: ModelMapping | None) -> dict[str, torch.Tensor]:
 
1
  """Boxes for defining PyTorch models."""
2
 
3
  import copy
4
+ import enum
5
  import graphlib
 
6
 
7
  import pydantic
8
  from lynxkite.core import ops, workspace
9
  from lynxkite.core.ops import Parameter as P
10
  import torch
11
+ import torch_geometric.nn as pyg_nn
12
  import dataclasses
13
  from . import core
14
 
15
  ENV = "PyTorch model"
16
 
17
 
18
+ def op(name, **kwargs):
19
+ _op = ops.op(ENV, name, **kwargs)
20
+
21
+ def decorator(func):
22
+ _op(func)
23
+ op = func.__op__
24
+ for p in op.inputs.values():
25
+ p.position = "bottom"
26
+ for p in op.outputs.values():
27
+ p.position = "top"
28
+ return func
29
+
30
+ return decorator
31
+
32
+
33
  def reg(name, inputs=[], outputs=None, params=[]):
34
  if outputs is None:
35
  outputs = inputs
 
42
  )
43
 
44
 
45
+ reg("Input: tensor", outputs=["output"], params=[P.basic("name")])
46
  reg("Input: graph edges", outputs=["edges"])
 
 
 
47
  reg("Input: sequential", outputs=["y"])
 
48
 
49
  reg("LSTM", inputs=["x", "h"], outputs=["x", "h"])
50
  reg(
 
70
  ),
71
  ],
72
  )
73
+
74
+
75
  reg("Attention", inputs=["q", "k", "v"], outputs=["x", "weights"])
76
  reg("LayerNorm", inputs=["x"])
77
  reg("Dropout", inputs=["x"], params=[P.basic("p", 0.5)])
78
+
79
+
80
+ @op("Linear")
81
+ def linear(x, *, output_dim=1024):
82
+ return pyg_nn.Linear(-1, output_dim)
83
+
84
+
85
+ class ActivationTypes(enum.Enum):
86
+ ReLU = "ReLU"
87
+ Leaky_ReLU = "Leaky ReLU"
88
+ Tanh = "Tanh"
89
+ Mish = "Mish"
90
+
91
+
92
+ @op("Activation")
93
+ def activation(x, *, type: ActivationTypes = ActivationTypes.ReLU):
94
+ return getattr(torch.nn.functional, type.name.lower().replace(" ", "_"))
95
+
96
+
97
+ @op("MSE loss")
98
+ def mse_loss(x, y):
99
+ return torch.nn.functional.mse_loss
100
+
101
+
102
  reg("Softmax", inputs=["x"])
103
  reg(
104
  "Graph conv",
 
106
  outputs=["x"],
107
  params=[P.options("type", ["GCNConv", "GATConv", "GATv2Conv", "SAGEConv"])],
108
  )
 
 
 
 
 
109
  reg("Concatenate", inputs=["a", "b"], outputs=["x"])
110
  reg("Add", inputs=["a", "b"], outputs=["x"])
111
  reg("Subtract", inputs=["a", "b"], outputs=["x"])
112
  reg("Multiply", inputs=["a", "b"], outputs=["x"])
 
113
  reg("Triplet margin loss", inputs=["x", "x_pos", "x_neg"], outputs=["loss"])
114
  reg("Cross-entropy loss", inputs=["x", "y"], outputs=["loss"])
115
  reg(
 
140
  outputs=[ops.Output(name="output", position="bottom", type="tensor")],
141
  params=[
142
  ops.Parameter.basic("times", 1, int),
143
+ ops.Parameter.basic("same_weights", False, bool),
144
  ],
145
  )
146
 
 
158
  return "_".join("".join(c if c.isalnum() else "_" for c in s) for s in strings)
159
 
160
 
161
+ @dataclasses.dataclass
162
+ class Layer:
163
+ """Temporary data structure used by ModelBuilder."""
164
+
165
+ module: torch.nn.Module
166
+ origin_id: str
167
+ inputs: list[str]
168
+ outputs: list[str]
169
+
170
+ def for_sequential(self):
171
+ inputs = ", ".join(self.inputs)
172
+ outputs = ", ".join(self.outputs)
173
+ return self.module, f"{inputs} -> {outputs}"
174
+
175
+
176
  class ColumnSpec(pydantic.BaseModel):
177
  df: str
178
  column: str
 
189
  model_outputs: list[str]
190
  loss_inputs: list[str]
191
  loss: torch.nn.Module
192
+ optimizer_parameters: dict[str, any]
193
+ optimizer: torch.optim.Optimizer | None = None
194
  source_workspace: str | None = None
195
  trained: bool = False
196
 
197
+ def __post_init__(self):
198
+ self._make_optimizer()
199
+
200
+ def num_parameters(self) -> int:
201
+ return sum(p.numel() for p in self.model.parameters())
202
+
203
  def _forward(self, inputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
204
  model_inputs = [inputs[i] for i in self.model_inputs]
205
  output = self.model(*model_inputs)
 
226
  self.optimizer.step()
227
  return loss.item()
228
 
229
+ def _make_optimizer(self):
230
+ # We need to make a new optimizer when the model is copied. (It's tied to its parameters.)
231
+ p = self.optimizer_parameters
232
+ o = getattr(torch.optim, p["type"].name)
233
+ self.optimizer = o(self.model.parameters(), lr=p["lr"])
234
+
235
  def copy(self):
236
  """Returns a copy of the model."""
237
+ c = dataclasses.replace(
238
+ self,
239
+ model=copy.deepcopy(self.model),
240
+ )
241
+ c._make_optimizer()
242
+ c.optimizer.load_state_dict(self.optimizer.state_dict())
243
  return c
244
 
245
  def metadata(self):
 
254
  }
255
 
256
 
257
+ def build_model(ws: workspace.Workspace) -> ModelConfig:
258
  """Builds the model described in the workspace."""
259
+ builder = ModelBuilder(ws)
260
+ return builder.build_model()
261
+
262
+
263
+ class ModelBuilder:
264
+ """The state shared between methods that are used to build the model."""
265
+
266
+ def __init__(self, ws: workspace.Workspace):
267
+ self.catalog = ops.CATALOGS[ENV]
268
+ optimizers = []
269
+ self.nodes: dict[str, workspace.WorkspaceNode] = {}
270
+ repeats: list[str] = []
271
+ for node in ws.nodes:
272
+ self.nodes[node.id] = node
273
+ if node.data.title == "Optimizer":
274
+ optimizers.append(node.id)
275
+ elif node.data.title == "Repeat":
276
+ repeats.append(node.id)
277
+ self.nodes[f"START {node.id}"] = node
278
+ self.nodes[f"END {node.id}"] = node
279
+ assert optimizers, "No optimizer found."
280
+ assert len(optimizers) == 1, f"More than one optimizer found: {optimizers}"
281
+ [self.optimizer] = optimizers
282
+ self.dependencies = {n: [] for n in self.nodes}
283
+ self.in_edges: dict[str, dict[str, list[tuple[str, str]]]] = {n: {} for n in self.nodes}
284
+ self.out_edges: dict[str, dict[str, list[tuple[str, str]]]] = {n: {} for n in self.nodes}
285
+ for e in ws.edges:
286
+ self.dependencies[e.target].append(e.source)
287
+ self.in_edges.setdefault(e.target, {}).setdefault(e.targetHandle, []).append(
288
+ (e.source, e.sourceHandle)
289
+ )
290
+ self.out_edges.setdefault(e.source, {}).setdefault(e.sourceHandle, []).append(
291
+ (e.target, e.targetHandle)
292
+ )
293
+ # Split repeat boxes into start and end, and insert them into the flow.
294
+ # TODO: Think about recursive repeats.
295
+ for repeat in repeats:
296
+ if not self.out_edges[repeat] or not self.in_edges[repeat]:
297
+ continue
298
+ start_id = f"START {repeat}"
299
+ end_id = f"END {repeat}"
300
+ # repeat -> first <- real_input
301
+ # ...becomes...
302
+ # real_input -> start -> first
303
+ first, firsth = self.out_edges[repeat]["output"][0]
304
+ [(real_input, real_inputh)] = [
305
+ k for k in self.in_edges[first][firsth] if k != (repeat, "output")
306
+ ]
307
+ self.dependencies[first].remove(repeat)
308
+ self.dependencies[first].append(start_id)
309
+ self.dependencies[start_id] = [real_input]
310
+ self.out_edges[real_input][real_inputh] = [
311
+ k if k != (first, firsth) else (start_id, "input")
312
+ for k in self.out_edges[real_input][real_inputh]
313
+ ]
314
+ self.in_edges[start_id] = {"input": [(real_input, real_inputh)]}
315
+ self.out_edges[start_id] = {"output": [(first, firsth)]}
316
+ self.in_edges[first][firsth] = [(start_id, "output")]
317
+ # repeat <- last -> real_output
318
+ # ...becomes...
319
+ # last -> end -> real_output
320
+ last, lasth = self.in_edges[repeat]["input"][0]
321
+ [(real_output, real_outputh)] = [
322
+ k for k in self.out_edges[last][lasth] if k != (repeat, "input")
323
+ ]
324
+ del self.dependencies[repeat]
325
+ self.dependencies[end_id] = [last]
326
+ self.dependencies[real_output].append(end_id)
327
+ self.out_edges[last][lasth] = [(end_id, "input")]
328
+ self.in_edges[end_id] = {"input": [(last, lasth)]}
329
+ self.out_edges[end_id] = {"output": [(real_output, real_outputh)]}
330
+ self.in_edges[real_output][real_outputh] = [
331
+ k if k != (last, lasth) else (end_id, "output")
332
+ for k in self.in_edges[real_output][real_outputh]
333
+ ]
334
+ self.inv_dependencies = {n: [] for n in self.nodes}
335
+ for k, v in self.dependencies.items():
336
+ for i in v:
337
+ self.inv_dependencies[i].append(k)
338
+ self.layers: list[Layer] = []
339
+ # Clean up disconnected nodes.
340
+ disconnected = set()
341
+ for node_id in self.nodes:
342
+ op = self.catalog[self.nodes[node_id].data.title]
343
+ if len(self.in_edges[node_id]) != len(op.inputs):
344
+ disconnected.add(node_id)
345
+ disconnected |= self.all_upstream(node_id)
346
+ for node_id in disconnected:
347
+ del self.dependencies[node_id]
348
+ del self.in_edges[node_id]
349
+ del self.out_edges[node_id]
350
+ del self.inv_dependencies[node_id]
351
+ del self.nodes[node_id]
352
+
353
+ def all_upstream(self, node: str) -> set[str]:
354
+ """Returns all nodes upstream of a node."""
355
+ deps = set()
356
+ for dep in self.dependencies[node]:
357
+ deps.add(dep)
358
+ deps.update(self.all_upstream(dep))
359
+ return deps
360
+
361
+ def all_downstream(self, node: str) -> set[str]:
362
+ """Returns all nodes downstream of a node."""
363
+ deps = set()
364
+ for dep in self.inv_dependencies[node]:
365
+ deps.add(dep)
366
+ deps.update(self.all_downstream(dep))
367
+ return deps
368
+
369
+ def run_node(self, node_id: str) -> None:
370
+ """Adds the layer(s) produced by this node to self.layers."""
371
+ node = self.nodes[node_id]
372
  t = node.data.title
373
+ op = self.catalog[t]
374
  p = op.convert_params(node.data.params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  match t:
376
+ case "Repeat":
377
+ if node_id.startswith("END "):
378
+ repeat_id = node_id.removeprefix("END ")
379
+ start_id = f"START {repeat_id}"
380
+ [last_output] = self.in_edges[node_id]["input"]
381
+ after_start = self.all_downstream(start_id)
382
+ after_end = self.all_downstream(node_id)
383
+ before_end = self.all_upstream(node_id)
384
+ affected_nodes = after_start - after_end - {node_id}
385
+ repeated_nodes = after_start & before_end
386
+ assert affected_nodes == repeated_nodes, (
387
+ f"edges leave repeated section '{repeat_id}':\n{affected_nodes - repeated_nodes}"
 
 
388
  )
389
+ repeated_layers = [e for e in self.layers if e.origin_id in repeated_nodes]
390
+ assert p["times"] >= 1, f"Cannot repeat {repeat_id} {p['times']} times."
391
+ for i in range(p["times"] - 1):
392
+ # Copy repeat section's output to repeat section's input.
393
+ self.layers.append(
394
+ Layer(
395
+ torch.nn.Identity(),
396
+ origin_id=node_id,
397
+ inputs=[_to_id(*last_output)],
398
+ outputs=[_to_id(start_id, "output")],
399
+ )
400
+ )
401
+ # Repeat the layers in the section.
402
+ for layer in repeated_layers:
403
+ if p["same_weights"]:
404
+ self.layers.append(layer)
405
+ else:
406
+ self.run_node(layer.origin_id)
407
+ self.layers.append(self.run_op(node_id, op, p))
408
+ case "Optimizer" | "Input: tensor" | "Input: graph edges" | "Input: sequential":
409
+ return
410
+ case _:
411
+ self.layers.append(self.run_op(node_id, op, p))
412
+
413
+ def run_op(self, node_id: str, op: ops.Op, params) -> Layer:
414
+ """Returns the layer produced by this op."""
415
+ inputs = [_to_id(*i) for n in op.inputs for i in self.in_edges[node_id][n]]
416
+ outputs = [_to_id(node_id, n) for n in op.outputs]
417
+ if op.func == ops.no_op:
418
+ module = torch.nn.Identity()
419
+ else:
420
+ module = op.func(*inputs, **params)
421
+ return Layer(module, node_id, inputs, outputs)
422
+
423
+ def build_model(self) -> ModelConfig:
424
+ # Walk the graph in topological order.
425
+ ts = graphlib.TopologicalSorter(self.dependencies)
426
+ for node_id in ts.static_order():
427
+ self.run_node(node_id)
428
+ return self.get_config()
429
+
430
+ def get_config(self) -> ModelConfig:
431
+ # Split the design into model and loss.
432
+ loss_nodes = set()
433
+ for node_id in self.nodes:
434
+ if "loss" in self.nodes[node_id].data.title:
435
+ loss_nodes.add(node_id)
436
+ loss_nodes |= self.all_downstream(node_id)
437
+ layers = []
438
+ loss_layers = []
439
+ for layer in self.layers:
440
+ if layer.origin_id in loss_nodes:
441
+ loss_layers.append(layer)
442
+ else:
443
+ layers.append(layer)
444
+ used_in_model = set(input for layer in layers for input in layer.inputs)
445
+ used_in_loss = set(input for layer in loss_layers for input in layer.inputs)
446
+ made_in_model = set(output for layer in layers for output in layer.outputs)
447
+ made_in_loss = set(output for layer in loss_layers for output in layer.outputs)
448
+ layers = [layer.for_sequential() for layer in layers]
449
+ loss_layers = [layer.for_sequential() for layer in loss_layers]
450
+ cfg = {}
451
+ cfg["model_inputs"] = list(used_in_model - made_in_model)
452
+ cfg["model_outputs"] = list(made_in_model & used_in_loss)
453
+ cfg["loss_inputs"] = list(used_in_loss - made_in_loss)
454
+ # Make sure the trained output is output from the last model layer.
455
+ outputs = ", ".join(cfg["model_outputs"])
456
+ layers.append((torch.nn.Identity(), f"{outputs} -> {outputs}"))
457
+ # Create model.
458
+ cfg["model"] = pyg_nn.Sequential(", ".join(cfg["model_inputs"]), layers)
459
+ # Make sure the loss is output from the last loss layer.
460
+ [(lossb, lossh)] = self.in_edges[self.optimizer]["loss"]
461
+ lossi = _to_id(lossb, lossh)
462
+ loss_layers.append((torch.nn.Identity(), f"{lossi} -> loss"))
463
+ # Create loss function.
464
+ cfg["loss"] = pyg_nn.Sequential(", ".join(cfg["loss_inputs"]), loss_layers)
465
+ assert not list(cfg["loss"].parameters()), f"loss should have no parameters: {loss_layers}"
466
+ # Create optimizer.
467
+ op = self.catalog["Optimizer"]
468
+ cfg["optimizer_parameters"] = op.convert_params(self.nodes[self.optimizer].data.params)
469
+ return ModelConfig(**cfg)
470
 
471
 
472
  def to_tensors(b: core.Bundle, m: ModelMapping | None) -> dict[str, torch.Tensor]:
lynxkite-graph-analytics/tests/test_pytorch_model_ops.py CHANGED
@@ -4,14 +4,16 @@ import torch
4
  import pytest
5
 
6
 
7
- def make_ws(env, nodes: dict[str, dict], edges: list[tuple[str, str, str, str]]):
8
  ws = workspace.Workspace(env=env)
9
  for id, data in nodes.items():
 
 
10
  ws.nodes.append(
11
  workspace.WorkspaceNode(
12
  id=id,
13
  type="basic",
14
- data=workspace.WorkspaceNodeData(title=data["title"], params=data),
15
  position=workspace.Position(
16
  x=data.get("x", 0),
17
  y=data.get("y", 0),
@@ -31,35 +33,86 @@ def make_ws(env, nodes: dict[str, dict], edges: list[tuple[str, str, str, str]])
31
  return ws
32
 
33
 
 
 
 
 
 
 
 
 
 
 
 
34
  async def test_build_model():
35
  ws = make_ws(
36
  pytorch_model_ops.ENV,
37
  {
38
- "emb": {"title": "Input: embedding"},
39
- "lin": {"title": "Linear", "output_dim": "same"},
40
- "act": {"title": "Activation", "type": "Leaky ReLU"},
41
- "label": {"title": "Input: label"},
42
  "loss": {"title": "MSE loss"},
43
  "optim": {"title": "Optimizer", "type": "SGD", "lr": 0.1},
44
  },
45
  [
46
- ("emb:x", "lin:x"),
47
- ("lin:x", "act:x"),
48
- ("act:x", "loss:x"),
49
- ("label:y", "loss:y"),
50
- ("loss:loss", "optim:loss"),
51
  ],
52
  )
53
  x = torch.rand(100, 4)
54
  y = x + 1
55
- m = pytorch_model_ops.build_model(ws, {"emb_x": x, "label_y": y})
56
  for i in range(1000):
57
- loss = m.train({"emb_x": x, "label_y": y})
58
  assert loss < 0.1
59
- o = m.inference({"emb_x": x[:1]})
60
- error = torch.nn.functional.mse_loss(o["act_x"], x[:1] + 1)
61
  assert error < 0.1
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  if __name__ == "__main__":
65
  pytest.main()
 
4
  import pytest
5
 
6
 
7
+ def make_ws(env, nodes: dict[str, dict], edges: list[tuple[str, str]]):
8
  ws = workspace.Workspace(env=env)
9
  for id, data in nodes.items():
10
+ title = data["title"]
11
+ del data["title"]
12
  ws.nodes.append(
13
  workspace.WorkspaceNode(
14
  id=id,
15
  type="basic",
16
+ data=workspace.WorkspaceNodeData(title=title, params=data),
17
  position=workspace.Position(
18
  x=data.get("x", 0),
19
  y=data.get("y", 0),
 
33
  return ws
34
 
35
 
36
+ def summarize_layers(m: pytorch_model_ops.ModelConfig) -> str:
37
+ return "".join(str(e)[0] for e in m.model)
38
+
39
+
40
+ def summarize_connections(m: pytorch_model_ops.ModelConfig) -> str:
41
+ return " ".join(
42
+ "".join(n[0] for n in c.param_names) + "->" + "".join(n[0] for n in c.return_names)
43
+ for c in m.model._children
44
+ )
45
+
46
+
47
  async def test_build_model():
48
  ws = make_ws(
49
  pytorch_model_ops.ENV,
50
  {
51
+ "emb": {"title": "Input: tensor"},
52
+ "lin": {"title": "Linear", "output_dim": 4},
53
+ "act": {"title": "Activation", "type": "Leaky_ReLU"},
54
+ "label": {"title": "Input: tensor"},
55
  "loss": {"title": "MSE loss"},
56
  "optim": {"title": "Optimizer", "type": "SGD", "lr": 0.1},
57
  },
58
  [
59
+ ("emb:output", "lin:x"),
60
+ ("lin:output", "act:x"),
61
+ ("act:output", "loss:x"),
62
+ ("label:output", "loss:y"),
63
+ ("loss:output", "optim:loss"),
64
  ],
65
  )
66
  x = torch.rand(100, 4)
67
  y = x + 1
68
+ m = pytorch_model_ops.build_model(ws)
69
  for i in range(1000):
70
+ loss = m.train({"emb_output": x, "label_output": y})
71
  assert loss < 0.1
72
+ o = m.inference({"emb_output": x[:1]})
73
+ error = torch.nn.functional.mse_loss(o["act_output"], x[:1] + 1)
74
  assert error < 0.1
75
 
76
 
77
+ async def test_build_model_with_repeat():
78
+ def repeated_ws(times):
79
+ return make_ws(
80
+ pytorch_model_ops.ENV,
81
+ {
82
+ "emb": {"title": "Input: tensor"},
83
+ "lin": {"title": "Linear", "output_dim": 8},
84
+ "act": {"title": "Activation", "type": "Leaky_ReLU"},
85
+ "label": {"title": "Input: tensor"},
86
+ "loss": {"title": "MSE loss"},
87
+ "optim": {"title": "Optimizer", "type": "SGD", "lr": 0.1},
88
+ "repeat": {"title": "Repeat", "times": times, "same_weights": False},
89
+ },
90
+ [
91
+ ("emb:output", "lin:x"),
92
+ ("lin:output", "act:x"),
93
+ ("act:output", "loss:x"),
94
+ ("label:output", "loss:y"),
95
+ ("loss:output", "optim:loss"),
96
+ ("repeat:output", "lin:x"),
97
+ ("act:output", "repeat:input"),
98
+ ],
99
+ )
100
+
101
+ # 1 repetition
102
+ m = pytorch_model_ops.build_model(repeated_ws(1))
103
+ assert summarize_layers(m) == "IL<II"
104
+ assert summarize_connections(m) == "e->S S->l l->a a->E E->E"
105
+
106
+ # 2 repetitions
107
+ m = pytorch_model_ops.build_model(repeated_ws(2))
108
+ assert summarize_layers(m) == "IL<IL<II"
109
+ assert summarize_connections(m) == "e->S S->l l->a a->S S->l l->a a->E E->E"
110
+
111
+ # 3 repetitions
112
+ m = pytorch_model_ops.build_model(repeated_ws(3))
113
+ assert summarize_layers(m) == "IL<IL<IL<II"
114
+ assert summarize_connections(m) == "e->S S->l l->a a->S S->l l->a a->S S->l l->a a->E E->E"
115
+
116
+
117
  if __name__ == "__main__":
118
  pytest.main()