# ÁLGEBRA LINEAR COMPUTACIONAL

## SEGUNDA AVALIAÇÃO - 2020.2 - PROFESSOR ERITO MARQUES

---
### GRUPO:
**Ícaro Santos Barcelos Pereira - _2017780187_**\
**Leandro Bataglia Pereira - _2018780199_**\
**Pedro Henrique Farolfi Camelo - _2018780326_**

---

Repositório dos algoritmos solicitados na segunda prova da disciplina de Álgebra Linear Computacional.
##### Instruções:
As respostas devem ser enviadas no formato jupyter notebook.\
Os trabalhos devem ser feitos em grupo. Não é permitida troca de informações entre os grupos.\
Fixada a data de entrega, não serão aceitos trabalhos entregues fora dessa data.

```python
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore",category=UserWarning)
```

```python
import numpy as np
import pandas as pd
import math
from normas import *
from criterios import *
from cholesky import *
from iterativos import *
from benford import *
from fatoracaoLU import fatoracaoLU
from substituicao import resolve_substituicao
from sympy import Matrix, init_printing
init_printing()
#BIBLIOTECA QUE NÃO ESTOU USANDO MAS PODE SER ÚTIL PARA EXIBIR AS MATRIZES/VETORES = display(Matrix(*variavel que representa o dado*)
```

```python
# Coverti isso aqui em um procedimento, a parada é que vc pode mandar isso
# ocorrer no início de qualquer célula e pode alterar o excel em tempo de execução :)

# CONVERSÃO DOS DADOS EM ARRAYS DA NUMPY COM TYPE 'float64'
def ler_matriz_A_vetor_b(file_excel, sheet_matriz, sheet_vetor):
    df_matriz = pd.read_excel(r"dados/"+file_excel+".xlsx", header=None, sheet_name=sheet_matriz) # Le o arquivo do excel que contém a matriz A sem cabeçalho
    df_vetor = pd.read_excel(r"dados/"+file_excel+".xlsx", header=None, sheet_name=sheet_vetor) # Le o arquivo do excel que contém o vetor b sem cabeçalho
    matriz = np.array(df_matriz, dtype='f8') # Converte a matriz A em um array da numpy
    vetor = np.ravel(np.array(df_vetor, dtype='f8')) # Converte o vetor b em um array da numpy
    return matriz, vetor
```

### 1) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel) e avalie:

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes", "matriz_A(1)", "vetor_b(1)")
```

**a)** se A é tridiagonal;

```python
def verifica_tridiagonal(matriz_A, debug=False):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        if debug:
            print("A matriz não é quadrada -> não é tridiagonal")
        return False

    for i in range(linhas):
        for j in range(colunas):
            if np.abs(i-j) <= 1 and matriz_A[i,j] == 0:
                return False
            if np.abs(i-j) >= 2 and matriz_A[i,j] != 0:
                return False
    return True
```

```python
verifica_tridiagonal(matriz, debug=True)
```

    False

```python
#declarando vetor na mão:
A = np.array([[1,1,0],
              [1,1,1],
              [0,1,1]])
verifica_tridiagonal(A)
```

    True

**b)** se A é triangular superior;

```python
def verifica_tri_superior(matriz_A, debug=False):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        if debug:
            print("A matriz não é quadrada -> não é triangular superior")
        return False

    for i in range(linhas):
        for j in range(i):
            if matriz_A[i,j] != 0:
                return False
    return True
```

```python
verifica_tri_superior(matriz, debug=True)
```

    False

**c)** se A é triangular inferior;

```python
def verifica_tri_inferior(matriz_A):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        print("A matriz não é quadrada -> não é triangular inferior")
        return False

    for j in range(colunas):
        for i in range(j):
            if matriz_A[i,j] != 0:
                return False
    return True
```

```python
verifica_tri_inferior(matriz)
```

    False

**d)** se Traço de A é maior que 4.

```python
def traco(matriz_A, debug=False):
    linhas, colunas = np.shape(matriz_A)

    try:
        assert linhas == colunas
    except:
        if debug:
            print("A matriz não é quadrada -> traço não está definido.")
        return False

    n, traco = min((linhas, colunas)), 0
    for i in range(n):
        traco += matriz_A[i,i]
    if traco > 4:
        print("O traço da matriz é " + str(traco) + ", portanto é maior que 4")
        return True
    print("O traço da matriz é " + str(traco) + ", portanto não é maior que 4")
    return False
```

```python
A = np.array([[1,2,3],[1,2,3]])
traco(A, debug=True)
```

    A matriz não é quadrada -> traço não está definido.

    False

```python
traco(matriz, debug=True)
```

    O traço da matriz é 1179.0, portanto é maior que 4

    True

---
### 2) Implemente um algoritmo que leia uma matriz A (triangular) e um vetor b (de uma planilha excel) e resolva o sistema Ax = b por substituição para frente ou para trás conforme o caso.

```python
def resolve_substituicao(matriz_A, vetor_b):

    if checa_tamanho_matriz_vetor(matriz_A, vetor_b) == False:
        return False

    if(verifica_tri_inferior(matriz_A)):
        return resolve_subs_frente(matriz_A, vetor_b)

    elif(verifica_tri_superior(matriz_A)):
        return resolve_subs_tras(matriz_A,vetor_b)

    else:
        print("A matriz não é triangular")
        return False

def checa_tamanho_matriz_vetor(matriz_A, vetor_b):
    lin_A, col_A = np.shape(matriz_A)
    col_b = np.shape(vetor_b)

    try:
        assert lin_A == col_b[0]
    except:
        print("A quantidade de linhas em A e de colunas em b são distintas")
        return False
    return True

def resolve_subs_frente(matriz_A, vetor_b): # Resolve um sistema com matriz triangular inferior
    n = len(matriz_A)
    vetor_resultado = np.array(np.copy(vetor_b), dtype='f8')

    for i in range(n):
        for j in range(i):
            vetor_resultado[i] = vetor_resultado[i] - matriz_A[i,j] * vetor_resultado[j]
        vetor_resultado[i] = vetor_resultado[i]/matriz_A[i,i]
    return vetor_resultado

def resolve_subs_tras(matriz_A,vetor_b): # Resolve um sistema com matriz triangular superior
    n = len(matriz_A)
    vetor_resultado = np.array(np.copy(vetor_b), dtype='f8')

    for i in range(n-1,-1,-1):
        for j in range(n-1,i,-1):
            vetor_resultado[i] = vetor_resultado[i] - matriz_A[i,j] * vetor_resultado[j]
        vetor_resultado[i] = vetor_resultado[i]/matriz_A[i,i]
    return vetor_resultado
```

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes", "matriz_A(1)", "vetor_b(1)")

delta_x = resolve_substituicao(matriz, vetor)

if delta_x != False:
    print("Vetor x resultado da substituição:\n",delta_x,"\n")
```

    A matriz não é triangular

---
### 3) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel) e:

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes", "matriz_A(1)", "vetor_b(1)")
```

**a)** Avalie se é possível obter o fator de Cholesky;

```python
def checa_positiva_definida(matriz_A, debug=False):

    n = len(matriz_A) # n é a ordem da matriz a
    matriz_R = np.copy(np.triu(matriz_A)) # matriz_r é uma cópia triangular superior da matriz A
    for i in range(n):
            for k in range(i): # Não é executado quando i = 0
                matriz_R[i,i] = matriz_R[i,i] - math.pow(matriz_R[k,i],2) # Define os termos da diagonal principal
            if matriz_R[i,i] <= 0: # Se algum elemento da diagonal principal for menor ou igual a zero, por definição, a matriz não é positiva definida
                if debug:
                    print("Erro: A matriz não é positiva definida.")
                return False
            matriz_R[i,i] = math.sqrt(matriz_R[i,i])
            for j in range(i+1,n): # Não é executado quando i = n
                for k in range(i): # Não é executado quando i = 0
                    matriz_R[i,j] = matriz_R[i,j] - (matriz_R[k,i] * matriz_R[k,j])
                matriz_R[i,j] = matriz_R[i,j]/matriz_R[i,i]
    return True
```

```python
A = np.array([[2.,1.,0.],
              [1.,2.,-1.],
              [0.,-1.,2.]])
```

```python
A = np.array([[2.,1.,0.],
              [1.,2.,-1.],
              [0.,-1.,2.]])
```

```python
b = np.array([[3.],[0.],[1.]])
```

```python
checa_positiva_definida(A, debug=True)
```

    True

**b)** Resolva o sistema Ax = b usando a fatoração de Cholesky.

```python
def decomposicao_cholesky(matriz_A, vetor_b):
    n = len(matriz_A) # n é a ordem da matriz a
    matriz_R = np.copy(np.triu(matriz_A)) # matriz_r é uma cópia triangular superior da matriz A

    for i in range(n):
        for k in range(i): # Não é executado quando i = 0
            matriz_R[i,i] = matriz_R[i,i] - math.pow(matriz_R[k,i],2) # Define os termos da diagonal principal
        if matriz_R[i,i] <= 0: # Se algum elemento da diagonal principal for menor ou igual a zero, por definição, a matriz não é positiva definida
            print("Erro: A matriz não é positiva definida.")
            return
        matriz_R[i,i] = math.sqrt(matriz_R[i,i])
        for j in range(i+1,n): # Não é executado quando i = n
            for k in range(i): # Não é executado quando i = 0
                matriz_R[i,j] = matriz_R[i,j] - (matriz_R[k,i] * matriz_R[k,j])
            matriz_R[i,j] = matriz_R[i,j]/matriz_R[i,i]

    return resolve_cholesky(matriz_R, vetor_b)

def resolve_cholesky(matriz_R, vetor_b):
    matriz_Rt = np.copy(np.transpose(matriz_R)) # matriz_rt é a matriz R transposta

    vetor_y = subs.resolve_substituicao(matriz_Rt, vetor_b) # Faz o cálculo de Rt * y = b para obter o vetor y
    vetor_x = subs.resolve_substituicao(matriz_R, vetor_y) # Faz o cálculo de R * x = y para obter o vetor x (resultado)

    return vetor_x # Retorna o vetor x (resultado)
```

```python
# TESTE DO ALGORITMO DE DECOMPOSIÇÃO DE CHOLESKY E NORMA RESIDUAL (cholesky.py, normas.py)
delta_x = decomposicao_cholesky(matriz, vetor)
print("Vetor x resultado da Decomposição de Cholesky:\n",delta_x,"\n")
#residual(matriz, vetor, delta_x)
```

    Erro: A matriz não é positiva definida.
    Vetor x resultado da Decomposição de Cholesky:
     None

---
### 4) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel) e:

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes", "matriz_A(1)", "vetor_b(1)")
```

**a)** Avalie se algum critério é satisfeito para aplicação dos métodos de Gauss-Seidel, Jacobi e SOR;

```python
def criterio_linhas(matriz_A): # Verificar o critério das linhas para o método de Jacobi
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    soma = 0

    for i in range(n): # Percorre toda a matriz
        for j in range(m):
            if i != j: # Desconsidera os termos da diagonal principal
                soma += np.abs(matriz_A[i,j])

        if soma >= np.abs(matriz_A[i,i]): # Verifica se a soma de todos os termos (sem contar o termo da diagonal principal) de cada linha é menor ou maior que o termo da diagonal principal (Critério das linhas)
            return False

        soma = 0
    return True
```

```python
def criterio_sassenfeld(matriz_A):
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que auxilia nos somatórios dos termos tanto de Aij*Beta, quanto de Aij
    vetor_betao = np.zeros(m) # Vetor de Betas iniciado com termos iguais a zero

    for i in range(n):
        for j in range(i): # j vai até i-1 (desconsidera a primeira iteração)
            aux += np.abs(matriz_A[i,j]) * vetor_betao[j] # A partir da segunda iteração, inclui a multiplicação de Aij pelos Betas anteriores no somatório (já que na primeira iteração não há Betas anteriores, obviamente)
        for j in range(i,n):
            aux += np.abs(matriz_A[i,j]) # Somatório dos módulos dos termos de Aij
        vetor_betao[i] = aux/np.abs(matriz_A[i,i]) #Divide o valor da soma pelo termo da diagonal principal
        aux = 0 # Zera a variável auxiliar pra próxima iteração

    print("Maior betão:", np.max(vetor_betao))
    if np.max(vetor_betao) < 1: # Verifica se a condição do critério de Sassenfeld (max(B) < 1) é satisfeita
        return True
    return False
```

```python
criterio_linhas(matriz)
```

    False

```python
criterio_sassenfeld(matriz)
```

    Maior betão: 10602274533433.357

    False

**b)** Resolva o sistema Ax = b usando os métodos iterativos de Gauss-Seidel, Jacobi e SOR.

## Jacobi

```python
def jacobi(matriz_A, vetor_b, vetor_x=0, N=10000, p=1e-15): # Função que executa o algoritmo de Jacobi
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que vai auxiliar a armazenar as subtrações dos valores Aij*xj

    if isinstance(vetor_x, int): # Se não é fornecido um vetor de aproximação inicial para x, ele é iniciado aqui com todos os elementos iguais a zero
        vetor_x = np.zeros(m)

    vetor_aux = np.copy(vetor_x) # Vetor auxiliar igual ao vetor x que representa a iteração k+1

    if criterio_linhas(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério das linhas foi satisfeito: O sistema converge.")
    else:
        print("ATENÇÃO: O critério das linhas não foi satisfeito.")

    for k in range(N): # Executa todas as iterações especificadas na chamada da função (N) se a condição de parada não for satisfeita
        for i in range(n):
            for j in range(n):
                if i != j: # Pula os elementos da diagonal principal pois eles não são utilizados
                    aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj

            vetor_aux[i] = (vetor_b[i] - aux)/matriz_A[i,i] # Executa a iteração do método com (bi-Aij*xj)/Aii
            aux = 0 # Zera a contagem do somatório dos valores novamente

        termos_encontrados = 0
        for i in range(n): #Verifica elemento por elemento de vetor_x e do vetor_aux
            if vetor_x[i] != 0:
                if np.abs(vetor_aux[i] - vetor_x[i])/np.abs(vetor_x[i]) <= p: # Condição de parada caso a variação relativa de uma iteração para outra seja menor que a tolerância p
                    termos_encontrados += 1
            else:
                if np.abs(vetor_aux[i] - vetor_x[i]) <= p*np.abs(vetor_x[i]):
                    termos_encontrados += 1

        if(termos_encontrados == n):
            print("(Jacobi) Resultado encontrado com o critério de parada, na iteração:", k)
            return vetor_x

        vetor_x = np.copy(vetor_aux) # Atualiza o vetor_x (k) com os valores do vetor_aux (k+1)

    print("(Jacobi) Resultado encontrado com o número máximo de iterações.")
    return vetor_x # Retorna o vetor solução
```

```python
print(jacobi(matriz, vetor, N=100))
```

    ATENÇÃO: O critério das linhas não foi satisfeito.
    (Jacobi) Resultado encontrado com o número máximo de iterações.
    [-2.21507407e+191 -7.85062201e+191 -4.02063148e+191 -3.00124088e+191
     -2.34581480e+192 -2.32464880e+191 -4.58103429e+192 -4.79832453e+191
     -5.85375866e+191 -4.90112952e+191 -2.01091315e+191 -2.59706719e+191
     -6.26779852e+191 -1.21273532e+192 -8.04504337e+192 -2.42684232e+192
     -4.65327284e+191 -2.98587023e+191 -3.29553950e+191 -4.17899256e+191
     -4.88807065e+191 -4.20548399e+191 -3.16760574e+192 -1.26588784e+192
     -1.08434271e+193]

## Gauss-Seidel

```python
def seidel(matriz_A, vetor_b, vetor_x=0, N=1000, p=1e-15): # Função que executa o algoritmo de Gauss-Seidel
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que vai auxiliar a armazenar as subtrações dos valores Aij*xj

    if isinstance(vetor_x, int): # Se não é fornecido um vetor de aproximação inicial para x, ele é iniciado aqui com todos os elementos iguais a zero
        vetor_x = np.zeros(m)

    vetor_aux = np.copy(vetor_x) # Vetor auxiliar igual ao vetor x que representa a iteração k+1

    if criterio_linhas(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério das linhas foi satisfeito: O sistema converge.")
    else:
        print("ATENÇÃO: O critério das linhas não foi satisfeito.")

    if criterio_sassenfeld(matriz_A): # Verifica se o critério de Sassenfeld é satisfeito
        print("O critério de Sassenfeld foi satisfeito.")
    else:
        print("ATENÇÃO: O critério de Sassenfeld não foi satisfeito.")

    for k in range(N): # Executa todas as iterações especificadas na chamada da função
        for i in range(n):
            for j in range(m):
                if i != j: # Pula os elementos da diagonal principal pois eles não são utilizados
                    if i > j:
                        aux += matriz_A[i,j] * vetor_aux[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores que já foram atualizados (de 0 a i) que ficam no vetor auxiliar
                    else:
                        aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores ainda não foram atualizados (de i+1 a n) que ficam no vetor x

            vetor_aux[i] = (vetor_b[i] - aux)/matriz_A[i,i] # Executa a iteração do método com (bi-Aij*xj)/Aii
            aux = 0 # Zera a contagem do somatório dos valores novamente

        termos_encontrados = 0
        for i in range(n): #Verifica elemento por elemento de vetor_x e do vetor_aux
            if vetor_x[i] != 0:
                if np.abs(vetor_aux[i] - vetor_x[i])/np.abs(vetor_x[i]) <= p: # Condição de parada caso a variação relativa de uma iteração para outra seja menor que a tolerância p
                    termos_encontrados += 1
            else:
                if np.abs(vetor_aux[i] - vetor_x[i]) <= p*np.abs(vetor_x[i]):
                    termos_encontrados += 1

        if(termos_encontrados == n):
                print("(Seidel) Resultado encontrado com o critério de parada, na iteração:", k)
                return vetor_x

        vetor_x = np.copy(vetor_aux) # Atualiza o vetor_x (k) com os valores do vetor_aux (k+1)

    print("(Seidel) Resultado encontrado com o número máximo de iterações.")
    return vetor_x # Retorna o vetor solução
```

```python
print(seidel(matriz, vetor))
```

    ATENÇÃO: O critério das linhas não foi satisfeito.
    Maior betão: 10602274533433.357
    ATENÇÃO: O critério de Sassenfeld não foi satisfeito.
    (Seidel) Resultado encontrado com o número máximo de iterações.
    [nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan
     nan nan nan nan nan nan nan]

    <ipython-input-32-caf3047c1391>:25: RuntimeWarning: overflow encountered in double_scalars
      aux += matriz_A[i,j] * vetor_aux[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores que já foram atualizados (de 0 a i) que ficam no vetor auxiliar
    <ipython-input-32-caf3047c1391>:25: RuntimeWarning: invalid value encountered in double_scalars
      aux += matriz_A[i,j] * vetor_aux[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores que já foram atualizados (de 0 a i) que ficam no vetor auxiliar
    <ipython-input-32-caf3047c1391>:27: RuntimeWarning: invalid value encountered in double_scalars
      aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores ainda não foram atualizados (de i+1 a n) que ficam no vetor x
    <ipython-input-32-caf3047c1391>:27: RuntimeWarning: overflow encountered in double_scalars
      aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores ainda não foram atualizados (de i+1 a n) que ficam no vetor x

## SOR

```python
def calcular_omega_otimo(matriz_A):

    if checa_positiva_definida(matriz_A, debug=True) == False or verifica_tridiagonal(matriz_A, debug=True) == False:
        print("Não há como encontrar w ótimo.")
        return None

    D = np.copy(matriz_A)
    for i in range(3):
        for j in range(3):
            if i != j:
                D[i,j] = 0.
    D = np.linalg.inv(D)

    L = np.tril(A)
    U = np.triu(A)

    for i in range(3):
        for j in range(3):
            if i == j:
                L[i,j] = 0.
                U[i,j] = 0.

    LU = L + U
    T = np.matmul(D, LU)
    P = np.linalg.eig(T)
    p = np.amax(P[0])

    print("omega: {}".format(2/(1 + math.sqrt(1 - pow(p, 2)))))
    return 2/(1 + math.sqrt(1 - pow(p, 2)))

```

```python
def sor(matriz_A, vetor_b, vetor_x=0, N=10000, p=1e-15, w=1.7): # Quando w = 1 é igual ao Seidel
    n, m = len(matriz_A), len(matriz_A[0]) # n e m são as dimensões da matriz
    aux = 0 # Variável que vai auxiliar a armazenar as subtrações dos valores Aij*xj

    omega = calcular_omega_otimo(matriz_A)

    if omega != None:
        w = omega

    if isinstance(vetor_x, int): # Se não é fornecido um vetor de aproximação inicial para x, ele é iniciado aqui com todos os elementos iguais a zero
        vetor_x = np.zeros(m)

    vetor_aux = np.copy(vetor_x) # Vetor auxiliar igual ao vetor x que representa a iteração k+1

    if criterio_linhas(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério das linhas foi satisfeito: O sistema converge.")
    else:
        print("ATENÇÃO: O critério das linhas não foi satisfeito.")

    if criterio_sassenfeld(matriz_A): # Verifica se o critério das linhas é satisfeito
        print("O critério de Sassenfeld foi satisfeito.")
    else:
        print("ATENÇÃO: O critério de Sassenfeld não foi satisfeito.")

    for k in range(N): # Executa todas as iterações especificadas na chamada da função
        for i in range(n):
            for j in range(m):
                if i != j: # Pula os elementos da diagonal principal pois eles não são utilizados
                    if i > j:
                        aux += matriz_A[i,j] * vetor_aux[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores que já foram atualizados (de 0 a i) que ficam no vetor auxiliar
                    else:
                        aux += matriz_A[i,j] * vetor_x[j] # Acumula as subtrações dos valores de Aij*xj utilizando os valores ainda não foram atualizados (de i+1 a n) que ficam no vetor x

            vetor_aux[i] = (1-w)*vetor_x[i] + (w*(vetor_b[i] - aux))/matriz_A[i,i] # Executa a iteração do método com (1-w)*xi(bi-Aij*xj)/Aii
            aux = 0 # Zera a contagem do somatório dos valores novamente

        termos_encontrados = 0
        for i in range(n): #Verifica elemento por elemento de vetor_x e do vetor_aux
            if vetor_x[i] != 0:
                if np.abs(vetor_aux[i] - vetor_x[i])/np.abs(vetor_x[i]) <= p: # Condição de parada caso a variação relativa de uma iteração para outra seja menor que a tolerância p
                    termos_encontrados += 1
            else:
                if np.abs(vetor_aux[i] - vetor_x[i]) <= p*np.abs(vetor_x[i]):
                    termos_encontrados += 1

        if(termos_encontrados == n):
                print("(SOR) Resultado encontrado com o critério de parada, na iteração:", k)
                return vetor_x

        vetor_x = np.copy(vetor_aux) # Atualiza o vetor_x (k) com os valores do vetor_aux (k+1)

    print("(SOR) Resultado encontrado com o número máximo de iterações.")
    return vetor_x # Retorna o vetor solução
```

```python
a = np.array([[2.,1.,0.],[1.,2.,-1.],[0.,-1.,2.]])
```

```python
b = np.array([[3.],[0.],[1.]])
```

```python
x = np.array([[0.],[0.],[0.]])
```

```python
print(jacobi(a, b, x))
print(seidel(a, b, x))
print(sor(a, b, x))
```

    ATENÇÃO: O critério das linhas não foi satisfeito.
    (Jacobi) Resultado encontrado com o critério de parada, na iteração: 99
    [[ 2.0000000e+00]
     [-1.0000000e+00]
     [ 8.8817842e-16]]
    ATENÇÃO: O critério das linhas não foi satisfeito.
    Maior betão: 2.25
    ATENÇÃO: O critério de Sassenfeld não foi satisfeito.
    (Seidel) Resultado encontrado com o critério de parada, na iteração: 52
    [[ 2.]
     [-1.]
     [ 0.]]
    omega: 1.17157287525381
    ATENÇÃO: O critério das linhas não foi satisfeito.
    Maior betão: 2.25
    ATENÇÃO: O critério de Sassenfeld não foi satisfeito.
    (SOR) Resultado encontrado com o critério de parada, na iteração: 424
    [[ 2.]
     [-1.]
     [ 0.]]

---
### 5) Implemente um algoritmo que leia uma matriz A (de uma planilha excel) e:

```python
def ler_matriz_A(file_matrix, sheet_matriz):
    df_matriz = pd.read_excel(r"dados/"+file_matrix+".xlsx", header=None, sheet_name=sheet_matriz) # Le o arquivo do excel que contém a matriz A sem cabeçalho
    matriz = np.array(df_matriz, dtype='f8') # Converte a matriz A em um array da numpy
    return matriz
```

```python
matriz = ler_matriz_A("prova_ALC_testes", "matriz_A(2)")
```

**a)** Calcule a transposta de A;

```python
def calcula_transposta(matriz_A):
    if not verifica_quadrada(matriz_A):
        print("A matriz não é quadrada.")
        return None

    n = len(matriz_A)
    matriz_At = np.zeros((n,n), dtype='f8')

    for i in range(n):
        for j in range(n):
            if i == j:
                matriz_At[i,j] = matriz_A[i,j]
            else:
                matriz_At[i,j] = matriz_A[j,i]

    return matriz_At

```

```python
Matrix(calcula_transposta(matriz))
```

$\displaystyle \left[\begin{array}{ccccccccccccccccccccccccc}91452.0 & 55679.0 & 74196.0 & 50369.0 & 65933.0 & 58983.0 & 57282.0 & 86619.0 & 77605.0 & 58674.0 & 59851.0 & 64857.0 & 69439.0 & 55411.0 & 53319.0 & 66426.0 & 58593.0 & 64599.0 & 76799.0 & 73032.0 & 64518.0 & 48233.0 & 66645.0 & 60829.0 & 66596.0\\55679.0 & 65707.0 & 74216.0 & 37748.0 & 57071.0 & 53220.0 & 58214.0 & 72412.0 & 57615.0 & 47372.0 & 45828.0 & 51951.0 & 57106.0 & 52960.0 & 49850.0 & 52216.0 & 59587.0 & 62269.0 & 69177.0 & 70313.0 & 58249.0 & 50165.0 & 52239.0 & 53685.0 & 50243.0\\74196.0 & 74216.0 & 106855.0 & 53876.0 & 66014.0 & 75409.0 & 71392.0 & 100305.0 & 77671.0 & 61476.0 & 67259.0 & 69436.0 & 74697.0 & 67445.0 & 60738.0 & 74261.0 & 75472.0 & 80328.0 & 86587.0 & 88406.0 & 76591.0 & 70006.0 & 64575.0 & 66976.0 & 70245.0\\50369.0 & 37748.0 & 53876.0 & 48748.0 & 41012.0 & 44969.0 & 38317.0 & 58496.0 & 47805.0 & 41115.0 & 42991.0 & 51213.0 & 52266.0 & 45265.0 & 37281.0 & 46016.0 & 46906.0 & 45027.0 & 47163.0 & 46086.0 & 51440.0 & 37987.0 & 45395.0 & 38554.0 & 50585.0\\65933.0 & 57071.0 & 66014.0 & 41012.0 & 79341.0 & 54990.0 & 48071.0 & 73169.0 & 68617.0 & 48358.0 & 52529.0 & 59419.0 & 60988.0 & 49604.0 & 45862.0 & 47887.0 & 60380.0 & 61638.0 & 70678.0 & 67484.0 & 64183.0 & 40100.0 & 66726.0 & 61053.0 & 63560.0\\58983.0 & 53220.0 & 75409.0 & 44969.0 & 54990.0 & 81033.0 & 55404.0 & 71312.0 & 69545.0 & 39659.0 & 56638.0 & 54765.0 & 62119.0 & 48354.0 & 51622.0 & 53278.0 & 55752.0 & 58712.0 & 72928.0 & 68654.0 & 62901.0 & 54749.0 & 59374.0 & 48251.0 & 57492.0\\57282.0 & 58214.0 & 71392.0 & 38317.0 & 48071.0 & 55404.0 & 70860.0 & 66195.0 & 58152.0 & 40066.0 & 44798.0 & 44856.0 & 62974.0 & 56700.0 & 47595.0 & 56390.0 & 56159.0 & 62644.0 & 68640.0 & 62619.0 & 60449.0 & 54762.0 & 46380.0 & 45245.0 & 46442.0\\86619.0 & 72412.0 & 100305.0 & 58496.0 & 73169.0 & 71312.0 & 66195.0 & 119662.0 & 78127.0 & 73439.0 & 65880.0 & 71849.0 & 76429.0 & 72657.0 & 66130.0 & 81352.0 & 70572.0 & 85049.0 & 91567.0 & 92798.0 & 77968.0 & 65462.0 & 72862.0 & 71505.0 & 78249.0\\77605.0 & 57615.0 & 77671.0 & 47805.0 & 68617.0 & 69545.0 & 58152.0 & 78127.0 & 95010.0 & 47967.0 & 63304.0 & 70526.0 & 70080.0 & 49868.0 & 55068.0 & 65282.0 & 62660.0 & 63675.0 & 80032.0 & 80175.0 & 70347.0 & 53446.0 & 69325.0 & 64456.0 & 68950.0\\58674.0 & 47372.0 & 61476.0 & 41115.0 & 48358.0 & 39659.0 & 40066.0 & 73439.0 & 47967.0 & 62381.0 & 48742.0 & 46488.0 & 55011.0 & 55790.0 & 41509.0 & 57698.0 & 43944.0 & 51390.0 & 56613.0 & 59549.0 & 58542.0 & 38889.0 & 49767.0 & 48121.0 & 57648.0\\59851.0 & 45828.0 & 67259.0 & 42991.0 & 52529.0 & 56638.0 & 44798.0 & 65880.0 & 63304.0 & 48742.0 & 61735.0 & 54519.0 & 62740.0 & 45391.0 & 42671.0 & 55453.0 & 48817.0 & 52109.0 & 58966.0 & 62451.0 & 59620.0 & 44416.0 & 54339.0 & 47345.0 & 60586.0\\64857.0 & 51951.0 & 69436.0 & 51213.0 & 59419.0 & 54765.0 & 44856.0 & 71849.0 & 70526.0 & 46488.0 & 54519.0 & 86233.0 & 69696.0 & 46373.0 & 53911.0 & 56829.0 & 62156.0 & 61194.0 & 64494.0 & 72696.0 & 64713.0 & 41217.0 & 62938.0 & 64332.0 & 63511.0\\69439.0 & 57106.0 & 74697.0 & 52266.0 & 60988.0 & 62119.0 & 62974.0 & 76429.0 & 70080.0 & 55011.0 & 62740.0 & 69696.0 & 88343.0 & 63047.0 & 58429.0 & 70516.0 & 57778.0 & 72127.0 & 71249.0 & 73181.0 & 68927.0 & 54201.0 & 61029.0 & 56029.0 & 63691.0\\55411.0 & 52960.0 & 67445.0 & 45265.0 & 49604.0 & 48354.0 & 56700.0 & 72657.0 & 49868.0 & 55790.0 & 45391.0 & 46373.0 & 63047.0 & 72923.0 & 41999.0 & 58256.0 & 51047.0 & 65167.0 & 62432.0 & 56009.0 & 60077.0 & 52371.0 & 51691.0 & 49725.0 & 51968.0\\53319.0 & 49850.0 & 60738.0 & 37281.0 & 45862.0 & 51622.0 & 47595.0 & 66130.0 & 55068.0 & 41509.0 & 42671.0 & 53911.0 & 58429.0 & 41999.0 & 59338.0 & 50999.0 & 44441.0 & 59090.0 & 56983.0 & 62862.0 & 52121.0 & 38740.0 & 50123.0 & 46163.0 & 44724.0\\66426.0 & 52216.0 & 74261.0 & 46016.0 & 47887.0 & 53278.0 & 56390.0 & 81352.0 & 65282.0 & 57698.0 & 55453.0 & 56829.0 & 70516.0 & 58256.0 & 50999.0 & 80679.0 & 50279.0 & 64435.0 & 68755.0 & 68819.0 & 65497.0 & 57893.0 & 47295.0 & 48008.0 & 62149.0\\58593.0 & 59587.0 & 75472.0 & 46906.0 & 60380.0 & 55752.0 & 56159.0 & 70572.0 & 62660.0 & 43944.0 & 48817.0 & 62156.0 & 57778.0 & 51047.0 & 44441.0 & 50279.0 & 79370.0 & 64060.0 & 67823.0 & 72858.0 & 67075.0 & 53125.0 & 64238.0 & 54453.0 & 53791.0\\64599.0 & 62269.0 & 80328.0 & 45027.0 & 61638.0 & 58712.0 & 62644.0 & 85049.0 & 63675.0 & 51390.0 & 52109.0 & 61194.0 & 72127.0 & 65167.0 & 59090.0 & 64435.0 & 64060.0 & 89614.0 & 69058.0 & 75253.0 & 60567.0 & 60826.0 & 65264.0 & 56169.0 & 48050.0\\76799.0 & 69177.0 & 86587.0 & 47163.0 & 70678.0 & 72928.0 & 68640.0 & 91567.0 & 80032.0 & 56613.0 & 58966.0 & 64494.0 & 71249.0 & 62432.0 & 56983.0 & 68755.0 & 67823.0 & 69058.0 & 95485.0 & 87216.0 & 73030.0 & 57886.0 & 67621.0 & 63402.0 & 68153.0\\73032.0 & 70313.0 & 88406.0 & 46086.0 & 67484.0 & 68654.0 & 62619.0 & 92798.0 & 80175.0 & 59549.0 & 62451.0 & 72696.0 & 73181.0 & 56009.0 & 62862.0 & 68819.0 & 72858.0 & 75253.0 & 87216.0 & 104616.0 & 75308.0 & 57730.0 & 72067.0 & 66660.0 & 64582.0\\64518.0 & 58249.0 & 76591.0 & 51440.0 & 64183.0 & 62901.0 & 60449.0 & 77968.0 & 70347.0 & 58542.0 & 59620.0 & 64713.0 & 68927.0 & 60077.0 & 52121.0 & 65497.0 & 67075.0 & 60567.0 & 73030.0 & 75308.0 & 87788.0 & 51382.0 & 64059.0 & 61285.0 & 75877.0\\48233.0 & 50165.0 & 70006.0 & 37987.0 & 40100.0 & 54749.0 & 54762.0 & 65462.0 & 53446.0 & 38889.0 & 44416.0 & 41217.0 & 54201.0 & 52371.0 & 38740.0 & 57893.0 & 53125.0 & 60826.0 & 57886.0 & 57730.0 & 51382.0 & 63215.0 & 41599.0 & 39606.0 & 42276.0\\66645.0 & 52239.0 & 64575.0 & 45395.0 & 66726.0 & 59374.0 & 46380.0 & 72862.0 & 69325.0 & 49767.0 & 54339.0 & 62938.0 & 61029.0 & 51691.0 & 50123.0 & 47295.0 & 64238.0 & 65264.0 & 67621.0 & 72067.0 & 64059.0 & 41599.0 & 81339.0 & 59117.0 & 58287.0\\60829.0 & 53685.0 & 66976.0 & 38554.0 & 61053.0 & 48251.0 & 45245.0 & 71505.0 & 64456.0 & 48121.0 & 47345.0 & 64332.0 & 56029.0 & 49725.0 & 46163.0 & 48008.0 & 54453.0 & 56169.0 & 63402.0 & 66660.0 & 61285.0 & 39606.0 & 59117.0 & 70832.0 & 60604.0\\66596.0 & 50243.0 & 70245.0 & 50585.0 & 63560.0 & 57492.0 & 46442.0 & 78249.0 & 68950.0 & 57648.0 & 60586.0 & 63511.0 & 63691.0 & 51968.0 & 44724.0 & 62149.0 & 53791.0 & 48050.0 & 68153.0 & 64582.0 & 75877.0 & 42276.0 & 58287.0 & 60604.0 & 82109.0\end{array}\right]$

**b)** Calcule o determinante de A;

```python
def eliminacao_gauss(matriz_A):
    n = len(matriz_A)
    Am = np.array(matriz_A).astype("float64")

    for i in range(n):
        for j in range(i+1, n):
            if Am[i,i] == 0:
                Am[i,i] = 1e-18

            x = Am[j,i]/Am[i,i] # x é o multiplicador necessário para executar a eliminação
            for k in range(n):
               Am[j,k] -= Am[i,k] * x # aplica o multiplicador x para zerar os elementos abaixo da diagonal principal

    return calcula_determinante(Am, n) # retorna o vetor resultado

def calcula_determinante(matriz_A, n):
    det = 1
    for i in range(n):
        det *= matriz_A[i,i]
    return det

```

```python
eliminacao_gauss(matriz)
```

**c)** Calcule a matriz adjunta de A;

```python
def calcula_adjunta(matriz_A):
    if verifica_inversa(matriz_A):
        matriz_Adj = np.linalg.inv(matriz_A) * eliminacao_gauss(matriz_A)
        return matriz_Adj
```

```python
print(calcula_adjunta(matriz))
```

    A matriz é quadrada
    O determinante da matriz é: 1.809225640752816e+97
    [[ 2.18976648e+96  3.29994217e+96  5.58389985e+95  2.92783527e+95
      -2.03534984e+96 -2.36899618e+96 -5.50695114e+96  5.56505936e+95
      -3.82070029e+95 -5.35550547e+96  1.49829376e+96 -1.61176699e+96
       1.73225255e+96  1.54159730e+96  3.86518043e+95 -1.03851399e+96
      -1.03318594e+96  3.84136131e+95  2.15896487e+96 -5.31253148e+95
       3.99371498e+96  4.93413144e+95  6.98444992e+94 -3.03110404e+95
      -8.49040544e+95]
     [ 3.29994217e+96  4.98290950e+96  8.39954897e+95  4.43369933e+95
      -3.07182723e+96 -3.57137721e+96 -8.30586076e+96  8.42264733e+95
      -5.74222763e+95 -8.07654383e+96  2.26034329e+96 -2.43160828e+96
       2.61440020e+96  2.32211712e+96  5.78620430e+95 -1.56446934e+96
      -1.55871781e+96  5.80478295e+95  3.25585364e+96 -8.03621022e+95
       6.02497461e+96  7.40936689e+95  1.06077010e+95 -4.55738134e+95
      -1.28356107e+96]
     [ 5.58389985e+95  8.39954897e+95  1.46970788e+95  7.58187475e+94
      -5.17157626e+95 -6.05386182e+95 -1.40396100e+96  1.40097624e+95
      -9.81748804e+94 -1.36586070e+96  3.79552354e+95 -4.10949870e+95
       4.42039654e+95  3.92767708e+95  9.88910370e+94 -2.64190017e+95
      -2.65942854e+95  9.62952834e+94  5.50215187e+95 -1.34673858e+95
       1.01850705e+96  1.26762941e+95  2.01771484e+94 -7.79358071e+94
      -2.16839447e+95]
     [ 2.92783527e+95  4.43369933e+95  7.58187475e+94  4.58038632e+94
      -2.72962739e+95 -3.18733566e+95 -7.38671835e+95  7.30265511e+94
      -5.19362070e+94 -7.18343220e+95  2.00610517e+95 -2.18400931e+95
       2.32342773e+95  2.03108995e+95  4.99338622e+94 -1.38252449e+95
      -1.41493347e+95  5.34384453e+94  2.91933196e+95 -7.08999975e+94
       5.36874118e+95  6.57232328e+94  1.01775014e+94 -3.80500413e+94
      -1.15866344e+95]
     [-2.03534984e+96 -3.07182723e+96 -5.17157626e+95 -2.72962739e+95
       1.89906155e+96  2.20360327e+96  5.12389714e+96 -5.18181279e+95
       3.52662809e+95  4.97753565e+96 -1.39306724e+96  1.50054559e+96
      -1.61415227e+96 -1.42927771e+96 -3.56935914e+95  9.70109445e+95
       9.60132651e+95 -3.63289134e+95 -2.01245774e+96  4.97014091e+95
      -3.71606201e+96 -4.58408829e+95 -6.35382105e+94  2.81091830e+95
       7.87742552e+95]
     [-2.36899618e+96 -3.57137721e+96 -6.05386182e+95 -3.18733566e+95
       2.20360327e+96  2.56877123e+96  5.96220571e+96 -6.02374768e+95
       4.13359707e+95  5.79776975e+96 -1.62316672e+96  1.74499465e+96
      -1.87575009e+96 -1.66723623e+96 -4.19759224e+95  1.12666826e+96
       1.12046173e+96 -4.16113562e+95 -2.34105351e+96  5.76036517e+95
      -4.32489238e+96 -5.37173151e+95 -7.66356006e+94  3.28352177e+95
       9.19512046e+95]
     [-5.50695114e+96 -8.30586076e+96 -1.40396100e+96 -7.38671835e+95
       5.12389714e+96  5.96220571e+96  1.38606328e+97 -1.40225121e+96
       9.59233926e+95  1.34757179e+97 -3.77152498e+96  4.05771520e+96
      -4.36161389e+96 -3.87669505e+96 -9.71450649e+95  2.61399557e+96
       2.59937961e+96 -9.68520755e+95 -5.43653637e+96  1.33958148e+96
      -1.00518673e+97 -1.24079812e+96 -1.75262967e+95  7.61076386e+95
       2.13885366e+96]
     [ 5.56505936e+95  8.42264733e+95  1.40097624e+95  7.30265511e+94
      -5.18181279e+95 -6.02374768e+95 -1.40225121e+96  1.46862125e+95
      -9.61482337e+94 -1.36546482e+96  3.83751885e+95 -4.09833815e+95
       4.42167044e+95  3.93669270e+95  9.73064165e+94 -2.63232484e+95
      -2.61968508e+95  9.49822761e+94  5.47755925e+95 -1.36770882e+95
       1.01862060e+96  1.24009257e+95  1.78160539e+94 -7.69947548e+94
      -2.20177355e+95]
     [-3.82070029e+95 -5.74222763e+95 -9.81748804e+94 -5.19362070e+94
       3.52662809e+95  4.13359707e+95  9.59233926e+95 -9.61482337e+94
       6.96344917e+94  9.36325182e+95 -2.62509625e+95  2.80864668e+95
      -3.00281027e+95 -2.70041549e+95 -6.85582152e+94  1.78321131e+95
       1.80928039e+95 -6.48320641e+94 -3.75837012e+95  9.12374774e+94
      -6.95509182e+95 -8.62722678e+94 -1.35268798e+94  5.20035093e+94
       1.48651533e+95]
     [-5.35550547e+96 -8.07654383e+96 -1.36586070e+96 -7.18343220e+95
       4.97753565e+96  5.79776975e+96  1.34757179e+97 -1.36546482e+96
       9.36325182e+95  1.31143223e+97 -3.67205086e+96  3.94544911e+96
      -4.23699391e+96 -3.77732453e+96 -9.46905416e+95  2.53360631e+96
       2.52776567e+96 -9.34173064e+95 -5.28058717e+96  1.29899888e+96
      -9.77272504e+96 -1.20339271e+96 -1.73143840e+95  7.39381272e+95
       2.08258457e+96]
     [ 1.49829376e+96  2.26034329e+96  3.79552354e+95  2.00610517e+95
      -1.39306724e+96 -1.62316672e+96 -3.77152498e+96  3.83751885e+95
      -2.62509625e+95 -3.67205086e+96  1.03361014e+96 -1.10401978e+96
       1.18382626e+96  1.05884785e+96  2.66396770e+95 -7.08444645e+95
      -7.05810588e+95  2.60866895e+95  1.47891033e+96 -3.64363760e+95
       2.73542029e+96  3.36018409e+95  4.69179118e+94 -2.05925727e+95
      -5.85062505e+95]
     [-1.61176699e+96 -2.43160828e+96 -4.10949870e+95 -2.18400931e+95
       1.50054559e+96  1.74499465e+96  4.05771520e+96 -4.09833815e+95
       2.80864668e+95  3.94544911e+96 -1.10401978e+96  1.19074119e+96
      -1.27740554e+96 -1.13387221e+96 -2.83955391e+95  7.63869018e+95
       7.60174718e+95 -2.84647832e+95 -1.59168968e+96  3.91675735e+95
      -2.94191110e+96 -3.61433068e+95 -5.11361911e+94  2.20704557e+95
       6.25813808e+95]
     [ 1.73225255e+96  2.61440020e+96  4.42039654e+95  2.32342773e+95
      -1.61415227e+96 -1.87575009e+96 -4.36161389e+96  4.42167044e+95
      -3.00281027e+95 -4.23699391e+96  1.18382626e+96 -1.27740554e+96
       1.37656189e+96  1.21620210e+96  3.03350950e+95 -8.25095112e+95
      -8.18215039e+95  3.06125922e+95  1.71147882e+96 -4.23059166e+95
       3.16398041e+96  3.90968073e+95  5.53596469e+94 -2.39388194e+95
      -6.73022398e+95]
     [ 1.54159730e+96  2.32211712e+96  3.92767708e+95  2.03108995e+95
      -1.42927771e+96 -1.66723623e+96 -3.87669505e+96  3.93669270e+95
      -2.70041549e+95 -3.77732453e+96  1.05884785e+96 -1.13387221e+96
       1.21620210e+96  1.09458670e+96  2.75889646e+95 -7.26002522e+95
      -7.24598497e+95  2.63952103e+95  1.51507581e+96 -3.71443625e+95
       2.80908667e+96  3.44723419e+95  4.93165241e+94 -2.14264460e+95
      -5.98868959e+95]
     [ 3.86518043e+95  5.78620430e+95  9.88910370e+94  4.99338622e+94
      -3.56935914e+95 -4.19759224e+95 -9.71450649e+95  9.73064165e+94
      -6.85582152e+94 -9.46905416e+95  2.66396770e+95 -2.83955391e+95
       3.03350950e+95  2.75889646e+95  7.38470855e+94 -1.83198880e+95
      -1.80664877e+95  6.48794879e+94  3.80851595e+95 -9.25830388e+94
       7.02495718e+95  8.90345709e+94  1.19213708e+94 -5.43834646e+94
      -1.48245092e+95]
     [-1.03851399e+96 -1.56446934e+96 -2.64190017e+95 -1.38252449e+95
       9.70109445e+95  1.12666826e+96  2.61399557e+96 -2.63232484e+95
       1.78321131e+95  2.53360631e+96 -7.08444645e+95  7.63869018e+95
      -8.25095112e+95 -7.26002522e+95 -1.83198880e+95  5.03534133e+95
       4.91306361e+95 -1.88515645e+95 -1.03056884e+96  2.54574947e+95
      -1.89671883e+96 -2.39559015e+95 -3.08459546e+94  1.46026549e+95
       3.98655190e+95]
     [-1.03318594e+96 -1.55871781e+96 -2.65942854e+95 -1.41493347e+95
       9.60132651e+95  1.12046173e+96  2.59937961e+96 -2.61968508e+95
       1.80928039e+95  2.52776567e+96 -7.05810588e+95  7.60174718e+95
      -8.18215039e+95 -7.24598497e+95 -1.80664877e+95  4.91306361e+95
       4.93455295e+95 -1.81544956e+95 -1.02097468e+96  2.50906429e+95
      -1.88813846e+96 -2.35510963e+95 -3.56352360e+94  1.43272953e+95
       4.02770602e+95]
     [ 3.84136131e+95  5.80478295e+95  9.62952834e+94  5.34384453e+94
      -3.63289134e+95 -4.16113562e+95 -9.68520755e+95  9.49822761e+94
      -6.48320641e+94 -9.34173064e+95  2.60866895e+95 -2.84647832e+95
       3.06125922e+95  2.63952103e+95  6.48794879e+94 -1.88515645e+95
      -1.81544956e+95  7.81124055e+94  3.85188292e+95 -9.53781591e+94
       7.02128451e+95  8.69369491e+94  9.26093837e+93 -5.27277257e+94
      -1.44088462e+95]
     [ 2.15896487e+96  3.25585364e+96  5.50215187e+95  2.91933196e+95
      -2.01245774e+96 -2.34105351e+96 -5.43653637e+96  5.47755925e+95
      -3.75837012e+95 -5.28058717e+96  1.47891033e+96 -1.59168968e+96
       1.71147882e+96  1.51507581e+96  3.80851595e+95 -1.03056884e+96
      -1.02097468e+96  3.85188292e+95  2.13972703e+96 -5.27547030e+95
       3.94355611e+96  4.89901818e+95  6.76881886e+94 -2.98059398e+95
      -8.37026029e+95]
     [-5.31253148e+95 -8.03621022e+95 -1.34673858e+95 -7.08999975e+94
       4.97014091e+95  5.76036517e+95  1.33958148e+96 -1.36770882e+95
       9.12374774e+94  1.29899888e+96 -3.64363760e+95  3.91675735e+95
      -4.23059166e+95 -3.71443625e+95 -9.25830388e+94  2.54574947e+95
       2.50906429e+95 -9.53781591e+94 -5.27547030e+95  1.32868446e+95
      -9.72927167e+95 -1.20332439e+95 -1.65666869e+94  7.32709579e+94
       2.07893147e+95]
     [ 3.99371498e+96  6.02497461e+96  1.01850705e+96  5.36874118e+95
      -3.71606201e+96 -4.32489238e+96 -1.00518673e+97  1.01862060e+96
      -6.95509182e+95 -9.77272504e+96  2.73542029e+96 -2.94191110e+96
       3.16398041e+96  2.80908667e+96  7.02495718e+95 -1.89671883e+96
      -1.88813846e+96  7.02128451e+95  3.94355611e+96 -9.72927167e+95
       7.29439403e+96  9.00799205e+95  1.28130517e+95 -5.51918135e+95
      -1.55514582e+96]
     [ 4.93413144e+95  7.40936689e+95  1.26762941e+95  6.57232328e+94
      -4.58408829e+95 -5.37173151e+95 -1.24079812e+96  1.24009257e+95
      -8.62722678e+94 -1.20339271e+96  3.36018409e+95 -3.61433068e+95
       3.90968073e+95  3.44723419e+95  8.90345709e+94 -2.39559015e+95
      -2.35510963e+95  8.69369491e+94  4.89901818e+95 -1.20332439e+95
       9.00799205e+95  1.18476485e+95  1.64554514e+94 -7.00550870e+94
      -1.89540275e+95]
     [ 6.98444992e+94  1.06077010e+95  2.01771484e+94  1.01775014e+94
      -6.35382105e+94 -7.66356006e+94 -1.75262967e+95  1.78160539e+94
      -1.35268798e+94 -1.73143840e+95  4.69179118e+94 -5.11361911e+94
       5.53596469e+94  4.93165241e+94  1.19213708e+94 -3.08459546e+94
      -3.56352360e+94  9.26093837e+93  6.76881886e+94 -1.65666869e+94
       1.28130517e+95  1.64554514e+94  6.19513022e+93 -9.91850230e+93
      -2.87199985e+94]
     [-3.03110404e+95 -4.55738134e+95 -7.79358071e+94 -3.80500413e+94
       2.81091830e+95  3.28352177e+95  7.61076386e+95 -7.69947548e+94
       5.20035093e+94  7.39381272e+95 -2.05925727e+95  2.20704557e+95
      -2.39388194e+95 -2.14264460e+95 -5.43834646e+94  1.46026549e+95
       1.43272953e+95 -5.27277257e+94 -2.98059398e+95  7.32709579e+94
      -5.51918135e+95 -7.00550870e+94 -9.91850230e+93  4.56501078e+94
       1.15281685e+95]
     [-8.49040544e+95 -1.28356107e+96 -2.16839447e+95 -1.15866344e+95
       7.87742552e+95  9.19512046e+95  2.13885366e+96 -2.20177355e+95
       1.48651533e+95  2.08258457e+96 -5.85062505e+95  6.25813808e+95
      -6.73022398e+95 -5.98868959e+95 -1.48245092e+95  3.98655190e+95
       4.02770602e+95 -1.44088462e+95 -8.37026029e+95  2.07893147e+95
      -1.55514582e+96 -1.89540275e+95 -2.87199985e+94  1.15281685e+95
       3.40035651e+95]]

**d)** Calcule o traço de A;

```python
def traco(matriz_A):
    linhas, colunas = np.shape(matriz_A)
    n, traco = min((linhas, colunas)), 0

    for i in range(n):
        traco += matriz_A[i,i]
    return traco

```

```python
print("Traço de A:", traco(matriz))
```

    Traço de A: 2024668.0

**e)** Avalie se A é simétrica;

```python
def verifica_simetria(matriz_A):
    if verifica_quadrada(matriz_A):
        n = len(matriz_A)
        for i in range(n):
            for j in range(n):
                if (i != j) and (matriz_A[i,j] != matriz_A[j,i]):
                    print("A matriz não é simétrica")
                    return False
        print("A matriz é simétrica")
        return True
    print("A matriz não é quadrada")
    return False
```

```python
verifica_simetria(matriz)
```

    A matriz é simétrica

    True

**f)** Avalie se A é ortogonal;

```python
def verifica_ortogonal(matriz_A):
    if not verifica_quadrada:
        print("A matriz não é quadrada.")
        return False

    if not verifica_inversa:
        print("A matriz é singular.")
        return False

    n = len(matriz_A)
    matriz_At = calcula_transposta(matriz_A)
    matriz_Id = np.identity(n)

    matriz_Resultado = np.matmul(matriz_A, matriz_At)

    if (matriz_Resultado-matriz_Id).all() <= 1e-10:
        print("A matriz é ortogonal.")
        return True
    else:
        print("A matriz não é ortogonal.")
        return False
```

```python
verifica_ortogonal(matriz)
```

    A matriz não é ortogonal.

    False

**g)** Calcule a norma de linha, norma de Frobenius e norma coluna de A.

```python
def soma_linha(matriz_A): # norma-infinita matricial
    n, max, x = len(matriz_A), 0, 0

    for i in range(n):
        for j in range(n):
            x += math.fabs(matriz_A[i,j])

        if x > max:
            max = x

    return max
```

```python
soma_linha(matriz)
```

```python
def frobenius(matriz_A): # norma-2 matricial
    n, x = len(matriz_A), 0

    for i in range(n):
        for j in range(n):
            x += math.pow(math.fabs(matriz_A[i,j]), 2)

    return x ** (1/2)
```

```python
frobenius(matriz)
```

```python
def soma_coluna(matriz_A): # norma-1 matricial
    n, max, x = len(matriz_A), 0, 0

    for j in range(n):
        for i in range(n):
            x += math.fabs(matriz_A[i,j])

        if x > max:
            max = x

    return max
```

```python
soma_coluna(matriz)
```

**h)** Avalie se A é positiva definida.

```python
def checa_positiva_definida(matriz_A, debug=False):
    if not verifica_inversa or not verifica_quadrada or not verifica_simetria:
        return False

    n = len(matriz_A) # n é a ordem da matriz a
    matriz_R = np.copy(np.triu(matriz_A)) # matriz_r é uma cópia triangular superior da matriz A

    for i in range(n):
            for k in range(i): # Não é executado quando i = 0
                matriz_R[i,i] = matriz_R[i,i] - math.pow(matriz_R[k,i],2) # Define os termos da diagonal principal
            if matriz_R[i,i] <= 0: # Se algum elemento da diagonal principal for menor ou igual a zero, por definição, a matriz não é positiva definida
                if debug:
                    print("Erro: A matriz não é positiva definida.")
                return False
            matriz_R[i,i] = math.sqrt(matriz_R[i,i])
            for j in range(i+1,n): # Não é executado quando i = n
                for k in range(i): # Não é executado quando i = 0
                    matriz_R[i,j] = matriz_R[i,j] - (matriz_R[k,i] * matriz_R[k,j])
                matriz_R[i,j] = matriz_R[i,j]/matriz_R[i,i]
    return True
```

```python
checa_positiva_definida(matriz)
```

    True

---
### 6) Construa um algoritmo que receba dois vetores x e y e uma matriz A positiva definida (de uma planilha excel). Calcule:

```python
def ler_matriz_A_vetor_x_vetor_y(file_name, file_matrix, file_array_x, file_array_y):
    df_matriz = pd.read_excel(r"dados/"+file_name+".xlsx", header=None, sheet_name=file_matrix) # Le o arquivo do excel que contém a matriz A sem cabeçalho
    df_vetor_x = pd.read_excel(r"dados/"+file_name+".xlsx", header=None, sheet_name=file_array_x) # Le o arquivo do excel que contém o vetor x sem cabeçalho
    df_vetor_y = pd.read_excel(r"dados/"+file_name+".xlsx", header=None, sheet_name=file_array_y) # Le o arquivo do excel que contém o vetor y sem cabeçalho
    matriz = np.array(df_matriz, dtype='f8') # Converte a matriz A em um array da numpy
    vetor_x = np.ravel(np.array(df_vetor_x, dtype='f8')) # Converte o vetor b em um array da numpy
    vetor_y = np.ravel(np.array(df_vetor_y, dtype='f8')) # Converte o vetor b em um array da numpy
    return matriz, vetor_x, vetor_y

```

```python
matriz, vetor_x, vetor_y = ler_matriz_A_vetor_x_vetor_y("prova_ALC_testes", "matriz_A(2)", "vetor_b(1)", "vetor_b(1)")
```

**a)** Norma 1, norma 2 e norma infinito de x e y.

```python
def manhattan(vetor): # norma-1 vetorial
    n, x = len(vetor), 0

    for i in range(n):
        x += np.fabs(vetor[i])
    return x
```

```python
print(manhattan(vetor_x))
print(manhattan(vetor_y))
```

    26072.0
    26072.0

```python
def euclidiana(vetor): # norma-2 vetorial
    n, x = len(vetor), 0

    for i in range(n):
        x += np.fabs(vetor[i]) ** 2

    return x ** (1/2)
```

```python
print(euclidiana(vetor_x))
print(euclidiana(vetor_y))
```

    5264.014627639251
    5264.014627639251

```python
def infinita(vetor): # norma-infinita vetorial
    n, max = len(vetor), vetor[0]

    for i in range(1, n):
        if np.fabs(vetor[i]) > max:
            max = np.fabs(vetor[i])

    return max
```

```python
print(infinita(vetor_x))
print(infinita(vetor_y))
```

    1291.0
    1291.0

**b)** Ângulo entre x e y;

```python
def produto_interno(vetor_x, vetor_y):
    n, p_interno = len(vetor_x), 0

    for i in range(n):
        p_interno += vetor_x[i] * vetor_y[i]

    return p_interno

def angulo_vetores(vetor_x, vetor_y):
    n = len(vetor_x)

    p_interno = produto_interno(vetor_x, vetor_y)
    norma_x = euclidiana(vetor_x)
    norma_y = euclidiana(vetor_y)
    p_normas = norma_x * norma_y

    div = p_interno/p_normas

    return np.arccos(div)
```

```python
angulo_vetores(vetor_x, vetor_y)
```

**c)** Norma de x e y induzida por A;

```python
def matricial_induzida(matriz_A, vetor_b, norma=manhattan):

    if norma == manhattan:
        mat = np.matmul(matriz_A, vetor_b)
        return manhattan(mat)/manhattan(b)

    elif norma == euclidiana:
        mat = np.matmul(matriz_A, vetor_b)
        return euclidiana(mat)/euclidiana(mat)

    elif norma == infinita:
        mat = np.matmul(matriz_A, vetor_b)
        return infinita(mat)/infinita(mat)
```

```python
matricial_induzida(matriz, vetor_x)
```

    array([9.86160695e+09])

```python
a = np.array([[3,2,1],[1,4,2],[2,1,3]])
```

**d)** Produto interno de x e y.

```python
def produto_interno(vetor_x, vetor_y):
    n, p_interno = len(vetor_x), 0

    for i in range(n):
        p_interno += vetor_x[i] * vetor_y[i]

    return p_interno
```

```python
produto_interno(vetor_x, vetor_y)
```

---
### 7) Implemente um algoritmo que leia uma matriz A e um vetor b (de uma planilha excel). Encontre a fatoração LU de A.

```python
matriz, vetor = ler_matriz_A_vetor_b("prova_ALC_testes", "matriz_A(1)", "vetor_b(1)")
```

```python
fatoracaoLU(matriz,vetor)
```

    array([ 218.15690039, -134.91978861, -228.57366314,  196.17807308,
            -23.10822144,  120.75613378,   76.57658357,   32.56339464,
            112.28655838, -311.76230605,    9.73618607,  179.86106078,
             37.55533057,   -5.28610392,   20.3176418 ,  120.6584339 ,
            184.2906244 ,   34.16170845, -178.63243142,  -42.98537213,
           -108.87178633,  -70.23227857,  168.6609164 , -259.13412099,
           -164.019284  ])

---
### 8) Construa um algoritmo que receba uma matriz A positiva (de uma planilha excel). Calcule:

```python
matriz = ler_matriz_A("prova_ALC_testes", "matriz_A(2)")
```

**a)** O número condição;

```python
def condition_number(matriz):
    inv = np.linalg.inv(matriz)
    kond = frobenius(matriz) * frobenius(inv)
    print("Número condição de {} significa que, para um sistema linear esta matriz, o erro esperado causado por uma perturbação em b vai será de aproximadamente {}".format(kond, kond))
    return kond
```

**b)** Retorne uma mensagem ao usuário sobre sua interpretação.

```python
condition_number(matriz)
```

    Número condição de 1543177713.9375234 significa que, para um sistema linear esta matriz, o erro esperado causado por uma perturbação em b vai será de aproximadamente 1543177713.9375234

---
### 9) Construa um algoritmo que receba três matrizes: U, sigma e V (de uma planilha excel) e:

```python
def ler_matrizes_USV(file_excel):
    df_matriz_U = pd.read_excel(r"dados/"+file_excel+".xlsx", header=None, sheet_name='matriz_U') # Le o arquivo do excel que contém a matriz A sem cabeçalho
    df_matriz_S = pd.read_excel(r"dados/"+file_excel+".xlsx", header=None, sheet_name='matriz_S') # Le o arquivo do excel que contém o vetor x sem cabeçalho
    df_matriz_V = pd.read_excel(r"dados/"+file_excel+".xlsx", header=None, sheet_name='matriz_V') # Le o arquivo do excel que contém o vetor y sem cabeçalho
    matriz_U = np.array(df_matriz_U, dtype='f8') # Converte a matriz A em um array da numpy
    matriz_S = np.array(df_matriz_S, dtype='f8') # Converte a matriz A em um array da numpy
    matriz_Vt = np.array(df_matriz_V, dtype='f8') # Converte a matriz A em um array da numpy
    return matriz_U, matriz_S, matriz_Vt

matriz_U, matriz_S, matriz_Vt = ler_matrizes_USV("prova_ALC_testes")
```

**a)** Avalie se o produto das três matrizes representa uma decomposição SVD de alguma matriz X;

**b)** Caso a resposta à pergunta anterior seja negativa, retorne ao usuário uma explicação justificando esse fato.

```python
# U e V ortogonais e S não-quadrada e diagonal
def verifica_diagonal(matriz):
    n, m = np.shape(matriz)

    for i in range(n):
        for j in range(m):
            if i != j and matriz[i,j] != 0:
                print("A matriz não é diagonal.")
                return False
    print("A matriz é diagonal.")
    return True

def verifica_SVD(matriz_U, matriz_S, matriz_Vt):
    print("Para as matrizes USVt serem a decomposição SVD de uma matriz X:\n-> U e Vt devem ser ortogonais.\n-> S deve ser diagonal não-quadrada.\n")
    print("Verificando se U é ortogonal...")
    if verifica_ortogonal(matriz_U):
        print("\nVerificando se Vt é ortogonal...")
        if verifica_ortogonal(matriz_Vt):
            print("\nVerificando se S é diagonal e não-quadrada...")
            if verifica_diagonal(matriz_S):
                nS, mS = np.shape(matriz_S)
                if nS != mS:
                    print("A matriz é não-quadrada.")
                    print("")
                    mU = len(matriz_U)
                    nVt = len(matriz_Vt)

                    if mU == nS and mS == nVt:
                        print("As matrizes são a decomposição SVD da matriz X abaixo:")
                        matriz_X = np.matmul(np.matmul(matriz_U, matriz_S), matriz_Vt)
                        print(matriz_X)

                        return True
                    else:
                        print("As matrizes não têm dimensões compatíveis para executar um produto de matrizes.")
                        return False
                else:
                    print("A matriz S é quadrada.")
                    return False
            else:
                print("A matriz S não é diagonal.")
                return False
        else:
            print("A matriz Vt não é ortogonal.")
            return False
    else:
        print("A matriz U não é ortogonal.")
        return False
```

```python
verifica_SVD(matriz_U, matriz_S, matriz_Vt)
```

    Para as matrizes USVt serem a decomposição SVD de uma matriz X:
    -> U e Vt devem ser ortogonais.
    -> S deve ser diagonal não-quadrada.

    Verificando se U é ortogonal...
    A matriz é ortogonal.

    Verificando se Vt é ortogonal...
    A matriz não é ortogonal.
    A matriz Vt não é ortogonal.

    False

---
### 10) Construa um algoritmo que leia uma planilha em Excel com 4 colunas e calcule a estatística de Benford (para 1 dígito) para cada uma das colunas.

```python
benford(ler_dados_benford("dados_benford"))
```

    Porcentagem de ocorrência de "1" em cada coluna: 10.6692% | 11.1430% | 11.8036% | 10.9420% |
    Porcentagem de ocorrência de "2" em cada coluna: 11.1574% | 11.0281% | 10.8271% | 11.3297% |
    Porcentagem de ocorrência de "3" em cada coluna: 11.3297% | 10.8415% | 11.3441% | 11.2005% |
    Porcentagem de ocorrência de "4" em cada coluna: 11.3584% | 10.5112% | 10.3676% | 10.7553% |
    Porcentagem de ocorrência de "5" em cada coluna: 10.6117% | 11.1430% | 11.2866% | 11.0999% |
    Porcentagem de ocorrência de "6" em cada coluna: 11.2435% | 11.3584% | 10.8845% | 11.0425% |
    Porcentagem de ocorrência de "7" em cada coluna: 11.8323% | 11.1861% | 11.4733% | 10.7266% |
    Porcentagem de ocorrência de "8" em cada coluna: 11.0999% | 11.1717% | 11.4159% | 11.6743% |
    Porcentagem de ocorrência de "9" em cada coluna: 10.6979% | 11.6169% | 10.5974% | 11.2005% |


# Single perceptron

We want to implement a single layer perceptron to classify AND, OR and XOR using perceptron learning and delta rule.

In this particular case, we will use the same 4 sets of binary data to train the perceptron. We call this `epoch`.

For the OR case:

            inputs
            target

                0
                0
                0

                0
                1
                1

                1
                0
                1

                1
                1
                1

For the AND case:

            inputs
            target

                0
                0
                0

                0
                1
                0

                1
                0
                0

                1
                1
                1

And for the XOR case:

            inputs
            target

                0
                0
                0

                0
                1
                1

                1
                0
                1

                1
                1
                0

The perceptron will have two inputs $ x_0 $ and $ x_1 $ and an extra "bias" input $ x_2 = 1 $. There will be three weights $ w_0, w_1, w_2 $ and an output $ y $.

The inputs, outputs and bias are related by the relation:

$$ y = \mathbf{Wx}^\text{T} $$

where $ \mathbf{W} = [w_0, w_1, w_2] $ and $ \mathbf{x} = [x_0, x_1, x_2] $

This relation is derived from the following figure:

Explicitly:

$$ y = w_0 \cdot x_0 + w_1 \cdot x_1 + w_2 \cdot x_2 $$

This particular perceptron does not have a threshold unit. We will add a threshold unit to the output defined as:

$$
\text{sgn}(x) = \cases{
    -1, & if $x\lt0$\cr
    1, & if $x\gt0$ \cr
} = \frac{x}{|x|}
$$

We break the process into two steps: training and prediction. In the training phase we show the perceptron example data and expected outputs and let it adjust its weights. In the prediction phase we validate whether the perceptron was able to successfully classify the data.

The weights and biases form a plane in 3-dimensional space that bisects the 2d plane. The weights $ w_0 $ and $ w_1 $ define the normal of the plane (and thus its orientation) while the weight $ w_2 $ defines its translation from the origin.

```python
# import python packages

%matplotlib inline

from pylab import * # for plotting
from numpy import * # for matrix and numerical libraries

matplotlib.rcParams['figure.figsize'] = (10,5)
```

## Training

We will first implement the training step of the perceptron. The training step will accept the following parameters:

```
inputs (x) : the input patterns (a 2xn matrix containing n examples)
targets (t) : the target patterns (a 1xn vector)
learningRate (eta) : the step size of the gradient descent
epochs : number of times the input and target will be trained
```

### Perceptron learning

As stated earlier, the network learns by adjusting its weights. We need a strategy to update these weights. The simple strategy is to perform a forward pass

$$ y = \text{sgn}(\mathbf{Wx}^T) $$

and compute the error

$$ e = y - t $$

(where t is the target pattern) and calculating

$$ \Delta W = \eta \mathbf{x} e $$.

We can then update the weights with:

$$ \mathbf{W} = \mathbf{W} + \Delta W $$

This is called perceptron learning.

Let us create a simple training function.

```python
def train_pl(inputs, targets, eta=0.01, epochs=200):
    bias = ones((1, inputs.shape[1])) # create a 1xn vector of 1s as our bias
    X = vstack((inputs, bias)) # add bias to the inputs, resulting in a 3xn matrix

    weights = random.randn(1, X.shape[0]) # create a 1x3 vector of random weights
    error = zeros(epochs) # create a vector to store error computed at each epoch

    for i in range(epochs):
        err = (sign(weights * X) - targets)
        deltaW = -eta * err * X.T
        weights = weights + deltaW # update weights
        error[i] = mean(power(err, 2)) # calculate mean square error for each epoch

    return weights, error
```

The training function takes inputs and targets and returns weights of the trained network. It also returns an error so that we can observe the evolution of the network. Let us run it with a OR dataset, note that we replace 0 with a -1 because we want to make the inputs and targets symmetric (to prevent premature termination of learning).

Let us define a function that makes our binary values symmetric.

```python
def symmetric(v):
    """
        if v = 0:
            2 * (0 - 0.5) = 2 * (-0.5) = -1
        if v = 1
            2 * (1 - 0.5) = 2 * (0.5) = 1
    """
    return 2 * (v - 0.5)

OR_X = symmetric(matrix("""
0 1 0 1;
0 0 1 1
"""))

OR_T = symmetric(matrix("""
0 1 1 1
"""))

print(OR_X)
print(OR_T)
```

    [[-1.  1. -1.  1.]
     [-1. -1.  1.  1.]]
    [[-1.  1.  1.  1.]]

We are now ready to train our simple perceptron for the OR dataset.

```python
W, err = train_pl(OR_X, OR_T, eta=0.01, epochs=100) # perform training

# plot error
figure()
plot(err)
xlabel('epoch')
ylabel('mean square error')
title('MSE when training with pereptron learning')

```

Hmm ... we can see the the network was able to classify successfully. But the curve looks really strange, it is difficult to see how the network has drawn the decision boundary (the plane in 3-d space) from this view. A better way is to rearrange our output equation so that we can see the decision boundary in a more clear way.

Given that we have:

$$
\begin{align}
y & = w_0 x_0 + w_1 x_1 + w_2 x_2 \\\\
& = w_0 x_0 + w_1 x_1 + w_2 \ \text{(since $ x_2 = 1 $)}
\end{align}
$$

The definition of a plane is:

$$
0 = ax + by + c
$$

Where a, b and c are the normals of the plane. Since we want to find the plane defined by the weight matrices, we set $ y = 0 $.

Let us rearrange the equation in terms of $ x_0 $ (the decision is arbitrary, we could have chosen $ x_1 $ as well).

$$
\begin{align}
0 & = w_0 x_0 + w_1 x_1 + w_2 \\\\
w_0 x_0 + w_1 x_1 & = -w_2 \\\\
w_0 x_0 & = -w_1 x_1 - w_2 \\\\
x_0 & = -\frac{w_1}{w_0} x_1 - \frac{w_2}{w_0} \\\\
x_0 & = m x + b
\end{align}
$$

In order to draw the decision boundary we need to first plot our data points.

```python
pos = OR_X[:,where(OR_T > 0)[1]] # find points where T > 0
neg = OR_X[:,where(OR_T < 0)[1]] # find points where T < 0

figure()
scatter([pos[0,:]], [pos[1,:]], marker='o', c='g') # plot positive points with a green circle
scatter([neg[0,:]], [neg[1,:]], marker='x', c='r') # plot negative points with a red cross
xlim([-2, 2])
ylim([-2, 2])
grid()
```

Now we can calculate the decision boundary using the weights and plot it on top of our plot to get a better understanding of how the training went.

```python
x = linspace(-2, 2, 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary

figure()
scatter([pos[0,:]], [pos[1,:]], marker='o', c='g') # plot positive points with a green circle
scatter([neg[0,:]], [neg[1,:]], marker='x', c='r') # plot negative points with a red cross
plot(x, boundary)
xlim([-2, 2])
ylim([-2, 2])
grid()
```

It is now much easier to see the decision boundary. However, it is quite clear that this is not the optimal decision boundary and it looks as if the network stopped learning a bit early as the line is closer to the (1,-1) point than the (-1, 1) point. We know it is not an issue of not enough epochs because the MSE graph shows that the error converged to 0.

We need a better solution.

### Delta Rule (Widrow-Hoff rule)

When using perceptron learning we calculated the error by comparing the thresholded output to the targets. A better strategy is to use the unthresholded output to the target as this allows a much better estimate of the error surface allowing us to find the optimal decision boundary for the single-perceptron case.

We change the error function from:

$$
e = \text{sgn}(\mathbf{Wx}^T) - t
$$

to:

$$
e = \mathbf{Wx}^T - t
$$

This requires only a single change in our training function.

```python
def train_dr(inputs, targets, eta=0.01, epochs=200):
    bias = ones((1, inputs.shape[1])) # create a 1xn vector of 1s as our bias
    X = vstack((inputs, bias)) # add bias to the inputs, resulting in a 3xn matrix

    weights = random.randn(1, X.shape[0]) # create a 1x3 vector of random weights
    error = zeros(epochs) # create a vector to store error computed at each epoch

    for i in range(epochs):
        err = ((weights * X) - targets) # NOTE CHANGE HERE!!
        deltaW = -eta * err * X.T
        weights = weights + deltaW # update weights
        error[i] = mean(power(err, 2)) # calculate mean square error for each epoch

    return weights, error
```

Training and plotting the decision boundary and error curve:

```python
W, err = train_dr(OR_X, OR_T, eta=0.01, epochs=100) # perform training

# plot error
figure()
plot(err)
xlabel('epoch')
ylabel('mean square error')
title('MSE when training with delta rule')
```

We see that the error curve is much smoother because we are not thresholding the output.

```python
x = linspace(-2, 2, 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary

figure()
scatter([pos[0,:]], [pos[1,:]], marker='o', c='g') # plot positive points with a green circle
scatter([neg[0,:]], [neg[1,:]], marker='x', c='r') # plot negative points with a red cross
plot(x, boundary)
xlim([-2, 2])
ylim([-2, 2])
grid()
```

The decision boundary separates the two datasets well. Let us now use the network to classify the AND function.

```python
AND_X = symmetric(matrix("""
0 1 0 1;
0 0 1 1
"""))

AND_T = symmetric(matrix("""
0 0 0 1
"""))

pos = AND_X[:,where(AND_T > 0)[1]] # find points where T > 0
neg = AND_X[:,where(AND_T < 0)[1]] # find points where T < 0

print(AND_X)
print(AND_T)
```

    [[-1.  1. -1.  1.]
     [-1. -1.  1.  1.]]
    [[-1. -1. -1.  1.]]

```python
W, err = train_dr(AND_X, AND_T, eta=0.01, epochs=100) # perform training

x = linspace(-2, 2, 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary

# plot error
subplot(2, 1, 1)
plot(err)
xlabel('epoch')
ylabel('mean square error')
title('MSE when training with delta rule')

subplot(2, 1, 2)
scatter([pos[0,:]], [pos[1,:]], marker='o', c='g') # plot positive points with a green circle
scatter([neg[0,:]], [neg[1,:]], marker='x', c='r') # plot negative points with a red cross
plot(x, boundary)
xlim([-2, 2])
ylim([-2, 2])
grid()
```

# Assignment Part I

## Classification with single-layer perceptron

### Generate linearly separable data

We can generate a sample of normally distributed multivariate data centered at `mu` and with covariance `cov` using the following functions.

```python
mu = [0, 0]
cov = [[1, 0], [0, 100]]
x, y = random.multivariate_normal(mu, cov, 100).T
scatter(x, y)
```

Let us package it into a function so that we can generate labels and data.

```python
def gen_data(mu, label, n=100):
    cov = matrix("8 3; 3 8")
    data = random.multivariate_normal(mu, cov, n).T
    labels = label * ones((1, n))
    return vstack((data, labels))

data = hstack((gen_data(random.randint(0, 20, size=2), 0), gen_data(random.randint(-30, 0, size=2), 1)))
colors = ['red', 'green']
random.shuffle(data.T)
scatter(data[0,:], data[1,:], c=data[2,:], cmap=matplotlib.colors.ListedColormap(colors))
```

Let us implement sequential learning for both delta and perceptron learning. Instead of creating new functions, let us define a function `train` that takes different parameters.

One important thing we must consider is the scale of the inputs and targets. So far, we have defined our inputs to be in the range $ x \in [-1, 1] $ however if we want to generalise our learning algorithm it must be able to handle inputs of any scale. A simple solution is to normalise our data into the symmetric $ [-1, 1] $ range, if we don't the error function for the delta rule:

$$
e = \mathbf{w}\mathbf{x}^\text{T} - \mathbf{t}
$$

the term $ \mathbf{w}\mathbf{x}^\text{T} $ will grow arbitrarily large (due to $ \mathbf{x} $ being non-normalised) and as a result the weights will grow really large after each training.

Our `train` function should take the following parameters:

```
inputs : the inputs to train
targets : the targets we are trying to match
eta : the learning rate
activation : the activation function (such as sgn)
batch : whether to train in batch mode or not
```

```python
def train(inputs, targets, eta=0.01, activation=lambda x: x, batch=True, epochs=200):
    bias = ones((1, inputs.shape[1])) # create a 1xn vector of 1s as our bias
    input_min = ndarray.min(inputs)
    input_range = ndarray.max(inputs) - input_min
    normalised_inputs = 2 * (((inputs - input_min) / input_range) - 0.5) # we normalize the inputs between -1 and 1
    X = vstack((normalised_inputs, bias)) # add bias to the inputs, resulting in a 3xn matrix

    weights = random.randn(1, X.shape[0]) # create a 1x3 vector of random weights
    error = zeros(epochs) # create a vector to store error computed at each epoch

    if batch:
        for i in range(epochs):
            err = (activation(dot(weights, X)) - targets)
            deltaW = -eta * dot(err, X.T)
            weights = weights + deltaW # update weights
            error[i] = mean(power(err, 2)) # calculate mean square error for each epoch
    else:
        for i in range(epochs):
            for j in range(X.shape[1]):
                err = (activation(dot(weights, X[:,j])) - targets[j])
                deltaW = -eta * err * X[:,j]
                weights = weights + deltaW # update weights

            err = (activation(dot(weights, X)) - targets)
            error[i] = mean(power(err, 2)) # calculate mean square error for each epoch
    return weights, error

W, err = train(data[0:2,:], data[2,:], batch=False, epochs=20)
print(err[-1:])
subplot(1, 2, 1)
plot(err)
subplot(1, 2, 2)
scatter(data[0,:], data[1,:], c=data[2,:], cmap=matplotlib.colors.ListedColormap(colors))
x = linspace(min(data[0,:]), max(data[0,:]), 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary
plot(x, boundary)
ylim((min(data[1,:]) - 5, max(data[1,:]) + 5))
```

#### Perceptron learning batch mode

```python
W, err = train(data[0:2,:], data[2,:], activation=sign, batch=True, epochs=20)
subplot(1, 2, 1)
plot(err)
print(err[-1:])

subplot(1, 2, 2)
scatter(data[0,:], data[1,:], c=data[2,:], cmap=matplotlib.colors.ListedColormap(colors))
x = linspace(min(data[0,:]), max(data[0,:]), 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary
plot(x, boundary)
ylim((min(data[1,:]) - 5, max(data[1,:]) + 5))
```

#### Perceptron learning sequential mode

```python
W, err = train(data[0:2,:], data[2,:], activation=sign, batch=False, epochs=50)
subplot(1, 2, 1)
plot(err)
print(err[-1:])

subplot(1, 2, 2)
scatter(data[0,:], data[1,:], c=data[2,:], cmap=matplotlib.colors.ListedColormap(colors))
x = linspace(min(data[0,:]), max(data[0,:]), 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary
plot(x, boundary)
ylim((min(data[1,:]) - 5, max(data[1,:]) + 5))
```

#### Delta Rule batch mode

```python
W, err = train(data[0:2,:], data[2,:])
plot(err)
```

Interestingly we can see that the error increases after each epochs. This is possibly caused by a too-large learning rate which.

```python
W, err = train(data[0:2,:], data[2,:], eta=0.001)
print(err[-1:])
subplot(1, 2, 1)
plot(err)
subplot(1, 2, 2)
scatter(data[0,:], data[1,:], c=data[2,:], cmap=matplotlib.colors.ListedColormap(colors))
x = linspace(-20, 20, 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary
plot(x, boundary)
```

#### Delta Rule sequential mode

```python
W, err = train(data[0:2,:], data[2,:], batch=False, epochs=50)
print(err[-1:])
subplot(1, 2, 1)
plot(err)
subplot(1, 2, 2)
scatter(data[0,:], data[1,:], c=data[2,:], cmap=matplotlib.colors.ListedColormap(colors))
x = linspace(-20, 20, 2) # generate two values between -2 and 2
boundary = -(W[0,1] / W[0,0]) * x - (W[0,2] / W[0,0]) # calculate the decision boundary
plot(x, boundary)
```

```python

```


# Open Jackson Network Lab

In the lab you will learn

* How to represent arrival rates in a network as a system of linear equations in matrix form
* How to use `scipy.linalg` functions to solve for the arrival rates
* How to analyse the network

## Imports

```python
from scipy import linalg, math
import numpy as np
import pandas as pd
```

## The example queueing network

**Question**: What is the expected number of customers in the entire queuing system?

## Step 1: Solve for $\lambda_i$

Given:

\begin{equation}
        \lambda_j = a_j + \sum_{i=1}^m \lambda_i p_{ij}
    \end{equation}

---

Plug the $a_j$ and $p_{ij}$ figures into the equation for $\lambda_j$

$$\lambda_1 = 1 + 0.1\lambda_2 + 0.4\lambda_3 $$
$$\lambda_2 = 4 + 0.6\lambda_1 + 0.4\lambda_3 $$
$$\lambda_3 = 3 + 0.3\lambda_1 + 0.3\lambda_2 $$

---
(optional) rearrange the equations with $a_j$ on the RHS

$$\lambda_1 - 0.1\lambda_2 - 0.4\lambda_3 = 1$$
$$-0.6\lambda_1 + \lambda_2 - 0.4\lambda_3 = 4$$
$$-0.3\lambda_1 - 0.3\lambda_2 + \lambda_3 = 3$$

---

Express the system of equations describing arrival rates in matrix form.

$$ \begin{bmatrix} 1 & -0.1 & -0.4\\ -0.6 & 1 & -0.4 \\ -0.3 & -0.3 & 1 \end{bmatrix} \begin{bmatrix} \lambda_1 \\ \lambda_2 \\ \lambda_3 \end{bmatrix}= \begin{bmatrix} 1\\ 4\\ 3 \end{bmatrix}$$

```python
# create the matricies as numpy arrays
a = np.array([[1, -0.1, -0.4], [-0.6, 1, -0.4], [-0.3, -0.3, 1]])
b = np.array([1, 4, 3])
```

```python
a
```

    array([[ 1. , -0.1, -0.4],
           [-0.6,  1. , -0.4],
           [-0.3, -0.3,  1. ]])

```python
b
```

    array([1, 4, 3])

## Solve for the unknowns

To solve for the unknowns we need to take the dot product of the inverse of a and b

$$\begin{bmatrix} \lambda_1 \\ \lambda_2 \\ \lambda_3 \end{bmatrix} = \begin{bmatrix} 1 & -0.1 & -0.4\\ -0.6 & 1 & -0.4 \\ -0.3 & -0.3 & 1 \end{bmatrix}^{-1} \begin{bmatrix} 1\\ 4\\ 3 \end{bmatrix}$$

```python
np.dot(linalg.inv(a), b)
```

    array([ 5. , 10. ,  7.5])

An alternative way to solve for the unknowns is to use linalg.solve

```python
arrival_rate = linalg.solve(a, b)
arrival_rate
```

    array([ 5. , 10. ,  7.5])

## Step 2: Calculate the traffic intensity

$\rho_i = \dfrac{\lambda_i} {s_i \mu_i}$

Remember that you are treating each queuing system as an independent M/M/s

* Facility 1 is a M/M/1
* Facility 2 is a M/M/2
* Facility 3 is a M/M/1

#### Example solution

```python
servers = np.array([1, 2, 1])
service_rate = np.array([10, 10, 10])

rho = arrival_rate / (servers * service_rate)
rho
```

    array([0.5 , 0.5 , 0.75])

## Step 3: Calculate the number of people in the queue

For an M/M/1

$L_s = \dfrac{\rho}{1 - \rho}$

and for an M/M/s

$p_0 = \left[ \sum_{n=0}^{s-1} \frac{\left(\lambda/ \mu \right)^n}{n!} + \frac{\left( \lambda / \mu \right)^s}{s!\left(1-\rho\right)}  \right]^{-1}$

$L_q = \dfrac{p_o(\lambda/\mu)^s\rho}{s!(1-\rho)^2}$

$L_s = L_q + \dfrac{\lambda}{\mu}$

```python
class MMSQueue:
    '''
    M/M/S/inf/inf/FIFO system
    '''
    def __init__(self, _lambda, mu, s):
        '''
        Constructor

        Parameters:
        -------
        _lambda: float
            The arrival rate of customers to the facility

        mu: float
            The service rate of the facility

        s: int
            The number of servers
        '''
        self._lambda = _lambda
        self.mu = mu
        self.s = s
        self.rho = self._get_traffic_intensity()

        #create a dict of performance metrics
        #solve for L_q then use little's law to calculate remaining KPIs
        self.metrics = {}
        self.metrics[r'$\rho$'] = self.rho
        self.metrics[r'$L_q$'] = self._get_mean_queue_length()
        self.metrics[r'$L_s$'] = self.metrics[r'$L_q$'] + (_lambda / mu)
        self.metrics[r'$W_s$'] = self.metrics[r'$L_s$'] / _lambda
        self.metrics[r'$W_q$'] = self.metrics[r'$W_s$'] - (1 / mu)

    @property
    def total_in_system(self):
        return self.metrics[r'$L_s$']

    def _get_traffic_intensity(self):
        '''
        calculate the traffic intensity (server utilisation)
        of an M/M/s queue
        '''
        return self._lambda / (self.s * self.mu)

    def _get_mean_queue_length(self):
        '''
        Mean length of queue Lq
        '''
        p0 = self.prob_system_empty()

        lq = (p0 * ((self._lambda / self.mu)**self.s) *
              self.rho) / (math.factorial(self.s) * (1 - self.rho)**2)
        return lq

    def prob_system_empty(self):
        '''
        The probability that a M/M/s/infinity queue is empty
        '''
        p0 = 0.0

        for n in range(self.s):
            p0 += ((self._lambda / self.mu) ** n) / math.factorial(n)

        p0 += ((self._lambda / self.mu) ** self.s) / (math.factorial(self.s)
                                                      * (1 - self.rho))
        return p0**-1

    def prob_n_in_system(self, n, return_all_solutions=True, as_frame=True):
        '''
        Calculate the probability that n customers
        in the system (queuing + service)

        Parameters:
        --------
        n: int,
            Number of customers in the system

        return_all_solutions: bool, optional (default=True)
            Returns all solutions for 0,1 ... n

        as_frame: bool, optional (default=True)
            If True, returns all solutions in a pd.DataFrame
            else returns all solutions as np.ndarray
            has no effect is return_all_solutions == False

        Returns:
        ------
            np.ndarray of solutions

        '''
        p0 = self.prob_system_empty()
        probs = [p0]

        #for n <= s
        for i in range(1, min(self.s+1, n+1)):
            pn = (((self._lambda / self.mu)**i) / math.factorial(i)) * p0
            probs.append(pn)

        #for n > s
        for i in range(self.s+1, n+1):
            pn = (((self._lambda / self.mu)**i) / (math.factorial(self.s)
                                                   * (self.s**(n-self.s)))) * p0
            probs.append(pn)

        if return_all_solutions:
            results = np.array(probs)
            if as_frame:
                return pd.DataFrame(results, columns=['P(X=n)'])
            else:
                return results
        else:
            return probs[:-1]

    def summary_frame(self):
        '''
        Return performance metrics

        Returns:
        ---------
        pd.DataFrame
        '''
        df = pd.Series(self.metrics).to_frame()
        df.columns = ['performance']
        return df
```

```python
queues = [MMSQueue(l, mu, s) for l, mu, s in zip(arrival_rate, service_rate,
                                                 servers)]

queues[0].summary_frame()
```

      performance

      $\rho$
      0.5

      $L_q$
      0.5

      $L_s$
      1.0

      $W_s$
      0.2

      $W_q$
      0.1

```python
queues[1].summary_frame().round(2)
```

      performance

      $\rho$
      0.50

      $L_q$
      0.33

      $L_s$
      1.33

      $W_s$
      0.13

      $W_q$
      0.03

```python
queues[2].summary_frame()
```

      performance

      $\rho$
      0.75

      $L_q$
      2.25

      $L_s$
      3.00

      $W_s$
      0.40

      $W_q$
      0.30

#### Example solution

```python
# total in system
total_in_system = sum([q.total_in_system for q in queues])
print(f'Expected # of customers in queuing network = {total_in_system:.2f}')
```

    Expected # of customers in queuing network = 5.33

## Convenience class for Network problem

```python
class JacksonNetwork:
    '''
    Open Jackson Queuing Network

    A simple implementation where a system of linear equations is specified in
    matrix form by a user as opposed to manually adding nodes.

    Create and conveniently holds a collection of MMSQueues.
    '''
    def __init__(self, a, b, service_times, servers):
        '''
        Init the network

        Params:
        ------
        a: np.ndarray
            routing of entities through network
            Note this rearranged for calculation.

        b: np.ndarray
            arrival rates to nodes from outside the network

        service_times: np.ndarray
            Service rate at each node

        servers: np.ndarray
            No. of servers at each node

        '''
        self.a = a
        self.b = b
        self.service_times = service_times
        self.servers = servers

        # number of queues
        self.n_nodes = len(service_times)

        # solve for arrival rates
        self.arrival_rate = linalg.solve(a, b)

        # create MMS queues
        self.queues = self._create_mms_queues()

    @property
    def total_in_system(self):
        '''
        The expected number of entities in the jackson network
        Sum of expected number of entities in each MMSQueue

        Returns:
        -------
        float
        '''
        return sum([q.total_in_system for q in queues])

    def summary_frame(self):
        '''
        Tabular summary the performance measures in each queuing node

        For each node returns Rho, L_q, L_s, W_q, W_s,

        Returns:
        --------
        pd.DataFrame
        '''
        summary = pd.DataFrame()

        for node_id, q in zip(range(len(self.queues)), self.queues):
            node_summary = q.summary_frame()
            node_summary.columns = ['node_' + str(node_id)]
            summary = pd.concat([summary, node_summary], axis=1)

        return summary

    def _create_mms_queues(self):
        '''
        Create and parameterise the required number of MMSQueue objects

        Returns:
        --------
        List

        '''
        return [MMSQueue(l, mu, s) for l, mu, s in zip(self.arrival_rate,
                                                       self.service_times,
                                                       self.servers)]

```

```python
model = JacksonNetwork(a, b, service_rate, servers)
model.summary_frame().round(2)
```

      node_0
      node_1
      node_2

      $\rho$
      0.5
      0.50
      0.75

      $L_q$
      0.5
      0.33
      2.25

      $L_s$
      1.0
      1.33
      3.00

      $W_s$
      0.2
      0.13
      0.40

      $W_q$
      0.1
      0.03
      0.30

```python
print(f'Expected # of customers in queuing network = {model.total_in_system:.2f}')
```

    Expected # of customers in queuing network = 5.33


# Logistic Regression

## Regression and Classification

regression answers how much? or how many? questions, for example:

1. predict the number of dollors at which a house will be sold.
2. predict the revenue of a restaurant.

in practice, we are more often interested in classification: asking not "how much", but "which one":

1. does this email belong in the spam foler or the inbox?
2. does this image depict a donkey, a dog, a cat, or a rooster?
3. which movie is Jean most likely to watch next?

linear regression not fit to solve classification problem for two reasons:

1. linear regression range in $\mathbb{R}$, while classification label is discrete.
2. linear regression uses euclid distance, result in d(class 3, class 1) > d(class 2, class 1), this is often not true in classification.

## Logistic Regression Model

we first foucus on binary classification

suppose dataset $D=\left \{ (x^{(1)},y^{(1)}),...,(x^{(n)},y^{(n)}) \right \} $, where $x^{(i)} \in \mathbb{R}^{d},\ y^{(i)} \in \left \{ 0, 1\right \}$.

to tackle this problem, after deriving $\theta^{T}x$

we uses a function that transform value in $\mathbb{R}$ into value in $[0, 1]$ and view this as the probability of being positive

we choose:

$$\sigma(x) = \frac{1}{1 + e^{-x}}$$

as that function, it is the so called sigmoid function, we choose sigmoid function because:

1. it is a monotony increase, differentiable, symmetric function that maps $\mathbb{R}$ into $[0,1]$.
2. simple form
3. derivative can calculate easily: ${\sigma}'(x) = \sigma(x)(1 - \sigma(x))$

now the model is:

$$h_{\theta}(x) = \frac{1}{1 + exp(-\theta^{T}x)}$$

this is the logistic regression model.

additionally, in our view, we have:

$$h_{\theta}(x) = p(y=1|x)$$

## Entropy

self information $I(x)$ indicates the amount of information an event $x$ to happen.

we want $I(x)$ to satisfy:

1. $I(x) \ge 0$
2. $I(x) = 1 \text{ if }p(x)=1$
3. $\text{if }p(x_{1}) > p(x_{2}) \text{, then } I(x_{1}) < I(x_{2})$
4. $I(x_{1}, x_{2}) = I(x_{1}) + I(x_{2}) \text{ for independent }x_{1},x_{2}$

this leads to $I(x) = -log\ p(x)$

while self-information measures the information of a single discrete event, entropy measures the information of a random variable:

$$
\begin{equation}
\begin{split}
H(X)=&E(I(x))\\
=&E(-log\ p(x))\\
=&-\sum_{x \in \mathcal{X}}log\ p(x)
\end{split}
\end{equation}
$$

it is exactly the optimal encoding length of $X$.

cross entropy $H(p, q)$ is the encoding length of $p$ by optimal encoding of $q$:

$$H(p,q)=E_{p}\left[-log\ q(x)\right] = -\sum_{x}p(x)log\ q(x)$$

fix $p$, the closer $q$ is to $p$, the less is $H(p,q)$.

we can use $H(p,q)$ to define the distance of $q$ to $p$.

turn to our binary classification problem, for $y^{(i)} \in \left\{0,1\right\}, \hat{y}^{(i)} \in (0, 1)$, we have:

$$
H(y^{(i)}, \hat{y}^{(i)}) =
\begin{cases}
-log(\hat{y}^{(i)}),\text{ if } y^{(i)} = 1\\
-log(1 - \hat{y}^{(i)}),\text{ if } y^{(i)} = 0
\end{cases}
$$

combine the two cases:

$$H(y^{(i)}, \hat{y}^{(i)}) = -y^{(i)}log(\hat{y}^{(i)}) - (1-y^{(i)})log(1 - \hat{y}^{(i)})$$

we use cross entropy to define the loss of logistic regression model:

$$
\begin{equation}
\begin{split}
J(\theta) =& \sum_{i=1}^{n}H(y^{(i)}, H(\hat{y}^{(i)})) \\
=& \sum_{i=1}^{n}-y^{(i)}log(\hat{y}^{(i)}) - (1-y^{(i)})log(1 - \hat{y}^{(i)}) \\
=& \sum_{i=1}^{n}-y^{(i)}log(h_{\theta}(x^{(i)})) - (1 - y^{(i)})log(1 - h_{\theta}(x^{(i)}))
\end{split}
\end{equation}
$$

this is the so called logistic regression.

in addition, we can write cross entropy loss in matrix form:

$$J(\theta) = -y^{T}log(\sigma(\theta^{T}X)) - (1 - y)^{T}log(1 - \sigma(\theta^{T}X))$$

```python
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

iris = load_iris()
X = iris["data"][:, 2:]
y = (iris["target"] == 2).astype(np.int)

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)
```

```python
from sklearn.linear_model import LogisticRegression

log_reg = LogisticRegression(solver="lbfgs", random_state=42)
log_reg.fit(X_train, y_train)
```

    LogisticRegression(random_state=42)

```python
from sklearn.metrics import accuracy_score

y_pred = log_reg.predict(X_test)
accuracy_score(y_test, y_pred)
```

    0.9736842105263158

## Probability Interpretation of Cross Entropy Loss

as we suppose

$$p(y=1|x) = h_{\theta}(x)$$

and

$$p(y=0|x) = 1 - h_{\theta}(x)$$

combine these two, in our prediction:

$$p(y^{(i)}|x^{(i)}) = \left [ h_{\theta}(x^{(i)}) \right ]^{y^{(i)}}\left [ 1 - h_{\theta}(x^{(i)}) \right ]^{1 - y^{(i)}} $$

log likelikhood function:

$$
\begin{equation}
\begin{split}
L(\theta) &= log\prod_{i=1}^{n} \left [ h_{\theta}(x^{(i)}) \right ]^{y^{(i)}}\left [ 1 - h_{\theta}(x^{(i)}) \right ]^{1 - y^{(i)}} \\
&= \sum_{i=1}^{n}\left [y^{(i)}log\ h_{\theta}(x^{(i)}) + (1 - y^{(i)})log(1 - h_{\theta}(x^{(i)})) \right ]
\end{split}
\end{equation}
$$

maximum the above likelihood is equal to minimize the cross entropy loss:

$$J(\theta) = \sum_{i=1}^{n}-y^{(i)}log(h_{\theta}(x^{(i)})) - (1 - y^{(i)})log(1 - h_{\theta}(x^{(i)}))$$

## Update-rule

we have:

$$
\begin{equation}
\begin{split}
\frac{\partial }{\partial \theta_{j}}J(\theta ) &= \frac{\partial }{\partial \theta_{j}}\sum_{i=1}^{n}-log(\sigma(\theta^{T}x^{(i)}))y^{(i)} - log(1 - \sigma(\theta^{T}x^{(i)}))(1 - y^{(i)}) \\
&= \sum_{i=1}^{n} \left (-y^{(i)}\frac{1}{\sigma(\theta^{T}x^{(i)})} + (1 - y^{(i)})\frac{1}{1 - \sigma(\theta^{T}x^{(i)})} \right )\frac{\partial }{\partial \theta_{j}}\sigma(\theta^{T}x^{(i)})\\
&=\sum_{i=1}^{n} \left (-y^{(i)}\frac{1}{\sigma(\theta^{T}x^{(i)})} + (1 - y^{(i)})\frac{1}{1 - \sigma(\theta^{T}x^{(i)})} \right )\sigma(\theta^{T}x^{(i)})(1-\sigma(\theta^{T}x^{(i)}))\frac{\partial }{\partial \theta_{j}}\theta^{T}x^{(i)} \\
&=\sum_{i=1}^{n}(\sigma(\theta^{T}x^{(i)}) - y^{(i)})x_{j}^{(i)} \\
&=\sum_{i=1}^{n}(h_{\theta}(x^{(i)}) - y^{(i)})x_{j}^{(i)}
\end{split}
\end{equation}
$$

the same form with linear regression!

as linear regression, we have the update rule for logistic regression:

$$\theta_{j}: =\theta_{j} - \alpha\sum_{i=1}^{n} (h_{\theta }(x^{(i)}) - y^{(i)})x_{j}^{(i)} $$

combine all dimensions, we have:

$$\theta: =\theta - \alpha\sum_{i=1}^{n} (h_{\theta }(x^{(i)}) - y^{(i)})\cdot x^{(i)} $$

write in matrix form：
$$
\frac{\partial }{\partial \theta}J(\theta ) = X^{T}(\sigma(X\theta) -y)
$$
matrix form of update formula：
$$
\theta: =\theta - \alpha X^{T}(\sigma(X\theta)-\mathbf{y} )
$$

## Regularization

like linear regression, we add penalty term on $J(\theta)$ for regularization

$l2$ penalty：
$$J(\theta) := J(\theta) + \lambda \left \| \theta \right \|_{2}^{2} $$

$l1$ penalty：
$$J(\theta) := J(\theta) + \lambda \left \| \theta \right \|_{1} $$

## Softmax Regression

now we turn to multi-class classification.

we start off with a simple image classification problem, each input consists of a $2\times{2}$ grayscale image, represent each pixel with a scalar, giving us features $\left\{x_{1},x_{2},x_{3}, x_{4}\right\}$. assume each image belong to one among the categories "cat", "chiken" and "dog".

we have a nice way to represent categorical data: the one-hot encoding, i.e a vector with as many components as we have categories, the component corresponding to particular instance's category is 1 and all others are 0.

for our problem, "cat" represents by $(1,0,0)$, "chicken" by $(0, 1, 0)$, "dog" by $(0, 0, 1)$.

to estimate the conditional probabilities of all classes, we need a model with multiple outputs, one per class.

address classification with linear models, we will need as many affine functions as we have outputs:

$$o_{1} = x_{1}w_{11} + x_{2}w_{12} + x_{3}w_{13} + x_{4}w_{14}$$
$$o_{2} = x_{1}w_{21} + x_{2}w_{22} + x_{3}w_{23} + x_{4}w_{24}$$
$$o_{3} = x_{1}w_{31} + x_{2}w_{32} + x_{3}w_{33} + x_{4}w_{34}$$

depict as:

we would like output $\hat{y_{j}}$ to be interpreted as probability that a given item belong to class $j$.

to transform our current outputs $\left\{o_{1},o_{2},o_{3},o_{4}\right\}$ to probability distribution $\left\{\hat{y}_{1},\hat{y}_{2},\hat{y}_{3},\hat{y}_{4}\right\}$, we use the softmax operation:

$$\hat{y}_{j} = \frac{exp(o_{j})}{\sum_{k}exp(o_{k})}$$

when predicting:

$$\text{predict class} = \underset{j}{argmax}\ \hat{y}_{j} = \underset{j}\ o_{j}$$

$\hat{y}_{j}$ is necessary when compute loss.

as logistic regression, we use the cross entropy loss:

$$H(y,\hat{y}) = -\sum_{k}y_{j}log\ \hat{y}_{j} = -log\ \hat{y}_{\text{category of y}}$$

now complete the construction of softmax regression.

```python
y = iris["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)

softmax_reg = LogisticRegression(multi_class="multinomial", solver="lbfgs", C=10)
softmax_reg.fit(X_train, y_train)
```

    LogisticRegression(C=10, multi_class='multinomial')

```python
y_pred = softmax_reg.predict(X_test)
accuracy_score(y_test, y_pred)
```

    0.9736842105263158

```python

```


Ok. So I want to find $P\{X > 3\}$ where $X \sim \mathcal{N}(0,1)$. I will use *adaptive importance sampling* to answer this question.

I start with defining the error term as:

\begin{equation}
    e(\Theta) = \frac{1}{2} \int_z (f_\Theta(z) - f^*(z))^2 dz
\end{equation}

So we have:

\begin{equation}
    \frac{\partial e}{\partial \theta} = \int_z \frac{\partial f}{\partial \theta}(z)  (f_\Theta(z) - f^*(z)) dz
\end{equation}

Which we can approximate by:

\begin{equation}
    \int_z \frac{\partial f}{\partial \theta}(z)   (1 - \frac{w_\Theta(z)}{\hat{G}} ) f_\Theta(z) dz
\end{equation}

And this can be approximated by
\begin{equation}
    \frac{1}{N} \sum \frac{\partial f}{\partial \theta}(z) (1 - \frac{w_\theta(z)}{\hat{G}})
\end{equation}

In this exmaple, we parameterize the proposal distribution by $\mu, \sigma$, which means that $f(z) = \mathcal{N}(\mu, \sigma)$. By definition, $g(z) = \mathbb{I}[Z>3] \mathcal{N}(0, 1)$.

```python
%matplotlib inline
from matplotlib import pyplot as plt
from __future__ import print_function
```

```python
import numpy as np
from scipy.stats import norm
from utlis import fprime_normal_m, fprime_normal_s

mu = 0.0
# sigma = exp(s) = 1
s = 0.0

alpha = 0.05
tol = 1e-4
N = 100

# Start with a weight of 0.5
Gs = [0.5]
w1 = 0.1

mu_list = []
sig_list = []
s_list = []
dmu_list = []
ds_list = []
G_avg_list = []

condition = False
num_iter = 0
while condition is False:
    num_iter += 1
    sigma = np.exp(s)

    # for plotting
    mu_list.append(mu)
    s_list.append(s)
    sig_list.append(sigma)

    w = [0.0] * N
    z = [0.0] * N
    for i in range(N):
        rnd = np.random.normal(mu, sigma)
        z[i] = rnd
        if rnd < 3:
            w[i] = 0
        else:
            w[i] = norm.pdf(rnd) / norm.pdf(rnd, mu, sigma)

    G_t = sum(w) / float(N)
    Gs.append(G_t)
    # compute the average here
    # Gw = [w1] + range(1, len(Gs))
    Gw = [w1] + ([1] * (len(Gs)-1))
    G_avg = sum([Gw[i] * Gs[i] for i in range(len(Gw))]) / sum(Gw)
    G_avg_list.append(G_avg)

    d_mu = 0.0
    d_s = 0.0
    for i in range(N):
        coef = 1 - w[i]/G_avg
        f_mu = fprime_normal_m(z[i], mu, s)
        f_s = fprime_normal_s(z[i], mu, s)
        d_mu += f_mu * coef
        d_s += f_s * coef
    d_mu /= float(N)
    d_s /= float(N)

    dmu_list.append(d_mu)
    ds_list.append(d_s)

    mu -= (alpha * d_mu)
    s -= (alpha * d_s)

    if abs(d_mu) < tol and abs(d_s) < tol:
        condition = True
    if num_iter > 10000:
        condition = True

```

```python
plt.plot(mu_list)
plt.plot(sig_list)
plt.legend([r'$\mu$', r'$\sigma$'], loc=2)
```

```python
plt.plot(s_list)
```

```python
plt.plot(dmu_list)
```

```python
plt.plot(ds_list)
```

```python
plt.plot(G_avg_list)
```

```python
plt.plot(Gs)
```


```python
import numpy as np
from sympy.solvers import solve
from sympy import Symbol
import sympy

import matplotlib.pyplot as plt
```

#### Реализовать явный метод Эйлера

```python
def euler_method(f, t0, tn, tau, y0):

    eps = tau / 10000

    while t0 < tn and abs(t0 - tn) > eps:

        y0 += tau * f(t0, y0)

        t0 += tau

    return y0

```

##### Euler method testing
Function: $y'(t) = t\sqrt{y(t)}$ \
Initials: $t_0=0, \ y_0=y(t_0)=y(0)=1$ \
Step: $\tau=0.1$

```python
t = np.arange(0, 11, 1, int)
y1 = np.array([euler_method(lambda t, y: t * y ** 0.5, 0, i, 0.1, 1) for i in t])
y2 = (t ** 2 + 4) ** 2 / 16

plt.plot(t, y1, label='estimated', marker='.')
plt.plot(t, y2, label='calculated', marker='.')
plt.grid(linestyle='--')

plt.title("Euler method")
plt.xlabel('t')
plt.ylabel('y')

plt.legend()

plt.show()

for _t, _y1, _y2 in zip(t, y1, y2):
    print(f"t = {_t}:\n\tEstimated: {_y1}\n\tCalculated: {_y2}\n")

```

```python
t = np.arange(0, 11, 1, int)
y1 = [euler_method(lambda t, y: t * y ** 0.5, 0, t[0], 0.1, 1)]

for i in range(1, len(t)):
    y1 += [euler_method(lambda t, y: t * y ** 0.5, t[i - 1], t[i], 0.1, y1[-1])]

y2 = (t ** 2 + 4) ** 2 / 16

plt.plot(t, y1, label='estimated', marker='.')
plt.plot(t, y2, label='calculated', marker='.')
plt.grid(linestyle='--')

plt.title("Euler method")
plt.xlabel('t')
plt.ylabel('y')

plt.legend()

plt.show()

for _t, _y1, _y2 in zip(t, y1, y2):
    print(f"t = {_t}:\n\tEstimated: {_y1}\n\tCalculated: {_y2}\n")

```

##### Implicit Euler method testing
Function: $y'(t) = t\sqrt{y(t)}$ \
Initials: $t_0=0, \ y_0=y(t_0)=y(0)=1$ \
Step: $\tau=0.1$

#### Реализовать неявный метод Эйлера

```python
def implicit_euler_method(f, t0, tn, tau, y0):

    eps = tau / 10000

    while t0 + tau < tn and abs(tn - t0) > eps:

        t0 += tau
        y = Symbol('y')

        y0 = solve(y - tau * f(t0, y) - y0, y)[0]

    return y0

```

```python
t = np.arange(0, 11, 1, int)
y1 = [implicit_euler_method(lambda t, y: t * y ** 0.5, 0, t[0], 0.1, 1)]

for i in range(1, len(t)):
    y1 += [implicit_euler_method(lambda t, y: t * y ** 0.5, t[i - 1], t[i], 0.1, y1[-1])]

y2 = (t ** 2 + 4) ** 2 / 16

plt.plot(t, y1, label='estimated', marker='.')
plt.plot(t, y2, label='calculated', marker='.')
plt.grid(linestyle='--')

plt.title("Implicit Euler method")
plt.xlabel('t')
plt.ylabel('y')

plt.legend()

plt.show()

for _t, _y1, _y2 in zip(t, y1, y2):
    print(f"t = {_t}:\n\tEstimated: {_y1}\n\tCalculated: {_y2}\n")

```

#### Реализовать метод Рунге-Кутты 4 порядка

```python
def runge_kutta(f, t0, tn, tau, y0):

    eps = tau / 10000

    while t0 < tn and (tn - t0) > eps:

        k1 = f(t0, y0)
        k2 = f(t0 + tau / 2, y0 + tau * k1 / 2)
        k3 = f(t0 + tau / 2, y0 + tau * k2 / 2)
        k4 = f(t0 + tau, y0 + tau * k3)

        y0 += tau / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
        t0 += tau

    return y0
```

```python
t = np.arange(0, 11, 1, int)
y1 = [runge_kutta(lambda t, y: t * y ** 0.5, 0, t[0], 0.1, 1)]

for i in range(1, len(t)):

    y1 += [runge_kutta(lambda t, y: t * y ** 0.5, t[i - 1], t[i], 0.1, y1[-1])]

y2 = (t ** 2 + 4) ** 2 / 16

plt.plot(t, y1, label='estimated', marker='.')
plt.plot(t, y2, label='calculated', marker='.')
plt.grid(linestyle='--')

plt.title("Runge-Kutta method")
plt.xlabel('t')
plt.ylabel('y')

plt.legend()

plt.show()

for _t, _y1, _y2 in zip(t, y1, y2):
    print(f"t = {_t}:\n\tEstimated: {_y1}\n\tCalculated: {_y2}\n")

```

```python

```