Está en la página 1de 50

Traducido del inglés al español - www.onlinedoctranslator.

com

v[:,2].asignar([0.,1.])# => [[2., 42., 0.], [8., 10., 1.]] v.


dispersión_nd_actualización(índices=[[0,0], [1,2]],actualizaciones=[100.,200.])
# => [[100., 42., 0.], [8., 10., 200.]]

En la práctica, rara vez tendrá que crear variables manualmente, ya que Keras
proporciona unañadir_peso()método que se encargará de ello por usted, como
veremos. Además, los parámetros del modelo generalmente serán actualizados
directamente por los optimizadores, por lo que rara vez necesitará actualizar las
variables manualmente.

Otras estructuras de datos

TensorFlow admite varias otras estructuras de datos, incluidas las siguientes (consulte el
cuaderno o???para más detalles):

• Tensores dispersos(tf. SparseTensor)representar eficientemente tensores que contienen principalmente


ceros. lostf.dispersoEl paquete contiene operaciones para tensores dispersos.

• Matrices de tensores(tf.TensorArray)son listas de tensores. Tienen un tamaño fijo por defecto,


pero opcionalmente se pueden hacer dinámicos. Todos los tensores que contienen deben tener
la misma forma y tipo de datos.

• tensores irregulares(tf.RaggedTensor)representan listas estáticas de listas de tensores, donde


cada tensor tiene la misma forma y tipo de datos. lostf.raggedEl paquete contiene operaciones
para tensores irregulares.

• tensores de cuerdason tensores regulares de tipotf.cadena.En realidad, representan


cadenas de bytes, no cadenas Unicode, por lo que si crea un tensor de cadena usando una
cadena Unicode (por ejemplo, una cadena normal de Python 3 como "café" )̀,luego se
codificará en UTF-8 automáticamente (p. ej.,b"caf\xc3\xa9").Alternativamente, puede
representar cadenas Unicode usando tensores de tipotf.int32,donde cada elemento
representa un punto de código Unicode (por ejemplo, [99, 97, 102, 233]).lostf.cuerdas
paquete (con uns)contiene operaciones para cadenas de bytes y cadenas Unicode (y para
convertir una en la otra).

• Conjuntossimplemente se representan como tensores regulares (o tensores dispersos) que


contienen uno o más conjuntos, y puede manipularlos usando operaciones de latf.conjuntos
paquete.

• Colas, incluidas las colas Primero en entrar, Primero en salir (FIFO) (Cola FIFO),colas que pueden
priorizar algunos artículos (cola de prioridad),colas que barajan sus elementos (Cola aleatoria
aleatoria),y colas que pueden agrupar elementos de diferentes formas mediante el relleno
(Relleno FIFOQueue).Estas clases están todas en eltf.colapaquete.

Con tensores, operaciones, variables y varias estructuras de datos a su disposición, ¡ahora está
listo para personalizar sus modelos y algoritmos de entrenamiento!

Usar TensorFlow como NumPy | 375


Personalización de modelos y algoritmos de entrenamiento

Comencemos por crear una función de pérdida personalizada, que es un caso de uso simple y común.

Funciones de pérdida personalizadas

Suponga que desea entrenar un modelo de regresión, pero su conjunto de entrenamiento es un poco
ruidoso. Por supuesto, comienza intentando limpiar su conjunto de datos eliminando o reparando los
valores atípicos, pero resulta ser insuficiente, el conjunto de datos sigue siendo ruidoso. ¿Qué función
de pérdida debería utilizar? El error cuadrático medio puede penalizar demasiado los errores grandes,
por lo que su modelo terminará siendo impreciso. El error absoluto medio no penalizaría tanto a los
valores atípicos, pero el entrenamiento podría tardar un tiempo en converger y el modelo entrenado
podría no ser muy preciso. Este es probablemente un buen momento para usar la pérdida de Huber
(introducida enCapítulo 10) en lugar del buen viejo MSE. La pérdida de Huber no es actualmente parte
de la API oficial de Keras, pero está disponible en tf.keras (solo use una instancia de la
keras.losses.Huberclase). Pero supongamos que no está allí: ¡implementarlo es muy fácil! Simplemente
cree una función que tome las etiquetas y las predicciones como argumentos, y use las operaciones
de TensorFlow para calcular la pérdida de cada instancia:

definitivamentehuber_fn(y_verdadero,y_pred):

error=y_verdadero-y_pred es_pequeño_error
=t.f..abdominales(error)<1 pérdida_cuadrada
=t.f..cuadrado(error)/2 pérdida_lineal=t.f..
abdominales(error)-0.5
devolvert.f..dónde(es_pequeño_error,pérdida_cuadrada,pérdida_lineal)

Para un mejor rendimiento, debe usar una implementación vectorizada,


como en este ejemplo. Además, si desea beneficiarse de las
características gráficas de TensorFlow, debe usar solo operaciones de
TensorFlow.

También es preferible devolver un tensor que contenga una pérdida por instancia, en lugar de
devolver la pérdida media. De esta manera, Keras puede aplicar pesos de clase o pesos de muestra
cuando se le solicite (verCapítulo 10).

A continuación, puede usar esta pérdida cuando compile el modelo de Keras y luego entrene su
modelo:

modelo.compilar(pérdida=huber_fn,optimizador=
"nadam") modelo.adaptar(X_tren,y_tren, [...])

¡Y eso es! Para cada lote durante el entrenamiento, Keras llamará alhuber_fn()para calcular
la pérdida y usarla para realizar un paso de descenso de gradiente. Además, realizará un
seguimiento de la pérdida total desde el comienzo de la época y mostrará la pérdida
media.

376 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


Pero, ¿qué sucede con esta pérdida personalizada cuando guardamos el modelo?

Guardar y cargar modelos que contienen componentes personalizados

Guardar un modelo que contiene una función de pérdida personalizada en realidad funciona bien, ya
que Keras solo guarda el nombre de la función. Sin embargo, siempre que lo cargue, debe
proporcionar un diccionario que asigne el nombre de la función a la función real. En términos más
generales, cuando carga un modelo que contiene objetos personalizados, debe asignar los nombres a
los objetos:

modelo=queras.modelos.cargar_modelo("mi_modelo_con_una_pérdida_personalizada.h5",
objetos_personalizados={"huber_fn":huber_fn})

Con la implementación actual, cualquier error entre -1 y 1 se considera "pequeño". Pero,


¿y si queremos un umbral diferente? Una solución es crear una función que cree una
función de pérdida configurada:

definitivamentecrear_huber(límite=1.0):
definitivamentehuber_fn(y_verdadero,y_pred):

error=y_verdadero-y_pred es_pequeño_error=t.f..
abdominales(error)<Umbral de pérdida al cuadrado=
t.f..cuadrado(error)/2
pérdida_lineal=límite*t.f..abdominales(error)-límite**2/2 devolvert.f..
dónde(es_pequeño_error,pérdida_cuadrada,pérdida_lineal) devolver
huber_fn

modelo.compilar(pérdida=crear_huber(2.0),optimizador="nadam")

Desafortunadamente, cuando guarda el modelo, ellímiteno se salvará. Esto significa que


tendrá que especificar ellímitevalor al cargar el modelo (tenga en cuenta que el nombre a
utilizar es "huber_fn",que es el nombre de la función que le dimos a Keras, no el nombre de
la función que lo creó):

modelo=queras.modelos.cargar_modelo("mi_modelo_con_un_umbral_de_pérdida_personalizado_2.h5",
objetos_personalizados={"huber_fn":crear_huber(2.0)})

Puede resolver esto creando una subclase de lakeras.losses.Lossclase, e


implementar suget_config()método:
claseHuberLoss(queras.pérdidas.Pérdida):
definitivamente__en eso__(uno mismo,límite=1.0,**kwargs):
uno mismo.límite=límite súper().__en
eso__(**kwargs) definitivamente
llamar(uno mismo,y_verdadero,y_pred):
error=y_verdadero-y_pred
es_pequeño_error=t.f..abdominales(error)<uno mismo.Umbral de
pérdida al cuadrado=t.f..cuadrado(error)/2
pérdida_lineal=uno mismo.límite*t.f..abdominales(error)-uno mismo.límite**2/2 devolvert.f..
dónde(es_pequeño_error,pérdida_cuadrada,pérdida_lineal) definitivamenteget_config(uno
mismo):
base_config=súper().get_config()
devolver{**base_config,"límite":uno mismo.límite}

Personalización de modelos y algoritmos de entrenamiento | 377


La API de Keras solo especifica cómo usar subclases para definir capas,
modelos, devoluciones de llamada y regularizadores. Si crea otros
componentes (como pérdidas, métricas, inicializadores o restricciones)
utilizando subclases, es posible que no sean portátiles para otras
implementaciones de Keras.

Recorramos este código:

• El constructor acepta **kwargsy los pasa al constructor principal, que maneja


los hiperparámetros estándar: elnombrede la pérdida y lareducción
algoritmo a usar para agregar las pérdidas de instancias individuales. Por defecto, es
"sum_over_batch_size",lo que significa que la pérdida será la suma de las pérdidas de las
instancias, posiblemente ponderadas por los pesos de la muestra, si los hay, y luego se dividirá el
resultado por el tamaño del lote (no por la suma de los pesos, por lo que esto esnola media
ponderada).5. Otros valores posibles son "suma"yNinguna.

• Losllamar()El método toma las etiquetas y predicciones, calcula todas las pérdidas de
instancias y las devuelve.

• Losget_config()El método devuelve un diccionario que asigna cada nombre de


hiperparámetro a su valor. Primero llama a la clase padreget_config()método, luego agrega
los nuevos hiperparámetros a este diccionario (tenga en cuenta que el conveniente {**X}la
sintaxis se agregó en Python 3.5).

Luego puede usar cualquier instancia de esta clase cuando compile el modelo:

modelo.compilar(pérdida=HuberLoss(2.),optimizador="nadam")

Cuando guarde el modelo, el umbral se guardará junto con él, y cuando cargue el
modelo, solo necesita asignar el nombre de la clase a la clase en sí:
modelo=queras.modelos.cargar_modelo("my_model_with_a_custom_loss_class.h5",
objetos_personalizados={"HuberLoss":HuberLoss})

Cuando guarda un modelo, Keras llama a la instancia de pérdidaget_config()método y


guarda la configuración como JSON en el archivo HDF5. Cuando carga el modelo, llama al
desde_config()método de clase en elHuberLossclase: este método es implementado por la clase
base (Pérdida)y simplemente crea una instancia de la clase, pasando **configuraciónal
constructor.

¡Eso es todo por pérdidas! No fue demasiado difícil, ¿verdad? Bueno, es igual de simple para funciones de
activación personalizadas, inicializadores, regularizadores y restricciones. Veamos estos ahora.

5 No sería una buena idea usar una media ponderada: si lo hiciéramos, entonces dos instancias con el mismo peso pero en
diferentes lotes tendrían un impacto diferente en el entrenamiento, dependiendo del peso total de cada lote.

378 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


Funciones de activación personalizadas, inicializadores, regularizadores y
restricciones

La mayoría de las funciones de Keras, como pérdidas, regularizadores, restricciones, inicializadores,


métricas, funciones de activación, capas e incluso modelos completos, se pueden personalizar de la
misma manera. La mayoría de las veces, solo necesitará escribir una función simple, con las entradas y
salidas adecuadas. Por ejemplo, aquí hay ejemplos de una activación personalizada.
función de función (equivalente akeras.activaciones.softplusotf.nn.softplus),a
inicializador Glorot personalizado (equivalente akeras.initializers.glorot_normal),un ℓ
personalizado1regularizador (equivalente akeras.regularizadores.l1(0.01))y una restricción
personalizada que asegura que los pesos sean todos positivos (equivalente a
keras.restricciones.nonneg()otf.nn.relu):
definitivamentemi_softplus(z):#el valor de retorno es solo tf.nn.softplus(z)
devolvert.f..Matemáticas.Iniciar sesión(t.f..Exp(z)+1.0)

definitivamentemi_glorot_inicializador(forma,tipo de d=t.f..flotar32):
dev estándar=t.f..sqrt(2./(forma[0]+forma[1]))
devolvert.f..aleatorio.normal(forma,dev estándar=dev estándar,tipo de d=tipo de d)

definitivamentemy_l1_regularizador(pesos):
devolvert.f..reducir_sum(t.f..abdominales(0.01*pesos))

definitivamentemis_pesos_positivos(pesos):#el valor de retorno es solo tf.nn.relu(pesos)


devolvert.f..dónde(pesos<0.,t.f..ceros_como(pesos),pesos)

Como puede ver, los argumentos dependen del tipo de función personalizada. Estas funciones
personalizadas se pueden usar normalmente, por ejemplo:

capa=queras.capas.Denso(30,activación=mi_softplus,
kernel_initializer=mi_glorot_inicializador,
kernel_regularizer=my_l1_regularizador,
kernel_constraint=mis_pesos_positivos)

La función de activación se aplicará a la salida de esteDensocapa, y su resultado se pasará a la


siguiente capa. Los pesos de la capa se inicializarán utilizando el valor devuelto por el
inicializador. En cada paso de entrenamiento, los pesos se pasarán a la función de
regularización para calcular la pérdida de regularización, que se agregará a la pérdida principal
para obtener la pérdida final utilizada para el entrenamiento. Finalmente, la función de
restricción se llamará después de cada paso de entrenamiento, y los pesos de la capa serán
reemplazados por los pesos restringidos.

Si una función tiene algunos hiperparámetros que deben guardarse junto


con el modelo, querrá subclasificar la clase apropiada, comokeras.regulariz
ers.Regularizer, keras.constraints.Constraint, keras.initializers.Initial
izerokeras.capas.Layer (para cualquier capa, incluidas las funciones de activación). Por ejemplo, al
igual que hicimos con la pérdida personalizada, aquí hay una clase simple para ℓ1regulariza-

Personalización de modelos y algoritmos de entrenamiento | 379


ción, que guarda sufactorhiperparámetro (esta vez no necesitamos llamar al constructor
principal o alget_config()método, ya que no están definidos por la clase padre):

claseRegularizador MyL1(queras.regularizadores.regularizador):
definitivamente__en eso__(uno mismo,factor):

uno mismo.factor=factor definitivamente

__llamar__(uno mismo,pesos):

devolvert.f..reducir_sum(t.f..abdominales(uno mismo.factor*pesos))
definitivamenteget_config(uno mismo):
devolver{"factor":uno mismo.factor}

Tenga en cuenta que debe implementar elllamar()método para pérdidas, capas (incluyendo funciones
de activación) y modelos, o el __llamar__()método para regularizadores, inicializadores y restricciones.
Para las métricas, las cosas son un poco diferentes, como veremos ahora.

Métricas personalizadas

Las pérdidas y las métricas no son conceptualmente lo mismo: Gradient Descent utiliza las
pérdidas paratrenun modelo, por lo que deben ser diferenciables (al menos donde se evalúan) y
sus gradientes no deben ser 0 en todas partes. Además, está bien si los humanos no los pueden
interpretar fácilmente (por ejemplo, entropía cruzada). Por el contrario, las métricas se utilizan
para evaluarun modelo, deben ser más fáciles de interpretar y pueden ser no diferenciables o
tener gradientes de 0 en todas partes (p. ej., precisión).

Dicho esto, en la mayoría de los casos, definir una función de métrica personalizada es exactamente lo mismo
que definir una función de pérdida personalizada. De hecho, incluso podríamos usar la función de pérdida de
Huber que creamos anteriormente como una métrica6, funcionaría bien (y la persistencia también funcionaría
de la misma manera, en este caso solo guardando el nombre de la función, "huber_fn"):

modelo.compilar(pérdida="mse",optimizador="nadam",métrica=[crear_huber(2.0)])

Para cada lote durante el entrenamiento, Keras calculará esta métrica y realizará un seguimiento de su
media desde el comienzo de la época. La mayoría de las veces, esto es exactamente lo que quieres.
¡Pero no siempre! Considere la precisión de un clasificador binario, por ejemplo. Como vimos en
Capítulo 3, la precisión es el número de verdaderos positivos dividido por el número de predicciones
positivas (incluidos tanto los verdaderos positivos como los falsos positivos). Suponga que el modelo
hizo 5 predicciones positivas en el primer lote, 4 de las cuales fueron correctas: eso es 80% de
precisión. Luego suponga que el modelo hizo 3 predicciones positivas en el segundo lote, pero todas
fueron incorrectas: eso es 0% de precisión para el segundo lote. Si solo calcula la media de estas dos
precisiones, obtiene el 40%. Pero espera un segundo, esto esnola precisión del modelo sobre estos
dos lotes! De hecho, hubo un total de 4 verdaderos positivos (4 + 0) de 8 predicciones positivas (5 + 3),
por lo que la precisión general es del 50 %, no del 40 %. Lo que necesitamos es un objeto que pueda
realizar un seguimiento del número de verdaderos positivos y el número

6 Sin embargo, la pérdida de Huber rara vez se usa como métrica (se prefieren el MAE o el MSE).

380 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


ber de falsos positivos, y calcular su relación cuando se le solicite. Esto es precisamente lo que
lakeras.metrics.precisiónla clase hace:
> > > precisión=queras.métrica.Precisión()
> > > precisión([0,1,1,1,0,1,0,1], [1,1,0,1,0,1,0,1]) <tf.Tensor: id=581729,
forma=(), dtype=float32, numpy=0.8>
> > > precisión([0,1,0,0,1,0,1,1], [1,0,1,1,0,0,0,0]) <tf.Tensor: id=581780,
forma=(), dtype=float32, numpy=0.5>

En este ejemplo, creamos unPrecisiónobjeto, luego lo usamos como una función, pasándole las
etiquetas y predicciones para el primer lote, luego para el segundo lote (tenga en cuenta que también
podríamos haber pasado pesos de muestra). Usamos el mismo número de verdaderos y falsos
positivos que en el ejemplo que acabamos de discutir. Después del primer lote, devuelve la precisión
del 80 %, luego, después del segundo lote, devuelve el 50 % (que es la precisión general hasta el
momento, no la precisión del segundo lote). Esto se llama unmétrica de transmisión(o métrica con
estado), ya que se actualiza gradualmente, lote tras lote.

En cualquier momento, podemos llamar alresultado()para obtener el valor actual de la métrica.


También podemos mirar sus variables (rastrear el número de verdaderos y falsos positivos)
usando elVariablesatributo, y restablezca estas variables usando elrestablecer_estados()
método:

> > > pags.resultado()


<tf.Tensor: id=581794, forma=(), dtype=float32, numpy=0.5>
> > > pags.Variables
[<tf.Variable 'verdaderos_positivos:0' [...] numpy=array([4.], dtype=float32)>,
<tf.Variable 'falsos_positivos:0' [...] numpy=array([4 .], dtype=float32)>]
> > > pags.restablecer_estados()#ambas variables se restablecen a 0.0

Si necesita crear una métrica de transmisión de este tipo, simplemente puede crear una subclase de la
keras.metrics.Metricclase. Aquí hay un ejemplo simple que realiza un seguimiento de la pérdida total de Huber
y la cantidad de instancias vistas hasta el momento. Cuando se le pregunta por el resultado, devuelve la
relación, que es simplemente la pérdida media de Huber:

claseHuberMetric(queras.métrica.Métrico):
definitivamente__en eso__(uno mismo,límite=1.0,**kwargs):
súper().__en eso__(**kwargs)#maneja argumentos base (por ejemplo,
dtype) uno mismo.límite=límite
uno mismo.huber_fn=crear_huber(límite)
uno mismo.total=uno mismo.añadir_peso("total",inicializador="ceros") uno mismo.
contar=uno mismo.añadir_peso("contar",inicializador="ceros") definitivamente
actualizar_estado(uno mismo,y_verdadero,y_pred,muestra_peso=Ninguna):
métrico=uno mismo.huber_fn(y_verdadero,y_pred) uno
mismo.total.asignar_añadir(t.f..reducir_sum(métrico))
uno mismo.contar.asignar_añadir(t.f..emitir(t.f..Talla(y_verdadero),t.f..flotar32)) definitivamente
resultado(uno mismo):
devolveruno mismo.total/uno mismo.contar

definitivamenteget_config(uno mismo):

base_config=súper().get_config()
devolver{**base_config,"límite":uno mismo.límite}

Personalización de modelos y algoritmos de entrenamiento | 381


Recorramos este código:7:

• El constructor utiliza elañadir_peso()método para crear las variables necesarias para realizar
un seguimiento del estado de la métrica en varios lotes, en este caso, la suma de todas las
pérdidas de Huber (total)y el número de instancias vistas hasta ahora (contar).Si lo prefiere,
puede crear variables manualmente. Keras rastrea cualquiertf.Variableque se establece
como un atributo (y más generalmente, cualquier objeto "rastreable", como capas o
modelos).

• Losactualizar_estado()se llama al método cuando usa una instancia de esta clase como una
función (como hicimos con elPrecisiónobjeto). Actualiza las variables dadas las etiquetas y
predicciones para un lote (y pesos de muestra, pero en este caso simplemente los
ignoramos).

• Losresultado()El método calcula y devuelve el resultado final, en este caso solo la métrica de
Huber media en todas las instancias. Cuando usa la métrica como una función, el
actualizar_estado()Primero se llama al método, luego se llama alresultado()Se llama al
método y se devuelve su salida.

• También implementamos elget_config()método para garantizar lalímitese guarda


junto con el modelo.
• La implementación por defecto delrestablecer_estados()El método simplemente restablece todas las
variables a 0.0 (pero puede anularlo si es necesario).

Keras se encargará de la persistencia variable sin problemas, no se requiere ninguna


acción.

Cuando define una métrica usando una función simple, Keras la llama automáticamente para cada
lote y realiza un seguimiento de la media durante cada época, tal como lo hicimos manualmente. Así
que el único beneficio de nuestroHuberMetricclase es que ellímiteserá salvado. Pero, por supuesto,
algunas métricas, como la precisión, no se pueden promediar simplemente en lotes: en esos casos, no
hay otra opción que implementar una métrica de transmisión.

Ahora que hemos creado una métrica de transmisión, ¡crear una capa personalizada parecerá
pan comido!

7 Esta clase es solo para fines ilustrativos. Una implementación más simple y mejor simplemente subclasificaría el
keras.metrics.Mediaclase, consulte el cuaderno para ver un ejemplo.

382 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


Capas personalizadas

En ocasiones, es posible que desee crear una arquitectura que contenga una capa exótica para
la que TensorFlow no proporcione una implementación predeterminada. En este caso, deberá
crear una capa personalizada. O, a veces, es posible que desee construir una arquitectura muy
repetitiva, que contenga bloques idénticos de capas repetidos muchas veces, y sería
conveniente tratar cada bloque de capas como una sola capa. Por ejemplo, si el modelo es una
secuencia de capas A, B, C, A, B, C, A, B, C, es posible que desee definir una capa D
personalizada que contenga las capas A, B, C y su modelo. sería simplemente D, D, D. Veamos
cómo crear capas personalizadas.

Primero, algunas capas no tienen pesos, comokeras.layers.Flattenokeras.lay


ers.ReLU.Si desea crear una capa personalizada sin pesos, la opción más simple
es escribir una función y envolverla en unkeras.layers.Lambdacapa. Por ejemplo,
la siguiente capa aplicará la función exponencial a sus entradas:
capa_exponencial=queras.capas.lambda(lambdaX:t.f..Exp(X))

Esta capa personalizada se puede usar como cualquier otra capa, usando la API secuencial, la
API funcional o la API de subclases. También puede usarlo como una función de activación.
(o simplemente podría usaractivación=tf.exp,oactivación=keras.activaciones.expo
esencial,o simplementeactivación="exponencial").La capa exponencial a veces se usa en la
capa de salida de un modelo de regresión cuando los valores a predecir tienen escalas
muy diferentes (p. ej., 0,001, 10, 1000).

Como probablemente ya haya adivinado, para crear una capa con estado personalizada (es
decir, una capa con pesos), necesita crear una subclase de lakeras.layers.Capaclase. Por ejemplo,
la siguiente clase implementa una versión simplificada delDensocapa:

clasemidenso(queras.capas.Capa):
definitivamente__en eso__(uno mismo,unidades,activación=Ninguna,**kwargs):
súper().__en eso__(**kwargs) uno
mismo.unidades=unidades
uno mismo.activación=queras.activaciones.obtener(activación)

definitivamenteconstruir(uno mismo,lote_entrada_forma):

uno mismo.núcleo=uno mismo.añadir_peso(


nombre="núcleo",forma=[lote_entrada_forma[-1],uno mismo.unidades],
inicializador="glorot_normal") uno mismo.parcialidad=uno mismo.añadir_peso(

nombre="parcialidad",forma=[uno mismo.unidades],inicializador=
"ceros") súper().construir(lote_entrada_forma)#debe estar al final

definitivamentellamar(uno mismo,X):

devolveruno mismo.activación(X@uno mismo.núcleo+uno mismo.parcialidad)

definitivamenteforma_de_salida_de_cómputo(uno mismo,lote_entrada_forma):
devolvert.f..TensorShape(lote_entrada_forma.como_lista()[:-1]+[uno mismo.unidades])

Personalización de modelos y algoritmos de entrenamiento | 383


definitivamenteget_config(uno mismo):

base_config=súper().get_config() devolver{**
base_config,"unidades":uno mismo.unidades,
"activación":queras.activaciones.publicar por fascículos(uno mismo.activación)}

Recorramos este código:

• El constructor toma todos los hiperparámetros como argumentos (en este ejemplo solo
unidadesyactivación),y lo que es más importante, también se necesita un **kwargs
argumento. Llama al constructor padre, pasándole elkwargs:esto se encarga de los
argumentos estándar comoinput_shape, entrenable, nombre,y así. Luego guarda los
hiperparámetros como atributos, convirtiendo elactivaciónargumento a la función de
activación apropiada usando elkeras.activaciones.get()función (acepta funciones,
cadenas estándar como "relú"o "selu",o simplementeNinguna)8.

• Losconstruir()El papel del método es crear las variables de la capa, llamando al


añadir_peso()método para cada peso. losconstruir()se llama al método la primera vez
que se utiliza la capa. En ese momento, Keras conocerá la forma de las entradas de
esta capa y se la pasará alconstruir()método9, que a menudo es necesario para crear
algunos de los pesos. Por ejemplo, necesitamos saber el número de neuronas en la
capa anterior para crear la matriz de pesos de conexión (es decir, el "núcleo"):esto
corresponde al tamaño de la última dimensión de las entradas. Al final deconstruir()
método (y sólo al final), debe llamar al padreconstruir()
método: esto le dice a Keras que la capa está construida (simplemente estableceself.construido = Verdadero).

• Losllamar()El método realmente realiza las operaciones deseadas. En este caso,


calculamos la multiplicación matricial de las entradasXy el núcleo de la capa,
agregamos el vector de sesgo, aplicamos la función de activación al resultado, y esto
nos da la salida de la capa.

• Loscomputar_salida_forma()simplemente devuelve la forma de las salidas de esta capa. En


este caso, tiene la misma forma que las entradas, excepto que la última dimensión se
reemplaza con el número de neuronas en la capa. Tenga en cuenta que en tf.keras, las
formas son instancias de latf.TensorShapeclase, que puede convertir a listas de Python
usandocomo_lista().

• Losget_config()El método es como antes. Tenga en cuenta que guardamos la configuración


completa de la función de activación llamandokeras.activaciones.serialize().

Ahora puede utilizar unmidensocapa como cualquier otra capa!

8 Esta función es específica de tf.keras. podrías usarkeras.activaciones.Activaciónen cambio.

9 La API de Keras llama a este argumentoforma_entrada,pero como también incluye la dimensión de lote, prefiero llamar
esolote_entrada_forma.Igual porcomputar_salida_forma().

384 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


Por lo general, puede omitir elcomputar_salida_forma()método, ya que tf.keras
infiere automáticamente la forma de salida, excepto cuando la capa es
dinámica (como veremos en breve). En otras implementaciones de Keras, este
método es obligatorio o, de forma predeterminada, asume que la forma de
salida es la misma que la forma de entrada.

Para crear una capa con múltiples entradas (p. ej.,Concatenar),el argumento a lallamar()
El método debe ser una tupla que contenga todas las entradas y, de manera similar, el argumento del
computar_salida_forma()El método debe ser una tupla que contenga la forma del lote de
cada entrada. Para crear una capa con múltiples salidas, elllamar()El método debe devolver
la lista de salidas, y elcomputar_salida_forma()debe devolver la lista de formas de salida por
lotes (una por salida). Por ejemplo, la siguiente capa de juguetes toma dos entradas y
devuelve tres salidas:

claseMiMultiLayer(queras.capas.Capa):
definitivamentellamar(uno mismo,X):

X1,X2=X
devolver[X1+X2,X1*X2,X1/X2]

definitivamenteforma_de_salida_de_cómputo(uno mismo,lote_entrada_forma):
b1,b2=lote_entrada_forma
devolver[b1,b1,b1]#probablemente debería manejar las reglas de transmisión

Esta capa ahora se puede usar como cualquier otra capa, pero por supuesto solo usando
las API funcionales y de subclasificación, no la API secuencial (que solo acepta capas con
una entrada y una salida).

Si su capa necesita tener un comportamiento diferente durante el entrenamiento y durante la prueba


(por ejemplo, si usaAbandonaroNormalización por lotescapas), entonces debe agregar uncapacitación
argumento a lallamar()método y utilice este argumento para decidir qué hacer. Por ejemplo, vamos a
crear una capa que agregue ruido gaussiano durante el entrenamiento (para la regularización), pero
que no haga nada durante la prueba (Keras en realidad tiene una capa que hace lo mismo).
cosa:keras.layers.GaussianNoise):
claseMi ruido gaussiano(queras.capas.Capa):
definitivamente__en eso__(uno mismo,dev estándar,**kwargs):

súper().__en eso__(**kwargs) uno


mismo.dev estándar=dev estándar

definitivamentellamar(uno mismo,X,capacitación=Ninguna):

sicapacitación:
ruido=t.f..aleatorio.normal(t.f..forma(X),dev estándar=uno mismo.dev estándar)
devolverX+ruido más:

devolverX

definitivamenteforma_de_salida_de_cómputo(uno mismo,lote_entrada_forma):
devolverlote_entrada_forma

Personalización de modelos y algoritmos de entrenamiento | 385


¡Con eso, ahora puede crear cualquier capa personalizada que necesite! Ahora vamos a crear modelos personalizados.

Modelos personalizados

Ya analizamos las clases de modelos personalizados enCapítulo 10cuando discutimos


la API de subclasificación.10En realidad, es bastante sencillo, solo subclase el
keras.models.Modelclase, crear capas y variables en el constructor, e implementar el
llamar()para hacer lo que quieras que haga el modelo. Por ejemplo, suponga que desea
construir el modelo representado enFigura 12-3:

Figura 12-3. Ejemplo de modelo personalizado

Las entradas pasan por una primera capa densa, luego por unabloque residualcompuesto por dos
capas densas y una operación de suma (como veremos encapitulo 14, un bloque residual suma sus
entradas a sus salidas), luego a través de este mismo bloque residual 3 veces más, luego a través de
un segundo bloque residual, y el resultado final pasa por una densa capa de salida. Tenga en cuenta
que este modelo no tiene mucho sentido, es solo un ejemplo para ilustrar el hecho de que puede
construir fácilmente cualquier tipo de modelo que desee, incluso contener:

10 El nombre “API de subclases” generalmente se refiere solo a la creación de modelos personalizados mediante subclases, aunque
se pueden crear muchas otras cosas mediante subclases, como vimos en este capítulo.

386 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


bucles de ing y conexiones de salto. Para implementar este modelo, lo mejor es crear primero un
ResidualBlockcapa, ya que vamos a crear un par de bloques idénticos (y es posible que
queramos reutilizarlos en otro modelo):

claseResidualBlock(queras.capas.Capa):
definitivamente__en eso__(uno mismo,n_capas,n_neuronas,**kwargs):
súper().__en eso__(**kwargs)
uno mismo.oculto=[queras.capas.Denso(n_neuronas,activación="elú",
kernel_initializer="él_normal")
por_enrango(n_capas)]

definitivamentellamar(uno mismo,entradas):

Z=entradas
porcapaenuno mismo.oculto:
Z=capa(Z)
devolverentradas+Z

Esta capa es un poco especial ya que contiene otras capas. Keras lo gestiona de forma transparente:
detecta automáticamente que elocultoEl atributo contiene objetos rastreables (capas en este caso),
por lo que sus variables se agregan automáticamente a la lista de variables de esta capa. El resto de
esta clase se explica por sí mismo. A continuación, usemos la API de subclases para definir el modelo
en sí:

claseRegresor Residual(queras.modelos.Modelo):
definitivamente__en eso__(uno mismo,salida_dim,**kwargs):
súper().__en eso__(**kwargs)
uno mismo.oculto1=queras.capas.Denso(30,activación="elú",
kernel_initializer="él_normal")
uno mismo.bloque1=ResidualBlock(2,30) uno mismo.
bloque2=ResidualBlock(2,30) uno mismo.afuera=
queras.capas.Denso(salida_dim)

definitivamentellamar(uno mismo,entradas):

Z=uno mismo.oculto1(
entradas) por_enrango(1+3):
Z=uno mismo.bloque1(Z) Z=
uno mismo.bloque2(Z) devolveruno
mismo.afuera(Z)

Creamos las capas en el constructor y las usamos en elllamar()método. Este modelo se


puede usar como cualquier otro modelo (compilarlo, ajustarlo, evaluarlo y usarlo para
hacer predicciones). Si también desea poder guardar el modelo utilizando elahorrar()
y cárguelo usando elkeras.modelos.load_model()función, debe implementar la
get_config()método (como hicimos antes) tanto en elResidualBlock
clase y elRegresor Residualclase. Alternativamente, puede simplemente guardar y cargar el
pesas usando elguardar_pesos()ycargar_pesos()métodos.
losModeloclase es en realidad una subclase de laCapaclase, por lo que los modelos se pueden definir y utilizar
exactamente como capas. Pero un modelo también tiene algunas funcionalidades extra, incluyendo de
por supuesto que escompilar(), ajustar(), evaluar()ypredecir()métodos (y algunos var‐

Personalización de modelos y algoritmos de entrenamiento | 387


iantes, tales comotren_en_lote()oajuste_generador()),más elobtener_capas()
(que puede devolver cualquiera de las capas del modelo por nombre o por índice), y el
ahorrar()método (y soporte parakeras.modelos.load_model()ykeras.mod
els.clon_modelo()).Entonces, si los modelos brindan más funcionalidades que las capas, ¿por qué
no simplemente definir cada capa como un modelo? Bueno, técnicamente podría, pero
probablemente sea más limpio distinguir los componentes internos de su modelo (capas o
bloques de capas reutilizables) del propio modelo. El primero debe subclasificar elCapaclase,
mientras que el último debe subclasificar laModeloclase.

Con eso, puede construir de forma bastante natural y concisa casi cualquier modelo que encuentre en un
documento, ya sea utilizando la API secuencial, la API funcional, la API de subclases o incluso una
combinación de estas. ¿“Casi” cualquier modelo? Sí, todavía hay un par de cosas que debemos analizar:
primero, cómo definir pérdidas o métricas en función de las características internas del modelo y, segundo,
cómo crear un ciclo de entrenamiento personalizado.

Pérdidas y métricas basadas en modelos internos

Las pérdidas y métricas personalizadas que definimos anteriormente se basaron todas en las
etiquetas y las predicciones (y, opcionalmente, en los pesos de muestra). Sin embargo, en ocasiones
querrá definir pérdidas en función de otras partes de su modelo, como los pesos o las activaciones de
sus capas ocultas. Esto puede ser útil para fines de regularización o para monitorear algún aspecto
interno de su modelo.

Para definir una pérdida personalizada basada en las características internas del modelo, simplemente
calcúlela en función de cualquier parte del modelo que desee, luego pase el resultado alañadir_pérdida()
método. Por ejemplo, el siguiente modelo personalizado representa un regresor MLP estándar con 5 capas
ocultas, excepto que también implementa unpérdida de reconstrucción(ver???): añadimos un extraDenso
capa encima de la última capa oculta, y su función es tratar de reconstruir las entradas del
modelo. Dado que la reconstrucción debe tener la misma forma que las entradas del modelo,
necesitamos crear esteDensocapa en elconstruir()para tener acceso a la forma de las entradas.
En elllamar()método, calculamos tanto la salida regular de la MLP, más la salida de la capa de
reconstrucción. Luego calculamos la diferencia cuadrática media entre las reconstrucciones y
las entradas, y agregamos este valor (multiplicado por 0.05) a la lista de pérdidas del modelo
llamandoañadir_pérdida().Durante el entrenamiento, Keras agregará esta pérdida a la pérdida
principal (por eso redujimos la pérdida de reconstrucción, para garantizar que la pérdida
principal domine). Como resultado, el modelo se verá obligado a conservar la mayor cantidad
de información posible a través de las capas ocultas, incluso información que no sea
directamente útil para la tarea de regresión en sí. En la práctica, esta pérdida a veces mejora la
generalización; es una pérdida de regularización:

claseReconstruyendo Regresor(queras.modelos.Modelo):
definitivamente__en eso__(uno mismo,salida_dim,**kwargs):
súper().__en eso__(**kwargs)
uno mismo.oculto=[queras.capas.Denso(30,activación="selu",
kernel_initializer="lecun_normal")

388 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


por_enrango(5)]
uno mismo.afuera=queras.capas.Denso(salida_dim)

definitivamenteconstruir(uno mismo,lote_entrada_forma):

n_entradas=lote_entrada_forma[-1]
uno mismo.reconstruir=queras.capas.Denso(n_entradas)
súper().construir(lote_entrada_forma)

definitivamentellamar(uno mismo,entradas):

Z=entradas
porcapaenuno mismo.oculto:
Z=capa(Z)
reconstrucción=uno mismo.reconstruir(Z)
recon_loss=t.f..reducir_media(t.f..cuadrado(reconstrucción-entradas)) uno mismo.
añadir_pérdida(0.05*recon_loss) devolveruno mismo.afuera(Z)

De manera similar, puede agregar una métrica personalizada basada en las características internas del
modelo calculándola de la forma que desee, siempre que el resultado sea la salida de un objeto de métrica.
Por ejemplo, puede crear unkeras.metrics.Mean()objeto en el constructor, luego llámelo en el
llamar()método, pasándole elrecon_pérdida,y finalmente agréguelo al modelo llamando al
modeloañadir_métrica()método. De esta forma, cuando entrene el modelo, Keras mostrará
la pérdida media de cada época (la pérdida es la suma de la pérdida principal más 0,05
veces la pérdida de reconstrucción) y el error de reconstrucción medio de cada época.
Ambos bajarán durante el entrenamiento:

Época 1/5
11610/11610 [=============] [...] pérdida: 4.3092 - reconstrucción_error: 1.7360 Época
2/5
11610/11610 [=============] [...] pérdida: 1.1232 - reconstrucción_error: 0.8964 [...]

En más del 99% de los casos, todo lo que hemos discutido hasta ahora será suficiente para
implementar cualquier modelo que desee construir, incluso con arquitecturas complejas,
pérdidas, métricas, etc. Sin embargo, en algunos casos excepcionales, es posible que deba
personalizar el ciclo de entrenamiento. Sin embargo, antes de llegar allí, debemos ver cómo
calcular gradientes automáticamente en TensorFlow.

Cálculo de gradientes usando Autodiff


Para entender cómo usar autodiff (verCapítulo 10y???) para calcular gradientes
automáticamente, consideremos una función de juguete simple:

definitivamenteF(w1,w2):

devolver3*w1**2+2*w1*w2

Si sabes de cálculo, puedes encontrar analíticamente que la derivada parcial de esta


función con respecto aw1es6 * w1 + 2 * w2.También puedes encontrar que su derivada
parcial con respecto aw2es2 * w1.Por ejemplo, en el punto (w1, w2) = (5, 3),estos par-

Personalización de modelos y algoritmos de entrenamiento | 389


las derivadas potenciales son iguales a 36 y 10, respectivamente, por lo que el vector
gradiente en este punto es (36, 10). Pero si se tratara de una red neuronal, la función sería
mucho más compleja, típicamente con decenas de miles de parámetros, y encontrar las
derivadas parciales analíticamente a mano sería una tarea casi imposible. Una solución
podría ser calcular una aproximación de cada derivada parcial midiendo cuánto cambia la
salida de la función cuando modifica el parámetro correspondiente:

> > > w1,w2=5,3


> > > eps=1e-6
> > > (F(w1+eps,w2)-F(w1,w2))/eps
36.000003007075065
> > > (F(w1,w2+eps)-F(w1,w2))/eps
10.000000003174137

¡Se ve bien! Esto funciona bastante bien y es trivial de implementar, pero es solo una
aproximación y, lo que es más importante, debe llamarF()al menos una vez por parámetro (no
dos, ya que podríamos calcularf(w1, w2)sólo una vez). Esto hace que este enfoque sea intratable
para grandes redes neuronales. Entonces, en su lugar, deberíamos usar autodiff (verCapítulo 10
y???). TensorFlow hace esto bastante simple:

w1,w2=t.f..Variable(5.),t.f..Variable(3.) cont.f..
cinta de degradado()comocinta:
z=F(w1,w2)

gradientes=cinta.degradado(z, [w1,w2])

Primero definimos dos variablesw1yw2,entonces creamos untf.GradientTapecontexto que


registrará automáticamente cada operación que involucre una variable, y finalmente le
pediremos a esta cinta que calcule los gradientes del resultadozcon respecto a ambas variables
[w1, w2].Echemos un vistazo a los gradientes que calculó TensorFlow:

> > > gradientes


[<tf.Tensor: id=828234, forma=(), dtype=float32, numpy=36.0>,
<tf.Tensor: id=828229, forma=(), dtype=float32, numpy=10.0>]

¡Perfecto! No solo el resultado es preciso (la precisión solo está limitada por los errores de coma
flotante), sino que ladegradado()El método solo realiza los cálculos registrados una vez (en
orden inverso), sin importar cuántas variables haya, por lo que es increíblemente eficiente. ¡Es
como magia!

Sólo ponga el mínimo estricto dentro de latf.GradientTape()bloque, para


ahorrar memoria. Alternativamente, puede pausar la grabación creando
acon cinta.stop_recording()bloque dentro de latf.Gradient Tape()
bloquear.

La cinta se borra automáticamente inmediatamente después de llamar a sudegradado()método, por lo


que obtendrá una excepción si intenta llamardegradado()dos veces:

390 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


cont.f..cinta de degradado()comocinta:
z=F(w1,w2)

dz_dw1=cinta.degradado(z,w1)# =>tensor 36.0 dz_dw2=


cinta.degradado(z,w2)#¡Error de tiempo de ejecución!

Si necesitas llamardegradado()más de una vez, debe hacer que la cinta sea persistente y
eliminarla cuando haya terminado con ella para liberar recursos:

cont.f..cinta de degradado(persistente=Verdadero)comocinta:
z=F(w1,w2)

dz_dw1=cinta.degradado(z,w1)# =>tensor 36.0


dz_dw2=cinta.degradado(z,w2)# =>tensor 10.0, ¡funciona bien ahora!
delcinta

De manera predeterminada, la cinta solo rastreará las operaciones que involucren variables, por lo que si
intenta calcular el gradiente dezcon respecto a cualquier otra cosa que no sea una variable, el resultado será
serNinguna:

c1,c2=t.f..constante(5.),t.f..constante(3.) cont.f..
cinta de degradado()comocinta:
z=F(c1,c2)

gradientes=cinta.degradado(z, [c1,c2])#devuelve [Ninguno, Ninguno]

Sin embargo, puede forzar la cinta para que mire los tensores que desee, para grabar cada
operación que los involucre. Luego puede calcular gradientes con respecto a estos tensores,
como si fueran variables:

cont.f..cinta de degradado()comocinta:
cinta.reloj(c1)
cinta.reloj(c2) z=
F(c1,c2)

gradientes=cinta.degradado(z, [c1,c2])#devuelve [tensor 36., tensor 10.]

Esto puede ser útil en algunos casos, por ejemplo si se quiere implementar una regularización
de pérdidas que penalice activaciones que varíen mucho cuando las entradas varíen poco: la
pérdida estará en función del gradiente de las activaciones respecto a las entradas. Dado que
las entradas no son variables, deberá indicarle a la cinta que las observe.

Si calcula el gradiente de una lista de tensores (por ejemplo, [z1, z2, z3])con respecto a algunas
variables (por ejemplo, [w1, w2]),TensorFlow en realidad calcula de manera eficiente la suma de
los gradientes de estos tensores (es decir,gradiente (z1, [w1, w2]),másgradiente (z2, [w1, w2]),
másgradiente (z3, [w1, w2])).Debido a la forma en que funciona la diferencia automática en
modo inverso, no es posible calcular los gradientes individuales (z1, z2yz3)sin realmente llamar
degradado()varias veces (una vez porz1,una vez porz2y de una vez porz3), lo que requiere hacer
que la cinta sea persistente (y borrarla después).

Personalización de modelos y algoritmos de entrenamiento | 391


¡Además, es realmente posible calcular derivadas parciales de segundo orden (las hessianas, es
decir, las derivadas parciales de las derivadas parciales)! Para hacer esto, necesitamos registrar
las operaciones que se realizan al calcular las derivadas parciales de primer orden (los
jacobianos): esto requiere una segunda cinta. Así es como funciona:

cont.f..cinta de degradado(persistente=Verdadero)comocinta_hessian:
cont.f..cinta de degradado()comocinta_jacobiana:
z=F(w1,w2)
jacobianos=cinta_jacobiana.degradado(z, [w1,w2])
arpilleras=[cinta_hessian.degradado(jacobiano, [w1,w2])
porjacobianoenjacobianos]
delcinta_hessian

La cinta interior se usa para calcular los jacobianos, como hicimos antes. La cinta exterior se utiliza para
calcular las derivadas parciales de cada jacobiano. Ya que tenemos que llamardegradado()una vez para cada
jacobiano (o de lo contrario obtendríamos la suma de las derivadas parciales sobre todos los jabobianos,
como se explicó anteriormente), necesitamos que la cinta exterior sea persistente, por lo que la eliminamos al
final. Los jacobianos son obviamente los mismos que antes (36 y 5), pero ahora también tenemos los
hessianos:

> > > arpilleras#dz_dw1_dw1, dz_dw1_dw2, dz_dw2_dw1, dz_dw2_dw2


[[<tf.Tensor: id=830578, forma=(), dtype=float32, numpy=6.0>,
<tf.Tensor: id=830595, forma=(), dtype=float32, numpy=2.0>], [<tf.Tensor:
id=830600, forma=(), dtype=float32, numpy=2.0>, Ninguno] ]

Verifiquemos estas arpilleras. Las dos primeras son las derivadas parciales de6 * w1 + 2 * w2
(que es, como vimos antes, la derivada parcial deFcon respecto aw1),con respecto a
w1yw2.El resultado es correcto: 6 paraw1y 2 paraw2.Los dos siguientes son las
derivadas parciales de2 * w1 (la derivada parcial deFcon respecto aw2),con respecto a
w1yw2,cuales son 2 paraw1y 0 paraw2.Tenga en cuenta que TensorFlow devuelve
Ningunaen lugar de 0 desdew2no aparece en absoluto en2 * w1.TensorFlow también regresa
Ningunacuando utiliza una operación cuyos gradientes no están definidos (por ejemplo,tf.argmax()).

En algunos casos raros, es posible que desee evitar que los gradientes se propaguen hacia atrás
a través de alguna parte de su red neuronal. Para ello, debe utilizar eltf.stop_gradient()
función: simplemente devuelve sus entradas durante el pase hacia adelante (comotf.identidad()),pero
no deja pasar los gradientes durante la retropropagación (actúa como una constante). Por ejemplo:

definitivamenteF(w1,w2):

devolver3*w1**2+t.f..detener_gradiente(2*w1*w2)

cont.f..cinta de degradado()comocinta:
z=F(w1,w2)#mismo resultado que sin stop_gradient()

gradientes=cinta.degradado(z, [w1,w2])# =>devuelve [tensor 30., Ninguno]

392 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


Finalmente, ocasionalmente puede encontrarse con algunos problemas numéricos al calcular
gradientes. Por ejemplo, si calcula los gradientes de lami_softplus()función para entradas
grandes, el resultado será NaN:

> > > X=t.f..Variable([100.])


> > > cont.f..cinta de degradado()comocinta:
... z=mi_softplus(X)
...
> > > cinta.degradado(z, [X])
<tf.Tensor: [...] numpy=array([nan], dtype=float32)>

Esto se debe a que calcular los gradientes de esta función usando autodiff genera algunas
dificultades numéricas: debido a errores de precisión de punto flotante, autodiff termina
calculando infinito dividido por infinito (lo que devuelve NaN). Afortunadamente, podemos
encontrar analíticamente que la derivada de la función softplus es solo 1 / (1 + 1 / exp(x)), que es
numéricamente estable. Luego, podemos decirle a TensorFlow que use esta función estable
cuando calcule los gradientes delmi_softplus()función, decorándolo con
@tf.gradiente_personalizado,y haciendo que devuelva tanto su salida normal como la función que
calcula las derivadas (tenga en cuenta que recibirá como entrada los gradientes que se propagaron
hacia atrás hasta el momento, hasta la función softplus, y de acuerdo con la regla de la cadena
deberíamos multiplicarlos con el valor de esta función gradientes):

@tf.gradiente_personalizado
definitivamentemi_mejor_softplus(z):
Exp=t.f..Exp(z)
definitivamentemy_softplus_gradientes(graduado):

devolvergraduado/(1+1/Exp)
devolvert.f..Matemáticas.Iniciar sesión(Exp+1),my_softplus_gradientes

Ahora, cuando calculamos los gradientes de lami_mejor_softplus()función, obtenemos el


resultado adecuado, incluso para valores de entrada grandes (sin embargo, la salida principal
aún explota debido a la exponencial: una solución es usartf.dónde()para simplemente devolver
las entradas cuando son grandes).

¡Felicidades! Ahora puede calcular los gradientes de cualquier función (siempre que sea diferenciable
en el punto donde lo calcula), incluso puede calcular hessianos, bloquear la propagación hacia atrás
cuando sea necesario e incluso escribir sus propias funciones de gradiente. Esta es probablemente
más flexibilidad de la que necesitará, incluso si crea sus propios bucles de entrenamiento
personalizados, como veremos ahora.

Bucles de entrenamiento personalizados

En algunos casos raros, eladaptar()El método puede no ser lo suficientemente flexible para lo
que necesita hacer. Por ejemplo, el documento Wide and Deep que discutimos enCapítulo 10en
realidad usa dos optimizadores diferentes: uno para el camino ancho y el otro para el camino
profundo. Desde eladaptar()El método solo usa un optimizador (el que especificamos cuando

Personalización de modelos y algoritmos de entrenamiento | 393


compilar el modelo), la implementación de este documento requiere escribir su propio ciclo personalizado.

También puede escribir sus propios bucles de entrenamiento personalizados simplemente para
sentirse más seguro de que hace exactamente lo que pretende que haga (tal vez no esté seguro de
algunos detalles deladaptar()método). A veces puede sentirse más seguro hacer todo explícito. Sin
embargo, recuerde que escribir un ciclo de entrenamiento personalizado hará que su código sea más
largo, más propenso a errores y más difícil de mantener.

A menos que realmente necesite la flexibilidad adicional, debería preferir usar


eladaptar()método en lugar de implementar su propio ciclo de capacitación,
especialmente si trabaja en equipo.

Primero, construyamos un modelo simple. No es necesario compilarlo, ya que manejaremos el ciclo de entrenamiento
manualmente:

l2_reg=queras.regularizadores.l2(0.05)
modelo=queras.modelos.Secuencial([
queras.capas.Denso(30,activación="elú",kernel_initializer="él_normal",
kernel_regularizer=l2_reg),
queras.capas.Denso(1,kernel_regularizer=l2_reg)
])
A continuación, vamos a crear una pequeña función que muestree aleatoriamente un lote de instancias del
conjunto de entrenamiento (enCapítulo 13hablaremos de la API de datos, que ofrece una alternativa mucho
mejor):

definitivamentelote_aleatorio(X,y,tamaño del lote=32):


idx=notario público.aleatorio.al azar(Len(X),Talla=tamaño del
lote) devolverX[idx],y[idx]

Definamos también una función que muestre el estado de entrenamiento, incluido el número
de pasos, el número total de pasos, la pérdida media desde el comienzo de la época (es decir,
usaremos elSignificarmétrica para calcularlo), y otras métricas:

definitivamenteprint_status_bar(iteración,total,pérdida,métrica=Ninguna):
métrica="-".unirse(["{}: {:.4f}".formato(metro.nombre,metro.resultado())
pormetroen[pérdida]+(métricao[])])
final=""siiteración<totalmás"\norte"
impresión("\r{}/{} - ".formato(iteración,total)+métrica,
final=final)

Este código se explica por sí mismo, a menos que no esté familiarizado con el formato de cadena de
Python: {:.4f}formateará un flotante con 4 dígitos después del punto decimal. Además, usando
\r (retorno de carro) junto confinal=""asegura que la barra de estado siempre se imprima en la
misma línea. En el cuaderno, elprint_status_bar()La función también incluye una barra de
progreso, pero podría usar la práctica biblioteca tqdm en su lugar.

394 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


Con eso, ¡vamos al grano! Primero, necesitamos definir algunos hiperparámetros,
elegir el optimizador, la función de pérdida y las métricas (solo el MAE en este
ejemplo):
n_épocas=5
tamaño del lote=32
n_pasos=Len(X_tren)//optimizador de tamaño de lote=
queras.optimizadores.Nadam(yo=0.01) loss_fn=queras.
pérdidas.error_medio_cuadrado_pérdida_media=queras.
métrica.Significar() métrica=[queras.métrica.Error
absoluto medio()]

¡Y ahora estamos listos para construir el bucle personalizado!

porépocaenrango(1,n_épocas+1):
impresión("Época {}/{}".formato(época,n_épocas))
porpasoenrango(1,n_pasos+1):
X_lote,y_lote=lote_aleatorio(X_train_scaled,y_tren) cont.f..cinta
de degradado()comocinta:
y_pred=modelo(X_lote,capacitación=Verdadero) pérdida_principal=t.f..
reducir_media(loss_fn(y_lote,y_pred)) pérdida=t.f..añadir_n([pérdida_principal]+
modelo.pérdidas) gradientes=cinta.degradado(pérdida,modelo.
variables_entrenables) optimizador.aplicar_gradientes(Código Postal(gradientes,modelo
.variables_entrenables)) pérdida_media(pérdida) pormétricoenmétrica:

métrico(y_lote,y_pred)
print_status_bar(paso*tamaño del lote,Len(y_tren),pérdida_media,métrica)
print_status_bar(Len(y_tren),Len(y_tren),pérdida_media,métrica) pormétricoen[
pérdida_media]+métrica:
métrico.restablecer_estados()

Están sucediendo muchas cosas en este código, así que analicemos:

• Creamos dos bucles anidados: uno para las épocas, el otro para los lotes dentro de
una época.
• Luego muestreamos un lote aleatorio del conjunto de entrenamiento.

• Dentro detf.GradientTape()bloque, hacemos una predicción para un lote (usando el


modelo como una función), y calculamos la pérdida: es igual a la pérdida principal
más las otras pérdidas (en este modelo, hay una pérdida de regularización por capa).
Desde elerror medio cuadrado()función devuelve una pérdida por instancia, calculamos
la media sobre el lote usandotf.reduce_mean() (si quisiera aplicar diferentes pesos a
cada instancia, aquí es donde lo haría). Las pérdidas de regularización ya están
reducidas a un solo escalar cada una, por lo que solo necesitamos sumarlas (usando
tf.add_n(),que suma múltiples tensores de la misma forma y tipo de datos).

Personalización de modelos y algoritmos de entrenamiento | 395


• A continuación, le preguntamos alcintapara calcular el gradiente de la pérdida con respecto a cada
variable entrenable (no¡todas las variables!), y las aplicamos al optimizador para realizar un paso
de Descenso de Gradiente.

• A continuación, actualizamos la pérdida media y las métricas (sobre la época actual) y mostramos
la barra de estado.

• Al final de cada época, mostramos la barra de estado nuevamente para que se vea completa11y
para imprimir un salto de línea, y restablecemos los estados de la pérdida media y las métricas.

Si configura el optimizadorclipnormovalor de cliphiperparámetros, se encargará de esto por


usted. Si desea aplicar cualquier otra transformación a los degradados, simplemente hágalo
antes de llamar alaplicar_gradientes()método.

Si agrega restricciones de peso a su modelo (por ejemplo, al establecerkernel_constrainto


restricción_sesgoal crear una capa), debe actualizar el ciclo de entrenamiento para aplicar
estas restricciones justo despuésaplicar_gradientes():

porvariableenmodelo.Variables:
sivariable.restricciónno esNinguna:
variable.asignar(variable.restricción(variable))

Lo que es más importante, este ciclo de entrenamiento no maneja capas que se comporten de manera
diferente durante el entrenamiento y la prueba (p. ej.,Normalización por lotesoAbandonar).Para manejar
esto, necesita llamar al modelo conentrenamiento=Verdaderoy asegúrese de que propague esto a cada capa
que lo necesite.12

Como puede ver, hay muchas cosas que debe hacer bien, es fácil cometer un error. Pero
en el lado positivo, obtienes el control total, así que es tu decisión.

Ahora que ya sabes cómo personalizar cualquier parte de tus maquetas13y algoritmos de
entrenamiento, veamos cómo puede usar la función de generación automática de gráficos de
TensorFlow: puede acelerar considerablemente su código personalizado y también lo hará portátil a
cualquier plataforma compatible con TensorFlow (ver???).

Funciones y gráficos de TensorFlow


En TensorFlow 1, los gráficos eran inevitables (al igual que las complejidades que los
acompañaban): eran una parte central de la API de TensorFlow. En TensorFlow 2, todavía están

11 La verdad es que no procesamos cada una de las instancias del conjunto de entrenamiento porque muestreamos las instancias ejecutadas.

domly, por lo que algunos se procesaron más de una vez, mientras que otros no se procesaron en absoluto. En la práctica está bien. Además, si el

tamaño del conjunto de entrenamiento no es un múltiplo del tamaño del lote, nos perderemos algunos casos.

12 Como alternativa, echa un vistazoK.aprendizaje_fase(), K.set_aprendizaje_fase()yK.aprendizaje_fase_ámbito().

13 Con la excepción de los optimizadores, muy pocas personas los personalizan: consulte el cuaderno para ver un ejemplo.

396 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


allí, pero no tan central, y mucho (¡mucho!) más simple de usar. Para demostrar esto,
comencemos con una función trivial que solo calcula el cubo de su entrada:

definitivamentecubo(X):

devolverX**3

Obviamente, podemos llamar a esta función con un valor de Python, como un int o un float, o
podemos llamarla con un tensor:

> > > cubo(2)


8
> > > cubo(t.f..constante(2.0))
<tf.Tensor: id=18634148, forma=(), dtype=float32, numpy=8.0>

Ahora, usemostf.función()para convertir esta función de Python en unaFunción TensorFlow


:

> > > cubo_tf=t.f..función(cubo)


> > > cubo_tf
<tensorflow.python.eager.def_function.Function en 0x1546fc080>

Esta función TF se puede usar exactamente como la función original de Python y devolverá el
mismo resultado (pero como tensores):

> > > cubo_tf(2)


<tf.Tensor: id=18634201, forma=(), dtype=int32, numpy=8>
> > > cubo_tf(t.f..constante(2.0))
<tf.Tensor: id=18634211, forma=(), dtype=float32, numpy=8.0>

Bajo el capó,tf.función()analizó los cálculos realizados por elcubo()


función y generó un gráfico de cálculo equivalente! Como puede ver, fue bastante indoloro
(veremos cómo funciona esto en breve). Alternativamente, podríamos haber usado
función tfcomo decorador; esto es en realidad más común:

@tf.función
definitivamentecubo_tf(X):

devolverX**3

La función Python original todavía está disponible a través de la función TF.función_python


atributo, en caso de que alguna vez lo necesite:

> > > cubo_tf.función_python(2) 8

TensorFlow optimiza el gráfico de cálculo, elimina los nodos no utilizados, simplifica las
expresiones (p. ej., 1 + 2 se reemplazaría por 3) y más. Una vez que el gráfico optimizado está
listo, la Función TF ejecuta eficientemente las operaciones en el gráfico, en el orden apropiado
(y en paralelo cuando puede). Como resultado, una función TF generalmente se ejecutará
mucho más rápido que la función original de Python, especialmente si realiza tareas complejas.

Funciones y gráficos de TensorFlow | 397


cálculos14La mayoría de las veces, no necesitará saber más que eso: cuando desee
impulsar una función de Python, simplemente transfórmela en una función TF. ¡Eso
es todo!

Además, cuando escribe una función de pérdida personalizada, una métrica personalizada, una capa
personalizada o cualquier otra función personalizada, y la usa en un modelo Keras (como hicimos a lo largo
de este capítulo), Keras convierte automáticamente su función en una función TF, no hay necesidad de usar
tf.función().Entonces, la mayoría de las veces, toda esta magia es 100% transparente.

Puedes decirle a Kerasnopara convertir sus funciones de Python en funciones


TF configurandodinámico=Verdaderoal crear una capa personalizada o un
modelo personalizado. Como alternativa, puede configurar
run_eagerly=Verdadero al llamar a la modelocompilar()método.

TF Function genera un nuevo gráfico para cada conjunto único de formas de entrada y tipos de
datos, y lo almacena en caché para llamadas posteriores. Por ejemplo, si llama
tf_cube(tf.constant(10)),se generará un gráfico para int32 tensores de forma []. Entonces si llamas
tf_cubo(tf.constante(20)),se reutilizará el mismo gráfico. Pero si luego llamas
tf_cubo(tf.constante([10, 20])),se generará un nuevo gráfico para int32 tensores de forma
[2]. Así es como las funciones TF manejan el polimorfismo (es decir, tipos y formas de
argumentos variables). Sin embargo, esto solo es cierto para los argumentos de tensor: si
pasa valores numéricos de Python a una función TF, se generará un nuevo gráfico para
cada valor distinto: por ejemplo, llamandotf_cubo(10)ytf_cubo(20)generará dos gráficos.

Si llama a una función TF muchas veces con diferentes valores numéricos


de Python, se generarán muchos gráficos, lo que ralentizará su programa
y consumirá una gran cantidad de RAM. Los valores de Python deben
reservarse para argumentos que tendrán pocos valores únicos, como
hiperparámetros como el número de neuronas por capa. Esto permite
que TensorFlow optimice mejor cada variante de su modelo.

Autógrafo y Trazado
Entonces, ¿cómo genera TensorFlow gráficos? Bueno, primero comienza analizando el código fuente
de la función de Python para capturar todas las declaraciones de flujo de control, comoporbucles y
tiempobucles,sideclaraciones, así comoromper, continuarydevolverdeclaraciones. Este primer paso se
llamaautógrafo. La razón por la que TensorFlow tiene que analizar el código fuente es que Python no
proporciona ninguna otra forma de capturar declaraciones de flujo de control: ofrece métodos
mágicos como __agregar__()o __mul__()para capturar operadores como

14 Sin embargo, en este ejemplo trivial, el gráfico de cálculo es tan pequeño que no hay nada que optimizar, por lo que
tf_cubo()en realidad corre mucho más lento quecubo().

398 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


+ y *, pero no hay __tiempo__()o __si__()métodos mágicos. Después de analizar el
código de la función, autograph genera una versión mejorada de esa función en la
que todas las declaraciones de flujo de control se reemplazan por las operaciones de
TensorFlow apropiadas, comotf.while_loop()para bucles ytf.cond()porsideclaraciones.
por ejemplo, enFigura 12-4, autógrafo analiza el código fuente delsuma_cuadrados()
función de Python, y genera eltf__suma_cuadrados()función. En esta función, elpor
bucle se sustituye por la definición de labucle_cuerpo()función (que contiene el cuerpo
del originalporbucle), seguido de una llamada alfor_stmt()función. Esta convocatoria
construirá la adecuadatf.while_loop()operación en el gráfico de cálculo.

Figura 12-4. Cómo TensorFlow genera gráficos usando autograph y tracing

Luego, TensorFlow llama a esta función "actualizada", pero en lugar de pasar el argumento real,
pasa untensor simbólico, es decir, un tensor sin ningún valor real, solo un nombre, un tipo de
datos y una forma. Por ejemplo, si llamasuma_cuadrados(tf. constante(10)),entonces el
tf__suma_cuadrados()La función se llamará con un tensor simbólico de tipo int32 y forma []. La
función se ejecutará enmodo gráfico, lo que significa que cada operación de TensorFlow
simplemente agregará un nodo en el gráfico para representarse a sí mismo y su(s) tensor(es) de
salida (a diferencia del modo normal, llamadoejecución ansiosa, o modo ansioso). En el modo
gráfico, las operaciones TF no realizan ningún cálculo real. Esto debería resultarte familiar si
conoces TensorFlow 1, ya que el modo gráfico era el modo predeterminado. EnFigura 12-4,
puedes ver eltf__suma_cuadrados()función que se llama con un tensor simbólico como
argumento (en este caso, un tensor int32 de forma []), y el gráfico final generado durante el
rastreo. Las elipses representan operaciones y las flechas representan tensores (tanto la función
generada como el gráfico están simplificados).

Funciones y gráficos de TensorFlow | 399


Para ver el código fuente de la función generada, puede llamartf.auto
graph.to_code(sum_squares.python_function).el codigo no es
destinado a ser bonito, pero a veces puede ayudar para la depuración.

Reglas de la función TF

La mayoría de las veces, convertir una función de Python que realiza operaciones de TensorFlow en
una función TF es trivial: simplemente decórela con @función tfo deja que Keras se encargue de ello
por ti. Sin embargo, hay algunas reglas a respetar:

• Si llama a cualquier biblioteca externa, incluida NumPy o incluso la biblioteca estándar, esta
llamada se ejecutará solo durante el seguimiento, no será parte del gráfico. De hecho, un gráfico
de TensorFlow solo puede incluir construcciones de TensorFlow (tensores, operaciones, variables,
conjuntos de datos, etc.). Así que asegúrate de usartf.reduce_sum()en vez de
np.suma(),ytf.sort()en lugar del incorporadoordenado ()función, y así sucesivamente (a menos que
realmente desee que el código se ejecute solo durante el seguimiento).

— Por ejemplo, si define una función TFf(x)que solo regresanp.ran


dom.rand(),un número aleatorio sólo se generará cuando la función es
rastreado, por lo quef(tf.constante(2.))yf(tf.constante(3.))devolverá el
mismo número aleatorio, perof(tf.constante([2., 3.]))devolverá una diferente
una. si reemplazasnp.aleatorio.rand()contf.aleatorio.uniforme([]),Entonces un
se generará un nuevo número aleatorio en cada llamada, ya que la operación será
parte del gráfico.
— Si su código que no es de TensorFlow tiene efectos secundarios (como registrar algo o
actualizar un contador de Python), entonces no debe esperar que ese efecto secundario
ocurra cada vez que llame a la función TF, ya que solo ocurrirá cuando la función - ción se
remonta.

— Puede envolver código Python arbitrario en untf.py_function()operación, pero esto


dificultará el rendimiento, ya que TensorFlow no podrá optimizar el gráfico en este
código, y también reducirá la portabilidad, ya que el gráfico solo se ejecutará en
plataformas donde Python esté disponible (y las bibliotecas correctas instaladas).

• Puede llamar a otras funciones de Python o funciones TF, pero deben seguir las
mismas reglas, ya que TensorFlow también capturará sus operaciones en el gráfico de
cálculo. Tenga en cuenta que estas otras funciones no necesitan estar decoradas con
@tf.función.
• Si la función crea una variable de TensorFlow (o cualquier otro objeto de TensorFlow con estado,
como un conjunto de datos o una cola), debe hacerlo en la primera llamada, y solo entonces, de
lo contrario obtendrá una excepción. Por lo general, es preferible crear variables fuera de la
función TF (por ejemplo, en elconstruir()método de una capa personalizada).

400 | Capítulo 12: Modelos personalizados y entrenamiento con TensorFlow


• El código fuente de su función de Python debería estar disponible para TensorFlow. Si el
código fuente no está disponible (por ejemplo, si define su función en el shell de Python,
que no da acceso al código fuente, o si implementa solo los archivos compilados de Python
*.pyca producción), entonces el proceso de generación de gráficos fallará o tendrá una
funcionalidad limitada.

• TensorFlow solo capturaráporbucles que iteran sobre un tensor o unConjunto de datos.Asi que
asegúrate de usarpara i en tf.range(10)más bien quepara i en el rango (10),o
de lo contrario, el bucle no se capturará en el gráfico. En su lugar, se ejecutará durante el rastreo.
Esto puede ser lo que quieres, si elporloop está destinado a construir el gráfico, por ejemplo,
para crear cada capa en una red neuronal.

• Y como siempre, por motivos de rendimiento, debería preferir una implementación


vectorizada siempre que pueda, en lugar de utilizar bucles.

¡Es hora de resumir! En este capítulo, comenzamos con una breve descripción general de
TensorFlow, luego analizamos la API de bajo nivel de TensorFlow, incluidos tensores,
operaciones, variables y estructuras de datos especiales. Luego usamos estas herramientas
para personalizar casi todos los componentes en tf.keras. Finalmente, analizamos cómo las
funciones TF pueden mejorar el rendimiento, cómo se generan los gráficos usando autógrafos y
trazados, y qué reglas seguir al escribir funciones TF (si desea abrir un poco más la caja negra,
por ejemplo, para explorar el gráficos generados, encontrará más detalles técnicos en???).

En el próximo capítulo, veremos cómo cargar y preprocesar datos de manera eficiente con
TensorFlow.

Funciones y gráficos de TensorFlow | 401


CAPÍTULO 13

Carga y preprocesamiento de datos con


TensorFlow

Con los libros electrónicos Early Release, obtiene libros en su forma más
temprana, el contenido sin editar y sin editar del autor mientras escribe,
para que pueda aprovechar estas tecnologías mucho antes del
lanzamiento oficial de estos títulos. El siguiente será el Capítulo 13 en la
versión final del libro.

Hasta ahora, solo hemos utilizado conjuntos de datos que caben en la memoria, pero los sistemas de
aprendizaje profundo a menudo se entrenan en conjuntos de datos muy grandes que no caben en la RAM.
Ingerir un gran conjunto de datos y preprocesarlo de manera eficiente puede ser complicado de implementar
con otras bibliotecas de aprendizaje profundo, pero TensorFlow lo hace fácil gracias a laAPI de datos:
simplemente crea un objeto de conjunto de datos, le dice dónde obtener los datos, luego lo transforma de la
manera que desee, y TensorFlow se encarga de todos los detalles de implementación, como subprocesos
múltiples, colas, procesamiento por lotes, captura previa, etc. .

De serie, la API de datos puede leer archivos de texto (como archivos CSV), archivos binarios con
registros de tamaño fijo y archivos binarios que usan el formato TFRecord de TensorFlow, que
admite registros de diferentes tamaños. TFRecord es un formato binario flexible y eficiente
basado en Protocol Buffers (un formato binario de código abierto). La API de datos también
tiene soporte para leer desde bases de datos SQL. Además, muchas extensiones de código
abierto están disponibles para leer todo tipo de fuentes de datos, como el servicio BigQuery de
Google.

Sin embargo, leer grandes conjuntos de datos de manera eficiente no es la única dificultad: los datos
también deben ser preprocesados. De hecho, no siempre se compone estrictamente de campos
numéricos convenientes: a veces habrá características de texto, características categóricas, etc. Para
manejar esto, TensorFlow proporciona laFunciones API: le permite convertir fácilmente estas
funciones en funciones numéricas que su red neuronal puede consumir. Para

403
ejemplo, las características categóricas con una gran cantidad de categorías (como ciudades o
palabras) se pueden codificar usandoincrustaciones(como veremos, una incrustación es un vector
denso entrenable que representa una categoría).

Tanto la API de datos como la API de funciones funcionan a la perfección con


tf.keras.

En este capítulo, cubriremos la API de datos, el formato TFRecord y la API de características en


detalle. También echaremos un vistazo rápido a algunos proyectos relacionados del ecosistema
de Tensor‐Flow:

• Transformación TF (tf.Transformar) permite escribir una sola función de preprocesamiento que se


puede ejecutar en modo por lotes en su conjunto de entrenamiento completo, antes del
entrenamiento (para acelerarlo), y luego exportar a una función TF e incorporarla a su modelo
entrenado, de modo que una vez se implementa en producción, puede encargarse del
preprocesamiento de nuevas instancias sobre la marcha.

• TF Datasets (TFDS) proporciona una función conveniente para descargar muchos conjuntos de datos
comunes de todo tipo, incluidos los grandes como ImageNet, y proporciona objetos de conjuntos de
datos convenientes para manipularlos usando la API de datos.

¡Entonces empecemos!

La API de datos
Toda la API de datos gira en torno al concepto de unconjunto de datos: como puede sospechar, esto representa una
secuencia de elementos de datos. Por lo general, usará conjuntos de datos que leen gradualmente los datos del disco,
pero para simplificar, creemos un conjunto de datos completamente en RAM usando

tf.data.Dataset.from_tensor_slices():
> > > X=t.f..rango(10)#cualquier tensor de datos
> > > conjunto de datos=t.f..datos.conjunto de datos.from_tensor_slices(X)
> > > conjunto de datos
<Formas de TensorSliceDataset: (), tipos: tf.int32>

losfrom_tensor_slices()función toma un tensor y crea untf.data.Conjunto de datos


cuyos elementos son todas las rebanadas deX (a lo largo de la primera dimensión), por lo que este conjunto
de datos contiene 10 elementos: tensores 0, 1, 2, …, 9. En este caso habríamos obtenido el mismo
conjunto de datos si hubiéramos usadotf.data.Conjunto de datos.rango(10).

Simplemente puede iterar sobre los elementos de un conjunto de datos como este:

> > > porartículoenconjunto de datos:


... impresión(artículo)

404 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


...
tf.Tensor(0, forma=(), dtype=int32)
tf.Tensor(1, forma=(), dtype=int32)
tf.Tensor(2, forma=(), dtype=int32) [...]

tf.Tensor(9, forma=(), dtype=int32)

Transformaciones de encadenamiento

Una vez que tenga un conjunto de datos, puede aplicarle todo tipo de transformaciones llamando a sus
métodos de transformación. Cada método devuelve un nuevo conjunto de datos, por lo que puede encadenar
transformaciones como esta (esta cadena se ilustra enFigura 13-1):

> > > conjunto de datos=conjunto de datos.repetir(3).lote(7)


> > > porartículoenconjunto de datos:
... impresión(artículo)

...
tf.Tensor([0 1 2 3 4 5 6], forma=(7,), dtype=int32)
tf.Tensor([7 8 9 0 1 2 3], forma=(7,), dtype=int32)
tf.Tensor([4 5 6 7 8 9 0], forma=(7,), dtype=int32)
tf.Tensor([1 2 3 4 5 6 7], forma=(7,), dtype=int32)
tf.Tensor([8 9], forma=(2,), dtype=int32)

Figura 13-1. Encadenamiento de transformaciones de conjuntos de datos

En este ejemplo, primero llamamos alrepetir()en el conjunto de datos original y devuelve un nuevo conjunto
de datos que repetirá los elementos del conjunto de datos original 3 veces. ¡Por supuesto, esto no copiará
todos los datos en la memoria 3 veces! De hecho, si llama a este método sin argumentos, el nuevo conjunto
de datos repetirá el conjunto de datos de origen para siempre. Entonces llamamos a lalote()método en este
nuevo conjunto de datos, y nuevamente esto crea un nuevo conjunto de datos. Este agrupará los elementos
del conjunto de datos anterior en lotes de 7 elementos. Finalmente, iteramos sobre los elementos de este
conjunto de datos final. Como puedes ver, ellote()
El método tuvo que generar un lote final de tamaño 2 en lugar de 7, pero puede llamarlo con
drop_remainder=Verdaderosi desea que suelte este lote final para que todos los lotes tengan exactamente el
mismo tamaño.

La API de datos | 405


Los métodos del conjunto de datos hacennomodifican conjuntos de datos, crean otros nuevos, así

que asegúrese de mantener una referencia a estos nuevos conjuntos de datos (por ejemplo,conjunto

de datos = ...),o de lo contrario no pasará nada.

También puede aplicar cualquier transformación que desee a los elementos llamando almapa()
método. Por ejemplo, esto crea un nuevo conjunto de datos con todos los elementos duplicados:

> > > conjunto de datos=conjunto de datos.mapa(lambdaX:X*2)#Elementos: [0,2,4,6,8,10,12]

Esta función es la que llamará para aplicar cualquier preprocesamiento que desee a sus datos. A
veces, esto incluirá cálculos que pueden ser bastante intensivos, como remodelar o rotar una
imagen, por lo que normalmente querrá generar varios subprocesos para acelerar las cosas: es
tan simple como configurar elnum_llamadas_paralelasargumento.

Mientras que lamapa()aplica una transformación a cada elemento, elaplicar()El método aplica una
transformación al conjunto de datos como un todo. Por ejemplo, el siguiente código "desmonta" el
conjunto de datos aplicando eldeshacer lote()función al conjunto de datos (esta función es actualmente
experimental, pero lo más probable es que se mueva a la API central en una versión futura). Cada
elemento del nuevo conjunto de datos será un único tensor de enteros en lugar de un lote de 7
enteros:

> > > conjunto de datos=conjunto de datos.aplicar(t.f..datos.experimental.desatar())#Artículos: 0,2,4,...

También es posible simplemente filtrar el conjunto de datos usando elfiltrar()método:

> > > conjunto de datos=conjunto de datos.filtrar(lambdaX:X<10)#Artículos: 0 2 4 6 8 0 2 4 6...

A menudo querrá ver solo algunos elementos de un conjunto de datos. Puedes usar eltomar()
método para eso:

> > > porartículoenconjunto de datos.tomar(3):


... impresión(artículo)

...
tf.Tensor(0, forma=(), dtype=int64)
tf.Tensor(2, forma=(), dtype=int64)
tf.Tensor(4, forma=(), dtype=int64)

Mezclar los datos


Como sabe, Gradient Descent funciona mejor cuando las instancias del conjunto de entrenamiento
son independientes y están distribuidas de manera idéntica (verCapítulo 4). Una forma sencilla de
garantizar esto es barajar las instancias. Para esto, solo puedes usar elbarajar()método. Creará un
nuevo conjunto de datos que comenzará llenando un búfer con los primeros elementos del conjunto
de datos de origen, luego, cada vez que se le solicite un elemento, extraerá uno al azar del búfer y lo
reemplazará con uno nuevo de el conjunto de datos de origen, hasta que haya iterado por completo a
través del conjunto de datos de origen. En este punto, continúa extrayendo elementos aleatoriamente
del búfer hasta que se vacía. Debe especificar el tamaño del búfer y

406 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


es importante que sea lo suficientemente grande o, de lo contrario, el barajado no será muy eficiente.1
Sin embargo, obviamente no exceda la cantidad de RAM que tiene, e incluso si tiene suficiente,
no hay necesidad de ir más allá del tamaño del conjunto de datos. Puede proporcionar una
semilla aleatoria si desea el mismo orden aleatorio cada vez que ejecuta su programa.

> > > conjunto de datos=t.f..datos.conjunto de datos.rango(10).repetir(3)#0 a 9, tres veces


> > > conjunto de datos=conjunto de datos.barajar(tamaño del búfer=5,semilla=42).lote(7)
> > > porartículoenconjunto de datos:
... impresión(artículo)

...
tf.Tensor([0 2 3 6 7 9 4], forma=(7,), dtype=int64)
tf.Tensor([5 0 1 1 8 6 5], forma=(7,), dtype=int64)
tf.Tensor([4 8 7 1 2 3 0], forma=(7,), dtype=int64)
tf.Tensor([5 4 2 7 8 9 9], forma=(7,), dtype=int64)
tf.Tensor([3 6], forma=(2,), dtype=int64)

si llamasrepetir()en un conjunto de datos mezclados, por defecto generará un


nuevo orden en cada iteración. En general, esta es una buena idea, pero si
prefiere reutilizar el mismo orden en cada iteración (por ejemplo, para pruebas
o depuración), puede establecerreshuffle_each_iteration=Falso.

Para un conjunto de datos grande que no cabe en la memoria, este enfoque simple de búfer de
barajado puede no ser suficiente, ya que el búfer será pequeño en comparación con el conjunto de
datos. Una solución es mezclar los datos de origen en sí (por ejemplo, en Linux puede mezclar
archivos de texto usando elshufdominio). ¡Esto definitivamente mejorará mucho la reproducción
aleatoria! Sin embargo, incluso si se mezclan los datos de origen, por lo general querrá mezclarlos un
poco más, o de lo contrario se repetirá el mismo orden en cada época, y el modelo puede terminar
sesgado (por ejemplo, debido a algunos patrones falsos presentes por probabilidad en el orden de los
datos de origen). Para mezclar un poco más las instancias, un enfoque común es dividir los datos de
origen en varios archivos y luego leerlos en un orden aleatorio durante el entrenamiento. Sin
embargo, las instancias ubicadas en el mismo archivo seguirán estando cerca unas de otras. Para
evitar esto, puede elegir varios archivos al azar y leerlos simultáneamente, intercalando sus líneas.
Luego, además de eso, puede agregar un búfer de barajado usando elbarajar()
método. Si todo esto le parece mucho trabajo, no se preocupe: la API de datos en realidad hace que todo esto
sea posible en solo unas pocas líneas de código. Veamos cómo hacer esto.

1 Imagine una baraja ordenada de cartas a su izquierda: suponga que solo toma las 3 cartas superiores y las baraja, luego elige
uno al azar y colócalo a tu derecha, manteniendo los otros 2 en tus manos. Tome otra carta a su izquierda, baraje
las 3 cartas en sus manos y elija una de ellas al azar, y colóquela a su derecha. Cuando termines de pasar todas
las cartas así, tendrás una baraja de cartas a tu derecha: ¿crees que estará perfectamente barajada?

La API de datos | 407


Intercalar líneas de varios archivos

Primero, supongamos que cargó el conjunto de datos de vivienda de California, lo mezcló (a menos que ya lo
haya hecho), lo dividió en un conjunto de entrenamiento, un conjunto de validación y un conjunto de prueba,
luego dividió cada conjunto en muchos archivos CSV que cada se vea así (cada fila contiene 8 características
de entrada más el valor de la casa mediana objetivo):

MedInc,HouseAge,AveRooms,AveBedrms,Popul,AveOccup,Lat,Long,MedianHouseValue
3.5214,15.0,3.0499,1.1065,1447.0,1.6059,37.63,-122.43,1.442
5.3275,5.0,6.4900,0.9910,3464.0,3.4433,33.69,-117.39,1.687
3.1,29.0,7.5423,1.5915,1328.0,2.2508,38.44,-122.98,1.621 [...]

Supongamos tambiéntren_filepathscontiene la lista de rutas de archivo (y también tiene


rutas_de_archivo_válidasyrutas_de_archivo_de_prueba):

> > > tren_filepaths


['conjuntos de datos/vivienda/mi_tren_00.csv', 'conjuntos de datos/vivienda/mi_tren_01.csv',...]

Ahora vamos a crear un conjunto de datos que contenga solo estas rutas de archivo:

ruta_archivo_conjunto_datos=t.f..datos.conjunto de datos.list_files(tren_filepaths,semilla=42)

Por defecto, ellist_files()La función devuelve un conjunto de datos que mezcla las rutas de los
archivos. En general, esto es algo bueno, pero puede establecerbarajar = falsosi no quieres eso,
por alguna razón.

A continuación, podemos llamar a laintercalar()método para leer de 5 archivos a la vez e intercalar sus
líneas (saltando la primera línea de cada archivo, que es la fila del encabezado, usando el
saltar()método):
n_lectores=5
conjunto de datos=ruta_archivo_conjunto_datos.intercalar(
lambdaruta de archivo:t.f..datos.TextLineDataset(ruta de archivo).saltar(1),
duración del ciclo=n_lectores)

losintercalar()El método creará un conjunto de datos que extraerá 5 rutas de archivo del
ruta de archivo_conjunto de datos,y para cada uno llamará a la función que le dimos (una lambda en este ejemplo) para

crear un nuevo conjunto de datos, en este caso unConjunto de datos de línea de texto.Luego recorrerá estos 5 conjuntos
de datos, leyendo una línea a la vez de cada uno hasta que todos los conjuntos de datos se queden sin elementos.
Luego obtendrá las siguientes 5 rutas de archivo desde elruta de archivo_conjunto de datos,e intercalarlos de la misma
manera, y así sucesivamente hasta que se quede sin rutas de archivo.

Para que el intercalado funcione mejor, es preferible tener archivos de la misma


longitud, de lo contrario, el final de los archivos más largos no se intercalará.

408 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


Por defecto,intercalar()no usa paralelismo, solo lee una línea a la vez de cada archivo,
secuencialmente. Sin embargo, si desea que lea archivos en paralelo, puede configurar el
num_llamadas_paralelasargumento al número de subprocesos que desea. Incluso puedes
configurarlo paratf.data.experimental.AUTÓNOMOpara hacer que TensorFlow elija la cantidad
correcta de subprocesos dinámicamente en función de la CPU disponible (sin embargo, esta es
una característica experimental por ahora). Veamos ahora lo que contiene el conjunto de datos:

> > > porlíneaenconjunto de datos.tomar(5):


... impresión(línea.entumecido())
...
b'4.2083,44.0,5.3232,0.9171,846.0,2.3370,37.47,-122.2,2.782'
b'4.1812,52.0,5.7013,0.9965,692.0,2.4027,33.73,-118.31,3.215'
0.9930,457.0,3.1958,34.04, -118.15,1.625
'b'3.3456,37.0,4.5140,0.9084,458.0,3.2253,36.67, -121.7,2.526'
b'3.5214,15.0,04999999. -122.43,1.442'

Estas son las primeras filas (ignorando la fila del encabezado) de 5 archivos CSV, elegidos al azar. ¡Se
ve bien! Pero como puede ver, estas son solo cadenas de bytes, necesitamos analizarlas y también
escalar los datos.

Preprocesamiento de los datos

Implementemos una pequeña función que realizará este preprocesamiento:

X_media,X_std=[...]#media y escala de cada característica en el conjunto de entrenamiento


n_entradas=8

definitivamentepreprocesar(línea):

defs=[0.]*n_entradas+[t.f..constante([],tipo de d=t.f..flotar32)]
campos=t.f..yo.decodificar_csv(línea,record_defaults=defs) X=t.f..pila
(campos[:-1]) y=t.f..pila(campos[-1:]) devolver(X-X_media)/X_std,y

Recorramos este código:

• Primero, asumimos que ha calculado previamente la media y la desviación estándar de cada


función en el conjunto de entrenamiento.X_mediayX_stdson solo tensores 1D (o matrices NumPy)
que contienen 8 flotantes, uno por función de entrada.

• Lospreprocesar()La función toma una línea CSV y comienza analizándola. Para ello utiliza el
tf.io.decode_csv()función, que toma dos argumentos: el primero es la línea para analizar, y el
segundo es una matriz que contiene el valor predeterminado para cada columna en el archivo
CSV. Esto le dice a TensorFlow no solo el valor predeterminado para cada columna, sino también
la cantidad de columnas y el tipo de cada columna. En este ejemplo, le decimos que todas las
columnas de características son flotantes y que los valores faltantes deberían ser 0 por defecto,
pero proporcionamos una matriz vacía de tipotf.float32como el valor predeterminado para la
última columna (el destino): esto le dice a TensorFlow que esta columna contiene

La API de datos | 409


contiene flotantes, pero que no hay un valor predeterminado, por lo que generará una excepción si
encuentra un valor faltante.

• Losdecodificar_csv()La función devuelve una lista de tensores escalares (uno por columna) pero
necesitamos devolver matrices de tensores 1D. Entonces llamamostf.pila()en todos los tensores
excepto el último (el objetivo): esto apilará estos tensores en una matriz 1D. Luego hacemos lo
mismo para el valor objetivo (esto lo convierte en una matriz de tensores 1D con un solo valor, en
lugar de un tensor escalar).

• Finalmente, escalamos las características de entrada restando las medias de las características y luego
dividiéndolas por las desviaciones estándar de las características, y devolvemos una tupla que contiene las
características escaladas y el objetivo.

Probemos esta función de preprocesamiento:

> > > preprocesar(b'4.2083,44.0,5.3232,0.9171,846.0,2.3370,37.47,-122.2,2.782')


(<tf.Tensor: id=6227, forma=(8,), dtype=float32, numpy=
matriz([ 0.16579159, 1.216324 , -0.05204564 , -0.39215982 , -0.5277444 ,
- 0.2633488 , 0.8543046 , -1.3072058 ], dtype=float32)>,
<tf.Tensor: [...], numpy=array([2.782], dtype=float32)>)

Ahora podemos aplicar esta función de preprocesamiento al conjunto de datos.

Poniendo todo junto


Para hacer que el código sea reutilizable, reunamos todo lo que hemos discutido hasta ahora en una pequeña función
de ayuda: creará y devolverá un conjunto de datos que cargará de manera eficiente los datos de viviendas de
California de múltiples archivos CSV, luego los barajará, los preprocesará y los procesará por lotes. eso (verFigura 13-2
):

definitivamentecsv_reader_dataset(rutas de archivos,repetir=Ninguna,n_lectores=5,
n_read_threads=Ninguna,shuffle_buffer_size=10000,
n_parse_hilos=5,tamaño del lote=32):
conjunto de datos=t.f..datos.conjunto de datos.list_files(rutas de archivos).repetir(repetir) conjunto de
datos=conjunto de datos.intercalar(
lambdaruta de archivo:t.f..datos.TextLineDataset(ruta de archivo).saltar(1),
duración del ciclo=n_lectores,num_llamadas_paralelas=n_read_threads) conjunto
de datos=conjunto de datos.barajar(shuffle_buffer_size)
conjunto de datos=conjunto de datos.mapa(preprocesar,num_llamadas_paralelas=n_parse_hilos) conjunto de
datos=conjunto de datos.lote(tamaño del lote) devolverconjunto de datos.captación previa(1)

410 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


Figura 13-2. Carga y preprocesamiento de datos de varios archivos CSV

Todo debería tener sentido en este código, excepto la última línea (captación previa(1)), que en
realidad es bastante importante para el rendimiento.

captación previa

Llamandocaptación previa(1)al final, estamos creando un conjunto de datos que hará todo lo posible
para estar siempre un lote por delante2. En otras palabras, mientras nuestro algoritmo de
entrenamiento está trabajando en un lote, el conjunto de datos ya estará trabajando en paralelo para
preparar el siguiente lote. Esto puede mejorar drásticamente el rendimiento, como se ilustra enFigura
13-3. Si también nos aseguramos de que la carga y el preprocesamiento sean multiproceso
(configurandonum_par allel_callsal llamarintercalar()ymapa()),podemos explotar múltiples núcleos en la
CPU y, con suerte, hacer que la preparación de un lote de datos sea más corta que ejecutar un paso de
entrenamiento en la GPU: de esta manera, la GPU se utilizará casi al 100% (excepto por el tiempo de
transferencia de datos de la CPU a la GPU) y el entrenamiento se ejecutará mucho más rápido.

2 En general, la obtención previa de un lote está bien, pero en algunos casos es posible que deba obtener algunos más. alternativa
Por lo tanto, puede dejar que TensorFlow decida automáticamente al pasartf.data.experimental.AUTOTONE (esta es una
característica experimental por ahora).

La API de datos | 411


Figura 13-3. Acelere el entrenamiento gracias a la captación previa y los subprocesos múltiples

Si planea comprar una tarjeta GPU, su potencia de procesamiento y el tamaño de su


memoria son, por supuesto, muy importantes (en particular, una memoria RAM
grande es crucial para la visión por computadora), pero suancho de banda de
memoriaes tan importante como la potencia de procesamiento para obtener un
buen rendimiento: esta es la cantidad de gigabytes de datos que puede entrar o salir
de su RAM por segundo.

Con eso, ahora puede crear canalizaciones de entrada eficientes para cargar y preprocesar
datos de varios archivos de texto. Hemos discutido los métodos de conjuntos de datos más
comunes, pero hay algunos más que quizás desee ver:concatenar(), zip(), ventana(),
reducir(), caché(), fragmento(), flat_map()ypadded_batch().También hay
ple más métodos de clase:desde_generador()yde_tensores(),que crean un nuevo conjunto de datos a
partir de un generador de Python o una lista de tensores, respectivamente. Consulte la
documentación de la API para obtener más detalles. También tenga en cuenta que hay características
experimentales disponibles entf.data.experimental,muchos de los cuales probablemente llegarán a la
API principal en futuras versiones (por ejemplo, consulte elCsvDatasetclase y elSqlDataset
clases).

412 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


Uso del conjunto de datos con tf.keras

Ahora podemos usar elcsv_reader_conjunto de datos()función para crear un conjunto de datos para el conjunto de entrenamiento

(asegurándose de que repite los datos para siempre), el conjunto de validación y el conjunto de prueba:

juego de trenes=csv_reader_dataset(tren_filepaths,repetir=Ninguna)
conjunto_valido=csv_reader_dataset(rutas_de_archivo_válidas) equipo de prueba=
csv_reader_dataset(rutas_de_archivo_de_prueba)

Y ahora podemos simplemente construir y entrenar un modelo de Keras utilizando estos conjuntos de datos.3Todo lo

que tenemos que hacer es llamar aladaptar()método con los conjuntos de datos en lugar deX_treny

y_tren,y especifique el número de pasos por época para cada conjunto:4

modelo=queras.modelos.Secuencial([...])
modelo.compilar([...])
modelo.adaptar(juego de trenes,pasos_por_época=Len(X_tren)//tamaño del lote,épocas=10,
datos_de_validación=conjunto_valido, pasos_de_validación=
Len(X_válido)//tamaño del lote)

Del mismo modo, podemos pasar un conjunto de datos alevaluar()ypredecir()métodos (y nuevamente


especifique el número de pasos por época):

modelo.evaluar(equipo de prueba,pasos=Len(X_prueba)//tamaño del lote)


modelo.predecir(nuevo set,pasos=Len(X_nuevo)//tamaño del lote)

A diferencia de los otros conjuntos, elnuevo setnormalmente no contendrá etiquetas (si las contiene, Keras
simplemente las ignorará). Tenga en cuenta que en todos estos casos, aún puede usar matrices NumPy en
lugar de conjuntos de datos si lo desea (pero, por supuesto, primero deben haberse cargado y preprocesado).

Si desea crear su propio ciclo de entrenamiento personalizado (como enCapítulo 12), puede simplemente iterar sobre
el conjunto de entrenamiento, de forma muy natural:

porX_lote,y_loteenjuego de trenes:
[...]#realizar un paso de descenso de gradiente

De hecho, incluso es posible crear una función tf. (verCapítulo 12) que realiza todo el ciclo
de entrenamiento.5

@tf.función
definitivamentetren(modelo,optimizador,loss_fn,n_épocas, [...]):
juego de trenes=csv_reader_dataset(tren_filepaths,repetir=n_épocas, [...]) por
X_lote,y_loteenjuego de trenes:
cont.f..cinta de degradado()comocinta:

3 La compatibilidad con conjuntos de datos es específica de tf.keras, no funcionará en otras implementaciones de la API de Keras.

4 La cantidad de pasos por época es opcional si el conjunto de datos solo pasa por los datos una vez, pero si no
especificarlo, la barra de progreso no se mostrará durante la primera época.

5 Tenga en cuenta que, por ahora, el conjunto de datos debe crearse dentro de la función TF. Esto puede solucionarse cuando lea
estas líneas (consulte el problema de TensorFlow n.º 25414).

La API de datos | 413


y_pred=modelo(X_lote)
pérdida_principal=t.f..reducir_media(loss_fn(y_lote,y_pred)) pérdida=t.f..añadir_n([
pérdida_principal]+modelo.pérdidas) graduados=cinta.degradado(pérdida,modelo.
variables_entrenables) optimizador.aplicar_gradientes(Código Postal(graduados,modelo.
variables_entrenables))

¡Felicitaciones, ahora sabe cómo crear canalizaciones de entrada potentes con la API de datos!
Sin embargo, hasta ahora hemos utilizado archivos CSV, que son comunes, simples y
convenientes, pero no son realmente eficientes y no admiten muy bien estructuras de datos
grandes o complejas, como imágenes o audio. Así que usemos TFRecords en su lugar.

Si está satisfecho con los archivos CSV (o cualquier otro formato que esté
utilizando), notenerpara usar TFRecords. Como dice el dicho, si no está roto,
¡no lo arregles! Los TFRecords son útiles cuando el cuello de botella durante el
entrenamiento es cargar y analizar los datos.

El formato TFRecord
El formato TFRecord es el formato preferido de TensorFlow para almacenar grandes cantidades de datos y
leerlos de manera eficiente. Es un formato binario muy simple que solo contiene una secuencia de registros
binarios de diferentes tamaños (cada registro solo tiene una longitud, una suma de verificación CRC para
verificar que la longitud no esté dañada, luego los datos reales y finalmente una suma de verificación CRC
para el datos). Puede crear fácilmente un archivo TFRecord usando el
tf.io.TFRecordWriterclase:
cont.f..yo.Escritor de registros TF("mis_datos.tfrecord")comoF:
F.escribe(b"Este es el primer disco") F.escribe(b
"Y este es el segundo disco")

Y luego puedes usar untf.data.TFRecordDatasetpara leer uno o más archivos TFRecord:

rutas de archivos=["mis_datos.tfrecord"] conjunto de datos=t.f..


datos.TFRecordDataset(rutas de archivos) porartículoenconjunto
de datos:
impresión(artículo)

Esto generará:
tf.Tensor(b'Este es el primer registro', forma=(), dtipo=cadena) tf.Tensor(b'Y
este es el segundo registro', forma=(), dtipo=cadena)

Por defecto, unTFRecordDatasetleerá los archivos uno por uno, pero


puede hacer que lea varios archivos en paralelo e intercalar sus
registros configurandonum_lecturas_paralelas.Alternativamente,
puede obtener el mismo resultado usandolist_files()yintercalar() como
hicimos antes para leer varios archivos CSV.

414 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


Archivos TFRecord comprimidos

A veces puede ser útil comprimir sus archivos TFRecord, especialmente si necesitan cargarse a
través de una conexión de red. Puede crear un archivo TFRecord comprimido configurando el
opcionesargumento:

opciones=t.f..yo.TFRecordOpciones(tipo_de_compresión="GZIP") cont.f..yo.
Escritor de registros TF("mi_comprimido.tfrecord",opciones)comoF:
[...]

Al leer un archivo TFRecord comprimido, debe especificar el tipo de compresión:


conjunto de datos=t.f..datos.TFRecordDataset(["mi_comprimido.tfrecord"],
tipo_de_compresión="GZIP")

Una breve introducción a los búferes de protocolo

Aunque cada registro puede usar cualquier formato binario que desee, los archivos TFRecord
generalmente contienen búferes de protocolo serializados (también llamadosprototipos). Este es un
formato binario portátil, extensible y eficiente desarrollado en Google en 2001 y de código abierto en
2008, y ahora se usa ampliamente, en particular engRPC, el sistema de llamadas a procedimientos
remotos de Google. Los búferes de protocolo se definen usando un lenguaje simple que se ve así:

sintaxis="proto3";
mensaje Persona{
nombre de cadena=1;
identificación int32=2;
correo electrónico de cadena repetida=3;
}
Esta definición dice que estamos usando la versión 3 del formato protobuf y especifica que cada
Personaobjeto6puede (opcionalmente) tener unnombrede tipocuerda,unidentificaciónde tipoint32,
y cero o másEmailcampos, cada uno de tipocuerda.Los números 1, 2 y 3 son los identificadores
de campo: se utilizarán en la representación binaria de cada registro. Una vez que tenga una
definición en un archivo .prototipoarchivo, puede compilarlo. Esto requiereprotocolo,el
compilador proto‐buf, para generar clases de acceso en Python (o algún otro lenguaje). Tenga
en cuenta que las definiciones de protobuf que usaremos ya se compilaron para usted, y sus
clases de Python son parte de TensorFlow, por lo que no necesitará usarprotocoloTodo lo que
necesita saber es cómo usar las clases de acceso protobuf en Python. Para ilustrar los conceptos
básicos, veamos un ejemplo simple que usa las clases de acceso generadas para elPersona
protobuf (el código se explica en los comentarios):

> > > depersona_pb2importarPersona#importar la clase de acceso generada


> > > persona=Persona(nombre="Alabama",identificación=123,Email=[" a@b.com "])#crear una persona
> > > impresión(persona)#mostrar la persona

6 Dado que los objetos protobuf están destinados a ser serializados y transmitidos, se denominanmensajes.

El formato TFRecord | 415


nombre: "Al"
identificación: 123

correo electrónico: " a@b.com "

> > > persona.nombre # leer un campo


"Alabama"

> > > persona.nombre="Alicia" # modificar un campo


> > > persona.Email[0]#se puede acceder a los campos repetidos como matrices "
a@b.com "
> > > persona.Email.adjuntar(" c@d.com ") # añadir una dirección de correo electrónico

> > > s=persona.SerializeToString() # serializar el objeto a una cadena de bytes


> > >s
b'\n\x05Alice\x10{\x1a\ x07a@b.com \x1a\ x07c@d.com '
> > > persona2=Persona()#crear una nueva persona
> > > persona2.ParseFromString(s)#analizar la cadena de bytes (27 bytes de largo) 27

> > > persona==persona2#ahora son iguales


Verdadero

En resumen, importamos elPersonaclase generada porprotocolo,creamos una instancia y


jugamos con ella, visualizándola, leyendo y escribiendo algunos campos, luego la serializamos
usando elSerializeToString()método. Estos son los datos binarios que están listos para ser
guardados o transmitidos a través de la red. Al leer o recibir estos datos binarios, podemos
analizarlos usando elParseFromString()método, y obtenemos una copia del objeto que se
serializó.7

Podríamos guardar el serializadoPersonaobjeto a un archivo TFRecord, entonces podríamos


cargarlo y analizarlo: todo funcionaría bien. Sin embargo,SerializeToString()yAnalizar desde la
cadena ()no son operaciones de TensorFlow (y tampoco lo son las otras operaciones en este
código), por lo que no se pueden incluir en una función de TensorFlow (excepto envolviéndolas
en untf.py_function()operación, lo que haría que el código fuera más lento y menos portátil,
como vimos enCapítulo 12). Afortunadamente, TensorFlow incluye definiciones especiales de
protobuf para las que proporciona operaciones de análisis.

Protobufs de TensorFlow

El protobuf principal que normalmente se usa en un archivo TFRecord es elEjemploprotobuf, que representa
una instancia en un conjunto de datos. Contiene una lista de funciones con nombre, donde cada función
puede ser una lista de cadenas de bytes, una lista de flotantes o una lista de enteros. Aquí está la definición
de protobuf:

sintaxis="proto3";
lista de bytes del mensaje{valor de bytes repetidos=1; }
mensaje lista flotante{repetidoflotarvalor=1[lleno=verdadero]; }
mensaje Int64List{valor int64 repetido=1[lleno=verdadero]; }

7 Este capítulo contiene lo mínimo que necesita saber sobre protobufs para usar TFRecords. Aprender más
sobre protobufs, visitehttps://homl.info/protobuf.

416 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


función de mensaje{
único en su tipo{
Lista de bytes lista de bytes=
1; lista_flotante lista_flotante
=2; Int64List int64_list=3;
}
};
Características del mensaje{mapa<cuerda,Rasgo>rasgo=1; };
mensaje Ejemplo{Funciones funciones=1; };

Las definiciones delista de bytes, lista flotanteyInt64Listason lo suficientemente sencillos ([empaquetado =


verdadero]se utiliza para campos numéricos repetidos, para una codificación más eficiente
En g). ARasgoo bien contiene unLista de bytes,aLista flotanteo unInt64Lista.AFea
turas (con una s) contiene un diccionario que asigna un nombre de característica al valor de
característica correspondiente. Y finalmente, unEjemplosolo contiene unCaracterísticasobjeto.8
Así es como podrías crear untf.tren.Ejemplorepresentando a la misma persona que antes, y
escríbalo en el archivo TFRecord:

detensorflow.trenimportarLista de bytes,Lista flotante,Int64Lista


detensorflow.trenimportarRasgo,Características,Ejemplo

persona_ejemplo=Ejemplo(
caracteristicas=Características(

rasgo={
"nombre":Rasgo(bytes_list=Lista de bytes(valor=[b"Alicia"])),
"identificación":Rasgo(lista_int64=Int64Lista(valor=[123])), "correos
electrónicos":Rasgo(bytes_list=Lista de bytes(valor=[b" a@b.com ",
b" c@d.com "]))
}))
El código es un poco detallado y repetitivo, pero es bastante sencillo (y podría envolverlo
fácilmente dentro de una pequeña función auxiliar). Ahora que tenemos unEjemploprotobuf,
podemos serializarlo llamando a suSerializeToString()luego escriba los datos resultantes en un
archivo TFRecord:

cont.f..yo.Escritor de registros TF("mis_contactos.tfrecord")comoF:


F.escribe(persona_ejemplo.SerializeToString())

¡Normalmente escribirías mucho más que un solo ejemplo! Por lo general, crearía un script de
conversión que lea desde su formato actual (por ejemplo, archivos CSV), cree unEjemplo
protobuf para cada instancia, los serializa y los guarda en varios archivos TFRecord, idealmente
mezclándolos en el proceso. Esto requiere un poco de trabajo, así que una vez más, asegúrese
de que sea realmente necesario (tal vez su tubería funcione bien con archivos CSV).

8 ¿Por qué fueEjemploincluso definido ya que no contiene más que unCaracterísticas¿objeto? Bueno, TensorFlow puede uno
day decide agregarle más campos. Mientras el nuevoEjemplodefinición todavía contiene elcaracteristicascampo, con la misma
identificación, será compatible con versiones anteriores. Esta extensibilidad es una de las grandes características de protobufs.

El formato TFRecord | 417


Ahora que tenemos un buen archivo TFRecord que contiene un serializadoEjemplo,intentemos
cargarlo.

Ejemplos de carga y análisis


Para cargar el serializadoEjemploprotobufs, usaremos untf.data.TFRecordDataset
una vez más, y analizaremos cadaEjemplousandotf.io.parse_single_example().
Esta es una operación de TensorFlow, por lo que se puede incluir en una función TF. Requiere al
menos dos argumentos: un tensor escalar de cadena que contiene los datos serializados y una
descripción de cada función. La descripción es un diccionario que asigna cada nombre de
función a untf.io.FixedLenFeaturedescriptor que indica la forma, el tipo y el valor predeterminado
de la característica, o untf.io.VarLenFeaturedescriptor que indica solo el tipo (si la longitud puede
variar, como para el "correos electrónicos"rasgo). Por ejemplo:

característica_descripción={
"nombre":t.f..yo.FixedLenFeature([],t.f..cuerda,valor por defecto=""),
"identificación":t.f..yo.FixedLenFeature([],t.f..int64,valor por defecto=0), "correos
electrónicos":t.f..yo.Característica VarLen(t.f..cuerda),
}

porserializado_ejemploent.f..datos.TFRecordDataset(["mis_contactos.tfrecord"]):
ejemplo_analizado=t.f..yo.parse_single_example(serializado_ejemplo,
característica_descripción)

Las características de longitud fija se analizan como tensores regulares, pero las características de longitud
variable se analizan como tensores dispersos. Puede convertir un tensor disperso en un tensor denso usando
tf.disperso.a_denso(),pero en este caso es más sencillo simplemente acceder a sus valores:

> > > t.f..escaso.to_dense(ejemplo_analizado["correos electrónicos"],valor por defecto=b"")


<tf.Tensor: [...] dtype=string, numpy=array([b' a@b.com ', b' c@d.com '], [...])>
> > > ejemplo_analizado["correos electrónicos"].valores
<tf.Tensor: [...] dtype=string, numpy=array([b' a@b.com ', b' c@d.com '], [...])>

ALista de bytespuede contener cualquier dato binario que desee, incluido cualquier objeto
serializado. Por ejemplo, puedes usartf.io.encode_jpeg()para codificar una imagen usando
el formato JPEG, y poner estos datos binarios en unLista de bytes.Más tarde, cuando su
código lea el TFRecord, comenzará analizando elEjemplo,entonces tendrás que llamar
tf.io.decode_jpeg()para analizar los datos y obtener la imagen original (o puede usar
tf.io.decode_image(),que puede decodificar cualquier imagen BMP, GIF, JPEG o PNG). También
puede almacenar cualquier tensor que desee en unLista de bytesserializando el tensor usando
tf.io.serialize_tensor(),luego colocando la cadena de bytes resultante en unLista de bytes
rasgo. Más tarde, cuando analice el TFRecord, puede analizar estos datos usando
tf.io.parse_tensor().

En lugar de analizar los ejemplos uno por uno usandotf.io.parse_single_example(),es posible


que desee analizarlos lote por lote usandotf.io.parse_example():

418 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


conjunto de datos=t.f..datos.TFRecordDataset(["mis_contactos.tfrecord"]).lote(10) por
ejemplos_serializadosenconjunto de datos:
ejemplos analizados=t.f..yo.analizar_ejemplo(ejemplos_serializados,
característica_descripción)

Como puedes ver, elEjemploproto probablemente será suficiente para la mayoría de los casos de uso.
Sin embargo, puede ser un poco engorroso de usar cuando se trata de listas de listas. Por ejemplo,
suponga que desea clasificar documentos de texto. Cada documento puede representarse como una
lista de oraciones, donde cada oración se representa como una lista de palabras. Y quizás cada
documento también tenga una lista de comentarios, donde cada comentario también se representa
como una lista de palabras. Además, también puede haber algunos datos contextuales, como el autor
del documento, el título y la fecha de publicación. TensorFlow
SecuenciaEjemploprotobuf está diseñado para tales casos de uso.

Manejo de listas de listas usando elSecuenciaEjemploProtobuf


Aquí está la definición de laSecuenciaEjemploprototipo:

mensaje FeatureList{característica repetida=1; };


Mensaje FeatureLists{mapa<cuerda,Lista de características>lista de características=1; };
Secuencia de mensajesEjemplo{
Contexto de características=1; Listas de
características listas_de_características=2;
};

ASecuenciaEjemplocontiene unaCaracterísticasobjeto para los datos contextuales y unListas de


característicasobjeto que contiene uno o más nombresLista de característicasobjetos (por ejemplo, un
Lista de característicasnombrada "contenido"y otro llamado "comentarios").CadaLista de características
solo contiene una lista deRasgoobjetos, cada uno de los cuales puede ser una lista de cadenas de bytes, una
lista de enteros de 64 bits o una lista de flotantes (en este ejemplo, cadaRasgorepresentaría una oración o un
comentario, quizás en forma de una lista de identificadores de palabras). Construyendo un
SecuenciaEjemplo,serializarlo y analizarlo es muy similar a construir, serializar
y analizando unEjemplo,pero debes usartf.io.parse_single_sequence_example()
para analizar un soloSecuenciaEjemplootf.io.parse_sequence_example()a
analiza un lote y ambas funciones devuelven una tupla que contiene las características de contexto (como un
diccionario) y las listas de características (también como un diccionario). Si las listas de características
contienen secuencias de diferentes tamaños (como en el ejemplo anterior), es posible que desee convertirlas
en tensores irregulares utilizandotf.RaggedTensor.from_sparse() (ver el cuaderno para el código completo):

contexto_analizado,listas_características analizadas=t.f..yo.parse_single_sequence_example(
serializado_secuencia_ejemplo,context_feature_descriptions,
descripciones_de_funciones_de_secuencia)
contenido_analizado=t.f..RaggedTensor.from_sparse(listas_características analizadas["contenido"])

Ahora que sabe cómo almacenar, cargar y analizar datos de manera eficiente, el siguiente paso es
prepararlos para que puedan alimentarse a una red neuronal. Esto significa convertir todas las características

El formato TFRecord | 419


en características numéricas (idealmente no demasiado escasas), escalarlas y más. En particular, si sus
datos contienen características categóricas o características de texto, deben convertirse en números.
Para esto, elFunciones APIpoder ayudar.

La API de características

El preprocesamiento de sus datos se puede realizar de muchas maneras: se puede hacer con anticipación al
preparar sus archivos de datos, utilizando cualquier herramienta que desee. O puede preprocesar sus datos
sobre la marcha al cargarlos con la API de datos (p. ej., utilizando el conjunto de datosmapa()
método, como vimos anteriormente). O puede incluir una capa de preprocesamiento directamente en
su modelo. Cualquiera que sea la solución que prefiera, la API de características puede ayudarlo: es un
conjunto de funciones disponibles en eltf.feature_columnpaquete, que le permite definir cómo se
debe preprocesar cada característica (o grupo de características) en sus datos (por lo tanto, puede
pensar en esta API como el análogo de Scikit-Learn).ColumnaTransformadorclase). Comenzaremos
observando los diferentes tipos de columnas disponibles y luego veremos cómo usarlas.

Volvamos a la variante del conjunto de datos de vivienda de California que usamos enCapitulo 2, ya
que incluye una característica categórica y datos faltantes. Aquí hay una numeración simple
columna cal denominada "vivienda_edad_media":

vivienda_median_age=t.f..característica_columna.columna_numérica("vivienda_median_age")

Las columnas numéricas le permiten especificar una función de normalización usando elnormalizador_fn
argumento. Por ejemplo, modifiquemos el "edad_media_vivienda"columna para definir cómo se debe
escalar. Tenga en cuenta que esto requiere calcular con anticipación la media y la desviación estándar
de esta característica en el conjunto de entrenamiento:

edad_media,edad_std=X_media[1],X_std[1]#La mediana de edad es la columna en 1


vivienda_median_age=t.f..característica_columna.columna_numérica(
"vivienda_median_age",normalizador_fn=lambdaX: (X-edad_media)/edad_std)

En algunos casos, podría mejorar el rendimiento dividir en cubos algunas características


numéricas, transformando efectivamente una característica numérica en una característica
categórica. Por ejemplo, vamos a crear una columna dividida en cubos basada en elingreso
mediocolumna, con 5 cubos: menos de 1,5 ($15 000), luego 1,5 a 3, 3 a 4,5, 4,5 a 6 y más de 6
(observe que cuando especifica 4 límites, en realidad hay 5 cubos):

ingreso medio=t.f..característica_columna.columna_numérica("ingreso medio")


ingresos_cubetados=t.f..característica_columna.columna_cubetada(
ingreso medio,límites=[1.5,3.,4.5,6.])

Si elingreso mediocaracterística es igual a, digamos, 3.2, entonces elingresos_cubetadosfunción


será automáticamente igual a 2 (es decir, el índice de la cubeta de ingresos correspondiente).
Elegir los límites correctos puede ser algo así como un arte, pero un enfoque es simplemente
usar percentiles de los datos (p. ej., el percentil 10, el percentil 20, etc.). Si una característica es
multimodal, lo que significa que tiene picos separados en su distribución, puede

420 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


desea definir un cubo para cada modo, colocando los límites entre los picos. Ya sea
que use los percentiles o las modas, debe analizar la distribución de sus datos con
anticipación, al igual que tuvimos que medir la media y la desviación estándar con
anticipación para normalizar lavivienda_median_agecolumna.

Características categóricas

Para características categóricas comoproximidad al océano,hay varias opciones Si ya está


representado como una ID de categoría (es decir, un número entero de 0 a la ID máxima), entonces
puede usar elcolumna_categórica_con_identidad()función (especificando el máximo
IDENTIFICACIÓN). Si no, y conoce la lista de todas las categorías posibles, entonces puede usarcategorías
cal_column_with_vocabulary_list():
ocean_prox_vocab=['<1H OCÉANO','INTERIOR','ISLA','CERCA DE LA BAHÍA','CERCA DEL OCÉANO']
proximidad_océano=t.f..característica_columna.columna_categórica_con_lista_de_vocabulario(
"proximidad_océano",ocean_prox_vocab)

Si prefiere que TensorFlow cargue el vocabulario desde un archivo, puede llamarcategoría


rical_column_with_vocabulary_file()en cambio. Como era de esperar, estos dos
funciones simplemente asignarán cada categoría a su índice en el vocabulario (por ejemplo,CERCA DE
LA BAHIAse asignará a 3), y las categorías desconocidas se asignarán a -1.

Para columnas categóricas con un vocabulario extenso (p. ej., para códigos postales, ciudades, palabras,
productos, usuarios, etc.), puede que no sea conveniente obtener la lista completa de posibles categorías, o
quizás se pueden agregar o eliminar categorías para frecuentemente que el uso de índices de categoría sería
demasiado poco confiable. En este caso, es posible que prefiera utilizar un
columna_categorial_con_hash_bucket().Si tuviéramos un "ciudad"característica en el conjunto de datos,
Podríamos codificarlo así:
ciudad_hash=t.f..característica_columna.categorical_column_with_hash_bucket(
"ciudad",hash_bucket_size=1000)

Esta función calculará un hash para cada categoría (es decir, para cada ciudad), módulo el
número de cubos de hash (hash_bucket_size).Debe configurar el número de cubos lo
suficientemente alto para evitar demasiadas colisiones (es decir, diferentes categorías que
terminan en el mismo cubo), pero cuanto más alto lo configure, más RAM se utilizará (por la
tabla de incrustación, como veremos). ver en breve).

Características categóricas cruzadas

Si sospecha que dos (o más) características categóricas son más significativas cuando se usan juntas,
entonces puede crear unacolumna cruzada. Por ejemplo, supongamos que a la gente le gustan
especialmente las casas antiguas en el interior y las casas nuevas cerca del océano, entonces podría ser útil

La API de funciones | 421


crear una columna segmentada para elvivienda_median_agerasgo9, y cruzarlo con el
proximidad_océanocolumna. La columna cruzada calculará un hash de cada combinación
de edad y proximidad al océano que encuentre, módulo elhash_bucket_size,y esto le dará el
ID de categoría cruzada. Luego puede optar por usar solo esta columna cruzada en su
modelo, o también incluir las columnas individuales.

cuboizado_edad=t.f..característica_columna.columna_cubetada(
vivienda_median_age,límites=[-1.,-0.5,0.,0.5,1.])#la edad fue escalada
edad_y_ocean_proximity=t.f..característica_columna.columna_cruzada(
[cuboizado_edad,proximidad_océano],hash_bucket_size=100)

Otro caso de uso común para las columnas cruzadas es cruzar la latitud y la longitud en
una sola característica categórica: comienza dividiendo la latitud y la longitud, por ejemplo
en 20 cubos cada uno, luego cruza estas características divididas en unubicacióncolumna.
Esto creará una cuadrícula de 20×20 sobre California, y cada celda de la cuadrícula
corresponderá a una categoría:

latitud=t.f..característica_columna.columna_numérica("latitud")
longitud=t.f..característica_columna.columna_numérica("longitud")
cuboizado_latitud=t.f..característica_columna.columna_cubetada(
latitud,límites=lista(notario público.espacio lineal(32.,42.,20-1)))
longitud_en cubos=t.f..característica_columna.columna_cubetada(
longitud,límites=lista(notario público.espacio lineal(-125.,-114.,20-1)))
ubicación=t.f..característica_columna.columna_cruzada(
[cuboizado_latitud,longitud_en cubos],hash_bucket_size=1000)

Codificación de características categóricas mediante vectores One-Hot

Independientemente de la opción que elija para crear una característica categórica (columnas
categóricas, columnas divididas en cubos o columnas cruzadas), debe codificarse antes de que
pueda enviarla a una red neuronal. Hay dos opciones para codificar una característica
categórica: vectores one-hot oincrustaciones. Para la primera opción, simplemente use el
columna_indicador()función:

ocean_proximity_one_hot=t.f..característica_columna.indicador_columna(proximidad_océano)

Una codificación de vector one-hot tiene el tamaño de la longitud del vocabulario, lo cual está
bien si solo hay unas pocas categorías posibles, pero si el vocabulario es grande, terminará con
demasiadas entradas alimentadas a su red neuronal: tendrá demasiados pesos para aprender y
probablemente no funcionará muy bien. En particular, este suele ser el caso cuando usa cubos
de hash. En este caso, probablemente debería codificarlos usando incrustacionesen cambio.

9 Desde elvivienda_median_agecaracterística fue normalizada, los límites son para edades normalizadas.

422 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow


Como regla general (¡pero su kilometraje puede variar!), si el número de
categorías es inferior a 10, entonces la codificación one-hot es generalmente el
camino a seguir. Si el número de categorías es superior a 50 (que suele ser el
caso cuando se utilizan cubos de hash), las incrustaciones suelen ser
preferibles. Entre 10 y 50 categorías, es posible que desee experimentar con
ambas opciones y ver cuál funciona mejor para su caso de uso. Además, las
incrustaciones suelen requerir más datos de entrenamiento, a menos que
pueda reutilizar incrustaciones previamente entrenadas.

Codificación de características categóricas mediante incrustaciones

Una incrustación es un vector denso entrenable que representa una categoría. De forma
predeterminada, las incrustaciones se inicializan aleatoriamente, por ejemplo, el "CERCA DE LA
BAHÍA"categoría podría representarse inicialmente por un vector aleatorio como [0,131, 0,890],
mientras que la "CERCA DEL OCÉANO"la categoría puede estar representada por otro vector
aleatorio como [0,631, 0,791] (en este ejemplo, estamos usando incrustaciones 2D, pero el
número de dimensiones es un hiperparámetro que puede modificar). Dado que estas
incrustaciones son entrenables, mejorarán gradualmente durante el entrenamiento y, dado que
representan categorías bastante similares, Gradient Descent ciertamente terminará
acercándolas, mientras que tenderá a alejarlas del "INTERIOR"incrustación de categoría (ver
Figura 13-4). De hecho, cuanto mejor sea la representación, más fácil será para la red neuronal
hacer predicciones precisas, por lo que el entrenamiento tiende a hacer que las incrustaciones
sean representaciones útiles de las categorías. Se llamarepresentación aprendizaje(veremos
otros tipos de aprendizaje de representación en???).

La API de características | 423


Figura 13-4. Las incorporaciones mejorarán gradualmente durante el entrenamiento

incrustaciones de palabras

Las incrustaciones generalmente no solo serán representaciones útiles para la tarea en cuestión, sino
que, con bastante frecuencia, estas mismas incrustaciones también se pueden reutilizar con éxito para
otras tareas. El ejemplo más común de esto esincrustaciones de palabras(es decir, incrustaciones de
palabras individuales): cuando está trabajando en una tarea de procesamiento de lenguaje natural, a
menudo es mejor reutilizar incrustaciones de palabras preentrenadas que entrenar las suyas propias. La
idea de utilizar vectores para representar palabras se remonta a la década de 1960 y se han utilizado
muchas técnicas sofisticadas para generar vectores útiles, incluido el uso de redes neuronales, pero las
cosas realmente despegaron en 2013, cuando Tomáš Mikolov y otros investigadores de Google
publicaron unpapel10describiendo cómo aprender incrustaciones de palabras utilizando redes
neuronales profundas, mucho más rápido que los intentos anteriores. Esto les permitió aprender
incrustaciones en un corpus de texto muy grande: entrenaron una red neuronal profunda para predecir
las palabras cercanas a cualquier palabra dada. Esto les permitió obtener asombrosas incrustaciones de
palabras. Por ejemplo, los sinónimos tenían incrustaciones muy cercanas y palabras relacionadas
semánticamente como Francia, España, Italia, etc., terminaron agrupadas. Pero no se trata solo de
proximidad: las incrustaciones de palabras también se organizaron a lo largo de ejes significativos en el
espacio de incrustación. He aquí un ejemplo famoso: si calcula Rey – Hombre
+ Mujer (sumando y restando los vectores de incrustación de estas palabras), entonces el
resultado será muy parecido a la incrustación de la palabra Reina (verFigura 13-5). En otras
palabras, ¡la palabra incrustaciones codifica el concepto de género! De manera similar, puede
calcular Madrid – España + Francia y, por supuesto, el resultado está cerca de París, lo que parece
mostrar que la noción de ciudad capital también estaba codificada en las incrustaciones.

10 “Representaciones distribuidas de palabras y frases y su composicionalidad”, T. Mikolov et al. (2013).

424 | Capítulo 13: Carga y preprocesamiento de datos con TensorFlow

También podría gustarte