πŸ”– Automatic Differentiation Series

  1. πŸ’» Numerical Differentiation
  2. πŸ–ŠοΈ Symbolic Differentiation
  3. πŸ€– Automatic Differentiation

미뢄은 ν¬λŒ€μ˜ μ²œμž¬μ˜€λ˜ μ•„μ΄μž‘ λ‰΄ν„΄μ΄λž˜λ‘œ μ—†μ–΄μ„œλŠ” μ•ˆ 될 μ€‘μš”ν•œ κ°œλ…μ΄ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ¬Έκ³Όλ‚˜ 이과 λͺ¨λ‘ ꡬ뢄없이 κ³ λ“±ν•™κ΅λ•Œ 적어도 λ‹€ν•­ν•¨μˆ˜μ˜ 미뢄법은 배우며 μ΄κ³΅κ³„λŠ” 거의 λͺ¨λ“  ν•™κ³Όμ—μ„œ 미뢄방정식을 λ‹€λ£Ήλ‹ˆλ‹€. λ¬Όλ¦¬ν•™κ³Όμ˜ κ²½μš°λŠ” μ’€ 더 λ―ΈλΆ„ μ˜μ‘΄λ„κ°€ μ‹¬ν•œλ°, λ‹Ήμž₯ 물리의 μ‹œμž‘μ΄λΌκ³  ν•  수 μžˆλŠ” κ³ μ „μ—­ν•™λΆ€ν„° 였일러-λΌκ·Έλž‘μ£Ό 방정식(Euler-Lagrange equation)에 μ˜μ‘΄ν•˜λ©° λ¬Όλ¦¬ν•™κ³Όμ˜ 핡심이라 ν•  수 μžˆλŠ” μ „μžκΈ°ν•™, μ–‘μžμ—­ν•™μ€ 거의 λͺ¨λ“  μˆ˜μ‹μ— 미뢄이 빠지지 μ•ŠμŠ΅λ‹ˆλ‹€.

λ‹Ήμ—°ν•˜κ²Œλ„ 수치 계산 λΆ„μ•Όμ—μ„œλ„ 미뢄은 항상 λ“±μž₯ν•©λ‹ˆλ‹€. λ‹€λ§Œ, 인간이 미뢄을 μ΄ν•΄ν•˜λŠ” 방식과 컴퓨터가 μ΄ν•΄ν•˜λŠ” 방식은 차이가 μžˆκΈ°μ— 미뢄을 λ°›μ•„λ“€μ΄λŠ” 방법 μ—­μ‹œ 쑰금 λ‹€λ¦…λ‹ˆλ‹€. 일단 λ―Έμ λΆ„ν•™μ—μ„œ κ°„λ‹¨ν•˜κ²Œ λ°°μš°λŠ” λ„ν•¨μˆ˜μ˜ μ •μ˜λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

$$ f’(x) = \lim_{h \rightarrow 0} \frac{f(x+h) - f(x)}{h} $$

예λ₯Ό λ“€μ–΄ $f(x) = x^2$을 λ―ΈλΆ„ν•œλ‹€λ©΄ λ‹€μŒκ³Ό 같이 κ°„λ‹¨ν•˜κ²Œ 계산할 수 μžˆμŠ΅λ‹ˆλ‹€.

$$ \lim_{h \rightarrow 0} \frac{(x+h)^2 - x^2}{h} = \lim_{h \rightarrow 0}\frac{2hx + h^2}{h} = 2x $$

ν•˜μ§€λ§Œ 컴퓨터가 이 문제λ₯Ό μ ‘ν•˜κ²Œ λœλ‹€λ©΄ μƒλ‹Ήνžˆ λ‚œκ°ν•œ 상황에 λ†“μž…λ‹ˆλ‹€. κ·Ήν•œμ΄λΌλŠ” κ°œλ…μ΄ μ»΄ν“¨ν„°μ˜ ꡬ쑰와 λŒ€μΉ˜λ˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. $h$κ°€ $0$으둜 κ°€λŠ” κ·Ήν•œμ΄λΌλŠ” 것은 0에 ν•œμ—†μ΄ κ°€κΉŒμ΄ μ ‘κ·Όν•œλ‹€λŠ” 의미둜 $h$와 $0$의 차이가 κ·Έ μ–΄λ–€ μˆ«μžλ³΄λ‹€ μž‘κ²Œ λ˜μ–΄μ•Ό ν•œλ‹€λŠ” 뜻인데, μ»΄ν“¨ν„°λŠ” ꡬ쑰 상 ν•œμ—†μ΄ κ°€κΉŒμ΄ κ°€λŠ” 것이 λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€. ν˜„μž¬ λŒ€λΆ€λΆ„μ„ μ°¨μ§€ν•˜κ³  μžˆλŠ” 64bit μ»΄ν“¨ν„°λŠ” $2^{-53}$ μ΄ν•˜, 즉, λŒ€λž΅ $10^{-16}$μ΄ν•˜μ˜ μ°¨μ΄λŠ” $0$κ³Ό ꡬ뢄할 수 μ—†μŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ μ‚¬λžŒλ“€μ€ 크게 두 가지 λ°©μ‹μœΌλ‘œ 이λ₯Ό ν•΄κ²°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

Β 


πŸ’» 수치적 λ―ΈλΆ„ (Numerical Differentiation)

μ»΄ν“¨ν„°λŠ” κ·Ήν•œμ„ 본질적으둜 λ‹€λ£° 수 μ—†μ§€λ§Œ, λŒ€λΆ€λΆ„μ˜ κ³„μ‚°μ—μ„œλŠ” $10^{-16}$ 정도면 μ•„μ£Ό μΆ©λΆ„ν•œ 정밀도일 수 μžˆμŠ΅λ‹ˆλ‹€. ν˜Ήμ€ λ‹¨μœ„λ₯Ό μ‘°μ •ν•˜λ©΄μ„œ μΆ©λΆ„ν•œ 정밀도가 λ˜λ„λ‘ λ§Œλ“œλŠ” 방법도 μ‘΄μž¬ν•©λ‹ˆλ‹€. λ”°λΌμ„œ κ·Ήν•œμ„ λ‹€λ£¨λŠ” λŒ€μ‹  μ•„μ£Ό μž‘μ€ $h$λ₯Ό μ΄μš©ν•˜μ—¬ κ·Ήν•œμ˜ 근삿값을 κ΅¬ν•˜μ—¬ 계산에 μ΄μš©ν•  수 μžˆλŠ”λ°, μ΄λŸ¬ν•œ 방법을 수치적 미뢄이라 ν•©λ‹ˆλ‹€. 일단 μ•„μ£Ό κ°„λ‹¨ν•˜κ²Œ 수치적 미뢄을 κ΅¬ν˜„ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

# Python
def diff(f, x, h):
    return (f(x+h) - f(x)) / h

수치적 λ―ΈλΆ„μ˜ Python κ΅¬ν˜„μ€ λ†€λΌμšΈ μ •λ„λ‘œ μ•„μ£Ό κ°„λ‹¨ν•©λ‹ˆλ‹€. ν•¨μˆ˜μ™€ λ³€μˆ˜ 그리고 정밀도λ₯Ό λ„£μ–΄μ£Όλ©΄ λ°”λ‘œ 미뢄값이 λ‚˜μ˜΅λ‹ˆλ‹€.

// Rust
fn diff<F: Fn(f64) -> f64>(f: F, x: f64, h: f64) -> f64 {
    (f(x+h) - f(x)) / h
}

Rust κ΅¬ν˜„λ„ 비ꡐ적 κ°„λ‹¨ν•œ νŽΈμ΄μ§€λ§Œ, νƒ€μž…μ„ λͺ…μ‹œν•΄μ•Ό λ˜λŠ” 점이 Python과의 차이λ₯Ό λ§Œλ“­λ‹ˆλ‹€. Rustμ—μ„œ ν•¨μˆ˜λ₯Ό 인수둜 받을 λ•ŒλŠ” μœ„μ™€ 같이 μ œλ„ˆλ¦­ νƒ€μž…(Generic Type)으둜 λ°›λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. κ·Έλž˜μ•Ό λͺ…μ‹œμ  ν•¨μˆ˜λ‚˜ ν΄λ‘œμ €(Closure) ꡬ뢄 없이 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 μ½”λ“œλ“€μ„ μ΄μš©ν•˜μ—¬ $f(x) = x^2$의 $x=1$μ—μ„œμ˜ λ―ΈλΆ„ κ³„μˆ˜λ₯Ό κ΅¬ν•΄λ΄…μ‹œλ‹€.

// Rust
fn main() {
    println!("{}", diff(f, 1f64, 1e-6));
}

fn diff<F: Fn(f64) -> f64>(f: F, x: f64, h: f64) -> f64 {
    (f(x+h)-f(x)) / h
}

fn f(x: f64) -> f64 {
    x.powi(2)
}

μ½”λ“œμ—μ„œ μ•Œ 수 μžˆλ“―μ΄ μ •λ°€λ„λŠ” $h=10^{-6}$을 λŒ€μž…ν•˜μ—¬ κ³„μ‚°ν•˜μ˜€μŠ΅λ‹ˆλ‹€. κ²°κ³ΌλŠ” $2.0000009999243673$으둜 μ†Œμˆ«μ  6번째 μžλ¦¬κΉŒμ§€λŠ” 이둠 값인 $2$와 μΌμΉ˜ν•¨μ„ λ³΄μ—¬μ€λ‹ˆλ‹€. 이 수치적 λ―ΈλΆ„μ½”λ“œλŠ” κ°„λ‹¨ν•˜κ³  λΉ λ₯΄κ²Œ λ―ΈλΆ„ 값을 ꡬ할 수 μžˆλ‹€λŠ” μž₯점이 μžˆμ§€λ§Œ, λ„ν•¨μˆ˜λ₯Ό κ΅¬ν•˜κΈ° μœ„ν•΄μ„œλŠ” 반볡적으둜 ν•¨μˆ˜λ₯Ό λŒ€μž…ν•΄μ•Ό λœλ‹€λŠ” μ μ—μ„œ λΆˆνŽΈν•¨μ„ μ•ΌκΈ°ν•©λ‹ˆλ‹€. λ”°λΌμ„œ λ„ν•¨μˆ˜λ₯Ό κ΅¬ν•˜κΈ° μœ„ν•΄μ„œλŠ” 쑰금 더 μ½”λ“œλ₯Ό λŠ˜λ €μ•Ό ν•©λ‹ˆλ‹€. λ¨Όμ € ꡬ쑰체λ₯Ό μ΄μš©ν•˜λŠ” 객체지ν–₯적 방법을 μ‚¬μš©ν•˜μ—¬ κ΅¬ν˜„ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

// Rust
struct Derivative<F: Fn(f64) -> f64> {
    pub f: F,
    pub h: f64,
}

impl<F: Fn(f64) -> f64> Derivative<F> {
    fn f(&self, x: f64) -> f64 {
        (self.f)(x)
    }

    fn calc(&self, x: f64) -> f64 {
        (self.f(x+self.h) - self.f(x)) / self.h
    }
}

μ΄λ ‡κ²Œ ν•˜λ©΄ ν•¨μˆ˜μ™€ μ •λ°€λ„λŠ” 초기 μ„ μ–Έμ‹œμ—λ§Œ μž…λ ₯ν•˜λ©΄ 되고, calc λ©”μ†Œλ“œλ₯Ό μ΄μš©ν•˜μ—¬ μ—¬λŸ¬ $x$ κ°’μ—μ„œ 계산이 κ°€λŠ₯ν•΄μ§‘λ‹ˆλ‹€. f λ©”μ†Œλ“œλŠ” 보닀 νŽΈν•˜κ²Œ self.f(x)λ₯Ό μ΄μš©ν•˜κΈ° μœ„ν•΄ μ„ μ–Έν•˜μ˜€μŠ΅λ‹ˆλ‹€. 만일 μ΄λŸ¬ν•œ λ©”μ†Œλ“œκ°€ μ—†λ‹€λ©΄ (self.f)(x) 꼴둜 μž…λ ₯ν•΄μ•Όλ§Œ ν•©λ‹ˆλ‹€. 그럼 이제 이 μ½”λ“œλ₯Ό μ΄μš©ν•˜μ—¬ μ•žμ—μ„œμ˜ μ˜ˆμ‹œλ₯Ό κ΅¬ν˜„ν•΄λ΄…μ‹œλ‹€.

// Rust
fn main() {
    let df = Derivative {
        f,
        h: 1e-6,
    };
    println!("{}", df.calc(1f64));
}

fn f(x: f64) -> f64 {
    x.powi(2)
}

struct Derivative<F: Fn(f64) -> f64> {
    pub f: F,
    pub h: f64,
}

impl<F: Fn(f64) -> f64> Derivative<F> {
    fn f(&self, x: f64) -> f64 {
        (self.f)(x)
    }

    fn calc(&self, x: f64) -> f64 {
        (self.f(x+self.h) - self.f(x)) / self.h
    }
}

λ‹Ήμ—°ν•˜κ²Œλ„ 닡은 μ•„κΉŒμ˜ κ²½μš°μ™€ κ°™κ²Œ λ‚˜μ˜΅λ‹ˆλ‹€. μ΄λ²ˆμ—λŠ” μ§„μ§œ “λ„ν•¨μˆ˜"λ₯Ό λ§Œλ“œλŠ” ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ˜ 고계 ν•¨μˆ˜(Higher order function) κ°œλ…μ„ μ΄μš©ν•˜μ—¬ κ΅¬ν˜„ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

// Rust
fn derivative<F: Fn(f64) -> f64>(f: F, h: f64) -> impl Fn(f64) -> f64 {
    move |x: f64| (f(x+h) - f(x)) / h
}

일단 F둜 f64 -> f64 ν•¨μˆ˜ 역할을 ν•˜λŠ” λͺ¨λ“  νƒ€μž…μ„ 받을 수 μžˆλ‹€λŠ” 것은 μ•žμ—μ„œμ™€ κ°™μŠ΅λ‹ˆλ‹€. λ‹€λ§Œ λ°˜ν™˜ νƒ€μž… 뢀뢄에 λ‚―μ„  ν‚€μ›Œλ“œλ“€μ΄ μžˆμŠ΅λ‹ˆλ‹€. Rust의 Genericμ—λŠ” 크게 두 가지 방식이 μ‘΄μž¬ν•©λ‹ˆλ‹€. 첫 λ²ˆμ§ΈλŠ” μ•žμ„œ 봀던 F와 같이 Type placeholderλ₯Ό μ‚¬μš©ν•˜λŠ” 방식이고, 두 λ²ˆμ§ΈλŠ” impl Trait처럼 implν‚€μ›Œλ“œλ₯Ό μ΄μš©ν•˜λŠ” 방식이 μžˆμŠ΅λ‹ˆλ‹€. 두 방식 λͺ¨λ‘ 큰 μ°¨μ΄λŠ” μ—†μ§€λ§Œ, μ—¬λŸ¬ 개의 νƒ€μž…μ΄ 같이 쓰일 λ•Œμ— 각 νƒ€μž…λ“€μ΄ 같은 νƒ€μž…μΈμ§€, λ‹€λ₯Έ νƒ€μž…μΈμ§€ λͺ…ν™•νžˆ ν•  λ•Œμ—λŠ” μ „μžμ˜ 방식을 μ“°κ³ , ν•œ 가지 νƒ€μž…λ§Œ μ‚¬μš©ν•˜κ±°λ‚˜ νƒ€μž… μ’…λ₯˜λ³΄λ‹€λŠ” 역할이 μ€‘μš”ν•  λ•Œμ—λŠ” ν›„μžμ˜ 방식을 μ‚¬μš©ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄ μœ„ μ½”λ“œλ₯Ό Type placeholderλ₯Ό μ΄μš©ν•˜μ—¬ κ΅¬ν˜„ν•˜λ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

// Rust
fn derivative<F, G>(f: F, h: f64) -> G 
where
    F: Fn(f64) -> f64,
    G: Fn(f64) -> f64,
{
    move |x: f64| (f(x+h) - f(x)) / h
}

μ•„λž˜μ˜ κ΅¬ν˜„μ΄ 가독성 λ©΄μ—μ„œλ‚˜ 의미 λ©΄μ—μ„œ μ’€ 더 쒋은 κ΅¬ν˜„μ΄μ§€λ§Œ, 이 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•  λ•Œ μ œμ•½μ΄ μ‹¬ν•œ νŽΈμž…λ‹ˆλ‹€. impl Trait 꼴둜 λ°˜ν™˜ν•˜λ©΄ RustλŠ” κ·Έ νƒ€μž…μ„ ν•¨μˆ˜ λ³Έλ¬Έμ—μ„œ λ°˜ν™˜ν•˜λŠ” κ°’μœΌλ‘œ μžλ™ μΆ”λ‘ ν•˜μ—¬ μ‚¬μš©ν•˜μ§€λ§Œ, Type placeholder둜 λ°˜ν™˜ν•˜λ©΄ κ·Έ νƒ€μž…μ„ λͺ…ν™•νžˆ ν•˜κΈ° μ „μ—λŠ” 컴파일 λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

λ˜ν•œ μœ„μ™€ 같은 μ½”λ“œλ₯Ό μž‘μ„±ν•  λ•Œ, ν΄λ‘œμ €μ˜ μ„±μ§ˆμ— μœ μ˜ν•΄μ•Όν•©λ‹ˆλ‹€. ν΄λ‘œμ €λŠ” 인수둜 λ“€μ–΄μ˜¨ 값이 μ•„λ‹Œ μ£Όλ³€ ν™˜κ²½λ„ 같이 캑쳐λ₯Ό ν•˜λŠ” μ„±μ§ˆμ΄ μžˆλŠ”λ°, μ΄λ•Œ, μ£Όλ³€ λ³€μˆ˜λ“€μ΄ ν΄λ‘œμ € λ°–μ—μ„œλ„ 생쑴할 수 μžˆλ‹€λ©΄ 컴파일 였λ₯˜κ°€ λ°œμƒν•©λ‹ˆλ‹€. μœ„ μ½”λ“œμ—μ„œλ„ f와 hλŠ” ν•¨μˆ˜μ˜ 인수둜 λ°›μ•˜κΈ°μ— ν•¨μˆ˜μ˜ 선언이 λλ‚˜λŠ” μ‹œμ μ— λ©”λͺ¨λ¦¬κ°€ ν•΄μ œλ©λ‹ˆλ‹€. ν•˜μ§€λ§Œ λ°˜ν™˜λ˜λŠ” ν΄λ‘œμ €λŠ” f와 h의 값을 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. λ”°λΌμ„œ move ν‚€μ›Œλ“œλ₯Ό μ΄μš©ν•˜μ—¬ f와 h의 μ†Œμœ κΆŒμ„ ν΄λ‘œμ €μ— λ„˜κ²¨μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.

이제 μ„€λͺ…이 λλ‚¬μœΌλ‹ˆ 이 κ³ κ³„ν•¨μˆ˜λ₯Ό μ΄μš©ν•˜μ—¬ λ„ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

// Rust
fn main() {
    let df = derivative(f, 1e-6);
    println!("{}", df(1f64));
}

fn f(x: f64) -> f64 {
    x.powi(2)
}

fn derivative<F: Fn(f64) -> f64>(f: F, h: f64) -> impl Fn(f64) -> f64 {
    move |x: f64| (f(x+h) - f(x)) / h
}

닡은 μœ„μ˜ 두 κ²½μš°μ™€ μ •ν™•νžˆ μΌμΉ˜ν•©λ‹ˆλ‹€.

그럼 이제 수치적 λ―ΈλΆ„ λ°©μ‹μ˜ μž₯점과 단점을 μš”μ•½ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

수치적 λ―ΈλΆ„μ˜ μž₯단점

  • μž₯점
    • κ΅¬ν˜„ν•˜λŠ” 것이 ꡉμž₯히 쉽닀.
    • μ•„μ£Ό λΉ λ₯΄κ²Œ λ―ΈλΆ„ 계산을 μˆ˜ν–‰ν•  수 μžˆλ‹€.
  • 단점
    • μ˜€μ°¨κ°€ μŒ“μ΄λ©΄μ„œ μ‹€μ œ κ°’κ³Ό 많이 λ‹€λ₯Έ 값이 λ‚˜μ˜¬ 수 μžˆλ‹€.

계산 속도와 편의 μƒμ˜ 큰 μž₯점을 가지고 μžˆμ§€λ§Œ μ˜€μ°¨κ°€ 계속 μŒ“μΌ 수 μžˆμœΌλ―€λ‘œ Step sizeλŠ” μž‘μ§€λ§Œ ꡬ간은 κΈ΄ μˆ˜μΉ˜λ―ΈλΆ„λ°©μ •μ‹ 등은 수치적 미뢄을 μ μš©ν•˜κΈ°μ— ν•œκ³„κ°€ μžˆμŠ΅λ‹ˆλ‹€. λ‹€ν–‰νžˆ 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•œ 방법듀은 μ‘΄μž¬ν•©λ‹ˆλ‹€. 이에 λŒ€ν•΄μ„œλŠ” λ‹€μŒμ— 닀뀄보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.