Classification — Iris Dataset¶
This notebook covers the classification workflow in depth:
- Training 4 different model types (DT, RF, SVM, MLP)
- Converting each to C with BlackBox2C
- Comparing fidelity, code size, and tree complexity
- Understanding the effect of
max_depthandoptimize_rules
In [1]:
Copied!
# !pip install blackbox2c -q # Uncomment on Colab
# !pip install blackbox2c -q # Uncomment on Colab
In [2]:
Copied!
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score, classification_report
from blackbox2c import Converter, ConversionConfig
iris = load_iris()
X, y = iris.data, iris.target
feature_names = list(iris.feature_names)
class_names = list(iris.target_names)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"Train: {len(X_train)} samples | Test: {len(X_test)} samples")
print(f"Features: {feature_names}")
print(f"Classes: {class_names}")
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score, classification_report
from blackbox2c import Converter, ConversionConfig
iris = load_iris()
X, y = iris.data, iris.target
feature_names = list(iris.feature_names)
class_names = list(iris.target_names)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"Train: {len(X_train)} samples | Test: {len(X_test)} samples")
print(f"Features: {feature_names}")
print(f"Classes: {class_names}")
Train: 105 samples | Test: 45 samples
Features: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
Classes: [np.str_('setosa'), np.str_('versicolor'), np.str_('virginica')]
1. Train multiple model types¶
In [3]:
Copied!
models = {
"DecisionTree": DecisionTreeClassifier(max_depth=5, random_state=42),
"RandomForest": RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42),
"SVM": SVC(kernel="rbf", random_state=42),
"MLP": MLPClassifier(hidden_layer_sizes=(20, 10), max_iter=1000, random_state=42),
}
for name, m in models.items():
m.fit(X_train, y_train)
acc = accuracy_score(y_test, m.predict(X_test))
print(f"{name:<15} test accuracy: {acc:.4f}")
models = {
"DecisionTree": DecisionTreeClassifier(max_depth=5, random_state=42),
"RandomForest": RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42),
"SVM": SVC(kernel="rbf", random_state=42),
"MLP": MLPClassifier(hidden_layer_sizes=(20, 10), max_iter=1000, random_state=42),
}
for name, m in models.items():
m.fit(X_train, y_train)
acc = accuracy_score(y_test, m.predict(X_test))
print(f"{name:<15} test accuracy: {acc:.4f}")
DecisionTree test accuracy: 0.9333
RandomForest test accuracy: 0.8889 SVM test accuracy: 0.9556
MLP test accuracy: 0.9778
2. Convert all models and collect metrics¶
In [4]:
Copied!
config = ConversionConfig(max_depth=5, optimize_rules="medium", precision=8)
rows = []
codes = {}
for name, m in models.items():
conv = Converter(config)
code = conv.convert(
model=m,
X_train=X_train,
X_test=X_test,
feature_names=feature_names,
class_names=class_names,
)
codes[name] = code
mx = conv.get_metrics()
rows.append({
"Model": name,
"Test Acc": round(accuracy_score(y_test, m.predict(X_test)), 4),
"Fidelity": round(mx["fidelity"], 4),
"Flash (bytes)": mx["size_estimate"]["flash_bytes"],
"Depth": mx["complexity"]["max_depth"],
"Nodes": mx["complexity"]["n_nodes"],
})
df = pd.DataFrame(rows).set_index("Model")
df
config = ConversionConfig(max_depth=5, optimize_rules="medium", precision=8)
rows = []
codes = {}
for name, m in models.items():
conv = Converter(config)
code = conv.convert(
model=m,
X_train=X_train,
X_test=X_test,
feature_names=feature_names,
class_names=class_names,
)
codes[name] = code
mx = conv.get_metrics()
rows.append({
"Model": name,
"Test Acc": round(accuracy_score(y_test, m.predict(X_test)), 4),
"Fidelity": round(mx["fidelity"], 4),
"Flash (bytes)": mx["size_estimate"]["flash_bytes"],
"Depth": mx["complexity"]["max_depth"],
"Nodes": mx["complexity"]["n_nodes"],
})
df = pd.DataFrame(rows).set_index("Model")
df
Starting conversion for model: DecisionTreeClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 5 [1/4] Extracting surrogate decision tree... Model is already a decision tree, using directly. [2/4] Optimizing decision rules... Nodes: 15, Leaves: 8, Depth: 5 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 166 bytes, RAM: 32 bytes [OK] Conversion complete! Starting conversion for model: RandomForestClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 5 [1/4] Extracting surrogate decision tree... Surrogate fidelity: 1.0000 [2/4] Optimizing decision rules... Nodes: 47, Leaves: 29, Depth: 5 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 382 bytes, RAM: 32 bytes [OK] Conversion complete! Starting conversion for model: SVC Task: Classification, Features: 4, Classes: 3, Max depth: 5 [1/4] Extracting surrogate decision tree... Surrogate fidelity: 0.9556 [2/4] Optimizing decision rules... Nodes: 63, Leaves: 42, Depth: 5 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 470 bytes, RAM: 32 bytes [OK] Conversion complete! Starting conversion for model: MLPClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 5 [1/4] Extracting surrogate decision tree...
Surrogate fidelity: 0.9111 [2/4] Optimizing decision rules... Nodes: 59, Leaves: 37, Depth: 5 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 462 bytes, RAM: 32 bytes [OK] Conversion complete!
Out[4]:
| Test Acc | Fidelity | Flash (bytes) | Depth | Nodes | |
|---|---|---|---|---|---|
| Model | |||||
| DecisionTree | 0.9333 | 1.0000 | 166 | 5 | 15 |
| RandomForest | 0.8889 | 1.0000 | 382 | 5 | 47 |
| SVM | 0.9556 | 0.9556 | 470 | 5 | 63 |
| MLP | 0.9778 | 0.9111 | 462 | 5 | 59 |
Key observations¶
- DecisionTree converts with perfect fidelity (1.0) — no surrogate needed, it IS a tree.
- RandomForest / SVM / MLP require surrogate extraction. Fidelity > 0.95 is generally excellent for embedded use.
- Flash bytes reflects the complexity of the generated if-else chain, not model accuracy.
3. Effect of max_depth and optimize_rules¶
In [5]:
Copied!
rf = models["RandomForest"]
configs = [
("depth=3, opt=high", ConversionConfig(max_depth=3, optimize_rules="high")),
("depth=5, opt=medium", ConversionConfig(max_depth=5, optimize_rules="medium")),
("depth=7, opt=low", ConversionConfig(max_depth=7, optimize_rules="low")),
("depth=7, no extra", ConversionConfig(max_depth=7, optimize_rules="low")),
]
cfg_rows = []
for label, cfg in configs:
conv = Converter(cfg)
conv.convert(model=rf, X_train=X_train, X_test=X_test,
feature_names=feature_names, class_names=class_names)
mx = conv.get_metrics()
cfg_rows.append({
"Configuration": label,
"Fidelity": round(mx["fidelity"], 4),
"Flash (bytes)": mx["size_estimate"]["flash_bytes"],
"Depth": mx["complexity"]["max_depth"],
"Nodes": mx["complexity"]["n_nodes"],
})
pd.DataFrame(cfg_rows).set_index("Configuration")
rf = models["RandomForest"]
configs = [
("depth=3, opt=high", ConversionConfig(max_depth=3, optimize_rules="high")),
("depth=5, opt=medium", ConversionConfig(max_depth=5, optimize_rules="medium")),
("depth=7, opt=low", ConversionConfig(max_depth=7, optimize_rules="low")),
("depth=7, no extra", ConversionConfig(max_depth=7, optimize_rules="low")),
]
cfg_rows = []
for label, cfg in configs:
conv = Converter(cfg)
conv.convert(model=rf, X_train=X_train, X_test=X_test,
feature_names=feature_names, class_names=class_names)
mx = conv.get_metrics()
cfg_rows.append({
"Configuration": label,
"Fidelity": round(mx["fidelity"], 4),
"Flash (bytes)": mx["size_estimate"]["flash_bytes"],
"Depth": mx["complexity"]["max_depth"],
"Nodes": mx["complexity"]["n_nodes"],
})
pd.DataFrame(cfg_rows).set_index("Configuration")
Starting conversion for model: RandomForestClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 3 [1/4] Extracting surrogate decision tree... Surrogate fidelity: 0.9556 [2/4] Optimizing decision rules... Nodes: 13, Leaves: 10, Depth: 3 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 126 bytes, RAM: 32 bytes [OK] Conversion complete! Starting conversion for model: RandomForestClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 5 [1/4] Extracting surrogate decision tree...
Surrogate fidelity: 1.0000 [2/4] Optimizing decision rules... Nodes: 47, Leaves: 29, Depth: 5 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 382 bytes, RAM: 32 bytes [OK] Conversion complete! Starting conversion for model: RandomForestClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 7 [1/4] Extracting surrogate decision tree...
Surrogate fidelity: 1.0000 [2/4] Optimizing decision rules... Nodes: 121, Leaves: 61, Depth: 7 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 1014 bytes, RAM: 32 bytes [OK] Conversion complete! Starting conversion for model: RandomForestClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 7 [1/4] Extracting surrogate decision tree... Surrogate fidelity: 1.0000 [2/4] Optimizing decision rules... Nodes: 121, Leaves: 61, Depth: 7 [3/4] Generating C code... [4/4] Estimating code size... Estimated FLASH: 1014 bytes, RAM: 32 bytes [OK] Conversion complete!
Out[5]:
| Fidelity | Flash (bytes) | Depth | Nodes | |
|---|---|---|---|---|
| Configuration | ||||
| depth=3, opt=high | 0.9556 | 126 | 3 | 13 |
| depth=5, opt=medium | 1.0000 | 382 | 5 | 47 |
| depth=7, opt=low | 1.0000 | 1014 | 7 | 121 |
| depth=7, no extra | 1.0000 | 1014 | 7 | 121 |
Interpretation¶
| Trade-off | Recommendation |
|---|---|
| Tight flash budget (< 4 KB) | max_depth=3, optimize_rules='high' |
| Balanced (most use cases) | max_depth=5, optimize_rules='medium' |
| Maximum accuracy | max_depth=7, optimize_rules='low' |
Increasing depth beyond 7 rarely improves fidelity and significantly grows code size.
4. Generated C code — RandomForest surrogate¶
In [6]:
Copied!
conv = Converter(ConversionConfig(max_depth=5, optimize_rules="medium", function_name="classify_iris"))
final_code = conv.convert(
model=models["RandomForest"],
X_train=X_train, X_test=X_test,
feature_names=["sepal_length", "sepal_width", "petal_length", "petal_width"],
class_names=["SETOSA", "VERSICOLOR", "VIRGINICA"],
)
print(final_code)
conv = Converter(ConversionConfig(max_depth=5, optimize_rules="medium", function_name="classify_iris"))
final_code = conv.convert(
model=models["RandomForest"],
X_train=X_train, X_test=X_test,
feature_names=["sepal_length", "sepal_width", "petal_length", "petal_width"],
class_names=["SETOSA", "VERSICOLOR", "VIRGINICA"],
)
print(final_code)
Starting conversion for model: RandomForestClassifier Task: Classification, Features: 4, Classes: 3, Max depth: 5 [1/4] Extracting surrogate decision tree...
Surrogate fidelity: 1.0000
[2/4] Optimizing decision rules...
Nodes: 47, Leaves: 29, Depth: 5
[3/4] Generating C code...
[4/4] Estimating code size...
Estimated FLASH: 382 bytes, RAM: 32 bytes
[OK] Conversion complete!
/*
* Auto-generated C code by BlackBox2C
*
* Model Information:
* - Input features: 4
* * - Output classes: 3
* - Precision: 8-bit
* - Fixed-point: No
*
* This code is optimized for embedded systems with limited resources.
*/
#include <stdint.h>
/* Class labels */
#define SETOSA 0
#define VERSICOLOR 1
#define VIRGINICA 2
/* Prediction function */
uint8_t classify_iris(float features[4]) {
if (features[2] <= 2.485839f) {
if (features[3] <= 0.717061f) {
return 0;
} else {
if (features[1] <= 3.143289f) {
if (features[0] <= 5.550231f) {
return 0;
} else {
if (features[3] <= 1.616252f) {
return 1;
} else {
return 2;
}
}
} else {
if (features[2] <= 2.395397f) {
return 0;
} else {
if (features[3] <= 1.791769f) {
return 1;
} else {
return 0;
}
}
}
}
} else {
if (features[3] <= 1.687708f) {
if (features[3] <= 0.698930f) {
if (features[2] <= 4.947937f) {
if (features[1] <= 3.690938f) {
return 1;
} else {
return 0;
}
} else {
return 0;
}
} else {
if (features[2] <= 4.950762f) {
return 1;
} else {
if (features[0] <= 6.622137f) {
return 1;
} else {
return 2;
}
}
}
} else {
if (features[2] <= 4.844587f) {
if (features[0] <= 6.045236f) {
if (features[2] <= 4.814570f) {
return 1;
} else {
return 2;
}
} else {
return 2;
}
} else {
if (features[3] <= 1.699546f) {
if (features[0] <= 5.970683f) {
return 1;
} else {
return 2;
}
} else {
return 2;
}
}
}
}
}
/*
* Usage Example:
*
* float input[4] = {...}; // Your feature values
* uint8_t result = classify_iris(input);
*
* Input features: sepal_length, sepal_width, petal_length, petal_width
* Output classes: SETOSA, VERSICOLOR, VIRGINICA
*/
5. Classification report — surrogate vs. original¶
In [7]:
Copied!
from sklearn.tree import DecisionTreeClassifier as DTC
# Re-extract the surrogate from the converter's internal state to compare per-class fidelity
rf = models["RandomForest"]
rf_preds_test = rf.predict(X_test)
# Train a quick surrogate for inspection
surrogate = DTC(max_depth=5, random_state=42)
rf_preds_train = rf.predict(X_train)
surrogate.fit(X_train, rf_preds_train)
surrogate_preds = surrogate.predict(X_test)
print("=== Original RF vs. Test Labels ===")
print(classification_report(y_test, rf_preds_test, target_names=class_names))
print("=== Surrogate vs. RF Predictions (fidelity) ===")
print(classification_report(rf_preds_test, surrogate_preds, target_names=class_names))
from sklearn.tree import DecisionTreeClassifier as DTC
# Re-extract the surrogate from the converter's internal state to compare per-class fidelity
rf = models["RandomForest"]
rf_preds_test = rf.predict(X_test)
# Train a quick surrogate for inspection
surrogate = DTC(max_depth=5, random_state=42)
rf_preds_train = rf.predict(X_train)
surrogate.fit(X_train, rf_preds_train)
surrogate_preds = surrogate.predict(X_test)
print("=== Original RF vs. Test Labels ===")
print(classification_report(y_test, rf_preds_test, target_names=class_names))
print("=== Surrogate vs. RF Predictions (fidelity) ===")
print(classification_report(rf_preds_test, surrogate_preds, target_names=class_names))
=== Original RF vs. Test Labels ===
precision recall f1-score support
setosa 1.00 1.00 1.00 15
versicolor 0.78 0.93 0.85 15
virginica 0.92 0.73 0.81 15
accuracy 0.89 45
macro avg 0.90 0.89 0.89 45
weighted avg 0.90 0.89 0.89 45
=== Surrogate vs. RF Predictions (fidelity) ===
precision recall f1-score support
setosa 1.00 1.00 1.00 15
versicolor 1.00 0.67 0.80 18
virginica 0.67 1.00 0.80 12
accuracy 0.87 45
macro avg 0.89 0.89 0.87 45
weighted avg 0.91 0.87 0.87 45