.. raw:: html
.. _sec_minibatch_sgd:
Hạ Gradient Ngẫu nhiên theo Minibatch
=====================================
.. raw:: html
Đến giờ, ta đã tiếp xúc với hai thái cực trong các phương pháp học dựa
theo gradient: tại mỗi lượt :numref:`sec_gd` sử dụng toàn bộ tập dữ
liệu để tính gradient và cập nhật tham số. Ngược lại,
:numref:`sec_sgd` xử lý từng điểm dữ liệu một để cập nhật các tham số.
Mỗi phương pháp đều có mặt hạn chế riêng. Hạ Gradient có *hiệu suất dữ
liệu* (*data efficiency*) thấp khi dữ liệu tương đối giống nhau. Hạ
Gradient Ngẫu nhiên có *hiệu suất tính toán* (*computational
efficiency*) thấp do CPU và GPU không được khai thác hết khả năng vector
hóa. Điều này gợi ý rằng có thể có một phương pháp thích hợp ở giữa, và
thực tế, ta đã sử dụng phương pháp đó trong các ví dụ đã thảo luận.
.. raw:: html
Vector hóa và Vùng nhớ đệm
--------------------------
.. raw:: html
Lý do sử dụng minibatch chủ yếu là vì hiệu suất tính toán. Để dễ hiểu,
ta xét trường hợp tính toán song song giữa nhiều GPU và giữa nhiều máy
chủ. Trong trường hợp này ta cần đưa ít nhất một ảnh vào mỗi GPU. Với 16
máy chủ và 8 GPU mỗi máy, ta có minibatch kích thước 128.
.. raw:: html
Vấn đề trở nên nhạy cảm hơn đối với GPU đơn hay ngay cả CPU đơn. Những
thiết bị này có nhiều loại bộ nhớ, thường có nhiều loại đơn vị thực hiện
tính toán và giới hạn băng thông giữa các đơn vị này cũng khác nhau. Ví
dụ, một CPU có số lượng ít thanh ghi, bộ nhớ đệm L1, L2 và trong một số
trường hợp có cả L3 (phần bộ nhớ được phân phối giữa các lõi của vi xử
lý). Các bộ nhớ đệm đang tăng dần về kích thước và độ trễ (và cùng với
đó là giảm băng thông). Nói vậy đủ thấy rằng vi xử lý có khả năng thực
hiện nhiều tác vụ hơn so với những gì mà giao diện bộ nhớ chính (*main
memory interface*) có thể cung cấp.
.. raw:: html
- Một CPU tốc độ 2GHz với 16 lõi và phép vector hóa AVX-512 có thể xử
lý lên lới :math:`2 \cdot 10^9 \cdot 16 \cdot 32 = 10^{12}` byte mỗi
giây. Khả năng của GPU dễ dàng vượt qua con số này cả trăm lần. Mặt
khác, trong vi xử lý của máy chủ cỡ trung bình, băng thông có lẽ
không vượt quá 100 GB/s, tức là chưa bằng một phần mười băng thông
yêu cầu để đưa dữ liệu vào bộ xử lý. Vấn đề còn tồi tệ hơn khi ta xét
đến việc không phải khả năng truy cập bộ nhớ nào cũng như nhau: đầu
tiên, giao diện bộ nhớ thường rộng 64 bit hoặc hơn (ví dụ như trên
GPU lên đến 384 bit), do đó việc đọc một byte duy nhất vẫn sẽ phải
chịu chi phí giống như truy cập một khoảng bộ nhớ rộng hơn.
- Tổng chi phí cho lần truy cập đầu tiên là khá lớn, trong khi truy cập
liên tiếp thường hao tổn ít (thường được gọi là đọc hàng loạt). Có
rất nhiều điều cần lưu ý, ví dụ như lưu trữ đệm khi ta có nhiều điểm
truy cập cuối (*sockets*), nhiều chiplet và các cấu trúc khác. Việc
thảo luận chi tiết vấn đề trên nằm ngoài phạm vi của phần này. Bạn có
thể tham khảo `bài viết
Wikipedia `__ này để
hiểu sâu hơn.
.. raw:: html
Cách để giảm bớt những ràng buộc trên là sử dụng hệ thống cấp bậc
(*hierarchy*) của các vùng nhớ đệm trong CPU, các vùng nhớ này đủ nhanh
để có thể cung cấp dữ liệu cho vi xử lý. Đây *chính là* động lực đằng
sau việc sử dụng batch trong học sâu. Để đơn giản, xét phép nhân hai ma
trận :math:`\mathbf{A} = \mathbf{B}\mathbf{C}`. Để tính
:math:`\mathbf{A}` ta có khá nhiều lựa chọn, ví dụ như:
.. raw:: html
1. Ta có thể tính
:math:`\mathbf{A}_{ij} = \mathbf{B}_{i,:} \mathbf{C}_{:,j}^\top`, tức
là tính từng phần tử bằng tích vô hướng.
2. Ta có thể tính
:math:`\mathbf{A}_{:,j} = \mathbf{B} \mathbf{C}_{:,j}^\top`, tức là
tính theo từng cột. Tương tự, ta có thể tính :math:`\mathbf{A}` theo
từng hàng :math:`\mathbf{A}_{i,:}`.
3. Ta đơn giản có thể tính :math:`\mathbf{A} = \mathbf{B} \mathbf{C}`.
4. Ta có thể chia :math:`\mathbf{B}` và :math:`\mathbf{C}` thành nhiều
khối ma trận nhỏ hơn và tính :math:`\mathbf{A}` theo từng khối một.
.. raw:: html
Nếu sử dụng cách đầu tiên, ta cần sao chép một vector cột và một vector
hàng vào CPU cho mỗi lần tính phần tử :math:`\mathbf{A}_{ij}`. Tệ hơn
nữa, do các phần tử của ma trận được lưu thành một dãy liên tục dưới bộ
nhớ, ta buộc phải truy cập nhiều vùng nhớ rời rạc khi đọc một trong hai
vector từ bộ nhớ. Cách thứ hai tốt hơn nhiều. Theo cách này, ta có thể
giữ vector cột :math:`\mathbf{C}_{:,j}` trong vùng nhớ đệm của CPU trong
khi ta tiếp tục quét qua :math:`B`. Cách này chỉ cần nửa băng thông cần
thiết của bộ nhớ, do đó truy cập nhanh hơn. Đương nhiên cách thứ ba là
tốt nhất. Đáng tiếc rằng đa số ma trận quá lớn để có thể đưa vào vùng
nhớ đệm (dù sao đây cũng chính là điều ta đang thảo luận). Cách thứ tư
là một phương pháp thay thế khá tốt: đưa các khối của ma trận vào vùng
nhớ đệm và thực hiện phép nhân cục bộ. Các thư viện đã được tối ưu sẽ
thực hiện việc này giúp chúng ta. Hãy xem xét hiệu suất của từng phương
pháp trong thực tế.
.. raw:: html
Ngoài hiệu suất tính toán, chi phí tính toán phát sinh đến từ Python và
framework học sâu cũng đáng cân nhắc. Mỗi lần ta thực hiện một câu lệnh,
bộ thông dịch Python gửi một câu lệnh đến MXNet để chèn câu lệnh đó vào
đồ thị tính toán và thực thi nó theo đúng lịnh trình. Chi phí đó có thể
khá bất lợi. Nói ngắn gọn, nên áp dụng vector hóa (và ma trận) bất cứ
khi nào có thể.
.. code:: python
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
timer = d2l.Timer()
A = np.zeros((256, 256))
B = np.random.normal(0, 1, (256, 256))
C = np.random.normal(0, 1, (256, 256))
.. raw:: html
Phép nhân theo từng phần tử chỉ đơn giản là duyệt qua tất cả các hàng và
cột của :math:`\mathbf{B}` và :math:`\mathbf{C}` theo thứ tự rồi gán kết
quả cho :math:`\mathbf{A}`.
.. code:: python
# Compute A = BC one element at a time
timer.start()
for i in range(256):
for j in range(256):
A[i, j] = np.dot(B[i, :], C[:, j])
A.wait_to_read()
timer.stop()
.. parsed-literal::
:class: output
45.49748992919922
.. raw:: html
Một cách nhanh hơn là nhân theo từng cột.
.. code:: python
# Compute A = BC one column at a time
timer.start()
for j in range(256):
A[:, j] = np.dot(B, C[:, j])
A.wait_to_read()
timer.stop()
.. parsed-literal::
:class: output
0.13073205947875977
.. raw:: html
Cuối cùng, cách hiệu quả nhất là thực hiện toàn bộ phép nhân trong một
khối. Hãy thử xem tốc độ tương ứng của phương pháp này là bao nhiêu.
.. code:: python
# Compute A = BC in one go
timer.start()
A = np.dot(B, C)
A.wait_to_read()
timer.stop()
# Multiply and add count as separate operations (fused in practice)
gigaflops = [2/i for i in timer.times]
print(f'performance in Gigaflops: element {gigaflops[0]:.3f}, '
f'column {gigaflops[1]:.3f}, full {gigaflops[2]:.3f}')
.. parsed-literal::
:class: output
performance in Gigaflops: element 0.044, column 15.298, full 2196.546
.. raw:: html
.. _sec_minibatches:
Minibatch
---------
.. raw:: html
Ở các phần trước ta mặc nhiên đọc dữ liệu theo *minibatch* thay vì từng
điểm dữ liệu đơn lẻ để cập nhật các tham số. Ta có thể giải thích ngắn
gọn mục đích như sau. Xử lý từng điểm dữ liệu đơn lẻ đòi hỏi phải thực
hiện rất nhiều phép nhân ma trận với vector (hay thậm chí vector với
vector). Cách này khá tốn kém và đồng thời phải chịu thêm chi phí khá
lớn đến từ các framework học sâu bên dưới. Vấn đề này xảy ra ở cả lúc
đánh giá một mạng với dữ liệu mới (thường gọi là suy luận - *inference*)
và khi tính toán gradient để cập nhật các tham số. Tức là vấn đề xảy ra
mỗi khi ta thực hiện
:math:`\mathbf{w} \leftarrow \mathbf{w} - \eta_t \mathbf{g}_t` trong đó
.. math:: \mathbf{g}_t = \partial_{\mathbf{w}} f(\mathbf{x}_{t}, \mathbf{w})
.. raw:: html
Ta có thể tăng hiệu suất *tính toán* của phép tính này bằng cách áp dụng
nó trên mỗi minibatch dữ liệu. Tức là ta thay thế gradient
:math:`\mathbf{g}_t` trên một điểm dữ liệu đơn lẻ bằng gradient trên một
batch nhỏ.
.. math:: \mathbf{g}_t = \partial_{\mathbf{w}} \frac{1}{|\mathcal{B}_t|} \sum_{i \in \mathcal{B}_t} f(\mathbf{x}_{i}, \mathbf{w})
.. raw:: html
Hãy thử xem phương pháp trên tác động thế nào đến các tính chất thống kê
của :math:`\mathbf{g}_t`: do cả :math:`\mathbf{x}_t` và tất cả các phần
tử trong minibatch :math:`\mathcal{B}_t` được lấy ra từ tập huấn luyện
với xác suất như nhau, kỳ vọng của gradient là không đổi. Mặt khác,
phương sai giảm một cách đáng kể. Do gradient của minibatch là trung
bình của :math:`b := |\mathcal{B}_t|` gradient độc lập, độ lệch chuẩn
của nó giảm đi theo hệ số :math:`b^{-\frac{1}{2}}`. Đây là một điều tốt,
cách cập nhật này có độ tin cậy gần bằng việc lấy gradient trên toàn bộ
tập dữ liệu.
.. raw:: html
Từ ý trên, ta sẽ nhanh chóng cho rằng chọn minibatch
:math:`\mathcal{B}_t` lớn luôn là tốt nhất. Tiếc rằng đến một mức độ nào
đó, độ lệch chuẩn sẽ giảm không đáng kể so với chi phí tính toán tăng
tuyến tính. Do đó trong thực tế, ta sẽ chọn kích thước minibatch đủ lớn
để hiệu suất tính toán cao trong khi vẫn đủ để đưa vào bộ nhớ của GPU.
Để minh hoạ quá trình lưu trữ này, hãy xem đoạn mã nguồn dưới đây. Trong
đó ta vẫn thực hiện phép nhân ma trận với ma trận, tuy nhiên lần này ta
tách thành từng minibatch 64 cột.
.. code:: python
timer.start()
for j in range(0, 256, 64):
A[:, j:j+64] = np.dot(B, C[:, j:j+64])
timer.stop()
print(f'performance in Gigaflops: block {2 / timer.times[3]:.3f}')
.. parsed-literal::
:class: output
performance in Gigaflops: block 639.766
.. raw:: html
Có thể thấy quá trình tính toán trên minibatch về cơ bản có hiệu suất
gần bằng thực hiện trên toàn ma trận. Tuy nhiên, cần lưu ý rằng Trong
:numref:`sec_batch_norm` ta sử dụng một loại điều chuẩn phụ thuộc chặt
chẽ vào phương sai của minibatch. khi tăng kích thước minibatch, phương
sai giảm xuống và cùng với đó là lợi ích của việc thêm nhiễu
(*noise-injection*) cũng giảm theo do phương pháp chuẩn hóa theo batch.
Đọc :cite:`Ioffe.2017` để biết chi tiết cách chuyển đổi giá trị và
tính các số hạng phù hợp.
.. raw:: html
Đọc Tập dữ liệu
---------------
.. raw:: html
Hãy xem cách tạo các minibatch từ dữ liệu một cách hiệu quả như thế nào.
Trong đoạn mã nguồn dưới ta sử dụng tập dữ liệu được phát triển bởi NASA
để kiểm tra `tiếng ồn từ các máy bay khác
nhau `__ để
so sánh các thuật toán tối ưu. Để thuận tiện ta chỉ sử dụng
:math:`1,500` ví dụ đầu tiên. Tập dữ liệu được tẩy trắng (*whitened*) để
xử lý, tức là với mỗi toạ độ ta trừ đi giá trị trung bình và chuyển đổi
giá trị phương sai về :math:`1`.
.. code:: python
#@save
d2l.DATA_HUB['airfoil'] = (d2l.DATA_URL + 'airfoil_self_noise.dat',
'76e5be1548fd8222e5074cf0faae75edff8cf93f')
#@save
def get_data_ch11(batch_size=10, n=1500):
data = np.genfromtxt(d2l.download('airfoil'),
dtype=np.float32, delimiter='\t')
data = (data - data.mean(axis=0)) / data.std(axis=0)
data_iter = d2l.load_array(
(data[:n, :-1], data[:n, -1]), batch_size, is_train=True)
return data_iter, data.shape[1]-1
.. raw:: html
Lập trình từ đầu
----------------
.. raw:: html
Hãy nhớ lại cách lập trình SGD theo minibatch trong
:numref:`sec_linear_scratch`. Trong phần tiếp theo, chúng tôi sẽ trình
bày cách lập trình tổng quát hơn một chút. Để thuận tiện, hàm lập trình
SGD và các thuật toán tối ưu khác được giới thiệu sau trong chương này
sẽ có danh sách tham số giống nhau. Cụ thể, chúng ta thêm trạng thái đầu
vào ``states`` và đặt siêu tham số trong từ điển ``hyperparams``. Bên
cạnh đó, chúng ta sẽ tính giá trị mất mát trung bình của từng minibatch
trong hàm huấn luyện, từ đó không cần phải chia gradient cho kích thước
batch trong thuật toán tối ưu nữa.
.. code:: python
def sgd(params, states, hyperparams):
for p in params:
p[:] -= hyperparams['lr'] * p.grad
.. raw:: html
Tiếp theo, chúng ta lập trình một hàm huấn luyện tổng quát, sử dụng được
cho cả các thuật toán tối ưu khác được giới thiệu sau trong chương này.
Hàm sẽ khởi tạo một mô hình hồi quy tuyến tính và có thể được sử dụng để
huấn luyện mô hình với SGD theo minibatch và các thuật toán khác.
.. code:: python
#@save
def train_ch11(trainer_fn, states, hyperparams, data_iter,
feature_dim, num_epochs=2):
# Initialization
w = np.random.normal(scale=0.01, size=(feature_dim, 1))
b = np.zeros(1)
w.attach_grad()
b.attach_grad()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
# Train
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[0, num_epochs], ylim=[0.22, 0.35])
n, timer = 0, d2l.Timer()
for _ in range(num_epochs):
for X, y in data_iter:
with autograd.record():
l = loss(net(X), y).mean()
l.backward()
trainer_fn([w, b], states, hyperparams)
n += X.shape[0]
if n % 200 == 0:
timer.stop()
animator.add(n/X.shape[0]/len(data_iter),
(d2l.evaluate_loss(net, data_iter, loss),))
timer.start()
print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch')
return timer.cumsum(), animator.Y[0]
.. raw:: html
Hãy cùng quan sát quá trình tối ưu của thuật toán hạ gradient theo toàn
bộ batch. Ta có thể sử dụng toàn bộ batch bằng cách thiết lập kích thước
minibatch bằng 1500 (chính là tổng số mẫu). Kết quả là các tham số mô
hình chỉ được cập nhật một lần duy nhất trong mỗi epoch. Có thể thấy
không có tiến triển đáng kể nào, sau 6 epoch việc tối ưu bị ngừng trệ.
.. code:: python
def train_sgd(lr, batch_size, num_epochs=2):
data_iter, feature_dim = get_data_ch11(batch_size)
return train_ch11(
sgd, None, {'lr': lr}, data_iter, feature_dim, num_epochs)
gd_res = train_sgd(1, 1500, 10)
.. parsed-literal::
:class: output
loss: 0.254, 0.058 sec/epoch
.. figure:: output_minibatch-sgd_vn_79a398_17_1.svg
.. raw:: html
Khi kích thước batch bằng 1, chúng ta sử dụng thuật toán SGD để tối ưu.
Để đơn giản hóa việc lập trình, chúng ta cố định tốc độ học bằng một
hằng số (có giá trị nhỏ). Trong SGD, các tham số mô hình được cập nhật
bất cứ khi nào một mẫu huấn luyện được xử lý. Trong trường hợp này, sẽ
có 1500 lần cập nhật trong mỗi epoch. Có thể thấy, sự suy giảm giá trị
của hàm mục tiêu chậm lại sau một epoch. Mặc dù cả hai thuật toán cùng
xử lý 1500 mẫu trong một epoch, SGD tốn thời gian hơn hạ gradient trong
thí nghiệm trên. Điều này là do SGD cập nhật các tham số thường xuyên
hơn và kém hiệu quả khi xử lý đơn lẻ từng mẫu.
.. code:: python
sgd_res = train_sgd(0.005, 1)
.. parsed-literal::
:class: output
loss: 0.243, 0.257 sec/epoch
.. figure:: output_minibatch-sgd_vn_79a398_19_1.svg
.. raw:: html
Cuối cùng, khi kích thước batch bằng 100, chúng ta sử dụng thuật toán
SGD theo minibatch để tối ưu. Thời gian cần thiết cho mỗi epoch ngắn hơn
thời gian tương ứng của SGD và hạ gradient theo toàn bộ batch.
.. code:: python
mini1_res = train_sgd(.4, 100)
.. parsed-literal::
:class: output
loss: 0.247, 0.006 sec/epoch
.. figure:: output_minibatch-sgd_vn_79a398_21_1.svg
.. raw:: html
Giảm kích thước batch bằng 10, thời gian cho mỗi epoch tăng vì thực thi
tính toán trên mỗi batch kém hiệu quả hơn.
.. code:: python
mini2_res = train_sgd(.05, 10)
.. parsed-literal::
:class: output
loss: 0.247, 0.030 sec/epoch
.. figure:: output_minibatch-sgd_vn_79a398_23_1.svg
.. raw:: html
Cuối cùng, chúng ta so sánh tương quan thời gian và giá trị hàm mất mát
trong bốn thí nghiệm trên. Có thể thấy, dù hội tụ nhanh hơn GD về số mẫu
được xử lý, nhưng SGD tốn nhiều thời gian hơn để đạt được cùng giá trị
mất mát như GD vì thuật toán này tính toán gradient trên từng mẫu một.
Thuật toán SGD theo minibatch có thể cân bằng giữa tốc độ hội tụ và hiệu
quả tính toán. Với kích thước minibatch bằng 10, thuật toán này hiệu quả
hơn SGD; và với kích thước minibatch bằng 100, thời gian chạy của thuật
toán này thậm chí nhanh hơn cả GD.
.. code:: python
d2l.set_figsize([6, 3])
d2l.plot(*list(map(list, zip(gd_res, sgd_res, mini1_res, mini2_res))),
'time (sec)', 'loss', xlim=[1e-2, 10],
legend=['gd', 'sgd', 'batch size=100', 'batch size=10'])
d2l.plt.gca().set_xscale('log')
.. figure:: output_minibatch-sgd_vn_79a398_25_0.svg
.. raw:: html
Lập trình Súc tích
------------------
.. raw:: html
Trong Gluon, chúng ta có thể sử dụng lớp ``Trainer`` để gọi các thuật
toán tối ưu. Cách này được sử dụng để có thể lập trình một hàm huấn
luyện tổng quát. Chúng ta sẽ sử dụng hàm này xuyên suốt các phần tiếp
theo của chương.
.. code:: python
#@save
def train_concise_ch11(tr_name, hyperparams, data_iter, num_epochs=2):
# Initialization
net = nn.Sequential()
net.add(nn.Dense(1))
net.initialize(init.Normal(sigma=0.01))
trainer = gluon.Trainer(net.collect_params(), tr_name, hyperparams)
loss = gluon.loss.L2Loss()
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[0, num_epochs], ylim=[0.22, 0.35])
n, timer = 0, d2l.Timer()
for _ in range(num_epochs):
for X, y in data_iter:
with autograd.record():
l = loss(net(X), y)
l.backward()
trainer.step(X.shape[0])
n += X.shape[0]
if n % 200 == 0:
timer.stop()
animator.add(n/X.shape[0]/len(data_iter),
(d2l.evaluate_loss(net, data_iter, loss),))
timer.start()
print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch')
.. raw:: html
Lặp lại thí nghiệm với kích thước batch bằng 10 sử dụng Gluon cho kết
quả tương tự như trên.
.. code:: python
data_iter, _ = get_data_ch11(10)
train_concise_ch11('sgd', {'learning_rate': 0.05}, data_iter)
.. parsed-literal::
:class: output
loss: 0.245, 0.028 sec/epoch
.. figure:: output_minibatch-sgd_vn_79a398_29_1.svg
Tóm tắt
-------
.. raw:: html
- Vector hóa tính toán sẽ giúp mã nguồn hiệu quả hơn vì nó giảm chi phí
phát sinh từ framework học sâu và tận dụng tính cục bộ của bộ nhớ và
vùng nhớ đệm trên CPU và GPU tốt hơn.
- Tồn tại sự đánh đổi giữa hiệu quả về mặt thống kê của SGD và hiệu quả
tính toán của việc xử lý các batch dữ liệu kích thước lớn cùng một
lúc.
- Thuật toán SGD theo minibatch kết hợp cả hai lợi ích trên: hiệu quả
tính toán và thống kê.
- Trong thuật toán đó ta xử lý các batch thu được từ hóan vị ngẫu nhiên
của dữ liệu huấn luyện (cụ thể, mỗi mẫu được xử lý chỉ một lần mỗi
epoch theo thứ tự ngẫu nhiên).
- Suy giảm tốc độ học trong quá trình huấn luyện được khuyến khích sử
dụng.
- Nhìn chung, SGD theo minibatch nhanh hơn SGD và hạ gradient về thời
gian hội tụ.
Bài tập
-------
.. raw:: html
1. Sửa đổi kích thước batch và tốc độ học, quan sát tốc độ suy giảm giá
trị của hàm mục tiêu và thời gian cho mỗi epoch.
2. Đọc thêm tài liệu MXNet và sử dụng hàm ``set_learning_rate`` của lớp
``Trainer`` để giảm tốc độ học của SGD theo minibatch bằng 1/10 giá
trị trước đó sau mỗi epoch.
3. Hãy so sánh SGD theo minibatch sử dụng một biến thể *lấy mẫu có hoàn
lại* từ tập huấn luyện. Điều gì sẽ xảy ra?
4. Một ác thần đã sao chép tập dữ liệu của bạn mà không nói cho bạn biết
(cụ thể, mỗi quan sát bị lặp lại hai lần và kích thước tập dữ liệu
tăng gấp đôi so với ban đầu). Cách hoạt động của các thuật toán hạ
gradient, SGD và SGD theo minibatch sẽ thay đổi như thế nào?
Thảo luận
---------
- `Tiếng Anh - MXNet `__
- `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ường Giang
- Nguyễn Văn Quang
- Phạm Minh Đức
- Lê Khắc Hồng Phúc
- Phạm Hồng Vinh
- Nguyễn Văn Cường