0ui41ijcqrmuhj M9.jpeg

Recientemente estuve jugando con modelos de aprendizaje profundo en Tensorflow y, en consecuencia, me introdujeron en la gestión de datos como tensores.

Como ingeniero de datos que trabaja todo el día en tablas que puedo dividir, dividir y visualizar fácilmente, no tenía absolutamente ninguna intuición sobre cómo trabajar con tensores y parecía encontrarme constantemente con los mismos errores que, especialmente al principio, superaban con creces. mi cabeza.

Sin embargo, profundizar en ellos me ha enseñado mucho sobre los tensores y TensorFlow, y quería consolidar esos aprendizajes aquí para usarlos como referencia.

Si tiene un error, una solución o un consejo de depuración favorito, ¡deje un comentario!

Antes de profundizar en los errores en sí, quería documentar algunos de los fragmentos de código simples y livianos que me han resultado útiles para la depuración. (Aunque por razones legales hay que indicar que, por supuesto, siempre depuramos con funciones de depuración oficiales y nunca solo con docenas de declaraciones impresas 🙂)

Ver el interior de nuestros conjuntos de datos de Tensorflow

En primer lugar, mirando nuestros datos reales. Cuando imprimimos un Dataframe o SELECT * en SQL, ¡vemos los datos! Cuando imprimimos un conjunto de datos tensoriales vemos…

<_TensorSliceDataset element_spec=(TensorSpec(shape=(2, 3), dtype=tf.int32, name=None), TensorSpec(shape=(1, 1), dtype=tf.int32, name=None))>

Toda esta es información bastante útil, pero no nos ayuda a comprender lo que realmente sucede en nuestros datos.

Para imprimir un único tensor dentro del gráfico de ejecución, podemos aprovechar tf.print. Este artículo es una maravillosa inmersión en tf.print que recomiendo encarecidamente si planeas usarlo con frecuencia: Usando tf.Print() en TensorFlow

Pero cuando trabajamos con conjuntos de datos de Tensorflow durante el desarrollo, a veces necesitamos ver algunos valores a la vez. Para eso podemos recorrer e imprimir datos individuales como este:


# Generate dummy 2D data
np.random.seed(42)
num_samples = 100
num_features = 5
X_data = np.random.rand(num_samples, num_features).astype(np.float32)
y_data = 2 * X_data[:, 0] + 3 * X_data[:, 1] - 1.5 * X_data[:, 2] + 0.5 * X_data[:, 3] + np.random.randn(num_samples)

# Turn it into a Tensorflow Dataset
dataset = tf.data.Dataset.from_tensor_slices((X_data, y_data))

# Print the first 10 rows
for i, (features, label) in enumerate(dataset.take(10)):
print(f"Row {i + 1}: Features - {features.numpy()}, Label - {label.numpy()}")

También podemos usar skip para llegar a un índice específico:

mini_dataset = dataset.skip(100).take(20)
for i, (features, label) in enumerate(mini_dataset):
print(f"Row {i + 1}: Features - {features.numpy()}, Label - {label.numpy()}")

Conociendo las especificaciones de nuestros tensores

Cuando trabajamos con tensores, también necesitamos conocer su forma, rango, dimensión y tipo de datos (si algo de ese vocabulario no le resulta familiar, como lo fue para mí inicialmente, no se preocupe, volveremos a ello más adelante en el artículo). De todos modos, a continuación se muestran algunas líneas de código para recopilar esta información:


# Create a sample tensor
sample_tensor = tf.constant([[1, 2, 3], [4, 5, 6]])

# Get the size of the tensor (total number of elements)
tensor_size = tf.size(sample_tensor).numpy()

# Get the rank of the tensor
tensor_rank = tf.rank(sample_tensor).numpy()

# Get the shape of the tensor
tensor_shape = sample_tensor.shape

# Get the dimensions of the tensor
tensor_dimensions = sample_tensor.shape.as_list()
# Print the results
print("Tensor Size:", tensor_size)
print("Tensor Rank:", tensor_rank)
print("Tensor Shape:", tensor_shape)
print("Tensor Dimensions:", tensor_dimensions)

Los resultados anteriores:

Tensor Size: 6
Tensor Rank: 2
Tensor Shape: (2, 3)
Tensor Dimensions: [2, 3]

Modelo de aumento.summary()

Finalmente, siempre es útil poder ver cómo se mueven los datos a través de un modelo y cómo cambia la forma en las entradas y salidas entre capas. La fuente de muchos errores será una falta de coincidencia entre estas formas de entrada y salida esperadas y la forma de un tensor determinado.

Resumen Modelo() Por supuesto, hace el trabajo, pero podemos complementar esa información con el siguiente fragmento, que agrega un poco más de contexto con las entradas y salidas del modelo y la capa:

print("###################Input Shape and Datatype#####################")
[print(i.shape, i.dtype) for i in model.inputs]
print("###################Output Shape and Datatype#####################")
[print(o.shape, o.dtype) for o in model.outputs]
print("###################Layer Input Shape and Datatype#####################")
[print(l.name, l.input, l.dtype) for l in model.layers]

¡Así que saltemos a algunos errores!

Rango

ValueError: la forma debe tener el rango x pero el rango y….

Bien, antes que nada, ¿qué es un rango? El rango es solo la unidad de dimensionalidad que usamos para describir tensores. Un tensor de rango 0 es un valor escalar; un tensor de rango uno es un vector; un rango dos es una matriz, y así sucesivamente para todas las n estructuras dimensionales.

Tomemos, por ejemplo, un tensor de 5 dimensiones.

rank_5_tensor = tf.constant([[[[[1, 2], [3, 4]], [[5, 6], [7, 8]]], [[[9, 10], [11, 12]], [[13, 14], [15, 16]]]],
[[[[17, 18], [19, 20]], [[21, 22], [23, 24]]], [[[25, 26], [27, 28]], [[29, 30], [31, 32]]]]])
print("\nRank 5 Tensor:", rank_5_tensor.shape)
Rank 5 Tensor: (2, 2, 2, 2, 2)

El código anterior muestra que cada dimensión de los cinco tiene un tamaño de dos. Si quisiéramos indexarlo podríamos hacerlo según cualquiera de estos ejes. Para llegar al último elemento, 32, ejecutaríamos algo como:

rank_5_tensor.numpy()[1][1][1][1][1]

El documentación oficial del tensor tiene algunas visualizaciones realmente útiles para hacer esto un poco más comprensible.

Volviendo al error: simplemente indica que el tensor proporcionado tiene una dimensión diferente a la que se espera para una función en particular. Por ejemplo, si el error declara que «La forma debe ser de rango 1 pero es de rango 0…» significa que estamos proporcionando un valor escalar y espera un tensor 1-D.

Tomemos el siguiente ejemplo en el que intentamos multiplicar tensores junto con el método matmul.

import tensorflow as tf
import numpy as np
# Create a TensorFlow dataset with random matrices
num_samples = 5
matrix_size = 3
dataset = tf.data.Dataset.from_tensor_slices(np.random.rand(num_samples, matrix_size, matrix_size))
mul = [1,2,3,4,5,6]

# Define a function that uses tf.matmul
def matmul_function(matrix):
return tf.matmul(matrix, mul)

# Apply the matmul_function to the dataset using map
result_dataset = dataset.map(matmul_function)

Si echamos un vistazo a la documentaciónmatmul espera al menos un tensor de rango 2, por lo que multiplicar la matriz por [1,2,3,4,5,6]que es solo una matriz, generará este error.

ValueError: Shape must be rank 2 but is rank 1 for '{{node MatMul}} = MatMul[T=DT_DOUBLE, transpose_a=false, transpose_b=false](args_0, MatMul/b)' with input shapes: [3,3], [2].

Un excelente primer paso para este error es profundizar en la documentación y comprender qué busca la función que está utilizando (aquí hay una buena lista de las funciones disponibles en tensores: operaciones_crudas.

Luego utilice el método de clasificación para determinar qué estamos proporcionando realmente.

print(tf.rank(mul))
tf.Tensor(1, shape=(), dtype=int32)

En lo que respecta a las soluciones, tf.reshape suele ser una buena opción para empezar. Tomemos un breve momento para hablar un poco sobre tf.reshape, ya que será un fiel compañero durante todo nuestro viaje por Tensorflow: tf.reshape(tensor,forma,nombre=Ninguno)

Reshape simplemente toma el tensor que queremos remodelar y otro tensor que contiene la forma que queremos que tenga la salida. Por ejemplo, remodelemos nuestra entrada de multiplicación:

mul = [1,2,3,4,5,6]
tf.reshape(mul, [3, 2]).numpy()
array([[1, 2],
[3, 4],
[5, 6]], dtype=int32)

Nuestra variable se convertirá en un tensor (3,2) (3 filas, 2 columnas). Una nota rápida, tf.reshape(t, [3, -1]).numpy() producirá lo mismo porque -1 le dice a Tensorflow que calcule el tamaño de la dimensión de manera que el tamaño total permanezca constante. El número de elementos en el tensor de forma es el rango.

Una vez que creemos un tensor con el rango adecuado, ¡nuestra multiplicación funcionará bien!

Forma

ValueError: la entrada de la capa es incompatible con la capa….

Tener una comprensión intuitiva de la forma del tensor y de cómo interactúa y cambia entre las capas del modelo ha hecho que la vida con el aprendizaje profundo sea mucho más fácil.

Primero, dejando de lado el vocabulario básico: la forma de un tensor se refiere al número de elementos a lo largo de cada dimensión o eje del tensor. Por ejemplo, un tensor 2D con 3 filas y 4 columnas tiene la forma (3, 4).

Entonces, ¿qué puede salir mal con la forma? Me alegra que hayas preguntado, ¡bastantes cosas!

En primer lugar, la forma y el rango de los datos de entrenamiento deben coincidir con la forma de entrada esperada por la capa de entrada. Echemos un vistazo a un ejemplo, una CNN básica:

import tensorflow as tf
from tensorflow.keras import layers, models

# Create a function to generate sample data
def generate_sample_data(num_samples=100):
for _ in range(num_samples):
features = tf.random.normal(shape=(64, 64, 3))
labels = tf.one_hot(tf.random.uniform(shape=(), maxval=10, dtype=tf.int32), depth=10)
yield features, labels

# Create a TensorFlow dataset using the generator function
sample_dataset = tf.data.Dataset.from_generator(generate_sample_data, output_signature=(tf.TensorSpec(shape=(64, 64, 3), dtype=tf.float32), tf.TensorSpec(shape=(10,), dtype=tf.float32)))

# Create a CNN model with an input layer expecting (128, 128, 3)
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(128, 128, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

# Compile the model
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
# Fit the model using the dataset
model.fit(sample_dataset.batch(32).repeat(), epochs=5, steps_per_epoch=100, validation_steps=20)

Intentar ejecutar el código anterior dará como resultado:

ValueError: Input 0 of layer "sequential_5" is incompatible with the layer: expected shape=(None, 128, 128, 3), found shape=(None, 64, 64, 3)

Esto se debe a que nuestro modelo espera que el tensor de entrada tenga la forma (128, 128, 3) y nuestros datos generados sean (64, 64, 3).

En una situación como esta, nuestro buen amigo, reshape, u otra función de Tensorflow, resize, pueden ayudar. Si, como en el caso anterior, estamos trabajando con imágenes, simplemente podemos ejecutar redimensionar o cambiar las expectativas de la entrada de nuestro modelo:

def resize_image(image, label):
resized_image = tf.image.resize(image, size=target_shape)
return resized_image, label

# Apply the resize function to the entire dataset
resized_dataset = sample_dataset.map(resize_image)

En este contexto, es útil saber un poco acerca de cómo los tipos comunes de modelos y capas de modelos esperan entradas de diferentes formas, así que tomemos un pequeño desvío.

Las redes neuronales profundas de capas densas toman tensores unidimensionales (o bidimensionales, dependiendo de si incluye el tamaño del lote, pero hablaremos sobre el tamaño del lote en un momento) del formato (feature_size, ) donde feature_size es el número de características en cada muestra.

Las redes neuronales convolucionales toman datos que representan imágenes, utilizando tensores tridimensionales (ancho, alto, canales), donde los canales son el esquema de color, 1 para la escala de grises y 3 para RBG.

Y, finalmente, las redes neuronales recurrentes, como las LTSM, toman 2 dimensiones (pasos de tiempo, feature_size)

¡Pero volvamos a los errores! Otro culpable común de los errores de forma de Tensorflow tiene que ver con cómo cambia la forma a medida que los datos pasan a través de las capas del modelo. Como se mencionó anteriormente, diferentes capas adoptan diferentes formas de entrada y también pueden remodelar la salida.

Volviendo a nuestro ejemplo de CNN anterior, analicémoslo nuevamente y veamos qué sucede cuando eliminamos la capa Aplanar. Si intentamos ejecutar el código veremos

ValueError: Shapes (None, 10) and (None, 28, 28, 10) are incompatible

Aquí es donde imprimir todas las formas de entrada y salida de nuestro modelo junto con nuestras formas de datos resulta útil para ayudarnos a identificar dónde hay una discrepancia.

model.summary() nos mostrará

Layer (type) Output Shape Param #
=================================================================
conv2d_15 (Conv2D) (None, 126, 126, 32) 896
max_pooling2d_10 (MaxPooli (None, 63, 63, 32) 0
ng2D)
conv2d_16 (Conv2D) (None, 61, 61, 64) 18496
max_pooling2d_11 (MaxPooling2D) (None, 30, 30, 64) 0
conv2d_17 (Conv2D) (None, 28, 28, 64) 36928
flatten_5 (Flatten) (None, 50176) 0
dense_13 (Dense) (None, 64) 3211328
dense_14 (Dense) (None, 10) 650
=================================================================
Total params: 3268298 (12.47 MB)
Trainable params: 3268298 (12.47 MB)
Non-trainable params: 0 (0.00 Byte)

Y nuestro diagnóstico adicional revelará

###################Input Shape and Datatype#####################
(None, 128, 128, 3) <dtype: 'float32'>
###################Output Shape and Datatype#####################
(None, 10) <dtype: 'float32'>
###################Layer Input Shape and Datatype#####################
conv2d_15 KerasTensor(type_spec=TensorSpec(shape=(None, 128, 128, 3), dtype=tf.float32, name='conv2d_15_input'), name='conv2d_15_input', description="created by layer 'conv2d_15_input'") float32
max_pooling2d_10 KerasTensor(type_spec=TensorSpec(shape=(None, 126, 126, 32), dtype=tf.float32, name=None), name='conv2d_15/Relu:0', description="created by layer 'conv2d_15'") float32
conv2d_16 KerasTensor(type_spec=TensorSpec(shape=(None, 63, 63, 32), dtype=tf.float32, name=None), name='max_pooling2d_10/MaxPool:0', description="created by layer 'max_pooling2d_10'") float32
max_pooling2d_11 KerasTensor(type_spec=TensorSpec(shape=(None, 61, 61, 64), dtype=tf.float32, name=None), name='conv2d_16/Relu:0', description="created by layer 'conv2d_16'") float32
conv2d_17 KerasTensor(type_spec=TensorSpec(shape=(None, 30, 30, 64), dtype=tf.float32, name=None), name='max_pooling2d_11/MaxPool:0', description="created by layer 'max_pooling2d_11'") float32
flatten_5 KerasTensor(type_spec=TensorSpec(shape=(None, 28, 28, 64), dtype=tf.float32, name=None), name='conv2d_17/Relu:0', description="created by layer 'conv2d_17'") float32
dense_13 KerasTensor(type_spec=TensorSpec(shape=(None, 50176), dtype=tf.float32, name=None), name='flatten_5/Reshape:0', description="created by layer 'flatten_5'") float32
dense_14 KerasTensor(type_spec=TensorSpec(shape=(None, 64), dtype=tf.float32, name=None), name='dense_13/Relu:0', description="created by layer 'dense_13'") float32

Es una gran cantidad de resultados, pero podemos ver que la capa densa_13 está buscando entradas de forma (Ninguna, 50176). Sin embargo, las salidas de la capa conv2d_17 (Ninguna, 28, 28, 64)

Aplanar capas transforma la salida multidimensional de capas anteriores en un vector unidimensional (plano) que espera la capa Densa.

Las capas Conv2d y Max Pooling también cambian sus datos de entrada de otras maneras interesantes, pero están fuera del alcance de este artículo. Para ver un desglose impresionante, eche un vistazo a: Guía definitiva para la forma de entrada y la complejidad del modelo en redes neuronales

Pero ¿qué pasa con el tamaño del lote? ¡No lo he olvidado!

Si desciframos nuestro código una vez más eliminando el .batch(32) del conjunto de datos en model.fit obtendremos el error:

ValueError: Input 0 of layer "sequential_10" is incompatible with the layer: expected shape=(None, 128, 128, 3), found shape=(128, 128, 3)

Esto se debe a que la primera dimensión de la entrada de una capa está reservada para el tamaño del lote o la cantidad de muestras con las que queremos que trabaje el modelo a la vez. Para una gran inmersión profunda, lea detenidamente Diferencia entre lote y época.

El tamaño de lote predeterminado es Ninguno antes del ajuste, como podemos ver en el resultado del resumen del modelo, y nuestro modelo espera que lo establezcamos en otro lugar, dependiendo de cómo ajustemos el hiperparámetro. También podemos forzarlo en nuestra capa de entrada usando lote_input_size en lugar de input_size, pero eso disminuye nuestra flexibilidad a la hora de probar diferentes valores.

Tipo

TypeError: no se pudo convertir el objeto de tipo a tensor. Tipo de objeto no admitido

Finalmente, hablemos un poco sobre algunos tipos de datos específicos en Tensores.

El error anterior es otro que, si está acostumbrado a trabajar en sistemas de bases de datos con tablas construidas a partir de todo tipo de datos, puede resultar un poco desconcertante, pero es uno de los más sencillos de diagnosticar y solucionar, aunque existen Un par de causas comunes a tener en cuenta.

El problema principal es que, aunque los tensores soportan una variedad de tipos de datos, cuando convertimos una matriz NumPy en tensores (un flujo común dentro del aprendizaje profundo), los tipos de datos deben ser flotantes. El siguiente script inicializa un ejemplo artificial de un marco de datos con Ninguno y con puntos de datos de cadena. Analicemos algunos problemas y soluciones para este ejemplo:

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
data = [
[None, 0.2, '0.3'],
[0.1, None, '0.3'],
[0.1, 0.2, '0.3'],
]
X_train = pd.DataFrame(data=data, columns=["x1", "x2", "x3"])
y_train = pd.DataFrame(data=[1, 0, 1], columns=["y"])

# Create a TensorFlow dataset
train_dataset = tf.data.Dataset.from_tensor_slices((X_train.to_numpy(), y_train.to_numpy()))
# Define the model
model = Sequential()
model.add(Dense(1, input_dim=X_train.shape[1], activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
# Fit the model using the TensorFlow dataset
model.fit(train_dataset.batch(3), epochs=3)

La ejecución de este código nos indicará que:

ValueError: Failed to convert a NumPy array to a Tensor (Unsupported object type float).

El problema más obvio es que está enviando una matriz NumPy que contiene algún tipo no flotante, un objeto. Si tiene una columna real de datos categóricos, hay muchas formas de convertirla en datos numéricos (codificación única, etc.), pero eso está fuera del alcance de esta discusión.

Podemos determinar eso si ejecutamos print(X_train.dtypes), lo que nos dirá qué hay en nuestro marco de datos que no le gusta a Tensorflow.

x1 float64
x2 float64
x3 object
dtype: object

Si nos encontramos con puntos de datos no flotantes, la siguiente línea resolverá mágicamente todos nuestros problemas:

X_train = np.asarray(X_train).astype('float32')

Otra cosa que debes comprobar es si tienes Ninguno o np.nan en algún lugar.

Para averiguarlo podemos usar algunas líneas de código como:

null_mask = X_train.isnull().any(axis=1)
null_rows = X_train[null_mask]
print(null_rows)

Lo que nos dice que tenemos nulos en las filas 0 y 1:

x1 x2 x3
0 NaN 0.2 0.3
1 0.1 NaN 0.3

Si es así, y eso es esperado/intencional, debemos reemplazar esos valores con una alternativa aceptable. Fillna puede ayudarnos aquí.

X_train.fillna(value=0, inplace=True)

Con estos cambios en el código siguiente, nuestra matriz NumPy se convertirá con éxito en un conjunto de datos tensorial y podremos entrenar nuestro modelo.

A menudo encuentro que aprendo más sobre una tecnología en particular cuando tengo que solucionar errores, y espero que esto también te haya resultado útil.

Si tiene consejos y trucos interesantes o errores divertidos de Tensorflow, ¡páselos!