Skip to main content

Cyber AI România

Vineri, 6 noiembrie 2026

TorchCodec cu PyTorch: pipeline video pe GPU, DataLoader și clasificare UCF101

Un folder cu fișiere video nu este încă un set de antrenare: clipurile trebuie împărțite fără contaminare între train și test, cadrele trebuie eșantionate identic, iar decodarea nu trebuie să trimită inutil datele înainte și înapoi între CPU și GPU. La finalul acestui proiect vei avea 260 de clipuri locale din cinci clase UCF101, decodate prin TorchCodec și NVDEC direct pe GPU, un DataLoader, un baseline majoritar, un clasificator 3D antrenat și două artefacte verificabile: artifacts/best.pt și artifacts/test_metrics.json.

Scenariul este un laborator intern care trebuie să distingă cinci acțiuni: ApplyEyeMakeup, Basketball, Biking, PlayingGuitar și Typing. Folosim splitul oficial UCF101 nr. 1, păstrăm câte 32 de clipuri pentru train, 8 pentru validare și 12 pentru test din fiecare clasă și fixăm seed-ul la 47. Pipeline-ul nu reprezintă un benchmark complet UCF101 și nu trebuie comparat cu rezultate publicate pe toate cele 101 clase.

Fixăm o stație CUDA pe care TorchCodec 0.17 poate folosi NVDEC

Varianta de aici este pentru Ubuntu 24.04 LTS, Python 3.11, PyTorch 2.11.0 și TorchCodec 0.17.0. Documentația TorchCodec 0.17 precizează compatibilitatea cu PyTorch 2.11 sau mai nou și Python 3.10 sau mai nou. VideoDecoder folosește implicit backendul nvdec când primește un dispozitiv CUDA.

Configurația practică presupune un GPU NVIDIA care poate decoda hardware codec-ul clipurilor, cel puțin 8 GB VRAM, 16 GB RAM și aproximativ 10 GB liberi pe disc. Arhiva UCF101 ocupă aproximativ 6,5 GB, iar fișierele extrase și artefactele cer spațiu suplimentar. Antrenarea celor cinci clase este estimată la 20–60 de minute pe un GPU modern de consum; aceasta este o estimare operațională, nu un benchmark. Implementarea nu oferă o cale CPU, deoarece rezultatul urmărit este verificarea decodării CUDA.

Rulează întâi comenzile de inventariere:

cat /etc/os-release
python3 --version
nvidia-smi
ffmpeg -version | head -n 1
df -h .

Un rezultat compatibil arată astfel; modelul GPU, versiunea driverului și spațiul liber vor fi diferite:

PRETTY_NAME="Ubuntu 24.04 LTS"
Python 3.11.x
NVIDIA-SMI ... Driver Version: ...
ffmpeg version 7.x ...
Filesystem ... Avail ...

Verifică și capabilitățile de decodare expuse de FFmpeg:

ffmpeg -hide_banner -hwaccels

Lista trebuie să conțină cuda. Apariția numelui confirmă că FFmpeg recunoaște interfața, nu că fiecare codec sau fiecare GPU este automat compatibil.

Dacă antrenarea produce un CUDA out of memory, diagnostichează întâi alocările, în loc să modifici simultan dimensiunea clipului și modelul. Metoda din ghidul despre CUDA Memory Fragmentation în PyTorch ajută la separarea fragmentării de lipsa reală a VRAM.

Instalăm mediul fix și aducem UCF101 pe discul local

UCF101 conține 13.320 de clipuri din 101 categorii și are trei împărțiri oficiale train/test. Pagina Universității Central Florida este sursa datasetului și a listelor de split. Pagina nu afișează o licență open-source standard, precum MIT sau Creative Commons; înaintea utilizării comerciale trebuie verificate drepturile asupra datasetului și ale clipurilor provenite din YouTube. Tutorialul îl folosește strict ca set de cercetare.

Creează mediul și instalează exact pachetele proiectului:

sudo apt update
sudo apt install -y python3.11-venv ffmpeg p7zip-full unzip wget
mkdir -p "$HOME/torchcodec-ucf101"
cd "$HOME/torchcodec-ucf101"
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install torch==2.11.0 torchcodec==0.17.0

PyTorch publică variante distincte în funcție de platforma CUDA. Dacă instalarea implicită nu oferă CUDA, nu ghici adresa unui index de pachete: selectează Linux, Pip, Python și versiunea CUDA a stației pe pagina oficială „Get Started Locally”, apoi păstrează versiunile torch==2.11.0 și torchcodec==0.17.0.

Confirmă ce a fost încărcat:

python - <<'PY'
import torch
import torchcodec

print("torch:", torch.__version__)
print("torchcodec:", torchcodec.__version__)
print("cuda:", torch.cuda.is_available())
print("gpu:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "indisponibil")
PY

Rezultatul necesar este:

torch: 2.11.0...
torchcodec: 0.17.0
cuda: True
gpu: <modelul plăcii NVIDIA>

Descarcă arhiva video și listele oficiale, apoi extrage-le local:

mkdir -p downloads data
wget -c https://www.crcv.ucf.edu/data/UCF101/UCF101.rar 
  -O downloads/UCF101.rar
wget -c https://www.crcv.ucf.edu/data/UCF101/UCF101TrainTestSplits-RecognitionTask.zip 
  -O downloads/UCF101TrainTestSplits-RecognitionTask.zip
7z x downloads/UCF101.rar -odata
unzip -q downloads/UCF101TrainTestSplits-RecognitionTask.zip -d data/splits
test -d data/UCF-101
test -f data/splits/ucfTrainTestlist/trainlist01.txt
test -f data/splits/ucfTrainTestlist/testlist01.txt

Dacă serverul UCF schimbă adresele, descarcă aceleași două arhive din pagina oficială, fără să înlocuiești datasetul cu o copie neverificată. Dimensiunea și hash-ul unei redistribuiri terțe pot fi diferite.

Păstrăm proiectul într-un singur program auditat

Arborele final este deliberat scurt:

torchcodec-ucf101/
├── pipeline.py                  # pregătire, antrenare, evaluare și inferență
├── data/
│   ├── UCF-101/                 # clipurile originale, grupate pe clase
│   ├── splits/ucfTrainTestlist/ # spliturile oficiale UCF101
│   └── manifests/               # train.csv, val.csv și test.csv
├── downloads/                   # arhivele originale
└── artifacts/
    ├── best.pt                  # ponderi, clase și configurația clipului
    ├── test_metrics.json        # baseline, accuracy, macro-F1 și matrice
    └── test_errors.csv          # predicțiile greșite și încrederea lor

pipeline.py este singurul fișier sursă. Manifestele păstrează căile relative, astfel încât proiectul poate fi mutat fără rescrierea lor.

Pasul 1 — Construim manifestele fără să atingem setul oficial de test

Scopul este să derivăm validarea numai din lista oficială de train. Seed-ul 47 stabilește ce opt clipuri pe clasă ajung în validare; setul de test rămâne separat până la evaluarea finală.

Creează fișierul pipeline.py cu întregul conținut de mai jos:

from __future__ import annotations

import argparse
import csv
import json
import random
from collections import Counter
from pathlib import Path

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
from torchcodec.decoders import VideoDecoder

CLASSES = (
    "ApplyEyeMakeup",
    "Basketball",
    "Biking",
    "PlayingGuitar",
    "Typing",
)
SEED = 47
NUM_FRAMES = 16
IMAGE_SIZE = 112
MEAN = (0.485, 0.456, 0.406)
STD = (0.229, 0.224, 0.225)


def set_determinism() -> None:
    random.seed(SEED)
    torch.manual_seed(SEED)
    torch.cuda.manual_seed_all(SEED)
    torch.use_deterministic_algorithms(True)


def require_cuda() -> torch.device:
    if not torch.cuda.is_available():
        raise RuntimeError(
            "CUDA indisponibilă: instalează buildul CUDA PyTorch și verifică driverul NVIDIA."
        )
    return torch.device("cuda:0")


def read_split(path: Path) -> list[str]:
    if not path.is_file():
        raise FileNotFoundError(f"Lista UCF101 lipsește: {path}")
    rows = []
    for line in path.read_text(encoding="utf-8").splitlines():
        value = line.strip()
        if value:
            rows.append(value.split()[0])
    return rows


def write_manifest(path: Path, rows: list[tuple[str, int, str]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.writer(handle)
        writer.writerow(("path", "label", "class_name"))
        writer.writerows(rows)


def prepare(args: argparse.Namespace) -> None:
    video_root = args.video_root.resolve()
    split_root = args.split_root.resolve()
    output = args.output.resolve()

    train_paths = read_split(split_root / "trainlist01.txt")
    test_paths = read_split(split_root / "testlist01.txt")
    rng = random.Random(SEED)

    train_rows: list[tuple[str, int, str]] = []
    val_rows: list[tuple[str, int, str]] = []
    test_rows: list[tuple[str, int, str]] = []

    for label, class_name in enumerate(CLASSES):
        class_train = [p for p in train_paths if p.startswith(f"{class_name}/")]
        class_test = [p for p in test_paths if p.startswith(f"{class_name}/")]
        rng.shuffle(class_train)
        class_test.sort()

        if len(class_train) < 40 or len(class_test) < 12:
            raise RuntimeError(
                f"Clasa {class_name} nu are minimum 40 train și 12 test în splitul 1."
            )

        selected_train = class_train[:32]
        selected_val = class_train[32:40]
        selected_test = class_test[:12]

        for relative in selected_train + selected_val + selected_test:
            if not (video_root / relative).is_file():
                raise FileNotFoundError(f"Clip UCF101 lipsă: {video_root / relative}")

        train_rows.extend((p, label, class_name) for p in selected_train)
        val_rows.extend((p, label, class_name) for p in selected_val)
        test_rows.extend((p, label, class_name) for p in selected_test)

    write_manifest(output / "train.csv", train_rows)
    write_manifest(output / "val.csv", val_rows)
    write_manifest(output / "test.csv", test_rows)
    print(json.dumps({"train": 160, "val": 40, "test": 60}, indent=2))


class VideoManifest(Dataset):
    def __init__(self, manifest: Path, video_root: Path, device: torch.device) -> None:
        self.video_root = video_root
        self.device = device
        with manifest.open(newline="", encoding="utf-8") as handle:
            self.rows = list(csv.DictReader(handle))
        if not self.rows:
            raise RuntimeError(f"Manifest gol: {manifest}")

    def __len__(self) -> int:
        return len(self.rows)

    def decode(self, path: Path) -> torch.Tensor:
        decoder = VideoDecoder(
            str(path),
            device=self.device,
            dimension_order="NCHW",
            seek_mode="exact",
        )
        frame_count = len(decoder)
        if frame_count < NUM_FRAMES:
            raise RuntimeError(
                f"Clip prea scurt: {path} are {frame_count} cadre; sunt necesare {NUM_FRAMES}."
            )

        indices = torch.linspace(0, frame_count - 1, NUM_FRAMES)
        indices = indices.round().to(torch.int64)
        frames = decoder.get_frames_at(indices).data
        frames = frames.to(torch.float32).div_(255.0)
        frames = F.interpolate(
            frames,
            size=(IMAGE_SIZE, IMAGE_SIZE),
            mode="bilinear",
            align_corners=False,
        )
        mean = torch.tensor(MEAN, device=self.device).view(1, 3, 1, 1)
        std = torch.tensor(STD, device=self.device).view(1, 3, 1, 1)
        return (frames - mean) / std

    def __getitem__(self, index: int) -> tuple[torch.Tensor, torch.Tensor, str]:
        row = self.rows[index]
        path = self.video_root / row["path"]
        clip = self.decode(path)
        label = torch.tensor(int(row["label"]), dtype=torch.long)
        return clip, label, row["path"]


class ClipClassifier(nn.Module):
    def __init__(self, num_classes: int) -> None:
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv3d(3, 32, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm3d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool3d((1, 2, 2)),
            nn.Conv3d(32, 64, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm3d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool3d((2, 2, 2)),
            nn.Conv3d(64, 128, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm3d(128),
            nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool3d(1),
        )
        self.classifier = nn.Linear(128, num_classes)

    def forward(self, clips: torch.Tensor) -> torch.Tensor:
        clips = clips.permute(0, 2, 1, 3, 4)
        return self.classifier(self.features(clips).flatten(1))


def make_loader(
    manifest: Path,
    video_root: Path,
    device: torch.device,
    shuffle: bool,
) -> DataLoader:
    dataset = VideoManifest(manifest, video_root, device)
    generator = torch.Generator().manual_seed(SEED)
    return DataLoader(
        dataset,
        batch_size=4,
        shuffle=shuffle,
        num_workers=0,
        pin_memory=False,
        generator=generator,
    )


def confusion_and_scores(
    true_labels: list[int],
    predictions: list[int],
) -> tuple[float, float, list[list[int]]]:
    count = len(CLASSES)
    matrix = [[0 for _ in range(count)] for _ in range(count)]
    for true, predicted in zip(true_labels, predictions):
        matrix[true][predicted] += 1

    accuracy = sum(matrix[i][i] for i in range(count)) / len(true_labels)
    f1_values = []
    for label in range(count):
        tp = matrix[label][label]
        fp = sum(matrix[row][label] for row in range(count) if row != label)
        fn = sum(matrix[label][column] for column in range(count) if column != label)
        denominator = 2 * tp + fp + fn
        f1_values.append(0.0 if denominator == 0 else (2 * tp) / denominator)
    return accuracy, sum(f1_values) / count, matrix


@torch.inference_mode()
def evaluate(
    model: nn.Module,
    loader: DataLoader,
    device: torch.device,
) -> tuple[dict[str, object], list[dict[str, object]]]:
    model.eval()
    true_labels: list[int] = []
    predictions: list[int] = []
    errors: list[dict[str, object]] = []

    for clips, labels, paths in loader:
        labels = labels.to(device)
        probabilities = model(clips).softmax(dim=1)
        confidence, predicted = probabilities.max(dim=1)
        true_labels.extend(labels.cpu().tolist())
        predictions.extend(predicted.cpu().tolist())

        for path, true, pred, score in zip(
            paths,
            labels.cpu().tolist(),
            predicted.cpu().tolist(),
            confidence.cpu().tolist(),
        ):
            if true != pred:
                errors.append(
                    {
                        "path": path,
                        "true": CLASSES[true],
                        "predicted": CLASSES[pred],
                        "confidence": round(score, 6),
                    }
                )

    accuracy, macro_f1, matrix = confusion_and_scores(true_labels, predictions)
    return {
        "accuracy": round(accuracy, 6),
        "macro_f1": round(macro_f1, 6),
        "confusion_matrix": matrix,
    }, errors


def train(args: argparse.Namespace) -> None:
    set_determinism()
    device = require_cuda()
    manifests = args.manifests.resolve()
    video_root = args.video_root.resolve()
    artifacts = args.artifacts.resolve()
    artifacts.mkdir(parents=True, exist_ok=True)

    train_loader = make_loader(manifests / "train.csv", video_root, device, True)
    val_loader = make_loader(manifests / "val.csv", video_root, device, False)
    test_loader = make_loader(manifests / "test.csv", video_root, device, False)

    train_labels = [int(row["label"]) for row in train_loader.dataset.rows]
    majority_label = Counter(train_labels).most_common(1)[0][0]
    test_labels = [int(row["label"]) for row in test_loader.dataset.rows]
    baseline_predictions = [majority_label] * len(test_labels)
    baseline_accuracy, baseline_f1, _ = confusion_and_scores(
        test_labels, baseline_predictions
    )

    model = ClipClassifier(len(CLASSES)).to(device)
    optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-2)
    criterion = nn.CrossEntropyLoss()
    best_accuracy = -1.0

    for epoch in range(1, 13):
        model.train()
        loss_sum = 0.0
        item_count = 0

        for clips, labels, _ in train_loader:
            labels = labels.to(device)
            optimizer.zero_grad(set_to_none=True)
            loss = criterion(model(clips), labels)
            loss.backward()
            optimizer.step()
            loss_sum += loss.item() * labels.size(0)
            item_count += labels.size(0)

        val_metrics, _ = evaluate(model, val_loader, device)
        print(
            f"epoch={epoch:02d} "
            f"train_loss={loss_sum / item_count:.6f} "
            f"val_accuracy={val_metrics['accuracy']:.6f}"
        )

        if float(val_metrics["accuracy"]) > best_accuracy:
            best_accuracy = float(val_metrics["accuracy"])
            torch.save(
                {
                    "state_dict": model.state_dict(),
                    "classes": list(CLASSES),
                    "num_frames": NUM_FRAMES,
                    "image_size": IMAGE_SIZE,
                    "seed": SEED,
                },
                artifacts / "best.pt",
            )

    checkpoint = torch.load(artifacts / "best.pt", map_location=device)
    if checkpoint["classes"] != list(CLASSES):
        raise RuntimeError("Checkpoint incompatibil: ordinea claselor este diferită.")
    model.load_state_dict(checkpoint["state_dict"])

    test_metrics, errors = evaluate(model, test_loader, device)
    result = {
        "baseline_majority_class": CLASSES[majority_label],
        "baseline_accuracy": round(baseline_accuracy, 6),
        "baseline_macro_f1": round(baseline_f1, 6),
        "model": test_metrics,
        "test_clips": len(test_labels),
    }
    (artifacts / "test_metrics.json").write_text(
        json.dumps(result, indent=2), encoding="utf-8"
    )

    with (artifacts / "test_errors.csv").open(
        "w", newline="", encoding="utf-8"
    ) as handle:
        writer = csv.DictWriter(
            handle,
            fieldnames=("path", "true", "predicted", "confidence"),
        )
        writer.writeheader()
        writer.writerows(errors)

    print(json.dumps(result, indent=2))


def infer(args: argparse.Namespace) -> None:
    set_determinism()
    device = require_cuda()
    checkpoint = torch.load(args.checkpoint.resolve(), map_location=device)
    if checkpoint["classes"] != list(CLASSES):
        raise RuntimeError("Checkpoint incompatibil: ordinea claselor este diferită.")

    model = ClipClassifier(len(CLASSES)).to(device)
    model.load_state_dict(checkpoint["state_dict"])
    model.eval()

    helper = VideoManifest(args.manifest.resolve(), args.video_root.resolve(), device)
    clip = helper.decode(args.video.resolve()).unsqueeze(0)
    with torch.inference_mode():
        probabilities = model(clip).softmax(dim=1)[0]
    confidence, predicted = probabilities.max(dim=0)
    print(
        json.dumps(
            {
                "class_name": CLASSES[int(predicted)],
                "confidence": round(float(confidence), 6),
            },
            indent=2,
        )
    )


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Clasificare UCF101 cu TorchCodec.")
    subparsers = parser.add_subparsers(dest="command", required=True)

    prepare_parser = subparsers.add_parser("prepare")
    prepare_parser.add_argument("--video-root", type=Path, required=True)
    prepare_parser.add_argument("--split-root", type=Path, required=True)
    prepare_parser.add_argument("--output", type=Path, required=True)
    prepare_parser.set_defaults(function=prepare)

    train_parser = subparsers.add_parser("train")
    train_parser.add_argument("--video-root", type=Path, required=True)
    train_parser.add_argument("--manifests", type=Path, required=True)
    train_parser.add_argument("--artifacts", type=Path, required=True)
    train_parser.set_defaults(function=train)

    infer_parser = subparsers.add_parser("infer")
    infer_parser.add_argument("--video", type=Path, required=True)
    infer_parser.add_argument("--video-root", type=Path, required=True)
    infer_parser.add_argument("--manifest", type=Path, required=True)
    infer_parser.add_argument("--checkpoint", type=Path, required=True)
    infer_parser.set_defaults(function=infer)
    return parser


if __name__ == "__main__":
    arguments = build_parser().parse_args()
    arguments.function(arguments)

Parametrii esențiali sunt NUM_FRAMES=16, rezoluția 112×112 și seek_mode="exact". Acest mod cere indexarea conținutului, dar garantează că solicitarea cadrului i întoarce cadrul i. dimension_order="NCHW" produce cadre în ordinea necesară transformărilor PyTorch.

Rulează pregătirea:

source .venv/bin/activate
python pipeline.py prepare 
  --video-root data/UCF-101 
  --split-root data/splits/ucfTrainTestlist 
  --output data/manifests

Trebuie să vezi exact numărul de exemple selectate:

{
  "train": 160,
  "val": 40,
  "test": 60
}

Punct de control: dacă numerele nu apar, verifică existența clipului indicat de excepție și faptul că ai folosit listele trainlist01.txt și testlist01.txt.

Pasul 2 — Decodăm 16 cadre pe GPU și lăsăm DataLoader-ul în procesul principal

TorchCodec întoarce cadre uint8; codul le transformă în float32, le scalează în intervalul [0, 1], le redimensionează pe GPU și aplică normalizarea. Eșantionarea uniformă păstrează începutul, mijlocul și finalul clipului, dar nu modelează precis durata temporală atunci când videoclipurile au frecvențe diferite.

DataLoader folosește intenționat num_workers=0. Datasetul creează decodoare CUDA în __getitem__, iar pornirea mai multor procese worker ar crea procese și contexte CUDA separate. pin_memory=False este, de asemenea, intenționat: cadrele sunt deja pe GPU, deci nu există un transfer CPU–GPU care să beneficieze de memorie page-locked.

Verifică un lot înainte de antrenare:

python - <<'PY'
from pathlib import Path
import torch
from pipeline import make_loader, require_cuda

device = require_cuda()
loader = make_loader(
    Path("data/manifests/train.csv"),
    Path("data/UCF-101"),
    device,
    False,
)
clips, labels, paths = next(iter(loader))
print("shape:", tuple(clips.shape))
print("device:", clips.device)
print("dtype:", clips.dtype)
print("labels:", tuple(labels.shape))
print("primul:", paths[0])
PY

Forma și dispozitivul trebuie să fie:

shape: (4, 16, 3, 112, 112)
device: cuda:0
dtype: torch.float32
labels: (4,)
primul: <o cale relativă din manifest>

Punct de control: dacă device este cpu, nu continua. VideoDecoder trebuie construit cu un dispozitiv CUDA valid, iar TorchCodec trebuie să poată încărca bibliotecile compatibile cu versiunea PyTorch.

Pasul 3 — Comparăm baseline-ul majoritar cu rețeaua 3D

Baseline-ul prezice mereu clasa cea mai frecventă din train. Deoarece manifestul este echilibrat, toate clasele au câte 32 de exemple; Counter selectează prima etichetă întâlnită, iar acuratețea baseline-ului pe cele 60 de clipuri echilibrate este 0.2. Macro-F1 penalizează faptul că celelalte patru clase nu sunt recunoscute.

Modelul primește tensorul [B,T,C,H,W], îl reordonează în [B,C,T,H,W] și aplică trei blocuri Conv3d. Nu este un model preantrenat și nu urmărește performanța unui backbone video modern; scopul său este să demonstreze întregul traseu decode–batch–train–evaluate fără dependențe ascunse.

Pornește antrenarea și măsoară timpul real al stației:

export CUBLAS_WORKSPACE_CONFIG=:4096:8
time python pipeline.py train 
  --video-root data/UCF-101 
  --manifests data/manifests 
  --artifacts artifacts

Vei primi câte un rând pentru fiecare epocă. Valorile sunt calculate local și nu trebuie copiate dintr-un exemplu:

epoch=01 train_loss=<valoare calculată> val_accuracy=<valoare calculată>
...
epoch=12 train_loss=<valoare calculată> val_accuracy=<valoare calculată>
{
  "baseline_majority_class": "ApplyEyeMakeup",
  "baseline_accuracy": 0.2,
  "baseline_macro_f1": 0.066667,
  "model": {
    "accuracy": <valoare calculată pe test>,
    "macro_f1": <valoare calculată pe test>,
    "confusion_matrix": <matricea 5x5 calculată>
  },
  "test_clips": 60
}

Valorile dintre marcajele descriptive nu sunt rezultate pretinse; sursa rezultatului este exclusiv artifacts/test_metrics.json. Pentru un job mai mare care nu încape în VRAM, Selective Activation Checkpointing în PyTorch explică unde checkpointing-ul poate reduce memoria. Nu îl adăugăm aici, deoarece ar schimba proiectul măsurat.

Punct de control: trebuie să existe artifacts/best.pt, artifacts/test_metrics.json și artifacts/test_errors.csv. Dacă lipsește checkpointul, inspectează ultima excepție din antrenare; nu evalua un model nefinalizat.

Pasul 4 — Citim matricea de confuzie și clipurile pe care modelul le încurcă

Acuratețea spune câte dintre cele 60 de clipuri au fost clasificate corect. Macro-F1 calculează F1 separat pentru fiecare clasă și apoi media, astfel încât o clasă ignorată nu este mascată de celelalte. Matricea folosește clasele reale pe rânduri și predicțiile pe coloane, în ordinea constantei CLASSES.

Afișează raportul fără recalcularea inferenței:

python -m json.tool artifacts/test_metrics.json
head -n 11 artifacts/test_errors.csv

Fișierul CSV are forma:

path,true,predicted,confidence
<clip greșit>,<clasa reală>,<predicția>,<încrederea>

Nu interpreta confidence ca probabilitate calibrată. Este maximul rezultatului softmax al unui model mic, antrenat pe un subset restrâns. Analiza utilă este să grupezi rândurile după perechea true–predicted și să inspectezi dacă acțiunea este vizibilă în cele 16 cadre. Un clip în care gestul relevant apare numai între două cadre eșantionate indică o problemă de sampling, nu neapărat o lipsă de capacitate a rețelei.

Pentru acest proiect, comparația validă este tabelul completat din JSON, nu un scor anticipat:

Sistem Date de test Accuracy Macro-F1
Baseline majoritar 60 0,200000 0,066667
ClipClassifier 3D 60 valoarea din JSON valoarea din JSON

Punct de control: suma tuturor celulelor matricei de confuzie trebuie să fie 60. Fiecare rând trebuie să însumeze 12, deoarece testul conține 12 clipuri din fiecare clasă.

Pasul 5 — Încărcăm checkpointul și clasificăm un clip local

Inferența folosește aceeași decodare, aceeași eșantionare și aceeași normalizare ca antrenarea. Pentru testul de capăt la capăt, alege un clip deja prezent în directorul UCF101:

python pipeline.py infer 
  --video data/UCF-101/Typing/v_Typing_g01_c01.avi 
  --video-root data/UCF-101 
  --manifest data/manifests/test.csv 
  --checkpoint artifacts/best.pt

Dacă numele v_Typing_g01_c01.avi nu există în copia locală, ia o cale reală cu:

find data/UCF-101/Typing -type f -name '*.avi' | sort | head -n 1

Răspunsul CLI are schema:

{
  "class_name": "<una dintre cele cinci clase>",
  "confidence": <valoare calculată între 0 și 1>
}

Testul final nu cere ca orice clip Typing să fie clasificat corect; cere ca pipeline-ul să încarce checkpointul, să decodeze pe cuda:0 și să emită o clasă validă. Calitatea se judecă pe întregul set de test, nu pe exemplul ales manual.

Punct de control: clasa trebuie să fie una dintre cele cinci nume salvate în checkpoint. Orice altă etichetă indică folosirea altui fișier sau modificarea ordinii claselor.

Cinci defecțiuni specifice acestui traseu video

RuntimeError: CUDA indisponibilă: instalează buildul CUDA PyTorch și verifică driverul NVIDIA. Scriptul a încărcat un PyTorch fără CUDA sau driverul nu poate inițializa placa. Verifică:

python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())"
nvidia-smi

Reinstalează comanda indicată de selectorul oficial PyTorch pentru stația respectivă.

FileNotFoundError: Lista UCF101 lipsește: .../trainlist01.txt Arhiva spliturilor a fost extrasă la alt nivel. Localizează fișierul și transmite directorul care îl conține:

find data -type f -name trainlist01.txt -print

FileNotFoundError: Clip UCF101 lipsă: ... Arhiva video este incompletă sau --video-root nu indică data/UCF-101. Verifică arhiva și extrage din nou într-un director curat:

7z t downloads/UCF101.rar
rm -rf data/UCF-101
7z x downloads/UCF101.rar -odata

Avertisment: rm -rf data/UCF-101 șterge toate clipurile extrase; rulează comanda numai dacă arhiva originală a trecut testul 7z t.

RuntimeError: Clip prea scurt: ... are N cadre; sunt necesare 16. Fișierul este trunchiat sau clipul local nu corespunde UCF101. Testează-l:

ffprobe -v error -count_frames -select_streams v:0 
  -show_entries stream=nb_read_frames 
  -of default=nokey=1:noprint_wrappers=1 
  calea/catre/clip.avi

Nu duplica artificial cadrele: restaurează clipul din arhiva validă.

RuntimeError: Checkpoint incompatibil: ordinea claselor este diferită. best.pt provine din altă versiune a proiectului. Nu elimina verificarea. Mută artefactele vechi și reantrenează:

mv artifacts "artifacts-incompatibile-$(date +%Y%m%d-%H%M%S)"
mkdir artifacts
export CUBLAS_WORKSPACE_CONFIG=:4096:8
python pipeline.py train 
  --video-root data/UCF-101 
  --manifests data/manifests 
  --artifacts artifacts

Ștergem doar artefactele produse, nu arhivele fără avertisment

Pentru reluarea antrenării de la zero, elimină checkpointul și rapoartele:

rm -rf artifacts
mkdir artifacts

Pentru refacerea selecției deterministe, elimină numai manifestele:

rm -rf data/manifests
python pipeline.py prepare 
  --video-root data/UCF-101 
  --split-root data/splits/ucfTrainTestlist 
  --output data/manifests

Avertisment: comanda următoare elimină mediul, datasetul local, arhivele și toate rezultatele proiectului:

cd "$HOME"
rm -rf "$HOME/torchcodec-ucf101"

Nu o executa dacă vrei să păstrezi arhiva de 6,5 GB sau checkpointul.

Lista de acceptanță pentru pipeline

  • ☐ torch.__version__ indică 2.11.0, iar TorchCodec indică 0.17.0.
  • ☐ torch.cuda.is_available() întoarce True.
  • ☐ Manifestele conțin 160 clipuri train, 40 validare și 60 test.
  • ☐ Nicio cale din test.csv nu apare în train.csv sau val.csv.
  • ☐ Primul lot are forma (4, 16, 3, 112, 112) pe cuda:0.
  • ☐ Baseline-ul raportează accuracy 0.2 pe testul echilibrat.
  • ☐ artifacts/best.pt se încarcă fără modificarea ordinii claselor.
  • ☐ Matricea de confuzie însumează 60, cu 12 exemple pe fiecare rând.
  • ☐ test_errors.csv conține numai predicțiile greșite.
  • ☐ Comanda infer întoarce o clasă validă și o încredere calculată local.

Următoarele trei extinderi păstrează experimentul măsurabil

Prima extindere este un sampler bazat pe timp, nu pe numărul cadrului, pentru clipuri cu rate de cadre foarte diferite. TorchCodec expune atât acces după index, cât și acces după timestamp; schimbarea trebuie evaluată pe același test înghețat.

A doua este înlocuirea rețelei mici cu un backbone video preantrenat, păstrând manifestele și raportarea. Dacă noul model depășește memoria, măsoară mai întâi alocările; nu presupune că batch-ul este singura cauză.

A treia este profilarea separată a decodării și a antrenării. Articolul despre Torch Profiler și golurile în care GPU-ul așteaptă nu este inclus în lista de legături furnizată și, prin urmare, nu este legat aici; folosește documentația oficială PyTorch Profiler pentru a măsura dacă NVDEC, transformările sau convoluțiile limitează debitul.

Întrebări frecvente

De ce are DataLoader num_workers=0?

Decodarea are loc direct pe CUDA în __getitem__. Menținerea încărcării în procesul principal evită procese worker cu contexte CUDA separate și păstrează acest laborator reproductibil.

Pot raporta rezultatul drept scor UCF101?

Nu. Experimentul folosește numai cinci clase și câte 52 de clipuri selectate pentru fiecare clasă. Este un test al pipeline-ului, nu benchmarkul complet pe 101 clase și trei splituri.

De ce baseline-ul are accuracy 0,2?

Setul de test are cinci clase echilibrate, cu 12 clipuri fiecare. Un predictor care alege mereu o singură clasă nimerește 12 dintre cele 60 de exemple.

Unde găsesc predicțiile greșite?

În artifacts/test_errors.csv. Fiecare rând păstrează calea clipului, clasa reală, predicția și valoarea maximă softmax, fără a o prezenta drept probabilitate calibrată.

Surse

Facebook
X
WhatsApp
TorchCodec cu PyTorch: pipeline video pe GPU, DataLoader și clasificare UCF101

Te-ar putea interesa si: