
-
Novedades de BNNS Graph
BNNS Graph Builder API ahora permite a los desarrolladores escribir gráficos de operaciones usando el lenguaje familiar Swift para generar rutinas de preprocesamiento y posprocesamiento, así como pequeños modelos de aprendizaje automático. BNNS compila gráficos antes de la ejecución y admite casos de uso en tiempo real y sensibles a la latencia, como el procesamiento de audio. En esta sesión, revisamos el ejemplo de bit-crusher del año pasado y simplificamos el componente Swift eliminando la dependencia de un archivo Python separado y, en su lugar, implementamos el efecto de audio completamente en Swift. La API BNNS Graph Builder también es adecuada para preprocesar datos de imágenes antes de pasar esos datos a un modelo de aprendizaje automático. La sesión también incluye una demostración de cómo recortar los pixeles transparentes de una imagen con un canal alfa.
Capítulos
- 0:00 - Introducción
- 3:12 - Resumen de BNNSGraph
- 6:15 - BNNSGraphBuilder
- 16:58 - Uso de BNNSGraphBuilder
Recursos
Videos relacionados
WWDC25
WWDC24
-
Buscar este video…
Hola, soy Simon Gladman y trabajo para el grupo de vectores y números en Apple. El grupo brinda un conjunto de bibliotecas para realizar cálculos complejos y eficientes en el CPU. Estas bibliotecas admiten procesamiento de imágenes y señales, álgebra lineal y lo que voy a tratar hoy: aprendizaje automático. Las subrutinas de redes neuronales básicas, o BNNS, es la biblioteca del aprendizaje automático. Te permite agregar inferencia basada en CPU a la app y se adapta perfectamente a casos de baja latencia y en tiempo real, como el procesamiento de audio. Podrías usarlo para implementar funcionalidades como: separar el audio para aislar o eliminar voces, segmentar audio en regiones según el contenido o aplicar transferencia de timbre para hacer que un instrumento suene como otro.
Pero BNNS es perfectamente adecuado para trabajar con otros datos, como imágenes. Entonces, si la app requiere inferencia de alto rendimiento, BNNS es para ti.
El año pasado presentamos BNNSGraph, una nueva y emocionante API que hizo a BNNS más rápido, eficiente energéticamente y fácil de trabajar. Y si recuerdas, demostramos BNNSGraph creando esta unidad de audio Bitcrusher
que muestra lo fácil que es agregar tus propios efectos a Logic Pro y Garage Band.
Este año le damos a BNNSGraph una mejor interfaz de programación, que permite aprovechar el poder de Swift para crear pequeños modelos para inferencia y gráficos de operaciones para pre y posprocesamiento. Hablaré sobre nuestra nueva incorporación a BNNSGraph, BNNSGraphBuilder. Comenzaré con un rápido resumen de BNNSGraph. Qué es, cómo puede optimizar el aprendizaje automático y los modelos de IA y cómo puedes adoptarlo. Luego, presentaré BNNSGraphBuilder. Resumiré el flujo de trabajo muy fácil que necesitas para implementarlo y, como parte de esta introducción, demostraré cómo escribir un gráfico simple. Después de presentar BNNSGraphBuilder, haré tres demostraciones. En primer lugar, preprocesamos una imagen mediante Graph Builder. Luego, usamos la nueva API para posprocesar los datos generados por un modelo de aprendizaje automático. Finalmente actualizaré la demostración de Bitcrusher del año pasado para construir el gráfico de operaciones en Swift. Y, sin más preámbulos, comencemos. Recomendamos BNNSGraph para audio y otros casos de uso a la latencia porque brinda control sobre la asignación de memoria y el subprocesamiento múltiple. Ambos pueden generar un cambio de contexto en el código del kernel y conducir a la derrota de los plazos en tiempo real. Antes, se construían redes basadas en BNNS utilizando capas discretas como convolución, multiplicación de matrices o activación. Si quisieras usar BNNS para implementar un modelo existente, tendrías que codificar cada capa y escribir el código para todos los tensores intermedios. BNNSGraph toma un gráfico completo que consta de múltiples capas y el flujo de datos entre esas capas y lo consume como un solo objeto gráfico.
Esto significa que no tienes que escribir el código para cada capa individual. Además, tú y tus usuarios se beneficiarán de un rendimiento más rápido y una mejor eficiencia energética.
Debido a que BNNSGraph conoce el modelo completo, puede realizar una serie de optimizaciones que antes no eran posibles. Estas reducen el tiempo de ejecución, el uso de memoria y el costo de energía. Y lo que es mejor, son gratis. Utilicemos esta sección de un modelo para explorar algunas de las optimizaciones. Una de las optimizaciones es una transformación matemática. Por ejemplo, reordenar una operación de corte de modo que BNNS solo necesita calcular el subconjunto de elementos en ese corte. Otra optimización es la fusión de capas. Por ejemplo, fusionar una capa de convolución y una capa de activación en una sola operación. La elisión evita copiar los datos en un porcionamiento haciendo referencia a ese subconjunto de datos en su lugar. Al garantizar que los tensores compartan memoria cuando sea posible, BNNSGraph optimiza el uso de la memoria y elimina asignaciones innecesarias. Finalmente, las optimizaciones de reempaquetado de peso de BNNSGraph pueden reempaquetar pesos para el caché. No es necesario escribir código para beneficiarse de estas optimizaciones. Suceden así sin más. Para crear un gráfico usando la API basada en archivos que presentamos el año pasado, debes comenzar con un paquete CoreML. Xcode compila automáticamente el paquete en un archivo mlmodelc y luego escribes el código que crea un gráfico a partir del archivo mlmodelc. El último paso es crear un contexto para envolver el gráfico y es ese contexto el que realiza la inferencia. Este enfoque sigue siendo la mejor manera de integrar modelos de PyTorch existentes en tu proyecto. Sin embargo, para modelos pequeños o gráficos de operaciones, puede ser beneficioso un flujo de trabajo más inmediato. ¿Qué pasaría si pudieras definir los elementos individuales de tu gráfico en Swift? Este año presentamos una nueva API que hace exactamente eso: BNNSGraphBuilder.
BNNSGraphBuilder te permite escribir gráficos de operaciones utilizando el conocido lenguaje Swift. Puedes escribir rutinas de pre y posprocesamiento o pequeños modelos de aprendizaje automático en Swift. Creas un contexto a partir de ese código Swift con una única llamada de función y sin pasos intermedios. Escribir los gráficos en Swift y en línea con el otro código trae algunos beneficios inmediatos. Usa un lenguaje familiar y el gráfico se verifica cuando se compila el proyecto Swift. Puedes compartir valores entre Swift y el gráfico que se conocen en tiempo de ejecución pero antes de generar el gráfico. Por ejemplo, si conoces la forma de un tensor en tiempo de ejecución antes de inicializar el gráfico, puedes pasar la forma directamente al código del gráfico y beneficiarte del mejor rendimiento sobre los tamaños flexibles. BNNSGraphBuilder te permite consultar tensores intermedios para obtener propiedades como la forma y el tipo de datos y esto te ayuda a depurar gráficos. Los tensores de tipo permiten que Xcode se autocomplete y reducen la posibilidad de errores de tiempo de ejecución. Veamos la nueva sintaxis, verificación de tipos, nuevos métodos y operadores y cómo consultar tensores intermedios, todas funcionalidades que antes no estaban. Este año agregamos un nuevo método de tipo al objeto BNNSGraph, llamado makeContext. Este nuevo método es el que convierte tu código Swift en un contexto reutilizable que usas para ejecutar el gráfico. Solo se crea el contexto una vez, normalmente mientras se inicia la app. La app ejecuta el contexto cuando es necesario y se beneficia de las optimizaciones que aporta el tratamiento holístico del gráfico. Un poco de codificación en vivo para ver makeContext en acción. Este método acepta un cierre y es ese cierre el que se usa para definir la serie de operaciones que constituyen el gráfico.
Tu gráfico normalmente aceptará uno o más argumentos. Usaré el parámetro constructor del cierre para especificar los dos argumentos, x e y.
Los parámetros son ambos vectores de ocho elementos con valores flotantes. El gráfico realiza una multiplicación de cada elemento en los dos argumentos
y escribe el resultado en el argumento de salida que llamé producto. Mi gráfico calcula el valor medio del producto y escribe ese valor en un segundo argumento de salida que llamé media y, finalmente, el código devuelve esos dos argumentos de salida.
Para ayudar a la depuración, podemos imprimir detalles de los tensores intermedios y de salida que genera BNNSGraphBuilder.
En este ejemplo, imprimí la forma de la media para asegurar que tenga forma de uno, es decir, que solo contenga un elemento. Una vez creado el contexto, puedes consultarlo para derivar los argumentos y sus posiciones. Aquí, el código consulta los nombres de los argumentos del contexto y crea una matriz de tensores que representan las entradas y salidas del gráfico. BNNSGraph ordena los nombres de los argumentos para que las salidas sean primeras y las entradas después.
La API BNNSGraphBuilder proporciona un amplio conjunto de funciones para inicializar argumentos de diferentes fuentes de datos y copiar o referenciar datos. En este ejemplo, el código inicializa las entradas de las matrices de Swift.
Con los argumentos de entrada y salida asignados, llamar a la función de ejecución ejecuta el gráfico y llena las dos salidas con los resultados.
Podemos imprimir los resultados ejecutando la función.
Pero no estamos limitados a simplemente multiplicar y calcular promedios. Hagamos un recorrido rápido por BNNSGraphBuilder y exploremos algunas de las otras operaciones que ofrece la nueva API. Esta es solo una pequeña selección de las primitivas incluidas en BNNSGraphBuilder. Por ejemplo, multiplicación y convolución de matrices, operaciones de reducción, de reunión y dispersión y como relleno, remodelación y transposición. La nueva API admite muchas operaciones como operadores simples y Swift. Tenemos operadores aritméticos: multiplicación, suma, resta y división. Tenemos operadores de comparación elemento por elemento, incluidos igualdad, menor que y mayor que. Para completar el conjunto, tenemos operadores lógicos elemento por elemento. Bien, esa es una presentación rápida de la API en sí. Analicemos profundamente dos funcionalidades de la API GraphBuilder muy familiares para los desarrolladores de Swift, comenzando con el tipado fuerte. El tipado fuerte te ayuda a detectar errores en tiempo de compilación que de otro modo podrían ocurrir en tiempo de ejecución. La API BNNSGraphBuilder garantiza que el tipo de datos de los tensores sea correcto para una operación determinada. Veámoslo en acción. En este ejemplo, el gráfico devuelve los resultados en cada elemento de valor base de punto flotante elevados a la potencia de exponentes enteros.
Es posible realizar este cálculo convirtiendo los exponentes enteros a FP16. Intentar realizar esto sin la conversión impediría que el método de contexto se compile. El gráfico enmascara el resultado poniendo a cero los elementos cuyos valores en la máscara 0 son menores que los correspondientes en la máscara 1. Debido a que los elementos tensoriales que genera la comparación son booleanos, el gráfico realiza otra conversión para multiplicar los resultados por la condición. Una vez más, sin la operación de conversión, el método de creación de contexto no se compilaría. Siempre es mejor detectar este tipo de error en el momento de la compilación y antes de que la app esté en manos de los usuarios. Esto es tipado fuerte en acción. Miremos el enfoque de BNNSGraphBuilder para porcionar, es decir, seleccionar partes con tensor. Esto será muy familiar para los desarrolladores de Swift.
Un porcionamiento es efectivamente una ventana hacia una parte de un tensor. Por ejemplo, un porcionamiento puede ser una sola columna o fila de una matriz. Lo bueno de BNNSGraph es que trata las secciones como referencias a datos existentes, sin la sobrecarga de copiar esos datos o asignar más memoria. Porcionar tensores es una operación común. La nueva API GraphBuilder te permite especificar partes de un tensor como operaciones de origen o destino. Por ejemplo, es posible que desees recortar una imagen en una región de interés antes de pasarla a un modelo de aprendizaje automático. Veamos lo fácil que es utilizar la API GraphBuilder para seleccionar una región cuadrada del centro de esta fotografía de una ardilla disfrutando de su almuerzo. Definiremos dos buffers de píxeles vImage. Estos almacenan en las imágenes datos de píxeles, dimensiones, profundidad de bits y número de canales. El primer buffer, fuente, contiene una fotografía de la ardilla. El segundo búfer de píxeles, destino, contendrá el recorte cuadrado. Cada búfer tiene tres canales, rojo, verde y azul y cada canal tiene un formato de punto flotante de 32 bits. Puedes obtener más información sobre los búferes de píxeles en la documentación de vImage. Los márgenes horizontales y verticales garantizan que el recorte de 640 x 640 esté centrado en la imagen de origen. Aquí hay un gráfico que usa la operación de porcionamiento para el recorte. Primero, definiremos la fuente como un argumento y especificaremos la altura y el ancho como tamaños flexibles. Pasar un valor negativo, menos 1 en este caso, le indica a BNNSGraph que esas dimensiones pueden ser cualquier valor. El valor final en la forma 3 especifica que la imagen contiene tres canales: rojo, verde y azul. La API BNNSGraphBuilder usa subíndices Swift para realizar una operación de porcionamiento. En este ejemplo, el código usa la nueva estructura SliceRange. Los índices de inicio para las dimensiones verticales y horizontales son los valores de margen correspondientes. Establecer los índices finales como valor de margen negativo indica que el índice final es el valor final de esa dimensión menos el margen.
En este ejemplo, no queremos recortar a lo largo de la dimensión del canal y el código especifica fillAll para garantizar que incluyamos los tres canales.
Este año presentamos un nuevo método para los buffers de píxeles vImage. El método withBNNSTensor te permite crear un BNNSTensor temporal que comparte memoria y propiedades como el tamaño y la cantidad de canales con el búfer de píxeles. Como demuestra el código aquí, el nuevo método hace que pasar imágenes hacia y desde tu gráfico sea muy fácil. Y como la memoria es compartida, no hay copia ni asignación, y eso significa que obtienes un gran rendimiento al trabajar con imágenes. Después de que el método de función regresa, el búfer de píxeles contiene una imagen recortada. Podemos comprobar que la ardilla esté bien recortada creando una imagen Core Graphics del resultado. Además de la nueva estructura de rango de porcionamiento de GraphBuilder, la API de porcionamiento de tensor admite todos los tipos de rango de Swift. Sean cuales sean tus necesidades de porcionamiento, tenemos lo que necesitas. Esa es una breve descripción general del porcionamiento. Ya que vimos BNNSGraphBuilder y algunas de sus funcionalidades, profundicemos en algunos casos de uso. BNNSGraphBuilder es una gran API para hacer gráficos para datos de pre y posprocesamiento que pasas o recibes de modelos de aprendizaje automático. Una técnica de preprocesamiento es aplicar un umbral a una imagen. Consiste en convertir una imagen de tono continuo en una imagen binaria que contenga solo píxeles negros o blancos. Veamos cómo podemos implementar esto usando GraphBuilder.
Antes de comenzar a escribir el gráfico, definiremos nuevamente algunos buffers vImage. El primero almacena la imagen de origen y el segundo es el destino que recibe la imagen con umbral. Ambos buffers son de canal único y formato de punto flotante de 16 bits. El primer paso en el gráfico es definir la fuente, que representa la imagen de entrada. El código pasa valores negativos para el tamaño de la imagen para especificar que el gráfico admite cualquier tamaño, pero especifica el tipo de datos FP16 que coincide con los búferes. Calcular el valor promedio de los píxeles continuos en escala de grises es simplemente una cuestión de llamar al método de la media. El operador “el elemento es mayor que” llena el tensor con umbral con unos para los píxeles correspondientes del valor mayor que el valor de píxel promedio y con ceros en caso contrario. Y el gráfico convierte los unos y ceros booleanos en la profundidad de bits del búfer de píxeles de destino. Utilizaremos nuevamente el método withBNNSTensor para pasar los dos buffers de imagen al contexto y ejecutar la función para generar la imagen con umbral.
Después de pasar esta imagen en escala de grises de tonos continuos de un par de pelícanos en vuelo al gráfico, obtenemos esta imagen donde todos los píxeles son negros o blancos. Otro caso de uso de la API BNNSGraphBuilder es el posprocesamiento de los resultados de un modelo de aprendizaje automático.
Apliquemos una función softmax seguida de una operación topK a los resultados de un modelo de aprendizaje automático. La función de posprocesamiento crea un pequeño gráfico sobre la marcha. El gráfico declara un argumento de entrada basado en el tensor de entrada de la función. Aplica una operación softmax al argumento de entrada y luego calcula sus valores topK e índices. El parámetro k que el código pasa a la función topK en realidad está definido fuera del cierre del contexto de creación. Por último, el gráfico devuelve los resultados topK. Después de definir el gráfico, la función declara los tensores que almacenan los resultados y pasa los tensores de entrada y salida al contexto. Finalmente, el método makeArray convierte los valores e índices topK en matrices Swift y los devuelve. Allí tenemos algunos ejemplos de pre y posprocesamiento de datos con la API GraphBuilder. Veamos la demostración de Bitcrusher del año pasado y actualicemos la pieza Swift para usar BNNSGraphBuilder. Esto demuestra cómo BNNSGraph hace que agregar efectos de audio basados en aprendizaje automático sea muy fácil.
El año pasado, demostramos BNNSGraph incorporándolo a una app de extensión de unidad de audio. Esta app agregó un efecto Bitcrusher en tiempo real a una señal de audio y la demostración utilizó Swift para aplicar el mismo efecto a una onda sinusoidal para mostrar una representación visual del efecto en la IU. Por ejemplo, dada esta onda sinusoidal, el gráfico puede reducir la resolución para agregar distorsión al sonido y la representación visual muestra la onda sinusoidal como una serie discreta de pasos en lugar de una onda continua. Incluimos ganancia de saturación para agregar riqueza al sonido. La demostración del año pasado utilizó un paquete CoreML que estaba basado en el código de PyTorch. Entonces, tomemos el código del año pasado y comparémoslo con la misma funcionalidad escrita en Swift usando la nueva API GraphBuilder. Aquí está PyTorch a la izquierda y el nuevo código Swift a la derecha. Una diferencia inmediata es que Swift usa let y var para definir tensores intermedios, por lo que eres libre de decidir si estos son inmutables o mutables. No necesitas ninguna importación adicional para acceder a la gama completa de operaciones disponibles. Además, operaciones como tanh son métodos sobre tensores en lugar de funciones libres.
Los operadores aritméticos elemento por elemento son los mismos con los dos enfoques.
Sin embargo, otra diferencia es que el cierre del contexto puede devolver más de un tensor de salida, por lo que siempre se devuelve una matriz. Swift GraphBuilder te permite definir argumentos de entrada dentro del cierre del contexto de creación. Si tienes un modelo de PyTorch existente, aún recomendamos usar ese código y la API basada en archivos existente. Pero, para proyectos nuevos, nuestra nueva API GraphBuilder te permite escribir gráficos usando Swift y, como demuestra esta comparación, con una estructura similar al mismo conjunto de operaciones escritas usando PyTorch. Podemos demostrar el soporte de 16 bits de BNNSGraph y cómo, en este ejemplo, es significativamente más rápido que las operaciones FP32. Este es el código Swift, pero esta vez usando un alias de tipo para especificar la precisión.
Al cambiar el alias de tipo, el gráfico cambia para utilizar FP16 en lugar de FP32. En este ejemplo, FP16 es mucho más rápido que su equivalente FP32. BNNSGraphBuilder ofrece una API Swift fácil de usar que te permite escribir modelos y gráficos de operaciones de alto rendimiento y eficiencia energética. Es ideal para casos de uso en tiempo real y sensibles a la latencia, pero su interfaz de programación fácil de usar significa que se puede utilizar para una amplia gama de apps. Antes de irme, permíteme sugerirte que visites la página de documentación, donde encontrarás mucho material de referencia que analiza BNNSGraph y BNNSGraphBuilder. Asegúrate de ver el video del año pasado, donde hablo sobre la API basada en archivos y el uso de BNNSGraph en C.
Muchas gracias por tu tiempo y te deseo lo mejor.
-
-
8:31 - Introduction to BNNSGraphBuilder
import Accelerate func demo() throws { let context = try BNNSGraph.makeContext { builder in let x = builder.argument(name: "x", dataType: Float.self, shape: [8]) let y = builder.argument(name: "y", dataType: Float.self, shape: [8]) let product = x * y let mean = product.mean(axes: [0], keepDimensions: true) // Prints "shape: [1] | stride: [1]". print("mean", mean) return [ product, mean] } var args = context.argumentNames().map { name in return context.tensor(argument: name, fillKnownDynamicShapes: false)! } // Output arguments args[0].allocate(as: Float.self, count: 8) args[1].allocate(as: Float.self, count: 1) // Input arguments args[2].allocate( initializingFrom: [1, 2, 3, 4, 5, 6, 7, 8] as [Float]) args[3].allocate( initializingFrom: [8, 7, 6, 5, 4, 3, 2, 1] as [Float]) try context.executeFunction(arguments: &args) // [8.0, 14.0, 18.0, 20.0, 20.0, 18.0, 14.0, 8.0] print(args[0].makeArray(of: Float.self)) // [15.0] print(args[1].makeArray(of: Float.self)) args.forEach { $0.deallocate() } }
-
12:04 - Strong typing
// Performs `result = mask0 .< mask1 ? bases.pow(exponents) : 0 let context = try BNNSGraph.makeContext { builder in let mask0 = builder.argument(dataType: Float16.self, shape: [-1]) let mask1 = builder.argument(dataType: Float16.self, shape: [-1]) let bases = builder.argument(dataType: Float16.self, shape: [-1]) let exponents = builder.argument(dataType: Int32.self, shape: [-1]) // `mask` contains Boolean values. let mask = mask0 .< mask1 // Cast integer exponents to FP16. var result = bases.pow(y: exponents.cast(to: Float16.self)) result = result * mask.cast(to: Float16.self) return [result] }
-
14:15 - Slicing
let srcImage = #imageLiteral(resourceName: "squirrel.jpeg").cgImage( forProposedRect: nil, context: nil, hints: nil)! var cgImageFormat = vImage_CGImageFormat( bitsPerComponent: 32, bitsPerPixel: 32 * 3, colorSpace: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(alpha: .none, component: .float, byteOrder: .order32Host))! let source = try vImage.PixelBuffer(cgImage: srcImage, cgImageFormat: &cgImageFormat, pixelFormat: vImage.InterleavedFx3.self) let cropSize = 640 let horizontalMargin = (source.width - cropSize) / 2 let verticalMargin = (source.height - cropSize) / 2 let destination = vImage.PixelBuffer(size: .init(width: cropSize, height: cropSize), pixelFormat: vImage.InterleavedFx3.self) let context = try BNNSGraph.makeContext { builder in let src = builder.argument(name: "source", dataType: Float.self, shape: [ -1, -1, 3]) let result = src [ BNNSGraph.Builder.SliceRange(startIndex: verticalMargin, endIndex: -verticalMargin), BNNSGraph.Builder.SliceRange(startIndex: horizontalMargin, endIndex: -horizontalMargin), BNNSGraph.Builder.SliceRange.fillAll ] return [result] } source.withBNNSTensor { src in destination.withBNNSTensor { dst in var args = [dst, src] print(src) print(dst) try! context.executeFunction(arguments: &args) } } let result = destination.makeCGImage(cgImageFormat: cgImageFormat)
-
17:31 - Preprocessing by thresholding on mean
let srcImage = #imageLiteral(resourceName: "birds.jpeg").cgImage( forProposedRect: nil, context: nil, hints: nil)! var cgImageFormat = vImage_CGImageFormat( bitsPerComponent: 16, bitsPerPixel: 16, colorSpace: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder16Little.rawValue | CGBitmapInfo.floatComponents.rawValue | CGImageAlphaInfo.none.rawValue))! let source = try! vImage.PixelBuffer<vImage.Planar16F>(cgImage: srcImage, cgImageFormat: &cgImageFormat) let destination = vImage.PixelBuffer<vImage.Planar16F>(size: source.size) let context = try BNNSGraph.makeContext { builder in let src = builder.argument(name: "source", dataType: Float16.self, shape: [-1, -1, 1]) let mean = src.mean(axes: [0, 1], keepDimensions: false) let thresholded = src .> mean let result = thresholded.cast(to: Float16.self) return [result] } source.withBNNSTensor { src in destination.withBNNSTensor { dst in var args = [dst, src] try! context.executeFunction(arguments: &args) } } let result = destination.makeCGImage(cgImageFormat: cgImageFormat)
-
19:04 - Postprocessing
func postProcess(result: BNNSTensor, k: Int) throws -> ([Float32], [Int32]) { let context = try BNNSGraph.makeContext { builder in let x = builder.argument(dataType: Float32.self, shape: [-1]) let softmax = x.softmax(axis: 1) let topk = softmax.topK(k, axis: 1, findLargest: true) return [topk.values, topk.indices] } let indices = context.allocateTensor(argument: context.argumentNames()[0], fillKnownDynamicShapes: false)! let values = context.allocateTensor(argument: context.argumentNames()[1], fillKnownDynamicShapes: false)! var arguments = [values, indices, result] try context.executeFunction(arguments: &arguments) return (values.makeArray(of: Float32.self), indices.makeArray(of: Int32.self)) }
-
21:03 - Bitcrusher in PyTorch
import coremltools as ct from coremltools.converters.mil import Builder as mb from coremltools.converters.mil.mil import ( get_new_symbol ) import torch import torch.nn as nn import torch.nn.functional as F class BitcrusherModel(nn.Module): def __init__(self): super(BitcrusherModel, self).__init__() def forward(self, source, resolution, saturationGain, dryWet): # saturation destination = source * saturationGain destination = F.tanh(destination) # quantization destination = destination * resolution destination = torch.round(destination) destination = destination / resolution # mix destination = destination * dryWet destination = 1.0 - dryWet source = source * dryWet destination = destination + source return destination
-
21:03 - Bitcrusher in Swift
typealias BITCRUSHER_PRECISION = Float16 let context = try! BNNSGraph.makeContext { builder in var source = builder.argument(name: "source", dataType: BITCRUSHER_PRECISION.self, shape: [sampleCount, 1, 1]) let resolution = builder.argument(name: "resolution", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) let saturationGain = builder.argument(name: "saturationGain", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) var dryWet = builder.argument(name: "dryWet", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) // saturation var destination = source * saturationGain destination = destination.tanh() // quantization destination = destination * resolution destination = destination.round() destination = destination / resolution // mix destination = destination * dryWet dryWet = BITCRUSHER_PRECISION(1) - dryWet source = source * dryWet destination = destination + source return [destination] }
-
22:34 - Changing precision
typealias BITCRUSHER_PRECISION = Float16 let context = try! BNNSGraph.makeContext { builder in var source = builder.argument(name: "source", dataType: BITCRUSHER_PRECISION.self, shape: [sampleCount, 1, 1]) let resolution = builder.argument(name: "resolution", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) let saturationGain = builder.argument(name: "saturationGain", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) var dryWet = builder.argument(name: "dryWet", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) // saturation var destination = source * saturationGain destination = destination.tanh() // quantization destination = destination * resolution destination = destination.round() destination = destination / resolution // mix destination = destination * dryWet dryWet = BITCRUSHER_PRECISION(1) - dryWet source = source * dryWet destination = destination + source return [destination] }
-
-
- 0:00 - Introducción
El Grupo Vector y Numerics de Apple desarrolló BNNS, una biblioteca de aprendizaje automático para inferencia basada en CPU en apps, particularmente útil para el procesamiento de audio e imágenes en tiempo real. BNNSGraph, presentado el año pasado, mejoró la velocidad, la eficiencia y la facilidad de uso. Ahora, se agregó BNNSGraphBuilder, que permite la creación basada en Swift de pequeños modelos y gráficos para preprocesamiento y posprocesamiento. Esto se demuestra con el preprocesamiento de imágenes, el posprocesamiento de datos y la actualización de la muestra de la unidad de audio Bitcrusher.
- 3:12 - Resumen de BNNSGraph
BNNSGraph se recomienda para tareas de audio y baja latencia con plazos en tiempo real porque permite el control de la memoria y de múltiples subprocesos, mejorando el rendimiento en tiempo real. Anteriormente, BNNS requería codificar cada capa manualmente, pero ahora BNNSGraph toma gráficos completos como objetos, lo que mejora el rendimiento y la eficiencia energética mediante la combinación de transformaciones matemáticas, fusión de capas, elisión de copias y uso compartido de memoria tensorial. BNNSGraph ofrece dos flujos de trabajo principales: usando un paquete CoreML y una compilación Xcode, o definiendo el gráfico directamente en Swift para modelos más pequeños con BNNSGraphBuilder.
- 6:15 - BNNSGraphBuilder
Una nueva API llamada BNNSGraphBuilder permite a los desarrolladores construir gráficos de operaciones directamente en Swift. Esto permite la creación de rutinas de preprocesamiento y posprocesamiento, y pequeños modelos de aprendizaje automático dentro del código Swift. Los beneficios incluyen el uso de un lenguaje familiar, verificación de tipos durante la compilación y la capacidad de compartir valores de tiempo de ejecución entre Swift y el gráfico, lo que genera un mejor rendimiento. La API también proporciona funcionalidades de depuración, como la consulta de tensores intermedios para propiedades como la forma y el tipo de datos, y habilita el autocompletado de Xcode. Un nuevo método de tipo, makeContext, convierte el código Swift en un contexto reutilizable para la ejecución de gráficos. El contexto se crea una vez durante el inicio de la app y luego se puede ejecutar varias veces, beneficiándose de las optimizaciones de gráficos holísticos. La API ofrece una amplia gama de operaciones matemáticas y lógicas, así como soporte para primitivas de redes neuronales comunes, como multiplicación de matrices, convolución y operaciones de reducción. La API BNNSGraphBuilder en Swift aprovecha la tipificación fuerte para detectar errores en el momento de la compilación, lo que garantiza la corrección del tipo de datos para las operaciones tensoriales. Esto se demuestra mediante la conversión automática de tipos de datos, como números enteros, a FP16, lo que evita errores de compilación y mejora la confiabilidad de la app. La API también trata los cortes tensoriales como referencias a datos existentes, optimizando el uso de la memoria. El corte de tensores se realiza mediante subíndices Swift y la nueva estructura SliceRange, lo que lo hace intuitivo para los desarrolladores de Swift. Esto permite operaciones eficientes como el recorte de imágenes, como se muestra en un ejemplo donde se recorta una región cuadrada de una fotografía de una ardilla usando buffers de píxeles vImage y el método withBNNSTensor, que comparte memoria para un mejor rendimiento.
- 16:58 - Uso de BNNSGraphBuilder
BNNSGraphBuilder es una potente API en Swift que te ayuda a construir gráficos de operaciones para el preprocesamiento y posprocesamiento eficiente de datos en apps de aprendizaje automático que usan audio e imágenes. Para el preprocesamiento, usa la API para establecer el umbral de las imágenes y convertir las imágenes de tono continuo en imágenes binarias. También puedes usarlo para tareas de posprocesamiento, como la app de funciones softmax y operaciones topK a los resultados de los modelos de aprendizaje automático. La API demuestra su versatilidad al aplicarse también a efectos de audio. Permite crear efectos de audio en tiempo real como bitcrushing, con importantes mejoras de rendimiento al utilizar una precisión de 16 bits en comparación con 32 bits. BNNSGraphBuilder ofrece una interfaz de programación fácil de usar, lo que lo hace accesible para una amplia gama de apps, desde casos de uso en tiempo real y sensibles a la latencia hasta tareas de aprendizaje automático de propósito general.