2.2.2. Feature extraction#

https://phamdinhkhanh.github.io/deepai-book/ch_ml/FeatureEngineering.html?fbclid=IwAR3jZLutFC58Bg_31esTUkwj_fG79S4uYIW5N2VXS4OkBeanzPuk47x_DK0#trich-loc-dac-trung-cho-van-ban

2.2.2.1. FE for text#

Dữ liệu văn bản có thể tồn tại ở nhiều dạng khác nhau như chữ cái thường, chữ cái hoa, dấu câu, các kí tự đặc biệt,… Các ngôn ngữ khác nhau cũng có mẫu kí tự khác nhau và cấu trúc ngữ pháp khác nhau.

Vấn đề chính của dữ liệu dạng văn bản đó là làm thể nào để mã hoá được kí tự về dạng số? Kĩ thuật tokenization sẽ giúp ta thực hiện điều này. tokenization là việc chúng ta chia văn bản theo đơn vị nhỏ nhất và xây dựng một từ điển đánh dấu index cho những đơn vị này. Có hai kiểu mã hoá chính là hoá theo từ hoá theo tự.

  • hoá theo từ thì các từ trong câu sẽ là đơn vị nhỏ nhất. Trong Tiếng Anh thì từ chủ yếu tồn tại ở dạng từ đơn trong khi Tiếng Việt tồn tại các từ ghép. Khi mã hoá theo từ thì kích thước của từ điển sẽ rất lớn, tuỳ thuộc vào số lượng các từ khác nhau xuất hiện trong toàn bộ các văn bản.

  • hoá theo tự thì chúng ta sẽ sử dụng các kí hiệu trong bảng chữ cái để làm từ điển mã hoá từ. Kích thước của bộ từ điển khi mã hoá theo kí tự sẽ nhỏ hơn so với mã hoá theo từ.

2.2.2.1.1. Bag-of-words#

Theo phương pháp bag-of-word chúng ta sẽ mã hoá các từ trong câu thành một véc tơ có độ dài bằng số lượng các từ trong từ điển và đếm tần suất xuất hiện của các từ. Tần xuất của từ thứ trong từ điển sẽ chính bằng phần tử thứ trong véc tơ.

from sklearn.feature_extraction.text import CountVectorizer

texts = ['i have a cat', 
        'he has a dog', 
        'he has a dog and he has a cat']

vect = CountVectorizer()
X = vect.fit_transform(texts)
print('words in dictionary: ', vect.get_feature_names_out())
print(X.toarray())
words in dictionary:  ['and' 'cat' 'dog' 'has' 'have' 'he']
[[0 1 0 0 1 0]
 [0 0 1 1 0 1]
 [1 1 1 2 0 2]]

Các biểu diễn theo túi từ có hạn chế đó là chúng ta không phân biệt được 2 câu văn có cùng các từ bởi túi từ không phân biệt thứ tự trước sau của các từ trong một câu. Chặng như ‘you have no dog’ và ‘no, you have dog’ là 2 câu văn có biểu diễn giống nhau mặc dù có ý nghĩa trái ngược nhau.

vect = CountVectorizer(ngram_range = (1, 1))
vect.fit_transform(['you have no dog', 'no, you have dog']).toarray()
array([[1, 1, 1, 1],
       [1, 1, 1, 1]])

Chính vì thế phương pháp bag-of-n-gram sẽ được sử dụng thay thế.

2.2.2.1.2. bag-of-n-gram#

Một n-grams là một chuỗi bao gồm tokens. Trong trường hợp từ ta gọi là unigram, đối với 2 từ là bigram và 3 từ là trigram. Khi thực hiện tokenization với n-grams thì trong từ điển sẽ xuất hiện những cụm từ nếu chúng xuất hiện trong các văn bản. Chẳng hạn như câu “I have a dog” sẽ được tokenize thành “I have”, “have a”, “a dog”. Như vậy số lượng các từ trong từ điển sẽ gia tăng một cách đáng kể. Nếu chúng ta có k từ đơn thì có thể lên tới \(k^2\) từ trong bigram. Nhưng thực tế không phải hầu hết các từ đều có thể ghép đôi với nhau nên véc tơ biểu diễn của câu trong bigram là một véc tơ rất thưa và có số chiều lớn. Điều này dẫn tới tốn kém về chi phí tính toán và lưu trữ.

# bigram
bigram = CountVectorizer(ngram_range = (2, 2))
n1, n2, n3 = bigram.fit_transform(['you have no dog', 'no, you have dog', 'you have a dog']).toarray()

# trigram
trigram = CountVectorizer(ngram_range = (3, 3))
n1, n2, n3 = trigram.fit_transform(['you have no dog', 'no, you have dog', 'you have a dog']).toarray()
# euclidean distance of vector
from scipy.spatial.distance import euclidean
print(euclidean(n1, n2), euclidean(n2, n3), euclidean(n1, n3))
2.0 1.0 1.7320508075688772

2.2.2.1.3. TF-IDF#

Giả sử chúng ta có một bộ văn bản (corpus) bao gồm rất nhiều các văn bản con. Những từ hiếm khi được tìm thấy trong bộ văn bản (corpus) nhưng có mặt trong một số chủ đề nhất định có thể chiếm vai trò quan trọng hơn. Ví dụ đối với chủ đề gia đình thì các từ như cha mẹ, ông bà, con cái, anh em, chị em xuất hiện nhiều hơn so với các chủ đề khác.

Ngoài ra cũng có những từ xuất hiện rất nhiều trong văn bản nhưng chúng xuất hiện ở hầu như mọi chủ đề, mọi văn bản chẳng hạn như the, a, an. Những từ như vậy được gọi là stopwords vì chúng không có nhiều ý nghĩa đối với việc phân loại văn bản. Khi mã hoá ngôn ngữ thì chúng ta sẽ tìm cách loại bỏ những từ stopwords bằng cách sử dụng từ điển có sẵn các từ stopwords quan trọng.

Phương pháp TF-IDF là một phương pháp mà chúng ta sẽ đánh trọng số cho các từ mà xuất hiện ở một vài văn bản cụ thể lớn hơn thông qua công thức:

\[\begin{split}\begin{eqnarray}\text{idf}(t,D) & = & \log\frac{\mid D \mid}{|\{d \in D; t \in d \}|+ 1} = \log \frac{\mid D\mid}{\text{df}(d, t)+ 1} \\ \text{tfidf}(t,d,D) & = & \text{tf}(t,d) \times \text{idf}(t,D) \end{eqnarray}\end{split}\]

trong đó:

  • \(\mid D \mid\) là số lượng các văn bản trong bộ văn bản.

  • \(\text{df}(d, t) = |\{d \in D; t \in d \}|\) là tần suất các văn bản \(d \in D\) mà từ \(t\) xuất hiện.

  • \(\text{tf}(t,d)\) là tần suất xuất hiện của từ \(t\) trong văn bản \(d\).

Như vậy \(\text{idf}(t, D)\) là chỉ số nghịch đảo tần suất văn bản (inverse document frequency) chỉ số này bằng logarith của nghịch đảo số lượng văn bản chia cho số lượng văn bản chứa một từ cụ thể \(t\). Một từ cụ thể có \(\text{idf}(t,D)\) lớn chứng tỏ rằng từ đó chỉ xuất hiện trong một số ít các văn bản.

\(\text{tfidf}(t, d, D)\) tỷ lệ thuận với tần suất của từ xuất hiện trong văn bảnnghịch đảo tần suất văn bản. Ta có thể giải thích ý nghĩa của \(\text{tfidf}\) đối với đánh giá mức độ quan trọng của từ như sau: Khi một từ càng quan trọng thì nó sẽ có tần suất xuất hiện trong một văn bản cụ thể, chẳng hạn văn bản \(d\) lớn, tức là \(\text{tf}(t,d)\) lớn; Đồng thời từ đó phải không là stopwords, tức là số lượng văn bản mà nó xuất hiện trong toàn bộ bộ văn bản nhỏ, suy ra \(\text{idf}(t, D)\) phải lớn.

Để mã hoá văn bản dựa trên phương pháp tfidf chúng ta sử dụng package sklearn như sau:

from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
 	'tôi thích ăn bánh mì nhân thịt',
	'cô ấy thích ăn bánh mì, còn tôi thích ăn xôi',
	'thị trường chứng khoán giảm làm tôi lo lắng',
	'chứng khoán sẽ phục hồi vào thời gian tới. danh mục của tôi sẽ tăng trở lại',
  'dự báo thời tiết hà nội có mưa vào chiều và tối. tôi sẽ mang ô khi ra ngoài'
]

# Tính tfidf cho mỗi từ. max_df để loại bỏ stopwords xuất hiện ở hơn 90% các câu
vectorizer = TfidfVectorizer(max_df = 0.9)
# Tokenize các câu theo tfidf
X = vectorizer.fit_transform(corpus)
print('words in dictionary:')
print(vectorizer.get_feature_names())
print('X shape: ', X.shape)
words in dictionary:
['bánh', 'báo', 'chiều', 'chứng', 'còn', 'có', 'cô', 'của', 'danh', 'dự', 'gian', 'giảm', 'hà', 'hồi', 'khi', 'khoán', 'lo', 'làm', 'lại', 'lắng', 'mang', 'mì', 'mưa', 'mục', 'ngoài', 'nhân', 'nội', 'phục', 'ra', 'sẽ', 'thích', 'thị', 'thịt', 'thời', 'tiết', 'trường', 'trở', 'tăng', 'tối', 'tới', 'và', 'vào', 'xôi', 'ăn', 'ấy']
X shape:  (5, 45)

Ta có thể thấy từ tôi xuất hiện ở toàn bộ các câu và không mang nhiều ý nghĩa của chủ đề của câu nên có thể coi là một stopword. Bằng phương pháp lọc cận trên của tần suất xuất hiện từ trong văn bản là 90% ta đã loại bỏ được từ này khỏi dictionary.

Các phương pháp bỏ túi có thể tìm được một số cuộc thi trên kaggle như Catch me if you can competition, bag of app, bag of event:

2.2.2.1.4. Word2vec#

Word2vec is a group of related models that are used to produce word embeddings. These models are shallow, two-layer neural networks that are trained to reconstruct linguistic contexts of words. Word2vec takes as its input a large corpus of text and produces a vector space, typically of several hundred dimensions, with each unique word in the corpus being assigned a corresponding vector in the space. Word vectors are positioned in the vector space such that words that share common contexts in the corpus are located close to one another in the space.[1]

word2vec là một nhóm các mô hình sử dụng để tạo ra biểu diễn nhúng cho từ. Những mô hình này tương đối nông, chỉ bao gồm những mạng neural 2 layers được huấn luyện để tái tạo lại bối cảnh ngôn ngữ cho từ. Thông qua mô hình word2vec mỗi một từ trong một bộ văn bản được biểu diễn thông qua một véc tơ trong không gian cao chiều, có thể lên tới hàng trăm chiều, sao cho các từ có chung ngữ cảnh sẽ được đặt gần nhau hơn trong không gian.

Chẳng hạn dưới đây là một ví dụ sau khi thực hiện mã hoá từ thông qua mô hình word2vec thì các từ king, queen, man, woman có mối liên hệ theo công thức: king - man + woman = queen

Hình 2: Mô hình word2vec đã định vị véc tơ biểu diễn cho những từ có chung ngữ cảnh thì được đặt gần nhau hơn. Để thực hiện được những biểu diễn từ chính xác, các mô hình cần được đào tạo trên các tập dữ liệu rất lớn để bao quát được đa dạng các ngữ cảnh khác nhau của từ. Các mô hình pretrained cho xử lý ngôn ngữ tự nhiên có thể được tải về tại word2vec - api.

Các phương pháp tương tự được áp dụng trong các lĩnh vực khác như trong tin sinh. Một ứng dụng khác nữa là food2vec.

Tại một vị trí cụ thể trong câu văn chúng ta sẽ xác định được một từ mục tiêu và các từ bối cảnh. Từ mục tiêu là từ ở vị trí được lựa chọn còn từ bối cảnh là những từ ở vị trí xung quanh giúp tạo ra bối cảnh ngữ nghĩa cho từ mục tiêu.

Giả sử chúng ta có một câu văn như sau: “Tôi muốn một chiếc cốc màu xanh”. Nếu lựa chọn một context window bao gồm 3 từ liền kề thì chúng ta sẽ lần lượt thu được các bộ 3 từ: tôi muốn một, muốn một chiếc, một chiếc cốc, chiếc cốc màu, cốc màu xanh. Đối với những bộ 3 từ này thì các từ ở giữa sẽ là từ mục tiêu và từ bối cảnh là những từ ở đầu và ở cuối. Như vậy chúng ta sẽ có các cặp từ mục tiêu và bối cảnh như sau:

[(('tôi', 'một'), 'muốn'), (('muốn', 'chiếc'), 'một'), (('một', 'cốc'), 'chiếc'), (('chiếc', 'màu'), 'cốc'), (('cốc', 'xanh'), 'màu')]

Mô hình word2vec có 2 phương pháp chính là skip-grams và CBOW như sau:

Hình 3: Mô hình CBOW và Skip-gram trong word2vec.

2.2.2.1.4.1. Phương pháp CBOW#

Đối với mô hình CBOW chúng ta sẽ xây dựng một mô hình học có giám sát sử dụng đầu vào là các từ bối cảnh, chẳng hạn như trong hình là các từ \(\mathbf{w}_{t-2}, \mathbf{w}_{t-1}, \mathbf{w}_{t+1}, \mathbf{w}_{t+2}\) để giải thích từ mục tiêu ở vị trí hiện tại là \(\mathbf{w}_t\).

Các từ \(\mathbf{w}_t\) đã được mã hoá dưới dạng véc tơ one-hot trong không gian \(\mathbb{R}^{d}\) chiều để có thể đưa vào huấn luyện. Ở đây \(d\) chính là kích thước của từ điển. Như vậy ở phương pháp CBOW chúng ta có 5 véc tơ one-hot đầu vào với số chiều bằng với số lượng từ trong từ điển. Sau đó những véc tơ này được giảm chiều dữ liệu thông qua một phép chiếu lên không gian thấp chiều, chẳng hạn 200 chiều, bước này chính là projection trên hình vẽ. Kết quả thu được là một véc tơ embedding \(\mathbf{e}_c \in \mathbb{R}^{200}\). Sau cùng, phân phối xác suất của từ mục tiêu được dự báo thông qua một hàm softmax áp dụng lên véc tơ \(\mathbf{e}_c\). Quá trình huấn luyện mô hình sẽ dựa trên hàm softmax dạng cross-entropy:

\[\mathcal{L}(\mathbf{y}, \hat{\mathbf{y}}) = -\sum_{i=1}^{d} y_i\log(\hat{y}_i)\]

Trong đó \(\hat{y}_i\) là xác suất dự báo từ mục tiêu tương ứng với từ ở vị trí index thứ \(i\) trong từ điển, được tính theo công thức softmax:

\[\hat{y_i} = \frac{\exp(\mathbf{w}_{:i}^{\intercal}\mathbf{e}_c)}{\sum_{i=1}^{d}\exp(\mathbf{w}_{:i}^{\intercal}\mathbf{e}_c)}\]

\(\mathbf{w}_{:i} \in \mathbb{R}^{200}\) chính là véc tơ tham số kết nối toàn bộ các node thuộc \(\mathbf{e}_c\) tới vị trí node thứ \(i\) của layer cuối cùng.

Sau quá trình lan truyền thuận và lan truyền ngược, các hệ số của mô hình sẽ được cập nhật và chúng ta sẽ thu được biểu diễn từ dần chuẩn xác hơn. Một từ đầu vào sẽ có biểu diễn thông qua phương pháp CBOW chính là véc tơ \(\mathbf{e}_c\).

from tensorflow.keras.preprocessing import text
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.preprocessing import sequence
from nltk.corpus import gutenberg
from string import punctuation
import nltk

# download bộ văn bản gutenberg
nltk.download('gutenberg')
nltk.download('punkt')
norm_bible = gutenberg.sents('bible-kjv.txt') 
norm_bible = [' '.join(doc) for doc in norm_bible]

# tokenize văn bản
tokenizer = text.Tokenizer()
tokenizer.fit_on_texts(norm_bible)
word2id = tokenizer.word_index

# khởi tạo từ điển cho bộ văn bản
word2id['PAD'] = 0
id2word = {v:k for k, v in word2id.items()}
vocab_size = len(word2id)

print('Vocabulary Size:', vocab_size)
print('Vocabulary Sample:', list(word2id.items())[:10])
# Mã hoá câu văn bằng index
wids = [[word2id[w] for w in text.text_to_word_sequence(doc)] for doc in norm_bible]
print('Embedding sentence by index: ', wids[:5])
%%script echo skipping
# Xác định context and target
import numpy as np
def generate_context_word_pairs(corpus, window_size, vocab_size):
    context_length = window_size*2
    for words in corpus:
        sentence_length = len(words)
        # print('words: ', words)
        for index, word in enumerate(words):
            context_words = []
            label_word   = [] 
            # Start index of context
            start = index - window_size
            # End index of context
            end = index + window_size + 1
            # List of context_words
            context_words.append([words[i] for i in range(start, end) if 0 <= i < sentence_length and i != index])
            # List of label_word (also is target word).
            # print('context words {}: {}'.format(context_words, index))
            label_word.append(word)
            # Padding the input 0 in the left in case it does not satisfy number of context_words = 2*window_size.
            x = sequence.pad_sequences(context_words, maxlen=context_length)
            # print('context words padded: ', x)
            # Convert label_word into one-hot vector corresponding with its index
            y = to_categorical(label_word, vocab_size)
            yield (x, y)
            
            
# Test this out for some samples
i = 0
window_size = 2 # context window size
for x, y in generate_context_word_pairs(corpus=wids, window_size=window_size, vocab_size=vocab_size):
    if 0 not in x[0]:
        print('Context (X):', [id2word[w] for w in x[0]], '-> Target (Y):', id2word[np.argwhere(y[0])[0][0]])
    
        if i == 10:
            break
        i += 1
# Xây dựng mô hình CBOW là một mạng fully connected gồm 3 layers
import tensorflow.keras.backend as K
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Embedding, Lambda
embed_size = 100
window_size=2
# build CBOW architecture
cbow = Sequential()
cbow.add(Embedding(input_dim=vocab_size, output_dim=embed_size, input_length=window_size*2))
cbow.add(Lambda(lambda x: K.mean(x, axis=1), output_shape=(embed_size,)))
cbow.add(Dense(vocab_size, activation='softmax'))
cbow.compile(loss='categorical_crossentropy', optimizer='rmsprop')

# view model summary
print(cbow.summary())
%%script echo skipping
# Huấn luyện model với 5 epochs với 100 quan sát đầu tiên

for epoch in range(1, 6):
    loss = 0.
    i = 0
    for x, y in generate_context_word_pairs(corpus=wids[:100], window_size=window_size, vocab_size=vocab_size):
        i += 1
        loss += cbow.train_on_batch(x, y)
        if i % 500 == 0:
            print('Processed {} (context, word) pairs'.format(i))

    print('Epoch:', epoch, '\tLoss:', loss)

2.2.2.1.4.2. Phương pháp skip-gram#

Phương pháp skip-gram thực chất là một phiên bản đảo ngược của phương pháp CBOW. Chúng ta sẽ sử dụng đầu vào là các từ mục tiêu và dự báo các từ bối cảnh dự vào từ mục tiêu. Như thể hiện ở hình 3 thì \(\mathbf{w}_t\) chính là từ mục tiêu được sử dụng làm đầu vào, các từ \(\mathbf{w}_{t-2}, \mathbf{w}_{t-1}, \mathbf{w}_{t+1}, \mathbf{w}_{t+2}\) là những từ bối cảnh cần được dự đoán. Những từ này đều được mã hoá thành véc tơ one-hot trong không gian \(\mathbb{R}^{d}\). Sau đó véc tơ one-hot sẽ được chiếu lên không gian nhằm giảm chiều dữ liệu xuống còn chẳng hạn \(200\) chiều. Đầu ra thu được là véc tơ \(\mathbf{e}_c\) có kích thước 200, đây cũng chính là biểu diễn nhúng của từ trong skip-gram. Cuối cùng chúng ta sử dụng một sigmoid layer để dự đoán xem từ mục tiêu \(\mathbf{w}_t\) và từ bối cảnh \(\mathbf{w}_j\) (\(\mathbf{w}_j\) được lựa chọn ngẫu nhiên từ từ điển) có cùng bối cảnh hay không?

from tensorflow.keras.preprocessing.sequence import skipgrams

window_size=2
# generate skip-grams
skip_grams = [skipgrams(wid, vocabulary_size=vocab_size, window_size=window_size) for wid in wids[:100]]

# view sample skip-grams
pairs, labels = skip_grams[0][0], skip_grams[0][1]
for i in range(10):
    print("({:s} ({:d}), {:s} ({:d})) -> {:d}".format(
          id2word[pairs[i][0]], pairs[i][0], 
          id2word[pairs[i][1]], pairs[i][1], 
          labels[i]))
from tensorflow.keras.layers import dot, concatenate
from tensorflow.keras import Input
from tensorflow.keras.layers import Dot, Dense, Reshape, Embedding
from tensorflow.keras.models import Sequential, Model

# build skip-gram architecture
word_input = Input(shape = (1,))
word_embed = Embedding(vocab_size, embed_size,
                         embeddings_initializer="glorot_uniform",
                         input_length=1, name = 'word_embedding')(word_input)
word_output = Reshape((embed_size, ))(word_embed)
word_model = Model(word_input, word_output)

print('word_model: \n', word_model.summary())
context_input = Input(shape = (1,))
context_embed = Embedding(vocab_size, embed_size,
                  embeddings_initializer="glorot_uniform",
                  input_length=1, name = 'context_embedding')(context_input)
context_output = Reshape((embed_size,))(context_embed)
context_model = Model(context_input, context_output)
print('context_model: \n', context_model.summary())

concate = dot([word_output, context_output], axes = -1)
dense = Dense(1, kernel_initializer="glorot_uniform", activation="sigmoid")(concate)
model = Model(inputs = [word_input, context_input], outputs = dense)
model.compile(loss="mean_squared_error", optimizer="rmsprop")

# view model summary
print('model merge word and context: \n', model.summary())
%%script echo skipping
# Để cho nhanh thì mình sẽ training trên 100 skip_grams đầu tiên.
for epoch in range(1, 6):
    loss = 0
    for i, elem in enumerate(skip_grams[:100]):
        pair_first_elem = np.array(list(zip(*elem[0]))[0], dtype='int32')
        pair_second_elem = np.array(list(zip(*elem[0]))[1], dtype='int32')
        labels = np.array(elem[1], dtype='int32')
        X = [pair_first_elem, pair_second_elem]
        Y = labels
        if i % 500 == 0:
            print('Processed {} (skip_first, skip_second, relevance) pairs'.format(i))
        loss += model.train_on_batch(X,Y)  

    print('Epoch:', epoch, 'Loss:', loss)

2.2.2.1.4.3. Sử dụng gensim huấn luyện mô hình word2vec#

Huấn luyện mô hình word2vec sử dụng mạng nơ ron là để chúng ta hiểu rõ hơn về cấu trúc mạng nơ ron và cách thức hoạt động của mạng. Trên thực tế để huấn luyện mô hình word2vec chúng ta có thể thông qua package gensim như sau:

from gensim.models import Word2Vec
# Training model với 1000 câu đầu tiên trong kinh thánh
sentences = [[item.lower() for item in doc.split()] for doc in norm_bible[:1000]]
model = Word2Vec(sentences, min_count = 1, vector_size = 150, window = 10, sg = 1, workers = 8)
model.train(sentences, total_examples = model.corpus_count, epochs = 10)

Tìm biểu diễn véc tơ nhúng của một từ:

print('embedding vector shape: ', model.wv['king'].shape)
model.wv['king'][:5]

2.2.2.2. FE for image#

Trong quãng thời gian trước đây khi tài nguyên tính toán còn hạn chế và “thời kì phục hưng của mạng thần kinh” vẫn chưa thực sự quay trở lại, khai phá đặc trưng cho dữ liệu hình ảnh là một lĩnh vực phức tạp. Người ta phải thiết kế những bộ trích lọc thủ công để trích lọc các đặc trưng như góc, cạnh, đường nét ngang, dọc, chéo,…. Những thuật toán như HOG, SHIFT là phương pháp thường được sử dụng để trích lọc đặc trưng. Nhược điểm của những phương pháp này đó là tách rời bộ trích lọc đặc trưng (feature extractor) và bộ phân loại (classifier) nên mô hình có tốc độ huấn luyện và dự báo chậm.

Thời kì tan băng của deep learning đã khiến mạng CNN phát triển mạnh mẽ. Những kiến trúc mạng CNN hiện đại ngày càng trở nên sâu hơn và đạt độ chính xác cao. Đây là những kiến trúc end-to-end cho phép các bộ trích lọc đặc trưng gắn liền với bộ phân loại trong một pipeline duy nhất. Các bộ trích lọc cũng không cần khởi tạo một cách thủ công mà trái lại chúng được sinh ngẫu nhiên theo các phân phối giả định.

Nhờ các nguồn tài nguyên gồm các mô hình pretrained sẵn có mà bạn không cần phải tìm ra kiến trúc và huấn luyện mạng từ đầu. Thay vào đó, có thể tải xuống một mạng hiện đại đã được huấn luyện với trọng số từ các nguồn đã được công bố. Các nhà khoa học dữ liệu thường thực hiện điều chỉnh để thích ứng với các mạng này theo nhu cầu của họ bằng cách “tách” các lớp kết nối đầy đủ (fully connected layers) cuối cùng của mạng, thêm các lớp mới được thiết kế cho một nhiệm vụ cụ thể, và sau đó đào tạo mạng trên dữ liệu mới. Nếu nhiệm vụ của bạn chỉ là vector hóa hình ảnh, bạn chỉ cần loại bỏ các lớp cuối cùng và sử dụng kết quả đầu ra từ các lớp trước đó:

Hình 4: Đây là một mô hình phân lớp được huấn luyện trên một bộ dữ liệu từ trước hay còn gọi là mô hình pretrained. Lớp cuối cùng của mạng được tách ra và sử dụng để huấn luyện lại trên tập dữ liệu mới nhằm điều chỉnh để dự báo cho bộ dữ liệu mới.

Tuy nhiên, chúng ta sẽ không tập trung quá nhiều vào kỹ thuật mạng nơ ron. Thay vào đó các feature được tạo thủ công vẫn rất hữu ích: ví dụ đối với bài toán trong cuộc thi Rental Listing Inquiries - Kaggle Competition, để dự đoán mức độ phổ biến của danh sách cho thuê, ta có thể giả định rằng các căn hộ có ánh sáng sẽ thu hút nhiều sự chú ý hơn và tạo một feature mới như “giá trị trung bình của pixel”.

Trích lọc thông tin văn bản trên hình ảnh

OCR (Optical character recognition) là dạng bài toán trích lọc thông tin văn bản trên hình ảnh. Chúng có tính ứng dụng cao và thường mang lại nhiều thông tin khi xử lý dữ liệu dạng hình ảnh.

Chằng hạn nếu có văn bản trên hình ảnh, bạn có thể đọc nó để khai thác một số thông tin thông qua gói phát hiện văn bản trong hình ảnh pytesseract.

%%script echo skipping
!sudo apt-get install tesseract-ocr
import requests
from io import BytesIO
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

%matplotlib inline

##### Just a random picture from search
img = 'http://ohscurrent.org/wp-content/uploads/2015/09/domus-01-google.jpg'
img = requests.get(img)

img = Image.open(BytesIO(img.content))

# show image
img_arr = np.array(img)
plt.imshow(img_arr)

Đọc một hình ảnh thiết kế căn hộ thông qua link.

%%script echo skipping
import cv2
from PIL import Image
import pytesseract

img_rgb = cv2.cvtColor(img_arr, cv2.COLOR_BGR2RGB)
print(image_to_string(img_rgb))

2.2.2.3. FE for area#

Trong python chúng ta có một package khá phổ biến trong việc khai thác các thông tin địa lý đó là reverse_geocoder. Có 2 dạng bài toán chính với thông tin địa lý gồm

  • geocoding: mã hóa một tọa độ địa lý từ một địa chỉ.

  • revert geocoding: từ thông tin cung cấp về kinh độ và vĩ độ trả về địa chỉ của địa điểm và các thông tin có liên quan.

Cả hai bài toán đều có thể giải quyết thông qua API của google map hoặc OpenStreetMap. Sau đây là ví dụ trích xuất thông tin địa lý từ một địa điểm thông qua kinh độ và vĩ độ.

%%script echo skipping
# install package reverse_geocoder
!pip install reverse_geocoder
import reverse_geocoder as revgc

# truyền vào latitude, longitude
revgc.search((21.0364466, 105.8450788))

Như chúng ta thấy, từ tọa độ có thể biết được căn hộ này nằm ở quận Hoàn Kiếm, Hà Nội, là một nơi phát triển và có mức sống cao. Như vậy mức giá của nó khả năng sẽ cao hơn. Từ quận và huyện ta xác định được căn hộ có nằm ở trung tâm hay không, các tiện nghi xung quan nó. Những thông tin trên rất quan trọng trong việc đánh giá khả năng bán được của căn hộ. Mặc dù trong bộ dữ liệu gốc không hề xuất hiện nhưng chúng có thể được trích xuất từ tọa độ địa lý.

2.2.2.4. FE for datetime#

Trong dự báo, các dữ liệu thường có trạng thái thay đổi. Trạng thái của ngày hôm qua có thể khác biệt so với ngày hôm nay. Chẳng hạn như chiều cao, cân nặng của một người hay giá thị trường của các cổ phiếu. Chính vì thế thời gian là một thông tin có ảnh hưởng lớn tới biến mục tiêu. Từ một mốc thời gian biết trước chúng ta có thể phân rã thông tin thành giờ trong ngày, ngày trong tháng, tháng, quí, năm,…. Sẽ có rất nhiều điều thú vị được khám phá từ các thông tin này. Chẳng hạn như các qui luật của một số chuỗi số thay đổi theo mùa vụ: Nhiệt độ các tháng thay đổi theo mùa, GDP thay đổi theo qui luật quí, doanh số tiêu thụ kem thay đổi theo mùa,…. Yếu tố thời gian còn giúp xác định xu hướng biến đổi của một biến theo thời gian và kết hợp với tính mùa vụ sẽ trở thành một chỉ số quan trọng để ước lượng chuỗi thời gian.

Biến đổi one-hot coding là một phương pháp quan trọng được sử dụng để mã hóa các biến chu kì thời gian. One-hot coding sẽ biến đổi một biến thành các vector có phần tử là 0 hoặc 1, trong đó 1 đại diện cho sự xuất hiện của đặc trưng và 0 đại diện cho các đặc trưng mà biến không có.

Ví dụ: Chúng ta có 1 ngày trong tuần có thể rơi vào các thứ từ 2 đến chủ nhật. Như vậy một biểu diễn one-hot encoding của ngày thứ 2 sẽ là một véc tơ có phần tử đầu tiên bằng 1 và các phần tử còn lại bằng 0. Biểu diễn này cũng tương tự như với mã hóa dữ liệu văn bản thành các sparse vector.

Trong python chúng ta có thể sử dụng hàm weekday() để xác định thứ tự của một ngày trong tuần. Thuộc tính weekday() chỉ tồn tại đối với dữ liệu dạng datetime. Do đó ta cần chuyển đổi các biến ngày đang ở dạng string về dạng datetime thông qua strftime (string format time). Bảng string format time có thể xem tại đây.

from datetime import datetime
import pandas as pd

dataset = pd.DataFrame({'created': ['2021-08-13 00:00:00', '2021-08-12 00:00:00', '2021-08-11 00:00:00', 
                                    '2021-08-10 00:00:00', '2021-08-09 00:00:00', '2021-08-08 00:00:00', '2021-08-07 00:00:00']})

def parser(x):
    # Để biết được định dạng strftime của một chuỗi kí tự ta phải tra trong bàng string format time
    return datetime.strptime(x, '%Y-%m-%d %H:%M:%S')

dataset['created'] = dataset['created'].map(lambda x: parser(x))
print(dataset['created'].dtypes)

Như vậy biến created đã được chuyển về dạng datetime. Chúng ta có thể tạo ra một one-hot encoding dựa vào hàm weekday().

dataset['weekday'] = dataset['created'].apply(lambda x: x.date().weekday())
dataset['weekday']

Ta có thể tạo ra một biến trả về trạng thái ngày có phải là cuối tuần bằng kiểm tra weekday() có rơi vào [5, 6] là những ngày cuối tuần hay không.

dataset['is_weekend'] = dataset['created'].apply(lambda x: 1 if x.date().weekday() in [5, 6] else 0)
dataset['is_weekend']

Trong một số bài toán dữ liệu có thể bị phụ thuộc vào thời gian. Chẳng hạn như lịch trả nợ của thẻ tín dụng sẽ rơi vào kì sao kê là một ngày cụ thể trong tháng. Khi làm việc với dữ liệu chuỗi thời gian chúng ta nên lưu ý tới danh sách các ngày đặc biệt trong năm như nghỉ tết âm lịch, quốc khánh, quốc tế lao động,…. Bởi những ngày này thường sẽ có biến động lớn về dữ liệu kinh doanh.

# first, let's create a toy dataframe with some timestamps in different time zones
# Work with different timezones¶
df = pd.DataFrame()

df['time'] = pd.concat([
    pd.Series(
        pd.date_range(
            start='2014-08-01 09:00', freq='H', periods=3,
            tz='Europe/Berlin')),
    pd.Series(
        pd.date_range(
            start='2014-08-01 09:00', freq='H', periods=3, tz='US/Central'))
    ], axis=0)

df
time
0 2014-08-01 09:00:00+02:00
1 2014-08-01 10:00:00+02:00
2 2014-08-01 11:00:00+02:00
0 2014-08-01 09:00:00-05:00
1 2014-08-01 10:00:00-05:00
2 2014-08-01 11:00:00-05:00
# to work with different time zones, first we unify the timezone to the central one
# setting utc = True

df['time_utc'] = pd.to_datetime(df['time'], utc=True)

# next we change all timestamps to the desired timezone, eg Europe/London
# in this example

df['time_london'] = df['time_utc'].dt.tz_convert('Europe/London')


df
time time_utc time_london
0 2014-08-01 09:00:00+02:00 2014-08-01 07:00:00+00:00 2014-08-01 08:00:00+01:00
1 2014-08-01 10:00:00+02:00 2014-08-01 08:00:00+00:00 2014-08-01 09:00:00+01:00
2 2014-08-01 11:00:00+02:00 2014-08-01 09:00:00+00:00 2014-08-01 10:00:00+01:00
0 2014-08-01 09:00:00-05:00 2014-08-01 14:00:00+00:00 2014-08-01 15:00:00+01:00
1 2014-08-01 10:00:00-05:00 2014-08-01 15:00:00+00:00 2014-08-01 16:00:00+01:00
2 2014-08-01 11:00:00-05:00 2014-08-01 16:00:00+00:00 2014-08-01 17:00:00+01:00

2.2.2.5. FE for website, log#

Các hệ thống website lớn sẽ tracking lại các session của người dùng. Những thông tin được tracking bao gồm thông tin thiết bị, loại event, customer ID, … Từ customer ID chúng có thể link tới database người dùng để biết được các thông tin về giới tính, độ tuổi, tài khoản, hành vi giao dịch,…. Trong một số trường hợp một khách hàng có thể thay đổi thiết bị truy cập, do đó không phải hầu hết các trường hợp chúng ta đều map được session với Customer ID trên dữ liệu local. Tuy nhiên từ các thông tin được lưu trong Cookie về người dùng (còn gọi là user agent) cũng cung cấp cho chúng ta khá nhiều điều. Chẳng hạn như: Thiết bị truy cập, trình duyệt, hệ điều hành,… Từ thiết bị di động chúng ta cũng ước đoán được người dùng có mức thu nhập như thế nào: Sử dụng Iphone X thì khả năng cao là người có thu nhập cao, sử dụng điện thoại xiaomi khả năng là người thu nhập trung bình và thấp,…. Để phân loại các thông tin về người dùng chúng ta có thể sử dụng package user_agents trong python.

%%script echo skipping
!pip install user_agents
import user_agents
# Giả định có một user agent như bên dưới
ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/56.0.2924.76 Chrome/56.0.2924.76 Safari/537.36'
# Parser thông tin user agent
ua = user_agents.parse(ua)
# Khai thác các thuộc tính của user
print('Is a bot? ', ua.is_bot)
print('Is mobile? ', ua.is_mobile)
print('Is PC? ',ua.is_pc)
print('OS Family: ',ua.os.family)
print('OS Version: ',ua.os.version)
print('Browser Family: ',ua.browser.family)
print('Browser Version: ',ua.browser.version)

2.2.2.6. FE for mixed variables#

df = pd.DataFrame({'col1':[1,2,'a3','b',3,'c','ce'], 
                   'col2':[2,'12a','b3',3,'c','b3c','a']})
df
col1 col2
0 1 2
1 2 12a
2 a3 b3
3 b 3
4 3 c
5 c b3c
6 ce a

2.2.2.6.1. Value is contain numbers and strings#

2.2.2.6.2. Value is numbers or strings#

df['col1_num1'] = pd.to_numeric(df['col1'], errors='coerce', downcast='integer')
df['col1_num2'] = df['col1'].astype(str).str.extract('(\d+)')
df['col1_num3'] = df['col1'].str.extract('(\d+)')
df
col1 col2 col1_num1 col1_num2 col1_num3
0 1 2 1.0 1 NaN
1 2 12a 2.0 2 NaN
2 a3 b3 NaN 3 3
3 b 3 NaN NaN NaN
4 3 c 3.0 3 NaN
5 c b3c NaN NaN NaN
6 ce a NaN NaN NaN