이상적분은 언제나 골칫덩이지만 풀이를 포기할 수는 없다. 보통 적분이 어려운 이유는 어지간한 트릭을 써봐도 정작 그 트릭들이 계속 새로운 문제를 만들어내기 때문이다. 다행스럽게도 수치적 적분에서는 함수만 정확히 가지고 있다면 자세한 과정이 없어도 어쨌든 계산을 해낼 수 있다. 대표적인 방법이 치환으로, 함수가 싱귤러할지라도 분모로 뺄 수 있는 항이 xp 정도라면 쉽게 해결할 수 있다.
예시
우회 적분
예로써 다음의 이상적분을 생각해보자.
∫01xe−xdx≈1.493648265625m(x):=∫0xt1dt=2x 으로 두면 xdm=dx 이므로
∫01xe−xdx===∫m=02xe−xxdm∫02e−xdm∫02e−m2/4dm
이와 같은 트릭을 통해 폐구간에 대한 적분을 만들었다면 적분 기법을 적용할 수 있다. 실제로 사다리꼴 룰을 통해 수치를 계산해보면 다음과 같은 결과를 얻을 수 있다.
속도 개선
한편 더욱 흥미로운 예로써 다음의 경우를 살펴보자.
∫01e−xxdx≈0.378944691641
가장 먼저 드는 생각은 ‘딱히 치환이 필요없다’는 점일 것이다. e−xx 는 [0,1] 에서 싱귤래러티를 갖지 않기 때문에 있는 그대로 사다리꼴 룰을 적용시킬 수 있다. 하지만 x=0 에서 미분가능하지 않은데, 이 점 때문에 에러의 바운드가 없어지고 수렴 속도가 느려진다. 위와 마찬가지로 m(x):=∫0xt1dt=2x 으로 두면 xdm=dx 이므로
∫01e−xxdx===∫m=02e−xxxdm∫02e−xxdm∫02e−m2/44m2dm
보다시피 치환적분을 하고 나면 m=0 에서의 싱귤래러티가 없어진다. 마찬가지로 사다리꼴 룰을 통해 수치를 계산해보면 다음과 같은 결과를 얻을 수 있다. 왼쪽이 그냥 계산한 것이고 오른쪽이 치환을 거치고 계산한 것이다.
치환 없이 계산해도 뭐 수렴이야 한다. 하지만 치환을 거친 계산 결과가 무섭게 참값으로 수렴하는 것에 비해, 치환 없이 계산한 값은 안 무서운 속도로 수렴함을 확인할 수 있다. 속도에 목숨 거는 수치해석의 특징상 이러한 로스는 가능한 한 줄여주는 게 좋다. 언젠가 선택해야할 때가 온다면 코드의 간결함, 아름다움보다는 수학자의 꼼꼼하고 치밀한 트릭을 골라야할 것이다.
증명
전략: 실제 계산은 컴퓨터에게 맡길 것이기 때문에 폐구간에서의 적분으로만 바꾸면 된다. 방법은 본질적으로 같다.
[1]
m:=∫0xtp1dt=1−p1x1−p
와 같이 두면 dxdm=x−p⟺xpdm=dx 이므로
∫0bxpf(x)dx==∫m=01−p1b1−pxpf(x)xpdm∫01−p1b1−pf([(1−p)m]1−p1)dm
■
[2]
m:=∫x∞tp1dt=−1−p1x1−p
와 같이 두면 dxdm=−x−p⟺−xpdm=dx 이므로
∫a∞xpf(x)dx==∫m=−1−p1a1−p0xpf(x)(−xp)dm∫0p−11a1−pf([(p−1)m]1−p1)dm
■
구현
다음은 예제들을 수치적으로 푼 파이썬 코드다.
import numpy as np
deff1(x) :
return np.exp(-x)
deff2(x) :
return np.exp(-x)*np.sqrt(x)
deff3(x) :
return np.exp(-x)*x
defI(f,a,b,n) :
h=(b-a)/n
x=np.linspace(a,b,n+1)
x=f(x)
x[0]=x[0]/2
x[-1]=x[-1]/2return(h*sum(x))
defCVI(f,a,b,n) :
b=2*np.sqrt(b)
h=(b-a)/n
x=np.linspace(a,b,n+1)
x=f((x**2)/4)
x[0]=x[0]/2
x[-1]=x[-1]/2return(h*sum(x))
TV1=1.49364826562485405079
TV2=0.37894469164098470380print("")
print("True Value 1 : %2.12f" % TV1)
print("| n | In | I-In |")
print("="*32)
for n inrange(3,10) :
print("| %3d | %8.6f | %9.2e |" % (
2**n,
CVI(f1,0,1,2**n),
TV1-CVI(f1,0,1,2**n)))
print("")
print("True Value 2 : %2.12f" % TV2)
print("| n | In | I-In |")
print("="*32)
for n inrange(3,10) :
print("| %3d | %8.6f | %9.2e |" % (
2**n,
I(f2,0,1,2**n),
TV2-I(f2,0,1,2**n)))
print("")
print("True Value 2 : %2.12f" % TV2)
print("| n | In | I-In |")
print("="*32)
for n inrange(3,10) :
print("| %3d | %8.6f | %9.2e |" % (
2**n,
CVI(f3,0,1,2**n),
TV2-CVI(f3,0,1,2**n)))
print("")
Atkinson. (1989). An Introduction to Numerical Analysis(2nd Edition): p305~306. ↩︎