Ứng dụng Machine Learning để phân loại spam SMS

Phân Tích Bài Toán

Quay trở lại với Series Machine Learning đã được triển khai bấy lâu nay mà mới được có 2 bài. Lần này thay vì trình bày 1 đống lí thuyết, và thực hành những bài toán không để làm gì. Thì mình sẽ đi luôn vào ứng dụng, rồi quay ngược trở lại giải thích về thuật toán, các bước thực hiện. Thẳng thắn mà nói thì bản thân mình không hề hợp với cách nhồi vào đầu 1 đống đại số tuyến tính, giải thuật, ... rồi ngồi code từng dòng ra được cái ứng dụng. Mà mình thích hợp với cách đi ngược lại hơn, từ ứng dụng rồi mới phân tích cách làm. Và ứng dụng lần này là "Phân loại spam sms dựa trên thuật toán Naive Bayes". Rất may cho các bạn và cho cả mình nữa là giải thuật này không quá phực tạp để hiểu mindset của nó.

Ứng Dụng

Ok, giờ mình sẽ nói trước về ứng dụng này. Gần gũi nhất với nó ta có thể kể đến như hệ thống tự động gắn nhán Spam của google mail. Bạn có bao giờ thắc mắc, tại sao các thư lại có thể được google đánh giá và tự động đưa vào mục spam chính xác đến vậy, hay, cùng một người gửi, có lúc thì nó nhận là spam, có lúc không (mình đã trải qua việc này rồi). Ứng dụng ngày hôm nay của mình cũng tương tự thế. Ta có dataset gồm khoảng 5000 tin nhắn, đã được gắn nhãn là "spam" hoặc "ham" (mình chả hiểu "ham" là gì 😄 ). Ta sẽ đọc file để trích xuất ra dữ liệu:

import pandas as pd
import numpy as np

file_path = 'data/smsspamcollection/SMSSpamCollection.txt'
df = pd.read_csv(file_path, delimiter='\t', header=None, skipinitialspace=True)
print(df)
         0                                                  1
0      ham  Go until jurong point, crazy.. Available only ...
1      ham                      Ok lar... Joking wif u oni...
2     spam  Free entry in 2 a wkly comp to win FA Cup fina...
3      ham  U dun say so early hor... U c already then say...
4      ham  Nah I don't think he goes to usf, he lives aro...
...    ...                                                ...
5568   ham               Will ü b going to esplanade fr home?
5569   ham  Pity, * was in mood for that. So...any other s...
5570   ham  The guy did some bitching but I acted like i'd...
5571   ham                         Rofl. Its true to its name

[5572 rows x 2 columns]

Ở đây có một vấn đề, các giải thuật Machine Learning chỉ làm việc được với số (thầy giáo nói thế), nên mình sẽ convert "ham", "spam" và cả các sms về định dạng số. Bắt đầu với "ham" (tương ứng với số 0) và "spam" (tương ứng với số 1)

mapping = {'ham': 0, 'spam': 1}
df[0].replace(mapping, inplace=True)
X_train, X_test, y_train, y_test = train_test_split(df.iloc[:,1], df.iloc[:,0], test_size=0.3, random_state=50)
print (X_train.head(10))
print (y_train.head(10))
2696    And whenever you and i see we can still hook u...
1659    RGENT! This is the 2nd attempt to contact U!U ...
4829    Lol no. Just trying to make your day a little ...
5319                         Kothi print out marandratha.
1394                                              Oh ok..
3251                             Babe, I need your advice
2988    I'm there and I can see you, but you can't see...
298     Hurt me... Tease me... Make me cry... But in t...
3525    Yeah that'd pretty much be the best case scenario
2867                        Smith waste da.i wanna gayle.
Name: 1, dtype: object
2696     ham
1659    spam
4829     ham
5319     ham
1394     ham
3251     ham
2988     ham
298      ham
3525     ham
2867     ham
Name: 0, dtype: object

Tiếp theo, ta sẽ transform sms messages thành dạng số, căng rồi đây vì mình không rành lắm xử lý text. Lên mạng search 1 hồi thì ra được ông này: http://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction cũng trong scikit learn luôn. Đùa nói thật chứ nếu không có scikit learn thì chắc mình cũng chẳng dám học machine learning ☺️ . Module mà scikit learn cung cấp cho phép chuyển đổi định dạng text thành vector, đúng cái mình cần. Mình sẽ import CountVectorizer và transform text thành vector. Cách transform thế này: mình có một mảng các string corpus, mình sẽ transform mảng này sao mỗi string sẽ chuyển đổi thành 1 vector có độ dài d (số từ xuất hiện ít nhất 1 lần trong corpus), giá trị của thành phần thứ i trong vector chính là số lần từ đó xuất hiện trong string.

from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
corpus = [
    'This is the first document.',
    'This is the second second document.',
    'And the third one.',
    'Is this the first document?',
]
X = vectorizer.fit_transform(corpus)
print(vectorizer.get_feature_names())
print(X.toarray())

Hãy nhìn vào result đầu tiên, vectorizer.get_feature_names() đã trả lại kết quả. Đó chính là các từ xuất hiện ít nhất 1 lần trong tất cả các string của corpus. Còn phần tử thứ 2 là mảng corpus sau khi transform. Nhìn vào mảng này và so sánh với với result thu được từ vectorizer.get_feature_names(), ta sẽ biết được là string đầu tiên ('This is the first document.') có một từ 'document, 'first', 'is', 'the', 'this'.

(
    ['and', 'document', 'first', 'is', 'one',
    'second', 'the', 'third', 'this']
)

array([[0, 1, 1, 1, 0, 0, 1, 0, 1],
       [0, 1, 0, 1, 0, 2, 1, 0, 1],
       [1, 0, 0, 0, 1, 0, 1, 1, 0],
       [0, 1, 1, 1, 0, 0, 1, 0, 1]]...)

Sử dụng phương pháp này với các sms messages, tôi thu được như sau:

vectorizer = CountVectorizer()
transformed_x_train = vectorizer.fit_transform(X_train.values).toarray()
transformed_x_test = vectorizer.fit_transform(X_test.values).toarray()
print(vectorizer.get_feature_names())
print(transformed_x_train)
['00', '000', '000pes', '008704050406', ......  'zaher', 'zed', 'zeros', 'zhong', 'zoe', 'zogtorius', 'zouk', 'èn', 'ú1']

[[0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]
 ..., 
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]]

Ok, ngon rồi. Đơn giản hơn tôi nghĩ. Giờ chỉ việc import Naive Bayes, fit rồi predict là xong.

from sklearn.naive_bayes import MultinomialNB
best_clf = MultinomialNB()
best_clf.fit(transformed_x_train, y_train)
y_pred = best_clf.predict(transformed_x_test)
Traceback (most recent call last):
  File "/var/www/html/python/spam_sms_classifier.py", line 61, in <module>
    y_pred = best_clf.predict(transformed_x_test)
  File "/home/james/virt3/lib/python3.5/site-packages/sklearn/naive_bayes.py", line 66, in predict
    jll = self._joint_log_likelihood(X)
  File "/home/james/virt3/lib/python3.5/site-packages/sklearn/naive_bayes.py", line 725, in _joint_log_likelihood
    return (safe_sparse_dot(X, self.feature_log_prob_.T) +
  File "/home/james/virt3/lib/python3.5/site-packages/sklearn/utils/extmath.py", line 140, in safe_sparse_dot
    return np.dot(a, b)
ValueError: shapes (1672,4556) and (7173,2) not aligned: 4556 (dim 1) != 7173 (dim 0)

Vâng, result đỏ lòm, có 1 vấn đề ở đây, các bạn chú ý 2 con số: 4556 và 7173. Đây chính là d ở tập train và ở tập test (hay chính là tập các từ mà ta thu được từ dữ liệu trainning và dữ liệu test), nó khác nhau. Cũng đúng thôi, tập train ta sẽ thu được một tập các từ khác, còn tập test ta cũng sẽ thu được một tập các từ khác. Nhưng Thuật toán MultinomialNB mà ta import từ scikit learn lại thấy ko đúng và không chấp nhận chạy. Thế là lại một hồi mày mò, tôi nhận ra mình có thể export cái tập thư viện từ tập train và ném nó vào tập test. Code giờ sẽ như sau:

vectorizer = CountVectorizer()
transformed_x_train = vectorizer.fit_transform(X_train.values).toarray()
trainVocab = vectorizer.vocabulary_ // export tập từ vựng

vectorizer = CountVectorizer(vocabulary=trainVocab)
transformed_x_test = vectorizer.fit_transform(X_test.values).toarray()

Sau đó ta sẽ train và predict, kết quả khá ok:

best_clf = MultinomialNB()
best_clf.fit(transformed_x_train, y_train)
y_pred = best_clf.predict(transformed_x_test)

print(np.asarray(y_test))
print(np.asarray(y_pred))
print('Training size = %d, accuracy = %.2f%%' % \
      (X_train.shape[0],accuracy_score(y_test, y_pred)*100))
[1 0 0 ..., 0 0 1]
[1 0 0 ..., 0 0 1]
Training size = 3900, accuracy = 98.62%

Ở đây có một vài cách để optimize, thứ nhất là chuẩn hóa dữ liệu đầu vào. Mình đọc được là ta nên lọc các stop words, tiền hành lemmatization, hoặc lọc non-words như số, kí tự xuống dòng, ... Nhưng sau khi optimize thì mình lại thấy accuracy ... giảm 😄 Ở đây mình dùng word_tokenize để tiến hành lemmatization, và stopwords để remove các stop words.

from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import string
from nltk.stem import PorterStemmer
from nltk import word_tokenize

wordnet_lemmatizer = WordNetLemmatizer()
def stemming_tokenizer(text):
    stemmer = PorterStemmer()
    return [stemmer.stem(w) for w in word_tokenize(text)]

vectorizer = CountVectorizer(
    tokenizer=stemming_tokenizer,
    stop_words=stopwords.words('english') + list(string.punctuation)
)
Training size = 3900, accuracy = 98.33%

Xuống còn 98.33%. Sau đây mình sẽ remove cái này đi và apply grid search với MultinomialNB xem thế nào:

params = {'alpha': [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4]}
clf = MultinomialNB()
clf = GridSearchCV(clf, params, cv=5)
clf.fit(transformed_x_train, y_train)

print(clf.best_params_)
best_clf = clf.best_estimator_
y_pred = best_clf.predict(transformed_x_test)
print('Training size = %d, accuracy = %.2f%%' % \
      (X_train.shape[0],accuracy_score(y_test, y_pred)*100))
{'alpha': 1.1}
Training size = 3900, accuracy = 98.68%

Lên được 98.68%. Param mà mình apply Grid Search ở đây là alpha, người ta thêm nó vào cải thiện độ chính xác.

Cơ Sở Lý Thuyết

Thuật toán mình dùng trong bài này là Naive Bayes. Các bạn có thể đọc thêm về cơ sở lý thuyết tại đây: machinelearningcoban.com Mình sẽ chỉ giải thích một cách đơn giản nhất, đủ để bạn hiểu những gì mình đã làm ở trên. Bài toán đặt ra ở đây là xác định xác suất để một điểm dữ liệu x bất kì rơi vào các class 1, 2, 3, ... C. Hay chính xác là đi tính $ p(y = c|x) $ Nói riêng về ứng dụng và bài toán mình đề cập ở trên. Mục tiêu ở đây, ví dụ với một string là "Tối nay có ăn tối không em". Chúng ta sẽ tách string này thành từng từ riêng biệt: "Tối", "nay", "có", ăn", "tối", "không", "em". Làm trương tự vậy với tất cả string. Và ta sẽ tính xác xuất trên tất cả các string với mỗi từ. Cứ hiểu đơn giản ở đây, là từ nào xuất hiện càng nhiều trong các tin nhắn được gán nhãn "ham" thì khi một tin nhắn chưa được gán nhãn chứa từ đấy. Xác suất nó là tin nhắn "ham" càng cao. Tương tự vậy với các từ trong sms "spam".

Kết Luận

Như mình có đề cập ở trên, mình chỉ hiểu căn bản về giải thuật, đủ để làm được cái ứng dụng Spam SMS Classifier chứ không hiểu chi tiết để giải thích tường minh cho các bạn. Nên phần Cơ Sở Lý Thuyết sẽ hơi ngắn, các bạn có thể tham khảo thêm ở phần link mình đã đính kèm. Cám ơn vì đã đọc tới phần này của bài viết. Hẹn gặp lại.


All Rights Reserved