¿Cómo adivinar el futuro en machine learning? - Primera parte

To forecast se puede traducir por predecir, normalmente tras un análisis de ciertos datos disponibles. En este sentido, todo el aprendizaje de máquinas podría ser considerado como forecasting, ya que su objetivo es predecir a través de modelos, el comportamiento de unos objetos de interés. Sin embargo, en machine learning se suele reservar el término forecasting para la predicción de series temporales, o time series forecasting.

¿Cómo se puede predecir una serie temporal?

Cuando disponemos de una serie de datos de una variable ordenados en el tiempo (ver Fig. 1) y deseamos predecir los siguientes valores que tomará esta serie temporal, lo ideal es conocer qué leyes rigen este comportamiento. Si bien para ciertos sistemas físicos o matemáticos es posible crear modelos que describan adecuadamente su comportamiento con los inputs correctos, aunque sea a corto plazo como puede ser el caso de la predicción del tiempo meteorológico (weather forecast), en general no se dispone de información sobre las leyes que rigen las series temporales que resultan de interés en machine learning. Debemos, por tanto, realizar ciertas hipótesis sobre la forma de la serie temporal para predecir su futuro.

Fig. 1. Exportaciones e importaciones de bienes en España. Fuente.

Conviene enfatizar que, al igual que en el resto de machine learning o en ciencia en general, no todo es predecible aunque usemos toda la información disponible. También es importante señalar que, cuanto más impredecible es un sistema, habitualmente los modelos simples son capaces de dar una predicción tan razonable como los sistemas complejos.

Algunos modelos simples

Existen varias maneras inmediatas de dar una predicción para una serie temporal. Con el modelo naïf o ingenuo (naïve model), asumimos que la serie mantendrá el valor de su última observación. Con el modelo deriva (drift), tomamos la variación media de la curva y suponemos que seguirá un crecimiento lineal dado por esta variación. Otra posible predicción es tomar la media de la serie temporal. Echa un vistazo a la Fig. 2.

Fig. 2. Precio del stock de Google a final de jornada entre enero de 2015 y enero de 2016. Varios modelos simples para los últimos datos han sido superpuestos. Fuente.

Descomposición de una serie temporal

Habitualmente, toda serie temporal que sea susceptible de ser predicha posee una tendencia (trend) y un número de estacionalidades(seasonalities).

  • La tendencia indica el crecimiento de la curva en un momento dado.
  • Las estacionalidades son curvas periódicas con un período que puede ser diario, semanal, mensual, anual…

Si la amplitud de la parte estacional es constante, podemos escribir la serie temporal como una suma de tendencia, estacionalidad o estacionalidades, y un término de resto que es impredecible, y que idealmente seguiría una distribución estadística controlada (gaussiana, por ejemplo)(mirar Fig. 3):

Fig. 3. Descomposición de una serie temporal (panel de arriba) en tendencia (segundo panel), estacionalidad periódica (tercer panel) y residuos (panel de abajo). Fuente.

Sin embargo, otras series temporales pueden presentar una amplitud de la parte estacional que depende de su tendencia (ver Fig. 4). Para estas series temporales suele ser más adecuado usar un productorio de las tres componentes:

Fig. 4. Número de pasajeros de avión en cientos de miles en función del año, extraídos del conjunto de ejemplo de Prophet. Se puede apreciar que la amplitud de la parte estacional crece con la tendencia.

Esta división en tendencia y estacionalidades es muy útil tanto conceptual como prácticamente, pues hay modelos como el suavizado exponencial (exponential smoothing) o Prophet, que modelan estas componentes por separado.

Modelos autorregresivos

Los modelos autorregresivos parten de la hipótesis de que la serie temporal se puede describir a partir de una combinación lineal de valores pasados:

Los coeficientes Φ se pueden obtener de un ajuste o entrenamiento con datos reales. Por ejemplo, minimizando la función de suma de distancias al cuadrado:

Donde representan yi los datos reales e ŷi las predicciones del modelo autorregresivo. Para un ejemplo práctico, échale un vistazo a este enlace.

Fig. 5. Demanda de energía eléctrica en función del tiempo enfrentada a su predicción con un modelo autorregresivo. Fuente.

Modelos de media móvil

Los modelos de media móvil son similares a los autorregresivos, pero en vez de suponer una combinación lineal de los valores pasados, supone una combinación lineal de los errores del modelo que sirva de predicción:

Para obtener los coeficientes, estos errores han de ser expresados en función de los datos de la serie, lo cual suele resultar en expresiones complicadas de minimizar que se han de tratar con métodos especiales.

Modelos ARIMA

Consideremos la diferencia de la serie temporal. Para ello, restamos a la propia serie y(t) la serie desplazada hacia el momento justo anterior. Suponiendo que la diferencia temporal entre muestras temporales de la serie es una unidad, podemos deducir que:

Esta diferencia tiene la propiedad de hacer que las series temporales exhiban un comportamiento más estacionario, que no dependa del tiempo, lo cual se suele prestar mejor a la aplicación de modelos autorregresivos y de media móvil. Los modelos ARIMA son una combinación de los modelos autorregresivos (AR) y los de moving average (MA), aplicados o bien a esta diferencia y', sucesivas diferencias y'', y'''..., o bien a la serie original y:

Estos modelos se pueden encontrar en la librería statsmodels de Python. La ventaja de ellos radica en que no necesitan más asunciones que los propios datos y tienen una expresión sencilla. Aunque se prestan también al modelado de estacionalidades, identificar el tipo de modelo ARIMA (el orden AR o MA que usar, el diferenciado…), puede llegar a ser bastante laborioso, y otros modelos como Prophet ofrecen resultados igual o más potentes con menos trabajo y partiendo de hipótesis razonables.

Prophet

Prophet es un programa creado por científicos de Facebook para la predicción de series temporales. Para una guía de cómo utilizar, échale un vistazo a este enlace, y para profundizar véase este artículo. Prophet sigue una descomposición en tendencia y estacionalidades, con la particularidad de que se le pueden añadir días festivos y regresores externos como temperatura:

  • La tendencia está modelada por una función continua que es una recta a trozos y que cambia de pendiente en un número de puntos. Se puede poner también un término de saturación para que la tendencia no crezca indefinidamente.
  • Las estacionalidades están constituidas por series de Fourier de un determinado orden, controlado por el usuario.
  • Los días festivos tienen su propia contribución, que es no nula en una ventana especificada en torno al festivo.
  • Los regresores externos son otras series temporales correlacionadas con nuestra serie que pueden ayudar a predecir nuestra serie temporal. Por ejemplo, si queremos saber el número de abrigos vendidos, esperamos que esto tenga una anticorrelación con la temperatura.

De este modo, el modelo de Prophet se puede escribir como:

Donde T es la curva variable de tendencia,, H la influencia de los festivos y R es el error restante. En cuanto a los términos que faltan, S es la suma de todas las estacionalidades con distinto período τi:

Donde cada Sii) es una serie de Fourier. El término del regresor externo es una constante multiplicada por la serie temporal del regresor externo:

El razonamiento para series multiplicativas es análogo, cambiando las sumas por multiplicaciones. Prophet además permite elegir qué componentes son aditivas y cuáles multiplicativas. Por ejemplo, se puede hacer una tendencia aditiva, una estacionalidad aditiva y otra multiplicativa y festivos aditivos.

Para obtener los parámetros del modelo, Prophet usa inferencia bayesiana. La idea de este método es usar el teorema de Bayes para, a partir de unas distribuciones de partida de los parámetros y los datos obtenidos, saber cuáles son los parámetros más probables que darían lugar a nuestra serie temporal.

Prophet tiene la ventaja de ser muy flexible, potente y fácil de usar. Además, debido a su forma matemática, no necesita que las series sean regulares.

La importancia de contar con un buen equipo de arquitectura rendimiento. Nuestro caso.

Uno de los puntos críticos de una empresa TIC es el correcto funcionamiento de sus sistemas. En el panorama actual, entendemos “correcto funcionamiento” no sólo al conjunto de requisitos funcionales, sino también a todos los requisitos no funcionales intrínsecos al mismo.

Dentro de estos requisitos no funcionales, la disponibilidad y la velocidad son quizás los más demandados por nuestros clientes. Para ello, el equipo de arquitectura rendimiento se encarga de garantizar dichas cualidades. Dentro de las buzzwords del mundillo, destacamos principalmente en APM y SRE.

Para garantizar el rendimiento de un aplicativo, es preciso poner el foco durante todo el ciclo de vida del mismo.

Prototipado y diseño

Nuestros ingenieros de solución son los encargados de extraer los requisitos para los aplicativos de nuestros clientes.

Los clientes confían en nosotros para que nos preocupemos de aquellos aspectos que un perfil centrado en el negocio no necesita controlar, como por ejemplo de la seguridad, del rendimiento y de la escalabilidad. Es decir, creen en la profesionalidad y experiencia de Qindel a la hora de tomar la mejor decisión.

Para garantizar estos requisitos no funcionales, hacemos que sean explícitos durante el ciclo de vida y realizamos un shift left para poder tenerlos siempre en cuenta. Durante esta fase se plantea una arquitectura (Microservicios, eventual, CQRS), qué sistemas de almacenamiento se van a emplear (SQL, noSql, S3, Kafka, Pulsar, cachés), y en qué proveedores a desplegar Openshift, AWS, Azure

Desarrollo

Durante el desarrollo, el equipo de Arquitectura rendimiento se encarga de solventar dudas, propone algoritmos eficientes y revisa con los equipos los posibles cuellos de botella. Entre las tecnologías que el equipo suele emplear, quizás java y spring son las más destacadas.

Para realizar esta labor, empleamos una gran variedad de herramientas, como por ejemplo jprofiler, jifa, visualvm o wireshark.

Gracias a esto podemos identificar una gran cantidad de problemas:

  • Consultas lentas a base de datos que han de ser revisadas con un Explain Plan.
  • Identificar problemas de rendimiento n+1.
  • Detectar problemas de rendimiento en el garbage collector como OOMKills, Memory churn, etc.
  • Problemas de deadlocks o de usos indebidos de los distritos threadpools.

El poder trabajar en estos puntos durante el desarrollo permite reducir la cantidad de problemas en un futuro. Esto a su vez minimiza el coste y facilita el correcto desarrollo. En el mundo IT, el poder detectar un error lo más temprano posible implica un ahorro importante.

Además, se recomienda el uso de patrones de resiliencia en los distintos puntos de las aplicaciones. Circuit breakers, retries, backpressure o feature toggles son los que ponemos más en práctica.

Pruebas de carga

Durante el desarrollo, una feature no se marca como resuelta si ésta no ha pasado sus pruebas de rendimiento apropiadas. En ellas estresamos la plataforma y el caso de uso, pudiendo comprobar que los tiempos y cargas definidas son alcanzables. Para ello hacemos uso de tecnologías como Jmeter o K6 donde modelamos los flujos de los usuarios y validamos que la solución cumple la especificación

Runtime

Mientras la aplicación se encuentra funcionando, es preciso contar con la mayor cantidad de información posible de esta. Una arquitectura basada en servicios ha de contar con un buen sistema de monitorización. Es por eso que nuestro equipo dispone de una gran cantidad de información a su alcance a través de grafana.

Mediante el uso de Métricas, Trazas y Logs, el equipo puede identificar qué servicios se ven afectados en rendimiento, pudiendo detectar problemas cuando los distintos SLOs dejan de cumplirse y detectando la causa raíz del problema.

Además, el disponer de esta información permite modelar mejores casos de uso en las aplicaciones, lo que a su vez permite mejorar todavía más los casos de las pruebas de rendimiento y retroalimenta el sistema.

Con el objetivo de que el equipo sea escalable y detecte dichos errores de forma rápida y efectiva, utilizamos sistemas de alertado y notificación de alertas para así detectar problemas en el rendimiento de forma casi inmediata. Incluso se habilita en multiples ocasiones el uso de monitorización sintética para poder observar distintos comportamientos en producción.

Errores y downtime

Pese a todo lo anterior, los errores acaban sucediendo. Sin embargo está en manos de un buen equipo minimizar el downtime que un servicio puede afrontar, en medida de lo posible. Para ello, la única forma para llevarlo a cabo es experimentar estos problemas mediante simulacros con el fin de emular situaciones de downtime como, por ejemplo, reinicios de máquinas, latencias en red, computo o similares durante sesiones de chaos testing. De tal forma que un equipo se familiarice con las distintas herramientas y pueda afrontar un problema en producción cuando este se produzca.

Conclusiones

Los miembros de los equipos de arquitectura rendimiento son profesionales con mucha experiencia, capaces de trabajar en los distintos niveles y capas de complejidad del software y que entienden la importancia de minimizar los errores. Son personas que acaban convirtiéndose en referentes dentro de los equipos de desarrollo debido al valor y tranquilidad que generan. Sin embargo, esta sensación es muy posible que no permee en las capas altas de la organización.

Debido a su propia naturaleza, un equipo de rendimiento que hace su trabajo de forma correcta puede pasar desapercibido puesto que "todo va bien". Con el objetivo de evitar esta casuística, disponer de informes de mejoras, problemas detectados con anterioridad, disminución del número de incidencias o mejoras en los tiempos de resolución de las mismas, son claves para justificar el valor que aporta. Además estas medidas objetivas ayudan al propio equipo a mejorar día a día y a mantener una actitud de mejora continua.

Mi experiencia en Qindel: desafíos con los que se crece

En el año 2012 estaba pasando por una etapa de búsqueda de crecimiento profesional y personal cuando Qindel tocó mi puerta. Haciendo una investigación inicial pude ver que su producto estrella era la plataforma VDI desarrollada in-house: QVD. Pero concretamente a mí me buscaban para un proyecto de desarrollo Java. Una vez entré, pude confirmar que era una empresa donde podía desarrollar mis aptitudes tecnológicas de la mano de profesionales consolidados en proyectos retadores para clientes internacionales: Vodafone, Inditex, BBVA ...

Crecer sin darse cuenta

Principalmente mis responsabilidades se ceñían al desarrollo Java pero siempre acompañado de grupos de trabajo de Perl, Python, Bash scripting, sysadmin... lo cual enriqueció mucho mis conocimientos además de despertar mis ganas de seguir aprendiendo.

A medida que aumentaba mi experiencia y me alineaba con la forma de trabajar del cliente me fueron asignando a proyectos de diferentes áreas: comercial, distribución y logística. Asimilaba así otro tipo de conocimientos a nivel empresarial que enriquecían mi currículum.

El cliente comprendió entonces que gran parte del conocimiento funcional de sus proyectos estaba en mi figura y también en la de otros líderes dentro de Qindel. Es por esto que fue inevitable el crecimiento bajo nuestro liderazgo conformando así equipos de hasta 10 miembros. Comenzaba entonces mi etapa como gestor de equipo.

No todo era trabajo en Qindel. Había tiempo para las conversaciones casuales, el café de media mañana, los cumpleaños de churros y porras, los desayunos por que sí... La convivencia siempre fue un punto que se reforzó desde dentro de la empresa. De esta forma, se fueron creando fuertes lazos entre los miembros del mismo o diferentes equipos que en muchos casos se convirtieron en buenas amistades.

Después de 6 años trabajando en Madrid, aproveché la presencia internacional de Qindel para expandir mis horizontes en Latinoamérica. Esto sin duda fue un salto a nivel personal que decidí tomar gracias a los más de 10 años que la empresa llevaba consolidada en México. Ahora el reto era acercarme al área de administración de sistemas Linux para lo cual Qindel me facilitó las herramientas necesarias para formarme y certificarme en LPI del que actualmente es partner oficial.

Ampliando horizontes allende los mares

Uno de los mayores retos en México fue sin duda la brecha cultural con España. A pesar de utilizar el mismo idioma, la idiosincrasia mexicana era algo completamente diferente a mi manera de ver las cosas, tanto profesional como personalmente. Conté con varios apoyos en el equipo de Qindel México que me ayudaron a reducir mi periodo de adaptación que todavía sigue.

Mi tarea en México no era continuista, necesitamos expandir equipos de desarrollo y soporte para un cliente internacional. Por lo tanto, otro reto importante fue revisar y validar varios aspectos laborales orientados a este tipo de perfiles. Después de mis primeros 4 años en México, se puede decir que se ha logrado una expansión en este lado del charco y con planes de crecimiento que permitirán deslocalizar y ampliar coberturas para diversos proyectos.

Sin duda para mí, el venir a México ha sido una decisión de vida. Me ha aportado una visión del mundo mucho más enriquecida que marcará huella en mis proyectos futuros.

A día de hoy ya son 9 años de relación con Qindel. A pesar de que aún tengo muchos años por delante en mi carrera profesional, Qindel ha sido y siempre será un pilar sobre el cual crecer en todos los aspectos de mi vida. Y si ha sido así es por la gente que me he encontrado en mi travesía por esta empresa.

Autor: N. Santos

¿Cómo buscamos imágenes parecidas usando redes neuronales?

La búsqueda de imágenes similares es una tarea común que podemos encontrar en gran variedad de aplicaciones. En la mayoría de los casos la entrada es una imagen y la salida debe ser una o más imágenes similares. En la imagen 1 podemos ver algunos ejemplos para diferentes significados de la palabra “similar”.

Imagen 1: Ejemplos de diferentes conjuntos de imágenes similares a una imagen base. Dependiendo de las características de interés la respuesta puede ser (muy) distinta.

Para diseñar un sistema suficientemente flexible decidimos basarnos en los modelos de redes neuronales profundas (si la tarea fuese encontrar imágenes idénticas o relativamente poco diferentes de una imagen base se podrían usar otros métodos como la función hash perceptual). En el resto de este texto explicaremos cómo se puede construir un modelo de redes neuronales capaz de buscar imágenes similares.

¿Cómo construimos un modelo usando redes neuronales?

Imagen 2: Diagrama esquemático mostrando un codificador de imágenes. La imagen de entrada (a la izquierda) es procesada por una red neuronal profunda convolucional cuya salida es convertida (mediante una pequeña red neuronal encima de la parte convolucional) en un vector de N componentes.

En la imagen 2 podemos ver la idea básica: queremos tener un codificador capaz de convertir una imagen arbitraria en un vector de N componentes (embedding). La idea es que el vector resultante represente coordenadas en un espacio de N dimensiones. Los puntos relacionados con dos imágenes similares deben estar cerca en ese espacio de N dimensiones, mientras que dos imágenes diferentes estarán colocadas lejos.

¿Cómo entrenamos una red para que discrimine imágenes?

El procedimiento de entrenamiento que usamos se basa en la idea de triplet loss[2]. La clave consiste en la selección de tripletes de imágenes:

  1. Una imagen base (anchor)
  2. Un ejemplo similar (imagen positiva)
  3. Un ejemplo diferente (imagen negativa)

El triplet loss es una función que mide la separación entre la base y los ejemplos positivos y negativos:

En la ecuación, dist es la distancia (p.ej. Euclidiana) entre los vectores que representan las imágenes. Cuánto más cerca está el ejemplo positivo a la imagen base y/o cuanto más lejos está el ejemplo negativo, más bajo será el valor de la función. La constante M representa el margen del triplet loss (la distancia mínima que debe separar los ejemplos positivos y negativos).

El algoritmo de entrenamiento modificará los pesos de la red neuronal para que el valor de la función objetivo siempre vaya bajando. En la imagen 3 podemos ver una ilustración del entrenamiento.

Imagen 3: El proceso de entrenamiento usando triplet loss. A: imagen base, P: imagen positiva, N: imagen negativa. Izquierda: al inicio del entrenamiento la red no es capaz de discriminar entre los ejemplos positivos y negativos y, por tanto, la distancia entre ambos y la imagen base es aproximadamente igual (y el valor de la función objetivo es alto). Derecha: después de suficientes iteraciones la red discrimina mejor, la distancia entre la imagen base y la positiva es (mucho) menor que entre la imagen base y la negativa. El valor de la función objetivo es bajo.

Para acelerar el proceso de entrenamiento se puede usar una versión especial de la función objetivo llamada batch hard loss[3]. En la imagen 4 podemos ver una ilustración.

Imagen 4: El proceso de entrenamiento usando batch hard loss. Se usan varios ejemplos positivos y negativos, y la función objetivo se calcula usando el más lejano de los positivos y el más cercano de los negativos. De esa manera se aumenta la dificultad (de ahí la palabra hard, es decir, difícil) del entrenamiento y se aprovecha mejor el conjunto de imágenes usadas para el entrenamiento. Izquierda: la situación al inicio del entrenamiento, cuando la red no es capaz de discriminar bien. Derecha: la situación al final, cuando la red siempre es capaz de colocar los ejemplos positivos más cerca que los ejemplos negativos.

El entrenamiento se acaba cuando el valor de la función objetivo deja de bajar. En este momento se guardan los pesos de la red y pueden ser usados en el futuro para convertir cualquier imágen en su representación numérica apta para ser comparada con representaciones numéricas de otras imágenes con el objetivo de determinar si son parecidas o no.

¿Cómo definimos imágenes positivas y negativas?

Esta es la parte más importante del entrenamiento. El criterio con el que seleccionamos imágenes positivas y negativas determinará el comportamiento del modelo. En la imagen 5 vemos un ejemplo que usamos para entrenar una red capaz de encontrar imágenes idénticas (o muy parecidas). Los positivos son una variación muy pequeña de la imagen base. De esta manera la red es capaz de encontrar versiones de la misma imagen (recortes, resoluciones diferentes, cambios de color).

Imagen 5: Un ejemplo de lote de imágenes para entrenar una red capaz de buscar imágenes idénticas. Los ejemplos positivos son una versión ligeramente variada de la imagen base. Los ejemplos negativos son imágenes diferentes.

Por otro lado, también podemos entrenar una red capaz de encontrar imágenes con la misma categoría de objetos, aunque no sean versiones de la misma imagen. En la imagen 6 podemos ver un ejemplo.

Imagen 6: El lote de entrenamiento de una red capaz de agrupar imágenes con la misma categoría de objeto. Las imágenes positivas son diferentes de la imagen base, pero contienen objetos suficientemente parecidos para que una red bien entrenada pueda discriminar entre diferentes tipos de objetos.

La selección correcta de imágenes positivas y negativas es crucial para obtener el resultado deseado.

Ejemplos

Entrenamos una red para que busque imágenes parecidas. En la imagen 7 mostramos algunos ejemplos.

Imagen 7: Tres ejemplos de la búsqueda de imágenes similares. La primera desde la izquierda es la imagen base. A continuación se muestran las cinco imágenes más cercanas a esa. Las distancias Euclídeas se muestran encima de las imágenes.

Como podemos ver, la red es capaz de encontrar imágenes parecidas razonablemente bien. Sin embargo, vemos que las distancias en la primera fila son mucho más pequeñas que en los otros dos ejemplos. Eso se debe al hecho que hay mucha menos variedad en hojas[4]. En el caso de los edificios hay más variedad y por eso las distancias son mayores. Sin embargo, podemos ver que, en el caso de la torre Eiffel, en la imagen más parecida el edificio está posicionado en el mismo lugar en la imagen (a la derecha del centro), mientras que las siguientes imágenes muestran cada vez más diferencias con respecto a la imagen base. En la última fila la imagen base es la segunda imagen más parecida a la torre Eiffel. En este caso vemos que el orden de las imágenes parecidas cambia, pero que los resultados siguen siendo consistentes con los vistos en la segunda fila.

Conclusiones

En este artículo mostramos cómo se puede entrenar una red neuronal que calcula un vector (embedding) asociado a una imagen, con el objetivo de usar ese vector para calcular distancias con otras imágenes. Las principales ventajas de este método son:

  • Los modelos son fáciles de construir y usar empleando librerías comunes (p.ej. Keras)
  • El método es adaptable a diferentes clases de problemas cambiando la definición de imágenes positivas y negativas.
  • El método permite entrenamiento iterativo (los modelos van mejorando con el tiempo y el entrenamiento se puede parar en cualquier momento para validar el modelo).

Las principales desventajas son:

  • Si la definición de los ejemplos positivos/negativos es equivocada, el modelo resultante dará resultados indeseados.
  • El entrenamiento es un proceso de muestreo aleatorio del conjunto de imágenes. El modelo resultante puede ser más o menos universal dependiendo de las propiedades del conjunto de entrenamiento. Cuánto más universal el conjunto, más general será el área de aplicabilidad del modelo.

Una de las implementaciones del modelo usando triplet loss se puede encontrar en nuestra página de GitHub: https://github.com/qindel-ml/siamese-nn/blob/master/train_triplet_loss.py


[1] La red neuronal convolucional puede ser una de las conocidas y bien estudiadas redes disponibles en p.ej. Keras (https://keras.io/api/applications/#available-models).

[2] Para la definición formal y más información puede consultar la página en Wikipedia: https://en.wikipedia.org/wiki/Triplet_loss

[3] Para más información puede leer el artículo “In Defense of the Triplet Loss for Person Re-Identification” de Hermans A., Beyer L. y Leibe B. (2017,  https://arxiv.org/abs/1703.07737).

[4] Las imágenes de hojas son parte del conjunto plant_leaves de TensorFlow (https://www.tensorflow.org/datasets/catalog/plant_leaves).

Autores: P. Mimica y S. Gutiérrez