ELAI S.r.l.

Stessi numeri, risultato sbagliato: perché l’ordine dei pixel conta per l’AI

Un esempio riproducibile mostra perché cambiare forma a un’immagine non equivale a riordinarne i canali, e quali verifiche servono nell’AI embedded.

Stessi numeri, risultato sbagliato: perché l’ordine dei pixel conta per l’AI

La scheda riceve tutti i dati. Perché il risultato cambia?

Una telecamera consegna un’immagine a un programma di visione: il numero di pixel è giusto, il buffer contiene tutti i byte e nessun valore è stato perso. Eppure il primo calcolo del modello produce numeri diversi da quelli attesi. Prima di accusare l’acceleratore o riaddestrare la rete, esiste una domanda più elementare: i tre numeri che il programma interpreta come canali di un pixel appartengono davvero a quel pixel? Questo articolo sviluppa un caso minimo nel quale il controllo delle dimensioni passa ma il significato dei dati cambia.

Impareremo a separare tre operazioni: rinominare le dimensioni, permutare gli assi e rendere contigui i dati in memoria. Il contributo è un’analisi didattica eseguita su computer con Python 3.14.0 e NumPy 2.5.3, non una prova su microcontrollore o su un prodotto EL-AI. Non riportiamo latenza, energia o accuratezza di una rete addestrata: il filtro lineare scelto serve a rendere visibile ogni passaggio senza nasconderlo dentro migliaia di pesi.

Un’immagine è una tabella con più indirizzi

Un tensore è qui semplicemente una collezione di numeri indicizzati da più coordinate. N indica quante immagini trattiamo insieme, H l’altezza, W la larghezza e C i canali. La sigla NHWC significa che scriviamo le coordinate nell’ordine immagine, riga, colonna, canale; NCHW mette il canale prima delle coordinate spaziali. Non sono due immagini differenti: sono due convenzioni per riferirsi agli stessi valori. Il requisito corretto è y[n, c, h, w]=x[n, h, w, c], per ogni coordinata valida, non soltanto un uguale numero di elementi.

Usiamo una sola immagine di due righe e due colonne, con tre canali sintetici. In ordine di lettura i pixel sono (1,10,100), (2,20,200), (3,30,300), (4,40,400). I valori sono volutamente distinti: rendono riconoscibile uno scambio. Non sono una fotografia RGB a otto bit: 400 supera 255 e perciò scegliamo interi int16, due byte per elemento. L’immagine contiene 12 elementi e 24 byte di contenuto numerico. Quest’ultimo conteggio non include metadati Python, allocatore o altri buffer.

Cambiare forma non significa cambiare ordine

reshape(1,3,2,2), con ordine C predefinito, rilegge la sequenza e la distribuisce nella nuova forma facendo avanzare più rapidamente l’ultimo indice. Nel nostro caso la sequenza resta 1,10,100,2,20,200,3,30,300,4,40,400. Il primo canale diventa quindi [1,10,100,2], non [1,2,3,4]. Le dimensioni sono compatibili, ma abbiamo mescolato valori che prima indicavano canali e pixel diversi. La documentazione NumPy descrive l’ordine di indicizzazione: non promette una conversione semantica fra formati di immagini.

transpose(0,3,1,2), invece, assegna al nuovo asse 0 il vecchio 0, al nuovo 1 il vecchio 3 e così via. Conserva esattamente l’uguaglianza fra coordinate desiderata. Nell’esperimento restituisce una vista che condivide i dati originali: nessun contenuto viene copiato in quel passaggio. Non generalizziamo questa osservazione a ogni libreria o dispositivo. La distinzione importante è tra il modo logico di accedere al dato e la sua disposizione fisica; la vista NumPy porta con sé informazioni che un’interfaccia basata soltanto su un puntatore potrebbe non ricevere.

Dove finiscono davvero i byte?

Per capire l’interfaccia, calcoliamo prima gli indirizzi nel caso contiguo. Poniamo b=2 byte per elemento e usiamo indici che partono da zero. Il seguente offset misura la distanza in byte dall’inizio del buffer; non è un indirizzo assoluto. Il formato stabilisce quale indice avanza più velocemente. Per una vista non contigua queste formule non bastano: bisogna usare gli stride effettivi, cioè gli spostamenti in byte associati a un incremento di ciascun indice.

offset_NHWC = (((n H + h) W + w) C + c) b offset_NCHW = (((n C + c) H + h) W + w) b

L’ingresso ha stride (24,12,6,2) byte; la vista NCHW ha (24,2,12,6). Cambiare canale nella vista avanza di soli due byte, mentre cambiare colonna ne avanza sei. La copia NCHW contigua ha invece (24,8,4,2): ogni canale occupa un piano di quattro elementi. ascontiguousarray applicata alla vista produce questa copia corretta. Applicata al reshape sbagliato non ricostruisce i pixel: rendere contiguo un errore di significato lo conserva. Nel caso eseguito reshape e copia corretta hanno perfino gli stessi stride, ma valori diversi alle stesse coordinate.

Un filtro che possiamo controllare a mano

Ora chiediamo quanto cambi un calcolo. Il filtro assegna pesi 1,2,3 ai tre canali e li somma per ogni pixel. Non applica attivazioni, normalizzazione o bias. Per il primo pixel corretto otteniamo 1+2·10+3·100=321. Nel reshape, i tre valori nella stessa posizione sono 1,20,300: il risultato diventa 1+40+900=941. Non è un errore numerico di arrotondamento: stiamo moltiplicando dati diversi. L’accumulo usa int64 per separare questo problema da un possibile overflow degli interi.

output[h,w] = Σ_(c=0..2) weight[c] · input[c,h,w] weight = (1,2,3)
PixelCorrettoReshape
(0,0)321941
(0,1)642422
(1,0)963226
(1,1)12841262
Confrontare le due barre di ciascun pixel, non soltanto la somma: tutte e quattro le uscite cambiano. Valori sintetici, unità arbitrarie; nessuna misura di accuratezza o hardware.
Confrontare le due barre di ciascun pixel, non soltanto la somma: tutte e quattro le uscite cambiano. Valori sintetici, unità arbitrarie; nessuna misura di accuratezza o hardware.

La somma delle uscite passa da 3210 a 2851, ma neppure conservarla sarebbe una prova sufficiente: permutare le posizioni può lasciare invariata una somma e alterare la mappa spaziale. Allo stesso modo, un ingresso fatto soltanto di uno nasconde l’errore: ogni raggruppamento produce 1+2+3=6. Il codice verifica questo controesempio. Un test con immagini uniformi può quindi dare falsa tranquillità; servono valori distinti, confronto per coordinata e un riferimento noto prima della conversione.

Che cosa garantisce il test, e che cosa manca sulla scheda

L’esperimento controlla tutti i dodici abbinamenti di coordinate, l’inversa della permutazione, l’uguaglianza della copia contigua e la condivisione della memoria. La permutazione logica cambia metadati; materializzare una copia di M elementi richiede leggere e scrivere quei dati, quindi lavoro proporzionale a M e un buffer aggiuntivo nel caso mostrato. I 24 byte di ogni array descrivono il contenuto, non il picco RAM del processo: viste condivise e copie hanno conseguenze diverse. Non ricaviamo tempo o potenza da questi conteggi.

Sul dispositivo resta da verificare il contratto preciso del runtime: ordine degli assi, tipo, contiguità, stride supportati, vita del buffer e trasformazioni di preprocessing. Una conversione NHWC/NCHW non scambia automaticamente rosso e blu, non ridimensiona l’immagine e non applica scale o medie di normalizzazione. Non proponiamo una regola universale “copiate sempre”: una copia inutile può costare memoria, mentre ometterla dove l’interfaccia la richiede può cambiare ciò che viene letto. L’alternativa corretta dipende dal contratto, da misurare sulla piattaforma reale.

La risposta: contare i byte non basta

Gli stessi numeri possono produrre un risultato sbagliato perché il modello non legge una collezione senza ordine: legge valori con un significato per ogni coordinata. Il caso eseguito distingue tre garanzie: reshape rende compatibile una forma, transpose conserva la corrispondenza degli assi, la copia contigua soddisfa una disposizione fisica. Nessuna sostituisce automaticamente le altre. Prima di ottimizzare l’inferenza embedded, rendere esplicita e verificabile questa corrispondenza evita di cercare nell’addestramento un errore nato nell’interfaccia.

Fonti e materiale eseguito

NumPy 2.5 — reshape: indexing order, views and copies.

NumPy 2.5 — transpose: axis correspondence.

NumPy 2.5 — ascontiguousarray: contiguous shape and content.

import numpy as np
x=np.array([[[[1,10,100],[2,20,200]],[[3,30,300],[4,40,400]]]],dtype=np.int16)
y=x.transpose(0,3,1,2)
z=x.reshape(1,3,2,2)
w=np.array([1,2,3],dtype=np.int64)[None,:,None,None]
print((y.astype(np.int64)*w).sum(axis=1))
print((z.astype(np.int64)*w).sum(axis=1))
assert np.array_equal(y.transpose(0,2,3,1),x)

Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 24 ottobre 2026.