Go Language learning repository
(c) Cleuton Sampaio 2019
Deep learning e redes neurais são um assunto muito interessante e a linguagem Go dá suporte a essa tecnologia, com o uso do framework Gorgonia.
Gorgonia funciona como frameworks semelhantes, por exemplo, o Tensorflow, permitindo a criação de grafos de operações e tensores.
Ainda não existe algo como o Keras para Gorgonia, embora o projeto Golgi seja promissor.
A programação é um pouco baixo nível, pois temos que criar os grafos contendo as operações e os tensores, portanto, não existe o conceito de neurônio nem de camadas, existente em outros frameworks.
Neste post, vou mostrar um exemplo bem básico de MLP - Multilayer Perceptron tentando encontrar os pesos para uma aproximação da função de disjunção exclusiva ou XOR.
Primeiramente, temos que instalar as bibliotecas necessárias. Ah, e para começar, seu ambiente Go tem que ser 1.12 ou superior!
$ go version
go version go1.13.5 linux/amd64
Depois, instale o pacote com as dependências:
go get gorgonia.org/gorgonia
Há vários pacotes auxiliares interessantes, como o
Mas, neste exemplo, usaremos só o gorgonia.
Teremos uma rede com duas camadas, conforme o modelo:
Neste modelo, para simplificar as coisas, não inclui os bias, o que pode fazer a rede demorar um pouco mais para convergir, mas, um modelo melhor seria assim:
Temos uma sequência de entrada de 4 pares de números: {1,0},{0,1},{1,1},{0,0}, ou seja, com shape 4,2 (quatro linhas e duas colunas). São dois nós de entrada, com quatro repetições.
Para isto, teremos uma camada oculta de 2 nós, portanto, teremos uma matriz de pesos com shape 2,2 (duas linhas e duas colunas), entre as entradas e a camada oculta.
E temos um nó de saída, portanto, temos uma coluna de pesos com shape 2.
O resultado esperado de uma operação de XOR seria assim: {1,1,0,0}.
O arquivo exemplo importa as bibliotecas necessárias. Começarei com a parte interessante, que é criar uma struct para representar nosso modelo de rede neural:
type nn struct {
g *ExprGraph
w0, w1 *Node
pred *Node
predVal Value
}
Esta struct contém ponteiros para o grafo de operações (g), para os nós das camadas de pesos (w0 - entrada/hidden e w1 - hidden/saída), o nó de saída (pred) e seu valor (predVal).
Criei um método para retornar as matrizes de pesos, ou learnables, que serão aquilo que o modelo terá que aprender. Isso facilita muito a parte de Backpropagation:
func (m *nn) learnables() Nodes {
return Nodes{m.w0, m.w1}
}
E criei um factory method para instanciar uma rede neural:
func newNN(g *ExprGraph) *nn {
// Create node for w/weight
w0 := NewMatrix(g, dt, WithShape(2, 2), WithName("w0"), WithInit(GlorotN(1.0)))
w1 := NewMatrix(g, dt, WithShape(2, 1), WithName("w1"), WithInit(GlorotN(1.0)))
return &nn{
g: g,
w0: w0,
w1: w1}
}
Aqui, criamos duas matrizes gorgonia, informando seus shapes e inicializando com números aleatórios (usando o algoritmo Glorot).
Só estamos criando nós no grafo! Nada será realmente executado pelo gorgonia!
Criei um método para o Forward propagation que recebe o vetor de entradas e passa os elementos por toda a rede:
func (m *nn) fwd(x *Node) (err error) {
var l0, l1, l2 *Node
var l0dot, l1dot *Node
// Camada de input
l0 = x
// Multiplicação pelos pesos e sigmoid
l0dot = Must(Mul(l0, m.w0))
// Input para a hidden layer
l1 = Must(Sigmoid(l0dot))
// Multiplicação pelos pesos:
l1dot = Must(Mul(l1, m.w1))
// Camada de saída:
l2 = Must(Sigmoid(l1dot))
m.pred = l2
Read(m.pred, &m.predVal)
return nil
}
Multiplicamos as entradas pelos pesos, calculamos o Sigmoid e passamos para a camada oculta, chegando até ao final.
Finalmente, no método main() instanciamos nosso vetor de entrada e nosso vetor de resultados:
// Set input x to network
xB := []float64{1,0,0,1,1,1,0,0}
xT := tensor.New(tensor.WithBacking(xB), tensor.WithShape(4, 2))
x := NewMatrix(g,
tensor.Float64,
WithName("X"),
WithShape(4, 2),
WithValue(xT),
)
// Define validation data set
yB := []float64{1, 1, 0, 0}
yT := tensor.New(tensor.WithBacking(yB), tensor.WithShape(4, 1))
y := NewMatrix(g,
tensor.Float64,
WithName("y"),
WithShape(4, 1),
WithValue(yT),
)
Colocamos a operação de Forward pass no grafo:
// Run forward pass
if err := m.fwd(x); err != nil {
log.Fatalf("%+v", err)
}
Calculamos a perda com MSE
// Calculate Cost w/MSE
losses := Must(Sub(y, m.pred))
square := Must(Square(losses))
cost := Must(Mean(square))
Colocamos a operação de cálculo dos gradientes e Backpropagation no grafo:
// Do Gradient updates
if _, err = Grad(cost, m.learnables()...); err != nil {
log.Fatal(err)
}
E finalmente, instanciamos uma máquina virtual do Gorgonia e comandamos a execução do grafo:
// Instantiate VM and Solver
vm := NewTapeMachine(g, BindDualValues(m.learnables()...))
solver := NewVanillaSolver(WithLearnRate(0.1))
for i := 0; i < 10000; i++ {
vm.Reset()
if err = vm.RunAll(); err != nil {
log.Fatalf("Failed at inter %d: %v", i, err)
}
solver.Step(NodesToValueGrads(m.learnables()))
vm.Reset()
}
fmt.Println("\n\nOutput after Training: \n", m.predVal)
Repetimos o treinamento por várias vezes, executando o grafo com o comando vm.RunAll().
Eis o resultado após o treinamento:
Output after Training:
C[ 0.6267103873881292 0.6195071561964745 0.47790055401989834 0.3560452019123115]
Dá para montar qualquer tipo de rede com o gorgonia, mas eu quis mostrar este exemplo simples para começar.