.. raw:: html .. raw:: html .. raw:: html Quản lý Tham số =============== .. raw:: html Một khi ta đã chọn được kiến trúc mạng và các giá trị siêu tham số, ta sẽ bắt đầu với vòng lặp huấn luyện với mục tiêu là tìm các giá trị tham số để cực tiểu hóa hàm mục tiêu. Sau khi huấn luyện xong, ta sẽ cần các tham số đó để đưa ra dự đoán trong tương lai. Hơn nữa, thi thoảng ta sẽ muốn trích xuất tham số để sử dụng lại trong một hoàn cảnh khác, có thể lưu trữ mô hình để thực thi trong một phần mềm khác hoặc để rút ra hiểu biết khoa học bằng việc phân tích mô hình. .. raw:: html Thông thường, ta có thể bỏ qua những chi tiết chuyên sâu về việc khai báo và xử lý tham số bởi Gluon sẽ đảm nhiệm công việc nặng nhọc này. Tuy nhiên, khi ta bắt đầu tiến xa hơn những kiến trúc chỉ gồm các tầng tiêu chuẩn được xếp chồng lên nhau, đôi khi ta sẽ phải tự đi sâu vào việc khai báo và xử lý tham số. Trong mục này, chúng tôi sẽ đề cập đến những việc sau: .. raw:: html - Truy cập các tham số để gỡ lỗi, chẩn đoán mô hình và biểu diễn trực quan. - Khởi tạo tham số. - Chia sẻ tham số giữa các thành phần khác nhau của mô hình. .. raw:: html Chúng ta sẽ bắt đầu từ mạng Perceptron đa tầng với một tầng ẩn. .. code:: python from mxnet import init, np, npx from mxnet.gluon import nn npx.set_np() net = nn.Sequential() net.add(nn.Dense(256, activation='relu')) net.add(nn.Dense(10)) net.initialize() # Use the default initialization method x = np.random.uniform(size=(2, 20)) net(x) # Forward computation .. parsed-literal:: :class: output array([[ 0.06240272, -0.03268593, 0.02582653, 0.02254182, -0.03728798, -0.04253786, 0.00540613, -0.01364186, -0.09915452, -0.02272738], [ 0.02816677, -0.03341204, 0.03565666, 0.02506382, -0.04136416, -0.04941845, 0.01738528, 0.01081961, -0.09932579, -0.01176298]]) .. raw:: html Truy cập Tham số ---------------- .. raw:: html Hãy bắt đầu với việc truy cập tham số của những mô hình mà bạn đã biết. Khi một mô hình được định nghĩa bằng lớp Tuần tự (*Sequential*), ta có thể truy cập bất kỳ tầng nào bằng chỉ số, như thể nó là một danh sách. Thuộc tính ``params`` của mỗi tầng chứa tham số của chúng. Ta có thể quan sát các tham số của mạng ``net`` định nghĩa ở trên. .. code:: python print(net[0].params) print(net[1].params) .. parsed-literal:: :class: output dense0_ ( Parameter dense0_weight (shape=(256, 20), dtype=float32) Parameter dense0_bias (shape=(256,), dtype=float32) ) dense1_ ( Parameter dense1_weight (shape=(10, 256), dtype=float32) Parameter dense1_bias (shape=(10,), dtype=float32) ) .. raw:: html Kết quả của đoạn mã này cho ta một vài thông tin quan trọng. Đầu tiên, mỗi tầng kết nối đầy đủ đều có hai tập tham số, ví dụ như ``dense0_weight`` và ``dense0_bias`` tương ứng với trọng số và hệ số điều chỉnh của tầng đó. Chúng đều được lưu trữ ở dạng số thực dấu phẩy động độ chính xác đơn. Lưu ý rằng tên của các tham số cho phép ta xác định tham số của từng tầng *một cách độc nhất*, kể cả khi mạng nơ-ron chứa hàng trăm tầng. .. raw:: html .. raw:: html Các tham số Mục tiêu ~~~~~~~~~~~~~~~~~~~~ .. raw:: html Lưu ý rằng mỗi tham số được biểu diễn bằng một thực thể của lớp ``Parameter``. Để làm việc với các tham số, trước hết ta phải truy cập được các giá trị số của chúng. Có một vài cách để làm việc này, một số cách đơn giản hơn trong khi các cách khác lại tổng quát hơn. Để bắt đầu, ta có thể truy cập tham số của một tầng thông qua thuộc tính ``bias`` hoặc ``weight`` rồi sau đó truy cập giá trị số của chúng thông qua phương thức ``data()``. Đoạn mã sau trích xuất hệ số điều chỉnh của tầng thứ hai trong mạng nơ-ron. .. code:: python print(net[1].bias) print(net[1].bias.data()) .. parsed-literal:: :class: output Parameter dense1_bias (shape=(10,), dtype=float32) [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] .. raw:: html Tham số là các đối tượng khá phức tạp bởi chúng chứa dữ liệu, gradient và một vài thông tin khác. Đó là lý do tại sao ta cần yêu cầu dữ liệu một cách tường minh. Lưu ý rằng vector hệ số điều chỉnh chứa các giá trị không vì ta chưa hề cập nhật mô hình kể từ khi nó được khởi tạo. Ta cũng có thể truy cập các tham số theo tên của chúng, chẳng hạn như ``dense0_weight`` ở dưới. Điều này khả thi vì thực ra mỗi tầng đều chứa một từ điển tham số. .. code:: python print(net[0].params['dense0_weight']) print(net[0].params['dense0_weight'].data()) .. parsed-literal:: :class: output Parameter dense0_weight (shape=(256, 20), dtype=float32) [[ 0.06700657 -0.00369488 0.0418822 ... -0.05517294 -0.01194733 -0.00369594] [-0.03296221 -0.04391347 0.03839272 ... 0.05636378 0.02545484 -0.007007 ] [-0.0196689 0.01582889 -0.00881553 ... 0.01509629 -0.01908049 -0.02449339] ... [-0.02055008 -0.02618652 0.06762936 ... -0.02315108 -0.06794678 -0.04618235] [ 0.02802853 0.06672969 0.05018687 ... -0.02206502 -0.01315478 -0.03791244] [-0.00638592 0.00914261 0.06667828 ... -0.00800052 0.03406764 -0.03954004]] .. raw:: html Chú ý rằng khác với hệ số điều chỉnh, trọng số chứa các giá trị khác không bởi chúng được khởi tạo ngẫu nhiên. Ngoài ``data``, mỗi ``Parameter`` còn cung cấp phương thức ``grad()`` để truy cập gradient. Gradient sẽ có cùng kích thước với trọng số. Vì ta chưa thực hiện lan truyền ngược với mạng nơ-ron này, các giá trị của gradient đều là 0. .. code:: python net[0].weight.grad() .. parsed-literal:: :class: output array([[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]) .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Tất cả các Tham số cùng lúc ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. raw:: html Khi ta cần phải thực hiện các phép toán với tất cả tham số, việc truy cập lần lượt từng tham số sẽ trở nên khá khó chịu. Việc này sẽ càng chậm chạp khi ta làm việc với các khối phức tạp hơn, ví dụ như các khối lồng nhau vì lúc đó ta sẽ phải duyệt toàn bộ cây bằng đệ quy để có thể trích xuất tham số của từng khối con. Để tránh vấn đề này, mỗi khối có thêm một phương thức ``collect_params`` để trả về một từ điển duy nhất chứa tất cả tham số. Ta có thể gọi ``collect_params`` với một tầng duy nhất hoặc với toàn bộ mạng nơ-ron như sau: .. code:: python # parameters only for the first layer print(net[0].collect_params()) # parameters of the entire network print(net.collect_params()) .. parsed-literal:: :class: output dense0_ ( Parameter dense0_weight (shape=(256, 20), dtype=float32) Parameter dense0_bias (shape=(256,), dtype=float32) ) sequential0_ ( Parameter dense0_weight (shape=(256, 20), dtype=float32) Parameter dense0_bias (shape=(256,), dtype=float32) Parameter dense1_weight (shape=(10, 256), dtype=float32) Parameter dense1_bias (shape=(10,), dtype=float32) ) .. raw:: html Từ đó, ta có cách thứ ba để truy cập các tham số của mạng: .. code:: python net.collect_params()['dense1_bias'].data() .. parsed-literal:: :class: output array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) .. raw:: html Xuyên suốt cuốn sách này ta sẽ thấy các khối đặt tên cho khối con theo nhiều cách khác nhau. Khối Sequential chỉ đơn thuần đánh số chúng. Ta có thể tận dụng quy ước định danh này cùng với một tính năng thông minh của ``collect_params`` để lọc ra các tham số được trả về bằng các biểu thức chính quy (*regular expression*). .. code:: python print(net.collect_params('.*weight')) print(net.collect_params('dense0.*')) .. parsed-literal:: :class: output sequential0_ ( Parameter dense0_weight (shape=(256, 20), dtype=float32) Parameter dense1_weight (shape=(10, 256), dtype=float32) ) sequential0_ ( Parameter dense0_weight (shape=(256, 20), dtype=float32) Parameter dense0_bias (shape=(256,), dtype=float32) ) .. raw:: html Thu thập Tham số từ các Khối lồng nhau ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. raw:: html Hãy cùng xem cách hoạt động của các quy ước định danh tham số khi ta lồng nhiều khối vào nhau. Trước hết ta định nghĩa một hàm tạo khối (có thể gọi là một nhà máy khối) và rồi kết hợp chúng trong các khối lớn hơn. .. code:: python def block1(): net = nn.Sequential() net.add(nn.Dense(32, activation='relu')) net.add(nn.Dense(16, activation='relu')) return net def block2(): net = nn.Sequential() for i in range(4): net.add(block1()) return net rgnet = nn.Sequential() rgnet.add(block2()) rgnet.add(nn.Dense(10)) rgnet.initialize() rgnet(x) .. parsed-literal:: :class: output array([[-4.1923025e-09, 1.9830502e-09, 8.9444063e-10, 6.2912990e-09, -3.3241778e-09, 5.4330038e-09, 1.6013515e-09, -3.7408681e-09, 8.5468477e-09, -6.4805539e-09], [-3.7507064e-09, 1.4866974e-09, 6.8314709e-10, 5.6925784e-09, -2.6349172e-09, 4.8626667e-09, 1.4280275e-09, -3.4603027e-09, 7.4127922e-09, -5.7896132e-09]]) .. raw:: html Bây giờ ta đã xong phần thiết kế mạng, hãy cùng xem cách nó được tổ chức. Hãy để ý ở dưới rằng dù hàm ``collect_params()`` trả về một danh sách các tham số được định danh, việc gọi ``collect_params`` như một thuộc tính sẽ cho ta biết cấu trúc của mạng. .. code:: python print(rgnet.collect_params) print(rgnet.collect_params()) .. parsed-literal:: :class: output 32, Activation(relu)) (1): Dense(32 -> 16, Activation(relu)) ) (1): Sequential( (0): Dense(16 -> 32, Activation(relu)) (1): Dense(32 -> 16, Activation(relu)) ) (2): Sequential( (0): Dense(16 -> 32, Activation(relu)) (1): Dense(32 -> 16, Activation(relu)) ) (3): Sequential( (0): Dense(16 -> 32, Activation(relu)) (1): Dense(32 -> 16, Activation(relu)) ) ) (1): Dense(16 -> 10, linear) )> sequential1_ ( Parameter dense2_weight (shape=(32, 20), dtype=float32) Parameter dense2_bias (shape=(32,), dtype=float32) Parameter dense3_weight (shape=(16, 32), dtype=float32) Parameter dense3_bias (shape=(16,), dtype=float32) Parameter dense4_weight (shape=(32, 16), dtype=float32) Parameter dense4_bias (shape=(32,), dtype=float32) Parameter dense5_weight (shape=(16, 32), dtype=float32) Parameter dense5_bias (shape=(16,), dtype=float32) Parameter dense6_weight (shape=(32, 16), dtype=float32) Parameter dense6_bias (shape=(32,), dtype=float32) Parameter dense7_weight (shape=(16, 32), dtype=float32) Parameter dense7_bias (shape=(16,), dtype=float32) Parameter dense8_weight (shape=(32, 16), dtype=float32) Parameter dense8_bias (shape=(32,), dtype=float32) Parameter dense9_weight (shape=(16, 32), dtype=float32) Parameter dense9_bias (shape=(16,), dtype=float32) Parameter dense10_weight (shape=(10, 16), dtype=float32) Parameter dense10_bias (shape=(10,), dtype=float32) ) .. raw:: html Bởi vì các tầng được lồng vào nhau theo cơ chế phân cấp, ta cũng có thể truy cập chúng tương tự như cách ta dùng chỉ số để truy cập các danh sách lồng nhau. Chẳng hạn, ta có thể truy cập khối chính đầu tiên, khối con thứ hai bên trong nó và hệ số điều chỉnh của tầng đầu tiên bên trong nữa như sau: .. code:: python rgnet[0][1][0].bias.data() .. parsed-literal:: :class: output array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) .. raw:: html .. raw:: html .. raw:: html Khởi tạo Tham số ---------------- .. raw:: html Bây giờ khi đã biết cách truy cập tham số, hãy cùng xem xét việc khởi tạo chúng đúng cách. Ta đã thảo luận về sự cần thiết của việc khởi tạo tham số trong :numref:`sec_numerical_stability`. Theo mặc định, MXNet khởi tạo các ma trận trọng số bằng cách lấy mẫu từ phân phối đều :math:`U[-0,07, 0,07]` và đặt tất cả các hệ số điều chỉnh bằng :math:`0`. Tuy nhiên, thường ta sẽ muốn khởi tạo trọng số theo nhiều phương pháp khác. Mô-đun ``init`` của MXNet cung cấp sẵn nhiều phương thức khởi tạo. Nếu ta muốn một bộ khởi tạo tùy chỉnh, ta sẽ cần làm thêm một chút việc. .. raw:: html Phương thức Khởi tạo có sẵn ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. raw:: html Ta sẽ bắt đầu với việc gọi các bộ khởi tạo có sẵn. Đoạn mã dưới đây khởi tạo tất cả các tham số với các biến ngẫu nhiên Gauss có độ lệch chuẩn bằng :math:`0.01`. .. code:: python # force_reinit ensures that variables are freshly initialized # even if they were already initialized previously net.initialize(init=init.Normal(sigma=0.01), force_reinit=True) net[0].weight.data()[0] .. parsed-literal:: :class: output array([-9.8788980e-03, 5.3957910e-03, -7.0842835e-03, -7.4317548e-03, -1.4880489e-02, 6.4959107e-03, -8.2659349e-03, 1.8743129e-02, 1.6201857e-02, 1.4534278e-03, 2.2331164e-03, 1.5926110e-02, -1.2915777e-02, -8.8592555e-05, -1.7293986e-03, -7.2338698e-03, 8.7698260e-03, -4.9947016e-03, -9.6906107e-03, 2.0079101e-03]) .. raw:: html Ta cũng có thể khởi tạo tất cả tham số với một hằng số (ví dụ như :math:`1`) bằng cách sử dụng bộ khởi tạo ``Constant(1)``. .. code:: python net.initialize(init=init.Constant(1), force_reinit=True) net[0].weight.data()[0] .. parsed-literal:: :class: output array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) .. raw:: html Ta còn có thể áp dụng các bộ khởi tạo khác nhau cho các khối khác nhau. Ví dụ, trong đoạn mã nguồn bên dưới, ta khởi tạo tầng đầu tiên bằng cách sử dụng bộ khởi tạo ``Xavier`` và khởi tạo tầng thứ hai với một hằng số là 42. .. code:: python net[0].weight.initialize(init=init.Xavier(), force_reinit=True) net[1].initialize(init=init.Constant(42), force_reinit=True) print(net[1].weight.data()[0, 0]) .. parsed-literal:: :class: output 42.0 .. raw:: html .. raw:: html .. raw:: html Phương thức Khởi tạo Tùy chỉnh ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. raw:: html Đôi khi, các phương thức khởi tạo mà ta cần không có sẵn trong mô-đun ``init``. Trong trường hợp đó, ta có thể khai báo một lớp con của lớp ``Initializer``. Thông thường, ta chỉ cần lập trình hàm ``_init_weight`` để nhận một đối số ``ndarray`` (``data``) và gán giá trị khởi tạo mong muốn cho nó. Trong ví dụ bên dưới, ta sẽ khai báo một bộ khởi tạo cho phân phối kì lạ sau: .. math:: \begin{aligned} w \sim \begin{cases} U[5, 10] & \text{ với xác suất } \frac{1}{4} \\ 0 & \text{ với xác suất } \frac{1}{2} \\ U[-10, -5] & \text{ với xác suất } \frac{1}{4} \end{cases} \end{aligned} .. code:: python class MyInit(init.Initializer): def _init_weight(self, name, data): print('Init', name, data.shape) data[:] = np.random.uniform(-10, 10, data.shape) data *= np.abs(data) >= 5 net.initialize(MyInit(), force_reinit=True) net[0].weight.data()[0] .. parsed-literal:: :class: output Init dense0_weight (256, 20) Init dense1_weight (10, 256) .. parsed-literal:: :class: output array([-5.172625 , -7.0209026, 5.1446533, -9.844563 , 8.545956 , -0. , 0. , -0. , 5.107664 , 9.658335 , 5.8564453, 7.4483128, 0. , 0. , -0. , 7.9034443, 0. , 5.4223766, 8.5655575, 5.1224785]) .. raw:: html Lưu ý rằng ta luôn có thể trực tiếp đặt giá trị cho tham số bằng cách gọi hàm ``data()`` để truy cập ``ndarray`` của tham số đó. Một lưu ý khác cho người dùng nâng cao: nếu muốn điều chỉnh các tham số trong phạm vi của ``autograd``, bạn cần sử dụng hàm ``set_data`` để tránh làm rối loạn cơ chế tính vi phân tự động. .. code:: python net[0].weight.data()[:] += 1 net[0].weight.data()[0, 0] = 42 net[0].weight.data()[0] .. parsed-literal:: :class: output array([42. , -6.0209026, 6.1446533, -8.844563 , 9.545956 , 1. , 1. , 1. , 6.107664 , 10.658335 , 6.8564453, 8.448313 , 1. , 1. , 1. , 8.903444 , 1. , 6.4223766, 9.5655575, 6.1224785]) .. raw:: html .. raw:: html .. raw:: html Các Tham số bị Trói buộc ------------------------ .. raw:: html Thông thường, ta sẽ muốn chia sẻ các tham số mô hình cho nhiều tầng. Sau này ta sẽ thấy trong quá trình huấn luyện embedding từ, việc sử dụng cùng một bộ tham số để mã hóa và giải mã các từ có thể khá hợp lý. Ta đã thảo luận về một trường hợp như vậy trong :numref:`sec_model_construction`. Hãy cùng xem làm thế nào để thực hiện việc này một cách tinh tế hơn. Sau đây ta sẽ tạo một tầng kết nối đầy đủ và sử dụng chính tham số của nó làm tham số cho một tầng khác. .. code:: python net = nn.Sequential() # We need to give the shared layer a name such that we can reference its # parameters shared = nn.Dense(8, activation='relu') net.add(nn.Dense(8, activation='relu'), shared, nn.Dense(8, activation='relu', params=shared.params), nn.Dense(10)) net.initialize() x = np.random.uniform(size=(2, 20)) net(x) # Check whether the parameters are the same print(net[1].weight.data()[0] == net[2].weight.data()[0]) net[1].weight.data()[0, 0] = 100 # Make sure that they are actually the same object rather than just having the # same value print(net[1].weight.data()[0] == net[2].weight.data()[0]) .. parsed-literal:: :class: output [ True True True True True True True True] [ True True True True True True True True] .. raw:: html Ví dụ này cho thấy các tham số của tầng thứ hai và thứ ba đã bị trói buộc với nhau. Chúng không chỉ có giá trị bằng nhau, chúng còn được biểu diễn bởi cùng một ``ndarray``. Vì vậy, nếu ta thay đổi các tham số của tầng này này thì các tham số của tầng kia cũng sẽ thay đổi theo. Bạn có thể tự hỏi rằng *chuyện gì sẽ xảy ra với gradient khi các tham số bị trói buộc?*. Vì các tham số mô hình chứa gradient nên gradient của tầng ẩn thứ hai và tầng ẩn thứ ba được cộng lại tại ``shared.params.grad( )`` trong quá trình lan truyền ngược. .. raw:: html Tóm tắt ------- .. raw:: html - Ta có vài cách để truy cập, khởi tạo và trói buộc các tham số mô hình. - Ta có thể sử dụng các phương thức khởi tạo tùy chỉnh. - Gluon có một cơ chế tinh vi để truy cập các tham số theo phân cấp một cách độc nhất. .. raw:: html Bài tập ------- .. raw:: html 1. Sử dụng FancyMLP được định nghĩa trong :numref:`sec_model_construction` và truy cập tham số của các tầng khác nhau. 2. Xem `tài liệu MXNet `__ và nghiên cứu các bộ khởi tạo khác nhau. 3. Thử truy cập các tham số mô hình sau khi gọi ``net.initialize()`` và trước khi gọi ``net(x)`` và quan sát kích thước của chúng. Điều gì đã thay đổi? Tại sao? 4. Xây dựng và huấn luyện một perceptron đa tầng mà trong đó có một tầng sử dụng tham số được chia sẻ. Trong quá trình huấn luyện, hãy quan sát các tham số mô hình và gradient của từng tầng. 5. Tại sao việc chia sẻ tham số lại là là một ý tưởng hay? .. 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 - Nguyễn Văn Cường - Lê Khắc Hồng Phúc - Lê Cao Thăng - Nguyễn Duy Du - Phạm Hồng Vinh - Phạm Minh Đức