TensorFlow 2 实现线性回归
线性回归是入门机器学习必学的算法,其也是最基础的算法之一。接下来,我们以线性回归为例,使用 TensorFlow 2 提供的 API 和 Eager Execution 机制对其进行实现。
线性回归是一种较为简单,但十分重要的机器学习方法,它也是神经网络的基础。如下所示,线性回归要解决的问题就是如何找到最理想的直线去拟合散点样本。
对于一个线性回归问题,一般来讲有 2 种解决方法,分别是:最小二乘法和梯度下降法。其中最小二乘法又分为两种求解思路:代数求解和矩阵求解。本次实验,我们将使用梯度下降方法来解决线性回归问题。同时,实验将使用 TensorFlow 低阶 API 和高阶 API 两种方法进行实现。
低阶 API 实现
TensorFlow 的低阶 API 实现,实际上就是利用最基本的函数和组件,结合 Eager Execution 机制来完成。实验首先初始化一组随机数据样本,并添加噪声,然后将其可视化出来。
在这个过程中,我们全部使用 TensorFlow 提供的 API 来完成。平时,你也可以使用 NumPy 来初始化数组,如今 TensorFlow 和 NumPy 结合的越来越紧密了。
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
上方展示了一个标准的 TensorFlow 模型类的构建方法,希望你能够对其留下印象,后面的复杂神经网络构建过程还会不断地使用这一结构。
对于随机初始化的w和b,我们可以将其拟合直线绘制到样本散点图中。
model = Model() # 实例化模型
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
正常情况下,你应该会看到直线并没有很好地拟合样本。示例效果如下(仅参考):
当然,由于是随机初始化,也有极小概率一开始拟合效果非常好,那么重新执行一次上面的单元格另外随机初始化一组数据即可。尽可能保证一开始随机初始化的直线无法准确拟合数据,方便后续直观感受优化过程的效果。
然后,我们定义线性回归使用到的损失函数。这里使用线性回归问题中常用的平方损失函数。对于线性回归问题中与数学相关的知识点,本次实验不再推导和讲解。
公式中,f(w,b,xi) 是模型根据 xi计算的预测值,yi则表示真实值。
接下来,根据公式实现损失计算函数,同样使用 TensorFlow 中的相关函数。对于这些函数的详细用法,我们不再罗列,推荐大家阅读最权威的、最准确的官方文档。
def loss_fn(model, x, y):
y_ = model(x)
return tf.reduce_mean(tf.square(y_ - y))
接下来,就可以开始迭代过程了,这也是最关键的一步。使用梯度下降法求解线性回归的问题中,我们首先需要计算参数的梯度,然后使用梯度下降法来更新参数。
梯度下降方法是数学中的一种最优化方法,常被用于神经网络的迭代优化过程。简单概括,梯度下降法就是沿着梯度下降方向去寻找损失函数的极小值(梯度的反方向)。过程如下图所示。
TensorFlow 2 中的 Eager Execution 提供了 tf.GradientTape 用于追踪梯度。所以,下面我们对照着参数更新公式来实现梯度下降法的迭代更新过程。
EPOCHS = 10 # 全部数据迭代 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(f'Epoch [{epoch}/{EPOCHS}], loss [{loss}], W/b [{model.W.numpy()}/{model.b.numpy()}]')
上面的代码中,我们初始化 tf.GradientTape() 以追踪梯度,然后使用 tape.gradient 方法就可以计算梯度了。值得注意的是,tape.gradient() 第二个参数支持以列表形式传入多个参数同时计算梯度。紧接着,使用 .assign_sub 即可完成公式中的减法操作用以更新梯度。
这是一个标准的梯度下降过程,希望大家能对代码留下深刻印象,后续会不断重复使用。你可以看到,损失函数的值随着迭代过程不断减小,意味着我们离最优化参数不断接近。
最终,我们绘制参数学习完成之后,模型的拟合结果。
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
如无意外,你将得到一个比随机参数好很多的拟合直线。示例效果如下(仅参考):
由于是随机初始化参数,如果迭代后拟合效果仍然不好,一般是迭代次数太少的原因。你可以重复执行上面的迭代单元格多次,增加参数更新迭代次数,即可改善拟合效果。此提示对后面的内容同样有效。
高阶 API 实现
TensorFlow 2 中提供了大量的高阶 API 帮助我们快速构建所需模型,接下来,我们使用一些新的 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() 来追踪梯度,我们简化损失计算和更新的过程。首先,损失函数无需再自行构造,我们可以直接使用 TensorFlow 提供的平方损失函数 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(f'Epoch [{epoch}/{EPOCHS}], loss [{loss}]')
其次,使用 model.variables 即可读取可参数的列表,无需像上面那样手动传入参数。这里不再按公式手动更新梯度,而是使用现有的随机梯度下降函数 tf.keras.optimizers.SGD,然后使用 apply_gradients 即可更新梯度。
TensorFlow 中没有提供上方手动实现的梯度下降算法,只提供了随机梯度下降算法。随机梯度下降可以看成梯度下降的升级版本,具体区别大家可以自行搜索了解。
最终,同样将迭代完成的参数绘制拟合直线到原图中。
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
一般情况下,你应该能看到一个更好的拟合结果。当然,如果拟合效果不好,可能是迭代批次太少,可以重复执行优化过程。
Keras 方式实现
配合 TensorFlow 提供的高阶 API,我们省去了定义线性函数,定义损失函数,以及定义优化算法等 3 个步骤。至此,你应该可以初步感受到 TensorFlow 的易用性和存在的必要性了。不过,上面的高阶 API 实现过程实际上还不够精简,我们可以完全使用 TensorFlow Keras API 来实现线性回归。
Keras 本来是一个用 Python 编写的独立高阶神经网络 API,它能够以 TensorFlow, CNTK,或者 Theano 作为后端运行。目前,TensorFlow 已经吸纳 Keras,并组成了 tf.keras 模块。官方介绍,tf.keras 和单独安装的 Keras 略有不同,但考虑到未来的发展趋势,实验以学习 tf.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, epochs=10, batch_size=32) # 训练模型
batch_size 是采用小批次训练的参数,主要用于解决一次性传入数据过多无法训练的问题。当然,由于示例数据本身较少,这里意义不大,但还是按照常规使用方法进行设置。
接下来,我们可以绘制最终的训练结果了。
plt.scatter(X, y)
plt.plot(X, model(X), c='r')
你会发现,完全使用 Keras 高阶 API 实际上只需要 4 行核心代码即可完成,相比于最开始的低阶 API 简化了很多。
model = tf.keras.Sequential() # 新建顺序模型
model.add(tf.keras.layers.Dense(units=1, input_dim=1)) # 添加线性层
model.compile(optimizer='sgd', loss='mse') # 定义损失函数和优化方法
model.fit(X, y, epochs=10, batch_size=32) # 训练模型
低阶 API 实现多项式回归
区别于线性函数,多项式是由多个单项式组合的代数表达式。例如,下面是一个一元二次多项式:
我们都知道,线性函数是直线,而多项式函数则可以是曲线。而在很多时候,散点通过曲线拟合比直线更加合理。例如下方这组散点样本:
from matplotlib import pyplot as plt
import numpy as np
%matplotlib inline
# 直接运行加载示例数据
x = np.array([0.12, 0.21, 0.32, 0.41, 0.51, 0.62, 0.73, 0.86, 0.97, 0.99])
y = np.array([0.53, 0.57, 0.63, 0.70, 0.79, 0.91, 1.05, 1.246, 1.42, 1.51])
plt.scatter(x, y)
接下来,我们希望你利用 TensorFlow 提供的方法,结合前面实验中学习到梯度下降法求解线性回归拟合参数的内容,完成一元二次多项式对示例样本的拟合求解过程。最终,找出合理的一元二次多项式参数,并绘制出拟合曲线。
import tensorflow as tf
tf.__version__
由于 TensorFlow Keras 并未提供多项式层,所以你只能使用 TensorFlow 低阶 API 完成构建。
首先,根据多项式公式,定义模型类:
class Model(object):
def __init__(self):
self.a = tf.Variable(tf.random.uniform([1])) # 随机初始化参数
self.b = tf.Variable(tf.random.uniform([1]))
self.c = tf.Variable(tf.random.uniform([1]))
def __call__(self, x):
return self.a * x * x + self.b * x + self.c # a*x^2 + b*x + c
然后,实现损失函数,可选择与线性回归实现时一样的损失函数:
def loss_fn(model, x, y):
y_ = model(x)
return tf.reduce_mean(tf.square(y_ - y))
最后,利用梯度下降法实现优化迭代过程:
EPOCHS = 20 # 全部数据迭代 20 次
LEARNING_RATE = 0.1 # 学习率
model = Model() # 实例化模型
x = tf.constant(x, dtype=tf.float32) # 转换为张量
y = tf.constant(y, dtype=tf.float32) # 转换为张量
for epoch in range(EPOCHS): # 迭代次数
with tf.GradientTape() as tape: # 追踪梯度
loss = loss_fn(model, x, y) # 计算损失
da, db, dc = tape.gradient(loss, [model.a, model.b, model.c]) # 计算梯度
model.a.assign_sub(LEARNING_RATE * da) # 更新梯度
model.b.assign_sub(LEARNING_RATE * db)
model.c.assign_sub(LEARNING_RATE * dc)
# 输出计算过程
print(f'Epoch [{epoch}/{EPOCHS}], loss [{loss}]')
绘制出迭代完成之后的拟合曲线图:
# 为了绘制曲线图生成的等间距 x 值
X = tf.linspace(0.0, 1.0, 50)
plt.scatter(x, y)
plt.plot(X, model(X), c='r')