环境准备
如果小伙伴们第一次接触TensorFlow与Keras,可以先看一下我的这篇文章做些环境准备(可以先忽略这篇文章里面代码实现部分,仅查看这里的环境准备部分即可)。
文章如下:
政安晨:【详细解析】【用TensorFlow从头实现】一个机器学习的神经网络小示例【解构演绎】https://blog.csdn.net/snowdenkeke/article/details/136108510大家准备好环境后,咱们开始。
再熟悉一些概念
神经网络的训练一般都是围绕以下这些概念进行的:
低阶张量操作:这是所有现代机器学习的底层架构,可以转化为TensorFlow API。
张量:包括存储神经网络状态的特殊张量(变量)。
张量运算:比如加法、relu、matmul。
反向传播:一种计算数学表达式梯度的方法(在TensorFlow中通过GradientTape对象来实现)。
层:多层可以构成模型。
损失函数:它定义了用于学习的反馈信号。
优化器:它决定学习过程如何进行。
指标:用于评估模型性能,比如精度等。
训练循环:执行小批量梯度随机下降。
概念再配合代码:热热身
咱们接下来,再用概念配合一些代码做些小尝试。
常数张量和变量
要使用TensorFlow,我们需要用到一些张量,创建张量需要给定初始值。
比如:可以创建全1张量或全0张量,也可以从随机分布中取值来创建张量。
为了进行接下来的演绎,咱们先导入tensorflow:
import tensorflow as tf
现在,咱们创建一个全1张量:
# 等同于np.ones(shape=(2, 1))
x = tf.ones(shape=(2, 1))
执行如下:
同时,咱们再创建一个全0张量:
# 等同于np.zeros(shape=(2, 1))
x = tf.zeros(shape=(2, 1))
执行如下:
接下来,咱们创建随机张量:
# 从均值为0、标准差为1的正态分布中抽取的随机张量
# 等同于np.random.normal(size=(3, 1), loc=0., scale=1.)
x = tf.random.normal(shape=(3, 1), mean=0., stddev=1.)
执行:
咱们再创建一个随机张量:
# 从0和1之间的均匀分布中抽取的随机张量
# 等同于np.random.uniform(size=(3, 1), low=0., high=1.)
x = tf.random.uniform(shape=(3, 1), minval=0., maxval=1.)
执行:
NumPy数组和TensorFlow张量之间的一个重要区别是:TensorFlow张量是不可赋值的,它是常量。
要训练模型,我们需要更新其状态,而模型状态是一组张量:
如果张量不可赋值,那么我们该怎么做呢?这时就需要用到变量(variable),tf.Variable是一个类,其作用是管理TensorFlow中的可变状态。
要创建一个变量,你需要为其提供初始值,比如随机张量,如下所示:
v = tf.Variable(initial_value=tf.random.normal(shape=(3, 1)))
print(v)
执行:
变量的状态可以通过其assign方法进行修改,如下:
v.assign(tf.ones((3, 1)))
这种方法也适用于变量的子集,这种方法也适用于变量的子集:
v[0, 0].assign(3.)
执行:
可以看到,咱们已经将这个张量的一个元素给改掉了。
与此类似,assign_add()和assign_sub()分别等同于+=和-=的效果。
比如 assign_add() :
v.assign_add(tf.ones((3, 1)))
上面这段代码的意思是在现有v张量上增加一个全1的形状为(3,1)的张量。
用TensorFlow进行张量运算
就像NumPy一样,TensorFlow提供了许多张量运算来表达数学公式。
下面是一些基本的数学运算(大家可以自己尝试):
a = tf.ones((2, 2))
# 求平方
b = tf.square(a)
# 求平方根
c = tf.sqrt(a)
# 两个张量(逐元素)相加
d = b + c
# 计算两个张量的积
e = tf.matmul(a, b)
# 两个张量(逐元素)相乘
e *= d
上述代码中每个运算都是立即执行的(执行完就可以打印出结果)。
GradientTape API
虽然您可能会觉得TensorFlow看起来很像NumPy,但是NumPy无法做到的却是:
TensorFlow能够检索任意可微表达式相对于其输入的梯度。
使用TensorFlow,您只需要创建一个GradientTape作用域,对一个或多个输入张量做一些计算,然后就可以检索计算结果相对于输入的梯度。
代码如下:
input_var = tf.Variable(initial_value=3.)
with tf.GradientTape() as tape:
result = tf.square(input_var)
gradient = tape.gradient(result, input_var)
到现在,您遇到的还只是tape.gradient()的输入张量是TensorFlow变量的情况,实际上,它的输入可以是任意张量,但在默认情况下只会监视可训练变量(trainable variable),如果要监视常数张量,那么必须对其调用tape.watch(),手动将其标记为被监视的张量。
对常数张量输入使用GradientTape:
input_const = tf.constant(3.)
with tf.GradientTape() as tape:
tape.watch(input_const)
result = tf.square(input_const)
gradient = tape.gradient(result, input_const)
之所以要监视数据,是因为如果预先存储计算梯度所需的全部信息,那么计算成本非常大。
为避免浪费资源,梯度带需要知道监视什么,它默认监视可训练变量,因为计算损失相对于可训练变量列表的梯度,是梯度带最常见的用途,梯度带是一个非常强大的工具,它甚至能够计算二阶梯度(梯度的梯度)。
要理解二阶梯度,咱们举个例子:
物体位置相对于时间的梯度是这个物体的速度,二阶梯度则是它的加速度。
咱们再进行一个小例子:
测量一个垂直下落的苹果的位置随时间的变化,并且发现它满足position(time) = 4.9 * time ** 2,那么它的加速度是多少?
我们可以用两个嵌套的梯度带找出答案:
代码如下:
time = tf.Variable(0.)
with tf.GradientTape() as outer_tape:
with tf.GradientTape() as inner_tape:
position = 4.9 * time ** 2
speed = inner_tape.gradient(position, time)
# 内梯度带计算出一个梯度,我们用外梯度带计算这个梯度的梯度。答案自然是4.9 * 2 = 9.8
acceleration = outer_tape.gradient(speed, time)
小伙伴们执行一下,可以看到结果:
热身完毕,咱们已经了解一些概念、完成了一些小尝试。
正式开始
咱们现在开始,用TensorFlow编写线性分类器。
现在您已经了解了张量、变量和张量运算,也知道如何计算梯度啦,这些已经足以构建任意基于梯度下降的机器学习模型。
首先,我们生成一些线性可分的数据:二维平面上的点,它们分为两个类别。
生成方法是从一个具有特定协方差矩阵和特定均值的随机分布中抽取坐标来生成每一类点。
直观上来看,协方差矩阵描述了点云的形状,均值则描述了点云在平面上的位置。我们设定,两个点云的协方差矩阵相同,但均值不同,也就是说,两个点云具有相同的形状,但位置不同。
现在有些小伙伴看不懂没关系,咱们跟着演绎,先走一遍。
现在,咱们在二维平面上随机生成两个类别的点:
import numpy as np
num_samples_per_class = 1000
# (本行及以下3行)生成第一个类别的点:1000个二维随机点。协方差矩阵为[[1, 0.5], [0.5, 1]],对应于一个从左下方到右上方的椭圆形点云
negative_samples = np.random.multivariate_normal(
mean=[0, 3],
cov=[[1, 0.5],[0.5, 1]],
size=num_samples_per_class)
# (本行及以下3行)生成第二个类别的点,协方差矩阵相同,均值不同
positive_samples = np.random.multivariate_normal(
mean=[3, 0],
cov=[[1, 0.5],[0.5, 1]],
size=num_samples_per_class)
negative_samples和positive_samples都是形状为(1000, 2)的数组。
咱们现在将二者堆叠成一个形状为(2000, 2)的数组:
inputs = np.vstack((negative_samples, positive_samples)).astype(np.float32)
接下来,我们来生成对应的目标标签(即一个形状为(2000, 1)的数组)。
其元素都是0或1:
如果输入inputs[i]属于类别0,则目标targets[i,0]为0;
如果inputs[i]属于类别1,则targets[i, 0]为1。
下列代码生成对应的目标标签(0和1):
targets = np.vstack((np.zeros((num_samples_per_class, 1), dtype="float32"),
np.ones((num_samples_per_class, 1), dtype="float32")))
咱们下面用Matplotlib来绘制数据图像(绘制两个点类的图像):
import matplotlib.pyplot as plt
plt.scatter(inputs[:, 0], inputs[:, 1], c=targets[:, 0])
plt.show()
关于Matplotlib的用法,我以前有几篇文章,大家可以参考:
政安晨:在Jupyter中【示例演绎】Matplotlib的官方指南(一){Pyplot tutorial}https://blog.csdn.net/snowdenkeke/article/details/136096870政安晨:在Jupyter中【示例演绎】Matplotlib的官方指南(二){Image tutorial}·{Python语言}https://blog.csdn.net/snowdenkeke/article/details/136100806政安晨:在Jupyter中【示例演绎】Matplotlib的官方指南(三){Plot全工作流程展示}·{Python语言}https://blog.csdn.net/snowdenkeke/article/details/136101342政安晨:在Jupyter中【示例演绎】Matplotlib的官方指南(四){Artist tutorial}·{Python语言}https://blog.csdn.net/snowdenkeke/article/details/136102477
从上面的图可以看到,咱们的数据确实是二维平面上的两类随机点。
现在咱们来创建一个线性分类器,用来学习划分这两个类别:
线性分类器采用仿射变换(prediction = W•input + b),我们对其进行训练,使预测值与目标值之差的平方最小化。
接下来,咱们来创建变量W和b,分别用随机值和零进行初始化:
# 输入是二维点
input_dim = 2
# 每个样本的输出预测值是一个分数值(如果分类器预测样本属于类别0,那么这个分数值会接近0;如果预测样本属于类别1,那么这个分数值会接近1)
output_dim = 1
W = tf.Variable(initial_value=tf.random.uniform(shape=(input_dim, output_dim)))
b = tf.Variable(initial_value=tf.zeros(shape=(output_dim,)))
下面是前向传播函数。
def model(inputs):
return tf.matmul(inputs, W) + b
因为这个线性分类器处理的是二维输入,所以W实际上只包含两个标量系数W1和W2:
W = [[w1], [w2]],b则是一个标量系数。
因此,对于给定的输入点[x,y],其预测值为:
prediction = [[w1], [w2]]•[x, y] + b = w1 * x + w2 * y + b。
下面是均方误差损失函数:
def square_loss(targets, predictions):
# per_sample_losses是一个与targets和predictions具有相同形状的张量,其中包含每个样本的损失值
per_sample_losses = tf.square(targets - predictions)
# 我们需要将每个样本的损失值平均为一个标量损失值,这由reduce_mean来实现
return tf.reduce_mean(per_sample_losses)
接下来就是训练步骤,即接收一些训练数据并更新权重W和b,以使数据损失值最小化:
下面是训练步骤函数:
learning_rate = 0.1
def training_step(inputs, targets):
# (本行及以下2行)在一个梯度带作用域内进行一次前向传播
with tf.GradientTape() as tape:
predictions = model(inputs)
loss = square_loss(targets, predictions)
# 检索损失相对于权重的梯度
grad_loss_wrt_W, grad_loss_wrt_b = tape.gradient(loss, [W, b])
# (本行及以下1行)更新权重
W.assign_sub(grad_loss_wrt_W * learning_rate)
b.assign_sub(grad_loss_wrt_b * learning_rate)
return loss
为简单起见,我们将进行批量训练,而不是小批量训练,即在所有数据上进行训练(计算梯度并更新权重),而不是小批量地进行迭代。
一方面,每个训练步骤的运行时间要长得多,因为我们要一次性计算2000个样本的前向传播和梯度。
另一方面,每次梯度更新将更有效地降低训练数据的损失,因为它包含了所有训练样本的信息,而不是只有128个随机样本。
因此,我们需要的迭代次数更少,而且应该使用比通常用于小批量训练更大的学习率(我们将使用 learning_rate =0.1)。如下代码所示:展示了批量训练循环。
for step in range(40):
loss = training_step(inputs, targets)
print(f"Loss at step {step}: {loss:.4f}")
经过40次迭代之后,训练损失值似乎稳定在0.025左右(0.0266)。
我们来绘制一下这个线性模型如何对训练数据点进行分类,由于目标值是0和1,因此如果一个给定输入点的预测值小于0.5,那么它将被归为类别0,而如果预测值大于0.5,则被归为类别1,如下图所示:
predictions = model(inputs)
plt.scatter(inputs[:, 0], inputs[:, 1], c=predictions[:, 0] > 0.5)
plt.show()
模型对训练数据的预测结果:与训练数据的目标值非常接近。
对于给定点[x, y],其预测值是prediction = [[w1],[w2]]•[x, y] + b = w1 * x + w2 * y + b。
因此,类别0的定义是w1 *x + w2 * y + b < 0.5,类别1的定义是w1 * x + w2 * y + b > 0.5。
你会发现,这实际上是二维平面上的一条直线的方程:w1 * x + w2 * y + b = 0.5。
在这条线上方的点属于类别1,下方的点属于类别0,你可能习惯看到像y = a * x + b这种形式的直线方程,如果将我们的直线方程写成这种形式,那么它将变为:y = - w1 / w2 * x + (0.5 - b) / w2。
我们来绘制这条直线:
# 在−1和4之间生成100个等间距的数字,用于绘制直线
x = np.linspace(-1, 4, 100)
# 直线方程
y = - W[0] / W[1] * x + (0.5 - b) / W[1]
# 绘制直线("-r"的含义是“将图像绘制为红色的线”)3
plt.plot(x, y, "-r")
# 在同一张图上绘制模型预测结果
plt.scatter(inputs[:, 0], inputs[:, 1], c=predictions[:, 0] > 0.5)
咱们将模型可视化为一条直线。
这就是线性分类器的全部内容:找到一条直线(或高维空间中的一个超平面)的参数,将两类数据整齐地分开。
这就是实现线性分类器的全部内容啦。