政安晨:【详细解析】【用TensorFlow从头实现】一个机器学习的神经网络小示例【解构演绎】

准备工作

咱们将通过这篇文章反复咀嚼我原来文章里提到的那篇《神经网络小实例》,大家可以先看看,比如做些环境准备等等(这是我的这篇文章的链接):

政安晨的机器学习笔记——基于Anaconda安装TensorFlow并尝试一个神经网络小实例icon-default.png?t=N7T8https://blog.csdn.net/snowdenkeke/article/details/135841281上面这个实例里是基于Windows系统的,如果是Ubuntu系统,可以参考我的下面这篇文章里面的Ubuntu系统的环境搭建部分:

政安晨的机器学习笔记——跟着演练快速理解TensorFlow(适合新手入门)icon-default.png?t=N7T8https://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,)的零向量b
        b_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 = layers

    def __call__(self, inputs):
        x = inputs
        for layer in self.layers:
            x = layer(x)
        return x

    @property
    def weights(self):
       weights = []
       for layer in self.layers:
           weights += layer.weights
       return 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 math

class BatchGenerator:
    def __init__(self, images, labels, batch_size=128):
        assert len(images) == len(labels)
        self.index = 0
        self.images = images
        self.labels = labels
        self.batch_size = batch_size
        self.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_size
        return 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-3

def 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 np

predictions = 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的高级功能。


相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-02-17 13:24:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-17 13:24:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-17 13:24:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-17 13:24:01       20 阅读

热门阅读

  1. 简单试验:用Excel进行爬虫

    2024-02-17 13:24:01       26 阅读
  2. Vue的双向绑定数据的原理

    2024-02-17 13:24:01       31 阅读
  3. 【Linux】指令 【scp】

    2024-02-17 13:24:01       26 阅读
  4. Matplotlib plt.plot数据可视化应用案例

    2024-02-17 13:24:01       30 阅读
  5. nlp中如何数据增强

    2024-02-17 13:24:01       30 阅读
  6. Windows安装minio

    2024-02-17 13:24:01       42 阅读