<jupyter_start><jupyter_text>LIVRO: Introduct ion to Unconstrained Optimization with R ___AUTORES: <NAME>; <NAME>___ ___EDITORA: Springer___ ___ANO: 2019___ AVISO: A editora Springer tem direitos sobre o capital intelectual dos códigos originais e trechos do livro texto que são utilizados aqui. Desta forma, este material é somente para estudo e melhoria de conhecimento, sendo ilegal a venda do mesmo ou a obtenção de qualquer lucro. Além disso, deve-se ter em mente de que este material é construído para melhorar meu conhecimento na área e por isso não deve-se assumir que os códigos vistos aqui são totalmente precisos ou corretos.PROPOSTA GERAL: Apresentar uma versão python dos algoritmos (ou códigos) contidos no capítulo do livro texto, assim como adicionar algumas informações extras e criar um material (simples) para complementar o estudo e agilizar alguma possível necessidade.FORMA DE CONSTRUÇÃO DO MATERIAL:    I) Leitura completa do capítulo;      II) Pesquisa por algum material complementar;    III) Teste dos códigos e algoritmos do livro na versão R;    IV) Migração dos códigos R em (III) para a versão Python no Jupyter Notebook;    V) Construção do arquivo final no Jupyter Notebook;COMENTÁRIO:    I) O livro trabalha os códigos em R, mas vou migrar tais códigos para Python de modo a melhorar meu aprendizado;    II) No inicio de cada código, vou indicar a página do livro no qual o código se encontra;    III) Quando eu tiver informação extra que não der para colocar no código, estarei fazendo uma célula do jupyter dedicada a comentar tal informação. (Semelhante a esta).    BIBLIOGRAFIA:        [1]: Introduct ion to Unconstrained Optimization with R - 2019 - <NAME>; <NAME>    [2]: OPTIMIZATION, Algorithms and Applications - 2015 - <NAME>    [3]: Cálculo com Geometria Analítica - Volume 1 - 1994 -  Louis Leithold    Extra - Seção 1.6 - Gradient Vector, Directional Derivative, and Hessian Matrix LIVRO TEXTO - Referência [2] Pg. 16 a 18; Referência [2].**Obs:** Estarei implementando esta parte extra com a finalidade de apresentar os métodos de aproximação de derivada de ordem 1 e 2 para funções com uma variável. Tais métodos são importantes e o livro texto [1] não os aborda de maneira adequada (em minha opinião).<jupyter_code>#### APROXIMAÇÃO DE DERIVADA PRIMEIRA -- FUNÇÃO: Forward Difference

# Pg. 28; Referência [2].

def fun_aprox_dev_forward_diff(f_, x_, h_):
    #f_: Função f na qual iremos aproximar a derivada;
    #x_: Valor do domínimo de f no qual aplicaremos a derivada;
    #h_: Valor de variação aplicada no ponto. Famoso delta x (ou h). Ver [2].
    
    DEV_ = ( f_(x_+h_) - f_(x_) ) / h_
    return DEV_

#### APROXIMAÇÃO DE DERIVADA PRIMEIRA -- FUNÇÃO: Backward Difference

# Pg. 28; Referência [2].

def fun_aprox_dev_back_diff(f_, x_, h_):    
    #f_: Função f na qual iremos aproximar a derivada;
    #x_: Valor do domínimo de f no qual aplicaremos a derivada;
    #h_: Valor de variação aplicada no ponto. Famoso deta x (ou h). Ver [3].

    DEV_ = ( f_(x_) - f_(x_-h_) ) / h_
    return DEV_
#### APROXIMAÇÃO DE DERIVADA PRIMEIRA -- FUNÇÃO: Central Difference

# Pg. 28; Referência [2].

def fun_aprox_dev_central_diff(f_, x_, h_):    
    #f_: Função f na qual iremos aproximar a derivada;
    #x_: Valor do domínimo de f no qual aplicaremos a derivada;
    #h_: Valor de variação aplicada no ponto. Famoso deta x (ou h). Ver [3].

    DEV_ = ( f_(x_+h_) - f_(x_-h_) ) / (2*h_)
    return DEV_


##obs: Em [2] é visto que a formula central difference é mais precisa que as anteriores. Desta forma utilizaremos ela
# sempre que for necesário obter derivada de uma função de interesse em um ponto de interesse.

#### Exemplo 01: Compute numericamente o valor da derivada primeira no ponto x=7 para 
# a função contínua f(x) = (x^2)-(2*x)+1 e uma variação h = 0.0001. 
# Lembre-se que a derivada primeira é 2*x-2 e o valor da mesma aplicada no ponto 7 resulta no valor 12.

### SOLUÇÃO:

def fun_ex01(x_):
    return (x_**2) - (2*x_) + 1


form_forward = fun_aprox_dev_forward_diff(f_ = fun_ex01, x_ = 7, h_ = 0.0001)
form_back = fun_aprox_dev_back_diff(f_ = fun_ex01, x_ = 7, h_ = 0.0001)
form_central = fun_aprox_dev_central_diff(f_ = fun_ex01, x_ = 7, h_ = 0.0001)

print("O resultado obtido na formula foward differece: {}".format(form_forward))
print("O resultado obtido na formula back differece: {}".format(form_back))
print("O resultado obtido na formula central differece: {}".format(form_central))

# Obs: Veja que foi um valor bem próximo do valor obtido com a derivada primeira em sua forma analítica.

#### APROXIMAÇÃO DE DERIVADA SEGUNDA -- FUNÇÃO: Central Difference

# Pg. 28; Referência [2].

def fun_aprox_dev_seg_central_diff(f_, x_, h_):    
    #f_: Função f na qual iremos aproximar a derivada;
    #x_: Valor do domínimo de f no qual aplicaremos a derivada;
    #h_: Valor de variação aplicada no ponto. Famoso deta x (ou h). Ver [3].

    DEV_ = ( f_(x_+h_) - 2*f_(x_) + f_(x_-h_) ) / (h_**2)
    return DEV_

#### Exemplo 02: Compute numericamente o valor da derivada SEGUNDA no ponto x=7 
# para a função contínua f(x) = (x^3)-(4*x)+5 e uma variação h = 0.0001. 
# Lembre-se que a derivada SEGUNDA é 6*x e o valor da mesma aplicada no ponto 7 resulta no valor 42.


### SOLUÇÃO:

def fun_ex02(x_):
    return (x_**3)-(4*x_)+5


form_central_dev_seg = fun_aprox_dev_seg_central_diff(f_ = fun_ex02, x_ = 7, h_ = 0.0001)

print("O resultado obtido na formula central differece para a derivada segunda: {}".format(form_central_dev_seg))

# Obs: Veja que foi um valor bem próximo do valor obtido com a derivada segunda em sua forma analítica.<jupyter_output>O resultado obtido na formula central differece para a derivada segunda: 41.99999921183917<jupyter_text>Extra - Conhecimento  Adicional em Python**CURIOSIDADE:** Desviando um pouco do assunto. É comum que haja momentos que necessitamos fazer uma cópia de uma variável, pois queremos fazer algum procedimento com a variável e em seguida comparar com o valor original da mesma. Quem vem do R sabe que para fazer uma cópia de uma variável X, basta fazer Y = X. Contudo, isso não funciona em python. Ao fazer isso, você irá criar um X e Y com o mesmo código na memória e com isso, ao modificar Y, estaremos modificando X. Desta maneira, existem algumas técnicas para criar cópias de variáveis. Assim, para uma variável X, podemos criar um cópia Y da forma:    I) import copy; Y = copy.deepcopy(X);    II) Y = X[:];    III) Y = [x for x in X];    IV) Y = X.copy() ;Vale comentar que o item (IV) não está utilizando uma função do pacote (biblioteca) copy. Além disso, vale apontar que (II) é mais rápido que (III) e que (IV) é mais fácil de lembrar o que é quando lemos o código pela segunda vez em um espaço longo de tempo.<jupyter_code>### Como curiosidade, vamos criar uma variável e algumas cópias da mesma. Em seguinda vamos usar 
# a função 'id()' para verificar o código de cada variável na memória do computador:

import copy

X = [1 , 3]

Y1 = X
Y2 = X[:]
Y3 = [i for i in X]
Y4 = X.copy()
Y5 = copy.deepcopy(X)

print('O id de X é: {}'.format(id(X)))
print('O id de Y1 = X é: {}'.format(id(Y1)))
print('O id de Y2 = X[:] é: {}'.format(id(Y2)))
print('O id de Y3 = [i for i in X] é: {}'.format(id(Y3)))
print('O id de Y4 = X.copy() é: {}'.format(id(Y4)))
print('O id de Y5 = copy.deepcopy(X) é: {}'.format(id(Y5)))

### Obs: Veja que Y1 apresenta o mesmo código de X e por isso uma modificação em Y1, modificará X. Já as outras cópias 
# não afetaram a variável original X, caso tais cópias recebam alguma modificação.<jupyter_output>O id de X é: 1368109502912
O id de Y1 = X é: 1368109502912
O id de Y2 = X[:] é: 1368109503040
O id de Y3 = [i for i in X] é: 1368109504640
O id de Y4 = X.copy() é: 1368109498304
O id de Y5 = copy.deepcopy(X) é: 1368109497792<jupyter_text>Chapter 4 - First-Order and Second-Order Necessary Conditions **CONTEÚDO DO LIVRO:**        4 First-Order and Second-Order Necessary Conditions ............. 57        4.1 Introduction ....................................... 57        4.2 Directional Derivative ................................ 64        4.3 First-Order Necessary Condition ........................ 67        4.4 Second-Order Necessary Condition...................... 73        4.5 Second-Order Sufficient Condition for Interior Case .......... 78        4.6 Exercises ......................................... 82    Seção 4.1 - Introduction LIVRO TEXTO - Referência [1] Código R: Pg. 60 - R Function 4.1 Grad_Vec.R; Referência [1]. Idéia geral do código: Esta é uma função para calcular numericamente o vetor gradiente de funções contínuas $f$ com mais de uma variável e um ponto de interesse no domínio.<jupyter_code>#### LIVRO TEXTO - Referência [1]
### Pg. 60 - R Function 4.1; Referência [1].

# Versão Python

def Grad_Vec(f_, x_, h_):
    #f_: Função f na qual iremos aproximar o gradiente;
    #x_: Valor do domínimo de f no qual aplicaremos o gradiente; Este parâmetro deve ser um array com valores numéricos (float/int).
    #h_: Valor de variação aplicada no ponto. Famoso deta x (ou h). Ver [2]; Esse parâmetro deve ser um número real.
    
    grad_vec_ = [0 for _ in range(len(x_))]

    for i in range(len(x_)):
#        x_low_ = [j for j in x_] # Primeira forma que usei pra copiar a variável (obj) x_, contudo eu suspeito que isso é lento devido ao 'for'.
#        x_up_ = [j for j in x_]
        
#        x_low_ = x_[:] # mudei para esse formato porque o mago Thiago Neps disse que assim perfoma bem e seria melhor que minha versão inicial.
#        x_up_ = x_[:]
        
        x_low_ = x_.copy() #mudei para esse formato depois que o mesmo mago disse que as vezes é melhor deixar mais fácil de ler e dessa forma seria mais fácil de lembrar o que é no futuro.
        x_up_ = x_.copy()

        x_low_[i] = (x_low_[i] - h_)
        x_up_[i] = (x_up_[i] + h_)
                
        grad_vec_[i] = ( f_(x_up_) - f_(x_low_) )/(2*h_)
    
    return grad_vec_ 


#### Exemplo 03: Compute numericamente o vetor gradiente no ponto V=(x=2; y=2) para 
# a função contínua f(V) = x**3 + y**3 + 3*x*(y**2). Use uma variação h = 0.0001. 
# Lembre-se que o gradiente analítico é (3*x**2 + 3*y**2; 3*y**2+6*x*y) e seu 
# valor no ponto V é (1, 1) é dado por (6, 9). Use uma precisão de 4 casas decimais com a função 'round()'.


### SOLUÇÃO:

# função do exemplo
def fun_ex03(V_):
    return (V_[0]**3) + (V_[1]**3) + (3*V_[0]*(V_[1]**2))

# ponto dado
V = [1, 1] 

# gradiente
GRADIENT_VECTOR = [round(i, 4) for i in Grad_Vec(fun_ex03, V, h_=0.0001)]


print("O valor do gradiente no ponto V=(1, 1) é: {}".format(GRADIENT_VECTOR))<jupyter_output>O valor do gradiente no ponto V=(1, 1) é: [6.0, 9.0]<jupyter_text>Seção 4.1 - Introduction LIVRO TEXTO - Referência [1] Código R: Pg. 61 - R Function 4.2 Hessian.R; Referência [1]. Idéia geral do código: Esta é uma função para calcular numericamente a matriz hessiana de funções contínuas $f$ com mais de uma variável e um  ponto de interesse no domínio.**Obs:** Eu também achei uma versão diferente desse código de Hessiana em:    https://github.com/cran/pracma/blob/master/R/hessian.R<jupyter_code>#### LIVRO TEXTO - Referência [1]

### Pg. 61 e 62 - R Function 4.2; Referência [1].

# Versão Python

def Hessian(f_, x_, h_):
    #f_: Função f na qual iremos aproximar o gradiente;
    #x_: Valor do domínimo de f no qual aplicaremos o gradiente; Este parâmetro deve ser um array com valores numéricos (float/int).
    #h_: Valor de variação aplicada no ponto. Famoso deta x (ou h). Ver [3]; Esse parâmetro deve ser um número real.
    
    n_ = len(x_)
    hessian_ = [[0 for _ in range(n_)] for _ in range(n_)]

    for i in range(n_):
        for j in range(n_):
            if i==j:
                aux01_ = x_.copy()
                aux01_[i] = aux01_[i] + h_
                f1_ = f_(aux01_)

                aux02_ =  x_.copy() ## OBS: O livro texto não apresenta essa linha de código. Acredito que seja um erro do mesmo, pois parece ser necessário ter esta linha.
                aux02_[i] = aux02_[i] - h_
                f2_ = f_(aux02_)

                f3_ = f_(x_)

                hessian_[i][j] = (f1_ - 2*f3_ + f2_) / (h_**2) # Veja que essa é a formula para segunda derivada como visto anteriormente. Você pode entender como uma derivada parcial aplicada duas vezes na mesma variável.

            else:
                aux01_ = x_.copy()
                aux01_[i] = aux01_[i]+h_
                aux01_[j] = aux01_[j]+h_
                f1_ = f_(aux01_)

                aux02_ = x_.copy()
                aux02_[i] = aux02_[i]+h_
                aux02_[j] = aux02_[j]-h_
                f2_ = f_(aux02_)

                aux03_ = x_.copy()
                aux03_[i] = aux03_[i]-h_
                aux03_[j] = aux03_[j]+h_
                f3_ = f_(aux03_)

                aux04_ = x_.copy()
                aux04_[i] = aux04_[i]-h_
                aux04_[j] = aux04_[j]-h_
                f4_ = f_(aux04_)

                hessian_[i][j] = (f1_ - f2_ - f3_ + f4_ ) / (4*(h_**2)) #Essa é a formula para o caso em que a segunda derivada na verdade é uma derivada parcial com relação a duas variáveis diferentes.

                
    return hessian_

### obs: você pode colocar um if e um parâmetro para arredondar o valor dos elementos dessa hessiana.
#### Exemplo 04: Compute numericamente o vetor gradiente no ponto V=(x=1; y=1) para 
# a função contínua f(V=[x, y]) = (x**2) + (y**3) + (3*(x**2)*(y**3)). Use uma variação h = 0.0001. 
# Lembre-se que as derivadas parciais analíticas de primeira ordem são: 
    #Dfx = 2*x+6*x*(y**3);
    #Dfy = (3*y**2)+9*(x**2)*(y**2);
    
# Lembre-se que as derivadas parciais analíticas de segunda ordem são: 
    #Dfxx = 2+6*(y**3);
    #Dfyy = (6*y)+18*(x**2)*(y)
    #Dfxy = Dfyx = 18*x*(y**2)

# Lembre-se que o valor analítico da Hessiana no ponto V é (1, 1) é dado por [[8, 18],[18, 24]]. 

# Use uma precisão de 5 casas decimais com a função 'round()'.


### SOLUÇÃO:

# função do exemplo

def f_test(V_):
    x = V_[0]
    y = V_[1]
    return (x**2) + (y**3) + (3*(x**2)*(y**3)) 

# Ponto
V=[1, 1]

# Matriz Hessiana

H_mat = Hessian(f_test, V, h_=0.001)
H_mat_round = [[round(j, 5) for j in i] for i in H_mat] # aqui é a matriz com valores arredondados para 5 casas decimais.

print('O valor numérico para a matriz Hessiana e o ponto V=[1, 1] é dado por: {}'.format(H_mat_round))<jupyter_output>O valor numérico para a matriz Hessiana e o ponto V=[1, 1] é dado por: [[8.0, 18.00001], [18.00001, 24.0]]
{'fraction_non_alphanumeric': 0.1138900372054568, 'fraction_numerical': 0.03582747691883698, 'mean_word_length': 3.021889720144084, 'pattern_counts': {'":': 0, '<': 18, '<?xml version=': 0, '>': 18, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '168401', 'license': 'MIT', 'n_tokens_mistral': 5847, 'n_tokens_neox': 5378, 'n_words': 1998, 'path': 'CHAPTER__04.ipynb', 'repo_name': 'henriquebsena/unconstrained_optmization_book', 'size': '14560'}