paint-brush
Tarefas de ML em Swift sem Python, redes neurais e bibliotecasby@pichukov
552
552

Tarefas de ML em Swift sem Python, redes neurais e bibliotecas

Aleksei Pichukov15m2024/01/04
Read on Terminal Reader

Este artigo não é um guia sobre como escrever algo em Swift; em vez disso, é mais como uma reflexão sobre a mentalidade atual de muitos desenvolvedores que veem o Python como uma ponte para a solução definitiva para bibliotecas de ML que resolverá qualquer problema ou tarefa que encontrarem, independentemente da linguagem que estejam usando. Eu apostaria que a maioria dos desenvolvedores prefere investir seu tempo em encontrar maneiras de integrar bibliotecas Python em sua linguagem/ambiente, em vez de considerar soluções alternativas sem elas. Embora isso não seja inerentemente ruim – a reutilização tem sido um impulsionador significativo do progresso em TI nas últimas décadas – comecei a sentir que muitos desenvolvedores nem sequer consideram soluções alternativas. Essa mentalidade se torna ainda mais arraigada com o estado atual e os avanços nos grandes modelos de linguagem. Pegaremos uma tarefa clássica de ML e a resolveremos usando a linguagem Swift e sem bibliotecas.
featured image - Tarefas de ML em Swift sem Python, redes neurais e bibliotecas
Aleksei Pichukov HackerNoon profile picture
0-item
1-item

As redes neurais estão na vanguarda do aprendizado de máquina (ML) hoje, e Python é, sem dúvida, a linguagem de programação ideal para qualquer tarefa de ML, independentemente de se pretender usar redes neurais para resolvê-la ou não. Há uma vasta gama de bibliotecas Python disponíveis que cobrem todo o espectro de tarefas de ML, como NumPy, Pandas, Keras, TensorFlow, PyTorch e assim por diante. Essas bibliotecas geralmente dependem de implementações C ou C++ de algoritmos e abordagens de ML nos bastidores porque Python é muito lento para elas. No entanto, Python não é a única linguagem de programação que existe e não é aquela que utilizo no meu trabalho diário.


Este artigo não é um guia sobre como escrever algo em Swift; em vez disso, é mais como um pensamento sobre a mentalidade atual de muitos desenvolvedores que veem o Python como uma ponte para a solução definitiva para bibliotecas de ML que resolverá qualquer problema ou tarefa que encontrarem, independentemente da linguagem que estiverem usando. Eu apostaria que a maioria dos desenvolvedores prefere investir seu tempo em encontrar maneiras de integrar bibliotecas Python em sua linguagem/ambiente, em vez de considerar soluções alternativas sem elas. Embora isso não seja inerentemente ruim – a reutilização tem sido um impulsionador significativo do progresso em TI nas últimas décadas – comecei a sentir que muitos desenvolvedores nem sequer consideram soluções alternativas. Essa mentalidade se torna ainda mais arraigada com o estado atual e os avanços nos grandes modelos de linguagem.


Falta o equilíbrio; estamos correndo para pedir aos LLMs que resolvam nossos problemas, obtendo algum código Python, copiando-o e aproveitando nossa produtividade com sobrecarga potencialmente significativa de dependências desnecessárias.


Vamos explorar uma abordagem alternativa para resolver a tarefa em questão usando apenas Swift, matemática e nenhuma outra ferramenta.


Quando as pessoas começam a aprender Redes Neurais, há dois exemplos clássicos do Hello World que você pode encontrar na maioria dos tutoriais e materiais introdutórios. O primeiro é um reconhecimento de dígitos manuscritos. A segunda é uma classificação de dados. Vou me concentrar no segundo neste artigo, mas a solução que apresentarei funcionará para o primeiro também.


Um exemplo visual muito bom disso pode ser encontrado no TensorFlow Playground, onde você pode brincar com diferentes estruturas de redes neurais e observar visualmente quão bem o modelo resultante resolve a tarefa.


Exemplo do TensorFlow Playground


Você pode perguntar qual é o significado prático desses pontos em uma imagem com cores diferentes? O problema é que é uma representação visual de alguns conjuntos de dados. Você pode apresentar muitos tipos diferentes de dados exatamente iguais ou semelhantes, como grupos sociais de pessoas que compram produtos específicos ou preferências musicais. Como meu foco principal é o desenvolvimento de iOS móvel, também darei um exemplo de uma tarefa real que estava resolvendo e que pode ser representada visualmente de maneira semelhante: encontrar fios elétricos dentro de paredes usando um giroscópio e um magnetômetro em um telefone celular. Neste exemplo específico, temos um conjunto de parâmetros relacionados ao fio encontrado e outro conjunto de parâmetros para nada estar dentro da parede.


Vamos dar uma olhada nos dados que usaremos.

Teste ML


Temos dois tipos de dados aqui: pontos vermelhos e pontos azuis. Como descrevi acima, pode ser uma representação visual de qualquer tipo de dado classificado. Por exemplo, vamos considerar a área vermelha como aquela onde temos sinal do magnetômetro e giroscópio nos casos em que temos um fio elétrico na parede, e a área azul caso não tenhamos.


Podemos ver que esses pontos estão agrupados de alguma forma e formam algum tipo de forma vermelha e azul. A forma como esses pontos foram gerados foi retirando pontos aleatórios da imagem a seguir:


Pontos agrupados


Usaremos esta imagem como um modelo aleatório para nosso processo de treinamento, pegando pontos aleatórios para treinar o modelo e outros pontos aleatórios para testar nosso modelo treinado.


A imagem original tem 300 x 300 pixels, contendo 90.000 pontos (pontos). Para fins de treinamento, usaremos apenas 0,2% desses pontos, o que equivale a menos de 100 pontos. Para entender melhor o desempenho do modelo, selecionaremos aleatoriamente 3.000 pontos e desenharemos círculos ao redor deles na imagem. Esta representação visual nos dará uma ideia mais abrangente dos resultados. Também podemos medir o percentual de precisão para verificar a eficiência do modelo.


Como vamos fazer um modelo? Se olharmos essas duas imagens juntas e tentarmos simplificar nossa tarefa, descobriremos que a tarefa, na verdade, é recriar a imagem da Origem a partir dos dados que temos (lote de pontos vermelhos e azuis). E quanto mais próxima a imagem que obtivermos do nosso modelo do original, mais preciso será o funcionamento do nosso modelo. Também podemos considerar nossos dados de teste como uma espécie de versão extremamente compactada de nossa imagem original e ter o objetivo de descompactá-los novamente.


O que vamos fazer é transformar nossos pontos em funções matemáticas que serão representadas no código como arrays ou vetores (usarei o termo vetor aqui no texto apenas porque está entre função do mundo matemático e array do desenvolvimento de software). Em seguida, usaremos esses vetores para desafiar cada ponto de teste e identificar a qual vetor ele pertence mais.


Para transformar nossos dados, tentarei uma Transformada Discreta de Cosseno (DCT). Não entrarei em explicações matemáticas sobre o que é e como funciona, pois você pode facilmente encontrar essa informação se desejar. No entanto, posso explicar em termos simples como isso pode nos ajudar e por que é útil. O DCT é usado em muitas áreas, incluindo compactação de imagens (como formato JPEG). Ele transforma os dados em um formato mais compacto, mantendo apenas as partes importantes da imagem e removendo os detalhes sem importância. Se aplicarmos o DCT à nossa imagem 300x300 contendo apenas pontos vermelhos, obteremos uma matriz 300x300 de valores que pode ser transformada em uma matriz (ou vetor) considerando cada linha separadamente.


Vamos finalmente escrever algum código para isso. Primeiro, precisamos criar um objeto simples que representará o nosso ponto (ponto).


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


Você pode notar que temos uma categoria adicional chamada none . Na verdade, criaremos três vetores no final: um para pontos red , o segundo para pontos blue e o terceiro para qualquer outra coisa que seja representada por none . Embora pudéssemos ter apenas dois deles, ter um vetor treinado para não vermelho e nem azul tornará as coisas um pouco mais simples.


Temos `Point` em conformidade com o protocolo Hashable para usar um Set para evitar pontos com as mesmas coordenadas em nosso vetor de teste.


 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) }


Agora podemos usá-lo para obter 0.2% de pontos aleatórios de nossa imagem original para pontos vermelhos, azuis e nenhum ponto.


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


Estamos prontos para transformar esses dados de treinamento usando DCT. Aqui está uma implementação disso:


 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)) } }


Vamos criar uma instância do objeto CosTransform e testá-la.


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


Usamos algumas funções auxiliares simples aqui:


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


Existe um parâmetro cosLimit em CosTransform que é usado dentro da função shortArray, explicarei o propósito dele mais tarde, por enquanto vamos ignorá-lo e verificar o resultado de 3000 pontos aleatórios da imagem original em relação aos nossos vetores criados redCosArray , blueCosArray e noneCosArray . Para que funcione, precisamos criar outro vetor DCT a partir de um único ponto retirado da imagem original. Fazemos isso exatamente da mesma maneira e usando as mesmas funções que já fizemos para nossos vetores Red , Blue e None cos. Mas como podemos descobrir a qual deles pertence esse novo vetor? Existe uma abordagem matemática muito simples para isso: Dot Product . Como temos a tarefa de comparar dois vetores e encontrar o par mais semelhante, o Produto escalar nos dará exatamente isso. Se você aplicar uma operação de produto escalar para dois vetores idênticos, isso fornecerá algum valor positivo que será maior do que qualquer outro resultado de produto escalar aplicado ao mesmo vetor e a qualquer outro vetor que tenha valores diferentes. E se você aplicar um produto escalar aos vetores ortogonais (vetores que não têm nada em comum entre si), obterá 0 como resultado. Levando isso em consideração, podemos criar um algoritmo simples:


  1. Percorra todos os nossos 3.000 pontos aleatórios, um por um.
  2. Crie um vetor a partir de uma matriz 300x300 com apenas um ponto usando DCT (Discrete Cosine Transform).
  3. Aplique um produto escalar para este vetor com redCosArray , depois com blueCosArray e depois com noneCosArray .
  4. O maior resultado da etapa anterior nos indicará a resposta certa: Red , Blue , None .


A única funcionalidade que falta aqui é um produto escalar, vamos escrever uma função simples para ele:


 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 }


E aqui está uma implementação do 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)) }


Tudo o que precisamos fazer agora é desenhar uma imagem de fillPoints . Vamos dar uma olhada nos pontos do trem que usamos, nos vetores DCT que criamos a partir dos dados do trem e no resultado final que obtivemos:

Resultado

Bem, parece um ruído aleatório. Mas vamos dar uma olhada na representação visual dos vetores. Você pode ver alguns picos aí, essa é exatamente a informação que precisamos focar e remover a maior parte do ruído do nosso resultado DCT. Se dermos uma olhada na representação visual simples da matriz DCT, descobriremos que a informação mais útil (aquela que descreve as características únicas da imagem) está concentrada no canto superior esquerdo:


Concentração


Agora vamos dar um passo atrás e verificar a função shortArray mais uma vez. Usamos um parâmetro cosLimit aqui exatamente pelo motivo de pegar o canto superior esquerdo da matriz DCT e usar apenas os parâmetros mais ativos que tornam nosso vetor ú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 }


Vamos criar nosso objeto math com cosLimit diferente:


 let math = CosTransform(cosLimit: 30)


Agora, em vez de usar todos os 90.000 valores, usaremos apenas 30 x 30 / 2 = 450 deles do canto superior esquerdo da matriz DCT. Vamos dar uma olhada no resultado que obtivemos:

Resultado

Como você pode ver, já está melhor. Também podemos observar que a maioria dos picos que tornam os Vectors únicos ainda estão localizados na parte frontal (conforme selecionado em verde na imagem), vamos tentar usar CosTransform(cosLimit: 6) o que significa que usaremos apenas 6 x 6 / 2 = 18 valores de 90.000 e confira o resultado:

Sucesso

Está muito melhor agora, muito próximo da imagem original. No entanto, há apenas um pequeno problema: esta implementação é lenta. Você não precisaria ser um especialista em complexidade de algoritmo para perceber que DCT é uma operação demorada, mas mesmo o produto escalar, que tem uma complexidade de tempo linear, não é rápido o suficiente ao trabalhar com vetores grandes usando matrizes Swift. A boa notícia é que podemos fazer isso de forma muito mais rápida e simples na implementação usando vDSP do framework Accelerate da Apple, que já temos como biblioteca padrão. Você pode ler sobre vDSP aqui , mas em palavras simples, é um conjunto de métodos para execução de tarefas de processamento digital de sinais de forma muito rápida. Ele tem muitas otimizações de baixo nível que funcionam perfeitamente com grandes conjuntos de dados. Vamos implementar nosso produto escalar e 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 torná-lo menos tedioso, usei alguns operadores para torná-lo mais legível. Agora você pode usar essas funções da seguinte maneira:


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


Há um problema com a nova implementação do DCT em relação ao tamanho atual da nossa matriz. Não funcionaria com nossa imagem 300 x 300, pois ela é otimizada para funcionar com tamanhos específicos que são potências de 2. Portanto, precisaremos fazer algum esforço para dimensionar a imagem antes de aplicá-la ao novo método.

Resumo

Obrigado a quem conseguiu ler este texto até agora ou teve preguiça de folhear sem ler. O objetivo deste artigo foi mostrar que muitas tarefas que as pessoas não consideram resolver com alguns instrumentos nativos podem ser resolvidas com o mínimo de esforço. É divertido procurar soluções alternativas e não se limitar à integração da biblioteca Python como a única opção para resolver tais tarefas.