文章目錄
- 卷積層的資料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方式卷積。
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]))