Praktikum
GDG Belgrade & VTŠ Apps Team
VTŠ Apps Team
GDG Belgrade
Jednodnevna radionica

Build with AI Bootcamp

Od nule do funkcionalne AI web aplikacije, klasifikacija modnih proizvoda pomoću konvolucionih neuronskih mreža (CNN).

25. april 2026.
FON, Beograd
10:00 - 19:00

Uvod i podešavanje okruženja

Počinjemo sa podešavanjem Google Colab-a i uključivanjem GPU-a. GPU je neophodan jer ubrzava treniranje modela 10-100x u odnosu na CPU.

Uključite GPU

  1. U Google Colab-u idite na Runtime → Change runtime type
  2. U Hardware accelerator izaberite GPU (T4 je besplatan)
  3. Kliknite Save

Provera GPU-a

Python
# Provera da li je GPU dostupan
import tensorflow as tf

print("TensorFlow verzija:", tf.__version__)
print("="*50)

gpus = tf.config.list_physical_devices('GPU')
if gpus:
    print(f"GPU je dostupan! Pronađeno {len(gpus)} GPU uređaj(a):")
    for gpu in gpus:
        print(f"  - {gpu.name}")
else:
    print("UPOZORENJE: GPU nije pronađen!")
    print("Idite na Runtime → Change runtime type → GPU")

Instalacija i import biblioteka

Python
# Instalacija
!pip install gradio gdown -q

# Importi
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os, zipfile, gdown

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models
from tensorflow.keras.preprocessing.image import ImageDataGenerator

from sklearn.metrics import confusion_matrix, classification_report
from sklearn.model_selection import train_test_split
from PIL import Image

import warnings
warnings.filterwarnings('ignore')

plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['figure.figsize'] = [12, 8]
plt.rcParams['font.size'] = 12

print("Sve biblioteke su uspešno učitane!")

Preuzimanje i učitavanje dataseta

Koristimo Fashion Product Images, realne fotografije modnih proizvoda. Radimo sa 4 klase: T-shirt, Jeans, Sneakers, Jacket.

Preuzimanje

Python
FILE_ID = '1YpceXZvh-8idz-nKDJjUh3-nrlVvhyYY'
print("Preuzimanje Fashion dataseta...")

url = f'https://drive.google.com/uc?id={FILE_ID}'
output = 'fashion_dataset.zip'
gdown.download(url, output, quiet=False)

with zipfile.ZipFile(output, 'r') as zip_ref:
    zip_ref.extractall('.')
os.remove(output)
print("Dataset uspešno preuzet i raspakovan!")

Pronalaženje i učitavanje

Python
def pronadji_dataset():
    for folder in ['fashion_subset gdg', 'fashion_subset', 'fashion_dataset']:
        if os.path.exists(folder):
            if os.path.exists(os.path.join(folder, 'styles.csv')):
                return folder
    for item in os.listdir('.'):
        if os.path.isdir(item) and os.path.exists(os.path.join(item, 'styles.csv')):
            return item
    return None

DATASET_PATH = pronadji_dataset()
STYLES_PATH = os.path.join(DATASET_PATH, 'styles.csv')
IMAGES_PATH = os.path.join(DATASET_PATH, 'images')

df = pd.read_csv(STYLES_PATH, on_bad_lines='skip')
print(f"Učitano {len(df):,} proizvoda")

Izbor klasa i učitavanje slika

Python
ODABRANE_KATEGORIJE = {
    'Tshirts': 'T-shirt', 'Jeans': 'Jeans',
    'Casual Shoes': 'Sneakers', 'Jackets': 'Jacket'
}
df_filtered = df[df['articleType'].isin(ODABRANE_KATEGORIJE.keys())].copy()
df_filtered['class_name'] = df_filtered['articleType'].map(ODABRANE_KATEGORIJE)

IMENA_KLASA = ['T-shirt', 'Jeans', 'Sneakers', 'Jacket']
BROJ_KLASA = len(IMENA_KLASA)
label_map = {name: idx for idx, name in enumerate(IMENA_KLASA)}
df_filtered['label'] = df_filtered['class_name'].map(label_map)

IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS = 80, 60, 3

def ucitaj_sliku(image_id, images_dir, target_size=(IMG_HEIGHT, IMG_WIDTH)):
    img_path = os.path.join(images_dir, f"{image_id}.jpg")
    if not os.path.exists(img_path): return None
    try:
        img = Image.open(img_path).convert('RGB')
        img = img.resize((target_size[1], target_size[0]))
        return np.array(img)
    except: return None

slike, labele = [], []
for klasa_idx, klasa_ime in enumerate(IMENA_KLASA):
    df_klasa = df_filtered[df_filtered['class_name'] == klasa_ime].head(1500)
    ucitano = 0
    for _, row in df_klasa.iterrows():
        img = ucitaj_sliku(row['id'], IMAGES_PATH)
        if img is not None:
            slike.append(img); labele.append(klasa_idx); ucitano += 1
    print(f"  {klasa_ime}: {ucitano} slika")

X = np.array(slike)
y = np.array(labele)
print(f"UKUPNO: {len(X):,} slika | Dimenzije: {X.shape}")

Preprocesiranje i podela podataka

Pre treniranja moramo da normalizujemo slike i podelimo podatke na trening, validacioni i test set.

Zadatak 1 Normalizacija slika

Pikseli imaju vrednosti 0-255. Neuronske mreže bolje rade sa malim brojevima (0.0 - 1.0). Kojim brojem treba podeliti?

Python - dopunite
print("Pre normalizacije:")
print(f"  Min: {X.min()}, Max: {X.max()}")

X = X.astype('float32') / _____  # <-- DOPUNITE: kojim brojem delimo?

print("Posle normalizacije:")
print(f"  Min: {X.min():.2f}, Max: {X.max():.2f}")
Hint: Koja je maksimalna vrednost piksela? Ako podelimo sa tom vrednošću, maksimum postaje 1.0.
Zadatak 2 Podela podataka

Delimo podatke: 70% trening, 15% validacija, 15% test. Koristimo train_test_split iz sklearn.

Python - dopunite
# Korak 1: Izdvajamo test set (15%)
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y,
    test_size=_____,      # <-- DOPUNITE: koliki procenat za test?
    random_state=42,
    stratify=_____         # <-- DOPUNITE: po čemu stratifikujemo?
)

# Korak 2: Od ostatka izdvajamo validation set
X_train, X_val, y_train, y_val = train_test_split(
    X_temp, y_temp,
    test_size=0.176,
    random_state=42,
    stratify=y_temp
)

print(f"Trening: {len(X_train):,} | Validation: {len(X_val):,} | Test: {len(X_test):,}")
Hint: test_size=0.15 znači 15%. stratify=y čuva proporcije klasa u svakom setu.

Vizualizacija distribucije

Python
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
colors = plt.cm.Set2(np.linspace(0, 1, BROJ_KLASA))

for ax, (data, name) in zip(axes, [(y_train, 'Trening'), (y_val, 'Validation'), (y_test, 'Test')]):
    unique, counts = np.unique(data, return_counts=True)
    bars = ax.bar([IMENA_KLASA[i] for i in unique], counts, color=colors)
    ax.set_title(f'{name} set', fontweight='bold')
    for bar, count in zip(bars, counts):
        ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5, str(count), ha='center')
plt.tight_layout()
plt.show()

CNN arhitektura

CNN (Convolutional Neural Network) je tip neuronske mreže specijalizovan za slike. Naš model ima 3 konvoluciona bloka sa rastućim brojem filtera: 32 → 64 → 128.

SlojFunkcija
Conv2DDetektuje obrasce (ivice, teksture, oblike)
BatchNormalizationStabilizuje i ubrzava treniranje
MaxPooling2DSmanjuje dimenzije, zadržava bitne informacije
DropoutSprečava overfitting (nasumično gasi neurone)
DensePotpuno povezan sloj za klasifikaciju
Zadatak 3 Kreiranje CNN modela

Najvažniji deo radionice! Popunite praznine u prvom bloku i poslednjem sloju. Blokovi 2 i 3 su dati kao primer.

Python - dopunite
INPUT_SHAPE = (IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS)

def kreiraj_cnn_model():
    model = models.Sequential([
        # ===== BLOK 1 =====
        layers.Conv2D(_____, (_____, _____), activation='_____', padding='same', input_shape=INPUT_SHAPE),
        layers.BatchNormalization(),
        layers.Conv2D(32, (3, 3), activation='relu', padding='same'),
        layers.MaxPooling2D((_____, _____)),  # <-- dimenzije pooling-a
        layers.Dropout(_____),                # <-- procenat dropout-a

        # ===== BLOK 2 (64 filtera) =====
        layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
        layers.BatchNormalization(),
        layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
        layers.MaxPooling2D((2, 2)),
        layers.Dropout(0.25),

        # ===== BLOK 3 (128 filtera) =====
        layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
        layers.BatchNormalization(),
        layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
        layers.MaxPooling2D((2, 2)),
        layers.Dropout(0.25),

        # ===== KLASIFIKACIJA =====
        layers.Flatten(),
        layers.Dense(256, activation='relu'),
        layers.BatchNormalization(),
        layers.Dropout(0.5),
        layers.Dense(128, activation='relu'),
        layers.Dropout(0.5),
        layers.Dense(_____, activation='_____')  # <-- koliko klasa? koji activation?
    ])
    return model

model = kreiraj_cnn_model()
model.summary()
Hintovi: Blok 1 ima 32 filtera, kernel 3x3, activation 'relu'. Pooling je 2x2. Dropout je 0.25. Poslednji Dense ima onoliko neurona koliko imamo klasa, sa 'softmax' aktivacijom.

Kompilacija i treniranje

Zadatak 4 Kompilacija modela

Kompilacija govori modelu kako da uči: kojim algoritmom, kako da meri grešku, i šta da prati.

Python - dopunite
model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=_____),  # <-- brzina učenja
    loss='_____',                                          # <-- loss funkcija
    metrics=['_____']                                      # <-- šta pratimo?
)
print("Model kompajliran!")
Hint: learning_rate=0.001 | loss='sparse_categorical_crossentropy' | metrics=['accuracy']

Callbacks

Python
callbacks = [
    keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6, verbose=1),
    keras.callbacks.EarlyStopping(monitor='val_loss', patience=7, restore_best_weights=True, verbose=1)
]
Zadatak 5 Treniranje modela

Odaberite broj epoha i veličinu batch-a. EarlyStopping će zaustaviti treniranje ako model prestane da se poboljšava.

Python - dopunite
EPOCHS = _____       # <-- broj epoha (preporuka: 25)
BATCH_SIZE = _____   # <-- veličina batch-a (preporuka: 32)

history = model.fit(
    X_train, y_train,
    epochs=EPOCHS,
    batch_size=BATCH_SIZE,
    validation_data=(X_val, y_val),
    callbacks=callbacks,
    verbose=1
)

Vizualizacija treniranja

Python
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(history.history['loss'], label='Trening', linewidth=2)
axes[0].plot(history.history['val_loss'], label='Validation', linewidth=2)
axes[0].set_title('Loss', fontweight='bold')
axes[0].legend()

axes[1].plot(history.history['accuracy'], label='Trening', linewidth=2)
axes[1].plot(history.history['val_accuracy'], label='Validation', linewidth=2)
axes[1].set_title('Accuracy', fontweight='bold')
axes[1].legend()

plt.tight_layout()
plt.show()

Evaluacija modela

Vreme je da vidimo koliko je naš model dobar na podacima koje nikada nije video, na test setu.

Python
test_loss, test_accuracy = model.evaluate(X_test, y_test, verbose=0)
print(f"Test Accuracy: {test_accuracy:.2%}")

y_pred_probs = model.predict(X_test, verbose=0)
y_pred = np.argmax(y_pred_probs, axis=1)

# Confusion Matrix
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=IMENA_KLASA, yticklabels=IMENA_KLASA, annot_kws={'size': 14})
plt.title('Confusion Matrix', fontweight='bold')
plt.xlabel('Prediktovana klasa')
plt.ylabel('Stvarna klasa')
plt.show()

print(classification_report(y_test, y_pred, target_names=IMENA_KLASA))

Ograničenja AI - model ne može da kaže "Ne znam"

Šta se desi kad modelu damo sliku nečega što nikada nije video? Hajde da testiramo sa besmislenim slikama.

Ključna lekcija: Model UVEK daje jednu od 4 klase, čak i za potpuno besmislenu sliku. Ne može da kaže "ne znam". Ovo je fundamentalno ograničenje klasifikacionih modela.
Python
nepoznate = {
    'Šum': np.random.rand(IMG_HEIGHT, IMG_WIDTH, 3).astype('float32'),
    'Crna': np.zeros((IMG_HEIGHT, IMG_WIDTH, 3), dtype='float32'),
    'Bela': np.ones((IMG_HEIGHT, IMG_WIDTH, 3), dtype='float32'),
    'Plava': np.concatenate([np.zeros((IMG_HEIGHT, IMG_WIDTH, 2)),
             np.ones((IMG_HEIGHT, IMG_WIDTH, 1))], axis=2).astype('float32'),
}

fig, axes = plt.subplots(1, 4, figsize=(14, 4))
fig.suptitle('Model daje predikciju čak i za besmislene slike!', color='red', fontweight='bold')

for ax, (naziv, slika) in zip(axes, nepoznate.items()):
    pred = model.predict(slika.reshape(1, IMG_HEIGHT, IMG_WIDTH, 3), verbose=0)[0]
    ax.imshow(slika)
    ax.set_title(f'{naziv}\n→ {IMENA_KLASA[np.argmax(pred)]} ({pred.max()*100:.0f}%)', color='darkred')
    ax.axis('off')
plt.tight_layout()
plt.show()

Data Augmentation

Augmentacija veštački kreira nove slike od postojećih: rotira, pomera, zumira, flipuje. Model vidi "nove" slike u svakoj epohi, što pomaže generalizaciji.

Zadatak 6 Definisanje augmentacije

Popunite parametre. Pazite, previše agresivna augmentacija može da pogorša rezultate!

Python - dopunite
datagen = ImageDataGenerator(
    rotation_range=_____,        # <-- stepeni rotacije (preporuka: 10)
    width_shift_range=_____,     # <-- horizontalno pomeranje (preporuka: 0.1)
    height_shift_range=_____,    # <-- vertikalno pomeranje (preporuka: 0.1)
    zoom_range=_____,            # <-- zoom (preporuka: 0.1)
    horizontal_flip=_____,       # <-- True ili False?
    fill_mode='nearest'
)

Treniranje sa augmentacijom i poređenje

Python
model_aug = kreiraj_cnn_model()
model_aug.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

datagen.fit(X_train)
history_aug = model_aug.fit(
    datagen.flow(X_train, y_train, batch_size=BATCH_SIZE),
    epochs=20,
    validation_data=(X_val, y_val),
    steps_per_epoch=len(X_train) // BATCH_SIZE,
    callbacks=callbacks, verbose=1
)

# Poređenje
_, test_acc_orig = model.evaluate(X_test, y_test, verbose=0)
_, test_acc_aug = model_aug.evaluate(X_test, y_test, verbose=0)
print(f"Bez augmentacije: {test_acc_orig:.2%}")
print(f"Sa augmentacijom: {test_acc_aug:.2%}")

best_model = model_aug if test_acc_aug >= test_acc_orig else model
best_model.save('fashion_classifier.keras')
Napomena: Augmentacija nije uvek bolja! Ako je previše agresivna ili dataset premali, može da pogorša rezultate. To je normalan deo ML procesa, učimo i iz grešaka.

Deploy sa Gradio

Poslednji korak, pravimo web aplikaciju! Gradio omogućava kreiranje interfejsa za ML modele sa svega nekoliko linija koda.

Zadatak 7 Funkcija za klasifikaciju

Funkcija prima sliku, resize-uje je, normalizuje, i prosleđuje modelu. Popunite dimenzije i normalizaciju.

Python - dopunite
import gradio as gr

def klasifikuj(slika):
    if isinstance(slika, Image.Image):
        slika = np.array(slika)
    if len(slika.shape) == 2:
        slika = np.stack([slika]*3, axis=-1)
    elif slika.shape[2] == 4:
        slika = slika[:,:,:3]

    slika_pil = Image.fromarray(slika.astype('uint8')).convert('RGB')
    slika_resized = slika_pil.resize((_____, _____))  # <-- (IMG_WIDTH, IMG_HEIGHT)
    slika_norm = np.array(slika_resized).astype('float32') / _____  # <-- normalizacija
    slika_input = slika_norm.reshape(1, IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS)

    pred = best_model.predict(slika_input, verbose=0)[0]
    return {IMENA_KLASA[i]: float(pred[i]) for i in range(BROJ_KLASA)}

Pokretanje aplikacije

Python
# Priprema primera
primeri = []
for klasa in range(BROJ_KLASA):
    idx = np.where(y_test == klasa)[0][0]
    slika = (X_test[idx] * 255).astype('uint8')
    path = f'primer_{IMENA_KLASA[klasa]}.png'
    Image.fromarray(slika).save(path)
    primeri.append(path)

# Gradio interfejs
demo = gr.Interface(
    fn=klasifikuj,
    inputs=gr.Image(label="Učitaj sliku"),
    outputs=gr.Label(num_top_classes=4, label="Predikcija"),
    title="Fashion Classifier",
    description="GDG Belgrade & VTŠ Apps Team - Build with AI Bootcamp",
    examples=primeri,
    theme=gr.themes.Soft()
)
demo.launch(share=True)

Šta smo naučili i kuda dalje?

Danas smo prošli

Prezentacija

Preuzmite slajdove sa radionice

Preuzmi

Google Cloud krediti

Besplatni krediti za Google Cloud platformu

Preuzmi kredite

Resursi za dalje učenje

ResursOpisLink
TensorFlowZvanična dokumentacijatensorflow.org
KerasAPI za neuronske mrežekeras.io
Google ML Crash CourseBesplatan kurs od Google-aML Crash Course
GradioML web aplikacijegradio.app
KaggleDataseti i takmičenjakaggle.com
fast.aiBesplatan DL kursfast.ai