2018-04-25 00:00:00 eo63y6pKI42Ilxr 阅读数 1105
  • 3D游戏引擎之GPU渲染(DX篇)

    本次课程主要是针对3D游戏引擎内部的GPU之Shader渲染处理,告诉大家引擎底层是如何与Shader代码结合的。从基本的灯光渲染,环境渲染到后的Bloom渲染。

    11854 人正在学习 去看看 姜雪伟

云栖君导读:本文根据实测数据,初步探讨了在弹性GPU云服务器上深度学习的性能模型,可帮助科学选择GPU实例的规格。


一、背景


得益于GPU强大的计算能力,深度学习近年来在图像处理、语音识别、自然语言处理等领域取得了重大突GPU服务器几乎成了深度学习加速的标配。


阿里云GPU云服务器在公有云上提供的弹性GPU服务,可以帮助用户快速用上GPU加速服务,并大大简化部署和运维的复杂度。如何提供一个合适的实例规格,从而以最高的性价比提供给深度学习客户,是我们需要考虑的一个问题,本文试图从CPU、内存、磁盘这三个角度对单机GPU云服务器的深度学习训练和预测的性能模型做了初步的分析,希望能对实例规格的选择提供一个科学的设计模型。


下面是我们使用主流的几个开源深度学习框架在NVIDIA GPU上做的一些深度学习的测试。涉及NVCaffe、MXNet主流深度学习框架,测试了多个经典CNN网络在图像分类领域的训练和推理以及RNN网络在自然语言处理领域的训练。


二、训练测试


我们使用NVCaffe、MXNet主流深度学习框架测试了图像分类领域和自然语言处理领域的训练模型。


2.1 图像分类


我们使用NVCaffe、MXNet测试了图像分类领域的CNN网络的单GPU模型训练。

NVCaffe和MXNet测试使用ImageNet ILSVRC2012数据集,训练图片1281167张,包含1000个分类,每个分类包含1000张左右的图片。

2.1.1 CPU+Memory


2.1.1.1 NVCaffe


NVCaffe是NVIDIA基于BVLC-Caffe针对NVIDIA GPU尤其是多GPU加速的开源深度学习框架。LMDB格式的ImageNet训练集大小为240GB ,验证集大小为9.4GB。


我们使用NVcaffe对AlexNet、GoogLeNet、ResNet50、Vgg16四种经典卷积神经网络做了图像分类任务的模型训练测试。分别对比了不同vCPU和Memory配置下的训练性能。性能数据单位是Images/Second(每秒处理的图像张数)。图中标注为10000指的是迭代次数10000次,其它都是测试迭代次数为1000次。


640?wx_fmt=png640?wx_fmt=png640?wx_fmt=png640?wx_fmt=png


2.1.1.2 MXNet


MXNet的数据集使用RecordIO格式,ImageNet训练集 93GB ,验证集 3.7GB。


我们使用网络Inception-v3(GoogLeNet的升级版)做了图像分类的训练测试。分别对比了不同vCPU和Memory配置下的训练性能。数据单位是Samples/Second(每秒处理的图像张数)。


640?wx_fmt=png


2.1.2 磁盘IO

我们在阿里云GN5(P100)实例上使用NVCaffe测试了GoogLeNet网络模型在NVMe SSD本地盘、SSD云盘和高效云盘上的训练性能,测试结果如下(性能数据单位是Images/Second):


640?wx_fmt=png


2.2 自然语言处理


我们使用MXNet测试了RNN网络的LSTM模型的训练,使用PennTreeBank自然语言数据集。PennTreeBank数据集的文本语料库包含近100万个单词,单词表被限定在10000个单词。分别对比了不同vCPU和Memory配置下的训练性能:


640?wx_fmt=png


三、推理测试


3.1 图像分类


我们使用NVCaffe测试了图像分类领域的CNN网络的模型推理。


测试使用ImageNet ILSVRC2012数据集,验证测试图片 50000张。


3.1.1 CPU+Memory


我们使用NVcaffe对AlexNet、GoogLeNet、ResNet50、VGG16四种经典卷积神经网络做了图像分类的推理测试。分别对比了不同vCPU和Memory配置下的训练性能。数据单位是Images/Second(每秒处理的图像张数)。


640?wx_fmt=png


3.1.2 磁盘IO


我们使用NVCaffe测试了GoogLeNet网络在NVMe SSD本地盘、SSD云盘和高效云盘上的图像分类推理性能,测试结果如下(数据单位是Images/Second):


640?wx_fmt=png


四、数据预处理测试


在训练模型之前,往往要对训练数据集做数据预处理,统一数据格式,并做一定的归一化处理。


我们使用NVCaffe对ImageNet ILSVRC2012数据集做了数据预处理的测试,分别对比了NVMe SSD本地盘、SSD云盘和高效云盘的数据预处理时间,数据单位是秒,数据如下:


640?wx_fmt=png


五、数据分析


5.1 训练


5.1.1 图像分类
  

从NVCaffe和MXNet的测试结果来看,图像分类场景单纯的训练阶段对CPU要求不高,单GPU 只需要4vCPU就可以。而内存需求则取决于深度学习框架、神经网络类型和训练数据集的大小:测试中发现NVCaffe随着迭代次数的增多,内存是不断增大的,但是内存需求增大到一定程度,对性能就不会有什么提升了,其中NVCaffe AlexNet网络的训练,相比其它网络对于内存的消耗要大得多。相比之下MXNet的内存占用则要小的多(这也是MXNet的一大优势),93G预处理过的训练数据集训练过程中内存占用不到5G。
对于磁盘IO性能,测试显示训练阶段NVMe SSD本地盘、SSD云盘性能基本接近,高效云盘上的性能略差1%。因此训练阶段对IO性能的要求不高。


5.1.2 自然语言处理
  

从MXNet的测试结果来看,对于PennTreeBank这样规模的数据集,2vCPU 1GB Mem就能满足训练需求。由于自然语言处理的原始数据不像图像分类一样是大量高清图片,自然语言处理的原始数据以文本文件为主,因此自然语言处理对内存和显存的要求都不高,从我们的测试来看,4vCPU 30GB 1GPU规格基本满足训练阶段需求。


5.2 推理


5.2.1 图像分类
  

从NVCaffe的图像分类推理测试来看,除AlexNet 2vCPU刚刚够用外,其它网络2vCPU对性能没有影响,而9.4GB的验证数据集推理过程中内存占用大概是7GB左右,因此对大部分模型来看,2vCPU 30GB 1GPU规格基本满足图像分类推理的性能需求。
  

对于磁盘IO性能,推理性能NVMe SSD本地盘、SSD云盘很接近,但高效云盘差15%。因此推理阶段至少应该使用SSD云盘保证性能。

5.2.2 自然语言处理
  

对于自然语言处理,参考训练性能需求,我们应该可以推测2vCPU 30GB 1GPU规格应该也能满足需求。


5.3 数据预处理
  

从NVCaffe对ImageNet ILSVRC2012数据集做数据预处理的测试来看,数据预处理阶段是IO密集型,NVMe SSD本地盘比SSD云盘快25%,而SSD云盘比高效云盘快10%。


六、总结
  

深度学习框架众多,神经网络类型也是种类繁多,我们选取了主流的框架和神经网络类型,尝试对单机GPU云服务器的深度学习性能模型做了初步的分析,结论是:


  1. 深度学习训练阶段是GPU运算密集型,对于CPU占用不大,而内存的需求取决于深度学习框架、神经网络类型和训练数据集的大小;对磁盘IO性能不敏感,云盘基本能够满足需求。

  2. 深度学习推理阶段对于CPU的占用更小,但是对于磁盘IO性能相对较敏感,因为推理阶段对于延迟有一定的要求,更高的磁盘IO性能对于降低数据读取的延时进而降低整体延迟有很大的帮助。

  3. 深度学习数据预处理阶段是IO密集型阶段,更高的磁盘IO性能能够大大缩短数据预处理的时间。


end


阿里巴巴千亿交易背后的0故障发布

阿里巴巴6大行业报告免费分享啦!

七本书籍带你打下机器学习和数据科学的数学基础

Logtail 从入门到精通:开启日志采集之旅

更多精彩

640?wx_fmt=jpeg

2018-07-04 20:08:23 gxc_8311 阅读数 1451
  • 3D游戏引擎之GPU渲染(DX篇)

    本次课程主要是针对3D游戏引擎内部的GPU之Shader渲染处理,告诉大家引擎底层是如何与Shader代码结合的。从基本的灯光渲染,环境渲染到后的Bloom渲染。

    11854 人正在学习 去看看 姜雪伟

最近深度学习十分火爆,作为一个程序员当然要多学习一下深度学习这种酷炫的新技能啦。现在Google的Tensorflow已经把深度学习需要的一些常用计算封装成了函数库,已经成为了事实上的深度计算标准。本来想在自己的电脑上跑一下Tensorflow的mnist手写数字识别demo,折腾了半天装好了Tensorflow之后想着终于可以高高兴兴的跑一下mnist_deep.py了。

但是跑起来之后发现......真是太TM的慢了!!


每迭代一次要花4-5秒的时间,老子的电脑可是带独立显卡GT 750M的MacBook Pro啊!这速度等它训练完收敛我估计我可以读完一本书了......

所以我开始在网上找有没有GPU加速的云主机可以便宜点儿租来跑一下这些深度计算,找了半天发现网上有很多可以跑深度学习的虚拟主机,但每一个我上去都要重新装一下环境。而且只要开机就收我每个小时十几二十块的费用,我这装个环境至少也得十几分钟……这几块钱就白送给人家了......

直到我发现了这个......

极客云

http://www.jikecloud.net/

打开之后就会看到网站对云主机的介绍,感兴趣的可以点击“如何使用”来查看帮助文档。


如果是新用户注册,可以点击“现在注册”进入注册页面。

嗯嗯......这个验证码真TM的炫动......

注册之后就来到了“我的云主机”页面,点创建实例跳转到创建页面。

在“创建实例”页面可以选择你需要的机型来创建虚拟主机,现在无论是注册还是登陆都会送很多的代金券。实际使用价格还是挺低的,经济实惠。

点击“创建”按钮并等待片刻后,会回到“我的云主机”页面,此时可以看到创建的云主机已经显示在列表界面里面了。

在列表界面有Jupyter Notebook的链接。点这个链接就可以进入Jupyter Notebook了,使用起来很方便。

我们跑一下mnist手写数字的训练Demo,点击文件的链接可以显示文件的内容。

在文件内容页面复制好代码以后,点屏幕左上角的那个图标回到Jupyter首页。

回到首页后点右边的New下拉菜单,选择新建一个Python 3的Notebook。

创建好Notebook之后,在图示处粘贴刚刚复制的代码。

复制好代码之后,就可以点击Run运行啦。

可以看到点击Run之后,在页面下方就出现了程序的log,这说明我们的程序已经欢快的运行起来啦~

上面那种方式其实是给大家演示了一下在Jupyter Notebook里面写程序并且运行程序的方法。如果你已经上传好了几个python文件的话,可以在主页点完的New下拉菜单新建一个Python 3的Notebook之后,运行 %run mnist_deep.py 来执行python文件。

看,它这就欢快的跑起来啦~

总结:

新版的极客云深度学习实例,既装好了所有的深度学习框架,又新加入了Jupyter Notebook方便使用。实现了零设置执行深度学习的程序,想了解更多细节可以去访问极客云的网站:http://www.jikecloud.net/


2017-08-21 12:01:14 u010159842 阅读数 1018
  • 3D游戏引擎之GPU渲染(DX篇)

    本次课程主要是针对3D游戏引擎内部的GPU之Shader渲染处理,告诉大家引擎底层是如何与Shader代码结合的。从基本的灯光渲染,环境渲染到后的Bloom渲染。

    11854 人正在学习 去看看 姜雪伟

这是《使用腾讯云GPU学习深度学习》系列文章的第四篇,主要举例介绍了深度学习计算过程中的一些数据预处理方法。本系列文章主要介绍如何使用 腾讯云GPU服务器 进行深度学习运算,前面主要介绍原理部分,后期则以实践为主。

往期内容:
使用腾讯云 GPU 学习深度学习系列之一:传统机器学习的回顾

使用腾讯云 GPU 学习深度学习系列之二:Tensorflow 简明原理

使用腾讯云 GPU 学习深度学习系列之三:搭建深度神经网络

上一节,我们基于Keras设计了一个用于 CIFAR-10 数据集的深度学习网络。我们的代码主要包括以下部分:

  • 批量输入模块
  • 各种深度学习零件搭建的深度神经网络
  • 凸优化模块
  • 模型的训练与评估

我们注意到,批量输入模块中,实际上就是运用了一个生成器,用来批量读取图片文件,保存成矩阵,直接用于深度神经网络的训练。由于在训练的过程中,图片的特征,是由卷积神经网络自动获取的,因此深度学习通常被认为是一种 端对端(End to end) 的训练方式,期间不需要人为的过多干预。

但是,在实际的运用过程中,这一条并不总是成立。深度神经网络在某些特定的情况下,是需要使用某些特定方法,在批量输入模块之前,对输入数据进行预处理,并且处理结果会极大的改善。

本讲将主要介绍几种数据预处理方法,并且通过这些预处理方法,进行特征提取,提升模型的准确性。

1. 结合传统数据处理方法的特征提取

这一部分我们举医学影像学的一个例子,以 Kaggle 社区第三届数据科学杯比赛的肺部 CT 扫描结节数据为例,来说明如何进行数据的前处理。以下代码改编自该 kaggle 比赛的官方指导教程,主要是特异性的提取 CT 影像图片在肺部的区域的扫描结果,屏蔽无关区域,进而对屏蔽其他区域后的结果,使用深度学习方法进行进一步分析。

屏蔽的程序本身其实并未用到深度学习相关内容,这里主要使用了skimage库。下面我们详细介绍一下具体方法。

第一步,读取医学影像图像。这里以 LUNA16数据集 中的1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192 这张CT 影像数据为例,这张片子可以在这里下载,然后解压缩,用下面的代码分析。其他片子请在 LUNA16 数据集)下载:

from __future__ import print_function, division

import numpy as np
import os
import csv
from glob import glob
import pandas as pd
import numpy as np
import SimpleITK as sitk

from skimage import measure,morphology
from sklearn.cluster import KMeans
from skimage.transform import resize
import matplotlib.pyplot as plt
import seaborn as sns

from glob import glob

try:
    from tqdm import tqdm # long waits are not fun
except:
    print('TQDM does make much nicer wait bars...')
    tqdm = lambda x: x

def make_mask(center,diam,z,width,height,spacing,origin):
    '''
    Center : centers of circles px -- list of coordinates x,y,z
    diam : diameters of circles px -- diameter
    widthXheight : pixel dim of image
    spacing = mm/px conversion rate np array x,y,z
    origin = x,y,z mm np.array
    z = z position of slice in world coordinates mm
    '''
    mask = np.zeros([height,width]) # 0's everywhere except nodule swapping x,y to match img
    #convert to nodule space from world coordinates

    # Defining the voxel range in which the nodule falls
    v_center = (center-origin)/spacing
    v_diam = int(diam/spacing[0]+5)
    v_xmin = np.max([0,int(v_center[0]-v_diam)-5])
    v_xmax = np.min([width-1,int(v_center[0]+v_diam)+5])
    v_ymin = np.max([0,int(v_center[1]-v_diam)-5])
    v_ymax = np.min([height-1,int(v_center[1]+v_diam)+5])

    v_xrange = range(v_xmin,v_xmax+1)
    v_yrange = range(v_ymin,v_ymax+1)

    # Convert back to world coordinates for distance calculation
    x_data = [x*spacing[0]+origin[0] for x in range(width)]
    y_data = [x*spacing[1]+origin[1] for x in range(height)]

    # Fill in 1 within sphere around nodule
    for v_x in v_xrange:
        for v_y in v_yrange:
            p_x = spacing[0]*v_x + origin[0]
            p_y = spacing[1]*v_y + origin[1]
            if np.linalg.norm(center-np.array([p_x,p_y,z]))<=diam:
                mask[int((p_y-origin[1])/spacing[1]),int((p_x-origin[0])/spacing[0])] = 1.0
    return(mask)

def matrix2int16(matrix):
    '''
    matrix must be a numpy array NXN
    Returns uint16 version
    '''
    m_min= np.min(matrix)
    m_max= np.max(matrix)
    matrix = matrix-m_min
    return(np.array(np.rint( (matrix-m_min)/float(m_max-m_min) * 65535.0),dtype=np.uint16))

df_node = pd.read_csv('./annotation.csv')
for fcount, img_file in enumerate(tqdm(df_node['seriesuid'])):
    mini_df = df_node[df_node["seriesuid"]==img_file] #get all nodules associate with file
    if mini_df.shape[0]>0: # some files may not have a nodule--skipping those
        # load the data once
        itk_img = sitk.ReadImage("%s.mhd" % img_file)
        img_array = sitk.GetArrayFromImage(itk_img) # indexes are z,y,x (notice the ordering)
        num_z, height, width = img_array.shape        #heightXwidth constitute the transverse plane
        origin = np.array(itk_img.GetOrigin())      # x,y,z  Origin in world coordinates (mm)
        spacing = np.array(itk_img.GetSpacing())    # spacing of voxels in world coor. (mm)
        # go through all nodes (why just the biggest?)
        for node_idx, cur_row in mini_df.iterrows():       
            node_x = cur_row["coordX"]
            node_y = cur_row["coordY"]
            node_z = cur_row["coordZ"]
            diam = cur_row["diameter_mm"]
            # just keep 3 slices
            imgs = np.ndarray([3,height,width],dtype=np.float32)
            masks = np.ndarray([3,height,width],dtype=np.uint8)
            center = np.array([node_x, node_y, node_z])   # nodule center
            v_center = np.rint((center-origin)/spacing)  # nodule center in voxel space (still x,y,z ordering)
            for i, i_z in enumerate(np.arange(int(v_center[2])-1,
                             int(v_center[2])+2).clip(0, num_z-1)): # clip prevents going out of bounds in Z
                mask = make_mask(center, diam, i_z*spacing[2]+origin[2],
                                     width, height, spacing, origin)
                masks[i] = mask
                imgs[i] = img_array[i_z]
            np.save(os.path.join("./images_%04d_%04d.npy" % (fcount,node_idx)),imgs)
            np.save(os.path.join("./masks_%04d_%04d.npy"  % (fcount,node_idx)),masks)

简单解释下,首先,CT 影像是一个三维的图像,以三维矩阵的形式保存在1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192.raw 这个文件中,.mhd文件则保存了影像文件的基本信息。具体而言,annotation.csv 文件中,图像中结节的坐标是:

x seriesuid coordX coordY coordZ diameter_mm
0 1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636... 56.208405 86.343413 -115.867579 23.350644

这里结节坐标的 coordX~Z 都是物理坐标, .mhd文件保存的,就是从这些物理坐标到 .raw文件中矩阵坐标的映射。于是上面整个函数,其实就是在从 CT 影像仪器的原始文件读取信息,转换物理坐标为矩阵坐标,并且 将结节附近的CT 切片存储成对应的 python 矩阵,用来进行进一步的分析。

然后,我们看一下读取的结果。可见输入文件中标注的结节就在右下方。

img_file = "./images_0000_0000.npy"
imgs_to_process = np.load(img_file).astype(np.float64)

fig = plt.figure(figsize=(12,4))
for i in range(3):
    ax = fig.add_subplot(1,3,i+1)
    ax.imshow(imgs_to_process[i,:,:], 'bone')
    ax.set_axis_off()

可见图中,除了中间亮度较低的肺部,还有亮度较高的脊柱、肋骨,以及肌肉、脂肪等组织。我们的一个思路,就是 留下暗的区域,去掉亮的区域。当然这里,亮度多高才算亮?这个我们可以对一张图中所有像素点的亮度做概率密度分布,然后用 Kmeans 算法,找出这个明暗分解的阈值(下文图中左上角):

i = 0
img = imgs_to_process[i]

#Standardize the pixel values
mean = np.mean(img)
std = np.std(img)
img = img-mean
img = img/std

# Find the average pixel value near the lungs
#         to renormalize washed out images
middle = img[100:400,100:400]
mean = np.mean(middle)  
max = np.max(img)
min = np.min(img)

# To improve threshold finding, I'm moving the
#         underflow and overflow on the pixel spectrum
img[img==max]=mean
img[img==min]=mean


# Using Kmeans to separate foreground (radio-opaque tissue)
#     and background (radio transparent tissue ie lungs)
# Doing this only on the center of the image to avoid
#     the non-tissue parts of the image as much as possible
kmeans = KMeans(n_clusters=2).fit(np.reshape(middle,[np.prod(middle.shape),1]))
centers = sorted(kmeans.cluster_centers_.flatten())
threshold = np.mean(centers)
thresh_img = np.where(img<threshold,1.0,0.0)  # threshold the image

然后使用 skimage 工具包。 skimage 是python一种传统图像处理的工具,我们这里,主要使用这个工具包,增强图像的轮廓,去除图像的细节,进而根据图像的轮廓信息,进行图像的分割,得到目标区域(Region of Interests, ROI)。

# 对一张图中所有像素点的亮度做概率密度分布, 用竖线标注阈值所在
fig = plt.figure(figsize=(8,12))
ax1 = fig.add_subplot(321)
sns.distplot(middle.ravel(), ax=ax1)
ax1.vlines(x=threshold, ymax=10, ymin=0)
ax1.set_title('Threshold: %1.2F' % threshold)
ax1.set_xticklabels([])

# 展示阈值对图像切割的结果。小于阈值的点标注为1,白色。大于阈值的点标注为0,黑色。
ax2 = fig.add_subplot(322)
ax2.imshow(thresh_img, "gray")
ax2.set_axis_off()
ax2.set_title('Step1, using threshold as cutoff')

# 增大黑色部分(非ROI)的区域,使之尽可能的连在一起
eroded = morphology.erosion(thresh_img,np.ones([4,4]))
ax3 = fig.add_subplot(323)
ax3.imshow(eroded, "gray")
ax3.set_axis_off()
ax3.set_title('Step2,  erosion shrinks bright\nregions and enlarges dark regions.')

# 增大白色部分(ROI)的区域,尽可能的消除面积较小的黑色区域
dilation = morphology.dilation(eroded,np.ones([10,10]))
ax4 = fig.add_subplot(324)
ax4.imshow(dilation, "gray")
ax4.set_axis_off()
ax4.set_title('Step3,  dilation shrinks dark\nregions and enlarges bright regions.')

# 上一张图中共有三片连续区域,即最外层的体外区域,内部的肺部区域,以及二者之间的身体轮廓区域。这里将其分别标出
labels = measure.label(dilation)
ax5 = fig.add_subplot(325)
ax5.imshow(labels, "gray")
#ax5.set_axis_off()
ax5.set_title('Step4, label connected regions\n of an integer array.')


# 提取regions 信息,这张图片的 region的 bbox位置分别在 [[0,0,512,512],[141, 86, 396, 404]],
#   分别对应 体外+轮廓 以及 肺部区域的左上角、右下角坐标。
#   于是这里通过区域的宽度 B[2]-B[0]、高度 B[3]-B[1]
#   以及距离图片上下的距离  B[0]>40 and B[2]<472,
#   最终保留需要的区域。
regions = measure.regionprops(labels)
good_labels = []
for prop in regions:
    B = prop.bbox
    if B[2]-B[0]<475 and B[3]-B[1]<475 and B[0]>40 and B[2]<472:
        good_labels.append(prop.label)

mask = np.zeros_like(labels)
for N in good_labels:
    mask = mask + np.where(labels==N,1,0)

mask = morphology.dilation(mask,np.ones([10,10])) # one last dilation

ax6 = fig.add_subplot(326)
ax6.imshow(mask, "gray")
ax6.set_axis_off()
ax6.set_title('Step5, remain the region of interests.')

最后再看一下提取的效果如何:

fig = plt.figure(figsize=(8,4))

ax1 = fig.add_subplot(1,2,1)
ax1.imshow(imgs_to_process[0,:,:], 'bone')
ax1.set_axis_off()
ax1.set_title("Raw CT image")

ax2 = fig.add_subplot(1,2,2)
ax2.imshow(imgs_to_process[0,:,:]*mask, 'bone')
ax2.set_axis_off()
ax2.set_title("Pre-processed Images with\nregion of interest in lung")

右图将进一步的放入深度学习模型,进行肺部结节的进一步检测。

2. 结合深度学习技术的特征提取增强

除了通过传统手段进行数据预先处理,我们同样可以使用深度学习技术进行这一步骤。

可能大家对手写数字识别数据集(MNIST)非常熟悉,Tensorflow 官网就有教程,指导如何搭建卷积神经网络,训练一个准确率高达 99.2% 的模型。

png

但实际运用过程中,我们会发现其实 MNIST 数据集其实书写的比较工整,于是我们就考虑到,对于比较潦草的书写,直接训练卷积神经网络,是否是最好的选择?是否可以将“草书”字体的数字,变得正规一点,然后放进卷积神经网络训练?于是我们利用一个“草书”版的 MNIST 数据集,来介绍一下spatial_transform 模块:

首先需要下载这个“草书”版的手写数字集:

wget https://github.com/daviddao/spatial-transformer-tensorflow/raw/master/data/mnist_sequence1_sample_5distortions5x5.npz

画风如下,明显凌乱了许多,但其实人还是可以看懂,所以我们可以尝试使用深度神经网络来解决。
png

我们开始分析数据。首先读数据:

import tensorflow as tf
# https://github.com/tensorflow/models/tree/master/transformer
from spatial_transformer import transformer  
import numpy as np
from tf_utils import weight_variable, bias_variable, dense_to_one_hot
import matplotlib.pyplot as plt
from keras.backend.tensorflow_backend import set_session

np.random.seed(0)
tf.set_random_seed(0)

config = tf.ConfigProto()
config.gpu_options.allow_growth=True
set_session(tf.Session(config=config))
%matplotlib inline

mnist_cluttered = np.load('./mnist_sequence1_sample_5distortions5x5.npz')

X_train = mnist_cluttered['X_train']
y_train = mnist_cluttered['y_train']
X_valid = mnist_cluttered['X_valid']
y_valid = mnist_cluttered['y_valid']
X_test = mnist_cluttered['X_test']
y_test = mnist_cluttered['y_test']

Y_train = dense_to_one_hot(y_train, n_classes=10)
Y_valid = dense_to_one_hot(y_valid, n_classes=10)
Y_test = dense_to_one_hot(y_test, n_classes=10)

初始化参数,然后直接得到一批原始数据,放入xout

x = tf.placeholder(tf.float32, [None, 1600])
keep_prob = tf.placeholder(tf.float32)

iter_per_epoch = 100
n_epochs = 500
train_size = 10000
indices = np.linspace(0, 10000 - 1, iter_per_epoch)
indices = indices.astype('int')
iter_i = 0

batch_xs = X_train[indices[iter_i]:indices[iter_i+1]]
x_tensor = tf.reshape(x, [-1, 40, 40, 1])

sess = tf.Session()
sess.run(tf.global_variables_initializer())
xout = sess.run(x_tensor, feed_dict={x: batch_xs})

然后搭建一个 spatial_transform 网络。网络结构如下图:

png

x = tf.placeholder(tf.float32, [None, 1600])
y = tf.placeholder(tf.float32, [None, 10])

x_tensor = tf.reshape(x, [-1, 40, 40, 1])
W_fc_loc1 = weight_variable([1600, 20])
b_fc_loc1 = bias_variable([20])

W_fc_loc2 = weight_variable([20, 6])
initial = np.array([[1., 0, 0], [0, 1., 0]])
initial = initial.astype('float32')
initial = initial.flatten()
b_fc_loc2 = tf.Variable(initial_value=initial, name='b_fc_loc2')

# %% Define the two layer localisation network
h_fc_loc1 = tf.nn.tanh(tf.matmul(x, W_fc_loc1) + b_fc_loc1)

# %% We can add dropout for regularizing and to reduce overfitting like so:
keep_prob = tf.placeholder(tf.float32)
h_fc_loc1_drop = tf.nn.dropout(h_fc_loc1, keep_prob)

# %% Second layer
h_fc_loc2 = tf.nn.tanh(tf.matmul(h_fc_loc1_drop, W_fc_loc2) + b_fc_loc2)

# %% We'll create a spatial transformer module to identify discriminative
# %% patches
out_size = (40, 40)
h_trans = transformer(x_tensor, h_fc_loc2, out_size)

再得到一批经过变换后的数据,放入xtransOut

iter_i = 0
batch_xs = X_train[0:101]
batch_ys = Y_train[0:101]

sess = tf.Session()
sess.run(tf.global_variables_initializer())

xtransOut = sess.run(h_trans,
                            feed_dict={
                                x: batch_xs,
                                y: batch_ys,
                                keep_prob: 1.0
})

展示两批数据。上面一行是原始数据,下面一行是变换后的数据。可见数字在局部被放大,有的数字写的歪的被自动正了过来:

fig = plt.figure(figsize=(10,2))

for idx in range(10):
    ax1 = fig.add_subplot(2,10,idx+1)
    ax2 = fig.add_subplot(2,10,idx+11)

    ax1.imshow(xout[idx,:,:,0], "gray")
    ax2.imshow(xtransOut[idx,:,:,0], "gray")
    ax1.set_axis_off()
    ax2.set_axis_off()
    ax1.set_title(np.argmax(batch_ys, axis=1)[idx])
    ax2.set_title(np.argmax(batch_ys, axis=1)[idx])

也就是说,通过 spatial_transform 层,对同一批输入数据的参数学习,我们最后实际上得到了一个坐标的映射 Grid generator
$T_{\theta(G)}$ ,可以将一个倾斜的、“草书”书写的数字,变得更正一点。

png

接下来,我们构建一个卷积神经网络:

x = tf.placeholder(tf.float32, [None, 1600])
y = tf.placeholder(tf.float32, [None, 10])
keep_prob = tf.placeholder(tf.float32)

def Networks(x,keep_prob, SpatialTrans=True):
    x_tensor = tf.reshape(x, [-1, 40, 40, 1])
    W_fc_loc1 = weight_variable([1600, 20])
    b_fc_loc1 = bias_variable([20])

    W_fc_loc2 = weight_variable([20, 6])
    initial = np.array([[1., 0, 0], [0, 1., 0]])
    initial = initial.astype('float32')
    initial = initial.flatten()
    b_fc_loc2 = tf.Variable(initial_value=initial, name='b_fc_loc2')

    # %% Define the two layer localisation network
    h_fc_loc1 = tf.nn.tanh(tf.matmul(x, W_fc_loc1) + b_fc_loc1)
    # %% We can add dropout for regularizing and to reduce overfitting like so:

    h_fc_loc1_drop = tf.nn.dropout(h_fc_loc1, keep_prob)
    # %% Second layer
    h_fc_loc2 = tf.nn.tanh(tf.matmul(h_fc_loc1_drop, W_fc_loc2) + b_fc_loc2)

    # %% We'll create a spatial transformer module to identify discriminative
    # %% patches
    out_size = (40, 40)
    h_trans = transformer(x_tensor, h_fc_loc2, out_size)

    # %% We'll setup the first convolutional layer
    # Weight matrix is [height x width x input_channels x output_channels]
    filter_size = 3
    n_filters_1 = 16
    W_conv1 = weight_variable([filter_size, filter_size, 1, n_filters_1])

    # %% Bias is [output_channels]
    b_conv1 = bias_variable([n_filters_1])

    # %% Now we can build a graph which does the first layer of convolution:
    # we define our stride as batch x height x width x channels
    # instead of pooling, we use strides of 2 and more layers
    # with smaller filters.
    if SpatialTrans:
        h_conv1 = tf.nn.relu(
            tf.nn.conv2d(input=h_trans,
                         filter=W_conv1,
                         strides=[1, 2, 2, 1],
                         padding='SAME') +
            b_conv1)
    else:
        h_conv1 = tf.nn.relu(
            tf.nn.conv2d(input=x_tensor,
                         filter=W_conv1,
                         strides=[1, 2, 2, 1],
                         padding='SAME') +
            b_conv1)

    # %% And just like the first layer, add additional layers to create
    # a deep net
    n_filters_2 = 16
    W_conv2 = weight_variable([filter_size, filter_size, n_filters_1, n_filters_2])
    b_conv2 = bias_variable([n_filters_2])
    h_conv2 = tf.nn.relu(
        tf.nn.conv2d(input=h_conv1,
                     filter=W_conv2,
                     strides=[1, 2, 2, 1],
                     padding='SAME') +
        b_conv2)

    # %% We'll now reshape so we can connect to a fully-connected layer:
    h_conv2_flat = tf.reshape(h_conv2, [-1, 10 * 10 * n_filters_2])

    # %% Create a fully-connected layer:
    n_fc = 1024
    W_fc1 = weight_variable([10 * 10 * n_filters_2, n_fc])
    b_fc1 = bias_variable([n_fc])
    h_fc1 = tf.nn.relu(tf.matmul(h_conv2_flat, W_fc1) + b_fc1)

    h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    # %% And finally our softmax layer:
    W_fc2 = weight_variable([n_fc, 10])
    b_fc2 = bias_variable([10])
    y_logits = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
    return y_logits


# %% We'll now train in minibatches and report accuracy, loss:
iter_per_epoch = 100
n_epochs = 100
train_size = 10000

indices = np.linspace(0, 10000 - 1, iter_per_epoch)
indices = indices.astype('int')

首先训练一个未经过变换的:

y_logits_F = Networks(x, keep_prob, SpatialTrans=False)
cross_entropy_F = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y_logits_F, labels=y))
opt = tf.train.AdamOptimizer()
optimizer_F = opt.minimize(cross_entropy_F)
grads_F = opt.compute_gradients(cross_entropy_F, [b_fc_loc2])
correct_prediction_F = tf.equal(tf.argmax(y_logits_F, 1), tf.argmax(y, 1))
accuracy_F = tf.reduce_mean(tf.cast(correct_prediction_F, 'float'))

sessF = tf.Session()
sessF.run(tf.global_variables_initializer())

l_acc_F = []
for epoch_i in range(n_epochs):
    for iter_i in range(iter_per_epoch - 1):
        batch_xs = X_train[indices[iter_i]:indices[iter_i+1]]
        batch_ys = Y_train[indices[iter_i]:indices[iter_i+1]]
        sessF.run(optimizer_F, feed_dict={
            x: batch_xs, y: batch_ys, keep_prob: 0.8})

    acc = sessF.run(accuracy_F,
                                                     feed_dict={
                                                         x: X_valid,
                                                         y: Y_valid,
                                                         keep_prob: 1.0
                                                     })
    l_acc_F.append(acc)
    if epoch_i % 10 == 0:
        print('Accuracy (%d): ' % epoch_i + str(acc))
Accuracy (0): 0.151
Accuracy (10): 0.813
Accuracy (20): 0.832
Accuracy (30): 0.825
Accuracy (40): 0.833
Accuracy (50): 0.837
Accuracy (60): 0.832
Accuracy (70): 0.837
Accuracy (80): 0.833
Accuracy (90): 0.843

可见这个神经网络对直接输入变形数据效果不好。我们再训练一个进过变换的:

y_logits = Networks(x, keep_prob)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y_logits, labels=y))
opt = tf.train.AdamOptimizer()
optimizer = opt.minimize(cross_entropy)
grads = opt.compute_gradients(cross_entropy, [b_fc_loc2])
correct_prediction = tf.equal(tf.argmax(y_logits, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))

sess = tf.Session()
sess.run(tf.global_variables_initializer())

l_acc = []
for epoch_i in range(n_epochs):
    for iter_i in range(iter_per_epoch - 1):
        batch_xs = X_train[indices[iter_i]:indices[iter_i+1]]
        batch_ys = Y_train[indices[iter_i]:indices[iter_i+1]]
        sess.run(optimizer, feed_dict={
            x: batch_xs, y: batch_ys, keep_prob: 0.8})

    acc = sess.run(accuracy,
                                                     feed_dict={
                                                         x: X_valid,
                                                         y: Y_valid,
                                                         keep_prob: 1.0
                                                     })
    l_acc.append(acc)
    if epoch_i % 10 == 0:
        print('Accuracy (%d): ' % epoch_i + str(acc))

发现变换后正确率还可以接受:

Accuracy (0): 0.25
Accuracy (10): 0.92
Accuracy (20): 0.94
Accuracy (30): 0.955
Accuracy (40): 0.943
Accuracy (50): 0.944
Accuracy (60): 0.957
Accuracy (70): 0.948
Accuracy (80): 0.941
Accuracy (90): 0.948

画图比较正确率与训练次数

plt.plot(l_acc, label="Using Spatial Transform")
plt.plot(l_acc_F, label="Raw input")
plt.legend(loc=8)

可见 Spatial Transform 确实可以提升分类的正确率。

最后,我们专门提出来直接分类分错、Spatial Transform 后分类正确的数字。上面一行是直接预测的结果(错误),下面一行是转换后分类的结果:

通过 Spatial Transform,我们确实可以强化数据的特征,增加数据分类的准确性。

此外,Spatial Transform 除了可以识别“草书”字体的手写数字,同样在交通标志分类中表现优异,通过Spatial Transform 元件与 LeNet-5 网络的组合,Yann LeCun团队实现了42种交通标志分类99.1%准确性(笔者直接用LeNet-5发现准确率只有87%左右),文章地址Traffic Sign Recognition with Multi-Scale Convolutional Networks

png

目前腾讯云 GPU 服务器已经在5月27日盛大公测,本章代码也可以用较小的数据量、较低的nb_epoch在普通云服务器上尝试一下,但是随着处理运算量越来越大,必须租用 云GPU服务器 才可以快速算出结果。服务器的租用方式、价格,详情请见 腾讯云 GPU 云服务器今日全量上线!

2017-08-21 10:45:34 u010159842 阅读数 1055
  • 3D游戏引擎之GPU渲染(DX篇)

    本次课程主要是针对3D游戏引擎内部的GPU之Shader渲染处理,告诉大家引擎底层是如何与Shader代码结合的。从基本的灯光渲染,环境渲染到后的Bloom渲染。

    11854 人正在学习 去看看 姜雪伟

这是《使用腾讯云GPU学习深度学习》系列文章的第六篇,本文以如何识别马路上的行人、车辆为主题,介绍了基于 Tensorflow 的 SSD 模型如何应用在物体识别定位项目中。本系列文章主要介绍如何使用腾讯云GPU服务器进行深度学习运算,前面主要介绍原理部分,后期则以实践为主。

往期内容:

  1. 使用腾讯云 GPU 学习深度学习系列之一:传统机器学习的回顾
  2. 使用腾讯云 GPU 学习深度学习系列之二:Tensorflow 简明原理
  3. 使用腾讯云 GPU 学习深度学习系列之三:搭建深度神经网络
  4. 使用腾讯云 GPU 学习深度学习系列之四:深度学习的特征工程
  5. 使用腾讯云GPU学习深度学习系列之五:文字的识别与定位

我们在第三讲中,提到过如何搭建一个简单的深度神经网络,识别一张图片中的单一物体。进而在上一节中,以车牌为例,谈到如何识别多个物体。

但是实际上,上一讲提到的技术,在实际应用过程中,会遇到问题,就是我们需要识别的物体,未必会给我们机会去“摆拍”。比如对于车牌识别,我们想要的数据是这样的:

但是我们从摄像头拿到的车牌数据,可能是这样的:

这下我们上一讲提到的 “93% 识别准确率”,可能就一点用都没有了,因为我们 不仅要识别目标,更需要对目标进行定位——没有人会帮我们用截图工具从第二张图中“抠图”,得到第一张图中的样式,再给我们去训练深度学习模型。而我们接下来要讲的,就是如何同时实现物体的识别与定位。这里我们以 SSD 模型为例,谈一谈如何在上图中,识别车辆和行人。

1. 物体识别与定位原理

1.1. 思路1——基于网格搜索

首先是物体 识别问题,这里回顾下第三讲最后的 CIFAR-10 分类

CIFAR-10 给出了物体的截图信息:

下一步可以根据这些截图,训练一个分类器:

model.fit_generator(datagen.flow(X_train, Y_train, batch_size=batch_size),
                    nb_epoch=nb_epoch,
                    validation_data=(X_test, Y_test),
                    callbacks=[tb])

我们训练好分类器 model 之后,可以载入新的图片,用训练好的模型预测分类结果:

# 使用训练好的模型,预测输入图片属于 CIFAR-10 哪个类
import cv2
NEW_IMG_MATRIX = cv2.imread("test.png")
NEW_IMG_MATRIX = cv2.resize(NEW_IMG_MATRIX, (32, 32))
predictions = model.predict(NEW_IMG_MATRIX)

有了分类器,接下来要做的,就是 用分类器扫描整张图像,定位特征位置。这里关键的方法,就是用什么算法扫描,比如可以将图片分成若干网格,用分类器一个格子、一个格子扫描,这种方法有几个问题:

  1. 问题1: 目标正好处在两个网格交界处,就会造成分类器的结果在两边都不足够显著,造成漏报(True Negative)。
  2. 问题2: 目标过大或过小,导致网格中结果不足够显著,造成漏报。

针对第一点,可以采用相互重叠的网格。比如一个网格大小是 32x32 像素,那么就网格向下移动时,只动 8 个像素,走四步才完全移出以前的网格。针对第二点,可以采用大小网格相互结合的策略,32x32 网格扫完,64x64 网格再扫描一次,16x16 网格也再扫一次。

但是这样会带来其他问题——我们为了保证准确率,对同一张图片扫描次数过多,严重影响了计算速度,造成这种策略 无法做到实时标注。也就是说,如果应用在无人驾驶汽车时,如果前面突然出现一个行人,此时模型一遍一遍扫描整个图片、终于完成标注之后,可能已经是三四秒以后了,此时汽车已经开出去几十米,会造成很大的危险。

1.2. 深度学习框架对网格搜索的改进

于是,为了快速、实时标注图像特征,对于整个识别定位算法,就有了诸多改进方法。

一个最基本的思路是,合理使用卷积神经网络的内部结构,避免重复计算。如前几篇文章所述,用卷积神经网络扫描某一图片时,实际上卷积得到的结果已经存储了不同大小的网格信息,这一过程实际上已经完成了我们上一部分提出的改进措施,如下图所示,我们发现前几层卷积核的结果更关注细节,后面的卷积层结果更加关注整体:

png

对于问题1,如果一个物体位于两个格子的中间,虽然两边都不一定足够显著,但是两边的基本特征如果可以合理组合的话,我们就不需要再扫描一次。而后几层则越来越关注整体,对问题2,目标可能会过大过小,但是特征同样也会留下。也就是说,用卷积神经网络扫描图像过程中,由于深度神经网络本身就有好几层卷积、实际上已经反复多次扫描图像,以上两个问题可以通过合理使用卷积神经网络的中间结果得到解决

我们先回顾下 SSD 之前的算法,在 SSD 算法之前,MultiBox,FastR-CNN 法都采用了两步的策略,即第一步通过深度神经网络,对潜在的目标物体进行定位,即先产生Box;至于Box 里面的物体如何分类,这里再进行第二步计算。此外第一代的 YOLO 算法可以做到一步完成计算加定位,但是结构中采用了全连接层,而我们在 第三讲 的 1.3.3. 部分提到过全连接层有很多问题,并且正在逐步被深度神经网络架构“抛弃”。

图片来源山人七深度学习知乎专栏

SSD 就是一种一步完成计算、不采用全连接层的计算架构。一步到位的计算框架可以使计算速度更快,并且由于完全抛弃全连接层, 第三讲 的 1.3.3. 部分提到全连接层带来的过拟合问题也会得到改善。:

图片来源山人七深度学习知乎专栏

2.SSD 算法原理

构建SSD 的深度神经网络框架

图片来源山人七深度学习知乎专栏

大体思路就是,用VGG 深度神经网络的前五层,在额外多加六层结构。然后提取其中几层进过卷积后的结果,进行网格搜索,找目标特征。

代码:

default_params = SSDParams(
        img_shape=(300, 300),
        num_classes=21,
        no_annotation_label=21,
        feat_layers=['block4', 'block7', 'block8', 'block9', 'block10', 'block11'],
        feat_shapes=[(38, 38), (19, 19), (10, 10), (5, 5), (3, 3), (1, 1)],
        anchor_size_bounds=[0.15, 0.90],
        anchor_sizes=[(21., 45.),
                      (45., 99.),
                      (99., 153.),
                      (153., 207.),
                      (207., 261.),
                      (261., 315.)],
        anchor_ratios=[[2, .5],
                       [2, .5, 3, 1./3],
                       [2, .5, 3, 1./3],
                       [2, .5, 3, 1./3],
                       [2, .5],
                       [2, .5]],
        anchor_steps=[8, 16, 32, 64, 100, 300],
        anchor_offset=0.5,
        normalizations=[20, -1, -1, -1, -1, -1],
        prior_scaling=[0.1, 0.1, 0.2, 0.2]
)

def ssd_net(inputs,
            num_classes=default_params.num_classes,
            feat_layers=default_params.feat_layers,
            anchor_sizes=default_params.anchor_sizes,
            anchor_ratios=default_params.anchor_ratios,
            normalizations=default_params.normalizations,
            is_training=True,
            dropout_keep_prob=0.5,
            prediction_fn=slim.softmax,
            reuse=None,
            scope='ssd_300_vgg'):
    """SSD net definition.
    """
    end_points = {}
    with tf.variable_scope(scope, 'ssd_300_vgg', [inputs], reuse=reuse):
        ######################################
        # 前五个 Blocks,首先照搬 VGG16 架构   #
        # 注意这里使用 end_points 标注中间结果 #
        ######################################
        # Block 1.
        net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='conv1')
        end_points['block1'] = net
        net = slim.max_pool2d(net, [2, 2], scope='pool1')
        # Block 2.
        net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='conv2')
        end_points['block2'] = net
        net = slim.max_pool2d(net, [2, 2], scope='pool2')
        # Block 3.
        net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3')
        end_points['block3'] = net
        net = slim.max_pool2d(net, [2, 2], scope='pool3')
        # Block 4.
        net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv4')
        end_points['block4'] = net
        net = slim.max_pool2d(net, [2, 2], scope='pool4')
        # Block 5.
        net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv5')
        end_points['block5'] = net
        net = slim.max_pool2d(net, [3, 3], stride=1, scope='pool5')

        ####################################
        # 后六个 Blocks,使用额外卷积层      #
        ####################################
        # Block 6: let's dilate the hell out of it!
        net = slim.conv2d(net, 1024, [3, 3], rate=6, scope='conv6')
        end_points['block6'] = net
        net = tf.layers.dropout(net, rate=dropout_keep_prob, training=is_training)
        # Block 7: 1x1 conv. Because the fuck.
        net = slim.conv2d(net, 1024, [1, 1], scope='conv7')
        end_points['block7'] = net
        net = tf.layers.dropout(net, rate=dropout_keep_prob, training=is_training)

        # Block 8/9/10/11: 1x1 and 3x3 convolutions stride 2 (except lasts).
        end_point = 'block8'
        with tf.variable_scope(end_point):
            net = slim.conv2d(net, 256, [1, 1], scope='conv1x1')
            net = custom_layers.pad2d(net, pad=(1, 1))
            net = slim.conv2d(net, 512, [3, 3], stride=2, scope='conv3x3', padding='VALID')
        end_points[end_point] = net
        end_point = 'block9'
        with tf.variable_scope(end_point):
            net = slim.conv2d(net, 128, [1, 1], scope='conv1x1')
            net = custom_layers.pad2d(net, pad=(1, 1))
            net = slim.conv2d(net, 256, [3, 3], stride=2, scope='conv3x3', padding='VALID')
        end_points[end_point] = net
        end_point = 'block10'
        with tf.variable_scope(end_point):
            net = slim.conv2d(net, 128, [1, 1], scope='conv1x1')
            net = slim.conv2d(net, 256, [3, 3], scope='conv3x3', padding='VALID')
        end_points[end_point] = net
        end_point = 'block11'
        with tf.variable_scope(end_point):
            net = slim.conv2d(net, 128, [1, 1], scope='conv1x1')
            net = slim.conv2d(net, 256, [3, 3], scope='conv3x3', padding='VALID')
        end_points[end_point] = net

        # Prediction and localisations layers.
        ######################################
        # 每个中间层 end_points 返回中间结果   #
        # 将各层预测结果存入列表,返回给优化函数 #
        ######################################
        predictions = []
        logits = []
        localisations = []
        for i, layer in enumerate(feat_layers):
            with tf.variable_scope(layer + '_box'):
                p, l = ssd_multibox_layer(end_points[layer],
                                          num_classes,
                                          anchor_sizes[i],
                                          anchor_ratios[i],
                                          normalizations[i])
            predictions.append(prediction_fn(p))
            logits.append(p)
            localisations.append(l)

        return predictions, localisations, logits, end_points

在卷积的结果中搜索网格的位置:

这里的关键是生成网格。首先,由于 3x3 大小的卷积核,在卷积神经网络部分已经扫描了整个输入图像,并且还反复扫了很多层,所以这里不再需要考虑手动分网格是否会遗漏信息,因为各种卷积核已经将整张图像完全扫描一遍了。同时,我们注意到 VGG16 卷基层的像素大小在逐步降低,而各层表示的都是同样的输入图像,也就是说,单个像素所能表征的物体大小,是在逐步增加的,所以也不需要考虑物体大小的影响。

根据刚才的网络定义,SSD 会在 4、7、8、9、10、11 这六层生成搜索网格(Anchor Boxes),并且其位置也是固定的。这几层搜索网格特征如下:

层数 卷积操作后特征大小 网格增强比例 单个网格增强得到网格数目 总网格数目
4 [38, 38] [2, 0.5] 4 4 x 38 x 38
7 [19, 19] [2, 0.5, 3, 1/3] 6 6 x 19 x 19
8 [10, 10] [2, 0.5, 3, 1/3] 6 6 x 10 x 10
9 [ 5, 5] [2, 0.5, 3, 1/3] 6 6 x 5 x 5
10 [ 3, 3] [2, 0.5] 4 4 x 3 x 3
11 [ 1, 1] [2, 0.5] 4 4 x 1 x 1

注意:
单个网格增强得到网格数目 = 1(原有) + 1(同时缩小) + 网格增强数目/2(长缩小宽放大、长扩大宽缩小)

网格增强比例,指的是在同一位置,原有宽度乘以一个系数、长度除以一个系数,得到新的长宽。当这个系数是 2 时,增强结果如下图:

整个过程如图所示:

图片来源晓雷机器学习笔记知乎专栏。最早应该是在 deepsystems.io 网站上,但是ppt 版本过早,晓雷对部分图片有修改。

这一步代码如下:

# 对所有层生成网格                                  
def ssd_anchors_all_layers(img_shape,
                           layers_shape,anchor_sizes,anchor_ratios,
                           anchor_steps,offset=0.5,dtype=np.float32):
    layers_anchors = []
    for i, s in enumerate(layers_shape):
        anchor_bboxes = ssd_anchor_one_layer(img_shape, s, 
                                anchor_sizes[i], anchor_ratios[i], 
                                anchor_steps[i], offset=offset, dtype=dtype)
        layers_anchors.append(anchor_bboxes)
    return layers_anchors

# 对其中一层生成网格
def ssd_anchor_one_layer(img_shape,
                         feat_shape,
                         sizes,
                         ratios,
                         step,
                         offset=0.5,
                         dtype=np.float32):
    y, x = np.mgrid[0:feat_shape[0], 0:feat_shape[1]]
    y = (y.astype(dtype) + offset) * step / img_shape[0]
    x = (x.astype(dtype) + offset) * step / img_shape[1]
    y = np.expand_dims(y, axis=-1)
    x = np.expand_dims(x, axis=-1)

    num_anchors = len(sizes) + len(ratios)
    h = np.zeros((num_anchors, ), dtype=dtype)
    w = np.zeros((num_anchors, ), dtype=dtype)
    # Add first anchor boxes with ratio=1.
    h[0] = sizes[0] / img_shape[0]
    w[0] = sizes[0] / img_shape[1]
    di = 1
    if len(sizes) > 1:
        h[1] = math.sqrt(sizes[0] * sizes[1]) / img_shape[0]
        w[1] = math.sqrt(sizes[0] * sizes[1]) / img_shape[1]
        di += 1
    for i, r in enumerate(ratios):
        h[i+di] = sizes[0] / img_shape[0] / math.sqrt(r)
        w[i+di] = sizes[0] / img_shape[1] * math.sqrt(r)
    return y, x, h, w

训练模型

训练模型的损失函数如下定义:

根据公式,写代码:

# =========================================================================== #
# SSD loss function.
# =========================================================================== #
def ssd_losses(logits, localisations,
               gclasses, glocalisations, gscores,
               match_threshold=0.5,
               negative_ratio=3.,
               alpha=1.,
               label_smoothing=0.,
               device='/cpu:0',
               scope=None):
    with tf.name_scope(scope, 'ssd_losses'):
        lshape = tfe.get_shape(logits[0], 5)
        num_classes = lshape[-1]
        batch_size = lshape[0]

        # Flatten out all vectors!
        flogits = []
        fgclasses = []
        fgscores = []
        flocalisations = []
        fglocalisations = []
        for i in range(len(logits)):
            flogits.append(tf.reshape(logits[i], [-1, num_classes]))
            fgclasses.append(tf.reshape(gclasses[i], [-1]))
            fgscores.append(tf.reshape(gscores[i], [-1]))
            flocalisations.append(tf.reshape(localisations[i], [-1, 4]))
            fglocalisations.append(tf.reshape(glocalisations[i], [-1, 4]))

        # And concat the crap!
        logits = tf.concat(flogits, axis=0)
        gclasses = tf.concat(fgclasses, axis=0)
        gscores = tf.concat(fgscores, axis=0)
        localisations = tf.concat(flocalisations, axis=0)
        glocalisations = tf.concat(fglocalisations, axis=0)
        dtype = logits.dtype

        # Compute positive matching mask...
        pmask = gscores > match_threshold
        fpmask = tf.cast(pmask, dtype)
        n_positives = tf.reduce_sum(fpmask)

        # Hard negative mining...
        ## 控制 负样本:正样本(negative_ratio) 在 3 左右
        no_classes = tf.cast(pmask, tf.int32)
        predictions = slim.softmax(logits)
        nmask = tf.logical_and(tf.logical_not(pmask),
                               gscores > -0.5)
        fnmask = tf.cast(nmask, dtype)
        nvalues = tf.where(nmask,
                           predictions[:, 0],
                           1. - fnmask)
        nvalues_flat = tf.reshape(nvalues, [-1])
        # Number of negative entries to select.
        max_neg_entries = tf.cast(tf.reduce_sum(fnmask), tf.int32)
        n_neg = tf.cast(negative_ratio * n_positives, tf.int32) + batch_size
        n_neg = tf.minimum(n_neg, max_neg_entries)

        val, idxes = tf.nn.top_k(-nvalues_flat, k=n_neg)
        max_hard_pred = -val[-1]
        # Final negative mask.
        nmask = tf.logical_and(nmask, nvalues < max_hard_pred)
        fnmask = tf.cast(nmask, dtype)

        # 识别物体损失函数 Lconf(x,c)
        ## 识别为分类物体
        with tf.name_scope('cross_entropy_pos'):
            loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,
                                                                  labels=gclasses)
            loss = tf.div(tf.reduce_sum(loss * fpmask), batch_size, name='value')
            tf.losses.add_loss(loss)
        ## 识别为背景
        with tf.name_scope('cross_entropy_neg'):
            loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,
                                                                  labels=no_classes)
            loss = tf.div(tf.reduce_sum(loss * fnmask), batch_size, name='value')
            tf.losses.add_loss(loss)

        # 预测位置误差损失函数 Lloc(x,l,g)
        with tf.name_scope('localization'):
            # Weights Tensor: positive mask + random negative.
            weights = tf.expand_dims(alpha * fpmask, axis=-1)
            loss = custom_layers.abs_smooth(localisations - glocalisations)
            loss = tf.div(tf.reduce_sum(loss * weights), batch_size, name='value')
            tf.losses.add_loss(loss)

在多个 GPU 训练模型,完整代码见 balancap Github。核心代码:

# =================================================================== #
# 定义单个 GPU 训练模型
# =================================================================== #
def clone_fn(batch_queue):
    """Allows data parallelism by creating multiple
    clones of network_fn."""
    # Dequeue batch.
    b_image, b_gclasses, b_glocalisations, b_gscores = \
        tf_utils.reshape_list(batch_queue.dequeue(), batch_shape)

    # Construct SSD network.
    arg_scope = ssd_net.arg_scope(weight_decay=FLAGS.weight_decay,
                                  data_format=DATA_FORMAT)
    with slim.arg_scope(arg_scope):
        predictions, localisations, logits, end_points = \
            ssd_net.net(b_image, is_training=True)
    # Add loss function.
    ssd_net.losses(logits, localisations,
                   b_gclasses, b_glocalisations, b_gscores,
                   match_threshold=FLAGS.match_threshold,
                   negative_ratio=FLAGS.negative_ratio,
                   alpha=FLAGS.loss_alpha,
                   label_smoothing=FLAGS.label_smoothing)
    return end_points

3. SSD 实战

我们使用VOC数据集,resize 到 300x300 大小的图像输入,SSD 发表文章给出的训练结果,作为模型,来标注我们自己的输入图片以及视频。
首先,安装需要的包,然后下载 SSD 程序.

pip install moviepy
git clone https://github.com/balancap/SSD-Tensorflow
unzip ./SSD-Tensorflow/checkpoints/ssd_300_vgg.ckpt.zip

# 下载测试图片
wget -O traffic.jpg http://www.gd-anda.com/upload/image/20170411/14918963579489456.jpg

VOC 数据集总共有 20 个类别:

导入必要的模块:

import os
import math
import random
import sys

import numpy as np
import tensorflow as tf
import cv2

import matplotlib.pyplot as plt
import matplotlib.cm as mpcm

sys.path.append('./SSD-Tensorflow/')
from nets import ssd_vgg_300, ssd_common, np_methods
from preprocessing import ssd_vgg_preprocessing

# TensorFlow session: grow memory when needed. TF, DO NOT USE ALL MY GPU MEMORY!!!
gpu_options = tf.GPUOptions(allow_growth=True)
config = tf.ConfigProto(log_device_placement=False, gpu_options=gpu_options)
isess = tf.InteractiveSession(config=config)

slim = tf.contrib.slim

%matplotlib inline

l_VOC_CLASS = [
                'aeroplane',   'bicycle', 'bird',  'boat',      'bottle',
                'bus',         'car',     'cat',   'chair',     'cow',
                'diningTable', 'dog',     'horse', 'motorbike', 'person',
                'pottedPlant', 'sheep',   'sofa',  'train',     'TV'
]
# 定义数据格式
net_shape = (300, 300)
data_format = 'NHWC'  # [Number, height, width, color],Tensorflow backend 的格式

# 预处理,以 Tensorflow backend, 将输入图片大小改成 300x300,作为下一步输入
img_input = tf.placeholder(tf.uint8, shape=(None, None, 3))
image_pre, labels_pre, bboxes_pre, bbox_img = ssd_vgg_preprocessing.preprocess_for_eval(
    img_input, 
    None, 
    None, 
    net_shape, 
    data_format, 
    resize=ssd_vgg_preprocessing.Resize.WARP_RESIZE
)
image_4d = tf.expand_dims(image_pre, 0)

# 定义 SSD 模型结构
reuse = True if 'ssd_net' in locals() else None
ssd_net = ssd_vgg_300.SSDNet()
with slim.arg_scope(ssd_net.arg_scope(data_format=data_format)):
    predictions, localisations, _, _ = ssd_net.net(image_4d, is_training=False, reuse=reuse)

# 导入官方给出的 SSD 模型参数
ckpt_filename = '../ssd_300_vgg.ckpt'
isess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
saver.restore(isess, ckpt_filename)

# 在网络模型结构中,提取搜索网格的位置
ssd_anchors = ssd_net.anchors(net_shape)

加载几个辅助做图的函数:

def colors_subselect(colors, num_classes=21):
    dt = len(colors) // num_classes
    sub_colors = []
    for i in range(num_classes):
        color = colors[i*dt]
        if isinstance(color[0], float):
            sub_colors.append([int(c * 255) for c in color])
        else:
            sub_colors.append([c for c in color])
    return sub_colors


def bboxes_draw_on_img(img, classes, scores, bboxes, colors, thickness=2):
    shape = img.shape
    for i in range(bboxes.shape[0]):
        bbox = bboxes[i]
        color = colors[classes[i]]
        # Draw bounding box...
        p1 = (int(bbox[0] * shape[0]), int(bbox[1] * shape[1]))
        p2 = (int(bbox[2] * shape[0]), int(bbox[3] * shape[1]))
        cv2.rectangle(img, p1[::-1], p2[::-1], color, thickness)
        # Draw text...
        s = '%s/%.3f' % ( l_VOC_CLASS[int(classes[i])-1], scores[i])
        p1 = (p1[0]-5, p1[1])
        #cv2.putText(img, s, p1[::-1], cv2.FONT_HERSHEY_DUPLEX, 1.5, color, 3)

colors_plasma = colors_subselect(mpcm.plasma.colors, num_classes=21)

process_image 函数可以对新的图片进行标注

# Main image processing routine.
def process_image(img, select_threshold=0.3, nms_threshold=.8, net_shape=(300, 300)):
    # Run SSD network.
    rimg, rpredictions, rlocalisations, rbbox_img = isess.run([image_4d, predictions, localisations, bbox_img],
                                                              feed_dict={img_input: img})

    # Get classes and bboxes from the net outputs.
    rclasses, rscores, rbboxes = np_methods.ssd_bboxes_select(
            rpredictions, rlocalisations, ssd_anchors,
            select_threshold=select_threshold, img_shape=net_shape, num_classes=21, decode=True)

    rbboxes = np_methods.bboxes_clip(rbbox_img, rbboxes)
    rclasses, rscores, rbboxes = np_methods.bboxes_sort(rclasses, rscores, rbboxes, top_k=400)
    rclasses, rscores, rbboxes = np_methods.bboxes_nms(rclasses, rscores, rbboxes, nms_threshold=nms_threshold)
    # Resize bboxes to original image shape. Note: useless for Resize.WARP!
    rbboxes = np_methods.bboxes_resize(rbbox_img, rbboxes)
    bboxes_draw_on_img(img, rclasses, rscores, rbboxes, colors_plasma, thickness=8)
    return img

img = cv2.imread("./traffic.jpg")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(process_image(img))

看起来标注的还可以。

然后我们再标注一段视频,

为了处理视频文件,需要额外使用 imageio moviepy 两个包,而使用 moviepy,则需要经过imageio 安装 ffmpeg

import imageio
imageio.plugins.ffmpeg.download()
from moviepy.editor import VideoFileClip

视频摄于中关村附近,读者也可以上传自己的视频:

def process_video (input_path, output_path):
    clip = VideoFileClip (input_path)
    result = clip.fl_image(process_image)
    %time result.write_videofile(output_path, audio=False)

process_video("./VID_20170612_090941.mp4", "zgc.mp4")
[MoviePy] >>>> Building video zgc.mp4
[MoviePy] Writing video zgc.mp4
100%|██████████| 1649/1649 [00:37<00:00, 43.82it/s]
[MoviePy] Done.
[MoviePy] >>>> Video ready: zgc.mp4 

CPU times: user 37.1 s, sys: 5.99 s, total: 43.1 s
Wall time: 38.2 s

这个短视频使用华为荣耀7拍摄,长度在 54s 左右,每秒记录 30 帧图像,而 SSD 模型的实际处理速度在我的 Pascal TitanX GPU 上,则达到了每秒钟 44 帧图像,真正实现了图像的实时处理。处理结果如下,基本实现了对周围物体的标注:

gif

结果是 mp4格式,这里转换成 gif 动图,做了些图像压缩,原版视频:

目前腾讯云 GPU 服务器已经在5月27日盛大公测,本章代码在普通云服务器上尝试,同样可以运行,但是无法做到图像的实时处理。要想做到实时标注处理图像数据,必须租用 云GPU服务器 。服务器的租用方式、价格,详情请见 腾讯云 GPU 云服务器今日全量上线

2017-08-21 10:36:55 u010159842 阅读数 1399
  • 3D游戏引擎之GPU渲染(DX篇)

    本次课程主要是针对3D游戏引擎内部的GPU之Shader渲染处理,告诉大家引擎底层是如何与Shader代码结合的。从基本的灯光渲染,环境渲染到后的Bloom渲染。

    11854 人正在学习 去看看 姜雪伟

这是《使用腾讯云GPU学习深度学习》系列文章的第三篇,主要是接着上一讲提到的如何自己构建深度神经网络框架中的功能模块,进一步详细介绍 Tensorflow 中 Keras 工具包提供的几种深度神经网络模块。本系列文章主要介绍如何使用 腾讯云GPU服务器 进行深度学习运算,前面主要介绍原理部分,后期则以实践为主。

往期内容:

使用腾讯云 GPU 学习深度学习系列之一:传统机器学习的回顾

使用腾讯云 GPU 学习深度学习系列之二:Tensorflow 简明原理

上一讲中,我们用最简单的代码,实现了最简单的深度学习框架,然后进一步的实现了 Input LinearSigmoid Tanh 以及 MSE 这几个模块,并且用这几个模块搭建了一个最简单的两层深度学习网络。

当然,我们没有必要自己亲自关注这些底层的部分,接下来的内容,我们将基于现在最火的深度学习框架Tensorflow,这里以 r1.1 版本为例,详细介绍一下更多的模块的原理,谈一谈怎么使用这些零件搭建深度学习网络。在 r1.1版本的 Tensorflow 中,已经集成了以前的 Keras 模块,使得搭建基本的Tensorflow模块更加简单、方便。

我们可以简单的将深度神经网络的模块,分成以下的三个部分,即深度神经网络上游的基于生成器的 输入模块,深度神经网络本身,以及深度神经网络下游基于批量梯度下降算法的 凸优化模块

  • 批量输入模块

  • 各种深度学习零件搭建的深度神经网络

  • 凸优化模块

其中,搭建深度神经网络的零件又可以分成以下类别:

  • 各种深度学习零件搭建的深度神经网络
    • 常用层
      • Dense
      • Activation
      • Dropout
      • Flatten
    • 卷积层
      • Conv2D
      • Cropping2D
      • ZeroPadding2D
    • 池化层
      • MaxPooling2D
      • AveragePooling2D
      • GlobalAveragePooling2D
    • 正则化层
      • BatchNormalization
    • 反卷积层(Keras中在卷积层部分)
      • UpSampling2D

需要强调一下,这些层与之前一样,都 同时包括了正向传播、反向传播两条通路。我们这里只介绍比较好理解的正向传播过程,基于其导数的反向过程同样也是存在的,其代码已经包括在 Tensorflow 的框架中对应的模块里,可以直接使用。

当然还有更多的零件,具体可以去keras 文档中参阅。

接下来的部分,我们将首先介绍这些深度神经网络的零件,然后再分别介绍上游的批量输入模块,以及下游的凸优化模块。

1. 深度神经网络的基本零件

1.1 常用层:

1.1.1. Dense

Dense 层,就是我们上一篇文章里提到的 Linear 层,即 y=wx+b ,计算乘法以及加法。

1.1.2. Activation

Activation 层在我们上一篇文章中,同样出现过,即 Tanh层以及Sigmoid层,他们都是 Activation 层的一种。当然 Activation 不止有这两种形式,比如有:

图片来源 激活函数与caffe及参数

这其中 relu 层可能是深度学习时代最重要的一种激发函数,在2011年首次被提出。由公式可见,relu 相比早期的 tanh 与 sigmoid函数, relu 有两个重要的特点,其一是在较小处都是0(sigmoid,relu)或者-5(tanh),但是较大值relu函数没有取值上限。其次是relu层在0除不可导,是一个非线性的函数:

即 y=x*(x>0)

对其求导,其结果是:

1.1.3. Dropout

Dropout 层,指的是在训练过程中,每次更新参数时将会随机断开一定百分比(rate)的输入神经元,这种方式可以用于防止过拟合。

图片来源Dropout: A Simple Way to Prevent Neural Networks from Overfitting

1.1.4. Flatten

Flatten 层,指的是将高维的张量(Tensor, 如二维的矩阵、三维的3D矩阵等)变成一个一维张量(向量)。Flatten 层通常位于连接深度神经网络的 卷积层部分 以及 全连接层部分

1.2 卷积层

提到卷积层,就必须讲一下卷积神经网络。我们在第一讲的最后部分提高深度学习模型预测的准确性部分,提了一句 “使用更复杂的深度学习网络,在图片中挖出数以百万计的特征”。这种“更复杂的神经网络”,指的就是卷积神经网络。卷积神经网络相比之前基于 Dense 层建立的神经网络,有所区别之处在于,卷积神经网络可以使用更少的参数,对局部特征有更好的理解。

1.2.1. Conv2D

我们这里以2D 的卷积神经网络为例,来逐一介绍卷积神经网络中的重要函数。比如我们使用一个形状如下的卷积核:

     
1 0 1
0 1 0
1 0 1

扫描这样一个二维矩阵,比如一张图片:

         
1 1 1 0 0
0 1 1 1 0
0 0 1 1 1
0 0 1 1 0
0 1 1 0 0

其过程与结果会是这样:

图片来源kdnuggets

当然,这里很重要的一点,就是正如我们上一讲提到的, Linear 函数的 wb两个参数都是变量,会在不断的训练中,不断学习更新。卷积神经网络中,卷积核其实也是一个变量。这里的

     
1 0 1
0 1 0
1 0 1

可能只是初始值,也可能是某一次迭代时选用的值。随着模型的不断训练,将会不断的更新成其他值,结果也将会是一个不规则的形状。具体的更新方式,同上一讲提到的 Linear 等函数模块相同,卷积层也有反向传播函数,基于反向函数计算梯度,即可用来更新现有的卷积层的值,具体方法可参考CNN的反向传导练习。举一个经过多次学习得到的卷积神经网络的卷积核为例:

AlexNet 第一层卷积层的96个、大小为 11x11 的卷积核结果矩阵。

图片来源Alexnet 2012年文章

清楚了其原理,卷积神经网络还需要再理解几个输入参数:

Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', ...)

其中:

  • filters 指的是输出的卷积层的层数。如上面的动图,只输出了一个卷积层,filters = 1,而实际运用过程中,一次会输出很多卷积层。

  • kernel_size 指的是卷积层的大小,是一个 二维数组,分别代表卷积层有几行、几列。

  • strides 指的是卷积核在输入层扫描时,在 x,y 两个方向,每间隔多长扫执行一次扫描。

  • padding 这里指的是是否扫描边缘。如果是 valid,则仅仅扫描已知的矩阵,即忽略边缘。而如果是 same,则将根据情况在边缘补上0,并且扫描边缘,使得输出的大小等于 input_size / strides。

1.2.2. Cropping2D

这里 Cropping2D 就比较好理解了,就是特地选取输入图像的某一个固定的小部分。比如车载摄像头检测路面的马路线时,摄像头上半部分拍到的天空就可以被 Cropping2D 函数直接切掉忽略不计。

图片来源Udacity自动驾驶课程

1.2.3. ZeroPadding2D

1.2.1部分提到输入参数时,提到 padding参数如果是same,扫描图像边缘时会补上0,确保输出数量等于 input / strides。这里 ZeroPadding2D 的作用,就是在图像外层边缘补上几层0。如下图,就是对原本 32x32x3 的图片进行 ZeroPadding2D(padding=(2, 2)) 操作后的结果:

图片来源Adit Deshpande博客

1.3. 池化层

1.3.1. MaxPooling2D

可能大家在上一部分会意识到一点,就是通过与一个相同的、大小为11x11的卷积核做卷积操作,每次移动步长为1,则相邻的结果会非常接近,正是由于结果接近,有很多信息是冗余的。

因此,MaxPooling 就是一种减少模型冗余程度的方法。以 2x 2 MaxPooling 为例。图中如果是一个 4x4 的输入矩阵,则这个 4x4 的矩阵,会被分割成由两行、两列组成的 2x2 子矩阵,然后每个 2x2 子矩阵取一个最大值作为代表,由此得到一个两行、两列的结果:

图片来源 斯坦福CS231课程

1.3.2. AveragePooling2D

AveragePooling 与 MaxPooling 类似,不同的是一个取最大值,一个是平均值。如果上图的MaxPooling 换成 AveragePooling2D,结果会是:

     
3.25 5.25
2 2

1.3.3. GlobalAveragePooling2D

GlobalAveragePooling,其实指的是,之前举例 MaxPooling 提到的 2x2 Pooling,对子矩阵分别平均,变成了对整个input 矩阵求平均值。

这个理念其实和池化层关系并不十分紧密,因为他扔掉的信息有点过多了,通常只会出现在卷积神经网络的最后一层,通常是作为早期深度神经网络 Flatten 层 + Dense 层结构的替代品:

前面提到过 Flatten 层通常位于连接深度神经网络的 卷积层部分 以及 全连接层部分,但是这个连接有一个大问题,就是如果是一个 1k x 1k 的全连接层,一下就多出来了百万参数,而这些参数实际用处相比卷积层并不高。造成的结果就是,早期的深度神经网络占据内存的大小,反而要高于后期表现更好的神经网络:

图片来源Training ENet on ImageNet

更重要的是,全连接层由于参数偏多,更容易造成 过拟合——前文提到的 Dropout 层就是为了避免过拟合的一种策略,进而由于过拟合,妨碍整个网络的泛化能力。于是就有了用更多的卷积层提取特征,然后不去 Flatten 这些 k x k 大小卷积层,直接把这些 k x k 大小卷积层变成一个值,作为特征,连接分类标签。

1.4. 正则化层

除了之前提到的 Dropout 策略,以及用 GlobalAveragePooling取代全连接层的策略,还有一种方法可以降低网络的过拟合,就是正则化,这里着重介绍下 BatchNormalization

1.4.1. BatchNormalization

BatchNormalization 确实适合降低过拟合,但他提出的本意,是为了加速神经网络训练的收敛速度。比如我们进行最优值搜索时,我们不清楚最优值位于哪里,可能是上千、上万,也可能是个负数。这种不确定性,会造成搜索时间的浪费。

BatchNormalization就是一种将需要进行最优值搜索数据,转换成标准正态分布,这样optimizer就可以加速优化:

输入:一批input 数据: B

期望输出: β,γ

具体如何实现正向传播和反向传播,可以看这里

1.5. 反卷积层

最后再谈一谈和图像分割相关的反卷积层。

之前在 1.2 介绍了卷积层,在 1.3 介绍了池化层。相信读者大概有了一种感觉,就是卷积、池化其实都是在对一片区域计算平均值、最大值,进而忽略这部分信息。换言之,卷积+池化,就是对输入图片打马赛克

但是马赛克是否有用?我们知道老司机可以做到“图中有码,心中无码”,就是说,图片即便是打了马赛克、忽略了细节,我们仍然可以大概猜出图片的内容。这个过程,就有点反卷积的意思了。

利用反卷积层,可以基于 卷积层+全连接层结构,构建新的、用于图像分割的神经网络 结构。这种结构不限制输入图片的大小,

卷积+全连接层结构 全卷积层结构

图片来源:Fully Convolutional Networks for Semantic Segmentation

1.5.1. UpSampling2D

上图在最后阶段使用了 Upsampling 模块,这个同样在 Tensorflow 的 keras 模块可以找到。用法和MaxPooling2D 基本相反,比如:

UpSampling2D(size=(2, 2))

就相当于将输入图片的长宽各拉伸一倍,整个图片被放大了。

当然,Upsampling 实际上未必在网络的最后才使用,我们后面文章提到的 unet 网络结构,每一次进行卷积操作缩小图片大小,后期都会使用 Upsampling 函数增大图片。

png

图片来源 U-Net: Convolutional Networks for Biomedical Image Segmentation

2. 深度神经网络的上下游结构

介绍完深度神经网络的基本结构以后,读者可能已经意识到了,1.3.3 部分提到的深度神经网络的参数大小动辄几十M、上百M,如何合理训练这些参数是个大问题。这就需要在这个网络的上下游,合理处理这个问题。

海量参数背后的意义是,深度神经网络可以获取海量的特征。第一讲中提到过,深度学习是脱胎于传统机器学习的,两者之间的区别,就是深度学习可以在图像处理中,自动进行特征工程,如我们第一讲所言:

想让计算机帮忙挖掘、标注这些更多的特征,这就离不开 更优化的模型 了。事实上,这几年深度学习领域的新进展,就是以这个想法为基础产生的。我们可以使用更复杂的深度学习网络,在图片中挖出数以百万计的特征

这时候,问题也就来了。机器学习过程中,是需要一个输入文件的。这个输入文件的行、列,分别指代样本名称以及特征名称。如果是进行百万张图片的分类,每个图片都有数以百万计的特征,我们将拿到一个 百万样本 x 百万特征 的巨型矩阵。传统的机器学习方法拿到这个矩阵时,受限于计算机内存大小的限制,通常是无从下手的。也就是说,传统机器学习方法,除了在多数情况下不会自动产生这么多的特征以外,模型的训练也会是一个大问题。

深度学习算法为了实现对这一量级数据的计算,做了以下算法以及工程方面的创新:

  • 将全部所有数据按照样本拆分成若干批次,每个批次大小通常在十几个到100多个样本之间。(详见下文输入模块
  • 将产生的批次逐一参与训练,更新参数。(详见下文 凸优化模块
  • 使用 GPU 等计算卡代替 CPU,加速并行计算速度。

这就有点《愚公移山》的意思了。我们可以把训练深度神经网络的训练任务,想象成是搬走一座大山。成语故事中,愚公的办法是既然没有办法直接把山搬走,那就子子孙孙,每人每天搬几筐土走,山就会越来越矮,总有一天可以搬完——这种任务分解方式就如同深度学习算法的分批训练方式。同时,随着科技进步,可能搬着搬着就用翻斗车甚至是高达来代替背筐,就相当于是用 GPU 等高并行计算卡代替了 CPU。

于是,我们这里将主要提到的上游输入模块,以及下游凸优化模块,实际上就是在说如何使用愚公移山的策略,用 少量多次 的方法,去“搬”深度神经网络背后大规模计算量这座大山。

2.2. 输入模块

这一部分实际是在说,当我们有成千上万的图片,存在硬盘中时,如何实现一个函数,每调用一次,就会读取指定张数的图片(以n=32为例),将其转化成矩阵,返回输出。

有 Python 基础的人可能意识到了,这里可能是使用了 Python 的 生成器 特性。其具体作用如廖雪峰博客所言:

创建一个包含100万个元素的 list,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果 list 元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

其关键的写法,是把传统函数的 return 换成 yield

def generator(samples, batch_size=32):
    num_samples = len(samples)
    while 1:
        sklearn.utils.shuffle(samples)
        for offset in range(0, num_samples, batch_size):
            batch_samples = samples.iloc[offset:offset+batch_size]

            images = []
            angles = []
            for idx in range(batch_samples.shape[0]):
                name = './data/'+batch_samples.iloc[idx]['center']
                center_image = cv2.cvtColor( cv2.imread(name), cv2.COLOR_BGR2RGB )
                center_angle = float(batch_samples.iloc[idx]['dir'])
                images.append(center_image)
                angles.append(center_angle)

            # trim image to only see section with road
            X_train = np.array(images)
            y_train = np.array(angles)
            yield sklearn.utils.shuffle(X_train, y_train)

然后调用时,使用

next(generator)

即可一次返回 32 张图像以及对应的标注信息。

当然,keras 同样提供了这一模块,ImageDataGenerator,并且还是加强版,可以对图片进行 增强处理(data argument)(如旋转、反转、白化、截取等)。图片的增强处理在样本数量不多时增加样本量——因为如果图中是一只猫,旋转、反转、颜色调整之后,这张图片可能会不太相同,但它仍然是一只猫:

png

datagen = ImageDataGenerator(
            featurewise_center=False,
            samplewise_center=False,
            featurewise_std_normalization=False,
            samplewise_std_normalization=False,
            zca_whitening=False,
            width_shift_range=0.1,
            height_shift_range=0.1,
            horizontal_flip=True,
            vertical_flip=False)

# compute quantities required for featurewise normalization
datagen.fit(X_train)

2.3 凸优化模块

这一部分谈的是,如何使用基于批量梯度下降算法的凸优化模块,优化模型参数。

前面提到,深度学习的“梯度下降”计算,可以理解成搬走一座大山,而“批量梯度下降”,则是一群人拿着土筐,一点一点把山上的土给搬下山。那么这一点具体应该如何实现呢?其实在第二讲,我们就实现了一个随机批量梯度下降(Stochastic gradient descent, SGD),这里再回顾一下:

def sgd_update(trainables, learning_rate=1e-2):
    for t in trainables:
        t.value = t.value - learning_rate * t.gradients[t]

#训练神经网络的过程
for i in range(epochs):
    loss = 0
    for j in range(steps_per_epoch):
        # 输入模块,将全部所有数据按照样本拆分成若干批次
        X_batch, y_batch = resample(X_, y_, n_samples=batch_size)
        # 各种深度学习零件搭建的深度神经网络
        forward_and_backward(graph)
        # 凸优化模块
        sgd_update(trainables, 0.1)

当然,SGD 其实并不是一个很好的方法,有很多改进版本,可以用下面这张gif图概况:

png

Keras 里,可以直接使用 SGDAdagradAdadeltaRMSProp 以及 Adam 等模块。其实在优化过程中,直接使用 Adam 默认参数,基本就可以得到最优的结果:

from keras.optimizers import Adam
adam = Adam()
model.compile(loss='categorical_crossentropy',
                optimizer=adam,
                metrics=['accuracy'])

3. 实战项目——CIFAR-10 图像分类

最后我们用一个keras 中的官方示例,来结束本讲。

首先做一些前期准备:

# 初始化
from __future__ import print_function
import numpy as np
from keras.callbacks import TensorBoard
from keras.models import Sequential
from keras.optimizers import Adam
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPool2D
from keras.utils import np_utils
from keras import backend as K
from keras.callbacks import ModelCheckpoint
from keras.preprocessing.image import ImageDataGenerator
from keras.datasets import cifar10

from keras.backend.tensorflow_backend import set_session
import tensorflow as tf
config = tf.ConfigProto()
config.gpu_options.allow_growth=True
set_session(tf.Session(config=config))


np.random.seed(0)
print("Initialized!")

# 定义变量
batch_size = 32
nb_classes = 10
nb_epoch = 50
img_rows, img_cols = 32, 32
nb_filters = [32, 32, 64, 64]
pool_size = (2, 2)
kernel_size = (3, 3)

#
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
X_train = X_train.astype("float32") / 255
X_test  = X_test.astype("float32") / 255

y_train = y_train
y_test = y_test


input_shape = (img_rows, img_cols, 3)
Y_train = np_utils.to_categorical(y_train, nb_classes)
Y_test = np_utils.to_categorical(y_test, nb_classes)

上游部分, 基于生成器的批量生成输入模块:

datagen = ImageDataGenerator(
            featurewise_center=False,
            samplewise_center=False,
            featurewise_std_normalization=False,
            samplewise_std_normalization=False,
            zca_whitening=False,
            rotation_range=0,
            width_shift_range=0.1,
            height_shift_range=0.1,
            horizontal_flip=True,
            vertical_flip=False)

datagen.fit(X_train)

核心部分,用各种零件搭建深度神经网络:

model = Sequential()
model.add(Conv2D(nb_filters[0], kernel_size, padding='same',input_shape=X_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(nb_filters[1], kernel_size))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=pool_size))
model.add(Dropout(0.25))

model.add(Conv2D(nb_filters[2], kernel_size, padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(nb_filters[3], kernel_size))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=pool_size))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation('softmax'))

构建的模型如下:

下游部分,使用凸优化模块:

adam = Adam(lr=0.0001)
model.compile(loss='categorical_crossentropy',
                   optimizer=adam,
                   metrics=['accuracy'])

最后,开始训练模型,并且评估模型准确性:

#训练模型
best_model = ModelCheckpoint("cifar10_best.h5", monitor='val_loss', verbose=0, save_best_only=True)
tb = TensorBoard(log_dir="./logs")
model.fit_generator(datagen.flow(X_train, Y_train, batch_size=batch_size),
                        steps_per_epoch=X_train.shape[0] // batch_size,
                        epochs=nb_epoch, verbose=1,
                        validation_data=(X_test, Y_test), callbacks=[best_model,tb])


# 模型评分
score = model.evaluate(X_test, Y_test, verbose=0)
# 输出结果
print('Test score:', score[0])
print("Accuracy: %.2f%%" % (score[1]*100))                   
print("Compiled!")

以上代码本人使用 Pascal TitanX 执行,50个 epoch 中,每个 epoch 用时 12s 左右,总计用时在十五分钟以内,约25 epoch 后,验证集的准确率数会逐步收敛在0.8左右。

本篇是继上一篇“如何造轮子”的主题的一个延续,介绍了 Tensorflow 中 Keras 工具包有哪些现成的轮子可以拿来直接用。

目前腾讯云 GPU 服务器还在内测阶段,暂时没有申请到内测资格的读者,也可以用较小的数据量、较低的nb_epoch在普通云服务器上尝试一下,但是最终结果准确率肯定不能与GPU的结果相比。使用普通的云服务器运行本讲的代码。并且在接下来的内容中,我们的数据处理运算量将越来越大,必须租用 云GPU服务器 才可以快速算出结果。服务器的租用方式,以及 Python 编程环境的搭建,我们将以腾讯云 GPU 为例,在接下来的内容中和大家详细介绍。

没有更多推荐了,返回首页