Để hiểu về deep learning, ta cần quen thuộc với nhiều khái niệm toán học đơn giản: tensors, các phép toán tensor, đạo hàm, gradient descent, và nhiều khái niệm khác nữa. Mình sẽ giới thiệu các khái niệm này bằng phương pháp xây dựng trực giác, không tập trung vào công thức toán học.
Mạng nơ-ron sâu trong Deep Learning
Bài toán đặt ra ở đây là phân loại hình ảnh xám của các chữ số viết tay (kích thước 28 × 28 pixel) vào 10 danh mục tương ứng (từ 0 đến 9). Chúng ta sẽ sử dụng bộ dữ liệu MNIST, một bộ dữ liệu cổ điển trong cộng đồng học máy, tồn tại gần như từ khi lĩnh vực này bắt đầu và đã được nghiên cứu một cách chặt chẽ. Đây là một tập hợp gồm:
- 60,000 hình ảnh huấn luyện
- 10,000 hình ảnh kiểm thử
Bộ dữ liệu này được thu thập bởi Viện Tiêu chuẩn và Công nghệ Quốc gia (NIST trong MNIST) vào những năm 1980. MNIST như là “Hello World” của deep learning.
from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images và train_labels tạo thành bộ dữ liệu huấn luyện, dữ liệu mà mô hình sẽ học. Sau đó, mô hình sẽ được kiểm thử trên bộ dữ liệu kiểm thử, test_images và test_labels.
Các hình ảnh được encode dưới dạng mảng NumPy, và nhãn là một mảng các chữ số từ 0 đến 9. Có một tương ứng one-to-one giữa các hình ảnh và nhãn.
>>> train_images.shape
(60000, 28, 28)
>>> len(train_labels)
60000
>>> train_labels
array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)
Và dữ liệu test:
>>> test_images.shape
(10000, 28, 28)
>>> len(test_labels)
10000
>>> test_labels
array([7, 2, 1, ..., 4, 5, 6], dtype=uint8)
Quy trình làm việc sẽ như sau:
- Đầu tiên, chúng ta sẽ cung cấp dữ liệu huấn luyện cho mạng neural, train_images và train_labels.
- Sau đó, mạng sẽ học cách liên kết giữa hình ảnh và nhãn.
- Cuối cùng, chúng ta sẽ yêu cầu mạng tạo ra dự đoán cho test_images, và chúng ta sẽ kiểm tra xem những dự đoán này có khớp với nhãn từ test_labels hay không.
from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(10, activation="softmax")
])
Core building block của các mạng neural là layer. Ta có thể xem về một layer như một bộ lọc cho dữ liệu: một số dữ liệu vào và nó trở ra dưới dạng một dạng khác hữu ích hơn. Cụ thể, các layer trích xuất các biểu diễn từ dữ liệu được đưa vào chúng. Hầu hết trong deep learning bao gồm việc kết hợp các layer đơn giản với nhau, tạo thành một dạng tiến trình chưng cất dữ liệu. Một mô hình deep learning giống như một cái rây cho xử lý dữ liệu, được tạo nên từ một chuỗi các bộ lọc dữ liệu ngày càng tinh lọc hơn – các layer.
Ở đây, mô hình của chúng ta bao gồm một chuỗi gồm hai Dense layers, là các layer neural được kết nối mật (còn được gọi là fully connected). Layer thứ hai (và cuối cùng) là một layer phân loại softmax 10 chiều, điều này có nghĩa là nó sẽ trả về một mảng gồm 10 điểm xác suất (tổng cộng bằng 1). Mỗi điểm xác suất sẽ là xác suất mà hình ảnh chữ số hiện tại thuộc về một trong 10 lớp chữ số của chúng ta.
Để làm cho mô hình sẵn sàng cho việc huấn luyện, ta cần chọn thêm ba điều trong quá trình biên dịch (compilation):
- Một bộ tối ưu hóa (optimizer) – Cơ chế thông qua đó mô hình sẽ tự cập nhật dựa trên dữ liệu huấn luyện mà nó nhìn thấy, nhằm cải thiện hiệu suất của mình.
- Một hàm mất mát (loss function) – Cách mà mô hình sẽ đo lường hiệu suất của mình trên dữ liệu huấn luyện, và do đó cách mà nó sẽ tự điều chỉnh mình theo hướng đúng.
- Các chỉ số để theo dõi trong quá trình huấn luyện và kiểm thử – Ở đây, chúng ta chỉ quan tâm đến độ chính xác (tỉ lệ phần trăm của các hình ảnh được phân loại đúng).
Mục đích chính xác của hàm mất mát và bộ tối ưu hóa sẽ trở nên rõ ràng trong các bài viết tiếp theo.
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
Trước khi huấn luyện, ta sẽ tiền xử lý dữ liệu bằng cách điều chỉnh hình dạng của nó thành hình dạng mà mô hình mong đợi và thực hiện việc tỷ lệ sao cho tất cả các giá trị nằm trong khoảng [0, 1]. Trước đó, hình ảnh huấn luyện của chúng ta được lưu trong một mảng có hình dạng (60000, 28, 28) kiểu uint8 với giá trị trong khoảng [0, 255]. Chúng ta sẽ biến đổi nó thành một mảng float32 có hình dạng (60000, 28 * 28) với giá trị từ 0 đến 1.
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
Bây giờ, ta đã sẵn sàng để huấn luyện mô hình, trong Keras, điều này được thực hiện thông qua một cuộc gọi đến phương thức fit() của mô hình.
>>> model.fit(train_images, train_labels, epochs=5, batch_size=128)
Epoch 1/5
60000/60000 [===========================] - 5s - loss: 0.2524 - acc: 0.9273
Epoch 2/5
51328/60000 [=====================>.....] - ETA: 1s - loss: 0.1035 - acc: 0.9692
Hai đại lượng được hiển thị trong quá trình huấn luyện là mất mát của mô hình trên dữ liệu huấn luyện và độ chính xác của mô hình trên dữ liệu huấn luyện. Chúng ta nhanh chóng đạt được độ chính xác là 0.989 (98.9%) trên dữ liệu huấn luyện.
Bây giờ chúng ta đã có một mô hình đã được huấn luyện, chúng ta có thể sử dụng nó để dự đoán xác suất lớp cho các chữ số mới.
>>> test_digits = test_images[0:10]
>>> predictions = model.predict(test_digits)
>>> predictions[0]
array([1.0726176e-10, 1.6918376e-10, 6.1314843e-08, 8.4106023e-06,
2.9967067e-11, 3.0331331e-09, 8.3651971e-14, 9.9999106e-01,
2.6657624e-08, 3.8127661e-07], dtype=float32)
Mỗi số có chỉ mục i trong mảng đó tương ứng với xác suất mà hình ảnh chữ số test_digits[0] thuộc về lớp i. Chữ số kiểm thử đầu tiên này có điểm xác suất cao nhất (0.99999106, gần 1) tại index 7, vì vậy theo mô hình của chúng ta, đó phải là số 7.
>>> predictions[0].argmax()
7
>>> predictions[0][7]
0.99999106
Ta có thể kiểm tra label:
>>> test_labels[0]
7
Ta kiểm tra bằng cách tính độ chính xác trung bình trên toàn bộ bộ kiểm thử.
>>> test_loss, test_acc = model.evaluate(test_images, test_labels)
>>> print(f"test_acc: {test_acc}")
test_acc: 0.9785
Độ chính xác trên bộ kiểm thử cuối cùng là 97.8% – điều này thấp hơn khá nhiều so với độ chính xác trên bộ huấn luyện (98.9%). Sự chênh lệch giữa độ chính xác trên tập huấn luyện và độ chính xác trên tập kiểm thử là một ví dụ về hiện tượng overfitting (quá mức). Sự thật là các mô hình học máy thường hoạt động kém trên dữ liệu mới hơn so với dữ liệu huấn luyện của chúng. Overfitting mình sẽ nói thêm trong các bài viết sau.
Biểu diễn dữ liệu trong mạng nơ-ron
Trong ví dụ trên, ta bắt đầu từ dữ liệu được lưu trữ trong các mảng NumPy đa chiều, còn được gọi là tensors. Nói chung, tất cả các hệ thống học máy hiện đại đều sử dụng tensors như cấu trúc dữ liệu cơ bản. Tensors là yếu tố cơ bản của lĩnh vực này. Vậy là tensor là gì?
Ở bản chất, một tensor là một container cho dữ liệu – thường là dữ liệu số. Vì vậy, đó là một container cho các số. Các bạn chắc hẳn đã quen thuộc với ma trận, nó được xem là tensors cấp 2. Tensors là sự tổng quát hóa của ma trận với một số chiều tùy ý (lưu ý rằng trong ngữ cảnh của tensors, một chiều thường được gọi là một trục).
Scalars (rank-0 tensors)
Một tensor chỉ chứa một số được gọi là scalar (hoặc scalar tensor, hoặc rank-0 tensor, hoặc 0D tensor). Trong NumPy, một số float32 hoặc float64 là một scalar tensor (hoặc scalar array). Ta có thể hiển thị số chiều của một tensor NumPy thông qua thuộc tính ndim; một scalar tensor có 0 chiều (ndim == 0). Số chiều của một tensor cũng được gọi là rank của nó. Dưới đây là một scalar:
>>> import numpy as np
>>> x = np.array(12)
>>> x
array(12)
>>> x.ndim
0
Vectors (rank-1 tensors)
Một mảng các số được gọi là vector, hoặc rank-1 tensor, hoặc 1D tensor. Một rank-1 tensor có chính xác 1 trục. Dưới đây là một vector NumPy:
>>> x = np.array([12, 3, 6, 14, 7])
>>> x
array([12, 3, 6, 14, 7])
>>> x.ndim
1
Vector này có năm phần tử và do đó được gọi là một vector 5 chiều. Đừng nhầm lẫn giữa một vector 5D và một tensor 5D! Một vector 5D chỉ có một trục và có năm chiều dọc theo trục của mình, trong khi một tensor 5D có năm trục (và có thể có bất kỳ số chiều nào dọc theo mỗi trục). Khả năng chiều có thể chỉ số entri theo một trục cụ thể (như trong trường hợp của vector 5D) hoặc số lượng trục trong một tensor (như một tensor 5D), điều này có thể gây nhầm lẫn.
Matrices (rank-2 tensors)
Một mảng của các vector được gọi là ma trận, hoặc rank-2 tensor, hoặc 2D tensor. Một ma trận có hai trục (thường được gọi là hàng và cột).
>>> x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
>>> x.ndim
2
Các phần tử từ trục thứ nhất được gọi là hàng, và các phần tử từ trục thứ hai được gọi là cột. Trong ví dụ trước, [5, 78, 2, 34, 0] là hàng đầu tiên của x, và [5, 6, 7] là cột đầu tiên.
Rank-3 và higher-rank tensors
>>> x = np.array([[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]]])
>>> x.ndim
3
Key attributes
Một tensor được định nghĩa bởi ba thuộc tính chính:
- Số lượng trục (rank) – Ví dụ, một tensor cấp 3 có ba trục, và một ma trận có hai trục. Điều này cũng được gọi là ndim của tensor trong các thư viện Python như NumPy hoặc TensorFlow.
- Hình dạng (shape) – Đây là một tuple của các số nguyên mô tả số chiều của tensor dọc theo mỗi trục. Ví dụ, ví dụ về ma trận trước đó có hình dạng (3, 5), và ví dụ về tensor cấp 3 có hình dạng (3, 3, 5). Một vector có hình dạng với một phần tử duy nhất, như (5,), trong khi một scalar có hình dạng trống, ().
- Loại dữ liệu (thường được gọi là dtype trong các thư viện Python) – Đây là loại dữ liệu được chứa trong tensor; ví dụ, loại của tensor có thể là float16, float32, float64, uint8, và nhiều loại khác. Trong TensorFlow, ta cũng có thể gặp các tensor kiểu chuỗi.
Ta xem lại bộ dữ liệu MNIST.
from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
Tiếp theo, ta hiển thị số lượng trục của tensor train_images, thông qua thuộc tính ndim.
>>> train_images.ndim
3
Còn shape:
>>> train_images.shape
(60000, 28, 28)
Và kiểu dữ liệu:
>>> train_images.dtype
uint8
Vậy chúng ta có ở đây là một tensor cấp 3 của số nguyên 8-bit. Cụ thể hơn, đó là một mảng của 60,000 ma trận kích thước 28 × 28 với giá trị là số nguyên. Mỗi ma trận như vậy là một hình ảnh xám, với các hệ số nằm trong khoảng từ 0 đến 255.
Hiển thị ký tự số trên:
import matplotlib.pyplot as plt
digit = train_images[4]
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()
Kết quả tương ứng:
>>> train_labels[4]
9
Thao tác với tensor
Trong bài viết trước, ta đã chọn một chữ số cụ thể theo chiều đầu tiên bằng cú pháp train_images[i]. Việc chọn các phần tử cụ thể trong một tensor được gọi là tensor slicing. Ví dụ sau đây chọn các chữ số từ #10 đến #100 và đặt chúng trong một mảng có hình dạng (90, 28, 28)
.
>>> my_slice = train_images[10:100]
>>> my_slice.shape
(90, 28, 28)
Nói chung, ta có thể chọn các lát cắt giữa hai chỉ số bất kỳ dọc theo mỗi trục của tensor. Ví dụ, để chọn các pixel có kích thước 14 × 14 ở góc dưới bên phải của tất cả các hình ảnh:
my_slice = train_images[:, 14:, 14:]
Để cắt ảnh thành các mảnh có kích thước 14 × 14 pixel ở giữa:
my_slice = train_images[:, 7:-7, 7:-7]
Data batches
Mô hình deep learning không xử lý toàn bộ tập dữ liệu một lần; thay vào đó, chúng chia dữ liệu thành các batch nhỏ. Cụ thể, đây là một batch của các chữ số MNIST với kích thước batch là 128:
batch = train_images[:128]
Do đó batch tiếp theo là:
batch = train_images[128:256]
Với batch thứ n:
n = 3
batch = train_images[128 * n:128 * (n + 1)]
Khi xem xét một tensor batch như vậy, trục đầu tiên (trục 0) được gọi là trục batch hoặc chiều batch. Đây là một thuật ngữ mà ta sẽ thường xuyên gặp khi sử dụng Keras và các thư viện deep learning khác.
Ví dụ thực tế về data tensors
Dữ liệu chúng ta sẽ thao tác hầu như luôn thuộc vào một trong các loại sau đây:
- Dữ liệu vector – Tensors rank 2 có shape là (samples, features), trong đó mỗi mẫu là một vector các thuộc tính số học (“features”).
- Dữ liệu chuỗi thời gian hoặc dữ liệu trình tự – Tensors rank 3 có shape là (samples, timesteps, features), trong đó mỗi mẫu là một chuỗi (chiều dài là timesteps) các vector đặc trưng.
- Hình ảnh – Tensors rank 4 có shape là (samples, height, width, channels), trong đó mỗi mẫu là một lưới 2D các pixel, và mỗi pixel được biểu diễn bằng một vector giá trị (“channels”).
- Video – Tensors rank 5 có shape là (samples, frames, height, width, channels), trong đó mỗi mẫu là một chuỗi (chiều dài là frames) các hình ảnh.
Vector data
Đây là một trong những trường hợp phổ biến nhất. Trong một bộ dữ liệu như vậy, mỗi điểm dữ liệu có thể được mã hóa dưới dạng một vector, và do đó, một batch dữ liệu sẽ được mã hóa dưới dạng một tensor hạng 2 (nghĩa là, một mảng các vector), trong đó trục đầu tiên là trục mẫu và trục thứ hai là trục đặc trưng.
Hãy xem xét hai ví dụ:
- Bộ dữ liệu bảo hiểm với thông tin về người, trong đó chúng ta xem xét tuổi, giới tính và thu nhập của mỗi người. Mỗi người có thể được mô tả dưới dạng một vector gồm 3 giá trị, và do đó, toàn bộ bộ dữ liệu của 100,000 người có thể được lưu trữ trong một tensor hạng 2 có hình dạng (100000, 3).
- Bộ dữ liệu văn bản, trong đó chúng ta biểu diễn mỗi tài liệu văn bản bằng cách đếm số lần xuất hiện của mỗi từ trong đó (từ một từ điển gồm 20,000 từ thông dụng). Mỗi tài liệu có thể được mã hóa dưới dạng một vector gồm 20,000 giá trị (một số lần đếm cho mỗi từ trong từ điển), và do đó, toàn bộ bộ dữ liệu của 500 tài liệu có thể được lưu trữ trong một tensor có hình dạng (500, 20000).
Timeseries data hay sequence data
Khi dữ liệu phụ thuộc chặt chẽ vào thời gian (hoặc khái niệm về sequence order), nó sẽ được lưu trong tensor hạng 3 với một trục thời gian rõ ràng. Mỗi mẫu có thể được mã hóa dưới dạng một chuỗi các vector (một tensor hạng 2), và do đó, một batch dữ liệu sẽ được mã hóa dưới dạng một tensor hạng 3.
Trục thời gian luôn luôn là trục thứ hai (trục có chỉ số 1) theo quy ước.
- Bộ dữ liệu giá cổ phiếu. Mỗi phút, chúng ta lưu trữ giá hiện tại của cổ phiếu, giá cao nhất trong phút trước đó và giá thấp nhất trong phút trước đó. Do đó, mỗi phút được mã hóa dưới dạng một vector 3D, một ngày giao dịch đầy đủ được mã hóa dưới dạng ma trận có hình dạng (390, 3) (có 390 phút trong một ngày giao dịch), và dữ liệu của 250 ngày có thể được lưu trữ trong một tensor hạng 3 có hình dạng (250, 390, 3). Ở đây, mỗi mẫu sẽ là dữ liệu của một ngày.
- Bộ dữ liệu tweet, trong đó chúng ta mã hóa mỗi tweet dưới dạng một chuỗi 280 ký tự từ một bảng chữ cái có 128 ký tự duy nhất. Trong cài đặt này, mỗi ký tự có thể được mã hóa dưới dạng một vector nhị phân có kích thước 128 (một vector toàn số 0 ngoại trừ một giá trị 1 tại chỉ số tương ứng với ký tự). Sau đó, mỗi tweet có thể được mã hóa dưới dạng một tensor hạng 2 có hình dạng (280, 128), và một bộ dữ liệu gồm 1 triệu tweet có thể được lưu trữ trong một tensor có hình dạng (1000000, 280, 128).
Image data
Hình ảnh thường có 3 chiều: chiều cao (height), chiều rộng (width), và độ sâu màu (color depth). Mặc dù hình ảnh xám chỉ có một kênh màu duy nhất và có thể được lưu trữ trong các tensor hạng 2, theo quy ước, tensor hình ảnh luôn có hạng là 3, với một kênh màu một chiều cho hình ảnh xám. Một batch gồm 128 hình ảnh xám có kích thước 256 × 256 có thể được lưu trữ trong một tensor có hình dạng (128, 256, 256, 1), và một batch gồm 128 hình ảnh màu có thể được lưu trữ trong một tensor có hình dạng (128, 256, 256, 3).
Có hai quy ước về hình dạng của các tensor hình ảnh: quy ước channels-last (phổ biến trong TensorFlow) và quy ước channels-first (đang ngày càng ít được ưa chuộng).
- Quy ước channels-last đặt trục độ sâu màu ở cuối: (samples, height, width, color_depth).
- Quy ước channels-first đặt trục độ sâu màu ngay sau trục batch: (samples, color_depth, height, width).
Với quy ước channels-first, các ví dụ trước đó sẽ trở thành (128, 1, 256, 256) và (128, 3, 256, 256). API Keras hỗ trợ cả hai định dạng này.
Video data
Dữ liệu video là một trong những loại dữ liệu thế giới thực cần đến các tensor hạng 5. Một video có thể được hiểu như một chuỗi các khung hình, mỗi khung hình là một hình ảnh màu. Vì mỗi khung hình có thể được lưu trữ trong một tensor hạng 3 (height, width, color depth), một chuỗi các khung hình có thể được lưu trữ trong một tensor hạng 4 (frames, height, width, color depth), và do đó, một batch của các video khác nhau có thể được lưu trữ trong một tensor hạng 5 có hình dạng (samples, frames, height, width, color depth).
Ví dụ, một đoạn video YouTube dài 60 giây, kích thước 144 × 256 và mẫu 4 khung hình mỗi giây sẽ có 240 khung hình. Một batch gồm 4 đoạn video như vậy sẽ được lưu trữ trong một tensor có hình dạng (4, 240, 144, 256, 3). Đó là tổng cộng 106,168,320 giá trị! Nếu kiểu dữ liệu của tensor là float32, mỗi giá trị sẽ được lưu trữ trong 32 bit, vì vậy tensor sẽ đại diện cho 405 MB. Rất nặng!
Tuy nhiên video thường gặp trong thực tế thường nhẹ hơn nhiều vì chúng không được lưu trữ dưới dạng float32 và thường được nén với một tỷ lệ lớn (ví dụ như trong định dạng MPEG).
Comments 1