0ki Swjoadwtbgkh2.jpeg

Los tres principios de diseño de software que aprendí como colaborador de código abierto

Foto por sheldon en desempaquetar

Los marcos de aprendizaje profundo son extremadamente transitorios. Si compara los marcos de aprendizaje profundo que la gente usa hoy con los que eran hace ocho años, encontrará que el panorama es completamente diferente. Estaban Theano, Caffe2 y MXNet, que quedaron obsoletos. Los frameworks más populares de la actualidad, como TensorFlow y PyTorch, acaban de ser lanzados al público.

A lo largo de todos estos años, Keras ha sobrevivido como una biblioteca de alto nivel orientada al usuario que admite diferentes backends, incluidos TensorFlow, PyTorch y JAX. Como colaborador de Keras, aprendí cuánto se preocupa el equipo por la experiencia del usuario del software y cómo garantizan una buena experiencia de usuario siguiendo algunos principios simples pero poderosos en su proceso de diseño.

En este artículo, compartiré los 3 principios de diseño de software más importantes que aprendí contribuyendo a Keras durante los últimos años, que pueden generalizarse a todo tipo de software y ayudarlo a generar un impacto en la comunidad de código abierto con el suyo. .

Por qué la experiencia del usuario es importante para el software de código abierto

Antes de profundizar en el contenido principal, analicemos rápidamente por qué la experiencia del usuario es tan importante. Podemos aprender esto a través del caso PyTorch vs. TensorFlow.

Fueron desarrollados por dos gigantes tecnológicos, Meta y Google, y tienen fortalezas culturales bastante diferentes. Meta es bueno en productos, mientras que Google es bueno en ingeniería. Como resultado, los marcos de Google como TensorFlow y JAX son los más rápidos de ejecutar y técnicamente superiores a PyTorch, ya que admiten tensores dispersos y entrenamiento distribuido. Sin embargo, PyTorch aún le quitó la mitad de la participación de mercado a TensorFlow porque prioriza la experiencia del usuario sobre otros aspectos del software.

Una mejor experiencia de usuario gana para los científicos investigadores que construyen los modelos y los propagan a los ingenieros, quienes toman modelos de ellos ya que no siempre quieren convertir los modelos que reciben de los científicos investigadores a otro marco. Crearán nuevo software en torno a PyTorch para facilitar su flujo de trabajo, lo que establecerá un ecosistema de software en torno a PyTorch.

TensorFlow también cometió algunos errores que hicieron perder a sus usuarios. La experiencia general del usuario de TensorFlow es buena. Sin embargo, su guía de instalación para compatibilidad con GPU estuvo rota durante años antes de que se solucionara en 2022. TensorFlow 2 rompió la compatibilidad con versiones anteriores, lo que costó a sus usuarios millones de dólares migrar.

Entonces, la lección que aprendimos aquí es que a pesar de la superioridad técnica, la experiencia del usuario decide qué software elegirían los usuarios de código abierto.

Todos los marcos de aprendizaje profundo invierten mucho en la experiencia del usuario

Todos los marcos de aprendizaje profundo (TensorFlow, PyTorch y JAX) invierten mucho en la experiencia del usuario. Una buena evidencia es que todos tienen un porcentaje relativamente alto de Python en sus bases de código.

Toda la lógica central de los marcos de aprendizaje profundo, incluidas las operaciones tensoriales, la diferenciación automática, la compilación y la distribución, se implementan en C++. ¿Por qué querrían exponer un conjunto de API de Python a los usuarios? Es simplemente porque a los usuarios les encanta Python y quieren pulir su experiencia de usuario.

Invertir en la experiencia del usuario tiene un alto retorno de la inversión

Imagínese cuánto esfuerzo de ingeniería se requiere para hacer que su marco de aprendizaje profundo sea un poco más rápido que otros. Mucho.

Sin embargo, para una mejor experiencia de usuario, siempre que sigas un determinado proceso de diseño y algunos principios, podrás lograrlo. Para atraer a más usuarios, su experiencia de usuario es tan importante como la eficiencia informática de su marco. Por tanto, invertir en la experiencia del usuario genera un alto retorno de la inversión (ROI).

Los tres principios

Compartiré los tres principios importantes de diseño de software que aprendí al contribuir a Keras, cada uno con ejemplos de código buenos y malos de diferentes marcos.

Principio 1: Diseñar flujos de trabajo de un extremo a otro

Cuando pensamos en diseñar las API de una pieza de software, es posible que se vea así.

class Model:
def __call__(self, input):
"""The forward call of the model.

Args:
input: A tensor. The input to the model.
"""
pass

Defina la clase y agregue la documentación. Ahora conocemos todos los nombres de clases, nombres de métodos y argumentos. Sin embargo, esto no nos ayudaría a comprender mucho sobre la experiencia del usuario.

Lo que deberíamos hacer es algo como esto.

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'
)

Queremos escribir todo el flujo de trabajo del usuario al utilizar el software. Lo ideal sería que fuera un tutorial sobre cómo utilizar el software. Proporciona mucha más información sobre la experiencia del usuario. Puede ayudarnos a detectar muchos más problemas de UX durante la fase de diseño en comparación con simplemente escribir la clase y los métodos.

Veamos otro ejemplo. Así es como descubrí un problema en la experiencia del usuario al seguir este principio al implementar KerasTuner.

Al usar KerasTuner, los usuarios pueden usar esta clase RandomSearch para seleccionar el mejor modelo. Tenemos las métricas y los objetivos en los argumentos. De forma predeterminada, el objetivo equivale a la pérdida de validación. Entonces, nos ayuda a encontrar el modelo con la menor pérdida de validación.

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

Nuevamente, no proporciona mucha información sobre la experiencia del usuario. Entonces, todo parece estar bien por ahora.

Sin embargo, si escribimos un flujo de trabajo de un extremo a otro como el siguiente. Expone muchos más problemas. El usuario está intentando definir una función métrica personalizada denominada custom_metric. El objetivo ya no es tan sencillo de utilizar. ¿Qué debemos pasar ahora al argumento objetivo?

tuner = RandomSearch(
...,
metrics=[custom_metric],
objective="val_???",
)

debería ser solo "val_custom_metric”. Sólo usa el prefijo de "val_" y el nombre de la función métrica. No es lo suficientemente intuitivo. Queremos mejorarlo en lugar de obligar al usuario a aprender esto. Detectamos fácilmente un problema en la experiencia del usuario al escribir este flujo de trabajo.

Si escribiera el diseño de manera más completa al incluir la implementación del custom_metric función, descubrirá que incluso necesita aprender a escribir una métrica personalizada de Keras. Debe seguir la firma de la función para que funcione, como se muestra en el siguiente fragmento de código.

def custom_metric(y_true, y_pred):
squared_diff = ops.square(y_true - y_pred)
return ops.mean(squared_diff, axis=-1)

Después de descubrir este problema. Diseñamos especialmente un mejor flujo de trabajo para métricas personalizadas. Sólo necesitas anular HyperModel.fit() para calcular su métrica personalizada y devolverla. Sin condiciones para nombrar el objetivo. No hay firma de función a seguir. Sólo un valor de retorno. La experiencia del usuario es mucho mejor en este 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)

Una cosa más para recordar es que siempre debemos partir de la experiencia del usuario. Los flujos de trabajo diseñados se propagan hacia atrás a la implementación.

Principio 2: Minimizar la carga cognitiva

No obligues al usuario a aprender nada a menos que sea realmente necesario. Veamos algunos buenos ejemplos.

La API de modelado de Keras es un buen ejemplo que se muestra en el siguiente fragmento de código. Los creadores de modelos ya tienen estos conceptos en mente; por ejemplo, un modelo es una pila de capas. Necesita una función de pérdida. Podemos ajustarlo con datos o hacer que prediga sobre datos.

model = keras.Sequential([
layers.Dense(10, activation="relu"),
layers.Dense(num_classes, activation="softmax"),
])
model.compile(loss='categorical_crossentropy')
model.fit(...)
model.predict(...)

Básicamente, no se aprendieron conceptos nuevos sobre el uso de Keras.

Otro buen ejemplo es el modelado de PyTorch. El código se ejecuta igual que el código Python. Todos los tensores son simplemente tensores reales con valores reales. Puedes depender del valor de un tensor para decidir tu camino con código Python simple.

class MyModel(nn.Module):
def forward(self, x):
if x.sum() > 0:
return self.path_a(x)
return self.path_b(x)

También puede hacer esto con Keras con TensorFlow o JAX backend, pero debe escribirse de manera diferente. Todos if las condiciones deben escribirse con esto ops.cond funcionar como se muestra en el siguiente fragmento de código.

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),
)

Esto le enseña al usuario a aprender una nueva operación en lugar de usar la cláusula if-else con la que está familiarizado, lo cual es malo. En compensación, aporta una mejora significativa en la velocidad del entrenamiento.

Aquí está el problema de la flexibilidad de PyTorch. Si alguna vez necesitara optimizar la memoria y la velocidad de su modelo, tendría que hacerlo usted mismo utilizando las siguientes API y nuevos conceptos para hacerlo, incluidos los argumentos in situ para las operaciones, las API de operaciones paralelas y la ubicación explícita del dispositivo. . Introduce una curva de aprendizaje bastante alta para los usuarios.

torch.relu(x, inplace=True)
x = torch._foreach_add(x, y)
torch._foreach_add_(x, y)
x = x.cuda()

Algunos otros buenos ejemplos son keras.ops, tensorflow.numpy, jax.numpy. Son solo una reimplementación de la API numpy. Al introducir alguna carga cognitiva, simplemente reutilice lo que la gente ya sabe. Cada marco debe proporcionar algunas operaciones de bajo nivel en estos marcos. En lugar de permitir que las personas aprendan un nuevo conjunto de API, que pueden tener cientos de funciones, simplemente usan la API existente más popular para ello. Las numerosas API están bien documentadas y tienen toneladas de preguntas y respuestas de Stack Overflow relacionadas.

Lo peor que puedes hacer con la experiencia del usuario es engañar a los usuarios. Engañe al usuario haciéndole creer que su API es algo con lo que está familiarizado, pero no lo es. Daré dos ejemplos. Uno está en PyTorch. El otro está en TensorFlow.

¿Qué deberíamos pasar como argumento pad en F.pad() función si desea rellenar el tensor de entrada de la 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=???,
)

Mi primera intuición es que debería ser ((0, 0), (0, 0), (1, 1), (2, 2)), donde cada subtupla corresponde a una de las 4 dimensiones, y los dos números son el tamaño de relleno antes y después de los valores existentes. Mi conjetura se origina en la API numpy.

Sin embargo, la respuesta correcta es (2, 2, 1, 1). No hay ninguna subtupla, sino una simple tupla. Además, las dimensiones están invertidas. La última dimensión pasa a la primera.

El siguiente es un mal ejemplo de TensorFlow. ¿Puedes adivinar cuál es el resultado del siguiente fragmento de código?

value = True

@tf.function
def get_value():
return value

value = False
print(get_value())

Sin el tf.function decorador, la salida debería ser False, lo cual es bastante simple. Sin embargo, con el decorador, el resultado es Verdadero. Esto se debe a que TensorFlow compila la función y cualquier variable de Python se compila en una nueva constante. Cambiar el valor de la variable anterior no afectaría la constante creada.

Engaña al usuario haciéndole creer que es el código Python con el que está familiarizado, pero en realidad no lo es.

Principio 3: Interacción sobre documentación

A nadie le gusta leer documentación extensa si puede resolverla simplemente ejecutando un código de ejemplo y modificándolo por sí mismo. Por lo tanto, intentamos que el flujo de trabajo del usuario del software siga la misma lógica.

Aquí hay un buen ejemplo que se muestra en el siguiente fragmento de código. En PyTorch, todos los métodos con guión bajo son operaciones in situ, mientras que los que no lo tienen no lo son. Desde una perspectiva interactiva, son buenos porque son fáciles de seguir y los usuarios no necesitan consultar los documentos cada vez que desean la versión local de un método. Sin embargo, por supuesto, introdujeron cierta carga cognitiva. Los usuarios necesitan saber qué significa inplace y cuándo usarlos.

x = x.add(y)
x.add_(y)
x = x.mul(y)
x.mul_(y)

Otro buen ejemplo son las capas de Keras. Siguen estrictamente la misma convención de nomenclatura que se muestra en el siguiente fragmento de código. Con una convención de nomenclatura clara, los usuarios pueden recordar fácilmente los nombres de las capas sin consultar la documentación.

from keras import layers

layers.MaxPooling2D()
layers.GlobalMaxPooling1D()
layers.GlobalAveragePooling3D()

Otra parte importante de la interacción entre el usuario y el software es el mensaje de error. No se puede esperar que el usuario escriba todo correctamente la primera vez. Siempre debemos hacer las comprobaciones necesarias en el código e intentar imprimir mensajes de error útiles.

Veamos los siguientes dos ejemplos que se muestran en el fragmento de código. El primero no tiene mucha información. Simplemente dice que la forma del tensor no coincide. El
el segundo contiene información mucho más útil para que el usuario encuentre el error. No solo le indica que el error se debe a una discrepancia en la forma del tensor, sino que también muestra cuál es la forma esperada y cuál es la forma incorrecta que recibió. Si no pretendías pasar esa forma, tienes una idea mejor.
del error ahora.

# Bad example:
raise ValueError("Tensor shape mismatch.")

# Good example:
raise ValueError(
"Tensor shape mismatch. "
"Expected: (batch, num_features). "
f"Received: {x.shape}"
)

El mejor mensaje de error sería señalar directamente al usuario la solución. El siguiente fragmento de código muestra un mensaje de error general de Python. Adivinó cuál era el problema con el código y señaló directamente al usuario la solución.

import math

math.sqr(4)
"AttributeError: module 'math' has no attribute 'sqr'. Did you mean: 'sqrt'?"

Ultimas palabras

Hasta ahora hemos presentado los tres principios de diseño de software más valiosos que he aprendido al contribuir a los marcos de aprendizaje profundo. Primero, escriba flujos de trabajo de un extremo a otro para descubrir más problemas en la experiencia del usuario. En segundo lugar, reducir la carga cognitiva y no enseñar nada al usuario a menos que sea necesario. En tercer lugar, siga la misma lógica en el diseño de su API y envíe mensajes de error significativos para que los usuarios puedan aprender su software interactuando con él en lugar de revisar constantemente la documentación.

Sin embargo, hay muchos más principios a seguir si desea mejorar aún más su software. Puedes consultar el Directrices de diseño de la API de Keras como una guía completa de diseño de API.