🏷️sec_language_model
:numref:sec_text_preprocessing
đã trình bày cách ánh xạ dữ liệu văn bản sang token, những token này có thể được xem như một chuỗi thời gian của các quan sát rời rạc.
Giả sử văn bản độ dài
Mô hình ngôn ngữ vô cùng hữu dụng.
Chẳng hạn, một mô hình lý tưởng có thể tự tạo ra văn bản tự nhiên, chỉ bằng cách chọn một từ
Tuy nhiên, mô hình ngôn ngữ vẫn rất hữu dụng ngay cả khi còn hạn chế. Chẳng hạn, cụm từ “nhận dạng giọng nói” và “nhân gian rộng lối” có phát âm khá giống nhau. Điều này có thể gây ra sự mơ hồ trong việc nhận dạng giọng nói, nhưng có thể dễ dàng được giải quyết với một mô hình ngôn ngữ. Mô hình sẽ loại bỏ ngay phương án thứ hai do mang ý nghĩa kì lạ. Tương tự, một thuật toán tóm tắt tài liệu nên phân biệt được rằng câu “chó cắn người" xuất hiện thường xuyên hơn nhiều so với “người cắn chó”, hay như “Cháu muốn ăn bà ngoại" nghe khá kinh dị trong khi “Cháu muốn ăn, bà ngoại" lại là bình thường.
Làm thế nào để mô hình hóa một tài liệu hay thậm chí là một chuỗi các từ? Ta có thể sử dụng cách phân tích đã dùng trong mô hình chuỗi ở phần trước. Bắt đầu bằng việc áp dụng quy tắc xác suất cơ bản sau:
Ví dụ, xác suất của chuỗi văn bản chứa bốn token bao gồm các từ và dấu chấm câu được tính như sau:
Để tính toán mô hình ngôn ngữ, ta cần tính xác suất các từ và xác suất có điều kiện của một từ khi đã có vài từ trước đó. Đây chính là các tham số của mô hình ngôn ngữ. Ở đây chúng ta giả định rằng, tập dữ liệu huấn luyện là một kho ngữ liệu lớn, chẳng hạn như là tất cả các mục trong Wikipedia của Dự án Gutenberg, hoặc tất cả văn bản được đăng trên mạng. Xác suất riêng lẻ của từng từ có thể tính bằng tần suất của từ đó trong tập dữ liệu huấn luyện.
Ví dụ,
Ở đây
Một kỹ thuật phổ biến là làm mượt Laplace (Laplace smoothing).
Chúng ta đã biết kỹ thuật này khi thảo luận về Naive Bayes trong :numref:sec_naive_bayes
, với giải pháp là cộng thêm một hằng số nhỏ vào tất cả các số đếm như sau
Ở đây các hệ số Wood.Gasthaus.Archambeau.ea.2011
để biết thêm chi tiết.
Thật không may, các mô hình như vậy là bất khả thi vì những lý do sau.
Đầu tiên, chúng ta cần lưu trữ tất cả các số đếm.
Thứ hai, các mô hình hoàn toàn bỏ qua ý nghĩa của các từ.
Chẳng hạn, danh từ “mèo”(“cat") và tính từ “thuộc về mèo”(“feline”) nên xuất hiện trong các ngữ cảnh có liên quan đến nhau.
Rất khó để thêm các ngữ cảnh bổ trợ vào các mô hình đó, trong khi các mô hình ngôn ngữ dựa trên học sâu hoàn toàn có thể làm được.
Cuối cùng, các chuỗi từ dài gần như hoàn toàn mới lạ, do đó một mô hình chỉ đơn giản đếm tần số của các chuỗi từ đã thấy trước đó sẽ hoạt động rất kém.
Trước khi thảo luận các giải pháp sử dụng học sâu, chúng ta sẽ giải thích một số thuật ngữ và khái niệm.
Hãy nhớ lại mô hình Markov đề cập ở phần trước,
và áp dụng để mô hình hóa ngôn ngữ.
Một phân phối trên các chuỗi thỏa mãn điều kiện Markov bậc nhất nếu
Các công thức xác suất liên quan đến một, hai và ba biến được gọi là các mô hình unigram, bigram và trigram. Sau đây, chúng ta sẽ tìm hiểu cách thiết kế các mô hình tốt hơn.
Hãy cùng xem mô hình hoạt động thế nào trên dữ liệu thực tế.
Chúng ta sẽ xây dựng bộ từ vựng dựa trên tập dữ liệu "cỗ máy thời gian" tương tự như ở :numref:sec_text_preprocessing
và in ra
from d2l import mxnet as d2l
from mxnet import np, npx
import random
npx.set_np()
tokens = d2l.tokenize(d2l.read_time_machine())
vocab = d2l.Vocab(tokens)
print(vocab.token_freqs[:10])
Có thể thấy những từ xuất hiện nhiều nhất không có gì đáng chú ý.
Các từ này được gọi là từ dừng (stop words) và vì thế chúng thường được lọc ra.
Dù vậy, những từ này vẫn có nghĩa và ta vẫn sẽ sử dụng chúng.
Tuy nhiên, rõ ràng là tần số của từ suy giảm khá nhanh.
Từ phổ biến thứ
freqs = [freq for token, freq in vocab.token_freqs]
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
xscale='log', yscale='log')
Chúng ta đang tiến gần tới một đặc điểm cơ bản: tần số của từ suy giảm nhanh chóng theo một cách được xác định rõ. Ngoại trừ bốn từ đầu tiên ('the', 'i', 'and', 'of'), tất cả các từ còn lại đi theo một đường thẳng trên biểu đồ thang log. Theo đó các từ tuân theo định luật Zipf, tức là tần suất xuất hiện của từ được xác định bởi
Điều này khiến chúng ta cần suy nghĩ kĩ khi mô hình hóa các từ bằng cách đếm và kỹ thuật làm mượt. Rốt cuộc, chúng ta sẽ ước tính quá cao những từ có tần suất xuất hiện thấp. Vậy còn các tổ hợp từ khác như 2-gram, 3-gram và nhiều hơn thì sao? Hãy xem liệu tần số của bigram có tương tự như unigram hay không.
bigram_tokens = [[pair for pair in zip(
line[:-1], line[1:])] for line in tokens]
bigram_vocab = d2l.Vocab(bigram_tokens)
print(bigram_vocab.token_freqs[:10])
Có một điều đáng chú ý ở đây. 9 trong số 10 cặp từ thường xuyên xuất hiện là các từ dừng và chỉ có một là liên quan đến cuốn sách --- cặp từ "the time". Hãy xem tần số của trigram có tương tự hay không.
trigram_tokens = [[triple for triple in zip(line[:-2], line[1:-1], line[2:])]
for line in tokens]
trigram_vocab = d2l.Vocab(trigram_tokens)
print(trigram_vocab.token_freqs[:10])
Cuối cùng, hãy quan sát biểu đồ tần số token của các mô hình: unigram, bigram, và trigram.
bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs]
trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs]
d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token',
ylabel='frequency', xscale='log', yscale='log',
legend=['unigram', 'bigram', 'trigram'])
Có vài điều khá thú vị ở biểu đồ này. Thứ nhất, ngoài unigram, các cụm từ cũng tuân theo định luật Zipf, với số mũ thấp hơn tùy vào chiều dài cụm từ. Thứ hai, số lượng các n-gram độc nhất là không nhiều. Điều này có thể liên quan đến số lượng lớn các cấu trúc trong ngôn ngữ. Thứ ba, rất nhiều n-gram hiếm khi xuất hiện, khiến phép làm mượt Laplace không thích hợp để xây dựng mô hình ngôn ngữ. Thay vào đó, chúng ta sẽ sử dụng các mô hình học sâu.
Giả sử cần sử dụng mạng nơ-ron để huấn luyện mô hình ngôn ngữ.
Với tính chất tuần tự của dữ liệu chuỗi, làm thế nào để đọc ngẫu nhiên các mini-batch gồm các mẫu và nhãn?
Ví dụ đơn giản trong :numref:sec_sequence
đã giới thiệu một cách thực hiện.
Hãy tổng quát hóa cách làm này một chút.
:numref:fig_timemachine_5gram
, biểu diễn các cách để chia một câu thành các 5-gram, ở đây mỗi token là một ký tự.
Ta có thể chọn tùy ý độ dời ở vị trí bắt đầu.
Chúng ta nên chọn giá trị độ dời nào? Trong thực tế, tất cả các giá trị đó đều tốt như nhau.
Nhưng nếu chọn tất cả các giá trị độ dời, dữ liệu sẽ khá dư thừa do trùng lặp lẫn nhau, đặc biệt trong trường hợp các chuỗi rất dài.
Việc chỉ chọn một tập ngẫu nhiên các vị trí đầu cũng không tốt vì không đảm bảo sẽ bao quát đồng đều cả mảng.
Ví dụ, nếu lấy ngẫu nhiên có hoàn lại
Đoạn mã sau tạo ngẫu nhiên một minibatch dữ liệu.
Ở đây, kích thước batch batch_size
biểu thị số mẫu trong mỗi minibatch, num_steps
biểu thị chiều dài mỗi mẫu (là số bước thời gian trong trường hợp chuỗi thời gian).
Trong phép lấy mẫu ngẫu nhiên, mỗi mẫu là một chuỗi tùy ý được lấy ra từ chuỗi gốc.
Hai minibatch ngẫu nhiên liên tiếp không nhất thiết phải liền kề nhau trong chuỗi góc.
Mục tiêu của ta là dự đoán phần tử tiếp theo dựa trên các phần tử đã thấy cho đến hiện tại, do đó nhãn của một mẫu chính là mẫu đó dịch chuyển sang phải một phần tử.
# Saved in the d2l package for later use
def seq_data_iter_random(corpus, batch_size, num_steps):
# Offset the iterator over the data for uniform starts
corpus = corpus[random.randint(0, num_steps):]
# Subtract 1 extra since we need to account for label
num_examples = ((len(corpus) - 1) // num_steps)
example_indices = list(range(0, num_examples * num_steps, num_steps))
random.shuffle(example_indices)
def data(pos):
# This returns a sequence of the length num_steps starting from pos
return corpus[pos: pos + num_steps]
# Discard half empty batches
num_batches = num_examples // batch_size
for i in range(0, batch_size * num_batches, batch_size):
# Batch_size indicates the random examples read each time
batch_indices = example_indices[i:(i+batch_size)]
X = [data(j) for j in batch_indices]
Y = [data(j + 1) for j in batch_indices]
yield np.array(X), np.array(Y)
Hãy tạo ra một chuỗi từ 0 đến 29, rồi sinh các minibatch từ chuỗi đó với kích thước batch là 2 và số bước thời gian là 6.
Nghĩa là tùy vào độ dời, ta có thể sinh tối đa 4 hoặc 5 cặp
my_seq = list(range(30))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY:', Y)
Ngoài phép lấy mẫu ngẫu nhiên từ chuỗi gốc, chúng ta cũng có thể làm hai minibatch ngẫu nhiên liên tiếp có vị trí liền kề nhau trong chuỗi gốc.
# Saved in the d2l package for later use
def seq_data_iter_consecutive(corpus, batch_size, num_steps):
# Offset for the iterator over the data for uniform starts
offset = random.randint(0, num_steps)
# Slice out data - ignore num_steps and just wrap around
num_indices = ((len(corpus) - offset - 1) // batch_size) * batch_size
Xs = np.array(corpus[offset:offset+num_indices])
Ys = np.array(corpus[offset+1:offset+1+num_indices])
Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
num_batches = Xs.shape[1] // num_steps
for i in range(0, num_batches * num_steps, num_steps):
X = Xs[:, i:(i+num_steps)]
Y = Ys[:, i:(i+num_steps)]
yield X, Y
Sử dụng các đối số như ở trên, ta sẽ in đầu vào X
và nhãn Y
cho mỗi minibatch sau khi phân tách tuần tự.
Hai minibatch liên tiếp sẽ có vị trí trên chuỗi ban đầu liền kề nhau.
for X, Y in seq_data_iter_consecutive(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY:', Y)
Hãy gộp hai hàm lấy mẫu theo hai cách trên vào một lớp để duyệt dữ liệu trong Gluon ở các phần sau.
# Saved in the d2l package for later use
class SeqDataLoader:
"""A iterator to load sequence data."""
def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
if use_random_iter:
self.data_iter_fn = d2l.seq_data_iter_random
else:
self.data_iter_fn = d2l.seq_data_iter_consecutive
self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)
self.batch_size, self.num_steps = batch_size, num_steps
def __iter__(self):
return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)
Cuối cùng, ta sẽ viết hàm load_data_time_machine
trả về cả iterator dữ liệu và bộ từ vựng để sử dụng như các hàm load_data
khác.
# Saved in the d2l package for later use
def load_data_time_machine(batch_size, num_steps, use_random_iter=False,
max_tokens=10000):
data_iter = SeqDataLoader(
batch_size, num_steps, use_random_iter, max_tokens)
return data_iter, data_iter.vocab
- Mô hình ngôn ngữ là một kĩ thuật quan trọng trong xử lý ngôn ngữ tự nhiên.
-
$n$ -gram là một mô hình khá tốt để xử lý các chuỗi dài bằng cách cắt giảm số phụ thuộc. - Vấn đề của các chuỗi dài là chúng rất hiếm hoặc thậm chí không bao giờ xuất hiện.
- Định luật Zipf không chỉ mô tả phân phối từ 1-gram mà còn cả các
$n$ -gram khác. - Có nhiều cấu trúc trong ngôn ngữ nhưng tần suất xuất hiện lại không đủ cao, để xử lý các tổ hợp từ hiếm ta sử dụng làm mượt Laplace.
- Hai giải pháp chủ yếu cho bài toán phân tách chuỗi là lấy mẫu ngẫu nhiên và phân tách tuần tự.
- Nếu tài liệu đủ dài, việc lãng phí một chút và loại bỏ các minibatch rỗng một nửa là điều chấp nhận được.
- Giả sử có
$100.000$ từ trong tập dữ liệu huấn luyện. Mô hình 4-gram cần phải lưu trữ bao nhiêu tần số của từ đơn và cụm từ liền kề? - Hãy xem lại các ước lượng xác suất đã qua làm mượt. Tại sao chúng không chính xác? Gợi ý: chúng ta đang xử lý một chuỗi liền kề chứ không phải riêng lẻ.
- Bạn sẽ mô hình hóa một cuộc đối thoại như thế nào?
- Hãy ước tính luỹ thừa của định luật Zipf cho 1-gram, 2-gram, và 3-gram.
- Hãy thử tìm các cách lấy mẫu minibatch khác.
- Tại sao việc lấy giá trị độ dời ngẫu nhiên lại là một ý tưởng hay?
- Liệu việc đó có làm các chuỗi dữ liệu văn bản tuân theo phân phối đều một cách hoàn hảo không?
- Phải làm gì để có phân phối đều hơn?
- Những vấn đề gì sẽ nảy sinh khi lấy mẫu minibatch từ một câu hoàn chỉnh? Có lợi ích gì khi lấy mẫu một câu hoàn chỉnh?
Bản dịch trong trang này được thực hiện bởi:
- Đoàn Võ Duy Thanh
- Nguyễn Văn Cường
- Lê Khắc Hồng Phúc
- Nguyễn Lê Quang Nhật
- Đinh Đắc
- Nguyễn Văn Quang
- Phạm Hồng Vinh
- Nguyễn Cảnh Thướng
- Phạm Minh Đức