Model iz Pythona na ESP32

Modul 4 · Lekcija 3 od 5 · Veštačka inteligencija

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
Nacrtano stablo odlučivanja sa pitanjima o temperaturi i vlažnosti
Stablo koje je program naučio. U svakom polju: pitanje, broj primera (samples), koliko primera je kog stanja (value) i odgovor (class).

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);
}
StanjePrstenProzor
hladnoplavozatvoren
prijatnozelenozatvoren
toplocrvenootvoren
zagušljivoljubičastootvoren
  • 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.
Za čas (nastavnicima): trajanje 2 × 45 minuta; pribor: ESP32 sa delovima pametne učionice, računar sa Pythonom.
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.

  1. Uporedi stabla dubine 1, 2, 3, 5 i bez ograničenja: koliko imaju listova (odgovora) i kolika im je tačnost?
  2. Neka prozor ostane otvoren još 60 sekundi pošto prestane potreba za provetravanjem (bez delay za čekanje).
  3. 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?