Basic Data Science : การประยุกต์ PCA มาใช้กับงานด้าน Neural Network training

ข้อเขียนก่อนหน้านี้ ได้พยายามอธิบายความหมายและการสร้าง Principal Component Analysis (PCA) ด้วยอาศัย python package sklearn มาแล้ว ในตอนนี้จะได้แสดงให้เห็นตัวอย่างการใช้ประโยชน์ของ PCA กับงานด้าน neural network training กัน

รู้จักกับ CIFA-10 [1]
 CIFA-10 คือ dataset ที่มีข้อมูลภาพขนาด 32 x 32 pixels จำนวน 60,000 ภาพ แบ่งเป็น training set 50,000 ภาพ และ testing  set 10,000 ภาพ แบ่ง  class ของภาพออกเป็น 10 class  คือ

airplane
automobile
bird
cat
deer
dog
frog
horse
ship
truck

ก่อนจะ download มาใช้งานควรอ่านคำอธิบายให้เข้าใจก่อน เพราะข้อมูลถูกจัดแยกไว้ให้เหมาะสมกับระบบที่จะนำไปใช้งาน

รู้จักโครงสร้างข้อมูลและการจัดการ
หลังจากได้ข้อมูลมาแล้ว ทำการแตกไฟล์ออก จะพบไฟล์ที่มีชื่อ data_batch_1, data_batch_2, data_batch_3, data_batch_4, data_batch_5 เป็นไฟล์เก็บข้อมูลภาพในรูปแบบของ pickle [3] ยังเอามาใช้งานไม่ได้ทันทีต้องจัดการแปลงให้อยู่ในรูปแบบที่เหมาะสมก่อน โดยการสร้าง function ขึ้นมา

import pickle

def unpickle(file):
 with open(file, 'rb') as fo:
  dict = pickle.load(fo, encoding='bytes')
 return dict


unpickle รับชื่อไฟล์เข้าไป แล้วจะทำการอ่านแล้วแปลงข้อมูลให้อยู่ในโครงสร้างของ python dictionary [2]

n = 10000
dict_1 = unpickle('data_batch_1')
images = dict_1[b'data'][:n,:]
labels = dict_1[b'labels'][:n]
print(images)
print(lables)


รูปที่ 1 

รูปที่ 2 แสดงข้อมูลบางส่วนของ labels ตัวเลขแต่ละตัวแสดง class number ของ image

จากภาพที่ 1 แสดงให้เห็นโครงสร้างข้อมูลของ image ที่ได้จากการนำไฟล์ data_batch_1 ซึ่งเป็นข้อมูลภาพจำนวน 10,000 ภาพแรกมาประมวลผล มีลักษณะเป็น matrix ขนาด \( 10000 \times 3072 \)

ในแต่ละ row ของ matrix แทนข้อมูลของ 1 ภาพ แบ่งออกเป็นสามชุดคือ
ชุดที่ 1 ลำดับที่ 0 - 1023 แทนค่าของ channel สีแดง (red channel)
ชุดที่ 2 ลำดับที่ 1024 - 2047 แทนค่าของ channel สีเขียว (green channel)
ชุดที่ 3 ลำดับที่ 2048 - 3071 แทนค่าของ channel สีน้ำเงิน (blue channel)
และภายในข้อมูลแต่ละชุดถูกแบ่งออกเป็นชุดย่อยอีก \( 32 \times 32 \) ชุด

รูปที่ 3

ในกรณีที่ต้องการแสดงภาพจึงต้องสร้าง function ขึ้นมาช่วย

import matplotlib.pyplot as plt

def creat_image(data):
        imgs = []
        for d in data :
             r = d[:1024]
             g = d[1024:2048]
             b = d[2048:]
 
             r = np.reshape(r,(32,32))
             g = np.reshape(g,(32,32))
             b = np.reshape(b,(32,32))
             img = np.zeros((32,32,3),dtype='uint8')
             img[:,:,0] = r
             img[:,:,1] = g
             img[:,:,2] = b
             imgs.append(img)

        return imgs

# display 10 images
images = np.array(creat_image(dict_1[b'data'][:10,:]))

fig = plt.figure() 
for i in range(10):
        sub = fig.add_subplot(1,10, i + 1)
        sub.imshow(images[i])
plt.show()


ใช้ PCA เพื่อลดจำนวน dimension 
ข้อมูลภาพ 1 ภาพ มีโครงสร้างเป็น array 3 มิติ คือ width, height, color channels  ซึ่งก็สามารถพิจารณาเป็นข้อมูลที่มีโครงสร้างแบบ vector 1 มิติ ได้ ดังภาพ



 matrix ข้อมูลที่ได้มีจำนวน  dimension มากถึง 3072 dimensions ลองมาใช้ PCA เพื่อลดจำนวน dimension กัน โดยใช้ sklearn package [4]


from sklearn.decomposition import PCA

std_mat = 
pca = PCA(0.90) # retain 90% of information
pca.fit(std_mat)
print(pca.n_components_)

>>>> 99

ในตอนนี้ได้มีการกำหนดค่า parameter ให้กับ PCA เป็น 0.90 เหตุผลคือ ถ้าเรากำหนดค่า parameter น้อยกว่า 1 ให้กับ PCA จะถูกตีความเป็นการกำหนดความต้องการที่เก็บรักษา information ไว้กี่เปอร์เซ็นต์ ในที่นี้คือ 0.90 หรือ 90 %
เมื่อใช้คำสั่ง print(pca.n_components_) หลังการ fitting กับ  dataset แล้ว จะได้ค่าจำนวน principal components ออกมาคือ 99 components ซึ่งตีความได้ว่า
PCA ช่วยลดจำนวน dimension ของ  dataset จาก 3072 เหลือ 99 โดยที่ยังคงรักษาสาระในข้อมูลไว้ 90 % ซึ่งคิดว่าน่าจะเพียงพอในการนำไปใช้ทำ classification แล้ว

เพื่อให้ง่ายต่อการนำไป plot จะนำเอาข้อมูลมาบางส่วน 2000 รายการ แล้วใช้ PCA ช่วยลดให้เหลือเพียง 2 dimension แล้วนำมาดูการกระจายตัวของข้อมูล

import matplotlib.pyplot as plt
import pandas as pd

n = 2000
data_mat = np.array(dict_1[b'data'][:n,:])
std_mat = StandardScaler().fit_transform(data_mat)
labels = dict_1[b'labels'][:n]

pca = PCA(2) # need only 2 dimension
pca.fit(std_mat)
score = pca.transform(std_mat)
score = score.astype('float')

# create pandas dataframe to ease of manipulate
score_frame = pd.DataFrame(data=score,columns=['Principal 1','Principal 2'])
score_frame['label']= labels

# do scatter plot
plt.figure(figsize=(10,10))
plt.xticks(fontsize=8)
plt.yticks(fontsize=8)
plt.xlabel('Principal Component 1',fontsize=20)
plt.ylabel('Principal Component 1',fontsize=20)

classes = [0,1,2,3,4,5,6,7,8,9]
NAMES = ['airplane','automobile','bird','cat','deer',
         'dog','frog','horse','ship','truck']
colors = ['b','r', 'g','c','m','y','b','r', 'g','c']
for cl, color in zip(classes,colors):
    indicesToKeep = score_frame['label'] == cl
    data1 = score_frame.loc[indicesToKeep, 'Principal 1']
    data2 = score_frame.loc[indicesToKeep, 'Principal 2']
    plt.scatter(data1,
                data2, c = color, s = 20)

plt.legend(NAMES,prop={'size': 10})
plt.grid()
plt.show()

เมื่อสังเกตุด้วยตา จะพบว่าไม่ปรากฏรูปแบบที่ชัดเจนของความสัมพันธ์ระว่าง principal ทั้งสอง เมื่อเป็นแบบนี้เราจึงต้องใช้วิธีการสร้าง Neural Network ขึ้นมาเพื่อทำการแยกภาพเหล่านี้ออกจากกัน

ทดลองกับ Neural Network
1. แบ่งข้อมูลเป็นสองชุด
เพื่อให้เห็นความต่าง (หรือความเหมือน) ผมจะใช้ข้อมูล 2 ชุด นำมาใช้ train ตัวแบบเดียวกัน แล้วจะเปรียบเทียบผลของความถูกต้องในการทำนาย
  • ชุดแรกคือ  PCA ที่หาได้จาก dataset และกำหนดจำนวน principal components เป็น 100 (เราทราบว่า จำนวน component ที่ 99 สามารถรักษาสารข้อมูลได้ถึง 90 % เลยใช้ 100 component เพื่อให้ง่าย) 
  • ชุดที่สองคือข้อมูลจาก dataset โดยตรง 
2. เตรียมข้อมูล
2.1  One-hot-vector สิ่งที่เราต้องการในการทำ classification คือการเปลี่ยนข้อมูล label ให้อยู่ในรูป one-hot vector [6] สำหรับ label แต่ละค่า เช่น label ที่แทนรูปของ bird คือ "2" เมื่อทำให้เป็น one-hot-vector คือ \( \begin{bmatrix} 0&0 &1 & 0& 0& 0& 0& 0& 0& 0 \end{bmatrix}\) โดยเขียน code ดังนี้

import time
import numpy as np
import pickle
from sklearn.decomposition import PCA

def create_onehot_vect(data,max_len=10):
          n = data.shape[0]
          x = np.zeros((n,max_len))
          for i in range(n) :
                v = data[i]
                x[i,v-1] = 1.

          return x


2.2 Merge data

# turn pickle  to bytes array
dict_1 = unpickle('cifar-10-batches-py/data_batch_1')
dict_2 = unpickle('cifar-10-batches-py/data_batch_2')
dict_3 = unpickle('cifar-10-batches-py/data_batch_3')
dict_4 = unpickle('cifar-10-batches-py/data_batch_4')
dict_5 = unpickle('cifar-10-batches-py/data_batch_5')

# turn to numpy array
data_1 = np.array(dict_1[b'data'][:,:])
data_2 = np.array(dict_2[b'data'][:,:])
data_3 = np.array(dict_3[b'data'][:,:])
data_4 = np.array(dict_4[b'data'][:,:])
data_5 = np.array(dict_5[b'data'][:,:])

label_1 = np.array(dict_1[b'labels'][:])
label_2 = np.array(dict_2[b'labels'][:])
label_3 = np.array(dict_3[b'labels'][:])
label_4 = np.array(dict_4[b'labels'][:])
label_5 = np.array(dict_5[b'labels'][:])

# merge all together
data_mat = np.vstack((data_1,data_2,data_3,data_4,data_5))
labels = np.hstack((label_1,label_2,label_3,label_4,label_5))

# standardize data -- good habit
# std_data_mat will contains data value ranging from 0.0 - 1.0
std_data_mat = data_mat / 255 

# one-hot vector
one_hot_label = create_onehot_vect(labels)

print(std_data_mat.shape)
print(one_hot_label.shape)

>>>> (50000,3072)
>>>> (50000,10)

2.3 สร้าง PCA

pca = PCA() 
pca.n_components = 100
pca.fit(std_data_mat)
score = pca.transform(std_data_mat)
print(score.shape)

>>>> (50000,100)

ตัวแปร score เป็นตัวแปรที่เก็บ PCA ที่มี component 100 component เป็นตัวแทนของ dataset เดิม


3.สร้าง และ train Neural Network Model 
ในตัวอย่างนี้จะใช้ machine learning framework ชื่อ  Keras [5] (ท่านต้องทำการติดตั้งก่อน โดยศึกษาจากขั้นตอนที่ระบุบนเว็บไซต์ของ Keras)

เนื่องจากในข้อเขียนนี้ต้องการชี้ให้เห็นถึงประโยชน์ของการนำ PCA มาช่วยในการทำงานการสร้างระบบ machine learning ไม่ได้มุ่งเน้นไปเพื่อให้ได้ผลการทดลองที่ดี ดังนั้น ผมจะสร้างตัวแบบแบบง่าย ๆ ขึ้นมาดังนี้




import tensorflow as tf
from tensorflow import keras
#my machine has 1 gpu and 4 cores of cpu
config = tf.ConfigProto( device_count = {'GPU': 1 , 'CPU': 4} )  
sess = tf.Session(config=config) 
keras.backend.set_session(sess)

from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import RMSprop


เราต้องเปลี่ยนค่า input_shape ของตัวแบบให้เท่ากับจำนวน dimension ข้อมูลที่นำเข้า ในที่นี้คือ 100

3.1 Train PCA


#create network
_n_class = 10
net_pca  = Sequential()
net_pca.add(Dense(200, activation='relu', input_shape=(100,)))
net_pca.add(Dense(100, activation='relu'))
net_pca.add(Dense(50, activation='relu'))
net_pca.add(Dense(_n_class, activation='softmax'))
#print(net_pca.summary())
net_pca.compile(loss='categorical_crossentropy',
   optimizer=RMSprop(),
   metrics=['accuracy'])

epochs =  50
batch_size = 100

net_pac.fit(score,
 labels,
 batch_size=batch_size,
 epochs=epochs,
 verbose=1)

เวลารวมที่ใช้ในการทำงานทั้งหมดประมาณ 13 นาทีกับ 20 วินาที ทั้งนี้เวลานี้รวมเวลาในการสร้าง PCA ด้วยนะ

3.2 Train ข้อมูลจริง

net  = Sequential()
net.add(Dense(200, activation='relu', input_shape=(3072,)))
net.add(Dense(100, activation='relu'))
net.add(Dense(50, activation='relu'))
net.add(Dense(_n_class, activation='softmax'))
#print(net.summary())
net.compile(loss='categorical_crossentropy',
   optimizer=RMSprop(),
   metrics=['accuracy'])
net.fit(std_data_mat,
 labels,
 batch_size=batch_size,
 epochs=epochs,
 verbose=1)

ในการ train ข้อมูลเดิม จำเป็นต้องเปลี่ยนค่า input_shape ของ ตัวแบบให้เป็น 3072 ซึ่งเป็นค่าเดียวกับ dimension ของ dataset


ในการ train ด้วยตัวข้อมูลจริงใช้เวลามากกว่า PCA นิดหน่อย คือประมาณ 14 นาที 24 วินาที ซึ่งก็ไม่ใช่เรื่องน่าแปลกอะไรเพราะข้อมูลมีขนาดใหญ่กว่า

4. ทดสอบผล
4.1 ใช้ตัวแบบที่ train ด้วย PCA

dict = unpickle('cifar-10-batches-py/test_batch')
data_mat = np.array(dict[b'data'][:,:])
test_data = data_mat / 255
labels = np.array(dict[b'labels'][:])

test_score = pca.transform(test_data)

y_hat_pca = net_pca.predict(test_score, verbose=1)

y_hat_class = np.where(y_hat_pca < 0.5,0, 1)
pred = np.argmax(y_hat_class_pca,axis=1)
accuracy = np.sum(labels==pred)/len(labels)
print("Accuracy = {}% ".format(accuracy*100)) 



ได้ผลความถูกต้องที่ประมาณ 47 %

4.2 ใช้ตัวแบบที่ train ด้วย data จริง

y_hat = net.predict(test_data, verbose=1)
y_hat_class = np.where(y_hat < 0.5,0, 1)
pred = np.argmax(y_hat_class,axis=1)
accuracy = np.sum(labels==pred)/len(labels)
print("Accuracy = {} % ".format(accuracy*100)) 


 ได้ผลความถูกต้องออกมาประมาณ 39 %

ข้อสรุป
จากการทดลองครั้งนี้จะเห็นว่า การลดจำนวน dimension ลง นอกจากจะช่วยการใช้ทรัพยากร และอาจเพิ่มความแม่นยำให้กับตัวแบบแล้วยังช่วยลดการเกิดอาการ over-fit ของตัวแบบกับข้อมูล 


เฮฮา หลังอ่าน


https://imgs.xkcd.com/comics/felsius.png


--------------------------------
เอกสารอ้างอิง
[1] https://www.cs.toronto.edu/~kriz/cifar.html
[2] https://docs.python.org/3/tutorial/datastructures.html#dictionaries
[3] https://docs.python.org/3/library/pickle.html
[4] https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html
[5] https://keras.io/
[6] https://en.wikipedia.org/wiki/One-hot

ความคิดเห็น