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.

Eager execution

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:

  • Một giao diện dễ hiểu—Sắp xếp code của bạn một cách tự nhiên và sử dụng các cấu trúc dữ liệu của Python. Nhanh chóng thử nghiệm với các mô hình nhỏ và dữ liệu nhỏ.
  • Debug dễ hơn—Gọi các phép tính trực tiếp để giám sát mô hình đang chạy và kiểm tra thay đổi. Sử dụng các công cụ debug tiêu chuẩn của Python để báo cáo lỗi ngay lập tức.
  • Control flow tự nhiên—Sử dụng control flow của Python thay vì dùng graph control flow, đơn giản hóa chỉ dẫn kỹ thuật của các mô hình động.

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.

Cài đặt và sử dụng cơ 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]]

Luồng kiểm soát động

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.

Eager training

Tính toán đạo hàm

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.evaluateModel.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:

  1. Tạo mô hình.
  2. Tìm đạo hàm của hàm mất mát theo các tham số của mô hình.
  3. Tìm một chiến lược để cập nhật các biến dựa trên đạo hàm.

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.

Lưu trữ file theo hình thức object

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.

Các phép đo hướng đối tượng

tf.keras.metrics được lưu trữ dưới dạng objects. Cập nhật một phép đo bằng cách truyền dữ liệu mới cho callable, và nhận kết quả bằng phương thức tf.keras.metrics.result, ví dụ:


In [ ]:
m = tf.keras.metrics.Mean("loss")
m(0)
m(5)
m.result()  # => 2.5
m([8, 9])
m.result()  # => 5.5

Tổng quan và TensorBoard

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/

Các chủ đề đạo hàm tự động nâng cao

Các mô hình động

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

Gradients tùy chỉnh

Gradients tùy chỉnh là một cách để dễ dàng ghi đè gradients. Trong hàm forward, định nghĩa gradient theo đầu vào, đầu ra và các kết quả trung gian. Ví dụ, sau đây là một cách để dễ dạng cắt norm của gradient trong một lần lan truyền ngược:


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()

Hiệu nặng

Việc tính toán được tự động chuyển cho các GPU trong quá trình eager execution. Nếu bạn muốn nắm quyền kiểm soát khi một phép tính toán được thực hiện, bạn có thể để nó trong một khối lệnh tf.device(/gpu:0') (hoặc là phiên bản tương ứng với CPU):


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

Các Benchmark

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ỏ.

Làm việc với các functions

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