16회 실험 · 기울기가 하나로 정해지는 자리#

이 실험

짝 서술

16회 · 도함수의 정의

확인하는 것

차분몫이 \(p\)에서 정의되지 않는다는 것을 오류로 확인한다. (16.2)와 (16.3)이 같은 값을 주는지 본다. 공식으로 구한 값과 정의로 구한 값을 대조한다

이 회차의 정의는 새 개념 하나를 세우지 않는다. 11회의 극한을 특정한 함수 하나에 적용한 것이 도함수이다. 그 함수가 차분몫이고, 차분몫은 \(p\)에서 정의되지 않는다.

그래서 이 노트북은 “정의되지 않는다”는 말부터 코드로 확인한다. \(x = p\)를 대입하면 무슨 일이 일어나는지, 그런데도 \(x\)를 \(p\)에 가까이 보내면 값이 어디로 가는지를 나란히 놓는다.

그다음이 이 노트북의 중심이다. 수치로 극한을 구하려고 \(h\)를 계속 줄이면 어느 지점부터 답이 나빠진다. 부동소수점 뺄셈에서 자릿수가 사라지기 때문이며, \(h = 10^{-16}\)에서는 아예 \(0\)이 나온다. 11회 정의 18이 대입이 아닌 이유가 여기서 눈에 보인다.

서술 파트의 내용

여기서 확인하는 방법

정의 29의 정의역

1절이 \(x = p\)에서 오류를 받는다

약분은 \(x \ne p\)를 전제로 한다

1절이 sympy의 약분 결과와 원래 식을 비교한다

보조정리 8

2절이 (16.2)와 (16.3)을 각각 계산한다

극한은 대입이 아니다

2절이 \(h\)를 줄일 때 오차가 다시 커지는 것을 본다

예제 11

3절이 좌우 한쪽 극한을 따로 구한다

정리 48부터 정리 51까지

4절이 공식과 정의를 대조한다

정의 32

5절이 할선이 접선으로 가는 것을 그린다

0. 준비#

이 회차에서 쓸 것만 불러온다. 이번에는 계단함수를 그리지 않으므로 상합·하합의 색 상수는 두지 않는다.

Hide code cell source

# Setup: imports, plotting defaults, and the colors used in this lecture
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp

plt.rcParams["figure.figsize"] = (11.0, 4.2)
plt.rcParams["figure.dpi"] = 110
plt.rcParams["axes.grid"] = True
plt.rcParams["grid.alpha"] = 0.3
plt.rcParams["font.size"] = 11

C_FUNC = "#000000"
C_SEC = "#2ca02c"
C_TAN = "#d62728"
C_CEX = "#9467bd"

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\)에서 끊기지 않는다.

Hide code cell source

# |x| is continuous at 0 but has no tangent there: two slopes meet
fig, ax = plt.subplots()
xs = np.linspace(-1.0, 1.0, 401)
ax.plot(xs, np.abs(xs), color=C_CEX, linewidth=2.0, label="g(x) = |x|")

left_part = np.linspace(-1.0, 0.0, 50)
right_part = np.linspace(0.0, 1.0, 50)
ax.plot(left_part, -left_part, color=C_TAN, linestyle="--", linewidth=1.2,
        label="slope -1 from the left")
ax.plot(right_part, right_part, color=C_SEC, linestyle="--", linewidth=1.2,
        label="slope +1 from the right")
ax.plot([0.0], [0.0], marker="o", color=C_FUNC, zorder=5)

ax.set_xlim(-1.05, 1.05)
ax.set_ylim(-0.2, 1.15)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("continuous at 0, but the two one-sided slopes disagree")
ax.legend(loc="upper center", fontsize=9)
fig.tight_layout()
plt.show()
../_images/937126800e69e20cb662ef9900eadfbd36ff3c349020190c3c515cbd6e7dfcd6.png

그래프가 \(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\)이다.

Hide code cell source

# Secants through (3,9) closing in on the tangent line y = 6x - 9
fig, ax = plt.subplots()
xs = np.linspace(1.0, 5.0, 400)
ax.plot(xs, xs ** 2, color=C_FUNC, linewidth=1.8, label="f(x) = x**2")

for second in (5.0, 4.0, 3.5):
    slope = (second ** 2 - 9.0) / (second - 3.0)
    ax.plot(xs, 9.0 + slope * (xs - 3.0), color=C_SEC, linewidth=0.9, alpha=0.75)
    ax.plot([second], [second ** 2], marker="o", color=C_SEC, markersize=4)

ax.plot(xs, 6.0 * xs - 9.0, color=C_TAN, linestyle="--", linewidth=1.6,
        label="tangent at p = 3 : y = 6x - 9")
ax.plot([3.0], [9.0], marker="o", color=C_FUNC, zorder=5)

ax.set_xlim(1.0, 5.0)
ax.set_ylim(-2.0, 26.0)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("secants through (3,9) approach the tangent")
ax.legend(loc="upper left", fontsize=9)
fig.tight_layout()
plt.show()
../_images/7faf982a1e6ddb04b9ff9aae084407b30a90bc2dc4431f4f4d88b35fe657b23b.png

얇은 선 세 개가 할선이고 점선이 접선이다. 두 번째 점이 \((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\)인 것도 확인했다.

서술 파트의 내용

이 노트북의 코드

정의 29의 정의역

1절 ZeroDivisionError

약분과 원래 식의 차이

1절 Q_cancelled와 Q_raw의 subs

보조정리 8

2절 by_x와 by_h

극한은 대입이 아니다

2절 오차가 다시 커지는 표

정의 31

3절 sp.limit(..., "+")과 "-"

정리 47의 역이 거짓

3절 \(\lvert x \rvert\) 그림

4절 정리 다섯 개

4절 by_the_rules와 by_the_definition 표

(16.7)이 두 항인 이유

4절 naive와 formula의 대비

(16.11)

5절 접선 그림

더 해 볼 것#

  1. 2절의 오차 표를 \(f(x) = x^2\) 대신 \(f(x) = x^{10}\)으로 \(p = 1\)에서 다시 만들어 보자. 오차가 가장 작아지는 \(h\)가 같은 자리인가.

  2. 3절에서 \(g(x) = x\,|x|\)를 \(0\)에서 보자. 좌우 한쪽 도함수값이 얼마인가. 이 함수는 \(0\)에서 미분 가능한가.

  3. 4절의 표에 \(f(x) = (x^2+1)^{10}\)을 넣으면 두 열이 같은 값을 주는가. sp.diff는 어떤 규칙을 쓴 것인가. 이 회차의 정리 다섯 개만으로 그 값을 구하려면 무엇을 해야 하는가.

  4. 5절의 할선 기울기를 \(f(x) = |x|\)의 \(p = 0\)에서 계산해 보자. 두 번째 점을 오른쪽에서 보낼 때와 왼쪽에서 보낼 때 무엇이 다른가.

짝이 되는 서술

16회 · 도함수의 정의