5  Redes de neuronas artificiales

Las redes de neuronas artificiales son un modelo computacional inspirado en el funcionamiento del cerebro humano. Una neurona artificial es una unidad de cómputo bastante simple, que recibe una serie de entradas, las procesa y produce una salida. La salida de una neurona puede ser la entrada de otra neurona, formando así una red de neuronas interconectadas, donde cada conexión tiene un peso asociado. Es esta red, que a veces contiene miles y millones de neuronas, la que dota de gran potencia de cálculo a este modelo, siendo capaces de aprender patrones de datos muy complejos, como imágenes, texto o sonido, y por tanto, se utilizan a menudo en tareas de clasificación o regresión.

El aprendizaje en una red neuronal consiste en ajustar los pesos de las conexiones para minimizar el error entre la salida predicha y la salida real. Este proceso se realiza mediante algoritmos de optimización, como el del gradiente descendente que ya se vio en el capítulo de regresión.

5.1 Ejercicios Resueltos

Para la realización de esta práctica se requieren los siguientes paquetes:

using CSV  # Para la lectura de archivos CSV.
using DataFrames  # Para el manejo de datos tabulares.
using GLMakie  # Para el dibujo de gráficas.
using MLJ # Para la creación y entrenamiento de modelos de aprendizaje automático.
using Flux # Para la creación y entrenamiento de redes neuronales.
using MLJFlux # Interfaz de Flux para MLJ.
using Optimisers # Para la optimización de funciones.
using Statistics # Para las funciones de coste.

Ejercicio 5.1 El conjunto de datos viviendas.csv contiene información sobre el precio de venta de viviendas en una ciudad.

  1. Cargar los datos del archivo viviendas.csv en un data frame.

    using CSV, DataFrames
    # Creamos un data frame a partir del archivo CSV.
    df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/viviendas.csv"), DataFrame)
    # Mostramos las primeras cinco filas del data frame.
    first(df, 5)
    5×13 DataFrame
    Row precio area dormitorios baños habitaciones calleprincipal huespedes sotano calentador climatizacion garaje centrico amueblado
    Int64 Int64 Int64 Int64 Int64 String3 String3 String3 String3 String3 Int64 String3 String15
    1 13300000 7420 4 2 3 si no no no si 2 si amueblado
    2 12250000 8960 4 4 4 si no no no si 3 no amueblado
    3 12250000 9960 3 2 2 si no si no no 2 si semi-amueblado
    4 12215000 7500 4 2 2 si no si no si 3 si amueblado
    5 11410000 7420 4 1 2 si si si no si 2 no amueblado
  2. Extraer las columnas area y precio del data frame y convertirlas a un vector de tipo Float32. Pasar el precio a miles de euros.

    # Extraemos las columnas area y precio.
    X = Float32.(df.area)
    y = Float32.(df.precio) ./ 1000
    545-element Vector{Float32}:
     13300.0
     12250.0
     12250.0
     12215.0
     11410.0
     10850.0
     10150.0
     10150.0
      9870.0
      9800.0
      9800.0
      9681.0
      9310.0
         ⋮
      2100.0
      2100.0
      2100.0
      1960.0
      1890.0
      1890.0
      1855.0
      1820.0
      1767.15
      1750.0
      1750.0
      1750.0
  3. Dibujar un diagrama de dispersión entre el precio y el area de las viviendas.

    using GLMakie
    fig = Figure()
    ax = Axis(fig[1, 1], title = "Precio vs Area", xlabel = "Área (m²)", ylabel = "Precio (€)")
    scatter!(ax, X, y)
    fig
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238
  4. Construir un modelo lineal simple usando un perceptrón como el de la figura, tomando como función de activación la función identidad.

    Perceptrón de una sola entrada..

    Inicializar los parámetros del modelo a 0 y dibujarlo en el diagrama de dispersión.

    # Definimos el modelo lineal.
    perceptron(W, b, x) = @. W[1] * x + b[1]
    # Inicializamos los pesos y el término independiente.
    W = Float32[0]
    b = Float32[0]
    lines!(ax, X, perceptron(W, b, X), label = "Modelo 0", color = :red)
    fig
  5. Aplicar el modelo a los datos y calcular el error cuadrático medio del modelo.

    using Statistics
    # Definimos la función de coste.
    coste(W, b, X, y) = mean((y .- perceptron(W, b, X)).^2)
    # Calculamos el coste del modelo inicial.
    println("Error cuadrático medio: ", coste(W, b, X, y))
    Error cuadrático medio: 2.6213836e7

    Se observa que el error cuadrático medio es bastante alto, lo que indica que el modelo no se ajusta bien a los datos.

  6. ¿En qué dirección deben modificarse los parámetros del modelo para reducir el error cuadrático medio? Actualizar los parámetros del modelo en esa dirección utilizando una tasa de aprendizaje η=109. Comprobar que el error cuadrático medio ha disminuido con los nuevos parámetros.

    Los parámetros deben modificarse en la dirección en la que más rápidamente decrezca el error cuadrático medio. Esta dirección está dada por el gradiente del error cuadrático respecto a los parámetros, que se puede calcular como:

    E(W,b)=(EW,Eb).

    using Flux
    # Declaramos los pesos como variables simbólicas
    
    # Calculamos el gradiente del coste.
    ∂E_∂W, ∂E_∂b = gradient(coste, W, b, X, y)
    # Mostramos el gradiente.
    println("Gradiente del coste: ($E_∂W, $E_∂b)")
    # Definimos la tasa de aprendizaje.
    η = 1e-8
    # Mostramos los parámetros iniciales.
    println("Parámetros iniciales: W = $W, b = $b")
    # Actualizamos los parámetros en la dirección de
    W -= η * ∂E_∂W
    b -= η * ∂E_∂b
    # Mostramos los nuevos parámetros.
    println("Nuevos parámetros: W = $W, b = $b")
    # Comprobamos que el error cuadrático medio ha disminuido.
    println("Error cuadrático medio: ", coste(W, b, X, y))
    # Dibujamos el nuevo modelo en el diagrama de dispersión.
    lines!(ax, X, perceptron(W, b, X), label = "Modelo 1")
    fig
    Gradiente del coste: (Float32[-5.344584f7], Float32[-9533.46])
    Parámetros iniciales: W = Float32[0.0], b = Float32[0.0]
    Nuevos parámetros: W = [0.5344584], b = [9.5334599609375e-5]
    Error cuadrático medio: 6.569670916877353e6
  7. Definir una función para entrenar el perceptrón modificando los pesos en la dirección opuesta al gradiente del error cuadrático medio.

    function entrenar_perceptron!(W, b, X, y, η)
        """
        Función para entrenar el perceptrón.
        W: peso del modelo.
        b: término independiente del modelo.
        X: vector de entradas.
        y: vector de salidas.
        η: tasa de aprendizaje.
        """
        # Calculamos el gradiente del coste.
        ∂E_∂W, ∂E_∂b = gradient(coste, W, b, X, y)
        # Actualizamos los parámetros en la dirección opuesta al gradiente.
        W .-= η * ∂E_∂W
        b .-= η * ∂E_∂b
    end
    entrenar_perceptron! (generic function with 1 method)
  8. Usar la función anterior para entrenar el perceptrón y repetir el proceso durante 9 iteraciones más. Dibujar los modelos actualizados en el diagrama de dispersión.

    # Repetimos el proceso de entrenamiento del perceptrón.
    for i = 2:10
        entrenar_perceptron!(W, b, X, y, η)
        # Mostramos los parámetros y el coste del modelo.
        ecm = coste(W, b, X, y)
        println("Iteración ", i, ", Parámetros: W = $W, b = $b, Coste: $ecm")
        # Dibujamos el modelo actualizado en el diagrama de dispersión.
        lines!(ax, X, perceptron(W, b, X), label = "Modelo $i")
    end
    axislegend(ax)
    fig
    Iteración 2, Parámetros: W = [0.7351053379977437], b = [0.00013561418157921425], Coste: 3.8010036630367776e6
    Iteración 3, Parámetros: W = [0.8104324229132014], b = [0.0001552249559885306], Coste: 3.4107850089995693e6
    Iteración 4, Parámetros: W = [0.838711796053412], b = [0.00016707622479181458], Coste: 3.355787208775712e6
    Iteración 5, Parámetros: W = [0.8493284674839952], b = [0.00017601441178095897], Coste: 3.348035761002203e6
    Iteración 6, Parámetros: W = [0.8533141887596891], b = [0.00018385896650121626], Coste: 3.3469432599931033e6
    Iteración 7, Parámetros: W = [0.8548105117157216], b = [0.00019129294862501068], Coste: 3.34678927740313e6
    Iteración 8, Parámetros: W = [0.8553722621219417], b = [0.00019857279313692855], Coste: 3.3467675705099306e6
    Iteración 9, Parámetros: W = [0.855583154313161], b = [0.00020579477113007419], Coste: 3.3467645066817994e6
    Iteración 10, Parámetros: W = [0.855662326942257], b = [0.00021299502480003158], Coste: 3.3467640704253544e6
  9. Definir de nuevo el perceptrón como una red neuronal de una sola capa con una entrada y una salida con el paquete Flux.jl y mostrar los parámetros iniciales del modelo.

    Utilizar la función Dense(n => m) del paquete Flux.jl para definir una capa de m neuronas con n entradas cada una. Por defecto la función de activación es la identidad.

    Flux inicializa los pesos de las conexiones de forma aleatoria y el término independiente a cero.

    # Definimos una capa con una sola neurona con una entrada.
    modelo = Dense(1 => 1)
    # Mostramos los parámetros del modelo.
    println("Pesos: ", modelo.weight, ", Término independiente: ", modelo.bias)
    Pesos: Float32[0.8322743;;], Término independiente: Float32[0.0]
  10. Calcular las predicciones de los precios de las viviendas con el modelo inicial.

    Para obtener las salidas de una red neuronal definida con Flux, debe pasarse al modelo una matriz con las entradas, donde cada columna es un caso. Para convertir el vector de las areas en una matriz de una sola fila se puede usar la función reshape.

    # Convertimos el vector de las areas en una matriz de una sola fila.
    X = reshape(X, 1, length(X))
    = modelo(X)
    1×545 Matrix{Float32}:
     6175.48  7457.18  8289.45  6242.06  …  1997.46  3012.83  2421.92  3204.26
  11. Definir una función de coste que calcule el error cuadrático medio entre la salida del modelo y la salida real usando el paquete Flux.jl. Calcular el coste del modelo inicial.

    Usar la función Flux.mse para calcular el error cuadrático medio entre la salida del modelo y la salida real.

    Para calcular el coste de un modelo en Flux, el vector con las etiquetas también debe ser una matriz de una fila.

    # Convertimos el vector de los precios en una matriz de una sola columna.
    y = reshape(y, 1, length(y))
    # Definimos la función de coste como el error cuadrático medio.
    coste(modelo, X, y) = Flux.mse(modelo(X),y)
    coste(modelo, X, y)
    3.3639158f6
  12. Definir una función para entrenar el modelo con y usarla para entrenar el modelo hasta que la reducción en el error cuadrático medio sea menor del 0.01%. ¿Cuántas iteraciones hacen falta? ¿Cuál es el coste del último modelo? Dibujar el coste en cada iteración.

    function entrenar_modelo!(modelo, coste, X, y, η)
        """
        Función para entrenar el modelo.
        modelo: modelo a entrenar.
        coste: función de coste.
        X: matriz de entradas.
        y: matriz de salidas.
        η: tasa de aprendizaje.
        """
        # Calculamos el gradiente del coste.
    = gradient(coste, modelo, X, y)
        # Actualizamos los parámetros del modelo en la dirección opuesta al gradiente.
        @. modelo.weight = modelo.weight - η * ∇[1].weight
        @. modelo.bias = modelo.bias - η * ∇[1].bias
    end
    # Creamos un vector para guardar los costes del proceso de entrenamiento.
    costes = [coste(modelo, X, y)]
    reduccion_coste = Inf
    iteraciones = 0
    # Iteramos el proceso de aprendizaje hasta que la reducción del coste sea menor del 0.01%.
    while reduccion_coste > 0.0001
        iteraciones += 1
        entrenar_modelo!(modelo, coste, X, y, η)
        # Calculamos el nuevo coste y lo añadimos al vector de costes.
        push!(costes, coste(modelo, X, y))
        # Calculamos la reducción del coste.
        reduccion_coste = abs((costes[end] - costes[end-1]) / costes[end])
    end
    # Mostramos el número de iteraciones y el coste final.
    println("Número de iteraciones: ", iteraciones)
    println("Coste final: ", costes[end])
    # Dibujamos el coste en cada iteración.
    fig2 = Figure()
    ax2 = Axis(fig2[1, 1], title = "Evolución del coste en el entrenamiento", xlabel = "Iteraciones", ylabel = "Coste")
    lines!(ax2, 0:iteraciones, costes)
    fig2
    Número de iteraciones: 3
    Coste final: 3.3468115e6
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238
  13. Mostrar el modelo final en el diagrama de dispersión y compararlo con el último modelo obtenido con el perceptrón.

    # Dibujamos el modelo final en el diagrama de dispersión.
    lines!(ax, vec(X), vec(modelo(X)), label = "Modelo final", color = :red, linewidth = 2)
    fig

    Se observa que el modelo final prácticamente coincide con el último modelo obtenido con el perceptrón, lo que indica que ambos modelos son equivalentes.

  14. Crear una red neuronal para predecir el precio de las viviendas usando todas las características de las viviendas y mostrar los pesos y el término independiente del modelo.

    Ahora el modelo tiene que tener tantas entradas como características tenga el conjunto de datos, en este caso 12 características de entrada y una salida para el precio.

    # Definimos el modelo con 12 entradas y 1 salida.
    modelo = Dense(12 => 1)
    # Mostramos los parámetros del modelo.
    println("Pesos: ", modelo.weight, ", Término independiente: ", modelo.bias)
    Pesos: Float32[-0.33077675 0.475148 0.45860797 -0.65326595 -0.3067425 0.46475366 -0.4920061 0.33808297 0.2910382 -0.040311083 -0.041551717 0.27110094], Término independiente: Float32[0.0]
  15. Extraer las características de entrada del conjunto de datos y convertir las que sean cualitativas en cuantitativas.

    Usar la función coerce! del paquete MLJScientificTypes.jl para convertir los tipos de las columnas del data frame.

    using MLJ
    # Extraemos las etiquetas.
    y = Float32.(df.precio / 1000)
    # Extraemos las características de entrada.
    X = select(df, Not(:precio))
    # Convertimos las columnas cualitativas en cuantitativas.
    schema(X)
    ┌────────────────┬──────────┬──────────┐
    │ names          │ scitypes │ types    │
    ├────────────────┼──────────┼──────────┤
    │ area           │ Count    │ Int64    │
    │ dormitorios    │ Count    │ Int64    │
    │ baños          │ Count    │ Int64    │
    │ habitaciones   │ Count    │ Int64    │
    │ calleprincipal │ Textual  │ String3  │
    │ huespedes      │ Textual  │ String3  │
    │ sotano         │ Textual  │ String3  │
    │ calentador     │ Textual  │ String3  │
    │ climatizacion  │ Textual  │ String3  │
    │ garaje         │ Count    │ Int64    │
    │ centrico       │ Textual  │ String3  │
    │ amueblado      │ Textual  │ String15 │
    └────────────────┴──────────┴──────────┘
    

    Las columnas calleprincipal, huespedes, sotano, calentador, climatizacion, centrico y amueblado son cualitativas y deben convertirse a cuantitativas.

    # Convertimos las columnas de tipo texto a tipo categórico.
    coerce!(X, Textual => Multiclass)
    # Convertimos las columnas categóricas a tipo numérico.
    coerce!(X, Multiclass => Count)
    # Convertimos las columnas de tipo Int64 a tipo Int32 para ganar eficiencia.
    X = Float32.(X)
    # Observamos el nuevo esquema del data frame.
    schema(X)
    ┌────────────────┬────────────┬─────────┐
    │ names          │ scitypes   │ types   │
    ├────────────────┼────────────┼─────────┤
    │ area           │ Continuous │ Float32 │
    │ dormitorios    │ Continuous │ Float32 │
    │ baños          │ Continuous │ Float32 │
    │ habitaciones   │ Continuous │ Float32 │
    │ calleprincipal │ Continuous │ Float32 │
    │ huespedes      │ Continuous │ Float32 │
    │ sotano         │ Continuous │ Float32 │
    │ calentador     │ Continuous │ Float32 │
    │ climatizacion  │ Continuous │ Float32 │
    │ garaje         │ Continuous │ Float32 │
    │ centrico       │ Continuous │ Float32 │
    │ amueblado      │ Continuous │ Float32 │
    └────────────────┴────────────┴─────────┘
    
  16. Convertir el data frame a una matriz, transponerla y normalizar los datos.

    Cuando se trabaja con variables de entrada de diferentes escalas, es recomendable normalizarlas para que todas tengan la misma importancia en el modelo. Para ello, se puede usar la función normalise del paquete Flux.jl para normalizar los datos.

    # Convertimos el data frame a una matriz y la transponemos.
    X = Matrix(X)'
    X = Flux.normalise(X)
    y = y'
    # Definimos como función de coste el error cuadrático medio.
    coste(modelo, X, y) = Flux.mse(modelo(X), y)
    # Calculamos el coste del modelo inicial.
    println("Error cuadrático medio: ", coste(modelo, X, y))
    Error cuadrático medio: 2.621381e7
  17. Definir una función para entrenar el modelo con y usarla para entrenar el modelo hasta que la reducción en el error cuadrático medio sea menor de 104. ¿Cuántas iteraciones hacen falta? ¿Cuál es el coste del último modelo? Dibujar el coste en cada iteración. ¿Es mejor modelo que el percetrón?

    function entrenar_modelo!(modelo, coste, X, y, η)
        """
        Función para entrenar el modelo.
        modelo: modelo a entrenar.
        coste: función de coste.
        X: matriz de entradas.
        y: matriz de salidas.
        η: tasa de aprendizaje.
        """
        # Calculamos el gradiente del coste.
    = gradient(coste, modelo, X, y)
        # Actualizamos los parámetros del modelo en la dirección opuesta al gradiente.
        @. modelo.weight = modelo.weight - η * ∇[1].weight
        @. modelo.bias = modelo.bias - η * ∇[1].bias
    end
    # Definimos la tasa de aprendizaje.
    η = 1e-2
    # Creamos un vector para guardar los costes del proceso de entrenamiento.
    costes = [coste(modelo, X, y)]
    reduccion_coste = Inf
    iteraciones = 0
    # Iteramos el proceso de aprendizaje hasta que la reducción del coste sea menor del 0.01%.
    while reduccion_coste > 1e-4
        iteraciones += 1
        entrenar_modelo!(modelo, coste, X, y, η)
        # Calculamos el nuevo coste y lo añadimos al vector de costes.
        push!(costes, coste(modelo, X, y))
        # Calculamos la reducción del coste.
        reduccion_coste = abs((costes[end] - costes[end-1]))
    end
    # Mostramos el número de iteraciones y el coste final.
    println("Número de iteraciones: ", iteraciones)
    println("Coste final: ", costes[end])
    Número de iteraciones: 413
    Coste final: 1.1411414e6

    Ahora dibujamos la evolución del coste con las iteraciones en el entrenamiento.

    # Dibujamos el coste en cada iteración.
    fig3 = Figure()
    ax3 = Axis(fig3[1, 1], title = "Evolución del coste en el entrenamiento", xlabel = "Iteraciones", ylabel = "Coste")
    lines!(ax3, 0:iteraciones, costes)
    fig3
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

    El coste final es menor que el del perceptrón, lo que indica que el modelo es mejor.

    Finalmente, mostramos los parámetros del modelo entrenado.

    # Mostramos los pesos y el término independiente del modelo.
    println("Pesos: ", modelo.weight, ", Término independiente: ", modelo.bias)
    Pesos: Float32[536.325 97.91486 504.9331 392.71478 164.30511 123.32048 179.2231 187.51067 411.07886 257.39856 279.6773 10.985558], Término independiente: Float32[4765.594]

Ejercicio 5.2 El conjunto de datos pingüinos.csv contiene un conjunto de datos sobre tres especies de pingüinos con las siguientes variables:

  • Especie: Especie de pingüino, comúnmente Adelie, Chinstrap o Gentoo.
  • Isla: Isla del archipiélago Palmer donde se realizó la observación.
  • Longitud_pico: Longitud del pico en mm.
  • Profundidad_pico: Profundidad del pico en mm
  • Longitud_ala: Longitud de la aleta en mm.
  • Peso: Masa corporal en gramos.
  • Sexo: Sexo
  1. Cargar los datos del archivo pinguïnos.csv en un data frame.

    using CSV, DataFrames
    df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/pingüinos.csv"), DataFrame, missingstring="NA")
    first(df, 5)
    5×7 DataFrame
    Row Especie Isla Longitud_pico Profundidad_pico Longitud_ala Peso Sexo
    String15 String15 Float64? Float64? Int64? Int64? String7?
    1 Adelie Torgersen 39.1 18.7 181 3750 macho
    2 Adelie Torgersen 39.5 17.4 186 3800 hembra
    3 Adelie Torgersen 40.3 18.0 195 3250 hembra
    4 Adelie Torgersen missing missing missing missing missing
    5 Adelie Torgersen 36.7 19.3 193 3450 hembra
  2. Seleccionar las columnas Longitud_pico, Profundidad_pico, Longitud_ala y Especie del data frame.

    # Seleccionamos las columnas Longitud_pico, Profundidad_pico, Longitud_ala y Especie.
    select!(df, [:Longitud_pico, :Profundidad_pico, :Longitud_ala, :Especie])
    first(df, 5)
    5×4 DataFrame
    Row Longitud_pico Profundidad_pico Longitud_ala Especie
    Float64? Float64? Int64? String15
    1 39.1 18.7 181 Adelie
    2 39.5 17.4 186 Adelie
    3 40.3 18.0 195 Adelie
    4 missing missing missing Adelie
    5 36.7 19.3 193 Adelie
  3. Hacer un análisis de los datos perdidos en el data frame.

    describe(df, :nmissing)
    4×2 DataFrame
    Row variable nmissing
    Symbol Int64
    1 Longitud_pico 2
    2 Profundidad_pico 2
    3 Longitud_ala 2
    4 Especie 0
  4. Eliminar del data frame los casos con valores perdidos.

    dropmissing!(df)
    342×4 DataFrame
    317 rows omitted
    Row Longitud_pico Profundidad_pico Longitud_ala Especie
    Float64 Float64 Int64 String15
    1 39.1 18.7 181 Adelie
    2 39.5 17.4 186 Adelie
    3 40.3 18.0 195 Adelie
    4 36.7 19.3 193 Adelie
    5 39.3 20.6 190 Adelie
    6 38.9 17.8 181 Adelie
    7 39.2 19.6 195 Adelie
    8 34.1 18.1 193 Adelie
    9 42.0 20.2 190 Adelie
    10 37.8 17.1 186 Adelie
    11 37.8 17.3 180 Adelie
    12 41.1 17.6 182 Adelie
    13 38.6 21.2 191 Adelie
    331 45.2 16.6 191 Chinstrap
    332 49.3 19.9 203 Chinstrap
    333 50.2 18.8 202 Chinstrap
    334 45.6 19.4 194 Chinstrap
    335 51.9 19.5 206 Chinstrap
    336 46.8 16.5 189 Chinstrap
    337 45.7 17.0 195 Chinstrap
    338 55.8 19.8 207 Chinstrap
    339 43.5 18.1 202 Chinstrap
    340 49.6 18.2 193 Chinstrap
    341 50.8 19.0 210 Chinstrap
    342 50.2 18.7 198 Chinstrap
  5. Mostrar los tipos de datos científicos de cada columna del data frame.

    Usar la función schema del paquete MLJ.

    using MLJ
    schema(df)
    ┌──────────────────┬────────────┬──────────┐
    │ names            │ scitypes   │ types    │
    ├──────────────────┼────────────┼──────────┤
    │ Longitud_pico    │ Continuous │ Float64  │
    │ Profundidad_pico │ Continuous │ Float64  │
    │ Longitud_ala     │ Count      │ Int64    │
    │ Especie          │ Textual    │ String15 │
    └──────────────────┴────────────┴──────────┘
    
  6. Convertir las columnas Longitud_pico, Profundidad_pico, Longitud_ala a tipo científico Continuous y la columna Especie a tipo Multiclass.

    Usar la función coerce! del paquete MLJ para transformar las variables categóricas en variables numéricas.

    # Convertimos la longitud del ala a tipo científico continuo.
    coerce!(df, :Longitud_ala => Continuous)
    # Convertimos la columna Especie a tipo Multiclass.
    coerce!(df, Textual => Multiclass)
    # Mostramos el nuevo esquema del data frame.
    schema(df)
    ┌──────────────────┬───────────────┬────────────────────────────────────┐
    │ names            │ scitypes      │ types                              │
    ├──────────────────┼───────────────┼────────────────────────────────────┤
    │ Longitud_pico    │ Continuous    │ Float64                            │
    │ Profundidad_pico │ Continuous    │ Float64                            │
    │ Longitud_ala     │ Continuous    │ Float64                            │
    │ Especie          │ Multiclass{3} │ CategoricalValue{String15, UInt32} │
    └──────────────────┴───────────────┴────────────────────────────────────┘
    
  7. Dividir el data frame en dos partes, una con las variables de entrada y otra con la variable de salida (Especie).

    Usar la función unpack del paquete MLJ para dividir el data frame en dos partes, una con las columnas de entrada del modelo y otra con la columna de salida.

    y, X = unpack(df, ==(:Especie), name -> true);
  8. Crear un modelo de red neuronal con la siguiente estructura.

    • Capa de entrada con 3 neuronas (una por cada variable de entrada).
    • Una capa oculta con 6 neuronas y función de activación relu.
    • Capa de salida con 3 neuronas (una por cada especie de pingüino) y función de activación softmax.

    Usar el algoritmo de aprendizaje Adam (Adaptative Moment Estimation) con una tasa de aprendizaje de 0.01. Introducir en el modelo 0 etapas (epochs) de entrenamiento, para trabajar con los pesos aleatorios iniciales.

    Cargar el constructor de modelos de redes neuronales NeuralNetworkClassifier del paquete MLJFlux e inicializarlo con los siguientes parámetros:

    • builder: Permite definir el tipo de red neuronal. En este caso, usar la función MLP (Multi Layer Perceptron) para crear el modelo de red neuronal. Indicar el número de neuronas de las capas ocultas con hidden y la función de activación con σ.
    • optimiser: Permite definir el optimizador, es decir, el algoritmo de aprendizaje. En este caso usar el optimizador Adam del paquete Optimisers.jl con una tasa de aprendizaje de 0.01.
    • batch_size: Tamaño del lote de entrenamiento. En este caso usar un tamaño de 10.
    • epochs: Número de etapas de entrenamiento. En este caso usar 0.
    • acceleration: Permite usar la aceleración de la GPU si se dispone de tarjeta gráfica. Normalmente CUDALibs().

    Usar la función machine del paquete MLJ para crear una máquina de aprendizaje con el modelo y los datos.

    using Flux, MLJFlux, Optimisers
    # Cargamos el código que define las redes neuronales.
    RedNeuronal = @load NeuralNetworkClassifier pkg = "MLJFlux"
    # Creamos un modelo de red neuronal con los parámetros por defecto.
    modelo = RedNeuronal(
        builder = MLJFlux.MLP(; hidden = (6,), σ = relu),
        optimiser=Optimisers.Adam(0.01),
        batch_size = 10,
        epochs = 0,
        # acceleration = CUDALibs()         # Para utilizar targetas gráficas GPU
        )
    # Creamos una máquina de aprendizaje con el modelo y los datos.
    mach = machine(modelo, X, y)
    [ Info: For silent loading, specify `verbosity=0`. 
    import MLJFlux ✔
    untrained Machine; caches model-specific representations of data
      model: NeuralNetworkClassifier(builder = MLP(hidden = (6,), …), …)
      args: 
        1:  Source @444 ⏎ Table{AbstractVector{Continuous}}
        2:  Source @566 ⏎ AbstractVector{Multiclass{3}}
  9. Dividir el conjunto de datos en un conjunto de entrenamiento con el 70% de los ejemplos y otro de prueba con el 30% restante.

    Usar la función partition del paquete MLJ para dividir el conjunto de datos en un conjunto de entrenamiento y otro de prueba.

    # Dividimos el conjunto de datos en un conjunto de entrenamiento y otro de prueba.
    train, test = partition(eachindex(y), 0.7, shuffle=true, rng=123)
    ([279, 255, 57, 6, 34, 267, 165, 35, 148, 56  …  320, 19, 85, 89, 284, 44, 169, 182, 98, 66], [236, 304, 118, 198, 80, 297, 257, 117, 67, 65  …  9, 329, 73, 121, 309, 54, 299, 71, 265, 127])
  10. Entrenar el modelo con el conjunto de ejemplos de entrenamiento y predecir la especie de los pingüinos del conjunto de prueba. Calcular la matriz de confusión y la precisión del modelo y la entropía cruzada.

    Usar la función fit! del paquete MLJ para entrenar el modelo con el conjunto de entrenamiento.

    Para predecir la especie de los pingüinos del conjunto de prueba, usar la función predict del paquete MLJ.

    Para calcular la matriz de confusión, usar la función confusion_matrix del paquete StatisticalMeasures.

    Usar la función accuracy del paquete StatisticalMeasures.

    # Entrenamos el modelo con el conjunto de entrenamiento.
    fit!(mach, rows = train)
    # Predecimos las probabilidades de cada ejemplo de pertenecer a cada clase.
    = predict(mach, rows = test)
    [ Info: Training machine(NeuralNetworkClassifier(builder = MLP(hidden = (6,), …), …), …).
    [ Info: MLJFlux: converting input data to Float32
    103-element CategoricalDistributions.UnivariateFiniteVector{Multiclass{3}, String15, UInt32, Float32}:
     UnivariateFinite{Multiclass{3}}(Adelie=>4.72e-10, Chinstrap=>2.34e-32, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>1.43e-10, Chinstrap=>4.9999998e-33, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>6.27e-8, Chinstrap=>2.28e-27, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>4.22e-10, Chinstrap=>2.7e-32, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>9.68e-8, Chinstrap=>4.53e-27, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>4.3e-9, Chinstrap=>4.24e-29, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>3.5e-10, Chinstrap=>6.45e-33, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>5.07e-8, Chinstrap=>1.9e-28, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>1.5e-8, Chinstrap=>2.0099999e-28, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>8.44e-9, Chinstrap=>4.57e-29, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>2.71e-10, Chinstrap=>1.3800001e-32, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>7.35e-9, Chinstrap=>7.76e-30, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>7.82e-11, Chinstrap=>5.11e-35, Gentoo=>1.0)
     ⋮
     UnivariateFinite{Multiclass{3}}(Adelie=>4.9e-10, Chinstrap=>3.59e-31, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>4.89e-10, Chinstrap=>2.66e-32, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>1.28e-8, Chinstrap=>9.54e-29, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>6.42e-9, Chinstrap=>7.4700004e-29, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>1.88e-9, Chinstrap=>1.4999999e-30, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>3.93e-8, Chinstrap=>1.62e-28, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>4.76e-10, Chinstrap=>2.32e-31, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>1.32e-7, Chinstrap=>9.42e-27, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>1.19e-9, Chinstrap=>1.13e-30, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>1.96e-8, Chinstrap=>2.42e-28, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>3.77e-11, Chinstrap=>1.29e-35, Gentoo=>1.0)
     UnivariateFinite{Multiclass{3}}(Adelie=>8.28e-9, Chinstrap=>2.56e-29, Gentoo=>1.0)

    Obtenemos distribuciones de probabilidad. La predicciones son las clases con mayor probabilidad.

    # Obtenemos la clase más probable.
    mode.(ŷ)
    103-element CategoricalArrays.CategoricalArray{String15,1,UInt32}:
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     ⋮
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")
     String15("Gentoo")

    A continuación obtenemos la matriz de confusión.

    # Calculamos la matriz de confusión.
    cm = confusion_matrix(y[test], mode.(ŷ))
              ┌─────────────────────────────┐
              │        Ground Truth         │
    ┌─────────┼─────────┬─────────┬─────────┤
    │Predicted│ Adelie  │Chinstrap│ Gentoo  │
    ├─────────┼─────────┼─────────┼─────────┤
    │ Adelie  │    0    │    0    │   45    │
    ├─────────┼─────────┼─────────┼─────────┤
    │Chinstrap│    0    │    0    │   21    │
    ├─────────┼─────────┼─────────┼─────────┤
    │ Gentoo  │    0    │    0    │   37    │
    └─────────┴─────────┴─────────┴─────────┘

    Finalmente calculamos la precisión del modelo y la entropía cruzada.

    # Calculamos la precisión del modelo.
    precision = sum(mode.(ŷ) .== y[test]) / length(test)
    # O directamente usando la función accuracy
    accuracy(mode.(ŷ), y[test])
    println("Precisión del modelo: ", precision)
    # Calculamos la entropía cruzada.
    println("Entropía cruzada: ", cross_entropy(ŷ, y[test]))
    Precisión del modelo: 0.3592233009708738
    Entropía cruzada: 14.96195555472543
  11. Entrenar el modelo durante 100 etapas y evaluar de nuevo la precisión del modelo y la entropía cruzada. ¿Ha mejorado el modelo?

    Usar la función evaluate del paquete MLJ para evaluar el modelo. Los parámetros más importantes de esta función son:

    • resampling: Indica el método de muestreo para definir los conjuntos de entrenamiento y test. Los métodos más habituales son:
      • Holdout(fraction_train = p): Divide el conjunto de datos tomando una proporción de p ejemplos en el conjunto de entrenamiento y 1p en el conjunto de test.
      • CV(nfolds = n, shuffle = true|false): Utiliza validación cruzada con n iteraciones. Si se indica shuffle = true, se utiliza validación cruzada aleatoria.
      • StratifiedCV(nfolds = n, shuffle = true|false): Utiliza validación cruzada estratificada con n iteraciones. Si se indica shuffle = true, se utiliza validación cruzada estratificada aleatoria.
      • InSample(): Utiliza el conjunto de entrenamiento como conjunto de test.
    • measures: Indica las métricas a utilizar para evaluar el modelo. Las métricas más habituales son:
      • cross_entropy: Pérdida de entropía cruzada.
      • confusion_matrix: Matriz de confusión.
      • true_positive_rate: Tasa de verdaderos positivos.
      • true_negative_rate: Tasa de verdaderos negativos.
      • ppv: Valor predictivo positivo.
      • npv: Valor predictivo negativo.
      • accuracy: Precisión.
      Se puede indicar más de una en un vector.
    # Definimos el número de épocas de entrenamiento.
    modelo.epochs = 100
    # Actualizamos la máquina de aprendizaje con el nuevo modelo.
    mach = machine(modelo, X, y)
    # Entrenamos el modelo con el conjunto de entrenamiento y evaluamos el modelo.
    evaluate!(mach, resampling = Holdout(fraction_train = 0.7, rng = 123), measure = [accuracy, cross_entropy])
    PerformanceEvaluation object with these fields:
      model, measure, operation,
      measurement, per_fold, per_observation,
      fitted_params_per_fold, report_per_fold,
      train_test_rows, resampling, repeats
    Extract:
    ┌───┬──────────────────────┬──────────────┬─────────────┐
    │   │ measure              │ operation    │ measurement │
    ├───┼──────────────────────┼──────────────┼─────────────┤
    │ A │ Accuracy()           │ predict_mode │ 0.359       │
    │ B │ LogLoss(             │ predict      │ 23.1        │
    │   │   tol = 2.22045e-16) │              │             │
    └───┴──────────────────────┴──────────────┴─────────────┘
    

    La precisión del modelo es muy baja. Esto puede deberse a que la estructura de la red neuronal no es la adecuada, o a que las variables de entrada no están normalizadas.

  12. Estandarizar las variables de entrada y volver a entrenar el modelo una sola etapa. Evaluar la precisión del modelo y la entropía cruzada.

    Usar la función Standardizer del paquete MLJ para estandarizar las variables de entrada. La estandarización consiste en restar la media y dividir por la desviación típica de cada variable.

    Usar el operador de tubería |> para encadenar la estandarización y la red neuronal en un modelo.

    # Definimos la transformación de estandarización.
    estandarizacion = Standardizer()
    # Definimos de nuevo la red neuronal.
    red = RedNeuronal(
        builder = MLJFlux.MLP(; hidden = (6,), σ = relu),
        optimiser = Optimisers.Adam(0.01),
        batch_size = 10,
        epochs = 1
        )
    # Definimos el modelo mediante un flujo que aplique primero la estandarización y luego la red neuronal.
    modelo = estandarizacion  |> red
    # Creamos una máquina de aprendizaje con el modelo y los datos.
    mach = machine(modelo, X, y)
    # Entrenamos el modelo con el conjunto de entrenamiento y evaluamos el modelo.
    evaluate!(mach, resampling = Holdout(fraction_train = 0.7, rng = 123), measure = [accuracy, cross_entropy])
    PerformanceEvaluation object with these fields:
      model, measure, operation,
      measurement, per_fold, per_observation,
      fitted_params_per_fold, report_per_fold,
      train_test_rows, resampling, repeats
    Extract:
    ┌───┬──────────────────────┬──────────────┬─────────────┐
    │   │ measure              │ operation    │ measurement │
    ├───┼──────────────────────┼──────────────┼─────────────┤
    │ A │ Accuracy()           │ predict_mode │ 0.786       │
    │ B │ LogLoss(             │ predict      │ 0.662       │
    │   │   tol = 2.22045e-16) │              │             │
    └───┴──────────────────────┴──────────────┴─────────────┘
    
  13. Volver a entrenar el modelo durante 100 etapas y evaluar de nuevo la precisión del modelo y la entropía cruzada. ¿Ha mejorado el modelo?

    # Definimos el número de épocas de entrenamiento.
    red.epochs = 100
    # Actualizamos la máquina de aprendizaje con el nuevo modelo.
    evaluate!(mach, resampling = Holdout(fraction_train = 0.7, rng = 123), measure = [accuracy, cross_entropy])
    PerformanceEvaluation object with these fields:
      model, measure, operation,
      measurement, per_fold, per_observation,
      fitted_params_per_fold, report_per_fold,
      train_test_rows, resampling, repeats
    Extract:
    ┌───┬──────────────────────┬──────────────┬─────────────┐
    │   │ measure              │ operation    │ measurement │
    ├───┼──────────────────────┼──────────────┼─────────────┤
    │ A │ Accuracy()           │ predict_mode │ 0.99        │
    │ B │ LogLoss(             │ predict      │ 0.0577      │
    │   │   tol = 2.22045e-16) │              │             │
    └───┴──────────────────────┴──────────────┴─────────────┘
    

    El modelo ha mejorado enormemente y ahora la precisión del modelo es casi del 100%. Esto indica que el modelo se ha ajustado muy bien a los datos de entrenamiento y es capaz de predecir a los datos de prueba casi a la perfección.

  14. Volver a repetir el proceso de entrenamiento y evaluación del modelo con validación cruzada de 10 pliegues y calcular la precisión del modelo y la entropía cruzada.

    evaluate!(mach, resampling = CV(nfolds = 10), measure = [accuracy, cross_entropy])
    Evaluating over 10 folds:  20%[=====>                   ]  ETA: 0:00:01Evaluating over 10 folds:  70%[=================>       ]  ETA: 0:00:00Evaluating over 10 folds:  90%[======================>  ]  ETA: 0:00:00Evaluating over 10 folds: 100%[=========================] Time: 0:00:00
    PerformanceEvaluation object with these fields:
      model, measure, operation,
      measurement, per_fold, per_observation,
      fitted_params_per_fold, report_per_fold,
      train_test_rows, resampling, repeats
    Extract:
    ┌───┬──────────────────────┬──────────────┬─────────────┐
    │   │ measure              │ operation    │ measurement │
    ├───┼──────────────────────┼──────────────┼─────────────┤
    │ A │ Accuracy()           │ predict_mode │ 0.985       │
    │ B │ LogLoss(             │ predict      │ 0.0712      │
    │   │   tol = 2.22045e-16) │              │             │
    └───┴──────────────────────┴──────────────┴─────────────┘
    ┌───┬───────────────────────────────────────────────────────────────────────────
    │   │ per_fold                                                                 ⋯
    ├───┼───────────────────────────────────────────────────────────────────────────
    │ A │ [1.0, 1.0, 0.941, 1.0, 1.0, 1.0, 1.0, 1.0, 0.971, 0.941]                 ⋯
    │ B │ [0.00305, 0.00192, 0.287, 0.0255, 0.00442, 0.00154, 2.9e-5, 0.000122, 0. ⋯
    └───┴───────────────────────────────────────────────────────────────────────────
                                                                   2 columns omitted
    
  15. Volver a repetir el proceso de entrenamiento y evaluación del modelo con validación cruzada tomando distintas tasas de aprendizaje. ¿Para qué tasa de aprendizaje se obtiene el mejor modelo? ¿Cuál es la precisión del modelo y la entropía cruzada?

    Usar la función range del paquete MLJ para definir un rango etapas. La función range permite definir un rango de valores para un parámetro del modelo.

    # Definimos un rango de tasas de aprendizaje.
    r = range(modelo, :(neural_network_classifier.epochs), lower=1, upper=100, scale=:log)
    # Obtenemos las precisiones para cada número de etapas.
    _, _, etapas, entropia = learning_curve(mach, range = r, resampling = CV(nfolds = 10), measure = cross_entropy)
    using GLMakie
    fig = Figure()
    ax = Axis(fig[1, 1], title = "Precisión del modelo con distintas etapas de entrenamiento", xlabel = "Etapas", ylabel = "Precisión")
    lines!(ax, etapas, entropia)
    display(fig)
    # Obtenemos el número de etapas con la mejor precisión.
    etapas_optimas = etapas[argmin(entropia)]
    println("Número de etapas óptimas: ", etapas_optimas)
    [ Info: Training machine(ProbabilisticTunedModel(model = ProbabilisticPipeline(standardizer = Standardizer(features = Symbol[], …), …), …), …).
    [ Info: Attempting to evaluate 24 models.
    Evaluating over 24 metamodels:   8%[==>                      ]  ETA: 0:00:10Evaluating over 24 metamodels:  12%[===>                     ]  ETA: 0:00:06Evaluating over 24 metamodels:  17%[====>                    ]  ETA: 0:00:05Evaluating over 24 metamodels:  21%[=====>                   ]  ETA: 0:00:04Evaluating over 24 metamodels:  25%[======>                  ]  ETA: 0:00:03Evaluating over 24 metamodels:  29%[=======>                 ]  ETA: 0:00:03Evaluating over 24 metamodels:  33%[========>                ]  ETA: 0:00:02Evaluating over 24 metamodels:  38%[=========>               ]  ETA: 0:00:02Evaluating over 24 metamodels:  42%[==========>              ]  ETA: 0:00:02Evaluating over 24 metamodels:  46%[===========>             ]  ETA: 0:00:02Evaluating over 24 metamodels:  50%[============>            ]  ETA: 0:00:01Evaluating over 24 metamodels:  54%[=============>           ]  ETA: 0:00:01Evaluating over 24 metamodels:  58%[==============>          ]  ETA: 0:00:01Evaluating over 24 metamodels:  62%[===============>         ]  ETA: 0:00:01Evaluating over 24 metamodels:  67%[================>        ]  ETA: 0:00:01Evaluating over 24 metamodels:  71%[=================>       ]  ETA: 0:00:01Evaluating over 24 metamodels:  75%[==================>      ]  ETA: 0:00:01Evaluating over 24 metamodels:  79%[===================>     ]  ETA: 0:00:01Evaluating over 24 metamodels:  83%[====================>    ]  ETA: 0:00:01Evaluating over 24 metamodels:  88%[=====================>   ]  ETA: 0:00:00Evaluating over 24 metamodels:  92%[======================>  ]  ETA: 0:00:00Evaluating over 24 metamodels:  96%[=======================> ]  ETA: 0:00:00Evaluating over 24 metamodels: 100%[=========================] Time: 0:00:04
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238
    Número de etapas óptimas: 39
  16. Entrenar de nuevo el modelo con todo el conjunto de ejemplos y con el número de etapas óptimas, y predecir la especie de los 5 primeros pingüinos del conjunto de ejemplos.

    # Definimos el número de épocas de entrenamiento.
    red.epochs = etapas_optimas
    # Entrenamos el modelo con todo el conjunto de ejemplos.
    fit!(mach)
    # Predecimos la especie de los 5 primeros pingüinos del conjunto de ejemplos.
    predict_mode(mach, X[1:5, :])
    [ Info: Training machine(ProbabilisticPipeline(standardizer = Standardizer(features = Symbol[], …), …), …).
    [ Info: Training machine(:standardizer, …).
    [ Info: Training machine(:neural_network_classifier, …).
    [ Info: MLJFlux: converting input data to Float32
    Optimising neural net:   5%[=>                       ]  ETA: 0:00:00Optimising neural net:   8%[=>                       ]  ETA: 0:00:00Optimising neural net:  10%[==>                      ]  ETA: 0:00:00Optimising neural net:  12%[===>                     ]  ETA: 0:00:00Optimising neural net:  15%[===>                     ]  ETA: 0:00:00Optimising neural net:  18%[====>                    ]  ETA: 0:00:00Optimising neural net:  20%[=====>                   ]  ETA: 0:00:00Optimising neural net:  22%[=====>                   ]  ETA: 0:00:00Optimising neural net:  25%[======>                  ]  ETA: 0:00:00Optimising neural net:  28%[======>                  ]  ETA: 0:00:00Optimising neural net:  30%[=======>                 ]  ETA: 0:00:00Optimising neural net:  32%[========>                ]  ETA: 0:00:00Optimising neural net:  35%[========>                ]  ETA: 0:00:00Optimising neural net:  38%[=========>               ]  ETA: 0:00:00Optimising neural net:  40%[==========>              ]  ETA: 0:00:00Optimising neural net:  42%[==========>              ]  ETA: 0:00:00Optimising neural net:  45%[===========>             ]  ETA: 0:00:00Optimising neural net:  48%[===========>             ]  ETA: 0:00:00Optimising neural net:  50%[============>            ]  ETA: 0:00:00Optimising neural net:  52%[=============>           ]  ETA: 0:00:00Optimising neural net:  55%[=============>           ]  ETA: 0:00:00Optimising neural net:  58%[==============>          ]  ETA: 0:00:00Optimising neural net:  60%[===============>         ]  ETA: 0:00:00Optimising neural net:  62%[===============>         ]  ETA: 0:00:00Optimising neural net:  65%[================>        ]  ETA: 0:00:00Optimising neural net:  68%[================>        ]  ETA: 0:00:00Optimising neural net:  70%[=================>       ]  ETA: 0:00:00Optimising neural net:  72%[==================>      ]  ETA: 0:00:00Optimising neural net:  75%[==================>      ]  ETA: 0:00:00Optimising neural net:  78%[===================>     ]  ETA: 0:00:00Optimising neural net:  80%[====================>    ]  ETA: 0:00:00Optimising neural net:  82%[====================>    ]  ETA: 0:00:00Optimising neural net:  85%[=====================>   ]  ETA: 0:00:00Optimising neural net:  88%[=====================>   ]  ETA: 0:00:00Optimising neural net:  90%[======================>  ]  ETA: 0:00:00Optimising neural net:  92%[=======================> ]  ETA: 0:00:00Optimising neural net:  95%[=======================> ]  ETA: 0:00:00Optimising neural net:  98%[========================>]  ETA: 0:00:00Optimising neural net: 100%[=========================] Time: 0:00:00
    5-element CategoricalArrays.CategoricalArray{String15,1,UInt32}:
     String15("Adelie")
     String15("Adelie")
     String15("Adelie")
     String15("Adelie")
     String15("Adelie")