ย้ายค่าย Caffe ไป Keras/Theano
หลังจากใช้งาน Caffe มาพักนึงเริ่มรู้สึกว่ามันค่อนข้างจำกัด คือถ้าจะทำอย่างอื่นนอกจาก train+test แล้วนี่ tools ที่มีให้ดูจะไม่ค่อยยืดหยุ่นเท่าไร คือทำได้แต่ไม่สะดวก อย่างถ้าจะเอา CNN มาใช้เป็น feature extractor ก็ต้องเอาข้อมูลเข้า lmdb format -> สั่ง feature_extract แล้ว save เป็น lmdb อีกอัน -> เปิด lmdb จากไฟล์อื่นที่จะเอาไปใช้ ถ้าเราสามารถรวมขั้นตอนทั้งหมดนี้ไว้ในโปรแกรมเดียวมันก็จะสะดวกกว่าในการใช้งาน
นอกจากนี้เครื่องของผมก็ไม่ได้ลง Caffe ไว้ การจะเขียน script เพื่อเรียกใช้ Caffe library โดยตรงเลยทำไม่ได้อีก
พอดีช่วงหลังนี้เริ่มมาสนใจ Keras บน python ที่ใช้ได้ทั้งกับ Theano และ TensorFlow ตอนนี้ผมใช้ Theano backend อยู่ TensoFllow ยังไม่ได้ลอง
ตอนนี้เลยต้องหาวิธี convert model ที่สร้างจาก Caffe เพื่อมาใช้งานกับ Keras ก่อนเพราะเสียดายอุสาห์ train มาอย่างดี
หลังจาก Google ดูก็ไม่เจอวิธีที่ใช้ได้หรือถูกใจ เลยมามั่วๆ เอาเอง เป็นวิธีที่ไม่อัตโนมัติ แต่ก็ work อยู่
Export Caffe model
เข้าไปบนเครื่องที่มี Caffe install อยู่ ของผมคือเครื่องของแลป สังเกตว่าใน directory Caffe จะมี sub-directory ชื่อ python จดชื่อ path นั้นไว้
เข้าไปที่ directory ที่เราเก็บ Caffe model ไว้ เช่นตอนที่ผม train LeNet บน MNIST ก็จะมี lenet_train_test.prototxt, lenet_solver.prototxt และมี mnist_train กับ mnist_test ที่เก็บ lmdb ของ MNIST data นอกจากนี้ก็มีไฟล์ mnist_iter_500000.caffemodel และ mnist_iter_500000.solverstate ที่ได้จากการ train
เริ่มจากสั่ง run python จากนั้น import caffe และ load model โดย
>>> import sys
>>> sys.path.append(‘/opt/caffe/python/’)
>>> import caffe
>>> net = caffe.Net(‘lenet_train_test.prototxt’, ‘mnist_iter_500000.caffemodel’, caffe.TEST)
ขั้นตอนนี้จะเป็นการ load model ที่อยู่ใน .caffemodel ลงไปใน caffe.Net data structure โดย instance ที่มารับค่านี้ชื่อ net
ในขั้นตอนนี้เราต้อง run ใน directory ที่มี lmdb ตามที่เขียนไว้ใน .prototxt ถึงแม้ว่าเราจะไม่ได้อยากใช้ data นี้ก็ตาม
ผมไม่สามารถหารายละเอียดของ caffe.Net ได้ (เพราะขี้เกียจ :P) แต่โชคดีที่เจอว่าเราสามารถดูรายละเอียดของแต่ละ layer ได้ไม่ยากนัก เช่นใน lenet_train_test.prototxt ผมมี layer แรกเป็น convolution layer ชื่อ ‘conv1’ เราก็สามารถดูรายละเอียดของมันได้จาก
>>> net.params[‘conv1’]
<caffe._caffe.BlobVec object at 0x7f7ec4d22b40>
>>> len(net.params[‘conv1’])
2
นั่นคือ net.params[‘conv1’] นั้นเป็น array 2 มิติ ที่ประกอบด้วย
- net.params[‘conv1’][0] เท่าที่เข้าใจคือ weights ของ ชั้นนี้
- net.params[‘conv1’][1] เท่าที่เข้าใจคือ bias ของ ชั้นนี้
โดยเราสามารถดูค่าของทั้งสองค่าได้จาก net.params[‘conv1’][0].data และ net.params[‘conv1’][1].data
>>> net.params[‘conv1’][0]
<caffe._caffe.Blob object at 0x7f7ed8839938>
>>> net.params[‘conv1’][0].data
array([[[[ 3.16468179e-02, 1.08534239e-01, 1.72735259e-01,
2.08028495e-01, 1.99960262e-01],
….>>> net.params[‘conv1’][1].data
array([-0.04597092, 0.06176386, -0.00523308, 0.02104107, -0.03713251,
-0.01622008, 0.02467155, -0.06594995, 0.00843894, -0.00171885,
0.03085016, -0.05871302, 0.01809652, -0.03184966, -0.00869165,
-0.03705993, -0.00378206, -0.03097503, 0.0016596 , 0.0449267 ], dtype=float32)
เมื่อเราดูค่าได้เราก็ย่อมสามารถ save ค่าเหล่านี้ได้ ใน python ทำได้โดยการสั่ง save ใน module numpy ซึ่งเป็น format ของ numpy
>>> import numpy as np
>>> np.save(‘lenet.conv1.kernel_weights.npy’,net.params[‘conv1’][0].data)
>>> np.save(‘lenet.conv1.kernel_bias.npy’,net.params[‘conv1’][1].data)
หลังจากนี้เราก็ไปดูว่ายังมี layer ใดอีก ปรากฏว่า layer พวก maxpooling นี่ save ไม่ได้ (เพราะคงไม่มีอะไรให้ save) ดังนั้นชั้นที่เหลือของ LeNet ก็มี ‘conv2’, ‘ip1’, ‘ip2’ โดยแต่ละชั้นก็ save ด้วยวิธีเดียวกัน
หลังจากนั้นผมก็ copy ไฟล์เหล่านี้มาเครื่องผมที่ไม่มี Caffe แต่มี Keras/Theano
สร้าง LeNet จาก Caffe weights
บนเครื่องผมก็เริ่มจาก run python และ import Keras
>>> import keras
Using Theano backend.
>>> from keras.datasets import mnist
>>> from keras.models import Sequential
>>> from keras.layers import Dense, Dropout, Activation, Flatten
>>> from keras.layers import Convolution2D, MaxPooling2D
>>> from keras.utils import np_utils
>>> import numpy as np
จากนั้นก็ load บรรดา weights ต่างๆ เตรียมไว้ก่อน
>>> c1_w = np.load(‘lenet.conv1.kernel_weights.npy’)
>>> c1_b = np.load(‘lenet.conv1.kernel_bias.npy’)
>>> c2_w = np.load(‘lenet.conv2.kernel_weights.npy’)
>>> c2_b = np.load(‘lenet.conv2.kernel_bias.npy’)
>>> ip1_0 = np.load(‘lenet.ip1.0.npy’)
>>> ip1_1 = np.load(‘lenet.ip1.1.npy’)
>>> ip2_0 = np.load(‘lenet.ip2.0.npy’)
>>> ip2_1 = np.load(‘lenet.ip2.1.npy’)
ขั้นตอนปกติที่ผมใช้สร้าง LeNet บน Keras คือ
model = Sequential()
model.add(Convolution2D(10, 5, 5, border_mode=’valid’, input_shape=(1, 28, 28))
model.add(MaxPooling2D(pool_size=(2,2), dim_ordering=’th’))
model.add(Convolution2D(20, 5, 5, border_mode=’valid’))
model.add(MaxPooling2D(pool_size=(2,2), dim_ordering=’th’))
model.add(Flatten())
model.add(Dense(100, activation=’relu’))
model.add(Dense(10, activation=’softmax’))
model.compile(loss=’categorical_crossentropy’, optimizer=’adam’, metrics=[‘accuracy’])
คราวนี้ที่ต่างกันก็คือใส่ weights ต่างๆ เข้าไปตอนสร้าง layer ด้วย
ข้อควรระวังคือ Caffe นั้น implement “correlation” ในชั้น Convolution ต่างจาก Theano ที่ implement “convolution” จริงๆ ดังนั้น (หาดูเอาเองนะว่าต่างกันไง ;)) ดังนั้น weights ของ Caffe ที่จะนำมาใส่ใน Keras/Theano ต้องทำการ flip ก่อน สำหรับ Keras/TensorFlow ยังไม่แน่ใจ
นั่นคือเราสร้างฟังก์ชันการหมุนต่อไปนี้
def rot90(W):
for i in range(W.shape[0]):
for j in range(W.shape[1]):
W[i, j] = np.rot90(W[i, j], 2)
return W
Code นี้มาจาก https://github.com/MarcBS/keras/blob/master/keras/caffe/convert.py
เมื่อนำมาสร้าง Convolution2D Layer จะได้เป็น
conv1 = Convolution2D(20, 5, 5, border_mode=’valid’, input_shape=(1, 28, 28), weights=[rot90(c1_w),c1_b])
model=Sequential()
>>> model.add(conv1)
>>> model.output_shape
(None, 20, 24, 24)
>>> model.add(MaxPooling2D(pool_size=(2,2), dim_ordering=’th’))
>>> model.output_shape
(None, 20, 12, 12)
คำสั่งสุดท้ายนั้นใช้ในการตรวจสอบ dimension ของ output
>>> conv2 = Convolution2D(50, 5, 5, border_mode=’valid’, input_shape=(20, 12, 12), weights=[rot90(c2_w),c2_b])
>>> model.add(conv2)
>>> model.output_shape
(None, 50, 8, 8)
>>> model.add(MaxPooling2D(pool_size=(2,2), dim_ordering=’th’))
>>> model.output_shape
(None, 50, 4, 4)
>>> model.add(Flatten())
>>> model.output_shape
(None, 800)
ในการสร้างชั้น ip1 และ ip2 นั้นต้องปรับนิดๆ เพราะมันต้องสลับ dimension ด้วย นั่นคือ
>>> ip1 = Dense(500, input_dim=800, weights=[ip1_0.swapaxes(0,1),ip1_1])
>>> model.add(ip1)
>>> model.output_shape
(None, 500)
>>> ip2 = Dense(10, input_dim=500, weights=[ip2_0.swapaxes(0,1),ip2_1])
>>> model.add(ip2)
>>> model.output_shape
(None, 10)
หากเราไม่เรียก swapaxes ในตอนสร้าง ip1 จะทำได้ แต่จะมีปัญหาตอนเรียก model.add(ip1) เหมือนกับว่าตอนสร้าง layer แยกมันไม่ได้ตรวจหมด มาตรวจละเอียดอีกทีตอน add
จากนั้นก็ compile model และ save ไว้ใช้งานต่อไป
>>> model.compile(loss=’categorical_crossentropy’, optimizer=’adadelta’, metrics=[‘accuracy’])
>>> model_json = model.to_json()
>>> with open(“lenet_caffe.struct.json”, “w”) as json_file: json_file.write(model_json)
>>>model.save_weights(“lenet_caffe.weights.h5”)