| | | |

Introdução a PINNs

Ajude a manter o site livre, gratuito e sem propagandas. Colabore!

2.5 Aproximação de funções

Redes perceptron multicamadas (MLPs) são aproximadoras universais e vamos aplicá-las na aproximação de funções.

2.5.1 Teorema da aproximação universal

O teorema da aproximação universal pode ser visto como uma extensão do teorema de Weierstrass111111Karl Theodor Wilhelm Weierstrass, 1815 - 1897, matemático alemão. Fonte: Wikipédia: Karl Weierstrass. para redes neurais. Ele afirma que uma MLP com uma camada oculta e uma função de ativação não linear pode aproximar qualquer função contínua em um espaço compacto, com precisão arbitrária, desde que a rede tenha um número suficiente de neurônios na camada oculta.

Teorema 2.5.1.(Aproximação universal)

Seja φ: uma função contínua monotonicamente crescente e limitada. Sejam In=[0,1]n e fC(In;) uma dada função contínua de In em . Então, para todo ϵ>0, existe um m e um conjunto de constantes reais αi, bi e 𝒘i,j, i=1,2,,m, tais que a função

F(𝒙)=i=1mαiφ(j=1nwi,jxj+bi),𝒙In, (2.71)

é uma aproximação de f em In no sentido de que

|F(𝒙)f(𝒙)|<ϵ,𝒙In. (2.72)
Demonstração.

Consulte [4] para o caso de funções de ativação sigmoidal e [9] para o caso de funções de ativação mais gerais. ∎

O Teorema 2.5.1 é diretamente aplicável a uma rede MLP com uma camada escondida, tangente hiperbólica como função de ativação e uma camada de saída com função de ativação identidade. Ele garante que, para qualquer função contínua f em um espaço compacto, existe uma MLP com essa arquitetura que pode aproximar f com precisão arbitrária, desde que a camada escondida tenha um número suficiente de neurônios. A extensão do teorema para a aproximação de funções multivariadas é direta.

Redes MLP com função de ativação ReLU não se encaixam no Teorema 2.5.1. Entretanto, resultados teóricos sobre a aproximação de funções com tais redes também estão disponíveis. Consulte, por exemplo, [5].

O Teorema 2.5.1 é um resultado teórico importante, pois estabelece a capacidade de aproximação das MLPs. No entanto, ele não fornece uma maneira prática de determinar o número de neurônios necessários na camada escondida para alcançar uma determinada precisão, nem garante que o processo de treinamento da rede encontrará os pesos e biases adequados para realizar a aproximação desejada. Ainda, o teorema não estabelece que redes com apenas uma camada escondida sejam as melhores para a aproximação de funções, e sim que elas são suficientes. Na prática, redes com múltiplas camadas escondidas (redes profundas) podem ser mais eficientes para aproximar funções complexas.

2.5.2 Aproximação de função de uma variável

Vamos criar uma MLP para aproximar a função

y=sen(πx), (2.73)

para x[1,1]. A Figura 2.12 contém o gráfico da função e da aproximação obtida com a MLP. A linha contínua representa a função, a linha tracejada representa a MLP e os pontos representam as amostras de treinamento na última época do treinamento. O resultado foi obtido com o Código 11 que discutiremos com mais detalhes na sequência.

Refer to caption
Figura 2.12: Aproximação MLP da função y=sen(πx). Linha contínua: função. Linha tracejada: MLP. Pontos: amostras de treinamento na última época do treinamento.

No início do Código 11 verificamos se há uma GPU disponível para o treinamento da rede. Caso haja, o treinamento é feito na GPU, caso contrário, ele é feito na CPU. A GPU é um dispositivo de processamento paralelo que pode acelerar significativamente o treinamento de redes neurais, especialmente para redes grandes e conjuntos de dados extensos (mesmo que este não seja o caso aqui).

Usamos uma arquitetura de rede 11×251 (uma entrada, uma camada escondida com 25 neurônios e uma saída). A função de ativação da camada escondida é a tangente hiperbólica e a função de ativação da camada de saída é a função identidade. A cada época, ns=100 pontos randômicos são gerados no intervalo [1,1] e usados como amostras de treinamento. A função de perda é o erro médio quadrático

ε=1nss=1ns|y~(s)y(s)|2, (2.74)

dos valores estimados y~(s) e dos valores esperados y(s). O treinamento é interrompido quando a função de perda de validação atinge uma tolerância de 105 por 10 épocas consecutivas (persistência).

O otimizador escolhido é uma variante do método GDE com momentum para o treinamento da rede. Nesta variante, a direção de atualização dos parâmetros da rede é uma combinação linear da direção de atualização da época anterior e do gradiente atual. Esta abordagem tem um grande potencial de acelerar a convergência do treinamento, principalmente quando quando o GDE com momentum é aplicado em conjunto com uma boa inicialização dos parâmetros da rede [16]. O algoritmo do GDE com momentum utilizado no código é dado por121212O algoritmo do GDE com momentum apresentado aqui é a variante implementada na biblioteca PyTorch.

𝒅(e)=μ𝒅(e1)+𝜽ε(𝜽(e)), (2.75)
𝜽(e)=𝜽(e1)lr𝒅(e), (2.76)

onde 𝒅(e) é o vetor de atualização dos parâmetros da rede na época e, μ é o parâmetro de momentum. Na primeira época, aplica-se o método GDE sem momentum. No código, usamos μ=0.9 e lr=0.1.

Código 11: mlp_apfun_1d.py
1import torch
2import matplotlib.pyplot as plt
3
4# dispositivo CPU/GPU?
5device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
6print(f'device = {device}')
7
8# modelo
9model = torch.nn.Sequential()
10model.add_module('layer_1', torch.nn.Linear(1,25))
11model.add_module('fun_1', torch.nn.Tanh())
12model.add_module('layer_2', torch.nn.Linear(25,1))
13model.to(device)
14
15# treinamento
16
17# fun obj
18fun = lambda x: torch.sin(torch.pi*x)
19a = torch.tensor(-1., device=device)
20b = torch.tensor(1., device=device)
21
22# optimizador
23optim = torch.optim.SGD(model.parameters(),
24 lr=0.1, momentum=0.9)
25
26# num de amostras por época
27ns = 100
28
29# critério de parada
30# num max épocas
31nepochs = 50_000
32# tolerância
33tol = 1e-5
34# persistência
35ne_persist = 10
36count_persist = 0
37
38# amostras de validação
39X_val = torch.linspace(a, b, steps=100, device=device)
40X_val = X_val.reshape(-1,1) # pontos de validação
41y_vexp = fun(X_val) # valores esperados
42
43for epoch in range(nepochs):
44
45 model.train() # modo de treinamento
46
47 # amostras
48 X = (a - b) * torch.rand((ns,1), device=device) + b
49 y_exp = fun(X) # valores esperados
50
51 # propagação
52 y_est = model(X) # valores estimados
53
54 # função de perda
55 loss = torch.mean((y_est - y_exp)**2)
56
57 # retropropagação
58 optim.zero_grad()
59 loss.backward()
60 optim.step()
61
62 # validação
63 model.eval() # modo de avaliação
64 y_vest = model(X_val)
65 loss_val = torch.mean((y_vest - y_vexp)**2)
66
67 if epoch % 100 == 0:
68 print(f'{epoch}: loss train: {loss.item():.4e}, val: {loss_val.item():.4e}')
69
70 # critério de parada
71 if (loss_val.item() < tol):
72 count_persist += 1
73 print(f'{epoch}: loss train: {loss.item():.4e}, val: {loss_val.item():.4e}, persist: {count_persist}/{ne_persist}')
74 if count_persist >= ne_persist:
75 print('Critério de parada atingido com sucesso!')
76 break
77
78
79# verificação
80device = torch.device('cpu')
81model.to(device)
82model.eval() # modo de avaliação
83
84# plot
85fig, ax = plt.subplots()
86
87x = torch.linspace(a, b, steps=100)
88x = x.reshape(-1,1) # pontos de validação
89
90y_exp = fun(x) # valores esperados
91ax.plot(x, y_esp, label='fun')
92
93y_est = model(x).detach() # valores estimados
94ax.plot(x, y_est, ls='--', label='model')
95
96ax.legend()
97ax.grid()
98ax.set_xlabel('x')
99ax.set_ylabel('y')
100plt.show()

2.5.3 Aproximação de uma função de duas variáveis

Vamos criar uma MLP para aproximar a função

f(x1,x2)=sen(πx1)sen(πx2), (2.77)

para (x1,x2)𝒟:=[1,1]2. A Figura 2.13 contém o gráfico da função e de uma aproximação obtida com uma MLP. As linhas representam as isolinhas da função estimada pela MLP, o mapa de cores representa a saída da MLP e as estrelas representam os pontos de treinamento na última época do treinamento. O resultado foi obtido com o Código 12 que discutiremos com mais detalhes na sequência.

Refer to caption
Figura 2.13: Aproximação MLP da função y=sen(πx1)sen(πx2). Linhas: isolinhas da função. Mapa de cores: MLP. Estrelas: pontos de treinamentos na última época.

No Código 12, treinamos uma MLP com arquitetura 22×301 (duas entradas, duas camadas escondidas com 30 neurônios cada e uma saída). A função de ativação das camadas escondidas é a tangente hiperbólica e a função de ativação da camada de saída é a função identidade. A cada época, ns=400 pontos randômicos são gerados no domínio 𝒟 e usados como amostras de treinamento. A função de perda é o erro médio quadrático

ε=1nss=1ns|y~(s)y(s)|2, (2.78)

dos valores estimados y~(s)=𝒩(x1(s),x2(s)) e dos valores esperados y(s)=f(x1(s),x2(s)). O treinamento é interrompido quando a função de perda em pontos de validação atinge uma tolerância de 104.

Para este problema, o otimizador escolhido foi o método Adam (do inglês, Adaptive Moment Estimation, [10]). Assim como o GDE, ele é um método de otimização baseado no gradiente. Ele calcula taxas de aprendizado adaptativas para cada parâmetro da rede, com base nas estimativas de primeira e segunda ordem dos momentos dos gradientes. Os detalhes de implementação deste método foge aos objetivos aqui. Para mais detalhes, consulte [10]131313Para detalhes da implementação do método Adam na biblioteca PyTorch, consulte: ttps://docs.pytorch.org/docs/2.14/generated/torch.optim.Adam.htm..

Código 12: mlp_apfun_2d.py
1import torch
2import matplotlib.pyplot as plt
3
4# dispositivo CPU/GPU?
5device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
6print(f'device = {device}')
7
8# modelo
9n_h = 2 # num de camadas escondidas
10n_n = 30 # num de neurônios por camada escondida
11act_fun = torch.nn.Tanh() # função de ativação
12
13model = torch.nn.Sequential()
14# camada de entrada
15model.add_module('layer_1', torch.nn.Linear(2,n_n))
16model.add_module('fun_1', act_fun)
17# camadas escondidas
18for i in range(n_h-1):
19 model.add_module(f'layer_{i+2}', torch.nn.Linear(n_n,n_n))
20 model.add_module(f'fun_{i+2}', act_fun)
21# camada de saída
22model.add_module(f'layer_{n_h+1}', torch.nn.Linear(n_n,1))
23model.to(device) # envia modelo para o dispositivo
24
25# treinamento
26
27# fun obj
28def fun(X):
29 return torch.sin(torch.pi*X[:,0:1]) * \
30 torch.sin(torch.pi*X[:,1:2])
31
32
33# optimizador
34optim = torch.optim.Adam(model.parameters(), lr=0.01)
35
36# num de amostras por direção
37ns_train = 400
38
39# critério de parada
40# num max épocas
41nepochs = 50_000
42# tolerância
43tol = 1e-4
44
45# amostras de validação
46ns_x0_val = 20
47ns_x1_val = 20
48x0_v = torch.linspace(-1., 1., steps=ns_x0_val, device=device)
49x1_v = torch.linspace(-1., 1., steps=ns_x1_val, device=device)
50X0_v, X1_v = torch.meshgrid(x0_v, x1_v, indexing='ij')
51X0_v = X0_v.reshape(-1,1)
52X1_v = X1_v.reshape(-1,1)
53X_val = torch.hstack((X0_v, X1_v))
54y_vexp = fun(X_val) # valores esperados
55
56for epoch in range(nepochs):
57
58 model.train() # modo de treinamento
59
60 # pontos de amostragem
61 X = 2 * torch.rand(ns_train, 2, device=device) - 1.
62 # valores esperados
63 y_exp = fun(X)
64
65 # propagação (valores estimados)
66 y_est = model(X)
67
68 # função de perda
69 loss = torch.mean((y_est - y_exp)**2)
70
71 # retropropagação
72 optim.zero_grad()
73 loss.backward()
74 optim.step()
75
76 if epoch % 100 == 0:
77 # validação
78 model.eval() # modo de avaliação
79 y_vest = model(X_val)
80 loss_val = torch.mean((y_vest - y_vexp)**2)
81
82 print(f'{epoch}: loss train: {loss.item():.4e}, val: {loss_val.item():.4e}')
83
84 # critério de parada
85 if (loss_val.item() < tol):
86 print('Critério de parada atingido com sucesso!')
87 break
88
89
90# verificação
91device = torch.device('cpu')
92model.to(device)
93model.eval() # modo de avaliação
94
95# plot
96fig, ax = plt.subplots()
97
98x_plot = torch.linspace(-1., 1., steps=100)
99y_plot = torch.linspace(-1., 1., steps=100)
100X_plot, Y_plot = torch.meshgrid(x_plot, y_plot, indexing='ij')
101X = torch.hstack((X_plot.reshape(-1,1),
102 Y_plot.reshape(-1,1)))
103
104 # valores esperados
105y_exp = fun(X).reshape(100,100).numpy()
106 # valores estimados
107y_est = model(X).detach().reshape(100,100).numpy()
108
109X_plot = X_plot.numpy()
110Y_plot = Y_plot.numpy()
111
112# função objetivo
113levels = torch.linspace(y_exp.min(), y_exp.max(),
114 steps=10).numpy()
115cb = ax.contourf(X_plot, Y_plot, y_exp,
116 levels=levels, cmap='viridis')
117fig.colorbar(cb, ax=ax, label=r'$y$')
118
119# MLP
120cl = ax.contour(X_plot, Y_plot, y_est,
121 levels=levels, colors='k')
122
123ax.set_xlabel(r'$x_1$')
124ax.set_ylabel(r'$x_2$')
125plt.show()

2.5.4 Exercícios

E. 2.5.1.

Crie uma MLP para aproximar a função gaussiana f(x)=ex2 para x[1,1].

E. 2.5.2.

Crie uma MLP para aproximar a função y=sin(x) para x[π,π].

E. 2.5.3.

Crie uma MLP para aproximar a função y=2sin(x)+cos(x) para x[0,2π].

E. 2.5.4.

Crie uma MLP para aproximar a função gaussiana f(x,y)=ex2y2 para (x,y)[1,1]2.

E. 2.5.5.

Crie uma MLP para aproximar a função y=2sin(x1)cos(x2) para (x1,x2)[0,π]×[π,0].


Envie seu comentário

Aproveito para agradecer a todas/os que de forma assídua ou esporádica contribuem enviando correções, sugestões e críticas!

Opcional. Preencha seu nome para que eu possa lhe contatar.
Opcional. Preencha seu e-mail para que eu possa lhe contatar.
As informações preenchidas são enviadas por e-mail para o desenvolvedor do site e tratadas de forma privada. Consulte a política de uso de dados para mais informações.

Licença Creative Commons
Este texto é disponibilizado nos termos da Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional. Ícones e elementos gráficos podem estar sujeitos a condições adicionais.

Pedro H A Konzen
| | | |