Entrenar modelos con CoreML3 en iOS

0
403

Índice de contenidos

  • 1. Introducción
  • 2. Entorno
  • 3. Probando nuestro modelo
  • 4. Entrenando nuestro modelo
  • 5. Conclusiones

 

1. Introducción

Machine Learning es una tecnología cada vez más demandada en el mundo del desarrollo, desde que salió iOS11 en 2017 tenemos la posibilidad de incorporar a nuestras aplicaciones iOS modelos entrenados de Machine learning, esto se podía hacer a través del framework de Apple CoreML. La limitación que teníamos en ese punto es que no podíamos entrenar a nuestros modelos en el propio dispositivo, únicamente podíamos evolucionar nuestro modelo «fuera» y actualizar la aplicación con el nuevo modelo. Puedes ver un ejemplo de implantación en este tutorial.

En 2018 Apple se alió con IBM para poder entrenar los modelos mediante la interacción de CoreML con Watson Services, básicamente CoreML enviaba el feedback a Watson para que éste se entrenase y Watson enviaba el modelo nuevo a CoreML. Los inconvenientes de esta forma de entrenamiento eran que dependíamos de conexión a internet para entrenar los modelos y por supuesto la privacidad, ya que el modelo no está únicamente en nuestro dispositivo.

Con la llegada de iOS13 , coreML3 y el avance de los procesadores que llevan los dispositivos, ya por fin tenemos la capacidad de entrenar a nuestro modelos en el dispositivo. Si lo pensamos, con FaceId ya se estaba haciendo esta función, ya que el dispositivo va aprendiendo a reconocer nuestra cara y con el tiempo va siendo más exacto sin tener que enviar el modelo fuera. CoreML 3 ya nos ofrece una API para entrenar los modelos, vamos a ver cómo hacerlo con una aplicación que reconozca tipos de animales.

 

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i9 de 8 núcleos, 32GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.4
  • Entorno de desarrollo: XCode 11.4.1

 

3. Conociendo el contexto

Para hacer uso de CoreML necesitas primero un modelo previamente entrenado que luego podrás refinar tú mediante el entrenamiento. El formato específico de Apple es mlmodel. Existen muchos modelos ya creados por la comunidad para su descarga, nosotros vamos a usar un modelo creado con la herramienta Create ML que la tenemos disponibles en las herramientas de Xcode:

En esta herramienta podemos crear distintos tipos de modelo, os invito a investigarla ya que podremos hacer nuestros propios modelos de forma rápida y fácil, no obstante, no podremos crear modelos actualizables, para ello tendremos que crearla con alguna herramienta externa (TensorFlow, Keras…) y exportarla como CoreMLModel vía coreMLTools.

Vamos a descargar el proyecto inicial desde donde partir nuestro tutorial aquí.

Como hemos comentado antes, vamos a crear una app que se encargue  de reconocer perros y gatos. Una vez descargado vamos a ver que tenemos un proyecto con dos pestañas en el tabbar:

Test: el controlador FirstViewController es el que corresponde a esta pantalla. En él se muestran imágenes de perros y gatos, si pulsamos en next(), cargará una nueva imagen, que de forma aleatoria será de perros o gatos. Aquí vamos a ver cómo funciona nuestro modelo de forma genérica.

Train: El controlador SecondViewController es la clase que corresponde a esta pantalla, aquí vamos a mostrar de nuevo un carrousel de imágenes aleatorias de perros y gatos y es donde vamos a entrenar a nuestro modelo indicándole a qué tipo de animal pertenece.

 

4. Probando nuestro modelo

Ahora vamos a ver como funciona el modelo, en primer lugar vamos a crear un nuevo fichero en Swift llamado MLManager y vamos a escribir lo siguiente:

import UIKit
import Vision

class MLManager {
    
    var model = CatDogUpdatable()

    func predict(image: UIImage) -> String? {
        do{
            let imageOptions: [MLFeatureValue.ImageOption: Any] = [.cropAndScale: VNImageCropAndScaleOption.scaleFill.rawValue]
            let featureValue = try MLFeatureValue(cgImage: image.cgImage!, constraint: model.imageConstraint, options: imageOptions)
            return model.predictLabelFor(featureValue)
        }catch(let error){
            return "\(error.localizedDescription)"
        }
    }
}

extension CatDogUpdatable {
    
    var imageConstraint: MLImageConstraint {
        return model.modelDescription.inputDescriptionsByName["image"]!.imageConstraint!
    }
    
    func predictLabelFor(_ value: MLFeatureValue) -> String? {
      guard
        let pixelBuffer = value.imageBufferValue,
          let prediction = try? prediction(image: pixelBuffer).classLabel
        else {
           return nil
      }
      if prediction == "unknown" {
        print("No prediction found")
        return nil
      }
      return prediction
    }
}

Lo que hemos hecho aquí es crear un manager como capa de abstracción de CoreML, contiene una referencia a nuestro modelo de datos y una función predict() que nos va devolver una predicción de la imagen que le pasemos. También hemos creado una extensión de nuestro modelo que nos devuelva la información necesaria para la predicción, y las constrains de la imagen, que tratamos en el modelo, veamos las clases involucradas:

MLFeatureValue es la clase que se encarga de empaquetar los datos que entran y salen del modelo. Los valores que obtenemos como predicción y los que enviamos para entrenarlo han de ser de este tipo.

MLImageConstraints es la clase que se encarga de informar del tamaño de las imágenes que se tratan en el modelo de datos.

Para continuar nos vamos a nuestro FirstViewController y creamos una variable de nuestro MLManager

    let mlManager = MLManager()

Y en la función next() escribimos lo siguiente:

@IBAction func next(_ sender: Any?) {
     self.imageView.image = nil
     self.textLabel.text = ""
     imageProvider.animalImage(type: .random, onCompletion: { image in
         self.imageView.image = image
         self.textLabel.text = self.mlManager.predict(image: image)
     })
}

 

Únicamente hemos asignado a nuestro Label el valor de la predicción, si compilamos y ejecutamos vamos a empezar a obtener predicciones:

Como podemos ver, no siempre acierta, pero vamos a intentar arreglar eso 😉

 

5. Entrenando nuestro modelo

Vamos a empezar a entrenar a entrenar a nuestro modelo, para ello nos vamos a ir a nuestra clase MLManager y vamos a añadir la siguiente función:

private func trainingData(image: UIImage, animalType: Animal ) throws -> MLBatchProvider {

}

Veamos paso a paso lo que va hacer esta función:

En primer lugar creamos un array vacío de tipo MLDictionaryFeatureProvider, que es donde vamos a almacenar los valores de entrada y salida de nuestro modelo:

var featureProviders: [MLDictionaryFeatureProvider] = []

Después creamos las variables necesarias para identificar los inputs y los outputs de nuestro modelo así como las opciones de la imagen que vamos a tratar:

let inputName = "image"
let outputName = "classLabel"
let imageOptions: [MLFeatureValue.ImageOption: Any] = [
     .cropAndScale: VNImageCropAndScaleOption.scaleFill.rawValue
]

Ahora crearemos los valores de entrada y salida:

let inputValue = try MLFeatureValue(cgImage: image.cgImage!, constraint: model.imageConstraint, options: imageOptions)
let outputValue = MLFeatureValue(string: animalType.rawValue)

Un vez creamos estos valores vamos a envolverlos en un MLDictionaryFeatureProvider de la siguiente forma:

let dataPointFeatures: [String: MLFeatureValue] = [inputName: inputValue, outputName: outputValue]
if let provider = try? MLDictionaryFeatureProvider(dictionary: dataPointFeatures) {
     featureProviders.append(provider)
}

Por último, vamos a devolver una instancia MLArrayBatchProvider que es una hija de MLBatchProvider, ésta clase simplemente representa una colección de MLFeatureProviders que como hemos comentado antes es lo que necesita el modelo para su entrenamiento.

Ahora vamos a crear la función con la tarea de entrenamiento, añadimos lo siguiente:

private func updateModel(with trainingData: MLBatchProvider, completionHandler: @escaping (MLUpdateContext) -> Void) {
        do {
            let updateTask = try MLUpdateTask(forModelAt: CatDogUpdatable.urlOfModelInThisBundle, trainingData: trainingData, configuration: nil, completionHandler: completionHandler)
            updateTask.resume()
        } catch (let error){
            print("Could't create an MLUpdateTask. \(error)")
        }
}

Aquí hemos creado una tarea de actualización mediante el constructor de la clase MLUpdateTask en el que le pasamos la URL de nuestro modelo, los datos de entrenamiento (que van a ser lo que hemos creado en la anterior función), y le pasamos un closure en el que vamos a recibir el contexto de la actualización, en ésta vamos a poder ver si la actualización del modelo ha sido válida.

La última función que vamos a crear en nuestro manager es la de guardado como tal:

func saveImage(_ image: UIImage, animalType: Animal, completionHandler: @escaping () -> Void) {
     do {
            let trainingData = try self.trainingData(image: image, animalType: animalType)
            DispatchQueue.global(qos: .userInitiated).async {
                self.updateModel(with: trainingData) { context in
                    print(context)
                    DispatchQueue.main.async { completionHandler() }
                }
            }
        } catch {
          print("Error updating model", error)
        }
 }

Aquí hemos obtenido nuestros datos de entrenamiento llamando a la función trainingData() que hemos escrito anteriormente y a continuación llamamos de forma asíncrona la función de actualización del modelo updateModel(), cuando ésta ejecute el closure obtendremos el contexto que hemos mencionado antes.

Si ahora nos vamos a nuestra clase SecondViewController en la función tag() escribimos lo siguiente:

@IBAction func tag(_ sender: Any) {
     if let button = sender as? UIButton {
            let animalType: Animal = button.tag == 1 ? .dog : .cat
            mlManager.saveImage(imageView.image!, animalType: animalType) {
                self.next(nil)
            }
        }
}

No nos olvidemos de añadir nuestra referencia al MLManager:

let mlManager = MLManager()

Si compilamos y ejecutamos ya podremos ir a la pantalla Train, y empezar a entrenar nuestro modelo. Cuando identifiquemos alguna podremos ver en las trazas que nuestro modelo ha sido actualizado:

model: 0x0, 
event: (
    "Training End"
), 
metrics: {
}, 
parameters: {
}

 

6. Conclusiones

Existen muchas posibles aplicaciones de la tecnología Machine Learning, por desgracia, en las aplicaciones móviles, no hay muchos desarrolladores que hagan uso de ellas, pero como posibles ejemplos, podríamos predecir comportamientos de navegación entre pantallas de nuestra app, mejorar algoritmos de predicción de sueño, predecir que artículos prefiere leer el usuario, saber que items suele comprar…etc.

Lo que se puede comprobar en este tutorial es que con una implementación muy sencilla podremos hacer uso de esta maravillosa tecnología.

Puedes descargar el proyecto finalizado aquí.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad