Ở các bài trước các bạn đã hình dung được mạng nơ-ron có gì. Mô hình phân loại chữ viết tay được tạo thành từ các layer được kết nối với nhau, ánh xạ dữ liệu đầu vào thành các dự đoán. Hàm mất mát sau đó so sánh những dự đoán này với mục tiêu, tạo ra một giá trị mất mát – là một hàm đo lường về mức độ mà dự đoán của mô hình khớp với những gì được mong đợi. Bộ tối ưu hóa sử dụng giá trị mất mát này để cập nhật trọng số của mô hình.

Nhắc lại mô hình
Đây là dữ liệu đầu vào:
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255
Nhìn vào đoạn code trên, ta có thể thấy dữ liệu là các hình ảnh đầu vào được lưu trữ dưới dạng tensors NumPy, được định dạng ở đây là tensors float32 có shape của dữ liệu huấn luyện là (60000, 784), còn dữ liệu kiểm thử là (10000, 784).
Mô hình của chúng ta:
model = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(10, activation="softmax")
])
Mô hình này bao gồm một chuỗi của hai layer Dense, mỗi layer áp dụng một số phép toán tensor đơn giản cho dữ liệu đầu vào và rằng những phép toán này liên quan đến các tensor trọng số. Các tensor trọng số, là các thuộc tính của các lớp, là nơi kiến thức của mô hình được lưu trữ.
Bước biên dịch mô hình:
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
Trong đó sparse_categorical_crossentropy là hàm mất mát được sử dụng làm tín hiệu phản hồi để học các tensor trọng số, và trong quá trình huấn luyện, mục tiêu là giảm thiểu hàm mất mát này. Ta cũng biết rằng việc giảm thiểu mất mát xảy ra thông qua phương pháp mini-batch stochastic gradient descent. Các quy tắc cụ thể được định nghĩa bởi bộ tối ưu hóa rmsprop. Cuối cùng, đây là vòng lặp huấn luyện:
model.fit(train_images, train_labels, epochs=5, batch_size=128)
Khi gọi hàm fit(), mô hình sẽ bắt đầu lặp lại trên dữ liệu huấn luyện theo các mini-batch có 128 mẫu, lặp lại quá trình này 5 lần (mỗi lần lặp qua toàn bộ dữ liệu huấn luyện được gọi là một epoch). Đối với mỗi batch, mô hình sẽ tính toán độ dốc của hàm mất mát đối với trọng số (sử dụng thuật toán Backpropagation, có nguồn gốc từ quy tắc chuỗi trong giải tích như trình bày ở phần trước) và di chuyển trọng số theo hướng sẽ giảm giá trị mất mát cho batch này.
Sau 5 epoch, mô hình sẽ thực hiện 2,345 cập nhật độ dốc (469 mỗi epoch), và giá trị mất mát của mô hình sẽ đủ thấp để mô hình có khả năng phân loại chữ số viết tay với độ chính xác cao.
Triển khai mô hình phân loại chữ viết tay với Tensorflow
Layer Dense cơ bản
Như các bài trước mình đã nói, Dense thực hiện phép biến đổi đầu vào như sau, trong đó W và b là các tham số của mô hình, và activation là một hàm tích lũy theo phần tử (thường là relu, nhưng có thể là softmax cho layer cuối cùng):
output = activation(dot(W, input) + b)
Ta xây dựng một class Python đơn giản, NaiveDense, tạo ra hai biến TensorFlow là W và b, và hiển thị một phương thức call() áp dụng phép biến đổi đã nói trước đó.
import tensorflow as tf
class NaiveDense:
def __init__(self, input_size, output_size, activation):
self.activation = activation
w_shape = (input_size, output_size)
w_initial_value = tf.random.uniform(w_shape, minval=0, maxval=1e-1)
self.W = tf.Variable(w_initial_value)
b_shape = (output_size,
b_initial_value = tf.zeros(b_shape)
self.b = tf.Variable(b_initial_value)
def __call__(self, inputs)::
return self.activation(tf.matmul(inputs, self.W) + self.b)
@property
def weights(self):
return [self.W, self.b]
Sequential đơn giản
Tiếp theo, tạo một class NaiveSequential để kết nối những layer này. Nó bao bọc một danh sách các layer và hiển thị một phương thức call() đơn giản gọi các layer cơ bản trên đầu vào theo thứ tự. Nó cũng có một thuộc tính weights để dễ dàng theo dõi các tham số của các lớp.
class NaiveSequential:
def __init__(self, layers):
self.layers = layers
def __call__(self, inputs):
x = inputs
for layer in self.layers:
x = layer(x)
return x
@property
def weights(self):
weights = []
for layer in self.layers:
weights += layer.weights
return weights
Sử dụng lớp NaiveDense và lớp NaiveSequential này, chúng ta có thể tạo ra một mô hình giả Keras như sau:
model = NaiveSequential([
NaiveDense(input_size=28 * 28, output_size=512, activation=tf.nn.relu),
NaiveDense(input_size=512, output_size=10, activation=tf.nn.softmax)
])
assert len(model.weights) == 4
Batch generator
Tiếp theo, ta cần lặp qua dữ liệu MNIST theo các mini-batch.
import math
class BatchGenerator:
def __init__(self, images, labels, batch_size=128):
assert len(images) == len(labels)
self.index = 0
self.images = images
self.labels = labels
self.batch_size = batch_size
self.num_batches = math.ceil(len(images) / batch_size)
def next(self):
images = self.images[self.index : self.index + self.batch_size]
labels = self.labels[self.index : self.index + self.batch_size]
self.index += self.batch_size
return images, labels
Chạy bước huấn luyện
Chúng ta cần thực hiện các bước sau:
- Tính toán dự đoán của mô hình cho các hình ảnh trong lô dữ liệu.
- Tính toán giá trị mất mát cho những dự đoán này, dựa trên các nhãn thực tế.
- Tính toán độ dốc của mất mát đối với trọng số của mô hình.
- Di chuyển trọng số một lượng nhỏ theo hướng ngược lại với độ dốc.
Để tính toán độ dốc, ta sẽ sử dụng đối tượng GradientTape trong TensorFlow:
def one_training_step(model, images_batch, labels_batch):
with tf.GradientTape() as tape:
predictions = model(images_batch)
per_sample_losses = tf.keras.losses.sparse_categorical_crossentropy(
labels_batch, predictions)
average_loss = tf.reduce_mean(per_sample_losses)
gradients = tape.gradient(average_loss, model.weights)
update_weights(gradients, model.weights)
return average_loss
Mục đích của bước cập nhật trọng số (được đại diện bởi hàm update_weights) là di chuyển trọng số “một chút” theo một hướng sẽ giảm mất mát trên lô dữ liệu này. Độ lớn của bước di chuyển được xác định bởi “tốc độ học” thường là một lượng nhỏ. Cách đơn giản nhất để triển khai hàm update_weights này là trừ gradient * learning rate
từ mỗi trọng số:
learning_rate = 1e-3
def update_weights(gradients, weights):
for g, w in zip(gradients, weights):
w.assign_sub(g * learning_rate)
Trong thực tế, ta sẽ sử dụng một Optimizer từ Keras như sau:
from tensorflow.keras import optimizers
optimizer = optimizers.SGD(learning_rate=1e-3)
def update_weights(gradients, weights):
optimizer.apply_gradients(zip(gradients, weights))
Khi bước đào tạo từng lô đã sẵn sàng, ta có thể tiếp tục triển khai một epoch đào tạo toàn bộ.
Đào tạo toàn bộ
Một epoch đào tạo đơn giản chỉ bao gồm việc lặp lại bước đào tạo cho mỗi lô trong dữ liệu đào tạo, và vòng lặp đào tạo đầy đủ đơn giản chỉ là sự lặp lại của một epoch:
def fit(model, images, labels, epochs, batch_size=128):
for epoch_counter in range(epochs):
print(f"Epoch {epoch_counter}")
batch_generator = BatchGenerator(images, labels)
for batch_counter in range(batch_generator.num_batches):
images_batch, labels_batch = batch_generator.next()
loss = one_training_step(model, images_batch, labels_batch)
if batch_counter % 100 == 0:
print(f"loss at batch {batch_counter}: {loss:.2f}")
Thử nghiệm:
from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255
fit(model, train_images, train_labels, epochs=10, batch_size=128)
Đánh giá mô hình
Chúng ta có thể đánh giá mô hình bằng cách lấy giá trị lớn nhất của các dự đoán trên các hình ảnh kiểm thử và so sánh nó với các nhãn mong đợi:
predictions = model(test_images)
predictions = predictions.numpy()
predicted_labels = np.argmax(predictions, axis=1)
matches = predicted_labels == test_labels
print(f"accuracy: {matches.mean():.2f}")
Vậy là xong, trong thực tế chúng ta sẽ sử dụng các hàm có sẵn của Keras, tuy nhiên việc build các bước bằng tay như vậy cho ta hiểu biết rõ ràng hơn về những gì diễn ra bên trong một mạng neural khi gọi hàm fit().
Tóm tắt
- Tensor là nền tảng của các hệ thống học máy hiện đại. Chúng xuất hiện dưới nhiều dạng khác nhau về dtype, rank và shape.
- Bạn có thể thao tác trên tensor số học thông qua các phép toán tensor (như cộng, tích tensor hoặc nhân từng phần tử), có thể được hiểu như là mã hóa các biến đổi hình học. Nói chung, mọi thứ trong học sâu có thể được hiểu bằng cách hình học.
- Mô hình học sâu bao gồm chuỗi các phép toán tensor đơn giản, được tham số hóa bằng trọng số, chính chúng là tensor. Trọng số của một mô hình là nơi “kiến thức” của nó được lưu trữ.
- Học nghĩa là tìm một bộ giá trị cho trọng số của mô hình sao cho giảm thiểu một hàm mất mát đối với một bộ mẫu dữ liệu đào tạo cụ thể và các mục tiêu tương ứng của chúng. Học diễn ra bằng cách rút ra các lô ngẫu nhiên của mẫu dữ liệu và mục tiêu của chúng, sau đó tính độ dốc của các tham số mô hình đối với mất mát trên lô. Các tham số mô hình sau đó được di chuyển một chút (độ lớn của chuyển động được xác định bởi tốc độ học) theo hướng ngược lại từ độ dốc. Điều này được gọi là hạ gradient ngẫu nhiên theo mini-batch (mini-batch stochastic gradient descent).
- Toàn bộ quá trình học là có thể nhờ vào việc tất cả các phép toán tensor trong mạng neural đều có thể được đạo hàm, và do đó có thể áp dụng quy tắc chuỗi đạo hàm để tìm hàm đạo hàm ánh xạ từ các tham số hiện tại và lô dữ liệu hiện tại đến một giá trị độ dốc. Điều này được gọi là lan truyền ngược.
- Hai khái niệm quan trọng các bạn sẽ thường xuyên thấy trong các bài viết sau là mất mát và bộ tối ưu hóa. Đây là hai điều ta cần định nghĩa trước khi bắt đầu đưa dữ liệu vào một mô hình.
- Mất mát là lượng bạn sẽ cố gắng giảm thiểu trong quá trình đào tạo, nên nó phải đại diện cho một độ đo thành công cho nhiệm vụ mà ta đang cố gắng giải quyết.
- Bộ tối ưu hóa chỉ định cách chính xác mà độ dốc của mất mát sẽ được sử dụng để cập nhật các tham số: ví dụ, nó có thể là bộ tối ưu hóa RMSProp, SGD với đà động, và vân vân.