improve questions 2 9 20 24

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-02-17 22:40:15 +01:00
parent 2e5121a6f0
commit 2c970368c9
42 changed files with 3605 additions and 697 deletions

View File

@ -468,197 +468,95 @@ Przykład — kluczowa różnica: decrease-key:
### Pseudokod (Python) ### Pseudokod (Python)
**Dijkstra:** **Dijkstra** (graph = słownik sąsiedztwa, np. `{'A': [('B',2), ('C',4)]}`):
def dijkstra(graph, start): # graph = słownik, klucz = wierzchołek, def dijkstra(graph, source):
# wartość = lista par (sąsiad, waga) dist = {v: float('inf') for v in graph}
# np. graph = {'A': [('B',2), ('C',4)], dist[source] = 0
# 'B': [('D',3)], ...} visited = set()
# start = wierzchołek startowy, np. 'A' for _ in range(len(graph)):
current = None # szukaj nieodwiedzonego wierzchołka o min dist — O(V)
d = {v: float('inf') for v in graph} # d = słownik odległości (distance) for v in graph:
# Klucz: wierzchołek v if v not in visited and (current is None or dist[v] < dist[current]):
# Wartość: najkrótsza DOTYCHCZAS ZNANA current = v
# odległość od start do v if dist[current] == float('inf'):
# Na początku: ∞ (nieskończoność) dla break # reszta nieosiągalna
# wszystkich — bo jeszcze niczego visited.add(current) # zamknij — NIE wracamy (zachłanność)
# nie odkryliśmy for neighbor, weight in graph[current]: # relaksacja sąsiadów
# float('inf') = Python-owa nieskończoność, if dist[current] + weight < dist[neighbor]:
# każda liczba jest od niej mniejsza dist[neighbor] = dist[current] + weight
return dist # O(V²) z tablicą
d[start] = 0 # odległość od startu do samego siebie = 0
visited = set() # visited = zbiór ZAMKNIĘTYCH wierzchołków
# (już przetworzonych, nie wracamy do nich)
# set() = pusty zbiór Pythona — O(1) lookup
for _ in range(len(graph)): # powtórz V razy (raz na każdy wierzchołek)
# W każdej iteracji wybieramy JEDEN
# wierzchołek o min d[v] i go przetwarzamy
# --- Szukanie minimum w tablicy d --- → O(V) na każde szukanie
u = None # u = wierzchołek o najmniejszej odległości
# (jeszcze nie odwiedzony)
for v in graph: # przejrzyj WSZYSTKIE wierzchołki
if v not in visited: # pomiń już odwiedzone
if u is None or d[v] < d[u]: # jeśli v ma mniejszą odległość
u = v # zapamiętaj go jako kandydata
# Po tej pętli: u = wierzchołek z min d, spośród nieodwiedzonych
# To jest O(V) — przeszukujemy całą tablicę!
if d[u] == float('inf'): # jeśli minimum to ∞, reszta jest
break # nieosiągalna — koniec
visited.add(u) # oznacz u jako odwiedzony (zamknięty)
# NIE WRACAMY do u — to jest ZACHŁANNOŚĆ
# Dijkstry (i dlatego ujemne wagi psują!)
for v, w in graph[u]: # iteruj po sąsiadach wierzchołka u
# v = sąsiad (vertex), w = waga krawędzi u→v
# np. graph['A'] = [('B',2), ('C',4)]
# → v='B', w=2, potem v='C', w=4
if d[u] + w < d[v]: # RELAKSACJA: czy droga do v PRZEZ u
# jest krótsza niż dotychczas znana?
# d[u] = koszt dotarcia do u
# w = koszt krawędzi u→v
# d[u]+w = koszt drogi start→u→v
# d[v] = dotychczasowy najlepszy koszt do v
d[v] = d[u] + w # TAK, jest krótsza → zaktualizuj!
# (tablica d pełni tu rolę kolejki
# priorytetowej — po prostu szukamy
# minimum w niej w każdej iteracji)
return d # zwróć słownik najkrótszych odległości
# np. {'A': 0, 'B': 2, 'C': 4, 'D': 5}
# Złożoność: O(V²) — V szukań min × O(V) każde
![Przejście grafu algorytmem Dijkstry — krok po kroku](img/dijkstra_traversal.png) ![Przejście grafu algorytmem Dijkstry — krok po kroku](img/dijkstra_traversal.png)
**Bellman-Ford:** **Bellman-Ford** (vertices = lista wierzchołków, edges = lista krotek (src, dst, weight)):
def bellman_ford(vertices, edges, start): # vertices = lista wierzchołków, np. ['A','B','C','D'] def bellman_ford(vertices, edges, source):
# edges = lista krawędzi, każda to (skąd, dokąd, waga) dist = {v: float('inf') for v in vertices}
# np. [('A','B',2), ('A','C',4), ('B','D',3), ...] dist[source] = 0
# start = wierzchołek startowy for _ in range(len(vertices) - 1): # V1 iteracji (najdłuższa ścieżka = V1 krawędzi)
# UWAGA: format inny niż w Dijkstrze! for src, dst, weight in edges: # relaksuj WSZYSTKIE krawędzie
# Dijkstra: graf jako słownik sąsiedztwa if dist[src] + weight < dist[dst]:
# B-F: explicite lista krawędzi dist[dst] = dist[src] + weight
for src, dst, weight in edges: # V-ta iteracja: wykrywanie cyklu ujemnego
if dist[src] + weight < dist[dst]:
return None # cykl ujemny!
return dist # O(V·E)
d = {v: float('inf') for v in vertices} # d = słownik odległości — identycznie Przykład — graf z ujemnymi wagami (Dijkstra daje ZŁY wynik, B-F poprawny):
# jak w Dijkstrze. Klucz = wierzchołek,
# wartość = najkrótsza znana odległość.
# Na starcie: ∞ dla wszystkich.
d[start] = 0 # odległość do siebie = 0 Graf: S→A(2), A→C(3), S→B(5), B→A(4)
for _ in range(len(vertices) - 1): # powtórz V1 razy (V = liczba wierzchołków) Dijkstra:
# DLACZEGO V1? Bo najdłuższa najkrótsza 1. S(0): dist[A]=2, dist[B]=5
# ścieżka (bez cykli) ma co najwyżej V1 2. A(2) zamknięty: dist[C]=5
# krawędzi. Po k iteracjach mamy poprawne 3. B(5): B→A = 54 = 1 < 2, ALE A już zamknięty POMIJA!
# odległości dla ścieżek o ≤ k krawędziach. Wynik: A=2, C=5 ← BŁĄD (prawidłowe: A=1, C=4)
# _ = zmienna, której nie używamy (konwencja)
for u, v, w in edges: # w KAŻDEJ iteracji przejrzyj WSZYSTKIE krawędzie Bellman-Ford — relaksuje WSZYSTKIE krawędzie, V1 = 3 razy:
# u = początek krawędzi, v = koniec, w = waga Start: dist = [S:0, A:∞, B:∞, C:∞]
# To jest brute-force — stąd O(V·E)
if d[u] + w < d[v]: # RELAKSACJA identyczna jak w Dijkstrze: Iteracja 1:
# czy droga start→u→v jest krótsza niż d[v]? S→A: 0+2=2 < ∞ → A=2
A→C: 2+3=5 < ∞ → C=5
S→B: 0+5=5 < ∞ → B=5
B→A: 54=1 < 2 A=1 ujemna waga poprawia!
d[v] = d[u] + w # TAK → zaktualizuj Iteracja 2:
A→C: 1+3=4 < 5 C=4 propagacja poprawionego A
# --- Wykrywanie cyklu ujemnego --- Iteracja 3: brak zmian → stabilne.
for u, v, w in edges: # dodatkowe (V-te) przejście po krawędziach Wynik: [S:0, A:1, B:5, C:4] ← POPRAWNE
# Jeśli NADAL da się poprawić odległość,
# to znaczy, że istnieje cykl ujemny!
# (po V1 iteracjach powinno być stabilne)
if d[u] + w < d[v]: # nadal można polepszyć? cykl ujemny! Wykrywanie cyklu ujemnego — dodaj krawędź C→B(3):
return None # zwróć None = sygnał "cykl ujemny wykryty" Cykl B→A→C→B = 4 + 3 + (3) = 4 < 0.
Po V1 iteracjach dist nadal maleje → V-ta iteracja:
return d # zwróć słownik odległości (jak Dijkstra) dist[src] + weight < dist[dst] return None
![Przejście grafu algorytmem Bellmana-Forda — krok po kroku](img/bellman_ford_traversal.png) ![Przejście grafu algorytmem Bellmana-Forda — krok po kroku](img/bellman_ford_traversal.png)
**A*:** **A\*** (graph jak Dijkstra; heuristic = h(v) → oszacowanie odl. do celu):
def a_star(graph, start, goal, h): # graph = słownik sąsiedztwa (jak Dijkstra) def a_star(graph, source, goal, heuristic):
# start = wierzchołek startowy cost_so_far = {source: 0} # g(n) — faktyczny koszt dotarcia
# goal = wierzchołek DOCELOWY (cel) priority = {source: heuristic(source)} # f(n) = g(n) + h(n)
# → to jedyna różnica od Dijkstry: came_from = {} # do odtworzenia ścieżki
# szukamy ścieżki do JEDNEGO celu visited = set()
# h = FUNKCJA heurystyczna: h(v) zwraca while priority:
# oszacowanie odległości od v do goal current = min(priority, key=priority.get) # wierzchołek o min f(n)
# np. h = lambda v: odl_euklidesowa(v, goal) del priority[current]
if current == goal:
d = {start: 0} # d = słownik g(n) = faktyczny koszt break # dotarliśmy — A* kończy (Dijkstra przetworzyłby wszystko)
# dotarcia od start do n visited.add(current)
# Tu trzymamy TYLKO odkryte wierzchołki for neighbor, weight in graph[current]:
# (nie inicjalizujemy ∞ dla reszty) if neighbor in visited:
f = {start: h(start)} # f = słownik f(n) = g(n) + h(n)
# f to szacunkowy ŁĄCZNY koszt ścieżki:
# dotychczasowy koszt g + heurystyka h
# Sortujemy po f (nie po g!) — to kieruje
# przeszukiwanie W STRONĘ CELU
# Na starcie: f(start) = 0 + h(start)
came_from = {} # came_from = słownik "skąd przyszliśmy"
# Klucz: wierzchołek v
# Wartość: wierzchołek, z którego dotarliśmy do v
# Służy do ODTWORZENIA ścieżki po znalezieniu celu
# np. came_from = {'B':'A', 'D':'B'}
# → ścieżka: A → B → D
visited = set() # visited = zbiór zamkniętych wierzchołków
# (już przetworzonych)
while f: # dopóki są odkryte, nieprzetworzone wierzchołki
# (f zawiera tylko te, do których dotarliśmy)
# --- Szukanie minimum f w tablicy --- → O(V)
u = min(f, key=f.get) # u = wierzchołek o najniższym f(n)
# min() przeszukuje WSZYSTKIE klucze w f
# key=f.get → porównuj po wartościach f[v]
# Równoważne: for v in f: if f[v] < f[best]...
del f[u] # usuń u z open set (przetwarzamy go teraz)
if u == goal: break # ZNALEZIONO CEL! → przerwij
# Kluczowa optymalizacja A*:
# Dijkstra przetwarza WSZYSTKIE wierzchołki,
# A* KOŃCZY gdy dotrze do celu
visited.add(u) # oznacz u jako przetworzony
for v, w in graph[u]: # iteruj po sąsiadach u
# v = sąsiad, w = waga krawędzi u→v
if v in visited: # jeśli v już przetworzony → pomiń
continue continue
new_cost = cost_so_far[current] + weight
g_new = d[u] + w # g_new = potencjalny nowy koszt dotarcia do v if neighbor not in cost_so_far or new_cost < cost_so_far[neighbor]:
# (koszt do u + krawędź u→v) cost_so_far[neighbor] = new_cost
priority[neighbor] = new_cost + heuristic(neighbor)
if v not in d or g_new < d[v]: # jeśli v jeszcze nie odkryty came_from[neighbor] = current
# LUB znaleźliśmy krótszą drogę return came_from, cost_so_far.get(goal) # ścieżka + koszt
d[v] = g_new # zaktualizuj g(v) = faktyczny koszt do v
f[v] = g_new + h(v) # zaktualizuj f(v) = g(v) + h(v)
# f kieruje przeszukiwanie:
# niskie f = „obiecujący" wierzchołek
# (blisko celu wg heurystyki)
came_from[v] = u # zapamiętaj: do v dotarliśmy z u
# (do odtworzenia ścieżki)
return came_from, d.get(goal) # came_from = mapa do odtworzenia ścieżki
# d.get(goal) = koszt najkrótszej ścieżki
# do celu (None jeśli nieosiągalny)
# Złożoność: O(V²) z tablicą, ale w praktyce
# dużo szybciej dzięki heurystyce
![Przejście grafu algorytmem A* — krok po kroku](img/astar_traversal.png) ![Przejście grafu algorytmem A* — krok po kroku](img/astar_traversal.png)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -128,197 +128,95 @@ Przykład — kluczowa różnica: decrease-key:
### Pseudokod (Python) ### Pseudokod (Python)
**Dijkstra:** **Dijkstra** (graph = słownik sąsiedztwa, np. `{'A': [('B',2), ('C',4)]}`):
def dijkstra(graph, start): # graph = słownik, klucz = wierzchołek, def dijkstra(graph, source):
# wartość = lista par (sąsiad, waga) dist = {v: float('inf') for v in graph}
# np. graph = {'A': [('B',2), ('C',4)], dist[source] = 0
# 'B': [('D',3)], ...} visited = set()
# start = wierzchołek startowy, np. 'A' for _ in range(len(graph)):
current = None # szukaj nieodwiedzonego wierzchołka o min dist — O(V)
d = {v: float('inf') for v in graph} # d = słownik odległości (distance) for v in graph:
# Klucz: wierzchołek v if v not in visited and (current is None or dist[v] < dist[current]):
# Wartość: najkrótsza DOTYCHCZAS ZNANA current = v
# odległość od start do v if dist[current] == float('inf'):
# Na początku: ∞ (nieskończoność) dla break # reszta nieosiągalna
# wszystkich — bo jeszcze niczego visited.add(current) # zamknij — NIE wracamy (zachłanność)
# nie odkryliśmy for neighbor, weight in graph[current]: # relaksacja sąsiadów
# float('inf') = Python-owa nieskończoność, if dist[current] + weight < dist[neighbor]:
# każda liczba jest od niej mniejsza dist[neighbor] = dist[current] + weight
return dist # O(V²) z tablicą
d[start] = 0 # odległość od startu do samego siebie = 0
visited = set() # visited = zbiór ZAMKNIĘTYCH wierzchołków
# (już przetworzonych, nie wracamy do nich)
# set() = pusty zbiór Pythona — O(1) lookup
for _ in range(len(graph)): # powtórz V razy (raz na każdy wierzchołek)
# W każdej iteracji wybieramy JEDEN
# wierzchołek o min d[v] i go przetwarzamy
# --- Szukanie minimum w tablicy d --- → O(V) na każde szukanie
u = None # u = wierzchołek o najmniejszej odległości
# (jeszcze nie odwiedzony)
for v in graph: # przejrzyj WSZYSTKIE wierzchołki
if v not in visited: # pomiń już odwiedzone
if u is None or d[v] < d[u]: # jeśli v ma mniejszą odległość
u = v # zapamiętaj go jako kandydata
# Po tej pętli: u = wierzchołek z min d, spośród nieodwiedzonych
# To jest O(V) — przeszukujemy całą tablicę!
if d[u] == float('inf'): # jeśli minimum to ∞, reszta jest
break # nieosiągalna — koniec
visited.add(u) # oznacz u jako odwiedzony (zamknięty)
# NIE WRACAMY do u — to jest ZACHŁANNOŚĆ
# Dijkstry (i dlatego ujemne wagi psują!)
for v, w in graph[u]: # iteruj po sąsiadach wierzchołka u
# v = sąsiad (vertex), w = waga krawędzi u→v
# np. graph['A'] = [('B',2), ('C',4)]
# → v='B', w=2, potem v='C', w=4
if d[u] + w < d[v]: # RELAKSACJA: czy droga do v PRZEZ u
# jest krótsza niż dotychczas znana?
# d[u] = koszt dotarcia do u
# w = koszt krawędzi u→v
# d[u]+w = koszt drogi start→u→v
# d[v] = dotychczasowy najlepszy koszt do v
d[v] = d[u] + w # TAK, jest krótsza → zaktualizuj!
# (tablica d pełni tu rolę kolejki
# priorytetowej — po prostu szukamy
# minimum w niej w każdej iteracji)
return d # zwróć słownik najkrótszych odległości
# np. {'A': 0, 'B': 2, 'C': 4, 'D': 5}
# Złożoność: O(V²) — V szukań min × O(V) każde
![Przejście grafu algorytmem Dijkstry — krok po kroku](img/dijkstra_traversal.png) ![Przejście grafu algorytmem Dijkstry — krok po kroku](img/dijkstra_traversal.png)
**Bellman-Ford:** **Bellman-Ford** (vertices = lista wierzchołków, edges = lista krotek (src, dst, weight)):
def bellman_ford(vertices, edges, start): # vertices = lista wierzchołków, np. ['A','B','C','D'] def bellman_ford(vertices, edges, source):
# edges = lista krawędzi, każda to (skąd, dokąd, waga) dist = {v: float('inf') for v in vertices}
# np. [('A','B',2), ('A','C',4), ('B','D',3), ...] dist[source] = 0
# start = wierzchołek startowy for _ in range(len(vertices) - 1): # V1 iteracji (najdłuższa ścieżka = V1 krawędzi)
# UWAGA: format inny niż w Dijkstrze! for src, dst, weight in edges: # relaksuj WSZYSTKIE krawędzie
# Dijkstra: graf jako słownik sąsiedztwa if dist[src] + weight < dist[dst]:
# B-F: explicite lista krawędzi dist[dst] = dist[src] + weight
for src, dst, weight in edges: # V-ta iteracja: wykrywanie cyklu ujemnego
if dist[src] + weight < dist[dst]:
return None # cykl ujemny!
return dist # O(V·E)
d = {v: float('inf') for v in vertices} # d = słownik odległości — identycznie Przykład — graf z ujemnymi wagami (Dijkstra daje ZŁY wynik, B-F poprawny):
# jak w Dijkstrze. Klucz = wierzchołek,
# wartość = najkrótsza znana odległość.
# Na starcie: ∞ dla wszystkich.
d[start] = 0 # odległość do siebie = 0 Graf: S→A(2), A→C(3), S→B(5), B→A(4)
for _ in range(len(vertices) - 1): # powtórz V1 razy (V = liczba wierzchołków) Dijkstra:
# DLACZEGO V1? Bo najdłuższa najkrótsza 1. S(0): dist[A]=2, dist[B]=5
# ścieżka (bez cykli) ma co najwyżej V1 2. A(2) zamknięty: dist[C]=5
# krawędzi. Po k iteracjach mamy poprawne 3. B(5): B→A = 54 = 1 < 2, ALE A już zamknięty POMIJA!
# odległości dla ścieżek o ≤ k krawędziach. Wynik: A=2, C=5 ← BŁĄD (prawidłowe: A=1, C=4)
# _ = zmienna, której nie używamy (konwencja)
for u, v, w in edges: # w KAŻDEJ iteracji przejrzyj WSZYSTKIE krawędzie Bellman-Ford — relaksuje WSZYSTKIE krawędzie, V1 = 3 razy:
# u = początek krawędzi, v = koniec, w = waga Start: dist = [S:0, A:∞, B:∞, C:∞]
# To jest brute-force — stąd O(V·E)
if d[u] + w < d[v]: # RELAKSACJA identyczna jak w Dijkstrze: Iteracja 1:
# czy droga start→u→v jest krótsza niż d[v]? S→A: 0+2=2 < ∞ → A=2
A→C: 2+3=5 < ∞ → C=5
S→B: 0+5=5 < ∞ → B=5
B→A: 54=1 < 2 A=1 ujemna waga poprawia!
d[v] = d[u] + w # TAK → zaktualizuj Iteracja 2:
A→C: 1+3=4 < 5 C=4 propagacja poprawionego A
# --- Wykrywanie cyklu ujemnego --- Iteracja 3: brak zmian → stabilne.
for u, v, w in edges: # dodatkowe (V-te) przejście po krawędziach Wynik: [S:0, A:1, B:5, C:4] ← POPRAWNE
# Jeśli NADAL da się poprawić odległość,
# to znaczy, że istnieje cykl ujemny!
# (po V1 iteracjach powinno być stabilne)
if d[u] + w < d[v]: # nadal można polepszyć? cykl ujemny! Wykrywanie cyklu ujemnego — dodaj krawędź C→B(3):
return None # zwróć None = sygnał "cykl ujemny wykryty" Cykl B→A→C→B = 4 + 3 + (3) = 4 < 0.
Po V1 iteracjach dist nadal maleje → V-ta iteracja:
return d # zwróć słownik odległości (jak Dijkstra) dist[src] + weight < dist[dst] return None
![Przejście grafu algorytmem Bellmana-Forda — krok po kroku](img/bellman_ford_traversal.png) ![Przejście grafu algorytmem Bellmana-Forda — krok po kroku](img/bellman_ford_traversal.png)
**A*:** **A\*** (graph jak Dijkstra; heuristic = h(v) → oszacowanie odl. do celu):
def a_star(graph, start, goal, h): # graph = słownik sąsiedztwa (jak Dijkstra) def a_star(graph, source, goal, heuristic):
# start = wierzchołek startowy cost_so_far = {source: 0} # g(n) — faktyczny koszt dotarcia
# goal = wierzchołek DOCELOWY (cel) priority = {source: heuristic(source)} # f(n) = g(n) + h(n)
# → to jedyna różnica od Dijkstry: came_from = {} # do odtworzenia ścieżki
# szukamy ścieżki do JEDNEGO celu visited = set()
# h = FUNKCJA heurystyczna: h(v) zwraca while priority:
# oszacowanie odległości od v do goal current = min(priority, key=priority.get) # wierzchołek o min f(n)
# np. h = lambda v: odl_euklidesowa(v, goal) del priority[current]
if current == goal:
d = {start: 0} # d = słownik g(n) = faktyczny koszt break # dotarliśmy — A* kończy (Dijkstra przetworzyłby wszystko)
# dotarcia od start do n visited.add(current)
# Tu trzymamy TYLKO odkryte wierzchołki for neighbor, weight in graph[current]:
# (nie inicjalizujemy ∞ dla reszty) if neighbor in visited:
f = {start: h(start)} # f = słownik f(n) = g(n) + h(n)
# f to szacunkowy ŁĄCZNY koszt ścieżki:
# dotychczasowy koszt g + heurystyka h
# Sortujemy po f (nie po g!) — to kieruje
# przeszukiwanie W STRONĘ CELU
# Na starcie: f(start) = 0 + h(start)
came_from = {} # came_from = słownik "skąd przyszliśmy"
# Klucz: wierzchołek v
# Wartość: wierzchołek, z którego dotarliśmy do v
# Służy do ODTWORZENIA ścieżki po znalezieniu celu
# np. came_from = {'B':'A', 'D':'B'}
# → ścieżka: A → B → D
visited = set() # visited = zbiór zamkniętych wierzchołków
# (już przetworzonych)
while f: # dopóki są odkryte, nieprzetworzone wierzchołki
# (f zawiera tylko te, do których dotarliśmy)
# --- Szukanie minimum f w tablicy --- → O(V)
u = min(f, key=f.get) # u = wierzchołek o najniższym f(n)
# min() przeszukuje WSZYSTKIE klucze w f
# key=f.get → porównuj po wartościach f[v]
# Równoważne: for v in f: if f[v] < f[best]...
del f[u] # usuń u z open set (przetwarzamy go teraz)
if u == goal: break # ZNALEZIONO CEL! → przerwij
# Kluczowa optymalizacja A*:
# Dijkstra przetwarza WSZYSTKIE wierzchołki,
# A* KOŃCZY gdy dotrze do celu
visited.add(u) # oznacz u jako przetworzony
for v, w in graph[u]: # iteruj po sąsiadach u
# v = sąsiad, w = waga krawędzi u→v
if v in visited: # jeśli v już przetworzony → pomiń
continue continue
new_cost = cost_so_far[current] + weight
g_new = d[u] + w # g_new = potencjalny nowy koszt dotarcia do v if neighbor not in cost_so_far or new_cost < cost_so_far[neighbor]:
# (koszt do u + krawędź u→v) cost_so_far[neighbor] = new_cost
priority[neighbor] = new_cost + heuristic(neighbor)
if v not in d or g_new < d[v]: # jeśli v jeszcze nie odkryty came_from[neighbor] = current
# LUB znaleźliśmy krótszą drogę return came_from, cost_so_far.get(goal) # ścieżka + koszt
d[v] = g_new # zaktualizuj g(v) = faktyczny koszt do v
f[v] = g_new + h(v) # zaktualizuj f(v) = g(v) + h(v)
# f kieruje przeszukiwanie:
# niskie f = „obiecujący" wierzchołek
# (blisko celu wg heurystyki)
came_from[v] = u # zapamiętaj: do v dotarliśmy z u
# (do odtworzenia ścieżki)
return came_from, d.get(goal) # came_from = mapa do odtworzenia ścieżki
# d.get(goal) = koszt najkrótszej ścieżki
# do celu (None jeśli nieosiągalny)
# Złożoność: O(V²) z tablicą, ale w praktyce
# dużo szybciej dzięki heurystyce
![Przejście grafu algorytmem A* — krok po kroku](img/astar_traversal.png) ![Przejście grafu algorytmem A* — krok po kroku](img/astar_traversal.png)

View File

@ -8,18 +8,9 @@
**Proces (process)** — program w trakcie wykonania. Każdy proces ma własną, izolowaną przestrzeń adresową. System operacyjny zarządza procesami — tworzy, planuje (scheduling), kończy. Np. przeglądarka i edytor to osobne procesy. **Proces (process)** — program w trakcie wykonania. Każdy proces ma własną, izolowaną przestrzeń adresową. System operacyjny zarządza procesami — tworzy, planuje (scheduling), kończy. Np. przeglądarka i edytor to osobne procesy.
**Wątek (thread)** — lekka jednostka wykonania wewnątrz procesu. Wątki jednego procesu współdzielą pamięć (kod, dane, heap), ale mają własny stos i rejestry CPU. Tworzenie wątku jest ~100x szybsze niż procesu. **Wątek (thread)** — lekka jednostka wykonania wewnątrz procesu. Wątki jednego procesu współdzielą pamięć (kod, dane, heap), ale mają własny stos i rejestry CPU. Tworzenie wątku jest ~100× szybsze niż procesu.
Proces = mieszkanie (własny adres, izolacja) ![Proces vs Wątek — porównanie](img/q9_process_vs_thread.png)
Wątek = pokój w mieszkaniu (współdzielona kuchnia = heap)
Cecha Proces Wątek
─────────────────────────────────────────
Pamięć własna współdzielona
Tworzenie ~1-10 ms ~10-100 μs
Przełączanie wolne (TLB) szybkie (rejestry)
Komunikacja IPC/pipe bezpośrednia
Awaria izolowana może zabić proces
--- ---
@ -32,23 +23,20 @@
- **HEAP** — pamięć alokowana dynamicznie (malloc/new), rośnie w górę - **HEAP** — pamięć alokowana dynamicznie (malloc/new), rośnie w górę
- **STACK** — zmienne lokalne, adresy powrotu, rośnie w dół - **STACK** — zmienne lokalne, adresy powrotu, rośnie w dół
┌──────────┐ wysoki adres ![Segmenty pamięci procesu](img/q9_memory_layout.png)
│ STACK ↓ │
│ ... │
│ HEAP ↑ │
│ BSS │
│ DATA │
│ TEXT │
└──────────┘ niski adres
**PCB (Process Control Block)** — struktura danych w jądrze OS opisująca proces: PID, stan, rejestry CPU, tablice stron, otwarte pliki, priorytety. Przełączenie kontekstu = zapisanie PCB starego procesu i wczytanie nowego. **PCB (Process Control Block)** — struktura danych w jądrze OS opisująca proces: PID, stan, rejestry CPU, tablice stron, otwarte pliki, priorytety. Przełączenie kontekstu = zapisanie PCB starego procesu i wczytanie nowego.
![PCB — Process Control Block](img/q9_pcb_structure.png)
**PID (Process ID)** — unikalny identyfikator procesu w systemie. Np. `PID 1` = init/systemd w Linux. **PID (Process ID)** — unikalny identyfikator procesu w systemie. Np. `PID 1` = init/systemd w Linux.
**TID (Thread ID)** — unikalny identyfikator wątku. **TID (Thread ID)** — unikalny identyfikator wątku.
**Stany procesu:** NEW (tworzony) → READY (gotowy, czeka na CPU) ↔ RUNNING (wykonywany) → BLOCKED (czeka na I/O), TERMINATED (zakończony). Scheduler decyduje, który READY staje się RUNNING. **Stany procesu:** NEW (tworzony) → READY (gotowy, czeka na CPU) ↔ RUNNING (wykonywany) → BLOCKED (czeka na I/O), TERMINATED (zakończony). Scheduler decyduje, który READY staje się RUNNING.
![Stany procesu — diagram przejść](img/q9_process_states.png)
--- ---
**Przełączanie kontekstu (context switch)** — zapisanie stanu aktualnego procesu/wątku i wczytanie stanu następnego. Dla procesów kosztowne (wymaga TLB flush = unieważnienie cache translacji adresów). Dla wątków tańsze (ta sama przestrzeń adresowa = brak TLB flush). **Przełączanie kontekstu (context switch)** — zapisanie stanu aktualnego procesu/wątku i wczytanie stanu następnego. Dla procesów kosztowne (wymaga TLB flush = unieważnienie cache translacji adresów). Dla wątków tańsze (ta sama przestrzeń adresowa = brak TLB flush).
@ -70,17 +58,13 @@
**Wyścig (race condition)** — sytuacja, gdy wynik programu zależy od kolejności wykonania operacji przez wątki. Przykład: dwa wątki zwiększają x=0 o 1 → wynik może być 1 zamiast 2, bo oba czytają 0 zanim zapiszą. **Wyścig (race condition)** — sytuacja, gdy wynik programu zależy od kolejności wykonania operacji przez wątki. Przykład: dwa wątki zwiększają x=0 o 1 → wynik może być 1 zamiast 2, bo oba czytają 0 zanim zapiszą.
Wątek A: czytaj x(=0) → dodaj 1 → zapisz x(=1) ![Wyścig — race condition](img/q9_race_condition.png)
Wątek B: czytaj x(=0) → dodaj 1 → zapisz x(=1)
Wynik: x = 1 zamiast oczekiwanego 2!
**Sekcja krytyczna (critical section)** — fragment kodu, który może być wykonywany przez najwyżej jeden wątek naraz. Chroni współdzielone zasoby przed race condition. **Sekcja krytyczna (critical section)** — fragment kodu, który może być wykonywany przez najwyżej jeden wątek naraz. Chroni współdzielone zasoby przed race condition.
**Zakleszczenie (deadlock)** — sytuacja, w której dwa lub więcej wątków czekają na siebie nawzajem i żaden nie może kontynuować. Jak dwa samochody na skrzyżowaniu — oba czekają, nikt nie jedzie. **Zakleszczenie (deadlock)** — sytuacja, w której dwa lub więcej wątków czekają na siebie nawzajem i żaden nie może kontynuować. Jak dwa samochody na skrzyżowaniu — oba czekają, nikt nie jedzie.
Wątek A: trzyma mutex1, czeka na mutex2 ![Zakleszczenie — deadlock](img/q9_deadlock_scenario.png)
Wątek B: trzyma mutex2, czeka na mutex1
→ Zakleszczenie! Żaden nie puści swojego.
**Warunki Coffmana** — 4 warunki konieczne deadlocka (wszystkie muszą zachodzić jednocześnie): **Warunki Coffmana** — 4 warunki konieczne deadlocka (wszystkie muszą zachodzić jednocześnie):
1. **Mutual exclusion** — zasób jest wyłączny (tylko jeden wątek) 1. **Mutual exclusion** — zasób jest wyłączny (tylko jeden wątek)
@ -97,9 +81,7 @@ Złam jeden = brak deadlocka.
**Semafor (semaphore)** — uogólniony mutex z licznikiem. Semafor binarny (0/1) = mutex. Semafor zliczający (n) — pozwala n wątkom jednocześnie. P() = probeer (zmniejsz), V() = verhoog (zwiększ). **Semafor (semaphore)** — uogólniony mutex z licznikiem. Semafor binarny (0/1) = mutex. Semafor zliczający (n) — pozwala n wątkom jednocześnie. P() = probeer (zmniejsz), V() = verhoog (zwiększ).
semafor(3): 3 wątki mogą wejść naraz ![Semafor — koncepcja](img/q9_semaphore_concept.png)
P() → counter-- (jeśli 0 → czekaj)
V() → counter++ (obudź czekającego)
**Monitor** — wysokopoziomowy mechanizm synchronizacji. Obiekt z mutexem wbudowanym — tylko jeden wątek może wykonywać metody monitora. Java: `synchronized`. **Monitor** — wysokopoziomowy mechanizm synchronizacji. Obiekt z mutexem wbudowanym — tylko jeden wątek może wykonywać metody monitora. Java: `synchronized`.
@ -119,25 +101,21 @@ Proces = program w trakcie wykonania + cały jego kontekst. Składa się z:
**Pamięć (oddzielna przestrzeń adresowa):** **Pamięć (oddzielna przestrzeń adresowa):**
┌──────────┐ wysoki adres ![Segmenty pamięci procesu — szczegóły](img/q9_memory_layout.png)
│ STACK ↓ │ zmienne lokalne, adresy powrotu (każdy wątek ma WŁASNY)
│ ... │
│ HEAP ↑ │ malloc/new — dynamiczna alokacja (współdzielony między wątkami)
│ BSS │ zmienne globalne niezainicjalizowane (zerowane)
│ DATA │ zmienne globalne zainicjalizowane
│ TEXT │ kod maszynowy (read-only, współdzielony)
└──────────┘ niski adres
**PCB (Process Control Block)** — struktura w jądrze OS opisująca proces: **PCB (Process Control Block)** — struktura w jądrze OS opisująca proces:
PCB = { PID, stan (READY/RUNNING/BLOCKED), rejestry CPU, ![PCB — struktura](img/q9_pcb_structure.png)
tablice stron, otwarte pliki, priorytety, statystyki }
Przełączenie kontekstu = zapisanie PCB starego procesu → wczytanie PCB nowego. Przełączenie kontekstu = zapisanie PCB starego procesu → wczytanie PCB nowego.
**Stany procesu:** NEW → READY ↔ RUNNING → BLOCKED → TERMINATED. **Stany procesu:** NEW → READY ↔ RUNNING → BLOCKED → TERMINATED.
Scheduler decyduje, który READY staje się RUNNING. Scheduler decyduje, który READY staje się RUNNING.
> **Mnemonik BUDOWY PROCESU — „TDBHS" (segmenty od dołu):**
> **T**ata **D**aje **B**abci **H**erbatę ze **S**mietanką = TEXT → DATA → BSS → HEAP → STACK.
> Proces = mieszkanie (własny adres, izolacja), PCB = dowód osobisty procesu (PID, stan, rejestry).
### Budowa wątku ### Budowa wątku
Wątek = lekka jednostka wykonania WEWNĄTRZ procesu. Wątek = lekka jednostka wykonania WEWNĄTRZ procesu.
@ -145,36 +123,30 @@ Wątek = lekka jednostka wykonania WEWNĄTRZ procesu.
**Współdzielone** z innymi wątkami procesu: TEXT, DATA, BSS, HEAP, otwarte pliki, PID. **Współdzielone** z innymi wątkami procesu: TEXT, DATA, BSS, HEAP, otwarte pliki, PID.
**Prywatne** (każdy wątek ma własne): stos (stack), rejestry CPU, program counter (PC), TID. **Prywatne** (każdy wątek ma własne): stos (stack), rejestry CPU, program counter (PC), TID.
Proces P (PID=42) ![Wątki wewnątrz procesu](img/q9_thread_structure.png)
┌────────────────────────────────────────┐
│ TEXT │ DATA │ BSS │ HEAP │ ← współdzielone
├────────┴────────┴───────┴──────────────┤
│ Wątek 1: [stos₁] [rejestry₁] [PC₁] │ ← prywatne
│ Wątek 2: [stos₂] [rejestry₂] [PC₂] │
│ Wątek 3: [stos₃] [rejestry₃] [PC₃] │
└────────────────────────────────────────┘
Kluczowa różnica: proces ma CAŁĄ przestrzeń adresową, wątek to tylko kontekst wykonania (stos + rejestry) w ramach tej przestrzeni. Kluczowa różnica: proces ma CAŁĄ przestrzeń adresową, wątek to tylko kontekst wykonania (stos + rejestry) w ramach tej przestrzeni.
> **Mnemonik BUDOWY WĄTKU — „Wspólna kuchnia, własny pokój":**
> Współdzielone = KOD + DANE + HEAP + PLIKI (kuchnia, salon, łazienka).
> Prywatne = STOS + REJESTRY + PC (twój pokój, twój telefon, twoja pozycja w książce).
> Skrót: **„SRP"** — **S**tos, **R**ejestry, **P**C = prywatne.
### Szybkość — porównanie ilościowe ### Szybkość — porównanie ilościowe
| Operacja | Proces | Wątek | Dlaczego różnica? | | Operacja | Proces | Wątek | Dlaczego różnica? |
|-------------------|------------------|---------------------|--------------------------------------| |-------------------|------------------|---------------------|--------------------------------------|
| Tworzenie | ~110 ms | ~10100 μs (100x) | Proces: nowa przestrzeń adresowa, tablice stron, kopiowanie struktur jądra. Wątek: tylko nowy stos + wpis w schedulerze. | | Tworzenie | ~110 ms | ~10100 μs (100×) | Proces: nowa przestrzeń adresowa, tablice stron, kopiowanie struktur jądra. Wątek: tylko nowy stos + wpis w schedulerze. |
| Przełączanie | ~10005000 ns | ~100500 ns (10x) | Proces: TLB flush (unieważnienie cache adresów). Wątek: te same tablice stron, TLB zostaje. | | Przełączanie | ~10005000 ns | ~100500 ns (10×) | Proces: TLB flush (unieważnienie cache adresów). Wątek: te same tablice stron, TLB zostaje. |
| Komunikacja | ~μs (IPC) | ~ns (pamięć) | Proces: kopiowanie przez jądro (pipe/socket). Wątek: bezpośredni zapis/odczyt heapu. | | Komunikacja | ~μs (IPC) | ~ns (pamięć) | Proces: kopiowanie przez jądro (pipe/socket). Wątek: bezpośredni zapis/odczyt heapu. |
| Zakończenie | wolne | szybkie | Proces: zwolnienie przestrzeni adresowej, zamknięcie plików. Wątek: zwolnienie stosu. | | Zakończenie | wolne | szybkie | Proces: zwolnienie przestrzeni adresowej, zamknięcie plików. Wątek: zwolnienie stosu. |
Konkretny przykład tworzenia (Linux): ![Szybkość — benchmarki Linux](img/q9_speed_comparison.png)
fork() (nowy proces): ~1-5 ms → kopiowanie tablic stron (copy-on-write) > **Mnemonik SZYBKOŚCI — „100, 10, 1000":**
pthread_create() (wątek): ~50 μs → alokacja stosu (~8 MB) + wpis w schedulerze > Tworzenie wątku **100×** szybsze, przełączanie **10×** szybsze, komunikacja **1000×** szybsza.
> Dlaczego? **„TLB zostaje"** — wątek nie zmienia mieszkania (przestrzeni adresowej), więc cache adresów (TLB) nie trzeba czyścić.
Przełączanie kontekstu (benchmarki): > Zapamiętaj: **fork() = przeprowadzka**, **pthread_create() = nowy pokój w tym samym mieszkaniu**.
Proces → Proces: ~3000 ns (TLB flush + cache cold)
Wątek → Wątek: ~300 ns (TLB ciepły, ta sama pamięć)
Zysk: ~10x szybciej
### Zastosowanie — kiedy proces, kiedy wątek? ### Zastosowanie — kiedy proces, kiedy wątek?
@ -198,18 +170,13 @@ Przełączanie kontekstu (benchmarki):
- Rendering: każdy wątek renderuje część klatki - Rendering: każdy wątek renderuje część klatki
- **Responsywność UI** — wątek główny obsługuje interfejs, wątek tła liczy/pobiera dane - **Responsywność UI** — wątek główny obsługuje interfejs, wątek tła liczy/pobiera dane
**Typowe scenariusze w praktyce:** ![Kiedy proces, kiedy wątek? — scenariusze](img/q9_scenario_table.png)
Scenariusz Proces czy wątek? Dlaczego? > **Mnemonik ZASTOSOWANIA — „MURY vs OKNA":**
────────────────────────────────────────────────────────────── > **Proces jak MURY** = izolacja, bezpieczeństwo, crash isolation (Chrome karty, Apache, sandboxing).
Serwer WWW (Apache pre-fork) Proces izolacja klientów > **Wątek jak OKNA** = szybki dostęp do wspólnych danych, lekki, wydajny (gry, thread pool, UI).
Serwer WWW (nginx) Wątek/async szybkość, cooperacja > Pytanie-test: „Czy awaria jednego ma zabić resztę?" → TAK = wątek OK, NIE = proces.
Przeglądarka (karty) Proces crash isolation > Regułka: **„IBW → P, WSO → W"** = **I**zolacja/**B**ezpieczeństwo/**W**ieloprogramowość → **P**roces. **W**spółdzielenie/**S**zybkość/**O**bliczenia → **W**ątek.
Przeglądarka (JS + rendering) Wątek współdzielony DOM
Gra (fizyka + rendering) Wątek współdzielony świat
Kompilacja wieloplikowa Proces (make -j8) izolacja, prostota
Baza danych (zapytania) Wątek współdzielony cache
Microservices Proces (kontener) izolacja, deployment
### Porównanie zbiorcze ### Porównanie zbiorcze
@ -237,18 +204,12 @@ Procesy mają **izolowane** przestrzenie adresowe — nie mogą bezpośrednio cz
#### Mechanizmy IPC z przykładami #### Mechanizmy IPC z przykładami
![Mechanizmy IPC — szczegóły: Pipe, Shared Memory, Socket](img/q9_ipc_details.png)
**Pipe (potok anonimowy)** — jednokierunkowy strumień bajtów w pamięci jądra. Tylko między procesem-rodzicem a potomkiem (fork). Klasyczny przykład Unix: **Pipe (potok anonimowy)** — jednokierunkowy strumień bajtów w pamięci jądra. Tylko między procesem-rodzicem a potomkiem (fork). Klasyczny przykład Unix:
$ ls -la | grep ".txt" | wc -l $ ls -la | grep ".txt" | wc -l
Jak to działa wewnętrznie:
┌────────┐ write() ┌─────────────┐ read() ┌────────┐
│ ls │──────────→│ bufor jądra │──────────→│ grep │
│ stdout │ fd[1] │ (4 KB) │ fd[0] │ stdin │
└────────┘ └─────────────┘ └────────┘
Proces A pisze do fd[1], Proces B czyta z fd[0].
Jądro buforuje dane. Gdy bufor pełny → write() blokuje.
Kod C: Kod C:
int fd[2]; int fd[2];
pipe(fd); // tworzy potok: fd[0]=read, fd[1]=write pipe(fd); // tworzy potok: fd[0]=read, fd[1]=write
@ -268,14 +229,6 @@ Procesy mają **izolowane** przestrzenie adresowe — nie mogą bezpośrednio cz
**Shared Memory (pamięć współdzielona)** — najszybszy IPC. OS mapuje ten sam region pamięci fizycznej do obu procesów. Zero kopiowania — oba procesy czytają/piszą bezpośrednio. ALE: wymaga synchronizacji (semafor/mutex). **Shared Memory (pamięć współdzielona)** — najszybszy IPC. OS mapuje ten sam region pamięci fizycznej do obu procesów. Zero kopiowania — oba procesy czytają/piszą bezpośrednio. ALE: wymaga synchronizacji (semafor/mutex).
┌───────────┐ ┌───────────┐
│ Proces A │ │ Proces B │
│ │ │ │
│ strona 7 ─┼──→ RAM ←─┼─ strona 3 │ ← ta sama ramka fizyczna!
│ │ ramka 42 │ │
└───────────┘ └───────────┘
Bez kopiowania — A pisze, B widzi od razu.
Kod C (POSIX): Kod C (POSIX):
int fd = shm_open("/my_shm", O_CREAT|O_RDWR, 0666); int fd = shm_open("/my_shm", O_CREAT|O_RDWR, 0666);
ftruncate(fd, 4096); ftruncate(fd, 4096);
@ -285,22 +238,14 @@ Procesy mają **izolowane** przestrzenie adresowe — nie mogą bezpośrednio cz
**Message Queue (kolejka wiadomości)** — strukturalne wiadomości w jądrze. Asynchroniczna: nadawca wrzuca, odbiorca pobiera z kolejki kiedy chce. Typ wiadomości pozwala filtrować. **Message Queue (kolejka wiadomości)** — strukturalne wiadomości w jądrze. Asynchroniczna: nadawca wrzuca, odbiorca pobiera z kolejki kiedy chce. Typ wiadomości pozwala filtrować.
Kod C:
Proces A: msgsnd(qid, &msg, size, 0) // wyślij wiadomość Proces A: msgsnd(qid, &msg, size, 0) // wyślij wiadomość
Proces B: msgrcv(qid, &msg, size, typ, 0) // odbierz (filtruj typ) Proces B: msgrcv(qid, &msg, size, typ, 0) // odbierz (filtruj typ)
Przewaga nad pipe: wiele nadawców/odbiorców, filtrowanie typów, Przewaga nad pipe: wiele nadawców/odbiorców, filtrowanie typów,
wiadomość ma granice (pipe to surowy strumień bajtów). wiadomość ma granice (pipe to surowy strumień bajtów).
**Socket** — dwukierunkowa komunikacja, działa lokalnie (Unix domain) i przez sieć (TCP/UDP). Najbardziej uniwersalny mechanizm IPC. **Socket** — dwukierunkowa komunikacja, działa lokalnie (Unix domain) i przez sieć (TCP/UDP). Najbardziej uniwersalny mechanizm IPC.
┌──────────┐ TCP/IP ┌──────────┐
│ Klient │←────────→│ Serwer │ sieciowy (różne maszyny)
└──────────┘ └──────────┘
┌──────────┐ Unix ┌──────────┐
│ Proces A │←────────→│ Proces B │ lokalny (ten sam host)
└──────────┘ socket └──────────┘ /tmp/app.sock
**Signal (sygnał)** — asynchroniczne powiadomienie od jądra/procesu. Przesyła TYLKO numer (nie dane). Użycie: obsługa Ctrl+C (SIGINT), zabijanie procesów (SIGKILL), powiadomienie o zdarzeniu. **Signal (sygnał)** — asynchroniczne powiadomienie od jądra/procesu. Przesyła TYLKO numer (nie dane). Użycie: obsługa Ctrl+C (SIGINT), zabijanie procesów (SIGKILL), powiadomienie o zdarzeniu.
kill(pid, SIGUSR1); // wyślij sygnał SIGUSR1 do procesu kill(pid, SIGUSR1); // wyślij sygnał SIGUSR1 do procesu
@ -309,14 +254,13 @@ Procesy mają **izolowane** przestrzenie adresowe — nie mogą bezpośrednio cz
#### Porównanie mechanizmów IPC #### Porównanie mechanizmów IPC
Mechanizm Kierunek Szybkość Zastosowanie ![Porównanie mechanizmów IPC — tabela](img/q9_ipc_table.png)
──────────────────────────────────────────────────────────
Pipe jednokier. średnia ls | grep > **Mnemonik KOMUNIKACJI — „PNMSSS" (6 mechanizmów IPC):**
Named Pipe jednokier. średnia demon → klient > **P**iotrek **N**ie **M**a **S**iedmiu **S**tarych **S**karpet = **P**ipe, **N**amed pipe, **M**essage queue, **S**hared memory, **S**ocket, **S**ignal.
Shared Memory dwukier. najszybsza video, bazy danych > Szybkość: **Shared Memory > Pipe ≈ MsgQueue > Socket** (sieciowy najwolniejszy).
Message Queue dwukier. średnia wieloproducentowe > Zapamiętaj: **„Shared = zero kopii"** — najszybszy bo oba procesy piszą do tej samej ramki RAM.
Socket dwukier. wolna (sieć) klient-serwer > **Pipe = rura z wodą** (jednokierunkowa), **Socket = telefon** (dwukierunkowy, też przez sieć).
Signal jednokier. natychmiast. powiadomienia
--- ---
@ -330,142 +274,71 @@ Gdy wątki (lub procesy z shared memory) współdzielą dane, pojawiają się 4
Wynik programu zależy od losowej kolejności operacji wątków. Źródło: operacja „czytaj-modyfikuj-zapisz" nie jest atomowa. Wynik programu zależy od losowej kolejności operacji wątków. Źródło: operacja „czytaj-modyfikuj-zapisz" nie jest atomowa.
Przykład: konto bankowe, saldo = 1000 zł ![Wyścig — dwa przykłady](img/q9_race_condition.png)
Wątek A: wpłata 500 zł Wątek B: wypłata 200 zł
BEZ synchronizacji (błąd!):
─────────────────────────────────────────────────────────
Czas Wątek A Wątek B
─────────────────────────────────────────────────────────
t1 czytaj saldo → 1000
t2 czytaj saldo → 1000
t3 saldo = 1000 + 500 = 1500
t4 saldo = 1000 - 200 = 800
t5 zapisz saldo ← 1500
t6 zapisz saldo ← 800
─────────────────────────────────────────────────────────
Wynik: 800 zł (powinno być 1300!) ← wypłata nadpisała wpłatę!
Poprawka — mutex: Poprawka — mutex:
lock(mutex); lock(mutex);
saldo = saldo + kwota; // sekcja krytyczna saldo = saldo + kwota; // sekcja krytyczna
unlock(mutex); unlock(mutex);
Z mutex: A blokuje → czyta 1000 → pisze 1500 → B blokuje → czyta 1500 → pisze 1300. ✓
#### Problem 2 — Zakleszczenie (Deadlock) #### Problem 2 — Zakleszczenie (Deadlock)
Dwa lub więcej wątków czekają na siebie nawzajem — żaden nie może kontynuować. System „zamiera". Dwa lub więcej wątków czekają na siebie nawzajem — żaden nie może kontynuować. System „zamiera".
Klasyczny scenariusz: 2 wątki, 2 mutexy ![Zakleszczenie — scenariusz i cykl oczekiwania](img/q9_deadlock_scenario.png)
─────────────────────────────────────────────────────────
Wątek A: Wątek B:
lock(mutex1) ✓ ←trzyma lock(mutex2) ✓ ←trzyma
lock(mutex2) ⏳ ←czeka! lock(mutex1) ⏳ ←czeka!
─────────────────────────────────────────────────────────
A czeka na mutex2 (B go trzyma), B czeka na mutex1 (A go trzyma).
→ DEADLOCK — żaden nie odpuści!
Diagram cyklu:
┌──────────┐ czeka na ┌──────────┐
│ Wątek A │───────────→│ Mutex 2 │
│ trzyma │ │ trzyma │
│ Mutex 1 │←───────────│ Wątek B │
└──────────┘ czeka na └──────────┘
**Warunki Coffmana** — 4 warunki konieczne deadlocka (WSZYSTKIE muszą zachodzić): **Warunki Coffmana** — 4 warunki konieczne deadlocka (WSZYSTKIE muszą zachodzić):
1. Mutual Exclusion — zasób wyłączny (tylko 1 wątek) ![Warunki Coffmana — strategie zapobiegania](img/q9_coffman_strategies.png)
2. Hold and Wait — trzymaj zasób, czekaj na kolejny
3. No Preemption — nie można zabrać zasobu siłą
4. Circular Wait — cykliczne oczekiwanie (A→B→...→A)
Strategie zapobiegania (złam jeden warunek):
────────────────────────────────────────────────────
Warunek Jak złamać Przykład
────────────────────────────────────────────────────
Mutual Exclusion Zrób zasób współdzielony Read-write lock
Hold and Wait Bierz WSZYSTKIE naraz lock(m1, m2) atomowo
No Preemption Pozwól na timeout/trylock pthread_mutex_trylock()
Circular Wait Porządek liniowy zamków Zawsze m1 przed m2
────────────────────────────────────────────────────
Najczęstsza strategia: PORZĄDEK LINIOWY (Circular Wait).
Zasada: numeruj mutexy, zawsze blokuj w rosnącej kolejności.
Jeśli mutex1 < mutex2 ZAWSZE lock(mutex1) przed lock(mutex2).
#### Problem 3 — Zagłodzenie (Starvation) #### Problem 3 — Zagłodzenie (Starvation)
Wątek nigdy nie dostaje zasobu, bo inni ciągle go wyprzedzają. Nie jest deadlockiem (inni się wykonują). Przykład: 10 wątków, wątek niskopriorytetowy nigdy nie dostaje CPU bo wysoko priorytetowe ciągle dominują. Wątek nigdy nie dostaje zasobu, bo inni ciągle go wyprzedzają. Nie jest deadlockiem (inni się wykonują). Przykład: 10 wątków, wątek niskopriorytetowy nigdy nie dostaje CPU bo wysoko priorytetowe ciągle dominują.
Rozwiązanie: aging (starzenie) — priorytet rośnie z czasem oczekiwania.
Po 100 ms bez CPU: priorytet +1, po 200 ms: +2, itd.
W końcu nawet najniższy wątek dostanie CPU.
#### Problem 4 — Inwersja priorytetów (Priority Inversion) #### Problem 4 — Inwersja priorytetów (Priority Inversion)
Wątek wysokopriorytetowy (H) czeka na mutex trzymany przez niskopriorytetowy (L), a średniopriorytetowy (M) blokuje L. Efekt: H czeka na M (mimo wyższego priorytetu!). Wątek wysokopriorytetowy (H) czeka na mutex trzymany przez niskopriorytetowy (L), a średniopriorytetowy (M) blokuje L. Efekt: H czeka na M (mimo wyższego priorytetu!).
Priorytet: H > M > L ![Zagłodzenie i inwersja priorytetów](img/q9_starvation_priority.png)
─────────────────────────────────────────────────
Czas L M H
─────────────────────────────────────────────────
t1 lock(mutex)
t2 (gotowy, wypycha L!)
t3 pracuje... (czeka na mutex!)
t4 pracuje... (CZEKA — bo M blokuje L)
t5 gotowy
t6 unlock(mutex) (wreszcie!)
─────────────────────────────────────────────────
H czekał, dopóki M nie skończył, mimo że H > M!
Rozwiązanie: Priority Inheritance Protocol. > **Mnemonik SYNCHRONIZACJI — „WZZI" (4 problemy):**
L dziedziczy priorytet H (tymczasowo L=H), więc M nie może wypchać L. > **W**szystkie **Z**egarki **Z**atrzymały się **I**naczej = **W**yścig, **Z**akleszczenie, **Z**agłodzenie, **I**nwersja priorytetów.
Mars Pathfinder (1997) — klasyczny bug priority inversion w kosmosie! > Coffman — **„MHNC"**: **M**uszę **H**amować, **N**ie **C**ofam = **M**utual exclusion, **H**old-and-wait, **N**o preemption, **C**ircular wait. Złam JEDEN = brak deadlocka.
> Najłatwiej złamać **Circular Wait** → numeruj mutexy, blokuj ROSNĄCO.
> Wyścig → **mutex**. Zakleszczenie → **porządek zamków**. Zagłodzenie → **aging**. Inwersja → **priority inheritance**.
--- ---
### Klasyczne problemy synchronizacji ### Klasyczne problemy synchronizacji
![Producent-konsument z buforem cyklicznym](img/producer_consumer.png) ![Klasyczne problemy synchronizacji — 3 panele](img/q9_classic_problems.png)
#### Producent-Konsument (Bounded Buffer) #### Producent-Konsument (Bounded Buffer)
n producentów wrzuca elementy do bufora o ograniczonej pojemności, m konsumentów pobiera. Bufor pełny → producent czeka. Bufor pusty → konsument czeka. n producentów wrzuca elementy do bufora o ograniczonej pojemności, m konsumentów pobiera. Bufor pełny → producent czeka. Bufor pusty → konsument czeka.
![Producent-konsument z buforem cyklicznym](img/producer_consumer.png)
Rozwiązanie z semaforami: Rozwiązanie z semaforami:
─────────────────────────────────────────────────
semaphore mutex = 1; // wyłączny dostęp do bufora semaphore mutex = 1; // wyłączny dostęp do bufora
semaphore empty = N; // ile wolnych slotów (początkowo N) semaphore empty = N; // ile wolnych slotów (początkowo N)
semaphore full = 0; // ile pełnych slotów (początkowo 0) semaphore full = 0; // ile pełnych slotów (początkowo 0)
Producent: Konsument: Producent: Konsument:
P(empty) // czekaj na P(full) // czekaj na P(empty) P(full)
// wolny slot // pełny slot P(mutex) P(mutex)
P(mutex) // wejdź do P(mutex) // wejdź do wstaw(elem) elem = pobierz()
// sek. kryt. // sek. kryt. V(mutex) V(mutex)
wstaw(elem) elem = pobierz() V(full) V(empty)
V(mutex) // wyjdź V(mutex) // wyjdź
V(full) // +1 pełny V(empty) // +1 wolny
─────────────────────────────────────────────────
Bufor (N=4): UWAGA: kolejność P(empty/full) PRZED P(mutex)!
┌────┬────┬────┬────┐ Odwrotnie (P(mutex) → P(empty)) = DEADLOCK!
│ A │ B │ │ │ ← full=2, empty=2
└────┴────┴────┴────┘
↑ ↑
konsument producent
BŁĄD jeśli zamienimy kolejność P(empty) i P(mutex) w producencie:
Producent: P(mutex) → P(empty) ← bufor pełny → czeka z mutexem!
Konsument: P(full) → P(mutex) ← mutex zajęty → DEADLOCK!
#### Czytelnicy-Pisarze (Readers-Writers) #### Czytelnicy-Pisarze (Readers-Writers)
Wielu czytelników może czytać jednocześnie. Pisarz wymaga wyłącznego dostępu (ani czytelnicy, ani inni pisarze). Wielu czytelników może czytać jednocześnie. Pisarz wymaga wyłącznego dostępu (ani czytelnicy, ani inni pisarze).
Rozwiązanie (first readers-writers): Rozwiązanie (first readers-writers):
─────────────────────────────────────────────────
int readers = 0; int readers = 0;
mutex rw_mutex; // pisarz LUB pierwszy/ostatni czytelnik mutex rw_mutex; // pisarz LUB pierwszy/ostatni czytelnik
mutex count_mutex; // ochrona zmiennej readers mutex count_mutex; // ochrona zmiennej readers
@ -474,15 +347,14 @@ Wielu czytelników może czytać jednocześnie. Pisarz wymaga wyłącznego dost
lock(count_mutex) lock(rw_mutex) lock(count_mutex) lock(rw_mutex)
readers++ // PISZ (wyłączny) readers++ // PISZ (wyłączny)
if (readers == 1) unlock(rw_mutex) if (readers == 1) unlock(rw_mutex)
lock(rw_mutex) // 1. czytelnik blokuje pisarzy lock(rw_mutex)
unlock(count_mutex) unlock(count_mutex)
// CZYTAJ (wielu jednocześnie!) // CZYTAJ (wielu jednocześnie!)
lock(count_mutex) lock(count_mutex)
readers-- readers--
if (readers == 0) if (readers == 0)
unlock(rw_mutex) // ostatni odblokowuje pisarzy unlock(rw_mutex)
unlock(count_mutex) unlock(count_mutex)
─────────────────────────────────────────────────
Problem: pisarze mogą głodować (readers=0 nigdy nie zachodzi). Problem: pisarze mogą głodować (readers=0 nigdy nie zachodzi).
Rozwiązanie: fairness — kolejka FIFO czytelników i pisarzy. Rozwiązanie: fairness — kolejka FIFO czytelników i pisarzy.
@ -498,34 +370,22 @@ Wielu czytelników może czytać jednocześnie. Pisarz wymaga wyłącznego dost
Alternatywa: semafor(4) — max 4 filozofów próbuje jeść naraz Alternatywa: semafor(4) — max 4 filozofów próbuje jeść naraz
→ jeden widelec zawsze wolny → brak deadlocka. → jeden widelec zawsze wolny → brak deadlocka.
> **Mnemonik KLASYCZNYCH PROBLEMÓW — „PCF":**
> **P**yszne **C**iastka **F**ilozofów = **P**roducent-konsument, **C**zytelnicy-pisarze, **F**ilozofowie.
> Producent-konsument: „P(empty) PRZED P(mutex)" — inaczej deadlock!
> Czytelnicy-pisarze: „wielu czyta, jeden pisze" — pisarze mogą głodować.
> Filozofowie: „jeden bierze odwrotnie" — łamie circular wait.
--- ---
### Mechanizmy synchronizacji — porównanie ### Mechanizmy synchronizacji — porównanie
Mechanizm Opis Kiedy używać ![Mechanizmy synchronizacji — porównanie i mutex vs semafor vs spinlock](img/q9_sync_comparison.png)
──────────────────────────────────────────────────────────────────
Mutex Zamek: 1 wątek w sekcji Sekcja krytyczna
Semafor(n) Licznik: max n wątków Ograniczone zasoby
Monitor Obiekt z wbudowanym mutex Java synchronized
Cond. Variable wait()/signal() na warunek Producent-konsument
Spinlock Aktywne czekanie (busy-wait) Bardzo krótkie sekcje
RW Lock Wielu czytelników LUB 1 pisarz Bazy danych, cache
Barrier Czekaj aż wszyscy dotrą Obliczenia równoległe
Mutex vs Semafor: > **Mnemonik MECHANIZMÓW — „MSMCSBR":**
┌────────────────────────────────────────────────────────────┐ > **M**ała **S**owa **M**oże **C**zasem **S**pinać na **B**ardzo **R**ówno = **M**utex, **S**emafor, **M**onitor, **C**ond.Variable, **S**pinlock, **B**arrier, **R**W Lock.
│ Mutex = klucz do łazienki (1 osoba) │ > Reguła kciuka: sekcja **> 1 μs → MUTEX** (wątek zasypia). Sekcja **< 1 μs SPINLOCK** (kręci się). **n wątków naraz → SEMAFOR(n)**.
│ Semafor(3) = parking na 3 miejsca (3 samochody naraz) │ > Mutex = klucz do łazienki (1 osoba). Semafor(3) = parking na 3 miejsca. Spinlock = obrotowe drzwi.
│ Semafor(1) = mutex (szczególny przypadek) │
└────────────────────────────────────────────────────────────┘
Mutex vs Spinlock:
┌────────────────────────────────────────────────────────────┐
│ Mutex: wątek ZASYPIA gdy czeka → OS go obudzi (koszt ~μs)│
│ Spinlock: wątek KRĘCI się w pętli → marnuje CPU │
│ Spinlock lepszy gdy sekcja < 1 μs (koszt uśpienia > spin)│
│ Mutex lepszy gdy sekcja > 1 μs (nie marnuje CPU) │
└────────────────────────────────────────────────────────────┘
### Etymologia ### Etymologia
@ -536,4 +396,12 @@ Wielu czytelników może czytać jednocześnie. Pisarz wymaga wyłącznego dost
- **„Proces = mieszkanie, Wątek = pokój"** — każde mieszkanie ma adres (przestrzeń), pokoje dzielą kuchnię (heap) - **„Proces = mieszkanie, Wątek = pokój"** — każde mieszkanie ma adres (przestrzeń), pokoje dzielą kuchnię (heap)
- Wątki szybsze bo nie trzeba zmieniać „mieszkania" (TLB flush) - Wątki szybsze bo nie trzeba zmieniać „mieszkania" (TLB flush)
- **4 warunki Coffmana** zakleszczenia: złam jeden → brak deadlocka - **4 warunki Coffmana** zakleszczenia: złam jeden → brak deadlocka
- **„100, 10, 1000"** — tworzenie 100× szybsze, przełączanie 10×, komunikacja 1000×
- **PNMSSS** — 6 mechanizmów IPC (Pipe, Named pipe, Msg queue, Shared mem, Socket, Signal)
- **WZZI** — 4 problemy synchronizacji (Wyścig, Zakleszczenie, Zagłodzenie, Inwersja)
- **MHNC** — 4 warunki Coffmana (Mutual excl., Hold&wait, No preemption, Circular wait)
- **PCF** — 3 klasyczne problemy (Producent-konsument, Czytelnicy-pisarze, Filozofowie)
- **SRP** — prywatne części wątku (Stos, Rejestry, PC)
- **IBW → Proces, WSO → Wątek** — kiedy co stosować
\newpage

View File

@ -0,0 +1,539 @@
## PYTANIE 9: Procesy i wątki (SOI)
**Budowa, szybkość, zastosowanie. Problemy komunikacji i synchronizacji.**
---
### Tło pojęciowe — słowniczek
**Proces (process)** — program w trakcie wykonania. Każdy proces ma własną, izolowaną przestrzeń adresową. System operacyjny zarządza procesami — tworzy, planuje (scheduling), kończy. Np. przeglądarka i edytor to osobne procesy.
**Wątek (thread)** — lekka jednostka wykonania wewnątrz procesu. Wątki jednego procesu współdzielą pamięć (kod, dane, heap), ale mają własny stos i rejestry CPU. Tworzenie wątku jest ~100x szybsze niż procesu.
Proces = mieszkanie (własny adres, izolacja)
Wątek = pokój w mieszkaniu (współdzielona kuchnia = heap)
Cecha Proces Wątek
─────────────────────────────────────────
Pamięć własna współdzielona
Tworzenie ~1-10 ms ~10-100 μs
Przełączanie wolne (TLB) szybkie (rejestry)
Komunikacja IPC/pipe bezpośrednia
Awaria izolowana może zabić proces
---
**Przestrzeń adresowa (address space)** — zakres adresów pamięci wirtualnej dostępnych procesowi. Każdy proces widzi swoją „prywatną" pamięć, nawet jeśli fizycznie jest mapowana gdzieś indziej.
**Segmenty pamięci procesu:**
- **TEXT** — kod maszynowy (read-only)
- **DATA** — zainicjalizowane zmienne globalne/statyczne
- **BSS** — niezainicjalizowane zmienne globalne (zerowane)
- **HEAP** — pamięć alokowana dynamicznie (malloc/new), rośnie w górę
- **STACK** — zmienne lokalne, adresy powrotu, rośnie w dół
┌──────────┐ wysoki adres
│ STACK ↓ │
│ ... │
│ HEAP ↑ │
│ BSS │
│ DATA │
│ TEXT │
└──────────┘ niski adres
**PCB (Process Control Block)** — struktura danych w jądrze OS opisująca proces: PID, stan, rejestry CPU, tablice stron, otwarte pliki, priorytety. Przełączenie kontekstu = zapisanie PCB starego procesu i wczytanie nowego.
**PID (Process ID)** — unikalny identyfikator procesu w systemie. Np. `PID 1` = init/systemd w Linux.
**TID (Thread ID)** — unikalny identyfikator wątku.
**Stany procesu:** NEW (tworzony) → READY (gotowy, czeka na CPU) ↔ RUNNING (wykonywany) → BLOCKED (czeka na I/O), TERMINATED (zakończony). Scheduler decyduje, który READY staje się RUNNING.
---
**Przełączanie kontekstu (context switch)** — zapisanie stanu aktualnego procesu/wątku i wczytanie stanu następnego. Dla procesów kosztowne (wymaga TLB flush = unieważnienie cache translacji adresów). Dla wątków tańsze (ta sama przestrzeń adresowa = brak TLB flush).
**TLB (Translation Lookaside Buffer)** — sprzętowy cache translacji adres wirtualny → fizyczny. Przy zmianie procesu TLB trzeba wyczyścić (flush), bo nowy proces ma inne mapowania. Koszt: ~1000 ns. Przy zmianie wątku — TLB zostaje (ten sam proces).
---
**IPC (Inter-Process Communication)** — mechanizmy komunikacji między procesami. Konieczne, bo procesy mają izolowane przestrzenie adresowe i nie mogą czytać wzajemnej pamięci bezpośrednio.
- **Pipe** — jednokierunkowy strumień bajtów (ls | grep foo). Anonimowy, tylko między spokrewnionymi procesami.
- **Named Pipe (FIFO)** — pipe z nazwą w systemie plików, mogą go używać niespokrewnione procesy.
- **Message Queue** — kolejka wiadomości w jądrze; asynchroniczna komunikacja.
- **Shared Memory** — wspólny region pamięci; najszybszy IPC (brak kopiowania), ale wymaga synchronizacji.
- **Socket** — komunikacja sieciowa lub lokalna (Unix domain socket). Uniwersalny, działa między maszynami.
- **Signal** — asynchroniczne powiadomienie (np. SIGKILL, SIGTERM). Ograniczony — przesyła tylko numer sygnału.
---
**Wyścig (race condition)** — sytuacja, gdy wynik programu zależy od kolejności wykonania operacji przez wątki. Przykład: dwa wątki zwiększają x=0 o 1 → wynik może być 1 zamiast 2, bo oba czytają 0 zanim zapiszą.
Wątek A: czytaj x(=0) → dodaj 1 → zapisz x(=1)
Wątek B: czytaj x(=0) → dodaj 1 → zapisz x(=1)
Wynik: x = 1 zamiast oczekiwanego 2!
**Sekcja krytyczna (critical section)** — fragment kodu, który może być wykonywany przez najwyżej jeden wątek naraz. Chroni współdzielone zasoby przed race condition.
**Zakleszczenie (deadlock)** — sytuacja, w której dwa lub więcej wątków czekają na siebie nawzajem i żaden nie może kontynuować. Jak dwa samochody na skrzyżowaniu — oba czekają, nikt nie jedzie.
Wątek A: trzyma mutex1, czeka na mutex2
Wątek B: trzyma mutex2, czeka na mutex1
→ Zakleszczenie! Żaden nie puści swojego.
**Warunki Coffmana** — 4 warunki konieczne deadlocka (wszystkie muszą zachodzić jednocześnie):
1. **Mutual exclusion** — zasób jest wyłączny (tylko jeden wątek)
2. **Hold and wait** — trzymaj zasób, czekaj na kolejny
3. **No preemption** — nie można zabrać zasobu siłą
4. **Circular wait** — cykliczne oczekiwanie (A→B→C→A)
Złam jeden = brak deadlocka.
**Zagłodzenie (starvation)** — wątek nigdy nie dostaje zasobu, bo inni ciągle go wyprzedzają (np. nisko priorytetowy wątek przy high-priority scheduling).
---
**Mutex (MUTual EXclusion)** — zamek na sekcję krytyczną. Tylko jeden wątek może go „zamknąć" (lock); reszta czeka (sleep). Tryb: lock → sekcja krytyczna → unlock.
**Semafor (semaphore)** — uogólniony mutex z licznikiem. Semafor binarny (0/1) = mutex. Semafor zliczający (n) — pozwala n wątkom jednocześnie. P() = probeer (zmniejsz), V() = verhoog (zwiększ).
semafor(3): 3 wątki mogą wejść naraz
P() → counter-- (jeśli 0 → czekaj)
V() → counter++ (obudź czekającego)
**Monitor** — wysokopoziomowy mechanizm synchronizacji. Obiekt z mutexem wbudowanym — tylko jeden wątek może wykonywać metody monitora. Java: `synchronized`.
**Condition Variable** — pozwala wątkowi czekać (wait) na spełnienie warunku i być obudzonym (signal/notify) przez inny wątek. Używane z mutexem.
**Spinlock** — zamek, w którym wątek aktywnie czeka w pętli (busy-wait) zamiast zasypiać. Szybki dla bardzo krótkich sekcji krytycznych (~ns), marnotrawny dla dłuższych.
**Read-Write Lock** — pozwala wielu czytelnikom jednocześnie LUB jednemu pisarzowi. Optymalizacja dla scenariuszy z dużo odczytów i rzadkimi zapisami.
**Barrier** — punkt synchronizacji: wszystkie wątki muszą dotrzeć do bariery, zanim którykolwiek może kontynuować. Użyteczna w obliczeniach równoległych (np. po każdej iteracji).
---
### Budowa procesu
Proces = program w trakcie wykonania + cały jego kontekst. Składa się z:
**Pamięć (oddzielna przestrzeń adresowa):**
┌──────────┐ wysoki adres
│ STACK ↓ │ zmienne lokalne, adresy powrotu (każdy wątek ma WŁASNY)
│ ... │
│ HEAP ↑ │ malloc/new — dynamiczna alokacja (współdzielony między wątkami)
│ BSS │ zmienne globalne niezainicjalizowane (zerowane)
│ DATA │ zmienne globalne zainicjalizowane
│ TEXT │ kod maszynowy (read-only, współdzielony)
└──────────┘ niski adres
**PCB (Process Control Block)** — struktura w jądrze OS opisująca proces:
PCB = { PID, stan (READY/RUNNING/BLOCKED), rejestry CPU,
tablice stron, otwarte pliki, priorytety, statystyki }
Przełączenie kontekstu = zapisanie PCB starego procesu → wczytanie PCB nowego.
**Stany procesu:** NEW → READY ↔ RUNNING → BLOCKED → TERMINATED.
Scheduler decyduje, który READY staje się RUNNING.
### Budowa wątku
Wątek = lekka jednostka wykonania WEWNĄTRZ procesu.
**Współdzielone** z innymi wątkami procesu: TEXT, DATA, BSS, HEAP, otwarte pliki, PID.
**Prywatne** (każdy wątek ma własne): stos (stack), rejestry CPU, program counter (PC), TID.
Proces P (PID=42)
┌────────────────────────────────────────┐
│ TEXT │ DATA │ BSS │ HEAP │ ← współdzielone
├────────┴────────┴───────┴──────────────┤
│ Wątek 1: [stos₁] [rejestry₁] [PC₁] │ ← prywatne
│ Wątek 2: [stos₂] [rejestry₂] [PC₂] │
│ Wątek 3: [stos₃] [rejestry₃] [PC₃] │
└────────────────────────────────────────┘
Kluczowa różnica: proces ma CAŁĄ przestrzeń adresową, wątek to tylko kontekst wykonania (stos + rejestry) w ramach tej przestrzeni.
### Szybkość — porównanie ilościowe
| Operacja | Proces | Wątek | Dlaczego różnica? |
|-------------------|------------------|---------------------|--------------------------------------|
| Tworzenie | ~110 ms | ~10100 μs (100x) | Proces: nowa przestrzeń adresowa, tablice stron, kopiowanie struktur jądra. Wątek: tylko nowy stos + wpis w schedulerze. |
| Przełączanie | ~10005000 ns | ~100500 ns (10x) | Proces: TLB flush (unieważnienie cache adresów). Wątek: te same tablice stron, TLB zostaje. |
| Komunikacja | ~μs (IPC) | ~ns (pamięć) | Proces: kopiowanie przez jądro (pipe/socket). Wątek: bezpośredni zapis/odczyt heapu. |
| Zakończenie | wolne | szybkie | Proces: zwolnienie przestrzeni adresowej, zamknięcie plików. Wątek: zwolnienie stosu. |
Konkretny przykład tworzenia (Linux):
fork() (nowy proces): ~1-5 ms → kopiowanie tablic stron (copy-on-write)
pthread_create() (wątek): ~50 μs → alokacja stosu (~8 MB) + wpis w schedulerze
Przełączanie kontekstu (benchmarki):
Proces → Proces: ~3000 ns (TLB flush + cache cold)
Wątek → Wątek: ~300 ns (TLB ciepły, ta sama pamięć)
Zysk: ~10x szybciej
### Zastosowanie — kiedy proces, kiedy wątek?
**Procesy — stosuj gdy:**
- **Izolacja jest krytyczna** — awaria jednego nie zabija reszty
- Przeglądarka Chrome: każda karta = osobny proces. Crash Flash w jednej karcie nie zabija reszty.
- Serwer: każde połączenie = fork() → klient nie może uszkodzić serwera (Apache pre-fork MPM)
- **Bezpieczeństwo** — procesy nie widzą nawzajem pamięci
- Sandboxing: proces renderujący PDFa nie ma dostępu do pamięci procesu z hasłami
- **Wieloprogramowość** — różne programy (edytor + kompilator + przeglądarka)
- **Fork-exec** — klasyczny model Unix: fork() + exec() → nowy program
**Wątki — stosuj gdy:**
- **Współdzielenie danych** — wątki czytają/piszą ten sam heap bez kopiowania
- Serwer WWW: wątki obsługujące requesty współdzielą cache w pamięci (nginx worker threads)
- Gra: wątek renderujący i wątek fizyki czytają ten sam świat gry
- **Szybkość tworzenia/przełączania** — potrzeba wielu lekkich zadań
- Thread pool: 8 wątków obsługuje tysiące zadań (zamiast tysiąca procesów)
- **Obliczenia równoległe** — podział pracy na rdzenie CPU
- Mnożenie macierzy: każdy wątek liczy fragment wyniku
- Rendering: każdy wątek renderuje część klatki
- **Responsywność UI** — wątek główny obsługuje interfejs, wątek tła liczy/pobiera dane
**Typowe scenariusze w praktyce:**
Scenariusz Proces czy wątek? Dlaczego?
──────────────────────────────────────────────────────────────
Serwer WWW (Apache pre-fork) Proces izolacja klientów
Serwer WWW (nginx) Wątek/async szybkość, cooperacja
Przeglądarka (karty) Proces crash isolation
Przeglądarka (JS + rendering) Wątek współdzielony DOM
Gra (fizyka + rendering) Wątek współdzielony świat
Kompilacja wieloplikowa Proces (make -j8) izolacja, prostota
Baza danych (zapytania) Wątek współdzielony cache
Microservices Proces (kontener) izolacja, deployment
### Porównanie zbiorcze
| Cecha | Proces | Wątek |
|-----------------|------------------|----------------------|
| Przestrzeń addr | Własna, izolowana| Współdzielona |
| Tworzenie | ~1-10 ms | ~10-100 μs |
| Przełączanie | Wolne (TLB flush)| Szybkie (rejestry) |
| Komunikacja | IPC (pipe, shm) | Współdzielona pamięć |
| Izolacja | Pełna | Brak |
| Awaria | Nie zabija innych | Może zabić cały proces|
| Zastosowanie | Izolacja, bezpieczeństwo | Wydajność, współdzielenie |
### Problemy komunikacji
Procesy mają **izolowane** przestrzenie adresowe — nie mogą bezpośrednio czytać/pisać wzajemnej pamięci. Komunikacja wymaga pośrednictwa jądra OS (IPC). Wątki mają odwrotny problem: współdzielą pamięć, więc komunikacja jest trywialna, ale wymaga synchronizacji.
![Mechanizmy IPC — porównanie wizualny](img/ipc_mechanisms.png)
**Problem 1 — Overhead kopiowania (procesy).** Większość mechanizmów IPC wymaga kopiowania danych: proces A → jądro → proces B (2 kopie!). Przy dużych danych (np. klatki wideo 4K = 24 MB) to kosztowne.
**Problem 2 — Synchronizacja dostępu (wątki).** Wątki komunikują się przez wspólny heap, ale muszą pilnować, by nie pisać jednocześnie w to samo miejsce (race condition).
**Problem 3 — Blokowanie.** IPC może być synchroniczne (blokujące — nadawca czeka aż odbiorca przeczyta) lub asynchroniczne (nieblokujące — nadawca idzie dalej). Wybór wpływa na wydajność i złożoność kodu.
#### Mechanizmy IPC z przykładami
**Pipe (potok anonimowy)** — jednokierunkowy strumień bajtów w pamięci jądra. Tylko między procesem-rodzicem a potomkiem (fork). Klasyczny przykład Unix:
$ ls -la | grep ".txt" | wc -l
Jak to działa wewnętrznie:
┌────────┐ write() ┌─────────────┐ read() ┌────────┐
│ ls │──────────→│ bufor jądra │──────────→│ grep │
│ stdout │ fd[1] │ (4 KB) │ fd[0] │ stdin │
└────────┘ └─────────────┘ └────────┘
Proces A pisze do fd[1], Proces B czyta z fd[0].
Jądro buforuje dane. Gdy bufor pełny → write() blokuje.
Kod C:
int fd[2];
pipe(fd); // tworzy potok: fd[0]=read, fd[1]=write
if (fork() == 0) { // potomek
close(fd[1]); // zamknij pisanie
read(fd[0], buf, n); // czytaj od rodzica
} else { // rodzic
close(fd[0]); // zamknij czytanie
write(fd[1], "hello", 5); // pisz do potomka
}
**Named Pipe (FIFO)** — jak pipe, ale ma nazwę w systemie plików. Niespokrewnione procesy mogą go używać:
$ mkfifo /tmp/moj_potok # stwórz FIFO
$ echo "dane" > /tmp/moj_potok & # proces A pisze
$ cat /tmp/moj_potok # proces B czyta → "dane"
**Shared Memory (pamięć współdzielona)** — najszybszy IPC. OS mapuje ten sam region pamięci fizycznej do obu procesów. Zero kopiowania — oba procesy czytają/piszą bezpośrednio. ALE: wymaga synchronizacji (semafor/mutex).
┌───────────┐ ┌───────────┐
│ Proces A │ │ Proces B │
│ │ │ │
│ strona 7 ─┼──→ RAM ←─┼─ strona 3 │ ← ta sama ramka fizyczna!
│ │ ramka 42 │ │
└───────────┘ └───────────┘
Bez kopiowania — A pisze, B widzi od razu.
Kod C (POSIX):
int fd = shm_open("/my_shm", O_CREAT|O_RDWR, 0666);
ftruncate(fd, 4096);
char *ptr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
sprintf(ptr, "dane z procesu A"); // Proces A pisze
// Proces B: shm_open + mmap → czyta ptr → "dane z procesu A"
**Message Queue (kolejka wiadomości)** — strukturalne wiadomości w jądrze. Asynchroniczna: nadawca wrzuca, odbiorca pobiera z kolejki kiedy chce. Typ wiadomości pozwala filtrować.
Proces A: msgsnd(qid, &msg, size, 0) // wyślij wiadomość
Proces B: msgrcv(qid, &msg, size, typ, 0) // odbierz (filtruj typ)
Przewaga nad pipe: wiele nadawców/odbiorców, filtrowanie typów,
wiadomość ma granice (pipe to surowy strumień bajtów).
**Socket** — dwukierunkowa komunikacja, działa lokalnie (Unix domain) i przez sieć (TCP/UDP). Najbardziej uniwersalny mechanizm IPC.
┌──────────┐ TCP/IP ┌──────────┐
│ Klient │←────────→│ Serwer │ sieciowy (różne maszyny)
└──────────┘ └──────────┘
┌──────────┐ Unix ┌──────────┐
│ Proces A │←────────→│ Proces B │ lokalny (ten sam host)
└──────────┘ socket └──────────┘ /tmp/app.sock
**Signal (sygnał)** — asynchroniczne powiadomienie od jądra/procesu. Przesyła TYLKO numer (nie dane). Użycie: obsługa Ctrl+C (SIGINT), zabijanie procesów (SIGKILL), powiadomienie o zdarzeniu.
kill(pid, SIGUSR1); // wyślij sygnał SIGUSR1 do procesu
$ kill -9 1234 // wyślij SIGKILL (nie do przechwycenia)
signal(SIGINT, handler); // zarejestruj handler dla Ctrl+C
#### Porównanie mechanizmów IPC
Mechanizm Kierunek Szybkość Zastosowanie
──────────────────────────────────────────────────────────
Pipe jednokier. średnia ls | grep
Named Pipe jednokier. średnia demon → klient
Shared Memory dwukier. najszybsza video, bazy danych
Message Queue dwukier. średnia wieloproducentowe
Socket dwukier. wolna (sieć) klient-serwer
Signal jednokier. natychmiast. powiadomienia
---
### Problemy synchronizacji
Gdy wątki (lub procesy z shared memory) współdzielą dane, pojawiają się 4 fundamentalne problemy:
![Ilustracja zakleszczenia — cykl oczekiwania](img/deadlock_illustration.png)
#### Problem 1 — Wyścig (Race Condition)
Wynik programu zależy od losowej kolejności operacji wątków. Źródło: operacja „czytaj-modyfikuj-zapisz" nie jest atomowa.
Przykład: konto bankowe, saldo = 1000 zł
Wątek A: wpłata 500 zł Wątek B: wypłata 200 zł
BEZ synchronizacji (błąd!):
─────────────────────────────────────────────────────────
Czas Wątek A Wątek B
─────────────────────────────────────────────────────────
t1 czytaj saldo → 1000
t2 czytaj saldo → 1000
t3 saldo = 1000 + 500 = 1500
t4 saldo = 1000 - 200 = 800
t5 zapisz saldo ← 1500
t6 zapisz saldo ← 800
─────────────────────────────────────────────────────────
Wynik: 800 zł (powinno być 1300!) ← wypłata nadpisała wpłatę!
Poprawka — mutex:
lock(mutex);
saldo = saldo + kwota; // sekcja krytyczna
unlock(mutex);
Z mutex: A blokuje → czyta 1000 → pisze 1500 → B blokuje → czyta 1500 → pisze 1300. ✓
#### Problem 2 — Zakleszczenie (Deadlock)
Dwa lub więcej wątków czekają na siebie nawzajem — żaden nie może kontynuować. System „zamiera".
Klasyczny scenariusz: 2 wątki, 2 mutexy
─────────────────────────────────────────────────────────
Wątek A: Wątek B:
lock(mutex1) ✓ ←trzyma lock(mutex2) ✓ ←trzyma
lock(mutex2) ⏳ ←czeka! lock(mutex1) ⏳ ←czeka!
─────────────────────────────────────────────────────────
A czeka na mutex2 (B go trzyma), B czeka na mutex1 (A go trzyma).
→ DEADLOCK — żaden nie odpuści!
Diagram cyklu:
┌──────────┐ czeka na ┌──────────┐
│ Wątek A │───────────→│ Mutex 2 │
│ trzyma │ │ trzyma │
│ Mutex 1 │←───────────│ Wątek B │
└──────────┘ czeka na └──────────┘
**Warunki Coffmana** — 4 warunki konieczne deadlocka (WSZYSTKIE muszą zachodzić):
1. Mutual Exclusion — zasób wyłączny (tylko 1 wątek)
2. Hold and Wait — trzymaj zasób, czekaj na kolejny
3. No Preemption — nie można zabrać zasobu siłą
4. Circular Wait — cykliczne oczekiwanie (A→B→...→A)
Strategie zapobiegania (złam jeden warunek):
────────────────────────────────────────────────────
Warunek Jak złamać Przykład
────────────────────────────────────────────────────
Mutual Exclusion Zrób zasób współdzielony Read-write lock
Hold and Wait Bierz WSZYSTKIE naraz lock(m1, m2) atomowo
No Preemption Pozwól na timeout/trylock pthread_mutex_trylock()
Circular Wait Porządek liniowy zamków Zawsze m1 przed m2
────────────────────────────────────────────────────
Najczęstsza strategia: PORZĄDEK LINIOWY (Circular Wait).
Zasada: numeruj mutexy, zawsze blokuj w rosnącej kolejności.
Jeśli mutex1 < mutex2 ZAWSZE lock(mutex1) przed lock(mutex2).
#### Problem 3 — Zagłodzenie (Starvation)
Wątek nigdy nie dostaje zasobu, bo inni ciągle go wyprzedzają. Nie jest deadlockiem (inni się wykonują). Przykład: 10 wątków, wątek niskopriorytetowy nigdy nie dostaje CPU bo wysoko priorytetowe ciągle dominują.
Rozwiązanie: aging (starzenie) — priorytet rośnie z czasem oczekiwania.
Po 100 ms bez CPU: priorytet +1, po 200 ms: +2, itd.
W końcu nawet najniższy wątek dostanie CPU.
#### Problem 4 — Inwersja priorytetów (Priority Inversion)
Wątek wysokopriorytetowy (H) czeka na mutex trzymany przez niskopriorytetowy (L), a średniopriorytetowy (M) blokuje L. Efekt: H czeka na M (mimo wyższego priorytetu!).
Priorytet: H > M > L
─────────────────────────────────────────────────
Czas L M H
─────────────────────────────────────────────────
t1 lock(mutex)
t2 (gotowy, wypycha L!)
t3 pracuje... (czeka na mutex!)
t4 pracuje... (CZEKA — bo M blokuje L)
t5 gotowy
t6 unlock(mutex) (wreszcie!)
─────────────────────────────────────────────────
H czekał, dopóki M nie skończył, mimo że H > M!
Rozwiązanie: Priority Inheritance Protocol.
L dziedziczy priorytet H (tymczasowo L=H), więc M nie może wypchać L.
Mars Pathfinder (1997) — klasyczny bug priority inversion w kosmosie!
---
### Klasyczne problemy synchronizacji
![Producent-konsument z buforem cyklicznym](img/producer_consumer.png)
#### Producent-Konsument (Bounded Buffer)
n producentów wrzuca elementy do bufora o ograniczonej pojemności, m konsumentów pobiera. Bufor pełny → producent czeka. Bufor pusty → konsument czeka.
Rozwiązanie z semaforami:
─────────────────────────────────────────────────
semaphore mutex = 1; // wyłączny dostęp do bufora
semaphore empty = N; // ile wolnych slotów (początkowo N)
semaphore full = 0; // ile pełnych slotów (początkowo 0)
Producent: Konsument:
P(empty) // czekaj na P(full) // czekaj na
// wolny slot // pełny slot
P(mutex) // wejdź do P(mutex) // wejdź do
// sek. kryt. // sek. kryt.
wstaw(elem) elem = pobierz()
V(mutex) // wyjdź V(mutex) // wyjdź
V(full) // +1 pełny V(empty) // +1 wolny
─────────────────────────────────────────────────
Bufor (N=4):
┌────┬────┬────┬────┐
│ A │ B │ │ │ ← full=2, empty=2
└────┴────┴────┴────┘
↑ ↑
konsument producent
BŁĄD jeśli zamienimy kolejność P(empty) i P(mutex) w producencie:
Producent: P(mutex) → P(empty) ← bufor pełny → czeka z mutexem!
Konsument: P(full) → P(mutex) ← mutex zajęty → DEADLOCK!
#### Czytelnicy-Pisarze (Readers-Writers)
Wielu czytelników może czytać jednocześnie. Pisarz wymaga wyłącznego dostępu (ani czytelnicy, ani inni pisarze).
Rozwiązanie (first readers-writers):
─────────────────────────────────────────────────
int readers = 0;
mutex rw_mutex; // pisarz LUB pierwszy/ostatni czytelnik
mutex count_mutex; // ochrona zmiennej readers
Czytelnik: Pisarz:
lock(count_mutex) lock(rw_mutex)
readers++ // PISZ (wyłączny)
if (readers == 1) unlock(rw_mutex)
lock(rw_mutex) // 1. czytelnik blokuje pisarzy
unlock(count_mutex)
// CZYTAJ (wielu jednocześnie!)
lock(count_mutex)
readers--
if (readers == 0)
unlock(rw_mutex) // ostatni odblokowuje pisarzy
unlock(count_mutex)
─────────────────────────────────────────────────
Problem: pisarze mogą głodować (readers=0 nigdy nie zachodzi).
Rozwiązanie: fairness — kolejka FIFO czytelników i pisarzy.
#### Ucztujący filozofowie (Dining Philosophers)
5 filozofów siedzi przy okrągłym stole, między każdą parą 1 widelec. Filozofowie myślą lub jedzą. Jedzenie wymaga 2 widelców (lewego i prawego). Naiwne rozwiązanie: każdy bierze lewy → prawy → deadlock (wszyscy trzymają lewy, czekają na prawy).
Rozwiązanie — złamanie cyklu:
Filozofowie 0-3: bierz lewy, potem prawy.
Filozof 4: bierz PRAWY, potem lewy. ← łamie circular wait!
Alternatywa: semafor(4) — max 4 filozofów próbuje jeść naraz
→ jeden widelec zawsze wolny → brak deadlocka.
---
### Mechanizmy synchronizacji — porównanie
Mechanizm Opis Kiedy używać
──────────────────────────────────────────────────────────────────
Mutex Zamek: 1 wątek w sekcji Sekcja krytyczna
Semafor(n) Licznik: max n wątków Ograniczone zasoby
Monitor Obiekt z wbudowanym mutex Java synchronized
Cond. Variable wait()/signal() na warunek Producent-konsument
Spinlock Aktywne czekanie (busy-wait) Bardzo krótkie sekcje
RW Lock Wielu czytelników LUB 1 pisarz Bazy danych, cache
Barrier Czekaj aż wszyscy dotrą Obliczenia równoległe
Mutex vs Semafor:
┌────────────────────────────────────────────────────────────┐
│ Mutex = klucz do łazienki (1 osoba) │
│ Semafor(3) = parking na 3 miejsca (3 samochody naraz) │
│ Semafor(1) = mutex (szczególny przypadek) │
└────────────────────────────────────────────────────────────┘
Mutex vs Spinlock:
┌────────────────────────────────────────────────────────────┐
│ Mutex: wątek ZASYPIA gdy czeka → OS go obudzi (koszt ~μs)│
│ Spinlock: wątek KRĘCI się w pętli → marnuje CPU │
│ Spinlock lepszy gdy sekcja < 1 μs (koszt uśpienia > spin)│
│ Mutex lepszy gdy sekcja > 1 μs (nie marnuje CPU) │
└────────────────────────────────────────────────────────────┘
### Etymologia
**Proces** — łac. „processus" = posuwanie się naprzód. **Wątek (Thread)** — metafora nitki wykonania (jak nić Ariadny). **Mutex** — portmanteau MUTual EXclusion. **Semafor** — Dijkstra (1965); od semaforów kolejowych; P() = hol. „proberen" (próbować), V() = hol. „verhogen" (podnosić). **Coffman** — Edward Coffman Jr. et al. (1971): 4 warunki konieczne deadlocka. **Deadlock (zakleszczenie)** — jak zablokowane koła zębate. **IPC** — Inter-Process Communication.
### Jak zapamiętać
- **„Proces = mieszkanie, Wątek = pokój"** — każde mieszkanie ma adres (przestrzeń), pokoje dzielą kuchnię (heap)
- Wątki szybsze bo nie trzeba zmieniać „mieszkania" (TLB flush)
- **4 warunki Coffmana** zakleszczenia: złam jeden → brak deadlocka

View File

@ -8,8 +8,7 @@
**Dane strumieniowe (streaming data)** — ciągły, potencjalnie nieskończony przepływ zdarzeń (events) przychodzących w czasie rzeczywistym. Przykłady: kliknięcia użytkowników, odczyty sensorów IoT, transakcje bankowe, logi serwerów. W odróżnieniu od danych wsadowych (batch): nie możesz „poczekać na wszystkie" — musisz analizować na bieżąco. **Dane strumieniowe (streaming data)** — ciągły, potencjalnie nieskończony przepływ zdarzeń (events) przychodzących w czasie rzeczywistym. Przykłady: kliknięcia użytkowników, odczyty sensorów IoT, transakcje bankowe, logi serwerów. W odróżnieniu od danych wsadowych (batch): nie możesz „poczekać na wszystkie" — musisz analizować na bieżąco.
Batch: [cały zbiór] → analiza → wynik (minuty/godziny) ![Batch vs Streaming](img/q20_batch_vs_streaming.png)
Streaming: event → event → event → ...→ analiza ciągła (ms/sekundy)
**Strumień (stream)** — abstrakcja: nieograniczona (unbounded) sekwencja zdarzeń, każde ze stemplem czasowym. Musisz przetwarzać „w locie" — nie mieścisz wszystkiego w pamięci. **Strumień (stream)** — abstrakcja: nieograniczona (unbounded) sekwencja zdarzeń, każde ze stemplem czasowym. Musisz przetwarzać „w locie" — nie mieścisz wszystkiego w pamięci.
@ -20,10 +19,6 @@
- **Processing Time** — moment GDY system przetwarza zdarzenie (np. o 14:00:07) - **Processing Time** — moment GDY system przetwarza zdarzenie (np. o 14:00:07)
- Różnica wynika z opóźnień sieciowych. Zdarzenia mogą przychodzić **out-of-order** (pozamiejscowe). - Różnica wynika z opóźnień sieciowych. Zdarzenia mogą przychodzić **out-of-order** (pozamiejscowe).
Zdarzenie A (event time 14:00:01) → dociera o 14:00:05
Zdarzenie B (event time 14:00:03) → dociera o 14:00:04
B dociera PRZED A, mimo że A było wcześniej!
**Watermark** — znacznik postępu: „z dużym prawdopodobieństwem nie przyjdą już zdarzenia z event time < W". Pozwala systemowi zdecydować, kiedy zamknąć okno i wyemitować wynik. Zdarzenia po watermarku = „late data" (spóźnione). **Watermark** — znacznik postępu: „z dużym prawdopodobieństwem nie przyjdą już zdarzenia z event time < W". Pozwala systemowi zdecydować, kiedy zamknąć okno i wyemitować wynik. Zdarzenia po watermarku = „late data" (spóźnione).
--- ---
@ -32,19 +27,14 @@
**Tumbling window (okno przerzutne)** — stały rozmiar, rozłączne. Np. „liczba kliknięć co 5 minut". **Tumbling window (okno przerzutne)** — stały rozmiar, rozłączne. Np. „liczba kliknięć co 5 minut".
|---5min---|---5min---|---5min---|
[events A] [events B] [events C] ← 0 nakładania
**Sliding window (okno przesuwne)** — stały rozmiar + krok przesunięcia. Nakładają się. Np. „średnia z 10 min, co 1 min". **Sliding window (okno przesuwne)** — stały rozmiar + krok przesunięcia. Nakładają się. Np. „średnia z 10 min, co 1 min".
|----10min----|
|----10min----|
|----10min----| ← nakładanie
**Session window (okno sesji)** — dynamiczny rozmiar, oparte na aktywności. Nowa sesja po przerwie (gap). Np. „sesja użytkownika: od pierwszego kliknięcia do 30 min nieaktywności". **Session window (okno sesji)** — dynamiczny rozmiar, oparte na aktywności. Nowa sesja po przerwie (gap). Np. „sesja użytkownika: od pierwszego kliknięcia do 30 min nieaktywności".
**Global window** — jedno okno na cały strumień. Trigger decyduje kiedy wyemitować wynik. **Global window** — jedno okno na cały strumień. Trigger decyduje kiedy wyemitować wynik.
![4 typy okien](img/q20_window_types.png)
--- ---
**True streaming vs Micro-batch:** **True streaming vs Micro-batch:**
@ -61,15 +51,10 @@
**Algorytmy strumieniowe (probabilistyczne):** **Algorytmy strumieniowe (probabilistyczne):**
**HyperLogLog** — estymacja liczby unikalnych elementów (cardinality). Zużywa O(1) pamięci (~1.5 KB) niezależnie od liczby elementów. Błąd ~2%. **HyperLogLog** — estymacja liczby unikalnych elementów (cardinality). Zużywa O(1) pamięci (~1.5 KB) niezależnie od liczby elementów. Błąd ~2%. Np. 100 mln unikalnych URL-i → HyperLogLog: ~100 mln ± 2%, pamięć: 1.5 KB zamiast ~800 MB (hash set).
100 mln unikalnych URL-i → HyperLogLog odpowiada "~100 mln ± 2%"
Pamięć: 1.5 KB zamiast ~800 MB (hash set)
**Count-Min Sketch** — estymacja częstości elementów. Macierz d×w z hashami. Gwarantuje overestimates (nigdy nie zaniży). O(1) per query/update. **Count-Min Sketch** — estymacja częstości elementów. Macierz d×w z hashami. Gwarantuje overestimates (nigdy nie zaniży). O(1) per query/update.
"Ile razy pojawił się IP 192.168.1.1?" → CMS: ~4523 (± ε·N)
**Reservoir Sampling** — równomierne próbkowanie k elementów ze strumienia o nieznanym rozmiarze n. Każdy element ma szansę k/n. O(k) pamięci. **Reservoir Sampling** — równomierne próbkowanie k elementów ze strumienia o nieznanym rozmiarze n. Każdy element ma szansę k/n. O(k) pamięci.
**Late data strategies:** **Late data strategies:**
@ -82,7 +67,7 @@
### Rozwiązania analityczne — przegląd ### Rozwiązania analityczne — przegląd
Rozwiązanie analityczne na strumieniu = odpowiedź na pytanie biznesowe w czasie rzeczywistym, gdy dane przychodzą ciągle i nie można ich wszystkich zapamiętać. Trzy filary: **windowing** (jak grupować), **platformy** (gdzie przetwarzać), **algorytmy probabilistyczne** (jak liczyć w O(1) pamięci). Rozwiązanie analityczne na strumieniu = odpowiedź na pytanie biznesowe w czasie rzeczywistym, gdy dane przychodzą ciągle i nie można ich wszystkich zapamiętać. Dwa filary: **windowing** (jak grupować zdarzenia w skończone porcje) i **platformy** (gdzie i jak przetwarzać strumień).
--- ---
@ -90,7 +75,9 @@ Rozwiązanie analityczne na strumieniu = odpowiedź na pytanie biznesowe w czasi
Problem: strumień jest nieskończony, a analiza wymaga skończonej porcji danych. Okno wyodrębnia fragment strumienia do obliczenia agregatu (count, sum, avg, max). Problem: strumień jest nieskończony, a analiza wymaga skończonej porcji danych. Okno wyodrębnia fragment strumienia do obliczenia agregatu (count, sum, avg, max).
**4 typy okien:** **Analogia:** Strumień to rzeka. Nie możesz zmierzyć „wszystkiej wody w rzece", ale możesz nabierać wiadra (okna) i mierzyć każde z nich.
**4 typy okien — TSSG (Tumbling, Sliding, Session, Global):**
| Okno | Rozmiar | Nakładanie | Kiedy użyć | | Okno | Rozmiar | Nakładanie | Kiedy użyć |
|------|---------|------------|------------| |------|---------|------------|------------|
@ -99,27 +86,79 @@ Problem: strumień jest nieskończony, a analiza wymaga skończonej porcji danyc
| **Session** | dynamiczny (gap) | rozłączne per klucz | sesje użytkowników: „aktywność do 30 min przerwy" | | **Session** | dynamiczny (gap) | rozłączne per klucz | sesje użytkowników: „aktywność do 30 min przerwy" |
| **Global** | cały strumień | — | trigger-based: „emituj po N zdarzeniach" | | **Global** | cały strumień | — | trigger-based: „emituj po N zdarzeniach" |
**Przykład — Tumbling window (fraud detection):** ---
Strumień transakcji bankowych, okno = 1 minuta: **Tumbling Window — szczegółowo:**
[14:0014:01] → 3 transakcje z karty X → OK
[14:0114:02] → 47 transakcji z karty X → ALERT! (>10 = podejrzane)
**Przykład — Sliding window (monitoring SLA):** Stały rozmiar, zero nakładania. Każde zdarzenie trafia do DOKŁADNIE jednego okna.
Okno = 5 min, krok = 1 min (nakładanie): **Przykład — fraud detection (tumbling 1 min):**
t=14:05 → avg latency [14:0014:05] = 120ms ✓
t=14:06 → avg latency [14:0114:06] = 340ms ✗ → alert ![Tumbling window — fraud detection](img/q20_tumbling_fraud.png)
**Przykład — raporty sprzedaży (tumbling 1h):**
Sklep internetowy, okno = 1 godzina: [10:00-11:00] zamówienia: 142, suma: 28 400 zł | [11:00-12:00] zamówienia: 89, suma: 17 800 zł | [12:00-13:00] zamówienia: 201, suma: 40 200 zł (szczyt). Dashboard aktualizuje się CO GODZINĘ.
---
**Sliding Window — szczegółowo:**
Stały rozmiar + krok przesunięcia. Okna NAKŁADAJĄ SIĘ → jedno zdarzenie trafia do WIELU okien.
**Dlaczego nakładanie jest przydatne?** Tumbling 5 min: nagły skok dokładnie na granicę okien → podzielony na dwa okna, żaden nie pokaże pełnego obrazu. Sliding 5 min / 1 min: okna co minutę → skok ZAWSZE widoczny w co najmniej jednym pełnym oknie.
**Przykład — monitoring SLA (sliding 5 min, krok 1 min):**
![Sliding window — monitoring SLA](img/q20_sliding_sla.png)
---
**Session Window — szczegółowo:**
Rozmiar DYNAMICZNY, zależy od aktywności. Gap (przerwa) definiuje koniec sesji.
![Session window — sesje użytkowników](img/q20_session_users.png)
**Przykład — analiza zachowań użytkowników (e-commerce, gap = 30 min):**
- Sesja Anny: strona główna → kategoria → produkt → koszyk → ZAKUP (22 min, 5 stron, konwersja: TAK)
- Sesja Boba: strona główna → kategoria → wychodzi (3 min, 2 strony, konwersja: NIE)
- Metryka: avg session duration, pages/session, conversion rate
---
**Global Window:**
Jedno okno na cały strumień. Potrzebujesz **triggera** aby wyemitować wynik (np. „emituj po każdych 1000 zdarzeń" lub „emituj co 1 minutę" → de facto tumbling na processing time).
---
**Event Time vs Processing Time + Watermark:**
![Event Time vs Processing Time + Watermark](img/q20_event_vs_processing_time.png)
**Event Time vs Processing Time:**
- Okna na **event time** = poprawne biznesowo (kiedy zdarzenie faktycznie nastąpiło) - Okna na **event time** = poprawne biznesowo (kiedy zdarzenie faktycznie nastąpiło)
- Okna na **processing time** = prostsze, ale podatne na out-of-order delivery - Okna na **processing time** = prostsze, ale podatne na out-of-order delivery
- **Watermark** rozwiązuje problem: „z prawdopodobieństwem ~100% nie przyjdą zdarzenia z event time < W" - **Watermark** rozwiązuje problem: „z prawdopodobieństwem ~100% nie przyjdą zdarzenia z event time < W"
**Watermark krok po kroku:** e1 (event=14:00:01, arrives=14:00:02) → watermark=14:00:01. e2 (event=14:00:03, arrives=14:00:03) → watermark=14:00:03. e3 (event=14:00:02, arrives=14:00:04) → LATE! (event < watermark). Okno [14:00:00-14:00:05]: watermark przeszedł 14:00:05 okno ZAMKNIĘTE. Strategia: allowed lateness +2s jeszcze przyjmij, albo drop/side output.
**Mnemonik okien — „TSSG jak Termometr/Suwak/Sesja/Glob":** T = Tumbling = Termometr (stały odczyt co X minut, ZERO nakładania). S = Sliding = Suwak (przesuwasz suwak = nakładanie). S = Session = Sesja (zależy od aktywności użytkownika). G = Global = Glob (jedna wielka kula = cały strumień). Albo: „TeSt SG" — Testujesz Strumieniowe Grupowanie.
---
**Late data — 4 strategie (mnemonik „DRAS"):**
![Late data — 4 strategie DRAS](img/q20_late_data_strategies.png)
--- ---
### Rozwiązanie 2 — Platformy przetwarzania strumieniowego ### Rozwiązanie 2 — Platformy przetwarzania strumieniowego
**Analogia:** Masz fabrykę (dane strumieniowe). Potrzebujesz maszyny do ich przetwarzania. Trzy główne maszyny na rynku — każda z inną filozofią.
![Ekosystem streamingu](img/q20_streaming_ecosystem.png)
| Cecha | Kafka Streams | Apache Flink | Spark Streaming | | Cecha | Kafka Streams | Apache Flink | Spark Streaming |
|-------|---------------|--------------|-----------------| |-------|---------------|--------------|-----------------|
| **Model** | event-by-event | event-by-event | micro-batch (~100ms) | | **Model** | event-by-event | event-by-event | micro-batch (~100ms) |
@ -128,106 +167,107 @@ Problem: strumień jest nieskończony, a analiza wymaga skończonej porcji danyc
| **Exactly-once** | tak (Kafka TXN) | tak (checkpointing) | tak (WAL) | | **Exactly-once** | tak (Kafka TXN) | tak (checkpointing) | tak (WAL) |
| **State management** | RocksDB local | RocksDB + checkpoints | in-memory/external | | **State management** | RocksDB local | RocksDB + checkpoints | in-memory/external |
| **Okna** | tumbling, sliding, session | wszystkie + custom | tumbling, sliding | | **Okna** | tumbling, sliding, session | wszystkie + custom | tumbling, sliding |
| **Skalowalność** | liczba partycji Kafka | auto-scaling klaster | klaster Spark |
| **Use case** | transformacja Kafka → Kafka | złożona analityka real-time | ETL z ekosystemem Spark | | **Use case** | transformacja Kafka → Kafka | złożona analityka real-time | ETL z ekosystemem Spark |
**True streaming vs Micro-batch — co wybrać?** ---
True streaming (Flink, Kafka Streams): **True streaming vs Micro-batch — wizualnie:**
Latencja: < 10 ms trade fraud, click tracking
Semantyka: event-by-event
Złożoność: wyższa (watermarks, state, exactly-once)
Micro-batch (Spark Streaming): ![True Streaming vs Micro-batch](img/q20_true_vs_microbatch.png)
Latencja: ~100 ms sekundy
Semantyka: mini-batch (prostsza, batch-like API)
Ekosystem: Spark SQL, MLlib → łatwa integracja z ML
**Architektura Lambda vs Kappa:** **Kiedy co wybrać? (drzewo decyzyjne)**
Lambda: [batch layer (Spark)] + [speed layer (Flink)] → merge ![Drzewo decyzyjne wyboru platformy](img/q20_decision_tree.png)
Dwa systemy, dwa kody — skomplikowane ale pewne
Kappa: [streaming only (Flink/Kafka)] → replay z Kafka
Jeden system — prostsze, ale replay = I/O koszt
--- ---
### Rozwiązanie 3 — Algorytmy probabilistyczne (Sketches) **Kafka Streams — architektura (library, nie klaster!):**
Problem: na strumieniu nie zmieścisz WSZYSTKICH danych w pamięci. Algorytmy probabilistyczne dają przybliżone odpowiedzi w **O(1) pamięci** z gwarantowanym błędem. ![Kafka Streams — architektura](img/q20_kafka_streams_arch.png)
Kluczowe: NIE potrzebujesz osobnego klastra!
Skalujesz = dodajesz instancje swojej aplikacji.
Partycje Kafki automatycznie rozdzielane między instancje.
| Algorytm | Pytanie | Pamięć | Błąd | Przykład | **Przykład — Kafka Streams (zliczanie kliknięć co 5 min):**
|----------|---------|--------|------|----------|
| **HyperLogLog** | „Ile unikalnych?" | ~1.5 KB | ~2% | unique visitors na stronie |
| **Count-Min Sketch** | „Ile razy element X?" | d×w counters | ε·N (overestimate) | częstość IP w logach |
| **Bloom Filter** | „Czy element X był?" | m bitów | false positives, 0 false neg | cache: „czy URL widziany?" |
| **Reservoir Sampling** | „Losowa próbka k z n?" | O(k) | dokładna (nie przybliżona) | próbka logów do debugowania |
| **T-Digest** | „Jaki percentyl?" | O(δ) | <1% na ogonach | p99 latency monitorowanie |
**Dlaczego HyperLogLog zużywa O(1)?** StreamsBuilder builder = new StreamsBuilder();
Idea: hashuj każdy element, licz pozycję pierwszego bitu 1. builder.stream("clicks") // input topic
Jeśli widzisz dużo zer na początku → prawdopodobnie dużo unikalnych. .groupByKey() // grupuj po user_id
.windowedBy(TimeWindows.of(Duration.ofMinutes(5))) // tumbling 5 min
100 mln unikalnych URL-i: .count() // zlicz
- HashSet: ~800 MB pamięci (8 bajtów × 10⁸) .toStream()
- HyperLogLog: 1.5 KB pamięci, odpowiedź ~100 mln ± 2% .to("click-counts"); // output topic
- Oszczędność: 500 000× mniej pamięci!
// Deploy = uruchom JAR. Skalujesz = uruchom więcej JARów.
**Count-Min Sketch — jak działa:**
Macierz d wierszy × w kolumn (np. 5 × 2048), d funkcji hashowych.
Insert("X"): dla każdego hash h_i, zwiększ cell[i][h_i("X")]++
Query("X"): min over i of cell[i][h_i("X")]
Gwarancja: nigdy nie ZANIŻY (overestimate, no underestimate)
--- ---
### Rozwiązanie 4 — Obsługa opóźnień i spójność **Apache Flink — architektura (klaster):**
**Problem late data:** zdarzenie z event time 14:00:01 przychodzi o 14:00:30, gdy okno [14:0014:05] już zamknięte. ![Apache Flink — architektura](img/q20_flink_arch.png)
| Strategia | Opis | Trade-off | **Exactly-once w Flinku (algorytm Chandy-Lamport):**
|-----------|------|-----------|
| **Drop** | odrzuć spóźnione | proste, ale utrata danych |
| **Allowed lateness** | czekaj dodatkowy czas (np. +5 min) | wyższe zużycie pamięci |
| **Recompute** | przelicz okno z nowym zdarzeniem | poprawne ale kosztowne |
| **Side output** | przekieruj late events do osobnego strumienia | elastyczne, ręczna analiza |
**Exactly-once semantics** — gwarancja, że każde zdarzenie wpływa na wynik dokładnie raz, mimo awarii: Exactly-once w Flinku działa algorytmem Chandy-Lamport: Job Manager co N ms wysyła „barrier" przez graf przetwarzania. Gdy operator otrzyma barrier → zapisuje stan do checkpointu (HDFS/S3). Po awarii → odtworzenie z ostatniego checkpointu.
- **At-most-once** — mogą zginąć (szybkie, proste)
- **At-least-once** — mogą się zduplikować (retry)
- **Exactly-once** — żadnych duplikatów ani strat (checkpoint + transakcje, kosztowne)
Flink: distributed snapshots (algorytm Chandy-Lamport) → checkpoint co N ms **Przykład — Flink (średnia temperatura sensorów co 10s):**
Kafka Streams: transakcje Kafka (idempotent producer + TX coordinator)
Spark: WAL (Write-Ahead Log) + idempotent sinks DataStream<SensorReading> readings = env.addSource(kafkaConsumer);
readings
.keyBy(r -> r.sensorId) // grupuj po sensorze
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.aggregate(new AvgTemperature()) // średnia w oknie
.addSink(new AlertSink()); // sink: alert jeśli > 50°C
// Flink obsługuje event time natywnie → poprawne wyniki
// mimo out-of-order zdarzeń z sensorów
--- ---
### Rozwiązanie 5 — CEP (Complex Event Processing) **Spark Streaming — architektura (micro-batch):**
Wykrywanie złożonych wzorców w strumieniach zdarzeń. Reguły definiowane deklaratywnie. ![Spark Streaming — architektura](img/q20_spark_streaming_arch.png)
Pattern: "Jeśli 3 nieudane logowania z tego samego IP w ciągu 5 minut, ZALETA: ten sam kod co batch Spark → łatwa migracja. WADA: latencja = rozmiar micro-batcha (min ~100ms).
a potem udane logowanie z INNEGO IP → alert: konto przejęte"
Flink CEP: ---
Pattern.<LoginEvent>begin("fails")
.where(event -> !event.isSuccess())
.times(3).within(Time.minutes(5))
.next("success")
.where(event -> event.isSuccess())
Zastosowania: fraud detection, cybersecurity, monitoring IoT, trading algorytmiczny. **Architektura Lambda vs Kappa — wizualnie:**
![Lambda vs Kappa — architektura](img/q20_lambda_vs_kappa.png)
Lambda: 2 systemy, 2 bazy kodu, 2× utrzymanie. Kappa: 1 system, 1 kod. Potrzebujesz przeliczyć historię? → Replay z Kafka od początku. WADA: replay = dużo I/O, Kafka musi trzymać historię.
**Porównanie Lambda vs Kappa:**
![Lambda vs Kappa — porównanie](img/q20_lambda_kappa_table.png)
---
**Exactly-once — porównanie mechanizmów:**
![Exactly-Once — mechanizmy](img/q20_exactly_once.png)
**Mnemonik platform — „KFS: Kawa, Filiżanka, Szklanka":** K = Kafka Streams = Kawa (szybka, w filiżance = w Twojej JVM), F = Flink = Filiżanka (elegancka, pełna = true streaming + state), S = Spark = Szklanka (duża, paczka = micro-batch). Albo: „Kafka = lekki (library), Flink = fastest, Spark = safest (batch-like)".
---
### Etymologia ### Etymologia
**Flink** — niem. „flink" = zwinny/szybki (TU Berlin, 2014). **Spark** — „iskra"; Matei Zaharia (UC Berkeley, 2012). **HyperLogLog** — Philippe Flajolet et al. (2007); „Hyper" = ulepszenie LogLog; „LogLog" = zużywa log(log(n)) pamięci. **Count-Min Sketch** — Cormode & Muthukrishnan (2005); „sketch" = probabilistyczny skrót danych. **Reservoir Sampling** — Jeffrey Vitter (1985); „reservoir" = stały zbiornik prób. **Watermark** — znacznik postępu czasu zdarzeń w strumieniu. **Flink** — niem. „flink" = zwinny/szybki (TU Berlin, 2014). **Spark** — „iskra"; Matei Zaharia (UC Berkeley, 2012). **Kafka** — Franz Kafka; Jay Kreps (LinkedIn, 2011) — „system zoptymalizowany do pisania" (jak Kafka pisarz). **Watermark** — „znak wodny" — znacznik postępu czasu zdarzeń w strumieniu, analogicznie do znaku wodnego na papierze: niewidoczny, ale wyznacza granicę. **Tumbling** — ang. „koziołkowanie" — okna „przewracają się" jedno po drugim bez przerwy. **Sliding** — ang. „przesuwanie" — okno przesuwa się po osi czasu. **Lambda/Kappa** — litery greckie; Lambda = dwie ścieżki (λ ma dwie nóżki), Kappa = jedna ścieżka (κ prostsza).
### Jak zapamiętać ### Jak zapamiętać
- **4 okna: „TSSG"** — Tumbling, Sliding, Session, Global - **4 okna: „TSSG"** — Tumbling (stały, rozłączne), Sliding (stały+krok, nakładające), Session (gap), Global (trigger)
- **Flink = szybki (true streaming)**, Spark = safe (micro-batch) - **TSSG = „TeSt SG — Testujesz Strumieniowe Grupowanie"**
- **HyperLogLog = „ile unikalnych?" z kilobajtem pamięci** - **Tumbling = Termometr** (odczyt co X, zero nakładania), **Sliding = Suwak** (przesuwasz = nakładanie)
- **Late data: „DRAS"** — Drop, Recompute, Allowed lateness, Side output
- **Platformy: „KFS"** — Kafka Streams (library, lekki), Flink (fastest, true streaming), Spark (safest, micro-batch)
- **Lambda = λ = 2 nóżki = 2 ścieżki** (batch + speed), **Kappa = κ = 1 ścieżka** (streaming only)
- **Event time vs processing time** — „kiedy NAPRAWDĘ się stało" vs „kiedy system to ZOBACZYŁ"
- **Watermark** — „linia w piasku" = poniżej niej = już na pewno dotarło

View File

@ -14,11 +14,215 @@
**Bounding box (prostokąt ograniczający, bbox)** — prostokąt opisujący położenie obiektu. Zwykle: (x_min, y_min, x_max, y_max) lub (x_center, y_center, width, height). Przybliżenie — obiekty rzadko są prostokątne. **Bounding box (prostokąt ograniczający, bbox)** — prostokąt opisujący położenie obiektu. Zwykle: (x_min, y_min, x_max, y_max) lub (x_center, y_center, width, height). Przybliżenie — obiekty rzadko są prostokątne.
**Confidence (pewność)** — wynik 0-1 mówiący jak pewny jest detektor, że wykrył obiekt danej klasy. Zwykle próg np. 0.5: detiekcje poniżej odrzucane. **Confidence (pewność)** — wynik 0-1 mówiący jak pewny jest detektor, że wykrył obiekt danej klasy. Zwykle próg np. 0.5: detekcje poniżej odrzucane.
--- ---
**Klasyfikator (classifier)** — model przypisujący etykietę do wejścia. Np. CNN trenowany na ImageNet: obraz → „kot" (+ prawdopodobieństwo). SAM nie lokalizuje — mówi tylko co jest na obrazie. Pytanie brzmi: jak z takiego modelu zbudować detektor? **CNN (Convolutional Neural Network, konwolucyjna sieć neuronowa)** — typ sieci neuronowej zaprojektowany specjalnie do przetwarzania OBRAZÓW. Używany w KAŻDYM nowoczesnym detektorze (R-CNN, YOLO, SSD, DETR). Kluczowa idea: zamiast łączyć KAŻDY piksel z KAŻDYM neuronem (→ miliardy parametrów), CNN używa MAŁYCH filtrów (np. 3×3 piksele) przesuwanych po obrazie. Dzięki temu:
1. Mało parametrów (filtr 3×3 = 9 wag, niezależnie od rozmiaru obrazu)
2. Wykrywa lokalne wzorce (krawędzie, rogi, tekstury)
3. Inwariantność na przesunięcie (kot w lewym rogu = kot w prawym rogu)
Dlaczego CNN a nie zwykła sieć neuronowa?
Obraz 224×224×3 = 150 528 pikseli.
Zwykła sieć (FC): 150 528 × 4096 neuronów = 616 MILIONÓW wag w 1 warstwie!
CNN: filtr 3×3×3 = 27 wag, przesuwany po CAŁYM obrazie → 27 wag zamiast 616M!
Mnemonik: CNN = „Czytaj Nie Naraz" — nie bierzesz całego obrazu naraz,
tylko małe fragmenty (filtry 3×3), krok po kroku.
**Konwolucja (convolution)** — podstawowa operacja CNN: mały filtr (macierz np. 3×3) przesuwa się po obrazie, w każdej pozycji mnoży element-po-elemencie z fragmentem obrazu i sumuje → jedna liczba na wyjściu. Wynik = „feature mapa" — mapa pokazująca GDZIE na obrazie dany wzorzec jest obecny.
Przykład liczbowy:
Fragment obrazu 3×3: Filtr 3×3: Wynik (1 piksel feature mapy):
[1 2 3] [-1 0 1]
[4 5 6] × [-1 0 1] = 1(-1)+2(0)+3(1)+4(-1)+5(0)+6(1)+7(-1)+8(0)+9(1)
[7 8 9] [-1 0 1] = (-1+0+3) + (-4+0+6) + (-7+0+9) = 6
Ten filtr wykrywa PIONOWE KRAWĘDZIE (liczy różnicę prawa-lewa strona).
Duży wynik (6) = silna krawędź. Wynik ≈ 0 = brak krawędzi.
Filtr przesuwa się po CAŁYM obrazie → cała mapa cech.
Pseudokod konwolucji:
def convolve(image, filter_3x3):
output = zeros(image.height - 2, image.width - 2)
for y in range(1, image.height - 1):
for x in range(1, image.width - 1):
patch = image[y-1:y+2, x-1:x+2] # wycinek 3×3
output[y-1][x-1] = sum(patch * filter) # iloczyn + suma
return output
**Filtr / Kernel** — mała macierz wag (np. 3×3, 5×5) uczona AUTOMATYCZNIE podczas treningu. CNN ma WIELE filtrów — każdy uczy się wykrywać INNY wzorzec. 64 filtry w jednej warstwie → 64 map cech.
KLUCZOWA RÓŻNICA: w HOG cechy projektuje CZŁOWIEK.
W CNN filtry uczy się SIEĆ SAMA — to główna przewaga deep learning!
Warstwa conv z 64 filtrami 3×3:
Filtr 1: nauczył się wykrywać pionowe krawędzie
Filtr 2: nauczył się wykrywać poziome krawędzie
Filtr 3: nauczył się wykrywać rogi
...
Filtr 64: jakiś inny wzorzec pomocny w rozpoznawaniu
**Feature map (mapa cech)** — wynik zastosowania JEDNEGO filtra do obrazu. Jasne piksele = „tu jest ten wzorzec". 64 filtry → 64 map cech → tensor [H × W × 64]. Feature mapy to WEWNĘTRZNA REPREZENTACJA tego, co sieć „widzi" na obrazie.
Hierarchia cech w CNN (każda warstwa coraz bardziej abstrakcyjna):
Warstwa 1: krawędzie, gradienty (jak HOG!)
Warstwa 2: rogi, proste tekstury
Warstwa 3: fragmenty obiektów (oko, koło, ucho)
Warstwa 4+: całe obiekty (twarz = oczy+nos+usta, samochód = koła+okna+dach)
Mnemonik: „K-R-F-O" = „Każdy Rycerz Znajduje Obiekt"
(Krawędzie → Rogi → Fragmenty → Obiekty)
**Pooling (łączenie / podpróbkowanie)** — warstwa ZMNIEJSZAJĄCA rozmiar feature mapy. Najczęstsza: **max pooling 2×2** — z każdego bloku 2×2 pikseli zachowaj MAKSIMUM. Wynik: mapa 2× mniejsza w każdym wymiarze (= 4× mniej pikseli), ale zachowuje najsilniejsze cechy.
Feature map 4×4: Po Max Pool 2×2:
[1 3 | 2 1] [3 2] ← max(1,3,0,3)=3 max(2,1,1,2)=2
[0 3 | 1 2] [4 3] ← max(0,4,1,2)=4 max(1,0,3,1)=3
─────────────
[0 4 | 1 0] Rozmiar: 4×4 → 2×2 (4× mniej danych!)
[1 2 | 3 1] Zachowane: najsilniejsze cechy z każdego bloku
Dlaczego max pooling?
1. Mniej pikseli = mniej obliczeń w następnych warstwach
2. Większe „pole widzenia" (receptive field) — warstwa „widzi" większy fragment
3. Odporność na małe przesunięcia: obiekt ±1px → ten sam max
**Stride (krok)** — o ile pikseli filtr przesuwa się za jednym krokiem. Stride=1: co 1 piksel (wyjście duże). Stride=2: co 2 piksele (wyjście 2× mniejsze). Max pool 2×2 ze stride 2 = typowy pooling.
**FC (Fully Connected layer, warstwa w pełni połączona)** — warstwa, w której KAŻDY neuron jest połączony z KAŻDYM wyjściem poprzedniej warstwy. W CNN zwykle na KOŃCU sieci: feature mapy (3D) → spłaszczone do wektora 1D → FC klasyfikuje.
CNN: Conv → Pool → Conv → Pool → [Flatten] → FC(4096) → FC(1000) → "kot"
↑ ↑
spłaszcz 3D→1D 1000 klas (ImageNet)
FC = „warstwa decyzyjna" — łączy cechy z CAŁEGO obrazu w jedną decyzję.
Mnemonik: FC = „Full Connection" — każdy z każdym, jak klasa każdy-z-każdym.
Problem FC: DUŻO parametrów (np. 25088 × 4096 = 102M wag w VGG-16!)
**Forward pass (przejście w przód)** — JEDNO przetworzenie danych przez sieć od wejścia do wyjścia. Obraz wchodzi → przechodzi przez Conv, Pool, FC → wychodzi predykcja. Nie aktualizuje wag (to backward pass / backpropagation = uczenie).
Forward pass CNN (czasy na GPU):
Jeden obraz przez ResNet-50: ~5ms
R-CNN: 2000 regionów × 5ms = 10 SEKUND (dlatego był wolny!)
Fast R-CNN: 1 forward pass cały obraz + ROI Pool = ~200ms (50× szybciej!)
**ReLU (Rectified Linear Unit)** — funkcja aktywacji: f(x) = max(0, x). Przepuszcza wartości dodatnie, zeruje ujemne. Standard w CNN — stosowana PO KAŻDEJ warstwie konwolucyjnej.
Wejście: [-3, 5, -1, 2, 0, -7, 4]
ReLU: [ 0, 5, 0, 2, 0, 0, 4]
Dlaczego potrzebna? Bez ReLU sieć = seria mnożeń macierzy = JEDNA liniowa
transformacja → nie potrafi uchwycić złożonych wzorców.
ReLU dodaje NIELINIOWOŚĆ → sieć aproksymuje DOWOLNĄ funkcję.
**Softmax** — funkcja na WYJŚCIU klasyfikatora: zamienia surowe wyniki (logits) na prawdopodobieństwa sumujące się do 1.
Logits: [2.0, 1.0, 0.1]
Softmax: [0.66, 0.24, 0.10] ← e^2.0 / (e^2.0 + e^1.0 + e^0.1) ≈ 0.66
Klasy: ["kot", "pies", "ryba"]
→ „66% szans, że to kot"
**Tensor** — wielowymiarowa tablica liczb. Uogólnienie wektora i macierzy.
Skalar = 0D tensor: 5
Wektor = 1D: [1, 2, 3]
Macierz = 2D: [[1,2],[3,4]]
Obraz RGB = 3D: [224 × 224 × 3] ← wysokość × szerokość × kanały
Batch obrazów = 4D: [32 × 224 × 224 × 3] ← 32 obrazy naraz
Wyjście YOLO = 3D: [7 × 7 × 30] ← siatka × predykcje
**Architektura CNN — pełny przykład (AlexNet, wygrał ImageNet 2012):**
Obraz [224×224×3] ← 150 528 wartości (piksele RGB)
↓ Conv1: 96 filtrów 11×11, stride 4
[55×55×96] ← 96 map cech, każda 55×55
↓ MaxPool 3×3, stride 2
[27×27×96]
↓ Conv2: 256 filtrów 5×5
[27×27×256]
↓ MaxPool → Conv3-5 → MaxPool
[6×6×256] = 9 216 liczb spłaszczonych
↓ FC(4096) → FC(4096) → FC(1000) → Softmax
→ "golden retriever" (klasa 207, pewność 0.89)
ROZMIARY MALEJĄ: 224 → 55 → 27 → 13 → 6 (kompresja przestrzenna)
KANAŁY ROSNĄ: 3 → 96 → 256 → 384 → 256 (coraz więcej wyuczonych cech)
---
**Backbone (kręgosłup / sieć bazowa)** — duża, pretrenowana sieć CNN (np. ResNet-50, VGG-16) używana jako „ekstraktor cech". Backbone przetwarza obraz → feature mapa. Na wierzch dodaje się GŁOWICĘ (head) specyficzną dla zadania.
Analogia: backbone = SILNIK samochodu, head = KAROSERIA.
Ten sam silnik (ResNet) w różnych karoseriach:
Sedan → klasyfikacja: FC head → "kot"
SUV → detekcja: RPN + ROI Pool head → bbox + klasa
Pickup → segmentacja: dekoder head → maska pikseli
Backbone PRETRENOWANY na ImageNet (miliony obrazów).
Head TRENOWANY od zera na konkretnym zadaniu (detekcja, segmentacja).
**Detection head (głowa detekcyjna)** — warstwy dodane NA WIERZCH backbone'u. Predykują klasy obiektów + pozycje bbox. W Faster R-CNN: RPN + ROI Pool + FC. W YOLO: warstwy conv + wyjście S×S×(B×5+C).
**ResNet, VGG, AlexNet — popularne backbone'y:**
Sieć Rok Warstw Parametrów Top-5 ImageNet Innowacja
─────────────────────────────────────────────────────────────────────
AlexNet 2012 8 60M 84.7% Pierwsza głęboka CNN
VGG-16 2014 16 138M 92.7% Małe filtry 3×3
ResNet-50 2015 50 25M 96.4% Skip connections
Mnemonik: A → V → R = „Architektura Bardzo Rezylientna" (2012 → 2014 → 2015)
Skip connection (ResNet): y = F(x) + x
Wejście bloku DODAWANE do wyjścia → gradient nie zanika
→ można trenować 50-152 warstw (bez skip: >20 warstw = DEGRADACJA!)
**ImageNet** — ogromny zbiór danych: 14M obrazów, 1000 klas (pies, samolot, gitara...). Standard pretrenowania w computer vision. ILSVRC (coroczne zawody) — AlexNet wygrał 2012 → rewolucja deep learning.
**Transfer learning (uczenie transferowe)** — weź sieć pretrenowaną na dużym zbiorze (ImageNet), użyj do INNEGO zadania (detekcja, segmentacja). Backbone „wie" jak wyglądają krawędzie i kształty — trzeba tylko nauczyć nowej głowicy.
Krok po kroku:
1. ResNet-50 pretrenowany na ImageNet (1000 klas, miliony obrazów)
2. Odtnij warstwę FC (klasyfikujse 1000 klas ImageNet) ← WYRZUĆ
3. Dodaj nową głowicę detekcji (bbox + 80 klas COCO) ← NOWA
4. Trenuj głowicę na danych detekcyjnych (COCO/VOC)
5. Opcjonalnie: fine-tune = odmroź backbone, ucz z MAŁYM learning rate
Dlaczego działa? Cechy niskiego poziomu (krawędzie, tekstury) SĄ UNIWERSALNE.
Kot, samochód, twarz — wszystko ma krawędzie i tekstury!
**Fine-tuning (dostrajanie)** — forma transfer learning: odmrażasz backbone i uczysz CAŁĄ sieć z MAŁYM learning rate, żeby subtelnie dopasować cechy do nowego zadania.
**COCO (Common Objects in Context)** — benchmark detekcji: 330K obrazów, 80 klas (samochód, osoba, pies...), 1.5M bboxów. Standard oceny detektorów.
**Pascal VOC (Visual Object Classes)** — starszy benchmark: 20 klas. Używany w oryginalnym YOLO i R-CNN.
**mAP (mean Average Precision)** — główna metryka jakości detekcji. Łączy trafność klasy z trafnością lokalizacji.
mAP@0.5: detekcja „trafna" jeśli IoU ≥ 0.5 (≥50% pokrycia z prawdą)
mAP@0.5:0.95: średnia po progach 0.5, 0.55, ..., 0.95 (dużo surowsza)
Faster R-CNN (COCO): mAP ≈ 42%
YOLOv8-X (COCO): mAP ≈ 53%
**End-to-end (od końca do końca)** — cała sieć trenowana jako JEDNOŚĆ, jeden loss, jeden trening. Przeciwieństwo: R-CNN miał ODDZIELNIE Selective Search + CNN + SVM = 3 osobne kroki. Faster R-CNN = end-to-end → komponenty uczą się WSPÓŁPRACOWAĆ → lepsze wyniki.
**FPN (Feature Pyramid Network)** — technika łączenia feature map z RÓŻNYCH warstw backbone'u. Wczesne warstwy (wysoka rozdzielczość) → małe obiekty. Późne warstwy (niska rozdzielczość) → duże obiekty. FPN łączy obie → wykrywa obiekty WSZYSTKICH rozmiarów.
Backbone (ResNet):
Warstwa 1: 56×56 → dużo detali, dobre dla MAŁYCH obiektów
Warstwa 2: 28×28 → średnie obiekty
Warstwa 3: 14×14 → duże obiekty
Warstwa 4: 7×7 → bardzo duże obiekty
FPN: łączy top-down (7×7 → 14×14 → 28×28 → 56×56) + lateral connections
→ predykcje na KAŻDYM poziomie → małe I duże obiekty!
---
**Klasyfikator (classifier)** — model przypisujący etykietę do wejścia. Np. CNN trenowany na ImageNet: obraz → „kot" (+ prawdopodobieństwo). Klasyfikator nie mówi GDZIE jest obiekt — mówi tylko CO jest na obrazie. Pytanie brzmi: jak z takiego modelu zbudować detektor?
**Sliding window (okno przesuwane)** — najprostsza metoda budowy detektora z klasyfikatora: wytnij prostokątny fragment obrazu (wiele rozmiarów, wiele pozycji), każdy fragment sklasyfikuj. Jeśli „pozytywny" → detekcja. Ekstremalnie wolne: tysiące fragmentów × klasyfikacja per fragment. **Sliding window (okno przesuwane)** — najprostsza metoda budowy detektora z klasyfikatora: wytnij prostokątny fragment obrazu (wiele rozmiarów, wiele pozycji), każdy fragment sklasyfikuj. Jeśli „pozytywny" → detekcja. Ekstremalnie wolne: tysiące fragmentów × klasyfikacja per fragment.
@ -118,18 +322,120 @@
3. NMS (Non-Maximum Suppression) → usuń duplikaty 3. NMS (Non-Maximum Suppression) → usuń duplikaty
4. Wynik: lista bounding boxów z detekcjami pieszych 4. Wynik: lista bounding boxów z detekcjami pieszych
**Viola-Jones (2001)** — przełomowy detektor twarzy real-time. Kluczowe innowacje: **Viola-Jones (2001)** — przełomowy detektor twarzy w CZASIE RZECZYWISTYM. Trzy kluczowe innowacje wyjasnione szczegółowo:
- **Haar features** — proste cechy prostokątne (jasne/ciemne regiony)
- **Integral Image** — obliczenie dowolnej sumy prostokąta w O(1)! **Haar features (cechy Haarowe)** — najprostsze cechy obrazowe: prostokąty podzielone na jasną i ciemną część. Wartość cechy = (suma pikseli jasnych) (suma pikseli ciemnych). Proste, ale skuteczne — wykrywają kontrasty typowe dla twarzy.
- **AdaBoost cascade** — kaskada klasyfikatorów: szybkie odrzucenie 99% okien w pierwszych etapach, szczegółowa analiza tylko obiecujących
Przykłady cech Haar:
Krawędź pionowa: Krawędź pozioma: Linia (3 prostokąty):
┌──────┬──────┐ ┌────────────┐ ┌────┬──────┬────┐
│JASNY │CIEMNY│ │ JASNY │ │CIEM│JASNY │CIEM│
│ +Σ₁ │ -Σ₂ │ │ +Σ₁ │ │ -Σ₁│ +Σ₂ │ -Σ₃│
│ │ │ ├────────────┤ │ │ │ │
└──────┴──────┘ │ CIEMNY │ └────┴──────┴────┘
wartość = Σ₁ Σ₂ │ -Σ₂ │ wartość = Σ₂ Σ₁ Σ₃
└────────────┘
Dlaczego działa na TWARZACH?
- Oczy CIEMNIEJSZE niż czoło → cecha "krawędź pozioma" daje dużą wartość
- Nos JAŚNIEJSZY niż policzki → cecha "linia pionowa" daje dużą wartość
- Twarz = charakterystyczna KOMBINACJA takich kontrastów!
Ile cech? W oknie 24×24 pikseli: ponad 160 000 możliwych cech Haar
(różne rozmiary × różne pozycje). AdaBoost wybiera ~200 NAJLEPSZYCH.
**Integral Image (obraz całkowy)** — precomputed tabela pozwalająca obliczyć sumę pikseli w DOWOLNYM prostokącie w O(1) — stały czas, niezależnie od rozmiaru! To dlatego Haar features liczą się tak szybko.
Jak? Integral Image[x,y] = suma WSZYSTKICH pikseli od (0,0) do (x,y).
Obraz oryginalny: Integral Image (sumy kumulatywne):
[1 2 3] [ 1 3 6]
[4 5 6] [ 5 12 21]
[7 8 9] [12 27 45]
Chcemy sumę prostokąta (1,1)-(2,2) = piksele [5,6,8,9] = 28:
Z Integral Image: II[2,2] II[0,2] II[2,0] + II[0,0]
= 45 6 12 + 1 = 28 ✓
Zawsze 4 odczyty z tabeli → O(1)!
Czy prostokąt ma 4 piksele czy 4 MILIONY — czas TEN SAM!
Bez Integral Image: O(w×h) — suma 1000×1000 = milion operacji.
Z Integral Image: O(1) — 4 operacje. ZAWSZE.
Pseudokod:
def integral_image(img):
II = zeros_like(img)
for y in range(H):
for x in range(W):
II[y][x] = img[y][x] + II[y-1][x] + II[y][x-1] - II[y-1][x-1]
return II
def rect_sum(II, x1, y1, x2, y2): # O(1) zawsze!
return II[y2][x2] - II[y1-1][x2] - II[y2][x1-1] + II[y1-1][x1-1]
**AdaBoost (Adaptive Boosting)** — algorytm uczenia maszynowego łączący wiele SŁABYCH klasyfikatorów w jeden SILNY. Słaby = niewiele lepszy od losowego (>50% trafień). AdaBoost iteracyjnie:
1. Trenuj słaby klasyfikator (np. 1 cecha Haar + próg: "czy wartość > 1200?")
2. Sprawdź, które przykłady ŹLE sklasyfikował
3. Nadaj źle sklasyfikowanym WIĘKSZĄ wagę → następny klasyfikator SKUPI się na nich
4. Powtórz 200× → suma ważona 200 słabych klasyfikatorów ≈ silny klasyfikator
Intuicja: jak PANEL EKSPERTÓW, z których każdy zna się na JEDNEJ rzeczy.
Ekspert 1: "czy okolice oczu ciemne?" (trafność 55%)
Ekspert 2: "czy nos jaśniejszy niż policzki?" (trafność 60%)
Ekspert 3: "czy brwi ciemne?" (trafność 53%)
...
200 ekspertów razem → trafność >95%!
Mnemonik: AdaBoost = "ADAptacyjnie BOOSTuj" słabe modele do silnego.
**Cascade (kaskada klasyfikatorów)** — genialna optymalizacja szybkości: zamiast sprawdzać WSZYSTKIE 200 cech na każdym oknie, użyj KASKADY etapów. Każdy etap = prosty klasyfikator, który szybko ODRZUCA "na pewno nie-twarz".
Etap 1: 2 cechy → odrzuca 50% okien (czas: ~1 μs)
Etap 2: 10 cech → odrzuca 80% reszty (czas: ~5 μs)
Etap 3: 25 cech → odrzuca 90% reszty
...
Etap 25: 200 cech → szczegółowa analiza (czas: ~100 μs)
Sliding window: ~500 000 okien do sprawdzenia (różne pozycje × skale)
BEZ kaskady: 500 000 × 200 cech = WOLNO
Z kaskadą: 99% okien odrzuconych w etapach 1-3 (za ~5μs każde!)
Tylko 0.01% dochodzi do etapu 25
→ CAŁY obraz w ~30ms = 30+ fps = REAL-TIME!
Mnemonik: kaskada = "SITO" — coraz drobniejsze oczka,
na początku odpada piach, na końcu zostaje ZŁOTO (twarz).
Pseudokod kaskady:
def cascade_classify(window):
for stage in cascade_stages: # etap 1, 2, ..., 25
score = stage.evaluate(window) # oblicz kilka cech Haar
if score < stage.threshold: # za niski wynik
return "NIE-TWARZ" # SZYBKIE odrzucenie!
return "TWARZ" # przeszło WSZYSTKIE etapy
--- ---
![Ewolucja detektorów: R-CNN → Faster R-CNN → YOLO](img/rcnn_evolution.png) ![Ewolucja detektorów: R-CNN → Faster R-CNN → YOLO](img/rcnn_evolution.png)
**R-CNN family (two-stage detectors)** — dwuetapowe: najpierw generuj propozycje regionów, potem klasyfikuj każdy region. **R-CNN family (two-stage detectors)** — dwuetapowe: najpierw generuj propozycje regionów, potem klasyfikuj każdy region. Nazwa: Region-based CNN.
**Czym jest „region proposal" (propozycja regionu)?** — prostokąt, w którym MOŻE BYĆ obiekt. Zamiast sprawdzać miliony pozycji okna (sliding window), algorytm propozycji generuje ~2000 „obiecujących" prostokątów. Jak? Metoda Selective Search analizuje kolory, tekstury i rozmiary → łączy podobne regiony → generuje kandydatów. **Selective Search (wyszukiwanie selektywne)** — klasyczny algorytm (NIE sieć neuronowa!) generowania propozycji regionów. Zamiast MILIONÓW pozycji okna (sliding window), inteligentnie łączy podobne fragmenty obrazu i proponuje ~2000 prostokątów, w których MOGĄ być obiekty.
Algorytm krok po kroku:
1. Over-segmentation: podziel obraz na ~1000 małych regionów (superpixele)
(na podstawie koloru i tekstury — algorytm Felzenszwalb)
2. Powtarzaj aż zostanie 1 region:
a) Znajdź 2 najbardziej PODOBNE sąsiednie regiony:
- podobny kolor? (histogram kolorów)
- podobna tekstura? (histogram gradientów)
- pasujący rozmiar? (preferuj łączenie MAŁYCH regionów)
b) Połącz je w jeden → zapamiętaj bounding box nowego regionu
3. Zebrane bbox-y ze WSZYSTKICH kroków → ~2000 propozycji
Sliding window: ~500 000 okien → 99.9% to "tło" → marnujesz czas
Selective Search: ~2 000 regionów → ~50% zawiera coś → 250× wydajniej
RPN (Faster R-CNN): ~300 propozycji → sieć neuronowa (najszybciej!)
**Czym jest "region proposal" (propozycja regionu)?** — prostokąt, w którym MOŻE być obiekt. Dużo mniej niż sliding window (2000 zamiast milionów), ale każda propozycja ma WYSOKIE prawdopodobieństwo trafienia obiektu.
**R-CNN (2014, Ross Girshick)** — pierwszy detektor oparty na CNN. Pipeline: **R-CNN (2014, Ross Girshick)** — pierwszy detektor oparty na CNN. Pipeline:
@ -145,26 +451,90 @@
Dlaczego tak wolno? Bo CNN liczy cechy na KAŻDYM wyciętym regionie OSOBNO, Dlaczego tak wolno? Bo CNN liczy cechy na KAŻDYM wyciętym regionie OSOBNO,
choć regiony się częściowo nakładają → redundantne obliczenia choć regiony się częściowo nakładają → redundantne obliczenia
**Fast R-CNN (2015)** — kluczowa optymalizacja: przepuść cały obraz przez CNN RAZ, uzyskaj mapę cech" (feature map). Potem wytnij cechy regionów z tej mapy (ROI Pooling), zamiast odpalać CNN 2000 razy. **Fast R-CNN (2015)** — kluczowa optymalizacja: przepuść cały obraz przez CNN RAZ, uzyskaj "mapę cech" (feature map). Potem wytnij cechy regionów z tej mapy (ROI Pooling), zamiast odpalać CNN 2000 razy.
Dlaczego „ROI Pooling"? ROI = Region of Interest. Regiony mają RÓŻNE rozmiary, **ROI (Region of Interest, region zainteresowania)** — prostokątny fragment feature mapy odpowiadający propozycji regionu na oryginalnym obrazie. Np. Selective Search zaproponował bbox (100,50)-(200,150) na obrazie 800×600 → odpowiadający ROI na feature mapie (po redukcji 16× przez pooling) to mniej więcej (6,3)-(12,9).
ale warstwa FC wymaga stałego. ROI Pooling dzieli region na siatkę np. 7×7
i w każdej komórce bierze MAX → stały rozmiar wyjścia niezależnie od wejścia.
CNN raz na obraz → feature map → ROI Pool 2000 regionów → FC → klasy + bbox **ROI Pooling (pooling regionu zainteresowania)** — operacja zamieniająca ROI o DOWOLNYM rozmiarze na tensor o STAŁYM rozmiarze (np. 7×7). Konieczne, bo warstwa FC wymaga stałego rozmiaru wejścia!
Przyspieszenie: ~2 sec/obraz (vs 50 sec w R-CNN)
**Faster R-CNN (2015)** — ostatni krok: zastąp Selective Search (osobny algorytm) siecią neuronową! **RPN (Region Proposal Network)** — mała sieć przesuwana po feature mapie, która w KAŻDEJ pozycji predykuje: „czy tu jest obiekt?" + proponuje bbox. Wszystko w jednej sieci, end-to-end. Problem: region 1 = 14×10 na feature mapie, region 2 = 8×6 → RÓŻNE!
Warstwa FC wymaga np. 7×7 → STAŁY rozmiar.
Rozwiązanie — ROI Pooling:
1. Weź ROI (np. 14×10) z feature mapy
2. Podziel go na siatkę 7×7 (= 7 wierszy × 7 kolumn)
Każda komórka obejmuje ok. 2×1.4 pikseli feature mapy
3. W każdej komórce weź MAX (jak max pooling)
4. Wynik: tensor 7×7 — STAŁY rozmiar niezależnie od oryginalnego ROI!
Przykład (ROI Pool 2×2 dla prostoty):
ROI na feature mapie [4×4]: Po ROI Pool 2×2:
[1 3 | 2 1] [5 6] ← max(1,3,0,5)=5 max(2,1,1,6)=6
[0 5 | 1 6] [7 9] ← max(0,4,7,2)=7 max(1,0,9,1)=9
─────────────
[0 4 | 1 0]
[7 2 | 9 1]
Kluczowa sztuczka Fast R-CNN:
CNN raz na CAŁY obraz → JEDNA feature mapa → ROI Pool 2000 regionów z TEJ SAMEJ mapy
(zamiast 2000× odpalać CNN jak w R-CNN!)
Przyspieszenie: ~2 sec/obraz (vs 50 sec) → 25× szybciej!
Pseudokod ROI Pooling:
def roi_pool(feature_map, roi_bbox, output_size=7):
roi = feature_map[roi_bbox] # wycinek z feature mapy
h, w = roi.shape
cell_h, cell_w = h // output_size, w // output_size
output = zeros(output_size, output_size)
for i in range(output_size):
for j in range(output_size):
cell = roi[i*cell_h:(i+1)*cell_h, j*cell_w:(j+1)*cell_w]
output[i][j] = max(cell) # max pooling w komórce
return output # stały rozmiar 7×7!
CNN raz → feature map → ROI Pool 2000 regionów → FC → klasy + bbox
**Bbox regression (regresja prostokąta ograniczającego)** — sieć predykuje nie bezpośrednie współrzędne bbox, ale PRZESUNIĘCIA (offsets) od propozycji: Δx, Δy (przesunięcie środka), Δw, Δh (zmiana szerokości/wysokości).
Propozycja (z RPN/Selective Search): (x=100, y=80, w=60, h=90) ← przybliżone
Predykcja regresji: (Δx=+5, Δy=-3, Δw=+10, Δh=+5)
Ostateczny bbox: (x=105, y=77, w=70, h=95) ← dokładniejsze!
Dlaczego offsets a nie współrzędne bezpośrednio?
Łatwiejsze zadanie! Sieć poprawia przybliżony prostokąt O TROCHĘ,
zamiast zgadywać lokalizację od zera.
Mnemonik: bbox regression = "GPS korekta" — masz przybliżoną pozycję,
poprawiasz o parę metrów w prawo i w górę.
**Faster R-CNN (2015)** — ostatni krok ewolucji: zastąp Selective Search (osobny algorytm) siecią neuronową! **RPN (Region Proposal Network)** — mała sieć przesuwana po feature mapie, która w KAŻDEJ pozycji predykuje: "czy tu jest obiekt?" + proponuje bbox. Wszystko w jednej sieci, end-to-end.
Pipeline Faster R-CNN:
Obraz → CNN backbone (np. ResNet) → Feature Map → RPN (proposals) → ROI Pool → FC → klasy + bbox Obraz → CNN backbone (np. ResNet) → Feature Map → RPN (proposals) → ROI Pool → FC → klasy + bbox
RPN szczegóły: RPN krok po kroku:
- W każdym punkcie feature mapy rozważ k=9 „anchor boxes" (3 rozmiary × 3 proporcje) Feature mapa [40×60×256] ← z backbone
- Dla każdego anchora: P(obiekt) + przesunięcie bbox (Δx, Δy, Δw, Δh) ↓ Filtr 3×3 przesuwa się po feature mapie
- Zachowaj ~300 propozycji z najwyższym P(obiekt) → do ROI Pool ↓ W KAŻDEJ pozycji (x,y) rozważ k=9 "anchor boxes":
9 anchorów = 3 rozmiary × 3 proporcje:
┌───┐ ┌─────┐ ┌───────┐ ← 128×128, 256×256, 512×512
│ │ │ │ │ │ × proporcje 1:1, 1:2, 2:1
└───┘ └─────┘ └───────┘
↓ Dla KAŻDEGO z 9 anchorów sieć predykuje:
- P(obiekt) = prawdopodobieństwo, że tu jest obiekt
- (Δx, Δy, Δw, Δh) = przesunięcie bbox względem anchora
40×60 = 2400 pozycji × 9 anchorów = 21 600 potencjalnych propozycji!
→ Weź ~300 z najwyższym P(obiekt) → ROI Pool → FC → klasy + bbox
Faster R-CNN: ~5 fps (~0.2 sec/obraz) — 250× szybciej niż R-CNN! Faster R-CNN: ~5 fps (~0.2 sec/obraz) — 250× szybciej niż R-CNN!
Mnemonik ewolucji R-CNN: "CORAZ MNIEJ MARNOWANIA"
R-CNN: Selective Search + 2000×CNN = 50s → WOLNE
Fast R-CNN: Selective Search + 1×CNN + ROI Pool = 2s → lepiej
Faster R-CNN: RPN (w sieci!) + 1×CNN + ROI Pool = 0.2s → 250× szybciej!
--- ---
**One-stage detectors** — klasyfikacja i lokalizacja w jednym przejściu (bez osobnego etapu propozycji). Szybsze, ale historycznie mniej precyzyjne. **One-stage detectors** — klasyfikacja i lokalizacja w jednym przejściu (bez osobnego etapu propozycji). Szybsze, ale historycznie mniej precyzyjne.
@ -192,27 +562,126 @@ Jedno przejście przez sieć → WSZYSTKIE detekcje naraz. 45-155 fps!
**Anchor-free** — nowoczesne podejście (FCOS, YOLOv8): bezpośrednia predykcja środka i wymiarów, bez predefiniowanych anchorów. Prostsza architektura, mniej hyperparametrów. **Anchor-free** — nowoczesne podejście (FCOS, YOLOv8): bezpośrednia predykcja środka i wymiarów, bez predefiniowanych anchorów. Prostsza architektura, mniej hyperparametrów.
**DETR (DEtection TRansformer, 2020)** — Facebook AI. Zamiast CNN + anchor + NMS, używa **transformera** z mechanizmem self-attention. Predykuje bezpośrednio ZESTAW obiektów (set prediction, nie grid). NIE potrzebuje NMS (unik duplikatów rozwiązany przez Hungarian matching w treningu). Najprostsza architektura w detekcji, ale wolniejsza w treningu. **Transformer** — architektura sieci neuronowej pierwotnie z NLP (2017, "Attention is All You Need"), ale skutecznie zaadaptowana do wizji komputerowej (ViT, DETR). Kluczowy mechanizm: **self-attention** — każdy element wejścia "patrzy" na WSZYSTKIE inne elementy i decyduje, które są dla niego ważne.
W tekście: słowo "bank" patrzy na "rzeka" i "pieniądze" →
attention decyduje: "w tym zdaniu chodzi o brzeg RZEKI, nie bank pieniędzy"
W obrazie (DETR): fragment obrazu "patrzy" na inne fragmenty →
attention: "ta łapa jest częścią TEGO kota, a nie tamtego psa"
**Self-attention (samo-uwaga)** — mechanizm: dla każdego elementu oblicz "uwagę" do KAŻDEGO innego elementu. Matematycznie: Query × Key → wagi attention → ważona suma Values.
Uproszczony pseudokod:
def self_attention(features): # features = N elementów
Q = features × W_query # Query: "czego szukam?"
K = features × W_key # Key: "co oferuję?"
V = features × W_value # Value: "jaką informację niosę?"
attention = softmax(Q × K^T / sqrt(d)) # macierz N×N: "kto ważny dla kogo"
output = attention × V # ważona kombinacja wartości
return output
Złożoność: O(n^2) — każdy element z każdym → wolne dla dużych obrazów.
Dlatego DETR wolniej się TRENUJE niż YOLO (ale architektura jest PROSTSZA).
**DETR (DEtection TRansformer, 2020)** — model Facebooka stosujący Transformer do detekcji. Radykalnie prostszy pipeline: BRAK anchorów, BRAK NMS! Sieć predykuje bezpośrednio ZESTAW N obiektów (np. N=100).
Pipeline DETR:
Obraz → CNN backbone → Feature mapa → Transformer Encoder (self-attention)
→ Transformer Decoder (z N=100 "object queries")
→ N predykcji: [(klasa₁, bbox₁), ..., (klasa₁₀₀, bbox₁₀₀)]
"Object queries" = 100 wyuczonych wektorów, każdy "szuka" jednego obiektu.
Obraz z 5 obiektami → 5 queries dopasuje się do obiektów,
95 queries zwróci klasę "brak obiektu" (empty set).
Pseudokod DETR:
def detr_forward(image):
features = backbone(image) # ResNet → feature mapa
encoded = transformer_encoder(features) # self-attention na feat. mapie
queries = learnable_queries(100) # 100 wyuczonych zapytań
decoded = transformer_decoder(queries, encoded) # cross-attention
predictions = []
for q in decoded:
cls = classify(q) # "samochód" / "pies" / "brak"
box = regress(q) # (x, y, w, h)
predictions.append((cls, box))
return predictions # 100 predykcji (większość = brak)
Mnemonik DETR: "Detekcja Eliminująca Trikowe Redundancje"
→ bez NMS, bez anchorów, prosty pipeline.
**Hungarian matching (dopasowanie węgierskie)** — algorytm używany podczas TRENINGU DETR. Problem: sieć daje 100 predykcji, na obrazie jest 5 obiektów — która predykcja odpowiada któremu obiektowi? Algorytm węgierski znajduje OPTYMALNE dopasowanie 1:1 minimalizując łączny koszt (błąd klasy + błąd bbox).
Predykcje DETR: Ground truth:
pred_1: "samochód" gt_1: "samochód" (bbox A)
pred_2: "pies" gt_2: "pies" (bbox B)
pred_3: "brak"
... Hungarian matching:
pred_100: "brak" pred_1 ↔ gt_1 (najlepsze dopasowanie!)
pred_2 ↔ gt_2
reszta ↔ "brak obiektu"
Efekt: BRAK DUPLIKATÓW → BRAK NMS!
(Każdy obiekt dopasowany do DOKŁADNIE jednej predykcji)
--- ---
**NMS (Non-Maximum Suppression)** — post-processing: detektor generuje wiele nakładających się bbox dla tego samego obiektu. NMS: weź najlepszą (max confidence), usuń wszystkie mocno nakładające się (IoU > prog), powtórz. **NMS (Non-Maximum Suppression, tłumienie nie-maksymalnych)** — algorytm post-processingu usuwający ZDUPLIKOWANE detekcje. Problem: detektor generuje WIELE nakładających się bbox dla tego samego obiektu. NMS zachowuje NAJLEPSZĄ i usuwa resztę. Jedyny detektor BEZ NMS = DETR.
Detections: [bbox1, 0.95], [bbox2, 0.90], [bbox3, 0.85] (nakładające się) Algorytm NMS krok po kroku:
NMS: zachowaj bbox1 (0.95), usuń bbox2 i bbox3 (IoU > 0.5 z bbox1) Wejście: detekcje posortowane malejąco po confidence
[bbox_1 conf=0.95], [bbox_2 conf=0.90], [bbox_3 conf=0.85], [bbox_4 conf=0.40]
**IoU (Intersection over Union)** — miara nakładania dwóch bbox: pole przecięcia / pole sumy. IoU=1 → identyczne; IoU=0 → brak nakładania. Próg NMS typowo 0.5. Pseudokod NMS:
def nms(detections, iou_threshold=0.5):
detections.sort(by=confidence, descending=True)
keep = []
while detections:
best = detections.pop(0) # weź najlepszą
keep.append(best) # ZACHOWAJ ją
detections = [d for d in detections
if iou(best, d) < iou_threshold] # usuń nakładające
return keep
**Backbone** — sieć bazowa (np. ResNet, VGG) wyciągająca cechy z obrazu. Detection head (głowa detekcyjna) jest dodawana „na wierzch" backbone i predykuje bbox + klasy. Fine-tuning backbone na detekcję = transfer learning. Krok 1: Weź bbox_1 (0.95) → ZACHOWAJ
Krok 2: IoU(bbox_1, bbox_2) = 0.82 > 0.5 → USUŃ (duplikat tego samego kota!)
IoU(bbox_1, bbox_3) = 0.75 > 0.5 → USUŃ (duplikat!)
IoU(bbox_1, bbox_4) = 0.10 < 0.5 ZACHOWAJ (INNY obiekt!)
Krok 3: Wynik: [bbox_1, bbox_4] — 2 unikalne obiekty
Mnemonik: NMS = "Najlepszy Ma Się dobrze" — zachowaj najlepszą, usuń resztę.
**IoU (Intersection over Union)** — miara nakładania dwóch prostokątów. IoU = pole przecięcia / pole sumy. Wartości: 0.0 (nie nakładają się) do 1.0 (identyczne).
bbox A: bbox B: Przecięcie:
┌──────────┐ ┌────┐
│ A │ ┌──────────┐ │ ∩ │
│ ┌───┼────────┼──┐ │ └────┘
│ │ ∩ │ │ │ B │
└──────┼───┘ │ │ │
└────────────┴──┘
IoU = pole(∩) / pole(A B)
= pole(∩) / (pole(A) + pole(B) pole(∩))
Przykład liczbowy:
A = [0, 0, 100, 100] → pole = 10 000
B = [50, 50, 150, 150] → pole = 10 000
∩ = [50, 50, 100, 100] → pole = 2 500
IoU = 2500 / (10000 + 10000 2500) = 2500 / 17500 ≈ 0.14
IoU > 0.5 w NMS → "to TEN SAM obiekt" → usuń słabszą detekcję
IoU > 0.5 w mAP → "detekcja TRAFNA" → poprawna lokalizacja
--- ---
**Jak zbudować detektor z klasyfikatora? Trzy podejścia:** **Jak zbudować detektor z klasyfikatora? Trzy podejścia (+ bonus):**
1. **Sliding window** — wytnij, sklasyfikuj, NMS. Bardzo wolne. 1. **Sliding window** — wytnij, sklasyfikuj, NMS. Bardzo wolne (miliony klasyfikacji).
2. **Region proposals + klasyfikator** — Selective Search generuje ~2000 regionów, sklasyfikuj każdy + NMS. Szybsze. 2. **Region proposals + klasyfikator** — Selective Search → ~2000 regionów → klasyfikuj + NMS. Wolne ale działa (= R-CNN).
3. **Fine-tune backbone** — weź pretrained classifier (np. ResNet z ImageNet), dodaj detection head (bbox regression + cls), dotrenuj na danych detekcyjnych. **Najlepsza jakość.** 3. **Fine-tune backbone** — weź pretrained classifier (ResNet z ImageNet), dodaj detection head (bbox regression + cls), dotrenuj na danych detekcyjnych. **Najlepsza jakość** (= Faster R-CNN, YOLO, SSD).
4. **Transformer (DETR)** — bez anchorów, bez NMS, predykcja zestawu obiektów end-to-end.
**DETR (DEtection TRansformer, 2020)** — Facebook AI. Transformer zamiast CNN, bezpośrednia predykcja zestawu obiektów (set prediction), bez NMS. Uproszczona architektura.
--- ---
@ -354,11 +823,26 @@ Masz wytrenowany klasyfikator (np. ResNet na ImageNet: obraz → „kot"). Jak g
### Etymologia ### Etymologia
**YOLO** — You Only Look Once (Joseph Redmon et al., 2016). **R-CNN** — Region-based CNN (Ross Girshick, 2014). **HOG** — Histogram of Oriented Gradients (Dalal & Triggs, 2005). **SVM** — Support Vector Machine (Vapnik, 1995). **Viola-Jones** — Paul Viola + Michael Jones (2001). **DETR** — DEtection TRansformer (Facebook AI, 2020). **SSD** — Single Shot MultiBox Detector (Liu et al., 2016). **NMS** — Non-Maximum Suppression; tłumienie nie-maksymalnych detekcji. **CNN** — Convolutional Neural Network (sieć z konwolucjami). **YOLO** — You Only Look Once (Joseph Redmon et al., 2016). **R-CNN** — Region-based CNN (Ross Girshick, 2014). **HOG** — Histogram of Oriented Gradients (Dalal & Triggs, 2005). **SVM** — Support Vector Machine (Vapnik, 1995). **Viola-Jones** — Paul Viola + Michael Jones (2001). **DETR** — DEtection TRansformer (Facebook AI, 2020). **SSD** — Single Shot MultiBox Detector (Liu et al., 2016). **NMS** — Non-Maximum Suppression; tłumienie nie-maksymalnych detekcji. **ROI** — Region of Interest (region zainteresowania). **RPN** — Region Proposal Network (sieć propozycji regionów). **FPN** — Feature Pyramid Network (piramida cech). **IoU** — Intersection over Union (przecięcie przez sumę). **FC** — Fully Connected (w pełni połączona). **ReLU** — Rectified Linear Unit (wyprostowana jednostka liniowa). **mAP** — mean Average Precision (średnia precyzja).
### Jak zapamiętać ### Jak zapamiętać
- **YOLO = „You Only Look Once"** — jednoetapowy, szybki - **CNN = „Czytaj Nie Naraz"** — małe filtry 3×3 przesuwane po obrazie, nie cały obraz naraz
- **Hierarchia CNN: „K-R-F-O" = „Każdy Rycerz Znajduje Obiekt"** — Krawędzie → Rogi → Fragmenty → Obiekty
- **FC = „Full Connection"** — każdy z każdym, warstwa decyzyjna na końcu CNN
- **Backbone = SILNIK samochodu** — ten sam silnik (ResNet), różne karoserie (klasyfikacja/detekcja/segmentacja)
- **Backbone'y: A→V→R = „Architektura Bardzo Rezylientna"** — AlexNet (2012) → VGG (2014) → ResNet (2015)
- **Transfer learning** — nie ucz się od zera, przenieś wiedzę z ImageNet
- **Viola-Jones: kaskada = „SITO"** — piach odpada wcześnie, złoto (twarz) zostaje na końcu
- **AdaBoost = „ADAptacyjnie BOOSTuj"** — słabe modele razem = silny
- **Integral Image** — 4 odczyty = suma dowolnego prostokąta, zawsze O(1)
- **Selective Search** — inteligentne łączenie regionów zamiast milionów okien
- **ROI Pooling** — dowolny rozmiar → stały rozmiar (siatkowanie + max)
- **Bbox regression = „GPS korekta"** — popraw przybliżoną pozycję o Δx, Δy, Δw, Δh
- **Ewolucja R-CNN: „CORAZ MNIEJ MARNOWANIA"** — R-CNN (50s) → Fast (2s) → Faster (0.2s)
- **YOLO = „You Only Look Once"** — jednoetapowy, szybki, siatka S×S
- **Faster R-CNN = CNN + RPN + ROI Pool** — dwuetapowy, dokładny - **Faster R-CNN = CNN + RPN + ROI Pool** — dwuetapowy, dokładny
- **Detektor z klasyfikatora:** sliding window (wolno) → proposals (lepiej) → fine-tune backbone (najlepiej) - **NMS = „Najlepszy Ma Się dobrze"** — zachowaj najlepszą detekcję, usuń duplikaty
- **DETR = „Detekcja Eliminująca Trikowe Redundancje"** — bez NMS, bez anchorów, transformer
- **Detektor z klasyfikatora:** sliding window (wolno) → proposals (lepiej) → fine-tune backbone (najlepiej) → DETR (najprościej)