paint-brush
Tareas de aprendizaje automático en Swift sin Python, redes neuronales y bibliotecasby@pichukov
552
552

Tareas de aprendizaje automático en Swift sin Python, redes neuronales y bibliotecas

Aleksei Pichukov15m2024/01/04
Read on Terminal Reader

Este artículo no es una guía sobre cómo escribir algo en Swift; más bien, es más bien una reflexión sobre la mentalidad actual de muchos desarrolladores que ven a Python como un puente hacia la solución definitiva para las bibliotecas de aprendizaje automático que resolverá cualquier problema o tarea que encuentren, independientemente del lenguaje que estén utilizando. Apostaría a que la mayoría de los desarrolladores prefieren invertir su tiempo en encontrar formas de integrar las bibliotecas de Python en su lenguaje/entorno en lugar de considerar soluciones alternativas sin ellas. Si bien esto no es intrínsecamente malo (la reutilización ha sido un importante impulsor del progreso en TI durante las últimas décadas), he comenzado a sentir que muchos desarrolladores ya ni siquiera consideran soluciones alternativas. Esta mentalidad se vuelve aún más arraigada con el estado actual y los avances en los modelos de lenguajes grandes. Tomaremos una tarea de ML clásica y la resolveremos utilizando lenguaje Swift y sin bibliotecas.
featured image - Tareas de aprendizaje automático en Swift sin Python, redes neuronales y bibliotecas
Aleksei Pichukov HackerNoon profile picture
0-item
1-item

Las redes neuronales están a la vanguardia del aprendizaje automático (ML) en la actualidad, y Python es sin duda el lenguaje de programación ideal para cualquier tarea de ML, independientemente de si se pretende utilizar redes neuronales para resolverla o no. Existe una amplia gama de bibliotecas de Python disponibles que cubren todo el espectro de tareas de aprendizaje automático, como NumPy, Pandas, Keras, TensorFlow, PyTorch, etc. Estas bibliotecas generalmente dependen de implementaciones C o C++ de algoritmos y enfoques de ML internos porque Python es demasiado lento para ellas. Sin embargo, Python no es el único lenguaje de programación que existe y no es el que uso en mi trabajo diario.


Este artículo no es una guía sobre cómo escribir algo en Swift; más bien, es más bien una reflexión sobre la mentalidad actual de muchos desarrolladores que ven a Python como un puente hacia la solución definitiva para las bibliotecas de aprendizaje automático que resolverá cualquier problema o tarea que encuentren, independientemente del lenguaje que estén utilizando. Apostaría a que la mayoría de los desarrolladores prefieren invertir su tiempo en encontrar formas de integrar las bibliotecas de Python en su lenguaje/entorno en lugar de considerar soluciones alternativas sin ellas. Si bien esto no es intrínsecamente malo (la reutilización ha sido un importante impulsor del progreso en TI durante las últimas décadas), he comenzado a sentir que muchos desarrolladores ya ni siquiera consideran soluciones alternativas. Esta mentalidad se vuelve aún más arraigada con el estado actual y los avances en los modelos de lenguajes grandes.


Falta el equilibrio; Nos apresuramos a pedirle a los LLM que resuelvan nuestros problemas, obtengan algo de código Python, lo copien y disfruten de nuestra productividad con una sobrecarga potencialmente significativa debido a dependencias innecesarias.


Exploremos un enfoque alternativo para resolver la tarea en cuestión utilizando solo Swift, matemáticas y ninguna otra herramienta.


Cuando la gente empieza a aprender redes neuronales, hay dos ejemplos clásicos de Hello World que puedes encontrar en la mayoría de los tutoriales y materiales introductorios. El primero es el reconocimiento de dígitos escritos a mano. El segundo es una clasificación de datos. Me centraré en el segundo en este artículo, pero la solución que explicaré también funcionará para el primero.


El muy buen ejemplo visual se puede encontrar en TensorFlow Playground, donde puedes jugar con diferentes estructuras de redes neuronales y observar visualmente qué tan bien el modelo resultante resuelve la tarea.


Ejemplo de zona de juegos de TensorFlow


Quizás te preguntes cuál es el significado práctico de estos puntos en una imagen con diferentes colores. La cuestión es que es una representación visual de algunos conjuntos de datos. Puede presentar muchos tipos diferentes de datos exactamente igual o similar, como grupos sociales de personas que compran productos específicos o preferencias musicales. Dado que me centro principalmente en el desarrollo de iOS móvil, también daré un ejemplo de una tarea real que estaba resolviendo y que se puede representar visualmente de manera similar: encontrar cables eléctricos dentro de las paredes usando un giroscopio y un magnetómetro en un teléfono móvil. En este ejemplo particular, tenemos un conjunto de parámetros relacionados con el cable encontrado y otro conjunto de parámetros para nada que esté dentro de la pared.


Echemos un vistazo a los datos que usaremos.

Prueba ML


Aquí tenemos dos tipos de datos: puntos rojos y puntos azules. Como describí anteriormente, puede ser una representación visual de cualquier tipo de datos clasificados. Por ejemplo, tomemos el área roja como aquella donde tenemos una señal del magnetómetro y giroscopio en los casos en que tenemos un cable eléctrico en la pared, y el área azul en caso de que no lo tengamos.


Podemos ver que estos puntos están agrupados de alguna manera y forman una especie de formas rojas y azules. La forma en que se generaron estos puntos es tomando puntos aleatorios de la siguiente imagen:


Puntos agrupados


Usaremos esta imagen como modelo aleatorio para nuestro proceso de entrenamiento tomando puntos aleatorios para entrenar el modelo y otros puntos aleatorios para probar nuestro modelo entrenado.


La imagen original mide 300 x 300 píxeles y contiene 90.000 puntos (puntos). Para fines de capacitación, usaremos solo el 0,2% de estos puntos, que es menos de 100 puntos. Para comprender mejor el rendimiento del modelo, seleccionaremos aleatoriamente 3000 puntos y dibujaremos círculos alrededor de ellos en la imagen. Esta representación visual nos proporcionará una idea más completa de los resultados. También podemos medir el porcentaje de precisión para verificar la eficiencia del modelo.


¿Cómo vamos a hacer un modelo? Si miramos estas dos imágenes juntas e intentamos simplificar nuestra tarea, descubriremos que la tarea, de hecho, es recrear la imagen de Origen a partir de los datos que tenemos (un lote de puntos rojos y azules). Y cuanto más cercana sea la imagen que obtengamos de nuestro modelo a la original, más preciso será nuestro modelo. También podemos considerar nuestros datos de prueba como una especie de versión extremadamente comprimida de nuestra imagen original y tener el objetivo de descomprimirla nuevamente.


Lo que vamos a hacer es transformar nuestros puntos en funciones matemáticas que se representarán en el código como matrices o vectores (usaré el término vector aquí en el texto solo porque está entre función del mundo de las matemáticas y matriz del desarrollo de software). Luego, usaremos estos vectores para desafiar cada punto de prueba e identificar a qué vector pertenece más.


Para transformar nuestros datos, probaré una transformada de coseno discreta (DCT). No entraré en explicaciones matemáticas sobre qué es y cómo funciona, ya que puedes encontrar esa información fácilmente si lo deseas. Sin embargo, puedo explicar en términos sencillos cómo puede ayudarnos y por qué es útil. El DCT se utiliza en muchas áreas, incluida la compresión de imágenes (como el formato JPEG). Transforma los datos a un formato más compacto manteniendo sólo las partes importantes de la imagen y eliminando los detalles sin importancia. Si aplicamos la DCT a nuestra imagen de 300x300 que contiene solo puntos rojos, obtendremos una matriz de valores de 300x300 que se puede transformar en una matriz (o vector) tomando cada fila por separado.


Finalmente escribamos algo de código para ello. Primero, necesitamos crear un objeto simple que represente nuestro punto (punto).


 enum Category { case red case blue case none } struct Point: Hashable { let x: Int let y: Int let category: Category }


Puede notar que tenemos una categoría adicional llamada none . De hecho, al final crearemos tres vectores: uno para los puntos red , el segundo para los puntos blue y el tercero para cualquier otra cosa que esté representada por none . Si bien podríamos tener solo dos de ellos, tener un vector entrenado para que no sea rojo ni azul simplificará un poco las cosas.


Tenemos que `Point` se ajusta al protocolo Hashable para usar un Set para evitar tener puntos con las mismas coordenadas en nuestro vector de prueba.


 func randomPoints(from points: [Point], percentage: Double) -> [Point] { let count = Int(Double(points.count) * percentage) var result = Set<Point>() while result.count < count { let index = Int.random(in: 0 ..< points.count) result.insert(points[index]) } return Array<Point>(result) }


Ahora podemos usarlo para tomar 0.2% de puntos aleatorios de nuestra imagen original para puntos rojos, azules y ninguno.


 redTrainPoints = randomPoints(from: redPoints, percentage: 0.002) blueTrainPoints = randomPoints(from: bluePoints, percentage: 0.002) noneTrainPoints = randomPoints(from: nonePoints, percentage: 0.002)


Estamos listos para transformar estos datos de entrenamiento usando DCT. Aquí hay una implementación del mismo:


 final class CosTransform { private var sqrtWidthFactorForZero: Double = 0 private var sqrtWidthFactorForNotZero: Double = 0 private var sqrtHeightFactorForZero: Double = 0 private var sqrtHeightFactorForNotZero: Double = 0 private let cosLimit: Int init(cosLimit: Int) { self.cosLimit = cosLimit } func discreteCosTransform(for points: [Point], width: Int, height: Int) -> [[Double]] { if sqrtWidthFactorForZero == 0 { prepareSupportData(width: width, height: height) } var result = Array(repeating: Array(repeating: Double(0), count: width), count: height) for y in 0..<height { for x in 0..<width { let cos = cosSum( points: points, width: width, height: height, x: x, y: y ) result[y][x] = cFactorHeight(index: y) * cFactorWidth(index: x) * cos } } return result } func shortArray(matrix: [[Double]]) -> [Double] { let height = matrix.count guard let width = matrix.first?.count else { return [] } var array: [Double] = [] for y in 0..<height { for x in 0..<width { if y + x <= cosLimit { array.append(matrix[y][x]) } } } return array } private func prepareSupportData(width: Int, height: Int) { sqrtWidthFactorForZero = Double(sqrt(1 / CGFloat(width))) sqrtWidthFactorForNotZero = Double(sqrt(2 / CGFloat(width))) sqrtHeightFactorForZero = Double(sqrt(1 / CGFloat(height))) sqrtHeightFactorForNotZero = Double(sqrt(2 / CGFloat(height))) } private func cFactorWidth(index: Int) -> Double { return index == 0 ? sqrtWidthFactorForZero : sqrtWidthFactorForNotZero } private func cFactorHeight(index: Int) -> Double { return index == 0 ? sqrtHeightFactorForZero : sqrtHeightFactorForNotZero } private func cosSum( points: [Point], width: Int, height: Int, x: Int, y: Int ) -> Double { var result: Double = 0 for point in points { result += cosItem(point.x, x, height) * cosItem(point.y, y, width) } return result } private func cosItem( _ firstParam: Int, _ secondParam: Int, _ lenght: Int ) -> Double { return cos((Double(2 * firstParam + 1) * Double(secondParam) * Double.pi) / Double(2 * lenght)) } }


Creemos una instancia del objeto CosTransform y probémosla.


 let math = CosTransform(cosLimit: Int.max) ... redCosArray = cosFunction(points: redTrainPoints) blueCosArray = cosFunction(points: blueTrainPoints) noneCosArray = cosFunction(points: noneTrainPoints)


Usamos algunas funciones auxiliares simples aquí:


 func cosFunction(points: [Point]) -> [Double] { return math.shortArray( matrix: math.discreteCosTransform( for: points, width: 300, height: 300 ) ) }


Hay un parámetro cosLimit en CosTransform que se usa dentro de la función shortArray. Explicaré su propósito más adelante, por ahora ignorémoslo y verifiquemos el resultado de 3000 puntos aleatorios de la imagen original con nuestros vectores creados redCosArray , blueCosArray y noneCosArray . Para que funcione, necesitamos crear otro vector DCT a partir de un único punto tomado de la imagen original. Esto lo hacemos exactamente de la misma manera y usando las mismas funciones que ya hicimos para nuestros vectores Red , Blue y None cos. Pero ¿cómo podemos saber a cuál pertenece este nuevo vector? Existe un enfoque matemático muy simple para ello: Dot Product . Dado que tenemos la tarea de comparar dos vectores y encontrar el par más similar, Producto escalable nos dará exactamente esto. Si aplica una operación de Producto escalar para dos vectores idénticos, obtendrá un valor positivo que será mayor que cualquier otro resultado de Producto escalar aplicado al mismo vector y a cualquier otro vector que tenga valores diferentes. Y si aplica un producto escalar a los vectores ortogonales (vectores que no tienen nada en común entre sí), obtendrá un 0 como resultado. Teniendo esto en cuenta, podemos llegar a un algoritmo simple:


  1. Revise todos nuestros 3000 puntos aleatorios uno por uno.
  2. Cree un vector a partir de una matriz de 300x300 con un solo punto usando DCT (Transformada de coseno discreta).
  3. Aplique un producto escalar para este vector con redCosArray , luego con blueCosArray y luego con noneCosArray .
  4. El mejor resultado del paso anterior nos indicará la respuesta correcta: Red , Blue , None .


La única funcionalidad que falta aquí es un producto punto, escribamos una función simple para él:


 func dotProduct(_ first: [Double], _ second: [Double]) -> Double { guard first.count == second.count else { return 0 } var result: Double = 0 for i in 0..<first.count { result += first[i] * second[i] } return result }


Y aquí hay una implementación del algoritmo:


 var count = 0 while count < 3000 { let index = Int.random(in: 0 ..< allPoints.count) let point = allPoints[index] count += 1 let testArray = math.shortArray( matrix: math.discreteCosTransform( for: [point], width: 300, height: 300 ) ) let redResult = dotProduct(redCosArray, testArray) let blueResult = dotProduct(blueCosArray, testArray) let noneResult = dotProduct(noneCosArray, testArray) var maxValue = redResult var result: Category = .red if blueResult > maxValue { maxValue = blueResult result = .blue } if noneResult > maxValue { maxValue = noneResult result = .none } fillPoints.append(Point(x: point.x, y: point.y, category: result)) }


Todo lo que tenemos que hacer ahora es dibujar una imagen desde fillPoints . Echemos un vistazo a los puntos del tren que hemos utilizado, los vectores DCT que hemos creado a partir de los datos de nuestro tren y el resultado final que obtuvimos:

Resultado

Bueno, parece un ruido aleatorio. Pero echemos un vistazo a la representación visual de los vectores. Puede ver algunos picos allí, esa es exactamente la información en la que debemos centrarnos y eliminar la mayor parte del ruido de nuestro resultado DCT. Si echamos un vistazo a la representación visual simple de la matriz DCT, encontraremos que la información más útil (la que describe las características únicas de la imagen) se concentra en la esquina superior izquierda:


Concentración


Ahora demos un paso atrás y verifiquemos la función shortArray una vez más. Usamos un parámetro cosLimit aquí exactamente por el motivo de tomar la esquina superior izquierda de la matriz DCT y usar solo los parámetros más activos que hacen que nuestro vector sea único.


 func shortArray(matrix: [[Double]]) -> [Double] { let height = matrix.count guard let width = matrix.first?.count else { return [] } var array: [Double] = [] for y in 0..<height { for x in 0..<width { if y + x <= cosLimit { array.append(matrix[y][x]) } } } return array }


Creemos nuestro objeto math con diferentes cosLimit :


 let math = CosTransform(cosLimit: 30)


Ahora, en lugar de usar los 90.000 valores, usaremos solo 30 x 30 / 2 = 450 de ellos de la esquina superior izquierda de la matriz DCT. Echemos un vistazo al resultado que hemos obtenido:

Resultado

Como puedes ver, ya está mejor. También podemos observar que la mayoría de los picos que hacen que los vectores sean únicos todavía están ubicados en la parte frontal (como se selecciona en verde en la imagen), intentemos usar CosTransform(cosLimit: 6) , lo que significa que usaremos solo 6 x 6 / 2 = 18 valores de 90.000 y comprueba el resultado:

Éxito

Está mucho mejor ahora, muy cerca de la imagen original. Sin embargo, sólo hay un pequeño problema: esta implementación es lenta. No sería necesario ser un experto en complejidad de algoritmos para darse cuenta de que DCT es una operación que requiere mucho tiempo, pero incluso el producto escalar, que tiene una complejidad de tiempo lineal, no es lo suficientemente rápido cuando se trabaja con vectores grandes usando matrices Swift. La buena noticia es que podemos hacerlo de forma mucho más rápida y sencilla en su implementación utilizando vDSP del framework Accelerate de Apple, que ya tenemos como biblioteca estándar. Puede leer sobre vDSP aquí , pero en palabras simples, es un conjunto de métodos para la ejecución de tareas de procesamiento de señales digitales de una manera muy rápida. Tiene muchas optimizaciones de bajo nivel internas que funcionan perfectamente con grandes conjuntos de datos. Implementemos nuestro producto punto y DCT usando vDSP :


 infix operator • public func •(left: [Double], right: [Double]) -> Double { return vDSP.dot(left, right) } prefix operator ->> public prefix func ->>(value: [Double]) -> [Double] { let setup = vDSP.DCT(count: value.count, transformType: .II) return setup!.transform(value.compactMap { Float($0) }).compactMap { Double($0) } }


Para hacerlo menos tedioso, he usado algunos operadores para hacerlo más legible. Ahora puedes utilizar estas funciones de la siguiente manera:


 let cosRedArray = ->> redValues let redResult = redCosArray • testArray


Hay un problema con la nueva implementación de DCT con respecto al tamaño de nuestra matriz actual. No funcionaría con nuestra imagen de 300 x 300 ya que está optimizada para funcionar con tamaños específicos que son potencias de 2. Por lo tanto, necesitaremos hacer un esfuerzo para escalar la imagen antes de aplicar el nuevo método.

Resumen

Gracias a cualquiera que haya logrado leer este texto hasta ahora o haya tenido la pereza de hojearlo sin leerlo. El propósito de este artículo fue mostrar que muchas tareas que la gente no considera resolver con algunos instrumentos nativos se pueden resolver con un mínimo esfuerzo. Es divertido buscar soluciones alternativas y no limitarse a la integración de la biblioteca Python como la única opción para resolver este tipo de tareas.