+7

Cài đặt mô hình phân loại cảm xúc tiếng Việt

🐱 Tuy hơi trễ so với dự kiến, mình kính chúc các bạn năm Quý Mão an khang thịnh vượng nè!!!! 🎆

Để phân loại và lọc bớt các bình luận tiêu cực, hay để biết sắc thái cảm xúc của đoạn tin vừa gõ có phù hợp với mục đích của chúng ta hay không, các yêu cầu như trên đều quy về bài toán phân loại cảm xúc cho văn bản. Phân loại cảm xúc là một chủ đề phổ biến trong lĩnh vực xử lý ngôn ngữ tự nhiên (NLP) hay học sâu. Trong bài viết này, mình sẽ hướng dẫn các bước để cài đặt một mô hình học sâu cơ bản để phân loại cảm xúc cho các review phim trên IMDB đã được dịch sang tiếng Việt, với thư viện cài đặt mô hình là PyTorch.
Bài viết này mình giả sử các bạn đã có các kiến thức cơ bản về mạng nơ ron, mạng LSTM, ... nên sẽ có một số nội dung mình sẽ không giải thích kĩ, mong các bạn bỏ qua cho.

my friends

Mình có một nhóm bạn lúc nào cũng nhắn :) cho mọi tình huống, mình nghĩ đây cũng là một bài toán phân loại cảm xúc không có nhãn

1. Các bước cài đặt

Tuy tiêu đề là cài đặt mô hình học sâu, nhưng quá trình cài đặt chỉ là một bước nhỏ trong cả dự án này. Phần tốn nhiều thời gian nhất vẫn là quá trình xử lý dữ liệu.

  • Bước 1: Cài đặt class Vocabulary, chúng ta cần một class giúp chuyển các đoạn văn bản thành dạng số được lưu trong tensor, trước khi đưa vào mô hình.
  • Bước 2: Cài đặt class IMDBDatabase, sau khi chuyển dữ liệu thành dạng số, chúng ta cần sắp xếp lại dữ liệu có hệ thống để lúc truy xuất và huấn luyện được đơn giản hơn. Sau đó chia dữ liệu thành 3 tập train, valid và test.
  • Bước 3: Cài đặt mô hình hồi quy RNN, vì bước này mình lấy mô hình từ ngài bentrevett nên chỉ tốn tí thời gian để chỉnh thôi.
  • Bước 4: Huấn luyện và đánh giá, bước này chả lạ gì với các bài toán học sâu.
  • Bước 5: Kiểm tra, dùng tập test để kiểm tra lại lần cuối và kiểm tra trên các câu mà mình tự nhập.

VSA_Flow

Các bước cài đặt chính

Mình thấy trọng tâm bài này là ở bước 1 và bước 2, vì mình có tạo và sử dụng các thư viện để xử lý riêng cho tiếng Việt. Trong khi các tài liệu mình đọc phần lớn họ dùng cho tiếng Anh và các thư viện cài đặt sẵn cho tiếng Anh không dùng để kế thừa.

Bài viết này chủ yếu là giải thích mã nên với các bạn thích xem code hơn có thể dùng các link sau.

Các mã nguồn tiếp theo sẽ dựa trên Google Colab của mình
Repo dành cho các bạn cần mã nguồn dưới dạng các file .py

2. Biểu diễn từ bằng Word Embedding

Tất nhiên là bạn không thể cứ nhét một mớ chữ vào mô hình rồi bắt nó tự hiểu được. Mình cần phải chuyển các văn bản sang dạng số và lưu trong các tensor - một class rất hay dùng để làm input cho các mô hình viết bằng PyTorch.
Việc này được chia thành nhiều bài toán con mà ta thường hay gặp trong xử lý ngôn ngữ tự nhiên:

  • Tách từ: từ chuỗi văn bản, ta tách thành các từ con. Ví dụ từ chuỗi "Mình xin cảm ơn" sẽ được tách thành danh sách ["Mình", "xin", "cảm_ơn"].
  • Chuyển từ thành số: sau khi thu được danh sách các từ, ta cần chuyển thành số hoặc véc tơ để mô hình có thể thực hiện các phép toán trên đó.

Việc tách từ được thực hiện một cách dễ dàng nhờ thư viện underthesea - một thư viện chuyên dùng để hỗ trợ xử lý ngôn ngữ Tiếng Việt.

from underthesea import word_tokenize

sentence = "Với cộng đồng người Bách Việt trước đây, việc thuần hóa mèo cũng có thể theo cách thức như vậy."
print(word_tokenize(sentence))
# >> ['Với', 'cộng đồng', 'người', 'Bách', 'Việt', 'trước', 'đây', ',', 'việc', 'thuần hóa', 'mèo', 'cũng', 'có thể', 'theo', 'cách thức', 'như vậy', '.']

Việc chuyển từ thành số, mình sẽ sử dụng phương pháp word embedding đã được huấn luyện sẵn PhoW2V. Mình sẽ giải thích một tí về phương pháp này, bạn nào đã biết rồi thì có thể lướt qua đọc phần tiếp theo.

word representations

Một số phương pháp biểu diễn từ khác nhau

Cách đơn giản nhất là lưu một từ điển từ - số như {"tôi": 1, "xin_chào": 2} sau đó liên tục dò và thay thế trên câu. Tuy nhiên mô hình học máy có thể bị hiểu nhầm bởi thứ tự 1, 2, 3, ... (các số có quan hệ với nhau tăng dần trong khi các từ thì không có quan hệ như thế).
Để giải quyết tình trạng đó chúng ta sử dụng phương pháp mã hóa one-hot, giả sử chúng ta có 5 từ ["tôi", "xin_chào", "cam", "chanh", "táo"], khi đó mỗi từ sẽ được biểu diễn bởi véc tơ độ dài là 5 bao gồm các số 0 và số 1 biểu diễn vị trí của từ như "tôi": [1, 0, 0, 0, 0], "xin_chào": [0, 1, 0, 0, 0]. Bởi vì các véc tơ này có tích vô hướng là 0 nên được xem là không có quan hệ gì với nhau. Tuy nhiên các từ không hẳn là như vậy, như "đông", "tây", "nam", "bắc" sẽ có nghĩa gần nhau, để mô hình học tốt hơn ta cũng cần phải biểu diễn các ngữ nghĩa của từ chứ không phải để các từ độc lập hoàn toàn với nhau. Ngoài ra các dữ liệu huấn luyện thường sử dụng một lượng lớn từ vựng trên 10.000 từ, việc này có thể làm không gian biểu diễn véc tơ trở nên rất lớn, làm tốn không gian lưu trữ.
Phương pháp khắc phục được các hạn chế trên, cũng là phương pháp phổ biến nhất là word embedding. Tương tự với mã hóa one-hot, các từ được lưu dưới dạng các vec tơ nhưng thay vì chỉ gồm số 0 và 1 thì là các số thực dương. Ví dụ "tôi": [0.4, 0.23, 0.13, 0.58], khi đó các con số này sẽ dùng để biểu diễn ngữ nghĩa của từ. Các từ có nghĩa càng gần nhau sẽ có khoảng cách euler càng thấp. Phương pháp này chưa hẳn là hoàn hảo, vẫn gặp phải các vấn đề như thiếu từ vựng (out of vocabulary - OOV), từ vựng theo ngữ cảnh (contextual word representation). Nhưng đủ tốt để giải quyết bài toán biểu diễn từ.
Nếu các bạn thắc mắc là các con số thực trong véc tơ của từ được tạo ra từ đâu thì sẽ có hai cách. Cách thứ nhất là được huấn luyện thông qua một mạng nơ ron như Skip-Gram, CBOW, .... Cách thứ hai là được khởi tạo ngẫu nhiên và được thay đổi trong quá trình huấn luyện cho bài toán khác. Ở đây, mình sử dụng kết quả đã được huấn luyện PhoW2V bao gồm 1587507 từ với véc tơ kích thước là 100 , được lưu trong vi_word2vec.txt, để tiết kiệm thời gian và tăng độ hiệu quả huấn luyện.

import torch
import torchtext.vocab as vocab

word_embedding = vocab.Vectors(name = "vi_word2vec.txt",
                               unk_init = torch.Tensor.normal_)

word_embedding.vectors.shape
# >> torch.Size([1587507, 100])

Kiểm tra word embedding bằng tìm các từ gần nghĩa với "Việt_Nam"

def get_vector(embeddings, word):
    """ Get embedding vector of the word
    @param embeddings (torchtext.vocab.vectors.Vectors)
    @param word (str)
    @return vector (torch.Tensor)
    """
    assert word in embeddings.stoi, f'*{word}* is not in the vocab!'
    return embeddings.vectors[embeddings.stoi[word]]

def closest_words(embeddings, vector, n=10):
    """ Return n words closest in meaning to the word
    @param embeddings (torchtext.vocab.vectors.Vectors)
    @param vector (torch.Tensor)
    @param n (int)
    @return words (list(tuple(str, float)))
    """
    distances = [(word, torch.dist(vector, get_vector(embeddings, word)).item())
                 for word in embeddings.itos]
    
    return sorted(distances, key = lambda w: w[1])[:n]


word_vector = get_vector(word_embedding, "Việt_Nam")

closest_words(word_embedding, word_vector)
# >> [('Việt_Nam', 0.0),
# ('VN', 0.6608753204345703),
# ('Trung_Quốc', 0.6805075407028198),
# ('nước', 0.7456551790237427),
# ('TQ', 0.7542526721954346),
# ('của', 0.7784993648529053),
# ('biển', 0.7814522385597229),
# ('vùng_biển', 0.7835540175437927),
# ('Singapore', 0.7879586219787598),
# ('và', 0.7881312966346741)]

Kết quả trả về cho thấy từ "Việt_Nam" gần với nhiều từ cũng là tên của quốc gia khác.

3. Lớp Vocabulary

Class Vocabulary được tạo ra dùng để tách từ và chuyển văn bản thành các số được lưu trong tensor (các số này sau đó sẽ được dùng để ánh xạ với word embedding). Mình cài đặt lớp này dựa trên mã nguồn của Assigment 4 trong khóa học Stanford CS224n.

class Vocabulary:
    """ The Vocabulary class is used to record words, which are used to convert 
        text to numbers and vice versa.
    """

    def __init__(self):
        self.word2id = dict()
        self.word2id['<pad>'] = 0   # Pad Token
        self.word2id['<unk>'] = 1   # Unknown Token
        self.unk_id = self.word2id['<unk>']
        self.id2word = {v: k for k, v in self.word2id.items()}

    def __getitem__(self, word):
        return self.word2id.get(word, self.unk_id)

    def __contains__(self, word):
        return word in self.word2id 
    
    def __len__(self):
        return len(self.word2id)

Trong phương thức khởi tạo, Vocabulary mặc định bao gồm 2 chữ "<unk>" dùng để biểu diễn chữ không có trong từ điển và "<pad>" được dùng làm chữ đệm để cho các câu có cùng kích thước mà mình sẽ giải thích sau.
Các phương thức đặc biệt như __getitem__, __contains__, __len__ được cài đặt để thực hiện các câu lệnh như vocab[idx]; word in vocab; len(vocab), giúp thao tác với lớp được đơn giản hơn.

    def id2word(self, word_index):
        """
        @param word_index (int)
        @return word (str)
        """
        return self.id2word[word_index]

    def add(self, word):
        """ Add word to vocabulary
        @param word (str)
        @return index (str): index of the word just added
        """
        if word not in self:
            word_index = self.word2id[word] = len(self.word2id)
            self.id2word[word_index] = word
            return word_index
        else:
            return self[word]

    @staticmethod
    def tokenize_corpus(corpus):
        """Split the documents of the corpus into words
        @param corpus (list(str)): list of documents
        @return tokenized_corpus (list(list(str))): list of words
        """
        print("Tokenize the corpus...")
        tokenized_corpus = list()
        for document in tqdm(corpus):
            tokenized_document = [word.replace(" ", "_") for word in word_tokenize(document)]
            tokenized_corpus.append(tokenized_document)

        return tokenized_corpus

Lớp được thực hiện để chuyển chữ thành số, việc này đã được thực hiện thông qua phương thức __getitem__ và thuộc tính word2id. Tuy nhiên, để kiểm tra class có chuyển đổi đúng hay không, ta cài đặt thuộc tính và phương thức id2word.
Phương thức add được dùng để thêm các chữ vào từ điển. Sau này được dùng để thêm các từ có trong word embedding vào.
Từ lúc này, mình bắt đầu dùng từ document để ám chỉ chuỗi văn bản (kiểu string), và corpus để chỉ một danh sách các document (kiểu list(string)). Phương thức tĩnh tokenize_corpus sử dụng hàm word_tokenize của thư viện underthesea để tách các document của corpus thành list các từ riêng biệt nhau.

    def corpus_to_tensor(self, corpus, is_tokenized=False):
        """ Convert corpus to a list of indices tensor
        @param corpus (list(str) if is_tokenized==False else list(list(str)))
        @param is_tokenized (bool)
        @return indicies_corpus (list(tensor))
        """
        if is_tokenized:
            tokenized_corpus = corpus
        else:
            tokenized_corpus = self.tokenize_corpus(corpus)
        indicies_corpus = list()
        for document in tqdm(tokenized_corpus):
            indicies_document = torch.tensor(list(map(lambda word: self[word], document)),
                                             dtype=torch.int64)
            indicies_corpus.append(indicies_document)

        return indicies_corpus

    def tensor_to_corpus(self, tensor):
        """ Convert list of indices tensor to a list of tokenized documents
        @param indicies_corpus (list(tensor))
        @return corpus (list(list(str)))
        """
        corpus = list()
        for indicies in tqdm(tensor):
            document = list(map(lambda index: self.id2word[index.item()], indicies))
            corpus.append(document)

        return corpus

Chức năng cuối cùng của Vocabulary là chuyển corpus thành tensor và ngược lại. Trong phương thức corpus_to_tensor có nhận tham số is_tokenized, tham số này là True để bỏ qua bước tách từ đối với corpus đã được tách sẵn và ngược lại.

Để kiểm tra lớp có hoạt động đúng hay không, ta tạo đối tượng vocab và thêm vào đó các từ có trong word_embedding đã tạo ở trên. Sau đó dịch một câu sang tensor và chuyển tensor đó lại thành câu.

corpus_sample = ["Với cộng đồng người Bách Việt trước đây, việc thuần hóa mèo cũng có thể theo cách thức như vậy."]

vocab = Vocabulary()

# create vocabulary from pretrained word2vec
words_list = list(word_embedding.stoi.keys())
for word in words_list:
    vocab.add(word)

# test the vocabulary
tensor = vocab.corpus_to_tensor(corpus_sample)
corpus = vocab.tensor_to_corpus(tensor)
" ".join(corpus[0])
# >> 'Với cộng_đồng người Bách Việt trước đây , việc <unk> mèo cũng có_thể theo cách_thức như_vậy .'

Lớp hoạt động đúng như mong đợi, từ thuần_hóa vì không nằm trong từ điển nên đã trả về từ <unk>.

4. Lớp IMDBDataset

Dữ liệu dùng để huấn luyện mô hình ở đây lấy từ dữ liệu IMDB - gồm 50.000 câu review phim kèm theo cảm xúc (sentiment) tích cực (positive) hoặc tiêu cực (negative). Các câu review này đã được dịch sang tiếng Việt bằng google dịch để phục vụ cho mục đích của mô hình.

Từ dữ liệu trên, mình cần tạo một lớp IMDBDataset có thể thực hiện vai trò sau:

  • Tải và lưu các dữ liệu trong file csv VI_IMDB.csv.
  • Cho biết kích thước của tập dữ liệu (số cặp review - sentiment).
  • Chuyển các câu review và sentiment về dạng tensor để có thể đưa vào model.
  • Trả về tuple (review, sentiment) thứ idx đã được chuyển về dạng tensor khi gọi dataset[idx].
from scipy.linalg.special_matrices import dft
import pandas as pd
import torch
from torch.utils.data import Dataset


class IMDBDataset(Dataset):
    """ Load dataset from file csv"""

    def __init__(self, vocab, csv_fpath=None, tokenized_fpath=None):
        """
        @param vocab (Vocabulary)
        @param csv_fpath (str)
        @param tokenized_fpath (str)
        """
        self.vocab = vocab
        self.pad_idx = vocab["<pad>"]
        df = pd.read_csv(csv_fpath)
        self.sentiments_list = list(df.sentiment)
        self.reviews_list = list(df.vi_review)

        sentiments_type = list(set(self.sentiments_list))
        sentiments_type.sort()

        self.sentiment2id = {sentiment: i for i, sentiment in enumerate(sentiments_type)}

        if tokenized_fpath:
            self.tokenized_reviews = torch.load(tokenized_fpath)
        else:
            self.tokenized_reviews = self.vocab.tokenize_corpus(self.reviews_list)

        self.tensor_data = self.vocab.corpus_to_tensor(self.tokenized_reviews, is_tokenized=True)
        self.tensor_label = torch.tensor([self.sentiment2id[sentiment] for sentiment in self.sentiments_list],
                                         dtype=torch.float64)
        
    def __len__(self):
        return len(self.tensor_data)

    def __getitem__(self, idx):
        return self.tensor_data[idx], self.tensor_label[idx]
        
   def collate_fn(self, examples):
        examples = sorted(examples, key=lambda e: len(e[0]), reverse=True)

        reviews = [e[0] for e in examples]
        reviews = torch.nn.utils.rnn.pad_sequence(reviews, 
                                                  batch_first=False, 
                                                  padding_value=self.pad_idx)
        reviews_lengths = torch.tensor([len(e[0]) for e in examples])
        sentiments = torch.tensor([e[1] for e in examples])

        return {"reviews": (reviews, reviews_lengths), "sentiments": sentiments} 

Ở đây mình có kế thừa từ lớp Dataset của PyTorch với mục đích về sau tạo DataLoader.

Lớp trên được viết để thực hiện những gì mình vừa liệt kê. Tuy nhiên cần lưu ý đoạn sau:

        sentiments_type = list(set(self.sentiments_list))
        sentiments_type.sort()

Biến sentiments_type dùng để lưu các loại cảm xúc, được dùng để tạo thuộc tính sentiment2id. Vì thứ tự trong set là ngẫu nhiên nên mình cần phải đưa về dạng list và sort lại, nhằm giúp cho sentiment2id sẽ luôn mang giá trị là {'negative': 0, 'positive': 1}.

Chúng ta khởi tạo đối tượng dataset. Quá trình này cần khoảng hơn 15 phút để tách từ cho 50.000 câu. Để tiết kiệm thời gian, mình tải về file tokenized.pt - là các câu đã được tách từ sẵn cho trong file VI_IMDB.csv giúp cho quá trình tạo đối tượng diễn ra nhanh hơn.

# dataset = IMDBDataset(vocab, "VI_IMDB.csv", "tokenized.pt")
dataset = IMDBDataset(vocab, "VI_IMDB.csv")

Sau khi đã tải toàn bộ dữ liệu vào dataset, ta tách ra làm 3 bộ dữ liệu train_dataset, valid_dataset, test_dataset để huấn luyện và kiểm tra.

from torch.utils.data import random_split


split_rate = 0.8
full_size = len(dataset)
train_size = (int)(split_rate * full_size)
valid_size = (int)((full_size - train_size)/2)
test_size = full_size - train_size - valid_size
train_dataset, valid_dataset, test_dataset = random_split(dataset, 
                                                          lengths=[train_size, valid_size, test_size])

len(train_dataset), len(valid_dataset), len(test_dataset)
# >> (40000, 5000, 5000)

5. Tạo DataLoader từ IMDBDataset

Chúng ta sẽ dùng hết Dataset để huấn luyện trong 1 epoch, và trong 1 epoch sẽ chia ra làm nhiều batch nhỏ. Việc chia Dataset thành các batch được thực hiện bằng DataLoader của PyTorch. Ngoài ra các batch cần được xử lý thêm bằng phương pháp packed padded sequences sẽ được giải thích sau. Do đó các câu trong 1 batch cần phải được sắp xếp theo thứ tự độ dài từ lớn đến bé, các độ dài này sẽ được dùng để làm input cho model. Sau đó thêm các chữ đệm <pad> để các câu có chiều dài bằng nhau để tạo tensor đưa vào huấn luyện model. Mình có thể chỉ định cách xử lý batch trên trước khi trả về trong DataLoader bằng cách truyền tham số hàm collate_fn mà mình đã cài đặt trong IMDBDataset:

from torch.utils.data import DataLoader


BATCH_SIZE = 100
train_dataloader = DataLoader(train_dataset,
                              batch_size=BATCH_SIZE,
                              shuffle=True,
                              collate_fn=dataset.collate_fn)
valid_dataloader = DataLoader(valid_dataset,
                              batch_size=BATCH_SIZE,
                              shuffle=True,
                              collate_fn=dataset.collate_fn)
test_dataloader = DataLoader(test_dataset,
                              batch_size=BATCH_SIZE,
                              shuffle=False,
                              collate_fn=dataset.collate_fn) 

6. Lớp RNN

Từ đoạn này trở đi, hầu hết mã nguồn mình dựa trên pytorch-sentiment-analysis tutorial của bentrevett. Tutorial này miễn phí và giải thích rất chi tiết, là cảm hứng cho mình viết bài này.

Mô hình đơn giản bao gồm lớp embedding để đưa tensor chứa index thành tensor chứa embedding véc tơ. Sau đó được đưa qua lớp hồi quy. Cuối cùng kết quả của lớp hồi quy được đưa qua lớp tuyến tính để trả về tensor gồm các số biểu diễn cảm xúc tích cực (gần với 1) hoặc tiêu cực (gần với 0)

import torch.nn as nn


class RNN(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, n_layers, 
                 bidirectional, dropout, pad_idx):
        """
        @param vocab_size (int)
        @param embedding_dim (int)
        @param hidden_dim (int)
        @param n_layers (int)
        @param bidirectional (bool)
        @param dropout (float)
        @param pad_idx (int)
        """
        super().__init__()
        
        self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx)
        
        self.rnn = nn.LSTM(embedding_dim, 
                           hidden_dim, 
                           num_layers=n_layers, 
                           bidirectional=bidirectional, 
                           dropout=dropout)
        
        self.fc = nn.Linear(hidden_dim * 2, 1)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, text, text_lengths):
        """
        @param text (torch.Tensor): shape = [sent len, batch size]
        @param text_lengths (torch.Tensor): shape = [batch size]
        @return
        """
        #text = [sent len, batch size]
        embedded = self.dropout(self.embedding(text))
        
        #embedded = [sent len, batch size, emb dim]
        
        #pack sequence
        # lengths need to be on CPU!
        packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths.to('cpu'))
        
        packed_output, (hidden, cell) = self.rnn(packed_embedded)
        
        #unpack sequence
        output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output)

        #output = [sent len, batch size, hid dim * num directions]
        #output over padding tokens are zero tensors
        
        #hidden = [num layers * num directions, batch size, hid dim]
        #cell = [num layers * num directions, batch size, hid dim]
        
        #concat the final forward (hidden[-2,:,:]) and backward (hidden[-1,:,:]) hidden layers
        #and apply dropout
        
        hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1))
                
        #hidden = [batch size, hid dim * num directions]
            
        return self.fc(hidden)

Như đã nói, mình sẽ không đi sâu vào việc giải thích các mô hình hồi quy LSTM, lớp DropOut mà chỉ nhấn mạnh các ý quan trọng. Ở đây là phương pháp packed padded sequences hay còn gọi là packing.
Trong một batch sẽ có nhiều câu có độ dài khác nhau, có thể có câu 50 chữ và câu 100 chữ. Khi đó câu 50 chữ cần phải thêm chữ đệm <pad> tới 50 lần. Vì các chữ đệm này không mang ý nghĩa gì nên việc học và xử lý chúng chỉ làm giảm hiệu suất của mô hình.

pack padded sequence

Từ đệm chỉ có tác dụng để đưa các câu có độ dài bằng nhau để tạo tensor. Trong lúc huấn luyện mạng hồi quy, từ đệm không mang nhiều ý nghĩa

PyTorch cung cấp hàm pack_padded_sequence để bỏ qua các vị trí chữ đệm trong dữ liệu khi đưa vào mạng hồi quy. Hàm này yêu cầu tensor biểu diễn các câu đã được đệm và tensor biểu diễn độ dài gốc của từng câu. Kết quả trả về của mạng hồi quy lúc này cần được "unpack" bằng hàm pad_packed_sequence để có thể đưa vào các lớp mạng khác.

Ngoài ra trong lúc khởi tạo lớp embedding, ta có chỉ định id của chữ đệm, để trong lúc huấn luyện, lớp embedding sẽ không thay đổi giá trị embedding của chữ này.

model.embedding.weight.data.copy_(word_embedding.vectors)
model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)

Sau khi đã khởi tạo mô hình, mình cần gán word embedding đã huấn luyện trước cho lớp embedding của mô hình. Việc này giúp mô hình đạt kết quả tốt trong thời gian nhanh hơn so với việc huấn luyện lại lớp embedding từ đầu.
Ngoài ra véc tơ embedding của hai chữ <unk>pad được khởi tạo là véc tơ 0 như một cách để thông báo cho mô hình rằng hai từ này không cung cấp thông tin gì cho quá trình huấn luyện.
Khác với <pad>, chữ <unk> có véc tơ embedding sẽ được thay đổi trong quá trình huấn luyện.

7. Huấn luyện mô hình

Đây là giai đoạn không thể thiếu khi làm việc với mạng nơ ron. Mình sử dụng optimizer Adam để giúp tối ưu mô hình và hàm loss Binary Cross-entropy (BCELoss) vì đây là bài toán Binary Classification. Mình lần lượt tính loss và accuracy của mô hình qua mỗi epoch. Vì giai đoạn này khá đơn giản nên mình chỉ ghi lại kết quả huấn luyện. Mã nguồn bạn có thể xem ở link Google Colab sau

Epoch: 01 | Epoch Time: 1m 55s
	Train Loss: 0.667 | Train Acc: 59.06%
	 Val. Loss: 0.600 |  Val. Acc: 71.24%
Epoch: 02 | Epoch Time: 1m 55s
	Train Loss: 0.554 | Train Acc: 72.38%
	 Val. Loss: 0.617 |  Val. Acc: 64.54%
Epoch: 03 | Epoch Time: 1m 57s
	Train Loss: 0.458 | Train Acc: 78.98%
	 Val. Loss: 0.339 |  Val. Acc: 86.46%
Epoch: 04 | Epoch Time: 1m 56s
	Train Loss: 0.312 | Train Acc: 87.33%
	 Val. Loss: 0.279 |  Val. Acc: 88.88%
Epoch: 05 | Epoch Time: 1m 56s
	Train Loss: 0.262 | Train Acc: 89.84%
	 Val. Loss: 0.338 |  Val. Acc: 84.34%

Mô hình đạt Accuracy gần như nhau trên 80% cho tập train và tập validation. Để đảm bảo mô hình không bị overfit, ta kiểm tra mô hình trên tập test.

test_loss, test_acc = evaluate(model, test_dataset, BATCH_SIZE, 
                               criterion, PAD_IDX, device)

print(f"Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%")
# >> Test Loss: 0.345 | Test Acc: 84.14%

Tập test cũng đạt accuracy trên 80%. Tuyệt vời!

8. Nhập review để kiểm tra

Mình sẽ thử tạo hai review phim cho hai cảm xúc khác nhau. Nhắc lại cảm xúc sẽ có nhãn như sau:

dataset.sentiment2id
# >> {'negative': 0, 'positive': 1}

Tức là càng gần với 0 thì review càng mang cảm xúc tiêu cực, càng gần với 1 càng mang tính tích cực.

sentence = "Bộ phim này rất dở! Nội dung cực kì nhàm chán"

predict_sentiment(model, sentence, vocab, device)
# >> 0.012241137214004993

sentence = "Bộ phim này rất hay! Nhiều tình tiết rất kịch tính."

predict_sentiment(model, sentence, vocab, device)
# >> 0.9765468835830688  

Đúng như mong đợi!

9. Lời kết

Qua bài này, chúng ta đã đi qua một số nội dung quan trọng sau:

  • Biểu diễn tiếng Việt dưới dạng word embedding.
  • Đưa văn bản tiếng Việt về dạng tensor để huấn luyện mô hình học sâu.
  • Phương pháp packing dùng cho xử lý ngôn ngữ tự nhiên.
  • Huấn luyện mô hình phân loại cảm xúc tiếng Việt.

Với bạn nào cần phiên bản dạng .py hơn là file jupyter notebook. Các bạn có thể tham khảo repo này của mình

Bài viết này hơi dài, nên mình xin cảm ơn vì các bạn đã đọc đến đây, hi vọng bài viết của mình giúp ích cho bạn. Nếu có chỗ nào sai hoặc có thể tối ưu hơn, xin hãy cho mình biết trong phần bình luận.

10. Tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí