import os
import tensorflow as tf
from tensorflow.keras import datasets
# 索引
x = tf.random.normal([4, 32, 32, 3]) # 创建 4D 张量
# 取第 1 张图片的数据
print(x[0])
# 取第 1 张图片的第 2 行
print(x[0][1])
# 取第 1 张图片,第 2 行,第 3 列的数据
print(x[0][1][2])
# 取第 3 张图片,第 2 行,第 1 列的像素,B 通道(第 2 个通道)颜色强度值
print(x[2][1][0][1])
# 当张量的维度数较高时,使用[𝑗][𝑘]...[𝑙]的方式书写不方便,可以采用[𝑗,𝑘,…,𝑙]的方式索引,它们是等价的
# 切片:start:end:step
# 读取第 2,3 张图片
print(x[1:3])
# 读取第 1 张图片的所有行
print(x[0, ::])
# 为了更加简洁,::可以简写为单个冒号:
print(x[:, 0:28:2, 0:28:2, :])
# # start:end:step 从 start 开始读取到 end(不包含 end),步长为 step
# # start:end 从 start 开始读取到 end(不包含 end),步长为 1
# # start: 从 start 开始读取完后续所有元素,步长为 1
# # start::step 从 start 开始读取完后续所有元素,步长为 step
# # :end:step 从 0 开始读取到 end(不包含 end),步长为 step
# # :end 从 0 开始读取到 end(不包含 end),步长为 1
# # ::step 步长为 step 采样
# # :: 读取所有元素
# # : 读取所有元素
# # step 可以为负数,考虑最特殊的一种例子,当step = −1时,start:end:−1表示从 start 开始,逆序读取至 end 结束(不包含 end),索引号𝑓𝑜𝑒 ≤ 𝑡𝑢𝑏𝑠𝑢。
x = tf.range(9) # 创建 0~9 向量
print(x[8:0:-1]) # 从 8 取到 0,逆序,不包含 0
x[::-1] # 逆序全部元素
x[::-2] # 逆序间隔采样
x = tf.random.normal([4, 32, 32, 3])
x[0, ::-2, ::-2] # 行、列逆序间隔采样
x[:, :, :, 1] # 取 G 通道数据
# # a,⋯,b a 维度对齐到最左边,b 维度对齐到最右边,中间的维度全部读取,其他维度按 a/b 的方式读取
# # a,⋯ a 维度对齐到最左边,a 维度后的所有维度全部读取,a 维度按 a 方式读取。这种情况等同于 a 索引/切片方式
# # ⋯,b b 维度对齐到最右边,b 之前的所有维度全部读取,b 维度按 b 方式读取
# # ⋯ 读取张量所有数据
# x[0:2, ..., 1:] # 高宽维度全部采集,读取第 1~2 张图片的 G/B 通道数据
# x[2:, ...] # 高、宽、通道维度全部采集,等价于 x[2:] 读取最后 2 张图片
# x[..., :2] # 所有样本,所有高、宽的前 2 个通道,读取 R/G 通道数据
# 维度变换
# 在神经网络运算过程中,维度变换是最核心的张量操作,通过维度变换可以将数据任意地切换形式,满足不同场合的运算需求。
# 基本维度变换函数 改变视图 reshape、插入新维度 expand_dims,删除维度 squeeze、交换维度 transpose、复制数据 tile 等
# 1.改变视图
x = tf.range(96) # 生成向量
x = tf.reshape(x, [2, 4, 4, 3]) # 改变 x 的视图,获得 4D 张量,存储并未改变
print(x)
print('张量维度:', x.ndim, '形状列表:', x.shape) # 获取张量的维度数和形状列表
y = tf.reshape(x, [2, -1]) # 可以将张量的视图任意地合法改变 参数−1表示当前轴上长度需要根据张量总元素不变的法则自动推导
print(y)
z = tf.reshape(x, [2, 4, 12])
print(z)
# 增,删维度
# 增加维度 tf.expand_dims 的 axis 为正时,表示在当前维度之前插入一个新维度;为负时,表示当前维度之后插入一个新的维度。
x = tf.random.uniform([2, 2], maxval=10, dtype=tf.int32)
print(x)
x1 = tf.expand_dims(x, axis=2) # axis=2 表示宽维度后面的一个维度
print(x1)
x2 = tf.expand_dims(x, axis=0) # 高维度之前插入新维度
print(x2)
# 删除维度
# 是增加维度的逆操作,与增加维度一样,删除维度只能删除长度为 1 的维度,也不会改变张量的存储。
# 可以通过 tf.squeeze(x, axis)函数,axis 参数为待删除的维度的索引号
x = tf.squeeze(x, axis=0) # 删除图片数量维度
# 交换维度
x = tf.random.normal([2, 32, 32, 3])
y = tf.transpose(x, perm=[0, 3, 1, 2]) # 交换维度 图片数量、行、列、通道数 -> 图片数量、通道数、行、列
z = tf.transpose(x, perm=[0, 2, 1, 3]) # 交换维度 图片数量、行、列、通道数 -> 图片数量、列、行、通道数
# 复制数据
# 可以通过 tf.tile(x, multiples)函数完成数据在指定维度上的复制操作,
# multiples 分别指定了每个维度上面的复制倍数,
# 对应位置为 1 表明不复制,为 2 表明新长度为原来长度的2 倍,即数据复制一份,以此类推。
b = tf.constant([1, 2]) # 创建向量 b
print(b)
b = tf.expand_dims(b, axis=0) # 插入新维度,变成矩阵
print(b)
b = tf.tile(b, multiples=[2, 1]) # 样本维度上复制一份
print(b)
x = tf.range(4)
print(x,'\n')
x = tf.reshape(x, [2, 2]) # 创建 2 行 2 列矩阵
print(x,'\n')
x = tf.tile(x, multiples=[1, 3]) # 列维度复制两份
print(x,'\n')
x = tf.tile(x, multiples=[2, 1]) # 行维度复制一份
print(x,'\n')
# Broadcasting Broadcasting
# 称为广播机制(或自动扩展机制),它是一种轻量级的张量复制手段,在逻辑上扩展张量数据的形状,但是只会在需要时才会执行实际存储复制操作
# 通过 tf.broadcast_to(x, new_shape)函数可以显式地执行自动扩展功能,将现有 shape 扩张为 new_shape
a = tf.random.normal([32, 1], dtype=tf.float16) # 创建矩阵
print(a)
a = tf.broadcast_to(a, [2, 32, 32, 3]) # 扩展为 4D 张量
print(a)
# 在𝑑维度上,张量已经有 2 个特征数据,新 shape 对应维度的长度为𝑑(𝑑 ≠ 2,如𝑑=3),那么
# 当前维度上的这 2 个特征无法普适到其它位置,故不满足普适性原则,无法应用Broadcasting 机制,将会触发错误
# b = tf.random.normal([32, 2])
# b = tf.broadcast_to(b, [2, 32, 32, 4]) # 不符合 Broadcasting 条件
# 在进行张量运算时,有些运算在处理不同 shape 的张量时,会隐式地自动调用Broadcasting 机制,如+,-,*,/等运算等,
# 将参与运算的张量 Broadcasting 成一个公共shape,再进行相应的计算。
a = tf.random.normal([2, 32, 32, 1])
b = tf.random.normal([32, 32])
print(a + b, a - b, a * b, a / b) # 测试加减乘除运算的 Broadcasting 机制
# 数学运算
# 加、减、乘、除是最基本的数学运算,分别通过 tf.add, tf.subtract, tf.multiply, tf.divide函数实现
# TensorFlow 已经重载了+、 −、 ∗ 、/运算符,一般推荐直接使用运算符来完成加、减、乘、除运算
a = tf.range(5)
b = tf.constant(2)
print(a // b) # 整除运算
print(a % b) # 余除运算
# 乘方运算 通过 tf.pow(x, a)可以方便地完成𝑦 = 𝑎 的乘方运算,也可以通过运算符**实现 ∗∗𝑏运算
x = tf.range(4)
print(tf.pow(x, 3)) # 乘方运算
print(x ** 2) # 乘方运算符
# 设置指数为 1/𝑎 形式,即可实现√𝑎根号运算
x = tf.constant([1., 4., 9.])
print(x ** (0.5)) # 平方根
# 对于常见的平方和平方根运算,可以使用 tf.square(x)和 tf.sqrt(x)实现。
x = tf.range(5)
print(x)
x = tf.cast(x, dtype=tf.float32) # 转换为浮点数
print(x)
x = tf.square(x) # 平方
print(x)
x = tf.sqrt(x) # 平方根
print(x)
# 指数和对数运算
# 通过 tf.pow(a, x)或者**运算符也可以方便地实现指数运算a^x
x = tf.constant([1., 2., 3.])
print(2 ** x) # 指数运算
# 对于自然指数e 𝑥 ,可以通过 tf.exp(x)实现
print(tf.exp(1.)) # 自然指数运算
# 在 TensorFlow 中,自然对数log e 可以通过 tf.math.log(x)实现
x = tf.exp(3.)
print(tf.math.log(x)) # 对数运算
# 如果希望计算其它底数的对数,可以根据对数的换底公式:log(a)x=log(e)x/log(e)a
# 间接地通过 tf.math.log(x)实现
x = tf.constant([1., 2.])
x = 10 ** x
print(tf.math.log(x) / tf.math.log(10.)) # 换底公式
# 矩阵相乘运算
a = tf.random.normal([4, 3, 28, 32])
b = tf.random.normal([4, 3, 32, 2])
print(a @ b)
# 矩阵相乘函数同样支持自动 Broadcasting 机制,
a = tf.random.normal([4,28,32])
b = tf.random.normal([32,16])
tf.matmul(a,b) # 先自动扩展,再矩阵相乘
# 前向传播实战
# out = 𝑅𝑓𝐿𝑈{𝑅𝑓𝐿𝑈{𝑅𝑓𝐿𝑈[X@W1+𝒃1]@W2+𝒃2}@W3+𝒃3}
# 采用的数据集是 MNIST 手写数字图片集
# 输入节点数为 784,第一层的输出节点数是256,第二层的输出节点数是 128,第三层的输出节点是 10
# 也就是当前样本属于 10 类别的概率。
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
(x, y), (x_val, y_val) = datasets.mnist.load_data() # 加载 MNIST数据集 返回两个元组,一个是训练集,一个是测试集
x = 2 * tf.convert_to_tensor(x, dtype=tf.float32) / 255. - 1 # 转换为浮点张量,并缩放到-1~1
y = tf.convert_to_tensor(y, dtype=tf.int32) # 转换为整形张量
y_onehot = tf.one_hot(y, depth=10) # one-hot 编码 depth指的是总类别数
lr = 0.1
with tf.GradientTape() as tape:
# Step1:创建每个非线性层的𝑿和𝒃张量参数
# 每层的张量都需要被优化,故使用 Variable 类型,并使用截断的正太分布初始化权值张量,偏置向量初始化为 0 即可
# 第一层的参数
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
# 第二层的参数
w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1))
b2 = tf.Variable(tf.zeros([128]))
# 第三层的参数
w3 = tf.Variable(tf.random.truncated_normal([128, 10], stddev=0.1))
b3 = tf.Variable(tf.zeros([10]))
# Step2:改变视图,[b, 28, 28] => [b, 28*28]
x = tf.reshape(x, [-1, 28 * 28])
# Step3:向前计算
# 第一层计算,[b, 784]@[784, 256] + [256] => [b, 256] + [256] => [b,256] + [b, 256]
h1 = x @ w1 + tf.broadcast_to(b1, [x.shape[0], 256])
h1 = tf.nn.relu(h1) # 通过激活函数
# 第二层计算,[b, 256] => [b, 128]
h2 = h1 @ w2 + b2
h2 = tf.nn.relu(h2)
# 输出层计算,[b, 128] => [b, 10]
out = h2 @ w3 + b3
# Step4:将真实的标注张量𝒛转变为 One-hot 编码,并计算与 out 的均方差
# 计算网络输出与标签之间的均方差,mse = mean(sum(y-out)^2)
# [b, 10]
loss = tf.square(y_onehot - out)
# 误差标量,mean: scalar
loss = tf.reduce_mean(loss)
# 自动梯度,需要求梯度的张量有[w1, b1, w2, b2, w3, b3]
grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
# 梯度更新,assign_sub 将当前值减去参数值,原地更新
w1.assign_sub(lr * grads[0])
b1.assign_sub(lr * grads[1])
w2.assign_sub(lr * grads[2])
b2.assign_sub(lr * grads[3])
w3.assign_sub(lr * grads[4])
b3.assign_sub(lr * grads[5])