侧边栏壁纸
  • 累计撰写 247 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

TensorFlow 2 简单神经网络

kaixindeken
2021-09-22 / 0 评论 / 0 点赞 / 83 阅读 / 4,784 字

人工神经网络是一种发展时间较早且十分常用的机器学习算法。因其模仿人类神经元工作的特点,在监督学习和非监督学习领域都给予了人工神经网络较高的期望。目前,由传统人工神经网络发展而来的卷积神经网络、循环神经网络已经成为了深度学习的基石。

这里,我们先加载一组数据,DIGITS 数据集是 scikit-learn 提供的简单手写字符识别数据集。

截屏20210922 上午10.34.06.png

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

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]  # 标签独热编码
# 切分数据集,测试集占 20%
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, y_train.shape, X_test.shape, y_test.shape

独热编码是一种稀疏向量,其中:一个元素设为 1,所有其他元素均设为 0。例如手写字符标签 1 会被表示为向量:

低阶 API 构建

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

截屏20210922 上午10.35.54.png

特别地,我们对隐含层进行 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))  # 全连接层 1
        fc2 = tf.add(tf.matmul(fc1, self.W2), self.b2)  # 全连接层 2
        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))

下面开始构建最关键的训练迭代过程。实际上,这部分和构建线性回归非常相似,我们的目的是对神经网络参数进行迭代优化。其中,优化器选择了 Adam,一种比 SDG 随机梯度下降更常用于深度学习的优化算法。

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)  # Adam 优化器
    optimizer.apply_gradients(zip(grads, trainable_variables))  # 更新梯度
    accuracy = accuracy_fn(model(X_test), y_test)  # 计算准确度

    # 输出各项指标
    if (epoch + 1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{EPOCHS}], Train loss: {loss}, Test accuracy: {accuracy}')

Keras 高阶 API 实现

线性回归的实验中,我们使用了 Keras 提供的 Sequential 顺序模型来实现,最终只用了 4 行代码。接下来,我们同样使用顺序模型重写上方的模型构建过程,尝试用更精简的代码来完成训练过程。

顺序模型

建立顺序模型后,向其中添加所需的 2 个全连接层,然后查看模型结构。

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()  # 查看模型结构

下面,可以开始编译和训练模型。这里使用 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='adam', loss='categorical_crossentropy', metrics=['accuracy'])
# 训练模型
model.fit(X_train, y_train, batch_size=64, epochs=20, validation_data=(X_test, y_test))

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

同样,随着迭代次数的增加,你可以看到模型的分类准确率稳步提升。

函数式模型

除了顺序模型,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()  # 查看模型结构

值得注意的是,函数式模型中需要使用 tf.keras.Model 来最终确定输入和输出。同样开始模型编译和训练:

# 编译模型,添加优化器,损失函数和评估方法
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
# 训练模型
model.fit(X_train, y_train, batch_size=64, epochs=20, validation_data=(X_test, y_test))

Keras 实现过程比低阶 API 要简单太多了,顺序模型只用了 5 行代码,函数式模型只用了 6 行代码,优势非常明显。对于顺序模型和函数式模型而言,二者并没有性能上的区别,主要是看你更习惯于哪一种书写方式。当然,如果你了解深度学习,可能会知道有一些多输入模型只能使用函数式 API 进行构建,这也就是二者同时存在的必要性了。

混合模型

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

# 继承 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, epochs=10, validation_data=(X_test, y_test))

对于这类方法,会发现其同时兼具了低阶 API 的灵活性和高阶 API 的易用性。实际应用中,我们可以自由去组合模型的结构,又可以同时使用 Keras 提供的 API 编译和训练模型,非常方便。

0

评论区