文章目录
实战Fashion-Mnsit
代码与上一篇文章基本相同,只是最下面改动成 每轮大循环都计算一下准确率,可以看出随着模型的训练、参数的修改,准确率不断波动提升
代码及运行截图
导入各种包
- layers里面包含了各种”层“,目前只用到Dense层(线性层)
- optimizers(优化器)里面包含了各种梯度下降方法,来优化模型的参数,目前用过“SGD”、“Adam”
- datasets包含了一些内置数据集,目前用过“Mnsit”、“Fashion-Mnsit”
- Sequential顺序模型是多个网络层的线性堆叠。将想要搭建的模型按顺序排放。与“layers”是包含关系
import os # “os”是系统包
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # TensorFlow的最小打印日志的等级为2。
# TensorFlow集成了深度学习环境,每次运行都会显示一些电脑环境信息,设置了这个值,就可以隐藏一些不紧要的输出,更简洁。
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, optimizers, datasets, Sequential
from __future__ import division
运行截图:
获取数据
(x, y), (x_val, y_val) = datasets.fashion_mnist.load_data()
# (x,y)是训练集;(x_val, y_val)是验证集。
print("训练集: ", x.shape, y.shape)
print(type(x),'/',type(y))
print(x.dtype, ' / ', y.dtype)
print('-----------------------------')
print("验证集: ", x_val.shape, y_val.shape)
print(type(x_val),'/',type(y_val))
print(x_val.dtype, ' / ', y_val.dtype)
运行截图:
处理数据
“除以255”的原因是“normalization method 归一化法”,对于计算机而言,更适合处理-1~1之间的数字
# 预处理函数,处理训练集、验证集时都用这个函数
def preprocessing(x_data, y_data):
x_data = tf.cast(x_data, dtype = tf.float32)/255 #不要忘记图片输入数据除以255
y_data = tf.cast(y_data, dtype = tf.int32)
#y_data = tf.one_hot(y_data, depth=10) #将标量y_data转换成独热编码y_data
print(f'类型为: {type(x_data)} ; {type(y_data)}')
print(f"各自里面元素类型为: {x_data.dtype} ; {y_data.dtype}")
print('数据格式为: ', x_data.shape, y_data.shape)
# print(tf.reduce_max(x_data), tf.reduce_min(x_data))
print('--------------------------------------')
return x_data, y_data
# 数据预处理,切片。600组,每组100对。切片、预处理顺序无所谓。一般是先切片。
# 下面两行是把数据切成“散片”,还没有batch
db_train = tf.data.Dataset.from_tensor_slices((x, y))
db_test = tf.data.Dataset.from_tensor_slices((x_val, y_val))
# 重点: 将 切成 散片的数据“反射”到与preprocessing()函数。
# 输出完毕后,再以100张图片为一组
print("训练集: ")
db_train = db_train.map(preprocessing).batch(100) # 每组100张图片,共 600 组
print("验证集: ")
db_test = db_test.map(preprocessing).batch(100)# 每组100张图片,共 100 组
运行截图:
# 看一下是否每一组是100张图片
# iter()将db_train变成可迭代对象,上一篇文章里面的train_dataset也可以这样操作
# sample是元组,保存了100张图片的输入、标签。里面有2个元素,某个100张图片的每张图片输入x、标签y
sample = next(iter(db_train))
# 直接用sample.shape错误。sample[0]是100个图片的输入x,sample[1]是100个标签y
print(sample[0].shape, sample[1].shape)
print('-----------------------')
print(sample[0])
print('-----------------------')
print(sample[1])
运行截图:
搭建模型
model = keras.Sequential([
layers.Dense(512, activation='relu', name='layer1'),
layers.Dense(256, activation='relu', name='layer2'),
layers.Dense(128, activation='relu', name='layer3'),
layers.Dense(64, activation='relu', name='layer4'),
layers.Dense(10, activation='relu', name='layer5'),
])
# 这里优化器不用SGD,换个用Adam,不用管具体区别,只要牢记优化器里的所有优化函数的核心就是“梯度下降”
optimizer = optimizers.Adam(learning_rate=0.0001)
# 下面两行代码只是用来看看这个框架都有些什么参数
model.build(input_shape=(None, 784)) # None意思是没有输入图片, 仅是测试而已
model.summary() # 用来看框架内部结构的函数
# 下面输出解读:
# 第一列Layer层:这里是Dense函数产生的层
# 第二列就是每道工序的“输出维度”
# 第三列的参数就是 w、b的个数,以第一层工序举例: 第一层的输入是[None ,784],
# 那么第一层的“W参数”就是[784, 512],“b参数”就是[512],所以784*512+512=401920个参数
运行截图:
使用“训练数据集”训练模型过程及每轮计算准确率
# 大循环,整个数据集(这里是6万张图片)循环的次数
for epoch in range(20):
print(f"当前是 第 {epoch} 次 大循环")
# 小循环,一次循环处理一个batch(这里是100张图片),共600次小循环
for step, (x, y) in enumerate(db_train):
# 打平操作
# x:[b, 28, 28] => [b, 784]
# y:[b, 1] => [b, 10]
x = tf.reshape(x, (-1, 28*28))
#将y转换成独热编码,可以放在预处理函数里,也可以放在这里
y = tf.one_hot(y, depth=10)
# 构建梯度环境
with tf.GradientTape() as tape:
# 用单词“logits”代表多分类问题的预测输出,也可以跟上一篇文章一样用“out”
# x的维度,注意要与模型搭建时给的输入维度一致
logits = model(x)
# 计算loss。下面loss_mse、loss_ce是两种计算损失值的方法
# 均方差
# tf.losses.MSE(y, logits)相当于tf.square(logits-y)
loss_mse = tf.reduce_mean(tf.losses.MSE(y, logits))
# 多分类常用的一种loss函数
# categorical: 分类;crossentropy: 交叉熵
loss_ce = tf.losses.categorical_crossentropy(y, logits, from_logits=True)
loss_ce = tf.reduce_mean(loss_ce) # 计算平均交叉熵损失
# 注意缩进
# 计算梯度,“model.trainable_variables”里面是各个w、b参数,更新参数w、b后也放入其中
# grads 里面是 w、b的梯度
grads = tape.gradient(loss_ce, model.trainable_variables)
# 更新梯度 w' = w - learning rate * grad w b' = b - learning rate * grad b
optimizer.apply_gradients(zip(grads, model.trainable_variables))
if step % 100 == 0:
print('epoch: {}, step: {}, loss: {}'.format(epoch, step, float(loss_ce)))
# 算每轮准确率
total_correct = 0
total_number = 0
for (xval, yval) in db_test:
xval = tf.reshape(xval, (-1, 784))
logits_test = model(xval)
probability = tf.nn.softmax(logits_test, axis=1)
predict = tf.argmax(probability, axis=1)
predict = tf.cast(predict, dtype=tf.int32)
bool_correct = tf.equal(predict, yval)
correct = tf.cast(bool_correct, dtype=tf.int32)
correct = tf.reduce_sum(correct)
total_correct += int(correct)
total_number += yval.shape[0]
print(f"正确个数为: {total_correct}; 总图片数是: {total_number}")
accuracy = total_correct / total_number
print(f"准确率为: {accuracy}")
print('---------------------------')
部分运行截图:
…
…
…
创建张量(tensor)
在 TensorFlow 中,可以通过多种方式创建张量,如从 Python 列表对象创建,从 Numpy 数组创建,或者创建采样自某种已知分布的张量等。
从数组、列表对象创建
Numpy Array 数组和 Python List 列表是 Python 程序中间非常重要的数据载体容器,很多数据都是通过 Python 语言将数据加载至 Array 或者 List 容器,再转换到 Tensor 类型,通过 TensorFlow 运算处理后导出到 Array 或者 List 容器,方便其他模块调用
。
通过 tf.convert_to_tensor 函数可以创建新 Tensor,并将保存在 Python List 对象或者 Numpy Array 对象中的数据导入到新 Tensor 中
,例如:
需要注意的是,Numpy 浮点数数组默认使用 64 位精度保存数据,转换到 Tensor 类型时精度为 tf.float64,可以在需要的时候将其转换为 tf.float32 类型。
实际上,tf.constant()和 tf.convert_to_tensor()都能够自动的把 Numpy 数组或者 Python 列表数据类型转化为 Tensor 类型
,这两个 API 命名来自 TensorFlow 1.x 的命名习惯,在 TensorFlow 2 中函数的名字并不是很贴切,使用其一即可。
创建全0或全1的张量
将张量创建为全 0 或者全 1 数据是非常常见的张量初始化手段。考虑线性变换𝒚 = 𝑾𝒙 + 𝒃,将权值矩阵𝑾初始化为全 1 矩阵,偏置 b 初始化为全 0 向量,此时线性变化层输出𝒚 = 𝒙,因此是一种比较好的层初始化状态。通过 tf.zeros()和 tf.ones()即可创建任意形状,且内容全 0 或全 1 的张量。
例如,创建为 0 和为 1 的标量:
In [24]: tf.zeros([]),tf.ones([]) # 创建全0,全1的标量
Out[24]:
(<tf.Tensor: id=90, shape=(), dtype=float32, numpy=0.0>,
<tf.Tensor: id=91, shape=(), dtype=float32, numpy=1.0>)
创建全 0 和全 1 的向量,实现如下:
In [25]: tf.zeros([1]),tf.ones([1]) # 创建全0,全1的向量
Out[25]:
(<tf.Tensor: id=96, shape=(1,), dtype=float32, numpy=array([0.],
dtype=float32)>,
<tf.Tensor: id=99, shape=(1,), dtype=float32, numpy=array([1.],
dtype=float32)>)
创建全 0 的矩阵,例如:
In [26]:tf.zeros([2,2]) # 创建全0矩阵,指定shape为2行2列
Out[26]:
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[0., 0.],
[0., 0.]], dtype=float32)>
创建全 1 的矩阵,例如:
In [27]:tf.ones([3,2]) # 创建全1矩阵,指定shape为3行2列
Out[27]:
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[1., 1.],
[1., 1.],
[1., 1.]], dtype=float32)>
通过 tf.zeros_like, tf.ones_like 可以方便地新建与某个张量 shape 一致,且内容为全 0 或 全 1 的张量。例如,创建与张量𝑨形状一样的全 0 张量:
创建与张量𝑨形状一样的全 1 张量:
张量索引
高维索引示例:
start:end
start : end : step、 : : step
: : -1
解释 a[ : : -2] : 先把数组倒过来,再从左至右依据步长每两步取值
创建已知分布的张量
正态分布(Normal Distribution,或 Gaussian Distribution)
和均匀分布(Uniform Distribution)
是最常见的分布之一,创建采样自这 2 种分布的张量非常有用,比如在卷积神经网络中,卷积核张量𝑾初始化为正态分布有利于网络的训练;在对抗生成网络中,隐藏变量𝒛一般采样自均匀分布。
务必注意下面两个函数第一个参数都是“shape(维度)”
正态分布(random.normal)
通过 tf.random.normal(shape, mean=0.0, stddev=1.0)可以创建形状为 shape,均值为 mean,标准差为 stddev 的正态分布𝒩(mean, stddev2)。
例如,创建均值为 0,标准差为 1 的正态分布:
In [33]:tf.random.normal([2,2]) # 创建标准正态分布的张量
Out[33]:
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-1.2156643 , -1.6855826 ],
[ 0.68651384, -0.60394895]], dtype=float32)>
务必注意[2, 2]指的是shape
例如,创建均值为 1,标准差为 2 的正态分布:
In [34]:tf.random.normal([2,2], mean=1,stddev=2) # 创建正态分布的张量
Out[34]:
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-3.175376 , 2.0691204 ],
[-2.5508618 , -0.33188474]], dtype=float32)>
均匀分布(random.uniform)
通过 tf.random.uniform(shape, minval=0, maxval=None, dtype=tf.float32)可以创建采样自 [minval, maxval)区间的均匀分布的张量。
例如创建采样自区间[0,1),shape 为[2,2]的矩阵:
In [35]:tf.random.uniform([2,2]) # 创建采样自[0,1)均匀分布的矩阵
Out[35]:
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[0.42767704, 0.37408912],
[0.27021766, 0.2663933 ]], dtype=float32)>
例如,创建采样自区间[0,10),shape 为[2,2]的矩阵:
In [36]: tf.random.uniform([2,2],maxval=10) # 创建采样自[0,10)均匀分布的矩阵
Out[36]:
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[4.2767706, 3.7408912],
[2.7021766, 2.663933 ]], dtype=float32)>
如果需要均匀采样整形类型的数据,必须指定采样区间的最大值 maxval 参数,同时指定数据类型为 tf.int*型:
In [37]: # 创建采样自[0, 100) 均匀分布的整型矩阵
tf.random.uniform([2,2],maxval=100,dtype=tf.int32)
Out [37]:
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[31, 20],
[ 3, 66]], dtype=int32)>
改变视图(reshape)
增、删维度
增加维度(expand_dims)
增加一个长度为 1 的维度相当于给原有的数据添加一个新维度,维度长度为 1,故数据并不需要改变,仅仅是改变数据的理解方式,因此它其实可以理解为改变视图的一种特殊方式。
考虑一个具体例子,一张28 × 28大小的灰度图片的数据保存为 shape 为[28,28]的张量,在末尾给张量增加一新维度,定义为通道数维度,此时张量的 shape 变为[28,28,1], 实现如下:
In [72]: # 产生矩阵
x = tf.random.uniform([28,28],maxval=10,dtype=tf.int32)
Out[72]:
<tf.Tensor: shape=(28, 28), dtype=int32, numpy=
array([[7, 3, 2, 1, 6, 5, 9, 0, 9, 9, 8, 6, 7, 5, 6, 4, 9, 7, 5, 1, 4, 4,
3, 7, 1, 9, 2, 8],
[7, 6, 0, 2, 9, 1, 0, 7, 2, 3, 0, 0, 0, 7, 4, 7, 9, 0, 0, 5, 4, 6,
8, 6, 9, 2, 7, 0],
[9, 0, 1, 6, 8, 2, 2, 0, 9, 2, 1, 1, 3, 6, 1, 4, 3, 2, 0, 4, 0, 2,
0, 9, 4, 9, 0, 6],.............
通过 tf.expand_dims(x, axis)可在指定的 axis 轴前
可以插入一个新的维度:
In [73]: x = tf.expand_dims(x,axis=2) # axis=2 表示宽维度后面的一个维度
Out[73]:
<tf.Tensor: shape=(28, 28, 1), dtype=int32, numpy=
array([[[7],
[3],
[2],
[1],
[6],
[5],
[9],
[0],
[9],
[9],
[8],................
可以看到,插入一个新维度后,数据的存储顺序并没有改变,依然按着7,3,2,1,6,5, ⋯的顺序保存,仅仅是在插入一个新的维度后,改变了数据的视图。
同样的方法,我们可以在最前面插入一个新的维度,并命名为图片数量维度,长度为 1,此时张量的 shape 变为[1,28,28,1],实现如下:
In [74]: x = tf.expand_dims(x,axis=0) # 高维度之前插入新维度
Out[74]:
<tf.Tensor: shape=(1, 28, 28, 1), dtype=int32, numpy=
array([[[[7],
[3],
[2],
[1],
[6],
[5],
[9],
[0],
[9],
[9],
[8],
[6],..................
需要注意的是,tf.expand_dims 的 axis 为正
时,表示在当前维度之前
插入一个新维度; 为负
时,表示当前维度之后
插入一个新的维度。以[b, h, w, c]张量为例,不同 axis 参数的实际插入位置如下图所示:
根据上面所述,用下面的例子复习一下概念:
删除维度(squeeze)
删除维度是增加维度的逆操作,与增加维度一样,删除维度只能删除长度为 1 的维度,也不会改变张量的存储。
继续考虑增加维度后 shape 为[1,28,28,1]的例子,如果希望将图片数量维度删除,可以通过 tf.squeeze(x, axis)函数,axis 参数为待删除的维度的索引号。
例如,图片数量的维度轴 axis=0:
In [75]: x = tf.squeeze(x, axis=0) # 删除图片数量维度
Out[75]:
<tf.Tensor: shape=(28, 28, 1), dtype=int32, numpy=
array([[[7],
[3],
[2],
[1],
[6],
[5],
[9],
[0],
[9],
[9],
[8],
[6],..........
继续删除通道数维度,由于已经删除了图片数量维度,此时的 x 的 shape 为[28,28,1],因此删除通道数维度时指定 axis=2,实现如下:
In [76]: x = tf.squeeze(x, axis=2) # 删除图片通道数维度
Out[76]:
<tf.Tensor: shape=(28, 28), dtype=int32, numpy=
array([[7, 3, 2, 1, 6, 5, 9, 0, 9, 9, 8, 6, 7, 5, 6, 4, 9, 7, 5, 1, 4, 4,
3, 7, 1, 9, 2, 8],
[7, 6, 0, 2, 9, 1, 0, 7, 2, 3, 0, 0, 0, 7, 4, 7, 9, 0, 0, 5, 4, 6,
8, 6, 9, 2, 7, 0],
[9, 0, 1, 6, 8, 2, 2, 0, 9, 2, 1, 1, 3, 6, 1, 4, 3, 2, 0, 4, 0, 2,
0, 9, 4, 9, 0, 6],..................
如果不指定维度参数 axis,即 tf.squeeze(x),那么它会默认删除所有长度为 1 的维度,例如(观察输出里面的shape):
交换维度—tf.transpose(x, perm)
改变视图、增删维度都不会影响张量的存储。在实现算法逻辑时,在保持维度顺序不变的条件下,仅仅改变张量的理解方式是不够的,有时需要直接调整shape的顺序,即交换维度(Transpose)。通过交换维度操作,改变了张量的存储顺序(改变了shape的顺序),同时也改变了张量的视图(改变了shape)
。
交换维度操作是非常常见的,比如在 TensorFlow 中,图片张量的默认存储格式是通道后行格式: [b, h, w, c],但是部分库的图片格式是通道先行格式: [b, c, h, w],因此需要完成 [b, h, w, c]到[b, c, h, w]维度交换运算,此时若简单的使用改变视图函数 reshape,则新视图的存储方式需要改变,因此使用改变视图函数是不合法的。
我们以[b, h, w, c]转换到[b, c, h, w]为例,介绍如何使用 tf.transpose(x, perm)函数完成维度交换操作,其中参数 perm 表示新维度的顺序 List
。考虑图片张量 shape 为[2,32,32,3],
“图片数量、行、列、通道 数”的维度索引
分别为 0、1、2、3,如果需要交换为[b, c, h, w]格式,则新维度的排序为 “图片数量、通道数、行、列”,对应的索引号
为[0,3,1,2],因此参数 perm 需设置为 [0,3,1,2],实现如下:
注意参数perm里面放的是索引
如果希望将[b, h, w, c]交换为[b, w, h, c],即将高、宽维度互换,则新维度索引为 [0,2,1,3],实现如下:
需要注意的是,通过 tf.transpose 完成维度交换后,张量的存储顺序已经改变,视图也随之改变,后续的所有操作必须基于新的存续顺序和视图进行。相对于改变视图操作,维度交换操作的计算代价更高。
根据上面概念,复习思考下面例子: