前言

VisualDL是一個面向深度學習任務設計的可視化工具,包含了scalar、參數分佈、模型結構、圖像可視化等功能。可以這樣說:“所見即所得”。我們可以藉助VisualDL來觀察我們訓練的情況,方便我們對訓練的模型進行分析,改善模型的收斂情況。

  1. scalar,趨勢圖,可用於訓練測試誤差的展示
  2. image, 圖片的可視化,可用於卷積層或者其他參數的圖形化展示
  3. histogram, 用於參數分佈及變化趨勢的展示
  4. graph,用於訓練模型結構的可視化

    以上的圖像來自VisualDL的Github

既然那麼方便,那麼我們就來嘗試一下吧。VisualDL底層採用C++編寫,但是它在提供C++ SDK的同時,也支持Python SDK,我們主要是使用Python的SDK。順便說一下,VisualDL除了支持PaddlePaddle,之外,還支持pytorch, mxnet在內的大部分主流DNN平臺。

VisualDL的安裝

本章只講述在Ubuntu系統上的安裝和使用,Mac的操作應該也差不多。

使用pip安裝

使用pip安裝非常簡單,只要一條命令就夠了,如下:

pip3 install --upgrade visualdl

測試一下是否安裝成功了,運行一個例子下載日誌文件:

# 在當前位置下載一個日誌
vdl_create_scratch_log

然後再輸入,啓動VisualDL並加載這個日誌信息:

visualdl --logdir=scratch_log/ --port=8080

這裏說明一下,visualDL的參數:

  • host 設定IP
  • port 設定端口
  • model_pb 指定 ONNX 格式的模型文件,這木方我們還沒要用到

注意: 如果是報以下的錯誤,那是因爲protobuf版本過低的原因。

root@test:/home/test/VisualDL# visualdl --logdir ./scratch_log --port 8080
Traceback (most recent call last):
  File "/usr/local/bin/visualdl", line 29, in <module>
    import visualdl.server.graph as vdl_graph
  File "/usr/local/lib/python2.7/dist-packages/visualdl/server/graph.py", line 23, in <module>
    from . import onnx
  File "/usr/local/lib/python2.7/dist-packages/visualdl/server/onnx/__init__.py", line 8, in <module>
    from .onnx_pb2 import ModelProto
  File "/usr/local/lib/python2.7/dist-packages/visualdl/server/onnx/onnx_pb2.py", line 213, in <module>
    options=None, file=DESCRIPTOR),
TypeError: __init__() got an unexpected keyword argument 'file'

protobuf的版本要不小於3.5.0,如何小於這個版本可以使用以下命令升級:

pip3 install protobuf -U

然後在瀏覽器上輸入:

http://127.0.0.1:8080

即可看到一個可視化的界面,如下:

使用源碼安裝

如果讀者出於各種情況,使用pip安裝不能滿足需求,那可以考慮使用源碼安裝VisualDL,操作如下:
首先要安裝依賴庫:

# 安裝npm
apt install npm
# 安裝node
apt install nodejs-legacy
# 安裝cmake
apt install cmake
# 安裝unzip
apt install unzip

然後在GitHub上clone最新的源碼並打開:

git clone https://github.com/PaddlePaddle/VisualDL.git
cd VisualDL

之後是編譯生成whl安裝包:

python3 setup.py bdist_wheel

生成whl安裝包之後,就可以使用pip命令安裝這個安裝包了,*號對應的是visualdl版本號,讀者要根據實際情況來安裝:

pip3 install --upgrade dist/visualdl-*.whl

安裝完成之後,同樣可以使用在上一部分的使用pip安裝的測試方法測試安裝是否成功。

簡單使用VisualDL

我們編寫下面這一小段的代碼來學習VisualDL的使用,test_visualdl.py的代碼如下:

# 導入VisualDL的包
from visualdl import LogWriter

# 創建一個LogWriter,第一個參數是指定存放數據的路徑,
# 第二個參數是指定多少次寫操作執行一次內存到磁盤的數據持久化
logw = LogWriter("./random_log", sync_cycle=10000)

# 創建訓練和測試的scalar圖,
# mode是標註線條的名稱,
# scalar標註的是指定這個組件的tag
with logw.mode('train') as logger:
    scalar0 = logger.scalar("scratch/scalar")

with logw.mode('test') as logger:
    scalar1 = logger.scalar("scratch/scalar")

# 讀取數據
for step in range(1000):
    scalar0.add_record(step, step * 1. / 1000)
    scalar1.add_record(step, 1. - step * 1. / 1000)

運行Python代碼之後,在終端上輸入,從上面的代碼可以看到我們定義的路徑是./random_log

visualdl --logdir=random_log/ --port=8080

然後在瀏覽器上輸入:

http://127.0.0.1:8080

然後就可以看到剛纔編寫Python代碼生成的圖像了:

經過這個例子,讀者對VisualDL有了進一步的瞭解了,那麼在接下來的我們就在實際的PaddlePaddle例子中使用我們的VisualDL。

在PaddlePaddle使用VisualDL

下面就介紹在PaddlePaddle訓練中使用VisualDL,通過在訓練的時候使用VisualDL不斷收集訓練的數據集,最終通過可視化展示出來。

定義MobileNet V2神經網絡

創建一個mobilenet_v2.py來定義一個MobileNet V2神經網絡。MobileNet V2是MobileNet V1的升級版,從名字可以看出這個網絡是爲例移動設備而誕生的,它最大的特點就是模型小,預測速度快,適合部署在移動設備上。MobileNet V2是將MobileNet V1和殘差網絡ResNet的殘差單元結合起來,用Depthwise Convolutions代替殘差單元的bottleneck,最重要的是與residuals block相反,通常的residuals block是先經過1×1的卷積,降低feature map通道數,然後再通過3×3卷積,最後重新經過1×1卷積將feature map通道數擴張回去;而且爲了避免ReLU對特徵的破壞,用線性層替換channel數較少層後的ReLU非線性激活。

import paddle.fluid as fluid

def conv_bn_layer(input, filter_size, num_filters, stride, padding, num_groups=1, if_act=True, use_cudnn=True):
    conv = fluid.layers.conv2d(input=input,
                               num_filters=num_filters,
                               filter_size=filter_size,
                               stride=stride,
                               padding=padding,
                               groups=num_groups,
                               use_cudnn=use_cudnn,
                               bias_attr=False)
    bn = fluid.layers.batch_norm(input=conv)
    if if_act:
        return fluid.layers.relu6(bn)
    else:
        return bn


def shortcut(input, data_residual):
    return fluid.layers.elementwise_add(input, data_residual)


def inverted_residual_unit(input,
                           num_in_filter,
                           num_filters,
                           ifshortcut,
                           stride,
                           filter_size,
                           padding,
                           expansion_factor):
    num_expfilter = int(round(num_in_filter * expansion_factor))

    channel_expand = conv_bn_layer(input=input,
                                   num_filters=num_expfilter,
                                   filter_size=1,
                                   stride=1,
                                   padding=0,
                                   num_groups=1,
                                   if_act=True)

    bottleneck_conv = conv_bn_layer(input=channel_expand,
                                    num_filters=num_expfilter,
                                    filter_size=filter_size,
                                    stride=stride,
                                    padding=padding,
                                    num_groups=num_expfilter,
                                    if_act=True,
                                    use_cudnn=False)

    linear_out = conv_bn_layer(input=bottleneck_conv,
                               num_filters=num_filters,
                               filter_size=1,
                               stride=1,
                               padding=0,
                               num_groups=1,
                               if_act=False)
    if ifshortcut:
        out = shortcut(input=input, data_residual=linear_out)
        return out
    else:
        return linear_out

def invresi_blocks(input, in_c, t, c, n, s, name=None):
    first_block = inverted_residual_unit(input=input,
                                         num_in_filter=in_c,
                                         num_filters=c,
                                         ifshortcut=False,
                                         stride=s,
                                         filter_size=3,
                                         padding=1,
                                         expansion_factor=t)

    last_residual_block = first_block
    last_c = c

    for i in range(1, n):
        last_residual_block = inverted_residual_unit(input=last_residual_block,
                                                     num_in_filter=last_c,
                                                     num_filters=c,
                                                     ifshortcut=True,
                                                     stride=1,
                                                     filter_size=3,
                                                     padding=1,
                                                     expansion_factor=t)
    return last_residual_block


def net(input, class_dim, scale=1.0):
    bottleneck_params_list = [
        (1, 16, 1, 1),
        (6, 24, 2, 2),
        (6, 32, 3, 2),
        (6, 64, 4, 2),
        (6, 96, 3, 1),
        (6, 160, 3, 2),
        (6, 320, 1, 1),
    ]

    # conv1
    input = conv_bn_layer(input,
                          num_filters=int(32 * scale),
                          filter_size=3,
                          stride=2,
                          padding=1,
                          if_act=True)

    # bottleneck sequences
    i = 1
    in_c = int(32 * scale)
    for layer_setting in bottleneck_params_list:
        t, c, n, s = layer_setting
        i += 1
        input = invresi_blocks(input=input,
                               in_c=in_c,
                               t=t,
                               c=int(c * scale),
                               n=n,
                               s=s,
                               name='conv' + str(i))
        in_c = int(c * scale)
    # last_conv
    input = conv_bn_layer(input=input,
                          num_filters=int(1280 * scale) if scale > 1.0 else 1280,
                          filter_size=1,
                          stride=1,
                          padding=0,
                          if_act=True)

    feature = fluid.layers.pool2d(input=input,
                                  pool_size=7,
                                  pool_stride=1,
                                  pool_type='avg',
                                  global_pooling=True)
    net = fluid.layers.fc(input=feature,
                          size=class_dim,
                          act='softmax')
    return net

創建一個train.py開始訓練。首先導入相關的依賴包。

import paddle as paddle
import paddle.dataset.cifar as cifar
import paddle.fluid as fluid
import mobilenet_v2
from visualdl import LogWriter

創建VisualDL的記錄器,通過這個記錄器可以記錄每次訓練的數據,並存儲在log/目錄下。

# 創建記錄器
log_writer = LogWriter(dir='log/', sync_cycle=10)

# 創建訓練和測試記錄數據工具
with log_writer.mode('train') as writer:
    train_cost_writer = writer.scalar('cost')
    train_acc_writer = writer.scalar('accuracy')
    histogram = writer.histogram('histogram', num_buckets=50)

with log_writer.mode('test') as writer:
    test_cost_writer = writer.scalar('cost')
    test_acc_writer = writer.scalar('accuracy')

這裏是定義一系列的操作,如定義輸入層,獲取MobileNet V2的分類器,克隆預測程序,定義優化方法。

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

# 獲取分類器
model = mobilenet_v2.net(image, 10)

# 獲取損失函數和準確率函數
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)

獲取CIFAR的訓練數據和測試數據,並創建一個執行器,MobileNet V2這個模型雖然使用在手機上的,但是在訓練起來卻不是那麼快,最好使用GPU進行訓練,要不是相當的慢。

# 獲取CIFAR數據
train_reader = paddle.batch(cifar.train10(), batch_size=32)
test_reader = paddle.batch(cifar.test10(), batch_size=32)

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

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

這裏從初始化程序中獲取全部參數的名稱,用於之後訓練過程中輸出參數的值,並記錄到VisualDL中。

# 定義日誌的開始位置和獲取參數名稱
train_step = 0
test_step = 0
params_name = fluid.default_startup_program().global_block().all_parameters()[0].name

開始訓練模型,在訓練過程中,把訓練時的損失值保存到train_cost_writer中,把訓練時的準確率保存到train_acc_writer中,把訓練過程中的參數變化保存到histogram中。把測試時的損失值保存到test_cost_writer中,把測試時的準確率保存到test_acc_writer中。

# 訓練10次
for pass_id in range(10):
    # 進行訓練
    for batch_id, data in enumerate(train_reader()):
        train_cost, train_acc, params = exe.run(program=fluid.default_main_program(),
                                                feed=feeder.feed(data),
                                                fetch_list=[avg_cost, acc, params_name])
        # 保存訓練的日誌數據
        train_step += 1
        train_cost_writer.add_record(train_step, train_cost[0])
        train_acc_writer.add_record(train_step, train_acc[0])
        histogram.add_record(train_step, params.flatten())

        # 每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_step += 1
        test_cost_writer.add_record(test_step, test_cost[0])
        test_acc_writer.add_record(test_step, test_acc[0])

        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:2.79566, Accuracy:0.03125
Pass:0, Batch:100, Cost:2.48199, Accuracy:0.15625
Pass:0, Batch:200, Cost:2.49757, Accuracy:0.18750
Pass:0, Batch:300, Cost:2.10605, Accuracy:0.28125
Pass:0, Batch:400, Cost:2.24151, Accuracy:0.15625
Pass:0, Batch:500, Cost:1.99807, Accuracy:0.21875
Pass:0, Batch:600, Cost:1.92178, Accuracy:0.34375
Pass:0, Batch:700, Cost:1.81583, Accuracy:0.28125
Pass:0, Batch:800, Cost:2.22559, Accuracy:0.25000
Pass:0, Batch:900, Cost:1.79611, Accuracy:0.34375
Pass:0, Batch:1000, Cost:2.00520, Accuracy:0.25000

訓練結束之後,啓動VisualDL工具,指定日誌文件的目錄和端口號。

visualdl --logdir=log/ --port=8080

訪問網頁地址:http://localhost:8080/,我們會得到以下的圖片。

  • 訓練時的準確率和損失值的變化,從這些圖片可以看到模型正在收斂,準確率在不斷提升。

  • 下圖是使用測試集的準確率和損失值,從圖中可以看出後期的測試情況準確率在下降,損失值在增大,也對比上圖訓練的準確率還在上升,證明模型出現過擬合的情況。

  • 下圖是訓練是參數的histogram圖,從圖中可以看出參數正在趨於穩定,同時的沒有出現異常值,如極大值或者極小值。

本章關於VisualDL的使用就介紹到這裏,讀者在實際開發中可以使用VisualDL,通過利用VisualDL給予的訓練可視化,不斷優化模型。

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

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


上一章:《PaddlePaddle從入門到煉丹》九——遷移學習
下一章:《PaddlePaddle從入門到煉丹》十一——自定義圖像數據集識別


參考資料

  1. https://blog.csdn.net/qq_33200967/article/details/79127175
  2. https://github.com/PaddlePaddle/VisualDL
  3. https://www.jianshu.com/p/4c9404d4998c
小夜