python cnn_Python · CNN(一)· 层结构
(這里是最終成品的 GitHub 地址)
(這里是本章用到的 GitHub 地址)
========== 寫在前面的話 ==========
其實在 4 個月之前我寫過一篇叫“Python · 神經(jīng)網(wǎng)絡(八)· ConvLayer”的文章,不過現(xiàn)在看回去覺得寫的有點太概括性了;如果直接往下寫的話,估計觀眾老爺們(以及我自己)的邏輯都理不順 _(:з」∠)_
所以我打算重寫一次,而且這次會對之前 NN 系列的文章做一個匯總性說明;換句話說,我會從頭開始講如何實現(xiàn) CNN 而不是接著 NN 的邏輯來講(這也是為什么我沒有接著用“神經(jīng)網(wǎng)絡”這個系列名而是開了個新的“CNN”系列) _(:з」∠)_
這意味著本文(及接下來的 CNN 系列)會巨長無比,畢竟我會試圖把兩三百行的東西一次性講清楚 _(:з」∠)_
如果覺得這些都無所謂并愿意看的話,我會覺得很開心的 _(:з」∠)_
一些數(shù)學基礎(chǔ):數(shù)學 · 神經(jīng)網(wǎng)絡(二)· BP(反向傳播)數(shù)學 · CNN · 從 NN 到 CNN
========== 分割線的說 ==========
往簡單里說、CNN 只是多了卷積層、池化層和 FC 的 NN 而已,雖然卷積、池化對應的前向傳導算法和反向傳播算法的高效實現(xiàn)都很不平凡,但得益于 Tensorflow 的強大、我們可以在僅僅知道它們思想的前提下進行相應的實現(xiàn),因為 Tensorflow 能夠幫我們處理所有數(shù)學與技術(shù)上的細節(jié)(Tensorflow 的應用式入門教程可以參見這里)
實現(xiàn)普通層
我們在Python · 神經(jīng)網(wǎng)絡(一)· 層和Python · 神經(jīng)網(wǎng)絡(二)· 層里面非常瑣碎地說明了如何實現(xiàn) Layer 結(jié)構(gòu),這里我們就詳盡地把整個實現(xiàn)捋一捋。鑒于 Tensorflow 能夠自動獲取梯度、同時考慮到要擴展出 CNN 的功能,我們需要實現(xiàn)如下功能:對于激活函數(shù),只用定義其原始形式、不必定義其導函數(shù)形式
解決特殊層結(jié)構(gòu)(Dropout、Normalize 等等)的實現(xiàn)問題
要考慮當前層為 FC(全連接層)時的表現(xiàn)
讓用戶可以選擇是否給 Layer 加偏置量
其中的第四點可能有些讓人不明所以:要知道偏置量可是對破壞對稱性是很重要的,為什么要讓用戶選擇是否使用偏置量呢?這主要是因為特殊層結(jié)構(gòu)中 Normalize 的特殊性會使偏置量顯得冗余。具體細節(jié)會在后文討論特殊層結(jié)構(gòu)處進行說明,這里就暫時按下不表
以下是 Layer 結(jié)構(gòu)基類的具體代碼:
import numpy as np
import tensorflow as tf
from math import ceil
class Layer:
"""初始化結(jié)構(gòu)self.shape:記錄該Layer和上個Layer所含神經(jīng)元的個數(shù),具體而言:self.shape[0] = 上個Layer所含神經(jīng)元的個數(shù)self.shape[1] = 該Layer所含神經(jīng)元的個數(shù)self.is_fc、self.is_sub_layer:記錄該Layer是否為FC、特殊層結(jié)構(gòu)的屬性self.apply_bias:記錄是否對該Layer加偏置量的屬性"""
def __init__(self, shape, **kwargs):
self.shape = shape
self.is_fc = self.is_sub_layer = False
self.apply_bias = kwargs.get("apply_bias", True)
def __str__(self):
return self.__class__.__name__
def __repr__(self):
return str(self)
@property
def name(self):
return str(self)
@property
def root(self):
return self
# 定義兼容特殊層結(jié)構(gòu)和CNN的、前向傳導算法的封裝
def activate(self, x, w, bias=None, predict=False):
# 如果當前層是FC、就需要先將輸入“鋪平”
if self.is_fc:
x = tf.reshape(x, [-1, int(np.prod(x.get_shape()[1:]))])
# 如果是特殊的層結(jié)構(gòu)、就調(diào)用相應的方法獲得結(jié)果
if self.is_sub_layer:
return self._activate(x, predict)
# 如果不加偏置量的話、就只進行矩陣相乘和激活函數(shù)的作用
if not self.apply_bias:
return self._activate(tf.matmul(x, w), predict)
# 否則就進行“最正常的”前向傳導算法
return self._activate(tf.matmul(x, w) + bias, predict)
# 前向傳導算法的核心、留待子類定義
def _activate(self, x, predict):
pass
注意到我們前向傳導算法中有一項“predict”參數(shù),這主要是因為特殊層結(jié)構(gòu)的訓練過程和預測過程表現(xiàn)通常都會不一樣、所以要加一個標注。該標注的具體意義會在后文進行特殊層結(jié)構(gòu) SubLayer 的相關(guān)說明時體現(xiàn)出來、這里暫時按下不表
在實現(xiàn)好基類后、就可以實現(xiàn)具體要用在神經(jīng)網(wǎng)絡中的 Layer 了。以 Sigmoid 激活函數(shù)對應的 Layer 為例:
class Sigmoid(Layer):
def _activate(self, x, predict):
return tf.nn.sigmoid(x)
得益于 Tensorflow 框架的強大(你除了這句話就沒別的話說了嗎……)、我們甚至連激活函數(shù)的形式都無需手寫,因為它已經(jīng)幫我們封裝好了(事實上、絕大多數(shù)常用的激活函數(shù)在 Tensorflow 里面都有封裝)
實現(xiàn)特殊層
我們在Python · 神經(jīng)網(wǎng)絡(三*)· 網(wǎng)絡這里曾經(jīng)簡要介紹過特殊層 SubLayer 的思想,這里我們將介紹如何利用 Tensorflow 框架實現(xiàn)它,同時也會對十分常用的兩種 SubLayer —— Dropout 和 Normalize 做深入一些的介紹
先來看看應該如何定義 SubLayer 的基類:
# 讓SubLayer繼承Layer以合理復用代碼
class SubLayer(Layer):
"""初始化結(jié)構(gòu)self.shape:和Layer相應屬性意義一致self.parent:記錄該Layer的父層的屬性self.description:用于可視化的屬性,記錄著對該SubLayer的“描述”"""
def __init__(self, parent, shape):
Layer.__init__(self, shape)
self.parent = parent
self.description = ""
# 輔助獲取Root Layer的property
@property
def root(self):
_root = self.parent
while _root.parent:
_root = _root.parent
return _root
可以看到,得益于 Tensorflow 框架(Tensorflow 就是很厲害嘛……),本來難以處理的SubLayer 的實現(xiàn)變得非常簡潔清晰。在實現(xiàn)好基類后、就可以實現(xiàn)具體要用在神經(jīng)網(wǎng)絡中的 SubLayer 了,先來看 Dropout:
class Dropout(SubLayer):
# self._prob:訓練過程中每個神經(jīng)元被“留下”的概率
def __init__(self, parent, shape, drop_prob=0.5):
# 神經(jīng)元被Drop的概率必須大于等于0和小于1
if drop_prob < 0 or drop_prob >= 1:
raise ValueError(
"(Dropout) Probability of Dropout should be a positive float smaller than 1")
SubLayer.__init__(self, parent, shape)
# 被“留下”的概率自然是1-被Drop的概率
self._prob = tf.constant(1 - drop_prob, dtype=tf.float32)
self.description = "(Drop prob: {})".format(drop_prob)
def _activate(self, x, predict):
# 如果是在訓練過程,那么就按照設(shè)定的、被“留下”的概率進行Dropout
if not predict:
return tf.nn.dropout(x, self._prob)
# 如果是在預測過程,那么直接返回輸入值即可
return x
Dropout 的詳細說明自然是看原 paper最好,這里我就大概翻譯、總結(jié)一下主要內(nèi)容。Dropout 的核心思想在于提高模型的泛化能力:它會在每次迭代中依概率去掉對應 Layer 的某些神經(jīng)元,從而每次迭代中訓練的都是一個小的神經(jīng)網(wǎng)絡。這個過程可以通過下圖進行說明:
上圖所示的即為當 drop_prob 為 50%(我們所設(shè)的默認值)時、Dropout 的一種可能的表現(xiàn)。左圖所示為原網(wǎng)絡、右圖所示的為 Dropout 后的網(wǎng)絡,可以看到神經(jīng)元 a、b、e、g、j 都被 Drop 了
Dropout 過程的合理性需要概率論上一些理論的支撐,不過鑒于 Tensorflow 框架有封裝好的相應函數(shù)、我們就不深入介紹其具體的數(shù)學原理而僅僅說明其直觀(以 drop_prob 為 50%為例,其余 drop_prob 的情況是同理的):在訓練過程中,由于 Dropout 后留下來的神經(jīng)元可以理解為“在 50%死亡概率下幸存”的神經(jīng)元,所以給將它們對應的輸出進行“增幅”是合理的。具體而言,假設(shè)一個神經(jīng)元
的輸出本來是
,那么如果 Dropout 后它被留下來了的話、其輸出就應該變成
(換句話說、應該讓帶 Dropout 的期望輸出和原輸出一致:對于任一個神經(jīng)元
,設(shè) drop_prob 為p 而其原輸出為
,那么當帶 Dropout 的輸出為
時、
的期望輸出即為
)
由于在訓練時我們保證了神經(jīng)網(wǎng)絡的期望輸出不變、所以在預測過程中我們還是應該讓整個網(wǎng)絡一起進行預測而不進行 Dropout(關(guān)于這一點,原論文似乎也表示這是一種“經(jīng)試驗證明行之有效”的辦法而沒有給出具體的、原理層面的說明)
Normalize 說起來有點長,所以我開了一個單獨的章節(jié)來說(數(shù)學 · 神經(jīng)網(wǎng)絡(四)· Normalize)。下面就直接看看如何實現(xiàn)它:
class Normalize(SubLayer):
"""初始化結(jié)構(gòu)self._eps:記錄增強數(shù)值穩(wěn)定性所用的小值的屬性self._activation:記錄自身的激活函數(shù)的屬性,主要是為了兼容圖7.17 A的情況self.tf_rm、self.tf_rv:記錄μ_run、σ_run^2的屬性self.tf_gamma、self.tf_beta:記錄γ、β的屬性self._momentum:記錄動量值m的屬性"""
def __init__(self, parent, shape, activation="Identical", eps=1e-8, momentum=0.9):
SubLayer.__init__(self, parent, shape)
self._eps, self._activation = eps, activation
self.tf_rm = self.tf_rv = None
self.tf_gamma = tf.Variable(tf.ones(self.shape[1]), name="norm_scale")
self.tf_beta = tf.Variable(tf.zeros(self.shape[1]), name="norm_beta")
self._momentum = momentum
self.description = "(eps: {}, momentum: {})".format(eps, momentum)
def _activate(self, x, predict):
# 若μ_run、σ_run^2還未初始化,則根據(jù)輸入x進行相應的初始化
if self.tf_rm is None or self.tf_rv is None:
shape = x.get_shape()[-1]
self.tf_rm = tf.Variable(tf.zeros(shape), trainable=False, name="norm_mean")
self.tf_rv = tf.Variable(tf.ones(shape), trainable=False, name="norm_var")
if not predict:
# 利用Tensorflow相應函數(shù)計算當前Batch的舉止、方差
_sm, _sv = tf.nn.moments(x, list(range(len(x.get_shape()) - 1)))
_rm = tf.assign(
self.tf_rm, self._momentum * self.tf_rm + (1 - self._momentum) * _sm)
_rv = tf.assign(
self.tf_rv, self._momentum * self.tf_rv + (1 - self._momentum) * _sv)
# 利用Tensorflow相應函數(shù)直接得到Batch Normalization的結(jié)果
with tf.control_dependencies([_rm, _rv]):
_norm = tf.nn.batch_normalization(
x, _sm, _sv, self.tf_beta, self.tf_gamma, self._eps)
else:
_norm = tf.nn.batch_normalization(
x, self.tf_rm, self.tf_rv, self.tf_beta, self.tf_gamma, self._eps)
# 如果指定了激活函數(shù)、就再用相應激活函數(shù)作用在BN結(jié)果上以得到最終結(jié)果
# 這里只定義了ReLU和Sigmoid兩種,如有需要可以很方便地進行拓展
if self._activation == "ReLU":
return tf.nn.relu(_norm)
if self._activation == "Sigmoid":
return tf.nn.sigmoid(_norm)
return _norm
實現(xiàn)損失層
# 定義一個簡單的基類
class CostLayer(Layer):
# 定義一個方法以獲取損失值
def calculate(self, y, y_pred):
return self._activate(y_pred, y)
# 定義Cross Entropy對應的CostLayer(整合了Softmax變換)
class CrossEntropy(CostLayer):
def _activate(self, x, y):
return tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=x, labels=y))
# 定義MSE準則對應的CostLayer
class MSE(CostLayer):
def _activate(self, x, y):
return tf.reduce_mean(tf.square(x - y))
我自己用 Numpy 寫的話,相同功能要寫那么個 113 行,然后用 Tensorflow 的話 15 行就行了……由此可窺見 Tensorflow 框架的強大
(話說我這么賣力地安利 Tensorflow,Google 是不是應該給我些廣告費什么的)(喂
實現(xiàn)卷積層
回憶我們說過的卷積層和普通層的性質(zhì)、不難發(fā)現(xiàn)它們的表現(xiàn)極其相似,區(qū)別大體上來說只在于如下三點(以下我們用
、
、
表示第 i 層的輸入、輸出、激活函數(shù)):普通層自身對數(shù)據(jù)的處理只有“激活”(
)這一個步驟,層與層之間的數(shù)據(jù)傳遞則是通過權(quán)值矩陣、偏置量(
、
)和線性變換(
)來完成的;卷積層自身對數(shù)據(jù)的處理則多了“卷積”這個步驟(通常來說是先卷積再激活:
)、同時層與層之間的數(shù)據(jù)傳遞是直接傳遞的(
)
卷積層自身多了 Kernel 這個屬性并因此帶來了諸如 Stride、Padding 等屬性,不過與此同時、卷積層之間沒有權(quán)值矩陣
卷積層和普通層的 shape 屬性記錄的東西不同,具體而言:普通層的 shape 記錄著上個 Layer 和該 Layer 所含神經(jīng)元的個數(shù)
卷積層的 shape 記錄著上個卷積層的輸出和該卷積層的 Kernel 的信息(注意卷積層的上一層必定還是卷積層)
接下來就看看具體實現(xiàn):
class ConvLayer(Layer):
"""初始化結(jié)構(gòu)self.shape:記錄著上個卷積層的輸出和該Layer的Kernel的信息,具體而言:self.shape[0] = 上個卷積層的輸出的形狀(頻道數(shù)×高×寬)常簡記為self.shape[0] =(c,h_old,w_old)self.shape[1] = 該卷積層Kernel的信息(Kernel數(shù)×高×寬)常簡記為self.shape[1] =(f,h_new,w_new)self.stride、self.padding:記錄Stride、Padding的屬性self.parent:記錄父層的屬性"""
def __init__(self, shape, stride=1, padding="SAME", parent=None):
if parent is not None:
_parent = parent.root if parent.is_sub_layer else parent
shape = _parent.shape
Layer.__init__(self, shape)
self.stride = stride
# 利用Tensorflow里面對Padding功能的封裝、定義self.padding屬性
if isinstance(padding, str):
# "VALID"意味著輸出的高、寬會受Kernel的高、寬影響,具體公式后面會說
if padding.upper() == "VALID":
self.padding = 0
self.pad_flag = "VALID"
# "SAME"意味著輸出的高、寬與Kernel的高、寬無關(guān)、只受Stride的影響
else:
self.padding = self.pad_flag = "SAME"
# 如果輸入了一個整數(shù)、那么就按照VALID情形設(shè)置Padding相關(guān)的屬性
else:
self.padding = int(padding)
self.pad_flag = "VALID"
self.parent = parent
if len(shape) == 1:
self.n_channels = self.n_filters = self.out_h = self.out_w = None
else:
self.feed_shape(shape)
# 定義一個處理shape屬性的方法
def feed_shape(self, shape):
self.shape = shape
self.n_channels, height, width = shape[0]
self.n_filters, filter_height, filter_width = shape[1]
# 根據(jù)Padding的相關(guān)信息、計算輸出的高、寬
if self.pad_flag == "VALID":
self.out_h = ceil((height - filter_height + 1) / self.stride)
self.out_w = ceil((width - filter_width + 1) / self.stride)
else:
self.out_h = ceil(height / self.stride)
self.out_w = ceil(width / self.stride)
上述代碼的最后幾行對應著下述兩個公式、這兩個公式在 Tensorflow 里面有著直接對應的實現(xiàn):當 Padding 設(shè)置為 VALID 時,輸出的高、寬分別為:
其中,符號“
”代表著“向上取整”,stride 代表著步長
當 Padding 設(shè)置為 SAME 時,輸出的高、寬分別為:
同時不難看出、上述代碼其實沒有把 CNN 的前向傳導算法囊括進去,這是因為考慮到卷積層會利用到普通層的激活函數(shù)、所以期望能夠合理復用代碼。所以期望能夠把上述代碼定義的 ConvLayer 和前文重寫的 Layer 整合在一起以成為具體用在 CNN 中的卷積層,為此我們需要利用到 Python 中一項比較高級的技術(shù)——元類(元類的介紹可以參見這里):
class ConvLayerMeta(type):
def __new__(mcs, *args, **kwargs):
name, bases, attr = args[:3]
# 規(guī)定繼承的順序為ConvLayer→Layer
conv_layer, layer = bases
def __init__(self, shape, stride=1, padding="SAME"):
conv_layer.__init__(self, shape, stride, padding)
# 利用Tensorflow的相應函數(shù)定義計算卷積的方法
def _conv(self, x, w):
return tf.nn.conv2d(x, w, strides=[self.stride] * 4, padding=self.pad_flag)
# 依次進行卷積、激活的步驟
def _activate(self, x, w, bias, predict):
res = self._conv(x, w) + bias
return layer._activate(self, res, predict)
# 在正式進行前向傳導算法之前、先要利用Tensorflow相應函數(shù)進行Padding
def activate(self, x, w, bias=None, predict=False):
if self.pad_flag == "VALID" and self.padding > 0:
_pad = [self.padding] * 2
x = tf.pad(x, [[0, 0], _pad, _pad, [0, 0]], "CONSTANT")
return _activate(self, x, w, bias, predict)
# 將打包好的類返回
for key, value in locals().items():
if str(value).find("function") >= 0:
attr[key] = value
return type(name, bases, attr)
在定義好基類和元類后、定義實際應用在 CNN 中的卷積層就非常簡潔了。以在深度學習中應用最廣泛的 ReLU 卷積層為例:
class ConvReLU(ConvLayer, ReLU, metaclass=ConvLayerMeta):
pass
實現(xiàn)池化層
池化層比起卷積層而言要更簡單一點:對于最常見的兩種池化——極大池化和平均池化而言,它們所做的只是取輸入的極大值和均值而已、本身并沒有可以更新的參數(shù)。是故對池化層而言,我們無需維護其 Kernel、而只用定義相應的池化方法(極大、平均)即可,因此我們要求用戶在調(diào)用池化層時、只提供“高”和“寬”而不提供“Kernel 個數(shù)”
注意:Kernel 個數(shù)從數(shù)值上來說與輸出頻道個數(shù)一致,所以對于池化層的實現(xiàn)而言、我們應該直接用輸入頻道數(shù)來賦值 Kernel 數(shù),因為池化不會改變數(shù)據(jù)的頻道數(shù)
class ConvPoolLayer(ConvLayer):
def feed_shape(self, shape):
shape = (shape[0], (shape[0][0], *shape[1]))
ConvLayer.feed_shape(self, shape)
def activate(self, x, w, bias=None, predict=False):
pool_height, pool_width = self.shape[1][1:]
# 處理Padding
if self.pad_flag == "VALID" and self.padding > 0:
_pad = [self.padding] * 2
x = tf.pad(x, [[0, 0], _pad, _pad, [0, 0]], "CONSTANT")
# 利用self._activate方法進行池化
return self._activate(None)(
x, ksize=[1, pool_height, pool_width, 1],
strides=[1, self.stride, self.stride, 1], padding=self.pad_flag)
def _activate(self, x, *args):
pass
同樣的,由于 Tensorflow 已經(jīng)幫助我們做好了封裝、我們可以直接調(diào)用相應的函數(shù)來完成極大池化和平均池化的實現(xiàn):
# 實現(xiàn)極大池化
class MaxPool(ConvPoolLayer):
def _activate(self, x, *args):
return tf.nn.max_pool
# 實現(xiàn)平均池化
class AvgPool(ConvPoolLayer):
def _activate(self, x, *args):
return tf.nn.avg_pool
實現(xiàn) CNN 中的特殊層結(jié)構(gòu)
在 CNN 中同樣有著 Dropout 和 Normalize 這兩種特殊層結(jié)構(gòu)。它們的表現(xiàn)和 NN 中相應特殊層結(jié)構(gòu)的表現(xiàn)是完全一致的,區(qū)別只在于作用的對象不同
我們知道,CNN 每一層數(shù)據(jù)的維度要比 NN 中每一層數(shù)據(jù)的維度多一維:一個典型的 NN 中每一層的數(shù)據(jù)通常是
的,而 CNN 則通常是
的、其中 r是當前數(shù)據(jù)的頻道數(shù)。為了讓適用于 NN 的特殊層結(jié)構(gòu)適配于 CNN,一個自然而合理的做法就是將r 個頻道的數(shù)據(jù)當做一個整體來處理、或說將 CNN 中r 個頻道的數(shù)據(jù)放在一起并視為 NN 中的一個神經(jīng)元,這樣做的話就能通過簡易的封裝來直接利用上我們對 NN 定義的特殊層結(jié)構(gòu)。封裝的過程則仍要用到元類:
# 定義作為封裝的元類
class ConvSubLayerMeta(type):
def __new__(mcs, *args, **kwargs):
name, bases, attr = args[:3]
conv_layer, sub_layer = bases
def __init__(self, parent, shape, *_args, **_kwargs):
conv_layer.__init__(self, None, parent=parent)
# 與池化層類似、特殊層輸出數(shù)據(jù)的形狀應保持與輸入數(shù)據(jù)的形狀一致
self.out_h, self.out_w = parent.out_h, parent.out_w
sub_layer.__init__(self, parent, shape, *_args, **_kwargs)
self.shape = ((shape[0][0], self.out_h, self.out_w), shape[0])
# 如果是CNN中的Normalize、則要提前初始化好γ、β
if name == "ConvNorm":
self.tf_gamma = tf.Variable(tf.ones(self.n_filters), name="norm_scale")
self.tf_beta = tf.Variable(tf.zeros(self.n_filters), name="norm_beta")
# 利用NN中的特殊層結(jié)構(gòu)的相應方法獲得結(jié)果
def _activate(self, x, predict):
return sub_layer._activate(self, x, predict)
def activate(self, x, w, bias=None, predict=False):
return _activate(self, x, predict)
# 將打包好的類返回
for key, value in locals().items():
if str(value).find("function") >= 0 or str(value).find("property"):
attr[key] = value
return type(name, bases, attr)
# 定義CNN中的Dropout,注意繼承順序
class ConvDrop(ConvLayer, Dropout, metaclass=ConvSubLayerMeta):
pass
# 定義CNN中的Normalize,注意繼承順序
class ConvNorm(ConvLayer, Normalize, metaclass=ConvSubLayerMeta):
pass
以上就是所有層結(jié)構(gòu)的相關(guān)實現(xiàn)了……看到這里的觀眾老爺們真的要給你們筆芯!至少我是看不下去的(喂
實例
感謝評論區(qū)@崔斯特的建議,我打算弄些栗子……不過雖然我非常努力地憋了三個栗子,但總感覺不太對勁……總之歡迎各種吐槽和各種意見 ( σ'ω')σ
第一個栗子是普通層的栗子,假設(shè)我們的輸入矩陣為:
亦即有 4 個樣本、每個樣本的維度是 5 維。然后我們的權(quán)值矩陣為:
偏置量則簡單地取為
。現(xiàn)在我們要計算
的話,核心代碼只有兩行:
# Identical 為“無激活函數(shù)”的意思
# 需要提供輸入維度( 5 )和輸出維度( 2 )
nn_id = Identical([5, 2])
# 調(diào)用相應函數(shù)進行計算
# 其中 eval 是為了把數(shù)值從 Tensorflow 的 Graph 中提取出來
print(nn_id.activate(nn_x, nn_w, nn_b).eval())
完整代碼如下:
with tf.Session().as_default() as sess:
nn_x = np.array([
[ 0, 1, 2, 1, 0],
[-1, -2, 0, 2, 1],
[ 0, 1, -2, -1, 2],
[ 1, 2, -1, 0, -2]
], dtype=np.float32)
nn_w = np.array([
[-2, -1, 0, 1, 2],
[ 2, 1, 0, -1, -2]
], dtype=np.float32).T
nn_b = 1.
nn_id = Identical([nn_x.shape[1], 2])
print(nn_id.activate(nn_x, nn_w, nn_b).eval())
上面這段代碼將會輸出:
要計算 Sigmoid 的話,只需要把 Identical 換成 Sigmoid 即可
第二、三個栗子是卷積的過程,我們統(tǒng)一假設(shè)輸入只有一個樣本、頻道也只有一個
第二個栗子是無 Padding 無 Stride 的情形,假設(shè)唯一的頻道(Channel)所對應的矩陣如下:
假設(shè)我們的卷積核(Kernel)有兩個 Channel:
再假設(shè)我們的偏置量為
?,F(xiàn)在我們要計算相應的卷積時,核心代碼仍只有兩行:
# 接收的參數(shù)中,第一個是輸入的 shape,第二個是 Kernel 的 shape,具體而言:
# 輸入的 shape 為 height x width x channel = 4 x 4 x 1
# Kernel 的 shape 為 channel x height x width = 2 x 3 x 3
conv_id = ConvIdentical([([4, 4, 1], [2, 3, 3])], padding="VALID")
可能有觀眾老爺看到這就想吐槽:為什么輸入的 channel 放在最后,而 Kernel 的 channel 放在前面?其中的原因主要有兩點:Tensorflow 默認 channel 在最后
我在用 Numpy 實現(xiàn)框架時把 channel 放在了前面
然后……然后就是為了兼容、就變成這樣了(捂臉
不得不說把 channel 放在最后是非常合乎自然語言邏輯的:比如在描述圖片時,我們會自然地說它是
的圖片,其中最后那個 3 就是 channel
那么為什么我用 Numpy 實現(xiàn)時把 channel 放在了前面呢?因為這樣的數(shù)組輸出時會更好看(捂臉)
就拿我們這第二個栗子來說吧,如果把 channel 放在最后:
conv_x = np.array([
[
[ 0, 2, 1, 2],
[-1, 0, 0, 1],
[ 1, 1, 0, 1],
[-2, 1, -1, 0]
]
], dtype=np.float32).reshape(1, 4, 4, 1)
# 第一個 1 代表樣本數(shù),最后那個 1 代表 channel 數(shù)
這樣的矩陣打印出來是這樣子的:
換句話說,同一個 channel 的東西會被放在同一列(很丑對不對!!);而如果我們把 channel 放前面:
conv_x = np.array([
[
[ 0, 2, 1, 2],
[-1, 0, 0, 1],
[ 1, 1, 0, 1],
[-2, 1, -1, 0]
]
], dtype=np.float32).reshape(1, 1, 4, 4)
# 第一個 1 代表樣本數(shù),第二個 1 代表 channel 數(shù)
這樣的矩陣打印出來是這樣子的:
好看多了對不對!!
總之大概就這么個感覺……接下來看看第二個栗子的完整代碼:
with tf.Session().as_default() as sess:
conv_x = np.array([
[
[ 0, 2, 1, 2],
[-1, 0, 0, 1],
[ 1, 1, 0, 1],
[-2, 1, -1, 0]
]
], dtype=np.float32).reshape(1, 4, 4, 1)
# 這里有些兼容 Tensorflow 的 trick,大抵可以不必太在意……
conv_w = np.array([
[[ 1, 0, 1],
[-1, 0, 1],
[ 1, 0, -1]],
[[0, 1, 0],
[1, 0, -1],
[0, -1, 1]]
], dtype=np.float32).transpose([1, 2, 0])[..., None, :]
conv_b = np.array([1, -1], dtype=np.float32)
conv_id = ConvIdentical([(conv_x.shape[1:], [2, 3, 3])], padding="VALID")
print(conv_id.activate(conv_x, conv_w, conv_b).eval())
上面這段代碼將會輸出:
稍微解釋一下,比如說左上角的 4 是這樣求得的:
右上角的 -1 是這樣求得的:
這里需要特別指出的是,Kernel 的第一個 channel 卷積出來的結(jié)果在第一列、第二個卷積出來的則在第二列
如果想計算帶 ReLU 的卷積的話,把上述 ConvIdentical 改成 ConvReLU 即可
第三個栗子是 Padding、Stride 均為 1 的情形,假設(shè)唯一的 Channel 所對應的矩陣如下:
加了 1 的 Padding 之后、輸入將變?yōu)?#xff1a;
假設(shè) Kernel、偏置量都不變,那么在上述代碼的基礎(chǔ)上、只需如下的代碼即可完成第三個栗子所要求的卷積:
conv_x = np.array([
[
[ 1, 2, 1],
[-1, 0, -2],
[ 1, -1, 2]
]
], dtype=np.float32).reshape(1, 3, 3, 1)
conv_id = ConvIdentical([([3, 3, 1], [2, 3, 3])], padding=1, stride=2)
print(conv_id.activate(conv_x, conv_w, conv_b).eval())
上面這段代碼將會輸出:
下一章會說明如何定義一個網(wǎng)絡結(jié)構(gòu)來封裝我們這章講的這些層結(jié)構(gòu),然后我們就能實際地跑跑 CNN 了 ( σ'ω')σ
希望觀眾老爺們能夠喜歡~
總結(jié)
以上是生活随笔為你收集整理的python cnn_Python · CNN(一)· 层结构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: apt-clone:备份已安装的软件包并
- 下一篇: wxpython bind自定义_wxP