15.3. Phân tích Cảm xúc: Sử dụng Mạng Nơ-ron Tích Chập

Trong Section 6, chúng ta đã tìm hiểu cách xử lý dữ liệu ảnh hai chiều với mạng nơ-ron tích chập hai chiều. Ở chương trước về các mô hình ngôn ngữ và các tác vụ phân loại văn bản, ta coi dữ liệu văn bản như là dữ liệu chuỗi thời gian với chỉ một chiều duy nhất, và vì vậy, chúng sẽ được xử lí bằng mạng nơ-ron hồi tiếp. Thực tế, ta cũng có thể coi văn bản như một bức ảnh một chiều, và sử dụng mạng nơ-ron tích chập một chiều để tìm ra mối liên kết giữa những từ liền kề nhau. Như mô tả trong Fig. 15.3.1, chương này sẽ miêu tả một hướng tiếp cận đột phá bằng cách áp dụng mạng nơ-ron tích chập để phân tích cảm xúc: textCNN [Kim, 2014].

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

Fig. 15.3.1 Phần này truyền mô hình tiền huấn luyện GloVe vào một kiến trúc mạng nơ-ron tích chập cho tác vụ phân loại cảm xúc

Đầu tiên, nhập những gói thư viện và mô-đun cần thiết cho thử nghiệm.

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

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

15.3.1. Mạng Nơ-ron Tích chập Một chiều

Trước khi giới thiệu mô hình, chúng ta hãy xem mạng nơ-ron tích chập một chiều họat động như thế nào. Tương tự như mạng nơ-ron tích chập hai chiều, mạng nơ-ron tích chập một chiều sử dụng phép tính tương quan chéo một chiều. Trong phép tính tương quan chéo một chiều, cửa sổ tích chập bắt đầu từ phía ngoài cùng bên trái của mảng đầu vào và trượt lần lượt từ trái qua phải. Xét trên một vị trí nhất định của cửa sổ tích chập khi trượt, ta nhân từng phần tử của mảng đầu vào con trong cửa sổ đó với mảng hạt nhân rồi cộng lại để lấy được phần tử ở vị trí tương ứng trong mảng đầu ra. Như ví dụ ở Fig. 15.3.2, đầu vào là một mảng một chiều với độ rộng là 7 và độ rộng của mảng hạt nhân là 2. Ta có thể thấy rằng độ rộng của đầu ra là \(7-2+1=6\) và phần tử đầu tiên được tính bằng cách nhân theo từng phần tử mảng đầu vào con chứa 2 phần tử ngoài cùng bên trái với mảng hạt nhân, rồi cộng lại với nhau.

../_images/conv1d.svg

Fig. 15.3.2 Phép tính tương quan chéo một chiều. Những vùng in đậm là phần tử đầu ra đầu tiên, cùng phần tử đầu vào và mảng hạt nhân được dùng trong phép tính đó: \(0\times1+1\times2=2\).

Tiếp theo, chúng ta sẽ lập trình phép tương quan chéo một chiều trong hàm corr1d. Hàm này nhận mảng đầu vào X và mảng hạt nhân K và cho ra đầu ra là mảng Y.

def corr1d(X, K):
    w = K.shape[0]
    Y = np.zeros((X.shape[0] - w + 1))
    for i in range(Y.shape[0]):
        Y[i] = (X[i: i + w] * K).sum()
    return Y

Bây giờ chúng ta sẽ tái tạo lại kết quả của phép tính tương quan chéo một chiều ở Fig. 15.3.2.

X, K = np.array([0, 1, 2, 3, 4, 5, 6]), np.array([1, 2])
corr1d(X, K)
array([ 2.,  5.,  8., 11., 14., 17.])

Phép tính tương quan chéo một chiều cho nhiều kênh đầu vào cũng tương tự như phép tương quan chéo hai chiều cho nhiều kênh đầu vào. Với mỗi kênh, toán tử này thực hiện phép tính tương quan chéo một chiều trên từng hạt nhân và đầu vào tương ứng, và cộng các kết quả trên từng kênh lại với nhau để thu được đầu ra. Fig. 15.3.3 minh họa phép tính tương quan chéo một chiều với ba kênh đầu vào.

../_images/conv1d-channel.svg

Fig. 15.3.3 Phép tính tương quan chéo một chiều với ba kênh đầu vào. Những vùng được in đậm là phần tử đầu ra thứ nhất cũng như đầu vào và các phần tử của mảng hạt nhân được sử dụng trong phép tính: \(0\times1+1\times2+1\times3+2\times4+2\times(-1)+3\times(-3)=2\).

Bây giờ, ta sẽ tái tạo lại kết quả của phép tính tương quan chéo một chiều với đa kênh đầu vào trong Fig. 15.3.3.

def corr1d_multi_in(X, K):
    # First, we traverse along the 0th dimension (channel dimension) of `X`
    # and `K`. Then, we add them together by using * to turn the result list
    # into a positional argument of the `add_n` function
    return sum(corr1d(x, k) for x, k in zip(X, K))

X = np.array([[0, 1, 2, 3, 4, 5, 6],
              [1, 2, 3, 4, 5, 6, 7],
              [2, 3, 4, 5, 6, 7, 8]])
K = np.array([[1, 2], [3, 4], [-1, -3]])
corr1d_multi_in(X, K)
array([ 2.,  8., 14., 20., 26., 32.])

Định nghĩa phép tính tương quan chéo hai chiều cho ta thấy phép tính tương quan chéo một chiều với đa kênh đầu vào có thể được coi là phép tính tương quan chéo hai chiều với một kênh đầu vào. Như minh họa trong Fig. 15.3.4, ta có thể biểu diễn phép tính tương quan chéo một chiều với đa kênh đầu vào trong Fig. 15.3.3 tương tự như phép tính tương quan chéo hai chiều với một kênh đầu vào. Ở đây, chiều cao của hạt nhân bằng với chiều cao của đầu vào.

../_images/conv1d-2d.svg

Fig. 15.3.4 Phép tính tương quan chéo hai chiều với một kênh đầu vào. Vùng được tô đậm là phần tử đầu ra thứ nhất và đầu vào cũng như các phần tử của mảng hạt nhân được sử dụng trong phép tính: \(2\times(-1)+3\times(-3)+1\times3+2\times4+0\times1+1\times2=2\).

Cả hai đầu ra trong Fig. 15.3.2Fig. 15.3.3 chỉ có một kênh. Ta đã thảo luận cách chỉ định đa kênh đầu ra trong tầng tích chập hai chiều tại Section 6.4. Tương tự, ta cũng có thể chỉ định đa kênh đầu ra trong tầng tích chập một chiều để mở rộng các tham số mô hình trong tầng tích chập đó.

15.3.2. Tầng Gộp Cực đại Theo Thời gian

Tương tự, ta có tầng gộp một chiều. Tầng gộp cực đại theo thời gian được dùng trong TextCNN thực chất tương tự như tầng gộp cực đại toàn cục một chiều. Giả sử đầu vào có nhiều kênh, mỗi kênh bao gồm các giá trị bước thời gian khác nhau, đầu ra của mỗi kênh sẽ là giá trị lớn nhất qua tất cả bước thời gian trong từng kênh. Do đó, đầu vào của tầng gộp cực đại theo thời gian có thể có số lượng bước thời gian khác nhau tại mỗi kênh.

Để cải thiện chất lượng tính toán, ta thường kết hợp những mẫu thời gian có độ dài khác nhau vào một minibatch và làm cho chiều dài theo thời gian của từng mẫu đồng nhất bằng cách thêm các ký tự đặc biệt (ví dụ 0) vào cuối những mẫu ngắn hơn. Tất nhiên, các ký tự được thêm vào không làm thay đổi bản chất ngữ nghĩa. Bởi vì, mục tiêu chính của tầng gộp cực đại theo thời gian là học được những đặc trưng quan trọng của thời gian, thông thường điều đó cho phép mô hình không bị ảnh hưởng bởi các ký tự được thêm vào thủ công.

15.3.3. Mô hình TextCNN

TextCNN chủ yếu sử dụng tầng tích chập một chiều và tầng gộp cực đại theo thời gian. Giả sử chuỗi văn bản đầu vào gồm \(n\) từ, mỗi từ được biểu diễn bởi một vector \(d\) chiều. Lúc này mẫu đầu vào có chiều rộng là \(n\), chiều cao là 1, và \(d\) kênh đầu vào. Quá trình tính toán của textCNN chủ yếu được chia thành các bước sau:

  1. Định nghĩa nhiều hạt nhân tích chập một chiều để thực hiện các phép tính tích chập trên đầu vào. Những hạt nhân tích chập với độ rộng khác nhau có thể học được sự tương quan của các cụm từ liền kề với số lượng khác nhau.
  2. Thực hiện gộp cực đại theo thời gian trên tất cả các kênh đầu ra, sau đó nối các giá trị gộp được của các kênh này thành một vector.
  3. Vector nối trên sẽ được biến đổi thành đầu ra cho từng hạng mục bằng thông qua tầng kết nối đầy đủ. Tầng dropout có thể được sử dụng ở bước này để giải quyết tình trạng quá khớp.
../_images/textcnn.svg

Fig. 15.3.5 Thiết kế TextCNN.

Fig. 15.3.5 minh họa một ví dụ cho textCNN. Đầu vào ở đây là một câu gồm 11 từ, với mỗi từ được biểu diễn bằng một vector từ 6 chiều. Vì vậy, câu đầu vào có độ rộng là 11 và số kênh đầu vào là 6. Chúng ta giả sử rằng 2 hạt nhân tích chập một chiều có độ rộng lần lượt là 2 và 4, tương ứng với số kênh đầu ra là 4 và 5. Cho nên sau phép tính tích chập một chiều, đầu ra 4 kênh có chiều rộng là là \(11-2+1=10\), trong khi đó độ rộng của đầu ra 5 kênh còn lại là \(11-4+1=8\). Thậm chí độ rộng của mỗi kênh có khác nhau đi nữa, chúng ta vẫn có thể thực hiện gộp cực đại theo thời gian cho mỗi kênh và nối đầu ra sau gộp của 9 kênh thành một vector 9 chiều. Cuối cùng, chúng ta dùng một tầng kết nối đầy đủ để biến đổi vector 9 chiều đó thành một đầu ra 2 chiều: dự đoán cảm xúc tích cực và cảm xúc tiêu cực.

Tiếp theo chúng ta bắt đầu lập trình mô hình textCNN. So với phần trước, ngoài việc thay mạng nơ-ron hồi tiếp bằng một tầng tích chập một chiều, ở đây chúng ta dùng 2 tầng embedding, một được giữ trọng số cố định và tầng còn lại tham gia quá trình huấn luyện.

class TextCNN(nn.Block):
    def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels,
                 **kwargs):
        super(TextCNN, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        # The embedding layer does not participate in training
        self.constant_embedding = nn.Embedding(vocab_size, embed_size)
        self.dropout = nn.Dropout(0.5)
        self.decoder = nn.Dense(2)
        # The max-over-time pooling layer has no weight, so it can share an
        # instance
        self.pool = nn.GlobalMaxPool1D()
        # Create multiple one-dimensional convolutional layers
        self.convs = nn.Sequential()
        for c, k in zip(num_channels, kernel_sizes):
            self.convs.add(nn.Conv1D(c, k, activation='relu'))

    def forward(self, inputs):
        # Concatenate the output of two embedding layers with shape of
        # (batch size, no. of words, word vector dimension) by word vector
        embeddings = np.concatenate((
            self.embedding(inputs), self.constant_embedding(inputs)), axis=2)
        # According to the input format required by Conv1D, the word vector
        # dimension, that is, the channel dimension of the one-dimensional
        # convolutional layer, is transformed into the previous dimension
        embeddings = embeddings.transpose(0, 2, 1)
        # For each one-dimensional convolutional layer, after max-over-time
        # pooling, an ndarray with the shape of (batch size, channel size, 1)
        # can be obtained. Use the flatten function to remove the last
        # dimension and then concatenate on the channel dimension
        encoding = np.concatenate([
            np.squeeze(self.pool(conv(embeddings)), axis=-1)
            for conv in self.convs], axis=1)
        # After applying the dropout method, use a fully connected layer to
        # obtain the output
        outputs = self.decoder(self.dropout(encoding))
        return outputs

Tạo một thực thể TextCNN có 3 tầng tích chập với chiều rộng hạt nhân là 3, 4 và 5, tất cả đều có 100 kênh đầu ra.

embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
devices = d2l.try_all_gpus()
net = TextCNN(len(vocab), embed_size, kernel_sizes, nums_channels)
net.initialize(init.Xavier(), ctx=devices)

15.3.3.1. Nạp Vector Từ đã được Tiền huấn luyện

Tương tự phần trước, ta nạp GloVe 100 chiều đã được tiền huấn luyện và khởi tạo các tầng embedding embeddingconstant_embedding. Ở đây, embedding sẽ tham gia quá trình huấn luyện trong khi constant_embedding có trọng số cố định.

glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.set_data(embeds)
net.constant_embedding.weight.set_data(embeds)
net.constant_embedding.collect_params().setattr('grad_req', 'null')

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

Bây giờ ta có thể huấn luyện mô hình.

lr, num_epochs = 0.001, 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.091, train acc 0.968, test acc 0.869
6584.2 examples/sec on [gpu(0)]
../_images/output_sentiment-analysis-cnn_vn_c71b21_15_1.svg

Dưới đây, ta sử dụng mô hình đã được huấn luyện để phân loại cảm xúc của hai câu đơn giản.

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

15.3.4. Tóm tắt

  • Ta có thể dùng tích chập một chiều để xử lý và phân tích dữ liệu theo thời gian.
  • Phép tương quan chéo một chiều đa kênh đầu vào có thể xem như phép tương quan chéo hai chiều đơn kênh đầu vào.
  • Đầu vào của tầng gộp cực đại theo thời gian có thể có số bước thời gian trên mỗi kênh khác nhau.
  • TextCNN chủ yếu sử dụng một tầng chập một chiều và một tầng gộp cực đại theo thời gian.

15.3.5. Bài tập

  1. Điều chỉnh các tham số mô hình và so sánh hai phương pháp phân tích cảm xúc giữa mạng nơ-ron truy hồi và mạng nơ-ron tích chập, xét trên khía cạnh độ chính xác và hiệu suất tính toán.
  2. Bạn có thể cải thiện thêm độ chính xác của mô hình trên tập kiểm tra thông qua việc sử dụng ba phương pháp đã được giới thiệu ở phần trước: điều chỉnh các tham số mô hình, sử dụng các vector từ tiền huấn luyện lớn hơn, và sử dụng công cụ token hóa từ spaCy.
  3. Bạn còn có thể sử dụng TextCNN cho những tác vụ xử lý ngôn ngữ tự nhiên nào khác?

15.3.6. Thảo luận

15.3.7. 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
  • Trương Lộc Phát
  • Nguyễn Văn Quang
  • Lý Phi Long
  • Nguyễn Mai Hoàng Long
  • Nguyễn Lê Quang Nhật
  • Phạm Hồng Vinh
  • Lê Khắc Hồng Phúc
  • Nguyễn Văn Cường

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: 20/09/2020)