From 2d15714f9387c446f6ac7fa69a3ddf3d8f1d89d4 Mon Sep 17 00:00:00 2001 From: HowToSD Date: Thu, 13 Mar 2025 19:58:17 -0700 Subject: [PATCH] Added simple optimizer and single layer linear model as well as a workflow to show case those nodes. --- __init__.py | 15 ++ docs/model_training.md | 21 ++- docs/reference/node_reference.md | 4 + docs/reference/pt_data_loader_from_tensors.md | 28 +++ docs/reference/pt_predict_regression_model.md | 14 ++ docs/reference/pt_train_regression_model.md | 30 ++++ docs/reference/ptn_linear.md | 24 +++ .../linear_regression_data_creation.json | 1 + .../simple_linear_regression_train.json | 1 + .../pt_data_loader_from_tensors.py | 67 +++++++ .../pt_predict_classification_model.py | 2 +- .../pt_predict_regression_model.py | 61 +++++++ .../pt_train_regression_model.py | 167 ++++++++++++++++++ modules/pytorch_wrapper/ptn_linear.py | 61 +++++++ modules/pytorch_wrapper/pto_adam.py | 6 +- modules/pytorch_wrapper/pto_simple.py | 88 +++++++++ pyproject.toml | 2 +- .../pt_data_loader_from_tensors_test.py | 72 ++++++++ ..._predict_classification_conv_model_test.py | 2 +- .../pt_predict_regression_model_test.py | 44 +++++ .../pytorch_wrapper/ptn_linear_model_test.py | 2 +- tests/pytorch_wrapper/ptn_linear_test.py | 47 +++++ tests/pytorch_wrapper/pto_simple_test.py | 35 ++++ web/js/ShowTextGeneric.js | 2 +- 24 files changed, 787 insertions(+), 9 deletions(-) create mode 100644 docs/reference/pt_data_loader_from_tensors.md create mode 100644 docs/reference/pt_predict_regression_model.md create mode 100644 docs/reference/pt_train_regression_model.md create mode 100644 docs/reference/ptn_linear.md create mode 100644 examples/workflows/linear_regression_data_creation.json create mode 100644 examples/workflows/simple_linear_regression_train.json create mode 100644 modules/pytorch_wrapper/pt_data_loader_from_tensors.py create mode 100644 modules/pytorch_wrapper/pt_predict_regression_model.py create mode 100644 modules/pytorch_wrapper/pt_train_regression_model.py create mode 100644 modules/pytorch_wrapper/ptn_linear.py create mode 100644 modules/pytorch_wrapper/pto_simple.py create mode 100644 tests/pytorch_wrapper/pt_data_loader_from_tensors_test.py create mode 100644 tests/pytorch_wrapper/pt_predict_regression_model_test.py create mode 100644 tests/pytorch_wrapper/ptn_linear_test.py create mode 100644 tests/pytorch_wrapper/pto_simple_test.py diff --git a/__init__.py b/__init__.py index ae33279..8958226 100644 --- a/__init__.py +++ b/__init__.py @@ -20,6 +20,7 @@ from .modules.pytorch_wrapper.pt_cos import PtCos from .modules.pytorch_wrapper.pt_cosh import PtCosh from .modules.pytorch_wrapper.pt_crop import PtCrop from .modules.pytorch_wrapper.pt_data_loader import PtDataLoader +from .modules.pytorch_wrapper.pt_data_loader_from_tensors import PtDataLoaderFromTensors from .modules.pytorch_wrapper.pt_div import PtDiv from .modules.pytorch_wrapper.pt_einsum import PtEinsum from .modules.pytorch_wrapper.pt_eq import PtEq @@ -64,6 +65,7 @@ from .modules.pytorch_wrapper.pt_pad import PtPad from .modules.pytorch_wrapper.pt_permute import PtPermute from .modules.pytorch_wrapper.pt_pow import PtPow from .modules.pytorch_wrapper.pt_predict_classification_model import PtPredictClassificationModel +from .modules.pytorch_wrapper.pt_predict_regression_model import PtPredictRegressionModel from .modules.pytorch_wrapper.pt_prod import PtProd from .modules.pytorch_wrapper.pt_rand import PtRand from .modules.pytorch_wrapper.pt_rand_int import PtRandInt @@ -94,18 +96,21 @@ from .modules.pytorch_wrapper.pt_to_numpy import PtToNumpy from .modules.pytorch_wrapper.pt_to_rgb_tensors import PtToRgbTensors from .modules.pytorch_wrapper.pt_train_classification_model import PtTrainClassificationModel from .modules.pytorch_wrapper.pt_train_classification_model_lr import PtTrainClassificationModelLr +from .modules.pytorch_wrapper.pt_train_regression_model import PtTrainRegressionModel from .modules.pytorch_wrapper.pt_unsqueeze import PtUnsqueeze from .modules.pytorch_wrapper.pt_var import PtVar from .modules.pytorch_wrapper.pt_view import PtView from .modules.pytorch_wrapper.pt_where import PtWhere from .modules.pytorch_wrapper.pt_zeros import PtZeros from .modules.pytorch_wrapper.ptn_conv_model import PtnConvModel +from .modules.pytorch_wrapper.ptn_linear import PtnLinear from .modules.pytorch_wrapper.ptn_linear_model import PtnLinearModel from .modules.pytorch_wrapper.ptn_resnet_model import PtnResnetModel from .modules.pytorch_wrapper.pto_adam import PtoAdam from .modules.pytorch_wrapper.pto_lr_scheduler_cosine_annealing import PtoLrSchedulerCosineAnnealing from .modules.pytorch_wrapper.pto_lr_scheduler_reduce_on_plateau import PtoLrSchedulerReduceOnPlateau from .modules.pytorch_wrapper.pto_lr_scheduler_step import PtoLrSchedulerStep +from .modules.pytorch_wrapper.pto_simple import PtoSimple from .modules.pytorch_wrapper.ptv_dataset import PtvDataset from .modules.pytorch_wrapper.ptv_dataset_len import PtvDatasetLen from .modules.pytorch_wrapper.ptv_dataset_loader import PtvDatasetLoader @@ -142,6 +147,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PtCosh": PtCosh, "PtCrop": PtCrop, "PtDataLoader": PtDataLoader, + "PtDataLoaderFromTensors": PtDataLoaderFromTensors, "PtDiv": PtDiv, "PtEinsum": PtEinsum, "PtEq": PtEq, @@ -186,6 +192,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PtPermute": PtPermute, "PtPow": PtPow, "PtPredictClassificationModel": PtPredictClassificationModel, + "PtPredictRegressionModel": PtPredictRegressionModel, "PtProd": PtProd, "PtRand": PtRand, "PtRandInt": PtRandInt, @@ -216,18 +223,21 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PtToRgbTensors": PtToRgbTensors, "PtTrainClassificationModel": PtTrainClassificationModel, "PtTrainClassificationModelLr": PtTrainClassificationModelLr, + "PtTrainRegressionModel": PtTrainRegressionModel, "PtUnsqueeze": PtUnsqueeze, "PtVar": PtVar, "PtView": PtView, "PtWhere": PtWhere, "PtZeros": PtZeros, "PtnConvModel": PtnConvModel, + "PtnLinear": PtnLinear, "PtnLinearModel": PtnLinearModel, "PtnResnetModel": PtnResnetModel, "PtoAdam": PtoAdam, "PtoLrSchedulerCosineAnnealing": PtoLrSchedulerCosineAnnealing, "PtoLrSchedulerReduceOnPlateau": PtoLrSchedulerReduceOnPlateau, "PtoLrSchedulerStep": PtoLrSchedulerStep, + "PtoSimple": PtoSimple, "PtvDataset": PtvDataset, "PtvDatasetLen": PtvDatasetLen, "PtvDatasetLoader": PtvDatasetLoader, @@ -264,6 +274,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PtCosh": "Pt Cosh", "PtCrop": "Pt Crop", "PtDataLoader": "Pt Data Loader", + "PtDataLoaderFromTensors": "Pt Data Loader From Tensors", "PtDiv": "Pt Div", "PtEinsum": "Pt Einsum", "PtEq": "Pt Eq", @@ -308,6 +319,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PtPermute": "Pt Permute", "PtPow": "Pt Pow", "PtPredictClassificationModel": "Pt Predict Classification Model", + "PtPredictRegressionModel": "Pt Predict Regression Model", "PtProd": "Pt Prod", "PtRand": "Pt Rand", "PtRandInt": "Pt Rand Int", @@ -338,18 +350,21 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PtToRgbTensors": "Pt To Rgb Tensors", "PtTrainClassificationModel": "Pt Train Classification Model", "PtTrainClassificationModelLr": "Pt Train Classification Model Lr", + "PtTrainRegressionModel": "Pt Train Regression Model", "PtUnsqueeze": "Pt Unsqueeze", "PtVar": "Pt Var", "PtView": "Pt View", "PtWhere": "Pt Where", "PtZeros": "Pt Zeros", "PtnConvModel": "Ptn Conv Model", + "PtnLinear": "Ptn Linear", "PtnLinearModel": "Ptn Linear Model", "PtnResnetModel": "Ptn Resnet Model", "PtoAdam": "Pto Adam", "PtoLrSchedulerCosineAnnealing": "Pto Lr Scheduler Cosine Annealing", "PtoLrSchedulerReduceOnPlateau": "Pto Lr Scheduler Reduce On Plateau", "PtoLrSchedulerStep": "Pto Lr Scheduler Step", + "PtoSimple": "Pto Simple", "PtvDataset": "Ptv Dataset", "PtvDatasetLen": "Ptv Dataset Len", "PtvDatasetLoader": "Ptv Dataset Loader", diff --git a/docs/model_training.md b/docs/model_training.md index 545e63b..f2208f3 100644 --- a/docs/model_training.md +++ b/docs/model_training.md @@ -3,10 +3,13 @@ You can train a PyTorch model without any coding. Training is as simple as a single click! Currently, classification models are supported for following architecture (node names are in parentheses): -* Linear (Ptn Linear Model) +* Linear for classificication (Ptn Linear Model) * Convolutional Network (Ptn Conv Model) * ResNet (Ptn Resnet Model) +In addition, a single layer Linear model is supported as an example to illustrage the use (see the bottom of this page on how to use it): +* Linear(Ptn Linear) + You can adjust the model configuration including output dimension on the UI. The system is designed to be modular. For example, to switch from a linear model to a convolutional model, you only need to replace the model node (see screenshot below). @@ -69,3 +72,19 @@ This train_v8 model achieved 93.12% accuracy & train_v11_2nd_run model achieved **ResNet model** ![Eval](images/resnet_eval.png) + +# Using the Single-Layer Linear Model + +A single-layer linear model is included to illustrate how to connect various nodes. +A workflow containing this model and related nodes is in the following file: +`examples/workflows/simple_linear_regression_train.json`. + +This workflow illustrates: +- How to generate linear regression test data +- How to plot the data +- How to train a model by connecting it to an optimizer +- How to make predictions using a trained model + +If you run training, you will notice that the loss remains in the 20s–30s instead of approaching 0, as you might expect. + +Do you know why this happens? diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index 189cc44..040818c 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -132,12 +132,16 @@ | Node | Description | | --- | --- | | [Pt Data Loader](pt_data_loader.md) | Loads data from a dataset node and creates a PyTorch DataLoader. | +| [Pt Data Loader From Tensors](pt_data_loader_from_tensors.md) | Creates a Torchvision Dataloader from a pair of tensors. | | [Pt Evaluate Classification Model](pt_evaluate_classification_model.md) | Performs inference on test data and computes evaluation metrics. | | [Pt Load Model](pt_load_model.md) | A wrapper class for saving a PyTorch model. | | [Pt Predict Classification Model](pt_predict_classification_model.md) | Performs inference on input data. | +| [Pt Predict Regression Model](pt_predict_regression_model.md) | Performs inference on input data. | | [Pt Train Classification Model](pt_train_classification_model.md) | Trains a classification model using a given dataset, optimizer, and number of epochs. | | [Pt Train Classification Model Lr](pt_train_classification_model_lr.md) | Trains a classification model using a given dataset, optimizer, and number of epochs with learning rate decay. | +| [Pt Train Regression Model](pt_train_regression_model.md) | Trains a regression model using a given dataset, optimizer, and number of epochs. | | [Ptn Conv Model](ptn_conv_model.md) | A convolutional model consisting of multiple convolutional layers. | +| [Ptn Linear](ptn_linear.md) | A linear model consisting of a single dense layer. | | [Ptn Linear Model](ptn_linear_model.md) | A linear model consisting of dense layers. | | [Ptn Resnet Model](ptn_resnet_model.md) | A Resnet model consisting of multiple Resnet layers. | | [Pto Adam](pto_adam.md) | Instantiates the Adam optimizer. | diff --git a/docs/reference/pt_data_loader_from_tensors.md b/docs/reference/pt_data_loader_from_tensors.md new file mode 100644 index 0000000..cb65e39 --- /dev/null +++ b/docs/reference/pt_data_loader_from_tensors.md @@ -0,0 +1,28 @@ +# Pt Data Loader From Tensors +Creates a Torchvision Dataloader from a pair of tensors. + +Parameters + +**x** + Specify a tensor name that contains x. + +**y** + Specify a tensor name that contains y. + +## Input +| Name | Data type | +|---|---| +| x | Tensor | +| y | Tensor | +| batch_size | Int | +| shuffle | Boolean | + +## Output +| Data type | +|---| +| Ptdataloader | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_predict_regression_model.md b/docs/reference/pt_predict_regression_model.md new file mode 100644 index 0000000..1cb4e86 --- /dev/null +++ b/docs/reference/pt_predict_regression_model.md @@ -0,0 +1,14 @@ +# Pt Predict Regression Model +Performs inference on input data. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| inputs | Tensor | +| use_gpu | Boolean | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_train_regression_model.md b/docs/reference/pt_train_regression_model.md new file mode 100644 index 0000000..3307430 --- /dev/null +++ b/docs/reference/pt_train_regression_model.md @@ -0,0 +1,30 @@ +# Pt Train Regression Model +Trains a regression model using a given dataset, optimizer, and number of epochs. + +early_stopping_rounds: Specifies the number of rounds to monitor the evaluation loss when early_stopping is enabled and validation data is specified. If the loss does not decrease compared to the current best loss within this period, training will be stopped. +output_best_val_model: Whether to outputs the model with the best val loss or the model from the last epoch. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| train_loader | Ptdataloader | +| optimizer | Ptoptimizer | +| epochs | Int | +| use_gpu | Boolean | +| early_stopping | Boolean | +| early_stopping_rounds | Int | +| output_best_val_model | Boolean | +| val_loader | Ptdataloader | + +## Output +| Data type | +|---| +| Ptmodel | +| Tensor | +| Tensor | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptn_linear.md b/docs/reference/ptn_linear.md new file mode 100644 index 0000000..07bcb2d --- /dev/null +++ b/docs/reference/ptn_linear.md @@ -0,0 +1,24 @@ +# Ptn Linear +A linear model consisting of a single dense layer. + +### Fields: +- `in_features`: The number of input features. +- `out_features`: The number of output features. +- `bias`: Use bias or not. + +## Input +| Name | Data type | +|---|---| +| in_features | Int | +| out_features | Int | +| bias | Boolean | + +## Output +| Data type | +|---| +| Ptmodel | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/examples/workflows/linear_regression_data_creation.json b/examples/workflows/linear_regression_data_creation.json new file mode 100644 index 0000000..a2d373e --- /dev/null +++ b/examples/workflows/linear_regression_data_creation.json @@ -0,0 +1 @@ +{"last_node_id":35,"last_link_id":30,"nodes":[{"id":18,"type":"PtFloatCreate","pos":[2421.7119140625,2536.761962890625],"size":[210,76],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[17],"slot_index":0}],"properties":{"Node name for S&R":"PtFloatCreate"},"widgets_values":["5"]},{"id":22,"type":"PandasHorizontalConcat","pos":[3513.65576171875,1898.435302734375],"size":[219.87892150878906,80.06787109375],"flags":{},"order":18,"mode":0,"inputs":[{"name":"a_dataframe","type":"DATAFRAME","link":23},{"name":"b_dataframe","type":"DATAFRAME","link":24}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[25,26],"slot_index":0}],"properties":{"Node name for S&R":"PandasHorizontalConcat"},"widgets_values":[true]},{"id":24,"type":"PreviewImage","pos":[4241.6123046875,1893.748779296875],"size":[708.0863037109375,877.3831787109375],"flags":{},"order":21,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":27}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"}},{"id":20,"type":"SNSScatter","pos":[3812.416748046875,1897.8026123046875],"size":[315,250],"flags":{},"order":19,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":25}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[27],"slot_index":0}],"properties":{"Node name for S&R":"SNSScatter"},"widgets_values":["0","1","Linear regression data","x","y","",false,"",""]},{"id":21,"type":"PandasShowDataFrame","pos":[3815.650634765625,2223.204345703125],"size":[311.7101745605469,246.7060546875],"flags":{},"order":20,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":26}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[" 0 1\n0 -10.0 -7.978490\n1 -9.9 -20.898081\n2 -9.8 -8.654678\n3 -9.7 -16.395887\n4 -9.6 -23.496311\n.. ... ...\n195 9.5 17.094564\n196 9.6 27.086693\n197 9.7 22.323931\n198 9.8 19.835020\n199 9.9 23.677130\n\n[200 rows x 2 columns]"]},{"id":11,"type":"PtMul","pos":[2431.72021484375,1969.7384033203125],"size":[210,46],"flags":{},"order":12,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":10},{"name":"tens_b","type":"TENSOR","link":11}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[28],"slot_index":0}],"properties":{"Node name for S&R":"PtMul"},"widgets_values":[]},{"id":12,"type":"PtFloatCreate","pos":[2428.683837890625,2065.244873046875],"size":[210,76],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[11],"slot_index":0}],"properties":{"Node name for S&R":"PtFloatCreate"},"widgets_values":["2"]},{"id":14,"type":"PtFloatCreate","pos":[2701.57568359375,2069.322265625],"size":[210,76],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[29],"slot_index":0}],"properties":{"Node name for S&R":"PtFloatCreate"},"widgets_values":["5"]},{"id":25,"type":"PtAdd","pos":[2702.57763671875,1973.42431640625],"size":[210,46],"flags":{},"order":15,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":28,"localized_name":"tens_a"},{"name":"tens_b","type":"TENSOR","link":29,"localized_name":"tens_b"}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[30],"localized_name":"TENSOR"}],"properties":{"Node name for S&R":"PtAdd"}},{"id":16,"type":"PtAdd","pos":[2971.2314453125,1975.4918212890625],"size":[210,46],"flags":{},"order":16,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":30},{"name":"tens_b","type":"TENSOR","link":18}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[19],"slot_index":0}],"properties":{"Node name for S&R":"PtAdd"}},{"id":19,"type":"PandasCreateFromTensor","pos":[3225.404296875,1973.4239501953125],"size":[229.67843627929688,32.88075256347656],"flags":{},"order":17,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":19}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[24],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"}},{"id":30,"type":"Note","pos":[2705.520751953125,1849.6453857421875],"size":[140,60],"flags":{},"order":3,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y = x * 2 + 5"],"color":"#432","bgcolor":"#653"},{"id":10,"type":"PtArange","pos":[2026.09375,1685.531982421875],"size":[315,130],"flags":{},"order":4,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[10,22],"slot_index":0}],"properties":{"Node name for S&R":"PtArange"},"widgets_values":["-10","10","0.1","float32"]},{"id":23,"type":"PandasCreateFromTensor","pos":[3229.68212890625,1687.1087646484375],"size":[210,37.259437561035156],"flags":{},"order":13,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":22}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[23],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"}},{"id":29,"type":"Note","pos":[2438.76806640625,1849.6453857421875],"size":[140,60],"flags":{},"order":5,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y = x * 2"],"color":"#432","bgcolor":"#653"},{"id":31,"type":"Note","pos":[2974.888916015625,1849.6453857421875],"size":[210,62.56344985961914],"flags":{},"order":6,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y = x * 2 + 5 + 5N"],"color":"#432","bgcolor":"#653"},{"id":15,"type":"PtRandn","pos":[2421.7119140625,2373.174072265625],"size":[210,100],"flags":{},"order":7,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[16],"slot_index":0}],"properties":{"Node name for S&R":"PtRandn"},"widgets_values":["(200,)","float32"]},{"id":32,"type":"Note","pos":[2421.75927734375,2255.706787109375],"size":[210,62.56344985961914],"flags":{},"order":8,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["Generate noise: 5N"],"color":"#432","bgcolor":"#653"},{"id":17,"type":"PtMul","pos":[2676.08837890625,2463.640869140625],"size":[210,46],"flags":{},"order":14,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":16},{"name":"tens_b","type":"TENSOR","link":17}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[18],"slot_index":0}],"properties":{"Node name for S&R":"PtMul"}},{"id":34,"type":"Note","pos":[3226.182861328125,1854.1197509765625],"size":[210,62.56344985961914],"flags":{},"order":9,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y"],"color":"#432","bgcolor":"#653"},{"id":33,"type":"Note","pos":[3228.68408203125,1577.637939453125],"size":[210,62.56344985961914],"flags":{},"order":10,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["x"],"color":"#432","bgcolor":"#653"},{"id":35,"type":"MarkdownNote","pos":[2027.58447265625,1470.28125],"size":[639.1666259765625,69.1666259765625],"flags":{},"order":11,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Install ComfyUI-Data-Analysis extension to run this workflow"],"color":"#322","bgcolor":"#533"}],"links":[[10,10,0,11,0,"TENSOR"],[11,12,0,11,1,"TENSOR"],[16,15,0,17,0,"TENSOR"],[17,18,0,17,1,"TENSOR"],[18,17,0,16,1,"TENSOR"],[19,16,0,19,0,"TENSOR"],[22,10,0,23,0,"TENSOR"],[23,23,0,22,0,"DATAFRAME"],[24,19,0,22,1,"DATAFRAME"],[25,22,0,20,0,"DATAFRAME"],[26,22,0,21,0,"DATAFRAME"],[27,20,0,24,0,"IMAGE"],[28,11,0,25,0,"TENSOR"],[29,14,0,25,1,"TENSOR"],[30,25,0,16,0,"TENSOR"]],"groups":[],"config":{},"extra":{"ds":{"scale":1,"offset":[-1873.6998218852477,-1032.1097643012154]},"node_versions":{"pt-wrapper":"7fa7a82a1d9a69ce1191594167f79f0aa2a992e9","data-analysis":"bde4a07755390554f858e0b8fd8554a3123a6612","comfy-core":"v0.3.10-40-gd0f3752"},"ue_links":[]},"version":0.4} \ No newline at end of file diff --git a/examples/workflows/simple_linear_regression_train.json b/examples/workflows/simple_linear_regression_train.json new file mode 100644 index 0000000..b7590f4 --- /dev/null +++ b/examples/workflows/simple_linear_regression_train.json @@ -0,0 +1 @@ +{"last_node_id":57,"last_link_id":52,"nodes":[{"id":18,"type":"PtFloatCreate","pos":[2421.267333984375,3006.0869140625],"size":[210,76],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[17],"slot_index":0}],"properties":{"Node name for S&R":"PtFloatCreate"},"widgets_values":["5"]},{"id":14,"type":"PtFloatCreate","pos":[2678.386962890625,2538.647216796875],"size":[210,76],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[29],"slot_index":0}],"properties":{"Node name for S&R":"PtFloatCreate"},"widgets_values":["5"]},{"id":25,"type":"PtAdd","pos":[2678.386962890625,2442.75],"size":[210,46],"flags":{},"order":23,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":28},{"name":"tens_b","type":"TENSOR","link":29}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[30]}],"properties":{"Node name for S&R":"PtAdd"},"widgets_values":[]},{"id":31,"type":"Note","pos":[2974.4443359375,2323.44580078125],"size":[210,62.56344985961914],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y = x * 2 + 5 + 5N"],"color":"#432","bgcolor":"#653"},{"id":15,"type":"PtRandn","pos":[2421.267333984375,2842.4990234375],"size":[210,100],"flags":{},"order":3,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[16],"slot_index":0}],"properties":{"Node name for S&R":"PtRandn"},"widgets_values":["(200,)","float32"]},{"id":34,"type":"Note","pos":[3225.73828125,2323.44580078125],"size":[210,62.56344985961914],"flags":{},"order":4,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y"],"color":"#432","bgcolor":"#653"},{"id":16,"type":"PtAdd","pos":[2970.786865234375,2442.75],"size":[210,46],"flags":{},"order":24,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":30},{"name":"tens_b","type":"TENSOR","link":18}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[19,41],"slot_index":0}],"properties":{"Node name for S&R":"PtAdd"},"widgets_values":[]},{"id":17,"type":"PtMul","pos":[2678.386962890625,2932.9658203125],"size":[210,46],"flags":{},"order":17,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":16},{"name":"tens_b","type":"TENSOR","link":17}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[18,46],"slot_index":0}],"properties":{"Node name for S&R":"PtMul"},"widgets_values":[]},{"id":19,"type":"PandasCreateFromTensor","pos":[3224.959716796875,2442.75],"size":[229.67843627929688,32.88075256347656],"flags":{},"order":25,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":19}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[24],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"},"widgets_values":[]},{"id":23,"type":"PandasCreateFromTensor","pos":[3153.83740234375,2144.796142578125],"size":[210,37.259437561035156],"flags":{},"order":18,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":22}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[23],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"},"widgets_values":[]},{"id":24,"type":"PreviewImage","pos":[4380.41259765625,2306.112548828125],"size":[708.0863037109375,877.3831787109375],"flags":{},"order":32,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":27}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":10,"type":"PtArange","pos":[1932.921630859375,2285.625732421875],"size":[315,130],"flags":{},"order":5,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[10,22,43],"slot_index":0}],"properties":{"Node name for S&R":"PtArange"},"widgets_values":["-10","10","0.1","float32"]},{"id":33,"type":"Note","pos":[3152.83935546875,2035.3255615234375],"size":[210,62.56344985961914],"flags":{},"order":6,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["x"],"color":"#432","bgcolor":"#653"},{"id":35,"type":"MarkdownNote","pos":[1777.5826416015625,1682.30810546875],"size":[639.1666259765625,69.1666259765625],"flags":{},"order":7,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Install ComfyUI-Data-Analysis extension to run this workflow"],"color":"#322","bgcolor":"#533"},{"id":49,"type":"PtShowText","pos":[2678.386962890625,3031.3681640625],"size":[479.40484619140625,76],"flags":{},"order":22,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":46}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PtShowText"},"widgets_values":["tensor([ 2.2741, -3.0160, 7.7803, ..., 3.7806, 3.5735, -4.5967])"]},{"id":32,"type":"Note","pos":[2421.267333984375,2725.03173828125],"size":[210,62.56344985961914],"flags":{},"order":8,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["Generate noise: 5N"],"color":"#432","bgcolor":"#653"},{"id":55,"type":"Note","pos":[1788.75,4102.658203125],"size":[210,62.56344985961914],"flags":{},"order":9,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y"],"color":"#432","bgcolor":"#653"},{"id":47,"type":"PtUnsqueeze","pos":[1788.75,3960.348388671875],"size":[210,58],"flags":{},"order":19,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":43}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[44,45],"slot_index":0}],"properties":{"Node name for S&R":"PtUnsqueeze"},"widgets_values":[-1]},{"id":20,"type":"SNSScatter","pos":[4021.54150390625,2306.751953125],"size":[315,250],"flags":{},"order":29,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":25}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[27],"slot_index":0}],"properties":{"Node name for S&R":"SNSScatter"},"widgets_values":["0","1","Linear regression data","x","y","",false,"",""]},{"id":45,"type":"PtShowText","pos":[3398.84423828125,4166.6142578125],"size":[224.1166229248047,155.83897399902344],"flags":{},"order":35,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":40}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PtShowText"},"widgets_values":["tensor([[-17.6886],\n [-17.4626],\n [-17.2365],\n ...,\n [ 26.8479],\n [ 27.0740],\n [ 27.3001]])"]},{"id":22,"type":"PandasHorizontalConcat","pos":[3735.7724609375,2302.51220703125],"size":[219.87892150878906,80.06787109375],"flags":{},"order":27,"mode":0,"inputs":[{"name":"a_dataframe","type":"DATAFRAME","link":23},{"name":"b_dataframe","type":"DATAFRAME","link":24}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[25,26,52],"slot_index":0}],"properties":{"Node name for S&R":"PandasHorizontalConcat"},"widgets_values":[true]},{"id":21,"type":"PandasShowDataFrame","pos":[4018.99267578125,2615.068359375],"size":[311.7101745605469,246.7060546875],"flags":{},"order":30,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":26}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[" 0 1\n0 -10.0 -12.725923\n1 -9.9 -17.816046\n2 -9.8 -6.819702\n3 -9.7 -19.436806\n4 -9.6 -12.207089\n.. ... ...\n195 9.5 29.544865\n196 9.6 19.246469\n197 9.7 28.180630\n198 9.8 28.173508\n199 9.9 20.203293\n\n[200 rows x 2 columns]"]},{"id":51,"type":"PandasHorizontalConcat","pos":[4161.61865234375,3973.836181640625],"size":[219.87892150878906,80.06787109375],"flags":{},"order":37,"mode":0,"inputs":[{"name":"a_dataframe","type":"DATAFRAME","link":52},{"name":"b_dataframe","type":"DATAFRAME","link":47}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[48],"slot_index":0}],"properties":{"Node name for S&R":"PandasHorizontalConcat"},"widgets_values":[true]},{"id":53,"type":"PreviewImage","pos":[4727.55615234375,3973.836181640625],"size":[708.0863037109375,877.3831787109375],"flags":{},"order":39,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":49}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":38,"type":"PtDataLoaderFromTensors","pos":[2052.479248046875,4048.978759765625],"size":[236.6166534423828,103.83332061767578],"flags":{},"order":28,"mode":0,"inputs":[{"name":"x","type":"TENSOR","link":44},{"name":"y","type":"TENSOR","link":42}],"outputs":[{"name":"PTDATALOADER","type":"PTDATALOADER","links":[36],"slot_index":0}],"properties":{"Node name for S&R":"PtDataLoaderFromTensors"},"widgets_values":[8,false]},{"id":54,"type":"Note","pos":[1788.75,3837.5029296875],"size":[210,62.56344985961914],"flags":{},"order":10,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["x"],"color":"#432","bgcolor":"#653"},{"id":44,"type":"PtShowText","pos":[2732.6708984375,4344.0634765625],"size":[302.7361755371094,193.5445098876953],"flags":{},"order":34,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":38}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PtShowText"},"widgets_values":["tensor([60.0400, 46.7947, 40.0566, 35.8485, 33.1856, 31.4754, 30.3591, 29.6179, 29.1170, 28.7726, 28.5319, 28.3610, 28.2381, 28.1485, 28.0826, 28.0338, 27.9972, 27.9698, 27.9491, 27.9334, 27.9215, 27.9124, 27.9055, 27.9002, 27.8962, 27.8931, 27.8907, 27.8889, 27.8875, 27.8865, 27.8857, 27.8850, 27.8846, 27.8842,\n 27.8839, 27.8837, 27.8835, 27.8834, 27.8833, 27.8832, 27.8832, 27.8831, 27.8831, 27.8831, 27.8830, 27.8830, 27.8830, 27.8830, 27.8830, 27.8830])"]},{"id":41,"type":"PtTrainRegressionModel","pos":[2732.6708984375,4028.3388671875],"size":[315,214],"flags":{},"order":31,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":34},{"name":"train_loader","type":"PTDATALOADER","link":36},{"name":"optimizer","type":"PTOPTIMIZER","link":33},{"name":"val_loader","type":"PTDATALOADER","link":null,"shape":7}],"outputs":[{"name":"Model","type":"PTMODEL","links":[35],"slot_index":0},{"name":"train loss","type":"TENSOR","links":[38],"slot_index":1},{"name":"val loss","type":"TENSOR","links":null}],"properties":{"Node name for S&R":"PtTrainRegressionModel"},"widgets_values":[50,true,false,10,false]},{"id":46,"type":"PtUnsqueeze","pos":[1788.75,4212.8330078125],"size":[210,72.64090728759766],"flags":{},"order":26,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":41}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[42],"slot_index":0}],"properties":{"Node name for S&R":"PtUnsqueeze"},"widgets_values":[-1]},{"id":36,"type":"PtnLinear","pos":[2405.291015625,3869.361572265625],"size":[223.48721313476562,106],"flags":{},"order":11,"mode":0,"inputs":[],"outputs":[{"name":"PTMODEL","type":"PTMODEL","links":[31,34],"slot_index":0}],"properties":{"Node name for S&R":"PtnLinear"},"widgets_values":[1,1,true]},{"id":37,"type":"PtoSimple","pos":[2732.6708984375,3869.361572265625],"size":[259.8880310058594,79.7412338256836],"flags":{},"order":20,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":31}],"outputs":[{"name":"PTOPTIMIZER","type":"PTOPTIMIZER","links":[33],"slot_index":0}],"properties":{"Node name for S&R":"PtoSimple"},"widgets_values":[0.01]},{"id":50,"type":"PandasCreateFromTensor","pos":[3861.576171875,3973.836181640625],"size":[229.67843627929688,32.88075256347656],"flags":{},"order":36,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":50}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[47],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"},"widgets_values":[]},{"id":40,"type":"PtPredictRegressionModel","pos":[3394.962158203125,3973.836181640625],"size":[226.8000030517578,78],"flags":{},"order":33,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":35},{"name":"inputs","type":"TENSOR","link":45}],"outputs":[{"name":"y_hat","type":"TENSOR","links":[40,50],"slot_index":0}],"properties":{"Node name for S&R":"PtPredictRegressionModel"},"widgets_values":[true]},{"id":29,"type":"Note","pos":[2421.267333984375,2323.44580078125],"size":[210,60],"flags":{},"order":12,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y = x * 2"],"color":"#432","bgcolor":"#653"},{"id":30,"type":"Note","pos":[2678.386962890625,2323.44580078125],"size":[210,60],"flags":{},"order":13,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["y = x * 2 + 5"],"color":"#432","bgcolor":"#653"},{"id":11,"type":"PtMul","pos":[2421.267333984375,2442.75],"size":[210,46],"flags":{},"order":21,"mode":0,"inputs":[{"name":"tens_a","type":"TENSOR","link":10},{"name":"tens_b","type":"TENSOR","link":11}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[28],"slot_index":0}],"properties":{"Node name for S&R":"PtMul"},"widgets_values":[]},{"id":12,"type":"PtFloatCreate","pos":[2421.267333984375,2538.647216796875],"size":[210,76],"flags":{},"order":14,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[11],"slot_index":0}],"properties":{"Node name for S&R":"PtFloatCreate"},"widgets_values":["2"]},{"id":56,"type":"MarkdownNote","pos":[1737.6376953125,3654.15087890625],"size":[267.55413818359375,67.76414489746094],"flags":{},"order":15,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Train"],"color":"#432","bgcolor":"#653"},{"id":57,"type":"MarkdownNote","pos":[3301.96630859375,3655.156494140625],"size":[267.55413818359375,67.76414489746094],"flags":{},"order":16,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Predict"],"color":"#432","bgcolor":"#653"},{"id":52,"type":"SNSScatter","pos":[4454.83740234375,3973.836181640625],"size":[215.1750030517578,250],"flags":{},"order":38,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":48}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[49],"slot_index":0}],"properties":{"Node name for S&R":"SNSScatter"},"widgets_values":["0","1,2","Prediction - linear regression","x","y","label,predicted",false,"",""]}],"links":[[10,10,0,11,0,"TENSOR"],[11,12,0,11,1,"TENSOR"],[16,15,0,17,0,"TENSOR"],[17,18,0,17,1,"TENSOR"],[18,17,0,16,1,"TENSOR"],[19,16,0,19,0,"TENSOR"],[22,10,0,23,0,"TENSOR"],[23,23,0,22,0,"DATAFRAME"],[24,19,0,22,1,"DATAFRAME"],[25,22,0,20,0,"DATAFRAME"],[26,22,0,21,0,"DATAFRAME"],[27,20,0,24,0,"IMAGE"],[28,11,0,25,0,"TENSOR"],[29,14,0,25,1,"TENSOR"],[30,25,0,16,0,"TENSOR"],[31,36,0,37,0,"PTMODEL"],[33,37,0,41,2,"PTOPTIMIZER"],[34,36,0,41,0,"PTMODEL"],[35,41,0,40,0,"PTMODEL"],[36,38,0,41,1,"PTDATALOADER"],[38,41,1,44,0,"TENSOR"],[40,40,0,45,0,"TENSOR"],[41,16,0,46,0,"TENSOR"],[42,46,0,38,1,"TENSOR"],[43,10,0,47,0,"TENSOR"],[44,47,0,38,0,"TENSOR"],[45,47,0,40,1,"TENSOR"],[46,17,0,49,0,"TENSOR"],[47,50,0,51,1,"DATAFRAME"],[48,51,0,52,0,"DATAFRAME"],[49,52,0,53,0,"IMAGE"],[50,40,0,50,0,"TENSOR"],[52,22,0,51,0,"DATAFRAME"]],"groups":[{"id":1,"title":"Data generation","bounding":[1751.9085693359375,1843.1531982421875,1839.2666015625,1466.07080078125],"color":"#3f789e","font_size":24,"flags":{}},{"id":2,"title":"Train","bounding":[1731.6951904296875,3754.14794921875,1364.1678466796875,820.7886352539062],"color":"#88A","font_size":24,"flags":{}},{"id":3,"title":"Predict","bounding":[3296.763671875,3756.31640625,432.30755615234375,804.2732543945312],"color":"#8AA","font_size":24,"flags":{}},{"id":4,"title":"Visualize","bounding":[3841.342529296875,3756.410888671875,1617.6470947265625,1164.656005859375],"color":"#3f789e","font_size":24,"flags":{}},{"id":5,"title":"Visualize","bounding":[3706.822998046875,2133.1875,1541.466796875,1171.3060302734375],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":0.8121082206462121,"offset":[-4022.9390482114786,-3385.1826244077993]},"node_versions":{"pt-wrapper":"7915d119bdf3c85ff0ed83f0e18b9fa23574af9f","data-analysis":"bde4a07755390554f858e0b8fd8554a3123a6612","comfy-core":"v0.3.10-40-gd0f3752"},"ue_links":[]},"version":0.4} \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_data_loader_from_tensors.py b/modules/pytorch_wrapper/pt_data_loader_from_tensors.py new file mode 100644 index 0000000..6ae0d37 --- /dev/null +++ b/modules/pytorch_wrapper/pt_data_loader_from_tensors.py @@ -0,0 +1,67 @@ +from typing import Any, Dict, Tuple +import torch +from torch.utils.data import TensorDataset, DataLoader + + +class PtDataLoaderFromTensors: + """ + Creates a Torchvision Dataloader from a pair of tensors. + + Parameters + + **x** + Specify a tensor name that contains x. + + **y** + Specify a tensor name that contains y. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "x": ("TENSOR", {}), + "y": ("TENSOR", {}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 1e6}), + "shuffle": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES: tuple = ("PTDATALOADER",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + x: torch.Tensor, + y: torch.Tensor, + batch_size: int, + shuffle: bool) -> Tuple: + """ + Creates a Torchvision Dataloader from a pair of tensors. + + Args: + x (torch.Tensor): A tensor containing x. + y (torch.Tensor): A tensor containing y. + batch_size (int): The number of samples per batch. + shuffle (bool): If True, shuffles the dataset before each epoch. + + Returns: + Tuple: A tuple containing the dataset instance. + """ + + with torch.inference_mode(False): + dataset = TensorDataset(x, y) + dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle) + return (dataloader,) diff --git a/modules/pytorch_wrapper/pt_predict_classification_model.py b/modules/pytorch_wrapper/pt_predict_classification_model.py index e9e1e70..49f94e3 100644 --- a/modules/pytorch_wrapper/pt_predict_classification_model.py +++ b/modules/pytorch_wrapper/pt_predict_classification_model.py @@ -38,7 +38,7 @@ class PtPredictClassificationModel: class_id_to_name_map: Dict, use_gpu: bool) -> Tuple[str, int, float]: """ - Performs inference on test data and computes evaluation metrics. + Performs inference on test data. Args: model (torch.nn.Module): The PyTorch model to evaluate. diff --git a/modules/pytorch_wrapper/pt_predict_regression_model.py b/modules/pytorch_wrapper/pt_predict_regression_model.py new file mode 100644 index 0000000..6458c85 --- /dev/null +++ b/modules/pytorch_wrapper/pt_predict_regression_model.py @@ -0,0 +1,61 @@ +from typing import Any, Dict, Tuple +import torch +import torch.nn as nn +import torch.nn.functional as F + +class PtPredictRegressionModel: + """ + Performs inference on input data. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "inputs": ("TENSOR", {}), + "use_gpu": ("BOOLEAN", {"default":False}) + } + } + + RETURN_NAMES: Tuple[str] = ("y_hat",) + RETURN_TYPES: Tuple[str] = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + def f(self, model: nn.Module, + inputs: torch.Tensor, + use_gpu: bool) -> Tuple[torch.Tensor]: + """ + Performs inference on test data. + + Args: + model (torch.nn.Module): The PyTorch model to evaluate. + inputs (torch.Tensor): Input tensor. + use_gpu (bool): Whether to use GPU for inference. + + Returns: + Tuple[torch.Tensor]: A tuple containing the prediction. + """ + + with torch.inference_mode(): + if use_gpu: + model.to("cuda") + model.train(False) # TODO: Change to model.*e*v*a*l() once Comfy's security checker is fixed. + x = inputs + if x.dim() == 1: # Add axis for col + x = torch.unsqueeze(x, -1) + if use_gpu: + x = x.to("cuda") + outputs = model(x) + if use_gpu: + outputs = outputs.to("cpu") + return (outputs,) diff --git a/modules/pytorch_wrapper/pt_train_regression_model.py b/modules/pytorch_wrapper/pt_train_regression_model.py new file mode 100644 index 0000000..419aae8 --- /dev/null +++ b/modules/pytorch_wrapper/pt_train_regression_model.py @@ -0,0 +1,167 @@ +import os +import sys +from typing import Any, Dict, Optional, Tuple +import torch +import torch.nn as nn +import torch.nn.functional as F +import numpy as np + +class PtTrainRegressionModel: + """ + Trains a regression model using a given dataset, optimizer, and number of epochs. + + early_stopping_rounds: Specifies the number of rounds to monitor the evaluation loss when early_stopping is enabled and validation data is specified. If the loss does not decrease compared to the current best loss within this period, training will be stopped. + output_best_val_model: Whether to outputs the model with the best val loss or the model from the last epoch. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types required for training the model. + + Returns: + Dict[str, Any]: A dictionary specifying required and optional input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "train_loader": ("PTDATALOADER", {}), + "optimizer": ("PTOPTIMIZER", {}), + "epochs": ("INT", {"default":1, "min":1, "max":1e6}), + "use_gpu": ("BOOLEAN", {"default":False}), + "early_stopping": ("BOOLEAN", {"default":False}), + "early_stopping_rounds": ("INT", {"default":10, "min":1, "max":1000}), + "output_best_val_model": ("BOOLEAN", {"default":True}) + }, + "optional": { + "val_loader": ("PTDATALOADER", {}), + } + } + + RETURN_NAMES: tuple = ("Model", "train loss", "val loss") + RETURN_TYPES: tuple = ("PTMODEL","TENSOR","TENSOR") + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, model: nn.Module, + train_loader: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + epochs: int, + use_gpu: bool, + early_stopping: bool, + early_stopping_rounds: int, + output_best_val_model:bool, + val_loader: Optional[torch.utils.data.DataLoader] = None) -> Tuple[nn.Module, torch.Tensor, torch.Tensor]: + """ + Trains a regression model. + + Args: + model (torch.nn.Module): The PyTorch model to be trained. + train_loader (torch.utils.data.DataLoader): DataLoader containing the training dataset. + optimizer (torch.optim.Optimizer): Optimizer used for training. + epochs (int): Number of training epochs. + use_gpu (bool): Whether to use GPU for training. + early_stopping (bool): Whether to turn on early stopping + early_stopping_rounds (int): Specifies the number of rounds to monitor the evaluation loss when early_stopping is enabled and validation data is specified. If the loss does not decrease compared to the current best loss within this period, training will be stopped. + output_best_val_model (bool): Whether to outputs the model with the best val loss or the model from the last epoch. + val_loader (Optional[torch.utils.data.DataLoader]): DataLoader for validation dataset (optional). + + Returns: + Tuple[torch.nn.Module, torch.Tensor, torch.Tensor]: A tuple containing the trained model, + training loss history, and validation loss history. + """ + + checkpoint_dir = os.path.realpath( + os.path.join(os.path.dirname(__file__), "..", "..", "models", "tmp")) + print(f"Checkpoint tmp directory is set to {checkpoint_dir}") + if os.path.exists(checkpoint_dir) is False: + os.makedirs(checkpoint_dir) + print(f"Created {checkpoint_dir}") + best_checkpoint_path = os.path.join(checkpoint_dir, "best_model.pt") + + best_val_loss = None + rounds_since_best_val_loss = 0 + + with torch.inference_mode(False): + train_loss = [] + val_loss = [] + if use_gpu: + model.to("cuda") + + loss_function = nn.MSELoss() # apply mean loss + + # Train the model + for epoch in range(epochs): + model.train() + total_loss = 0 + total_samples = len(train_loader.dataset) + for images, labels in train_loader: + if use_gpu: + images = images.to("cuda") + labels = labels.to("cuda") + optimizer.zero_grad() + outputs = model(images) + loss = loss_function(outputs, labels) + loss.backward() + optimizer.step() + + batch_size = labels.size(0) + batch_loss = loss.item() * batch_size + total_loss += batch_loss + + epoch_loss = total_loss / total_samples + + print(f"Epoch (train) {epoch+1}/{epochs}, Loss: {epoch_loss:.4f}") + train_loss.append(epoch_loss) + + if val_loader: + with torch.no_grad(): + model.train(False) # TODO: Change to model.*e*v*a*l() once Comfy's security checker is fixed. + total_loss = 0 + total_samples = len(val_loader.dataset) + for images, labels in val_loader: + if use_gpu: + images = images.to("cuda") + labels = labels.to("cuda") + outputs = model(images) + loss = loss_function(outputs, labels) + batch_size = labels.size(0) + batch_loss = loss.item() * batch_size + total_loss += batch_loss + + epoch_loss = total_loss / total_samples + print(f"Epoch (val) {epoch+1}/{epochs}, Loss: {epoch_loss:.4f}") + val_loss.append(epoch_loss) + + if best_val_loss is None or epoch_loss < best_val_loss: + best_val_loss = epoch_loss + rounds_since_best_val_loss = 0 + torch.save(model.state_dict(), best_checkpoint_path) + print(f"Updated best val loss. Saved the model as {best_checkpoint_path}") + else: + rounds_since_best_val_loss += 1 + if early_stopping: + if rounds_since_best_val_loss >= early_stopping_rounds: + print(f"Val loss did not improve in {early_stopping_rounds} rounds. Stopping training.") + break + + if use_gpu: + model.to("cpu") + + if val_loader: + if epoch_loss > best_val_loss and output_best_val_model: + sd = torch.load(best_checkpoint_path, weights_only=True) + model.load_state_dict(sd) + print("Loaded the best model for output") + else: + print("Using the final epoch model for output.") + + return (model, + torch.tensor(train_loss, dtype=torch.float32), + torch.tensor(val_loss, dtype=torch.float32)) \ No newline at end of file diff --git a/modules/pytorch_wrapper/ptn_linear.py b/modules/pytorch_wrapper/ptn_linear.py new file mode 100644 index 0000000..19671b4 --- /dev/null +++ b/modules/pytorch_wrapper/ptn_linear.py @@ -0,0 +1,61 @@ +from typing import Any, Dict, Tuple +import torch +import torch.nn as nn + + +class PtnLinear: + """ + A linear model consisting of a single dense layer. + + ### Fields: + - `in_features`: The number of input features. + - `out_features`: The number of output features. + - `bias`: Use bias or not. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "in_features": ("INT", {"default": 1, "min": 1, "max": 1e6}), + "out_features": ("INT", {"default": 1, "min": 1, "max": 1e6}), + "bias": ("BOOLEAN", {"default": True}) + } + } + + RETURN_TYPES: Tuple[str] = ("PTMODEL",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + in_features: int, + out_features: int, + bias: bool + ) -> Tuple[nn.Module]: + """ + Constructs a dense model. + + Args: + in_features (int): The number of input features + out_features (int): The number of output features + bias (bool): Use bias or not. + + Returns: + Tuple[nn.Module]: A tuple containing the instantiated PyTorch model. + """ + with torch.inference_mode(False): + model = nn.Linear(in_features, out_features, bias) + + return (model,) diff --git a/modules/pytorch_wrapper/pto_adam.py b/modules/pytorch_wrapper/pto_adam.py index ddc1c1a..5b8a9cf 100644 --- a/modules/pytorch_wrapper/pto_adam.py +++ b/modules/pytorch_wrapper/pto_adam.py @@ -29,14 +29,14 @@ class PtoAdam: RETURN_TYPES: tuple = ("PTOPTIMIZER",) FUNCTION: str = "f" - CATEGORY: str = "Data Analysis" + CATEGORY: str = "Training" def f(self, model, learning_rate, beta1, beta2) -> tuple: """ - Converts PyTorch tensor to NumPy ndarray. + Instantiates the optimizer. Args: - tens (torch.Tensor): PyTorch Tensor + model (torch.nn.Module): PyTorch model Returns: tuple: A tuple containing the ndarray. diff --git a/modules/pytorch_wrapper/pto_simple.py b/modules/pytorch_wrapper/pto_simple.py new file mode 100644 index 0000000..93e8eb6 --- /dev/null +++ b/modules/pytorch_wrapper/pto_simple.py @@ -0,0 +1,88 @@ +from typing import Any, Dict, Union, Iterable +import torch +import torch.optim + + +class SimpleOptimizer(torch.optim.Optimizer): + """ + Implementation of the most basic optimizer to update W using the below formula: + +$$ +W -= lr * G +$$ + + In Pytorch, +$$ +param.data -= lr * param.grad +$$ + +but accessing param.data directly can be tricky, so we can use: +$$ +param -= lr * param.grad +$$ + + step() is used instead of forward() so that LR scheduler can call. + + pragma: skip_doc + """ + def __init__(self, + parameters:Iterable[Union[torch.nn.Parameter, Dict[str, Any]]], + lr: float): + additional_dict = {"lr": lr} + super().__init__(parameters, additional_dict) + + def step(self): + with torch.no_grad(): + # Normally, parameter_group is a list containing a single dict. + # However, if the user passes a dict to have separater parameter + # for each layer, it can have multiple dicts inside. + # param_groups is set up in the super constructor. + # You do not need to define this yourself. + for g in self.param_groups: + for parameter in g["params"]: + if hasattr(parameter, "grad") and parameter.grad is not None: + parameter -= g["lr"] * parameter.grad + + +class PtoSimple: + """ + Instantiates the most basic optimizer to update W using the below formula: +$$ +W -= lr * G +$$ + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "learning_rate": ("FLOAT", {"default":0.001, "min":1e-10, "max":1, "step":0.0000000001}), + } + } + + RETURN_TYPES: tuple = ("PTOPTIMIZER",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + def f(self, model, learning_rate) -> tuple: + """ + Instantiates the optimizer. + + Args: + model (torch.nn.Module): PyTorch model + learning_rate (float): Learning rate + + Returns: + tuple: A tuple containing the ndarray. + """ + with torch.inference_mode(False): + opt = SimpleOptimizer(model.parameters(), lr=learning_rate) + return(opt,) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d17504e..30b3568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "pt-wrapper" description = "PyTorch extension for ComfyUI featuring extensive PyTorch wrapper nodes for seamless tensor operations and PyTorch model training." -version = "3.0.7" +version = "3.0.8" license = {file = "LICENSE"} [project.urls] diff --git a/tests/pytorch_wrapper/pt_data_loader_from_tensors_test.py b/tests/pytorch_wrapper/pt_data_loader_from_tensors_test.py new file mode 100644 index 0000000..bd91ffe --- /dev/null +++ b/tests/pytorch_wrapper/pt_data_loader_from_tensors_test.py @@ -0,0 +1,72 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_data_loader_from_tensors import PtDataLoaderFromTensors + +class TestPtDataLoader(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.data_loader_node = PtDataLoaderFromTensors() + + def test_with_1d(self): + """Tests with transform""" + x = torch.Tensor([0, 1, 2, 3]) + y = torch.Tensor([10, 20, 30, 40]) + + bs = 2 + train_data = self.data_loader_node.f( + x=x, + y=y, + batch_size=bs, + shuffle=True + )[0] + + x, y = next(iter(train_data)) + self.assertEqual(x.size(), torch.Size([bs])) + self.assertEqual(y.size(), torch.Size([bs])) + + def test_with_2d(self): + """Tests with transform""" + x = torch.Tensor([0,1,2,3]) + x = torch.unsqueeze(x, -1) + y = torch.Tensor([10,20,30,40]) + + bs = 2 + train_data = self.data_loader_node.f( + x=x, + y=y, + batch_size=bs, + shuffle=True + )[0] + + x, y = next(iter(train_data)) + self.assertEqual(x.size(), torch.Size([bs, 1])) + self.assertEqual(y.size(), torch.Size([bs])) + + def test_with_2d_no_shuffle(self): + """Tests with transform""" + x = torch.Tensor([0,1,2,3]) + x = torch.unsqueeze(x, -1) + y = torch.Tensor([10,20,30,40]) + + bs = 2 + train_data = self.data_loader_node.f( + x=x, + y=y, + batch_size=bs, + shuffle=False + )[0] + + x, y = next(iter(train_data)) + self.assertEqual(x[0], 0) + self.assertEqual(y[0], 10) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_predict_classification_conv_model_test.py b/tests/pytorch_wrapper/pt_predict_classification_conv_model_test.py index c99eda4..001407b 100644 --- a/tests/pytorch_wrapper/pt_predict_classification_conv_model_test.py +++ b/tests/pytorch_wrapper/pt_predict_classification_conv_model_test.py @@ -14,7 +14,7 @@ from pytorch_wrapper.pt_data_loader import PtDataLoader from pytorch_wrapper.pt_predict_classification_model import PtPredictClassificationModel from pytorch_wrapper.pt_load_model import PtLoadModel -class TestPtPrediction(unittest.TestCase): +class TestPtPredictionClassificationConvModel(unittest.TestCase): def setUp(self): """Set up test instance.""" diff --git a/tests/pytorch_wrapper/pt_predict_regression_model_test.py b/tests/pytorch_wrapper/pt_predict_regression_model_test.py new file mode 100644 index 0000000..af6c9cc --- /dev/null +++ b/tests/pytorch_wrapper/pt_predict_regression_model_test.py @@ -0,0 +1,44 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.pt_predict_regression_model import PtPredictRegressionModel + + +class TestModel(nn.Module): + def __init__(self): + super().__init__() + self.linear = nn.Linear(1, 1, True) + def forward(self, inputs): + return self.linear(inputs) + +class TestPtPredictionRegressionModel(unittest.TestCase): + + def setUp(self): + """Set up test instance.""" + self.model = TestModel() + self.x = torch.arange(0, 11, dtype=torch.float32) + + def test_prediction(self): + """ + Tests prediction. + + Note that this test requires a trained model file on the file system. + Unit test will fail if you run this for the first time. + Run the unit test for training to generate the weight, and re-run this unit test. + """ + self.predict = PtPredictRegressionModel().f( + model = self.model, + inputs = self.x, + use_gpu = True + )[0] + + self.assertTrue(self.predict.size() == torch.unsqueeze(self.x,-1).size()) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptn_linear_model_test.py b/tests/pytorch_wrapper/ptn_linear_model_test.py index 124a15a..cbf6c27 100644 --- a/tests/pytorch_wrapper/ptn_linear_model_test.py +++ b/tests/pytorch_wrapper/ptn_linear_model_test.py @@ -6,7 +6,7 @@ import torch PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") sys.path.append(MODULE_ROOT) -from pytorch_wrapper.ptn_linear_model import PtnLinearModel # Updated import to PtnLinear +from pytorch_wrapper.ptn_linear_model import PtnLinearModel class TestPtnLinearModel(unittest.TestCase): diff --git a/tests/pytorch_wrapper/ptn_linear_test.py b/tests/pytorch_wrapper/ptn_linear_test.py new file mode 100644 index 0000000..9f62c81 --- /dev/null +++ b/tests/pytorch_wrapper/ptn_linear_test.py @@ -0,0 +1,47 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_linear import PtnLinear + + +class TestPtnLinearModel(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.model_node = PtnLinear() + + def test_1(self): + self.model = self.model_node.f( + 2, # in + 32, # out + True # bias + )[0] + x = torch.ones(4, 2) + out = self.model(x) + self.assertEqual(out.size(), torch.Size([4, 32])) + # e contains [0] name and [1] param value, so extract the first element. + param_names = list( + map(lambda e: e[0], self.model.named_parameters())) + self.assertTrue("bias" in param_names) + + def test_2(self): + self.model = self.model_node.f( + 2, # in + 1, # out + False # bias + )[0] + x = torch.ones(4, 2) + out = self.model(x) + self.assertEqual(out.size(), torch.Size([4, 1])) + # e contains [0] name and [1] param value, so extract the first element. + param_names = list( + map(lambda e: e[0], self.model.named_parameters())) + self.assertFalse("bias" in param_names) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pto_simple_test.py b/tests/pytorch_wrapper/pto_simple_test.py new file mode 100644 index 0000000..eb115d1 --- /dev/null +++ b/tests/pytorch_wrapper/pto_simple_test.py @@ -0,0 +1,35 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pto_simple import PtoSimple + + +class TestPtoSimple(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtoSimple() + + def test_1(self): + model = torch.nn.Linear(2, 3, bias=False) + torch.nn.init.constant_(model.weight, 5.0) + model.weight.grad = torch.full_like(model.weight, 1.0) + opt = self.node.f(model, 0.1)[0] + opt.step() # 5.0 - 0.1 * 1.0 = 4.9 + expected = 4.9 + result = model.weight.data[0, 0] + self.assertAlmostEqual( + result.item(), + expected, + places=6, + msg=f"Expected {expected}, got {result}") + + +if __name__ == "__main__": + unittest.main() diff --git a/web/js/ShowTextGeneric.js b/web/js/ShowTextGeneric.js index f4ef06e..6c6b323 100644 --- a/web/js/ShowTextGeneric.js +++ b/web/js/ShowTextGeneric.js @@ -10,7 +10,7 @@ import { ComfyWidgets } from "../../../scripts/widgets.js"; // Displays input text on a node app.registerExtension({ - name: "HowToSD.ShowTextGeneric", + name: "HowToSD.PtWrapperShowTextGeneric", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (["PtShowText", "PtShowSize"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python function populate(text) {