Nhận diện đa cảm xúc trong văn bản là bài toán xác định một hoặc nhiều trạng thái cảm xúc được thể hiện qua câu, bình luận hoặc hội thoại. Khác với phân tích cảm xúc truyền thống chỉ dự đoán tích cực, tiêu cực hoặc trung tính, emotion recognition có thể phân biệt những trạng thái chi tiết hơn như vui mừng, thất vọng, biết ơn, tức giận, sợ hãi, yêu thích hay ngạc nhiên.
Bài viết này sử dụng bộ dữ liệu GoEmotions làm ví dụ và xây dựng một mô hình LSTM để xử lý văn bản theo dạng chuỗi. Điểm quan trọng là GoEmotions cho phép một bình luận mang nhiều nhãn cùng lúc, vì vậy đây là bài toán phân loại đa nhãn (multi-label classification) chứ không đơn thuần là phân loại nhiều lớp [1].
- Nhận diện cảm xúc trong văn bản có thể là multi-class hoặc multi-label; hai bài toán cần lớp đầu ra và hàm loss khác nhau.
- GoEmotions chứa hơn 58.000 bình luận Reddit tiếng Anh được gán nhãn theo 27 cảm xúc cùng một nhãn neutral [1].
- Pipeline LSTM gồm: văn bản → token ID → embedding → LSTM → sigmoid cho 28 nhãn.
- Với multi-label classification, micro-F1, macro-F1 và ngưỡng dự đoán thường hữu ích hơn việc chỉ nhìn accuracy.
Nhận diện cảm xúc trong văn bản khác sentiment analysis như thế nào?
Sentiment analysis thường tập trung vào cực tính của văn bản. Một hệ thống cơ bản có thể phân loại câu thành positive, negative hoặc neutral. Emotion recognition đi sâu hơn bằng cách cố gắng xác định trạng thái cảm xúc cụ thể được thể hiện trong nội dung.
Ví dụ, hai câu “Tôi rất vui vì bạn đã giúp tôi” và “Tôi thực sự biết ơn vì bạn đã giúp tôi” đều có thể mang sentiment tích cực, nhưng cảm xúc nổi bật không hoàn toàn giống nhau. Câu thứ nhất có thể liên quan đến joy, còn câu thứ hai liên quan đến gratitude.
| Bài toán | Đầu ra | Ví dụ |
|---|---|---|
| Sentiment analysis | Cực tính | Positive |
| Multi-class emotion recognition | Một cảm xúc | Joy |
| Multi-label emotion recognition | Nhiều cảm xúc đồng thời | Joy + gratitude |
Sự khác biệt giữa multi-class và multi-label đặc biệt quan trọng khi thiết kế neural network. Với multi-class, các lớp cạnh tranh với nhau và đầu ra thường dùng softmax. Với multi-label, mỗi cảm xúc được xem như một quyết định nhị phân riêng và đầu ra thường sử dụng sigmoid.
GoEmotions: dữ liệu cảm xúc chi tiết từ văn bản
GoEmotions được giới thiệu tại ACL 2020 và gồm 58.009 bình luận tiếng Anh được thu thập từ Reddit. Dataset sử dụng một hệ thống gồm 27 loại cảm xúc cùng nhãn neutral, tạo thành 28 nhãn đầu ra [1].
Các cảm xúc bao gồm admiration, amusement, anger, annoyance, approval, caring, confusion, curiosity, desire, disappointment, disapproval, disgust, embarrassment, excitement, fear, gratitude, grief, joy, love, nervousness, optimism, pride, realization, relief, remorse, sadness và surprise [1].
Điểm đáng chú ý là một mẫu có thể được gán một hoặc nhiều cảm xúc. Chẳng hạn, một câu thể hiện sự hào hứng trước thành công của người khác có thể đồng thời được gán excitement và approval. Vì thế, việc ép mỗi câu về duy nhất một nhãn có thể làm mất thông tin của annotation gốc.
Google Research cũng cung cấp một phiên bản đã lọc theo mức độ đồng thuận của người gán nhãn, gồm 43.410 mẫu train, 5.426 mẫu validation và 5.427 mẫu test [2]. Các file này lưu văn bản cùng danh sách ID của một hoặc nhiều emotion được gán cho mẫu.
Vì sao LSTM có thể xử lý cảm xúc trong câu?
Văn bản là dữ liệu có thứ tự. Hai câu chứa gần như cùng các từ nhưng sắp xếp khác nhau có thể mang ý nghĩa khác. Các từ phủ định cũng có thể làm thay đổi hoàn toàn cảm xúc của một đoạn văn.
Ví dụ, “I am happy” và “I am not happy” chỉ khác một từ nhưng không nên tạo ra cùng biểu diễn. Vì thế, mô hình cần xử lý chuỗi thay vì chỉ đếm số lần xuất hiện của từng token.
LSTM là một dạng mạng nơ-ron hồi quy được thiết kế để duy trì thông tin qua nhiều bước thời gian [3]. Với văn bản, mỗi bước thời gian tương ứng với một token hoặc một biểu diễn token.
Mỗi xt là vector embedding của token thứ t. LSTM lần lượt cập nhật trạng thái ẩn ht và trạng thái bộ nhớ ct, từ đó tạo một biểu diễn chứa thông tin về toàn bộ chuỗi.
Trong bài toán emotion recognition, mục tiêu không phải diễn giải từng cổng LSTM theo một cảm xúc cụ thể. Ý nghĩa thực tế hơn là mạng có khả năng học một hàm biến đổi chuỗi token thành biểu diễn hữu ích cho việc dự đoán các nhãn cảm xúc.
Đọc GoEmotions và biến nhãn thành multi-hot vector
Phiên bản dữ liệu đã lọc của GoEmotions có ba file train.tsv, dev.tsv và test.tsv. Mỗi dòng gồm văn bản, danh sách ID emotion cách nhau bằng dấu phẩy và ID của bình luận [2].
Một mẫu có nhãn 17,25 không có nghĩa đây là một lớp mới. Nó có nghĩa hai vị trí tương ứng trong vector nhãn đều bằng 1. Với 28 nhãn, mỗi mẫu vì vậy được chuyển thành một vector multi-hot có 28 phần tử.
import numpy as np
import pandas as pd
BASE_URL = (
"https://raw.githubusercontent.com/"
"google-research/google-research/"
"master/goemotions/data/"
)
NUM_LABELS = 28
def load_split(name):
df = pd.read_csv(
BASE_URL + name + ".tsv",
sep="\t",
header=None,
names=["text", "labels", "id"]
)
y = np.zeros(
(len(df), NUM_LABELS),
dtype="float32"
)
for i, label_string in enumerate(df["labels"]):
label_ids = [
int(x)
for x in str(label_string).split(",")
]
y[i, label_ids] = 1.0
return df["text"].astype(str).values, y
x_train, y_train = load_split("train")
x_dev, y_dev = load_split("dev")
x_test, y_test = load_split("test")
print(x_train.shape, y_train.shape)
print(x_dev.shape, y_dev.shape)
print(x_test.shape, y_test.shape)
Kích thước đầu ra của y_train là số mẫu × 28. Nếu một câu chỉ mang một cảm xúc thì vector có một phần tử bằng 1; nếu có ba cảm xúc thì ba phần tử tương ứng bằng 1.
Từ câu chữ đến chuỗi vector cho LSTM
Neural network không làm việc trực tiếp với chuỗi ký tự. Trước tiên, mỗi câu cần được chuyển thành một chuỗi số nguyên đại diện cho token. Lớp TextVectorization của Keras có thể học vocabulary từ tập train và biến câu thành chuỗi token ID [4].
Sau đó, lớp Embedding ánh xạ mỗi ID thành một vector liên tục. Ví dụ, với embedding 128 chiều, một câu gồm 30 token sẽ được chuyển thành ma trận 30 × 128 trước khi đi vào LSTM.
import tensorflow as tf
MAX_TOKENS = 20000
MAX_LEN = 30
vectorizer = tf.keras.layers.TextVectorization(
max_tokens=MAX_TOKENS,
standardize="lower_and_strip_punctuation",
split="whitespace",
output_mode="int",
output_sequence_length=MAX_LEN
)
vectorizer.adapt(x_train)
vocab_size = len(
vectorizer.get_vocabulary()
)
print("Vocabulary:", vocab_size)
Giá trị 30 phù hợp để minh họa vì phiên bản train/evaluation do Google Research cung cấp có độ dài chuỗi tối đa được ghi nhận là 30 trong repository chính thức [2]. Tuy nhiên, nếu sử dụng bản raw hoặc một corpus khác, nên đo phân phối độ dài thực tế thay vì sao chép giá trị này.
Một nguyên tắc quan trọng là chỉ chạy adapt() trên tập train. Nếu vocabulary được học từ cả test, pipeline đã sử dụng thông tin từ dữ liệu đánh giá trước khi huấn luyện.
Xây dựng LSTM cho bài toán multi-label emotion recognition
Kiến trúc baseline có thể giữ khá đơn giản: TextVectorization chuyển văn bản thành token ID, Embedding biến ID thành vector, LSTM tổng hợp chuỗi và lớp Dense cuối dự đoán 28 nhãn cảm xúc.
Điểm khác biệt quan trọng nhất so với bài toán multi-class nằm ở lớp cuối: chúng ta sử dụng sigmoid thay vì softmax.
Mỗi pk được tính độc lập. Vì vậy mô hình hoàn toàn có thể tạo xác suất cao cho joy, gratitude và admiration cùng lúc.
inputs = tf.keras.Input(
shape=(),
dtype=tf.string
)
x = vectorizer(inputs)
x = tf.keras.layers.Embedding(
input_dim=vocab_size,
output_dim=128,
mask_zero=True
)(x)
x = tf.keras.layers.LSTM(
128,
dropout=0.2
)(x)
x = tf.keras.layers.Dense(
128,
activation="relu"
)(x)
x = tf.keras.layers.Dropout(0.3)(x)
outputs = tf.keras.layers.Dense(
28,
activation="sigmoid"
)(x)
model = tf.keras.Model(
inputs,
outputs
)
model.compile(
optimizer=tf.keras.optimizers.Adam(
learning_rate=1e-3
),
loss="binary_crossentropy"
)
model.summary()
Đây là mô hình minh họa phục vụ học tập, không phải implementation chính thức của GoEmotions. Paper gốc sử dụng các baseline khác, bao gồm BiLSTM và mô hình dựa trên BERT; tác giả báo cáo BERT có kết quả cao hơn BiLSTM trong thiết lập của nghiên cứu đó [1]. Điều này không biến kết quả ấy thành kết luận rằng một kiến trúc luôn tốt hơn kiến trúc khác trên mọi corpus.
Binary cross-entropy được dùng vì mỗi nhãn có thể được xem như một mục tiêu nhị phân riêng:
Loss thực tế được tính trên các nhãn và các mẫu trong batch. Khi y = 1, mô hình bị phạt nếu xác suất cho emotion tương ứng quá thấp; khi y = 0, mô hình bị phạt nếu xác suất quá cao.
Ngưỡng dự đoán quan trọng không kém kiến trúc
Sigmoid không tự quyết định nhãn cuối cùng. Nó chỉ tạo một giá trị từ 0 đến 1 cho mỗi emotion. Để biến xác suất thành nhãn, chúng ta cần một threshold.
Nếu chọn threshold quá thấp, mô hình sẽ dự đoán quá nhiều cảm xúc. Nếu threshold quá cao, nhiều cảm xúc đúng có thể bị bỏ sót. Do đó không nên mặc định 0,5 luôn là lựa chọn tối ưu.
Một cách đơn giản là thử nhiều threshold trên validation set rồi chọn giá trị tạo micro-F1 tốt nhất. Test set vẫn được giữ lại để đánh giá cuối cùng.
from sklearn.metrics import f1_score
dev_probs = model.predict(
x_dev,
batch_size=256,
verbose=0
)
best_threshold = None
best_f1 = -1.0
for threshold in np.arange(
0.10,
0.91,
0.05
):
dev_pred = (
dev_probs >= threshold
).astype("int32")
score = f1_score(
y_dev,
dev_pred,
average="micro",
zero_division=0
)
if score > best_f1:
best_f1 = score
best_threshold = threshold
print("Threshold:", best_threshold)
print("Dev micro-F1:", best_f1)
Đây là cách chọn một threshold toàn cục đơn giản. Trong các thí nghiệm sâu hơn, mỗi emotion có thể có threshold riêng vì phân phối và độ khó của các nhãn không giống nhau. Tuy nhiên, việc tăng số tham số quyết định cũng làm tăng nguy cơ overfit vào validation set.
Đánh giá multi-label: vì sao accuracy dễ gây hiểu nhầm?
Trong phân loại một nhãn, accuracy khá trực quan. Với 28 đầu ra multi-label, khái niệm “đúng” trở nên phức tạp hơn. Một mô hình có thể dự đoán đúng hai trong ba cảm xúc của một câu nhưng vẫn không khớp hoàn toàn với vector nhãn thật.
Vì vậy, micro-F1 và macro-F1 thường cung cấp góc nhìn hữu ích hơn. Micro-F1 gộp toàn bộ quyết định trên tất cả các nhãn trước khi tính precision và recall, còn macro-F1 tính F1 cho từng emotion rồi lấy trung bình.
Macro-F1 nhạy với những nhãn hiếm hoặc khó vì mỗi emotion có trọng số như nhau trong trung bình cuối. Micro-F1 lại chịu ảnh hưởng nhiều hơn từ các nhãn xuất hiện thường xuyên. Vì vậy nên báo cáo cả hai thay vì chọn một con số duy nhất.
test_probs = model.predict(
x_test,
batch_size=256,
verbose=0
)
y_pred = (
test_probs >= best_threshold
).astype("int32")
micro_f1 = f1_score(
y_test,
y_pred,
average="micro",
zero_division=0
)
macro_f1 = f1_score(
y_test,
y_pred,
average="macro",
zero_division=0
)
print("Micro-F1:", micro_f1)
print("Macro-F1:", macro_f1)
Ngoài hai metric này, nên xem precision và recall của từng emotion. Chẳng hạn, một mô hình có thể nhận ra gratitude khá tốt nhưng gần như không phát hiện grief. Nếu chỉ đọc micro-F1, kiểu lỗi này có thể khó nhận thấy.
LSTM thực sự học được gì từ văn bản cảm xúc?
Embedding và LSTM được huấn luyện cùng nhau để tìm biểu diễn có ích cho loss của bài toán. Mô hình có thể học rằng những chuỗi từ nhất định thường xuất hiện cùng một emotion, nhưng không nên diễn giải điều này thành việc neural network “hiểu cảm xúc” giống con người.
Ví dụ, các biểu thức như “thank you so much” có thể trở thành tín hiệu mạnh cho gratitude, trong khi những cấu trúc thể hiện tiếc nuối có thể liên quan đến remorse hoặc sadness. LSTM còn có khả năng sử dụng thứ tự token, nhờ đó một phần ngữ cảnh xung quanh từ khóa có thể ảnh hưởng đến dự đoán cuối.
Tuy nhiên, một câu có thể mang cảm xúc mà không chứa từ khóa trực tiếp. Châm biếm, mỉa mai, hàm ý và ngữ cảnh hội thoại là những trường hợp khó hơn đáng kể. Một câu như “Great, another meeting at 7 AM” chứa từ “Great” nhưng không nhất thiết thể hiện cảm xúc tích cực nếu người viết đang mỉa mai.
Ngoài ra, GoEmotions được lấy từ Reddit tiếng Anh và chịu ảnh hưởng của cộng đồng, ngôn ngữ, văn hóa và quy trình annotation cụ thể của dataset [1], [2]. Một LSTM được huấn luyện trên GoEmotions không nên được mặc định là mô hình nhận diện cảm xúc chung cho tiếng Việt, email doanh nghiệp, hội thoại chăm sóc khách hàng hoặc dữ liệu y tế.
Khi nào nên dùng LSTM cho nhận diện cảm xúc văn bản?
LSTM là một baseline phù hợp khi mục tiêu là học rõ pipeline xử lý chuỗi: tokenization, embedding, padding, recurrent network, multi-label output và thresholding. Kiến trúc cũng tương đối nhỏ và có thể huấn luyện từ đầu mà không cần tải một language model lớn.
Nếu dữ liệu ít, yêu cầu tổng quát hóa cao hoặc ngôn ngữ phức tạp, các mô hình pretrained thường là hướng so sánh quan trọng. Chính paper GoEmotions sử dụng BERT và báo cáo mô hình này vượt BiLSTM trong điều kiện thực nghiệm của nghiên cứu [1]. Tuy nhiên, một so sánh thực tế cần giữ nguyên train/dev/test, taxonomy, preprocessing và metric trước khi kết luận mô hình nào phù hợp hơn.
Một hướng cải tiến khác là Bidirectional LSTM. Trong bài toán phân loại cả câu, toàn bộ văn bản đã có sẵn trước khi dự đoán nên mô hình có thể xử lý chuỗi theo cả hai hướng. Ngoài ra có thể thử pretrained embedding, attention hoặc kiến trúc Transformer.
Dù dùng kiến trúc nào, quy trình đánh giá vẫn quan trọng hơn việc chỉ tăng độ phức tạp của mạng. Threshold phải được chọn trên validation set, test chỉ dùng sau khi cấu hình đã chốt, và metric cần phản ánh đầy đủ các nhãn thay vì chỉ ưu tiên những emotion phổ biến.
Kết luận
Nhận diện đa cảm xúc trong văn bản là một bước mở rộng tự nhiên từ sentiment analysis sang mô hình hóa những trạng thái cảm xúc chi tiết hơn. Với GoEmotions, một câu có thể mang nhiều nhãn cùng lúc, vì vậy pipeline phù hợp là tokenization → embedding → LSTM → 28 đầu ra sigmoid thay vì một softmax duy nhất.
LSTM là một baseline dễ hiểu để học cách mô hình hóa văn bản dạng chuỗi, nhưng chất lượng hệ thống phụ thuộc đồng thời vào taxonomy cảm xúc, dữ liệu gán nhãn, ngưỡng quyết định và metric đánh giá. Khi các thành phần này được kiểm soát rõ ràng, mô hình LSTM tạo một nền tảng hữu ích trước khi chuyển sang BiLSTM, attention hoặc các language model pretrained.
References
[1] D. Demszky, D. Movshovitz-Attias, J. Ko, A. Cowen, G. Nemade, and S. Ravi, “GoEmotions: A Dataset of Fine-Grained Emotions,” in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, 2020, pp. 4040–4054. DOI: 10.18653/v1/2020.acl-main.372.
[2] Google Research, “GoEmotions,” google-research repository. https://github.com/google-research/google-research/tree/master/goemotions.
[3] S. Hochreiter and J. Schmidhuber, “Long Short-Term Memory,” Neural Computation, vol. 9, no. 8, pp. 1735–1780, 1997. DOI: 10.1162/neco.1997.9.8.1735.
[4] Keras, “TextVectorization layer,” Keras API Documentation. https://keras.io/api/layers/preprocessing_layers/text/text_vectorization/.
Nội dung gốc thuộc tác giả. Vui lòng không đăng lại toàn văn khi chưa được phép, trừ trường hợp pháp luật hoặc giấy phép áp dụng cho phép. Nội dung của bên thứ ba tuân theo quyền và giấy phép tương ứng.
