【深度學習系列】卷積神經網絡CNN原理詳解(一)——基本原理

  上篇文章咱們給出了用paddlepaddle來作手寫數字識別的示例,並對網絡結構進行到了調整,提升了識別的精度。有的同窗表示不是很理解原理,爲何傳統的機器學習算法,簡單的神經網絡(如多層感知機)均可以識別手寫數字,咱們要採用卷積神經網絡CNN來進行別呢?CNN究竟是怎麼識別的?用CNN有哪些優點呢?咱們下面就來簡單分析一下。在講CNN以前,爲避免徹底零基礎的人看不懂後面的講解,咱們先簡單回顧一下傳統的神經網絡的基本知識。html


 

  神經網絡的預備知識python

     爲何要用神經網絡?git

  • 特徵提取的高效性。

   你們可能會疑惑,對於同一個分類任務,咱們能夠用機器學習的算法來作,爲何要用神經網絡呢?你們回顧一下,一個分類任務,咱們在用機器學習算法來作時,首先要明確feature和label,而後把這個數據"灌"到算法裏去訓練,最後保存模型,再來預測分類的準確性。可是這就有個問題,即咱們須要實現肯定好特徵,每個特徵即爲一個維度,特徵數目過少,咱們可能沒法精確的分類出來,即咱們所說的欠擬合,若是特徵數目過多,可能會致使咱們在分類過程當中過於注重某個特徵致使分類錯誤,即過擬合。github

  舉個簡單的例子,如今有一堆數據集,讓咱們分類出西瓜和冬瓜,若是隻有兩個特徵:形狀和顏色,可能無法分區來;若是特徵的維度有:形狀、顏色、瓜瓤顏色、瓜皮的花紋等等,可能很容易分類出來;若是咱們的特徵是:形狀、顏色、瓜瓤顏色、瓜皮花紋、瓜蒂、瓜籽的數量,瓜籽的顏色、瓜籽的大小、瓜籽的分佈狀況、瓜籽的XXX等等,頗有可能會過擬合,譬若有的冬瓜的瓜籽數量和西瓜的相似,模型訓練後這類特徵的權重較高,就很容易分錯。這就致使咱們在特徵工程上須要花不少時間和精力,才能使模型訓練獲得一個好的效果。然而神經網絡的出現使咱們不須要作大量的特徵工程,譬如提早設計好特徵的內容或者說特徵的數量等等,咱們能夠直接把數據灌進去,讓它本身訓練,自我「修正」,便可獲得一個較好的效果。算法

  • 數據格式的簡易性

  在一個傳統的機器學習分類問題中,咱們「灌」進去的數據是不能直接灌進去的,須要對數據進行一些處理,譬如量綱的歸一化,格式的轉化等等,不過在神經網絡裏咱們不須要額外的對數據作過多的處理,具體緣由能夠看後面的詳細推導。api

  • 參數數目的少許性

  在面對一個分類問題時,若是用SVM來作,咱們須要調整的參數須要調整核函數,懲罰因子,鬆弛變量等等,不一樣的參數組合對於模型的效果也不同,想要迅速而又準確的調到最適合模型的參數須要對背後理論知識的深刻了解(固然,若是你說所有都試一遍也是能夠的,可是花的時間可能會更多),對於一個基本的三層神經網絡來講(輸入-隱含-輸出),咱們只須要初始化時給每個神經元上隨機的賦予一個權重w和偏置項b,在訓練過程當中,這兩個參數會不斷的修正,調整到最優質,使模型的偏差最小。因此從這個角度來看,咱們對於調參的背後理論知識並不須要過於精通(只不過作多了以後可能會有一些經驗,在初始值時賦予的值更科學,收斂的更快罷了)數組

   有哪些應用?網絡

  應用很是廣,不過你們注意一點,咱們如今所說的神經網絡,並不能稱之爲深度學習,神經網絡很早就出現了,只不過如今由於不斷的加深了網絡層,複雜化了網絡結構,才成爲深度學習,並在圖像識別、圖像檢測、語音識別等等方面取得了不錯的效果。app

    基本網絡結構框架

  一個神經網絡最簡單的結構包括輸入層、隱含層和輸出層,每一層網絡有多個神經元,上一層的神經元經過激活函數映射到下一層神經元,每一個神經元之間有相對應的權值,輸出即爲咱們的分類類別。

 詳細數學推導

  去年中旬我參考吳恩達的UFLDL和mattmazur的博客寫了篇文章詳細講解了一個最簡單的神經網絡從前向傳播到反向傳播的直觀推導,你們能夠先看看這篇文章--一文弄懂神經網絡中的反向傳播法--BackPropagation

 優缺點

  前面說了不少優勢,這裏就很少說了,簡單說說缺點吧。咱們試想一下若是加深咱們的網絡層,每個網絡層增長神經元的數量,那麼參數的個數將是M*N(m爲網絡層數,N爲每層神經元個數),所需的參數會很是多,參數一多,模型就複雜了,越是複雜的模型就越很差調參,也越容易過擬合。此外咱們從神經網絡的反向傳播的過程來看,梯度在反向傳播時,不斷的迭代會致使梯度愈來愈小,即梯度消失的狀況,梯度一旦趨於0,那麼權值就沒法更新,這個神經元至關因而不起做用了,也就很難致使收斂。尤爲是在圖像領域,用最基本的神經網絡,是不太合適的。後面咱們會詳細講講爲啥不合適。

 


  爲何要用卷積神經網絡?

   傳統神經網絡的劣勢

  前面說到在圖像領域,用傳統的神經網絡並不合適。咱們知道,圖像是由一個個像素點構成,每一個像素點有三個通道,分別表明RGB顏色,那麼,若是一個圖像的尺寸是(28,28,1),即表明這個圖像的是一個長寬均爲28,channel爲1的圖像(channel也叫depth,此處1表明灰色圖像)。若是使用全鏈接的網絡結構,即,網絡中的神經與與相鄰層上的每一個神經元均鏈接,那就意味着咱們的網絡有28 * 28 =784個神經元,hidden層採用了15個神經元,那麼簡單計算一下,咱們須要的參數個數(w和b)就有:784*15*10+15+10=117625個,這個參數太多了,隨便進行一次反向傳播計算量都是巨大的,從計算資源和調參的角度都不建議用傳統的神經網絡。(評論中有同窗對這個參數計算不太理解,我簡單說一下:圖片是由像素點組成的,用矩陣表示的,28*28的矩陣,確定是無法直接放到神經元裏的,咱們得把它「拍平」,變成一個28*28=784 的一列向量,這一列向量和隱含層的15個神經元鏈接,就有784*15=11760個權重w,隱含層和最後的輸出層的10個神經元鏈接,就有11760*10=117600個權重w,再加上隱含層的偏置項15個和輸出層的偏置項10個,就是:117625個參數了)

                                    圖1 三層神經網絡識別手寫數字

  

  卷積神經網絡是什麼?

  三個基本層

  •  卷積層(Convolutional Layer)

  上文提到咱們用傳統的三層神經網絡須要大量的參數,緣由在於每一個神經元都和相鄰層的神經元相鏈接,可是思考一下,這種鏈接方式是必須的嗎?全鏈接層的方式對於圖像數據來講彷佛顯得不這麼友好,由於圖像自己具備「二維空間特徵」,通俗點說就是局部特性。譬如咱們看一張貓的圖片,可能看到貓的眼鏡或者嘴巴就知道這是張貓片,而不須要說每一個部分都看完了才知道,啊,原來這個是貓啊。因此若是咱們能夠用某種方式對一張圖片的某個典型特徵識別,那麼這張圖片的類別也就知道了。這個時候就產生了卷積的概念。舉個例子,如今有一個4*4的圖像,咱們設計兩個卷積核,看看運用卷積核後圖片會變成什麼樣。

 圖2 4*4 image與兩個2*2的卷積核操做結果

 

  由上圖能夠看到,原始圖片是一張灰度圖片,每一個位置表示的是像素值,0表示白色,1表示黑色,(0,1)區間的數值表示灰色。對於這個4*4的圖像,咱們採用兩個2*2的卷積核來計算。設定步長爲1,即每次以2*2的固定窗口往右滑動一個單位。以第一個卷積核filter1爲例,計算過程以下:

 

1 feature_map1(1,1) = 1*1 + 0*(-1) + 1*1 + 1*(-1) = 1 
2 feature_map1(1,2) = 0*1 + 1*(-1) + 1*1 + 1*(-1) = -1 
3 ``` 4 feature_map1(3,3) = 1*1 + 0*(-1) + 1*1 + 0*(-1) = 2

 

  能夠看到這就是最簡單的內積公式。feature_map1(1,1)表示在經過第一個卷積覈計算完後獲得的feature_map的第一行第一列的值,隨着卷積核的窗口不斷的滑動,咱們能夠計算出一個3*3的feature_map1;同理能夠計算經過第二個卷積核進行卷積運算後的feature_map2,那麼這一層卷積操做就完成了。feature_map尺寸計算公式:[ (原圖片尺寸 -卷積核尺寸)/ 步長 ] + 1。這一層咱們設定了兩個2*2的卷積核,在paddlepaddle裏是這樣定義的:

1 conv_pool_1 = paddle.networks.simple_img_conv_pool(
2         input=img,
3         filter_size=3,
4         num_filters=2,
5         num_channel=1,
6         pool_stride=1,
7         act=paddle.activation.Relu())

  這裏調用了networks裏simple_img_conv_pool函數,激活函數是Relu(修正線性單元),咱們來看一看源碼裏外層接口是如何定義的:

 1 def simple_img_conv_pool(input,
 2                          filter_size,
 3                          num_filters,
 4                          pool_size,
 5                          name=None,
 6                          pool_type=None,
 7                          act=None,
 8                          groups=1,
 9                          conv_stride=1,
10                          conv_padding=0,
11                          bias_attr=None,
12                          num_channel=None,
13                          param_attr=None,
14                          shared_bias=True,
15                          conv_layer_attr=None,
16                          pool_stride=1,
17                          pool_padding=0,
18                          pool_layer_attr=None):
19     """
20     Simple image convolution and pooling group.
21     Img input => Conv => Pooling => Output.
22     :param name: group name.
23     :type name: basestring
24     :param input: input layer.
25     :type input: LayerOutput
26     :param filter_size: see img_conv_layer for details.
27     :type filter_size: int
28     :param num_filters: see img_conv_layer for details.
29     :type num_filters: int
30     :param pool_size: see img_pool_layer for details.
31     :type pool_size: int
32     :param pool_type: see img_pool_layer for details.
33     :type pool_type: BasePoolingType
34     :param act: see img_conv_layer for details.
35     :type act: BaseActivation
36     :param groups: see img_conv_layer for details.
37     :type groups: int
38     :param conv_stride: see img_conv_layer for details.
39     :type conv_stride: int
40     :param conv_padding: see img_conv_layer for details.
41     :type conv_padding: int
42     :param bias_attr: see img_conv_layer for details.
43     :type bias_attr: ParameterAttribute
44     :param num_channel: see img_conv_layer for details.
45     :type num_channel: int
46     :param param_attr: see img_conv_layer for details.
47     :type param_attr: ParameterAttribute
48     :param shared_bias: see img_conv_layer for details.
49     :type shared_bias: bool
50     :param conv_layer_attr: see img_conv_layer for details.
51     :type conv_layer_attr: ExtraLayerAttribute
52     :param pool_stride: see img_pool_layer for details.
53     :type pool_stride: int
54     :param pool_padding: see img_pool_layer for details.
55     :type pool_padding: int
56     :param pool_layer_attr: see img_pool_layer for details.
57     :type pool_layer_attr: ExtraLayerAttribute
58     :return: layer's output
59     :rtype: LayerOutput
60     """
61     _conv_ = img_conv_layer(
62         name="%s_conv" % name,
63         input=input,
64         filter_size=filter_size,
65         num_filters=num_filters,
66         num_channels=num_channel,
67         act=act,
68         groups=groups,
69         stride=conv_stride,
70         padding=conv_padding,
71         bias_attr=bias_attr,
72         param_attr=param_attr,
73         shared_biases=shared_bias,
74         layer_attr=conv_layer_attr)
75     return img_pool_layer(
76         name="%s_pool" % name,
77         input=_conv_,
78         pool_size=pool_size,
79         pool_type=pool_type,
80         stride=pool_stride,
81         padding=pool_padding,
82         layer_attr=pool_layer_attr)
View Code

  咱們在Paddle/python/paddle/v2/framework/nets.py 裏能夠看到simple_img_conv_pool這個函數的定義:

 

 1 def simple_img_conv_pool(input,
 2                          num_filters,
 3                          filter_size,
 4                          pool_size,
 5                          pool_stride,
 6                          act,
 7                          pool_type='max',
 8                          main_program=None,
 9                          startup_program=None):
10     conv_out = layers.conv2d(
11         input=input,
12         num_filters=num_filters,
13         filter_size=filter_size,
14         act=act,
15         main_program=main_program,
16         startup_program=startup_program)
17 
18     pool_out = layers.pool2d(
19         input=conv_out,
20         pool_size=pool_size,
21         pool_type=pool_type,
22         pool_stride=pool_stride,
23         main_program=main_program,
24         startup_program=startup_program)
25     return pool_out

 

  能夠看到這裏面有兩個輸出,conv_out是卷積輸出值,pool_out是池化輸出值,最後只返回池化輸出的值。conv_out和pool_out分別又調用了layers.py的conv2d和pool2d,去layers.py裏咱們能夠看到conv2d和pool2d是如何實現的:

  conv2d:

def conv2d(input,
           num_filters,
           name=None,
           filter_size=[1, 1],
           act=None,
           groups=None,
           stride=[1, 1],
           padding=None,
           bias_attr=None,
           param_attr=None,
           main_program=None,
           startup_program=None):
    helper = LayerHelper('conv2d', **locals())
    dtype = helper.input_dtype()

    num_channels = input.shape[1]
    if groups is None:
        num_filter_channels = num_channels
    else:
        if num_channels % groups is not 0:
            raise ValueError("num_channels must be divisible by groups.")
        num_filter_channels = num_channels / groups

    if isinstance(filter_size, int):
        filter_size = [filter_size, filter_size]
    if isinstance(stride, int):
        stride = [stride, stride]
    if isinstance(padding, int):
        padding = [padding, padding]

    input_shape = input.shape
    filter_shape = [num_filters, num_filter_channels] + filter_size

    std = (2.0 / (filter_size[0]**2 * num_channels))**0.5
    filter = helper.create_parameter(
        attr=helper.param_attr,
        shape=filter_shape,
        dtype=dtype,
        initializer=NormalInitializer(0.0, std, 0))
    pre_bias = helper.create_tmp_variable(dtype)

    helper.append_op(
        type='conv2d',
        inputs={
            'Input': input,
            'Filter': filter,
        },
        outputs={"Output": pre_bias},
        attrs={'strides': stride,
               'paddings': padding,
               'groups': groups})

    pre_act = helper.append_bias_op(pre_bias, 1)

    return helper.append_activation(pre_act)
View Code

  pool2d:

 1 def pool2d(input,
 2            pool_size,
 3            pool_type,
 4            pool_stride=[1, 1],
 5            pool_padding=[0, 0],
 6            global_pooling=False,
 7            main_program=None,
 8            startup_program=None):
 9     if pool_type not in ["max", "avg"]:
10         raise ValueError(
11             "Unknown pool_type: '%s'. It can only be 'max' or 'avg'.",
12             str(pool_type))
13     if isinstance(pool_size, int):
14         pool_size = [pool_size, pool_size]
15     if isinstance(pool_stride, int):
16         pool_stride = [pool_stride, pool_stride]
17     if isinstance(pool_padding, int):
18         pool_padding = [pool_padding, pool_padding]
19 
20     helper = LayerHelper('pool2d', **locals())
21     dtype = helper.input_dtype()
22     pool_out = helper.create_tmp_variable(dtype)
23 
24     helper.append_op(
25         type="pool2d",
26         inputs={"X": input},
27         outputs={"Out": pool_out},
28         attrs={
29             "poolingType": pool_type,
30             "ksize": pool_size,
31             "globalPooling": global_pooling,
32             "strides": pool_stride,
33             "paddings": pool_padding
34         })
35 
36     return pool_out
View Code

  你們能夠看到,具體的實現方式還調用了layers_helper.py:

  1 import copy
  2 import itertools
  3 
  4 from paddle.v2.framework.framework import Variable, g_main_program, \
  5     g_startup_program, unique_name, Program
  6 from paddle.v2.framework.initializer import ConstantInitializer, \
  7     UniformInitializer
  8 
  9 
 10 class LayerHelper(object):
 11     def __init__(self, layer_type, **kwargs):
 12         self.kwargs = kwargs
 13         self.layer_type = layer_type
 14         name = self.kwargs.get('name', None)
 15         if name is None:
 16             self.kwargs['name'] = unique_name(self.layer_type)
 17 
 18     @property
 19     def name(self):
 20         return self.kwargs['name']
 21 
 22     @property
 23     def main_program(self):
 24         prog = self.kwargs.get('main_program', None)
 25         if prog is None:
 26             return g_main_program
 27         else:
 28             return prog
 29 
 30     @property
 31     def startup_program(self):
 32         prog = self.kwargs.get('startup_program', None)
 33         if prog is None:
 34             return g_startup_program
 35         else:
 36             return prog
 37 
 38     def append_op(self, *args, **kwargs):
 39         return self.main_program.current_block().append_op(*args, **kwargs)
 40 
 41     def multiple_input(self, input_param_name='input'):
 42         inputs = self.kwargs.get(input_param_name, [])
 43         type_error = TypeError(
 44             "Input of {0} layer should be Variable or sequence of Variable".
 45             format(self.layer_type))
 46         if isinstance(inputs, Variable):
 47             inputs = [inputs]
 48         elif not isinstance(inputs, list) and not isinstance(inputs, tuple):
 49             raise type_error
 50         else:
 51             for each in inputs:
 52                 if not isinstance(each, Variable):
 53                     raise type_error
 54         return inputs
 55 
 56     def input(self, input_param_name='input'):
 57         inputs = self.multiple_input(input_param_name)
 58         if len(inputs) != 1:
 59             raise "{0} layer only takes one input".format(self.layer_type)
 60         return inputs[0]
 61 
 62     @property
 63     def param_attr(self):
 64         default = {'name': None, 'initializer': UniformInitializer()}
 65         actual = self.kwargs.get('param_attr', None)
 66         if actual is None:
 67             actual = default
 68         for default_field in default.keys():
 69             if default_field not in actual:
 70                 actual[default_field] = default[default_field]
 71         return actual
 72 
 73     def bias_attr(self):
 74         default = {'name': None, 'initializer': ConstantInitializer()}
 75         bias_attr = self.kwargs.get('bias_attr', None)
 76         if bias_attr is True:
 77             bias_attr = default
 78 
 79         if isinstance(bias_attr, dict):
 80             for default_field in default.keys():
 81                 if default_field not in bias_attr:
 82                     bias_attr[default_field] = default[default_field]
 83         return bias_attr
 84 
 85     def multiple_param_attr(self, length):
 86         param_attr = self.param_attr
 87         if isinstance(param_attr, dict):
 88             param_attr = [param_attr]
 89 
 90         if len(param_attr) != 1 and len(param_attr) != length:
 91             raise ValueError("parameter number mismatch")
 92         elif len(param_attr) == 1 and length != 1:
 93             tmp = [None] * length
 94             for i in xrange(length):
 95                 tmp[i] = copy.deepcopy(param_attr[0])
 96             param_attr = tmp
 97         return param_attr
 98 
 99     def iter_inputs_and_params(self, input_param_name='input'):
100         inputs = self.multiple_input(input_param_name)
101         param_attrs = self.multiple_param_attr(len(inputs))
102         for ipt, param_attr in itertools.izip(inputs, param_attrs):
103             yield ipt, param_attr
104 
105     def input_dtype(self, input_param_name='input'):
106         inputs = self.multiple_input(input_param_name)
107         dtype = None
108         for each in inputs:
109             if dtype is None:
110                 dtype = each.data_type
111             elif dtype != each.data_type:
112                 raise ValueError("Data Type mismatch")
113         return dtype
114 
115     def create_parameter(self, attr, shape, dtype, suffix='w',
116                          initializer=None):
117         # Deepcopy the attr so that parameters can be shared in program
118         attr_copy = copy.deepcopy(attr)
119         if initializer is not None:
120             attr_copy['initializer'] = initializer
121         if attr_copy['name'] is None:
122             attr_copy['name'] = unique_name(".".join([self.name, suffix]))
123         self.startup_program.global_block().create_parameter(
124             dtype=dtype, shape=shape, **attr_copy)
125         return self.main_program.global_block().create_parameter(
126             name=attr_copy['name'], dtype=dtype, shape=shape)
127 
128     def create_tmp_variable(self, dtype):
129         return self.main_program.current_block().create_var(
130             name=unique_name(".".join([self.name, 'tmp'])),
131             dtype=dtype,
132             persistable=False)
133 
134     def create_variable(self, *args, **kwargs):
135         return self.main_program.current_block().create_var(*args, **kwargs)
136 
137     def create_global_variable(self, persistable=False, *args, **kwargs):
138         return self.main_program.global_block().create_var(
139             *args, persistable=persistable, **kwargs)
140 
141     def set_variable_initializer(self, var, initializer):
142         assert isinstance(var, Variable)
143         self.startup_program.global_block().create_var(
144             name=var.name,
145             type=var.type,
146             dtype=var.data_type,
147             shape=var.shape,
148             persistable=True,
149             initializer=initializer)
150 
151     def append_bias_op(self, input_var, num_flatten_dims=None):
152         """
153         Append bias operator and return its output. If the user does not set 
154         bias_attr, append_bias_op will return input_var
155          
156         :param input_var: the input variable. The len(input_var.shape) is larger
157         or equal than 2.
158         :param num_flatten_dims: The input tensor will be flatten as a matrix 
159         when adding bias.
160         `matrix.shape = product(input_var.shape[0:num_flatten_dims]), product(
161                 input_var.shape[num_flatten_dims:])`
162         """
163         if num_flatten_dims is None:
164             num_flatten_dims = self.kwargs.get('num_flatten_dims', None)
165             if num_flatten_dims is None:
166                 num_flatten_dims = 1
167 
168         size = list(input_var.shape[num_flatten_dims:])
169         bias_attr = self.bias_attr()
170         if not bias_attr:
171             return input_var
172 
173         b = self.create_parameter(
174             attr=bias_attr, shape=size, dtype=input_var.data_type, suffix='b')
175         tmp = self.create_tmp_variable(dtype=input_var.data_type)
176         self.append_op(
177             type='elementwise_add',
178             inputs={'X': [input_var],
179                     'Y': [b]},
180             outputs={'Out': [tmp]})
181         return tmp
182 
183     def append_activation(self, input_var):
184         act = self.kwargs.get('act', None)
185         if act is None:
186             return input_var
187         if isinstance(act, basestring):
188             act = {'type': act}
189         tmp = self.create_tmp_variable(dtype=input_var.data_type)
190         act_type = act.pop('type')
191         self.append_op(
192             type=act_type,
193             inputs={"X": [input_var]},
194             outputs={"Y": [tmp]},
195             attrs=act)
196         return tmp
View Code

  詳細的源碼細節咱們下一節會講這裏指寫一下實現的方式和調用的函數。

 

  因此這個卷積過程就完成了。從上文的計算中咱們能夠看到,同一層的神經元能夠共享卷積核,那麼對於高位數據的處理將會變得很是簡單。而且使用卷積核後圖片的尺寸變小,方便後續計算,而且咱們不須要手動去選取特徵,只用設計好卷積核的尺寸,數量和滑動的步長就可讓它本身去訓練了,省時又省力啊。

 

  爲何卷積核有效?

  那麼問題來了,雖然咱們知道了卷積核是如何計算的,可是爲何使用卷積覈計算後分類效果要因爲普通的神經網絡呢?咱們仔細來看一下上面計算的結果。經過第一個卷積覈計算後的feature_map是一個三維數據,在第三列的絕對值最大,說明原始圖片上對應的地方有一條垂直方向的特徵,即像素數值變化較大;而經過第二個卷積覈計算後,第三列的數值爲0,第二行的數值絕對值最大,說明原始圖片上對應的地方有一條水平方向的特徵。

  仔細思考一下,這個時候,咱們設計的兩個卷積核分別可以提取,或者說檢測出原始圖片的特定的特徵。此時咱們其實就能夠把卷積核就理解爲特徵提取器啊!如今就明白了,爲何咱們只須要把圖片數據灌進去,設計好卷積核的尺寸、數量和滑動的步長就可讓自動提取出圖片的某些特徵,從而達到分類的效果啊!

  :1.此處的卷積運算是兩個卷積核大小的矩陣的內積運算,不是矩陣乘法。即相同位置的數字相乘再相加求和。不要弄混淆了。

    2.卷積核的公式有不少,這只是最簡單的一種。咱們所說的卷積核在數字信號處理裏也叫濾波器,那濾波器的種類就多了,均值濾波器,高斯濾波器,拉普拉斯濾波器等等,不過,無論是什麼濾波器,都只是一種數學運算,無非就是計算更復雜一點。

            3.每一層的卷積核大小和個數能夠本身定義,不過通常狀況下,根據實驗獲得的經驗來看,會在越靠近輸入層的卷積層設定少許的卷積核,越日後,卷積層設定的卷積核數目就越多。具體緣由你們能夠先思考一下,小結裏會解釋緣由。

 

  • 池化層(Pooling Layer)

  經過上一層2*2的卷積核操做後,咱們將原始圖像由4*4的尺寸變爲了3*3的一個新的圖片。池化層的主要目的是經過降採樣的方式,在不影響圖像質量的狀況下,壓縮圖片,減小參數。簡單來講,假設如今設定池化層採用MaxPooling,大小爲2*2,步長爲1,取每一個窗口最大的數值從新,那麼圖片的尺寸就會由3*3變爲2*2:(3-2)+1=2。從上例來看,會有以下變換:

       圖3 Max Pooling結果

     一般來講,池化方法通常有一下兩種:

  • MaxPooling:取滑動窗口裏最大的值
  • AveragePooling:取滑動窗口內全部值的平均值

 

  爲何採用Max Pooling?

  從計算方式來看,算是最簡單的一種了,取max便可,可是這也引起一個思考,爲何須要Max Pooling,意義在哪裏?若是咱們只取最大值,那其餘的值被捨棄難道就沒有影響嗎?不會損失這部分信息嗎?若是認爲這些信息是可損失的,那麼是否意味着咱們在進行卷積操做後仍然產生了一些沒必要要的冗餘信息呢?

  其實從上文分析卷積核爲何有效的緣由來看,每個卷積核能夠看作一個特徵提取器,不一樣的卷積核負責提取不一樣的特徵,咱們例子中設計的第一個卷積核可以提取出「垂直」方向的特徵,第二個卷積核可以提取出「水平」方向的特徵,那麼咱們對其進行Max Pooling操做後,提取出的是真正可以識別特徵的數值,其他被捨棄的數值,對於我提取特定的特徵並無特別大的幫助。那麼在進行後續計算使,減少了feature map的尺寸,從而減小參數,達到減少計算量,缺不損失效果的狀況。

  不過並非全部狀況Max Pooling的效果都很好,有時候有些周邊信息也會對某個特定特徵的識別產生必定效果,那麼這個時候捨棄這部分「不重要」的信息,就不划算了。因此具體狀況得具體分析,若是加了Max Pooling後效果反而變差了,不如把卷積後不加Max Pooling的結果與卷積後加了Max Pooling的結果輸出對比一下,看看Max Pooling是否對卷積核提取特徵起了反效果。

 

     Zero Padding

      因此到如今爲止,咱們的圖片由4*4,經過卷積層變爲3*3,再經過池化層變化2*2,若是咱們再添加層,那麼圖片豈不是會越變越小?這個時候咱們就會引出「Zero Padding」(補零),它能夠幫助咱們保證每次通過卷積或池化輸出後圖片的大小不變,如,上述例子咱們若是加入Zero Padding,再採用3*3的卷積核,那麼變換後的圖片尺寸與原圖片尺寸相同,以下圖所示:

  圖4 zero padding結果

   一般狀況下,咱們但願圖片作完卷積操做後保持圖片大小不變,因此咱們通常會選擇尺寸爲3*3的卷積核和1的zero padding,或者5*5的卷積核與2的zero padding,這樣經過計算後,能夠保留圖片的原始尺寸。那麼加入zero padding後的feature_map尺寸 =( width + 2 * padding_size - filter_size )/stride + 1

  注:這裏的width也可換成height,此處是默認正方形的卷積核,weight = height,若是二者不相等,能夠分開計算,分別補零。

 

  • Flatten層 & Fully Connected Layer

  到這一步,其實咱們的一個完整的「卷積部分」就算完成了,若是想要疊加層數,通常也是疊加「Conv-MaxPooing",經過不斷的設計卷積核的尺寸,數量,提取更多的特徵,最後識別不一樣類別的物體。作完Max Pooling後,咱們就會把這些數據「拍平」,丟到Flatten層,而後把Flatten層的output放到full connected Layer裏,採用softmax對其進行分類。

    圖5 Flatten過程

  

  • 小結

  這一節咱們介紹了最基本的卷積神經網絡的基本層的定義,計算方式和起的做用。有幾個小問題能夠供你們思考一下: 

1.卷積核的尺寸必須爲正方形嗎?能夠爲長方形嗎?若是是長方形應該怎麼計算?

2.卷積核的個數如何肯定?每一層的卷積核的個數都是相同的嗎? 

3.步長的向右和向下移動的幅度必須是同樣的嗎?

 

  若是對上面的講解真的弄懂了的話,其實這幾個問題並不難回答。下面給出個人想法,能夠做爲參考:

  1.卷積核的尺寸不必定非得爲正方形。長方形也能夠,只不過一般狀況下爲正方形。若是要設置爲長方形,那麼首先得保證這層的輸出形狀是整數,不能是小數。若是你的圖像是邊長爲 28 的正方形。那麼卷積層的輸出就知足 [ (28 - kernel_size)/ stride ] + 1 ,這個數值得是整數才行,不然沒有物理意義。譬如,你算得一個邊長爲 3.6 的 feature map 是沒有物理意義的。 pooling 層同理。FC 層的輸出形狀老是知足整數,其惟一的要求就是整個訓練過程當中 FC 層的輸入得是定長的。若是你的圖像不是正方形。那麼在製做數據時,能夠縮放到統一大小(非正方形),再使用非正方形的 kernel_size 來使得卷積層的輸出依然是整數。總之,撇開網絡結果設定的好壞不談,其本質上就是在作算術應用題:如何使得各層的輸出是整數。

  2.由經驗肯定。一般狀況下,靠近輸入的卷積層,譬如第一層卷積層,會找出一些共性的特徵,如手寫數字識別中第一層咱們設定卷積核個數爲5個,通常是找出諸如"橫線"、「豎線」、「斜線」等共性特徵,咱們稱之爲basic feature,通過max pooling後,在第二層卷積層,設定卷積核個數爲20個,能夠找出一些相對複雜的特徵,如「橫折」、「左半圓」、「右半圓」等特徵,越日後,卷積核設定的數目越多,越能體現label的特徵就越細緻,就越容易分類出來,打個比方,若是你想分類出「0」的數字,你看到這個特徵,能推測是什麼數字呢?只有越日後,檢測識別的特徵越多,試過能識別這幾個特徵,那麼我就可以肯定這個數字是「0」。

  3.有stride_w和stride_h,後者表示的就是上下步長。若是用stride,則表示stride_h=stride_w=stride。

 

 


  手寫數字識別的CNN網絡結構

  上面咱們瞭解了卷積神經網絡的基本結構後,如今來具體看一下在實際數據---手寫數字識別中是如何操做的。上文中我定義了一個最基本的CNN網絡。以下(代碼詳見github)

 1 def convolutional_neural_network_org(img):
 2     # first conv layer
 3     conv_pool_1 = paddle.networks.simple_img_conv_pool(
 4         input=img,
 5         filter_size=3,
 6         num_filters=20,
 7         num_channel=1,
 8         pool_size=2,
 9         pool_stride=2,
10         act=paddle.activation.Relu())
11     # second conv layer
12     conv_pool_2 = paddle.networks.simple_img_conv_pool(
13         input=conv_pool_1,
14         filter_size=5,
15         num_filters=50,
16         num_channel=20,
17         pool_size=2,
18         pool_stride=2,
19         act=paddle.activation.Relu())
20     # fully-connected layer
21     predict = paddle.layer.fc(
22         input=conv_pool_2, size=10, act=paddle.activation.Softmax())
23     return predict

 

  那麼它的網絡結構是:

  conv1----> conv2---->fully Connected layer

  很是簡單的網絡結構。第一層咱們採起的是3*3的正方形卷積核,個數爲20個,深度爲1,stride爲2,pooling尺寸爲2*2,激活函數採起的爲RELU;第二層只對卷積核的尺寸、個數和深度作了些變化,分別爲5*5,50個和20;最後連接一層全鏈接,設定10個label做爲輸出,採用Softmax函數做爲分類器,輸出每一個label的機率。

  那麼這個時候我考慮的問題是,既然上面咱們已經瞭解了卷積核,改變卷積核的大小是否會對個人結果形成影響?增多卷積核的數目可以提升準確率?因而我作了個實驗:

  •  第一次改進:僅改變第一層與第二層的卷積核數目的大小,其餘保持不變。能夠看到結果提高了0.06%
  •  第二次改進:保持3*3的卷積核大小,僅改變第二層的卷積核數目,其餘保持不變,能夠看到結果相較於原始參數提高了0.08%

  由以上結果能夠看出,改變卷積核的大小與卷積核的數目會對結果產生必定影響,在目前手寫數字識別的項目中,縮小卷積核尺寸,增長卷積核數目都會提升準確率。不過以上實驗只是一個小測試,有興趣的同窗能夠多作幾回實驗,看看參數帶來的具體影響,下篇文章咱們會着重分析參數的影響。

  這篇文章主要介紹了神經網絡的預備知識,卷積神經網絡的常見的層及基本的計算過程,看完後但願你們明白如下幾個知識點

  • 爲何卷積神經網絡更適合於圖像分類?相比於傳統的神經網絡優點在哪裏?
  • 卷積層中的卷積過程是如何計算的?爲何卷積核是有效的?
  • 卷積核的個數如何肯定?應該選擇多大的卷積覈對於模型來講纔是有效的?尺寸必須爲正方形嗎?若是是長方形因該怎麼作?
  • 步長的大小會對模型的效果產生什麼樣的影響?垂直方向和水平方向的步長是否得設定爲相同的?
  • 爲何要採用池化層,Max Pooling有什麼好處?
  • Zero Padding有什麼做用?若是已知一個feature map的尺寸,如何肯定zero padding的數目?

  

    上面的問題,有些在文章中已經詳細講過,有些你們能夠根據文章的內容多思考一下。最後給你們留幾個問題思考一下

  • 爲何改變卷積核的大小可以提升結果的準確率?卷積核大小對於分類結果是如何影響的?
  • 卷積核的參數是怎麼求的?一開始隨機定義一個,那麼後來是如何訓練才能使這個卷積核識別某些特定的特徵呢?
  • 1*1的卷積核有意義嗎?爲何有些網絡層結構裏會採用1*1的卷積核?

  

  下篇文章咱們會着重講解如下幾點

  • 卷積核的參數如何肯定?隨機初始化一個數值後,是如何訓練獲得一個可以識別某些特徵的卷積核的?
  • CNN是如何進行反向傳播的?
  • 如何調整CNN裏的參數?
  • 如何設計最適合的CNN網絡結構?
  • 可以不用調用框架的api,手寫一個CNN,並和paddlepaddle裏的實現過程作對比,看看有哪些能夠改進的?

 

 

ps:本篇文章是基於我的對CNN的理解來寫的,本人能力有限,有些地方可能寫的不是很嚴謹,若有錯誤或疏漏之處,請留言給我,我必定會仔細覈實並修改的^_^!不接受無腦噴哦~此外,文中的圖表結構均爲本身所作,但願不要被人隨意抄襲,能夠進行非商業性質的轉載,須要轉載留言或發郵件便可,但願可以尊重勞動成果,謝謝!有不懂的也請留言給我,我會盡力解答的哈~

相關文章
相關標籤/搜索