//styles, look here: https://cdnjs.com/libraries/highlight.js/9.12.0

December 13, 2020

755 palavras 4 mins

Remastered: Interpolação

Este é mais um post que já foi feito faz tempo, mas eu quero repetir mais pra mostrar um truque que eu aprendi, além de deixar a programação mais clara (com um pouco de sorte).

Imagina que você conhece a função em alguns pontos. A gente teve um exemplo disso no post passado de programação dinâmica: a gente calcula o valor da função valor ou da função política em alguns pontos. A interpolação garante que na hora de otimizar a gente vai poder visitar valores da função valor entre esses pontos.

O problema é como conectar esses pontos. Os que entre vocês já fizeram análise sabem que sempre podemos arranjar um polinômio “próximo” de uma função qualquer se o espaço é compacto. Se você tem n pontos, você poderia se sentir tentado a buscar um polinômio de ordem \(n-1\) que passe por todos os pontos.

Isso nem sempre é razoável e a função super inocente \(\frac{1}{1+10x^2}\) mostra porque não. Primeiro, vamos dar uma olhada nela:


using Plots; #isso carrega o pacote plots

grid2 =  range(-1,1,length=50) #um grid de pontos para avaliar a função

f(x)=(1+10*x^2)^(-1) #a função

plot(grid2,f.(grid2))

É uma função não muito maluca. Eu vou usar o pacote polynomials pra gerar um polinômio de grau 50, a partir de 50 pontos equiespaçados:


using Polynomials #o pacote polynomials tem uma função fit que busca o polinômio que passa por todos os pontos

grid =  range(-2,2,length=10) #um grid de pontos para avaliar a função

f_hat = fit(grid,f.(grid)) #isso vai gerar um polinômio que passa por todos os pontos - ele retorna uma função, então você pode passar pontos e obter os valores da função de volta em qualquer ponto

plot(grid2,f.(grid2))
plot!(grid2,f_hat.(grid2))

A interpolação gera uma função que oscila malucamente entre os pontos. O mais curioso é que aumentar o número de pontos piora a situação. Com 50 pontos:


f_hat = Polynomials.fit(grid2,f.(grid2))

plot(grid2,f.(grid2))
plot!(grid2,f_hat.(grid2))

Isso serve como exemplo de como interpolações podem dar profundamente erradas. Nós estamos acostumados a pensar em overfitting quando a gente extrapola os dados, mas mesmo interpolações podem ser “do mal”. A interpolação claramente está fazendo um overfitting: nos pontos observados a aproximação é exata, mas fora deles é completamente maluco.

No post original eu sugeria fazer interpolação por pedaços, ligando cada pedaço por uma reta: eu vou usar o pacote Interpolations e o comando LinearInterpolation pra isso:


using Interpolations

lin_interp = LinearInterpolation(grid,f.(grid)) #gera uma interpolação linear, e retorna uma função

plot(grid2,f.(grid2))
plot!(grid2,lin_interp.(grid2))

Veja que ficou bem feio porque tem só 10 pedaços. Com mais pedaços a interpolação fica indistinguível da função verdadeira:


grid3 = range(-2,2,length = 100)

lin_interp = LinearInterpolation(grid2,f.(grid2))

plot(grid3,f.(grid3))
plot!(grid3,lin_interp.(grid3))

Você pode conectar os pontos por coisas que não são lineares - por exemplo, polinômios. Usar polinômios tem a vantagem que as derivadas de ordem mais alta existem - splines fazem isso e evitam o problema de oscilarem malucamente.

Mas mais interessante é que o grande problema aqui não é simplesmente o método de interpolação: é como o grid é gerado. Via de regra o grid é gerado com pontos equiespaçados - que é a maneira mais normal de pensar no problema. A gente não precisa necessariamente gerar pontos assim. Uma maneira totalmente maluca de gerar \(m\) pontos é:

\[x_k = -\cos\left(\frac{2k-1}{2m}\pi\right) \quad k =1, \ldots,m \]

Que gera pontos entre \([-1,1]\). Esse grid se deve ao Chebyschev. Veja que podemos pensar em maneiras de mover e encolher/crescer o intervalo para ficar entre \([a,b]\). Vamos criar uma função que pega uma sequência de pontos e gera o grid:


function cheby_nod(seq)
    m = length(seq)
     return map(k->-cos((2k-1)/(2m)*pi),seq)
 end

Escrevendo o post eu me dei conta que era mais simples passar \(m\) e criar a sequência depois. Eu podia muito bem ter usado um for, mas o map é brutalmente mais conciso. Vamos ver a qualidade da interpolação:


new_grid = 2*cheby_nod(1:10)

fit_cheb = Polynomials.fit(new_grid,f.(new_grid))

plot(grid2,f.(grid2))
plot!(grid2,fit_cheb.(grid2))

A coisa realmente legal aqui é que aumentar a quantidade de pontos melhora a qualidade da interpolação:


new_grid = 2*cheby_nod(1:20)

fit_cheb = Polynomials.fit(new_grid,f.(new_grid))

plot(grid2,f.(grid2))
plot!(grid2,fit_cheb.(grid2))

Eu nunca pensaria que gerar um grid usando cosseno seria uma boa ideia!


Alguns de vocês podem ter notado que eu gerei dois grids, um para gerar a interpolação e outro para avaliar a interpolação. Se vocês ainda não sacaram, o plot é gerado conectando os pontos por segmentos de reta, então se eu fizesse o gráfico da função original com o grid da interpolação os dois iam ser indistinguíveis.


Este post basicamente deve a sua existência ao Numerical Methods in Economics do Kenneth Judd - aparentemente uma segunda edição sairá em breve.