.. raw:: html .. raw:: html .. raw:: html .. _sec_linear_scratch: Lập trình Hồi quy Tuyến tính từ đầu =================================== .. raw:: html Bây giờ bạn đã hiểu được điểm mấu chốt đằng sau thuật toán hồi quy tuyến tính, chúng ta đã có thể bắt đầu thực hành viết mã. Trong phần này, ta sẽ xây dựng lại toàn bộ kĩ thuật này từ đầu, bao gồm: pipeline dữ liệu, mô hình, hàm mất mát và phương pháp tối ưu hạ gradient. Vì các framework học sâu hiện đại có thể tự động hóa gần như tất cả các công đoạn ở trên, việc lập trình mọi thứ từ đầu chỉ để đảm bảo bạn biết rõ mình đang làm gì. Hơn nữa, việc hiểu rõ cách mọi thứ hoạt động sẽ giúp ta rất nhiều trong những lúc cần tùy chỉnh các mô hình, tự định nghĩa lại các tầng tính toán hay các hàm mất mát, v.v. Trong phần này, chúng ta chỉ dựa vào ``ndarray`` và ``autograd``. Sau đó, chúng tôi sẽ giới thiệu một phương pháp triển khai chặt chẽ hơn, tận dụng các tính năng tuyệt vời của Gluon. Để bắt đầu, chúng ta cần khai báo một vài gói thư viện cần thiết. .. code:: python %matplotlib inline from d2l import mxnet as d2l from mxnet import autograd, np, npx import random npx.set_np() .. raw:: html Tạo tập dữ liệu --------------- .. raw:: html Để giữ cho mọi thứ đơn giản, chúng ta sẽ xây dựng một tập dữ liệu nhân tạo theo một mô hình tuyến tính với nhiễu cộng. Nhiệm vụ của chúng ta là khôi phục các tham số của mô hình này bằng cách sử dụng một tập hợp hữu hạn các mẫu có trong tập dữ liệu đó. Chúng ta sẽ sử dụng dữ liệu ít chiều để thuận tiện cho việc minh họa. Trong đoạn mã sau, chúng ta đã tạo một tập dữ liệu chứa :math:`1000` mẫu, mỗi mẫu bao gồm :math:`2` đặc trưng được lấy ngẫu nhiên theo phân phối chuẩn. Do đó, tập dữ liệu tổng hợp của chúng ta sẽ là một đối tượng :math:`\mathbf{X}\in \mathbb{R}^{1000 \times 2}`. .. raw:: html Các tham số đúng để tạo tập dữ liệu sẽ là :math:`\mathbf{w} = [2, -3.4]^\top` và :math:`b = 4.2` và nhãn sẽ được tạo ra dựa theo mô hình tuyến tính với nhiễu :math:`\epsilon`: .. math:: \mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon. .. raw:: html .. raw:: html .. raw:: html Bạn đọc có thể xem :math:`\epsilon` như là sai số tiềm ẩn của phép đo trên các đặc trưng và các nhãn. Chúng ta sẽ mặc định các giả định tiêu chuẩn đều thỏa mãn và vì thế :math:`\epsilon` tuân theo phân phối chuẩn với trung bình bằng :math:`0`. Để đơn giản, ta sẽ thiết lập độ lệch chuẩn của nó bằng :math:`0.01`. Đoạn mã nguồn sau sẽ tạo ra tập dữ liệu tổng hợp: .. code:: python # Saved in the d2l package for later use def synthetic_data(w, b, num_examples): """Generate y = X w + b + noise.""" X = np.random.normal(0, 1, (num_examples, len(w))) y = np.dot(X, w) + b y += np.random.normal(0, 0.01, y.shape) return X, y true_w = np.array([2, -3.4]) true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 1000) .. raw:: html Lưu ý rằng mỗi hàng trong ``features`` chứa một điểm dữ liệu hai chiều và mỗi hàng trong ``labels`` chứa một giá trị mục tiêu một chiều (một số vô hướng). .. code:: python print('features:', features[0],'\nlabel:', labels[0]) .. parsed-literal:: :class: output features: [2.2122064 1.1630787] label: 4.662078 .. raw:: html Bằng cách vẽ đồ thị phân tán với chiều thứ hai ``features[:, 1]`` và ``labels``, ta có thể quan sát rõ mối tương quan tuyến tính giữa chúng. .. code:: python d2l.set_figsize((3.5, 2.5)) d2l.plt.scatter(features[:, 1].asnumpy(), labels.asnumpy(), 1); .. figure:: output_linear-regression-scratch_vn_279c44_7_0.svg .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Đọc từ Tập dữ liệu ------------------ .. raw:: html Nhắc lại rằng việc huấn luyện mô hình bao gồm tách tập dữ liệu thành nhiều phần (các mininbatch), lần lượt đọc từng phần của tập dữ liệu mẫu, và sử dụng chúng để cập nhật mô hình của chúng ta. Vì quá trình này rất căn bản để huấn luyện các giải thuật học máy, ta nên định nghĩa một hàm để trộn và truy xuất dữ liệu trong các minibatch một cách tiện lợi. .. raw:: html Ở đoạn mã dưới đây, chúng ta định nghĩa hàm ``data_iter`` để minh hoạ cho một cách lập trình chức năng này. Hàm này lấy kích thước một batch, một ma trận đặc trưng và một vector các nhãn rồi sinh ra các minibatch có kích thước ``batch_size``. Mỗi minibatch gồm một tuple các đặc trưng và nhãn. .. code:: python def data_iter(batch_size, features, labels): num_examples = len(features) indices = list(range(num_examples)) # The examples are read at random, in no particular order random.shuffle(indices) for i in range(0, num_examples, batch_size): batch_indices = np.array( indices[i: min(i + batch_size, num_examples)]) yield features[batch_indices], labels[batch_indices] .. raw:: html Lưu ý rằng thông thường chúng ta muốn dùng các minibatch có kích thước phù hợp để tận dụng tài nguyên phần cứng GPU để xử lý song song hiệu quả nhất. Vì mỗi mẫu có thể được mô hình xử lý và tính đạo hàm riêng của hàm mất mát song song với nhau, GPU cho phép xử lý hàng trăm mẫu cùng lúc mà chỉ tốn thời gian hơn một chút so với xử lý một mẫu duy nhất. .. raw:: html Để hiểu hơn, chúng ta hãy chạy đoạn chương trình để đọc và in ra batch đầu tiên của mẫu dữ liệu. Kích thước của các đặc trưng trong mỗi minibatch cho ta biết kích thước của batch lẫn số lượng của các đặc trưng đầu vào. Tương tự, tập minibatch của các nhãn sẽ có kích thước theo ``batch_size``. .. code:: python batch_size = 10 for X, y in data_iter(batch_size, features, labels): print(X, '\n', y) break .. parsed-literal:: :class: output [[-0.32445922 -0.91774726] [ 0.681858 1.3600351 ] [ 1.5702168 1.11278 ] [-0.05733065 -0.19965324] [ 1.2688328 -1.3046491 ] [ 0.71321625 0.85190713] [-0.21464506 0.6525396 ] [-0.02261727 0.41016445] [ 0.69248587 -0.26833388] [-0.5309896 -0.9410424 ]] [ 6.6650786 0.9486723 3.5531938 4.7575097 11.158839 2.7378514 1.5480783 2.7589145 6.4890523 6.3573055] .. raw:: html .. raw:: html .. raw:: html Khi chạy iterator, ta lấy từng minibatch riêng biệt cho đến khi lấy hết bộ dữ liệu (bạn hãy xử xem). Mặc dù sử dụng iterator như trên phục vụ tốt cho công tác giảng dạy, nó lại không phải là cách hiệu quả và có thể khiến chúng ta gặp nhiều rắc rối trong thực tế. Chẳng hạn, nó buộc ta phải nạp toàn bộ dữ liệu vào bộ nhớ và tốn rất nhiều thao tác truy cập bộ nhớ ngẫu nhiên. Các iterator trong Apache MXNet lại khá hiệu quả khi chúng có thể xử lý cả dữ liệu được lưu trữ trên tập tin lẫn trong các luồng dữ liệu. .. raw:: html .. raw:: html .. raw:: html Khởi tạo các Tham số Mô hình ---------------------------- .. raw:: html Để tối ưu các tham số của dữ liệu bằng hạ gradient, đầu tiên ta cần khởi tạo chúng. Trong đoạn mã dưới đây, ta khởi tạo các trọng số bằng cách lấy ngẫu nhiên các mẫu từ một phân phối chuẩn với giá trị trung bình bằng 0 và độ lệch chuẩn là :math:`0.01`, sau đó gán hệ số điều chỉnh :math:`b` bằng :math:`0`. .. code:: python w = np.random.normal(0, 0.01, (2, 1)) b = np.zeros(1) .. raw:: html Sau khi khởi tạo các tham số, bước tiếp theo là cập nhật chúng cho đến khi chúng ăn khớp với dữ liệu của ta đủ tốt. Mỗi lần cập nhật, ta tính gradient (đạo hàm nhiều biến) của hàm mất mát theo các tham số. Với gradient này, chúng ta có thể cập nhật mỗi tham số theo hướng giảm dần giá trị mất mát. .. raw:: html .. raw:: html .. raw:: html Vì không ai muốn tính gradient bằng tay (một việc rất nhàm chán và dễ sai sót), ta dùng chương trình để tính tự động gradient (autograd). Xem :numref:`sec_autograd` để biết thêm chi tiết. Nhắc lại từ mục tính vi phân tự động, để chỉ định hàm ``autograd`` lưu gradient của các biến số, ta cần gọi hàm ``attach_grad``, cấp phát bộ nhớ để lưu giá trị gradient mong muốn. .. code:: python w.attach_grad() b.attach_grad() .. raw:: html Định nghĩa Mô hình ------------------ .. raw:: html Tiếp theo, chúng ta cần định nghĩa mô hình dựa trên đầu vào và tham số liên quan tới đầu ra. Nhắc lại rằng để tính đầu ra của một mô hình tuyến tính, ta có thể đơn giản tính tích vô hướng ma trận-vector của các mẫu :math:`\mathbf{X}` và trọng số mô hình :math:`w`, sau đó thêm vào hệ số điều chỉnh :math:`b` cho từng mẫu. Ở đây, ``np.dot(X, w)`` là một vector trong khi ``b`` là một số vô hướng. Nhắc lại rằng khi tính tổng vector và số vô hướng, thì số vô hướng sẽ được cộng vào từng phần tử của vector. .. code:: python # Saved in the d2l package for later use def linreg(X, w, b): return np.dot(X, w) + b .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Định nghĩa Hàm Mất mát ---------------------- .. raw:: html Để cập nhật mô hình ta phải tính gradient của hàm mất mát, vậy nên ta cần định nghĩa hàm mất mát trước tiên. Chúng ta sẽ sử dụng hàm mất mát bình phương (SE) như đã trình bày ở phần trước đó. Trên thực tế, chúng ta cần chuyển đổi giá trị nhãn thật ``y`` sang kích thước của giá trị dự đoán ``y_hat``. Hàm dưới đây sẽ trả về kết quả có kích thước tương đương với kích thước của ``y_hat``. .. code:: python # Saved in the d2l package for later use def squared_loss(y_hat, y): return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 .. raw:: html Định nghĩa Thuật toán Tối ưu ---------------------------- .. raw:: html Như đã thảo luận ở mục trước, hồi quy tuyến tính có một `nghiệm dạng đóng `__. Tuy nhiên, đây không phải là một cuốn sách về hồi quy tuyến tính, mà là về Học sâu. Vì không một mô hình nào khác được trình bày trong cuốn sách này có thể giải được bằng phương pháp phân tích, chúng tôi sẽ nhân cơ hội này để giới thiệu với các bạn ví dụ đầu tiên về hạ gradient ngẫu nhiên (*stochastic gradient descent – SGD*). .. raw:: html .. raw:: html .. raw:: html Sử dụng một batch được lấy ngẫu nhiên từ tập dữ liệu tại mỗi bước, chúng ta sẽ ước tính được gradient của mất mát theo các tham số. Tiếp đó, ta sẽ cập nhật các tham số (với một lượng nhỏ) theo chiều hướng làm giảm sự mất mát. Nhớ lại từ :numref:`sec_autograd` rằng sau khi chúng ta gọi ``backward``, mỗi tham số (``param``) sẽ có gradient của nó lưu ở ``param.grad``. Đoạn mã sau áp dụng cho việc cập nhật SGD, đưa ra một bộ các tham số, tốc độ học và kích thước batch. Kích thước của bước cập nhật được xác định bởi tốc độ học ``lr``. Bởi vì các mất mát được tính dựa trên tổng các mẫu của batch, ta chuẩn hóa kích thước bước cập nhật theo kích thước batch (``batch_size``), sao cho độ lớn của một bước cập nhật thông thường không phụ thuộc nhiều vào kích thước batch. .. code:: python # Saved in the d2l package for later use def sgd(params, lr, batch_size): for param in params: param[:] = param - lr * param.grad / batch_size .. raw:: html .. raw:: html .. raw:: html Huấn luyện ---------- .. raw:: html Bây giờ, sau khi đã có tất cả các thành phần, chúng ta đã sẵn sàng để viết vòng lặp huấn luyện. Quan trọng nhất là bạn phải hiểu được rõ đoạn mã này bởi vì việc huấn luyện gần như tương tự thế này sẽ được lặp lại nhiều lần trong suốt qua trình chúng ta tìm hiểu và lập trình các thuật toán học sâu. .. raw:: html Trong mỗi vòng lặp, đầu tiên chúng ta sẽ lấy ra các minibatch dữ liệu và chạy nó qua mô hình để lấy ra tập kết quả dự đoán. Sau khi tính toán sự mất mát, chúng ta dùng hàm ``backward`` để bắt đầu lan truyền ngược qua mạng lưới, lưu trữ các gradient tương ứng với mỗi tham số trong từng thuộc tính ``.grad`` của chúng. Cuối cùng, chúng ta sẽ dùng thuật toán tối ưu ``sgd`` để cập nhật các tham số của mô hình. Từ đầu chúng ta đã đặt kích thước batch ``batch_size`` là :math:`10`, vậy nên mất mát ``l`` cho mỗi minibatch có kích thước là (:math:`10`, :math:`1`). .. raw:: html .. raw:: html .. raw:: html Tóm lại, chúng ta sẽ thực thi vòng lặp sau: .. raw:: html - Khởi tạo bộ tham số :math:`(\mathbf{w}, b)` - Lặp lại cho tới khi hoàn thành - Tính gradient :math:`\mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{\mathcal{B}} \sum_{i \in \mathcal{B}} l(\mathbf{x}^i, y^i, \mathbf{w}, b)` - Cập nhật bộ tham số :math:`(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}` .. raw:: html Trong đoạn mã dưới đây, ``l`` là một vector của các mất mát của từng mẫu trong minibatch. Vì ``l`` không phải là biến vô hướng, chạy ``l.backward()`` sẽ cộng các phần tử trong ``l`` để tạo ra một biến mới và sau đó mới tính gradient. .. raw:: html Với mỗi epoch, chúng ta sẽ lặp qua toàn bộ tập dữ liệu (sử dụng hàm ``data_iter``) cho đến khi đi qua toàn bộ mọi mẫu trong tập huấn luyện (giả định rằng số mẫu chia hết cho kích thước batch). Số epoch ``num_epochs`` và tốc độ học ``lr`` đều là siêu tham số, mà chúng ta đặt ở đây là tương ứng :math:`3` và :math:`0.03`. Không may thay, việc lựa chọn siêu tham số thường không đơn giản và đòi hỏi sự điều chỉnh qua nhiều lần thử và sai. Hiện tại chúng ta sẽ bỏ qua những chi tiết này nhưng sẽ bàn lại về chúng tại chương :numref:`chap_optimization`. .. code:: python lr = 0.03 # Learning rate num_epochs = 3 # Number of iterations net = linreg # Our fancy linear model loss = squared_loss # 0.5 (y-y')^2 for epoch in range(num_epochs): # Assuming the number of examples can be divided by the batch size, all # the examples in the training dataset are used once in one epoch # iteration. The features and tags of minibatch examples are given by X # and y respectively for X, y in data_iter(batch_size, features, labels): with autograd.record(): l = loss(net(X, w, b), y) # Minibatch loss in X and y l.backward() # Compute gradient on l with respect to [w, b] sgd([w, b], lr, batch_size) # Update parameters using their gradient train_l = loss(net(features, w, b), labels) print('epoch %d, loss %f' % (epoch + 1, train_l.mean().asnumpy())) .. parsed-literal:: :class: output epoch 1, loss 0.025013 epoch 2, loss 0.000090 epoch 3, loss 0.000051 .. raw:: html .. raw:: html .. raw:: html Trong trường hợp này, bởi vì chúng ta tự tổng hợp dữ liệu nên đã biết chính xác giá trị đúng của các tham số. Vì vậy, chúng ta có thể đánh giá sự thành công của việc huấn luyện bằng cách so sánh giá trị đúng của các tham số với những giá trị học được thông qua quá trình huấn luyện. Quả thật giá trị các tham số học được và giá trị chính xác của các tham số rất gần với nhau. .. code:: python print('Error in estimating w', true_w - w.reshape(true_w.shape)) print('Error in estimating b', true_b - b) .. parsed-literal:: :class: output Error in estimating w [ 0.0001483 -0.00020313] Error in estimating b [0.00082684] .. raw:: html Lưu ý là chúng ta không nên ngộ nhận rằng giá trị của các tham số có thể được khôi phục một cách chính xác. Điều này chỉ xảy ra với một số bài toán đặc biệt: các bài toán tối ưu lồi chặt với lượng dữ liệu “đủ” để đảm bảo rằng các mẫu nhiễu vẫn cho phép chúng ta khôi phục được các tham số. Trong hầu hết các trường hợp, điều này *không* xảy ra. Trên thực tế, các tham số của một mạng học sâu hiếm khi nào giống nhau (hoặc thậm chí là gần nhau) giữa hai lần chạy riêng biệt, trừ khi tất cả các điều kiện đều giống hệt nhau, bao gồm cả thứ tự mà dữ liệu được duyệt qua. Tuy nhiên, trong học máy, chúng ta thường ít quan tâm đến việc khôi phục chính xác giá trị của các tham số, mà quan tâm nhiều hơn đến bộ tham số nào sẽ dẫn tới việc dự đoán chính xác hơn. May mắn thay, thậm chí với những bài toán tối ưu khó, giải thuật hạ gradient ngẫu nhiên thường có thể tìm ra các lời giải đủ tốt, do một thực tế là đối với các mạng học sâu, có thể tồn tại nhiều bộ tham số dẫn đến việc dự đoán chính xác. .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Tóm tắt ------- .. raw:: html Chúng ta đã thấy cách một mạng sâu được thực thi và tối ưu hóa từ đầu chỉ với ``ndarray`` và ``autograd`` mà không cần định nghĩa các tầng, các thuật toán tối ưu đặc biệt, v.v. Điều này chỉ mới chạm đến bề mặt của những gì mà ta có thể làm. Trong các phần sau, chúng tôi sẽ mô tả các mô hình khác dựa trên những khái niệm vừa được giới thiệu cũng như cách thực thi chúng một cách chính xác hơn. .. raw:: html Bài tập ------- .. raw:: html 1. Điều gì sẽ xảy ra nếu chúng ta khởi tạo các trọng số :math:`\mathbf{w} = 0`. Liệu thuật toán sẽ vẫn hoạt động chứ? 2. Giả sử rằng bạn là `Georg Simon Ohm `__ và bạn đang cố gắng tìm ra một mô hình giữa điện áp và dòng điện. Bạn có thể sử dụng ``autograd`` để học các tham số cho mô hình của bạn không? 3. Bạn có thể sử dụng `Luật Planck `__ để xác định nhiệt độ của một vật thể sử dụng mật độ năng lượng quang phổ không? 4. Những vấn đề gặp phải nếu muốn mở rộng ``autograd`` đến các đạo hàm bậc hai? Cần sửa lại như thế nào? 5. Tại sao hàm ``reshape`` lại cần thiết trong hàm ``squared_loss``? 6. Thử nghiệm các tốc độ học khác nhau để kiểm tra mức độ giảm nhanh của giá trị hàm mất mát giảm. 7. Nếu số lượng mẫu không thể chia hết cho kích thước batch, thì hàm ``data_iter`` sẽ xử lý như thế nào? .. 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ý Phi Long - Vũ Hữu Tiệp - Phạm Hồng Vinh - Nguyễn Văn Tâm - Nguyễn Cảnh Thướng - Nguyễn Lê Quang Nhật - Dương Nhật Tân - Nguyễn Minh Thư - Nguyễn Trường Phát - Đinh Minh Tân - Trần Thị Hồng Hạnh - Lê Khắc Hồng Phúc - Phạm Minh Đức - Nguyễn Mai Hoàng Long