.. _sec_hybridize:
Trình biên dịch và phiên dịch
=============================
Cho đến nay, cuốn sách này đã tập trung vào lập trình bắt buộc, làm cho
việc sử dụng các tuyên bố như ``print``, ``+``, và ``if`` để thay đổi
trạng thái của một chương trình. Hãy xem xét ví dụ sau đây của một
chương trình bắt buộc đơn giản.
.. raw:: html
.. raw:: html
.. code:: python
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
.. parsed-literal::
:class: output
10
.. raw:: html
.. raw:: html
.. code:: python
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
.. parsed-literal::
:class: output
10
.. raw:: html
.. raw:: html
.. code:: python
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
.. parsed-literal::
:class: output
10
.. raw:: html
.. raw:: html
Python là một ngôn ngữ\ *được giải thị*. Khi đánh giá hàm ``fancy_func``
trên, nó thực hiện các thao tác tạo nên cơ thể của hàm \* theo trình tự
\*. Đó là, nó sẽ đánh giá ``e = add(a, b)`` và lưu trữ các kết quả dưới
dạng biến ``e``, do đó thay đổi trạng thái của chương trình. Hai câu
lệnh tiếp theo ``f = add(c, d)`` và ``g = add(e, f)`` sẽ được thực thi
tương tự, thực hiện bổ sung và lưu trữ các kết quả dưới dạng biến.
:numref:`fig_compute_graph` minh họa luồng dữ liệu.
.. _fig_compute_graph:
.. figure:: ../img/computegraph.svg
Data flow in an imperative program.
Mặc dù lập trình bắt buộc là thuận tiện, nó có thể không hiệu quả. Một
mặt, ngay cả khi hàm ``add`` được gọi nhiều lần trong suốt
``fancy_func``, Python sẽ thực hiện ba cuộc gọi hàm riêng lẻ. Nếu chúng
được thực thi, giả sử, trên GPU (hoặc thậm chí trên nhiều GPU), chi phí
phát sinh từ trình thông dịch Python có thể trở nên áp đảo. Hơn nữa, nó
sẽ cần phải lưu các giá trị biến của ``e`` và ``f`` cho đến khi tất cả
các câu lệnh trong ``fancy_func`` đã được thực thi. Điều này là do chúng
ta không biết liệu các biến ``e`` và ``f`` sẽ được sử dụng bởi các phần
khác của chương trình sau khi các câu lệnh ``e = add(a, b)`` và
``f = add(c, d)`` được thực thi.
Lập trình tượng trưng
---------------------
Hãy xem xét phương pháp thay thế, \*lập trình tượng trưng, trong đó tính
toán thường chỉ được thực hiện một khi quá trình đã được xác định đầy
đủ. Chiến lược này được sử dụng bởi nhiều khuôn khổ học sâu, bao gồm
Theano và TensorFlow (sau này đã có được các phần mở rộng bắt buộc). Nó
thường bao gồm các bước sau:
1. Xác định các hoạt động sẽ được thực thi.
2. Biên dịch các hoạt động thành một chương trình thực thi.
3. Cung cấp các đầu vào cần thiết và gọi chương trình được biên dịch để
thực hiện.
Điều này cho phép tối ưu hóa một lượng đáng kể. Đầu tiên, chúng ta có
thể bỏ qua trình thông dịch Python trong nhiều trường hợp, do đó loại bỏ
một nút cổ chai hiệu suất có thể trở nên đáng kể trên nhiều GPU nhanh
được ghép nối với một luồng Python duy nhất trên CPU. Thứ hai, một trình
biên dịch có thể tối ưu hóa và viết lại mã trên vào
``print((1 + 2) + (3 + 4))`` hoặc thậm chí ``print(10)``. Điều này là có
thể kể từ khi một trình biên dịch được để xem mã đầy đủ trước khi biến
nó thành hướng dẫn máy. Ví dụ, nó có thể giải phóng bộ nhớ (hoặc không
bao giờ phân bổ nó) bất cứ khi nào một biến không còn cần thiết nữa.
Hoặc nó có thể biến đổi mã hoàn toàn thành một phần tương đương. Để có
được một ý tưởng tốt hơn, hãy xem xét mô phỏng sau đây của lập trình bắt
buộc (nó là Python sau tất cả) dưới đây.
.. raw:: html
.. raw:: html
.. code:: python
def add_():
return '''
def add(a, b):
return a + b
'''
def fancy_func_():
return '''
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
'''
def evoke_():
return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))'
prog = evoke_()
print(prog)
y = compile(prog, '', 'exec')
exec(y)
.. parsed-literal::
:class: output
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
10
.. raw:: html
.. raw:: html
.. code:: python
def add_():
return '''
def add(a, b):
return a + b
'''
def fancy_func_():
return '''
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
'''
def evoke_():
return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))'
prog = evoke_()
print(prog)
y = compile(prog, '', 'exec')
exec(y)
.. parsed-literal::
:class: output
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
10
.. raw:: html
.. raw:: html
.. code:: python
def add_():
return '''
def add(a, b):
return a + b
'''
def fancy_func_():
return '''
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
'''
def evoke_():
return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))'
prog = evoke_()
print(prog)
y = compile(prog, '', 'exec')
exec(y)
.. parsed-literal::
:class: output
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
10
.. raw:: html
.. raw:: html
Sự khác biệt giữa lập trình bắt buộc (giải thích) và lập trình tượng
trưng như sau:
- Lập trình bắt buộc dễ dàng hơn. Khi lập trình bắt buộc được sử dụng
trong Python, phần lớn mã là đơn giản và dễ viết. Nó cũng dễ dàng hơn
để gỡ lỗi mã lập trình bắt buộc. Điều này là do dễ dàng hơn để có
được và in tất cả các giá trị biến trung gian có liên quan hoặc sử
dụng các công cụ gỡ lỗi tích hợp sẵn của Python.
- Lập trình tượng trưng hiệu quả hơn và dễ dàng hơn để cổng. Lập trình
tượng trưng giúp tối ưu hóa mã trong quá trình biên dịch dễ dàng hơn,
đồng thời có khả năng chuyển chương trình thành một định dạng độc lập
với Python. Điều này cho phép chương trình được chạy trong môi trường
không phải Python, do đó tránh mọi vấn đề hiệu suất tiềm ẩn liên quan
đến trình thông dịch Python.
Lập trình lai
-------------
Trong lịch sử hầu hết các khuôn khổ học sâu lựa chọn giữa một cách tiếp
cận bắt buộc hoặc mang tính biểu tượng. Ví dụ, Theano, TensorFlow (lấy
cảm hứng từ trước đây), Keras và CNTK xây dựng các mô hình tượng trưng.
Ngược lại, Chainer và PyTorch có một cách tiếp cận bắt buộc. Một chế độ
bắt buộc đã được thêm vào TensorFlow 2.0 và Keras trong các phiên bản
sau này.
.. raw:: html
.. raw:: html
Khi thiết kế Gluon, các nhà phát triển đã xem xét liệu có thể kết hợp
những lợi ích của cả hai mô hình lập trình hay không. Điều này dẫn đến
một mô hình lai cho phép người dùng phát triển và gỡ lỗi với lập trình
bắt buộc thuần túy, trong khi có khả năng chuyển đổi hầu hết các chương
trình thành các chương trình tượng trưng để chạy khi hiệu suất tính toán
cấp sản phẩm và triển khai được yêu cầu.
Trong thực tế, điều này có nghĩa là chúng tôi xây dựng các mô hình bằng
cách sử dụng lớp ``HybridBlock`` hoặc ``HybridSequential``. Theo mặc
định, một trong số chúng được thực thi theo cùng một cách lớp ``Block``
hoặc ``Sequential`` được thực thi trong lập trình bắt buộc. Lớp
``HybridSequential`` là một lớp con của ``HybridBlock`` (giống như
``Sequential`` lớp con ``Block``). Khi hàm ``hybridize`` được gọi, Gluon
biên dịch mô hình thành dạng được sử dụng trong lập trình tượng trưng.
Điều này cho phép người ta tối ưu hóa các thành phần chuyên sâu tính
toán mà không phải hy sinh theo cách thực hiện mô hình. Chúng tôi sẽ
minh họa những lợi ích dưới đây, tập trung vào các mô hình và khối tuần
tự.
.. raw:: html
.. raw:: html
Như đã đề cập ở trên, PyTorch dựa trên lập trình bắt buộc và sử dụng đồ
thị tính toán động. Trong nỗ lực tận dụng tính di động và hiệu quả của
lập trình tượng trưng, các nhà phát triển đã xem xét liệu có thể kết hợp
lợi ích của cả hai mô hình lập trình hay không. Điều này dẫn đến một
torchscript cho phép người dùng phát triển và gỡ lỗi bằng cách sử dụng
lập trình bắt buộc thuần túy, trong khi có khả năng chuyển đổi hầu hết
các chương trình thành các chương trình tượng trưng để chạy khi hiệu
suất và triển khai tính toán cấp sản phẩm được yêu cầu.
.. raw:: html
.. raw:: html
Mô hình lập trình bắt buộc bây giờ là mặc định trong Tensorflow 2, một
thay đổi chào đón cho những người mới đến ngôn ngữ. Tuy nhiên, các kỹ
thuật lập trình tượng trưng tương tự và đồ thị tính toán tiếp theo vẫn
tồn tại trong TensorFlow, và có thể được truy cập bởi trình trang trí
``tf.function`` dễ sử dụng. Điều này đã mang lại mô hình lập trình bắt
buộc cho TensorFlow, cho phép người dùng xác định các hàm trực quan hơn,
sau đó gói chúng và biên dịch chúng thành đồ thị tính toán tự động bằng
cách sử dụng một tính năng mà nhóm TensorFlow đề cập đến là
`autograph `__.
.. raw:: html
.. raw:: html
Lai tạo lớp ``Sequential``
--------------------------
Cách dễ nhất để có được cảm nhận về cách thức hoạt động của lai là xem
xét các mạng sâu với nhiều lớp. Thông thường, trình thông dịch Python sẽ
cần thực thi mã cho tất cả các lớp để tạo ra một lệnh sau đó có thể được
chuyển tiếp đến CPU hoặc GPU. Đối với một thiết bị điện toán (nhanh) duy
nhất, điều này không gây ra bất kỳ vấn đề lớn nào. Mặt khác, nếu chúng
ta sử dụng một máy chủ 8 GPU tiên tiến như một phiên bản AWS
P3dn.24xlarge Python sẽ phải vật lộn để giữ cho tất cả các GPU bận rộn.
Trình thông dịch Python đơn luồng trở thành nút cổ chai ở đây. Hãy để
chúng tôi xem cách chúng tôi có thể giải quyết vấn đề này cho các phần
quan trọng của mã bằng cách thay thế ``Sequential`` bằng
``HybridSequential``. Chúng tôi bắt đầu bằng cách xác định một MLP đơn
giản.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
# Factory for networks
def get_net():
net = nn.HybridSequential()
net.add(nn.Dense(256, activation='relu'),
nn.Dense(128, activation='relu'),
nn.Dense(2))
net.initialize()
return net
x = np.random.normal(size=(1, 512))
net = get_net()
net(x)
.. parsed-literal::
:class: output
array([[ 0.16526186, -0.14005628]])
Bằng cách gọi hàm ``hybridize``, chúng ta có thể biên dịch và tối ưu hóa
tính toán trong MLP. Kết quả tính toán của mô hình vẫn không thay đổi.
.. code:: python
net.hybridize()
net(x)
.. parsed-literal::
:class: output
array([[ 0.16526186, -0.14005628]])
Điều này có vẻ gần như quá tốt để đúng: chỉ cần chỉ định một khối là
``HybridSequential``, viết cùng một mã như trước và gọi ``hybridize``.
Khi điều này xảy ra, mạng được tối ưu hóa (chúng tôi sẽ chuẩn hóa hiệu
suất bên dưới). Thật không may, điều này không hoạt động kỳ diệu cho mỗi
lớp. Điều đó nói rằng, một layer sẽ không được tối ưu hóa nếu nó kế thừa
từ lớp ``Block`` thay vì lớp ``HybridBlock``.
.. raw:: html
.. raw:: html
.. code:: python
import torch
from torch import nn
from d2l import torch as d2l
# Factory for networks
def get_net():
net = nn.Sequential(nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 2))
return net
x = torch.randn(size=(1, 512))
net = get_net()
net(x)
.. parsed-literal::
:class: output
tensor([[ 0.0854, -0.0074]], grad_fn=
)
Bằng cách chuyển đổi mô hình bằng hàm ``torch.jit.script``, chúng ta có
thể biên dịch và tối ưu hóa tính toán trong MLP. Kết quả tính toán của
mô hình vẫn không thay đổi.
.. code:: python
net = torch.jit.script(net)
net(x)
.. parsed-literal::
:class: output
tensor([[ 0.0854, -0.0074]], grad_fn=)
Điều này có vẻ gần như quá tốt là đúng: viết cùng một mã như trước và
chỉ cần chuyển đổi mô hình bằng ``torch.jit.script``. Khi điều này xảy
ra, mạng được tối ưu hóa (chúng tôi sẽ chuẩn hóa hiệu suất bên dưới).
.. raw:: html
.. raw:: html
.. code:: python
import tensorflow as tf
from tensorflow.keras.layers import Dense
from d2l import tensorflow as d2l
# Factory for networks
def get_net():
net = tf.keras.Sequential()
net.add(Dense(256, input_shape = (512,), activation = "relu"))
net.add(Dense(128, activation = "relu"))
net.add(Dense(2, activation = "linear"))
return net
x = tf.random.normal([1,512])
net = get_net()
net(x)
.. parsed-literal::
:class: output
Trước đây, tất cả các chức năng được xây dựng trong TensorFlow được xây
dựng như một biểu đồ tính toán, và do đó JIT biên dịch theo mặc định.
Tuy nhiên, với việc phát hành TensorFlow 2.X và EagerTensor, đây không
còn là behavor mặc định. Chúng ta cen kích hoạt lại chức năng này với
tf.function. tf.function thường được sử dụng như một trình trang trí
hàm, tuy nhiên có thể gọi nó trực tiếp như một hàm python bình thường,
được hiển thị bên dưới. Kết quả tính toán của mô hình vẫn không thay
đổi.
.. code:: python
net = tf.function(net)
net(x)
.. parsed-literal::
:class: output
Điều này có vẻ gần như quá tốt để đúng: viết cùng một mã như trước và
chỉ cần chuyển đổi mô hình bằng ``tf.function``. Khi điều này xảy ra,
mạng được xây dựng như một biểu đồ tính toán trong đại diện trung gian
MLIR của TensorFlow và được tối ưu hóa rất nhiều ở cấp trình biên dịch
để thực hiện nhanh chóng (chúng tôi sẽ đánh dấu hiệu suất bên dưới).
Thêm cờ ``jit_compile = True`` một cách rõ ràng vào cuộc gọi
``tf.function()`` cho phép XLA (Accelerated Linear Algebra) chức năng
trong TensorFlow. XLA có thể tối ưu hóa hơn nữa mã được biên dịch JIT
trong một số trường hợp nhất định. Thực thi chế độ đồ thị được bật mà
không có định nghĩa rõ ràng này, tuy nhiên XLA có thể thực hiện một số
hoạt động đại số tuyến tính lớn nhất định (trong tĩnh mạch của những
người chúng ta thấy trong các ứng dụng học sâu) nhanh hơn nhiều, đặc
biệt là trong môi trường GPU.
.. raw:: html
.. raw:: html
Tăng tốc bằng cách lai
~~~~~~~~~~~~~~~~~~~~~~
Để chứng minh sự cải thiện hiệu suất đạt được bằng cách biên dịch, chúng
tôi so sánh thời gian cần thiết để đánh giá ``net(x)`` trước và sau khi
lai. Hãy để chúng tôi xác định một lớp để đo lần này đầu tiên. Nó sẽ có
ích trong suốt chương khi chúng tôi đặt ra để đo lường (và cải thiện)
hiệu suất.
.. raw:: html
.. raw:: html
.. code:: python
#@save
class Benchmark:
"""For measuring running time."""
def __init__(self, description='Done'):
self.description = description
def __enter__(self):
self.timer = d2l.Timer()
return self
def __exit__(self, *args):
print(f'{self.description}: {self.timer.stop():.4f} sec')
Bây giờ chúng ta có thể gọi mạng hai lần, một lần với và một lần mà
không cần lai tạo.
.. code:: python
net = get_net()
with Benchmark('Without hybridization'):
for i in range(1000): net(x)
npx.waitall()
net.hybridize()
with Benchmark('With hybridization'):
for i in range(1000): net(x)
npx.waitall()
.. parsed-literal::
:class: output
Without hybridization: 0.7786 sec
With hybridization: 0.2446 sec
Như đã quan sát thấy trong các kết quả trên, sau khi một phiên bản
``HybridSequential`` gọi hàm ``hybridize``, hiệu suất tính toán được cải
thiện thông qua việc sử dụng lập trình tượng trưng.
.. raw:: html
.. raw:: html
.. code:: python
#@save
class Benchmark:
"""For measuring running time."""
def __init__(self, description='Done'):
self.description = description
def __enter__(self):
self.timer = d2l.Timer()
return self
def __exit__(self, *args):
print(f'{self.description}: {self.timer.stop():.4f} sec')
Bây giờ chúng ta có thể gọi mạng hai lần, một lần với và một lần không
có torchscript.
.. code:: python
net = get_net()
with Benchmark('Without torchscript'):
for i in range(1000): net(x)
net = torch.jit.script(net)
with Benchmark('With torchscript'):
for i in range(1000): net(x)
.. parsed-literal::
:class: output
Without torchscript: 2.1717 sec
With torchscript: 2.1708 sec
Như đã quan sát thấy trong các kết quả trên, sau khi một phiên bản
``nn.Sequential`` được viết kịch bản bằng hàm ``torch.jit.script``, hiệu
suất tính toán được cải thiện thông qua việc sử dụng lập trình tượng
trưng.
.. raw:: html
.. raw:: html
.. code:: python
#@save
class Benchmark:
"""For measuring running time."""
def __init__(self, description='Done'):
self.description = description
def __enter__(self):
self.timer = d2l.Timer()
return self
def __exit__(self, *args):
print(f'{self.description}: {self.timer.stop():.4f} sec')
Bây giờ chúng ta có thể gọi mạng ba lần, một lần được thực hiện háo hức,
một lần với thực thi chế độ đồ thị, và một lần nữa sử dụng JIT biên dịch
XLA.
.. code:: python
net = get_net()
with Benchmark('Eager Mode'):
for i in range(1000): net(x)
net = tf.function(net)
with Benchmark('Graph Mode'):
for i in range(1000): net(x)
.. parsed-literal::
:class: output
Eager Mode: 1.4461 sec
Graph Mode: 0.4433 sec
Như đã quan sát thấy trong các kết quả trên, sau khi một phiên bản
``tf.keras.Sequential`` được viết kịch bản bằng hàm ``tf.function``,
hiệu suất tính toán được cải thiện thông qua việc sử dụng lập trình
tượng trưng thông qua thực thi chế độ đồ thị trong tensorflow.
.. raw:: html
.. raw:: html
Lập số sê-ri
~~~~~~~~~~~~
.. raw:: html
.. raw:: html
Một trong những lợi ích của việc biên dịch các mô hình là chúng ta có
thể serialize (lưu) mô hình và các tham số của nó vào đĩa. Điều này cho
phép chúng tôi lưu trữ một mô hình theo cách độc lập với ngôn ngữ
front-end của sự lựa chọn. Điều này cho phép chúng tôi triển khai các mô
hình được đào tạo cho các thiết bị khác và dễ dàng sử dụng các ngôn ngữ
lập trình front-end khác. Đồng thời mã thường nhanh hơn những gì có thể
đạt được trong lập trình bắt buộc. Chúng ta hãy xem chức năng ``export``
đang hoạt động.
.. code:: python
net.export('my_mlp')
!ls -lh my_mlp*
.. parsed-literal::
:class: output
-rw-rw-r-- 1 d2l-worker d2l-worker 643K Apr 26 10:53 my_mlp-0000.params
-rw-rw-r-- 1 d2l-worker d2l-worker 3.0K Apr 26 10:53 my_mlp-symbol.json
Mô hình được phân hủy thành một tệp tham số (nhị phân lớn) và mô tả JSON
của chương trình cần thiết để thực thi tính toán mô hình. Các tập tin có
thể được đọc bởi các ngôn ngữ front-end khác được hỗ trợ bởi Python hoặc
MXNet, chẳng hạn như C ++, R, Scala, và Perl. Chúng ta hãy xem xét một
vài dòng đầu tiên trong mô tả mô hình.
.. code:: python
!head my_mlp-symbol.json
.. parsed-literal::
:class: output
{
"nodes": [
{
"op": "null",
"name": "data",
"inputs": []
},
{
"op": "null",
"name": "dense3_weight",
Trước đó, chúng tôi đã chứng minh rằng, sau khi gọi hàm ``hybridize``,
mô hình này có thể đạt được hiệu suất tính toán và tính di động vượt
trội. Lưu ý, mặc dù việc lai tạo có thể ảnh hưởng đến tính linh hoạt của
mô hình, đặc biệt là về dòng chảy điều khiển.
Bên cạnh đó, trái với phiên bản ``Block``, cần sử dụng hàm ``forward``,
đối với một ví dụ ``HybridBlock`` chúng ta cần sử dụng hàm
``hybrid_forward``.
.. code:: python
class HybridNet(nn.HybridBlock):
def __init__(self, **kwargs):
super(HybridNet, self).__init__(**kwargs)
self.hidden = nn.Dense(4)
self.output = nn.Dense(2)
def hybrid_forward(self, F, x):
print('module F: ', F)
print('value x: ', x)
x = F.npx.relu(self.hidden(x))
print('result : ', x)
return self.output(x)
Mã trên thực hiện một mạng đơn giản với 4 đơn vị ẩn và 2 đầu ra. Hàm
``hybrid_forward`` mất một đối số bổ sung ``F``. Điều này là cần thiết
vì, tùy thuộc vào việc mã đã được lai hay không, nó sẽ sử dụng một thư
viện hơi khác (``ndarray`` hoặc ``symbol``) để xử lý. Cả hai lớp đều
thực hiện các hàm rất giống nhau và MXNet tự động xác định đối số. Để
hiểu những gì đang xảy ra, chúng ta in các đối số như một phần của lệnh
gọi hàm.
.. code:: python
net = HybridNet()
net.initialize()
x = np.random.normal(size=(1, 3))
net(x)
.. parsed-literal::
:class: output
module F:
value x: [[-0.6338663 0.40156594 0.46456942]]
result : [[0.01641375 0. 0. 0. ]]
.. parsed-literal::
:class: output
array([[0.00097611, 0.00019453]])
Lặp lại tính toán chuyển tiếp sẽ dẫn đến cùng một đầu ra (chúng tôi bỏ
qua chi tiết). Bây giờ chúng ta hãy xem những gì sẽ xảy ra nếu chúng ta
gọi hàm ``hybridize``.
.. code:: python
net.hybridize()
net(x)
.. parsed-literal::
:class: output
module F:
value x: <_Symbol data>
result : <_Symbol hybridnet0_relu0>
.. parsed-literal::
:class: output
array([[0.00097611, 0.00019453]])
Thay vì sử dụng ``ndarray``, bây giờ chúng tôi sử dụng mô-đun ``symbol``
cho ``F``. Hơn nữa, mặc dù đầu vào là loại ``ndarray``, dữ liệu chảy qua
mạng hiện được chuyển đổi thành loại ``symbol`` như một phần của quá
trình biên dịch. Lặp lại cuộc gọi hàm dẫn đến một kết quả đáng ngạc
nhiên:
.. code:: python
net(x)
.. parsed-literal::
:class: output
array([[0.00097611, 0.00019453]])
Điều này khá khác với những gì chúng ta đã thấy trước đây. Tất cả các
câu lệnh in, như được định nghĩa trong ``hybrid_forward``, đều bị bỏ
qua. Thật vậy, sau khi lai, việc thực hiện ``net(x)`` không liên quan
đến thông dịch viên Python nữa. Điều này có nghĩa là bất kỳ mã Python
giả nào đều bị bỏ qua (chẳng hạn như các câu lệnh in) có lợi cho việc
thực thi hợp lý hơn nhiều và hiệu suất tốt hơn. Thay vào đó, MXNet trực
tiếp gọi phụ trợ C ++. Cũng lưu ý rằng một số chức năng không được hỗ
trợ trong mô-đun ``symbol`` (ví dụ, ``asnumpy``) và các hoạt động tại
chỗ như ``a += b`` và ``a[:] = a + b`` phải được viết lại là
``a = a + b``. Tuy nhiên, việc tổng hợp các mô hình đáng để nỗ lực bất
cứ khi nào tốc độ quan trọng. Lợi ích có thể dao động từ các điểm phần
trăm nhỏ đến hơn gấp đôi tốc độ, tùy thuộc vào độ phức tạp của mô hình,
tốc độ của CPU và tốc độ và số lượng GPU.
.. raw:: html
.. raw:: html
Một trong những lợi ích của việc biên dịch các mô hình là chúng ta có
thể serialize (lưu) mô hình và các tham số của nó vào đĩa. Điều này cho
phép chúng tôi lưu trữ một mô hình theo cách độc lập với ngôn ngữ
front-end của sự lựa chọn. Điều này cho phép chúng tôi triển khai các mô
hình được đào tạo cho các thiết bị khác và dễ dàng sử dụng các ngôn ngữ
lập trình front-end khác. Đồng thời mã thường nhanh hơn những gì có thể
đạt được trong lập trình bắt buộc. Chúng ta hãy xem chức năng ``save``
đang hoạt động.
.. code:: python
net.save('my_mlp')
!ls -lh my_mlp*
.. parsed-literal::
:class: output
-rw-rw-r-- 1 d2l-worker d2l-worker 651K Apr 26 12:30 my_mlp
.. raw:: html
.. raw:: html
Một trong những lợi ích của việc biên dịch các mô hình là chúng ta có
thể serialize (lưu) mô hình và các tham số của nó vào đĩa. Điều này cho
phép chúng tôi lưu trữ một mô hình theo cách độc lập với ngôn ngữ
front-end của sự lựa chọn. Điều này cho phép chúng tôi triển khai các mô
hình được đào tạo cho các thiết bị khác và dễ dàng sử dụng các ngôn ngữ
lập trình front-end khác hoặc thực hiện một mô hình được đào tạo trên
máy chủ. Đồng thời mã thường nhanh hơn những gì có thể đạt được trong
lập trình bắt buộc. API cấp thấp cho phép chúng ta lưu trong tensorflow
là ``tf.saved_model``. Hãy xem phiên bản ``saved_model`` đang hoạt động.
.. code:: python
net = get_net()
tf.saved_model.save(net, 'my_mlp')
!ls -lh my_mlp*
.. parsed-literal::
:class: output
INFO:tensorflow:Assets written to: my_mlp/assets
total 68K
drwxr-xr-x 2 d2l-worker d2l-worker 4.0K Apr 26 14:22 assets
-rw-rw-r-- 1 d2l-worker d2l-worker 60K Apr 26 14:22 saved_model.pb
drwxr-xr-x 2 d2l-worker d2l-worker 4.0K Apr 26 14:22 variables
.. raw:: html
.. raw:: html
Tóm tắt
-------
- Lập trình bắt buộc giúp bạn dễ dàng thiết kế các mô hình mới vì có
thể viết mã với luồng điều khiển và khả năng sử dụng một lượng lớn hệ
sinh thái phần mềm Python.
- Lập trình tượng trưng yêu cầu chúng ta chỉ định chương trình và biên
dịch nó trước khi thực hiện nó. Lợi ích là cải thiện hiệu suất.
.. raw:: html
.. raw:: html
- MXNet có thể kết hợp những lợi thế của cả hai cách tiếp cận khi cần
thiết.
- Các mô hình được xây dựng bởi các lớp ``HybridSequential`` và
``HybridBlock`` có thể chuyển đổi các chương trình bắt buộc thành các
chương trình tượng trưng bằng cách gọi hàm ``hybridize``.
.. raw:: html
.. raw:: html
Bài tập
-------
.. raw:: html
.. raw:: html
1. Thêm ``x.asnumpy()`` vào dòng đầu tiên của hàm ``hybrid_forward`` của
lớp ``HybridNet`` trong phần này. Thực hiện mã và quan sát các lỗi
bạn gặp phải. Tại sao họ lại xảy ra?
2. Điều gì xảy ra nếu chúng ta thêm luồng điều khiển, tức là, các câu
lệnh Python ``if`` và ``for`` trong hàm ``hybrid_forward``?
3. Xem lại các mô hình mà bạn quan tâm trong các chương trước. Bạn có
thể cải thiện hiệu suất tính toán của họ bằng cách triển khai lại
chúng không?
`Discussions `__
.. raw:: html
.. raw:: html
1. Xem lại các mô hình mà bạn quan tâm trong các chương trước. Bạn có
thể cải thiện hiệu suất tính toán của họ bằng cách triển khai lại
chúng không?
`Discussions `__
.. raw:: html
.. raw:: html
1. Xem lại các mô hình mà bạn quan tâm trong các chương trước. Bạn có
thể cải thiện hiệu suất tính toán của họ bằng cách triển khai lại
chúng không?
`Discussions `__
.. raw:: html
.. raw:: html