This article compares the implementation of a classification model for the classic Iris flower dataset using two different approaches: PyTorch, the leading research library in Python, and OpenNN, a high-performance neural networks library written in C++.
While PyTorch provides great flexibility for prototyping, OpenNN offers a streamlined API for deploying neural networks in production environments where C++ performance and integration are critical. We will compare the entire workflow, from data loading to final model deployment.
Contents:
- Introduction.
- Code Complexity Analysis.
- Data Set.
- Model Construction.
- Training Strategy.
- Testing Analysis.
- Deployment.
- Model Export.
- Conclusions.
1. Introduction
The Iris flower data set is a multivariate data set that introduces the problem of pattern recognition. The goal is to classify iris flowers into three species (setosa, versicolor, and virginica) based on four physical measurements:
- Sepal length
- Sepal width
- Petal length
- Petal width
We will build, train, and test a feed-forward neural network to solve this classification task, observing the differences in API design and philosophy between the two libraries.
2. Code Complexity Analysis
Before diving into the implementation details, we quantified the development effort required by both libraries using the Logical Source Lines of Code (LSLOC) metric. This method ensures a fair comparison by ignoring non-executable lines (such as comments or syntactic C++ braces) and counting multi-line Python statements as single logical instructions.
The analysis yields the following results for the complete workflow (loading, training, evaluation, and deployment):
- PyTorch (Python): 43 logical instructions.
- OpenNN (C++): 26 logical instructions.
Below is the complete source code for both implementations. Notice how the C++ version, despite being a lower-level language, is more compact due to OpenNN’s encapsulation.
OpenNN (C++)
#include "../../opennn/dataset.h"
#include "../../opennn/standard_networks.h"
#include "../../opennn/training_strategy.h"
#include "../../opennn/testing_analysis.h"
using namespace opennn;
int main()
{
try
{
cout << "OpenNN. Iris Plant Example." << endl;
// Dataset
Dataset dataset("../data/iris_plant_original.csv", ";", true, false);
const Index inputs_number = dataset.get_variables_number("Input");
const Index targets_number = dataset.get_variables_number("Target");
// Neural network
const Index neurons_number = 16;
ClassificationNetwork classification_network({inputs_number}, {neurons_number}, {targets_number});
// Training Strategy
TrainingStrategy training_strategy(&classification_network, &dataset);
training_strategy.train();
// Testing Analysis
const TestingAnalysis testing_analysis(&classification_network, &dataset);
cout << "Confusion matrix:\n" << testing_analysis.calculate_confusion() << endl;
// Deployment
Tensor<type, 2> input_tensor(1, 4);
input_tensor.setValues({{5.1, 3.5, 1.4, 0.2}});
const Tensor<type, 2> output_tensor = classification_network.calculate_outputs<2, 2>(input_tensor);
cout << "Class probabilities: " << output_tensor << endl;
// Export
classification_network.save("iris_model.xml");
return 0;
}
catch(const exception& e)
{
cout << e.what() << endl;
return 1;
}
}
PyTorch (Python)
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 1) Load CSV
df = pd.read_csv("irisflowers.csv")
X = df[["sepal_length", "sepal_width", "petal_length", "petal_width"]].values.astype(np.float32)
y = pd.factorize(df["class"])[0].astype(np.int64)
# 2) Split + scale
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
X_train = torch.tensor(X_train)
X_test = torch.tensor(X_test)
y_train = torch.tensor(y_train)
y_test = torch.tensor(y_test)
# 3) Model
model = nn.Sequential(
nn.Linear(4, 16),
nn.Tanh(),
nn.Linear(16, 3)
)
opt = torch.optim.LBFGS(model.parameters(), lr=1.0)
loss_fn = nn.CrossEntropyLoss()
# 4) Training
def closure():
opt.zero_grad()
outputs = model(X_train)
loss = loss_fn(outputs, y_train)
loss.backward()
return loss
print("Starting PyTorch training with L-BFGS...")
for epoch in range(15):
loss = opt.step(closure)
print(f"Epoch {epoch+1}, Loss: {loss.item()}")
# 5) Confusion matrix
with torch.no_grad():
preds = model(X_test).softmax(dim=1).argmax(dim=1)
cm = torch.zeros(3, 3, dtype=torch.int64)
for t, p in zip(y_test, preds):
cm[t, p] += 1
print("Confusion matrix:\n", cm)
# 6) Deployment
x = torch.tensor(
scaler.transform([[5.1, 3.5, 1.4, 0.2]]),
dtype=torch.float32
)
with torch.no_grad():
y = model(x).argmax(1)
print("Predicted class:", y.item())
# 7) Export
example = torch.randn(1, 4)
torch.jit.trace(model, example).save("iris_model.pt")
torch.onnx.export(
model, example, "iris_model.onnx",
input_names=["input"], output_names=["logits"],
opset_version=17
)
The results show that the OpenNN implementation is approximately 40% more concise. This efficiency stems from OpenNN’s high-level abstraction: complex procedures—such as the L-BFGS optimization loop or the confusion matrix calculation—are encapsulated within the TrainingStrategy and TestingAnalysis classes. In contrast, PyTorch requires the user to explicitly define the optimization closure, the training loop iteration, and the metric accumulation logic.
3. Data Set
One of the most critical parts of machine learning is data preparation. In Python, this typically involves a combination of libraries: Pandas for loading, Scikit-Learn for splitting and scaling, and PyTorch for tensor conversion.
PyTorch
You are responsible for every step. This multi-library approach requires managing dependencies between different objects.
# Python: The multi-library approach
# 1. Load with Pandas
df = pd.read_csv("irisflowers.csv")
X = df[["sepal_length", "sepal_width", "petal_length", "petal_width"]].values.astype(np.float32)
y = pd.factorize(df["class"])[0].astype(np.int64)
# 2. Split & scale
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
X_train = torch.tensor(X_train)
X_test = torch.tensor(X_test)
y_train = torch.tensor(y_train)
y_test = torch.tensor(y_test)
OpenNN
OpenNN’s Dataset class is designed to handle the entire lifecycle of data management. By simply loading the file, OpenNN automatically handles scaling, splitting, and data structuring internally.
// C++: The unified approach
Dataset dataset("irisflowers.csv", ",", true);
// OpenNN automatically detects inputs and targets,
// calculates statistics, scales variables, and splits the data.
Beyond just loading data, the Dataset class allows for immediate inspection of the data quality. It can automatically calculate descriptive statistics (mean, standard deviation) and basic correlations, giving the engineer instant insight into the problem without writing extra analysis scripts.
4. Model Construction
To ensure a fair comparison, we will implement the same architecture in both frameworks: a hidden dense layer with 16 neurons and tanh activation, followed by a Softmax output layer.
PyTorch
You must manually define each layer and know the technical details of how the final layer and loss function should interact for numerical stability. The standard practice in PyTorch is to have the model output raw logits and use CrossEntropyLoss, which applies Softmax internally.
# Python: Manually defining the architecture to produce logits
model = nn.Sequential(
nn.Linear(4, 16),
nn.Tanh(),
nn.Linear(16, 3)
)
# This loss function expects logits and applies Softmax internally
loss_fn = nn.CrossEntropyLoss()
OpenNN
OpenNN’s ClassificationNetwork automatically builds a robust, production-ready architecture. By default, it uses tanh activation and adds the final Softmax layer, encapsulating best practices in a single line of code.
// C++: Professional architecture by default
const Index inputs_number = dataset.get_variables_number("Input");
const Index targets_number = dataset.get_variables_number("Target");
const Index neurons_number = 16;
// Creates: Scaling -> Perceptron(16, Tanh) -> Probabilistic(Softmax)
ClassificationNetwork classification_network(
{inputs_number},
{neurons_number},
{targets_number}
);
5. Training Strategy
The optimizer is critical for model performance. For datasets of this size (small to medium), second-order algorithms (Quasi-Newton) are often mathematically superior to standard stochastic gradient descent, offering faster convergence and greater stability.
OpenNN selects this optimal method by default. To ensure a fair comparison based on the best possible algorithm for this problem, we will use its equivalent in PyTorch, L-BFGS, which approximates second-order curvature information and is particularly effective in low-dimensional, full-batch optimization problems like Iris Flowers.
PyTorch
Although L-BFGS is not the most commonly used optimizer in PyTorch workflows, it is included here to provide a fair comparison with OpenNN’s default Quasi-Newton training strategy, which is particularly well suited for small, full-batch problems like Iris.
# Python: L-BFGS requires a complex closure structure
opt = torch.optim.LBFGS(model.parameters(), lr=1.0)
def closure():
opt.zero_grad()
outputs = model(X_train)
loss = loss_fn(model(X_train), y_train)
loss.backward()
return loss
for epoch in range(15):
loss = opt.step(closure)
OpenNN
The TrainingStrategy class automatically selects the best optimizer for the job. For the Iris dataset, it defaults to the Quasi-Newton method. The user code remains minimal and declarative, regardless of the underlying algorithm’s complexity.
// C++: The complexity is abstracted away TrainingStrategy training_strategy(&classification_network, &dataset); training_strategy.train();
Furthermore, OpenNN generates comprehensive training reports. While PyTorch requires manual logging and integration with external plotting libraries (like Matplotlib or TensorBoard) to visualize convergence, OpenNN provides built-in tools to monitor the loss history and selection errors automatically.
6. Testing Analysis
After training, both models are evaluated on their respective test sets to verify convergence and generalization.
PyTorch
The PyTorch model successfully converges, achieving excellent performance with a generic accuracy over 96% on the test set.
with torch.no_grad():
preds = model(X_test).softmax(dim=1).argmax(dim=1)
cm = torch.zeros(3, 3, dtype=torch.int64)
for t, p in zip(y_test, preds):
cm[t, p] += 1
print("Confusion matrix:\n", cm)
PyTorch confusion matrix:
tensor([[10, 0, 0],
[ 0, 9, 1],
[ 0, 0, 10]])
OpenNN
The OpenNN model also achieves excellent results, reaching perfect or near-perfect accuracy on the test set. This confirms that OpenNN’s implementation of the Quasi-Newton method is numerically stable and matches the predictive power of the leading research libraries, while handling the data pipeline internally.
// C++: concise analysis classes
TestingAnalysis testing_analysis(&classification_network, &dataset);
cout << "Confusion matrix:\n"
<< testing_analysis.calculate_confusion() << endl;
OpenNN confusion matrix: 11 0 0 11 0 10 0 10 0 0 9 9 11 10 9 30
The evaluation in OpenNN goes beyond a simple confusion matrix. The library is capable of automatically generating advanced engineering metrics such as ROC curves and Lift charts. To replicate this level of reporting in the Python ecosystem, one would need to manually calculate these metrics using Scikit-Learn and plot them, adding yet another layer of dependency.
7. Deployment
Finally, we use the trained model to predict the class of a new, unseen flower. Both frameworks expect inputs in a «batch» format, even for a single prediction.
PyTorch
For deployment, you must remember to use the scaler object that was fitted on the training data to preprocess any new input.
# Python: Manual scaling is required before prediction
x = torch.tensor(
scaler.transform([[5.1, 3.5, 1.4, 0.2]]),
dtype=torch.float32
)
with torch.no_grad():
y = model(x).argmax(1)
print("Predicted class:", y.item())
OpenNN
In OpenNN, the scaling layer is part of the model itself. The calculate_outputs method handles all necessary preprocessing automatically. The user only needs to provide the data in a Tensor object. This encapsulation eliminates the risk of «training-serving skew,» a common production issue where preprocessing logic in deployment subtly differs from training, leading to silent model failures.
// C++: Scaling is handled automatically within the model
Tensor<type, 2> input_tensor(1, 4);
input_tensor.setValues({{5.1, 3.5, 1.4, 0.2}});
const Tensor<type, 2> output_tensor = classification_network.calculate_outputs<2, 2>(input_tensor);
cout << "Class probabilities: " << output_tensor << endl;
8. Model Export
In production environments, the ability to save and load models efficiently is paramount.
PyTorch
Since PyTorch is Python-based, deploying the model into a C++ application usually requires converting it to an intermediate format like ONNX or using TorchScript (JIT). This adds an extra step in the pipeline and introduces potential version compatibility issues.
# Python: Exporting to ONNX for C++ usage
example = torch.randn(1, 4)
torch.jit.trace(model, example).save("iris_model.pt")
torch.onnx.export(
model, example, "iris_model.onnx",
input_names=["input"], output_names=["logits"],
opset_version=17
)
OpenNN
OpenNN saves the model in a standardized, clean XML format. Since the library is native C++, this file can be loaded directly by the executable without any intermediate conversion or interpreters, ensuring absolute reproducibility.
// C++: Native XML serialization
classification_network.save("iris_model.xml");
9. Conclusions
This comparison highlights two different philosophies:
- PyTorch provides a flexible, low-level toolkit that requires significant expertise to assemble correctly. The user is responsible for data scaling, choosing the right combination of output layers and loss functions, and writing complex training loops for advanced optimizers.
- OpenNN offers a high-level, integrated solution that encapsulates best practices into simple, powerful classes. It automates data preparation, model architecture, and solver selection, leading to more readable code, fewer errors, and excellent out-of-the-box results.
For engineers and developers who need to build and deploy reliable, high-performance C++ neural networks, OpenNN provides the most direct and robust path from data to deployment.