สนุกกับ Neural Network (2)

ออกทะเลอีกรอบ 555

Sanparith Marukatat
5 min readJun 13, 2017

โหลดข้อมูล: pima indians diabetes

คราวก่อนลองเล่นกับ MNIST ไปแล้ว คราวนี้มาลอง data อื่นบ้าง เช่นจาก UCI ลอง pima-indians-diabetes ดูเป็นข้อมูลเบาหวานที่มีคนทดสอบเล่นเยอะอยู่

ข้อควรรู้ ถึงข้อมูลนี้จะมาจากข้อมูลจริง แต่มันก็ยังไม่สมบูรณ์ถ้าเราดูคำอธิบายดีๆ จะเห็นว่า “… there are zeros in places where they are biologically impossible, such as the blood pressure attribute. It seems very likely that zero values encode missing data. However, since the dataset donors made no such statement we encourage you to use your best judgement and state your assumptions.” ดังนั้นต่อให้เราสามารถสร้างแบบจำลองที่ให้ผลดีบนชุดข้อมูลนี้ก็ตามก็ยังไม่สามารถ claim ได้ว่าแก้ปัญหาการทำนายโรคเบาหวานได้จริง
สำหรับข้อมูลอื่นๆ บน UCI ก็เช่นกัน ก่อนจะ claim อะไรควรอ่านคำอธิบายข้อมูลและที่มา ให้ชัดเจนก่อน
ในโพสนี้เราจะ focus ที่การนำมาทดลองใช้ในการสร้าง neural network เฉยๆ เพื่อให้เห็นภาพว่าในการใช้งานจริงต้องทำอย่างไร

เริ่มจาก save ไฟล์จาก https://archive.ics.uci.edu/ml/machine-learning-databases/pima-indians-diabetes/pima-indians-diabetes.data จะเห็นว่ามันเป็น CSV ธรรมดา ที่แต่ละบรรทัดคือตัวอย่าง 1 ตัว 8 ค่าแรกเป็น feature และค่าสุดท้ายเป็น class งานนี้มีแค่ 2 class คือ 0 และ 1

หลังจาก save ไฟล์มาแล้วก็ลองเปิดดูได้ใน python โดยใช้ numpy

import numpy
filename = ‘pima-indians-diabetes.data’
raw_data = open(filename, ‘rt’)
data = numpy.loadtxt(raw_data, delimiter=”,”)
print(data.shape)

จากนั้นก็แยกส่วน features กับ class โดย

X = data[:,0:8]
Y = data[:,8:9].reshape(data.shape[0])

หากลองดูค่าของ feature ต่างๆ จะเห็นว่ามี “ขนาด” ต่างกันมากอยู่ ถ้าจะนำมาใช้ร่วมกันก็ควรทำการ normalize ก่อน
ในฐานข้อมูลนี้มีค่าเฉลี่ยและค่าเบี่ยงเบนมาตรฐานให้อยู่แล้ว (ดูจาก Data Set Description) ดังนั้นเราสามารถทำการปรับได้โดย

mean = [ 3.8, 120.9, 69.1, 20.5, 79.8, 32.0, 0.5, 33.2 ]
sd = [ 3.4, 32.0, 19.4, 16.0, 115.2, 7.9, 0.3, 11.8 ]
X = (X-mean)/sd

ตอนนี้เราก็พร้อมจะทดลองสร้าง neural network (NN) มาวิเคราะห์ข้อมูลนี้แล้ว แต่สังเกตว่าข้อมูลชุดนี้มันเล็ก ถ้าจะแบ่งเป็นชุด train/test แล้วก็ยิ่งน้อย ผมเลยลองหาดูว่าคนอื่นทำไง เจอใน Kaggle ว่าเขาทำ random split โดยใช้ชุดคำสั่งจาก Scikit-learn (ซึ่ง install ได้ไม่ยาก แค่สั่ง ‘pip install -U scikit-learn’ ได้เลย) เลยทำตามบ้าง นั่นคือ

from sklearn.model_selection import train_test_split
for i in range(5):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)

XXXX

โดยใน XXXX ก็ให้แทนด้วยการสร้างและ train NN บน (X_train, y_train) และทดสอบบน (X_test, y_test) ทำทั้งหมด 5 รอบ แล้วมาดูผลกัน

MLP

เริ่มจากโครงสร้าง NN พื้นฐานคือ Multi-Layer Perceptron หรือ MLP สำหรับงานการจำแนกประเภท หรือ classification

  • จำนวน input node = จำนวน feature
  • จำนวน output node = 1 เพราะเรามีแค่ 2 class
  • จำนวน hidden node เป็นตัวแปรที่คนออกแบบระบบต้องระบุเอง

การสร้างก็คล้ายๆ กับในโพสก่อน ที่ใช้ Keras นั่นคือ

mlp = Sequential()
mlp.add( Dense(100, input_dim=8, activation=’sigmoid’) )
mlp.add( Dense(1, activation=’sigmoid’) )
mlp.compile(loss=’binary_crossentropy’, optimizer=’adam’, metrics=[‘accuracy’])
print mlp.summary()

mlp.fit(X_train, y_train, batch_size=64, epochs=5, verbose=1)
sc = mlp.evaluate(X_test, y_test, verbose=0)
score.append(sc[1])

เราสร้าง MLP ที่มี hidden layer 1 ชั้น ประกอบด้วย hidden node 100 โหนด และใช้ sigmoid activation ทั้งหมด (เพราะเรากำหนดให้คลาสเป็น 0/1 ถ้าใช้ class -1/1 ก็คงใช้ tanh แทน)
เราใช้ binary_crossentropy เพราะเราทำงานแยกประเภทแค่ 2 class
ค่า sc[1] คือผล accuracy บนชุดทดสอบ ซึ่งในโค้ดข้างบนนี้ผมเอาไปใส่รวมไว้ใน list ชื่อ score
หลังจาก run ครบ 5 รอบก็มาดู accuracy ทีได้นั่นคือ

0.69, 0.70, 0.73, 0.73, 0.74

ก็คือประมาณ 73% ถือว่าไม่เลวนักเมื่อเทียบกับ https://www.kaggle.com/hugues/basic-ml-best-of-10-classifiers (ผลอาจจะเทียบตรงๆ ไม่ได้เพราะเขาทำ cross validation อีก แต่ผมขี้เกียจทำ 555)

พอลองเปลี่ยนเป็น network ที่ใหญ่ขึ้นคือ 100–100–1 accuracy ที่ได้คือ

0.68, 0.68, 0.68, 0.68, 0.68

ซึ่งแย่กว่า แต่ก็ไม่แปลกใจเท่าไรเพราะ network 100–100–1 นี้มีจำนวน parameters ที่ต้องปรับคือ 11,101 ตัว เทียบกับ 1,001 ตัวก่อนนี้ถือว่าเยอะกว่ามาก โดยปกติแล้วในกรณีที่ข้อมูลน้อยนั้นส่วนมากแล้วแบบจำลองที่ซับซ้อนน้อยกว่ามักให้ผลดีกว่า
ผลข้างบนนี้สอดคล้องกับสิ่งที่เราเรียนรู้มาในอดีต คำถามคือแล้วตอนนี้มันยังจริงอยู่หรือไม่?

แบบฝึกหัด

  • ลองเปลี่ยนโครงสร้าง MLP นี้ เช่นเพิ่มจำนวนโหนด หรือเพิ่มจำนวนชั้น หรือเพิ่มทั้งคู่ หรือเปลี่ยน activation แล้วดูผลที่ได้
  • ลองเปลี่ยนไปเล่นกับข้อมูลอื่นจาก UCI ดู

CNN

ในโพสก่อนหน้านี้ผมเล่าถึงแนวความคิดของคุณ Hinton ที่เสนอให้เรา prefer แบบจำลองที่ซับซ้อนกว่าไว้ก่อน โดยแกอ้างว่าแบบจำลองเหล่านี้ภายใต้การกำกับที่เหมาะสมจะให้ผลที่ดีกว่า ในหัวข้อนี้เรามาลองสร้างแบบจำลองในแนวนี้ดีกว่า

เพื่อสร้าง neural network ใหญ่ๆ เราจะใช้โครงสร้าง convolution กัน เพราะผมรู้สึกว่าการ share ค่าถ่วงน้ำหนักที่ช่วยเพิ่มขนาดของเกรเดียนต์นั้นเป็นขบวนการที่เหมาะสมสำหรับรองรับ network ขนาดใหญ่ (ถ้าสนใจเรื่องเกรเดียนต์อ่านเพิ่มได้จากโพสก่อนนี้ “สนุกกับ Neural Network”)

คำถามคือแล้วเราจะเอา convolution มาใช้กับข้อมูลแบบนี้อย่างไรดี? คำตอบคือผ่านการทำ Transpose convolution (ผมชอบเรียกเองว่า de-convolution แต่มีคนท้วงว่าเดี๋ยวจะสับสนกับ signal de-convolution)

Transpose convolution เป็นกระบวนการกลับข้างของการทำ convolution นั่นเอง
การ convolve ด้วย kernel หรือหน้ากาก ขนาด 3x3 จะแปลงข้อมูลจากภาพขนาด 3x3 ลงมาเหลือค่าเดียว (ผมชอบมอง kernel เป็นหน้ากาก เพราะเห็นภาพดี และไม่สับสนกับ kernel ในพวก SVM)
การทำ transpose convolution ด้วยหน้ากากขนาด 3x3 ก็จะแปลงข้อมูลจากค่าเดียวไปเป็นภาพขนาด 3x3
บน Keras นั้นทำได้โดยใช้ layer ชื่อ Conv2DTranspose ข้อควรรู้คือ layer นี้รับข้อมูลพวกภาพไม่ใช่ feature vector ปกติ ดังนั้นเราจึงต้องทำการ reshape ข้อมูลก่อนให้อยู่ในรูปแบบเดียวกันกับข้อมูลภาพ นั่นคือ

cnn = Sequential()
cnn.add( Reshape((8,1,1), input_shape=(8,)) )
cnn.add( Conv2DTranspose(10, (10,10), padding=’valid’, use_bias=False, data_format=’channels_first’) )
cnn.add( BatchNormalization(axis=1) )
cnn.add( Activation(‘relu’) )
cnn.add( Conv2D(10, (3,3), padding=’same’, data_format=’channels_first’) )
cnn.add( BatchNormalization(axis=1) )
cnn.add( Activation(‘relu’) )
cnn.add( Conv2D(10, (3,3), padding=’same’, data_format=’channels_first’) )
cnn.add( BatchNormalization(axis=1) )
cnn.add( Activation(‘relu’) )
cnn.add( Flatten() )
cnn.add( Dense(1, activation=’sigmoid’) )
cnn.compile(loss=’binary_crossentropy’, optimizer=’adam’, metrics=[‘accuracy’])

โครงสร้างข้างบนนี้ประกอบด้วยชั้น

  • Reshape-TransposeConv-BN-ReLU
  • ตามด้วย block ปกติของการทำ convolution นั่นคือ Conv-BN-ReLU
  • ก่อนจะจบด้วย Flatten-Dense layers เพื่อทำการทำนาย class

โครงสร้างนี้มีจำนวนตัวแปรที่ต้องปรับถึง 10,881 ตัว เกือบเท่ากับ MLP 100–100–1 เลยทีเดียว แต่ในกรณีนี้ผล accuracy ที่ได้เทียบเท่ากับผลจาก MLP 100–1 นั่นคือ

0.69, 0.72, 0.73, 0.76, 0.76

ข้อสรุปจากการทดลองง่ายๆ นี้คือ ถ้าเรามีโครงสร้างที่มีการใช้งานตัวแปรที่เหมาะสมแล้วต่อให้มีจำนวนตัวแปรมากเราก็ยังสามารถ train จนให้ผลดีได้เช่นกัน การทำ weight sharing ในชั้น convolution ก็เป็นการกำกับ (regularize) แบบหนึ่ง

ลองสร้าง CNN เล่น

ในย่อหน้าก่อนนี้เราเห็นว่าเราสามารถนำเอากระบวนการ convolution มาใช้กับข้อมูลที่เป็น feature vector ใดๆ ก็ได้ซึ่งก็ให้ผลที่น่าสนใจ (แต่การตีความนี่ยังไม่แน่ใจว่าจะตีความได้ไง) ในช่วงหลังๆ นี่ส่วนมากผมเลยใช้แต่โครงสร้างที่อิงการทำ convolution เห็นหลัก

สิ่งสำคัญในการสร้าง CNN คือการใช้งานชั้น convolution ใน Keras นั้น document หาดูได้จาก https://keras.io/layers/convolutional/
ตัวแปรหลักๆ ที่ผมปรับในการสร้างชั้น convolution คือ

Conv2D(filters, kernel_size, padding=’valid’, data_format=None, use_bias=True, activation=None, strides=(1, 1))
  • filters คือจำนวน kernel หรือหน้ากาก ที่เราต้องการ ซึ่งค่านี้จะเท่ากับจำนวน channels ในข้อมูลที่ส่งออกจากชั้นนี้
  • kernel_size คือขนาดของ kernel โดยปกติผมมักใช้ขนาด (3,3)
  • padding มี 2 แบบหลักๆ คือ ‘same’ ที่บังคับให้ชั้นนี้ส่งออกข้อมูลขนาดเท่าเดิม ทำได้โดยการเพิ่มขอบที่มีค่า 0 (zero padding) และ ‘valid’ ที่อนุญาตให้ข้อมูลส่งออกมีขนาดเล็กลง
  • data_format ผมมักใช้ ‘channels_first’ แต่ก็มีอีกแบบคือ ‘channels_last’ ซึ่งอันนี้ก็แล้วแต่ว่าเราเก็บข้อมูลอย่างไร
  • use_bias ผมมักตั้งให้เป็น False แต่อันนี้ไม่ค่อยแน่ใจว่าถ้าตั้ง True แล้วผลจะต่างอย่างมีนัยสำคัญหรือไม่
  • activation ผมมักใช้ค่า default คือ None และไปเพิ่มชั้น BN กับ ReLU ทีหลังมากกว่า
  • strides คือตัวกำหนดการเลื่อนหน้ากาก การเลื่อน (1,1) คือการเลื่อนปกติ การเลื่อน (2,2) มองได้ว่าเป็นการลดขนาดภาพซึ่งสามารถใช้แทนการทำพวก Pooling ได้ strides แบบอื่นนั้นผมยังไม่เคยลองใช้

เมื่อเรารู้เช่นนี้เมื่อเราอ่านบทความต่างๆ เราก็สามารถสร้าง CNN ตามได้ เช่น Deep ID network จาก http://mmlab.ie.cuhk.edu.hk/pdf/YiSun_CVPR14.pdf

จากภาพนี้เรารู้ว่า

  • ข้อมูลนำเข้ามี 1 channel ขนาด 31x39 pixel
  • Convolution layer ชั้นแรกใช้หน้ากากขนาด 4x4 และส่งออกข้อมูลที่มี 20 channels ดังนั้นชั้นนี้ต้องมีจำนวนหน้ากากเท่ากับ 20 อัน และเรารู้ว่าภาพส่งออกมีขนาดเล็กลง (28x36) ดังนั้น padding ต้องเป็นแบบ ‘valid’ ในบทความบอกว่าเขาใช้ ReLU activation และใช้ bias ด้วย นั่นคือ
model = Sequential()
model.add( Conv2D(20, (4,4), padding=’valid’, use_bias=True, activation=’relu’, input_shape=(1,31,39), data_format=’channels_first’) )

ถ้าเราลองพิมพ์ model.summary() ดูจะได้ว่า ซึ่งดูจาก output shape แล้วคงถูก

  • ชั้นถัดมาเป็น MaxPooling 2D ที่มี pool size ขนาด 2x2 เราสามารถสร้างได้ง่ายๆ โดย
model.add( MaxPooling2D(pool_size=(2,2), data_format=’channels_first’) )

data_format นั้นผมชอบใส่ไว้เพราะรู้สึกว่าบางทีไม่ใส่แล้วมันเพี้ยนๆ

  • ชั้น Convolution 2,3 และ Max-pooling 2,3 นั้นก็ทำคล้ายๆ กันจะมี tricky นิดๆ ก็ตรงชั้น Convolution 4 ที่ต้องเอาค่าส่งออกมาต่อกับค่าจาก Convolution 3 ใน Keras นั้นทำได้โดย
def deepid_layerconv4( input_shape ):
x = Input(shape=input_shape)
y = Conv2D(80, (2,2), data_format=’channels_first’)(x)
y = Flatten()(y)
z = Flatten()(x)
w = Concatenate()([y,z])
r = Model(input=[x], output=[w])
return r

ในโค้ดข้างบนนี้ y คือค่าที่ได้จากชั้น Convolution 4 ที่เราแผ่ออกโดยใช้ Flatten
z คือค่านำเข้าของชั้น Convolution 4 นั่นคือค่าส่งออกของชั้น Max-pooling 3 นั่นเอง ซึ่งเราก็เอามาแผ่ออกด้วย Flatten เช่นกัน
จากนั้นเราจึงเอาค่า y และ z มาต่อกันเป็นข้อมูลส่งออก

สังเกตว่าในฟังก์ชันนี้เราสร้าง layer ด้วย object Model แทนที่จะใช้ Sequential แบบในตัวอย่างก่อนๆ
Sequential นั้นเหมาะเวลาที่เราเพิ่มชั้นเข้าไปเรื่อยๆ
Model นั้นเหมาะเวลาที่เรามี connection แปลกๆ เช่นในกรณีนี้เป็นต้น

หวังว่าคนที่อ่านจนถึงตอนนี้จะพอที่จะรู้วิธีสร้าง CNN ที่เห็นๆ ในบทความต่างๆ

แบบฝึกหัด

  • เขียนฟังก์ชันการสร้าง Deep ID network ให้จบ

ResNet

ในปี 2015 มีบทความที่น่าสนใจออกมาคือ https://arxiv.org/abs/1512.03385 ที่อธิบายการสร้าง CNN ที่ลึกมากและให้ผลดีบนชุดข้อมูลขนาดใหญ่อย่างงานแข่ง Imagenet

สิ่งที่น่าสนใจในบทความนี้คือผู้เขียนได้ทดลองสร้าง CNN 50 ชั้น ซึ่งลึกกว่าที่ใช้กันในตอนนั้น (คือประมาณ 20 ชั้น) และสังเกตว่าการสร้าง CNN ลึกๆ ไม่ได้ให้ผลที่ดีกว่าเสมอ
ข้อสรุปที่เขาเสนอคือชั้น Convolution บางชั้นนั้นอาจจะไม่จำเป็น และเสนอวิธีแก้ง่ายๆ คือให้เพิ่มการ skip ข้ามชั้นไว้ด้วย ถ้าชั้นหนึ่งๆ ไม่จำเป็นจริงๆ ค่าถ่วงน้ำหนักบนชั้นเหล่านั้นก็จะถูก train ให้เข้าใกล้ 0 ไปเอง ข้อมูลที่จำเป็นก็ยังสามารถ flow ผ่าน skip connection ไปชั้นต่อไปได้ นี่คือที่มาของชื่อ Residual Network หรือ ResNet

การสร้าง layer ที่มี skip connection บน Keras นั้นทำได้โดยใช้ Model คล้ายๆ กับตัวอย่าง Deep ID ก่อนนี้นั่นคือ

def create_res_basicblock(input_shape, k):
x = Input(shape=(input_shape))
# residual path
residual = BatchNormalization(axis=1)(x)
residual = Activation(‘relu’)(residual)
residual = Conv2D(k, (3,3), padding=’same’, use_bias=False, data_format=’channels_first’)(residual)
# Add the two paths
y = Add()([x, residual])
block = Model(inputs=[x], outputs=[y])
return block

เราสามารถใช้ layer นี้ได้ง่ายๆ เช่นถ้าข้อมูลก่อนชั้นนี้มี shape คือ (10,30,30)

model.add(create_res_basicblock((10,30,30), 10))

สังเกตว่าในกรณีนี้เราใช้จำนวนหน้ากาก k=10 ซึ่งต้องเท่ากับจำนวน channels ใน input_shape เช่นกัน ถ้าไม่งั้นจะสั่ง Add ไม่ได้
ซึ่งแปลว่าในตอนเราสร้าง model นั้นขั้นแรกเราต้องเพิ่มจำนวน channels ให้เท่ากับจำนวนหน้ากากที่เราต้องการใช้ก่อนนั่นคือ

model = Sequential()
model.add( Conv2D(10, (3,3), padding=’same’, use_bias=False, input_shape=(1,30,30), data_format=’channels_first’) )
model.add( BatchNormalization(axis=1) )
model.add( Activation(‘relu’) )
model.add( create_res_basicblock((10,30,30), 10) )

ในหลายๆ งานที่ใช้ ResNet นั้นเมื่อเราลดขนาดแล้วเรามักจะเพิ่มจำนวน channels ซึ่งในโค้ดข้างบนจะไม่รองรับตรงๆ เพราะเราไม่สามารถ Add ข้อมูลที่ shape ไม่เท่ากันได้ วิธีแก้คือ

  • ทำการ resize ภาพก่อนส่งเข้า residual path
  • ทำการ pad channels ด้วยค่า 0 ซึ่ง tricky เล็กน้อย ที่ผมทำคือเอา Convolution ขนาด 1x1 ที่มีค่าคงที่เท่ากับ 0 ไปคูณ เพราะไม่รู้ว่าเราสร้าง blob ค่า 0 เฉยๆ ได้ไง (ถ้าใครรู้ก็บอกด้วย)
def create_res_basicblock( input_shape, k, reduce_first ): 
x = Input(shape=(input_shape))
# shortcut path
xx = x
if reduce_first: # need to scale down
xx = ลดขนาด(x)
if k>input_shape[0]: # need to pad zero channels
tmp = Conv2D(k-input_shape[0], (1,1), padding=’same’, use_bias=False, data_format=’channels_first’, kernel_initializer=’zero’)(xx)
tmp.trainable = False # no train, just zero
xx = Concatenate(axis=1)([xx, tmp])
# residual path
residual = BatchNormalization(axis=1)(xx)
residual = Activation(‘relu’)(residual)
residual = Conv2D(k, (3,3), padding=’same’, use_bias=False, data_format=’channels_first’, kernel_initializer=kinit)(residual)
# Add two paths
y = Add()([xx, residual])
block = Model(inputs=[x], outputs=[y])
return block

แบบฝึกหัด

  • ทำโค้ดข้างบนให้สมบูรณ์
  • ในบางงานนั้น operation บน residual path คือ BN-ReLU-Conv 2 ครั้ง ลองแก้โค้ดให้รองรับโครงสร้างนี้ดู

ส่งท้าย

คราวก่อนมีน้องบอกว่าอ่านแล้วยังไม่เก็ตเท่าไร เลยพยายามเขียน tutorial ใหม่ แต่คราวนี้ก็ออกทะเลอีกเล็กน้อย 555 แต่หวังว่าคนที่อ่านจนจบจะพอเข้าใจ

  • กระบวนการทำงานของระบบที่ใช้ NN คร่าวๆ (หาข้อมูล, normalize, สร้างแบบจำลอง, train, test)
  • การใช้ Convolution layer ทั้งกับงาน image และงานอื่น
  • การใช้ Transpose convolution layer
  • ResNet

Have Fun :)

--

--

Responses (2)