Materiały kursów
Materiały / Machine Learning
Pobierz notatnik (.ipynb)

Ćwiczenie 10: Od notatnika do skryptu

Po co to ćwiczenie?

Przez dziewięć ćwiczeń pracowałeś wyłącznie w notatniku. I bardzo dobrze - notatnik jest znakomitym narzędziem do eksploracji: uruchamiasz kawałek kodu, oglądasz wynik, poprawiasz, uruchamiasz jeszcze raz. Do zaglądania w dane i szukania pomysłu nic lepszego nie wymyślono.

Problem zaczyna się wtedy, gdy notatnik przestaje być miejscem szukania rozwiązania, a staje się miejscem, gdzie rozwiązanie mieszka. Wtedy jego zalety zamieniają się w wady:

  • komórki można uruchamiać w dowolnej kolejności - i prawie zawsze ktoś to robi,
  • zmienna może istnieć w pamięci, chociaż komórka, która ją tworzyła, została skasowana - to ukryty stan (ang. hidden state),
  • Git widzi notatnik jako jeden wielki plik JSON razem z wynikami - różnice między wersjami są nieczytelne,
  • „u mnie zadziałało" nie oznacza, że zadziała u kogokolwiek innego, bo nikt nie wie, w jakiej kolejności komórki były uruchamiane.

Efekt końcowy jest zawsze ten sam: model, którego nie da się odtworzyć. Ktoś pyta „skąd wzięła się ta liczba w raporcie?", a Ty nie potrafisz odpowiedzieć.

Dlaczego to ćwiczenie jest dziesiąte, a nie pierwsze. Gdyby zacząć kurs od pisania skryptów i argumentów wiersza poleceń, brzmiałoby to jak zbędna biurokracja. Dopiero po dziewięciu ćwiczeniach - po zgubionych wynikach, NameError po uruchomieniu komórek w złej kolejności i modelach, których nie dało się powtórzyć - widać, na co to jest lekarstwo. Ból musi być pierwszy, lekarstwo drugie.

Czego się nauczysz

  1. Co notatnik robi dobrze, a czego nie powinien robić nigdy.
  2. Czym jest ukryty stan i jak go wykryć jednym kliknięciem.
  3. Jak przenieść wytrenowany model do pliku .py z funkcją main().
  4. Jak sterować skryptem z wiersza poleceń przez argparse, zamiast edytować kod.
  5. Jak zapisać model na dysk (joblib) i wczytać go w zupełnie osobnym procesie.
  6. Czym jest odtwarzalność (ang. reproducibility) i jak sprawdzić, czy faktycznie ją masz.

Zanim zaczniesz: uruchamiaj komórki po kolei (Shift+Enter). Notatnik zakłada, że jesteś w katalogu cwiczenia-ml/.

1. Notatnik kontra skrypt - do czego który

To nie jest konkurs, w którym jedno narzędzie wygrywa. To podział obowiązków.

Notatnik .ipynb Skrypt .py
Do czego służy eksploracja, wykresy, nauka, prezentacja wyników kod, który ma działać powtarzalnie
Kolejność wykonania dowolna - decyduje użytkownik zawsze od góry do dołu
Stan zmiennych żyje w jądrze (ang. kernel) między uruchomieniami świeży przy każdym uruchomieniu procesu
Parametry zmieniasz, edytując kod podajesz z wiersza poleceń
Różnice w Git JSON z wynikami - nieczytelne zwykły tekst - czytelne linia po linii
Uruchomienie automatyczne (np. co noc) trudne i kruche naturalne
Testy jednostkowe praktycznie niewykonalne standard (ćwiczenie 11)

Zdanie, które warto zapamiętać: notatnik jest brudnopisem, skrypt jest czystopisem. Brudnopis jest potrzebny - ale nikt nie oddaje brudnopisu jako pracy końcowej.

2. Ukryty stan - zobacz go na własne oczy

Uruchom poniższą komórkę. Potem uruchom ją jeszcze raz. I jeszcze raz.

In [ ]:
licznik = locals().get('licznik', 0) + 1
print("Ta komórka była uruchomiona razy:", licznik)

Wynik zależy od tego, ile razy kliknąłeś, a nie od treści kodu. To jest ukryty stan w najczystszej postaci: identyczny notatnik u dwóch osób daje dwie różne liczby.

W prawdziwym kodzie ML wygląda to groźniej. Klasyczny scenariusz:

# komórka A (uruchomiona rano)
X = dane.drop(columns=['PatientID', 'Diabetic'])

# komórka B (uruchomiona po południu, po eksperymentach)
X = X.drop(columns=['Age'])     # sprawdzam model bez wieku

# komórka C (uruchomiona wieczorem - "ostateczny model")
model.fit(X, y)

Po restarcie jądra komórka C nauczy model na innych danych niż wieczorem, bo komórka B już nie zadziała w tej samej kolejności. Wynik w raporcie przestaje pasować do kodu w pliku.

Test, który to wykrywa - i zajmuje 10 sekund

W JupyterLab: Kernel → Restart Kernel and Run All Cells...

To jedyny uczciwy sposób sprawdzenia, czy notatnik działa od początku do końca. Zasada na resztę życia zawodowego:

Notatnik, który nie przechodzi „Restart and Run All", jest zepsuty - nawet jeśli wszystkie komórki mają zielone wyniki.

Zrób to teraz z tym notatnikiem. Zwróć uwagę, że licznik powyżej wrócił do 1.

3. Punkt wyjścia: model w notatniku

Zaczynamy od kodu, który znasz z ćwiczenia 01. Nic w nim nowego - to jest właśnie ten „brudnopis", który zaraz przeniesiemy do skryptu.

In [ ]:
# sys.executable to sciezka do TEGO interpretera Pythona, ktory
# obsluguje notatnik. Uzywamy jej zamiast golego 'python', bo na
# PATH moze byc inna instalacja Pythona - bez zainstalowanych
# bibliotek kursu. Wtedy skrypt cicho by padal.
import sys
import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, recall_score

ZIARNO = 42

dane = pd.read_csv('dane/diabetes.csv')
X = dane.drop(columns=['PatientID', 'Diabetic'])
y = dane['Diabetic']

X_ucz, X_test, y_ucz, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=ZIARNO
)

model = DecisionTreeClassifier(max_depth=5, random_state=ZIARNO)
model.fit(X_ucz, y_ucz)

przewidywania = model.predict(X_test)
print(f"Skuteczność: {accuracy_score(y_test, przewidywania):.4f}")
print(f"Czułość:     {recall_score(y_test, przewidywania):.4f}")

Kod działa. I ma trzy wady, których w notatniku nie widać:

  1. Żeby zmienić max_depth, trzeba edytować kod. Po dziesięciu eksperymentach nie wiadomo, która wartość dała który wynik.
  2. Wynik istnieje tylko na ekranie. Zamknij notatnik - i tyle go widzieli.
  3. Model istnieje tylko w pamięci. Restart jądra kasuje godziny liczenia.

Wszystkie trzy naprawimy jednym ruchem: przenosząc kod do skryptu.

4. Skrypt - budowa

Zanim napiszemy plik, ustalmy jego szkielet. Dobry skrypt ML ma cztery części:

Część Po co
funkcje robocze każda robi jedną rzecz i ma jasne wejście i wyjście
parsuj_argumenty() wszystkie parametry z wiersza poleceń, w jednym miejscu
main() scenariusz: wczytaj → podziel → trenuj → oceń → zapisz
if __name__ == "__main__": uruchom main() tylko wtedy, gdy plik jest uruchamiany, a nie importowany

Ostatni punkt jest ważniejszy, niż wygląda. Dzięki niemu ten sam plik można uruchomić z terminala i zaimportować z innego kodu (na przykład z testów - patrz ćwiczenie 11), bez przypadkowego trenowania modelu przy imporcie.

Dlaczego argparse, a nie zmienne na górze pliku

Bo parametry zapisane w kodzie mają brzydki nawyk: zmieniasz je, uruchamiasz, zmieniasz z powrotem - i po tygodniu nie wiesz, z jakimi wartościami powstał model leżący na dysku. Parametr podany w wierszu poleceń zostaje w historii terminala, w logu, w pliku wyników. Jest dowodem.

In [ ]:
from pathlib import Path

# %%writefile nie tworzy katalogow - musimy je zalozyc sami
Path('src').mkdir(exist_ok=True)
Path('wyniki').mkdir(exist_ok=True)
print("Katalogi src/ i wyniki/ gotowe.")

Teraz piszemy plik. Używamy do tego magicznego polecenia %%writefile, które zapisuje całą zawartość komórki do pliku (i nic nie uruchamia).

Uwaga dydaktyczna. %%writefile jest tu użyte tylko po to, żeby cały materiał zmieścił się w jednym notatniku. W prawdziwym projekcie plik .py po prostu leży w repozytorium, otwierasz go w edytorze i trzymasz pod kontrolą Gita. Nikt nie generuje kodu źródłowego z notatnika.

In [ ]:
%%writefile src/trenuj.py
"""Trening klasyfikatora cukrzycy - wersja skryptowa.

Uruchomienie:
    python src/trenuj.py --max-depth 5
"""
import argparse
import json
from pathlib import Path

import joblib
import pandas as pd
from sklearn.metrics import accuracy_score, f1_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

KOLUMNA_ID = "PatientID"
KOLUMNA_ETYKIETY = "Diabetic"


def wczytaj_dane(sciezka):
    """Wczytuje plik CSV i zwraca ramke danych."""
    ramka = pd.read_csv(sciezka)
    print(f"Wczytano {len(ramka)} wierszy z {sciezka}")
    return ramka


def przygotuj_cechy(ramka):
    """Dzieli ramke na cechy X i etykiete y. Identyfikator NIE jest cecha."""
    X = ramka.drop(columns=[KOLUMNA_ID, KOLUMNA_ETYKIETY])
    y = ramka[KOLUMNA_ETYKIETY]
    return X, y


def trenuj_model(X_ucz, y_ucz, max_depth, ziarno):
    """Trenuje drzewo decyzyjne i zwraca wytrenowany model."""
    model = DecisionTreeClassifier(max_depth=max_depth, random_state=ziarno)
    model.fit(X_ucz, y_ucz)
    return model


def ocen_model(model, X_test, y_test):
    """Zwraca slownik metryk - slownik, bo latwo go zapisac do JSON-a."""
    przewidywania = model.predict(X_test)
    return {
        "skutecznosc": float(accuracy_score(y_test, przewidywania)),
        "czulosc": float(recall_score(y_test, przewidywania)),
        "f1": float(f1_score(y_test, przewidywania)),
    }


def parsuj_argumenty():
    """Wszystkie parametry skryptu w jednym miejscu."""
    parser = argparse.ArgumentParser(description="Trening modelu cukrzycy")
    parser.add_argument("--dane", default="dane/diabetes.csv",
                        help="sciezka do pliku CSV z danymi")
    parser.add_argument("--max-depth", type=int, default=5,
                        help="maksymalna glebokosc drzewa")
    parser.add_argument("--ziarno", type=int, default=42,
                        help="ziarno losowosci (ang. random seed)")
    parser.add_argument("--wyjscie", default="wyniki",
                        help="katalog na model i metryki")
    return parser.parse_args()


def main():
    args = parsuj_argumenty()

    ramka = wczytaj_dane(args.dane)
    X, y = przygotuj_cechy(ramka)

    X_ucz, X_test, y_ucz, y_test = train_test_split(
        X, y, test_size=0.2, stratify=y, random_state=args.ziarno
    )

    model = trenuj_model(X_ucz, y_ucz, args.max_depth, args.ziarno)
    metryki = ocen_model(model, X_test, y_test)

    # Zapisujemy razem z parametrami - inaczej metryki sa bezuzyteczne
    metryki["parametry"] = vars(args)
    metryki["liczba_cech"] = int(X.shape[1])
    metryki["liczba_wierszy"] = int(len(ramka))

    katalog = Path(args.wyjscie)
    katalog.mkdir(parents=True, exist_ok=True)

    sciezka_modelu = katalog / f"model_d{args.max_depth}.joblib"
    sciezka_metryk = katalog / f"metryki_d{args.max_depth}.json"

    joblib.dump(model, sciezka_modelu)
    with open(sciezka_metryk, "w", encoding="utf-8") as plik:
        json.dump(metryki, plik, indent=2, ensure_ascii=False)

    print(f"Skutecznosc: {metryki['skutecznosc']:.4f}")
    print(f"Czulosc:     {metryki['czulosc']:.4f}")
    print(f"Model   -> {sciezka_modelu}")
    print(f"Metryki -> {sciezka_metryk}")


if __name__ == "__main__":
    main()

5. Uruchomienie skryptu

Wykrzyknik na początku linii w notatniku oznacza: „uruchom to w powłoce systemowej, a nie w Pythonie". python src/trenuj.py startuje nowy proces - z pustą pamięcią, bez żadnych zmiennych z tego notatnika. To jest dokładnie ta izolacja, o którą nam chodzi.

In [ ]:
!{sys.executable} src/trenuj.py --max-depth 5

Zwróć uwagę, że skryptowi nie przeszkadza, w jakiej kolejności uruchamiałeś komórki powyżej. On nie wie, że ten notatnik istnieje.

Zmiana parametru nie wymaga teraz dotykania kodu:

In [ ]:
!{sys.executable} src/trenuj.py --max-depth 2
!{sys.executable} src/trenuj.py --max-depth 12

A argparse daje w prezencie pomoc - za darmo, z samych definicji argumentów:

In [ ]:
!{sys.executable} src/trenuj.py --help

6. Odtwarzalność - sprawdźmy ją naprawdę

Odtwarzalność (ang. reproducibility) oznacza: ten sam skrypt, uruchomiony z tymi samymi argumentami na tych samych danych, daje ten sam wynik. Nie „podobny". Ten sam.

Brzmi trywialnie, a jest jedną z najczęściej łamanych własności w projektach ML. Wystarczy jeden pominięty random_state, żeby to przestało działać.

Sprawdźmy to twardo: uruchomimy skrypt dwa razy i porównamy zapisane metryki co do bitu.

In [ ]:
import json

!{sys.executable} src/trenuj.py --max-depth 5 --wyjscie wyniki/bieg_A
!{sys.executable} src/trenuj.py --max-depth 5 --wyjscie wyniki/bieg_B

with open('wyniki/bieg_A/metryki_d5.json', encoding='utf-8') as f:
    bieg_A = json.load(f)
with open('wyniki/bieg_B/metryki_d5.json', encoding='utf-8') as f:
    bieg_B = json.load(f)

print("Bieg A, skuteczność:", bieg_A['skutecznosc'])
print("Bieg B, skuteczność:", bieg_B['skutecznosc'])
print()
print("Identyczne co do bitu?", bieg_A['skutecznosc'] == bieg_B['skutecznosc'])

Trzy rzeczy sprawiły, że to działa - i wszystkie trzy trzeba robić świadomie:

Element Co gwarantuje
random_state w train_test_split ten sam podział na zbiór uczący i testowy
random_state w modelu te same decyzje algorytmu tam, gdzie ma element losowy
te same dane wejściowe oczywiste, a i tak najczęstsza przyczyna rozjazdu

Czego to jeszcze nie gwarantuje: innej wersji scikit-learn, innej wersji Pythona, innego systemu operacyjnego. Dlatego w poważnych projektach do wyników dopisuje się wersje bibliotek, a środowisko zamraża w pliku (requirements.txt, environment.yml) albo w obrazie kontenera. Zajmiesz się tym w zadaniu 5.

7. Model na dysku: zapis i wczytanie w osobnym procesie

Wytrenowany model to obiekt Pythona żyjący w pamięci. joblib.dump zamienia go w plik; joblib.load odtwarza z pliku. Dzięki temu trening i używanie modelu to dwie różne rzeczy, wykonywane w różnym czasie, a nawet na różnych maszynach.

To jest fundament wdrożenia (ang. deployment): trenujesz raz, a przewidujesz tysiące razy, nie powtarzając treningu.

In [ ]:
%%writefile src/przewiduj.py
"""Wczytuje zapisany model i przewiduje dla pierwszych pacjentow z pliku.

Uruchomienie:
    python src/przewiduj.py --model wyniki/model_d5.joblib --ile 5
"""
import argparse

import joblib
import pandas as pd

KOLUMNA_ID = "PatientID"
KOLUMNA_ETYKIETY = "Diabetic"


def parsuj_argumenty():
    parser = argparse.ArgumentParser(description="Predykcja z zapisanego modelu")
    parser.add_argument("--model", default="wyniki/model_d5.joblib")
    parser.add_argument("--dane", default="dane/diabetes.csv")
    parser.add_argument("--ile", type=int, default=5,
                        help="ilu pierwszych pacjentow pokazac")
    return parser.parse_args()


def main():
    args = parsuj_argumenty()

    model = joblib.load(args.model)
    print(f"Wczytano model: {type(model).__name__} z pliku {args.model}")

    ramka = pd.read_csv(args.dane).head(args.ile)
    X = ramka.drop(columns=[KOLUMNA_ID, KOLUMNA_ETYKIETY])

    przewidywania = model.predict(X)
    prawdopodobienstwa = model.predict_proba(X)[:, 1]

    print()
    print(f"{'PatientID':>12} {'przewidziano':>13} {'p(cukrzyca)':>12} {'prawda':>8}")
    for identyfikator, pred, prob, prawda in zip(
        ramka[KOLUMNA_ID], przewidywania, prawdopodobienstwa, ramka[KOLUMNA_ETYKIETY]
    ):
        print(f"{identyfikator:>12} {pred:>13} {prob:>12.3f} {prawda:>8}")


if __name__ == "__main__":
    main()
In [ ]:
!{sys.executable} src/przewiduj.py --model wyniki/model_d5.joblib --ile 8

Ten proces nigdy nie widział danych uczących i nie wykonał ani jednej operacji treningu. Dostał plik i policzył predykcje. Tak wygląda używanie modelu w produkcji.

Dwie pułapki joblib. Po pierwsze: plik .joblib zawiera obiekt scikit-learn, więc wczytanie go w środowisku z inną wersją biblioteki może się nie udać albo - gorzej - udać się z ostrzeżeniem i innym zachowaniem. Po drugie: wczytanie pliku .joblib uruchamia kod, więc nigdy nie wczytuj modeli z niezaufanego źródła, tak samo jak nie uruchamiasz przypadkowych plików .exe.


Zadania

Wszystko, czego potrzebujesz, pojawiło się w przykładzie powyżej. Skrypty twórz przez %%writefile (ta linia musi być pierwszą linią komórki), a uruchamiaj przez !python ....

Zadanie 1: Porównanie głębokości bez edytowania kodu

Uruchom src/trenuj.py dla --max-depth równego 2, 4, 6, 8 i 12 (każdy bieg do osobnego katalogu przez --wyjscie, albo skorzystaj z tego, że nazwa pliku metryk już zawiera głębokość).

Następnie wczytaj zapisane pliki JSON w Pythonie, zbuduj z nich pandas.DataFrame i wypisz tabelę: głębokość, skuteczność, czułość, F1.

Zwróć uwagę na to, co właśnie zrobiłeś: wyniki eksperymentów nie są już „na ekranie" - leżą na dysku i da się je wczytać kiedykolwiek.

In [ ]:
# TWÓJ KOD TUTAJ
# Podpowiedź: pętla po głębokościach z !python nie zadziała wprost.
# Możesz uruchomić pięć osobnych linii z ! albo użyć subprocess.run([...]).

Zadanie 2: Dowód odtwarzalności

Uruchom src/trenuj.py dwa razy z identycznymi argumentami, ale do dwóch różnych katalogów wyjściowych.

Wczytaj oba pliki JSON i sprawdź programowo (==, a nie „na oko"), czy metryki są identyczne. Wypisz czytelny komunikat: „ODTWARZALNE" albo „NIEODTWARZALNE".

Dodatkowo porównaj predykcje obu zapisanych modeli na tych samych danych - czy tablice są identyczne co do elementu? (Przyda się numpy.array_equal.)

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 3: Zepsuj odtwarzalność celowo

Napisz plik src/trenuj_losowo.py - kopię src/trenuj.py, w której usuwasz random_state z modelu i z train_test_split.

Uruchom go trzy razy z tymi samymi argumentami i porównaj skuteczność.

Potem odpowiedz sobie na pytanie: gdyby ten skrypt był Twoim projektem zaliczeniowym, a prowadzący uruchomił go u siebie - czy dostałby liczbę, którą wpisałeś do sprawozdania?

In [ ]:
# TWÓJ KOD TUTAJ
# Podpowiedź: nie musisz przepisywać całego pliku ręcznie -
# skopiuj zawartość komórki z %%writefile i usuń random_state w dwóch miejscach.

Zadanie 4: Nowy argument wiersza poleceń

Rozbuduj skrypt (zapisz jako src/trenuj2.py) o dwa nowe argumenty:

  • --test-size (typ float, domyślnie 0.2) - jaka część danych idzie na test,
  • --model z choices=['drzewo', 'las'] - DecisionTreeClassifier albo RandomForestClassifier.

Zadbaj o to, żeby oba nowe parametry trafiły do zapisanego pliku JSON (vars(args) robi to samo). Uruchom skrypt dla obu modeli i porównaj wyniki.

Zastanów się przy okazji: dlaczego choices=[...] jest lepsze niż zwykły tekst, który sprawdzasz if-em w środku?

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 5: Metadane odtwarzalności

Model bez informacji o tym, jak powstał, jest zagadką. Rozbuduj zapisywany słownik metryk (plik src/trenuj3.py) o sekcję "srodowisko" zawierającą:

  • wersję Pythona (sys.version),
  • wersję scikit-learn (sklearn.__version__) i pandas (pd.__version__),
  • znacznik czasu uruchomienia (datetime.now().isoformat()),
  • sumę kontrolną pliku z danymi - skrót SHA-256 (podpowiedź: hashlib.sha256(Path(sciezka).read_bytes()).hexdigest()).

Wypisz powstały JSON. Zastanów się, który z tych elementów wykryłby podmianę pliku z danymi, o której nikt Cię nie poinformował.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 6: Zapis i wczytanie w dwóch osobnych procesach

  1. Uruchom trening z --max-depth 8 i zapisem do katalogu wyniki/model8.
  2. Zrestartuj jądro notatnika (Kernel → Restart Kernel). Wszystkie zmienne znikają.
  3. W nowej komórce wczytaj model przez joblib.load i policz jego skuteczność na zbiorze testowym - odtwarzając podział tym samym random_state=42.

Porównaj otrzymaną skuteczność z tą zapisaną w pliku JSON. Jeśli się różnią, coś w łańcuchu odtwarzalności jest złamane - znajdź co.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 7 (trudniejsze): Skrypt, który sam wybiera najlepszy model

Napisz src/porownaj.py, który:

  1. przyjmuje argument --max-depth z nargs='+', czyli listę wartości (np. --max-depth 2 4 6 8 12),
  2. trenuje model dla każdej z nich,
  3. wybiera najlepszy według metryki wskazanej argumentem --metryka (choices=['skutecznosc', 'czulosc', 'f1'], domyślnie f1),
  4. zapisuje tylko najlepszy model do pliku, ale wszystkie wyniki do jednego JSON-a z polami "ranking" (lista) i "najlepszy" (słownik),
  5. wypisuje na ekran czytelną tabelę i informację, która głębokość wygrała.

Punkt do przemyślenia na koniec: skrypt wybiera najlepszą głębokość, patrząc na zbiór testowy. W ćwiczeniu 06 ustaliliśmy, że to nie jest uczciwe. Dopisz w docstringu skryptu jedno zdanie o tym, co należałoby zrobić zamiast tego.

In [ ]:
# TWÓJ KOD TUTAJ

Pytania do przemyślenia

Na te pytania odpowiadasz słowami, nie kodem.

  1. Notatnik ma 40 komórek i wszystkie pokazują wyniki. Dlaczego to nie jest dowód, że kod działa?
  2. Dlaczego parametr podany jako --max-depth 5 jest lepszy od zmiennej MAX_DEPTH = 5 na górze pliku? Wymień co najmniej dwa powody.
  3. Po co komu if __name__ == "__main__":? Co konkretnie zepsułoby się bez tej linii, gdyby ktoś zaimportował Twój skrypt?
  4. Ustawiłeś random_state wszędzie, gdzie się dało, a kolega i tak dostał inny wynik. Wymień trzy możliwe przyczyny.
  5. Dlaczego metryki warto zapisywać do pliku razem z parametrami, a nie osobno?
  6. Kiedy notatnik jest lepszym wyborem niż skrypt? Podaj konkretną sytuację - bo takie istnieją.
  7. Model .joblib ma 300 MB. Czy powinien trafić do repozytorium Gita razem z kodem? Co przemawia za, a co przeciw?

Chcesz wiedzieć więcej

W ostatnim ćwiczeniu (11 - Jakość kodu w ML) pójdziemy o krok dalej: skoro kod leży już w pliku .py, da się go przetestować. Zobaczysz, co właściwie da się testować w kodzie uczenia maszynowego - bo na pewno nie skuteczność modelu.