.. raw:: html .. raw:: html .. raw:: html .. _sec_linear_gluon: Cách lập trình súc tích Hồi quy Tuyến tính ========================================== .. raw:: html Sự quan tâm nhiệt thành và rộng khắp với học sâu trong những năm gần đây đã tạo cảm hứng cho các công ty, học viện và những người đam mê tới học sâu phát triển nhiều framework mã nguồn mở hoàn thiện, giúp tự động hóa các công việc lặp đi lặp lại trong quá trình triển khai các thuật toán học dựa trên gradient. Trong chương trước, chúng ta chỉ dựa vào (i) ``ndarray`` để lưu dữ liệu và thực hiện tính toán đại số tuyến tính; và (ii) ``autograd`` để thực hiện tính đạo hàm. Trên thực tế, do các iterator dữ liệu, các hàm mất mát, các bộ tối ưu và các tầng của mạng nơ-ron (thậm chí là toàn bộ kiến trúc) rất phổ biển, các thư viện hiện đại đã cài đặt sẵn những thành phần này cho chúng ta. .. raw:: html Mục này sẽ hướng dẫn bạn cách để xây dựng mô hình hồi quy tuyến tính trong phần :numref:`sec_linear_scratch` một cách súc tích với Gluon. .. raw:: html Tạo Tập dữ liệu --------------- .. raw:: html Chúng ta bắt đầu bằng việc tạo một tập dữ liệu như ở mục trước. .. code:: python from d2l import mxnet as d2l from mxnet import autograd, gluon, np, npx npx.set_np() true_w = np.array([2, -3.4]) true_b = 4.2 features, labels = d2l.synthetic_data(true_w, true_b, 1000) .. raw:: html .. raw:: html .. raw:: html Đọc tập dữ liệu --------------- .. raw:: html Thay vì tự viết iterator riêng để đọc dữ liệu thì ta có thể gọi mô-đun ``data`` của Gluon để xử lý việc này. Bước đầu tiên sẽ là khởi tạo một ``ArrayDataset``. Hàm tạo của đối tượng này sẽ lấy một hoặc nhiều ``ndarray`` làm đối số. Tại đây, ta truyền vào hàm hai đối số là ``features`` và ``labels``. Kế tiếp, ta sử dụng ``ArrayDataset`` để khởi tạo một\ ``DataLoader``, lớp này yêu cầu ta truyền vào một giá trị ``batch_size`` và giá trị Boolean ``shuffle`` để cho biết chúng ta có muốn ``DataLoader`` xáo trộn dữ liệu trên mỗi epoch (một lần duyệt qua toàn bộ tập dữ liệu) hay không. .. code:: python # Saved in the d2l package for later use def load_array(data_arrays, batch_size, is_train=True): """Construct a Gluon data loader""" dataset = gluon.data.ArrayDataset(*data_arrays) return gluon.data.DataLoader(dataset, batch_size, shuffle=is_train) batch_size = 10 data_iter = load_array((features, labels), batch_size) .. raw:: html Bây giờ, ta có thể sử dụng ``data_iter`` theo cách tương tự như cách ta gọi hàm ``data_iter`` trong phần trước. Để biết rằng nó có hoạt động được hay không, ta có thể thử đọc và in ra minibatch đầu tiên. .. code:: python for X, y in data_iter: print(X, '\n', y) break .. parsed-literal:: :class: output [[-0.38151944 -1.3118169 ] [ 0.02099029 -0.73851013] [-0.31846237 -0.9492751 ] [-0.17870884 -0.20225924] [ 0.0901759 0.27758422] [-2.2036052 1.2875317 ] [-0.47063652 -0.59975994] [ 0.24499686 -1.6080191 ] [ 0.43582228 0.72849447] [-0.1742568 1.9691626 ]] [[ 7.871762 ] [ 6.737613 ] [ 6.796183 ] [ 4.4967184] [ 3.427059 ] [-4.5830226] [ 5.289975 ] [10.171097 ] [ 2.5919693] [-2.8460252]] .. raw:: html .. raw:: html .. raw:: html Định nghĩa Mô hình ------------------ .. raw:: html Khi ta lập trình hồi quy tuyến tính từ đầu (trong :numref:`sec_linear_scratch`), ta đã định nghĩa rõ ràng các tham số của mô hình và lập trình các tính toán cho giá trị đầu ra sử dụng các phép toán đại số tuyến tính cơ bản. Bạn *nên* biết cách để làm được điều này. Nhưng một khi mô hình trở nên phức tạp hơn và đồng thời khi bạn phải làm điều này gần như hàng ngày, bạn sẽ thấy vui mừng khi có sự hỗ trợ từ các thư viện. Tình huống này tương tự như việc lập trình blog của riêng bạn lại từ đầu. Làm điều này một hoặc hai lần thì sẽ bổ ích và mang tính hướng dẫn, nhưng bạn sẽ trở thành một nhà phát triển web “khó ở” nếu mỗi khi cần một trang blog bạn lại phải dành ra cả một tháng chỉ để phát triển lại từ đầu. .. raw:: html .. raw:: html .. raw:: html Đối với những tác vụ tiêu chuẩn, chúng ta có thể sử dụng các tầng đã được định nghĩa trước trong Gluon, điều này cho phép chúng ta tập trung vào những tầng được dùng để xây dựng mô hình hơn là việc phải tập trung vào cách lập trình các tầng đó. Để định nghĩa một mô hình tuyến tính, đầu tiên chúng ta cần nhập vào mô-đun ``nn``, giúp ta định nghĩa một lượng lớn các tầng trong mạng nơ-ron (lưu ý rằng “nn” là chữ viết tắt của “neural network”). Đầu tiên ta sẽ định nghĩa một biến mô hình là ``net``, tham chiếu đến một thực thể của lớp ``Sequential``. Trong Gluon, ``Sequential`` định nghĩa một lớp chứa nhiều tầng được liên kết với nhau. Khi nhận được dữ liệu đầu vào, ``Sequential`` sẽ truyền dữ liệu vào tầng đầu tiên, kết quả đầu ra từ đó trở thành đầu vào của tầng thứ hai và cứ tiếp tục như thế ở các tầng kế tiếp. Trong ví dụ tiếp theo, mô hình chúng ta chỉ có duy nhất một tầng, vì vậy không nhất thiết phải sử dụng ``Sequential``. Tuy nhiên vì hầu hết các mô hình chúng ta gặp phải trong tương lai đều có nhiều tầng, do đó dù sao cũng nên dùng để làm quen với quy trình làm việc tiêu chuẩn nhất. .. code:: python from mxnet.gluon import nn net = nn.Sequential() .. raw:: html Hãy cùng nhớ lại kiến trúc của mạng đơn tầng như đã trình bày tại :numref:`fig_singleneuron`. Tầng đó được gọi là *kết nối đầy đủ* bởi vì mỗi đầu vào được kết nối lần lượt với từng đầu ra bằng một phép nhân ma trận với vector. Trong Gluon, tầng kết nối đầy đủ được định nghĩa trong lớp ``Dense``. Bởi vì chúng ta chỉ mong xuất ra một số vô hướng duy nhất, nên ta gán giá trị là :math:`1`. .. _fig_singleneuron: .. figure:: ../img/singleneuron.svg Hồi quy tuyến tính là một mạng nơ-ron đơn tầng. .. code:: python net.add(nn.Dense(1)) .. raw:: html Để thuận tiện, điều đáng chú ý là Gluon không yêu cầu chúng ta chỉ định kích thước đầu vào mỗi tầng. Nên tại đây, chúng ta không cần thiết cho Gluon biết có bao nhiêu đầu vào cho mỗi tầng tuyến tính. Khi chúng ta cố gắng truyền dữ liệu qua mô hình lần đầu tiên, ví dụ: khi chúng ta thực hiện ``net(X)`` sau đó, Gluon sẽ tự động suy ra số lượng đầu vào cho mỗi tầng. Chúng ta sẽ mô tả cách hoạt động của cơ chế này một cách chi tiết hơn trong chương “Tính toán trong Học sâu”. .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Khởi tạo Tham số Mô hình ------------------------ .. raw:: html Trước khi sử dụng ``net``, chúng ta cần phải khởi tạo tham số cho mô hình, chẳng hạn như trọng số và hệ số điều chỉnh trong mô hình hồi quy tuyến tính. Chúng ta sẽ nhập mô-đun ``initializer`` từ MXNet. Mô-đun này cung cấp nhiều phương thức khác nhau để khởi tạo tham số cho mô hình. Gluon cho phép dùng ``init`` như một cách ngắn gọn (viết tắt) để truy cập đến gói ``initializer``. Bằng cách gọi ``init.Normal(sigma=0.01)``, chúng ta sẽ khởi tạo ngẫu nhiên các *trọng số* từ một phân phối chuẩn với trung bình bằng :math:`0` và độ lệch chuẩn bằng :math:`0.01`. Mặc định, tham số *hệ số điều chỉnh* sẽ được khởi tạo bằng không. Cả hai vector trọng số và hệ số điều chỉnh sẽ có gradient kèm theo. .. code:: python from mxnet import init net.initialize(init.Normal(sigma=0.01)) .. raw:: html Đoạn mã nguồn trên trông khá đơn giản nhưng bạn đọc hãy chú ý một vài điểm khác thường ở đây. Chúng ta khởi tạo các tham số cho một mạng mà thậm chí Gluon chưa hề biết số chiều của đầu vào là bao nhiêu! Nó có thể là :math:`2` trong trường hợp của chúng ta nhưng cũng có thể là :math:`2000`. Gluon khiến chúng ta không cần bận tâm về điều này bởi ở hậu trường, quá trình khởi tạo thực sự vẫn đang bị *trì hoãn*. Quá trình khởi tạo thực sự chỉ bắt đầu khi chúng ta truyền dữ liệu vào mạng lần đầu tiên. Hãy ghi nhớ rằng, do các tham số chưa thực sự được khởi tạo, chúng ta không thể truy cập hoặc thao tác với chúng. .. raw:: html .. raw:: html .. raw:: html Định nghĩa Hàm mất mát ---------------------- .. raw:: html Trong Gluon, mô-đun ``loss`` định nghĩa các hàm mất mát khác nhau. Chúng ta sẽ sử dụng mô-đun ``loss`` được thêm vào dưới tên gọi là ``gloss``, để tránh nhầm lẫn nó với biến đang giữ hàm mất mát mà ta đã chọn. Trong ví dụ này, chúng ta sẽ sử dụng triển khai trong Gluon của mất mát bình phương (``L2Loss``). .. code:: python from mxnet.gluon import loss as gloss loss = gloss.L2Loss() # The squared loss is also known as the L2 norm loss .. raw:: html .. raw:: html .. raw:: html Định nghĩa Thuật toán Tối ưu ---------------------------- .. raw:: html Minibatch SGD và các biến thể liên quan đều là các công cụ chuẩn cho việc tối ưu hóa mạng nơ-ron, vì vậy Gluon có hỗ trợ SGD cùng với một số biến thể của thuật toán này thông qua lớp ``Trainer``. Khi khởi tạo lớp ``Trainer``, ta cần chỉ định các tham số để tối ưu hóa (có thể lấy từ mạng thông qua ``net.collect_params()``), thuật toán tối ưu muốn sử dụng (``sgd``) và một từ điển gồm các siêu tham số cần thiết cho thuật toán tối ưu. SGD chỉ yêu cầu giá trị của ``learning_rate``, (ở đây chúng ta đặt nó bằng 0.03). .. code:: python from mxnet import gluon trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03}) .. raw:: html .. raw:: html .. raw:: html Huấn luyện ---------- .. raw:: html Bạn có thể thấy rằng việc biểu diễn mô hình thông qua Gluon đòi hỏi tương đối ít dòng lệnh. Chúng ta không cần phải khởi tạo từng tham số riêng lẻ, định nghĩa hàm mất mát hay lập trình thuật toán hạ gradient ngẫu nhiên. Lợi ích mà Gluon mang lại sẽ rất lớn khi chúng ta bắt đầu làm việc với những mô hình phức tạp hơn. Tuy nhiên, một khi ta có các mảnh ghép cơ bản, vòng lặp huấn luyện lại rất giống với những gì ta đã làm khi lập trình mọi thứ từ đầu. .. raw:: html Nhắc lại rằng: với số lượng epoch nhất định, trong mỗi epoch chúng ta sẽ duyệt qua toàn bộ tập dữ liệu (``train_data``), lần lượt lấy từng minibatch chứa dữ liệu đầu vào và các nhãn gốc tương ứng. Đối với mỗi minibatch, chúng ta cần tuân thủ theo trình tự sau: .. raw:: html - Đưa ra dự đoán bằng cách gọi ``net(X)`` và tính giá trị mất mát ``l`` (lượt truyền xuôi). - Tính gradient bằng cách gọi ``l.backward()`` (lượt truyền ngược). - Cập nhật các tham số của mô hình bằng cách gọi bộ tối ưu SGD (chú ý rằng ``trainer`` đã biết các tham số cần tối ưu, nên ta chỉ cần truyền thêm kích thước của minibatch). .. raw:: html Ngoài ra, ta tính giá trị mất mát sau mỗi epoch và in nó ra màn hình để giám sát tiến trình. .. code:: python num_epochs = 3 for epoch in range(1, num_epochs + 1): for X, y in data_iter: with autograd.record(): l = loss(net(X), y) l.backward() trainer.step(batch_size) l = loss(net(features), labels) print('epoch %d, loss: %f' % (epoch, l.mean().asnumpy())) .. parsed-literal:: :class: output epoch 1, loss: 0.024762 epoch 2, loss: 0.000089 epoch 3, loss: 0.000051 .. raw:: html .. raw:: html .. raw:: html Dưới đây, ta so sánh các tham số của mô hình đã được học thông qua việc huấn luyện trên tập dữ liệu hữu hạn với các tham số được dùng để tạo ra tập dữ liệu. Để truy cập các tham số trong Gluon, trước hết ta truy cập tầng ta quan tâm thông qua biến ``net``, sau đó truy cập trọng số (``weight``) và hệ số điều chỉnh (``bias``) của tầng đó. Để truy cập giá trị tham số dưới dạng một mảng ``ndarray``, ta sử dụng phương thức ``data``. Giống với phiên bản lập trình từ đầu của chúng ta, các tham số ước lượng có giá trị gần với giá trị chính xác của chúng. .. code:: python w = net[0].weight.data() print('Error in estimating w', true_w.reshape(w.shape) - w) b = net[0].bias.data() print('Error in estimating b', true_b - b) .. parsed-literal:: :class: output Error in estimating w [[ 0.00061905 -0.00038671]] Error in estimating b [0.00049639] .. raw:: html .. raw:: html .. raw:: html Tóm tắt ------- .. raw:: html - Sử dụng Gluon giúp việc lập trình các mô hình trở nên ngắn gọn hơn rất nhiều. - Trong Gluon, mô-đun ``data`` cung cấp các công cụ để xử lý dữ liệu, mô-đun ``nn`` định nghĩa một lượng lớn các tầng cho mạng nơ-ron, và mô-đun ``loss`` cho phép ta thiết lập nhiều hàm mất mát phổ biến. - Mô-đun ``initializer`` của MXNet cung cấp nhiều phương thức khác nhau để khởi tạo tham số cho mô hình. - Kích thước và dung lượng lưu trữ của các tham số sẽ được suy ra một cách tự động (nhưng nên cẩn thận tránh truy cập các tham số trước khi chúng được khởi tạo). .. raw:: html Bài tập ------- .. raw:: html 1. Nếu thay thế ``l = loss(output, y)`` bằng ``l = loss(output, y).mean()``, chúng ta cần đổi ``trainer.step(batch_size)`` thành ``trainer.step(1)`` để phần mã nguồn này hoạt động giống như trước. Tại sao lại thế? 2. Xem lại tài liệu về MXNet để biết các hàm mất mát và các phương thức khởi tạo được cung cấp trong hai mô-đun ``gluon.loss`` và ``init``. Hãy thay thế hàm mất mát đang sử dụng bằng hàm mất mát Huber (*Huber loss*). 3. Làm thế nào để truy cập gradient của ``dense.weight``? .. 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 - Trần Thị Hồng Hạnh - Phạm Hồng Vinh - Vũ Hữu Tiệp - Lý Phi Long - Phạm Đăng Khoa - Lê Khắc Hồng Phúc - Dương Nhật Tân - Nguyễn Văn Tâm - Bùi Nhật Quân - Nguyễn Mai Hoàng Long