8.3. Mô hình Ngôn ngữ và Tập dữ liệu

Section 8.2 đã 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 \(T\) có dãy token là \(x_1, x_2, \ldots, x_T\), thì \(x_t\)(\(1 \leq t \leq T\)) có thể coi là đầu ra (hoặc nhãn) tại bước thời gian \(t\). Khi đã có chuỗi thời gian trên, mục tiêu của mô hình ngôn ngữ là ước tính xác suất của

(8.3.1)\[p(x_1, x_2, \ldots, x_T).\]

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ừ \(w_t\) tại thời điểm \(t\) với \(w_t \sim p(w_t \mid w_{t-1}, \ldots, w_1)\). Khác hoàn toàn với việc chỉ gõ phím ngẫu nhiên như trong định lý con khỉ vô hạn (infinite monkey theorem), văn bản được sinh ra từ mô hình này giống ngôn ngữ tự nhiên, giống tiếng Anh chẳng hạn. Hơn nữa, mô hình đủ khả năng tạo ra một đoạn hội thoại có ý nghĩa mà chỉ cần dựa vào đoạn hội thoại trước đó. Trên thực tế, còn rất xa để thiết kế được hệ thống như vậy, vì mô hình sẽ cần hiểu văn bản hơn là chỉ tạo ra nội dung đúng ngữ pháp.

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.

8.3.1. Ước tính một Mô hình Ngôn 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:

(8.3.2)\[p(w_1, w_2, \ldots, w_T) = p(w_1) \prod_{t=2}^T p(w_t \mid w_1, \ldots, w_{t-1}).\]

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:

(8.3.3)\[p(\mathrm{Statistics}, \mathrm{is}, \mathrm{fun}, \mathrm{.}) = p(\mathrm{Statistics}) p(\mathrm{is} \mid \mathrm{Statistics}) p(\mathrm{fun} \mid \mathrm{Statistics}, \mathrm{is}) p(\mathrm{.} \mid \mathrm{Statistics}, \mathrm{is}, \mathrm{fun}).\]

Để 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ụ, \(p(\mathrm{Statistics})\) có thể được tính là xác suất của bất kỳ câu nào bắt đầu bằng “statistics”. Một cách thiếu chính xác hơn là đếm tất cả số lần xuất hiện của ”statistics” và chia số lần đó cho tổng số từ trong kho ngữ liệu văn bản. Cách làm này khá hiệu quả, đặc biệt là với các từ xuất hiện thường xuyên. Tiếp theo, ta tính

(8.3.4)\[\hat{p}(\mathrm{is} \mid \mathrm{Statistics}) = \frac{n(\mathrm{Statistics, is})}{n(\mathrm{Statistics})}.\]

Ở đây \(n(w)\)\(n(w, w')\) lần lượt là số lần xuất hiện của các từ đơn và cặp từ ghép. Đáng tiếc là việc ước tính xác suất của một cặp từ thường khó khăn hơn, bởi vì sự xuất hiện của cặp từ “Statistics is” hiếm khi xảy ra hơn. Đặc biệt, với các cụm từ ít đi cùng nhau, rất khó tìm đủ số lần xuất hiện để ước tính chính xác. Mọi thứ thậm chí sẽ khó hơn đối với các cụm ba từ trở lên. Sẽ có nhiều cụm ba từ hợp lý mà hầu như không hề xuất hiện trong tập dữ liệu. Trừ khi có giải pháp để đánh trọng số khác không cho các tổ hợp từ đó, nếu không sẽ không thể sử dụng chúng trong một mô hình ngôn ngữ. Nếu kích thước tập dữ liệu nhỏ hoặc nếu các từ rất hiếm, chúng ta thậm chí có thể không tìm thấy nổi một lần xuất hiện của các tổ hợp từ đó.

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 Section 18.9, 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

(8.3.5)\[\begin{split}\begin{aligned} \hat{p}(w) & = \frac{n(w) + \epsilon_1/m}{n + \epsilon_1}, \\ \hat{p}(w' \mid w) & = \frac{n(w, w') + \epsilon_2 \hat{p}(w')}{n(w) + \epsilon_2}, \\ \hat{p}(w'' \mid w',w) & = \frac{n(w, w',w'') + \epsilon_3 \hat{p}(w',w'')}{n(w, w') + \epsilon_3}. \end{aligned}\end{split}\]

Ở đây các hệ số \(\epsilon_i > 0\) xác định mức độ ảnh hưởng của chuỗi ngắn hơn khi ước tính chuỗi dài hơn, \(m\) là tổng số từ trong tập văn bản. Công thức trên là một biến thể khá nguyên thủy của kỹ thuật làm mượt Kneser-Ney và Bayesian phi tham số. Xem [Wood et al., 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.

8.3.2. Mô hình Markov và \(n\)-grams

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 \(p(w_{t+1} \mid w_t, \ldots, w_1) = p(w_{t+1} \mid w_t)\). Những bậc cao hơn tương ứng với những chuỗi phụ thuộc dài hơn. Do đó chúng ta có thể áp dụng các phép xấp xỉ để mô hình hóa một chuỗi:

(8.3.6)\[\begin{split}\begin{aligned} p(w_1, w_2, w_3, w_4) &= p(w_1) p(w_2) p(w_3) p(w_4),\\ p(w_1, w_2, w_3, w_4) &= p(w_1) p(w_2 \mid w_1) p(w_3 \mid w_2) p(w_4 \mid w_3),\\ p(w_1, w_2, w_3, w_4) &= p(w_1) p(w_2 \mid w_1) p(w_3 \mid w_1, w_2) p(w_4 \mid w_2, w_3). \end{aligned}\end{split}\]

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.

8.3.3. Thống kê Ngôn ngữ Tự nhiê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ư ở Section 8.2 và in ra \(10\) từ có tần suất xuất hiện cao nhất.

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])
[('the', 2261), ('', 1282), ('i', 1267), ('and', 1245), ('of', 1155), ('a', 816), ('to', 695), ('was', 552), ('in', 541), ('that', 443)]

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ứ \(10\) xuất hiện ít hơn, chỉ bằng \(1/5\) lần so với từ phổ biến nhất. Để hiểu rõ hơn, chúng ta sẽ vẽ đồ thị tần số của từ.

freqs = [freq for token, freq in vocab.token_freqs]
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
         xscale='log', yscale='log')
../_images/output_language-models-and-dataset_vn_c9ad2e_3_0.svg

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

(8.3.7)\[n(x) \propto (x + c)^{-\alpha} \text{ và~do~đó } \log n(x) = -\alpha \log (x+c) + \mathrm{const.}\]

Đ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])
[(('of', 'the'), 297), (('in', 'the'), 161), (('i', 'had'), 126), (('and', 'the'), 104), (('i', 'was'), 104), (('the', 'time'), 97), (('it', 'was'), 94), (('to', 'the'), 81), (('as', 'i'), 75), (('of', 'a'), 69)]

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])
[(('the', 'time', 'traveller'), 53), (('the', 'time', 'machine'), 24), (('the', 'medical', 'man'), 22), (('it', 'seemed', 'to'), 14), (('it', 'was', 'a'), 14), (('i', 'began', 'to'), 13), (('i', 'did', 'not'), 13), (('i', 'saw', 'the'), 13), (('here', 'and', 'there'), 12), (('i', 'could', 'see'), 12)]

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'])
../_images/output_language-models-and-dataset_vn_c9ad2e_9_0.svg

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.

8.3.4. Chuẩn bị Dữ liệu Huấn luyện

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 Section 8.1 đã 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.

Fig. 8.3.1, 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.

../_images/timemachine-5gram.svg

Fig. 8.3.1 Các độ dời khác nhau dẫn đến các chuỗi con khác nhau khi phân tách văn bản.

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 \(n\) phần tử từ một tập có \(n\) phần tử, xác suất một phần tử cụ thể không được chọn là \((1-1/n)^n \to e^{-1}​\). Nghĩa là ta không thể kỳ vọng vào sự bao quát đồng đều, ngay cả khi hoán vị ngẫu nhiên một tập giá trị độ dời. Thay vào đó, có thể sử dụng một cách đơn giản để có được cả tính bao quát và tính ngẫu nhiên, đó là: chọn một độ dời ngẫu nhiên, sau đó sử dụng tuần tự các giá trị tiếp theo. Điều này được mô tả trong phép lấy mẫu ngẫu nhiên và phép phân tách tuần tự dưới đây.

8.3.4.1. Lấy Mẫu Ngẫu nhiên

Đ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 \((x, y)\). Với kích thước batch bằng 2, ta thu được 2 minibatch.

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)
X:  [[ 0.  1.  2.  3.  4.  5.]
 [18. 19. 20. 21. 22. 23.]]
Y: [[ 1.  2.  3.  4.  5.  6.]
 [19. 20. 21. 22. 23. 24.]]
X:  [[12. 13. 14. 15. 16. 17.]
 [ 6.  7.  8.  9. 10. 11.]]
Y: [[13. 14. 15. 16. 17. 18.]
 [ 7.  8.  9. 10. 11. 12.]]

8.3.4.2. Phân tách Tuần tự

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)
X:  [[ 1.  2.  3.  4.  5.  6.]
 [15. 16. 17. 18. 19. 20.]]
Y: [[ 2.  3.  4.  5.  6.  7.]
 [16. 17. 18. 19. 20. 21.]]
X:  [[ 7.  8.  9. 10. 11. 12.]
 [21. 22. 23. 24. 25. 26.]]
Y: [[ 8.  9. 10. 11. 12. 13.]
 [22. 23. 24. 25. 26. 27.]]

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

8.3.5. Tóm tắt

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

8.3.6. Bài tập

  1. 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ề?
  2. 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ẻ.
  3. Bạn sẽ mô hình hóa một cuộc đối thoại như thế nào?
  4. Hãy ước tính luỹ thừa của định luật Zipf cho 1-gram, 2-gram, và 3-gram.
  5. Hãy thử tìm các cách lấy mẫu minibatch khác.
  6. 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?
  7. 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?

8.3.7. Thảo luận

8.3.8. Những người thực hiện

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