Ở bài tìm hiểu về SVM lần trước, ta đã tìm hiểu những khái niệm cơ bản về thuật toán SVM trong bài toán phân nhóm dữ liệu cũng như sử dụng thư viện Scikit-learn để giải bài toán đơn giản phân nhóm dữ liệu thành 2 lớp trong không gian 2 chiều.

Trong bài này, ta sẽ áp dụng SVM để giải một bài toán mang tính thực tế hơn: phân nhóm chữ số viết tay.

Mục lục

1. Bộ cơ sở dữ liệu MNIST

MNIST Database (Modified National Institute of Standards and Technology Database) là bộ cơ sở dữ liệu về chữ số viết tay, được cải biên từ bộ cơ sở dữ liệu gốc của NIST giúp dễ sử dụng hơn. MNIST Database vô cùng đồ sộ gồm 60k data training và 10k data test, được sử dụng phổ biến trong các thuật toán nhận dạng hình ảnh.

Mỗi data trong bộ dữ liệu này là một ảnh đen trắng, kích thước 28x28 (784 pixels), mỗi pixel có giá trị trong khoảng [0…255]. 0 tương ứng với màu trắng, 255 tương ứng với màu đen.

Có thể tải bộ dữ liệu này tại đây. Tải về và giải nén ta được 4 files như bên dưới:

train-images-idx3-ubyte: training set images 
train-labels-idx1-ubyte: training set labels 
t10k-images-idx3-ubyte:  test set images 
t10k-labels-idx1-ubyte:  test set labels

Chú ý 4 files này chứa dữ liệu các pixel ảnh ở dạng binary data (Cấu trúc file được mô tả chi tiết ở cùng trang trên, phần dưới).

2. Thao tác với bộ cơ sở dữ liệu

Để dễ hình dung bộ dữ liệu vừa tải, ta sẽ viết 1 đoạn code python để đọc data từ file trên và convert ảnh sang định png.

a. Đọc dữ liệu binary

Ta viết 2 hàm get_images()get_labels() để convert data từ file binary sang dạng mảng như sau:

preview_mnist.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_images(img_file, number):
    f = open(img_file, "rb") # Open file in binary mode
    f.read(16) # Skip 16 bytes header
    images = []

    for i in range(number):
        image = []
        for j in range(28*28):
            image.append(ord(f.read(1)))
        images.append(image)
    return images

def get_labels(label_file, number):
    l = open(label_file, "rb") # Open file in binary mode
    l.read(8) # Skip 8 bytes header
    labels = []
    for i in range(number):
        labels.append(ord(l.read(1)))
    return labels

number trong 2 hàm get_images(img_file, number)get_labels(label_file, number) là số data mà ta muốn lấy từ file binary.

b. Convert sang định dạng png

Từ 2 mảng imageslabels thu được ở trên, ta viết hàm convert data sang file ảnh định dạng png như sau:

preview_mnist.py
1
2
3
4
5
6
7
8
9
10
11
import os
import numpy as np
from skimage import io

def convert_png(images, labels, directory):
    if not os.path.exists(directory):
        os.mkdir(directory)

    for i in range(len(images)):
        out = os.path.join(directory, "%06d-num%d.png"%(i,labels[i]))
        io.imsave(out, np.array(images[i]).reshape(28,28))

Ở đây ta sử dụng thư viện scikit-image để tạo ảnh png. Nếu chưa có thư viện này trong máy, ta cần cài đặt bằng pip (dùng pip3 nếu muốn cài cho Python 3)

pip3 install scikit-image

Ta chạy các hàm vừa viết ở trên với file data train, in ra 100 ảnh như bên dưới:

preview_mnist.py
1
2
3
4
5
number = 100
train_images = get_images("mnist/train-images-idx3-ubyte", number)
train_labels = get_labels("mnist/train-labels-idx1-ubyte", number)

convert_png(train_images, train_labels, "preview")

Chú ý 4 file binary tải về ta để trong cùng thư mục mnist.
Ảnh png được in ra sẽ nằm trong thư mục preview, như hình dưới:

c. Convert sang định dạng csv

Để dễ dàng làm việc ta cũng có thể convert binary file sang định dạng csv như bên dưới:

preview_mnist.py
1
2
3
4
5
def output_csv(images, labels, out_file):
    o = open(out_file, "w")
    for i in range(len(images)):
        o.write(",".join(str(x) for x in [labels[i]] + images[i]) + "\n")
    o.close()

d. Load data bằng thư viện có sẵn

Nếu lười viết hàm load data, ta có thể sử dụng thư viện có sẵn do một cao nhân nào đó viết ra. Ở đây mình tìm được thư viện python-mnist.
Sau khi cài đặt, ta chỉ cần code thêm vài dòng đơn giản như sau là có thể load được data cần dùng. Nếu lười thì có thể ăn sẵn bằng cách này.

1
2
3
4
from mnist import MNIST
mndata = MNIST('./mnist')
train_images, train_labels = mndata.load_training()
test_images, test_labels = mndata.load_testing()

3. Train và test với SVM

a. Sử dụng thư viện SVC

Ở đây ta sẽ sử dụng thư viện SVM có sẵn của scikit-learn là SVC.

Chú ý nếu chưa cài scikit-learn trong máy, ta có thể cài đặt đơn giản bằng pip (thay bằng pip3 nếu muốn cài cho Python 3).

pip install scikit-learn

b. Feature scaling

Trước khi đưa dữ liệu vào train, ta cần thực hiện chuẩn hoá dữ liệu (Feature scaling).
Dữ liệu có thể đến từ nhiều nguồn, với đơn vị, các thành phần có giá trị chênh lệch lớn. Để tính toán ta cần đưa tất cả dữ liệu về một chuẩn chung, các thành phần có giá trị nằm cùng trong 1 khoảng như $[0, 1]$ hoặc $[-1, 1]$.

Để đưa giá trị về khoảng $[0, 1]$, ta sử dụng phương pháp rescaling bằng công thức sau:
$$ x’ = \frac{x - \min(x)}{\max(x) - \min(x)} $$

Áp dụng với data của ta, mỗi pixel có giá trị trong khoảng 0..255, vậy ta có $min(x) = 0$, $max(x) = 255$, từ đó ta có công thức chuẩn hoá dữ liệu $$ x’ = \frac{x}{255} $$

Ta thực hiện chuẩn hoá dữ liệu mảng train_images dễ dàng bằng thư viện numpy như sau:

1
2
import numpy
train_images = numpy.array(train_images)/255

c. Train

Ta đưa dữ liệu training vào để thực hiện train như bên dưới.

svm_mnist.py
1
2
3
4
5
6
7
8
9
10
11
import numpy as np
from sklearn import svm, metrics

print("TRAIN")
TRAINING_SIZE = 10000
train_images = get_images("mnist/train-images-idx3-ubyte", TRAINING_SIZE)
train_images = np.array(train_images)/255
train_labels = get_labels("mnist/train-labels-idx1-ubyte", TRAINING_SIZE)

clf = svm.SVC()
clf.fit(train_images, train_labels)

Ở đoạn code trên, vì mục đích chạy thử, ta chỉ load TRAINING_SIZE là 10k dữ liệu, giúp rút ngắn thời gian train so với việc load toàn bộ 60k data, tuy nhiên có thể khiến cho model sau khi train có độ chính xác không cao (điều này ta sẽ test sau).

d. Test

Ta load dữ liệu từ file test data và test với model vừa train được.

svm_mnist.py
1
2
3
4
5
6
7
8
9
10
11
12
13
TEST_SIZE = 500
test_images = get_images("mnist/t10k-images-idx3-ubyte", TEST_SIZE)
test_images = np.array(test_images)/255
test_labels = get_labels("mnist/t10k-labels-idx1-ubyte", TEST_SIZE)

print("PREDICT")
predict = clf.predict(test_images)

print("RESULT")
ac_score = metrics.accuracy_score(test_labels, predict)
cl_report = metrics.classification_report(test_labels, predict)
print("Score = ", ac_score)
print(cl_report)

Ở đoạn code trên, ta chỉ load TEST_SIZE là 500 dữ liệu với mục đích chạy thử. Ta thu được kết quả như bên dưới:

Score =  0.916
              precision    recall  f1-score   support

           0       0.89      0.95      0.92        42
           1       0.97      1.00      0.99        67
           2       0.96      0.89      0.92        55
           3       0.93      0.84      0.88        45
           4       0.90      0.96      0.93        55
           5       0.84      0.92      0.88        50
           6       0.97      0.86      0.91        43
           7       0.87      0.92      0.89        49
           8       0.90      0.88      0.89        40
           9       0.94      0.89      0.91        54

   micro avg       0.92      0.92      0.92       500
   macro avg       0.92      0.91      0.91       500
weighted avg       0.92      0.92      0.92       500

Model thu được có độ chính xác 0.916, không tồi nhưng vẫn chưa được tốt như mong đợi.
Ta có thể tìm cách đẩy độ chính xác lên cao hơn bằng cách điều chỉnh các parameters của SVC cho phù hợp.

e. Điều chỉnh parameters

Tương tự như ở bài trước, ta sử dụng GridSearchCV để tìm tham số tối ưu tham số cho SVC.
Ta sẽ tìm C tốt nhất trong tập [0.001, 0.01, 0.1, 1, 5, 10, 100, 1000] bằng đoạn code sau:

from sklearn.model_selection import GridSearchCV

parameter_candidates = [
  {'C': [0.001, 0.01, 0.1, 1, 5, 10, 100, 1000]},
]

clf = GridSearchCV(estimator=svm.SVC(), param_grid=parameter_candidates, n_jobs=-1)
clf.fit(train_images, train_labels)
print('Best score:', clf.best_score_)
print('Best C:',clf.best_estimator_.C)

Ta thu được kết quả:

Best score: 0.936
Best C: 100

Thay C=100 vào hàm khởi tạo SVC

clf = svm.SVC(C=100)

Tiến hành train và test lại với data ban đầu ta thu được kết quả khá tốt so với trước

Score =  0.95
              precision    recall  f1-score   support

           0       0.95      1.00      0.98        42
           1       0.99      1.00      0.99        67
           2       0.91      0.93      0.92        55
           3       0.91      0.93      0.92        45
           4       0.92      0.98      0.95        55
           5       0.98      0.94      0.96        50
           6       0.98      0.93      0.95        43
           7       0.92      0.96      0.94        49
           8       0.97      0.90      0.94        40
           9       0.98      0.91      0.94        54

   micro avg       0.95      0.95      0.95       500
   macro avg       0.95      0.95      0.95       500
weighted avg       0.95      0.95      0.95       500

Ngoài tham số C, ta còn có thể tối ưu kernel(default đang sử dụng rbf), gamma (default đang là auto) để thu được kết quả tốt hơn.
Ngoài ra còn có thể tăng khối lượng tập data train (hiện ta đang set TRAINING_SIZE là 10k)

f. Lưu model ra file

Sau khi training và thu được model với độ chính xác ưng ý, ta có lưu lại model ra file để sử dụng sau này.
Chú ý cần cài thư viện joblib bằng pip nếu trong máy không có sẵn.

from joblib import dump, load
dump(clf, 'mnist-svm.joblib') 

Hoặc load model có sẵn từ file:

clf = load('mnist-svm.joblib')