.. raw:: html .. raw:: html .. raw:: html .. _sec_autograd: Tính vi phân Tự động ==================== .. raw:: html Như đã giải thích trong :numref:`sec_calculus`, vi phân là phép tính thiết yếu trong hầu như tất cả mọi thuật toán học sâu. Mặc dù các phép toán trong việc tính đạo hàm khá trực quan và chỉ yêu cầu một chút kiến thức giải tích, nhưng với các mô hình phức tạp, việc tự tính rõ ràng từng bước khá là mệt (và thường rất dễ sai). .. raw:: html Gói thư viện ``autograd`` giải quyết vấn đề này một cách nhanh chóng và hiệu quả bằng cách tự động hoá các phép tính đạo hàm (*automatic differentiation*). Trong khi nhiều thư viện yêu cầu ta phải biên dịch một *đồ thị biểu tượng* (*symbolic graph*) để có thể tự động tính đạo hàm, ``autograd`` cho phép ta tính đạo hàm ngay lập tức thông qua các dòng lệnh thông thường. Mỗi khi đưa dữ liệu chạy qua mô hình, ``autograd`` xây dựng một đồ thị và theo dõi xem dữ liệu nào kết hợp với các phép tính nào để tạo ra kết quả. Với đồ thị này ``autograd`` sau đó có thể lan truyền ngược gradient lại theo ý muốn. *Lan truyền ngược* ở đây chỉ đơn thuần là truy ngược lại *đồ thị tính toán* và điền vào đó các giá trị đạo hàm riêng theo từng tham số. .. code:: python from mxnet import autograd, np, npx npx.set_np() .. raw:: html .. raw:: html .. raw:: html Một ví dụ đơn giản ------------------ .. raw:: html Lấy ví dụ đơn giản, giả sử chúng ta muốn tính vi phân của hàm số :math:`y = 2\mathbf{x}^{\top}\mathbf{x}` theo vector cột :math:`\mathbf{x}`. Để bắt đầu, ta sẽ tạo biến ``x`` và gán cho nó một giá trị ban đầu. .. code:: python x = np.arange(4) x .. parsed-literal:: :class: output array([0., 1., 2., 3.]) .. raw:: html Lưu ý rằng trước khi có thể tính gradient của :math:`y` theo :math:`\mathbf{x}`, chúng ta cần một nơi để lưu giữ nó. Điều quan trọng là ta không được cấp phát thêm bộ nhớ mới mỗi khi tính đạo hàm theo một biến xác định, vì ta thường cập nhật cùng một tham số hàng ngàn vạn lần và sẽ nhanh chóng dùng hết bộ nhớ. .. raw:: html Cũng lưu ý rằng, bản thân giá trị gradient của hàm số đơn trị theo một vector :math:`\mathbf{x}` cũng là một vector với cùng kích thước. Do vậy trong mã nguồn sẽ trực quan hơn nếu chúng ta lưu giá trị gradient tính theo ``x`` dưới dạng một thuộc tính của chính ``ndarray`` ``x``. Chúng ta cấp bộ nhớ cho gradient của một ``ndarray`` bằng cách gọi phương thức ``attach_grad``. .. code:: python x.attach_grad() .. raw:: html .. raw:: html .. raw:: html Sau khi đã tính toán gradient theo biến ``x``, ta có thể truy cập nó thông qua thuộc tính ``grad``. Để an toàn, ``x.grad`` được khởi tạo là một mảng chứa các giá trị không. Điều này hợp lý vì trong học sâu, việc lấy gradient thường là để cập nhật các tham số bằng cách cộng (hoặc trừ) gradient của một hàm để cực đại (hoặc cực tiểu) hóa hàm đó. Bằng cách khởi tạo gradient bằng mảng chứa giá trị không, ta đảm bảo rằng bất kỳ cập nhật vô tình nào trước khi gradient được tính toán sẽ không làm thay đổi giá trị các tham số. .. code:: python x.grad .. parsed-literal:: :class: output array([0., 0., 0., 0.]) .. raw:: html Giờ hãy tính :math:`y`. Bởi vì mục đích sau cùng là tính gradient, ta muốn MXNet tạo đồ thị tính toán một cách nhanh chóng. Ta có thể tưởng tượng rằng MXNet sẽ bật một thiết bị ghi hình để thu lại chính xác đường đi mà mỗi biến được tạo. .. raw:: html Chú ý rằng ta cần một số lượng phép tính không hề nhỏ để xây dựng đồ thị tính toán. Vậy nên MXNet sẽ chỉ dựng đồ thị khi được ra lệnh rõ ràng. Ta có thể thực hiện việc này bằng cách đặt đoạn mã trong phạm vi ``autograd.record``. .. code:: python with autograd.record(): y = 2 * np.dot(x, x) y .. parsed-literal:: :class: output array(28.) .. raw:: html Bởi vì ``x`` là một ``ndarray`` có độ dài bằng 4, ``np.dot`` sẽ tính toán tích vô hướng của ``x`` và ``x``, trả về một số vô hướng mà sẽ được gán cho ``y``. Tiếp theo, ta có thể tính toán gradient của ``y`` theo mỗi thành phần của ``x`` một cách tự động bằng cách gọi hàm ``backward`` của ``y``. .. code:: python y.backward() .. raw:: html Nếu kiểm tra lại giá trị của ``x.grad``, ta sẽ thấy nó đã được ghi đè bằng gradient mới được tính toán. .. code:: python x.grad .. parsed-literal:: :class: output array([ 0., 4., 8., 12.]) .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Gradient của hàm :math:`y = 2\mathbf{x}^{\top}\mathbf{x}` theo :math:`\mathbf{x}` phải là :math:`4\mathbf{x}`. Hãy kiểm tra một cách nhanh chóng rằng giá trị gradient mong muốn được tính toán đúng. Nếu hai ``ndarray`` là giống nhau, thì mọi cặp phần tử tương ứng cũng bằng nhau. .. code:: python x.grad == 4 * x .. parsed-literal:: :class: output array([ True, True, True, True]) .. raw:: html Nếu ta tiếp tục tính gradient của một biến khác mà giá trị của nó là kết quả của một hàm theo biến ``x``, thì nội dung trong ``x.grad`` sẽ bị ghi đè. .. code:: python with autograd.record(): y = x.sum() y.backward() x.grad .. parsed-literal:: :class: output array([1., 1., 1., 1.]) .. raw:: html Truyền ngược cho các biến không phải Số vô hướng ------------------------------------------------ .. raw:: html Về mặt kỹ thuật, khi ``y`` không phải một số vô hướng, cách diễn giải tự nhiên nhất cho vi phân của một vector ``y`` theo vector ``x`` đó là một ma trận. Với các bậc và chiều cao hơn của ``y`` và ``x``, kết quả của phép vi phân có thể là một tensor bậc cao. .. raw:: html Tuy nhiên, trong khi những đối tượng như trên xuất hiện trong học máy nâng cao (bao gồm học sâu), thường thì khi ta gọi lan truyền ngược trên một vector, ta đang cố tính toán đạo hàm của hàm mất mát theo mỗi *batch* bao gồm một vài mẫu huấn luyện. Ở đây, ý định của ta không phải là tính toán ma trận vi phân mà là tổng của các đạo hàm riêng được tính toán một cách độc lập cho mỗi mẫu trong batch. .. raw:: html Vậy nên khi ta gọi ``backward`` lên một biến vector ``y`` – là một hàm của ``x``, MXNet sẽ cho rằng ta muốn tính tổng của các gradient. Nói ngắn gọn, MXNet sẽ tạo một biến mới có giá trị là số vô hướng bằng cách cộng lại các phần tử trong ``y`` và tính gradient theo ``x`` của biến mới này. .. code:: python with autograd.record(): y = x * x # y is a vector y.backward() u = x.copy() u.attach_grad() with autograd.record(): v = (u * u).sum() # v is a scalar v.backward() x.grad == u.grad .. parsed-literal:: :class: output array([ True, True, True, True]) .. raw:: html .. raw:: html .. raw:: html Tách rời Tính toán ------------------ .. raw:: html Đôi khi chúng ta muốn chuyển một số phép tính ra khỏi đồ thị tính toán. Ví dụ, giả sử ``y`` đã được tính như một hàm của ``x``, rồi sau đó ``z`` được tính như một hàm của cả ``y`` và ``x``. Bây giờ, giả sử ta muốn tính gradient của ``z`` theo ``x``, nhưng vì lý do nào đó ta lại muốn xem ``y`` như là một hằng số và chỉ xét đến vai trò của ``x`` như là biến số của ``z`` sau khi giá trị của ``y`` đã được tính. .. raw:: html Trong trường hợp này, ta có thể gọi ``u = y.detach()`` để trả về một biến ``u`` mới có cùng giá trị như ``y`` nhưng không còn chứa các thông tin về cách mà ``y`` đã được tính trong đồ thị tính toán. Nói cách khác, gradient sẽ không thể chảy ngược qua ``u`` về ``x`` được. Bằng cách này, ta đã tính ``u`` như một hàm của ``x`` ở ngoài phạm vi của ``autograd.record``, dẫn đến việc biến ``u`` sẽ được xem như là một hằng số mỗi khi ta gọi ``backward``. Chính vì vậy, hàm ``backward`` sau đây sẽ tính đạo hàm riêng của ``z = u * x`` theo ``x`` khi xem ``u`` như là một hằng số, thay vì đạo hàm riêng của ``z = x * x * x`` theo ``x``. .. code:: python with autograd.record(): y = x * x u = y.detach() z = u * x z.backward() x.grad == u .. parsed-literal:: :class: output array([ True, True, True, True]) .. raw:: html Bởi vì sự tính toán của ``y`` đã được ghi lại, chúng ta có thể gọi ``y.backward()`` sau đó để lấy đạo hàm của ``y = x * x`` theo ``x``, tức là ``2 * x``. .. code:: python y.backward() x.grad == 2 * x .. parsed-literal:: :class: output array([ True, True, True, True]) .. raw:: html Lưu ý rằng khi ta gắn gradient vào một biến ``x``, ``x = x.detach()`` sẽ được gọi ngầm. Nếu ``x`` được tính dựa trên các biến khác, phần tính toán này sẽ không được sử dụng trong hàm ``backward``. .. code:: python y = np.ones(4) * 2 y.attach_grad() with autograd.record(): u = x * y u.attach_grad() # Implicitly run u = u.detach() z = 5 * u - x z.backward() x.grad, u.grad, y.grad .. parsed-literal:: :class: output (array([-1., -1., -1., -1.]), array([5., 5., 5., 5.]), array([0., 0., 0., 0.])) .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Tính gradient của Luồng điều khiển Python ----------------------------------------- .. raw:: html Một lợi thế của việc sử dụng vi phân tự động là khi việc xây dựng đồ thị tính toán đòi hỏi trải qua một loạt các câu lệnh điều khiển luồng Python, (ví dụ như câu lệnh điều kiện, vòng lặp và các lệnh gọi hàm tùy ý), ta vẫn có thể tính gradient của biến kết quả. Trong đoạn mã sau, hãy lưu ý rằng số lần lặp của vòng lặp ``while`` và kết quả của câu lệnh ``if`` đều phụ thuộc vào giá trị của đầu vào ``a``. .. code:: python def f(a): b = a * 2 while np.linalg.norm(b) < 1000: b = b * 2 if b.sum() > 0: c = b else: c = 100 * b return c .. raw:: html Một lần nữa, để tính gradient ta chỉ cần “ghi lại” các phép tính (bằng cách gọi hàm ``record``) và sau đó gọi hàm ``backward``. .. code:: python a = np.random.normal() a.attach_grad() with autograd.record(): d = f(a) d.backward() .. raw:: html Giờ ta có thể phân tích hàm ``f`` được định nghĩa ở phía trên. Hãy để ý rằng hàm này tuyến tính từng khúc theo đầu vào ``a``. Nói cách khác, với mọi giá trị của ``a`` tồn tại một hằng số ``k`` sao cho ``f(a) = k * a``, ở đó giá trị của ``k`` phụ thuộc vào đầu vào ``a``. Do đó, ta có thể kiểm tra giá trị của gradient bằng cách tính ``d / a``. .. code:: python a.grad == d / a .. parsed-literal:: :class: output array(True) .. raw:: html .. raw:: html .. raw:: html Chế độ huấn luyện và Chế độ dự đoán ----------------------------------- .. raw:: html Như đã thấy, sau khi gọi ``autograd.record``, MXNet sẽ ghi lại những tính toán xảy ra trong khối mã nguồn theo sau. Có một chi tiết tinh tế nữa mà ta cần để ý. ``autograd.record`` sẽ thay đổi chế độ chạy từ *chế độ dự đoán* sang *chế độ huấn luyện*. Ta có thể kiểm chứng hành vi này bằng cách gọi hàm ``is_training``. .. code:: python print(autograd.is_training()) with autograd.record(): print(autograd.is_training()) .. parsed-literal:: :class: output False True .. raw:: html Khi ta tìm hiểu tới các mô hình học sâu phức tạp, ta sẽ gặp một vài thuật toán mà mô hình hoạt động khác nhau khi huấn luyện và khi được sử dụng sau đó để dự đoán. Những khác biệt này sẽ được đề cập chi tiết trong các chương sau. .. raw:: html Tóm tắt ------- .. raw:: html - MXNet cung cấp gói ``autograd`` để tự động hóa việc tính toán đạo hàm. Để sử dụng nó, đầu tiên ta gắn gradient cho các biến mà ta muốn lấy đạo hàm riêng theo nó. Sau đó ta ghi lại tính toán của giá trị mục tiêu, thực thi hàm ``backward`` của nó và truy cập kết quả gradient thông qua thuộc tính ``grad`` của các biến. - Ta có thể tách rời gradient để kiểm soát những phần tính toán được sử dụng trong hàm ``backward``. - Các chế độ chạy của MXNet bao gồm chế độ huấn luyện và chế độ dự đoán. Ta có thể kiểm tra chế độ đang chạy bằng cách gọi hàm ``is_training``. .. raw:: html Bài tập ------- .. raw:: html 1. Tại sao đạo hàm bậc hai lại mất thêm rất nhiều tài nguyên để tính toán hơn đạo hàm bậc một? 2. Sau khi chạy ``y.backward()``, lập tức chạy lại lần nữa và xem chuyện gì sẽ xảy ra. 3. Trong ví dụ về luồng điều khiển khi ta tính toán đạo hàm của ``d`` theo ``a``, điều gì sẽ xảy ra nếu ta thay đổi biến ``a`` thành một vector hay ma trận ngẫu nhiên. Lúc này, kết quả của tính toán ``f(a)`` sẽ không còn là số vô hướng nữa. Điều gì sẽ xảy ra với kết quả? Ta có thể phân tích nó như thế nào? 4. Hãy tái thiết kế một ví dụ về việc tìm gradient của luồng điều khiển. Chạy ví dụ và phân tích kết quả. 5. Cho :math:`f(x) = \sin(x)`. Vẽ đồ thị của :math:`f(x)` và :math:`\frac{df(x)}{dx}` với điều kiện không được tính trực tiếp đạo hàm :math:`f'(x) = \cos(x)`. 6. Trong một cuộc đấu giá kín theo giá thứ hai (ví dụ như trong eBay hay trong quảng cáo điện toán), người thắng cuộc đấu giá chỉ trả mức giá cao thứ hai. Hãy tính gradient của mức giá cuối cùng theo mức đặt của người thắng cuộc bằng cách sử dụng ``autograd``. Kết quả cho bạn biết điều gì về cơ chế đấu giá này? Nếu bạn tò mò muốn tìm hiểu thêm về các cuộc đấu giá kín theo giá thứ hai, hãy đọc bài báo nghiên cứu của Edelman et al. :cite:`Edelman.Ostrovsky.Schwarz.2007`. .. raw:: html .. raw:: html .. raw:: html Thảo luận --------- - `Tiếng Anh `__ - `Tiếng Việt `__ Những người thực hiện --------------------- Bản dịch trong trang này được thực hiện bởi: - Đoàn Võ Duy Thanh - Lê Khắc Hồng Phúc - Nguyễn Cảnh Thướng - Phạm Hồng Vinh - Vũ Hữu Tiệp - Tạ H. Duy Nguyên - Phạm Minh Đức