Xin hỏi về sử dụng Fasttext để lấy word embeddings cho bài toán phân loại văn bản


#1

Chào các bạn,

Mình theo dõi trang machinelearningcoban đã lâu, nay trang có diễn đàn hữu ích như thế này mình cảm thấy vui và mong sau khi hoàn thành bài báo hiện tại, mình sẽ tham gia chia sẻ kiến thức cùng các bạn.

Mình có một câu hỏi cho các bạn làm/nghiên cứu/ứng dụng NLP ạ. Xin hỏi có bạn nào đã sử dụng công cụ fasttext của Facebook research group chưa ạ? Mình đã dùng vào bài toán của mình và đạt được kết quả tốt. Tuy nhiên, mình có chỗ này muốn hỏi các bạn có kinh nghiệm.

Theo nhóm tác giả thì điểm mạnh của fasttext so với các công cụ tương tự trong việc tạo ra các word embedding là fasttext cho phép sử dụng subword information. Theo đó, mỗi một word sẽ được biểu diễn bằng vector đã được học, nhưng vector này lại là tổng các các vector con, mà mỗi vector con đó là biểu diễn của các subword của từ đó.

Mình lấy ví dụ cho các bạn dễ hiểu. Giả sử ta đang muốn tính word embedding của từ “nature”. Từ này có độ dài là 6. Khi xử lý, có 2 kí tự prefix và subfix được thêm vào, nên nature sẽ thành và có độ dài 8.

Fasttext sẽ xem vector biểu diễn của từ nature bằng tổng các vector con của nó có độ dài từ 3 đến 6 (tham số này là mặc định và có thể thay đổi bằng các quy định lại tham số). Như vậy vector biểu diễn của nature sẽ bằng tổng vector biểu diễn của:

  • Nhóm subword có độ dài 3: <na, nat, atu, tur, ure, re>
  • Nhóm subword có độ dài 4: <nat, natu, atur, ture, ure>
  • Nhóm subword có độ dài 5: <natu, natur, ature, ture>
  • Nhóm subword có độ dài 6: <natur, nature, ature> Và bản thân từ đó:

Đó là mình hiểu theo định nghĩa từ bài báo có tên " Enriching Word Vectors with Subword Information". Ý tưởng của việc sử dụng subword information là từ bài báo này.

Tuy nhiên khi mình thực hành, mình thử query các vector con ở trên và cộng các vector này lại để xem có đúng tổng các vector con này có bằng vector ban đầu ko thì mình thấy ko phải như vậy. Ở đây, mỗi vector của mình chỉ có 1 chiều. Nghĩa là vector tổng chỉ đơn giản là cộng các số thực lại với nhau. Kết quả ko như mình mong đợi nên mình nghĩ có thể mình đã hiểu sai ở đâu đó.

Mong nhận được góp ý của các bạn.

Link đến fasttext cho các bạn chưa biết về nó: https://fasttext.cc/


#2

Trích từ Word representations docs:

A nice feature is that you can also query for words that did not appear in your data! Indeed words are represented by the sum of its substrings. As long as the unknown word is made of known substrings, there is a representation of it!

Em thấy thắc mắc anh đang nói có ở bài toán word representation của Fastext. Đối với một từ chưa có trong dữ liệu huấn luyện, Fasttext sẽ dự đoán dựa vào các subword đã biết.

Em có 1 thắc mắc: Những gì anh đang nói là cho bài toán wordrepresentation hay cho cả bài toán text classification ạ?


#3

Anh ơi,

Cái này đúng ra phải là <na, nat, atu, tur, ure, re> và <nature>


#4

Fasttext có 2 chức năng chính là text classification và word representation. Mình thắc mắc ở trên là ở chức năng word representation. Với bài toán của mình, mình ko cần dùng model của fasttext để classify, mà chỉ cần word embedding của fasttext để bỏ vào model của mình thôi.


#5

Bản thân từ chỉ được tính một lần thôi mà, mình có nhắc đến sau chỗ subword có độ dài 6 đó


#6

Vậy anh sửa lại tiêu đề của topic để tránh hiểu lầm ạ :expressionless:


#7

Sr anh, em không đọc kỹ ạ :sweat_smile:


#8

Cảm ơn em, mình sửa tiêu đề lại rồi


#9

Em hỏi một câu nữa ạ: Anh lấy vector giá trị của các subword bằng cách nào ạ?


#10

Em mới chạy thử theo cách hướng dẫn issue này và thu được kết quả như kết luận của anh.

Đây là code em đã thử. Em cũng không rõ tại sao :expressionless:

./fasttext skipgram -input train.txt -output model -dim 5 -minn 5 -maxn 5

>>> import fastText as ft
>>> model = ft.load_model('model.bin')

>>> model.get_word_vector('hello')
array([ 0.0463528 ,  0.11203098, -0.03840314, -0.00065155, -0.01394757],
      dtype=float32)

>>> model.get_subwords('hello')
(['<hell', 'hello', 'ello>'], array([1361402, 1831817, 1616975]))

>>> a = model.get_input_vector(1361402)
>>> b = model.get_input_vector(1616975)
>>> c = model.get_input_vector(1831817)

>>> a + b + c
array([ 0.13905838,  0.33609292, -0.11520942, -0.00195466, -0.04184271],
      dtype=float32)

Câu hỏi đặt ra: Giả sử nếu subword ‘<hell’ cũng không xuất hiện trong train data(không có từ nào để tách ra được subword này) thì fastText tính ra sao?


#11

Bạn tính mean sẽ ra được.

Original Fasttext:

void FastText::getWordVector(Vector& vec, const std::string& word) const {
  const std::vector<int32_t>& ngrams = dict_->getSubwords(word);
  vec.zero();
  for (int i = 0; i < ngrams.size(); i ++) {
    addInputVector(vec, ngrams[i]);
  }
  if (ngrams.size() > 0) {
    vec.mul(1.0 / ngrams.size());
  }
}

Fasttext trong gensim:

 def get_vocab_word_vecs(self, wv):
        """Calculate vectors for words in vocabulary and stores them in `vectors`."""
        for w, v in wv.vocab.items():
            word_vec = np.copy(wv.vectors_vocab[v.index])
            ngrams = _compute_ngrams(w, wv.min_n, wv.max_n)
            ngram_weights = wv.vectors_ngrams
            for ngram in ngrams:
                word_vec += ngram_weights[wv.hash2index[_ft_hash(ngram) % self.bucket]]
            word_vec /= (len(ngrams) + 1)
            wv.vectors[v.index] = word_vec

Cần chú ý thêm chỗ word_vec /= (len(ngrams) + 1) tại sao lại + 1. Nghĩa là nếu bản thân từ đó đã có trong tập vocabulary thì ngoài các vectors của subwords của từ đó ra thì ta phải tính luôn cả vector của bản thân từ đó (Code C++ cũng tương tự nhưng bạn sẽ không thấy rõ điều đó nếu chỉ nhìn đoạn mình trích ra).


#12

Bạn Anh Khoa ơi, mình tính trung bình như bạn mà ko ra kết quả như bạn nói Đây là vector mình cần tính: GEKGD Các subword của nó sẽ là:

Bạn có thời gian mong bạn xem giúp screenshot kết quả query của mình với

. Rõ ràng khi lấy tổng 15 giá trị đầu rồi chia cho 16, mình ko được giá trị cuối. Giá trị cuối là 2.1514 trong khi tổng 15 giá trị đầu chia cho 16 là 2.677.

Cảm ơn bạn!


#13

Quy ước của fasttext là vậy bạn à, nó tự thêm < vào đầu và > vào cuối. Chữ <hell là thuộc subword có độ dài là 5.


#14

Anh Khoa ơi, mình đưa thêm ví dụ với data mẫu trên tutorial của fasttext luôn nữa đây bạn ( stackexchange question about cooking). Mình query với chữ “should”.

Mình vẫn ko thấy đúng như bạn nói.


#15

Hi bạn, bạn test như cách của bạn nguyenvanhieu.vn giúp mình nha (điều chỉnh các tham số lại cho giống của bạn).

Bổ sung: mình đoán cách query của bạn bị sai, ví dụ bạn query từ subword <sh thì thực sự bên dưới sẽ là <<sh>

./fasttext skipgram -input train.txt -output model -dim 5 -minn 5 -maxn 5

>>> import fastText as ft
>>> model = ft.load_model('model.bin')

>>> model.get_word_vector('hello')
array([ 0.0463528 ,  0.11203098, -0.03840314, -0.00065155, -0.01394757],
      dtype=float32)

>>> model.get_subwords('hello')
(['<hell', 'hello', 'ello>'], array([1361402, 1831817, 1616975]))

>>> a = model.get_input_vector(1361402)
>>> b = model.get_input_vector(1616975)
>>> c = model.get_input_vector(1831817)

>>> a + b + c
array([ 0.13905838,  0.33609292, -0.11520942, -0.00195466, -0.04184271],
      dtype=float32)

Nghĩa là bạn gọi hàm get_subwords để xem model sinh ra các subwords nào, có giống chính xác với bạn tự sinh không. Sau khi gọi hàm đó bạn sẽ có 2 mảng, 1 mảng chứa danh sách các subwords dưới dạng string, 1 mảng chứa các ID của subwords đó, bạn gọi hàm get_input_vector để lấy vector ra dựa vào ID. Sau đó tính lại như mình nói.

Trong ví dụ của bạn nguyenvanhieu.vn thì đúng như những gì mình nói ở trên, lấy ví dụ chiều thứ 1 của vector hello: 0.13905838 / 3 \approx 0.0463528, tương tự cho các chiều còn lại.


#16

Trả lời ngắn gọn sẽ là nếu subword <hell không xuất hiện trước đó thì Fasttext vẫn tính được, nhưng vector của nó sẽ rơi vào 2 trường hợp sau:

  1. Vector “không có ý nghĩa” (1 vector sinh ngẫu nhiên theo uniform distribution)
  2. Là vector của một subword khác. (collision)

Cụ thể,

Phần này mình nói theo mình hiểu, sẽ hơi khác một chút trong cài đặt nhưng bản chất là như nhau.

Gọi:

d là số chiều của vector.

V có kích thước (\text{Vocab_size}, d) là ma trận chứa các vector của tập vocabulary của bạn (không chứa vector của subwords).

B có kích thước (\text{Bucket_size}, d) là ma trận chứa các vector của tập subwords được sinh ra từ vocabulary. Tham số Bucket_size của Fasttext mặc định là 2.000.000.

Để tính được index (vị trí để lưu vector của một word/subword) trong ma trận có k dòng, chúng ta tính như sau:

\begin{equation} ID = f_{hash}\text(word) \mod k \end{equation}\qquad(1)

(Không hiểu sao không thể đánh số cho cái formula trên được nhỉ, phải dùng trick :frowning: )

Khi đó, bởi vì modulo cho k nên giá trị của ID sẽ rơi vào đoạn [0, k-1].

Cũng nói thêm chút xíu trong quá trình TRAINING, nếu tại vị trí ID đó của ma trận đã chứa một vector của word/subword khác thì ID mới sẽ được tính như sau (nhảy sang dòng kế tiếp cho đến khi tìm thấy chỗ):

\begin{equation} ID = (ID + 1) \mod k \end{equation}\qquad(2)

Ban đầu, ma trận V, B sẽ được khởi tạo ngẫu nhiên theo uniform distribution. Kế đến, tính qua các words trong vocabulary để có được vector và cập nhật vào vị trí index (sử dụng (2)) của ma trận tương ứng (words thuộc vocabulary thì cập nhật vào V, còn subwords của words đó thì cập nhật vào B).

Dễ thấy rằng ma trận B có số dòng rất lớn, nếu như không “điền hết” các dòng của B thì những dòng chưa điền sẽ là các vector được khởi tạo ngẫu nhiên. Khi đó, trong quá trình INFERENCE, sẽ có khả năng tính ra ID mà rơi vào những dòng này trong ma trận dẫn đến trường hợp 1 mà mình nói, tức là vector không có ý nghĩa (có thể xem như là các NULL vectors).

Cũng dễ thấy rằng, việc tính ID theo (1) có khả năng xảy ra collision (trường hợp 2) nếu như hàm hash không đủ tốt, hoặc ma trận B đã được điền đầy. Chính vì thế mà các tác giả khuyến cáo chọn tham số bucket size tương đối lớn và trong Fasttext luôn “cắt tỉa” tập vocabulary khi nó gần đầy (khoảng 70% hay gì đó mình quên rồi). Để kiểm chứng bạn có thể chọn bucket size nhỏ cỡ 10 rồi train, sau đó infer sẽ thấy ID trùng.

Hi vọng giải đáp thắc mắc của bạn.


#17

Em cảm ơn anh, anh giải thích rất chi tiết và cụ thể ạ.


#18

Em có thắc mắc về việc sử dụng fasttext cho tiếng việt, nếu một token là “thành phố” thì lấy vector của nó luôn hay là tách thành “thành” và “phố” rồi lấy mean của 2 vector ạ? em cảm ơn!


#19

e cũng có thắc mắc giống bạn trên. ví dụ như với 1 token là “web service” thì vector của nó sẽ cộng vecto của “web” + “service” rồi chia 2 hay thế nào vậy ạ?


#20

Cho em hỏi model cho vietnamese có hd đọc như nào từ model có sẵn của nó ko ? Với để transfer learning lại trên small corpus của mình thì làm ntn aj? em cảm ơn