TensorFlow 入门

前两章讲过,训练神经网络主要围绕以下概念进行。

  • 首先是低阶张量操作。这是所有现代机器学习的底层架构,可以转化为 TensorFlow API。

    • 张量,包括存储神经网络状态的特殊张量(变量)。
    • 张量运算,比如加法、relu、matmul。
    • 反向传播,一种计算数学表达式梯度的方法(在 TensorFlow 中通过 GradientTape
      对象来实现)。
  • 然后是高阶深度学习概念。这可以转化为 Keras API。

  • ,多层可以构成模型。

  • 损失函数,它定义了用于学习的反馈信号。

  • 优化器,它决定学习过程如何进行。

  • 评估模型性能的指标,比如精度。

  • 训练循环,执行小批量梯度随机下降。

在第2 章中,你已经初步接触了TensorFlow 和Keras 的一些API,并使用了TensorFlow 的
Variable 类、matmul 运算和GradientTape。你已经将Keras 的Dense 层实例化,并将其
打包到一个Sequential 模型中,然后使用fit() 方法训练该模型。

下面我们来进一步了解,在实践中如何使用TensorFlow 和Keras 来处理这些概念。

常数张量和变量

要使用TensorFlow,我们需要用到一些张量。创建张量需要给定初始值。例如,可以创建
全1 张量或全0 张量(见代码清单3-1),也可以从随机分布中取值来创建张量(见代码清单
3-2)。

代码清单3-1 全1 张量或全0 张量

image

import tensorflow as tf

x=tf.ones(shape=(2,1))
print(x)

运行结果

tf.Tensor(
[[1.]
 [1.]], shape=(2, 1), dtype=float32)

代码清单3-2 随机张量

image

import tensorflow as tf

x=tf.random.normal(shape=(2,1))

print(x)

运行结果

tf.Tensor(
[[0.47284585]
 [0.06496517]], shape=(2, 1), dtype=float32)

image

import tensorflow as tf

x=tf.random.normal(shape=(3,1),mean=0,stddev=1.)

print(x)

运行结果

tf.Tensor(
[[-0.08254339]
 [-0.8263557 ]
 [-0.09176937]], shape=(3, 1), dtype=float32)
import tensorflow as tf

#x=tf.random.normal(shape=(3,1),mean=0,stddev=1.)
x=tf.random.uniform(shape=(3,1),minval=0.,maxval=1.)

print(x)

运行结果

tf.Tensor(
[[0.20912445]
 [0.04400301]
 [0.9201925 ]], shape=(3, 1), dtype=float32)

NumPy 数组和TensorFlow 张量之间的一个重要区别是,TensorFlow 张量是不可赋值的,它
是常量。举例来说,在NumPy 中,你可以执行以下操作,如代码清单3-3 所示。

代码清单3-3 NumPy 数组是可赋值的

import numpy as np

x=np.ones(shape=(2,2))

x[0,0]=0.

print(x)

代码清单3-4 TensorFlow 张量是不可赋值的

image
要训练模型,我们需要更新其状态,而模型状态是一组张量。如果张量不可赋值,那么
我们该怎么做呢?这时就需要用到变量(variable)。tf.Variable 是一个类,其作用是管理
TensorFlow 中的可变状态。第2 章末尾的训练循环实现已经初步展示了这个类的作用。

要创建一个变量,你需要为其提供初始值,比如随机张量,如代码清单3-5 所示。

代码清单3-5 创建一个TensorFlow 变量

import tensorflow as tf

v=tf.Variable(initial_value=tf.random.normal(shape=(3,1)))

print(v)

运行结果

<tf.Variable 'Variable:0' shape=(3, 1) dtype=float32, numpy=
array([[ 2.4723313 ],
       [-0.39040148],
       [-0.05866471]], dtype=float32)>

变量的状态可以通过其assign 方法进行修改,如代码清单3-6 所示。

代码清单3-6 为TensorFlow 变量赋值

>>> v.assign(tf.ones((3, 1)))
array([[1.],
[1.],
[1.]], dtype=float32)

这种方法也适用于变量的子集,如代码清单3-7 所示。

代码清单3-7 为TensorFlow 变量的子集赋值

>>> v[0, 0].assign(3.)
array([[3.],
[1.],
[1.]], dtype=float32)

与此类似,assign_add() 和assign_sub() 分别等同于+= 和-= 的效果,如代码清单3-8
所示。

代码清单3-8 使用assign_add()

>>> v.assign_add(tf.ones((3, 1)))
array([[2.],
[2.],
[2.]], dtype=float32)

张量运算:用TensorFlow 进行数学运算

就像NumPy 一样,TensorFlow 提供了许多张量运算来表达数学公式。我们来看几个例子,
如代码清单3-9 所示。

代码清单3-9 一些基本的数学运算

image
重要的是,代码清单3-9 中的每一个运算都是即刻执行的:任何时候都可以打印出当前结果,
就像在NumPy 中一样。我们称这种情况为急切执行(eager execution)。

重温GradientTape API

读到这里,你可能认为TensorFlow 看起来很像NumPy。但是NumPy 无法做到的是,检索
任意可微表达式相对于其输入的梯度。你只需要创建一个GradientTape 作用域,对一个或多
个输入张量做一些计算,然后就可以检索计算结果相对于输入的梯度,如代码清单3-10 所示。

代码清单3-10 使用GradientTape

input_var = tf.Variable(initial_value=3.)
with tf.GradientTape() as tape:
result = tf.square(input_var)
gradient = tape.gradient(result, input_var)

要检索模型损失相对于权重的梯度,最常用的方法是gradients = tape.gradient(loss,
weights)。我们在第2 章中用过这一方法。

至此,你只遇到过tape.gradient() 的输入张量是TensorFlow 变量的情况。实际上,它
的输入可以是任意张量。但在默认情况下只会监视可训练变量(trainable variable)。如果要监视
常数张量,那么必须对其调用tape.watch(),手动将其标记为被监视的张量,如代码清单3-11
所示。

代码清单3-11 对常数张量输入使用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,那么它的加速度是多少?我们可以用两个嵌套的梯度带找出答案,如代码
清单3-12 所示。

代码清单3-12 利用嵌套的梯度带计算二阶梯度

image

一个端到端的例子:用TensorFlow 编写线性分类器

你已经了解了张量、变量和张量运算,也知道如何计算梯度。这些知识足以构建任意基于
梯度下降的机器学习模型。而你现在才读到第3 章!

在参加机器学习面试时,面试官可能会要求你用TensorFlow 从头开始实现一个线性分类器。
这是一项非常简单的任务,可以用于考察求职者是否具有基本的机器学习背景。学完本节内容,
你将能够通过这道面试关,用新学到的TensorFlow 知识来实现这样一个线性分类器。

首先,我们生成一些线性可分的数据:二维平面上的点,它们分为两个类别。生成方法
是从一个具有特定协方差矩阵和特定均值的随机分布中抽取坐标来生成每一类点,如代码清单
3-13 所示。直观上来看,协方差矩阵描述了点云的形状,均值则描述了点云在平面上的位置,
如图3-6 所示。我们设定,两个点云的协方差矩阵相同,但均值不同。也就是说,两个点云具
有相同的形状,但位置不同。

代码清单3-13 在二维平面上随机生成两个类别的点

image

image
在代码清单3-13 中,negative_samples 和positive_samples 都是形状为(1000, 2)
的数组。我们将二者堆叠成一个形状为(2000, 2) 的数组,如代码清单3-14 所示。

代码清单3-14 将两个类别堆叠成一个形状为(2000, 2) 的数组

inputs = np.vstack((negative_samples, positive_samples)).astype(np.float32)

如代码清单3-15 所示,我们来生成对应的目标标签,即一个形状为(2000, 1) 的数组,其元
素都是0 或1:如果输入inputs[i] 属于类别0,则目标targets[i, 0] 为0;如果inputs[i]
属于类别1,则targets[i, 0] 为1。

代码清单3-15 生成对应的目标标签(0 和1)
下面用Matplotlib 来绘制数据图像,如代码清单3-16 和图3-6 所示。

代码清单3-16 绘制两个点类的图像

import matplotlib.pyplot as plt
plt.scatter(inputs[:, 0], inputs[:, 1], c=targets[:, 0])
plt.show()

我们的数据:二维平面上的两类随机点(见彩插)

现在我们来创建一个线性分类器,用来学习划分这两个类别。线性分类器采用仿射变换
(prediction = W • input + b),我们对其进行训练,使预测值与目标值之差的平方最
小化。

后面你会看到,这个例子实际上比第2 章结尾介绍的端到端的双层神经网络要简单得多。这次你应该能够理解每一行代码的含义。
我们来创建变量W 和b,分别用随机值和零进行初始化,如代码清单3-17 所示。

代码清单3-17 创建线性分类器的变量

image
代码清单3-18 展示了前向传播函数。

代码清单3-18 前向传播函数

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。

代码清单3-19 展示了均方误差损失函数。

代码清单3-19 均方误差损失函数

image
接下来就是训练步骤,即接收一些训练数据并更新权重W 和b,以使数据损失值最小化,
如代码清单3-20 所示。

代码清单3-20 训练步骤函数

image
为简单起见,我们将进行批量训练,而不是小批量训练,即在所有数据上进行训练(计算
梯度并更新权重),而不是小批量地进行迭代。一方面,每个训练步骤的运行时间要长得多,因
为我们要一次性计算2000 个样本的前向传播和梯度。另一方面,每次梯度更新将更有效地降
低训练数据的损失,因为它包含了所有训练样本的信息,而不是只有128 个随机样本。因此,
我们需要的迭代次数更少,而且应该使用比通常用于小批量训练更大的学习率(我们将使用
learning_rate = 0.1,如代码清单3-20 所示)。代码清单3-21 展示了批量训练循环。

代码清单3-21 批量训练循环

for step in range(40):
loss = training_step(inputs, targets)
print(f"Loss at step {step}: {loss:.4f}")

经过40 次迭代之后,训练损失值似乎稳定在0.025 左右。我们来绘制一下这个线性模型如
何对训练数据点进行分类。由于目标值是0 和1,因此如果一个给定输入点的预测值小于0.5,
那么它将被归为类别0,而如果预测值大于0.5,则被归为类别1,如图3-7 所示。

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

我们来绘制这条直线,如图3-8 所示。

image

将模型可视化为一条直线(见彩插)
这就是线性分类器的全部内容:找到一条直线(或高维空间中的一个超平面)的参数,将
两类数据整齐地分开。
完整代码

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# 生成线性可分的数据
num_samples_per_class = 1000

# 生成负样本 (类别0)
negative_samples = np.random.multivariate_normal(
    mean=[0, 0],  # 均值为(0,0)
    cov=[[1, 0.5], [0.5, 1]],  # 协方差矩阵
    size=num_samples_per_class
)

# 生成正样本 (类别1)
positive_samples = np.random.multivariate_normal(
    mean=[1, 1],  # 均值为(1,1)
    cov=[[1, 0.5], [0.5, 1]],  # 协方差矩阵
    size=num_samples_per_class
)

# 堆叠输入数据
inputs = np.vstack((negative_samples, positive_samples)).astype(np.float32)

# 生成目标标签 (0和1)
targets = np.vstack((np.zeros((num_samples_per_class, 1), dtype=np.float32),
                     np.ones((num_samples_per_class, 1), dtype=np.float32)))

# 绘制原始数据
plt.figure(figsize=(8, 6))
plt.scatter(inputs[:, 0], inputs[:, 1], c=targets[:, 0], cmap='bwr', alpha=0.7)
plt.title("原始数据分布 - 二维平面上的两类随机点")
plt.xlabel("X 坐标")
plt.ylabel("Y 坐标")
plt.grid(True, alpha=0.3)
plt.axis([-3, 4, -3, 4])  # 设置坐标轴范围
plt.show()

# 创建线性分类器变量
input_dim = 2  # 输入维度
output_dim = 1  # 输出维度

# 权重矩阵 W,形状为(2,1)
W = tf.Variable(initial_value=tf.random.uniform(shape=(input_dim, output_dim)))
# 偏置 b,形状为(1,)
b = tf.Variable(initial_value=tf.zeros(shape=(output_dim,)))


# 前向传播函数
def model(inputs):
    return tf.matmul(inputs, W) + b


# 均方误差损失函数
def square_loss(targets, predictions):
    per_sample_losses = tf.square(targets - predictions)
    return tf.reduce_mean(per_sample_losses)


# 学习率
learning_rate = 0.1


# 训练步骤函数
def training_step(inputs, targets):
    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])

    # 更新权重
    W.assign_sub(learning_rate * grad_loss_wrt_W)
    b.assign_sub(learning_rate * grad_loss_wrt_b)

    return loss


# 批量训练循环
print("开始训练线性分类器...")
for step in range(40):
    loss = training_step(inputs, targets)
    if step % 5 == 0:  # 每5步打印一次损失
        print(f"第 {step} 步: 损失值 = {loss:.4f}")

print("训练完成!")

# 绘制模型预测结果
plt.figure(figsize=(8, 6))
predictions = model(inputs)
plt.scatter(inputs[:, 0], inputs[:, 1], c=predictions[:, 0] > 0.5, cmap='bwr', alpha=0.7)
plt.title("模型预测结果 - 与训练数据的目标值非常接近")
plt.xlabel("X 坐标")
plt.ylabel("Y 坐标")
plt.grid(True, alpha=0.3)
plt.axis([-3, 4, -3, 4])  # 设置坐标轴范围
plt.show()

# 绘制决策边界直线
plt.figure(figsize=(8, 6))
plt.scatter(inputs[:, 0], inputs[:, 1], c=targets[:, 0], cmap='bwr', alpha=0.7)

# 决策边界方程: w1*x + w2*y + b = 0.5
# 转换为: y = (0.5 - b - w1*x) / w2
w1, w2 = W.numpy()[0][0], W.numpy()[1][0]
b_val = b.numpy()[0]
x = np.linspace(-3, 4, 100)
y = (0.5 - b_val - w1 * x) / w2

plt.plot(x, y, 'g-', linewidth=3, label='决策边界')
plt.title("决策边界可视化 - 线性分类器")
plt.xlabel("X 坐标")
plt.ylabel("Y 坐标")
plt.grid(True, alpha=0.3)
plt.legend()
plt.axis([-3, 4, -3, 4])  # 设置坐标轴范围
plt.show()

# 打印最终模型参数
print(f"\n最终模型参数:")
print(f"权重 W = [{w1:.4f}, {w2:.4f}]")
print(f"偏置 b = {b_val:.4f}")
print(f"决策边界方程: {w1:.4f} * x + {w2:.4f} * y + {b_val:.4f} = 0.5")

运行结果

开始训练线性分类器...
第 0 步: 损失值 = 2.1481
第 5 步: 损失值 = 0.2673
第 10 步: 损失值 = 0.2015
第 15 步: 损失值 = 0.1883
第 20 步: 损失值 = 0.1856
第 25 步: 损失值 = 0.1850
第 30 步: 损失值 = 0.1849
第 35 步: 损失值 = 0.1849
训练完成!

最终模型参数:
权重 W = [0.1307, 0.1291]
偏置 b = 0.3806
决策边界方程: 0.1307 * x + 0.1291 * y + 0.3806 = 0.5

image

image

image

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐