.. raw:: html .. raw:: html .. raw:: html .. _sec_softmax_scratch: Lập trình Hồi quy Sofmax từ đầu =============================== .. raw:: html Giống như khi ta lập trình hồi quy tuyến tính từ đầu, hồi quy (softmax) logistic đa lớp cũng là một kĩ thuật căn bản mà bạn nên hiểu biết tường tận các chi tiết để có thể tự xây dựng lại. Sau khi tự lập trình lại mọi thứ thì ta cũng sẽ dùng Gluon để so sánh như ở phần hồi quy tuyến tính. Để bắt đầu, chúng ta nhập các thư viện quen thuộc vào. .. code:: python from d2l import mxnet as d2l from mxnet import autograd, np, npx, gluon from IPython import display npx.set_np() .. raw:: html Ta sẽ làm việc trên tập dữ liệu Fashion-MNIST, vừa được giới thiệu trong :numref:`sec_fashion_mnist`, thiết lập một iterator với kích thước batch là :math:`256`. .. code:: python batch_size = 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) .. raw:: html Khởi tạo các Tham số của Mô hình -------------------------------- .. raw:: html Giống như ví dụ về hồi quy tuyến tính, mỗi mẫu sẽ được biểu diễn bằng một vector có chiều dài cố định. Mỗi mẫu trong tập dữ liệu thô là một ảnh :math:`28 \times 28`. Trong phần này, chúng ta sẽ trải phẳng mỗi tấm ảnh thành một vector có kích thước là :math:`784`. Sau này ta sẽ bàn về các chiến lược công phu hơn có khả năng khai thác cấu trúc không gian của ảnh, còn bây giờ ta hãy xem mỗi điểm ảnh là một đặc trưng. .. raw:: html Nhắc lại trong hồi quy softmax, mỗi lớp sẽ có một đầu ra. Vì tập dữ liệu của chúng ta có :math:`10` lớp, mạng của chúng ta sẽ có :math:`10` đầu ra. Do đó, các trọng số sẽ tạo thành một ma trận :math:`784 \times 10` và các hệ số điều chỉnh sẽ tạo thành một vector :math:`1 \times 10`. Cũng như hồi quy tuyến tính, ta sẽ khởi tạo các trọng số :math:`W` bằng nhiễu Gauss và các hệ số điều chỉnh sẽ được khởi tạo bằng :math:`0`. .. code:: python num_inputs = 784 num_outputs = 10 W = np.random.normal(0, 0.01, (num_inputs, num_outputs)) b = np.zeros(num_outputs) .. raw:: html Hãy nhớ rằng ta cần *đính kèm gradient* vào các tham số của mô hình. Cụ thể hơn, ta cho MXNet biết rằng ta sẽ muốn tính các gradient theo các tham số này và cần phân bổ bộ nhớ để lưu trữ chúng trong tương lai. .. code:: python W.attach_grad() b.attach_grad() .. raw:: html .. raw:: html .. raw:: html Softmax ------- .. raw:: html Trước khi xây dựng mô hình hồi quy softmax, hãy ôn nhanh tác dụng của các toán tử như ``sum`` trên những chiều cụ thể của một ``ndarray``. Cho một ma trận ``X``, chúng ta có thể tính tổng tất cả các phần tử (mặc định) hoặc chỉ trên các phần tử trong cùng một trục, *ví dụ*, cột (``axis=0``) hoặc cùng một hàng (``axis=1``). Lưu ý rằng nếu ``X`` là một mảng có kích thước ``(2, 3)``, tính tổng các cột (``X.sum (axis=0)``) sẽ trả về một vector (một chiều) có kích thước là ``(3,)``. Nếu chúng ta muốn giữ số lượng trục giống với mảng ban đầu thay vì thu gọn kích thước trên các trục đã tính toán (dẫn đến một mảng 2 chiều có kích thước ``(1, 3)``), ta có thể chỉ định ``keepdims=True`` khi gọi hàm ``sum``. .. code:: python X = np.array([[1, 2, 3], [4, 5, 6]]) print(X.sum(axis=0, keepdims=True), '\n', X.sum(axis=1, keepdims=True)) .. parsed-literal:: :class: output [[5. 7. 9.]] [[ 6.] [15.]] .. raw:: html Bây giờ chúng ta đã có thể bắt đầu xây dựng hàm softmax. Lưu ý rằng việc thực thi hàm softmax bao gồm hai bước: Đầu tiên, chúng ta lũy thừa từng giá trị (sử dụng ``exp``). Sau đó tính tổng trên mỗi hàng để lấy các hằng số chuẩn hóa cho mỗi mẫu (vì mỗi mẫu là một hàng). Cuối cùng, chia mỗi hàng theo hằng số chuẩn hóa của nó để đảm bảo rằng kết quả có tổng bằng :math:`1`. Trước khi xem đoạn mã, chúng ta hãy nhớ lại các bước này được thể hiện trong phương trình sau: .. math:: \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(X_{ij})}{\sum_k \exp(X_{ik})}. .. raw:: html Mẫu số hoặc hằng số chuẩn hóa đôi khi cũng được gọi là hàm phân hoạch (*partition function*) và logarit của nó được gọi là hàm log phân hoạch (*log-partition function*). Tên gốc của hàm được định nghĩa trong `cơ học thống kê `__ với phương trình liên quan đến mô hình hóa phân phối trên một tập hợp các phần tử. .. code:: python def softmax(X): X_exp = np.exp(X) partition = X_exp.sum(axis=1, keepdims=True) return X_exp / partition # The broadcast mechanism is applied here .. raw:: html Có thể thấy rằng với bất kỳ đầu vào ngẫu nhiên nào thì mỗi phần tử đều được biến đổi thành một số không âm. Hơn nữa, mỗi hàng đều có tổng là 1 nên thoả mãn yêu cầu làm một giá trị xác suất. Chú ý rằng đoạn mã trên tuy đúng về mặt toán học nhưng chúng tôi đã hơi cẩu thả về mặt lập trình vì đã không kiểm tra vấn đề tràn số trên và dưới gây ra bởi các giá trị vô cùng lớn hoặc vô cùng nhỏ trong ma trận, không giống với cách đã thực hiện tại :numref:`sec_naive_bayes`. .. code:: python X = np.random.normal(size=(2, 5)) X_prob = softmax(X) X_prob, X_prob.sum(axis=1) .. parsed-literal:: :class: output (array([[0.22376052, 0.06659239, 0.06583703, 0.29964197, 0.3441681 ], [0.63209665, 0.03179282, 0.194987 , 0.09209415, 0.04902935]]), array([1. , 0.99999994])) .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Mô hình ------- .. raw:: html Sau khi đã định nghĩa hàm softmax, chúng ta có thể bắt đầu lập trình mô hình hồi quy softmax. Đoạn mã sau định nghĩa lượt truyền xuôi qua mạng. Chú ý rằng chúng ta sẽ làm phẳng mỗi ảnh gốc trên batch thành một vector có độ dài ``num_inputs`` bằng hàm ``reshape``, trước khi truyền dữ liệu sang mô hình đã khởi tạo. .. code:: python def net(X): return softmax(np.dot(X.reshape(-1, num_inputs), W) + b) .. raw:: html Hàm mất mát ----------- .. raw:: html Tiếp đến chúng ta cần lập trình hàm mất mát entropy chéo đã được giới thiệu ở :numref:`sec_softmax`. Đây có lẽ là hàm mất mát thông dụng nhất trong phần lớn nghiên cứu về học sâu vì hiện nay số lượng bài toán phân loại đã vượt xa hơn số lượng bài toán hồi quy. .. raw:: html Nhắc lại rằng hàm entropy chéo lấy đầu vào là đối log hợp lý của xác suất dự đoán được gán cho nhãn thật :math:`-\log P(y \mid x)`. Thay vì lặp qua các dự đoán của mô hình bằng vòng lặp ``for`` trong Python (có xu hướng kém hiệu quả), chúng ta có thể sử dụng hàm ``pick`` để dễ dàng chọn các phần tử thích hợp từ ma trận của các giá trị softmax. Dưới đây, ta minh hoạ cách sử dụng hàm ``pick`` trong một ví dụ đơn giản với ma trận có :math:`3` lớp và :math:`2` mẫu. .. code:: python y_hat = np.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]]) y_hat[[0, 1], [0, 2]] .. parsed-literal:: :class: output array([0.1, 0.5]) .. raw:: html Bây giờ chúng ta có thể lập trình hàm mất mát entropy chéo hiệu quả hơn chỉ với một dòng lệnh. .. code:: python def cross_entropy(y_hat, y): return - np.log(y_hat[range(len(y_hat)), y]) .. raw:: html .. raw:: html .. raw:: html Độ chính xác cho bài toán Phân loại ----------------------------------- .. raw:: html Với phân phối xác suất dự đoán ``y_hat``, ta thường chọn lớp có xác suất dự đoán cao nhất khi phải đưa ra một dự đoán *cụ thể* vì nhiều ứng dụng trong thực tế có yêu cầu như vậy. Ví dụ, Gmail cần phải phân loại một email vào trong các danh mục sau: Email chính (Primary), Mạng xã hội (Social), Nội dung cập nhật (Updates) hoặc Diễn đàn (Forums). Có thể các xác suất được tính toán bên trong nội bộ hệ thống, nhưng cuối cùng kết quả vẫn chỉ là một trong các danh mục. .. raw:: html Các dự đoán được coi là chính xác khi chúng khớp với lớp thực tế ``y``. Độ chính xác phân loại được tính bởi tỉ lệ các dự đoán chính xác trên tất cả các dự đoán đã đưa ra. Dù ta có thể gặp khó khăn khi tối ưu hóa trực tiếp độ chính xác (chúng không khả vi), đây thường là phép đo chất lượng được quan tâm nhiều nhất và sẽ luôn được tính khi huấn luyện các bộ phân loại. .. raw:: html Độ chính xác được tính toán như sau: Đầu tiên, dùng lệnh ``y_hat.argmax(axis=1)`` nhằm lấy ra các lớp được dự đoán (được cho bởi chỉ số của phần tử lớn nhất trên mỗi hàng). Kết quả trả về sẽ có cùng kích thước với biến ``y`` và bây giờ ta chỉ cần so sánh hai vector này. Vì toán tử ``==`` so khớp cả kiểu dữ liệu của biến (ví dụ một biến ``int`` và một biến ``float32`` không thể bằng nhau), ta cần đưa chúng về cùng một kiểu dữ liệu (ở đây ta chọn kiểu ``float32``). Kết quả sẽ là một ``ndarray`` chứa các giá trị 0 (false) và 1 (true), giá trị trung bình này mang lại kết quả mà ta mong muốn. .. code:: python # Saved in the d2l package for later use def accuracy(y_hat, y): if y_hat.shape[1] > 1: return float((y_hat.argmax(axis=1) == y.astype('float32')).sum()) else: return float((y_hat.astype('int32') == y.astype('int32')).sum()) .. raw:: html Ta sẽ tiếp tục sử dụng biến ``y_hat`` và ``y`` đã được định nghĩa trong hàm ``pick``, lần lượt tương ứng với phân phối xác suất được dự đoán và nhãn. Có thể thấy rằng kết quả dự đoán lớp từ ví dụ đầu tiên là :math:`2` (phần từ lớn nhất trong hàng là :math:`0.6` với chỉ số tương ứng là :math:`2`) không khớp với nhãn thực tế là :math:`0`. Dự đoán lớp ở ví dụ thứ hai là :math:`2` (phần tử lớn nhất hàng là :math:`0.5` với chỉ số tương ứng là :math:`2`) khớp với nhãn thực tế là :math:`2`. Do đó, ta có độ chính xác phân loại cho hai ví dụ này là :math:`0.5`. .. code:: python y = np.array([0, 2]) accuracy(y_hat, y) / len(y) .. parsed-literal:: :class: output 0.5 .. raw:: html Tương tự như trên, ta có thể đánh giá độ chính xác của mô hình ``net`` trên tập dữ liệu (được truy xuất thông qua ``data_iter``). .. code:: python # Saved in the d2l package for later use def evaluate_accuracy(net, data_iter): metric = Accumulator(2) # num_corrected_examples, num_examples for X, y in data_iter: metric.add(accuracy(net(X), y), y.size) return metric[0] / metric[1] .. raw:: html ``Accumulator`` ở đây là một lớp đa tiện ích, có tác dụng tính tổng tích lũy của nhiều số. .. code:: python # Saved in the d2l package for later use class Accumulator(object): """Sum a list of numbers over time.""" def __init__(self, n): self.data = [0.0] * n def add(self, *args): self.data = [a+float(b) for a, b in zip(self.data, args)] def reset(self): self.data = [0] * len(self.data) def __getitem__(self, i): return self.data[i] .. raw:: html Vì ta đã khởi tạo mô hình ``net`` với trọng số ngẫu nhiên nên độ chính xác của mô hình lúc này sẽ gần với việc phỏng đoán ngẫu nhiên, tức độ chính xác bằng :math:`0.1` với :math:`10` lớp. .. code:: python evaluate_accuracy(net, test_iter) .. parsed-literal:: :class: output 0.0811 .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Huấn luyện Mô hình ------------------ .. raw:: html Vòng lặp huấn luyện cho hồi quy softmax trông khá quen thuộc nếu bạn đã xem cách lập trình cho hồi quy tuyến tính tại :numref:`sec_linear_scratch`. Ở đây, chúng ta tái cấu trúc (*refactor*) lại đoạn mã để sau này có thể tái sử dụng. Đầu tiên, chúng ta định nghĩa một hàm để huấn luyện với một epoch dữ liệu. Lưu ý rằng ``updater`` là một hàm tổng quát để cập nhật các tham số của mô hình và sẽ nhận kích thước batch làm đối số. Nó có thể là một wrapper của ``d2l.sgd`` hoặc là một đối tượng huấn luyện Gluon. .. code:: python # Saved in the d2l package for later use def train_epoch_ch3(net, train_iter, loss, updater): metric = Accumulator(3) # train_loss_sum, train_acc_sum, num_examples if isinstance(updater, gluon.Trainer): updater = updater.step for X, y in train_iter: # Compute gradients and update parameters with autograd.record(): y_hat = net(X) l = loss(y_hat, y) l.backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.size) # Return training loss and training accuracy return metric[0]/metric[2], metric[1]/metric[2] .. raw:: html Trước khi xem đoạn mã thực hiện hàm huấn luyện, ta định nghĩa một lớp phụ trợ để minh họa dữ liệu. Mục đích của nó là đơn giản hoá các đoạn mã sẽ xuất hiện trong những chương sau. .. code:: python # Saved in the d2l package for later use class Animator(object): def __init__(self, xlabel=None, ylabel=None, legend=[], xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=None, nrows=1, ncols=1, figsize=(3.5, 2.5)): """Incrementally plot multiple lines.""" d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: self.axes = [self.axes, ] # Use a lambda to capture arguments self.config_axes = lambda: d2l.set_axes( self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): """Add multiple data points into the figure.""" if not hasattr(y, "__len__"): y = [y] n = len(y) if not hasattr(x, "__len__"): x = [x] * n if not self.X: self.X = [[] for _ in range(n)] if not self.Y: self.Y = [[] for _ in range(n)] if not self.fmts: self.fmts = ['-'] * n for i, (a, b) in enumerate(zip(x, y)): if a is not None and b is not None: self.X[i].append(a) self.Y[i].append(b) self.axes[0].cla() for x, y, fmt in zip(self.X, self.Y, self.fmts): self.axes[0].plot(x, y, fmt) self.config_axes() display.display(self.fig) display.clear_output(wait=True) .. raw:: html Hàm huấn luyện sau đó sẽ chạy qua nhiều epoch và trực quan hoá quá trình huấn luyện. .. code:: python # Saved in the d2l package for later use def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train loss', 'train acc', 'test acc']) for epoch in range(num_epochs): train_metrics = train_epoch_ch3(net, train_iter, loss, updater) test_acc = evaluate_accuracy(net, test_iter) animator.add(epoch+1, train_metrics+(test_acc,)) .. raw:: html Nhắc lại, chúng ta sử dụng giải thuật hạ gradient ngẫu nhiên theo minibatch để tối ưu hàm mất mát của mô hình. Lưu ý rằng số lượng epoch (``num_epochs``), và hệ số học (``lr``) là hai siêu tham số được hiệu chỉnh. Bằng cách thay đổi các giá trị này, chúng ta có thể tăng độ chính xác khi phân loại của mô hình. Trong thực tế, chúng ta sẽ chia dữ liệu của mình theo ba hướng tiếp cận khác nhau gồm: dữ liệu huấn luyện, dữ liệu kiểm định và dữ liệu kiểm tra; sử dụng dữ liệu kiểm định để chọn ra những giá trị tốt nhất cho các siêu tham số. .. code:: python num_epochs, lr = 10, 0.1 def updater(batch_size): return d2l.sgd([W, b], lr, batch_size) train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater) .. figure:: output_softmax-regression-scratch_vn_6ac99c_37_0.svg .. raw:: html .. raw:: html .. raw:: html Dự đoán ------- .. raw:: html Giờ thì việc huấn luyện đã hoàn thành, mô hình của chúng ta đã sẵn sàng để phân loại ảnh. Cho một loạt các ảnh, chúng ta sẽ so sánh các nhãn thật của chúng (dòng đầu tiên của văn bản đầu ra) với những dự đoán của mô hình (dòng thứ hai của văn bản đầu ra). .. code:: python # Saved in the d2l package for later use def predict_ch3(net, test_iter, n=6): for X, y in test_iter: break trues = d2l.get_fashion_mnist_labels(y) preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1)) titles = [true+'\n' + pred for true, pred in zip(trues, preds)] d2l.show_images(X[0:n].reshape(n, 28, 28), 1, n, titles=titles[0:n]) predict_ch3(net, test_iter) .. figure:: output_softmax-regression-scratch_vn_6ac99c_39_0.svg .. raw:: html Tóm tắt ------- .. raw:: html Với hồi quy softmax, chúng ta có thể huấn luyện các mô hình cho bài toán phân loại đa lớp. Vòng lặp huấn luyện rất giống với vòng lặp huấn luyện của hồi quy tuyến tính: truy xuất và đọc dữ liệu, định nghĩa mô hình và hàm mất mát, và rồi huấn luyện mô hình sử dụng các giải thuật tối ưu. Rồi bạn sẽ sớm thấy rằng hầu hết các mô hình học sâu phổ biến đều có quy trình huấn luyện tương tự như vậy. .. raw:: html Bài tập ------- .. raw:: html 1. Trong mục này, chúng ta đã lập trình hàm softmax dựa vào định nghĩa toán học của phép toán softmax. Điều này có thể gây ra những vấn đề gì (gợi ý: thử tính :math:`\exp(50)`)? 2. Hàm ``cross_entropy`` trong mục này được lập trình dựa vào định nghĩa của hàm mất mát entropy chéo. Vấn đề gì có thể xảy ra với cách lập trình như vậy (gợi ý: xem xét miền của hàm log)? 3. Bạn có thể tìm ra giải pháp cho hai vấn đề trên không? 4. Việc trả về nhãn có khả năng nhất có phải lúc nào cũng là ý tưởng tốt không? Ví dụ, bạn có dùng phương pháp này cho chẩn đoán bệnh hay không? 5. Giả sử rằng chúng ta muốn sử dụng hồi quy softmax để dự đoán từ tiếp theo dựa vào một số đặc trưng. Những vấn đề gì có thể xảy ra nếu dùng một tập từ vựng lớn? .. raw:: html .. raw:: html .. raw:: html Thảo luận --------- - `Tiếng Anh `__ - `Tiếng Việt `__ .. raw:: html 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 - Bùi Nhật Quân - Lý Phi Long - Phạm Hồng Vinh - Lâm Ngọc Tâm - Vũ Hữu Tiệp - Lê Khắc Hồng Phúc - Nguyễn Cảnh Thướng - Phạm Minh Đức - Nguyễn Quang Hải - Đinh Minh Tân - Lê Cao Thăng