Post

Deep Learning com Haskell: Explorando Redes Neurais

Deep Learning com Haskell: Explorando Redes Neurais

Introdução

Você já se perguntou como seria implementar redes neurais usando uma linguagem funcional? Neste artigo, vou compartilhar minha experiência desenvolvendo um projeto de deep learning usando Haskell. A ideia surgiu da minha curiosidade em explorar como conceitos de programação funcional podem se aplicar ao desenvolvimento de redes neurais, especialmente para problemas de regressão linear.

O que torna essa abordagem interessante é que Haskell, com sua forte tipagem e pureza funcional, nos força a pensar diferente sobre como estruturar uma rede neural. Vamos usar a biblioteca massiv para manipulação eficiente de arrays e a hspec para garantir a qualidade do nosso código através de testes.

Estrutura do Projeto

O projeto está organizado da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
deep-learning-haskell
├── app
│   ├── Main.hs
├── src
│   ├── Lib.hs
├── test
│   ├── Spec
│   │   └── MainSpec.hs
│   └── Spec.hs
├── stack.yaml
└── package.yaml

Configuração do Ambiente

Antes de mergulharmos no código, vamos preparar nosso ambiente de desenvolvimento. Se você ainda não tem o Haskell Stack instalado, este é o momento. O Stack vai facilitar muito nossa vida gerenciando dependências e builds do projeto.

crie um novo projeto com o comando:

1
2
stack new deep-learning-haskell
cd deep-learning-haskell

Adicione as dependências no arquivo package.yaml:

1
2
3
4
5
6
dependencies:
- base >= 4.7 && < 5
- bytestring
- massiv
- random
- hspec

Implementação da Rede Neural

No arquivo src/Lib.hs, implementamos a estrutura da rede neural e as funções de inicialização, forward pass, cálculo de loss e treinamento.

Estrutura do Modelo de Rede Neural

A estrutura do modelo de rede neural é definida pela data type Model, que contém os pesos (weights) e o bias (bias).

1
2
3
4
data Model = Model 
    { weights :: Array U Ix1 Double
    , bias :: Double 
    } deriving Show

Função de Inicialização do Modelo

A função initModel inicializa o modelo com pesos e bias aleatórios.

1
2
3
4
5
6
initModel :: IO Model
initModel = do
    randW <- randomRIO (-1,1)
    let w = fromLists' Seq [randW]
    b <- randomRIO (-1,1)
    return $ Model w b

Função de Forward Pass

A função forward realiza o forward pass, calculando a saída da rede neural para uma entrada x.

1
2
forward :: Model -> Array U Ix1 Double -> Double
forward (Model w b) x = dot w x + b

Função de Cálculo de Loss

A função loss calcula o erro quadrático médio (MSE) comparando a saída prevista com a saída real.

1
2
3
4
5
loss :: Array U Ix1 Double -> Array U Ix1 Double -> Double
loss y_pred y_true =
  let diff = compute @U $ A.zipWith (-) y_pred y_true
      squared = compute @U $ A.map (^2) diff
   in sum squared / fromIntegral (unSz $ size squared)

Função de Treinamento do Modelo

A função train atualiza os pesos e bias do modelo usando gradiente descendente.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
train :: Model -> Array U Ix2 Double -> Array U Ix1 Double -> Int -> Double -> IO Model
train model x y epochs lr = do
    let xFlat = compute @U $ flatten x
    foldM (\m e -> do
        let y_pred = A.singleton $ forward m xFlat
            current_loss = loss y_pred y
            diff = compute @U $ A.zipWith (-) y_pred y
            grad_w = dot (A.singleton $ 2 * sum diff) xFlat
            grad_b = 2 * sum diff
            new_w = fromLists' Seq [index' (weights m) 0 - lr * grad_w]
            new_b = bias m - lr * grad_b
        when (e `mod` 100 == 0) $
            putStrLn $ "Epoch " ++ show e ++ " Loss: " ++ show current_loss
        return $ Model new_w new_b) model [1..epochs]

Executando o Projeto

Para executar o projeto, utilize o comando:

1
stack run

Para rodar os testes, utilize o comando:

1
stack test

Testes: Aprendendo com os Erros

Durante o desenvolvimento, aprendi da maneira mais difícil que testar redes neurais não é trivial. Aqui estão alguns dos testes mais importantes que implementei, depois de vários ciclos de tentativa e erro:

1
2
3
4
5
6
7
8
9
10
11
describe "Model Training" $ do
    it "should train the model and reduce loss" $ do
        -- Esse teste me salvou várias vezes durante refatorações
        arrList <- replicateM 100 (randomRIO (-10,10))
        let x = fromLists' Seq (Prelude.map (:[]) arrList) :: Array U Ix2 Double
            y = compute @U $ A.map (\v -> 2*v + 1) (compute @U $ flatten x)
        model <- initModel
        trained <- train model x y 1000 0.01
        let y_pred = A.singleton $ forward trained (compute @U $ flatten x)
            final_loss = loss y_pred y
        final_loss `shouldSatisfy` (< 1.0)

Uma dica que aprendi na prática: sempre teste com diferentes conjuntos de dados. No início, eu testava apenas com um conjunto fixo e perdi horas debugando problemas que só apareciam com dados diferentes.

Próximos Passos

Depois de implementar essa versão inicial, já tenho algumas ideias para expandir o projeto:

  • Implementar diferentes funções de ativação (ReLU foi minha primeira escolha, mas quero experimentar outras)
  • Adicionar suporte a redes neurais convolucionais
  • Otimizar o treinamento com processamento paralelo

Conclusão

Este projeto demonstra como podemos utilizar Haskell para implementar e treinar uma rede neural simples. A linguagem Haskell, com sua forte tipagem e imutabilidade, oferece uma abordagem interessante e segura para o desenvolvimento de algoritmos de machine learning. Experimente expandir este projeto adicionando novas camadas à rede neural ou implementando diferentes funções de ativação e loss.


Você pode encontrar o código completo no nosso repositório do GitHub. Pull requests são bem-vindas!

This post is licensed under CC BY 4.0 by the author.