Model iz Pythona na ESP32
Neuronska mreža iz modula 3 ima hiljade težina i teško se prenosi na mali čip. Za ESP32 biramo model koji je mali i koji čovek može da pročita: stablo odlučivanja. Python će ga naučiti iz tabele ucionica.csv, a zatim sam napisati C++ funkciju koju ubacujemo u program za ESP32. Tako nastaje pametna učionica koja je pravila naučila, umesto da smo ih mi napisali.
Stablo odlučivanja
Stablo odlučivanja je niz pitanja sa odgovorom da/ne, kao u igri „pogodi ko sam“: Da li je temperatura manja od 19,6 °C? Ako nije – da li je manja od 26 °C? Ako jeste – da li je vlažnost veća od 64,5 %?… Na kraju svake grane je odgovor (stanje).
Program bira pitanja tako da svako što bolje razdvoji primere različitih stanja. Parametar max_depth=3 kaže da u nizu sme da bude najviše tri pitanja – model ostaje mali i čitljiv.
Korak 1: učenje stabla
# Stablo odlučivanja uči kada je u učionici hladno, prijatno, toplo ili zagušljivo
import csv
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text, plot_tree
X, y = [], []
with open("ucionica.csv", encoding="utf-8") as f:
for red in csv.DictReader(f):
X.append([float(red["temperatura"]), float(red["vlaznost"])]) # osobine
y.append(red["stanje"]) # oznaka
X_u, X_p, y_u, y_p = train_test_split(X, y, test_size=0.2, random_state=0)
# max_depth=3: stablo sme da postavi najviše 3 pitanja zaredom – mali model za mali čip
stablo = DecisionTreeClassifier(max_depth=3, random_state=0)
stablo.fit(X_u, y_u)
print(f"Tačnost na proveri: {stablo.score(X_p, y_p) * 100:.0f} %")
print("Naučena pravila:")
print(export_text(stablo, feature_names=["temperatura", "vlaznost"]))
plot_tree(stablo, feature_names=["temperatura", "vlaznost"], class_names=list(stablo.classes_),
filled=True, rounded=True, impurity=False, fontsize=8)
plt.show()
Ispis programa:
Tačnost na proveri: 100 %
Naučena pravila:
|--- temperatura <= 19.55
| |--- class: hladno
|--- temperatura > 19.55
| |--- temperatura <= 26.05
| | |--- vlaznost <= 64.50
| | | |--- class: prijatno
| | |--- vlaznost > 64.50
| | | |--- class: zagusljivo
| |--- temperatura > 26.05
| | |--- class: toplo

Stablo je samo pronašlo granice: 19,55 °C (hladno), 26,05 °C (toplo) i 64,5 % vlažnosti (zagušljivo). To su pravila koja bismo možda i sami pogodili – ali ih je program izvukao iz podataka. Sa podacima iz tvoje učionice granice će biti drugačije.
Korak 2: Python piše C++ kod
Stablo je niz if/else naredbi, pa ga možemo napisati u C++. Ovaj program „obiđe“ stablo i za svaki čvor napiše jedan if – to je mali primer programa koji piše program:
# Prevođenje naučenog stabla u C++ funkciju za ESP32
import csv
from sklearn.tree import DecisionTreeClassifier
X, y = [], []
with open("ucionica.csv", encoding="utf-8") as f:
for red in csv.DictReader(f):
X.append([float(red["temperatura"]), float(red["vlaznost"])])
y.append(red["stanje"])
stablo = DecisionTreeClassifier(max_depth=3, random_state=0).fit(X, y) # učimo na svim podacima
t = stablo.tree_
imena = ["t", "h"] # nazivi parametara C++ funkcije
klase = list(stablo.classes_) # npr. ['hladno', 'prijatno', 'toplo', 'zagusljivo']
def cvor(i, uvlacenje):
"""Rekurzivno pretvara čvor stabla u if/else naredbe."""
p = " " * uvlacenje
if t.children_left[i] == -1: # list stabla: vraćamo broj klase
k = t.value[i][0].argmax()
return f"{p}return {k}; // {klase[k]}\n"
uslov = f"{imena[t.feature[i]]} <= {t.threshold[i]:.2f}"
return (f"{p}if ({uslov}) {{\n" + cvor(t.children_left[i], uvlacenje + 1) +
f"{p}}} else {{\n" + cvor(t.children_right[i], uvlacenje + 1) + f"{p}}}\n")
kod = "// Ovaj deo je napravio program p21_u_cpp.py iz naučenog stabla odlučivanja\n"
kod += "const char* STANJA[] = {" + ", ".join(f'"{k}"' for k in klase) + "};\n\n"
kod += "int proceni(float t, float h) {\n" + cvor(0, 1) + "}\n"
print(kod)
with open("model_ucionica.h", "w", encoding="utf-8") as f:
f.write(kod)
print("Sačuvano u model_ucionica.h")
Ispis programa:
// Ovaj deo je napravio program p21_u_cpp.py iz naučenog stabla odlučivanja
const char* STANJA[] = {"hladno", "prijatno", "toplo", "zagusljivo"};
int proceni(float t, float h) {
if (t <= 19.55) {
return 0; // hladno
} else {
if (t <= 26.05) {
if (h <= 64.50) {
return 1; // prijatno
} else {
return 3; // zagusljivo
}
} else {
return 2; // toplo
}
}
}
Sačuvano u model_ucionica.h
Proverili smo da C++ funkcija daje isti odgovor kao model u Pythonu za više od 500 različitih merenja. Funkcija vraća broj stanja (0–3), a naziv se čita iz niza STANJA.
Korak 3: program za ESP32
Povezivanje je isto kao u pametnoj učionici: DHT11 na GPIO4, svetleći prsten na GPIO27, servo na GPIO13. Deo između oznaka „početak/kraj dela iz Pythona“ zameni onim što je ispisao tvoj program.
// Pametna učionica koja je „naučila“ kada je hladno, prijatno, toplo ili zagušljivo
// DHT11 -> GPIO4, svetleći prsten (DIN) -> GPIO27, servo („prozor“) -> GPIO13
#include <DHT.h>
#include <Adafruit_NeoPixel.h>
#include <ESP32Servo.h>
const int DHT_PIN = 4;
const int PRSTEN_PIN = 27;
const int SERVO_PIN = 13;
const int BROJ_LED = 12;
DHT dht(DHT_PIN, DHT11);
Adafruit_NeoPixel prsten(BROJ_LED, PRSTEN_PIN, NEO_GRB + NEO_KHZ800);
Servo prozor;
// ===== početak dela koji je napravio Python (model_ucionica.h) =====
// Ovaj deo je napravio program p21_u_cpp.py iz naučenog stabla odlučivanja
const char* STANJA[] = {"hladno", "prijatno", "toplo", "zagusljivo"};
int proceni(float t, float h) {
if (t <= 19.55) {
return 0; // hladno
} else {
if (t <= 26.05) {
if (h <= 64.50) {
return 1; // prijatno
} else {
return 3; // zagusljivo
}
} else {
return 2; // toplo
}
}
}
// ===== kraj dela iz Pythona =====
// boja prstena za svako stanje (redosled kao u nizu STANJA)
const uint8_t BOJE[4][3] = {
{0, 0, 255}, // hladno – plavo
{0, 255, 0}, // prijatno – zeleno
{255, 0, 0}, // toplo – crveno
{160, 0, 255} // zagušljivo – ljubičasto
};
void setup() {
Serial.begin(115200);
dht.begin();
prsten.begin();
prsten.setBrightness(40);
prozor.setPeriodHertz(50);
prozor.attach(SERVO_PIN, 500, 2400);
prozor.write(0);
}
void loop() {
delay(2000);
float t = dht.readTemperature();
float h = dht.readHumidity();
if (isnan(t) || isnan(h)) {
return;
}
int s = proceni(t, h); // model odlučuje
Serial.print(t, 1);
Serial.print(" C, ");
Serial.print(h, 0);
Serial.print(" % -> ");
Serial.println(STANJA[s]);
prsten.fill(prsten.Color(BOJE[s][0], BOJE[s][1], BOJE[s][2]));
prsten.show();
bool otvori = (s == 2 || s == 3); // toplo ili zagušljivo -> provetravanje
prozor.write(otvori ? 90 : 0);
}
| Stanje | Prsten | Prozor |
|---|---|---|
| hladno | plavo | zatvoren |
| prijatno | zeleno | zatvoren |
| toplo | crveno | otvoren |
| zagušljivo | ljubičasto | otvoren |
- Model na ESP32 je samo nekoliko poređenja – zauzima manje od kilobajta i odgovara za delić milisekunde.
- Kada skupiš nove podatke, ponovo pokreneš Python, zameniš funkciju i pošalješ program – ESP32 je „naučio“ nešto novo.
- Za razliku od pametne učionice iz Arduino kursa, ovde postoji i stanje zagušljivo – učionica provetrava i kada nije toplo, ali je vazduh vlažan.
Prvi čas: učenje stabla na podacima odeljenja i poređenje pravila sa podacima drugih odeljenja. Drugi čas: prenos na ESP32 i provera – zagrejte senzor dlanom, dunite u njega.
Česte zablude
- „Model na ESP32 nastavlja da uči sam“ – ovde ESP32 samo koristi naučeno stablo; uči se na računaru, a novo stablo se ponovo šalje na ploču.
- „Dublje stablo je uvek bolje“ – dublje stablo može da „nabuba“ podatke (preprilagođavanje) i postaje nečitljivo.
Zadaci za vežbu
Pokušaj prvo sam, pa tek onda otvori rešenje.
- Uporedi stabla dubine 1, 2, 3, 5 i bez ograničenja: koliko imaju listova (odgovora) i kolika im je tačnost?
- Neka prozor ostane otvoren još 60 sekundi pošto prestane potreba za provetravanjem (bez
delayza čekanje). - Zašto smo za ESP32 izabrali stablo odlučivanja, a ne neuronsku mrežu iz modula 3?
Rešenje 1: dubina stabla
# Zadatak: kako dubina stabla utiče na tačnost i broj pravila
import csv
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
X, y = [], []
with open("ucionica.csv", encoding="utf-8") as f:
for red in csv.DictReader(f):
X.append([float(red["temperatura"]), float(red["vlaznost"])])
y.append(red["stanje"])
X_u, X_p, y_u, y_p = train_test_split(X, y, test_size=0.2, random_state=0)
for dubina in [1, 2, 3, 5, None]: # None = bez ograničenja
stablo = DecisionTreeClassifier(max_depth=dubina, random_state=0).fit(X_u, y_u)
print(f"dubina {str(dubina):4}: listova {stablo.get_n_leaves():2}, "
f"tačnost učenje {stablo.score(X_u, y_u) * 100:3.0f} %, provera {stablo.score(X_p, y_p) * 100:3.0f} %")
Ispis programa:
dubina 1 : listova 2, tačnost učenje 60 %, provera 59 %
dubina 2 : listova 3, tačnost učenje 81 %, provera 82 %
dubina 3 : listova 4, tačnost učenje 99 %, provera 100 %
dubina 5 : listova 6, tačnost učenje 100 %, provera 100 %
dubina None: listova 6, tačnost učenje 100 %, provera 100 %
Dubina 3 je dovoljna: veća stabla ne donose bolju proveru, a imaju više pravila. Dubina 1 može da postavi samo jedno pitanje, pa ne može da razlikuje četiri stanja.
Rešenje 2: prozor otvoren još 60 s
// Zadatak: prozor ostaje otvoren još 60 s pošto prestane potreba za provetravanjem
// (zameni loop() u programu iznad ovim delom; ostatak programa ostaje isti)
unsigned long poslednjePotrebno = 0; // kada je poslednji put trebalo provetravati
const unsigned long JOS = 60000; // 60 s
void azurirajProzor(int s) {
if (s == 2 || s == 3) {
poslednjePotrebno = millis(); // toplo ili zagušljivo – pamtimo trenutak
}
bool otvoren = poslednjePotrebno != 0 && millis() - poslednjePotrebno < JOS;
prozor.write(otvoren ? 90 : 0);
}
void loop() {
delay(2000);
float t = dht.readTemperature();
float h = dht.readHumidity();
if (isnan(t) || isnan(h)) {
return;
}
int s = proceni(t, h);
prsten.fill(prsten.Color(BOJE[s][0], BOJE[s][1], BOJE[s][2]));
prsten.show();
azurirajProzor(s);
}
Pamtimo trenutak kada je poslednji put bilo „toplo“ ili „zagušljivo“ i prozor držimo otvorenim dok od tog trenutka ne prođe 60 s.
Rešenje 3: objašnjenje
Stablo je veoma malo (nekoliko poređenja), lako se prevodi u C++ i čovek može da pročita pravila i proveri da li imaju smisla. Za dve osobine (temperatura i vlažnost) mreža ne bi bila tačnija, a bila bi veća i teže bi se prenela na ploču.
Proveri znanje
Otvori kviz
Kviz: model na ESP32
1. Šta je stablo odlučivanja?
2. Šta znači max_depth=3?
3. Kako stablo dospeva na ESP32?
4. Šta treba uraditi kada skupiš nove podatke?
