In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Tính năng eager execution của Tensorflow là một môi trường imperative programming cho phép thực hiện các phép tính toán ngay lập tức mà không cần xây dựng đồ thị: các phép tính toán trả về giá trị cụ thể thay vì xây dựng một đồ thị tính toán để thực hiện sau. Tính chất này giúp cho việc bắt đầu với Tensorflow và debug mô hình dễ dàng hơn, và nó cũng giúp giảm thiểu việc phải sử dụng boilerplate. Để theo dõi chỉ dẫn này, hãy thử chạy các đoạn code ở dưới trong một môi trường tương tác python
.
Eager execution là một nền tảng học máy linh hoạt dành cho nghiên cứu và thí nghiệm bằng việc cung cấp:
Eager execution hỗ trợ hầu hết các phép tính toán và tăng tốc GPU của Tensorflow.
Chú ý: Vài mô hình có thể gặp hiện tượng tính toán nhiều hơn cần thiết khi dùng eager execution. Các cải thiện về hiệu suất đang được thực hiện, nhưng hãy báo lỗi nếu như bạn tìm thấy một vấn đề và chia sẻ hệ thống kiểm chuẩn của bạn.
In [ ]:
import os
import tensorflow as tf
import cProfile
Trong Tensorflow 2.0, eager execution mặc định được bật.
In [ ]:
tf.executing_eagerly()
Bây giờ bạn có thể chạy các phép tính toán của Tensorflow và kết quả sẽ được trả về ngay lập tức:
In [ ]:
x = [[2.]]
m = tf.matmul(x, x)
print("hello, {}".format(m))
Việc bật eager execution thay đổi biểu hiện của các phép tính của TensorFlow - bây giờ chúng được tính và kết quả được trả về cho Python. Các đối tượng tf.Tensor
tham chiếu tới các giá trị cụ thể thay vì tới các handle mang tính biểu tượng trỏ tới các đỉnh trong đồ thị tính toán. Vì không tồn tại một đồ thị tính toán để xây dựng và chạy trong một session, kiểm tra kết quả bằng print()
hoặc một công cụ debug trở nên dễ dàng. Tính toán, in và kiểm trả giá trị của tensor không phá vỡ luồng tính toán đạo hàm.
Eager execution hoạt động rất tốt với NumPy. Các phép tính của Numpy đều nhận tham số dưới dạng tf.Tensor
. Các phép tính thuộc tf.math
trong TensorFlow chuyển đổi các đối tượng Python và mảng Numpy thành đối tượng tf.Tensor
. Phương thức tf.Tensor.numpy
trả về kết quả của object dưới dạng Numpy ndarray
.
In [ ]:
a = tf.constant([[1, 2],
[3, 4]])
print(a)
In [ ]:
# Broadcasting support
b = tf.add(a, 1)
print(b)
In [ ]:
# Overloading phép tính được hỗ trợ
print(a * b)
In [ ]:
# Sử dụng giá trị Numpy
import numpy as np
c = np.multiply(a, b)
print(c)
In [ ]:
# Lấy giá trị numpy từ một Tensor
print(a.numpy())
# => [[1 2]
# [3 4]]
Một lợi ích rất lớn của eager execution là tất cả tính năng của ngôn ngữ đang sử dụng đều có sẵn khi mô hình của bạn đang chạy. Ví dụ, rất dễ để viết fizzbuzz:
In [ ]:
def fizzbuzz(max_num):
counter = tf.constant(0)
max_num = tf.convert_to_tensor(max_num)
for num in range(1, max_num.numpy()+1):
num = tf.constant(num)
if int(num % 3) == 0 and int(num % 5) == 0:
print('FizzBuzz')
elif int(num % 3) == 0:
print('Fizz')
elif int(num % 5) == 0:
print('Buzz')
else:
print(num.numpy())
counter += 1
In [ ]:
fizzbuzz(15)
Đoạn code này có những điều kiện phụ thuộc vào giá trị của tensor và nó sẽ in những giá trị này vào lúc chạy.
Automatic differentiation rất hữu dụng cho việc cài đặt các thuật toán học máy như truyền ngược cho việc huấn luyện các mạng thần kinh. Trong lúc thực hiện eager execution, sử dụng tf.GradientTape
để theo dõi các phép tính cho việc tính toán đạo hàm sau đó.
Bạn có thể dùng tf.GradientTape
để huấn luyện và/hoặc tính toán đạo hàm trong eager. Nó đặc biệt hữu dụng cho các vòng lặp huấn luyện phức tạp.
Bởi vì các phép tính khác nhau có thể xuất hiện trong mỗi lần gọi, tất cả các phép tính truyền xuôi đều được lưu lại trong một "đoạn băng". Để tính đạo hàm, chạy đoạn băng đó ngược lại và sau đó hủy nó. Một đối tượng tf.GradientTape
chỉ có thể tính một đạo hàm; những lần gọi sau đó sẽ tạo ra runtime error.
In [ ]:
w = tf.Variable([[1.0]])
with tf.GradientTape() as tape:
loss = w * w
grad = tape.gradient(loss, w)
print(grad) # => tf.Tensor([[ 2.]], shape=(1, 1), dtype=float32)
Ví dụ sau đây tạo ra một mô hình nhiều layer, mô hình này có thể phân loại các chữ cái viết tay trong dataset MNIST tiêu chuẩn. Nó hướng dẫn trình tối ưu hóa và các layer APIs cách xây dựng một mô hình có thể huấn luyện được trong một môi trường có eager execution.
In [ ]:
# Lấy và format lại dữ liệu MNIST
(mnist_images, mnist_labels), _ = tf.keras.datasets.mnist.load_data()
dataset = tf.data.Dataset.from_tensor_slices(
(tf.cast(mnist_images[...,tf.newaxis]/255, tf.float32),
tf.cast(mnist_labels,tf.int64)))
dataset = dataset.shuffle(1000).batch(32)
In [ ]:
# Xây dựng mô hình
mnist_model = tf.keras.Sequential([
tf.keras.layers.Conv2D(16,[3,3], activation='relu',
input_shape=(None, None, 1)),
tf.keras.layers.Conv2D(16,[3,3], activation='relu'),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(10)
])
Kể cả khi không huấn luyện, bạn vẫn có thể gọi mô hình và kiểm tra đầu ra khi đang ở trạng thái eager execution.
In [ ]:
for images,labels in dataset.take(1):
print("Logits: ", mnist_model(images[0:1]).numpy())
Mặc dù các mô hình keras có sẵn vòng lặp huấn luyện (bằng cách dùng phương thức fit
), đôi lúc bạn cần nhiều khả năng chỉnh sửa hơn. Đây là ví dụ của một vòng lặp huấn luyện được xây dựng với eager:
In [ ]:
optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
loss_history = []
Chú ý: Sử dụng các hàm assert trong tf.debugging
để kiểm tra một điều kiện có được thỏa mãn hay không. Việc này có thể làm được cả trong eager và graph execution.
In [ ]:
def train_step(images, labels):
with tf.GradientTape() as tape:
logits = mnist_model(images, training=True)
# Thêm assert để kiểm tra hình dáng của output
tf.debugging.assert_equal(logits.shape, (32, 10))
loss_value = loss_object(labels, logits)
loss_history.append(loss_value.numpy().mean())
grads = tape.gradient(loss_value, mnist_model.trainable_variables)
optimizer.apply_gradients(zip(grads, mnist_model.trainable_variables))
In [ ]:
def train(epochs):
for epoch in range(epochs):
for (batch, (images, labels)) in enumerate(dataset):
train_step(images, labels)
print ('Epoch {} finished'.format(epoch))
In [ ]:
train(epochs = 3)
In [ ]:
import matplotlib.pyplot as plt
plt.plot(loss_history)
plt.xlabel('Batch #')
plt.ylabel('Loss [entropy]')
Các đối tượng tf.Variable
lưu trữ những đối tượng tf.Tensor
có thể thay đổi - những giá trị được truy xuất trong quá trình huấn luyện để làm cho quá trình tính toán đạo hàm tự động dễ dàng hơn.
Các nhóm biến có thể được đóng gói thành các layers hoặc các mô hình, cùng với những phương thức tính toán chúng. Xem Các layers và mô hình keras tùy chỉnh nếu bạn muốn tìm hiểu thêm. SỰ khác biệt chính giữa layers và mô hình là mô hình có thêm các phương thức như Model.fit
, Model.evaluate
và Model.save
.
Ví dụ về tính toán đạo hàm tự động ở trên có thể được viết lại như sau:
In [ ]:
class Linear(tf.keras.Model):
def __init__(self):
super(Linear, self).__init__()
self.W = tf.Variable(5., name='weight')
self.B = tf.Variable(10., name='bias')
def call(self, inputs):
return inputs * self.W + self.B
In [ ]:
# Một dataset nhỏ được xây dựng xung quanh hàm số 3 * x + 2
NUM_EXAMPLES = 2000
training_inputs = tf.random.normal([NUM_EXAMPLES])
noise = tf.random.normal([NUM_EXAMPLES])
training_outputs = training_inputs * 3 + 2 + noise
# The loss function to be optimized
def loss(model, inputs, targets):
error = model(inputs) - targets
return tf.reduce_mean(tf.square(error))
def grad(model, inputs, targets):
with tf.GradientTape() as tape:
loss_value = loss(model, inputs, targets)
return tape.gradient(loss_value, [model.W, model.B])
Tiếp theo:
In [ ]:
model = Linear()
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)
print("Initial loss: {:.3f}".format(loss(model, training_inputs, training_outputs)))
steps = 300
for i in range(steps):
grads = grad(model, training_inputs, training_outputs)
optimizer.apply_gradients(zip(grads, [model.W, model.B]))
if i % 20 == 0:
print("Loss at step {:03d}: {:.3f}".format(i, loss(model, training_inputs, training_outputs)))
In [ ]:
print("Final loss: {:.3f}".format(loss(model, training_inputs, training_outputs)))
In [ ]:
print("W = {}, B = {}".format(model.W.numpy(), model.B.numpy()))
Chú ý: các biến sẽ còn tồn tại cho đến khi tham chiếu cuối cùng tới đối tượng Python được xóa và biến được phá hủy.
Một mô hình tf.keras.Model
có kèm theo một phương thức save_weights
giúp bạn có thể dễ dàng tạo ra một checkpoint:
In [ ]:
model.save_weights('weights')
status = model.load_weights('weights')
Sử dụng tf.train.Checkpoint
giúp bạn có thể nắm hoàn toàn quyền làm chủ quá trình này.
Phần này của bài viết là một phiên bản ngắn gọn của hướng dẫn huấn luyện bằng checkpoints.
In [ ]:
x = tf.Variable(10.)
checkpoint = tf.train.Checkpoint(x=x)
In [ ]:
x.assign(2.) # Gán một giá trị mới cho biến và lưu.
checkpoint_path = './ckpt/'
checkpoint.save('./ckpt/')
In [ ]:
x.assign(11.) # Thay đổi giá trị của biến sau khi lưu.
# Hồi phục lại giá trị từ checkpoint
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_path))
print(x) # => 2.0
Để lưu và tải lại các mô hình, tf.train.Checkpoint
lưu trữ trạng thái bên trong của các đối tượng mà không cần đến các biến ẩn. Để lưu lại trạng thái của một model
, một optimizer
, và một bước toàn cục, truyền chúng cho một biến tf.train.Checkpoint
:
In [ ]:
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(16,[3,3], activation='relu'),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(10)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
checkpoint_dir = 'path/to/model_dir'
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
root = tf.train.Checkpoint(optimizer=optimizer,
model=model)
root.save(checkpoint_prefix)
root.restore(tf.train.latest_checkpoint(checkpoint_dir))
Lưu ý: Trong nhiều vòng lặp huấn luyện, các biến được tạo sau khi tf.train.Checkpoint.restore
được gọi. Những biến này sẽ được lưu trữ ngay khi chúng được tạo, và assertions có thể được sử dụng để đảm bảo rằng một checkpoint đã được tải hoàn toàn. Hãy xem hướng dẫn huấn luyện checkpoints để xem chi tiết.
In [ ]:
m = tf.keras.metrics.Mean("loss")
m(0)
m(5)
m.result() # => 2.5
m([8, 9])
m.result() # => 5.5
TensorBoard là một công cụ trực quan hóa dùng để hiểu, debug và tối ưu quá trình huấn luyện mô hình. Nó sử dụng các sự kiện tổng quan được viết trong lúc chương trình đang thực hiện.
Bạn có thể sử dụng tf.summary
để lưu lại tổng quan của các biến trong eager execution. Ví dụ: để lưu lại tổng quan của loss
một lần trong mỗi 100 bước huấn luyện
In [ ]:
logdir = "./tb/"
writer = tf.summary.create_file_writer(logdir)
steps = 1000
with writer.as_default(): # hoặc gọi writer.set_as_default() trước vòng lặp
for i in range(steps):
step = i + 1
# Tình giá trị hàm mất mát với các hàm huấn luyện của bạn
loss = 1 - 0.001 * step
if step % 100 == 0:
tf.summary.scalar('loss', loss, step=step)
In [ ]:
!ls tb/
tf.GradientTape
cũng có thể được sử dụng trong các mô hình động. Ví dụ sau đây cho thuật toán tìm kiếm đường thẳng sử dụng quay lui trong giống như code Numpy bình thường, ngoại trừ sự xuất hiện của gradient và việc ta có thể tính đạo hàm được mặc dù có cấu trúc điều khiển phức tạp:
In [ ]:
def line_search_step(fn, init_x, rate=1.0):
with tf.GradientTape() as tape:
# Các biến số được theo dõi tự động.
# Để tính gradient của một tensor, ta cần theo dõi (watch) nó.
tape.watch(init_x)
value = fn(init_x)
grad = tape.gradient(value, init_x)
grad_norm = tf.reduce_sum(grad * grad)
init_value = value
while value > init_value - rate * grad_norm:
x = init_x - rate * grad
value = fn(x)
rate /= 2.0
return x, value
In [ ]:
@tf.custom_gradient
def clip_gradient_by_norm(x, norm):
y = tf.identity(x)
def grad_fn(dresult):
return [tf.clip_by_norm(dresult, norm), None]
return y, grad_fn
Gradients tùy chỉnh thường được sử dụng để cung cấp một gradient ổn định về tính toán cho một chuỗi các phép tính toán:
In [ ]:
def log1pexp(x):
return tf.math.log(1 + tf.exp(x))
def grad_log1pexp(x):
with tf.GradientTape() as tape:
tape.watch(x)
value = log1pexp(x)
return tape.gradient(value, x)
In [ ]:
# Tính toán gradient hoạt động bình thường ở x = 0.
grad_log1pexp(tf.constant(0.)).numpy()
In [ ]:
# Tuy nhiên, tại x = 100 sẽ xảy ra thất bại vì sự thiếu ổn định tính toán.
grad_log1pexp(tf.constant(100.)).numpy()
Ở đây, hàm log1pexp
có thể được rút gọn thông qua phân tích vói một gradient tùy chỉnh. Cách cài đặt dưới đây sử dụng lại giá trị cho tf.exp(x)
đã được tính trong quá trình lan truyền xuôi - giúp nó hiệu quả hơn thông qua việc loại bỏ các phép tính dư thừa.
In [ ]:
@tf.custom_gradient
def log1pexp(x):
e = tf.exp(x)
def grad(dy):
return dy * (1 - 1 / (1 + e))
return tf.math.log(1 + e), grad
def grad_log1pexp(x):
with tf.GradientTape() as tape:
tape.watch(x)
value = log1pexp(x)
return tape.gradient(value, x)
In [ ]:
# Như trước, việc tính toán gradient hoạt động bình thường ở x = 0
grad_log1pexp(tf.constant(0.)).numpy()
In [ ]:
# Việc tính toán gradient cũng hoạt động ở x = 0
grad_log1pexp(tf.constant(100.)).numpy()
In [ ]:
import time
def measure(x, steps):
# TensorFlow khỏi tạo một GPU lần đầu tiên được sử dụng, ta không tính nó vào việc đếm thời gian
tf.matmul(x, x)
start = time.time()
for i in range(steps):
x = tf.matmul(x, x)
# tf.matmul có thể trả về trước khi thực hiện một phép nhân ma trận
# (vd có thể trả về trước khi phép tính được cho vào một stream CUDA).
# Lần gọi x.numpy() ở dưới sẽ đảm bảo rằng tất cả phép toán đang đợi
# đều đã được thực hiện (và cũng sẽ sao chép kết quả đến bộ nhớ của máy),
# nên chúng ta đang bao gồm thời gian nhiều hơn một chút so với thời gian của
# phép nhân ma trận
_ = x.numpy()
end = time.time()
return end - start
shape = (1000, 1000)
steps = 200
print("Time to multiply a {} matrix by itself {} times:".format(shape, steps))
# Chạy trên CPU:
with tf.device("/cpu:0"):
print("CPU: {} secs".format(measure(tf.random.normal(shape), steps)))
# Chạy trên GPU, nếu có thể:
if tf.config.experimental.list_physical_devices("GPU"):
with tf.device("/gpu:0"):
print("GPU: {} secs".format(measure(tf.random.normal(shape), steps)))
else:
print("GPU: not found")
Một đối tượng tf.Tensor
có thể được sao chép tới một thiết bị khác để thực hiện phép tính toán của nó:
In [ ]:
if tf.config.experimental.list_physical_devices("GPU"):
x = tf.random.normal([10, 10])
x_gpu0 = x.gpu()
x_cpu = x.cpu()
_ = tf.matmul(x_cpu, x_cpu) # Chạy trên CPU
_ = tf.matmul(x_gpu0, x_gpu0) # Chạy trên GPU:0
Với các mô hình nặng về mặt tính toán, ví dụ như ResNet50 huấn luyện trên GPU thì hiệu năng của eager execution sẽ tương đương với thực hiện bằng tf.function
. Tuy nhiên, khoảng cách này sẽ tăng lên khi với mô hình sử dụng ít phép tính toán hơn và vẫn còn rất nhiều việc phải làm để tối ưu các hot code paths cho mô hình với nhiều phép tính toán nhỏ.
Mặc dù eager execution giúp cho việc phát triển và debug tương tác hóa hơn, graph execution theo kiểu của TensorFlow 1.x có lợi thế trong huấn luyện được phân phối và triển khai sản phẩm. Để làm giảm khoảng cách này, TensorFlow 2.0 giới thiệu các function
thông qua API tf.function
. Để biết thêm thông tin, hãy xem hướng dẫn của tf.function