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ă.
Cuprinsul lecției
- Obiective de învățare
- Problema
- Conceptul
- Ce este un tensor
- Formele tensorilor în învățarea profundă
- Organizarea datelor în memorie
- Regulile de broadcasting
- Einsum: operația tensorială universală
- Construirea implementării
- Pasul 1: stocarea tensorului și pașii de memorie
- Pasul 2: reshape, squeeze și unsqueeze
- Pasul 3: transpose și permute
- Pasul 4: operații element cu element și reduceri
- Pasul 5: broadcasting cu NumPy
- Pasul 6: operații einsum
- Pasul 7: mecanismul de atenție prin einsum
- Utilizarea implementării
- Implementarea de la zero și NumPy
- Implementarea de la zero și PyTorch
- Fiecare strat al unei rețele neuronale ca operație tensorială
- Livrarea rezultatului
- Exerciții
- Termeni-cheie
- 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.
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.
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.
Î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.
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șisoftmaxtrebuie definite în etapele omise. Implementarea completă se află în funcțiademo_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:
-
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. -
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
-
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ă. -
Mediu – implementați broadcastingul. Extindeți clasa
Tensorcu o metodăbroadcast_to(shape)care extinde dimensiunile de mărime 1 pentru a corespunde unei forme țintă. Apoi modificați_elementwise_opastfel încât să aplice automat broadcastingul înaintea operației. Testați cu formele(3, 1)și(1, 4), care trebuie să producă(3, 4). -
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 cunp.einsum. -
Dificil – urmărirea formelor în atenție. Scrieți o funcție care primește
batch_size,seq_len,embed_dimșinum_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țieidemo_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
- NumPy Broadcasting – regulile canonice, cu exemple vizuale
- PyTorch Tensor Views – situațiile în care view-urile funcționează și cele în care copiază datele
- einops – bibliotecă ce face remodelarea tensorilor lizibilă și sigură
- The Illustrated Transformer – vizualizarea formelor tensorilor care circulă prin mecanismul de atenție
- Einstein Summation in NumPy – documentația completă pentru einsum, cu exemple
Sursă: Tensor Operations — original
Navigare: înapoi: 01.11 — Descompunerea în valori singulare · Faza 1 — Fundamente matematice · Catalog complet · în continuare: 01.13 — Stabilitate numerică.