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**

+
+# 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) {