I quadri di deep learning sono estremamente transitori. Se confronti i framework di deep learning utilizzati oggi con quelli di otto anni fa, scoprirai che il panorama è completamente diverso. C'erano Theano, Caffe2 e MXNet, che diventarono tutti obsoleti. I framework più popolari di oggi, come TensorFlow e PyTorch, sono stati appena rilasciati al pubblico.
In tutti questi anni, Keras è sopravvissuta come libreria di alto livello rivolta agli utenti che supporta diversi backend, tra cui TensorFlow, PyTorch e JAX. Come collaboratore di Keras, ho imparato quanto il team si preoccupi dell'esperienza utente per il software e come garantiscano una buona esperienza utente seguendo alcuni principi semplici ma potenti nel processo di progettazione.
In questo articolo condividerò i 3 principi di progettazione software più importanti che ho imparato contribuendo a Keras negli ultimi anni, che possono essere generalizzabili a tutti i tipi di software e aiutarti ad avere un impatto nella comunità open source con il tuo .
Perché l'esperienza dell'utente è importante per il software open source
Prima di immergerci nel contenuto principale, discutiamo rapidamente perché l'esperienza dell'utente è così importante. Possiamo apprenderlo attraverso il caso PyTorch vs. TensorFlow.
Sono stati sviluppati da due giganti della tecnologia, Meta e Google, e hanno punti di forza culturali piuttosto diversi. Meta è bravo nel prodotto, mentre Google è bravo nell'ingegneria. Di conseguenza, i framework di Google come TensorFlow e JAX sono i più veloci da eseguire e tecnicamente superiori a PyTorch, poiché supportano bene tensori sparsi e addestramento distribuito. Tuttavia, PyTorch ha comunque sottratto metà della quota di mercato a TensorFlow perché dà priorità all'esperienza dell'utente rispetto ad altri aspetti del software.
Una migliore esperienza utente è vantaggiosa per i ricercatori che costruiscono i modelli e li propagano agli ingegneri, che ne prendono modelli poiché non sempre vogliono convertire i modelli che ricevono dai ricercatori in un altro framework. Costruiranno un nuovo software attorno a PyTorch per semplificare il loro flusso di lavoro, stabilendo un ecosistema software attorno a PyTorch.
TensorFlow ha anche commesso alcuni errori che hanno causato la perdita dei suoi utenti. L'esperienza utente generale di TensorFlow è buona. Tuttavia, la sua guida di installazione per il supporto GPU è stata interrotta per anni prima di essere corretta nel 2022. TensorFlow 2 ha interrotto la compatibilità con le versioni precedenti, la cui migrazione è costata ai suoi utenti milioni di dollari.
Quindi, la lezione che abbiamo imparato qui è che, nonostante la superiorità tecnica, l’esperienza dell’utente decide quale software sceglieranno gli utenti open source.
Tutti i framework di deep learning investono molto nell’esperienza dell’utente
Tutti i framework di deep learning (TensorFlow, PyTorch e JAX) investono molto nell'esperienza dell'utente. Una buona prova è che tutti hanno una percentuale Python relativamente alta nelle loro basi di codice.
Tutta la logica fondamentale dei framework di deep learning, comprese le operazioni tensoriali, la differenziazione automatica, la compilazione e la distribuzione, è implementata in C++. Perché dovrebbero voler esporre una serie di API Python agli utenti? È solo perché gli utenti amano Python e vogliono migliorare la loro esperienza utente.
Investire nell’esperienza utente ha un ROI elevato
Immagina quanto impegno ingegneristico sia necessario per rendere il tuo framework di deep learning un po' più veloce degli altri. Molto.
Tuttavia, per una migliore esperienza utente, purché si segua un determinato processo di progettazione e alcuni principi, è possibile ottenerla. Per attirare più utenti, la tua esperienza utente è importante quanto l'efficienza computazionale del tuo framework. Pertanto, investire nell’esperienza utente ha un elevato ritorno sull’investimento (ROI).
I tre principi
Condividerò i tre importanti principi di progettazione del software che ho imparato contribuendo a Keras, ciascuno con esempi di codice buoni e cattivi provenienti da diversi framework.
Principio 1: progettare flussi di lavoro end-to-end
Quando pensiamo di progettare le API di un software, potresti assomigliare a questo.
class Model:
def __call__(self, input):
"""The forward call of the model.Args:
input: A tensor. The input to the model.
"""
pass
Definire la classe e aggiungere la documentazione. Ora conosciamo tutti i nomi delle classi, dei metodi e degli argomenti. Questo però non ci aiuterebbe a capire molto dell’esperienza dell’utente.
Quello che dovremmo fare è qualcosa del genere.
input = keras.Input(shape=(10,))
x = layers.Dense(32, activation='relu')(input)
output = layers.Dense(10, activation='softmax')(x)
model = keras.models.Model(inputs=input, outputs=output)
model.compile(
optimizer='adam', loss='categorical_crossentropy'
)
Vogliamo scrivere l'intero flusso di lavoro dell'utente relativo all'utilizzo del software. Idealmente, dovrebbe essere un tutorial su come utilizzare il software. Fornisce molte più informazioni sull'esperienza dell'utente. Potrebbe aiutarci a individuare molti più problemi UX durante la fase di progettazione rispetto alla semplice scrittura della classe e dei metodi.
Diamo un'occhiata a un altro esempio. È così che ho scoperto un problema di esperienza utente seguendo questo principio durante l'implementazione di KerasTuner.
Quando si utilizza KerasTuner, gli utenti possono utilizzare questa classe RandomSearch per selezionare il modello migliore. Abbiamo le metriche e gli obiettivi negli argomenti. Per impostazione predefinita, l'obiettivo equivale alla perdita di convalida. Quindi, ci aiuta a trovare il modello con la minore perdita di convalida.
class RandomSearch:
def __init__(self, ..., metrics, objective="val_loss", ...):
"""The initializer.Args:
metrics: A list of Keras metrics.
objective: String or a custom metric function. The
name of the metirc we want to minimize.
"""
pass
Ancora una volta, non fornisce molte informazioni sull'esperienza dell'utente. Quindi per ora sembra tutto a posto.
Tuttavia, se scriviamo un flusso di lavoro end-to-end come il seguente. Espone molti più problemi. L'utente sta tentando di definire una funzione metrica personalizzata denominata custom_metric. L'obiettivo non è più così semplice da usare. Cosa dovremmo passare ora all’argomento oggettivo?
tuner = RandomSearch(
...,
metrics=(custom_metric),
objective="val_???",
)
Dovrebbe essere giusto "val_custom_metric”
. Basta usare il prefisso di "val_"
e il nome della funzione metrica. Non è abbastanza intuitivo. Vogliamo migliorarlo invece di forzare l'utente a impararlo. Abbiamo individuato facilmente un problema relativo all'esperienza utente scrivendo questo flusso di lavoro.
Se hai scritto il progetto in modo più completo includendo l'implementazione del file custom_metric
funzione, scoprirai che dovrai persino imparare a scrivere una metrica personalizzata Keras. Devi seguire la firma della funzione per farla funzionare, come mostrato nel seguente frammento di codice.
def custom_metric(y_true, y_pred):
squared_diff = ops.square(y_true - y_pred)
return ops.mean(squared_diff, axis=-1)
Dopo aver scoperto questo problema. Abbiamo progettato appositamente un flusso di lavoro migliore per le metriche personalizzate. Devi solo eseguire l'override HyperModel.fit()
per calcolare la metrica personalizzata e restituirla. Nessuna stringa per nominare l'obiettivo. Nessuna firma di funzione da seguire. Solo un valore di ritorno. L'esperienza dell'utente è molto migliore in questo momento.
class MyHyperModel(HyperModel):
def fit(self, trial, model, validation_data):
x_val, y_true = validation_data
y_pred = model(x_val)
return custom_metric(y_true, y_pred)tuner = RandomSearch(MyHyperModel(), max_trials=20)
Un’altra cosa da ricordare è che dovremmo sempre partire dall’esperienza dell’utente. I flussi di lavoro progettati si propagano all'implementazione.
Principio 2: ridurre al minimo il carico cognitivo
Non forzare l'utente ad apprendere nulla a meno che non sia realmente necessario. Vediamo alcuni buoni esempi.
L'API di modellazione Keras è un buon esempio mostrato nel seguente frammento di codice. I costruttori di modelli hanno già in mente questi concetti, ad esempio un modello è una pila di livelli. Ha bisogno di una funzione di perdita. Possiamo adattarlo ai dati o fargli prevedere i dati.
model = keras.Sequential((
layers.Dense(10, activation="relu"),
layers.Dense(num_classes, activation="softmax"),
))
model.compile(loss='categorical_crossentropy')
model.fit(...)
model.predict(...)
Quindi, in sostanza, non sono stati appresi nuovi concetti per utilizzare Keras.
Un altro buon esempio è la modellazione PyTorch. Il codice viene eseguito proprio come il codice Python. Tutti i tensori sono solo tensori reali con valori reali. Puoi fare affidamento sul valore di un tensore per decidere il tuo percorso con un semplice codice Python.
class MyModel(nn.Module):
def forward(self, x):
if x.sum() > 0:
return self.path_a(x)
return self.path_b(x)
Puoi farlo anche con Keras con TensorFlow o backend JAX ma deve essere scritto diversamente. Tutti i if
le condizioni devono essere scritte con questo ops.cond
funzione come mostrato nel seguente frammento di codice.
class MyModel(keras.Model):
def call(self, inputs):
return ops.cond(
ops.sum(inputs) > 0,
lambda : self.path_a(inputs),
lambda : self.path_b(inputs),
)
Questo insegna all'utente a imparare una nuova operazione invece di usare la clausola if-else con cui ha familiarità, il che è negativo. In compenso, porta un miglioramento significativo nella velocità di allenamento.
Ecco il problema della flessibilità di PyTorch. Se mai avessi bisogno di ottimizzare la memoria e la velocità del tuo modello, dovresti farlo da solo utilizzando le seguenti API e nuovi concetti per farlo, inclusi gli argomenti inplace per le operazioni, le API delle operazioni parallele e il posizionamento esplicito del dispositivo . Introduce una curva di apprendimento piuttosto elevata per gli utenti.
torch.relu(x, inplace=True)
x = torch._foreach_add(x, y)
torch._foreach_add_(x, y)
x = x.cuda()
Alcuni altri buoni esempi lo sono keras.ops
, tensorflow.numpy
, jax.numpy
. Sono solo una reimplementazione dell'API Numpy. Quando introduci un carico cognitivo, riutilizza semplicemente ciò che le persone già sanno. Ogni framework deve fornire alcune operazioni di basso livello in questi framework. Invece di lasciare che le persone imparino un nuovo set di API, che possono avere un centinaio di funzioni, usano semplicemente le API esistenti più popolari. Le API Numpy sono ben documentate e contengono tantissime domande e risposte Stack Overflow ad esse correlate.
La cosa peggiore che puoi fare con l'esperienza utente è ingannare gli utenti. Induci l'utente a credere che la tua API sia qualcosa con cui ha familiarità, ma non lo è. Farò due esempi. Uno è su PyTorch. L'altro è su TensorFlow.
Cosa dovremmo passare come argomento pad in F.pad()
funzione se si desidera riempire il tensore di input della forma (100, 3, 32, 32)
A (100, 3, 1+32+1, 2+32+2)
O (100, 3, 34, 36)
?
import torch.nn.functional as F
# pad the 32x32 images to (1+32+1)x(2+32+2)
# (100, 3, 32, 32) to (100, 3, 34, 36)
out = F.pad(
torch.empty(100, 3, 32, 32),
pad=???,
)
La mia prima intuizione è che dovrebbe esserlo ((0, 0), (0, 0), (1, 1), (2, 2))
dove ciascuna sottotupla corrisponde a una delle 4 dimensioni e i due numeri rappresentano la dimensione del riempimento prima e dopo i valori esistenti. La mia ipotesi è originata dall'API Numpy.
Tuttavia, la risposta corretta è (2, 2, 1, 1). Non esiste una sottotupla, ma una semplice tupla. Inoltre le dimensioni sono invertite. L'ultima dimensione va alla prima.
Quello che segue è un cattivo esempio di TensorFlow. Riesci a indovinare qual è l'output del seguente frammento di codice?
value = True@tf.function
def get_value():
return value
value = False
print(get_value())
Senza il tf.function
decoratore, l'output dovrebbe essere False, il che è piuttosto semplice. Tuttavia, con il decoratore, l'output è True. Questo perché TensorFlow compila la funzione e qualsiasi variabile Python viene compilata in una nuova costante. La modifica del valore della vecchia variabile non influenzerebbe la costante creata.
Induce l'utente a credere che sia il codice Python con cui ha familiarità, ma in realtà non lo è.
Principio 3: Interazione sulla documentazione
A nessuno piace leggere una lunga documentazione se riesce a capirla semplicemente eseguendo qualche codice di esempio e modificandolo da solo. Quindi, cerchiamo di fare in modo che il flusso di lavoro dell'utente del software segua la stessa logica.
Ecco un buon esempio mostrato nel seguente frammento di codice. In PyTorch, tutti i metodi con il carattere di sottolineatura sono operazioni sul posto, mentre quelli senza non lo sono. Da un punto di vista interattivo, sono utili perché sono facili da seguire e gli utenti non hanno bisogno di controllare la documentazione ogni volta che desiderano la versione inplace di un metodo. Tuttavia, ovviamente, hanno introdotto un carico cognitivo. Gli utenti devono sapere cosa significa inplace e quando utilizzarli.
x = x.add(y)
x.add_(y)
x = x.mul(y)
x.mul_(y)
Un altro buon esempio sono gli strati Keras. Seguono rigorosamente la stessa convenzione di denominazione mostrata nel seguente frammento di codice. Con una convenzione di denominazione chiara, gli utenti possono ricordare facilmente i nomi dei livelli senza controllare la documentazione.
from keras import layerslayers.MaxPooling2D()
layers.GlobalMaxPooling1D()
layers.GlobalAveragePooling3D()
Un'altra parte importante dell'interazione tra l'utente e il software è il messaggio di errore. Non puoi aspettarti che l'utente scriva tutto correttamente la prima volta. Dovremmo sempre fare i controlli necessari nel codice e provare a stampare messaggi di errore utili.
Vediamo i due esempi seguenti mostrati nello snippet di codice. Il primo non contiene molte informazioni. Dice solo mancata corrispondenza della forma del tensore. IL
il secondo contiene informazioni molto più utili all'utente per individuare il bug. Non solo indica che l'errore è dovuto alla mancata corrispondenza della forma del tensore, ma mostra anche qual è la forma prevista e quale è la forma sbagliata ricevuta. Se non intendevi superare quella forma, hai un'idea migliore
del bug adesso.
# Bad example:
raise ValueError("Tensor shape mismatch.")# Good example:
raise ValueError(
"Tensor shape mismatch. "
"Expected: (batch, num_features). "
f"Received: {x.shape}"
)
Il miglior messaggio di errore indirizzerebbe direttamente l'utente alla correzione. Il seguente frammento di codice mostra un messaggio di errore generale di Python. Ha indovinato cosa c'era di sbagliato nel codice e ha indirizzato direttamente l'utente alla correzione.
import mathmath.sqr(4)
"AttributeError: module 'math' has no attribute 'sqr'. Did you mean: 'sqrt'?"
Parole finali
Finora abbiamo introdotto i tre principi di progettazione software più preziosi che ho imparato contribuendo ai framework di deep learning. Innanzitutto, scrivi flussi di lavoro end-to-end per scoprire ulteriori problemi relativi all'esperienza utente. In secondo luogo, ridurre il carico cognitivo e non insegnare nulla all’utente se non necessario. In terzo luogo, segui la stessa logica nella progettazione dell'API e invia messaggi di errore significativi in modo che gli utenti possano apprendere il tuo software interagendo con esso invece di controllare costantemente la documentazione.
Tuttavia, ci sono molti altri principi da seguire se vuoi migliorare ulteriormente il tuo software. Puoi fare riferimento a Linee guida per la progettazione dell'API Keras come guida completa alla progettazione dell'API.
Fonte: towardsdatascience.com