前言

在深度學習訓練中,例如圖像識別訓練,每次從零開始訓練都要消耗大量的時間和資源。而且當數據集比較少時,模型也難以擬合的情況。基於這種情況下,就出現了遷移學習,通過使用已經訓練好的模型來初始化即將訓練的網絡,可以加快模型的收斂速度,而且還能提高模型的準確率。這個用於初始化訓練網絡的模型是使用大型數據集訓練得到的一個模型,而且模型已經完全收斂。最好訓練的模型和預訓練的模型是同一個網絡,這樣可以最大限度地初始化全部層。

初步訓練模型

本章使用的預訓練模型是PaddlePaddle官方提供的ResNet50網絡模型,訓練的數據集是ImageNet,它的下載地址爲:http://paddle-imagenet-models-name.bj.bcebos.com/ResNet50_pretrained.zip ,讀者可以下載其他更多的模型,可以在這裏下載。下載之後解壓到models目錄下。

編寫一個pretrain_model.py的Python程序,用於初步訓練模型。首先導入相關的依賴包。

import os
import shutil
import paddle as paddle
import paddle.dataset.flowers as flowers
import paddle.fluid as fluid
from paddle.fluid.param_attr import ParamAttr

定義一個殘差神經網絡,這個網絡是PaddlePaddle官方提供的,模型地址爲models_name。這個網絡是在每一個層都由指定參數名字,這是爲了方便初始化網絡模型,如果網絡的結構發生變化了,但是名字沒有變化,之後使用預訓練模型初始化時,就可以根據每個參數的名字初始化對應的層。

# 定義殘差神經網絡(ResNet)
def resnet50(input):
    def conv_bn_layer(input, num_filters, filter_size, stride=1, groups=1, act=None, name=None):
        conv = fluid.layers.conv2d(input=input,
                                   num_filters=num_filters,
                                   filter_size=filter_size,
                                   stride=stride,
                                   padding=(filter_size - 1) // 2,
                                   groups=groups,
                                   act=None,
                                   param_attr=ParamAttr(name=name + "_weights"),
                                   bias_attr=False,
                                   name=name + '.conv2d.output.1')
        if name == "conv1":
            bn_name = "bn_" + name
        else:
            bn_name = "bn" + name[3:]
        return fluid.layers.batch_norm(input=conv,
                                       act=act,
                                       name=bn_name + '.output.1',
                                       param_attr=ParamAttr(name=bn_name + '_scale'),
                                       bias_attr=ParamAttr(bn_name + '_offset'),
                                       moving_mean_name=bn_name + '_mean',
                                       moving_variance_name=bn_name + '_variance', )

    def shortcut(input, ch_out, stride, name):
        ch_in = input.shape[1]
        if ch_in != ch_out or stride != 1:
            return conv_bn_layer(input, ch_out, 1, stride, name=name)
        else:
            return input

    def bottleneck_block(input, num_filters, stride, name):
        conv0 = conv_bn_layer(input=input,
                              num_filters=num_filters,
                              filter_size=1,
                              act='relu',
                              name=name + "_branch2a")
        conv1 = conv_bn_layer(input=conv0,
                              num_filters=num_filters,
                              filter_size=3,
                              stride=stride,
                              act='relu',
                              name=name + "_branch2b")
        conv2 = conv_bn_layer(input=conv1,
                              num_filters=num_filters * 4,
                              filter_size=1,
                              act=None,
                              name=name + "_branch2c")

        short = shortcut(input, num_filters * 4, stride, name=name + "_branch1")

        return fluid.layers.elementwise_add(x=short, y=conv2, act='relu', name=name + ".add.output.5")

    depth = [3, 4, 6, 3]
    num_filters = [64, 128, 256, 512]

    conv = conv_bn_layer(input=input, num_filters=64, filter_size=7, stride=2, act='relu', name="conv1")
    conv = fluid.layers.pool2d(input=conv, pool_size=3, pool_stride=2, pool_padding=1, pool_type='max')

    for block in range(len(depth)):
        for i in range(depth[block]):
            conv_name = "res" + str(block + 2) + chr(97 + i)
            conv = bottleneck_block(input=conv,
                                    num_filters=num_filters[block],
                                    stride=2 if i == 0 and block != 0 else 1,
                                    name=conv_name)

    pool = fluid.layers.pool2d(input=conv, pool_size=7, pool_type='avg', global_pooling=True)
    return pool

定義圖片數據和標籤數據的輸入層,本章使用的圖片數據集是flowers。這個通過使用PaddlePaddle的接口得到的flowers數據集的圖片是3通道寬高都是224的彩色圖,總類別是102種。

# 定義輸入層
image = fluid.layers.data(name='image', shape=[3, 224, 224], dtype='float32')
label = fluid.layers.data(name='label', shape=[1], dtype='int64')

獲取一個基本的模型,並從主程序中克隆一個基本的程序,用於之後加載參數使用。

# 獲取分類器的上一層
pool = resnet50(image)
# 停止梯度下降
pool.stop_gradient = True
# 由這裏創建一個基本的主程序
base_model_program = fluid.default_main_program().clone()

這裏再加上網絡的分類器,因爲預訓練模型的類別數量是1000,所以要重新修改分類器。這個也是訓練新模型的最大不同點,通過分離分類器來解決兩個數據集的不同類別的問題。

# 這裏再重新加載網絡的分類器,大小爲本項目的分類大小
model = fluid.layers.fc(input=pool, size=102, act='softmax')

然後是獲取損失函數,準確率函數和優化方法。

# 獲取損失函數和準確率函數
cost = fluid.layers.cross_entropy(input=model, label=label)
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=model, label=label)

# 定義優化方法
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=1e-3)
opts = optimizer.minimize(avg_cost)

獲取flowers數據集,因爲這裏不需要使用測試,所以這裏也不需要讀取測試數據集。

# 獲取flowers數據
train_reader = paddle.batch(flowers.train(), batch_size=16)

創建執行器,最好是使用GPU進行訓練,因爲數據集和網絡都是比較大的。

# 定義一個使用CPU的執行器
place = fluid.CUDAPlace(0)
# place = fluid.CPUPlace()
exe = fluid.Executor(place)
# 進行參數初始化
exe.run(fluid.default_startup_program())

這裏就是加載預訓練模型的重點,通過if_exist函數判斷網絡所需的模型文件是否存在,然後再通過調用fluid.io.load_vars加載存在的模型文件。要留意的是這裏使用的是之前克隆的基本程序。

# 官方提供的原預訓練模型
src_pretrain_model_path = 'models/ResNet50_pretrained/'


# 通過這個函數判斷模型文件是否存在
def if_exist(var):
    path = os.path.join(src_pretrain_model_path, var.name)
    exist = os.path.exists(path)
    if exist:
        print('Load model: %s' % path)
    return exist


# 加載模型文件,只加載存在模型的模型文件
fluid.io.load_vars(executor=exe, dirname=src_pretrain_model_path, predicate=if_exist, main_program=base_model_program)

然後使用這個預訓練模型進行訓練10個Pass。

# 優化內存
optimized = fluid.transpiler.memory_optimize(input_program=fluid.default_main_program(), print_log=False)

# 定義輸入數據維度
feeder = fluid.DataFeeder(place=place, feed_list=[image, label])

# 訓練10次
for pass_id in range(10):
    # 進行訓練
    for batch_id, data in enumerate(train_reader()):
        train_cost, train_acc = exe.run(program=fluid.default_main_program(),
                                        feed=feeder.feed(data),
                                        fetch_list=[avg_cost, acc])
        # 每100個batch打印一次信息
        if batch_id % 100 == 0:
            print('Pass:%d, Batch:%d, Cost:%0.5f, Accuracy:%0.5f' %
                  (pass_id, batch_id, train_cost[0], train_acc[0]))

執行訓練輸出的信息:

Load model: models/ResNet50_pretrained/res5a_branch2a_weights
Load model: models/ResNet50_pretrained/res4c_branch2a_weights
Load model: models/ResNet50_pretrained/res4f_branch2b_weights
Load model: models/ResNet50_pretrained/bn2a_branch2b_variance
Load model: models/ResNet50_pretrained/bn4d_branch2b_variance
Load model: models/ResNet50_pretrained/bn4f_branch2b_variance
Load model: models/ResNet50_pretrained/bn4e_branch2a_offset
Load model: models/ResNet50_pretrained/res4f_branch2c_weights
Load model: models/ResNet50_pretrained/res5c_branch2b_weights
......
Pass:0, Batch:0, Cost:6.92118, Accuracy:0.00000
Pass:0, Batch:100, Cost:3.31085, Accuracy:0.31250
Pass:0, Batch:200, Cost:3.32227, Accuracy:0.18750
Pass:0, Batch:300, Cost:3.85708, Accuracy:0.31250
Pass:1, Batch:0, Cost:3.36264, Accuracy:0.25000
......

訓練結束之後,使用fluid.io.save_params接口保存參數,這個是已經符合這個數據集類別數量的,所以之後會使用都這個模型直接初始化模型,不需要再分離分類器。

# 保存參數模型
save_pretrain_model_path = 'models/pretrain_model/'
# 刪除舊的模型文件
shutil.rmtree(save_pretrain_model_path, ignore_errors=True)
# 創建保持模型文件目錄
os.makedirs(save_pretrain_model_path)
# 保存參數模型
fluid.io.save_params(executor=exe, dirname=save_pretrain_model_path)

到這裏預訓練的第一步處理原預訓練模型算是完成了,接下來就是使用這個已經處理過的模型正式訓練了。

使用過的模型開始正式訓練

這一部分是使用已經處理過的模型開始正式訓練,創建一個train.py正式訓練。首先導入相關的依賴包。

import os
import shutil
import paddle as paddle
import paddle.dataset.flowers as flowers
import paddle.fluid as fluid
from paddle.fluid.param_attr import ParamAttr

定義一個殘差神經網絡,這個殘差神經網絡跟上面的基本一樣的,只是把分類器也加進去了,這是一個完整的神經網絡。

# 定義殘差神經網絡(ResNet)
def resnet50(input, class_dim):
    def conv_bn_layer(input, num_filters, filter_size, stride=1, groups=1, act=None, name=None):
        conv = fluid.layers.conv2d(input=input,
                                   num_filters=num_filters,
                                   filter_size=filter_size,
                                   stride=stride,
                                   padding=(filter_size - 1) // 2,
                                   groups=groups,
                                   act=None,
                                   param_attr=ParamAttr(name=name + "_weights"),
                                   bias_attr=False,
                                   name=name + '.conv2d.output.1')
        if name == "conv1":
            bn_name = "bn_" + name
        else:
            bn_name = "bn" + name[3:]
        return fluid.layers.batch_norm(input=conv,
                                       act=act,
                                       name=bn_name + '.output.1',
                                       param_attr=ParamAttr(name=bn_name + '_scale'),
                                       bias_attr=ParamAttr(bn_name + '_offset'),
                                       moving_mean_name=bn_name + '_mean',
                                       moving_variance_name=bn_name + '_variance', )

    def shortcut(input, ch_out, stride, name):
        ch_in = input.shape[1]
        if ch_in != ch_out or stride != 1:
            return conv_bn_layer(input, ch_out, 1, stride, name=name)
        else:
            return input

    def bottleneck_block(input, num_filters, stride, name):
        conv0 = conv_bn_layer(input=input,
                              num_filters=num_filters,
                              filter_size=1,
                              act='relu',
                              name=name + "_branch2a")
        conv1 = conv_bn_layer(input=conv0,
                              num_filters=num_filters,
                              filter_size=3,
                              stride=stride,
                              act='relu',
                              name=name + "_branch2b")
        conv2 = conv_bn_layer(input=conv1,
                              num_filters=num_filters * 4,
                              filter_size=1,
                              act=None,
                              name=name + "_branch2c")

        short = shortcut(input, num_filters * 4, stride, name=name + "_branch1")

        return fluid.layers.elementwise_add(x=short, y=conv2, act='relu', name=name + ".add.output.5")

    depth = [3, 4, 6, 3]
    num_filters = [64, 128, 256, 512]

    conv = conv_bn_layer(input=input, num_filters=64, filter_size=7, stride=2, act='relu', name="conv1")
    conv = fluid.layers.pool2d(input=conv, pool_size=3, pool_stride=2, pool_padding=1, pool_type='max')

    for block in range(len(depth)):
        for i in range(depth[block]):
            conv_name = "res" + str(block + 2) + chr(97 + i)
            conv = bottleneck_block(input=conv,
                                    num_filters=num_filters[block],
                                    stride=2 if i == 0 and block != 0 else 1,
                                    name=conv_name)

    pool = fluid.layers.pool2d(input=conv, pool_size=7, pool_type='avg', global_pooling=True)
    output = fluid.layers.fc(input=pool, size=class_dim, act='softmax')
    return output

然後定義一系列所需的函數,輸入層,神經網絡的分類器,損失函數,準確率函數,優化方法,獲取flowers訓練數據和測試數據,並創建一個執行器。

# 定義輸入層
image = fluid.layers.data(name='image', shape=[3, 224, 224], dtype='float32')
label = fluid.layers.data(name='label', shape=[1], dtype='int64')

# 獲取分類器
model = resnet50(image, 102)

# 獲取損失函數和準確率函數
cost = fluid.layers.cross_entropy(input=model, label=label)
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=model, label=label)

# 獲取訓練和測試程序
test_program = fluid.default_main_program().clone(for_test=True)

# 定義優化方法
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=1e-3)
opts = optimizer.minimize(avg_cost)

# 獲取MNIST數據
train_reader = paddle.batch(flowers.train(), batch_size=16)
test_reader = paddle.batch(flowers.test(), batch_size=16)

# 定義一個使用GPU的執行器
place = fluid.CUDAPlace(0)
# place = fluid.CPUPlace()
exe = fluid.Executor(place)
# 進行參數初始化
exe.run(fluid.default_startup_program())

這裏可以使用fluid.io.load_params接口加載已經處理過的預訓練模型文件。

# 經過處理的預訓練預訓練模型
pretrained_model_path = 'models/pretrain_model/'

# 加載經過處理的模型
fluid.io.load_params(executor=exe, dirname=pretrained_model_path)

之後就可以正常訓練了,從訓練輸出的日誌可以看出,模型收斂得非常快,而且準確率還非常高,如果沒有使用預訓練模型是很難達到這種準確率的。

# 定義輸入數據維度
feeder = fluid.DataFeeder(place=place, feed_list=[image, label])

# 訓練10次
for pass_id in range(10):
    # 進行訓練
    for batch_id, data in enumerate(train_reader()):
        train_cost, train_acc = exe.run(program=fluid.default_main_program(),
                                        feed=feeder.feed(data),
                                        fetch_list=[avg_cost, acc])
        # 每100個batch打印一次信息
        if batch_id % 100 == 0:
            print('Pass:%d, Batch:%d, Cost:%0.5f, Accuracy:%0.5f' %
                  (pass_id, batch_id, train_cost[0], train_acc[0]))

    # 進行測試
    test_accs = []
    test_costs = []
    for batch_id, data in enumerate(test_reader()):
        test_cost, test_acc = exe.run(program=test_program,
                                      feed=feeder.feed(data),
                                      fetch_list=[avg_cost, acc])
        test_accs.append(test_acc[0])
        test_costs.append(test_cost[0])
    # 求測試結果的平均值
    test_cost = (sum(test_costs) / len(test_costs))
    test_acc = (sum(test_accs) / len(test_accs))
    print('Test:%d, Cost:%0.5f, Accuracy:%0.5f' % (pass_id, test_cost, test_acc))

執行訓練輸出的信息:

Pass:0, Batch:0, Cost:0.11896, Accuracy:1.00000
Pass:0, Batch:100, Cost:1.73780, Accuracy:0.68750
Pass:0, Batch:200, Cost:1.32758, Accuracy:0.68750
Pass:0, Batch:300, Cost:1.56638, Accuracy:0.56250
Test:0, Cost:1.82441, Accuracy:0.53841
Pass:1, Batch:0, Cost:0.71874, Accuracy:0.87500
......

訓練結束之後,可以保存預測模型用於之後的預測使用。

# 保存預測模型
save_path = 'models/infer_model/'
# 刪除舊的模型文件
shutil.rmtree(save_path, ignore_errors=True)
# 創建保持模型文件目錄
os.makedirs(save_path)
# 保存預測模型
fluid.io.save_inference_model(save_path, feeded_var_names=[image.name], target_vars=[model], executor=exe)

同步到百度AI Studio平臺:http://aistudio.baidu.com/aistudio/projectdetail/38853
同步到科賽網K-Lab平臺:https://www.kesci.com/home/project/5c3f495589f4aa002b845d6b
項目代碼GitHub地址:https://github.com/yeyupiaoling/LearnPaddle2/tree/master/note9

注意: 最新代碼以GitHub上的爲準


上一章:《PaddlePaddle從入門到煉丹》八——模型的保存與使用
下一章:《PaddlePaddle從入門到煉丹》十——VisualDL 訓練可視化


參考資料

  1. https://github.com/oraoto/learn_ml/blob/master/paddle/pretrained.ipynb
  2. http://www.paddlepaddle.org/documentation/docs/zh/1.2/api_cn/io_cn.html
小夜