สนุกกับ Neural Network

ใช้ Keras น๊ะจ๊ะ

Sanparith Marukatat
8 min readJun 7, 2017

Multi-Layer Perceptron (MLP)

เป็นโครงสร้างมาตรฐานของ NN โดยทั่วไปแล้วก็มี

  • จำนวน input nodes = จำนวน features
  • จำนวน output nodes = 1 ในกรณี 2 class หรือ เท่ากับจำนวน class ที่มี
  • จำนวน hidden nodes เป็นตัวแปรที่ต้องเลือกเอาเอง

เริ่มจากข้อมูล MNIST ที่เป็นข้อมูลมาตรฐานก่อนเลย ข้อมูลนี้เป็นภาพ grayscale เลข 0–9 ลายมือเขียน ขนาด 28x28 พิกเซล มีจำนวน 60,000 ตัวอย่างสำหรับสอน และ 10,000 ตัวอย่างสำหรับทดสอบ

ใน Keras นั้นเราโหลดข้อมูล MNIST นี้ได้ง่ายๆ โดย สั่ง

from keras.datasets import mnist
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train.astype(‘float32’)
X_test = X_test.astype(‘float32’)
X_train /= 255
X_test /= 255
X_train = X_train.reshape(X_train.shape[0], 784)
X_test = X_test.reshape(X_test.shape[0], 784)

ใน code ข้างบนนี้เราทำ

  • โหลดข้อมูลเป็นชุดสอน (X_train, y_train) และชุดทดสอบ (X_test, y_test) ข้อมูลเหล่านี้อยู่ในรูป numpy.ndarray
  • แปลงชนิดข้อมูลเป็น type float32 และหารด้วย 255 ให้ค่าสีอยู่ในช่วง 0–1
  • แปลงข้อมูลจากภาพ 28x28 พิกเซล ให้เป็นเวคเตอร์ขนาด 784 มิติ โดยใช้คำสั่ง reshape อันนี้ผมไม่แน่ใจว่ามันเอา row หรือเอา column มาวางต่อกันกันแน่

สร้าง MLP โดย

  • ใช้ raw pixel เหล่านี้เป็น feature แปลว่าเราก็จะมี input nodes = 784 โหนด
  • มี output node=10 ตามจำนวน class
  • กำหนดให้จำนวน hidden node = 100
from keras.models import Sequential
from keras.layers import Dense
mlp = Sequential()
mlp.add( Dense(100, input_dim=784, activation=’tanh’) )
mlp.add( Dense(10, activation=’softmax’) )
mlp.compile(loss=’categorical_crossentropy’, optimizer=’adam’, metrics=[‘accuracy’])

Dense ในที่นี้หมายถึงโครงสร้างแบบ fully connected ระหว่างชั้นที่ติดกัน โดยเราเลือกใช้ activation function ชนิด tanh

เนื่องจากเป็นงาน classification ดังนั้นเราจะใช้ loss function คือ Cross Entropy ควรทราบว่า loss function อันนี้ต้องใช้คู่กับ softmax activation เพราะในการคำนวณ entropy นั้นเราต้องการตีความข้อมูลส่งออกจาก NN นี้ในเชิงความน่าจะเป็น ซึ่งหนึ่งในข้อแม้ก็คือผลรวมของค่าส่งออกทุกค่าต้องเท่ากับ 1 ซึ่งข้อแม้นี้นั้นเป็นจริงเมื่อใช้ softmax

ในตัวอย่างนี้เราจะนำข้อมูลเกรเดียนต์มาปรับโดยใช้อัลกอริทึมชื่อ Adam (https://arxiv.org/abs/1412.6980) ซึ่งเท่าที่ผมลองเล่นบน Keras นี้ก็ดูจะให้ผลดีกว่าอย่างอื่นเช่น SGD (Stochastic Gradient Descent มาตรฐาน)

ก่อนจะสั่ง train+test เราต้องทำการแปลงข้อมูล class ให้อยู่ในรูปของเวคเตอร์ 10 มิติเช่นเดียวกับ output ของ NN ข้างบนนี้ก่อน นั่นคือ

from keras.utils import np_utils
Y_train = np_utils.to_categorical(y_train, 10)
Y_test = np_utils.to_categorical(y_test, 10)

จากนั้นก็ สั่ง print model มาดูและตามด้วย train+test

print mlp.summary()
mlp.fit(X_train, Y_train, batch_size=64, epochs=5, verbose=1)
score = mlp.evaluate(X_test, Y_test, verbose=0)
print(‘Test score:’, score[0])
print(‘Test accuracy:’, score[1])

เราสั่ง train ทั้งหมด 5 รอบ โดยให้ขนาดของ mini-batch เป็น 64 ตัวอย่าง (นั่นคือเราจะสะสมค่าเกรเดียนต์บนตัวอย่าง 64 ตัวก่อนจะทำการปรับ 1 ครั้ง)

ผลที่ได้คือ

สังเกตว่าในขณะที่ train ค่า loss และ acc ก็เปลี่ยนไปเรื่อยๆ ผมเลยเข้าใจว่าเป็นค่าที่คำนวณบน mini-batch ส่วนค่า Test Score ข้างล่าง “น่าจะ” เป็น loss ที่คำนวณบนชุด test ทั้งหมดเลย

สังเกตต่อว่าถ้าเราลองคำนวณจำนวน parameter บนชั้นแรก ควรจะเป็น 784x100=78400 ซึ่งไม่ตรงกัน นั่นเพราะโดย default แล้ว Keras จะสร้างโหนดที่มี bias ด้วย ดังนั้นจำนวน parameter จึงเป็น 785x100=78500 นั่นเอง

เราสามารถลองปรับโครงสร้างของ MLP นี้ได้ โดยเพิ่มจำนวนชั้นซ่อนเร้นเข้าไปเรื่อยๆ ค่า accuracy ที่ได้คือ

  • Tanh: 100–10: 97.2%
  • Tanh: 100–100–10: 97.4%
  • Tanh: 100–100–100–10: 97.3%
  • Tanh: 100–100–100–100-10: 97.0%

จะเห็นว่ายิ่งเราเพิ่มจำนวนชั้นเข้าไป ผลที่ได้อาจไม่ได้ดีขึ้นตามที่เราต้องการ เราทำอะไรผิดหรือไม่? คำตอบคือไม่

ปัญหานี้มาจากอัลกอริทึม backprop นั้นทำการคำนวณเกรเดียนต์ของโหนดบนชั้นซ่อนเร้นนั้นทำตามสมการ

ค่าด้านซ้ายของสมการคือค่าเกรเดียนต์ของโหนด i บนชั้น l ซึ่งจะเห็นว่ามีค่าหน้าตาคล้ายๆ กันในผลรวมด้านขวา แต่เป็นของชั้น l+1 แทน (นี่คือที่มาของชื่อ backprop) ส่วนค่าอื่นด้านขวาอธิบายคร่าวๆ คือค่า derivative ของค่าส่งออกจากโหนดต่างๆ และค่าถ่วงน้ำหนักปัจจุบัน ค่าเกรเดียนต์ที่ได้นี้จะถูกนำไปปรับค่าถ่วงน้ำหนักนี้นั่นเอง

ตัวแปรในผลรวมด้านขวาของสมการนั้นมักจะมีค่าน้อยกว่า 1 ทำให้เมื่อเราคูณกันไปเรื่อยๆ ขนาด (amplitude) ของมันก็จะเล็กลงอีก และเมื่อเพิ่มจำนวนชั้นเข้าไปขนาดที่ได้ก็จะเล็กลงเรื่อยๆ ยิ่งเมื่อเรานำค่านี้ไปคูณกับ learning rate ค่าที่ได้ยิ่งเล็กลงไปอีก ผลก็คือค่าถ่วงน้ำหนักที่อยู่บนชั้นล่างๆ นั้นได้รับข้อมูลที่ไม่เพียงพอที่จะทำการปรับอย่างเหมาะสม เราเรียกปัญหานี้ว่า gradient vanishing

แล้วจะเกิดอะไรขึ้นเมื่อ gradient vanish? คำตอบคือค่าถ่วงน้ำหนักในชั้นล่างๆ ที่ถูกตั้งค่าตอนต้นจากการสุ่มจะถูกปรับน้อยมากจนอาจจะไม่ต่างจากค่าตั้งต้นเลย
ควรทราบว่าถึงแม้ค่าถ่วงน้ำหนักเหล่านี้จะยังคงเป็นค่าสุ่มก็ตาม ก็ไม่ได้หมายความว่าผลลัพธ์ที่ได้จะเลวร้ายเสมอ
จริงๆ แล้วหากชั้นล่างนั้นถูกตั้งค่าสุ่มและไม่ถูกปรับมันก็จะทำตัวเป็นเสมือน random projections ซึ่งยังสามารถคงข้อมูลไว้ได้ เช่นค่ามุมระหว่างเวคเตอร์ในปริภูมิตั้งต้นก็สามารถคำนวณได้จากเวคเตอร์ที่ผ่านการฉายสุ่มเป็นต้น ดังนั้นในบางครั้งผลที่ได้จาก NN ที่ลึกแม้ไม่ถูกปรับอย่างเหมาะสมก็ยังให้ผลดีอยู่ เช่นในตัวอย่างข้างบนนี้ MLP (Tanh: 100–100–100–100–10:) ผมลอง fix ค่าถ่วงน้ำหนักในชั้นล่างสุดให้คงที่ แล้วลอง train ดู ผลที่ได้ก็ยังอยู่ในช่วง 97% ทั้งที่เราลดจำนวน parameter ที่ปรับลงไปถึง 78400 ตัว
อย่างไรก็ดีเรายังเชื่อว่าถ้าเราสามารถปรับค่าเหล่านี้ได้อย่างเหมาะสม เราน่าจะได้ผลลัพธ์ที่ดีกว่านี้อีก

หนึ่งในวิธีแก้ปัญหา gradient vanishing คือการทำ weight sharing ใน Convolutional NN ที่เราจะพูดถึงในหัวข้อถัดไป อีกวิธีคือการทำ pre-training ที่ไม่ได้แก้ปัญหานี้ตรงๆ แต่ค่อยๆ สร้าง layer ทีละอันเพื่อให้ค่าถ่วงน้ำหนักตั้งต้นนั้นไม่ใช่ค่าสุ่ม
แต่ก่อนจะไปดูเทคนิคเหล่านี้ลองมาคิดเล่นๆ ว่าเราจะแก้ด้วยวิธีอื่นได้หรือไม่

อย่างแรกคือ activation function ที่ชื่อ Rectified Linear Unit หรือ ReLU

ReLU(x) = max{ 0, x } 

ซึ่งใน Keras เราทำได้ง่ายๆ โดย

mlp.add( Dense(100, input_dim=784, activation=’relu’) )

ReLU นั้นถูกเสนอขึ้นมาไม่นานนักเคยอ่านผ่านๆ เห็นคนอ้างว่าอาจแก้ปัญหา gradient vanishing ได้ เพราะค่า derivative ของมันคือ 1 ซึ่งทำให้ค่าเกรเดียนต์ที่ถูกส่งกลับมานั้นไม่โดนลดขนาดลง แต่ทั้งนี้ค่า derivative อีกค่าของมันคือ 0 ดังนั้นมันก็มีโอกาสที่จะโยนเกรเดียนต์นั้นทิ้งไปหมดก็ได้ ผลการทดลองง่ายๆ แบบข้างบนคือ

  • ReLU: 100–10: 97.6%
  • ReLU: 100–100–10: 97.4%
  • ReLU: 100–100–100–10: 97.4%
  • ReLU: 100–100–100–100–10: 97.3%

ผลที่ได้นั้นดีกว่า tanh แต่อย่างไรก็ดีเรายังสังเกตได้ว่าปัญหา gradient vanishing นั้นน่าจะยังมีอยู่ จากการที่ accuracy ตกลงเมื่อเราเพิ่มชั้นเข้าไป

อีกวิธีที่อาจจะไม่ค่อย standard เท่าไรคือใช้ linear activation ร่วมกับ Batch Normalization
idea คือ linear activation นั้นให้ค่า derivative เป็น 1 แน่นอน ไม่มีปัญหาของค่า 0 แบบ ReLU แต่กลับมีปัญหาใหม่ คือ linear function นั้น unbounded คือมีค่าที่เป็นไปได้ตั้งแต่ -infinity จนถึง infinity วิธีแก้ง่ายๆ คือเอาค่านั้นมาผ่าน Batch Normalization ก่อน

Batch Normalization (BN) ก็เป็นเทคนิคที่ถูกนำเสนอไม่นานมานี้
idea หลักคือเรารู้ว่าก่อนนำเอา feature vector มาวิเคราะห์ เราควรทำการ normalize มันก่อน วิธีง่ายสุดก็คือการลบค่าเฉลี่ยให้มันมีการกระจายรอบ 0 และทำการ normalize ให้มีการกระจายตัวมาตรฐาน เช่นให้มีค่าเบี่ยงเบนมาตรฐานเป็น 1 เป็นต้น
วิธีที่ดีกว่า แต่ complex กว่าคือทำการแปลง feature vector เหล่านี้ก่อนให้ feature ใหม่ที่ได้นั้นไม่ correlate กัน ซึ่งทำได้โดยใช้กระบวนการ PCA เป็นต้น เมื่อเรานำไปรวมกับการแปลงแบบง่ายจะได้การแปลงที่เรียกว่าเป็นการทำ whitening
idea ของ BN คือแทนที่จะทำกระบวนการนี้เฉพาะก่อนนำเข้าข้อมูล BN จะพยายามทำกระบวนการนี้ก่อนส่งข้อมูลระหว่าง layer ด้วย ซึ่งช่วยตบค่าจาก linear layer ให้อยู่ใน bound ที่เหมาะสม

ผลที่ได้คือ

  • Linear: 100-BN-10: 92.0%
  • Linear: 100-BN-100-BN-10: 90.2%
  • Linear: 100-BN-100-BN-100-BN-10: 91.3%
  • Linear: 100-BN-100-BN-100-BN-100-BN-10: 90.3%

ดูจากผลแล้วการเลือก activation ที่ให้ค่า derivative เป็น 1 ดูจะไม่ช่วยแก้ปัญหา gradient vanishing และผลก็ยังด้อยกว่า ReLU

Convolutional Neural Network (CNN)

CNN เป็น Neural Network ที่ประกอบด้วยชั้น convolution โดยชั้น convolution นั้นกล่าวโดยย่อได้ว่าเป็นการตรวจจับ “local feature” บนรูปนำเข้า โดย local feature เหล่านี้ก็คือ convolution kernel บน layer นั่นเอง จุดเด่นของ CNN ก็คือ

  • local feature เหล่านี้ถือเป็นส่วนหนึ่งของ NN ที่จะถูกปรับไปพร้อมๆ กับส่วน classifier ดังนั้นหลังจากการ train เราจะได้ feature extractor ที่เหมาะสมกับงาน
  • local feature เหล่านี้ถูก applied ที่ตำแหน่งต่างๆ บนรูปนำเข้า (เป็นการทำ weight sharing แบบหนึ่ง) ดังนั้นเมื่อเราคำนวณเกรเดียนต์ย้อนกลับ เราต้องทำการรวมค่าที่ได้จากตำแหน่งทั้งหมดนี้ด้วย ดังนั้นขนาด (amplitude) ของค่าที่ได้จึงมีขนาดใหญ่พอในการปรับ kernel เหล่านี้ที่มักอยู่ใน layer ล่างๆ

สำหรับงานด้าน image อย่างเช่นภาพจากฐานข้อมูล MNIST นั้น เราสามารถใช้ Conv2D layer ประมวลผลภาพนำเข้าได้โดยตรง ในกรณีนี้การสร้างตัว Network ทำได้โดย

from keras.layers import Dense, Activation, Flatten
cnn = Sequential()
cnn.add( Conv2D(10, (3,3), padding=’same’, use_bias=False, data_format=’channels_first’, input_shape=(1,28,28)) )
cnn.add( BatchNormalization(axis=1) )
cnn.add( Activation(‘relu’) )
cnn.add( Flatten() )
cnn.add( Dense(10, activation=’softmax’) )
cnn.compile(loss=’categorical_crossentropy’, optimizer=’adam’, metrics=[‘accuracy’])

ในตัวอย่างข้างบนนี้เราเริ่มจาก convolution layer ที่มี kernel ขนาด 3x3 จำนวน 10 kernel ที่ไม่มี bias และเราบังคับให้ระบบเติม (padding) ค่า 0 รอบๆ รูปนำเข้าเพื่อให้รูปส่งออกมีขนาดเท่าเดิม

Layer ‘Flatten’ นั้นมีไว้เพื่อแปลงข้อมูลจากภาพหลาย channel ให้เป็นเวคเตอร์ ที่เราสามารถส่งต่อให้ชั้น MLP มาตรฐานต่อได้

ในตัวอย่างนี้เรากำหนดให้ shape ของข้อมูลนำเข้าคือเป็นภาพขนาด 28x28 pixel จำนวน 1 channel และเราจะ encode ข้อมูลนี้โดยเอา channel ไว้ก่อน ซึ่งเป็นวิธี default ของ Theano ที่เป็น Backend ของ Keras ที่ผมใช้
ในกรณีนี้ก่อนที่เราจะ train/test เราก็ต้อง reshape ข้อมูลให้อยู่ใน shape ที่เราต้องการคือ

X_train = X_train.reshape(X_train.shape[0], 1, 28, 28)
X_test = X_test.reshape(X_test.shape[0], 1, 28, 28)

ค่า accuracy ที่ได้คือ 97.8% ที่สูงกว่า MLP ที่ใช้ ReLU ที่เราทดลองไปก่อนหน้านี้ สังเกตว่าจำนวน parameter บนชั้น convolution นั้นน้อยมากเมื่อเทียบกับชั้น MLP นั่นแปลว่าเราสามารถเพิ่มจำนวน kernel หรือจำนวนชั้น ได้โดยไม่ได้เพิ่มจำนวนตัวแปรมากนัก

เราสามารถลดขนาดของ CNN นี้ได้หลายวิธี เช่นลดขนาดของ feature map โดยการทำ max pooling ที่เป็นการเลือกค่าที่มากสุดในหน้าต่าง local ค่าเดียวมาใช้เป็นต้น

cnn = Sequential()
cnn.add( Conv2D(10, (3,3), padding=’same’, use_bias=False, data_format=’channels_first’, input_shape=(1,28,28)) )
cnn.add( BatchNormalization(axis=1) )
cnn.add( Activation(‘relu’) )
cnn.add( MaxPooling2D(pool_size=(2,2), data_format=’channels_first’) )
cnn.add( Conv2D(10, (3,3), padding=’same’, use_bias=False, data_format=’channels_first’) )
cnn.add( BatchNormalization(axis=1) )
cnn.add( Activation(‘relu’) )
cnn.add( Flatten() )
cnn.add( Dense(10, activation=’softmax’) )
cnn.compile(loss=’categorical_crossentropy’, optimizer=’adam’, metrics=[‘accuracy’])

ใน CNN นั้นเรามักทำการลดขนาดสลับกับ convolution ในงานวิจัยด้าน CNN ในตอนต้นนั้น เรามองว่าการทำ convolution เปรียบเสมือนการตรวจจับ local feature ซึ่งตำแหน่งที่พบอาจคลาดเคลื่อนได้บ้าง วิธีหนึ่งที่ใช้ลดความคลาดเคลื่อนนี้คือการลดขนาดนั่นเอง
แต่ก่อนนั้นเรามักใช้ average pooling แต่ในปัจจุบันนั้น max pooling ถือได้ว่าเป็นวิธีมาตรฐานไปแล้ว

นอกจากนี้ในปัจจุบันนั้นเราอาจทำ convolution ซ้อนกันหลายครั้งก่อนทำ max pooling ครั้งหนึ่ง นั่นคือ max pooling ถูกมองว่าเป็นการลดความซับซ้อนของ network มากกว่าการจัดการความคลาดเคลื่อนในตำแหน่งของ local feature

จะเห็นว่าผลที่ได้จาก CNN ใหม่นี้คือ 98.5% ซึ่งดีกว่า CNN ก่อนนี้ ทั้งที่จำนวน parameter ของมันมีจำนวนน้อยกว่ามาก (20,680 VS. 78,540)
นั่นคือโครงสร้างใหม่นี้มีการใช้งาน parameter ที่ดีกว่า

คราวนี้ลองดูหน่อยว่า แล้ว CNN มันจะช่วยแก้ gradient vanishing ได้จริงหรือเปล่า

  • Conv-MP-Conv-10: 98.5%
  • Conv-MP-Conv-Conv-10: 98.5%
  • Conv-MP-Conv-Conv-Conv-10: 98.5%

Conv ข้างบนนั้นแทนการประมวลผลผ่าน 3 layers convolution 3x3 10 kernel — Batch Normalization — ReLU
MP คือการทำ max pooling

ผลข้างบนดูจะไม่เลวร้ายเท่าไร แต่ก็ยังดูไม่ชัวร์ว่ามันช่วย

แกะ Gradient

การดูค่า accuracy อาจจะพอตีความเรื่อง gradient vanishing ได้ แต่ดูไม่ชัดมาก คราวนี้เรามาลองแกะใส้ในของ Keras ดูค่าเกรเดียนต์เลยดีกว่า หลังจาก google ดูพักนึงก็เจอวิธีทำจาก https://github.com/fchollet/keras/issues/2226
แต่โค้ดที่ได้ก็ไม่สมบูรณ์มาก + ขี้เกียจทำให้ดี ผมเลยสั่ง run แบบ python -i เพื่อให้สามารถทดลองต่อจากโค้ดข้างล่างนี้ได้ โดยโค้ดข้างล่างนี้เอาไปวางต่อจากโค้ดสำหรับสร้าง MLP แบบ Tanh: 100–100–100–100–10

weights = mlp.trainable_weights
gradients = mlp.optimizer.get_gradients(mlp.model.total_loss, weights)
import keras.backend as K
input_tensors = [mlp.inputs[0], # input data
mlp.model.sample_weights[0], # how much to weight each sample by
mlp.model.targets[0], # labels
K.learning_phase(), # train or test mode
]
inputs = [[X_train[0], X_train[1], X_train[2], X_train[3], X_train[4]],
[1,1,1,1,1], # sample weights
[Y_train[0], Y_train[1], Y_train[2], Y_train[3], Y_train[4]],
0 # learning phase in TEST mode
]
get_gradients = K.function(inputs=input_tensors, outputs=gradients)
g = zip(weights, get_gradients(inputs))

g นั้นเป็น list ที่เก็บคู่ชื่อของ layer และค่าเกรเดียนต์ของมัน หากเราสั่ง [ a[0] for a in g ] จะได้ว่า
[dense_1/kernel, dense_1/bias, dense_2/kernel, dense_2/bias, dense_3/kernel, dense_3/bias, dense_4/kernel, dense_4/bias, dense_5/kernel, dense_5/bias]
เราไม่สนใจพวก bias ดังนั้นเราจะดูแค่ g[0], g[2], g[4], g[6] และ g[8]

g[0][1] นั้นเก็บเกรเดียนต์ของค่าถ่วงน้ำหนักของชั้นแรกไว้ นั่นคือ g[0][1].shape จะเท่ากับ (784,100) เราสามารถคำนวณ amplitude เฉลี่ยได้ง่ายๆ โดยสั่ง

np.mean(np.abs(g[0][1].reshape(78400)))

ซึ่งผลที่ได้คือ 0.0052303853
เราสามารถทำอย่างเดียวกันกับ g[2], g[4], g[6] และ g[8] แต่ทั้งนี้ก็ต้องตรวจสอบ shape ของ layer เหล่านี้ก่อน
ผลที่ได้คือ

เห็นได้ว่าขนาดของเกรเดียนต์ของชั้นล่างสุดนั้นเล็กกว่าชั้นส่งออกเยอะอยู่ แต่ทั้งนี้ก็น่าแปลกใจนิดๆ ที่ชั้น 2–4 มีค่าเกรเดียนต์ที่ไล่เลี่ยกันมากกว่าที่คิด
ไหนลองดูซิว่าถ้าโครงสร้างลึกขึ้นจะเป็นอย่างไร ข้างล่างนี้เป็นผลที่ได้จากการต่อ Dense layer ที่มี 100 โหนด เพิ่มเข้าไปอีก

ดูแล้วขนาดของเกรเดียนต์ชั้นล่างสุดจะเล็กกว่าเพื่อนเยอะ ส่วนชั้นอื่นๆ ขนาดก็เกือบครึ่งหนึ่งของชั้นบนสุด ถ้าลองเปลี่ยนโครงสร้างเป็นชั้นละ 50 โหนด หรืออีกโครงสร้างที่ 5 ชั้นแรกมี 100 โหนดกับอีก 5 ชั้น 50 โหนด ก็ได้ผลคล้ายๆ กัน

คราวนี้มาลอง CNN ตามโครงสร้าง Conv-MP-Conv-Conv-Conv-10 บ้าง วิธีแกะเกรเดียนต์ก็คล้ายๆ กัน แต่ตอน reshape ต่างกันเล็กน้อย หลักๆ คือ reshape ให้เท่ากับขนาดของ parameter ของ kernel นั้น (ในการทดลองนี้เราไม่สนพวก Batch Normalization layer) ได้ผลตามตารางข้างล่างนี้

คราวนี้เห็นได้ชัดว่าขนาดของเกรเดียนต์ของชั้นล่างๆ นั้นใหญ่มากกว่าชั้นสุดท้ายที่เป็นชั้นส่งออกซะอีก นั่นคือเรามีข้อมูลมากพอสำหรับปรับ kernel พวกนี้แน่นอน
นอกจากนี้ต่อให้เราทำ Drop out คือ โยนข้อมูลทิ้งบางส่วน เช่นครึ่งหนึ่ง ขนาดของเกรเดียนต์ของชั้นเหล่านี้ก็ยังใหญ่มากพอสำหรับการปรับ

Pre-training

การทำ pre-training คือการค่อยๆ สร้าง NN ทีละชั้นต่อๆ กันไป โดยการสร้างแต่ละชั้นนั้นใช้ unsupervised loss พวก reconstruction error เป็นหลัก เมื่อได้จำนวนชั้นที่ต้องการค่อยทำ fine tuning อีกรอบด้วย backprop ปกติ กระบวนการนี้ทำให้ค่าเริ่มต้นของค่าถ่วงน้ำหนักต่างๆ ดีกว่าการตั้งค่าสุ่ม
paper แรกที่เสนอกระบวนการนี้นั้นอิง restricted Boltzmann machine (RBM) ที่ผมไม่ถนัดเพราะอิงกระบวนการ sampling ในการคำนวณเกรเดียนต์ NN อีกแบบที่ใช้แทน RBM ได้ก็คือ Autoencoder ที่เราจะมาลองเล่นกัน

Autoencoder เป็น NN ที่มี 2 ชั้น ชั้นแรกทำการเข้ารหัส (encode) ข้อมูลแบบ non-linear และชั้นที่สองทำการสร้างคืน (decode) ข้อมูลเดิมแบบ non-linear เช่นกัน
ใน paper ต้นฉบับนั้นชั้นทั้งสองนี้ใช้ค่าถ่วงน้ำหนักที่ผูกกันไว้ (shared weights)

ใน Keras นั้นไม่มี Layer แบบนี้เตรียมไว้ให้ ลอง google ดูก็เจอแค่ตัวอย่างการสร้าง Autoencoder แบบไม่ share ค่าถ่วงน้ำหนัก ดังนั้นมาลองสร้างเองเล่นซะเลยดีกว่า

Autoencoder ที่เราสร้างก็อิงจากตัวอย่างการทำ custom layer ของ Keras นั่นคือ

from keras import backend as K
from keras.engine.topology import Layer
class Autoencoder(Layer):
def __init__(self, hidden_dim, **kwargs):
self.hidden_dim = hidden_dim
super(Autoencoder, self).__init__(**kwargs)
def build(self, input_shape):
# Create a trainable weight variable for this layer.
self.kernel = self.add_weight(name=’Auto’,
shape=(input_shape[1], self.hidden_dim),
initializer=’uniform’,
trainable=True)
super(Autoencoder, self).build(input_shape) # Be sure to call this somewhere!
def call(self, x):
return K.relu( K.dot( K.relu(K.dot(x, self.kernel)), K.transpose( self.kernel ) ) )
def compute_output_shape(self, input_shape):
return input_shape[0]

โค้ดนี้ก็มั่วๆ เอาหลักๆ คือมันต้องคำนวณ ReLU(W’ ReLU(Wx)) เมื่อ W เป็นค่าถ่วงน้ำหนักของชั้นแรกและ W’ เป็น transpose ของมัน, x เป็นข้อมูลนำเข้า
หลังจากนั้นเราก็สามารถนำไปใส่ใน Sequential model เหมือน layer ปกติ

mod = Sequential()
mod.add( Autoencoder(100, input_shape=(784,)) )
mod.compile(loss=’mean_squared_error’, optimizer=’adam’)
print mod.summary()
mod.fit(X_train, X_train, batch_size=64, epochs=5, verbose=1)

เราสามารถสร้างชั้นใหม่ซ้อนทับไปได้โดย

w1 = mod1.layers[0].get_weights()[0]
mod2 = Sequential()
mod2.add( Dense( 100, input_dim=784, use_bias=False, weights=[w1], activation=’relu’ ) )
mod2.layers[-1].trainable = False
mod2.add( Autoencoder(100) )
mod2.add( Dense( 784, use_bias=False, weights=[np.transpose(w1)], activation=’relu’ ) )
mod2.layers[-1].trainable = False
mod2.compile(loss=’mean_squared_error’, optimizer=’adam’)
print mod2.summary()
mod2.fit(X_train, X_train, batch_size=64, epochs=10, verbose=1)

สังเกตว่าเราใช้ ReLU เช่นเดียวกับใน Autoencoder และเราตั้งให้ชั้นที่ train มาแล้วไม่ต้อง train ใหม่
เราสามารถต่อชั้นแบบนี้ไปเรื่อยๆ หลังจาก train ไปแล้วเราก็สามารถนำมา recon ข้อมูลดูก่อนได้
ในรูปข้างล่างนี้เราสร้างคืนข้อมูลจากการ train ชั้นที่ 1–4 ตามลำดับ โดยในการ display นั้นเราต้องทำการ scale ค่าส่งออกก่อนเพราะ ReLU มันไม่มี upper bound ซึ่งทำให้ display แล้วเพี้ยน (ตอนแรกลองใช้ sigmoid แต่ให้ผล recon ที่แย่กว่า)

เมื่อนำชั้นที่ได้ทั้งหมดไปใส่ MLP ReLU: 100–100–100–100–10 ได้ผลและ re-train ใหม่อีกที ได้ accuracy ประมาณ 97.3% คล้ายๆ ที่ train รวดเดียว ไม่แน่ใจว่าทำอะไรผิดหรือเปล่า

ในงานแรกที่มีการเสนอการต่อ Autoencoder นั้นแนะนำให้ train Autoencoder ด้วยข้อมูลที่ถูกสัญญาณรบกวน และให้สร้างคืนรูปเดิม

ใน Keras นั้นการใส่สัญญาณรบกวนสามารถทำได้โดยใช้ ImageDataGenerator
ทั้งนี้ควรทราบว่า shape ที่ ImageDataGenerator รับนั้นคือ (width,height,channel) ดังนั้นเราจึงต้องแปลง shape ของข้อมูลก่อน นั่นคือ

X_train = X_train.reshape(X_train.shape[0], 28, 28, 1 )
X_test = X_test.reshape(X_test.shape[0], 28, 28, 1 )

จากนั้นก็สร้างฟังก์ชันสำหรับเพิ่มสัญญาณรบกวนก่อนเรียกใช้งาน

def add_noise( img ):
noise = np.zeros( img.shape )
l = np.random.randint(50)
for i in range(l):
x = np.random.randint(28)
y = np.random.randint(28)
noise[x,y,0] = np.random.uniform(0.0, 1.0)
return img + noise

from keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(preprocessing_function=add_noise,
rotation_range=5,
width_shift_range=0.2,
height_shift_range=0.2,
zoom_range=0.1)
datagen.fit(X_train)

เนื่องจากเราทำการปรับ shape ของข้อมูล เลยต้องปรับที่ model เล็กน้อยก่อนใช้งาน ด้วยนั่นคือ

mod1 = Sequential()
mod1.add( Reshape((784,), input_shape=(28,28,1)) )
mod1.add( Autoencoder(100) )
mod1.compile(loss=’mean_squared_error’, optimizer=’adam’)
mod1.fit_generator(datagen.flow(X_train, X_train.reshape(X_train.shape[0], 784), batch_size=64), steps_per_epoch=len(X_train) / 64, epochs=5)

เราสามารถ train คล้ายๆ เดิมได้ ซึ่งคราวนี้เราได้ค่า accuracy เท่ากับ 97.7% ก็ดีขึ้นหน่อย ทั้งนี้เดาว่าถ้าเราปรับส่วนการสร้างข้อมูลเพิ่มอีกหน่อยอาจจะได้ดีขึ้นอีกก็ได้
สรุปคือ pre-training อาจจะปรับค่า accuracy ให้ดีขึ้นได้แต่ผลก็ยังด้อยกว่าโครงสร้างแบบ CNN

ส่งท้าย

จากตอนแรกที่ตั้งใจว่าจะเขียน tutorial Keras ไปๆ มาๆ กลับมานั่งทำอย่างอื่นเล่น แต่ก็ได้ความรู้ใหม่ทั้งการแกะค่าเกรเดียนต์และการสร้าง custom layer หวังว่าจะมีประโยชน์บ้างไม่มากก็น้อย

Have Fun :)

--

--