15.2. Phân tích Cảm xúc: Sử dụng Mạng Nơ-ron Hồi tiếp

Tương tự như tìm kiếm các từ đồng nghĩa và loại suy, phân loại văn bản cũng là một tác vụ xuôi dòng của embedding từ. Trong phần này, ta sẽ áp dụng các vector từ đã được tiền huấn luyện (GloVe) và mạng nơ-ron truy hồi hai chiều với nhiều lớp ẩn [Maas et al., 2011], như được minh họa trong Fig. 15.2.1. Ta sẽ sử dụng mô hình này để xác định xem một chuỗi văn bản có độ dài không xác định chứa cảm xúc tích cực hay tiêu cực.

../_images/nlp-map-sa-rnn.svg

Fig. 15.2.1 Phần này sẽ truyền các vector GloVe đã được tiền huấn luyện vào một kiến trúc RNN cho bài toán phân tích cảm xúc.

from d2l import mxnet as d2l
from mxnet import gluon, init, np, npx
from mxnet.gluon import nn, rnn
npx.set_np()

batch_size = 64
train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)

15.2.1. Sử dụng Mạng Nơ-ron Hồi tiếp

Trong mô hình này, đầu tiên mỗi từ nhận được một vector đặc trưng tương ứng từ tầng embedding. Sau đó, ta mã hóa thêm chuỗi đặc trưng bằng cách sử dụng mạng nơ-ron hồi tiếp hai chiều để thu được thông tin chuỗi. Cuối cùng, ta chuyển đổi thông tin chuỗi được mã hóa thành đầu ra thông qua tầng kết nối đầy đủ. Cụ thể, ta có thể ghép nối các trạng thái ẩn của bộ nhớ ngắn hạn dài hai chiều (bidirectional long-short term memory) ở bước thời gian ban đầu và bước thời gian cuối cùng và truyền nó tới tầng phân loại đầu ra như là đặc trưng mã hóa của thông tin chuỗi. Trong lớp BiRNN được lập trình bên dưới, thực thể Embedding là tầng embedding, thực thể LSTM là tầng ẩn để mã hóa chuỗi, và thực thể Dense là tầng đầu ra sinh kết quả phân loại.

class BiRNN(nn.Block):
    def __init__(self, vocab_size, embed_size, num_hiddens,
                 num_layers, **kwargs):
        super(BiRNN, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        # Set `bidirectional` to True to get a bidirectional recurrent neural
        # network
        self.encoder = rnn.LSTM(num_hiddens, num_layers=num_layers,
                                bidirectional=True, input_size=embed_size)
        self.decoder = nn.Dense(2)

    def forward(self, inputs):
        # The shape of `inputs` is (batch size, no. of words). Because LSTM
        # needs to use sequence as the first dimension, the input is
        # transformed and the word feature is then extracted. The output shape
        # is (no. of words, batch size, word vector dimension).
        embeddings = self.embedding(inputs.T)
        # Since the input (embeddings) is the only argument passed into
        # rnn.LSTM, it only returns the hidden states of the last hidden layer
        # at different time step (outputs). The shape of `outputs` is
        # (no. of words, batch size, 2 * no. of hidden units).
        outputs = self.encoder(embeddings)
        # Concatenate the hidden states of the initial time step and final
        # time step to use as the input of the fully connected layer. Its
        # shape is (batch size, 4 * no. of hidden units)
        encoding = np.concatenate((outputs[0], outputs[-1]), axis=1)
        outs = self.decoder(encoding)
        return outs

Ta sẽ tạo một mạng nơ-ron hồi tiếp hai chiều với hai tầng ẩn như sau.

embed_size, num_hiddens, num_layers, devices = 100, 100, 2, d2l.try_all_gpus()
net = BiRNN(len(vocab), embed_size, num_hiddens, num_layers)
net.initialize(init.Xavier(), ctx=devices)

15.2.1.1. Nạp các Vector Từ đã qua Tiền huấn luyện

Bởi vì tập dữ liệu huấn luyện cho việc phân loại cảm xúc không quá lớn, để xử lý vấn đề quá khớp, ta sẽ dùng trực tiếp các vector từ đã được tiền huấn luyện trên tập ngữ liệu lớn hơn làm các vector đặc trưng cho tất cả các từ. Ở đây, ta nạp vector từ Glove 100-chiều cho mỗi từ trong từ điển vocab.

glove_embedding = d2l.TokenEmbedding('glove.6b.100d')

Truy vấn các vector từ nằm trong từ vựng của chúng ta.

embeds = glove_embedding[vocab.idx_to_token]
embeds.shape
(49339, 100)

Tiếp theo, ta sử dụng các vector từ đó làm vector đặc trưng cho mỗi từ trong các đánh giá. Lưu ý là các chiều của vector từ đã qua tiền huấn luyện cần nhất quán với kích thước đầu ra embed_size của tầng embedding trong mô hình đã tạo. Thêm vào đó, ta không còn cập nhật các vector từ này trong suốt quá trình huấn luyện.

net.embedding.weight.set_data(embeds)
net.embedding.collect_params().setattr('grad_req', 'null')

15.2.1.2. Huấn luyện và Đánh giá Mô hình

Bây giờ ta có thể bắt đầu thực hiện huấn luyện.

lr, num_epochs = 0.01, 5
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr})
loss = gluon.loss.SoftmaxCrossEntropyLoss()
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)
loss 0.283, train acc 0.881, test acc 0.859
1451.3 examples/sec on [gpu(0)]
../_images/output_sentiment-analysis-rnn_vn_1038ca_13_1.svg

Cuối cùng, định nghĩa hàm dự đoán.

#@save
def predict_sentiment(net, vocab, sentence):
    sentence = np.array(vocab[sentence.split()], ctx=d2l.try_gpu())
    label = np.argmax(net(sentence.reshape(1, -1)), axis=1)
    return 'positive' if label == 1 else 'negative'

Tiếp theo, sử dụng mô hình đã huấn luyện để phân loại cảm xúc cho hai câu đơn giản.

predict_sentiment(net, vocab, 'this movie is so great')
'positive'
predict_sentiment(net, vocab, 'this movie is so bad')
'negative'

15.2.2. Tóm tắt

  • Phân loại văn bản ánh xạ một chuỗi văn bản có độ dài không xác định thành hạng mục tương ứng của văn bản đó. Đây là một tác vụ xuôi dòng của embedding từ.
  • Ta có thể áp dụng các vector từ được tiền huấn luyện và mạng nơ-ron hồi tiếp để để phân loại cảm xúc trong văn bản.

15.2.3. Bài tập

  1. Hãy tăng số epoch. Bạn có thể đạt được độ chính xác là bao nhiêu trên tập huấn luyện và tập kiểm tra? Thử tinh chỉnh các siêu tham số khác và đánh giá kết quả.
  2. Liệu sử dụng vector từ được tiền huấn luyện có kích thước lớn hơn, ví dụ vector từ GloVe có kích thước chiều là 300, có thể cải thiện độ chính xác hay không?
  3. Ta có thể cải thiện độ chính xác bằng cách sử dụng công cụ token hoá từ spaCy không? Bạn cần cài đặt spaCy bằng lệnh pip install spacy và cài đặt gói ngôn ngữ tiếng Anh bằng lệnh python -m spacy download en. Trong mã nguồn, đầu tiên hãy nhập thư viện spaCy với câu lệnh import spacy. Tiếp theo, hãy nạp gói spacy tiếng Anh spacy_en = spacy.load('en'). Cuối cùng, hãy định nghĩa hàm def tokenizer(text): return [tok.text for tok in spacy_en.tokenizer(text)] và thay thế hàm tokenizer ban đầu. Lưu ý rằng vector từ GloVe sử dụng “-” để kết nối mỗi từ trong cụm danh từ. Ví dụ, cụm từ “new york” được biểu diễn bằng “new-york” trong GloVe. Sau khi sử dụng công cụ token hoá spaCy, “new york” có thể sẽ được lưu thành “new york”.

15.2.4. Thảo luận

15.2.5. 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 Quang
  • Nguyễn Mai Hoàng Long
  • Nguyễn Lê Quang Nhật
  • Phạm Hồng Vinh
  • Phạm Minh Đức

Lần cập nhật gần nhất: 26/09/2020. (Cập nhật lần cuối từ nội dung gốc: 29/08/2020)