[VNLP Core] [3] Bài toán phân loại văn bản - Phân tích cảm xúc của bình luận (text classification)

classification
nlp-tiengviet
sentiment-analysis
text

#1

Bài toán phân loại văn bản (text classification) được thấy rất nhiều trong các ứng dụng NLP (xử lý ngôn ngữ tự nhiên), ví dụ bài toán phân loại cảm xúc hay thái độ của người dùng qua bình luận (comment) trên các trang phim, mv ca nhạc, đánh giá về sản phẩm … Hay như trong ứng dụng chatbot, bài toán phân loại văn bản được sử dụng để phát hiện mục đích của người dùng. Hãy xem một số ví dụ cụ thể dưới đây:

Ví dụ đánh giá phim:

Phim rất thú vị (class: positive)
Xem rất chán (class: negative)

Dựa vào việc phân loại được tự động các bình luận chúng ta có thể đánh giá được tác động của một sản phẩm, dịch vụ, xu hướng lên khách hàng, cộng đồng là tích cực hay tiêu cực để có những chiến lược kinh doanh phù hợp. Các công cụ như thế kết hợp với các công cụ thu thập dữ liệu tự động từ nhiều nguồn khác nhau (mạng xã hội, báo điện tử, diễn đàn…) sẽ tạo lên bộ công cụ điều tra thăm dò cực kỳ giá trị.

Ví dụ trong chatbot:

Tôi muốn đặt hoa (class: flower_order)
Mình huỷ đơn hàng được không (class: order_cancel)

Khi biết được mục đích của người dùng, chatbot có thể đưa ra những hành động phù hợp tương ứng. Ví dụ với class là flower_order thì chatbot có thể trả lời lại: “Bạn muốn đặt loại hoa nào?” Với bài toán phân loại văn bản, chúng ta đã có thể giải quyết khá nhiều các đoạn hội thoại đơn giản nhưng lại rất quan trọng để thay thế bớt nhiều sức lực cho con người.

Ở một bài trước mình đã giới thiệu một số hướng giải quyết bài toán phân loại văn bản Bài toán phân loại văn bản. Trong bài này mình sẽ giới thiệu một hướng tiếp cận cụ thể, với một ví dụ cụ thể cho dữ liệu tiếng Việt. Chúng ta sẽ cùng nhau xem và giải quyết bài toán phân tích cảm xúc/ thái độ của bình luận phim bằng mô hình deep learning.

  1. Chuẩn bị dữ liệu

Dữ liệu bạn có thể thu thập ở bất cứ nguồn nào, ví dụ crawl từ facebook, các trang phim… Miễn là bạn xử lý để tách được các câu bình luận tiếng Việt. Ví dụ bình luận trên youtube bạn có thể crawl bằng tài khoản google, bình luận phim có thể crawl không cần tài khoản (Bạn có thể sử dụng urllib, BeautifulSoup của python cũng có thể crawl được. Một số hàm bạn có thể tham khảo tại đây).

Ở đây mình đơn giản hoá bằng cách lấy sẵn một số câu bình luận phim, đặt vào 2 files khác nhau (thích, không thích). Bạn có thể mở rộng bằng cách thêm dữ liệu vào, tạo thêm file số 3, chẳng hạn như neutral.txt (không thích cũng không chê). Một số mẫu trong file positive.txt:

phim hay quá ae
hay thế
phim hay tuyệt
Hay
xem hay thật
phim này xem hay

Một số mẫu trong file negative.txt

phim nhảm
ghét xem phim này
coi tốn thời gian
bảo đảm nhảm nhí
không ra gì

  1. Xử lý dữ liệu (tách từ, biểu diễn vector)

Mỗi câu bình luận trước tiên được tách từ thành các từ có ý nghĩa. Ví dụ câu “bảo đảm nhảm nhí” được tách thành

[“bảo_đảm”, “nhảm_nhí”]

    def tokenize_sentences(self, sentences):
        """
        Tokenize or word segment sentences
        :param sentences: input sentences
        :return: tokenized sentence
        """
        tokens_list = []
        for sent in sentences:
            tokens = self.tokenizer.tokenize(sent)
            tokens_list.append(tokens)
        return tokens_list

Mỗi câu sẽ trở thành 1 mảng của các từ, mỗi dữ liệu đầu vào trước tiên được biến thành 1 mảng 2 chiều, chiều thứ nhất tương ứng với số câu bình luận. Chiều thứ hai tương ứng với số từ trong một câu.

Nếu bạn chưa rõ về tách từ (tokenization, word segmentation) thì hãy xem bài Tách từ tiếng Việt Tiếp theo chúng ta biến mỗi từ thành một vector, ví dụ từ “bảo_đảm” thành một vector 100 chiều:

[-0.46393627 0.16954394 0.83275014 -1.5028543 -0.01803644 1.2121786
0.5337349 1.0144993 0.14793205 -0.10679752 -0.4278082 1.3274498

0.71588534 0.37957552 0.55647874 -0.37985063 0.61336917 0.79634154
-1.1528205 -1.6517726 -0.29540744 -0.67167693]

Từ đồng nghĩa với từ này là từ “đảm_bảo”, với độ tương đồng khoảng 0.86123. Nếu bạn chưa hiểu rõ về cách sử dụng word2vec hãy xem lại bài Thực hành biểu diễn từ bằng vector.

    def word_embed_sentences(self, sentences, max_length=20):
        """
        Helper method to convert word to vector
        :param sentences: input sentences in list of strings format
        :param max_length: max length of sentence you want to keep, pad more or cut off
        :return: embedded sentences as a 3D-array
        """
        embed_sentences = []
        for sent in sentences:
            embed_sent = []
            for word in sent:
                if (self.sym_dict is not None) and (word.lower() in self.sym_dict):
                    replace_word = self.sym_dict[word.lower()]
                    embed_sent.append(self.word2vec[replace_word])
                elif word.lower() in self.word2vec:
                    embed_sent.append(self.word2vec[word.lower()])
                else:
                    embed_sent.append(np.zeros(shape=(self.word_dim,), dtype=float))
            if len(embed_sent) > max_length:
                embed_sent = embed_sent[:max_length]
            elif len(embed_sent) < max_length:
                embed_sent = np.concatenate((embed_sent, np.zeros(shape=(max_length - len(embed_sent),
                                                                         self.word_dim), dtype=float)),
                                            axis=0)
            embed_sentences.append(embed_sent)
        return embed_sentences

Như vậy mỗi từ sẽ biến thành 1 vector 100 chiều, mỗi câu sẽ thành 1 ma trận max_length x 100 với max_length = 20 là số từ tối đa trong 1 câu mà ta chọn. Nếu câu dài hơn ta sẽ cắt đi, nếu câu ngắn hơn ta sẽ thêm vào vector 0. Dữ liệu features X gồm n câu (sentences) sẽ được biểu diễn thành 1 tensor 3 chiều n x max_length x d; với n = số mẫu (hay số câu bình luận), max_length = chiều dài 1 câu (nhớ chọn là 1 số cố định), d = số chiều không gian vector biểu diễn 1 từ.

  1. Tạo mô hình deep learning

Khi đã đưa được mỗi từ (word) thành một vector, mỗi câu thành một ma trận, dữ liệu đầu vào trở thành 1 tensor 3 chiều, thì chúng ta có thể sử dụng các mô hình deeplearning để classify.

Với dữ liệu ma trận chúng ta có thể nghĩ tới Convolution Neural Network nhưng với ma trận mà chiều của không gian từ đã mang ý nghĩa của chính nó thì ta không cần quét bộ lọc (filter qua) mà chỉ cần quét đặc trưng theo chiều chuỗi các từ nên người ta hay dùng Conv1D cho text classification.

Dữ liệu văn bản là dữ liệu ở dạng chuỗi nên chúng ta nghĩ tới phương án sử dụng mạng Recurrrent Neural Network (ví dụ: Long Short-Term Memory). Để phân loại chuỗi của các từ ta thấy rằng cần xét cả câu và Bi-LSTM giúp tránh ảnh hưởng bởi sự mất cân bằng giữa các từ cuối câu so với đầu câu.

Tóm lại, về mô hình deeplearning trong trường hợp này chúng ta có thể nghĩ ngay tới Conv1D, LSTM, Bi-LSTM để làm ví dụ và thử nghiệm đầu tiên. Chi tiết về các mô hình các bạn có thể xem ở các bài khác trong diễn đàn, hoặc các nguồn khác vì đây là các mô hình được sử dụng rất rộng rãi hiện nay.

Để đơn giản, chúng ta sử dụng thư viện Keras, bạn chỉ cần chọn các hyper-parameters như số layer, số neuron trên 1 layer, kỹ thuật Dropout, hay chọn thuật toán tối ưu…

Với Keras, chúng ta có thể dễ dàng lưu và load model để sử dụng lại nhiều lần.

    def load_model(self):
        """
        Load model from file
        :return: None
        """
        self.model = self.build_model((self.max_length, self.word_dim))
        self.model.load_weights(self.model_path)

    def build_model(self, input_dim):
        """
        Build model structure
        :param input_dim: input dimension max_length x word_dim
        :return: Keras model
        """
        model = Sequential()

        model.add(LSTM(64, return_sequences=True, input_shape=input_dim))
        model.add(Dropout(0.2))
        model.add(LSTM(32))
        model.add(Dense(self.n_class, activation="softmax"))

        model.compile(loss=keras.losses.categorical_crossentropy,
                      optimizer=keras.optimizers.Adadelta(),
                      metrics=['accuracy'])
        return model

Xem thêm cách chuẩn bị dữ liệu, tạo mô hình tại

  1. Training và test Bước training và predict khá đơn giản khi chúng ta đã đưa dữ liệu về dạng ma trận số hay tensor, chỉ cần sử dụng hàm fit hay predict theo API của thư viện Keras. Chúng ta cũng dễ dàng lưu các tham số đã tối ưu bằng hàm save_weights(path)
    def train(self, X, y):
        """
        Training with data X, y
        :param X: 3D features array, number of samples x max length x word dimension
        :param y: 2D labels array, number of samples x number of class
        :return:
        """
        self.model = self.build_model(input_dim=(X.shape[1], X.shape[2]))
        self.model.fit(X, y, batch_size=self.batch_size, epochs=self.n_epochs)
        self.model.save_weights(self.model_path)

    def predict(self, X):
        """
        Predict for 3D feature array
        :param X: 3D feature array, converted from string to matrix
        :return: label array y as 2D-array
        """
        if self.model is None:
            self.load_model()
        y = self.model.predict(X)
        return y

Lưu ý:

  • Bạn hãy crawl dữ liệu và test thử xem có cần bổ sung thêm dữ liệu training hay không?
  • Kiểm tra phần tách từ xem có cần training lại cho phù hợp với domain (ví dụ ở đây thì domain là bình luận phim) hay không? Như phần xử lý viết tắt chẳng hạn.
  • Tương tự hãy kiểm tra word2vec xem có ổn không vì pre-trained model hiện tại là training cho dữ liệu khác. Đặc biệt với những từ (word) quan trọng. Mình đã sử dụng 1 trick nhỏ trong source code mà chưa đề cập đó là từ đồng nghĩa cho một số từ không có trong word2vec training sẵn hoặc mang ý nghĩa không đúng. Hãy xem kỹ source code để hiểu trick này.
  • Bạn có thể thay mô hình deeplearning đơn giản nếu bạn sử dụng Keras, chỉ cần extend lại class KerasTextClassifier và override lại hàm build_model(self, input_dim)

Có gì chưa rõ ràng mong các bạn comment bên dưới, mình sẽ sửa hoặc trả lời.

Chi tiết về các đoạn code và cách chạy thử nghiệm ví dụ phân tích cảm xúc / thái độ của bình luận được mô tả ở link dưới đây:

Source code:


Tìm giải pháp cho bài toán sentiments classification
[Hỏi] Xây dựng tập dữ liệu training cho n-gram language model phục vụ bài toán tách từ
#2

Cám ơn bài viết hay của bạn. Khi mình chạy thử đoạn này thì bị lỗi ở phần

tokenizer = CrfTokenizer(config_root_path=’…/tokenization/’, model_path=’…/models/pretrained_tokenizer.crfsuite’)

Lỗi ở hàm:

self.bi_grams = load_n_grams(config_root_path + bi_grams_path)

Mã lỗi

UnicodeDecodeError: ‘charmap’ codec can’t decode byte 0x81 in position 252: character maps to


#3

Bạn xem file demo.py nhé! Mình sử dụng python 3. Bạn xem thêm file requirements.txt nhé. https://github.com/deepai-solutions/core_nlp/blob/master/requirements.txt


#4

Mình đã search lỗi này, có thể do OS của bạn không mặc định đọc utf8. Bạn có thể thử chỗ open file, thêm vào encoding=“utf8”:

with open(file_path, encoding="utf8") as fr:

Mình đã sửa trong nhánh master trên github, bạn có thể pull về chạy thử lại. Nếu lỗi tương tự bạn viết issue trên github hoặc phản hồi lại đây nhé (mình sẽ sửa những chỗ bị lỗi)


#5

cảm ơn bài viết của anh. Em chạy thử code demo.py thì bị lỗi raise KeyError("word '%s' not in vocabulary" % word)

KeyError: "word 'xuất_sắc' not in vocabulary"

ở trong file

gensim\models\keyedvectors.py line 274 in word_vec


#6

Có lẽ dữ liệu của bạn có lỗi mã hoá tiếng việt ở từ “xuất_sắc”. Còn thông báo lỗi thì cho thấy từ “xuất_sắc” không tồn tại trong bộ từ vựng của model word_vec. Nếu bạn ko fix được lỗi có thể copy cả đoạn lỗi bắn ra, xem ở file nào trong project của mình. Còn nếu chỉ thông tin lỗi như này thì mình chỉ kết luận được như vậy. Cảm ơn bạn đã phản hồi.


#7

E chưa rõ đoạn này lắm ạ. E đang hiểu nó như sau: Input là “Tôi thích bộ phim này lắm. Nhân vật chính rất tuyệt” Như vậy nó sẽ thành 2 chiều, mỗi chiều là 1 câu, sau đó mỗi câu lại có các từ phải k ạ? [[Tôi, thích, bộ_phim này], [Nhân_vật_chính, tuyệt]]. Nếu vậy có cách nào để phân biệt các câu ạ? EM cảm ơn a


#8

Bạn có thể coi 2 câu là 1 text đầu vào (dấu chấm coi như 1 từ) thì có thể cho model học luôn. Hoặc sử dụng thuật toán khác để tách 1 đoạn dài thành nhiều câu đơn, sau đó cho input từng câu rồi dùng voting (hoặc attention) để đưa ra output


Xin hướng dẫn học về NLP
#9

Mình đang mắc chỗ cài đặt thuật toán phân loại theo từ điển. ban có gợi ý nào giúp mình được không ạ? Xin chân thành cảm ơn bạn.


#10

Bạn có thể nói rõ về vấn đề bạn gặp, mình đang làm đề tài liên quan và có tham khảo bài này. Nếu có gì giải đáp được mình sẽ giúp


#12

Các bác cho em hỏi làm sao để phân loại các bình luận không liên quan đến phim, ví dụ như bình luận “chào mọi người” chẳng hạn nó sẽ phân ra class irrelevant ạ


#13

Bạn có thể tạo thêm 1 nhãn riêng “irrelevant” như vậy. Hoặc build một model sàng lọc trước khi cho vào model chính.


#14

Em có một vấn đề là nếu nhãn của em càng nhiều thì độ chính xác sẽ càng giảm đi ạ ?


Cách tạo Word2Vec cho competition
#15

Bạn nêu chi tiết hơn về chỗ đang vướng được không ạ?


#16

Như vậy dữ liệu của bạn bị nhiễu (noise) hay bị mất cân bằng (imbalance) chăng? Nếu nhiễu thì bạn có thể dùng re-filtering (dùng model để lọc lại các mẫu bị sai có confidence thấp). Nếu mất cân bằng thì bạn có thể dùng các phương pháp để sampling dữ liệu.


#17

Dạ em định xây 1 cái dataset cho chatbot ấy ạ. Chia text ra thành từng nhãn để có response tương ứng


#18

Vậy thì bạn tham khảo một số mô hình của các chatbot framework nổi tiếng hiện nay. Nhưng trước hết mình nghĩ bạn nên đi từng bước một.

  1. Xây dựng bộ dữ liệu phân loại intent (ý định của người hỏi, ví dụ: intent mua hàng, intent hỏi giá…)
  2. Trích xuất các entity (các thuộc tính, ví dụ: số lượng hàng, mã mặt hàng…)
  3. Đưa ra các action tương ứng với intent, entity (cái này có thể dùng rule tạo thành cây quyết định). Ví dụ: intent hỏi giá => trả lời giá Chatbot không phải là bài toán đơn giản nên mình mô hàng sơ lược như vậy để dễ hình dung bước đầu tiếp cận.

#19

A cho em hỏi có em muốn phân loại bình luận dưa trên các khía cạnh nhằm mục đích thống kê. Ví dụ điện thoại Iphone chẳng hạn có bình luận như sau: Pin nhanh hết, nhưng màn hình mượt. Vậy mình sẽ có 2 khía cạnh là Pin và màn hình. 1 cái negative 1 cái là positive. E muốn phân loại và thống kê những cái nào là positive cái nào là negative. A có hướng giải quyết nào ko giúp e với. E cảm ơn a


#20

mình cũng đang bị lỗi giống bạn không biết bạn đã sửa được lỗi đó chưa?


#21

mình nghĩ phân tách thành các câu rồi phân loại từng câu. Sau hợp chúng lại nếu cùng nhãn. ko thì để phân tách vậy