paddlepaddle 图像分类与 visualDL 尝鲜体验

摘要

先使用 paddlepaddle 官方的例子,来学习下如何使用 paddlepaddle 构建一个靠谱的分类器。

paddlepaddle 图像分类

很早之前,有写过关于 TensorFlow,MXNet 中如何训练一个靠谱的图像分类器,这里我会先使用 paddlepaddle 官方的例子,来学习下如何使用 paddlepaddle 构建一个靠谱的分类器。

数据介绍

官方文档上使用的数据是 flowers-102,这个数据集早在当初 tflearn 学习深度学习网络的时候就有接触过,还是比较简单的,paddlepaddle 把它写成数据接口

模型介绍

paddlepaddle 的模型介绍 model overview。这里我们在实验当中使用大名鼎鼎的 resnet-50:

import paddle.v2 as paddle

__all__ = ['resnet_imagenet', 'resnet_cifar10']

def conv_bn_layer(input,

               ch_out,

               filter_size,

               stride,

               padding,

               active_type=paddle.activation.Relu(),

               ch_in=None):

   tmp = paddle.layer.img_conv(

       input=input,

       filter_size=filter_size,

       num_channels=ch_in,

       num_filters=ch_out,

       stride=stride,

       padding=padding,

       act=paddle.activation.Linear(),

       bias_attr=False)

   return paddle.layer.batch_norm(input=tmp, act=active_type)

def shortcut(input, ch_out, stride):

   if input.num_filters != ch_out:

       return conv_bn_layer(input, ch_out, 1, stride, 0,

                           paddle.activation.Linear())

   else:

       return input

def basicblock(input, ch_out, stride):

   short = shortcut(input, ch_out, stride)

   conv1 = conv_bn_layer(input, ch_out, 3, stride, 1)

   conv2 = conv_bn_layer(conv1, ch_out, 3, 1, 1, paddle.activation.Linear())

   return paddle.layer.addto(

       input=[short, conv2], act=paddle.activation.Relu())

def bottleneck(input, ch_out, stride):

   short = shortcut(input, ch_out * 4, stride)

   conv1 = conv_bn_layer(input, ch_out, 1, stride, 0)

   conv2 = conv_bn_layer(conv1, ch_out, 3, 1, 1)

   conv3 = conv_bn_layer(conv2, ch_out * 4, 1, 1, 0,

                       paddle.activation.Linear())

   return paddle.layer.addto(

       input=[short, conv3], act=paddle.activation.Relu())

def layer_warp(block_func, input, ch_out, count, stride):

   conv = block_func(input, ch_out, stride)

   for i in range(1, count):

       conv = block_func(conv, ch_out, 1)

   return conv

def resnet_imagenet(input, class_dim, depth=50):

   cfg = {

       18: ([2, 2, 2, 1], basicblock),

       34: ([3, 4, 6, 3], basicblock),

       50: ([3, 4, 6, 3], bottleneck),

       101: ([3, 4, 23, 3], bottleneck),

       152: ([3, 8, 36, 3], bottleneck)

   }

   stages, block_func = cfg[depth]

   conv1 = conv_bn_layer(

       input, ch_in=3, ch_out=64, filter_size=7, stride=2, padding=3)

   pool1 = paddle.layer.img_pool(input=conv1, pool_size=3, stride=2)

   res1 = layer_warp(block_func, pool1, 64, stages[0], 1)

   res2 = layer_warp(block_func, res1, 128, stages[1], 2)

   res3 = layer_warp(block_func, res2, 256, stages[2], 2)

   res4 = layer_warp(block_func, res3, 512, stages[3], 2)

   pool2 = paddle.layer.img_pool(

       input=res4, pool_size=7, stride=1, pool_type=paddle.pooling.Avg())

   out = paddle.layer.fc(input=pool2,

                       size=class_dim,

                       act=paddle.activation.Softmax())

   return out

def resnet_cifar10(input, class_dim, depth=32):

   # depth should be one of 20, 32, 44, 56, 110, 1202

   assert (depth - 2) % 6 == 0

   n = (depth - 2) / 6

   nStages = {16, 64, 128}

   conv1 = conv_bn_layer(

       input, ch_in=3, ch_out=16, filter_size=3, stride=1, padding=1)

   res1 = layer_warp(basicblock, conv1, 16, n, 1)

   res2 = layer_warp(basicblock, res1, 32, n, 2)

   res3 = layer_warp(basicblock, res2, 64, n, 2)

   pool = paddle.layer.img_pool(

       input=res3, pool_size=8, stride=1, pool_type=paddle.pooling.Avg())

   out = paddle.layer.fc(input=pool,

                       size=class_dim,

                       act=paddle.activation.Softmax())

   return out

运行

进入对应目录后

python train.py resnet

即可完成

但是事实上其实不是这样的,paddlepaddle 安装 whl 和 tensorflow 一样,gpu 版本都会对应不同的 cuda 和 cudnn,经常会出一些配置问题,所以直接在系统中安装其实是一个不好的选择,所以最好是不要选择直接安装,而是使用 nvidia-docker, 同理在 tensorflow,mxnet 中,感觉 nvidia-docker 也是很好的。

nvidia-docker 安装

  1. 安装 cuda、cudnn,最新的;

  2. 根据系统选择对应版本的 docker;

  3. 安装 nvidia-docker:

docker volume ls -q -f driver=nvidia-docker | xargs -r -I{} -n1 docker ps -q -a -f volume={} | xargs -r docker rm -f sudo apt-get purge -y nvidia-docker

docker volume ls -q -f driver=nvidia-docker | xargs -r -I{} -n1 docker ps -q -a -f volume={} | xargs -r docker rm -f sudo apt-get purge -y nvidia-docker

curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey |

sudo apt-key add - curl -s -L https://nvidia.github.io/nvidia-docker/ubuntu16.04/amd64/nvidia-docker.list |

sudo tee /etc/apt/sources.list.d/nvidia-docker.list sudo apt-get update

sudo apt-get install -y nvidia-docker2 sudo pkill -SIGHUP dockerd

  1. 拉 paddlepaddle 镜像 docker pull paddlepaddle/paddle:latest-gpu

  2. 进入 docker,nvidia-docker run -it -v $PWD:/work -v /data:/data paddlepaddle/paddle:latest-gpu /bin/bash。这里稍微注意下,paddlepaddle 的官方镜像源里面缺少一些必须的包,比如 opencv,好像有点问题,还有 vim 啥的也都没有装,这里为了运行最好把这些环境一次性都解决,然后 docker commit,保存对镜像的修改。

基本这样,你就可以在本机上有一个完全干净的 docker 环境,你就可以随便折腾啦。这里如果有运行不起来的问题,可在下方评论,我具体也不记得缺哪些东西,不过都很好解决的

docker 中运行

这里我运行过很长时间的一个 demo,但是因为没有保存好信息,被覆盖了,所以只能暂时演示一下。

使用自己的数据集来训练模型

实验完官方的 flower-102 之后,我们这里使用自己的数据集来训练模型,数据集是之前收集到的鉴黄数据,数据集主要包括三类:porn\sexy\normal,大概有 500w 张左右。首先,我们需要生成如下格式,格式为图像路径+"\t"+label,其中 label 为 0 表示 normal,1 表示 sexy,2 表示 porn。生成脚本如下:

import random

import os

import codecs

import sys

def gen_datalist(data_dir, class_label, data_type="train", shuffle=True, suffix_list=["jpg", "jpeg", "JPEG", "jpg"]):

   all_files = []

   for root, dirs, files in os.walk(data_dir):

       print "processing {0}".format(root)

       for file_name in files:

           file_name = os.path.join(root, file_name)

           suffix = file_name.split(".")[-1]

           if suffix in suffix_list:

               all_files.append(file_name)

   if shuffle:

       print "shuffle now"

       random.shuffle(all_files)

   print "begin to write to {0}".format(data_type+"_"+class_label+".lst")

   with codecs.open(data_type+"_"+class_label+".lst", "w", encoding="utf8") as fwrite:

       for each_file in all_files:

           fwrite.write(each_file+"\t"+class_label+"\n")

if __name__ == "__main__":

   argv = sys.argv

   data_dir = argv[1]

   class_label = argv[2]

   gen_datalist(data_dir, class_label)

有了脚本运行之后,发现了一些 cv2 库中 none 没有 shape 的问题,调试之后发现,原来收集的数据中,有部分大小为 0 或者很小的图像,这部分应该是有问题的数据,写了个滤除脚本,删除这些数据之后就妥了

from paddle.v2.image import load_and_transform

import paddle.v2 as paddle

def filter_imgs(file_path = "train.lst", write_file = "valid_train.lst"):

   fwrite = open(write_file, "w")

   with open(file_path, 'r') as fread:

       error=0

       for line in fread.readlines():

           img_path = line.strip().split("\t")[0]

           try:

               img = paddle.image.load_image(img_path)

               img = paddle.image.simple_transform(img, 256, 224, True)

               fwrite.write(line)

           except:

               error += 1

               print error

filter_imgs()

从头开始训练模型

在 paddlepaddle 中训练模型

image = paddle.layer.data(

   name="image", type=paddle.data_type.dense_vector(DATA_DIM))

conv, pool, out = resnet.resnet_imagenet(image, class_dim=CLASS_DIM)

cost = paddle.layer.classification_cost(input=out, label=lbl)

parameters = paddle.parameters.create(cost)

optimizer = paddle.optimizer.Momentum(

   momentum=0.9,

   regularization=paddle.optimizer.L2Regularization(rate=0.0005 *

                                                    BATCH_SIZE),

   learning_rate=learning_rate / BATCH_SIZE,

   learning_rate_decay_a=0.1,

   learning_rate_decay_b=128000 * 35,

   learning_rate_schedule="discexp", )

train_reader = paddle.batch(

   paddle.reader.shuffle(

       # flowers.train(),

       # To use other data, replace the above line with:

       reader.train_reader('valid_train0.lst'),

       buf_size=1000),

   batch_size=BATCH_SIZE)

def event_handler(event):

   if isinstance(event, paddle.event.EndIteration):

       if event.batch_id % 1 == 0:

           print "\nPass %d, Batch %d, Cost %f, %s" % (

               event.pass_id, event.batch_id, event.cost, event.metrics)

   if isinstance(event, paddle.event.EndPass):

       with gzip.open('params_pass_%d.tar.gz' % event.pass_id, 'w') as f:

           trainer.save_parameter_to_tar(f)

       result = trainer.test(reader=test_reader)

       print "\nTest with Pass %d, %s" % (event.pass_id, result.metrics)

trainer.train(

   reader=train_reader, num_passes=200, event_handler=event_handler)

  1. 需要配置 resnet 网络,确定好 input 和 out,配置 cost 函数,构建 parameter;

  2. 构建 optimizer, 使用 momentum 的 sgd;

  3. 构建 reader,设置训练数据读取,配置上文提到的图片路径\tlabel 的文件;

  4. event_handler 是用来记录 batch_id\pass 的事件处理函数,传入 train 函数,训练过程中会完成相应工作;

pretrain model + finetuning

resnet 官方提供一个在 imagenet 上训练好的 pretrained model,运行 model_download.sh。

sh model_download.sh ResNet50

会下载 Paddle_ResNet50.tar.gz, 这个文件是 paddlepaddle 在 ImageNet 上训练的模型文件,我们这里使用这个文件的参数做初始化,我们需要在代码,参数初始化的时候,使用这里的参数,修改代码如下:

if args.retrain_file is not None and ''!=args.retrain_file:

       print("restore parameters from {0}".format(args.retrain_file))

       exclude_params = [param for param in parameters.names() if param.startswith('___fc_layer_0__')]

       parameters.init_from_tar(gzip.open(args.retrain_file), exclude_params)

首先,我们需要指定 init_from_tar 的参数文件为 Paddle_ResNet50.tar.gz, 大家知道 ImageNet 是在 1000 类上的一个模型,它的输出为 1000 个节点,所以我们这里需要稍作修改,我们增加一个 exclud_params,指定最后一层___fc_layer_0__的参数,不要从文件当中初始化.

pretrain model + freeze layers + finetuning

查了文档和代码知道,只需要在某层增加 is_static=True,就可以 freeze 掉该层的参数,使该层参数不更新,但是我在使用这部分时遇到了 bug,提了 issue (core dumped with is_static=True)[https://github.com/PaddlePaddle/Paddle/issues/8355],出现 core 的问题,无法正常使用,后面能够搞定了,再更新这部分内容。

代码改进

examples 里面的代码 reader 部分在处理 data.lst 时,太过粗糙,没有考虑到数据如果出现一些问题时,训练代码会直接挂掉,这部分的代码至少要保证足够的鲁棒性

def train_reader(train_list, buffered_size=1024):

   def reader():

       with open(train_list, 'r') as f:

           lines = [line.strip() for line in f]

           for line in lines:

               try:

                   img_path, lab = line.strip().split('\t')

                   yield img_path, int(lab)

               except:

                   print "record in {0} get error".format(train_list)

                   continue

   return paddle.reader.xmap_readers(train_mapper, reader,

                                   cpu_count(), buffered_size)

visualDL 实践

可视化 acc\loss

之前有在小的 demo 上体验过 visualDL,在比较大的数据训练过程上没试验过,这次鉴黄数据上测试,打印出 loss 和 acc 看看,当小数量的 step 的时候,看起来是没有问题的 如

但是但 step 较大的时候,acc 打印不出来了,同样的代码,出错信息也看不出来,各种莫名的报错,看样子和使用的代码没有什么关系,应该是 visualDL 本身的容错做的不够好

出错提示:

这部分和之前提过的一个 issue 很类似: Unexpected error: <type 'exceptions.RuntimeError'> 因为信息量不够,其实很难自己这边做问题分析,希望 visualDL 把这块容错做好一些。

这块时间应该有些问题,我也不知道,我总觉的有点问题 是我用的姿势不对吗?

可视化 graph

使用有问题,使用 paddlepaddle 保存好的模型指定给 model_pb 出现如下问题,看了 repo 中的这部分的 demo 都是直接 curl 下来一个 model.pb 的文件,然后可视化,没有找到能直接导出 paddlepaddle 保存模型的导入到 visualdl 中,可能是我的使用方式有问题,保存模型方式如下图:

莫非需要先把 paddlepaddle 模型转换为 onnx 格式?

可视化 image

有问题,暂时没有测试,之后更新后同步

总结

paddlepaddle 现在在 dl 这块还只是刚开始,example 里面的 demo 和 tensorflow 最开始一样,并不能完全 hold 住实际业务需求,当初 tensorflow 的时候也有种种的问题,后来经过社区的帮助,到现在很多源码几乎都是开箱即用,paddlepaddle 现在可能在文档与 demo 上还是 0.7 版本的 tensorflow,不过希望能更加努力,毕竟作为同行,在参与了一些分布式 dl 模型的工作之后,深知其中的艰辛。visualdl 相当棒的工具,支持 onnx 的模型可视化, 虽然在测试过程中感觉有些瑕疵,但是十分支持,希望能快速发展,个人也在阅读这部分源码学习,histogram 的相关功能没有测试,挺有用的 尤其在训练跑偏的时候可以快速可视化参数的分布。最后,强烈希望 visualdl 能把文档弄的更友好一些,加油。


最新文章

极客公园

用极客视角,追踪你最不可错过的科技圈。

极客之选

新鲜、有趣的硬件产品,第一时间为你呈现。

顶楼

关注前沿科技,发表最具科技的商业洞见。