Faza 01 · lecția 12

Operații cu tensori

Scopul lecției: Tensorii sunt limbajul comun al datelor și al învățării profunde. Orice imagine și propoziție, precum și fiecare gradient, sunt reprezentate și procesate prin tensori.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Fundamente matematice
Lectură
16 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Ce este un tensor
  5. Formele tensorilor în învățarea profundă
  6. Organizarea datelor în memorie
  7. Regulile de broadcasting
  8. Einsum: operația tensorială universală
  9. Construirea implementării
  10. Pasul 1: stocarea tensorului și pașii de memorie
  11. Pasul 2: reshape, squeeze și unsqueeze
  12. Pasul 3: transpose și permute
  13. Pasul 4: operații element cu element și reduceri
  14. Pasul 5: broadcasting cu NumPy
  15. Pasul 6: operații einsum
  16. Pasul 7: mecanismul de atenție prin einsum
  17. Utilizarea implementării
  18. Implementarea de la zero și NumPy
  19. Implementarea de la zero și PyTorch
  20. Fiecare strat al unei rețele neuronale ca operație tensorială
  21. Livrarea rezultatului
  22. Exerciții
  23. Termeni-cheie
  24. Lecturi suplimentare

Tensorii sunt limbajul comun al datelor și al învățării profunde. Orice imagine și propoziție, precum și fiecare gradient, sunt reprezentate și procesate prin tensori.

Tip: Construire Limbaj: Python Cerințe preliminare: Faza 1, lecțiile 01 (Intuiție pentru algebra liniară), 02 (Vectori, matrice și operații) Durată: ~90 de minute

Obiective de învățare

  • Implementarea de la zero a unei clase pentru tensori, cu formă, pași de memorie, remodelare, transpunere și operații element cu element
  • Aplicarea regulilor de broadcasting pentru a efectua operații asupra tensorilor de forme diferite fără copierea datelor
  • Scrierea expresiilor einsum pentru produse scalare, înmulțiri matriciale, produse exterioare și operații pe loturi
  • Urmărirea formelor exacte ale tensorilor în fiecare etapă a atenției cu mai multe capete

Problema

Construiți un Transformer. Propagarea înainte pare clară. Rulați codul și primiți eroarea RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768). Priviți formele tensorilor. Încercați o transpunere. Acum mesajul este Expected 4D input (got 3D input). Adăugați un unsqueeze, iar altceva nu mai funcționează.

Erorile de formă sunt cele mai frecvente surse de erori din codul pentru învățare profundă. Conceptual, nu sunt dificile: fiecare operație are un contract dimensional. Însă se înmulțesc rapid. Un Transformer conține zeci de remodelări, transpuneri și operații de broadcasting înlănțuite. O singură axă greșită declanșează o cascadă de erori. Mai grav, unele greșeli de formă nu generează nicio eroare. Ele produc în tăcere rezultate eronate, prin extinderea de-a lungul dimensiunii greșite sau însumarea pe axa greșită.

Matricele descriu relații perechi între două mulțimi de elemente. Datele reale nu se încadrează în două dimensiuni. Un lot de 32 de imagini RGB cu rezoluția 224x224 este un tensor 4D: (32, 3, 224, 224). Autoatenția cu 12 capete este tot 4D: (batch, heads, seq_len, head_dim). Aveți nevoie de o structură de date care să se generalizeze la orice număr de dimensiuni și ale cărei operații să se compună clar între toate aceste dimensiuni. Această structură este tensorul. Dacă îi stăpâniți operațiile, erorile de formă devin ușor de depanat.

Conceptul

Ce este un tensor

Un tensor este un tablou multidimensional de numere cu un tip de date uniform. Numărul de dimensiuni reprezintă rangul sau ordinul. Fiecare dimensiune este o axă. Forma (shape) este un tuplu care enumeră dimensiunea de-a lungul fiecărei axe.

Диаграмма к уроку «Operații cu tensori»

Numărul total de elemente este produsul tuturor dimensiunilor. O formă (2, 3, 4) conține 2 * 3 * 4 = 24 de elemente.

Formele tensorilor în învățarea profundă

Prin convenție, diferitele tipuri de date corespund anumitor forme de tensor.

Диаграмма к уроку «Operații cu tensori»

PyTorch folosește NCHW, cu canalele înaintea dimensiunilor spațiale. TensorFlow folosește implicit NHWC, cu canalele după dimensiunile spațiale. Formatele incompatibile produc încetiniri greu de observat sau erori.

Organizarea datelor în memorie

Un tablou bidimensional este stocat în memorie ca o secvență unidimensională de octeți. Pașii de memorie (strides) indică deplasarea, exprimată în număr de elemente, necesară pentru a avansa cu o poziție de-a lungul fiecărei axe.

Диаграмма к уроку «Operații cu tensori»

În biblioteci precum NumPy și PyTorch, o transpunere poate fi reprezentată ca un view care nu mută datele: ea interschimbă pașii de memorie, iar tensorul poate deveni necontiguu, astfel încât elementele unei linii nu mai sunt alăturate în memorie.

Regulile de broadcasting

Broadcastingul vă permite să efectuați operații asupra tensorilor de forme diferite fără să copiați datele. Aliniați formele de la dreapta. Două dimensiuni sunt compatibile dacă sunt egale sau dacă una dintre ele este 1. Formele cu mai puține dimensiuni sunt completate în stânga cu valori 1.

Tensorul A:       (8, 1, 6, 1)
Tensorul B:          (7, 1, 5)
B completat:      (1, 7, 1, 5)
Rezultat:         (8, 7, 6, 5)

Einsum: operația tensorială universală

Convenția de sumare Einstein etichetează fiecare axă cu o literă. Axele prezente la intrare, dar absente la ieșire, sunt însumate. Axele prezente în ambele sunt păstrate.

Диаграмма к уроку «Operații cu tensori»

Tipare esențiale: i,i-> (produs scalar), i,j->ij (produs exterior), ii-> (urmă), ij->ji (transpunere), bij,bjk->bik (înmulțire matricială pe loturi), bhtd,bhsd->bhts (scoruri de atenție).

tensor-broadcast

Construirea implementării

Codul se află în code/tensors.py. Fiecare pas face referire la implementarea din acel fișier.

Pasul 1: stocarea tensorului și pașii de memorie

Un tensor stochează o listă liniară de numere împreună cu metadatele formei. Pașii de memorie indică logicii de indexare modul în care să transforme indicii multidimensionali în poziții din lista liniară.

class Tensor:
    def __init__(self, data, shape=None):
        if isinstance(data, (list, tuple)):
            self._data, self._shape = self._flatten_nested(data)
        elif isinstance(data, np.ndarray):
            self._data = data.flatten().tolist()
            self._shape = tuple(data.shape)
        else:
            self._data = [data]
            self._shape = ()

        if shape is not None:
            total = reduce(lambda a, b: a * b, shape, 1)
            if total != len(self._data):
                raise ValueError(
                    f"Cannot reshape {len(self._data)} elements into shape {shape}"
                )
            self._shape = tuple(shape)

        self._strides = self._compute_strides(self._shape)

    @staticmethod
    def _compute_strides(shape):
        if len(shape) == 0:
            return ()
        strides = [1] * len(shape)
        for i in range(len(shape) - 2, -1, -1):
            strides[i] = strides[i + 1] * shape[i + 1]
        return tuple(strides)

Pentru forma (3, 4), pașii sunt (4, 1): deplasarea necesară pentru a avansa cu o linie este de 4 elemente, iar pentru a avansa cu o coloană este de 1 element.

Pasul 2: reshape, squeeze și unsqueeze

reshape modifică forma fără să schimbe ordinea elementelor. Numărul total de elemente trebuie să rămână același. Folosiți -1 pentru una dintre dimensiuni dacă doriți să-i fie dedusă automat mărimea.

t = Tensor(list(range(12)), shape=(2, 6))
r = t.reshape((3, 4))
r = t.reshape((-1, 3))

squeeze elimină axele de dimensiune 1. unsqueeze inserează o axă. Pentru broadcasting, un vector de bias (D,) poate fi adăugat direct la un lot (B, T, D), deoarece formele se aliniază de la dreapta. unsqueeze este necesar atunci când doriți o altă aliniere a axelor și poate fi folosit explicit pentru claritate, de exemplu pentru a obține forma (1, 1, D).

t = Tensor(list(range(6)), shape=(1, 3, 1, 2))
s = t.squeeze()
v = Tensor([1, 2, 3])
u = v.unsqueeze(0)

Pasul 3: transpose și permute

transpose interschimbă două axe. permute reordonează toate axele. Astfel realizați conversia dintre NCHW și NHWC.

mat = Tensor(list(range(6)), shape=(2, 3))
tr = mat.transpose(0, 1)

t4d = Tensor(list(range(24)), shape=(1, 2, 3, 4))
perm = t4d.permute((0, 2, 3, 1))

În PyTorch, după transpose sau permute, tensorul este de regulă un view necontiguu; view nu funcționează pentru asemenea tensori, deci folosiți reshape sau apelați mai întâi .contiguous(). Clasa didactică din code/tensors.py are însă o semantică diferită: implementarea ei construiește o listă nouă, copiază datele și recalculează pași contigui, deci nu modelează fidel view-urile necontigue din PyTorch.

Pasul 4: operații element cu element și reduceri

Operațiile element cu element, precum adunarea, înmulțirea și scăderea, se aplică independent fiecărui element și păstrează forma. Reducerile, precum suma, media și maximul, elimină una sau mai multe axe.

a = Tensor([[1, 2], [3, 4]])
b = Tensor([[10, 20], [30, 40]])
c = a + b
d = a * 2
s = a.sum(axis=0)

Agregarea globală prin medie într-o CNN: (B, C, H, W).mean(axis=[2, 3]) produce (B, C). Agregarea unei secvențe prin medie în NLP: (B, T, D).mean(axis=1) produce (B, D).

Pasul 5: broadcasting cu NumPy

Funcția demo_broadcasting_numpy() din tensors.py prezintă tiparele de bază.

activations = np.random.randn(4, 3)
bias = np.array([0.1, 0.2, 0.3])
result = activations + bias

images = np.random.randn(2, 3, 4, 4)
scale = np.array([0.5, 1.0, 1.5]).reshape(1, 3, 1, 1)
result = images * scale

a = np.array([1, 2, 3]).reshape(-1, 1)
b = np.array([10, 20, 30, 40]).reshape(1, -1)
outer = a * b

Pentru distanțele dintre perechi calculate prin broadcasting, remodelați (M, 2) în (M, 1, 2) și (N, 2) în (1, N, 2), efectuați scăderea, ridicați la pătrat, însumați după ultima axă și extrageți rădăcina pătrată. Rezultatul are forma (M, N).

Pasul 6: operații einsum

Funcțiile demo_einsum() și demo_einsum_gallery() parcurg toate tiparele uzuale.

a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])
dot = np.einsum("i,i->", a, b)

A = np.array([[1, 2], [3, 4], [5, 6]], dtype=float)
B = np.array([[7, 8, 9], [10, 11, 12]], dtype=float)
matmul = np.einsum("ik,kj->ij", A, B)

batch_A = np.random.randn(4, 3, 5)
batch_B = np.random.randn(4, 5, 2)
batch_mm = np.einsum("bij,bjk->bik", batch_A, batch_B)

Costul de calcul al unei contracții este produsul dimensiunilor tuturor indicilor, atât cei păstrați, cât și cei însumați. Pentru bij,bjk->bik, cu B=32, I=128, J=64, K=128, rezultă 32 * 128 * 64 * 128 = 33,554,432 de operații de înmulțire și adunare.

Pasul 7: mecanismul de atenție prin einsum

Funcția demo_attention_einsum() implementează integral atenția cu mai multe capete.

B, H, T, D = 2, 4, 8, 16
E = H * D

X = np.random.randn(B, T, E)
W_q = np.random.randn(E, E) * 0.02

Q = np.einsum("bte,ek->btk", X, W_q)
Q = Q.reshape(B, T, H, D).transpose(0, 2, 1, 3)

scores = np.einsum("bhtd,bhsd->bhts", Q, K) / np.sqrt(D)
weights = softmax(scores, axis=-1)
attn_output = np.einsum("bhts,bhsd->bhtd", weights, V)

concat = attn_output.transpose(0, 2, 1, 3).reshape(B, T, E)
output = np.einsum("bte,ek->btk", concat, W_o)

Notă tehnică a traducerii: Fragmentul este o schiță didactică, nu un exemplu autonom executabil: K, V, W_o și softmax trebuie definite în etapele omise. Implementarea completă se află în funcția demo_attention_einsum() menționată mai sus.

Fiecare etapă este o operație tensorială: proiecție (înmulțire matricială prin einsum), separarea capetelor (reshape + transpose), scoruri de atenție (înmulțire matricială pe loturi prin einsum), sumă ponderată (înmulțire matricială pe loturi prin einsum), reunirea capetelor (transpose + reshape) și proiecția de ieșire (înmulțire matricială prin einsum).

Utilizarea implementării

Implementarea de la zero și NumPy

Operație De la zero (clasa Tensor) NumPy
Creare Tensor([[1,2],[3,4]]) np.array([[1,2],[3,4]])
Remodelare t.reshape((3,4)) a.reshape(3,4)
Transpunere t.transpose(0,1) a.T sau a.transpose(0,1)
Eliminarea axei t.squeeze(0) np.squeeze(a, 0)
Sumă t.sum(axis=0) a.sum(axis=0)
Einsum Indisponibil np.einsum("ij,jk->ik", a, b)

Implementarea de la zero și PyTorch

import torch

t = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
t.shape
t.stride()
t.is_contiguous()

t.reshape(3, 2)
t.unsqueeze(0)
t.transpose(0, 1)
t.transpose(0, 1).contiguous()

torch.einsum("ik,kj->ij", A, B)

PyTorch adaugă diferențiere automată, suport pentru GPU și nuclee BLAS optimizate. Regulile formelor urmează aceleași principii, dar modelul view-urilor și al contiguității diferă de clasa didactică. Dacă înțelegeți versiunea construită de la zero și această diferență, erorile de formă din PyTorch devin ușor de interpretat.

Fiecare strat al unei rețele neuronale ca operație tensorială

Operație Formă tensorială Einsum
Strat liniar Y = X @ W.T + b "bd,od->bo" + bias
Atenție QKV Q = X @ W_q "btd,dh->bth"
Scoruri de atenție Q @ K.T / sqrt(d) "bhtd,bhsd->bhts"
Ieșirea atenției softmax(scores) @ V "bhts,bhsd->bhtd"
Normalizare pe loturi (X - mu) / sqrt(var + epsilon) * gamma + beta element cu element + broadcasting
Softmax exp(x) / sum(exp(x)) element cu element + reducere

Livrarea rezultatului

În urma acestei lecții obțineți două prompturi reutilizabile:

  1. outputs/prompt-tensor-shapes.md – un prompt sistematic pentru depanarea nepotrivirilor dintre formele tensorilor. Include tabele de decizie pentru toate operațiile uzuale (matmul, broadcast, cat, Linear, Conv2d, BatchNorm, softmax) și un tabel de consultare a soluțiilor.

  2. outputs/prompt-tensor-debugger.md – un prompt de depanare pas cu pas, pe care îl introduceți în orice asistent IA atunci când o eroare de formă vă blochează. Furnizați-i mesajul de eroare și formele tensorilor, iar acesta vă returnează soluția exactă.

Exerciții

  1. Ușor – remodelare dus-întors. Luați un tensor de forma (2, 3, 4). Remodelați-l în (6, 4), apoi în (24,) și înapoi în (2, 3, 4). Verificați la fiecare pas dacă ordinea elementelor se păstrează, afișând datele în formă liniară.

  2. Mediu – implementați broadcastingul. Extindeți clasa Tensor cu o metodă broadcast_to(shape) care extinde dimensiunile de mărime 1 pentru a corespunde unei forme țintă. Apoi modificați _elementwise_op astfel încât să aplice automat broadcastingul înaintea operației. Testați cu formele (3, 1) și (1, 4), care trebuie să producă (3, 4).

  3. Dificil – construiți einsum de la zero. Implementați o funcție de bază einsum(subscripts, *tensors) care să accepte cel puțin produsul scalar (i,i->), înmulțirea matricială (ij,jk->ik), produsul exterior (i,j->ij) și transpunerea (ij->ji). Analizați șirul de indici, identificați indicii contractați și parcurgeți toate combinațiile de indici. Comparați rezultatele cu np.einsum.

  4. Dificil – urmărirea formelor în atenție. Scrieți o funcție care primește batch_size, seq_len, embed_dim și num_heads și afișează forma exactă din fiecare etapă a atenției cu mai multe capete: intrare, proiecția Q/K/V, separarea capetelor, scorurile de atenție, ponderile softmax, suma ponderată, reunirea capetelor și proiecția de ieșire. Verificați rezultatul prin comparație cu ieșirea funcției demo_attention_einsum().

Termeni-cheie

Termen Formulare uzuală Ce înseamnă de fapt
Tensor „O matrice cu mai multe dimensiuni” Un tablou multidimensional cu tip uniform, formă și pași de memorie definiți și operații asociate.
Rang „Numărul de dimensiuni” Numărul de axe. O matrice are rang tensorial 2, care nu trebuie confundat cu rangul său matricial.
Formă „Dimensiunea tensorului” Un tuplu care enumeră mărimea de-a lungul fiecărei axe. (2, 3) înseamnă 2 linii și 3 coloane.
Pas de memorie „Organizarea memoriei” Deplasarea, exprimată în număr de elemente, necesară pentru a avansa cu o poziție de-a lungul fiecărei axe.
Broadcasting „Funcționează pur și simplu când formele diferă” Un set strict de reguli: formele se aliniază de la dreapta, iar dimensiunile trebuie să fie egale sau una dintre ele să fie 1.
Contiguu „Tensorul este normal” Elementele sunt stocate secvențial în memorie, fără spații sau reordonări față de organizarea logică.
Einsum „O metodă sofisticată de a scrie matmul” O notație generală care exprimă într-o singură linie orice contracție tensorială, produs exterior, urmă sau transpunere.
View „Este același lucru cu reshape” Un tensor care folosește aceeași zonă de memorie, dar are metadate diferite pentru formă și pași. Nu funcționează pentru date necontigue.
Contracție „Însumarea după un indice” Operația generală prin care valorile corespunzătoare unui indice comun al tensorilor sunt înmulțite și însumate. Ordinul rezultatului este determinat de indicii liberi rămași și nu trebuie să fie neapărat mai mic decât ordinul fiecărui operand.
NCHW / NHWC „Formatul PyTorch față de TensorFlow” Convenții privind ordinea logică a axelor tensorilor de imagini. NCHW plasează axa canalelor înaintea axelor spațiale, iar NHWC după acestea; convenția nu garantează singură o anumită dispunere fizică în memorie.

Lecturi suplimentare


Sursă: Tensor Operations — original

Navigare: înapoi: 01.11 — Descompunerea în valori singulare · Faza 1 — Fundamente matematice · Catalog complet · în continuare: 01.13 — Stabilitate numerică.