Transfer learning với keras

classification
deep-learning
tutorial
transfer-learning
implementation

#1

Nhân bài viết về transfer learning của anh Thướng, em xin phép giới thiệu cách transfer learning model Mobilenet để phân loại các loại hoa (data sử dụng từ bài tutorial tensorflow nổi tiếng của Google. Bài viết này sử dụng Google Colab để thực hiện.

Link Colab: https://colab.research.google.com/drive/1--X4u7fylUy_dG51iYTU6KoLNb6E9KsF

Setting up Google Colab

Kết nối google drive, sử dụng google drive để lưu dữ liệu, model…

from google.colab import drive
drive.mount('/content/drive')

Tạo folder, download data, extract.

import os
os.chdir("drive/My Drive/")
!mkdir transfer_learning_with_keras
os.chdir("transfer_learning_with_keras")
#download data and extract
!wget http://download.tensorflow.org/example_images/flower_photos.tgz
!mkdir tf_file
!tar -xvf flower_photos.tgz -C tf_file

Preparing model

Trong bài này, ta sử dụng pretrained model MobileNet. Ở trong bài toán transfer learning, ta sẽ giữ nguyên các lớp Convolution và chỉ train các lớp Fully connected ở cuối.

#import needed packages
import cv2
import numpy as np
import tensorflow as tf
import keras
from keras import backend as K
from keras.optimizers import Adam
from keras.metrics import categorical_crossentropy
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Model
from keras.layers import Dense,GlobalAveragePooling2D,Dropout,SeparableConv2D,BatchNormalization, Activation, Dense
from keras.applications.mobilenet import MobileNet
from keras.optimizers import Adam

Dữ liệu trong dataset bao gồm 5 class: roses, sunflowers, tulips, daisy, dandelion. Khi load pretrained model, tham số include_top = False để không bao gồm các lớp Fully connected ở cuối, weights = 'imagenet' để load pretrained model train trên tập imagenet, input_shape = (224,224,3) là kích thước ảnh đầu vào.

Sau khi load xong Mobile pretrained model, ta thêm vào cuối vài lớp Fully connected, lớp cuối cùng có số unit bằng số class mà mình muốn classify, activation là softmax.

# dataset has 5 classes
num_class = 5

# Base model without Fully connected Layers
base_model = MobileNet(include_top=False, weights='imagenet', input_shape=(224,224,3))
x=base_model.output
# Add some new Fully connected layers to 
x=GlobalAveragePooling2D()(x)
x=Dense(1024,activation='relu')(x)
x = Dropout(0.25)(x)
x=Dense(512,activation='relu')(x) 
x = Dropout(0.25)(x)
preds=Dense(num_class, activation='softmax')(x) #final layer with softmax activation

model=Model(inputs=base_model.input,outputs=preds)

Sau khi load xong có thể kiểm tra model.

model.summary()
...
conv_pw_13 (Conv2D)          (None, 7, 7, 1024)        1048576   
_________________________________________________________________
conv_pw_13_bn (BatchNormaliz (None, 7, 7, 1024)        4096      
_________________________________________________________________
conv_pw_13_relu (ReLU)       (None, 7, 7, 1024)        0         
_________________________________________________________________
global_average_pooling2d_3 ( (None, 1024)              0         
_________________________________________________________________
dense_6 (Dense)              (None, 1024)              1049600   
_________________________________________________________________
dropout_5 (Dropout)          (None, 1024)              0         
_________________________________________________________________
dense_7 (Dense)              (None, 512)               524800    
_________________________________________________________________
dropout_6 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_8 (Dense)              (None, 5)                 2565      
=================================================================
Total params: 4,805,829
Trainable params: 4,783,941
Non-trainable params: 21,888

Đóng băng các lớp convolution trở về trước để chỉ train các lớp fully connected vừa thêm vào

for i,layer in enumerate(model.layers):
  print("{}: {}".format(i,layer))
...
82: <keras.layers.normalization.BatchNormalization object at 0x7f5910480dd8>
83: <keras.layers.advanced_activations.ReLU object at 0x7f5910423a90>
84: <keras.layers.convolutional.Conv2D object at 0x7f59103eb7b8>
85: <keras.layers.normalization.BatchNormalization object at 0x7f5910380c18>
86: <keras.layers.advanced_activations.ReLU object at 0x7f59103b3908>
87: <keras.layers.pooling.GlobalAveragePooling2D object at 0x7f5911abbdd8>
88: <keras.layers.core.Dense object at 0x7f5911abbd30>
89: <keras.layers.core.Dropout object at 0x7f5910298748>
90: <keras.layers.core.Dense object at 0x7f590de4d630>
91: <keras.layers.core.Dropout object at 0x7f590de1d278>
92: <keras.layers.core.Dense object at 0x7f590deeaf60>

Ta thấy các lớp vừa thêm vào là từ vị trí 87 trở đi, nên ta đóng băng từ layer 87 về trước.

for layer in model.layers[:87]:
    layer.trainable=False
for layer in model.layers[87:]:
    layer.trainable=True

Preparing data

Ta sử dụng ImageDataGenerator từ keras. Chia tỉ lệ train-val là 75-25.

train_datagen=ImageDataGenerator(preprocessing_function=keras.applications.mobilenet.preprocess_input,
                                 validation_split=0.25)

train_generator=train_datagen.flow_from_directory('tf_file/flower_photos/',
                                                 target_size=(224,224),
                                                 batch_size=64,
                                                 class_mode='categorical',
                                                 subset='training')


validation_generator = train_datagen.flow_from_directory(
                                                'tf_file/flower_photos/', # same directory as training data
                                                target_size=(224,224),
                                                batch_size=64,
                                                class_mode='categorical',
                                                subset='validation') # set as validation data
Found 2755 images belonging to 5 classes.
Found 915 images belonging to 5 classes.

Training

Set hyper parameter.

epochs = 50
learning_rate = 0.0005
decay_rate = learning_rate / epochs
opt = Adam(lr=learning_rate, beta_1=0.9, beta_2=0.999, epsilon=None, decay=decay_rate, amsgrad=False)
model.compile(optimizer=opt,loss='categorical_crossentropy',metrics=['accuracy'])

Set callback để lưu model và tensorboard.

from keras.callbacks import ModelCheckpoint
from keras.callbacks import TensorBoard

!mkdir ckpt
!mkdir logs

filepath="ckpt/best.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_weights_only = False, save_best_only=True, mode='min')
logdir="logs/mobilenet"
tfboard = TensorBoard(log_dir=logdir)

callbacks_list = [checkpoint, tfboard]

Train

step_size_train = train_generator.n/train_generator.batch_size
step_size_val = validation_generator.samples // validation_generator.batch_size
history = model.fit_generator(generator=train_generator,
                   steps_per_epoch=step_size_train,
                   validation_data = validation_generator, 
                   validation_steps =step_size_val,
                   callbacks = callbacks_list,
                   epochs=10)
...

Epoch 00006: val_acc did not improve from 0.80747
Epoch 7/10
87/86 [==============================] - 15s 170ms/step - loss: 0.1841 - acc: 0.9432 - val_loss: 0.6885 - val_acc: 0.8279

Epoch 00007: val_acc improved from 0.80747 to 0.82786, saving model to ckpt/best.hdf5
Epoch 8/10
87/86 [==============================] - 15s 172ms/step - loss: 0.2004 - acc: 0.9317 - val_loss: 0.7222 - val_acc: 0.8075

Epoch 00008: val_acc did not improve from 0.82786
Epoch 9/10
87/86 [==============================] - 15s 171ms/step - loss: 0.1713 - acc: 0.9379 - val_loss: 0.8382 - val_acc: 0.7916

Epoch 00009: val_acc did not improve from 0.82786
Epoch 10/10
87/86 [==============================] - 15s 170ms/step - loss: 0.1288 - acc: 0.9535 - val_loss: 1.0021 - val_acc: 0.7678

Epoch 00010: val_acc did not improve from 0.82786

Model tốt nhất tính theo val_acc sẽ được lưu lại. Trong ví dụ này, model tốt nhất đạt 82.78% accuracy trên tập val.

Inference

Load lại model tốt nhất để inference.

inf_model = keras.models.load_model("ckpt/best.hdf5")

Preprocess cho ảnh đầu vào.

def preprocess_image(img):
        if (img.shape[0] != 224 or img.shape[1] != 224):
            img = cv2.resize(img, (224, 224), interpolation=cv2.INTER_NEAREST)
        img = (img/127.5)
        img = img - 1
        img = np.expand_dims(img, axis=0)
        return img
classes = train_generator.class_indices
classes = list(classes.keys())

Ta lấy tạm ảnh trong dataset để test. Nên dùng ảnh bất kì để test.

import glob
files = glob.glob("tf_file/flower_photos/dandelion/*.jpg") # lấy ảnh trong folder dandelion để test

inference

img = cv2.imread(files[0])
pred = inf_model.predict(preprocess_image(img))
result = classes[np.argmax(pred)]
print(result)                     
'dandelion'

Nhận xét

Ở đây ta thấy có xảy ra tình trạng overfit khi val loss không giảm khi train loss vẫn giảm. Điều này có thể thấy rõ hơn khi xem lại trên Tensor board.

Để giải quyết vấn đề này, có thể thử 1 số cách sau:

  • Thay vì đóng băng hoàn toàn phần base model (từ lớp 87 trở về trước), đóng băng ít hơn (ví dụ 70). Con số này cần phải thực nghiệm.
  • Thêm các bước augmentation trong ImageDataGenerator, xem thêm ví dụ ở https://keras.io/preprocessing/image/.
  • Thay đổi các tham số trong phần hyper parameter.

Bonus

Freeze keras model (.hdf5) để tạo tensorflow model (.pb)

K.set_learning_phase(0)

print(model.outputs)
print(model.inputs)

def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True):
    from tensorflow.python.framework.graph_util import convert_variables_to_constants
    graph = session.graph
    with graph.as_default():
        freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))
        output_names = output_names or []
        output_names += [v.op.name for v in tf.global_variables()]
        # Graph -> GraphDef ProtoBuf
        input_graph_def = graph.as_graph_def()
        if clear_devices:
            for node in input_graph_def.node:
                node.device = ""
        frozen_graph = convert_variables_to_constants(session, input_graph_def,
                                                      output_names, freeze_var_names)
        return frozen_graph

frozen_graph = freeze_session(K.get_session(),
                              output_names=[out.op.name for out in model.outputs])
                              
tf.train.write_graph(frozen_graph, "model", "model.pb", as_text=False)

#2

Thanks em bài hướng dẫn hand-on rất chi tiết. Anh chỉ thắc mắc chút là nếu em thực hiện bước tiếp theo là để learning rate rất nhỏ cho cả mạng hiện tại thì chất lượng như thế nào.

Bởi vì finetuning có thể thực hiện theo hai hoặc nhiều bước Cách 1.

  • Bước 1. Fine tuning một số layer cuối. --> cái này em đã trình bày ở bài
  • Bước 2. Finetuning tất cả các layer ở learning rate thấp

Cách 2.

  • Fine tuning tất cả layer ngay từ đầu. Learning rate giữa layer gốc của MobileNet có thể khác, nhỏ hơn learning rate của mấy FCL cuối.

#3

Em có làm bài toán thực tế theo bước 1 cách 1 và đạt được kết quả tốt hơn sau khi thử đóng băng ở các lớp khác nhau, bước 2 em chưa thử nhưng em sẽ thử vì bước đó có vẻ khá hay. Còn cách 2 thì khi em làm rất khó để fit, thậm chí là không fit được.


#4

thanks member. bài viết khá hay. có 1 cái nhìn tổng quan cho tiếp cận transfer learning khi kết hợp bài viết tổng quan (ở link đã nêu trong bài).


#5

Mình đạt độ chính xác sau khi thay đổi một số tham số và mô hình

base_model = applications.MobileNet(weights=‘imagenet’, include_top=False, input_shape=(img_rows, img_cols, channel))

x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(1024, activation=‘relu’)(x) # we add dense layers so that the model can learn more complex functions and classify for better results. x = Dropout(0.25)(x) x = Dense(1024, activation=‘relu’)(x) # dense layer 2 x = Dropout(0.25)(x) x = Dense(512, activation=‘relu’)(x) # dense layer 3 predictions = Dense(num_classes, activation=‘softmax’)(x) # final layer with softmax activation

model = Model(inputs=base_model.input, outputs=predictions)

# Uncomment below to set the first 10 layers to non-trainable (weights will not be updated) for layer in base_model.layers[:10]: layer.trainable = False

# Learning rate is changed to 0.001 sgd = SGD(lr=1e-3, decay=1e-6, momentum=0.9, nesterov=True) model.compile(optimizer=sgd, loss=‘categorical_crossentropy’, metrics=[‘accuracy’])

2592/2752 [===========================>…] - ETA: 0s - loss: 0.0394 - acc: 0.9888 2624/2752 [===========================>…] - ETA: 0s - loss: 0.0403 - acc: 0.9886 2656/2752 [===========================>…] - ETA: 0s - loss: 0.0402 - acc: 0.9887 2688/2752 [============================>.] - ETA: 0s - loss: 0.0399 - acc: 0.9888 2720/2752 [============================>.] - ETA: 0s - loss: 0.0395 - acc: 0.9890 2752/2752 [==============================] - 9s 3ms/step - loss: 0.0393 - acc: 0.9891 - val_loss: 0.2766 - val_acc: 0.9172 Epoch 10/50

Và cuối cùng là Test data loss: 0.3696211445224441 Test data accuracy: 0.9270152505446623


#6

Trước hết cảm ơn bạn vì bài tutorial !

Cho mình hỏi phần Inference, bước tiền xử lý input có tham số interpolation, bạn có thể giải thích giúp mình được k? Mình cũng không hiểu sao lại chia img cho 127.5 rồi - 1 cũng như expand_dims làm gì? Bước này có gì khác so với /255 vậy?


#7

interpolation là tham số khi resize ảnh của opencv. Một vài loại interpolation khác nhau có thể tham khảo chi tiết ở https://docs.opencv.org/trunk/da/d6e/tutorial_py_geometric_transformations.html

vì phần training mình sử dụng hàm preprocessing của keras:

preprocessing_function=keras.applications.mobilenet.preprocess_input

nên lúc inference mình mô phỏng lại hàm đó để preprocess ảnh input.

xem thêm ở https://github.com/keras-team/keras-applications/blob/master/keras_applications/imagenet_utils.py dòng 42-45

    if mode == 'tf':
        x /= 127.5
        x -= 1.
        return x

Còn expand_dims là thêm chiều cho input vd: 224,224,3 thành 1,224,224,3. Vì input của mạng là 1 mảng có thể chứa nhiều ảnh nên phải làm như vậy.


#8

Cảm ơn bạn câu trả lời rất đầy đủ !


#9

Bạn có thể hướng dẫn mình chỗ ImageDataGenerator với dữ liệu dạng pickle như cifar10 được không. Mình làm mãi chưa được :frowning:.

Mình bê nguyên cái model của bạn để thử với cifa10 nhưng lại bị overfit. Có phải là do cifa10 ảnh bé quá không nhỉ .

code đây có thời gian bạn xem giúp mình với. Mình cảm ơn !


#10

Mình không đọc dữ liệu từ pickle mà dùng cifar10 từ keras.datasets và làm theo hướng dẫn của keras https://keras.io/examples/cifar10_cnn/, chỉ thay phần model bằng Mobilenet và đạt được 79% accuracy trên tập val sau 10 epoch, còn tốt hơn trong ví dụ của keras.

Epoch 10/100
1563/1563 [==============================] - 89s 57ms/step - loss: 0.7910 - acc: 0.7658 - val_loss: 0.7061 - val_acc: 0.7900

#11

Bạn sử dụng transfer learning hay là build lại model theo kiến trúc Mobilenet. Có thể cho mình xin code được không?

Mình đang muốn lấy weight của Mobilenet đc train trên imagenet để transfer cho cifar10 model. Nhưng kẹt chỗ ImageDataGenerator có sử dụng preprocessing_function nên mình không biết làm thế nào.

Với lại lúc đầu mình nghĩ cái base model không có weight cho ảnh 32x32, mình tưởng là phải UpSampling2D lên nhưng sau thử để input 32x32x3 vẫn được.

Không biết mình có hiểu sai chỗ nào không, liệu cái input_shape ấy để size nào cũng được hả bạn, nếu không được thì mình muốn thêm 2 layer UpSampling2D để scale lên 128x128 thì có làm được không, làm như thế nào?

Cảm ơn bạn, giúp đỡ tận tình quá !


#12

Mình thay phần model bằng Mobilenet nhưng để weight là None thì qua 10 epochs đạt acc đc 50%. Nhưng khi dùng weight imagenet thì bị overfit ngay epoch đầu tiên.

Epoch 1/10
1563/1562 [==============================] - 28s 18ms/step - loss: 1.9331 - acc: 0.3278 - val_loss: 2.2987 - val_acc: 0.0974
Epoch 2/10
1563/1562 [==============================] - 24s 15ms/step - loss: 1.6711 - acc: 0.4153 - val_loss: 2.3002 - val_acc: 0.1014
Epoch 3/10
1563/1562 [==============================] - 23s 15ms/step - loss: 1.6081 - acc: 0.4355 - val_loss: 2.2990 - val_acc: 0.1017

không hiểu sao mình thêm cả regularization và tăng hệ số dropout mà vẫn bị overfit :frowning:


#13

Về vấn đề overfitting hiện tại mình vẫn chưa trả lời được nhưng về UpSampling thì mình đã tìm được câu trả lời. Mình xin đăng lên đây dành ai thắc mắc giống mình.

Dưới đây là code model cho CIFAR10 sử dụng transfer learning từ model MobileNet với weight đc train từ imagenet.

Dữ liệu là ảnh (32x32x3). tập train có 50000 ảnh và test có 10000 của 10 classes.

Mình chưa biết nguyên nhân tại sao nhưng mình thử lấy dùng MobileNet với input_shape là 32x32x3 thì mình bị overfitting. Nên mình quyết định upsample lên 128x128x3.

Cho vào MobileNet extract features sau đó đưa vào 1 FC với 512 nodes.

Code dưới đây cũng dùng Google Colab để tiện download data, weight cũng như train nhanh hơn.

Import needed packages

from __future__ import print_function
import keras
from keras.datasets import cifar10
from keras.models import Sequential
from keras.layers import Dense, Dropout, UpSampling2D, GlobalAveragePooling2D
from keras.applications.mobilenet import MobileNet

Load data and convert label to one-hot

# The data, split between train and test sets:
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')
# Convert class vectors to binary class matrices.
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

Model

base_model = MobileNet(include_top=False, weights='imagenet', input_shape=(128,128,3))
model = Sequential()
model.add(UpSampling2D((2,2)))
model.add(UpSampling2D((2,2)))
model.add(base_model)
model.add(GlobalAveragePooling2D())
model.add(Dense(512,activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
for i,layer in enumerate(model.layers):
  print("{}: {}".format(i,layer)

0: <keras.layers.convolutional.UpSampling2D object at 0x7fad69d9b550>
1: <keras.layers.convolutional.UpSampling2D object at 0x7fad69d9b5f8>
2: <keras.engine.training.Model object at 0x7fad6a28f9b0>
3: <keras.layers.pooling.GlobalAveragePooling2D object at 0x7fad69d9b5c0>
4: <keras.layers.core.Dense object at 0x7fad69d9b6d8>
5: <keras.layers.core.Dropout object at 0x7fad69d9b630>
6: <keras.layers.core.Dense object at 0x7fad69d9b898>

model.layers[3].trainable=False
# initiate RMSprop optimizer
opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)

model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

Process Data

model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

Train

print('Not using data augmentation.')
model.fit(x_train, y_train,
              batch_size=batch_size,
              epochs=epochs,
              validation_data=(x_test, y_test),
              shuffle=True)
Epoch 1/1
50000/50000 [==============================] - 200s 4ms/step - loss: 0.4859 - acc: 0.8399 - val_loss: 0.2351 - val_acc: 0.9242

Có một vấn đề mình thắc mắc nữa là ở đây mình không xử lý dữ data theo cách mà MobileNet xử lý tuy nhiên vẫn đạt kết quả rất cao ở ngay epoch đầu tiên.