准备工作
咱们将通过这篇文章反复咀嚼我原来文章里提到的那篇《神经网络小实例》,大家可以先看看,比如做些环境准备等等(这是我的这篇文章的链接):
政安晨的机器学习笔记——基于Anaconda安装TensorFlow并尝试一个神经网络小实例https://blog.csdn.net/snowdenkeke/article/details/135841281上面这个实例里是基于Windows系统的,如果是Ubuntu系统,可以参考我的下面这篇文章里面的Ubuntu系统的环境搭建部分:
政安晨的机器学习笔记——跟着演练快速理解TensorFlow(适合新手入门)https://blog.csdn.net/snowdenkeke/article/details/135950931
待大家准备好环境后,咱们就开始。
先使用Keras回顾一下
咱们在这一部分先回顾一下我之前的文章,使用Keras API实现一下这个神经网络小实例,为后面仅用TensorFlow从头开始重新实现这个例子做好准备。
咱们先导入:
import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow import keras
from tensorflow.keras import layers
准备数据:
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255
现在,输入图像已经保存在float32类型的NumPy张量中,其形状分别为(60000, 784)(训练数据)和(10000, 784)(测试数据):
构建模型:
model = keras.Sequential([layers.Dense(512, activation="relu"),layers.Dense(10, activation="softmax")
])
这个模型里包含两个链接在一起的Dense层,每层都对是输入数据做一些简单的张量运算。每个层的属性里,都是权重张量,这里面保存了模型所学到的信息(知识)。
编译模型:
model.compile(optimizer="rmsprop",loss="sparse_categorical_crossentropy",metrics=["accuracy"])
训练循环:
model.fit(train_images, train_labels, epochs=5, batch_size=128)
模型开始在训练数据上进行迭代(每个小批量包含128个样本),共迭代5轮(在所有训练数据上迭代一次叫作一轮(epoch))。
对于每批数据,模型会计算损失相对于权重的梯度(利用反向传播算法,这一算法源自微积分的链式法则),并将权重沿着减小该批量对应损失值的方向移动。5轮之后,模型共执行2345次梯度更新(每轮469次),模型损失值将变得足够小,使得模型能够以很高的精度对手写数字进行分类。
测试预测:
咱们从刚才导入的测试图片中选取一张:test_images[7]
这张图片数字是:test_labels[7]
咱们在Jupyter Notebook中看到这个数字是 9 。
接下来,咱们用这张图片:test_images[7] 来测试咱们刚才训练好的模型:
test_digits = test_images[0:10]
predictions = model.predict(test_digits)
predictions[7]
predictions[7].argmax()
我的执行如下:
这个模型已经预测到了正确的数字图像:9。
正式开始
现在咱们开始用TensorFlow从头重新实现上面这个小示例。
如何完全理解神经网络呢,让咱们从头开始重新实现整个过程。
当然,咱们不会重新实现基本的张量运算,也不会手动实现反向传播,咱们只是几乎不会用到Keras的功能。
实现一个简单的Dense类
Dense层实现了下列输入变换,其中W和b是模型参数,activation是一个逐元素的函数。
(比如relu,但最后一层是softmax)。
output = activation(dot(W, input) + b)
现在咱们来实现一个简单的Python类NaiveDense,它创建了两个TensorFlow变量W和b,并定义了一个__call__()方法供外部调用,以实现上述变换。
class NaiveDense:def __init__(self, input_size, output_size, activation):self.activation = activation# 创建一个形状为(input_size, output_size)的矩阵W,并将其随机初始化w_shape = (input_size, output_size)w_initial_value = tf.random.uniform(w_shape, minval=0, maxval=1e-1)self.W = tf.Variable(w_initial_value)# 创建一个形状为(output_size,)的零向量bb_shape = (output_size,)b_initial_value = tf.zeros(b_shape)self.b = tf.Variable(b_initial_value)# 前向传播def __call__(self, inputs): return self.activation(tf.matmul(inputs, self.W) + self.b)@property # 获取该层权重的便捷方法def weights(self):return [self.W, self.b]
实现一个简单的Sequential类
接下来,咱们创建一个NaiveSequential类,将这些层链接起来,它封装了一个层列表,并定义了一个__call__()方法供外部调用,这个方法将按顺序调用输入的层,它还有一个weights属性,用于记录该层的参数。
class NaiveSequential:def __init__(self, layers):self.layers = layersdef __call__(self, inputs):x = inputsfor layer in self.layers:x = layer(x)return x@propertydef weights(self):weights = []for layer in self.layers:weights += layer.weightsreturn weights
利用这个NaiveDense类和NaiveSequential类,我们可以创建一个与Keras类似的模型。
model = NaiveSequential([NaiveDense(input_size=28 * 28, output_size=512, activation=tf.nn.relu),NaiveDense(input_size=512, output_size=10, activation=tf.nn.softmax)
])
assert len(model.weights) == 4
批量生成器
现在,咱们需要对MNIST数据进行小批量迭代:
import mathclass BatchGenerator:def __init__(self, images, labels, batch_size=128):assert len(images) == len(labels)self.index = 0self.images = imagesself.labels = labelsself.batch_size = batch_sizeself.num_batches = math.ceil(len(images) / batch_size)def next(self):images = self.images[self.index : self.index + self.batch_size]labels = self.labels[self.index : self.index + self.batch_size]self.index += self.batch_sizereturn images, labels
完成一次训练步骤
接下来一步就是“训练步骤”,即在一批数据上运行模型后更新模型权重。
咱们需要做到以下几点:
(1)计算模型对图像批量的预测值。
(2)根据实际标签,计算这些预测值的损失值。
(3)计算损失相对于模型权重的梯度。
(4)将权重沿着梯度的反方向移动一小步。
现在,咱们准备计算梯度:
def one_training_step(model, images_batch, labels_batch):# (本行及以下4行)运行前向传播,即在GradientTape作用域内计算模型预测值with tf.GradientTape() as tape: predictions = model(images_batch)per_sample_losses = tf.keras.losses.sparse_categorical_crossentropy(labels_batch, predictions)average_loss = tf.reduce_mean(per_sample_losses)# 计算损失相对于权重的梯度,输出gradients是一个列表,每个元素对应model.weights列表中的权重gradients = tape.gradient(average_loss, model.weights)# 利用梯度来更新权重(稍后给出这个函数的定义)update_weights(gradients, model.weights)return average_loss
“更新权重”这一步(由update_weights函数实现)的目的,就是将权重沿着减小批量损失值的方向移动“一小步”。
移动幅度由学习率决定,它通常是一个很小的数,要实现这个update_weights函数,最简单的方法就是从每个权重中减去gradient * learning_rate。
learning_rate = 1e-3def update_weights(gradients, weights):for g, w in zip(gradients, weights):# assign_sub相当于TensorFlow变量的-=w.assign_sub(g * learning_rate)
在实践中,你几乎不会像这样手动实现权重更新,而是会使用Keras的Optimizer实例,如下:
from tensorflow.keras import optimizers
optimizer = optimizers.SGD(learning_rate=1e-3)
def update_weights(gradients, weights):
optimizer.apply_gradients(zip(gradients, weights))
(最后这一段代码您不必在Python环境中输入)
训练循环
咱们现在开始完整的训练循环。
说明:一轮训练就是对训练数据的每个批量都重复上述训练步骤,而完整的训练循环就是重复多轮训练。
先定义一下这个拟合函数:
def fit(model, images, labels, epochs, batch_size=128):for epoch_counter in range(epochs):print(f"Epoch {epoch_counter}")batch_generator = BatchGenerator(images, labels)for batch_counter in range(batch_generator.num_batches):images_batch, labels_batch = batch_generator.next()loss = one_training_step(model, images_batch, labels_batch)if batch_counter % 100 == 0:print(f"loss at batch {batch_counter}: {loss:.2f}")
现在咱们开始运行一下:
依旧使用上面咱们用到的图像训练的例子(如果您已经导入了图像数据,则下述代码无需执行):
from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255
开始训练:
fit(model, train_images, train_labels, epochs=10, batch_size=128)
训练过程如下:
评估模型
咱们可以评估模型,方法是对模型在测试图像上的预测值取argmax,并将其与预期标签进行比较。
import numpy as nppredictions = model(test_images)# 对TensorFlow张量调用.numpy(),可将其转换为NumPy张量
predictions = predictions.numpy()predicted_labels = np.argmax(predictions, axis=1)
matches = predicted_labels == test_labels
print(f"accuracy: {matches.mean():.2f}")
我的执行如下:
好啦,聪明的您已经可以看到咱们对刚才“手工”实现的模型的评估值啦。
发现了吧,用几行Keras代码就能完成的工作,手动实现起来还是挺费劲的。
但手动实现一遍之后,你现在应该已经清楚地了解在调用fit()时神经网络内部都发生了什么。
拥有这种对代码底层原理的思维模型,可以让你更好地使用Keras API的高级功能。