16회 실험 · 기울기가 하나로 정해지는 자리#
이 실험
이 회차의 정의는 새 개념 하나를 세우지 않는다. 11회의 극한을 특정한 함수 하나에 적용한 것이 도함수이다. 그 함수가 차분몫이고, 차분몫은 \(p\)에서 정의되지 않는다.
그래서 이 노트북은 “정의되지 않는다”는 말부터 코드로 확인한다. \(x = p\)를 대입하면 무슨 일이 일어나는지, 그런데도 \(x\)를 \(p\)에 가까이 보내면 값이 어디로 가는지를 나란히 놓는다.
그다음이 이 노트북의 중심이다. 수치로 극한을 구하려고 \(h\)를 계속 줄이면 어느 지점부터 답이 나빠진다. 부동소수점 뺄셈에서 자릿수가 사라지기 때문이며, \(h = 10^{-16}\)에서는 아예 \(0\)이 나온다. 11회 정의 18이 대입이 아닌 이유가 여기서 눈에 보인다.
서술 파트의 내용 |
여기서 확인하는 방법 |
|---|---|
정의 29의 정의역 |
1절이 \(x = p\)에서 오류를 받는다 |
약분은 \(x \ne p\)를 전제로 한다 |
1절이 sympy의 약분 결과와 원래 식을 비교한다 |
극한은 대입이 아니다 |
2절이 \(h\)를 줄일 때 오차가 다시 커지는 것을 본다 |
3절이 좌우 한쪽 극한을 따로 구한다 |
|
4절이 공식과 정의를 대조한다 |
|
5절이 할선이 접선으로 가는 것을 그린다 |
0. 준비#
이 회차에서 쓸 것만 불러온다. 이번에는 계단함수를 그리지 않으므로 상합·하합의 색 상수는 두지 않는다.
1. 차분몫은 그 점에서 값이 없다#
정의 29가 정의역을 \(S = U \setminus \{p\}\)로 잡은 이유를 확인한다. \(f(x) = x^2\)이고 \(p = 3\)이다.
# The difference quotient of f(x) = x**2 at p = 3
def quotient(x):
return (x ** 2 - 3.0 ** 2) / (x - 3.0)
print(f"{'x':>10} {'Q(x)':>16}")
print("-" * 27)
for x in (3.1, 3.01, 3.001, 2.999, 2.99, 2.9):
print(f"{x:>10} {quotient(x):>16.9f}")
x Q(x)
---------------------------
3.1 6.100000000
3.01 6.010000000
3.001 6.001000000
2.999 5.999000000
2.99 5.990000000
2.9 5.900000000
양쪽에서 \(6\)으로 다가간다. 그런데 \(x = 3\)을 그대로 넣으면 어떻게 되는지 본다.
# Substituting x = p is not allowed: the denominator is zero
try:
print(quotient(3.0))
except ZeroDivisionError as error:
print("ZeroDivisionError:", error)
ZeroDivisionError: float division by zero
분모가 \(0\)이라는 오류이다. 12회 정의 24가 몫을 \(g(x) \ne 0\)인 곳에서만 정의한 것이 이 자리에서 그대로 작동한다.
서술 파트는 약분하여 \(Q(x) = x + 3\)을 얻었다. sympy로 같은 약분을 시키고, 그것이 원래 식과 어디서 같고 어디서 다른지를 확인한다.
# sympy cancels the common factor; the cancelled form has a value where Q does not
x_sym = sp.Symbol("x", real=True)
Q_raw = (x_sym ** 2 - 9) / (x_sym - 3)
Q_cancelled = sp.cancel(Q_raw)
print("Q(x) as written :", Q_raw)
print("after cancelling :", Q_cancelled)
print("value of the cancelled form at x = 3 :", Q_cancelled.subs(x_sym, 3))
print("value of Q as written at x = 3 :", Q_raw.subs(x_sym, 3))
Q(x) as written : (x**2 - 9)/(x - 3)
after cancelling : x + 3
value of the cancelled form at x = 3 : 6
value of Q as written at x = 3 : nan
약분한 식은 \(x = 3\)에서 \(6\)을 준다. 원래 식은 zoo를 주는데, 이것은 sympy가 정의되지 않은 몫에 붙이는 표시이다.
흔한 오해 — 약분한 식이 원래 함수라고 생각하는 것
Q_cancelled가 \(x = 3\)에서 값을 갖는다고 해서 \(Q(3) = 6\)인 것은 아니다. 3회 F6의 약분은 \(x - 3 \ne 0\)을 전제로 한 조작이므로 그 결과도 같은 전제 아래에서만 원래 식과 같다.
두 함수는 정의역이 다르다. 정의역이 다르면 다른 함수이며, 12회 4절에서 \(r(x) = \frac{x^2-9}{x-3}\)을 두고 같은 이야기를 했다.
2. 두 형태와 그 값의 신뢰도#
보조정리 8가 (16.2)와 (16.3)이 같은 극한임을 보였다. 두 식을 각각 sympy에 넣어 확인한다.
# Form (16.2) sends x to p; form (16.3) sends h to 0. Same limit.
t, h = sp.symbols("t h", real=True)
f = t ** 2
point = 3
by_x = sp.limit((f.subs(t, x_sym) - f.subs(t, point)) / (x_sym - point),
x_sym, point)
by_h = sp.limit((f.subs(t, point + h) - f.subs(t, point)) / h, h, 0)
print("limit as x -> p (16.2) :", by_x)
print("limit as h -> 0 (16.3) :", by_h)
print("agree :", by_x == by_h)
limit as x -> p (16.2) : 6
limit as h -> 0 (16.3) : 6
agree : True
두 값이 같다. 대수적으로도 확인해 두면 \(\frac{(3+h)^2-9}{h} = 6 + h\)이므로 \(h \to 0\)에서 \(6\)이다.
# The h-form simplifies to 6 + h before the limit is taken
print("(f(3+h) - f(3)) / h =",
sp.simplify((f.subs(t, point + h) - f.subs(t, point)) / h))
(f(3+h) - f(3)) / h = h + 6
이제 수치로 같은 값을 구해 본다. \(h\)를 계속 줄이면 \(6\)에 가까워질 것 같다.
# Shrinking h numerically: the error falls, and then it rises again
def quotient_h(h):
return ((3.0 + h) ** 2 - 9.0) / h
print(f"{'h':>8} {'Q(h)':>18} {'|Q(h) - 6|':>14}")
print("-" * 42)
for power in range(1, 18):
step = 10.0 ** (-power)
value = quotient_h(step)
print(f"1e-{power:<5} {value:>18.12f} {abs(value - 6.0):>14.3e}")
h Q(h) |Q(h) - 6|
------------------------------------------
1e-1 6.100000000000 1.000e-01
1e-2 6.010000000000 1.000e-02
1e-3 6.000999999999 1.000e-03
1e-4 6.000100000012 1.000e-04
1e-5 6.000009999951 1.000e-05
1e-6 6.000001000928 1.001e-06
1e-7 6.000000087880 8.788e-08
1e-8 5.999999963535 3.646e-08
1e-9 6.000000496442 4.964e-07
1e-10 6.000000496442 4.964e-07
1e-11 6.000000496442 4.964e-07
1e-12 6.000533403494 5.334e-04
1e-13 5.986322548779 1.368e-02
1e-14 6.217248937901 2.172e-01
1e-15 5.329070518201 6.709e-01
1e-16 0.000000000000 6.000e+00
1e-17 0.000000000000 6.000e+00
\(h = 10^{-8}\)까지는 오차가 줄어든다. 그런데 그 뒤로 다시 커진다. \(h = 10^{-14}\)에서는 \(6.21\)이 나오고, \(h = 10^{-16}\)에서는 \(0\)이 나온다.
이유는 분자에 있다. \(3 + 10^{-16}\)을 부동소수점으로 적으면 \(3\)과 구별되지 않으므로 분자가 정확히 \(0\)이 된다.
# Why it collapses: 3 + h is not representable as distinct from 3
for power in (8, 15, 16):
step = 10.0 ** (-power)
print(f"h = 1e-{power:<3} : 3 + h == 3 is {3.0 + step == 3.0}, "
f"numerator = {(3.0 + step) ** 2 - 9.0:.6e}")
h = 1e-8 : 3 + h == 3 is False, numerator = 6.000000e-08
h = 1e-15 : 3 + h == 3 is False, numerator = 5.329071e-15
h = 1e-16 : 3 + h == 3 is True, numerator = 0.000000e+00
극한은 대입이 아니다
\(h\)를 작게 넣어 보는 것은 극한의 정의가 아니다. 11회 정의 18은 임의의 \(\epsilon\)에 대하여 \(\delta\)가 있다고 말하지, 작은 값을 하나 넣어 보라고 하지 않는다.
이 표가 그 차이를 드러낸다. 수치 대입은 \(h\)가 어느 크기 아래로 내려가면 오히려 틀린 답을 준다. 반면 \(6+h\)로 약분한 뒤 \(h \to 0\)을 취하는 것은 어떤 \(h\)도 대입하지 않으므로 이 문제를 겪지 않는다.
\(10^{-8}\) 근처가 가장 정확한 것은 약분 오차와 자릿수 손실이 균형을 이루는 자리이기 때문이다. 그 자리가 어디인지는 극한과 아무 관계가 없다.
3. 미분 가능이면 연속, 그러나 역은 아니다#
예제 11가 \(g(x) = |x|\)를 \(0\)에서 다루었다. 차분몫이 어떤 값을 갖는지 본다.
# The difference quotient of |x| at 0 is +1 on the right and -1 on the left
print(f"{'h':>12} {'Q(h)':>8}")
print("-" * 21)
for step in (0.1, 0.001, 1e-6, -1e-6, -0.001, -0.1):
print(f"{step:>12} {abs(step) / step:>8.1f}")
h Q(h)
---------------------
0.1 1.0
0.001 1.0
1e-06 1.0
-1e-06 -1.0
-0.001 -1.0
-0.1 -1.0
\(h\)를 아무리 줄여도 오른쪽에서는 \(1\)이고 왼쪽에서는 \(-1\)이다. 두 값이 가까워지지 않는다.
sympy로 한쪽 극한을 따로 구한다. 정의 31가 이 두 값에 이름을 붙인 것이다.
# The one-sided derivatives exist and differ, so the two-sided limit does not exist
g = sp.Abs(t)
right = sp.limit((g.subs(t, h) - g.subs(t, 0)) / h, h, 0, "+")
left = sp.limit((g.subs(t, h) - g.subs(t, 0)) / h, h, 0, "-")
print("right-hand derivative :", right)
print("left-hand derivative :", left)
print("equal :", right == left)
right-hand derivative : 1
left-hand derivative : -1
equal : False
방향을 주지 않으면 어떻게 되는지도 보아 둔다.
# sympy defaults to the right-hand limit when no direction is given
print("sp.limit without a direction :", sp.limit(g / t, t, 0))
print("but the left-hand limit is :", sp.limit(g / t, t, 0, "-"))
sp.limit without a direction : 1
but the left-hand limit is : -1
도구가 답하는 물음이 우리 물음과 다를 수 있다
sp.limit에 방향을 주지 않으면 sympy는 오른쪽 극한을 계산한다. 위 셀의 첫 줄이 \(1\)인 것은 그 때문이다.
그 값을 보고 “극한이 존재한다”고 읽으면 안 된다. 11회 4절이 우극한과 좌극한이 모두 있어도 두 값이 다르면 극한이 없다고 하였고, 여기가 그 경우이다. 답을 받기 전에 무엇을 물었는지 확인해야 한다.
미분 가능하지 않아도 연속일 수 있다는 것을 그림으로 본다. \(|x|\)는 \(0\)에서 끊기지 않는다.
그래프가 \(0\)에서 이어져 있으므로 연속이다. 그런데 왼쪽에서 온 기울기와 오른쪽에서 온 기울기가 다르다. 그 둘 사이에 놓을 값이 하나로 정해지지 않으므로 (16.11)의 \(f'(p)\) 자리에 넣을 것이 없다.
정리 47는 한쪽 방향만 말한다. 미분 가능이면 연속이다. 이 그림이 그 역이 성립하지 않음을 보인다.
4. 공식과 정의를 대조한다#
4절의 정리들은 도함수를 조립으로 구하게 해 준다. 그 결과가 정의로 구한 값과 같은지 확인한다.
정의로 구하는 쪽은 극한을 직접 계산한다. 공식은 쓰지 않는다.
# Left column: sympy's rules. Right column: the limit in definition (16.3).
def by_the_rules(expr, at):
return sp.simplify(sp.diff(expr, t)).subs(t, at)
def by_the_definition(expr, at):
return sp.simplify(
sp.limit((expr.subs(t, at + h) - expr.subs(t, at)) / h, h, 0))
정리 48가 다루는 상수함수와 항등함수부터, 정리 50의 거듭제곱과 정리 51의 몫까지 한 표에 넣는다.
# One row per theorem of section 4, all evaluated at p = 2
cases = [
(sp.Integer(7) + 0 * t, "constant"),
(t, "identity"),
(t ** 5, "power"),
(3 * t ** 2 + 2 * t - 1, "sum and scalar multiple"),
(t ** 3 * (t + 1), "product"),
((t ** 2 + 1) / (t + 2), "quotient"),
]
print(f"{'function':>20} {'rule':>10} {'definition':>12} {'same':>6} which")
print("-" * 72)
for expr, name in cases:
rule = by_the_rules(expr, 2)
definition = by_the_definition(expr, 2)
same = sp.simplify(rule - definition) == 0
print(f"{str(expr):>20} {str(rule):>10} {str(definition):>12} "
f"{str(same):>6} {name}")
function rule definition same which
------------------------------------------------------------------------
7 0 0 True constant
t 1 1 True identity
t**5 80 80 True power
3*t**2 + 2*t - 1 14 14 True sum and scalar multiple
t**3*(t + 1) 44 44 True product
(t**2 + 1)/(t + 2) 11/16 11/16 True quotient
여섯 줄 모두 두 값이 같다. 조립으로 얻은 규칙이 정의를 벗어나지 않는다는 것이 4절 증명들이 보인 것이고, 이 표가 그 결과를 확인한다.
곱의 미분에서 (16.7)의 모양이 왜 그런지를 확인해 둔다. 두 도함수를 그냥 곱하면 어떻게 되는가.
# The derivative of a product is not the product of the derivatives
u, v = t ** 2, t ** 3
correct = sp.expand(sp.diff(u * v, t))
naive = sp.expand(sp.diff(u, t) * sp.diff(v, t))
formula = sp.expand(sp.diff(u, t) * v + u * sp.diff(v, t))
print("d/dt (u*v) :", correct)
print("u' * v' :", naive, " <- not the same")
print("u'*v + u*v' (16.7) :", formula)
print("(16.7) matches :", sp.simplify(correct - formula) == 0)
d/dt (u*v) : 5*t**4
u' * v' : 6*t**3 <- not the same
u'*v + u*v' (16.7) : 5*t**4
(16.7) matches : True
\(u' v'\)은 \(6t^3\)이고 정답은 \(5t^4\)이다. 차수부터 다르다. (16.7)이 두 항의 합인 것은 증명에서 분자에 \(f(x)g(p)\)를 더하고 뺐기 때문이며, 그 조작이 항 두 개를 만든다.
몫의 미분도 같은 방식으로 확인한다.
# The quotient rule, checked against sympy's own differentiation
numerator, denominator = t ** 2 + 1, t + 2
direct = sp.diff(numerator / denominator, t)
by_formula = (sp.diff(numerator, t) * denominator
- numerator * sp.diff(denominator, t)) / denominator ** 2
print("difference :", sp.simplify(direct - by_formula))
print("the two agree :", sp.simplify(direct - by_formula) == 0)
difference : 0
the two agree : True
차가 \(0\)이다. 분모가 \(0\)이 되는 \(t = -2\)는 정의역에서 빠져 있고, 정리 51가 \(g(p) \ne 0\)을 요구하는 것이 그 조건이다.
5. 할선이 접선으로 간다#
정의 32는 접선을 (16.11)로 정의했다. 유도한 것이 아니다. 그래도 그 정의가 왜 이 모양인지는 그림으로 보인다.
두 점을 지나는 직선의 기울기가 1회 (1.1)이었다. 한 점을 \((3,9)\)에 고정하고 다른 점을 그쪽으로 보내면 기울기가 어떻게 변하는지 본다.
# Slopes of secant lines through (3,9), as the second point approaches 3
print(f"{'second point':>14} {'slope':>12}")
print("-" * 27)
for second in (5.0, 4.0, 3.5, 3.1, 3.01):
print(f"{second:>14} {(second ** 2 - 9.0) / (second - 3.0):>12.6f}")
print(f"{'the limit':>14} {6.0:>12.6f}")
second point slope
---------------------------
5.0 8.000000
4.0 7.000000
3.5 6.500000
3.1 6.100000
3.01 6.010000
the limit 6.000000
기울기가 \(6\)으로 다가간다. 그 값이 \(f'(3)\)이고, (16.11)에 넣으면 \(y = 9 + 6(x-3) = 6x - 9\)이다.
얇은 선 세 개가 할선이고 점선이 접선이다. 두 번째 점이 \((3,9)\)에 가까워질수록 할선이 점선에 붙는다.
그림에서 할선이 접선에 가까워지는 것과 기울기의 극한이 존재하는 것은 다른 진술이다. 앞의 것은 눈으로 본 것이고 뒤의 것은 정의 30가 요구하는 것이다. 이 회차가 한 일은 뒤의 것을 정의하고, 그 값으로 접선을 정한 것이다.
마치며#
차분몫에 \(x = 3\)을 넣었더니 ZeroDivisionError가 났다. 정의역에서 그 점이 빠져 있다는 것이 코드에서 오류로 나타난다. 약분한 식은 그 점에서 값을 갖지만 정의역이 다르므로 다른 함수이다.
(16.2)와 (16.3)이 같은 값 \(6\)을 주었다. 보조정리 8가 보장하는 것이 그것이다.
\(h\)를 줄여 가며 수치로 구한 값은 \(10^{-8}\) 근처에서 가장 정확했고 그 뒤로 나빠졌다. \(10^{-16}\)에서는 \(0\)이 나왔다. 극한은 작은 값을 대입하는 것이 아니다.
\(|x|\)의 좌우 한쪽 도함수값이 \(-1\)과 \(1\)로 갈렸다. 그래프는 이어져 있으므로 연속이며, 연속이지만 미분 가능하지 않은 예이다. sp.limit이 방향 없이 \(1\)을 돌려준 것은 그 도구의 기본값이지 극한의 존재가 아니었다.
여섯 가지 함수에서 공식과 정의가 같은 값을 주었다. \(u'v'\)이 \(6t^3\)이고 정답이 \(5t^4\)인 것도 확인했다.
더 해 볼 것#
2절의 오차 표를 \(f(x) = x^2\) 대신 \(f(x) = x^{10}\)으로 \(p = 1\)에서 다시 만들어 보자. 오차가 가장 작아지는 \(h\)가 같은 자리인가.
3절에서 \(g(x) = x\,|x|\)를 \(0\)에서 보자. 좌우 한쪽 도함수값이 얼마인가. 이 함수는 \(0\)에서 미분 가능한가.
4절의 표에 \(f(x) = (x^2+1)^{10}\)을 넣으면 두 열이 같은 값을 주는가.
sp.diff는 어떤 규칙을 쓴 것인가. 이 회차의 정리 다섯 개만으로 그 값을 구하려면 무엇을 해야 하는가.5절의 할선 기울기를 \(f(x) = |x|\)의 \(p = 0\)에서 계산해 보자. 두 번째 점을 오른쪽에서 보낼 때와 왼쪽에서 보낼 때 무엇이 다른가.
짝이 되는 서술