可以将 python 函数编译成图
易于将模型导出成为 Tensorflow1.0 的 GraphDef+checkpoint 或者 Tensorflow2.0 里的 SavedModel
使得 eager execution 可以默认打开,保存中间的训练结果。(eager execution 被调用后,立刻被 python执行)
1.0 的代码可以通过 tf.function 来继续在 2.0 里使用
替代 session
常量与变量
① constant : 定义一个常量
# 创建一个二维矩阵
t = tf.constant([1.,2.,3.],[4.,5.,6.]])
# 显示第二列以及第二列往后的矩阵参数
print(t[:,1:])
# 只显示第二列
print(t[...,1])
这里我们可以看到我们直接使用 print 就可以输出,而在 TensorFlow 1 的版本是需要 session run
的,因为它的 eager execution 是默认打开的。
算子操作:基本上所有的算子操作 constant 都支持
例如:
# ops 比如:加法,平方,转置相乘
print(t+10)
print(tf.square(t))
print(t @ tf.transpose(t))
在 TensorFlow 2.0 还可以进行 numpy 对象和 TensorFlow 对象转换的功能
# numpy conversion
# 假如在 constant 上直接调用一个 numpy() 会直接获取它的值
print(t.numpy())
# 如把 costant 直接输入给 np 的一个函数,会获得运算后的值
print(np.square(t))
# 若建立一个 np 的矩阵,将其输入给 tf.constant 会转化为 Tensor
np_t = np.array([1.,2.,3.],[4.,5.,6.]])
print(tf.constant(np_t))
0 维矩阵的转化方法
# 0 维代表的是一个数字,它在 TensorFlow 中成为 scalars
t = tf.constant(2.728)
print(t.numpy())
print(t.shape)
② 字符串处理方法
# Strings
t = tf.constant("cafe")
print(t)
print(tf.strings.length(t))
# 因为我们这里使用的是纯英文,所以他的长度和他的 unicode 是一样的
print(tf.strings.length(t,unit="UTF8_CHAR"))
# 我们还可以通过调用 strings.unicode_decode 来将我们的字符串转化成 UTF8 格式
print(tf.strings.unicode_decode(t,"UTF8"))
除了字符串,我们还可存储数组的字符串
# string array
# 这里会显示数组中三个元素的长度,分别为 4 6 2 ,因为这里是使用 UTF8 来进行编码的
t = tf.constant(["cafe","coffee","咖啡"])
print([tf.strings.length(t,unit="UTF8_CHAR"))
# 这里将 t 转化成 UTF8 ,这里 print 数组后,在矩阵第一行输出 cafe 的四个 UTFF8 编码,形成不规则的矩阵,RaggedTensor 就是指这类不完整的矩阵
r = tf.strings.unicode_decode(t,"UTF8")
print(r)
① 这里出现了 RaggedTensor ,以下是使用 RaggedTensor 的方法
# ragged tensor
r = tf.ragged.constant([11,12],[21,22,23],[],[41]])
# 索引操作
print(r)
# 这里表示只显示第二列,转变为了规范的 Tensor
print(r[1])
# 显示第二行到第三行,第三行是空行,所以这个矩阵还是 raggedTensor
print(r[1:2])
RaggedTensor 上的 ops 操作
# ops on ragged Tensor
r2 = tf.ragged.constant([[51,52],[],[71]])
# concat 矩阵拼接函数,axis 表示以行的形式拼接
print(tf.concat([r,r2],axis = 0))
# 以列来拼接,会出现错误,因为两边的行数不一致
print(tf.concat([r,r2],axis = 1))
# 这样的话,我们就需要让他们有一样的行数才能进行拼接
r3 = tf.ragged.constant([[13,14],[15],[],[42,43]])
print(tf.concat([r,r3],axis = 1))
那如何转化不规则的矩阵为规则的矩阵呢
# 空缺部分会直接被 0 填满,并且了 0 会永远优先往后填充
print(r.to_tensor())
② 但这种方法可能会不适用一些数据的填充,这个时候就需要使用 sparse tensor(稀疏矩阵,使用非零值的位置来构建一个小的矩阵,配置整体矩阵大小,和 values ,这样就能用小的数据量代表一个大的矩阵)
# sparse tensor 将特定位置的值变为指定的值
# indices:所有正常值的 indices(指标:矩阵中的位置)
# values:indices 所对应的值
# dense_shape: 矩阵的大小
s = tf.SparseTensor(indices = [0,1],[1,0],[2,3]],
values = [1.,2.,3.],
dense_shape = [3,4])
我们还可以利用类似 to_tensor 的办法将 sparse 矩阵(稀疏矩阵,矩阵中大部分为 0 )转化为普通的 tensor(密集矩阵)
print(tf.sparse.to_dense(s))
Sparse 支持的运算类型,不支持加法运算,使用 tf.shparse.sparse_dense_matmul 将 s 与 密集矩阵s4 相乘
会得到一个普通的 3*2 Tensor
# ops on sparse tensors
s2 = s*2.0
print(s2)
# 加法会报错
try:
s3 = s + 1
except TypeError as ex:
print(ex+1)
# 定义一个 4*2 的矩阵
s4 = tf.constant([[10.,20.],
[30.,40.],
[50.,60.],
[70.,80.]])
print(tf.sparse.sparse_dense_matmul(s,s4))
但我们在建立 tf.SparseTensor 的时候必须将 indices 排好序,若不排好,不影响建立 SparseTensor ,但在将 SparseTensor to_dense 的时候,会出现错误
但我们遇到这种情况该怎么解决呢,我们只需要调用一下 reorder 即可
# sparse tensor 将特定位置的值变为指定的值
# indices:所有正常值的 indices(指标:矩阵中的位置)
# values:indices 所对应的值
# dense_shape: 矩阵的大小
s5 = tf.SparseTensor(indices = [0,2],[0,1],[2,3]],
values = [1.,2.,3.],
dense_shape = [3,4])
# 调用 reorder 扳回 SparseTensor 顺序
s6 = tf.sparse.reorder(s5)
print(tf.sparse.to_dense(s6))
③ tf.Varibles 变量
变量的创建
# Variables
v = tf.Variable([[1.,2.,3.],[4.,5.,6.]])
print(v)
print(v.value)
print(v.numpy())
# 详细资料
# v 的矩阵的值和矩阵大小
# v 的 numpy 的矩阵模式
变量的重新赋值
# assign value
v.assign(2*v)
v[0,1].asign(43)
# 将第二行的数替换为 7 8 9
v[1].assign(7.,8.,9.])
print(v.numpy())
# 赋值只能用 asign 不能用 = 号
try:
v[1] = [4.,5.,6.]
except TypeError as ex:
print(ex)
我们原本是处采用 model.compile(loss="mean_squared_error") 来求得均方误差,这里我们尝试来自己定义损失函
数,
# mse 为均方误差的缩写
def customized_mse(y_true,y_pred):
return tf.reduce_mean(tf.square(y_pred - y_true))
model = keras.models.Sequential([
keras.layers.Dense(30,activation = 'relu',
input_shape = x_train.shape[1:]),
keras.layers.Dense(1),
])
model.summary()
model.compile(loss=customized_mse,optimizer='sgd',
metrics = ["mean_squared_error"])
callbacks = [keras.callbacks.EarlyStopping(
patience = 5,min_delta=1e-2)]
接下来我们自己来实现自定义层次
一般情况我们是使用
layer = tf.keras.layers.Dense(100)
这里 100 就表示 100 个神经单元
# 不明确输入数据格式,输入不定值,输出是一个 None*100 的矩阵,input_shape 可以不设定,那她将会自动设定
layer = tf.keras.layers.Dense(100,input_shape=(None,5))
# 输入 10 * 5 的数据格式,将会输出一个 10 * 100 的矩阵
layer(tf.zeros([10,5]))
layer 层也对应很多参数,其中
variable 会输出全连接层的参数,第一部分为 kernel,属于 x * w + b 的 w,第二部分是 bias 对应 b
trainable_variables 会输出可训练的参数,也会是 kernel 和 bias
这里详细介绍一下 super 函数
下面的 super(CustomizedDenseLyaer,self).__init__(**kwargs) 代表的意义是:
self 代表的是 CustomizedDenseLayer 的实例
根据 self 去找 CustomizedDenseLayer 的父类,也就是 keras.layer.Layer
然后调用 keras.layer.Layer 的子类 __init__
但如果你的父类与子类的继承关系为多继承,将会遵循继承的顺序来进行调用
比如定义 class C( A , B ),且每个类中都使用 super 函数调用自己的父类
就会先调用 A 的类,再调用 C 的类,而 A 的父类将会是 B 类,也就是会先运行 B 的父类
再运行 B 类,最后运行 A 类。这里不是文章重点,就不描述过多,有问题可以私信我。
class CustomizedDenseLayer(keras.layer.Layer):
def __init__(self,units,activation = None,**kwargs):
# 第一个初始化单元,也就是输出
self.units = units
# 第二个是选择激活函数,使用 layers 的 Activation 实现
self.activation = keras.layers.Activation(activation)
# 接下来我们调用它父类的函数
super(CustomizeddDenseLayer,self).__init__(**kwargs)
# build 负责初始化参数
def build(self,input_shape):
'''构建所需要的参数'''
'''x* w + b. trainable 表示该 kernel 可被训练 ,initializer 表示初始化参数矩阵的方法'''
self.kernel = self.add_weight(name = 'kernel',
shape=(input_shape[1],self.units,
initializer = 'uniform',
trainable = True)
self.bias = self.add_weight(name = 'bias',
shape = (self.units,),
initializer ='zeros',
trainable = True)
super(CustomizedDenseLayer,self).build(input_shape)
# call 完成一次正向的计算,这里返回的是 x * kernel + bias
def call(self,x):
return self.activation(x @ self.kernel + self.bias)
那么我们如何简单地构建一个自定义层次呢?
我们可以使用 Lambda
# 举例一个激活函数定义的自定义层,tf.nn.softplus : log(1+e^X) ,作为 relu 的变种,将原折线变为一个曲线,这样我们一个没有参数的 softplus 就产生了
customized_softplus = keras.layers.Lambda(lambda x : tf.nn.softplus(x))
# 这里传入一个向量,来观察 softplus 的输出
print(customized_softplus([-10.,-5.,0.,5.,10.]))
在 Sequential 中层级的末尾添加一下代码即可增添激活函数层
customized_softplus,
以上操作等价于以下代码
keras.layers.Dense(1,activation = "softplus")
或者可以
keras.layers.Dense(1),keras.layers.Activation('softplus'),
tf.function 可以将 python 函数变为 TensorFlow 的图结构
autograph 是 TensorFlow 所依赖的机制,可以将 python 语法的东西变为 TensorFlow 内的图
# tf.function and auto-graph
# 这里使用 elu 实现另外一个激活函数 scaled_elu
def scaled_elu(z,scale = 1.0,alpha = 1.0):
# z>= 0 ? scale * z : scale * alpha * tf.nn.elu(z)
# 首先判断 z 是不是大于等于 0 ,返回 x >= y 的真值
is_positive = tf.greater_equal(z,0.0)
# 这里使用 where 来实现三元表达式,表达的意思是 若is_positive 是 True ,则返回 z 否则返回 alpha * tf.nn.elu(z)
return scale * tf.where(is_positive,z,alpha * tf.nn.elu(z))
# 先输入一个标量
print(scaled_elu(tf.constant(-3.)))
# 再输入一个向量,则对向量的每个值分别操作
print(scaled_elu(tf.))
下面用图的办法来实现
# 只需要一行代码就可以将 python 实现的函数变为 TensorFlow 的图结构
scaled_elu_tf = tf.function(scaled_elu)
print(scaled_elu_tf(tf.constant(-3.)))
print(scaled_elu_td(tf.constant([-3.,-2.5])))
我们为什么要使用 TensorFlow 的图结构来运行呢?
因为 TensorFlow 有针对编译器等等的优化,速度会得到提升
下面我们来测试一下两者的速度差别
# 运行下面的代码后,我们会发现有 3 ms 的差距,有 GPU 时差距会更大
%timeit scaled_elu(tf.random.normal((1000,10000)))
%timeit scaled_elu_tf(tf.random.normal((1000,1000)))
除了使用 tf.function ,我们还可以使用 @function 来实现同样的功能
@tf.function
def converge_to_2(n_iters):
total = tf.constant(0.)
increment = tf.constant(1.)
for _ in range(n_iters):
total += increament
increament /= 2.0
return total
print(converge_to_2(20))
这个函数可以 python 函数转为 TensorFlow 图结构的中间代码给大家展示出来
def display_tf_code(func):
# 只需要一步即可获取中间代码
code = tf.autograph.to_code(func)
from IPython.display import display,Markdown
# format 使用 {} 或 :来替代占位符
display(Markdown('''python\n{}\n'''.format(code)))
display_tf_code(scaled_elu)
但将 python 函数转化成 TensorFlow 图结构有一点需要着重强调一下
在神经网络中,我们一般使用的是 variable ,但变量是不能存在于 @function 定义的函数中的
我们需要提前先初始化好 Variable 变量,这里举例一个给变量加 21 的例子,其中变量的加法也需要使用
assign_add 函数
var = tf.Variable(0.)
@tf.function
def add_21():
return var.assign_add(21) # +=
print(add_21())
我们可以使用附加函数签名的办法来规范函数的输入
这里的 pow 是次方函数,意思为 z 的三次方
# 不规定数据形状,确定数据类型为 int32 ,设定一个名字 x
@tf.function(input_signature = [tf.TrnsorSpec([None],tf.int32,name = 'x'
def cube(z):
return tf.pow(z,3)
# 尝试使用浮点类型数据输入 int32 的函数
try:
print(cube(tf.constant([1.,2.,3.])))
# 输出错误信息
except ValueError as ex:
print(ex)
print(cube(tf.constant([1,2,3])))
get_concrete_function 是用于给被 @function 标注的函数,加上数据限制,也就是 input_signature
从而让这个 python 函数变为可以保存的 TensorFlow 图结构 -> SaveModel
cube_func_int32 = cube.get_concrete_function(
tf.TensorSpec([None],tf.int32))
print(cube_func_int32)
验证一下添加后的 ID 是否一致
print(cube_func_int32 is cube.get_concrete_function(
tf.TensorSpec([5],tf.int32)))
print(cube_func_int32 is cube.get_concrete_function(
tf.constant([1,2,3])))
可以看一下 cube_func_int32 的图
cube_func_int32.graph
还可以查看 cube_func_int32 做了哪些 operation
其中的 pow 就是立方操作
cube_func_int32.graph.get_operations()
我们来看一下其中计算立方的操作
pow_op = cube_func_int32.graph.get_operation()[2]
print(pow_op)
内容也可以再单独打印出来
print(list(pow_op.inputs))
print(list(pow_op.outputs))
在 graph 里面呢,我们还可以用名字来获取相应的 operation 或者 tensor
cube_func_int32.graph.get_operation_by_name("x")
通过名字获取 Tensor
# Tensor 后面一般需要加一个 :0
cube_func_int32.graph.get_tensor_by_name("x:0")
那我们怎么获取它的名字呢,我们可以用一个粗暴的办法来将名字打印出来
cube_func_int32.graph.as_graph_def()
以上这些操作有什么用处呢?
第一:如何去保存模型
第二:保存好模型之后,如何将保存好的模型载入进来
手动近似求导的办法
# 定义函数
def f(x):
return 3. * x ** 2 + 2. * x - 1
# 定义求导函数 ,定义一个极小的数,对求导位置前后相差,根据直线斜率定义该位置的导数
def approximae_derivative(f,x,eps = 1e-3):
return (f(x + eps) - f(x - eps)) / (2. * eps)
print(approximae_derivative(f,1.))
那么我们对于多元函数该怎么求导呢?我们就需要对两个元素分别求偏导
# 多元函数,分别求偏导
def g(x1,x2):
return (x1 + 5 ) * (x2 ** 2)
# 对其中一个求偏导,将另一个数设定为常数即可
def approximate_gradient(g,x1,x2,eps = 1e-3):
dg_x1 = approximate_derivative(lambda x: g(x,x2),x1,eps)
dg_x2 = approximate_derivative(lambda x: g(x1,x),x2,eps)
return dg_x1,dg_x2
print(approximate_gradient(g,2.,3.))
# 在TensorFlow 里面,我们想进行求导需要调用 tf.GradientTape ,但这个函数只能调用一次
# 我们通过以下来实现对 x1 的求导,但重复进行 x2 求导时会发现无法求导
x1 = tf.Variable(2.0)
x2 = tf.Variable(3.0)
with tf.GradientTape() as tape:
z = g(x1,x2)
dz_x1 = tape.gradient(z,x1)
print(dz_x1)
try:
dz_x2 = tape.gradient(z,x2)
except RuntimeError as ex:
print(ex)
上面是不能多次使用的 tf.GradientTape ,如果想多次使用,就需要定义 tf.GradientTape 的参数 persistent 为 True
不过采用这种办法系统就不会帮我们删除资源,我们就需要自己来删除 tf.GradientTape
x1 = tf.Variable(2.0)
x2 = tf.Variable(3.0)
with tf.GradientTape(persistent = True) as tape:
z = g(x1,x2)
dz_x1 = tape.gradient(z,x1)
dz_x2 = tape.gradient(z,x2)
print(dz_x1,dz_x2)
del tape
还可以通过以下办法来同时求 x1 和 x2 的偏导
x1 = tf.Variable(2.0)
x2 = tf.Variable(3.0)
with tf.GradientTape() as tape:
z = g(x1,x2)
dz_x1x2 = tape.gradient(z,[x1,x2])
print(dz_x1x2)
如果我们将变量变为常量 constant ,将无法进行求导,那我们该如何解决这个问题呢?
x1 = tf.constant(2.0)
x2 = tf.constant(3.0)
with tf.GradientTape() as tape:
tape.watch(x1)
tape.watch(x2)
z = g(x1,x2)
dz_x1x2 = tape.gradient(z,[x1,x2])
print(dz_x1x2)
那如果有两个目标函数对一个变量求导数,我们该如何实现呢
x = tf.Varible(5.0)
with tf.GradientTape() as tape:
z1 = 3 * x
z2 = x ** 2
tape.gradient([z1,z2]),x)
# 得出的 numpy 结果为 13 ,这个为两个导数之和
以上我们求得都是一阶导数,接下来我们求一下二阶导数
这里的双层求二阶导数,不是很理解其中的代码确切的运行原理,若本教程对读者有帮助的话,希望能详细解惑
# 这里可能会被调用多次,所以定义了 persisitant = True
x1 = tf.Variable(2.0)
x2 = tf.Variable(3.0)
with tf.GradientTape(persistent = True) as outer_tape:
with tf.GradientTape(persistent=True) as inner_tape:
z = g(x1,x2)
inner_grads = inner_tape.gradient(z,[x1,x2])
outer_grads = [outer_tape.gradient(inner_grad,[x1,x2])
for inner_grad in inner_grads]
print(outer_grads)
del inner_tape
del outer_tape
那引入 learning_rate 该如何求导呢?
def f(x):
return 3. * x ** 2 + 2. * x -1
learning_rate = 0.1
x = tf.Variable(0.0)
for _ in range(100):
with tf.GradientTape() as tape:
z = f(x)
dz_dx = tape.gradient(z,x)
# 更新 x ,更新 x 值为 -= 学习率乘以导数
x.assign_sub(learning_rate * dz_dx)
print(x)
还可以通过 optimizer 来调用梯度下降目标函数来进行最优求解
learning_rate = 0.1
x = tf.Variable(0.0)
optimizer = keras.optimizers.SGD(lr = learning_rate)
for _ in range(100):
with tf.GradientTape() as tape:
z = f(x)
dz_dx = tape.gradient(z,x)
optimizer.apply_gradients([(dz_dx,x)])
print(x)
为了方便理解,这里采用完整一点的代码
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import sklearn
import pandas as pd
import os
import sys
import time
import tensorflow as tf
from tensorflow import keras
from sklearn.datasets import fetch_california_housing
housing = fetch_california_housing()
print(housing.DESCR)
print(housing.data.shape)
print(housing.target.shape)
from sklearn.model_selection import train_test_split
x_train_all,x_test,y_train_all,y_test = train_test_split(
housing.data,housing.target,random_state = 7)
x_train,x_valid,y_train,y_valid = train_test_split(
x_train_all,y_train_all,random_state =11)
from sklearn.preprocessing import StandarScaler
scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(x_train)
x_valid_scaled = scaler.transform(x_valid)
x_test_scaled = scaler.transform(x_test)
做好前奏,接下来就是模型构建和训练部分,将是我们的重点
原模型构建:
model = keras.models.Sequential([
keras.layers.Dense(30,activation = 'relu',
input_shape = x_train.shape[1:]),
keras.layers.Dense(1),
])
model.summary()
model.compile(loss="mean_squared_error",optimizer="sgd")
callbacks = [keras.callbacks.EarlyStopping(
patience = 5,min_delta = 1e-2)]
原模型训练:
history = model.fit(x_train_scaled,y_train,
validation_data = (x_valid_scaled,y_valid),
epochs = 100,
callbacks = callbacks)
我们先看一下如何手工实现均方差,这里的 metric 还会自动求取均值
# metric 使用
# 这里会将两次 metric 的值取均值,如果不想这样,可以调用 reset_states
metric = keras.metrics.MeanSquaredError()
print(metric[5.],[2.]))
print(metric[0.],[1.]))
print(metric.result)
metric.reset_states()
metric.([1.],[3.])
print(metric.result())
模型构建和模型训练替换为以下代码
# 我们先整理一下 fit 函数中,我们做了什么
# 1. batch 遍历数据集 metric
# 1.1 自动求导
# 2. epoch 结束 验证集 metric
# 需要遍历 100 次
epochs = 100
# 每次取 32 个数据
batch_size = 32
# 获取总数据数
steps_per_epoch = len(x_train_scaled)
# 调用 keras.optimizers.SGD 为目标函数
optimizer = keras.optimizers.SGD()
# 损失函数使用差值平方
metric = keras.metrics.MeanSquaredError()
# 随机获取函数,取值不能超过 len(x)
def random_batch(x,y,batch_size = 32):
# 随机获取 32 个整数数组
idx = np.random.randint(0,len(x),size = batch_size)
return x[idx],y[idx]
model = keras.models.Sequential([
keras.layers.Dense(30,activation='relu',
input_shape = x_train.shape[1:]),
keras.layers.Dense(1),
])
# 循环 100 次
for epoch in range(epochs):
# 取消 metric 的自动取平均
metric.reset_states()
# 循环数据个数次,每次遍历里训练多少步
for step in range(steps_per_epoch):
# 获取 32 个数据
x_batch,y_batch = random_batch(x_train_scaled,y_train,batch_size)
#
with tf.GradientTape() as tape:
# 将获取到的 x_train 的 batch 输入到模型中,y_pred 就是输出的预测值
y_pred = model(x_batch)
# 损失函数:计算 y_batch 和 y_pred 的均方差
loss = tf.reduce_mean(
keras.losses.mean_squared_error(y_batch,y_pred))
# 求解 y_batch 和 y_pred 的均方差,并且循环数据个数次
metric(y_batch,y_pred)
# 以上我们就把目标函数确定完毕
# 一下进行求导
# 对 loss 函数的 model.variables 进行求导
grads = tape.gradient(loss,model.variables)
# 组合数组成元组,将梯度与变量一一绑定
grads_and_vars = zip(grads,model.variables)
# 引用前面定义的 SGD ,来进行随机梯度下降
# 输入起始位置和起始导数
optimizer.apply_gradients(grads_and_vars)
print("\rEpoch",epoch,"train_mse:",
metric.result().numpy(),end="")
# 训练完毕后我们对验证集进行损失计算,查看遍历一次后模型的性能
y_valid_pred = model(x_valid_scaled)
# 新建一个损失变量
valid_loss = tf.reduce_mean(
keras.losses.mean_squared_error(y_valid_pred,y_valid))
print("\t","valid_mse:",valid_loss.numpy())
实现完毕后,我们会发现不如使用默认的效果好,因为默认的 API 是进过一系列优化的,我们还需要进行一系列的优化,才能有比较好的效果。但我们也要学会使用自定义的方式来进行求导。方便我们修改与自建模型,对于将论文转化为代码,也有硕大的好处。
1 在这篇文章中,我们学习了基础 API 的使用
其中有:基本数据类型
2 接下来学习了 基础 API 与 keras 的集成
其中有:自定义损失函数、自定义层次
3 @tf.function 的使用
图结构
4 自定义求导