天天看點

深度學習:卷積層的實作卷積層的資料shape和普通層的資料shape差别:卷積層實作實作池化層實作 CNN 中的特殊層結構實作 LayerFactory網絡結構

文章目錄

  • 卷積層的資料shape和普通層的資料shape差别:
  • 卷積層實作
  • 實作池化層
  • 實作 CNN 中的特殊層結構
  • 實作 LayerFactory
  • 網絡結構

卷積層的資料shape和普通層的資料shape差别:

針對一般圖像資料shape: Npq,圖像就是二維浮點資料,N為資料個數,p,q為圖像的次元。

卷積層的中間層資料shape: Npq*r,r為channels。

資料的shape必須非常清楚,因為假如自己處理卷積層就需要用到shape

卷積層實作

1、卷積層自身多了 Kernel 這個屬性并是以帶來了諸如 Stride、Padding 等屬性,不過與此同時、卷積層之間沒有權值矩陣,

2、卷積層和普通層的shape屬性記錄的東西不同,具體而言:

普通層的shape記錄着上個 Layer 和該 Layer 所含神經元的個數

卷積層的shape記錄着上個卷積層的輸出和該卷積層的 Kernel 的資訊(注意卷積層的上一層必定還是卷積層)

3、卷積填充有2種方式,tesorflow支援兩種方式:一是不填充VALID,二是全部填充SAME,沒有部分填充的方式,假如需要實作部分填充,就需要在資料預處理填充0,然後使用VALID方式卷積。

深度學習:卷積層的實作卷積層的資料shape和普通層的資料shape差别:卷積層實作實作池化層實作 CNN 中的特殊層結構實作 LayerFactory網絡結構

padding 可以為VALID,可以為SAME,也可以為int整型數,為int整型數時就是自填充資料。

class ConvLayer(Layer):
    """
        初始化結構
        self.shape:記錄着上個卷積層的輸出和該Layer的Kernel的資訊,具體而言:
            self.shape[0] = 上個卷積層的輸出的形狀(頻道數×高×寬)
                常簡記為self.shape[0] =(c,h_old,w_old)
            self.shape[1] = 該卷積層Kernel的資訊(Kernel數×高×寬)
                常簡記為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的高、寬無關、隻受Stride的影響
            else:
                self.padding = self.pad_flag = "SAME"
        # 如果輸入了一個整數、那麼就按照VALID情形設定Padding相關的屬性
        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]
        # 根據Padding的相關資訊、計算輸出的高、寬
        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)
            
            
class ConvLayerMeta(type):
    def __new__(mcs, *args, **kwargs):
        name, bases, attr = args[:3]
        # 規定繼承的順序為ConvLayer→Layer
        conv_layer, layer = bases

        def __init__(self, shape, stride=1, padding="SAME"):
            conv_layer.__init__(self, shape, stride, padding)

        # 利用Tensorflow的相應函數定義計算卷積的方法
        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相應函數進行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)
    
    
class ConvReLU(ConvLayer, ReLU, metaclass=ConvLayerMeta):
    pass
   
           

實作池化層

對于最常見的兩種池化——極大池化和平均池化,Kernel 個數從數值上來說與輸出頻道個數一緻,是以對于池化層的實作而言、我們應該直接用輸入頻道數來指派 Kernel 數,因為池化不會改變資料的頻道數。

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
    
# 實作極大池化
class MaxPool(ConvPoolLayer):
    def _activate(self, x, *args):
        return tf.nn.max_pool

# 實作平均池化
class AvgPool(ConvPoolLayer):
    def _activate(self, x, *args):
        return tf.nn.avg_pool
    
           

實作 CNN 中的特殊層結構

在 CNN 中同樣有着 Dropout 和 Normalize 這兩種特殊層結構,CNN 則通常是N×p×q×r的、其中r是目前資料的頻道數。将 CNN 中r個頻道的資料放在一起并視為 NN 中的一個神經元,這樣做的話就能通過簡易的封裝來直接利用上我們對 NN 定義的特殊層結構。

# 定義作為封裝的元類
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)
            # 與池化層類似、特殊層輸出資料的形狀應保持與輸入資料的形狀一緻
            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中的特殊層結構的相應方法獲得結果
        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


           

實作 LayerFactory

集合所有的layer,這樣就可以通過字元串索引到對應的layer

class LayerFactory:
    # 使用一個字典記錄下所有的Root Layer
    available_root_layers = {
        "Tanh": Tanh, "Sigmoid": Sigmoid,
        "ELU": ELU, "ReLU": ReLU, "Softplus": Softplus,
        "Identical": Identical,
        "CrossEntropy": CrossEntropy, "MSE": MSE,
        "ConvTanh": ConvTanh, "ConvSigmoid": ConvSigmoid,
        "ConvELU": ConvELU, "ConvReLU": ConvReLU, "ConvSoftplus": ConvSoftplus,
        "ConvIdentical": ConvIdentical,
        "MaxPool": MaxPool, "AvgPool": AvgPool
    }
    # 使用一個字典記錄下所有特殊層
    available_special_layers = {
        "Dropout": Dropout,
        "Normalize": Normalize,
        "ConvDrop": ConvDrop,
        "ConvNorm": ConvNorm
    }
    # 使用一個字典記錄下所有特殊層的預設參數
    special_layer_default_params = {
        "Dropout": (0.5,),
        "Normalize": ("Identical", 1e-8, 0.9),
        "ConvDrop": (0.5,),
        "ConvNorm": ("Identical", 1e-8, 0.9)
    }
    
    # 定義根據“名字”擷取(Root)Layer的方法
    def get_root_layer_by_name(self, name, *args, **kwargs):
        # 根據字典判斷輸入的名字是否是Root Layer的名字
        if name in self.available_root_layers:
            # 若是、則傳回相應的Root Layer
            layer = self.available_root_layers[name]
            return layer(*args, **kwargs)
        # 否則傳回None
        return None
    # 定義根據“名字”擷取(任何)Layer的方法
    def get_layer_by_name(self, name, parent, current_dimension, *args, **kwargs):
        # 先看輸入的是否是Root Layer
        _layer = self.get_root_layer_by_name(name, *args, **kwargs)
        # 若是、直接傳回相應的Root Layer
        if _layer:
            return _layer, None
        # 否則就根據父層和相應字典進行初始化後、傳回相應的特殊層
        _current, _next = parent.shape[1], current_dimension
        layer_param = self.special_layer_default_params[name]
        _layer = self.available_special_layers[name]
        if args or kwargs:
            _layer = _layer(parent, (_current, _next), *args, **kwargs)
        else:
            _layer = _layer(parent, (_current, _next), *layer_param)
        return _layer, (_current, _next)  

           

網絡結構

class NN(ClassifierBase):
    def __init__(self):
        super(NN, self).__init__()
        self._layers = []
        self._optimizer = None
        self._current_dimension = 0
        self._available_metrics = {
            key: value for key, value in zip(["acc", "f1-score"], [NN.acc, NN.f1_score])
        }
        self.verbose = 0
        self._metrics, self._metric_names, self._logs = [], [], {}
        self._layer_factory = LayerFactory()
        # 定義Tensorflow中的相應變量
        self._tfx = self._tfy = None  # 記錄每個Batch的樣本、标簽的屬性
        self._tf_weights, self._tf_bias = [], []  # 記錄w、b的屬性
        self._cost = self._y_pred = None  # 記錄損失值、輸出值的屬性
        self._train_step = None  # 記錄“參數更新步驟”的屬性
        self._sess = tf.Session()  # 記錄Tensorflow Session的屬性
    # 利用Tensorflow相應函數初始化參數
    @staticmethod
    def _get_w(shape):
        initial = tf.truncated_normal(shape, stddev=0.1)
        return tf.Variable(initial, name="w")
    @staticmethod
    def _get_b(shape):
        return tf.Variable(np.zeros(shape, dtype=np.float32) + 0.1, name="b")
    # 做一個初始化參數的封裝,要注意相容CNN
    def _add_params(self, shape, conv_channel=None, fc_shape=None, apply_bias=True):
        # 如果是FC的話、就要根據鋪平後資料的形狀來初始化資料
        if fc_shape is not None:
            w_shape = (fc_shape, shape[1])
            b_shape = shape[1],
        # 如果是卷積層的話、就要定義Kernel而非權值矩陣
        elif conv_channel is not None:
            if len(shape[1]) <= 2:
                w_shape = shape[1][0], shape[1][1], conv_channel, conv_channel
            else:
                w_shape = (shape[1][1], shape[1][2], conv_channel, shape[1][0])
            b_shape = shape[1][0],
        # 其餘情況和普通NN無異
        else:
            w_shape = shape
            b_shape = shape[1],
        self._tf_weights.append(self._get_w(w_shape))
        if apply_bias:
            self._tf_bias.append(self._get_b(b_shape))
        else:
            self._tf_bias.append(None)
    # 由于特殊層不會用到w和b、是以要定義一個生成占位符的方法
    def _add_param_placeholder(self):
        self._tf_weights.append(tf.constant([.0]))
        self._tf_bias.append(tf.constant([.0]))