日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

python cnn_Python · CNN(一)· 层结构

發(fā)布時間:2023/12/19 python 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 python cnn_Python · CNN(一)· 层结构 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

(這里是最終成品的 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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。