TensorFlow 2 新增变化特性
TensorFlow 2 安装
2.0 版本将专注于简洁性和易用性的改善,主要升级方向包括:
使用 Keras 和 Eager Execution 轻松构建模型。
在任意平台上实现稳健的生产环境模型部署。
为研究提供强大的实验工具。
通过清理废弃的 API 和减少重复来简化 API。
import tensorflow as tf
tf.__version__
Eager Execution
TensorFlow 2 中的 Eager Execution 是一种命令式编程环境,可立即评估操作,无需构建图:操作会返回具体的值,而不是构建以后再运行的计算图。
Eager Execution 模式下则可以直接输出张量的数值了,并以 NumPy 数组方式呈现。
c = tf.Variable([[1, 2], [3, 4]])
c
还可以直接通过 .numpy() 输出张量的 NumPy 数组。Eager Execution 适合与 NumPy 一起使用。NumPy 操作接受 tf.Tensor 参数。TensorFlow 数学运算将 Python 对象和 NumPy 数组转换为 tf.Tensor 对象。tf.Tensor.numpy 方法返回对象的值作为 NumPy ndarray。
c.numpy()
Eager Execution 带来的好处是不再需要手动管理图和会话。例如,现在使用示例张量进行数学计算,可以像 Python 一样直接相加。
c + c # 加法计算
得益于自动微分的使用,在 Eager Execution 期间,可以使用 tf.GradientTape 这类跟踪操作以便稍后计算梯度。
w = tf.Variable([[1.0]]) # 新建张量
with tf.GradientTape() as tape: # 追踪梯度
loss = w * w
grad = tape.gradient(loss, w) # 计算梯度
grad
tf.GradientTape 会像磁带一样记录下计算图中的梯度信息,然后使用 .gradient 即可回溯计算出任意梯度,这对于使用 TensorFlow 低阶 API 构建神经网络时更新参数非常重要。
TensorFlow Keras
如果你熟悉 Keras 的使用,那么 tf.keras 用起来就得心应手了,因为其基本和单独发行版本一致,子模块结构也几乎完全一样。
实现线性回归
低阶 API 实现
低阶 API 实现,实际上就是利用 Eager Execution 机制来完成。实验首先初始化一组随机数据样本,并添加噪声,然后将其可视化出来。
import matplotlib.pyplot as plt
import tensorflow as tf
%matplotlib inline
TRUE_W = 3.0
TRUE_b = 2.0
NUM_SAMPLES = 100
# 初始化随机数据
X = tf.random.normal(shape=[NUM_SAMPLES, 1]).numpy()
noise = tf.random.normal(shape=[NUM_SAMPLES, 1]).numpy()
y = X * TRUE_W + TRUE_b + noise # 添加噪声
plt.scatter(X, y)
接下来,我们定义一元线性回归模型。
构建自定义模型类,并使用 TensorFlow 提供的 tf.Variable 随机初始化参数 w 和截距项 b。
class Model(object):
def __init__(self):
self.W = tf.Variable(tf.random.uniform([1])) # 随机初始化参数
self.b = tf.Variable(tf.random.uniform([1]))
def __call__(self, x):
return self.W * x + self.b # w*x + b
对于随机初始化的 w 和 b,我们可以将其拟合直线绘制到样本散点图中。
model = Model() # 实例化模型
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
由于是随机初始化,也有极小概率一开始拟合效果非常好,那么重新执行一次上面的单元格另外随机初始化一组数据即可。
定义线性回归使用到的损失函数。这里使用线性回归问题中常用的平方损失函数。
根据公式实现损失计算函数
def loss_fn(model, x, y):
y_ = model(x)
return tf.reduce_mean(tf.square(y_ - y))
使用迭代方法求解线性回归的问题中,我们首先需要计算参数的梯度,然后使用梯度下降法来更新参数。
EPOCHS = 10
LEARNING_RATE = 0.1
for epoch in range(EPOCHS): # 迭代次数
with tf.GradientTape() as tape: # 追踪梯度
loss = loss_fn(model, X, y) # 计算损失
dW, db = tape.gradient(loss, [model.W, model.b]) # 计算梯度
model.W.assign_sub(LEARNING_RATE * dW) # 更新梯度
model.b.assign_sub(LEARNING_RATE * db)
# 输出计算过程
print('Epoch [{}/{}], loss [{:.3f}], W/b [{:.3f}/{:.3f}]'.format(epoch, EPOCHS, loss,
float(model.W.numpy()),
float(model.b.numpy())))
初始化 tf.GradientTape() 以追踪梯度,然后使用 tape.gradient 方法就可以计算梯度了。值得注意的是,tape.gradient() 第二个参数支持以列表形式传入多个参数同时计算梯度。紧接着,使用 .assign_sub 即可完成公式中的减法操作用以更新梯度。
最终,我们绘制参数学习完成之后,模型的拟合结果。
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
由于是随机初始化参数,如果迭代后拟合效果仍然不好,一般是迭代次数太少的原因。你可以重复执行上面的迭代单元格多次,增加参数更新迭代次数,即可改善拟合效果。
高阶 API 实现
tf.keras 模块下提供的 tf.keras.layers.Dense 全连接层(线性层)实际上就是一个线性计算过程。所以,模型的定义部分我们就可以直接实例化一个全连接层即可。
model = tf.keras.layers.Dense(units=1) # 实例化线性层
model
units 为输出空间维度。此时,参数已经被初始化了,所以我们可以绘制出拟合直线。
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
使用 model.variables 打印出模型初始化的随机参数
model.variables
这里同样使用 tf.GradientTape() 来追踪梯度,我们简化损失计算和更新的过程。首先,损失可以使用现有 API tf.keras.losses.mean_squared_error 计算,最终使用 tf.reduce_sum 求得全部样本的平均损失。
EPOCHS = 10
LEARNING_RATE = 0.002
for epoch in range(EPOCHS): # 迭代次数
with tf.GradientTape() as tape: # 追踪梯度
y_ = model(X)
loss = tf.reduce_sum(tf.keras.losses.mean_squared_error(y, y_)) # 计算损失
grads = tape.gradient(loss, model.variables) # 计算梯度
optimizer = tf.keras.optimizers.SGD(LEARNING_RATE) # 随机梯度下降
optimizer.apply_gradients(zip(grads, model.variables)) # 更新梯度
print('Epoch [{}/{}], loss [{:.3f}]'.format(epoch, EPOCHS, loss))
其次,使用 model.variables 即可读取可参数的列表,无需像上面那样手动传入。这里不再按公式手动更新梯度,而是使用现有的随机梯度下降函数 tf.keras.optimizers.SGD,然后使用 apply_gradients 即可更新梯度。
最终,同样将迭代完成的参数绘制拟合直线到原图中
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
Keras 方式实现
这里使用 Keras 提供的 Sequential 序贯模型结构。和上面的例子相似,向其中添加一个线性层。不同的地方在于,Keras 序贯模型第一层为线性层时,规定需指定输入维度,这里为 input_dim=1。
model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(units=1, input_dim=1))
model.summary() # 查看模型结构
直接使用 .compile 编译模型,指定损失函数为 MSE 平方损失,优化器选择 SGD 随机梯度下降。然后,就可以使用 .fit 传入数据开始迭代了。
model.compile(optimizer='sgd', loss='mse')
model.fit(X, y, steps_per_epoch=100)
steps_per_epoch 只的是在默认小批量为 32 的条件下,传入相应次数的小批量样本。最终绘制出迭代完成的拟合图像。
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
TensorFlow 1.x 实现
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior() # 关闭 Eager Execution 特性
X_train = tf.placeholder(tf.float32) # 定义占位符张量
y_train = tf.placeholder(tf.float32)
W = tf.Variable(tf.random.normal([1])) # 初始化参数
b = tf.Variable(tf.random.normal([1]))
LEARNING_RATE = 0.001 # 学习率
y_train_ = W*X_train + b # 线性函数
loss = tf.reduce_mean(tf.square(y_train_ - y_train)) # 平方损失函数
optimizer = tf.train.GradientDescentOptimizer(
LEARNING_RATE).minimize(loss) # 梯度下降优化损失函数
EPOCHS = 1000 # 迭代次数
with tf.Session() as sess: # 启动会话
tf.global_variables_initializer().run() # 初始化全局变量
for epoch in range(EPOCHS): # 迭代优化
sess.run(optimizer, feed_dict={X_train:X, y_train:y})
final_weight = sess.run(W) # 最终参数
final_bias = sess.run(b)
print(final_weight, final_bias)
依据迭代更新的参数,将拟合直线绘制到原图中
preds = final_weight * X + final_bias # 计算预测值
plt.scatter(X, y)
plt.plot(X, preds, c='r')
构建神经网络
低阶 API 构建
我们先加载一组数据,DIGITS 数据集是 scikit-learn 提供的简单手写字符识别数据集。
读取数据集并进行简单切分,这里对字符标签进行了独热编码方便后面计算损失值。
import numpy as np
import tensorflow as tf
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
digits = load_digits()
digits_y = np.eye(10)[digits.target.reshape(-1)] # 标签独热编码
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits_y,
test_size=0.2, random_state=1)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
使用 TensorFlow 2 低阶 API 构建一个包含 1 个隐含层的简单神经网络结构。神经网络的输入是单个手写字符样本的向量长度 64,隐含层输入为 30,最终的输出层为 10。
我们对隐含层进行 RELU 激活,输出层不激活。输出层的单样本长度为 10,这样正好就和上方独热编码后的值对应上了。
class Model(object):
def __init__(self):
self.W1 = tf.Variable(tf.random.normal([64, 30])) # 随机初始化张量参数
self.b1 = tf.Variable(tf.random.normal([30]))
self.W2 = tf.Variable(tf.random.normal([30, 10]))
self.b2 = tf.Variable(tf.random.normal([10]))
def __call__(self, x):
x = tf.cast(x, tf.float32) # 转换输入数据类型
# 线性计算 + RELU 激活
fc1 = tf.nn.relu(tf.add(tf.matmul(x, self.W1), self.b1))
fc2 = tf.add(tf.matmul(fc1, self.W2), self.b2)
return fc2
开始构建损失函数。损失函数使用 TensorFlow 提供的 tf.nn.softmax_cross_entropy_with_logits,这是一个自带 Softmax 的交叉熵损失函数。最终通过 reduce_mean 求得全局平均损失。
def loss_fn(model, x, y):
preds = model(x)
return tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=preds, labels=y))
为了方便查看分类准确度,我们需要手动构建一个准确度评估函数。tf.argmax 可以将 Softmax 结果转换为对应的字符值。然后使用 tf.equal 比对各样本的结果是否正确,最终使用 reduce_mean 求得全部样本的分类准确度。
def accuracy_fn(logits, labels):
preds = tf.argmax(logits, axis=1) # 取值最大的索引,正好对应字符标签
labels = tf.argmax(labels, axis=1)
return tf.reduce_mean(tf.cast(tf.equal(preds, labels), tf.float32))
构建最关键的训练迭代过程,目的是对神经网络参数进行迭代优化。
EPOCHS = 100 # 迭代此时
LEARNING_RATE = 0.02 # 学习率
model = Model() # 模型
for epoch in range(EPOCHS):
with tf.GradientTape() as tape: # 追踪梯度
loss = loss_fn(model, X_train, y_train)
trainable_variables = [model.W1, model.b1, model.W2, model.b2] # 需优化参数列表
grads = tape.gradient(loss, trainable_variables) # 计算梯度
optimizer = tf.optimizers.Adam(learning_rate=LEARNING_RATE) # 优化器
optimizer.apply_gradients(zip(grads, trainable_variables)) # 更新梯度
accuracy = accuracy_fn(model(X_test), y_test) # 计算准确度
# 输出各项指标
if (epoch + 1) % 10 == 0:
print('Epoch [{}/{}], Train loss: {:.3f}, Test accuracy: {:.3f}'
.format(epoch+1, EPOCHS, loss, accuracy))
Keras 高阶 API 实现
Keras 中更容易理解的是函数式模型。函数式模型最直观的地方在于可以看清楚输入和输出。
函数式模型
开始定义函数式模型。首先是 Input 层,这在序贯模型中是没有的。然后我们将 inputs 传入 Dense 层,最终再输出。
# 函数式模型
inputs = tf.keras.Input(shape=(64,))
x = tf.keras.layers.Dense(30, activation='relu')(inputs)
outputs = tf.keras.layers.Dense(10, activation='softmax')(x)
# 指定输入和输出
model = tf.keras.Model(inputs=inputs, outputs=outputs)
model.summary() # 查看模型结构
函数式模型中需要使用 tf.keras.Model 来最终确定输入和输出。
编译和训练模型。这里使用 tf.optimizers.Adam 作为优化器,tf.losses.categorical_crossentropy 多分类交叉熵作为损失函数。与 tf.nn.softmax_cross_entropy_with_logits 不同的是,tf.losses.categorical_crossentropy 是从 Keras 中演化而来的,其去掉了 Softmax 的过程。而这个过程被我们直接加入到模型的构建中。
上面的 model 输出层使用了 softmax 激活。
# 编译模型
model.compile(optimizer=tf.optimizers.Adam(),
loss=tf.losses.categorical_crossentropy, metrics=['accuracy'])
# 训练和评估
model.fit(X_train, y_train, batch_size=64, epochs=10,
validation_data=(X_test, y_test))
Keras 的训练过程可以采用小批量迭代,直接指定 batch_size 即可。validation_data 可以传入测试数据得到准确度评估结果,非常方便。
序贯模式
model = tf.keras.Sequential() # 建立序贯模型
model.add(tf.keras.layers.Dense(units=30, input_dim=64, activation='relu')) # 隐含层
model.add(tf.keras.layers.Dense(units=10, activation='softmax')) # 输出层
model.summary() # 查看模型结构
使用参数简写来替代函数式模型中复杂的写法
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=64, epochs=10,
validation_data=(X_test, y_test))
自由度更高的模型
这种方法和 PyTorch 中继承 torch.nn.Module 来定义模型的思路非常相似。我们可以继承 tf.keras.Model 来构建模型。这种模型的定义方法自由度更高,我们可以添加更多的中间组件,相对灵活。
class Model(tf.keras.Model):
def __init__(self):
super(Model, self).__init__()
self.dense_1 = tf.keras.layers.Dense(30, activation='relu') # 初始化
self.dense_2 = tf.keras.layers.Dense(10, activation='softmax')
def call(self, inputs):
x = self.dense_1(inputs) # 前向传播过程
return self.dense_2(x)
实例化模型然后训练并评估
model = Model() # 实例化模型
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=64, epochs=10,
validation_data=(X_test, y_test))