TensorFlow 2.0 构建神经网络

image

前面,我们以线性回归作为例子演示了 TensorFlow 2.0 的新变化和新特性。实际上,TensorFlow 等深度学习框架更重要的作用是用于构建人工神经网络。所以,本次实验将利用 TensorFlow 2.0 来构建一个简单的前馈神经网络。

低阶 API 构建

学习完线性回归中的低阶 API 实现方法,那么使用 TensorFlow 2.0 低阶 API 构建神经网络实际上就比较简单了。这里,我们先加载一组数据,DIGITS 数据集是 scikit-learn 提供的简单手写字符识别数据集。

我们读取数据集并进行简单切分,这里对字符标签进行了独热编码方便后面计算损失值。

import numpy as np
import tensorflow as tf
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

digits = load_digits()
digits_y = np.eye(10)[digits.target.reshape(-1)]  # 标签独热编码
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits_y,
                                                    test_size=0.2, random_state=1)

X_train.shape, X_test.shape, y_train.shape, y_test.shape
((1437, 64), (360, 64), (1437, 10), (360, 10))

下面,使用 TensorFlow 2.0 低阶 API 构建一个包含 1 个隐含层的简单神经网络结构。神经网络的输入是单个手写字符样本的向量长度 64,隐含层输入为 30,最终的输出层为 10。

image

特别地,我们对隐含层进行 RELU 激活,输出层不激活。输出层的单样本长度为 10,这样正好就和上方独热编码后的值对应上了。

class Model(object):
    def __init__(self):
        self.W1 = tf.Variable(tf.random.normal([64, 30]))  # 随机初始化张量参数
        self.b1 = tf.Variable(tf.random.normal([30]))

        self.W2 = tf.Variable(tf.random.normal([30, 10]))
        self.b2 = tf.Variable(tf.random.normal([10]))

    def __call__(self, x):
        x = tf.cast(x, tf.float32)  # 转换输入数据类型
        # 线性计算 + RELU 激活
        fc1 = tf.nn.relu(tf.add(tf.matmul(x, self.W1), self.b1))
        fc2 = tf.add(tf.matmul(fc1, self.W2), self.b2)
        return fc2

下面,我们开始构建损失函数。损失函数使用 TensorFlow 提供的 tf.nn.softmax_cross_entropy_with_logits,这是一个自带 Softmax 的交叉熵损失函数。最终通过 reduce_mean 求得全局平均损失。

def loss_fn(model, x, y):
    preds = model(x)
    return tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=preds, labels=y))

于此同时,为了方便查看分类准确度,我们需要手动构建一个准确度评估函数。tf.argmax 可以将 Softmax 结果转换为对应的字符值。然后使用 tf.equal 比对各样本的结果是否正确,最终使用 reduce_mean 求得全部样本的分类准确度。

def accuracy_fn(logits, labels):
    preds = tf.argmax(logits, axis=1)  # 取值最大的索引,正好对应字符标签
    labels = tf.argmax(labels, axis=1)
    return tf.reduce_mean(tf.cast(tf.equal(preds, labels), tf.float32))

下面开始构建最关键的训练迭代过程。实际上,这部分和构建线性回归非常相似,我们的目的是对神经网络参数进行迭代优化。

EPOCHS = 100  # 迭代此时
LEARNING_RATE = 0.02  # 学习率
model = Model()  # 模型
for epoch in range(EPOCHS):
    with tf.GradientTape() as tape:  # 追踪梯度
        loss = loss_fn(model, X_train, y_train)

    trainable_variables = [model.W1, model.b1, model.W2, model.b2]  # 需优化参数列表
    grads = tape.gradient(loss, trainable_variables)  # 计算梯度

    optimizer = tf.optimizers.Adam(learning_rate=LEARNING_RATE)  # 优化器
    optimizer.apply_gradients(zip(grads, trainable_variables))  # 更新梯度
    accuracy = accuracy_fn(model(X_test), y_test)  # 计算准确度

    # 输出各项指标
    if (epoch + 1) % 10 == 0:
        print('Epoch [{}/{}], Train loss: {:.3f}, Test accuracy: {:.3f}'
              .format(epoch+1, EPOCHS, loss, accuracy))
Epoch [10/100], Train loss: 46.940, Test accuracy: 0.458
Epoch [20/100], Train loss: 9.335, Test accuracy: 0.614
Epoch [30/100], Train loss: 4.723, Test accuracy: 0.708
Epoch [40/100], Train loss: 3.033, Test accuracy: 0.739
Epoch [50/100], Train loss: 2.299, Test accuracy: 0.767
Epoch [60/100], Train loss: 1.814, Test accuracy: 0.786
Epoch [70/100], Train loss: 1.605, Test accuracy: 0.803
Epoch [80/100], Train loss: 1.424, Test accuracy: 0.797
Epoch [90/100], Train loss: 1.244, Test accuracy: 0.789
Epoch [100/100], Train loss: 1.203, Test accuracy: 0.800

Keras 高阶 API 实现

线性回归的实验中,我们使用了 Sequential 序贯模型,实际上 Keras 中更容易理解的是函数式模型。函数式模型最直观的地方在于可以看清楚输入和输出。

函数式模型

例如,下面我们开始定义函数式模型。首先是 Input 层,这在序贯模型中是没有的。然后我们将 inputs 传入 Dense 层,最终再输出。

# 函数式模型
inputs = tf.keras.Input(shape=(64,))
x = tf.keras.layers.Dense(30, activation='relu')(inputs)
outputs = tf.keras.layers.Dense(10, activation='softmax')(x)

# 指定输入和输出
model = tf.keras.Model(inputs=inputs, outputs=outputs)
model.summary()  # 查看模型结构
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 64)]              0         
_________________________________________________________________
dense (Dense)                (None, 30)                1950      
_________________________________________________________________
dense_1 (Dense)              (None, 10)                310       
=================================================================
Total params: 2,260
Trainable params: 2,260
Non-trainable params: 0
_________________________________________________________________

值得注意的是,函数式模型中需要使用 tf.keras.Model 来最终确定输入和输出。

下面,可以开始编译和训练模型。这里使用 tf.optimizers.Adam 作为优化器,tf.losses.categorical_crossentropy 多分类交叉熵作为损失函数。与 tf.nn.softmax_cross_entropy_with_logits 不同的是,tf.losses.categorical_crossentropy 是从 Keras 中演化而来的,其去掉了 Softmax 的过程。而这个过程被我们直接加入到模型的构建中。你可以看到,上面的 model 输出层使用了 softmax 激活。

# 编译模型
model.compile(optimizer=tf.optimizers.Adam(),
              loss=tf.losses.categorical_crossentropy, metrics=['accuracy'])
# 训练和评估
model.fit(X_train, y_train, batch_size=64, steps_per_epoch=200,
          validation_data=(X_test, y_test))
200/200 [==============================] - 1s 6ms/step - loss: 1.7035 - accuracy: 0.6705 - val_loss: 0.2691 - val_accuracy: 0.9194

Keras 的训练过程可以采用小批量迭代,直接指定 batch_size 即可。validation_data 可以传入测试数据得到准确度评估结果,非常方便。

序贯模型

当然,上方的函数式模型也可以被写为序贯模型。下面,我们就使用序贯模型进行实现。

model = tf.keras.Sequential()  # 建立序贯模型
model.add(tf.keras.layers.Dense(units=30, input_dim=64, activation='relu'))  # 隐含层 
model.add(tf.keras.layers.Dense(units=10, activation='softmax'))  # 输出层
model.summary()  # 查看模型结构
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_2 (Dense)              (None, 30)                1950      
_________________________________________________________________
dense_3 (Dense)              (None, 10)                310       
=================================================================
Total params: 2,260
Trainable params: 2,260
Non-trainable params: 0
_________________________________________________________________

此时,我们使用参数简写来替代函数式模型中复杂的写法。

model.compile(optimizer='adam', loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=64, steps_per_epoch=200,
          validation_data=(X_test, y_test))
200/200 [==============================] - 1s 6ms/step - loss: 2.2029 - accuracy: 0.6310 - val_loss: 0.2900 - val_accuracy: 0.9028

自由度更高的模型

TensorFlow 2.0 中还支持另外一种更为灵活的 Keras 定义模型方法,这种方法和 PyTorch 中继承 torch.nn.Module 来定义模型的思路非常相似。我们可以继承 tf.keras.Model 来构建模型。这种模型的定义方法自由度更高,我们可以添加更多的中间组件,相对灵活。

class Model(tf.keras.Model):
    def __init__(self):
        super(Model, self).__init__()
        self.dense_1 = tf.keras.layers.Dense(30, activation='relu')  # 初始化
        self.dense_2 = tf.keras.layers.Dense(10, activation='softmax')

    def call(self, inputs):
        x = self.dense_1(inputs)  # 前向传播过程
        return self.dense_2(x)

接下来的过程和上面相似,实例化模型然后训练并评估。

model = Model()  # 实例化模型
model.compile(optimizer='adam', loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=64, steps_per_epoch=200,
          validation_data=(X_test, y_test))
200/200 [==============================] - 1s 5ms/step - loss: 1.4068 - accuracy: 0.6827 - val_loss: 0.2458 - val_accuracy: 0.9139

在线学习

本文内容由实验楼提供 Jupyter Notebook 线上实验环境,可以 一键启动在线学习。本篇文章需 特别授权许可,内容版权归作者和实验楼所有,未经授权,禁止转载。