Bỏ qua

Giải tích đa biến

Giải tích đa biến mở rộng đạo hàm và tích phân cho các hàm nhiều biến, điều thiết yếu vì các mô hình học máy có đến hàng triệu tham số. Tài liệu này bao quát đạo hàm riêng, gradient, ma trận Jacobi, ma trận Hessian, và quy tắc dẫn hàm hàm hợp đa biến — thứ làm cho lan truyền ngược trở nên khả thi.

  • Cho đến giờ, các hàm của ta chỉ nhận một đầu vào duy nhất \(x\) và tạo ra một đầu ra duy nhất \(f(x)\). Nhưng trong học máy, hầu như ta không bao giờ chỉ làm việc với một biến.

  • Xét một hàm hai biến, như \(f(x, y) = x^2 + y^2\). Hàm này xác định một mặt cong trong không gian 3D, có dạng một cái bát. Ta muốn biết: nếu ta đẩy nhẹ \(x\) một chút trong khi giữ \(y\) cố định, \(f\) thay đổi thế nào? Đó là một đạo hàm riêng (partial derivative).

  • Đạo hàm riêng của \(f\) theo \(x\), viết là \(\frac{\partial f}{\partial x}\), coi mọi biến khác là hằng số và lấy đạo hàm thông thường theo \(x\).

  • Cho \(f(x, y) = x^2y + 3x - 2y\):

\[\frac{\partial f}{\partial x} = 2xy + 3 \qquad \frac{\partial f}{\partial y} = x^2 - 2\]
  • Để tính \(\frac{\partial f}{\partial x}\), ta coi \(y\) là hằng số, nên \(x^2y\) vi phân thành \(2xy\), \(3x\) thành \(3\), và \(-2y\) thành \(0\).

  • Để tính \(\frac{\partial f}{\partial y}\), ta coi \(x\) là hằng số, nên \(x^2y\) vi phân thành \(x^2\), \(3x\) thành \(0\), và \(-2y\) thành \(-2\).

  • Về mặt hình học, lấy đạo hàm riêng theo \(x\) cũng giống như cắt mặt cong 3D bằng một mặt phẳng song song với mặt phẳng \(xz\) (tại một giá trị \(y\) cố định) và tìm độ dốc của đường cong thu được.

Đạo hàm riêng: cắt mặt cong bằng cách giữ một biến cố định

  • Gradient tập hợp tất cả các đạo hàm riêng thành một vector duy nhất:
\[\nabla f = \left(\frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \ldots, \frac{\partial f}{\partial x_n}\right)\]
  • Với \(f(x, y) = x^2 + y^2\): \(\nabla f(x, y) = (2x, 2y)\). Tại điểm \((1, 2)\): \(\nabla f(1, 2) = (2, 4)\).

  • Gradient có hai tính chất quan trọng:

    • Hướng: nó chỉ về hướng tăng mạnh nhất. Hãy tưởng tượng một người đi bộ trên núi. Gradient tại vị trí của họ chỉ thẳng lên dốc, dọc theo con đường dốc nhất.

    • Độ lớn: \(\|\nabla f\|\) cho biết tốc độ tăng theo hướng dốc nhất đó. Gradient lớn nghĩa là địa hình dốc; gradient nhỏ nghĩa là gần như bằng phẳng.

Các vector gradient chỉ lên dốc, vuông góc với các đường đồng mức

  • Vì gradient chỉ lên dốc, đi theo hướng ngược lại (\(-\nabla f\)) sẽ xuống dốc, về phía các giá trị thấp hơn. Ý tưởng đơn giản này là nền tảng của hạ gradient (gradient descent), một kỹ thuật tối ưu hóa mà ta sẽ khám phá chi tiết ở các chương sau. Tạm thời, điểm chính cần nhớ là gradient cho bạn biết hướng nào là "lên" và con dốc cao bao nhiêu.

  • Đạo hàm theo hướng (directional derivative) khái quát hóa đạo hàm riêng. Thay vì hỏi "\(f\) thay đổi thế nào dọc theo trục \(x\)?", nó hỏi "\(f\) thay đổi thế nào dọc theo bất kỳ hướng \(\mathbf{u}\) nào?" Nó được tính bằng tích vô hướng của gradient với một vector đơn vị:

\[D_{\mathbf{u}} f = \nabla f \cdot \mathbf{u}\]
  • Với \(f(x, y) = x^2 + y^2\) tại \((1, 2)\) theo hướng \(\mathbf{v} = (3, 4)\): đầu tiên chuẩn hóa để được \(\mathbf{u} = (3/5, 4/5)\), sau đó \(D_{\mathbf{u}} f = (2, 4) \cdot (3/5, 4/5) = 6/5 + 16/5 = 22/5\).

  • Các đạo hàm riêng là trường hợp đặc biệt của đạo hàm theo hướng khi hướng dọc theo một trục tọa độ. Nếu đạo hàm theo hướng bằng không theo một hướng nào đó, thì hàm bằng phẳng theo hướng đó tại điểm ấy.

  • Đường đồng mức (contour lines) hay đường mức (level curves) nối các điểm tại đó một hàm có cùng giá trị. Với \(f(x, y) = x^2 + y^2\), các đường đồng mức là các đường tròn tâm tại gốc tọa độ: \(x^2 + y^2 = c\) với các giá trị \(c\) khác nhau.

  • Các đường đồng mức không bao giờ cắt nhau (một điểm không thể có hai giá trị hàm khác nhau).

  • Gradient luôn vuông góc với các đường đồng mức, chỉ từ giá trị thấp lên giá trị cao.

  • Các đường đồng mức dày đặc chỉ địa hình dốc; các đường đồng mức thưa thớt chỉ địa hình thoải.

  • Cho đến giờ, các hàm của ta tạo ra một đầu ra duy nhất. Nhưng nhiều hàm tạo ra nhiều đầu ra. Một hàm \(\mathbf{F}: \mathbb{R}^n \to \mathbb{R}^m\) nhận \(n\) đầu vào và tạo ra \(m\) đầu ra. Ma trận Jacobi (Jacobian matrix) sắp xếp tất cả các đạo hàm riêng của một hàm có giá trị vector như vậy:

\[ J = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n} \end{bmatrix} \]
  • Mỗi hàng của ma trận Jacobi là gradient của một thành phần đầu ra. Với một hàm có 3 đầu vào và 2 đầu ra, ma trận Jacobi là một ma trận \(2 \times 3\).

  • Ma trận Jacobi khái quát hóa đạo hàm cho các hàm có giá trị vector.

  • Cũng như đạo hàm của một hàm vô hướng cho bạn biết đầu ra thay đổi bao nhiêu trên một đơn vị thay đổi của đầu vào, ma trận Jacobi cho bạn biết mỗi đầu ra thay đổi thế nào đối với mỗi đầu vào.

  • Định thức của ma trận Jacobi đo lường mức độ một phép biến đổi kéo dãn hay nén cục bộ không gian.

  • Nếu định thức bằng 2, các vùng nhỏ tăng gấp đôi diện tích. Nếu nó bằng 0, phép biến đổi làm xẹp không gian thành một chiều thấp hơn (nhớ lại từ chương ma trận rằng định thức bằng không nghĩa là một phép biến đổi suy biến, không khả nghịch).

  • Khi nhiều phép biến đổi được hợp lại (cái đầu ra của phép biến đổi này là đầu vào của phép biến đổi tiếp theo), ma trận Jacobi của ánh xạ tổng thể là tích của các ma trận Jacobi riêng rẽ. Ta sẽ thấy ý tưởng này trở nên trung tâm trong các chương sau.

  • Nơi gradient nắm bắt thông tin bậc nhất (độ dốc), ma trận Hessian (Hessian matrix) nắm bắt thông tin bậc hai (độ cong).

  • Với một hàm vô hướng \(f(x_1, \ldots, x_n)\), ma trận Hessian là ma trận \(n \times n\) của tất cả các đạo hàm riêng bậc hai:

\[ H = \begin{bmatrix} \frac{\partial^2 f}{\partial x_1^2} & \frac{\partial^2 f}{\partial x_1 \partial x_2} & \cdots \\ \frac{\partial^2 f}{\partial x_2 \partial x_1} & \frac{\partial^2 f}{\partial x_2^2} & \cdots \\ \vdots & \vdots & \ddots \end{bmatrix} \]
  • Với \(f(x, y) = x^3 + 2xy^2 - y^3\), gradient là \((3x^2 + 2y^2,\; 4xy - 3y^2)\), và Hessian là:
\[ H = \begin{bmatrix} 6x & 4y \\ 4y & 4x - 6y \end{bmatrix} \]
  • Các phần tử trên đường chéo (\(6x\)\(4x - 6y\)) cho bạn biết độ dốc theo hướng \(x\) thay đổi thế nào khi bạn di chuyển theo \(x\), và tương tự cho \(y\).

  • Các phần tử ngoài đường chéo (\(4y\)) cho bạn biết độ dốc theo một hướng thay đổi thế nào khi bạn di chuyển theo hướng kia.

  • Định lý Clairaut đảm bảo rằng với các hàm có đạo hàm riêng bậc hai liên tục, các đạo hàm riêng hỗn hợp bằng nhau: \(\frac{\partial^2 f}{\partial x \partial y} = \frac{\partial^2 f}{\partial y \partial x}\).

  • Điều này có nghĩa là ma trận Hessian là đối xứng, điều này (như ta đã thấy ở chương ma trận) đảm bảo các trị riêng là thực và các vector riêng là trực giao.

  • Ma trận Hessian cho ta biết về hình dạng của hàm gần một điểm tới hạn (nơi gradient bằng không):

    • Nếu \(H\) là xác định dương (mọi trị riêng đều dương), điểm đó là một cực tiểu địa phương (local minimum), mặt cong cong lên trên theo mọi hướng như một cái bát.
    • Nếu \(H\) là xác định âm (mọi trị riêng đều âm), điểm đó là một cực đại địa phương (local maximum), mặt cong cong xuống dưới như một cái bát úp.
    • Nếu \(H\) có cả trị riêng dương và âm, điểm đó là một điểm yên ngựa (saddle point), mặt cong cong lên theo một số hướng và xuống theo các hướng khác, giống như một đèo núi.
  • Quy tắc dẫn hàm hàm hợp đa biến (multivariate chain rule) mở rộng quy tắc dẫn hàm hàm hợp cho các hàm nhiều biến. Nếu \(z = f(x, y)\) với \(x = g(t)\)\(y = h(t)\), thì:

\[\frac{dz}{dt} = \frac{\partial f}{\partial x}\frac{dx}{dt} + \frac{\partial f}{\partial y}\frac{dy}{dt}\]
  • Mỗi đường đi từ \(t\) đến \(z\) đóng góp một số hạng: đạo hàm riêng dọc theo đường đó nhân với đạo hàm của biến trung gian theo \(t\).

  • Ví dụ, nếu \(z = x^2 y + 3x - y^2\), \(x = \cos(t)\), \(y = \sin(t)\):

\[\frac{dz}{dt} = (2xy + 3)(-\sin t) + (x^2 - 2y)(\cos t)\]
  • Ngoài việc tính đạo hàm bằng tay, có ba cách tiếp cận:

    • Vi phân số học (numerical differentiation): xấp xỉ \(f'(x) \approx \frac{f(x+h) - f(x-h)}{2h}\) với \(h\) nhỏ. Đơn giản nhưng nhiễu và không chính xác.
    • Vi phân ký hiệu (symbolic differentiation): áp dụng các quy tắc vi phân một cách đại số để tạo ra một công thức chính xác. Có thể tạo ra các biểu thức tăng trưởng theo cấp số nhân.
    • Tự động vi phân (automatic differentiation / autodiff): theo dõi chuỗi các phép toán và tính các đạo hàm chính xác một cách hiệu quả. Đây là thứ mà JAX, PyTorch, và TensorFlow sử dụng. Nó cho các giá trị số chính xác (không xấp xỉ) mà không phải tạo ra các biểu thức ký hiệu cồng kềnh.

Bài tập lập trình (dùng CoLab hoặc notebook)

  1. Tính gradient của \(f(x, y) = x^2 y + 3x - 2y\) tại điểm \((1, 2)\) bằng jax.grad. Vì \(f\) nhận đầu vào là vector, dùng jax.grad với argnums.

    import jax
    import jax.numpy as jnp
    
    def f(x, y):
        return x**2 * y + 3*x - 2*y
    
    df_dx = jax.grad(f, argnums=0)
    df_dy = jax.grad(f, argnums=1)
    
    x, y = 1.0, 2.0
    print(f"∂f/∂x = {df_dx(x, y):.4f}  (expected: {2*x*y + 3:.4f})")
    print(f"∂f/∂y = {df_dy(x, y):.4f}  (expected: {x**2 - 2:.4f})")
    

  2. Tính ma trận Jacobi của một hàm có giá trị vector bằng jax.jacobian. So sánh với tính toán thủ công.

    import jax
    import jax.numpy as jnp
    
    def F(x):
        return jnp.array([x[0]**2 + x[1], x[0] * x[1]**2])
    
    J = jax.jacobian(F)
    x = jnp.array([1.0, 2.0])
    print(f"Jacobian at (1,2):\n{J(x)}")
    # Expected: [[2*x[0], 1], [x[1]**2, 2*x[0]*x[1]]] = [[2, 1], [4, 4]]
    

  3. Tính ma trận Hessian của \(f(x, y) = x^3 + 2xy^2 - y^3\) bằng jax.hessian và kiểm tra tính đối xứng.

    import jax
    import jax.numpy as jnp
    
    def f(xy):
        x, y = xy[0], xy[1]
        return x**3 + 2*x*y**2 - y**3
    
    H = jax.hessian(f)
    point = jnp.array([1.0, 2.0])
    hess = H(point)
    print(f"Hessian:\n{hess}")
    print(f"Symmetric: {jnp.allclose(hess, hess.T)}")
    # Expected: [[6x, 4y], [4y, 4x-6y]] = [[6, 8], [8, -8]]
    

  4. Xây dựng một engine tự động vi phân tối thiểu từ đầu.

    • Mỗi Var theo dõi giá trị của nó và cách truyền gradient ngược qua quy tắc dẫn hàm hàm hợp.
    • Hãy thử mở rộng nó với nhiều phép toán hơn (chia, lũy thừa, v.v.).
    • Đây là nền tảng của cách JAX, PyTorch và Numpy được thiết kế.
      class Var:
          def __init__(self, val, children=(), backward_fn=None):
              self.val = val
              self.grad = 0.0
              self.children = children
              self.backward_fn = backward_fn
      
          def __add__(self, other):
              out = Var(self.val + other.val, children=(self, other))
              def _backward():
                  self.grad += out.grad    # d(a+b)/da = 1
                  other.grad += out.grad   # d(a+b)/db = 1
              out.backward_fn = _backward
              return out
      
          def __mul__(self, other):
              out = Var(self.val * other.val, children=(self, other))
              def _backward():
                  self.grad += other.val * out.grad  # d(a*b)/da = b
                  other.grad += self.val * out.grad  # d(a*b)/db = a
              out.backward_fn = _backward
              return out
      
          def backward(self):
              # topological sort then propagate gradients
              # we will go through this in data structures and algorithms
              order, visited = [], set()
              def topo(v):
                  if v not in visited:
                      visited.add(v)
                      for c in v.children:
                          topo(c)
                      order.append(v)
              topo(self)
              self.grad = 1.0
              for v in reversed(order):
                  if v.backward_fn:
                      v.backward_fn()
      
      # f(x, y) = x*x*y + x  at (3, 2)
      x = Var(3.0)
      y = Var(2.0)
      f = x * x * y + x       # = 3*3*2 + 3 = 21
      
      f.backward()
      print(f"f = {f.val}")           # 21.0
      print(f"df/dx = {x.grad}")     # 2*x*y + 1 = 13.0
      print(f"df/dy = {y.grad}")     # x*x = 9.0