Word embeddings

Xem trên TensorFlow.org Chạy trong Google Colab Xem mã nguồn trên GitHub Tải notebook

Để đăng ký dịch hoặc duyệt lại nội dung bản dịch, các bạn hãy liên hệ docs@tensorflow.org.

Bài hướng dẫn này sẽ xoay quanh chủ đề về word embedding. Nội dung của bài hướng dẫn sẽ bao gồm code để huấn luyện word embedding trên tập dữ liệu nhỏ và trực quan hóa các embedding thu được với Embedding Projector (xem hình minh họa bên dưới).

Screenshot of the embedding projector

Biểu diễn văn bản dưới dạng số

Đầu vào của các mô hình máy học là tập hợp các vector (mảng các số). Để đưa một đoạn văn bản vào mô hình, điều đầu tiên chúng ta cần làm là chuyển đổi các ký tự sang số (thực hiện vector hóa văn bản). Trong phần này, chúng ta sẽ đi qua ba cách để thực hiện mục tiêu trên.

One-hot encoding

Cách làm đầu tiên chúng ta có thể nghĩ tới là encode "one-hot" từng từ trong danh sách từ vựng. Xét câu sau: "The cat sat on the mat". Danh sách từ vựng (hay tập hợp các từ phân biệt) trong câu trên là (cat, mat, on, sat, the). Để biểu diễn mỗi từ, ta tạo một vector 0 có độ dài bằng với số lượng từ trong danh sách từ vựng và thay 1 vào vị trí tương ứng của từ cần biểu diễn. Cách làm này được minh họa bằng sơ đồ bên dưới.

Diagram of one-hot encodings

Để thu được vector encode của một câu, ta chỉ việc nối các vector one-hot của từng từ trong câu.

Nhận xét: Cách làm này không hiệu quả vì các vector thu được từ phương pháp one-hot encoding đều là các vector thưa (nghĩa là hầu hết các phần tử đều là 0). Nếu danh sách từ vựng có 10,000 từ thì để one-hot encoding một từ bất kì, ta cần tạo ra một vector mà ở đó 99.9% các phần tử đều là 0.

Encode mỗi từ bằng một số phân biệt

Cách làm thứ hai mà ta có thể sử dụng là encode mỗi từ bằng một số phân biệt. Xét ví dụ trên, ta có thể sử dụng số 1 để encode cho từ "cat", số 2 cho từ "mat" và tương tự đối với các từ còn lại. Lúc này, cả câu "The cat sat on the mat" có thể được encode bằng một vector dày đặc, chẳng hạn [5, 1, 4, 3, 5, 2]. Cách làm này mang lại hiệu quả hơn vì kết quả thu được của ta là vector dày đặc thay vì vector thưa như cách làm trước.

Tuy nhiên, cách làm này có hai nhược điểm:

  • Các số được sử dụng để encode là hoàn toàn ngẫu nhiên (do đó, không thể hiện được mối quan hệ giữa các từ với nhau).

  • Cách encode này có thể gây nhầm lẫn cho mô hình. Ví dụ trong mô hình phân loại tuyến tính, với mỗi đặc trưng mô hình cần học một trọng số tương ứng. Tuy nhiên vì hai từ tương đồng nhau chưa chắc cách encode của chúng sẽ tương tự nhau, trọng số mà mô hình học được từ sự tương đồng trong cách biểu diễn bằng số của các từ sẽ không có ý nghĩa.

Word embedding

Word embedding là một phương pháp biểu diễn từ hiệu quả bằng các vector dày đặc cho phép sự tương đồng giữa các từ được phản ánh qua cách encode của nó. Đặc biệt hơn nữa, chúng ta sẽ không phải trực tiếp xác định cách encode của mỗi từ. Mỗi embedding là một vector số thực dày đặc với độ dài tùy chọn. Nếu xem các giá trị trong embedding như tham số thì ta hoàn toàn có thể huấn luyện mô hình để học các tham số này (tương tự như quá trình học trọng số trong các lớp Dense). Thông thường số chiều được chọn cho word embedding đối với tập dữ liệu nhỏ là 8 và có thể lên đến 1024 đối với tập dữ liệu lớn. Embedding có số chiều càng lớn thì càng có khả năng biểu diễn được những mối quan hệ phức tạp hơn giữa các từ, tuy nhiên cũng đòi hỏi một lượng lớn dữ liệu để học.

Diagram of an embedding

Hình trên là một ví dụ của word embedding. Mỗi từ được biểu diễn bởi một vector số thực 4 chiều. Ta có thể hình dung word embedding giống như một bảng tra. Sau khi mô hình đã học xong các trọng số (là các giá trị trong embedding), ta có thể encode mỗi từ bằng cách tra vào bảng để thu được vector dày đặc là biểu diễn của từ tương ứng.

Chuẩn bị

!pip install -q tf-nightly
import tensorflow as tf
WARNING: You are using pip version 20.1.1; however, version 20.2 is available.
You should consider upgrading via the '/tmpfs/src/tf_docs_env/bin/python -m pip install --upgrade pip' command.

from tensorflow import keras
from tensorflow.keras import layers

import tensorflow_datasets as tfds
tfds.disable_progress_bar()

Sử dụng lớp Embedding

Thư viện Keras giúp việc sử dụng word embedding trở nên vô cùng đơn giản. Sau đây, chúng ta sẽ cùng tìm hiểu về lớp Embedding được hỗ trợ trong thư viện này.

Lớp Embedding có thể được hiểu như một bảng tra cho phép ánh xạ từ một chỉ mục (đại diện cho một từ nhất định) sang một vector dày đặc (embedding của từ đó). Số chiều (hay độ dài) của embedding là tham số mà bạn có thể tùy biến để phù hợp với bài toán của mình, tương tự như việc tùy biến số lượng neuron khác nhau trong một lớp Dense.

embedding_layer = layers.Embedding(1000, 5)

Các trọng số gắn với lớp Embedding sẽ được khởi tạo hoàn toàn ngẫu nhiên. Trong quá trình huấn luyện, các trọng số này sẽ dần được điều chỉnh thông qua cơ chế lan truyền ngược. Word embedding sau khi huấn luyện có thể phần nào encode được sự tương đồng giữa các từ.

Với mỗi số nguyên được truyền vào (tương ứng với chỉ mục của từ cần encode), ta thu được một vector là embedding của từ ấy.

result = embedding_layer(tf.constant([1,2,3]))
result.numpy()
array([[-0.02556943,  0.04202983,  0.03743349, -0.02873744, -0.04249977],
       [-0.04686792, -0.03065403,  0.00914747, -0.02563633, -0.00877964],
       [-0.00782298,  0.03546638,  0.04270319, -0.01302241,  0.01551804]],
      dtype=float32)

Đối với văn bản hay chuỗi văn bản liên tiếp, lớp Embedding nhận đầu vào là một tensor số nguyên 2 chiều (mỗi số nguyên tương ứng với chỉ mục của từ) có kích thước (số lượng mẫu, độ dài chuỗi). Độ dài chuỗi truyền vào là tùy ý. Ví dụ, ta có thể đưa vào lớp embedding một batch với kích thước (32, 10) (32 chuỗi, mỗi chuỗi có độ dài là 10) hoặc (64, 15) (64 chuỗi, mỗi chuỗi có độ dài là 15).

Tensor trả về sẽ nhiều hơn tensor đầu vào một chiều. Ví dụ, nếu ta truyền vào một batch có kích thước (2, 3) thì kết quả trả về sẽ là một tensor có kích thước (2, 3, N). Chiều cuối cùng chính là embedding của các từ tương ứng.

result = embedding_layer(tf.constant([[0,1,2],[3,4,5]]))
result.shape
TensorShape([2, 3, 5])

Cụ thể hơn với đầu vào là một batch các chuỗi, lớp Embedding sẽ trả về kết quả là một tensor số thực 3 chiều với kích thước (số lượng mẫu, độ dài chuỗi, số chiều embedding). Như đã lưu ý ở trên, độ dài chuỗi có thể tùy biến. Do đó, để chuyển đổi tất cả kết quả thu được sang một biểu diễn có số chiều cố định trước khi đưa vào lớp Dense, ta có thể áp dụng một số phương pháp sau: sử dụng RNN, Attention hoặc một lớp Pooling. Trong phạm vi của bài hướng dẫn, chúng tôi sẽ dùng phương pháp đơn giản nhất là sử dụng lớp Pooling. Ngoài ra, bạn có thể tìm hiểu thêm ở Hướng dẫn phân loại văn bản với RNN.

Huấn luyện embedding

Tiếp theo chúng ta sẽ xây dựng một mô hình phân loại sắc thái từ các bình luận phim trên trang IMDB. Trong quá trình huấn luyện, mô hình sẽ học được các embedding cho các từ khác nhau. Ta sẽ tận dụng tập dữ liệu đã được tiền xử lý.

Bạn có thể xem thêm về cách lấy các tập dữ liệu văn bản tại Hướng dẫn lấy dữ liệu văn bản.

(train_data, test_data), info = tfds.load(
    'imdb_reviews/subwords8k', 
    split = (tfds.Split.TRAIN, tfds.Split.TEST), 
    with_info=True, as_supervised=True)
WARNING:absl:TFDS datasets with text encoding are deprecated and will be removed in a future version. Instead, you should use the plain text version and tokenize the text using `tensorflow_text` (See: https://www.tensorflow.org/tutorials/tensorflow_text/intro#tfdata_example)

Downloading and preparing dataset imdb_reviews/subwords8k/1.0.0 (download: 80.23 MiB, generated: Unknown size, total: 80.23 MiB) to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0...
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incompleteU4RW1L/imdb_reviews-train.tfrecord
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incompleteU4RW1L/imdb_reviews-test.tfrecord
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incompleteU4RW1L/imdb_reviews-unsupervised.tfrecord
Dataset imdb_reviews downloaded and prepared to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0. Subsequent calls will reuse this data.

Trước tiên ta cần truy cập vào encoder của tập dữ liệu (tfds.features.text.SubwordTextEncoder) để xem danh sách các từ vựng hiện có.

Kí hiệu "_" được dùng để biểu diễn khoảng trắng. Lưu ý rằng danh sách từ vựng sẽ bao gồm cả các từ toàn vẹn (kết thúc bằng "_") lẫn các từ không toàn vẹn (có thể ghép với nhau tạo thành từ có nghĩa):

encoder = info.features['text'].encoder
encoder.subwords[:20]
['the_',
 ', ',
 '. ',
 'a_',
 'and_',
 'of_',
 'to_',
 's_',
 'is_',
 'br',
 'in_',
 'I_',
 'that_',
 'this_',
 'it_',
 ' /><',
 ' />',
 'was_',
 'The_',
 'as_']

Mỗi bình luận phim có thể có độ dài khác nhau. Do đó, chúng ta sẽ sử dụng padded_batch để chuẩn hóa độ dài của các bình luận.

train_data
<PrefetchDataset shapes: ((None,), ()), types: (tf.int64, tf.int64)>
train_batches = train_data.shuffle(1000).padded_batch(10, padded_shapes=([None],[]))
test_batches = test_data.shuffle(1000).padded_batch(10, padded_shapes=([None],[]))

Lưu ý: Phiên bản TensorFlow 2.2 không yêu cầu khai báo tham số padded_shapes. Ở chế độ mặc định, dữ liệu đầu vào sẽ được đệm thêm các phần tử sao cho dữ liệu ở mọi chiều đều có độ dài bằng nhau và bằng với độ dài lớn nhất theo chiều được xét.

train_batches = train_data.shuffle(1000).padded_batch(10)
test_batches = test_data.shuffle(1000).padded_batch(10)

Như mong đợi, các bình luận dưới dạng văn bản đã được encode thành các số nguyên (mỗi số nguyên đại diện cho một từ toàn vẹn hoặc không toàn vẹn trong danh sách từ vựng).

Các số 0 được thêm vào ở cuối là kết quả của việc chuẩn hóa độ dài bằng với vector có độ dài lớn nhất.

train_batch, train_labels = next(iter(train_batches))
train_batch.numpy()
array([[  12,   56,  284, ...,    0,    0,    0],
       [ 133, 3641,   95, ...,    0,    0,    0],
       [ 286,    2,  481, ...,    0,    0,    0],
       ...,
       [  19,   32,  732, ..., 2868,   66, 7975],
       [7915, 7961,  989, ...,    0,    0,    0],
       [ 693, 1922,   83, ...,    0,    0,    0]])

Xây dựng mô hình đơn giản

Để định nghĩa kiến trúc mô hình, ta sử dụng API Sequential của Keras. Trong hướng dẫn này này, ta sẽ xây dựng một mô hình đơn giản theo kiểu "Continous bag of words":

  • Lớp Embedding nhận đầu vào là các chuỗi với mỗi từ đã được ký hiệu bằng một số nguyên và trả về vector embedding của từ tương ứng. Các vector này sẽ được học trong quá trình huấn luyện mô hình. Nói cách khác, đầu ra của lớp này sẽ có kích thước (kích thước batch, độ dài chuỗi, số chiều embedding).

  • Với mỗi mẫu trong batch, lớp GlobalAveragePooling1D sẽ trả về một vector có kích thước cố thước cố định. Vector này là kết quả của việc tính trung bình theo từng chiều. Đây được xem là cách làm đơn giản nhất để xử lý dữ liệu đầu vào có kích thước không cố định.

  • Các vector (đã chuẩn hóa kích thước) tiếp tục được đưa vào một lớp Dense gồm 16 unit ẩn.

  • Lớp cuối cùng được kết nối đầy đủ (fully-connected) tới một node đầu ra duy nhất với sigmoid làm hàm kích hoạt để thu được kết quả là một số thực nằm trong khoảng từ 0 đến 1. Số thực này đại diện cho xác suất (hay độ tin cậy) một bình luận mang ý nghĩa tích cực.

Chú ý: Mô hình trên không sử dụng kỹ thuật mask. Do đó các phần tử 0 thêm vào trong quá trình chuẩn hóa độ dài cũng được xem như một phần của dữ liệu đầu vào. Kết quả đầu ra có thể bị ảnh hưởng bởi độ dài của phần đệm này. Để khắc phục, xem thêm Hướng dẫn mask và đệm dữ liệu.

embedding_dim=16

model = keras.Sequential([
  layers.Embedding(encoder.vocab_size, embedding_dim),
  layers.GlobalAveragePooling1D(),
  layers.Dense(16, activation='relu'),
  layers.Dense(1)
])

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, None, 16)          130960    
_________________________________________________________________
global_average_pooling1d (Gl (None, 16)                0         
_________________________________________________________________
dense (Dense)                (None, 16)                272       
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 17        
=================================================================
Total params: 131,249
Trainable params: 131,249
Non-trainable params: 0
_________________________________________________________________

Biên dịch và huấn luyện mô hình

model.compile(optimizer='adam',
              loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              metrics=['accuracy'])

history = model.fit(
    train_batches,
    epochs=10,
    validation_data=test_batches, validation_steps=20)
Epoch 1/10
2500/2500 [==============================] - 11s 4ms/step - loss: 0.6139 - accuracy: 0.5792 - val_loss: 0.4322 - val_accuracy: 0.7950
Epoch 2/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.2981 - accuracy: 0.8781 - val_loss: 0.3598 - val_accuracy: 0.8750
Epoch 3/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.2396 - accuracy: 0.9052 - val_loss: 0.3618 - val_accuracy: 0.8850
Epoch 4/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.1990 - accuracy: 0.9233 - val_loss: 0.3839 - val_accuracy: 0.8550
Epoch 5/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.1755 - accuracy: 0.9322 - val_loss: 0.3457 - val_accuracy: 0.8850
Epoch 6/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.1613 - accuracy: 0.9378 - val_loss: 0.4131 - val_accuracy: 0.8600
Epoch 7/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.1446 - accuracy: 0.9461 - val_loss: 0.4950 - val_accuracy: 0.8300
Epoch 8/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.1322 - accuracy: 0.9533 - val_loss: 0.5036 - val_accuracy: 0.8700
Epoch 9/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.1207 - accuracy: 0.9568 - val_loss: 0.4107 - val_accuracy: 0.8650
Epoch 10/10
2500/2500 [==============================] - 10s 4ms/step - loss: 0.1088 - accuracy: 0.9614 - val_loss: 0.5803 - val_accuracy: 0.8450

Với cách làm đã nêu, mô hình đạt độ chính xác khoảng 88% trên tập kiểm thử (dễ dàng nhận thấy mô hình trên đang bị overfit vì độ chính xác thu được trên tập huấn luyện cao hơn rất nhiều).

import matplotlib.pyplot as plt

history_dict = history.history

acc = history_dict['accuracy']
val_acc = history_dict['val_accuracy']
loss=history_dict['loss']
val_loss=history_dict['val_loss']

epochs = range(1, len(acc) + 1)

plt.figure(figsize=(12,9))
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

plt.figure(figsize=(12,9))
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim((0.5,1))
plt.show()

png

png

Truy cập các embedding

Tiếp theo, ta sẽ lấy ra các word embedding đã được học trong quá trình huấn luyện. Tập hợp các embedding này là một ma trận có kích thước (số từ trong danh sách từ vựng, số chiều embedding).

e = model.layers[0]
weights = e.get_weights()[0]
print(weights.shape) # shape: (vocab_size, embedding_dim)
(8185, 16)

Thực hiện lưu lại các trọng số này. Lưu ý rằng để trực quan hóa các embedding thu được với Embedding Projector, ta cần hai tệp dữ liệu: một tệp chứa các vector embedding và một tệp chứa meta data (là các từ trong danh sách từ vựng).

import io

encoder = info.features['text'].encoder

out_v = io.open('vecs.tsv', 'w', encoding='utf-8')
out_m = io.open('meta.tsv', 'w', encoding='utf-8')

for num, word in enumerate(encoder.subwords):
  vec = weights[num+1] # skip 0, it's padding.
  out_m.write(word + "\n")
  out_v.write('\t'.join([str(x) for x in vec]) + "\n")
out_v.close()
out_m.close()

Nếu đang chạy hướng dẫn này trên Colaboratory, bạn có thể sử dụng đoạn chương trình sau để tải các tệp này về máy (hoặc sử dụng trình duyệt tệp bằng cách chọn View -> Table of contents -> File browser).

try:
  from google.colab import files
except ImportError:
   pass
else:
  files.download('vecs.tsv')
  files.download('meta.tsv')

Trực quan hóa các embedding

Để thực hiện trực quan hóa các embedding thu được, trước tiên ta cần tải chúng lên chương trình embedding projector.

Mở Embedding Projector (hoặc chạy TensorBoard ở local).

  • Chọn "Load data".

  • Tải lên hai tệp mà ta đã tạo ở trên gồm: vecs.tsvmeta.tsv.

Lúc này các embedding đã huấn luyện sẽ được hiển thị lên màn hình. Bạn có thể sử dụng chức năng tìm kiếm từ để xem các từ khác nằm gần nó nhất. Chẳng hạn, nếu thử với từ "beautiful", ta sẽ thấy các từ gần đó chẳng hạn như "wonderful".

Lưu ý: Kết quả mỗi người thu được có thể khác nhau đôi chút tùy thuộc vào các trọng số được khởi tạo ngẫu nhiên trước khi huấn luyện lớp embedding.

Lưu ý: Một số thực nghiệm sử dụng mô hình đơn giản hơn có thể cho kết quả embedding tốt hơn. Hãy thử xóa lớp Dense(16), huấn luyện lại mô hình và trực quan hóa các embedding một lần nữa để kiểm tra.

Screenshot of the embedding projector

Tiếp theo

Trong hướng dẫn này, chúng ta đã đi qua các bước để huấn luyện và trực quan hóa các word embedding từ đầu trên một tập dữ liệu nhỏ.

  • Để tìm hiểu thêm về mạng hồi quy, xem thêm tại Keras RNN Guide.

  • Để tìm hiểu thêm về bài toán phân loại văn bản (bao gồm cách thức thực hiện hoặc cho những bạn thắc mắc khi nào nên sử dụng embedding và khi nào nên sử dụng one-hot encoding), hãy xem qua Hướng dẫn phân loại văn bản.