把Github上TensorLayer的示例——tutorial_cifar10_tfrecord.py看了一下,做了一些中文注解,在此分享一下,方便学习Tensorflow的童鞋们快速了解之。针对TF r0.12的情况,将源代码做了相应的改动。
win10_x64 + Python3.5 + CUDA8.0 + Cudnn5.1环境下运行正常。
#! /usr/bin/python
# -*- coding: utf8 -*-
import tensorflow as tf
import tensorlayer as tl
# from tensorlayer.layers import set_keep
import numpy as np
import time
from PIL import Image
import os
import io
"""重现 TensorFlow 官方 CIFAR-10 卷积神经网络 指导书:
- 该模型有 1,068,298 个参数, 使用GPU训练数小时后准确率可达86%.
描述
-----------
图片作如下处理:
.. 图片被裁剪为 24 x 24 像素, 集中评估或随机训练.
.. 为使模型对动态范围不敏感,图片被近似白化.
为了改善训练效果,我们还对图片应用了一系列的随机变换来人为地增加数据集的大小:
.. 随机左右翻转.
.. 随机改变图片亮度.
.. 随机改变图片对比度.
加速
--------
从磁盘读取图像并进行变换耗费了不短的处理时间。为减轻这些操作对训练速度的影响,
我们在16个独立的线程运行,不断填补tensorflow队列
"""
model_file_name = r"W:\cifar10\models\model_cifar10_advanced.ckpt" # win10下自定义的model和checkpoint文件保存位置
resume = False # 载入已存在的模型, 从之前的checkpoint重新开始吗?
## 下载数据集, 并转化为TFRecord格式
X_train, y_train, X_test, y_test = tl.files.load_cifar10_dataset(
shape=(-1, 32, 32, 3), plotable=False)
X_train = np.asarray(X_train, dtype=np.float32)
y_train = np.asarray(y_train, dtype=np.int64)
X_test = np.asarray(X_test, dtype=np.float32)
y_test = np.asarray(y_test, dtype=np.int64)
print('X_train.shape', X_train.shape) # (50000, 32, 32, 3)
print('y_train.shape', y_train.shape) # (50000,)
print('X_test.shape', X_test.shape) # (10000, 32, 32, 3)
print('y_test.shape', y_test.shape) # (10000,)
print('X %s y %s' % (X_test.dtype, y_test.dtype))
def data_to_tfrecord(images, labels, filename): # 定义格式转化函数(转化为TFRecord格式)
""" 将数据转化为TFRecord格式 """
print("Converting data into %s ..." % filename)
cwd = os.getcwd() # 获取当前目录路径
writer = tf.python_io.TFRecordWriter(filename) # 创建TFRecord格式文件
for index, img in enumerate(images):
img_raw = img.tobytes() # 将numpy数组类型的图像转化为bytes类型
## 可视化一张图像
# tl.visualize.frame(np.asarray(img, dtype=np.uint8), second=1, saveable=False, name='frame', fig_idx=1236)
label = int(labels[index])
# print(label)
## 将bytes格式文件转换回图像的操作如下:
# image = Image.frombytes('RGB', (32, 32), img_raw)
# image = np.fromstring(img_raw, np.float32)
# image = image.reshape([32, 32, 3])
# tl.visualize.frame(np.asarray(image, dtype=np.uint8), second=1, saveable=False, name='frame', fig_idx=1236)
example = tf.train.Example(features=tf.train.Features(feature={
"label": tf.train.Feature(int64_list=tf.train.Int64List(value=[label])),
'img_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[img_raw])),
}))
writer.write(example.SerializeToString()) # Serialize To String
writer.close() # 关闭文件
def read_and_decode(filename, is_train=None):
""" 从TFRecord文件读取并返回tensor """
filename_queue = tf.train.string_input_producer([filename])
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
features = tf.parse_single_example(serialized_example,
features={
'label': tf.FixedLenFeature([], tf.int64),
'img_raw': tf.FixedLenFeature([], tf.string),
})
# You can do more image distortion here for training data
img = tf.decode_raw(features['img_raw'], tf.float32)
img = tf.reshape(img, [32, 32, 3])
# img = tf.cast(img, tf.float32) #* (1. / 255) - 0.5
if is_train == True:
# 1. 随机裁剪图像中[height, width] 的一部分.
img = tf.random_crop(img, [24, 24, 3])
# 2. 随机水平翻转.
img = tf.image.random_flip_left_right(img)
# 3. 随机改变亮度.
img = tf.image.random_brightness(img, max_delta=63)
# 4. 随机改变对比度.
img = tf.image.random_contrast(img, lower=0.2, upper=1.8)
# 5. 标准化(去均值,除方差).
img = tf.image.per_image_standardization(img)
elif is_train == False:
# 1. 裁剪图片中央的[height, width]部分.
img = tf.image.resize_image_with_crop_or_pad(img, 24, 24)
# 2. 标准化(去均值,除方差).
img = tf.image.per_image_standardization(img)
elif is_train == None:
img = img
label = tf.cast(features['label'], tf.int32)
return img, label
data_to_tfrecord(images=X_train, labels=y_train, filename="train.cifar10")
data_to_tfrecord(images=X_test, labels=y_test, filename="test.cifar10")
## 数据可视化举例
# img, label = read_and_decode("train.cifar10", None)
# img_batch, label_batch = tf.train.shuffle_batch([img, label],
# batch_size=4,
# capacity=50000,
# min_after_dequeue=10000,
# num_threads=1)
# print("img_batch : %s" % img_batch._shape)
# print("label_batch : %s" % label_batch._shape)
#
# init = tf.global_variables_initializer() # 定义初始化graph中所有变量的op
# with tf.Session() as sess: # 开启会话
# sess.run(init) # 变量初始化
# coord = tf.train.Coordinator() # 创建一个线程协调器
# threads = tf.train.start_queue_runners(sess=sess, coord=coord) # 创建线程
#
# for i in range(3): # number of mini-batch (step)
# print("Step %d" % i)
# val, l = sess.run([img_batch, label_batch])
# # exit()
# print(val.shape, l)
# tl.visualize.images2d(val, second=1, saveable=False, name='batch'+str(i), dtype=np.uint8, fig_idx=2020121)
#
# coord.request_stop() # 请求终止所有线程
# coord.join(threads) # 等待所有线程终止
# sess.close()
# with tf.device('/gpu:1'): # 使用GPU1
# sess = tf.InteractiveSession() # 开启交互式会话
batch_size = 128 # 设置batch大小
with tf.device('/cpu:0'): # 使用CPU0
# 定义会话,为了避免指定的设备不存在的情况,allow_soft_placement设为True可以使程序自动选择一个存在并且支持的设备来运行op
sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True))
# 准备数据
x_train_, y_train_ = read_and_decode("train.cifar10", True)
x_test_, y_test_ = read_and_decode("test.cifar10", False)
x_train_batch, y_train_batch = tf.train.shuffle_batch([x_train_, y_train_],
batch_size=batch_size,
capacity=2000,
min_after_dequeue=1000,
num_threads=32) # 这里设置线程数
# 测试时, 使用 batch 替代 shuffle_batch
x_test_batch, y_test_batch = tf.train.batch([x_test_, y_test_],
batch_size=batch_size,
capacity=50000,
num_threads=32)
def inference(x_crop, y_, reuse):
with tf.variable_scope("model", reuse=reuse): # 开启一个scope(作用域),指定可否复用
tl.layers.set_name_reuse(reuse) # 允许layer名称复用,当你想要两个或多个input placeholder(inference)共享相同的模型参数时
network = tl.layers.InputLayer(x_crop, name='input_layer') # 定义输入层
network = tl.layers.Conv2dLayer(network, # 定义卷积层
act=tf.nn.relu, # 定义激活函数,使用relu函数
shape=[5, 5, 3, 64], # 卷积核的shape:[高度, 宽度, 输入通道数, 输出通道数]
strides=[1, 1, 1, 1], # 卷积核的滑动步长strides = [1, 垂直步长, 水平步长, 1]
padding='SAME', # 定义padding方式为‘SAME’
W_init=tf.truncated_normal_initializer(stddev=5e-2),# 定义权值矩阵初始化器为0均值和指定标准差的截尾正态分布
b_init=tf.constant_initializer(value=0.0),# 定义偏置向量的初始化器为初值为0的常量初始化器
name='cnn_layer1') # 本层输出: (batch_size, 24, 24, 64)
network = tl.layers.PoolLayer(network, # 定义池化层
ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1],
padding='SAME',
pool=tf.nn.max_pool, # 定义池化方式为最大值池化
name='pool_layer1', ) # 本层输出:(batch_size, 12, 12, 64)
network.outputs = tf.nn.lrn(network.outputs, 4, bias=1.0, alpha=0.001 / 9.0,
beta=0.75, name='norm1') # Local Response Normalization局部响应正则化,详见Alex论文
network = tl.layers.Conv2dLayer(network,
act=tf.nn.relu,
shape=[5, 5, 64, 64],
strides=[1, 1, 1, 1],
padding='SAME',
W_init=tf.truncated_normal_initializer(stddev=5e-2),
b_init=tf.constant_initializer(value=0.1),
name='cnn_layer2') # 本层输出: (batch_size, 12, 12, 64)
network.outputs = tf.nn.lrn(network.outputs, 4, bias=1.0, alpha=0.001 / 9.0,
beta=0.75, name='norm2')
network = tl.layers.PoolLayer(network,
ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1],
padding='SAME',
pool=tf.nn.max_pool,
name='pool_layer2') # 本层输出: (batch_size, 6, 6, 64)
# 定义Flatten层,将多维输入压平(flatten)为一个向量,为全连接层(DenseLayer)做准备
network = tl.layers.FlattenLayer(network, name='flatten_layer') # 本层输出: (batch_size, 2304)
network = tl.layers.DenseLayer(network, n_units=384, act=tf.nn.relu, # 定义全连接层
W_init=tf.truncated_normal_initializer(stddev=0.04),
b_init=tf.constant_initializer(value=0.1),
name='relu1') # 本层输出: (batch_size, 384)
network = tl.layers.DenseLayer(network, n_units=192, act=tf.nn.relu,
W_init=tf.truncated_normal_initializer(stddev=0.04),
b_init=tf.constant_initializer(value=0.1),
name='relu2') # 本层输出: (batch_size, 192)
network = tl.layers.DenseLayer(network, n_units=10, act=tf.identity,
W_init=tf.truncated_normal_initializer(stddev=1 / 192.0),
b_init=tf.constant_initializer(value=0.0),
name='output_layer') # 本层输出: (batch_size, 10)
y = network.outputs
# 计算交叉熵损失ce,因此网络没有softmax层,因而使用softmax_cross_entropy_with_logits函数
ce = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(y, y_))
# L2范数正则化,防止过学习,没有它准确率将下降15%.
L2 = tf.contrib.layers.l2_regularizer(0.004)(network.all_params[4]) + \
tf.contrib.layers.l2_regularizer(0.004)(network.all_params[6])
cost = ce + L2 # 定义网络的损失函数
# correct_prediction = tf.equal(tf.argmax(tf.nn.softmax(y), 1), y_)
correct_prediction = tf.equal(tf.cast(tf.argmax(y, 1), tf.int32), y_) # 判断预测是否准确
acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) # 计算预测准确率
return cost, acc, network
def inference_batch_norm(x_crop, y_, reuse, is_train):
"""
For batch normalization, the normalization should be placed after cnn
with linear activation.
"""
with tf.variable_scope("model", reuse=reuse):
tl.layers.set_name_reuse(reuse)
network = tl.layers.InputLayer(x_crop, name='input_layer')
network = tl.layers.Conv2dLayer(network,
act=tf.identity, #定义激活函数为线性函数,即y = x
shape=[5, 5, 3, 64],
strides=[1, 1, 1, 1],
padding='SAME',
W_init=tf.truncated_normal_initializer(stddev=5e-2),
# b_init=tf.constant_initializer(value=0.0),
b_init=None, # 无偏置项
name='cnn_layer1') # 本层输出: (batch_size, 24, 24, 64)
# 定义batch正则化层,详见论文Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
network = tl.layers.BatchNormLayer(network, is_train=is_train, name='batch_norm1')
network.outputs = tf.nn.relu(network.outputs, name='relu1') # 对输出进行relu操作
network = tl.layers.PoolLayer(network,
ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1],
padding='SAME',
pool=tf.nn.max_pool,
name='pool_layer1', ) # 本层输出: (batch_size, 12, 12, 64)
network = tl.layers.Conv2dLayer(network,
act=tf.identity,
shape=[5, 5, 64, 64],
strides=[1, 1, 1, 1],
padding='SAME',
W_init=tf.truncated_normal_initializer(stddev=5e-2),
# b_init=tf.constant_initializer(value=0.1),
b_init=None,
name='cnn_layer2') # 本层输出: (batch_size, 12, 12, 64)
network = tl.layers.BatchNormLayer(network, is_train=is_train, name='batch_norm2')
network.outputs = tf.nn.relu(network.outputs, name='relu2')
network = tl.layers.PoolLayer(network,
ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1],
padding='SAME',
pool=tf.nn.max_pool,
name='pool_layer2') # 本层输出: (batch_size, 6, 6, 64)
network = tl.layers.FlattenLayer(network, name='flatten_layer')
network = tl.layers.DenseLayer(network, n_units=384, act=tf.nn.relu,
W_init=tf.truncated_normal_initializer(stddev=0.04),
b_init=tf.constant_initializer(value=0.1),
name='relu1') # 本层输出: (batch_size, 384)
network = tl.layers.DenseLayer(network, n_units=192, act=tf.nn.relu,
W_init=tf.truncated_normal_initializer(stddev=0.04),
b_init=tf.constant_initializer(value=0.1),
name='relu2') # 本层输出: (batch_size, 192)
network = tl.layers.DenseLayer(network, n_units=10, act=tf.identity,
W_init=tf.truncated_normal_initializer(stddev=1 / 192.0),
b_init=tf.constant_initializer(value=0.0),
name='output_layer') # 本层输出: (batch_size, 10)
y = network.outputs
ce = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(y, y_))
# L2范数正则化,防止过学习,没有它准确率将下降15%.
L2 = tf.contrib.layers.l2_regularizer(0.004)(network.all_params[4]) + \
tf.contrib.layers.l2_regularizer(0.004)(network.all_params[6])
cost = ce + L2
# correct_prediction = tf.equal(tf.argmax(tf.nn.softmax(y), 1), y_)
correct_prediction = tf.equal(tf.cast(tf.argmax(y, 1), tf.int32), y_)
acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
return cost, acc, network
## You can also use placeholder to feed_dict in data after using
## val, l = sess.run([x_train_batch, y_train_batch]) to get the data
# x_crop = tf.placeholder(tf.float32, shape=[batch_size, 24, 24, 3])
# y_ = tf.placeholder(tf.int32, shape=[batch_size,])
# cost, acc, network = inference(x_crop, y_, None)
with tf.device('/gpu:0'): # 使用GPU
cost, acc, network = inference(x_train_batch, y_train_batch, None)
cost_test, acc_test, _ = inference(x_test_batch, y_test_batch, True)
## 你可以尝试batch正则化方法
# cost, acc, network = inference_batch_norm(x_train_batch, y_train_batch, None, is_train=True)
# cost_test, acc_test, _ = inference_batch_norm(x_test_batch, y_test_batch, True, is_train=False)
## 训练
n_epoch = 50000 # 定义epoch数
learning_rate = 0.0001 # 定义学习速率
print_freq = 1 # 定义打印频率
n_step_epoch = int(len(y_train) / batch_size) # 计算每个epoch的迭代次数
n_step = n_epoch * n_step_epoch # 计算总的迭代次数
with tf.device('/gpu:0'):# 使用GPU
# 在GPU上训练
train_params = network.all_params # 待训练的参数为所有的网络参数
# 定义训练操作,使用自适应矩估计(ADAM)算法最小化损失函数
train_op = tf.train.AdamOptimizer(learning_rate, beta1=0.9, beta2=0.999,
epsilon=1e-08, use_locking=False).minimize(cost) # , var_list=train_params)
sess.run(tf.global_variables_initializer()) # 初始化所有变量
if resume: # 若是要从之前的checkpoint重新开始
print("Load existing model " + "!" * 10)
saver = tf.train.Saver()
saver.restore(sess, model_file_name) # 载入之前的model
network.print_params(False) # 不打印网络参数信息
network.print_layers() # 打印网络各个layer的信息
print(' learning_rate: %f' % learning_rate)
print(' batch_size: %d' % batch_size)
print(' n_epoch: %d, step in an epoch: %d, total n_step: %d' % (n_epoch, n_step_epoch, n_step))
coord = tf.train.Coordinator() # 创建一个线程协调器
threads = tf.train.start_queue_runners(sess=sess, coord=coord) # 创建线程
# for step in range(n_step):
step = 0
for epoch in range(n_epoch): # 对每一个epoch
start_time = time.time()# 计时开始
train_loss, train_acc, n_batch = 0, 0, 0 # 初始化为0
for s in range(n_step_epoch): # 对每一次迭代
## You can also use placeholder to feed_dict in data after using
# val, l = sess.run([x_train_batch, y_train_batch])
# tl.visualize.images2d(val, second=3, saveable=False, name='batch', dtype=np.uint8, fig_idx=2020121)
# err, ac, _ = sess.run([cost, acc, train_op], feed_dict={x_crop: val, y_: l})
err, ac, _ = sess.run([cost, acc, train_op]) # 计算损失函数、训练准确率
step += 1
train_loss += err
train_acc += ac
n_batch += 1
if epoch + 1 == 1 or (epoch + 1) % print_freq == 0:# 按预定的打印频率打印训练信息
print("Epoch %d : Step %d-%d of %d took %fs" % (
epoch, step, step + n_step_epoch, n_step, time.time() - start_time))
print(" train loss: %f" % (train_loss / n_batch))
print(" train acc: %f" % (train_acc / n_batch))
test_loss, test_acc, n_batch = 0, 0, 0 #打印测试信息
for _ in range(int(len(y_test) / batch_size)):
err, ac = sess.run([cost_test, acc_test])
test_loss += err
test_acc += ac
n_batch += 1
print(" test loss: %f" % (test_loss / n_batch))
print(" test acc: %f" % (test_acc / n_batch))
if (epoch + 1) % (print_freq * 20) == 0: # 定义保存model的时机
print("Save model " + "!" * 10)
saver = tf.train.Saver()
save_path = saver.save(sess, model_file_name)
coord.request_stop()# 请求终止所有线程
coord.join(threads)# 等待终止所有线程
sess.close()#关闭会话
另外,对涉及到的一些函数做一下说明:
1.tl.visualize.frame(I=None, second=5, saveable=True, name='frame', cmap=None, fig_idx=12836)
功能:显示一帧(图像).使用前确保 OpenAI Gym render() 为失效状态。
参数说明:
———-
I : numpy.array, numpy数组格式的图像
second : int,图像显示的时间(以秒计),若saveable为False.
saveable : boolean,保存(True) 或 画出图像(False).
name : a string,保存图像使用的名称, 若saveable为True.
cmap : None or string, ‘gray’ -灰度化, None-默认
fig_idx : int,matplotlib图像索引.
2.tf.train.string_input_producer(string_tensor, num_epochs=None, shuffle=True, seed=None, capacity=32, shared_name=None, name=None, cancel_op=None)
功能: 输出的字符串(例如文件名)到一个输入管道的队列。
参数说明:
———-
string_tensor: 1-D的字符串tensor.
num_epochs: 整数 (可选). 若指定该参数, string_input_producer
在出现OutOfRange
错误前从string_tensor
产生 num_epochs
次字符串。若不指定,string_input_producer
从string_tensor
循环地产生字符串无限多次。
shuffle: 布尔量. 若为true, 字符串在epoch内随机洗牌.
seed: 整数(可选).在shuffle == True使用.
capacity: 整数. 设置队列的容量.
shared_name: (可选). 若设置,则该队列将通过指定的名称在多个sessions中被共享.
name:操作名(可选).
cancel_op: 为该队列取消op(可选)
3.tf.cast(x, dtype, name=None)
功能:将tensor铸型为指定的类型。
参数说明:
———-
x: Tensor
或SparseTensor
.
dtype: 目标类型.
name: 操作名(可选)
4.tf.shuffle_batch(tensors, batch_size, capacity, min_after_dequeue, num_threads=1, seed=None, enqueue_many=False, shapes=None, allow_smaller_final_batch=False, shared_name=None, name=None)
功能:对tensors随机洗牌创建batches。
参数说明:
———-
tensors: 列表 或 tensors到enqueue的字典.
batch_size: batch大小.
capacity: 整数. 队列中元素的最大个数.
min_after_dequeue: 一次出列后,队列中最少的元素个数,用于保证元素的混合程度
num_threads: 列队 tensor_list
的线程数.
seed: 随机洗牌的seed
enqueue_many: tensor_list
的tensor是否为单个样本.
shapes: (可选l) 每个样本的shape.
allow_smaller_final_batch: (可选) 布尔量.若为True`, 当队列中剩下的元素不足时,允许最后的batch 小于预定值.
shared_name: (可选) 若设置,则该队列将通过指定的名称在多个sessions中被共享.
name: (可选)操作名.
5.start_queue_runners(sess=None, coord=None, daemon=True, start=True, collection='queue_runners')
功能: 为graph中所有的queue runners开启线程. 返回所有线程的列表。
参数说明:
———-
sess: 运行队列ops的会话.
coord: 可选,用于协调开始的线程的协调器.
daemon:是否将线程标记为守护进程, 这意味着他们不阻止程序退出.
start: 设置为False表示只创建进程,不开始进程.
6.variable_scope(name_or_scope, default_name=None, values=None, initializer=None, regularizer=None, caching_device=None, partitioner=None, custom_getter=None, reuse=None, dtype=None)
功能: 用于定义创建 variables (layers)的op的上下文管理器,返回一个能被捕获和复用的作用域(scope).
参数说明:
———-
name_or_scope: string
或 VariableScope
: 要开启的作用域(Scope).
default_name:使用的默认名称,若name_or_scope参数为None.若name_or_scope已存在,则它不被使用,因而可以不设置或为None.
values: 传递到op函数的 Tensor
参数列表
regularizer: 作用域内的变量的默认的正则化矩阵
caching_device: 作用域内的变量的默认的缓存设备.
partitioner: 作用域内的变量的默认的分割器.
custom_getter: 作用域内的变量的默认的custom getter
reuse: True
或 None
; 若为True
,将作用域及其所有的子作用域进入复用模式;若为 None
, 则仅继承其父作用域的复用模式.
dtype: 此作用域内创建的变量的类型
7.tl.layers.FlattenLayer
功能:把多维输入压平成一个向量 ,以应用到 DenseLayer, RNNLayer, ConcatLayer 等,[batch_size, mask_row, mask_col, n_mask] —> [batch_size, mask_row * mask_col * n_mask]
参数说明:
———-
layer : 供给到本层的layer.
name : a string or None 本层的名称(可选)
8.关于TF的Padding方式
有 ‘SAME’ 和 ‘VALID’两种。
对于’SAME’ padding, 输出的高和宽计算如下:
垂直步长:strides[1],水平步长:strides[2]
输出图像高度 = ceil(输入图像的高度 / 垂直步长) # ceil表示向上取整
输出图像宽度 = ceil(输入图像的宽度 / 水平步长)
顶部和左侧的padding计算如下:
pad_沿高度 = (输出图像高度 - 1) * 垂直步长 + 滤波器高度 - 输入图像的高度
pad_沿宽度 = (输出图像宽度 - 1) * 水平步长 + 滤波器宽度 - 输入图像的宽度
pad_顶部 = pad_沿高度/ 2
pad_左侧 = pad_沿宽度 / 2
注意的是当不能被2整除时,底部和右侧总是比顶部和左侧多一个像素。 比如, pad_沿高度 = 5时, pad 2个像素到顶部、3 个像素到底部。这与 cuDNN和Caffe不同,它们总是在两边pad同样的像素。
对于’VALID’ padding, 输出的高和宽计算如下:
输出图像高度 = ceil((输入图像的高度 - 滤波器高度 + 1) / 垂直步长)
输出图像宽度 = ceil((输入图像的宽度 - 滤波器宽度 + 1) / 水平步长)
padding值总是0. 原始输入图像区域外的任何值都被认为是零 (即填充0到图像的边界).