Named Entity Recognition – Nhận diện thực thể trong câu khi xử lý ngôn ngữ tự nhiên

Chào tuần mới các member Mì thân yêu, hôm nay chúng ta sẽ cùng đi tìm hiểu về Named Entity Recognition – Nhận diện thực thể trong câu khi xử lý ngôn ngữ tự nhiên nhé. Món này hay gọi là NER đó các mem.

Các bài về NLP – xử lý ngôn ngữ tự nhiên thường trừu tượng và khó hiểu hơn các bài Computer Vision rất nhiều nên mình sẽ đi thật từ từ và mọi thứ sẽ được diễn đạt ở dạng Mì ăn liền để bạn nào cũng có thể hiểu được.

Mình có mấy bài về NLP, bạn nào chưa đọc thì đọc lại tại đây nhé.

Let’s go thôi các mem!

Phần 1 – Named Entity Recognition (NER) là cái chi chi?

Ok, thì vừa nói ở trên đó, chúng ta học về NER – nhận diện các thực thể trong văn bản.

Vẫn trừu tượng vãi đúng không? Mình sẽ có ví dụ ngay, tóm lại bài hôm nay chúng ta làm bài toán như sau:

  • Ví dụ 1: Đầu vào là câu văn bản, ví dụ “Hôm nay Peter đi Mỹ”. Model phải đưa ra output là : Peter – Tên riêng, Mỹ – Địa điểm.
  • Ví dụ 2: Đầu vào “Số điện thoại công ty Apple là 091345678”. Model sẽ phải hiện ra: Apple – Tên công ty, 091345678 – Số điện thoại.

Vậy đó, NER là trích từ câu văn ra các thực thể có tên (nghĩa là các thực thể ấy được ta đặt tên rồi ấy). Ví dụ như trên là thực thể Tên riêng, thực tể Địa điểm, thực thể số điện thoại….

Xem thêm cái hình để biết NER là gì này, các thực thể được nhận ra khi đưa đoạn văn bản vào và bôi xanh, bôi đỏ đấy:

Named Entity Recognition
Nguồn: Tại đây

Lờ mờ hiểu mục đích bài hôm nay rồi chứ các bạn. Đi tiếp nhé!

Phần 2 – Phương pháp triển khai bài toán

Về vấn đề input, output

Như vậy để làm bài này ta sẽ thực hiện nhận một chuỗi văn bản đầu vào X gồm n từ đánh số từ 1 đến n. Chúng ta sẽ thực hiện predict ra vector y cũng có n phần tử là nhãn của các từ trong câu X.

Các nhãn có cấu trúc: P-Name. Trong đó

  • P có thể là B (bắt đầu), I (bên trong) và E (kết thúc) để miêu tả vị trí bắt đầu, bên trong và kết thúc của thực thể trong câu.
  • Name là tên thực tể. Ví dụ: org – tổ chức, per – tên riêng….

Ví dụ về việc gán nhãn cho các từ như sau:

Câu X: John   Michael Wick  like United Kingdom very much
Nhãn.  B-per  I-per   E-per O    B-loc  E-loc   O    O

Nhìn vào đó ta thấy:

  • John được gán B-per: Nghĩa là bắt đầu một tên riêng.
  • Michael là I-per, là phần giữa của một tên riêng.
  • Wick thì là E-per , kết thúc của tên riêng.
  • Tương tự cho United Kingdom là một thực thể loc – Location
  • like, very, much là các từ không cần nhận diện nên để là O – Outside.

Phần này bạn nào còn chưa hiểu có thể post lên Group trao đổi, chia sẻ: https://facebook.com/groups/miaigroup để giao lưu thêm nhé.

Về vấn đề mạng Deep learning

Nói đến Input là văn bản là chúng ta nghĩ ngay đến LSTM rồi. Vâng, bài này chúng ta sử dụng LSTM và cụ thể là Bidirection LSTM để xây dựng mạng Neural.

Nếu các bạn cần tìm hiểu sâu hơn về LSTM thì đọc link này nhé, nó sâu về lý thuyết chút.

LSTM
Nguồn: Tại đây

Sau khi trích xuất được vector đặc trưng cho từng từ trong câu, chúng ta sẽ sử dụng thuật toán Conditional Random Fields (CRF) để predict từ đó có là Named Entity hay không.

Lý do phải sử dụng CRF là vì :

  • Việc predict nhãn từng từ nhiều khi không mang lại hiệu quả cao. Ví dụ nếu xét riêng từng từ thì từ Apple trong câu “I eat Apple” sẽ giống như từ Apple trong câu “Apple makes iPhone”. Trong khi ta biết rõ là khác nhau 😀
  • CRF có cơ chế sử dụng ngữ cảnh (nhãn của các từ trước đó) vào việc predict nhãn của từ hiện tại nên nhãn của của 1 từ sẽ phụ thuộc vào từ đó nằm trong câu nào, điều đó hợp lý hơn.

Link dành cho bạn nào muốn tìm hiểu sâu hơn về CRF: tại đây.

Đó, lý thuyết cho bài này chỉ có thể, giờ ta tiến hành xử lý từng bước nào.

Phần 3 – Build và train model Named Entity Recognition

Dữ liệu cho train model

Bài toán này có thể sử dụng bất kì dữ liệu nào đã gán nhãn để train nhé, các bạn tuỳ vào bài toán của mình để chọn dữ liệu cho phù hợp. Ở đây để nhanh gọn mình sử dụng dữ liệu trên Kaggle, các bạn download tại đây. Mình cũng để trên github luôn cho các bạn cần tải nhanh.

Dữ liệu này đã được gán nhãn như tại phần 2 với các loại Entity gồm:

geo = Geographical Entity
org = Organization
per = Person
gpe = Geopolitical Entity
tim = Time indicator
art = Artifact
eve = Event
nat = Natural Phenomenon
Đọc và xử lý dữ liệu

Để load dữ liệu, chúng ta sử dụng thư viện Pandas và đọc file ‘ner_dataset.csv’.

def load_data(filename='../ner_dataset.csv'):
    df = pd.read_csv(filename, encoding = "ISO-8859-1")
    df = df.fillna(method = 'ffill')
    return dfCode language: JavaScript (javascript)

Trong khi đọc chúng ta tiến hành fill các ô bị Null luôn bằng lệnh df.fillna.

Dữ liệu đọc được sẽ được ghi thành từng dòng, mỗi dòng một từ kèm theo POS (Part of Speech) và nhãn Tag tương ứng theo đúng quy tắc tại Phần 2.

Named Entity Recognition (NER) - Nhận diện thực thể trong câu khi xử lý ngôn ngữ tự nhiên

Ở đây ta bỏ qua cột POS nhé. Quan tâm 3 cột còn lại thôi.

Tiếp theo ta sẽ group dataframe này theo cột Sentences # để nối các từ trong từng dòng riêng rẽ về một câu:

agg = lambda s: [(w, p, t) for w, p, t in zip(s['Word'].values.tolist(),                                                     s['POS'].values.tolist(),                                       s['Tag'].values.tolist())]
        self.grouped = self.df.groupby("Sentence #").apply(agg)
        self.sentences = [s for s in self.grouped]Code language: PHP (php)

Đoạn chương trình trên mình viết riêng ra 1 class riêng (file cls_sentences.py) để tránh rối chương trình chính nhé.

Rồi sau bước trên ta đã có danh sách các câu lưu trong biến sentences, mỗi item sẽ có 3 giá trị là [word , pos, tag].

Encoding dữ liệu

Bây giờ nếu như để dữ liệu ở dạng text thông thường thì chắc chắn model sẽ không thể xử lý được, chúng ta tiến ành xây dựng vocab và encode thành các vector số.

Đầu tiên là bước xây dựng vocab và 4 dictionary để map word thành index, tag thành index và ngược lại:

    # Xây dựng vocab cho word và tag
    words = list(df['Word'].unique())
    tags = list(df['Tag'].unique())

    # Tạo dict word to index, thêm 2 từ đặc biệt là Unknown và Padding
    word2idx = {w : i + 2 for i, w in enumerate(words)}
    word2idx["UNK"] = 1
    word2idx["PAD"] = 0

    # Tạo dict tag to index, thêm 1 tag đặc biệt và Padding
    tag2idx = {t : i + 1 for i, t in enumerate(tags)}
    tag2idx["PAD"] = 0

    # Tạo 2 dict index to word và index to tag
    idx2word = {i: w for w, i in word2idx.items()}
    idx2tag = {i: w for w, i in tag2idx.items()}Code language: PHP (php)

Ở đây các bạn chú ý một điểm là tại sao lại phải thêm 2 từ đặc biệt là Unknown và Padding? Lý do như sau:

  • Thêm từ Unknown để deal với các từ không có trong vocab khi predict, nếu như gặp các từ không biết thì quy hết về từ Unknow này.
  • Thêm từ Padding, chính là từ ta sẽ sử dụng để chền thêm vào cuối các câu ngắn hơn 1 length cố định do chúng ta quy định. Chắc bạn vẫn thắc mắc sao lại phải làm thế? Đơn giản vì khi feed vào vào các model ta luôn cần length cố định trong khi các câu thì câu dài câu ngắn khác nhau -> cần phải padding cho đều nhau mới đưa vào mạng được. Ta hay chọn độ dài câu dài nhất để padding các câu ngắn hơn về độ dài đó.

Sau khi đã tạo được các dict, ta tiến hành map các câu văn bản và các tag về index:

   # Chuyển các câu về dạng vector of index
    X = [[word2idx[w[0]] for w in s] for s in sentences]
    # Padding các câu về max_len
    X = pad_sequences(maxlen = max_len, sequences = X, padding = "post", value = word2idx["PAD"])
    # Chuyển các tag về dạng index
    y = [[tag2idx[w[2]] for w in s] for s in sentences]
    # Tiền hành padding về max_len
    y = pad_sequences(maxlen = max_len, sequences = y, padding = "post", value = tag2idx["PAD"])Code language: PHP (php)

Sau bước này các câu và các vector tag của câu sẽ có dạng:

# Câu slà vector cha các word index
[1 332 3300 760 87 3 22 300]
# Vector tag scha tag index tươngng vi các ttrong câu
[2 3 3 15 15 2 2 2 2]Code language: CSS (css)

Và đê tiến hành train, ta cần làm thêm một bước là chuyển các tag index về dạng One-hot

# Chuyển y về dạng one-hot
    num_tag = df['Tag'].nunique()
    y = [to_categorical(i, num_classes = num_tag + 1) for i in y]Code language: PHP (php)

Và cuối cùng là chia train, test:

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.15)

Sau bước này thì dữ liệu đã sẵn sàng, ta sang bước tiếp nhé

Build và train model

Model ở đây khá đơn giản, chỉ gồm các lớp:

  • Embedding: Để embed các câu văn bản. Cụ thể là biến các word index thành các vector n chiều cố định.
  • Bidirection LSTM với return_sequence=True
  • TimeDistributed Layer để lấy ra vector Dense cho từng từ lại mỗi step.
  • CRF ở trên cùng
    input = Input(shape=(max_len,))
    model = Embedding(input_dim=len(words) + 2, output_dim=embedding, input_length=max_len, mask_zero=False)(input)
    model = Bidirectional(LSTM(units=hidden_size, return_sequences=True, recurrent_dropout=0.1))(model)
    model = TimeDistributed(Dense(hidden_size, activation="relu"))(model)
    crf = CRF(num_tags + 1)  # CRF layer
    out = crf(model)  # outputCode language: PHP (php)

Model này sử dụng loss và accuracy của lớp CRF để fine tune model.

    model = Model(input, out)
    model.compile(optimizer="rmsprop", loss=crf.loss_function, metrics=[crf.accuracy])
Code language: JavaScript (javascript)

Trong bài mình có dùng một số thủ thuật dể lưu lại file data cũng như là check xem đã train hay chưa để thực hiện quá trình train, các bạn đọc source để hiểu rõ hơn nhé.

Ở đây mình chỉ nêu phần chính là train model kèm với một checkpoint để lưu lại weights nhé:

    checkpoint = ModelCheckpoint(filepath = 'model.hdf5',
                           verbose = 0,
                           mode = 'auto',
                           save_best_only = True,
                           monitor='val_loss')
    history = model.fit(X_train, np.array(y_train), batch_size=batch_size, epochs=epochs,
                        validation_split=0.1, callbacks=[checkpoint])Code language: PHP (php)

Phần 4 – Kiêm thử model Named Entity Recognition

Sau khi train xong ta thử Eval trên tập test xem kết quả như nào:

# Test với toàn bộ tập test
y_pred = model.predict(X_test)
y_pred = np.argmax(y_pred, axis=-1)
y_test_true = np.argmax(y_test, -1)

# Kiểm thử F1-Score
y_pred = [[idx2tag[i] for i in row] for row in y_pred]
y_test_true = [[idx2tag[i] for i in row] for row in y_test_true]
print("F1-score is : {:.1%}".format(f1_score(y_test_true, y_pred)))
Code language: PHP (php)

Ở đây F1- Score là 83.1%, cũng khá ổn rồi.

Hoặc ta cũng có thể test với 1 câu random trong tập test để kiểm tra kết quả:

# Test với một câu ngẫu nhiên trong tập test
idx = np.random.randint(0,X_test.shape[0])

p = model.predict(np.array([X_test[idx]]))
p = np.argmax(p, axis=-1)
true = np.argmax(y_test[i], -1)

print("Example #{}".format(idx))

print("{:15}||{:5}||{}".format("Word", "True", "Pred"))
print(40 * "*")
for w, t, pred in zip(X_test[idx], true, p[0]):
    if w != 0:
        print("{:15}: {:5} {}".format(words[w-2], idx2tag[t], idx2tag[pred]))Code language: PHP (php)

Kết quả in ra màn hình rất ổn, khá đúng với true label:

Example #6198
Word           ||True ||Pred
****************************************
Tickets        : O     O
for            : O     O
the            : O     O
so-called      : O     O
"              : O     O
Football       : O     O
for            : O     O
Hope           : O     O
"              : O     O
match          : O     O
February       : B-tim B-tim
15             : I-tim I-tim
in             : O     O
Barcelona      : B-geo B-geo
will           : O     O
cost           : O     O
between        : O     O
$              : O     O
13             : O     O
-              : O     O
$              : O     O
38             : O     O
.              : O     OCode language: PHP (php)

Chú ý các bạn sẽ thấy từ February 15 dã được nhận đúng là time (B-tim) và Barcelone là B-geo, nghĩa là Geographical Entity.

Toàn bộ source và data các bạn có thể tải tại github của mình tại đây nhé!

Okie, như vậy mình đã guide các bạn cách tự train một model NER – Named Entity Recognition chạy được và ổn. Nếu còn vướng gì các bạn cứ post lên Group trao đổi, chia sẻ: https://facebook.com/groups/miaigroup để cùng giao lưu nhé.

Hẹn gặp lại các bạn trong các bài tiếp theo!

Chúc các bạn thành công!

Fanpage: http://facebook.com/miaiblog
Group trao đổi, chia sẻ: https://www.facebook.com/groups/miaigroup
Website: https://miai.vn
Youtube: http://bit.ly/miaiyoutube

Cảm ơn bài tham khảo tuyệt vời tại đây.

Related Post

4 Replies to “Named Entity Recognition – Nhận diện thực thể trong câu khi xử lý ngôn ngữ tự nhiên”

  1. em cảm ơn tác giả vì bài viết này, đúng thông tin em đang tìm kiếm. Nhưng a có thể cho em hỏi thêm là có cách nào để đánh giá độ confidence của mỗi word mình predict ra k a. Mong a giải đáp giúp em

  2. Em cám ơn anh rất nhiều à bài viết rất hữu ích. Nhưng khi em train lại thì em gặp lỗi là AttributeError: ‘Tensor’ object has no attribute ‘_keras_history’. Em đã tìm kiếm cách fix từ nhiều nguồn nhưng không thành công. Mong anh có thể dành thời gian giải đáp giúp em ạ. Em cám ơn rất nhiều!

Leave a Reply

Your email address will not be published. Required fields are marked *