3
respostas

[Dúvida] O que faz esse outro código numpy ?

Eu tenho dificuldade em funções do numpy e em list compreension. Por isso, analisar esse código pra mim é complicado. Eu não consegui entender o que realmente é feito em cada uma dessas linhas.

import numpy as np;

class Network(object):
   
     def __init__(self, sizes):
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]

    def backprop(self, x, y):
            """Return a tuple ``(nabla_b, nabla_w)`` representing the
            gradient for the cost function C_x.  ``nabla_b`` and
            ``nabla_w`` are layer-by-layer lists of numpy arrays, similar
            to ``self.biases`` and ``self.weights``."""
            nabla_b = [np.zeros(b.shape) for b in self.biases]
            nabla_w = [np.zeros(w.shape) for w in self.weights]
            # feedforward
            activation = x
            activations = [x] # list to store all the activations, layer by layer
            zs = [] # list to store all the z vectors, layer by layer
            for b, w in zip(self.biases, self.weights):
                z = np.dot(w, activation)+b
                zs.append(z)
                activation = sigmoid(z)
                activations.append(activation)
            # backward pass
            delta = self.cost_derivative(activations[-1], y) * \
                sigmoid_prime(zs[-1])
            nabla_b[-1] = delta
            nabla_w[-1] = np.dot(delta, activations[-2].transpose())
            
            for l in xrange(2, self.num_layers):
                z = zs[-l]
                sp = sigmoid_prime(z)
                delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
                nabla_b[-l] = delta
                nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
            return (nabla_b, nabla_w)

    def cost_derivative(self, output_activations, y):
            """Return the vector of partial derivatives \partial C_x /
            \partial a for the output activations."""
            return (output_activations-y)
            
    #### Miscellaneous functions
    def sigmoid(z):
        """The sigmoid function."""
        return 1.0/(1.0+np.exp(-z))

    def sigmoid_prime(z):
        """Derivative of the sigmoid function."""
        return sigmoid(z)*(1-sigmoid(z))

Extrai esse trecho de código desse link: http://neuralnetworksanddeeplearning.com/chap1.html

Minha duvida maior esta na função "backprop". A partir da linha onde tem "# backward pass" começa a ficar bem mais dificil de entender. E fica ainda mais dificil de entender o passo a passo desse código, principalmente quando chega na parte desse laço FOR

for l in xrange(2, self.num_layers):
                z = zs[-l]
                sp = sigmoid_prime(z)
                delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
                nabla_b[-l] = delta
                nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())

Queria conseguir entender o que esta sendo feito nesses códigos.

Por isso tenho algumas perguntas que gostaria de fazer sobre as linhas do código:

1 - Dentro do método backprop, ao todo, o que o código dessa função faz? O que cada linha dentro da função backprop esta fazendo?

2 - Primeiro: O que esta sendo feito nessas linhas?: "

           # backward pass
            delta = self.cost_derivative(activations[-1], y) * \
                sigmoid_prime(zs[-1])

3 - O que esse simbolo "* \" significa no python?

4 - Também, o que esta sendo feito nessas outras linhas?:

 for l in xrange(2, self.num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())

4 - De acordo com o código da função backprop, que tipo de matrizes a função backprop deve retornar? em qual formato/estrutura?

Por favor, poderiam me explicar em detalhes, passo a passo o que esses códigos fazem? Também, se possivel, poderiam me mostrar outro código equivalente que faça a mesma coisa porém sem usar o numpy e sem usar list compreension? Ficaria mais facil entender.

3 respostas

Oii William, tudo bem?

O que faz a função backprop?

A função backprop é utilizada para realizar o algoritmo de retropropagação (backpropagation) em uma rede neural.

Esse algoritmo é essencial para o treinamento de redes neurais, pois é através dele que ajustamos os pesos e os bias da rede para minimizar o erro na saída. O objetivo é calcular o gradiente da função de custo em relação a cada peso e bias, que são usados para atualizar os pesos e bias durante o treinamento.

Explicação das linhas:

Linha com delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
  1. self.cost_derivative(activations[-1], y): Calcula a derivada da função de custo. activations[-1] é a ativação da última camada da rede, e y é o valor desejado (target). Essa função calcula a diferença entre a saída da rede e o valor desejado, que é o erro da previsão.

  2. sigmoid_prime(zs[-1]): Calcula a derivada da função sigmoid na última camada. zs[-1] representa o valor de entrada para a função de ativação na última camada antes de aplicar a sigmoid.

  3. O operador * é simplesmente uma multiplicação elemento a elemento entre os dois arrays resultantes das funções anteriores. O resultado, delta, representa o erro na última camada, ajustado pela inclinação da função de ativação.

Sobre o símbolo * \

O * é um operador de multiplicação, e o \ é um caractere de continuação de linha em Python. Ele permite que você divida uma linha de código em várias linhas para melhorar a legibilidade.

Explicação do loop for:

for l in xrange(2, self.num_layers):
    z = zs[-l]
    sp = sigmoid_prime(z)
    delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
    nabla_b[-l] = delta
    nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
  1. xrange(2, self.num_layers): Itera de 2 até o número total de camadas. O índice l representa a camada atual começando da última camada para a primeira.

  2. z = zs[-l]: Pega o vetor z para a camada l.

  3. sp = sigmoid_prime(z): Calcula a derivada da função sigmoid para o vetor z.

  4. np.dot(self.weights[-l+1].transpose(), delta) * sp: Calcula o novo delta para a camada atual. Multiplica o delta da camada seguinte pelo peso transposto da camada atual, aplicando a derivada da função sigmoid. Isso propaga o erro para trás na rede.

  5. nabla_b[-l] = delta: Armazena o gradiente do bias para a camada atual.

  6. nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()): Calcula o gradiente do peso para a camada atual.

Tipo de matrizes retornadas pela função backprop

A função backprop retorna duas listas de matrizes numpy (nabla_b, nabla_w), onde cada matriz contém os gradientes para os bias e os pesos de cada camada, respectivamente.

  • Código equivalente sem numpy e sem list comprehension:

Recriar esse código sem usar numpy e list comprehension exigiria uma implementação manual de operações de matriz e vetor, o que é bastante complexo e extenso.

  • O NumPy oferece bibliotecas otimizadas para operações matemáticas em arrays, o que é essencial para o cálculo eficiente dos gradientes na propagação de erros.

  • List comprehensions são usadas para criar listas de forma concisa, o que facilita a atualização dos gradientes em cada camada.

Sem essas ferramentas, o código ficaria extenso, menos legível e possivelmente menos performático.

Pra facilitar o aprendizado inicial, você pode tentar entender cada linha de código separadamente.

Um abraço e bons estudos.

Obrigado Lorena por responder. Mais ainda fiquei com uma duvida aqui, duvidas antigas. Por favor, poderia me explicar esse ponto especifico?

Nesse trecho desse loop

for l in xrange(2, self.num_layers):
    z = zs[-l]
    sp = sigmoid_prime(z)
    delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
    nabla_b[-l] = delta
    nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())

Mais espeficicamente nessa linha `

delta = np.dot(self.weights[-l+1].transpose(), delta) * sp

`

Voce me disse que esse trecho "np.dot(self.weights[-l+1].transpose(), delta) * sp: Calcula o novo delta para a camada atual. Multiplica o delta da camada seguinte pelo peso transposto da camada atual, aplicando a derivada da função sigmoid. Isso propaga o erro para trás na rede."

Eu ainda fiquei com uma duvida. Pois na internet em outros artigos, eles explicam que para eu calcular o delta de um neuronio na camada oculta eu preciso fazer uma soma ponderada dos deltas dos neuronios da camada seguinte junto com o peso de conexão, ou seja, o peso que conecta o neuronio da camada oculta com o neuronio da camada seguinte.

Por exemplo, nesse artigo https://home.agh.edu.pl/~vlsi/AI/backp_t_en/backprop.html Ele mostra essa imagem: imagem do link

Nessa imagem daquele site que mencionei, para calcular o delta do neuronio F3 da primeira camada oculta da rede, na fórmula ele soma o delta do neuronio F4 da camada seguinte(ou seja, da segunda camada oculta) multiplicado pelo peso W34 e mais o delta do neuronio F5 multiplicado pelo peso W35. Ou seja, nessa explicação, os pesos de conexão usados na formula foram os pesos dos neuronios da camada seguinte: o delta vezes o peso + delta vezes o peso, etc.....

Tambem, nesse outro link do mesmo autor do codigo python http://neuralnetworksanddeeplearning.com/chap2.html, numa parte mais teórica ele menciona uma formula que também parece usar os pesos dos neuronios da camada seguinte. Formula teorica matricial do autor do código que tenho duvida

Nessa formula tambem parece que o delta de uma camada oculta depende dos deltas e pesos da camada seguinte. Pelo menos foi isso que eu entendi, por favor, me corrija se eu estou interpretando errado.

Ou seja, nessas explicações da internet, os pesos de conexão usados na formula foram os pesos dos neuronios da camada seguinte.

Mais no código isso parece estar diferente. Por isso, isso me parece um pouco diferente do código em python que citei. Fiquei com duvida nessa parte da sua explicação: "[...] Multiplica o delta da camada seguinte pelo peso transposto da camada atual [...]".

Tenho algumas duvidas:

1 - "o peso transposto da camada atual" mencionado na sua explicação ele é o peso do neuronio da camada oculta(que é referente as entradas da camada de entrada), ou ele é o peso do neuronio da camada seguinte(ou seja, o peso da camada de saida referente as saidas da camada anterior, no caso a camada oculta)? Por favor, poderia me explicar melhor esse ponto?

2 - Essas duas formulas da internet que mencionei, elas são equivalentes a sua explicação? Talvez só tenha usado uma notação diferente?

Deis de já agradeço pela paciencia e pela oportunidade de tirar essas duvidas.

Olá!

  1. Na linha em questão:
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp

Vou tentar esclarecer:

  • self.weights[-l+1] se refere aos pesos da camada seguinte à camada atual (camada l).
  • Ao transpor essa matriz de pesos (self.weights[-l+1].transpose()), estamos preparando-a para a multiplicação de matrizes.
  • delta é o erro calculado na camada atual, que está sendo propagado para trás.
  • Ao multiplicar self.weights[-l+1].transpose() (os pesos da camada seguinte, transpostos) por delta, estamos essencialmente calculando a contribuição dos pesos da camada seguinte para o erro atual.
  • Multiplicar essa contribuição pelos gradientes locais (sp), que é a derivada da função de ativação aplicada à entrada ponderada (z), ajusta esse erro conforme a inclinação da função de ativação.

Em resumo, essa linha calcula como o erro na camada atual é influenciado pelos pesos da camada seguinte, considerando a inclinação da função de ativação.

  1. Quanto às fórmulas que você encontrou na internet, elas descrevem o mesmo conceito de backpropagation, mas podem usar notações diferentes. No final das contas, o objetivo é calcular como o erro em uma camada é influenciado pelos pesos conectando-a à próxima camada, levando em consideração as derivadas das funções de ativação.

    • A fórmula na imagem que você compartilhou e a fórmula teórica do autor do código Python descrevem esse processo de propagação do erro, levando em consideração os pesos da camada seguinte.

    • A notação pode variar, mas o conceito subjacente é o mesmo: ajustar os pesos da rede para minimizar o erro na saída.

Então, a explicação no código Python sobre a multiplicação pelo "peso transposto da camada atual" está se referindo aos pesos da camada seguinte na propagação do erro. As fórmulas que você encontrou online descrevem o mesmo conceito, embora possam usar notações diferentes.

Um abraço.