深度学习总结

深度学习总结


1 什么是深度学习

人工智能的简洁定义如下:努力将通常由人类完成的智力任务自动化。

机器学习的概念就来自于图灵的这个问题:对于计算机而言,除了“我们命令它做的任何 事情”之外,它能否自我学习执行特定任务的方法?计算机能否让我们大吃一惊?如果没有程序员精心编写的数据处理规则,计算机能否通过观察数据自动学会这些规则?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GHN5ZUE0-1672461065801)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229123428854.png)]

机器学习的技术定义:在预先定义好的可能性空间中,利用反馈信号的指引来寻找输入数据的有用表示。机器学习算法在寻找这些变换时仅仅是遍历一组预先定义好的操作,这组操作叫作假设空间(hypothesis space)。

深度学习是机器学习的一个分支领域:它是从数据中学习表示的一种新方法,强调从连续的层(layer)中进行学习,这些层对应于越来越有意义的表示。“深度学习”中的“深度”指的并不是利用这种方法所获取的更深层次的理解,而是指一系列连续的表示层。数据模型中包含多少层,这被称为模型的深度(depth)。这一领域的其他名称包括分层表示学习(layered representations learning)。现代深度学习通常包含数十个甚至上百个连续的表示层,这些表示层全都是从训练数据中自动学习的。与此 相反,其他机器学习方法的重点往往是仅仅学习一两层的数据表示,因此有时也被称为浅层学习(shallow learning)。

神经网络中每层对输入数据所做的具体操作保存在该层的权重(weight)中,其本质是一串数字。用术语来说,每层实现的变换由其权重来参数化(parameterize)。权重有时也被称为该层的参数(parameter)。在这种语境下,学习的意思是为神经网络的所有层找到一组权重值,使得该网络能够将每个示例输入与其目标正确地一一对应

损失函数的输入是网络预测值与真实目标值(即你希望网络输出的 结果),然后计算一个距离值,衡量该网络在这个示例上的效果好坏(见图 1-8)。

**优化器(optimizer)利用这个距离值作为反馈信号来对权重值进行微调,以降低当前示例对应的损失值(见图 1-9)来完成,它实现了所谓的反向传播(backpropagation)**算法,这是深度学习的核心算法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MowZ7HuY-1672461065802)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229131932274.png)]

2 神经网络入门

2.1 MNIST

MNIST-1

from 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

from keras import models
from keras import layers
# 构建模型
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))

from keras import optimizers
# 编译模型
network.compile(optimizer='rmsprop',
                loss='categorical_crossentropy',
                metrics=['accuracy'])

# 训练模型
network.fit(train_images, train_labels, 
            epochs=5, 
            batch_size=128)

Keys: 数据预处理(格式转换)

# 数据预处理(格式转换)
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

MNIST-API

# API模型创建:
# 先定义输入到输出的转换
input_tensor = layers.Input(shape=(784,))
x = layers.Dense(32, activation='relu')(input_tensor)
output_tensor = layers.Dense(10, activation='softmax')(x)

# 再定义模型,并指定输入和输出tensor
model = models.Model(inputs=input_tensor, outputs=output_tensor)	

from keras import optimizers
model.compile(optimizer=optimizers.RMSprop(lr=0.001),
              loss='mse',
              metrics=['accuracy'])

model.fit(input_tensor, target_tensor, batch_size=128, epochs=10)
Keys: API构建模型
# API模型创建:
# 先定义输入到输出的转换
input_tensor = layers.Input(shape=(784,))
x = layers.Dense(32, activation='relu')(input_tensor)
output_tensor = layers.Dense(10, activation='softmax')(x)

# 再定义模型,并指定输入和输出tensor
model = models.Model(inputs=input_tensor, outputs=output_tensor)	

2.2 二分类

from keras.datasets import imdb
# 导入数据
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)	# 仅保留训练数据中前 10 000 个最常出现的单词
# train_data[0]:[1, 14, 22, 16, ... 178, 32]	 train_labels[0]:1

import numpy as np
# 数据预处理,one-hot编码->[samples, word_indeces=10000]
def vectorize_sequences(sequences, dimension=10000):
     results = np.zeros((len(sequences), dimension))
     for i, sequence in enumerate(sequences):
         results[i, sequence] = 1.
     return results

x_train = vectorize_sequences(train_data)	# x_train[0]:array([ 0., 1., 1., ..., 0., 0., 0.])
x_test = vectorize_sequences(test_data)
y_train = np.asarray(train_labels).astype('float32')	# y_train[0]:array([0])
y_test = np.asarray(test_labels).astype('float32')

from keras import models
from keras import layers
# 构建模型
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

# 编译模型
model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['acc'])

# from keras import optimizers
# from keras import losses
# from keras import metrics
# 使用自定义的损失和指标
# model.compile(optimizer=optimizers.RMSprop(lr=0.001),
#               loss=losses.binary_crossentropy,		
#               metrics=[metrics.binary_accuracy])

# 验证集划分
x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]

# 训练模型
history = model.fit(partial_x_train,partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

model.fit() 返回了一个 History 对象。这个对象有一个成员 history,它 是一个字典,包含训练过程中的所有数据。我们来看一下。字典中包含 4 个条目,对应训练过程和验证过程中监控的指标。val_acc, acc, val_loss, loss

Keys: one-hot编码
import numpy as np
# 数据预处理,one-hot编码->[samples, word_indeces=10000]
def vectorize_sequences(sequences, dimension=10000):
     results = np.zeros((len(sequences), dimension))
     for i, sequence in enumerate(sequences):
         results[i, sequence] = 1.
     return results

x_train = vectorize_sequences(train_data)	# x_train[0]:array([ 0., 1., 1., ..., 0., 0., 0.])
x_test = vectorize_sequences(test_data)
y_train = np.asarray(train_labels).astype('float32')	# y_train[0]:array([0])
y_test = np.asarray(test_labels).astype('float32')
Keys: 使用自定义的损失和精度
from keras import optimizers
from keras import losses
from keras import metrics
# 使用自定义的损失和指标
model.compile(optimizer=optimizers.RMSprop(lr=0.001),
              loss=losses.binary_crossentropy,		
              metrics=[metrics.binary_accuracy])
Keys: 验证集简易划分
# 验证集划分
x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]
Keys: fit返回history结果
history = model.fit(partial_x_train,partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))
# 返回四个结果:val_acc, acc, val_loss, loss
Keys: 绘制训练损失和验证损失
import matplotlib.pyplot as plt

history_dict = history.history
loss_values = history_dict['loss']
val_loss_values = history_dict['val_loss']

epochs = range(1, len(loss_values) + 1)

plt.plot(epochs, loss_values, 'bo', label='Training loss')
plt.plot(epochs, val_loss_values, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0pCYt3pg-1672461065803)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229161435732.png)]

Keys: 绘制训练精度和验证精度
plt.clf()	# 清空图像
acc = history_dict['acc']
val_acc = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()

plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eVrLkukm-1672461065803)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229161449187.png)]

观察模型训练的精度和损失的结果,调整设置的参数,并重新训练一个新的模型并在测试集上进行评估。

# 从头构建模型训练并评估在测试集上的结果
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train, epochs=4, batch_size=512)

results = model.evaluate(x_test, y_test)

2.3 多分类

from keras.datasets import reuters
# 导入数据
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)

import numpy as np
# 数据预处理
def vectorize_sequences(sequences, dimension=10000):
    results = np.zeros((len(sequences), dimension))
    for i, sequence in enumerate(sequences):
        results[i, sequence] = 1.
    return results

# 向量化训练数据
x_train = vectorize_sequences(train_data)
# 向量化测试数据
x_test = vectorize_sequences(test_data)

def to_one_hot(labels, dimension=46):
    results = np.zeros((len(labels), dimension))
    for i, label in enumerate(labels):
        results[i, label] = 1.
    return results

# 向量化训练标签
one_hot_train_labels = to_one_hot(train_labels)
# 向量化测试标签
one_hot_test_labels = to_one_hot(test_labels)

# 或者使用keras内置函数,将标签向量化
from keras.utils.np_utils import to_categorical
one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

from keras import models
from keras import layers
# 构建模型
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(46, activation='softmax'))

# 编译模型
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# 划分验证集
x_val = x_train[:1000]
partial_x_train = x_train[1000:]
y_val = one_hot_train_labels[:1000]
partial_y_train = one_hot_train_labels[1000:]

# 训练模型
history = model.fit(partial_x_train, partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

# 测试模型
results = model.evaluate(x_test, one_hot_test_labels)
Keys: to_categorical标签向量化
# keras内置函数,将标签向量化
from keras.utils.np_utils import to_categorical

one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

2.4 回归

预测 20 世纪 70 年代中期波士顿郊区房屋价格的中位数,已知当时郊区的一些数据点,比如犯罪率、当地房产税率等13 个数值特征。数据集分为 404 个训练样本和 102 个测试样本。输入数据的每个特征(比如犯罪率)都有不同的取值范围。例如,有些特性是比例,取值范围为 0~1;有的取值范围为 1~12;还有的取值范围为 0~100,等等。将取值范围差异很大的数据输入到神经网络中,这是有问题的。网络可能会自动适应这种取值范围不同的数据,但学习肯定变得更加困难。对于这种数据,普遍采用的最佳实践是对每个特征做标准化,即对于输入数据的每个特征(输入数据矩阵中的列),减去特征平均值,再除以标准差,这样得到的特征平均值为 0,标准差为 1。用 Numpy 可以很容易实现标准化。

from keras.datasets import boston_housing
# 导入数据
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()

# 数据标准化 对于输入数据的每个特征(输入数据矩阵中的列),减去特征平均值,再除以标准差,这样得到的特征平均值为 0,标准差为 1。
mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std
# 用于测试集的数据标准化也是用的训练集的结果
test_data -= mean
test_data /= std

from keras import models
from keras import layers
# 构建模型
def build_model():
    # 因为需要将同一个模型多次实例化,所以用一个函数来构建模型
    model = models.Sequential()
    model.add(layers.Dense(64, activation='relu',
                           input_shape=(train_data.shape[1],)))
    model.add(layers.Dense(64, activation='relu'))
    model.add(layers.Dense(1))
    model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])		# mae的平均绝对误差与结果差了100倍
    return model

在数据集较小的情况下,验证分数在不同验证集上的划分产生较大的方差,此时的最佳做法是使用 K 折交叉验证。这种方法将可用数据划分为 K 个分区(K 通常取 4 或 5),实例化 K 个相同的模型,将每个模型在 K-1 个分区上训练,并在剩下的一个分区上进行评估模型的验证分数等于 K 个验证分数的平均值。这种方法的代码实现很简单。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iolhpaSK-1672461065803)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229180616354.png)]

Keys: K折交叉验证
num_epochs = 500
all_mae_histories = []
for i in range(k):
    print('processing fold #', i)
    # Prepare the validation data: data from partition # k
    val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
    val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]

    # Prepare the training data: data from all other partitions
    partial_train_data = np.concatenate(
        [train_data[:i * num_val_samples],
         train_data[(i + 1) * num_val_samples:]],
        axis=0)
    partial_train_targets = np.concatenate(
        [train_targets[:i * num_val_samples],
         train_targets[(i + 1) * num_val_samples:]],
        axis=0)

    # Build the Keras model (already compiled)
    model = build_model()
    # Train the model (in silent mode, verbose=0)
    history = model.fit(partial_train_data, partial_train_targets,
                        validation_data=(val_data, val_targets),
                        epochs=num_epochs, batch_size=1, verbose=0)
    mae_history = history.history['val_mean_absolute_error']
    all_mae_histories.append(mae_history)
    
# 计算所有轮次中的 K 折验证分数平均值
average_mae_history = [np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]
Keys: 平滑曲线绘图
# 绘制验证分数
import matplotlib.pyplot as plt

plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)
plt.xlabel('Epochs')
plt.ylabel('Validation MAE')
plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNKwqFHY-1672461065804)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229181247890.png)]

因为纵轴的范围较大,且数据方差相对较大,所以难以看清这张图的规律。我们来重新绘制一张图。

  • 删除前 10 个数据点,因为它们的取值范围与曲线上的其他点不同。
  • 将每个数据点替换为前面数据点的指数移动平均值,以得到光滑的曲线。
# 绘制验证分数(删除前 10 个数据点)
def smooth_curve(Keys, factor=0.9):
  smoothed_Keys = []
  for point in Keys:
    if smoothed_Keys:
      previous = smoothed_Keys[-1]
      smoothed_Keys.append(previous * factor + point * (1 - factor))
    else:
      smoothed_Keys.append(point)
  return smoothed_Keys

smooth_mae_history = smooth_curve(average_mae_history[10:])

plt.plot(range(1, len(smooth_mae_history) + 1), smooth_mae_history)
plt.xlabel('Epochs')
plt.ylabel('Validation MAE')
plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nno9O3jM-1672461065804)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229181407881.png)]

Keys: 训练最终模型
# Get a fresh, compiled model.
model = build_model()
# Train it on the entirety of the data.
model.fit(train_data, train_targets,
          epochs=80, 
          batch_size=16, 
          verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

现在你可以处理关于向量数据最常见的机器学习任务了:二分类问题、多分类问题和标量回归问题。前面三节的“小结”总结了你从这些任务中学到的要点。

  • 在将原始数据输入神经网络之前,通常需要对其进行预处理。
  • 如果数据特征具有不同的取值范围,那么需要进行预处理,将每个特征单独缩放。
  • 随着训练的进行,神经网络最终会过拟合,并在前所未见的数据上得到更差的结果。
  • 如果训练数据不是很多,应该使用只有一两个隐藏层的小型网络,以避免严重的过拟合。
  • 如果数据被分为多个类别,那么中间层过小可能会导致信息瓶颈。
  • 回归问题使用的损失函数和评估指标都与分类问题不同。
  • 如果要处理的数据很少,K 折验证有助于可靠地评估模型。

3 机器学习基础

机器学习的目的是得到可以泛化(generalize)的模型,即在前所未见的数据上表现很好的 模型,而过拟合则是核心难点。你只能控制可以观察的事情,所以能够可靠地衡量模型的泛化 能力非常重要。

3.1 验证集划分

评估模型的重点是将数据划分为三个集合:训练集、验证集和测试集。在训练数据上训练模型,在验证数据上评估模型。一旦找到了最佳参数,就在测试数据上最后测试一次。如果基于模型在验证集上的性能来调节模型配置,会很快导致模型在验证集上过拟合,即使你并没有在验证集上直接训练模型也会如此。

Keys: 简单留出

留出一定比例的数据作为测试集。在剩余的数据上训练模型,然后在测试集上评估模型。 如前所述,为了防止信息泄露,你不能基于测试集来调节模型,所以还应该保留一个验证集。

num_validation_samples = 10000

np.random.shuffle(data)		# 打乱数据

validation_data = data[:num_validation_samples]	# 划分验证集

data = data[num_validation_samples:]			
training_data = data[:]							# 划分训练集

# 在训练数据上训练模型,并在验证数据上评估模型
model = get_model()
model.train(training_data)
validation_score = model.evaluate(validation_data)

# 一旦调节好超参数,通常就在所有非测试数据上从头开始训练最终模型。现在你可以调节模型、重新训练、评估,然后再次调节……
model = get_model()
model.train(np.concatenate([training_data,validation_data]))

test_score = model.evaluate(test_data)
Keys: K折验证

K 折验证(K-fold validation)将数据划分为大小相同的 K 个分区。对于每个分区 i,在剩余的 K-1 个分区上训练模型,然后在分区 i 上评估模型。最终分数等于 K 个分数的平均值。

k = 4
num_validation_samples = len(data) // k

np.random.shuffle(data)

validation_scores = []

for fold in range(k):
 	validation_data = data[num_validation_samples * fold:
                           num_validation_samples * (fold + 1)]	# 选择验证数据分区
    
 	training_data = data[:num_validation_samples * fold] 
    					 +data[num_validation_samples * (fold + 1):] # 使用剩余数据作为训练数据,注意,+ 运算符是列表合并,不是求和
	
    # 创建一个全新的模型实例(未训练)
	model = get_model()		
 	model.train(training_data)
 	validation_score = model.evaluate(validation_data)
 	validation_scores.append(validation_score)
    
# 最终验证分数:K 折验证分数的平均值    
validation_score = np.average(validation_scores)	

# 在所有非测试数据上训练最终模型
model = get_model()		
model.train(data)
test_score = model.evaluate(test_data)
Keys: 带有打乱数据的重复 K 折验证

如果可用的数据相对较少,而你又需要尽可能精确地评估模型,那么可以选择带有打乱数 据的重复 K 折验证(iterated K-fold validation with shuffling)。我发现这种方法在 Kaggle 竞赛中 特别有用。具体做法是多次使用 K 折验证,在每次将数据划分为 K 个分区之前都先将数据打乱。 最终分数是每次 K 折验证分数的平均值。注意,这种方法一共要训练和评估 P×K 个模型(P 是重复次数),计算代价很大。

3.2 数据预处理

神经网络的数据预处理:数据预处理的目的是使原始数据更适于用神经网络处理,包括向量化、标准化、处理缺失值和特征提取。

Keys: 向量化

神经网络的所有输入和目标都必须是浮点数张量(在特定情况下可以是整数张量)。无论 处理什么数据(声音、图像还是文本),都必须首先将其转换为张量,这一步叫作数据向量化 (data vectorization)。例如,在前面两个文本分类的例子中,开始时文本都表示为整数列表(代 表单词序列),然后我们用 one-hot 编码将其转换为 float32 格式的张量。在手写数字分类和预 测房价的例子中,数据已经是向量形式,所以可以跳过这一步。

x_train = vectorize_sequences(train_data)	# x_train[0]:array([ 0., 1., 1., ..., 0., 0., 0.])
x_test = vectorize_sequences(test_data)
y_train = np.asarray(train_labels).astype('float32')	# y_train[0]:array([0])
y_test = np.asarray(test_labels).astype('float32')
# 使用keras内置函数,将标签向量化
from keras.utils.np_utils import to_categorical
one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)
Keys: 值标准化

在手写数字分类的例子中,开始时图像数据被编码为 0~255 范围内的整数,表示灰度值。 将这一数据输入网络之前,你需要将其转换为 float32 格式并除以 255,这样就得到 0~1 范围内的浮点数。同样,预测房价时,开始时特征有各种不同的取值范围,有些特征是较小的浮点数, 有些特征是相对较大的整数。将这一数据输入网络之前,你需要对每个特征分别做标准化,使 其均值为 0、标准差为 1。

一般来说,将取值相对较大的数据(比如多位整数,比网络权重的初始值大很多)或异质 数据(heterogeneous data,比如数据的一个特征在 0~1 范围内,另一个特征在 100~200 范围内) 输入到神经网络中是不安全的。这么做可能导致较大的梯度更新,进而导致网络无法收敛。为 了让网络的学习变得更容易,输入数据应该具有以下特征。

  • 取值较小:大部分值都应该在 0~1 范围内。
  • 同质性(homogenous):所有特征的取值都应该在大致相同的范围内。

此外,下面这种更严格的标准化方法也很常见,而且很有用,虽然不一定总是必需的(例如,对于数字分类问题就不需要这么做)。

  • 将每个特征分别标准化,使其平均值为 0。
  • 将每个特征分别标准化,使其标准差为 1。

这对于 Numpy 数组很容易实现。

x -= x.mean(axis=0) 
x /= x.std(axis=0)

3.3 特征工程

特征工程(feature engineering)是指将数据输入模型之前,利用你自己关于数据和机器学习算法(这里指神经网络)的知识对数据进行硬编码的变换(不是模型学到的),以改善模型的效果。多数情况下,一个机器学习模型无法从完全任意的数据中进行学习。呈现给模型的数据应该便于模型进行学习。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-icpGCrBu-1672461065804)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221229194350536.png)]

良好的特征可以让你用更少的数据解决问题。深度学习模型自主学习特征的能力依赖于 大量的训练数据。如果只有很少的样本,那么特征的信息价值就变得非常重要。

3.4 过拟合和欠拟合

优化(optimization)是指调节模型以在训练数据上得到最佳性能(即机器学习中的学习),而泛化(generalization)是指训练好的模型在前所未见的数据上的性能好坏。机器学习的目的当然是得到良好的泛化,但你无法控制泛化, 只能基于训练数据调节模型。

训练开始时,优化和泛化是相关的:训练数据上的损失越小,测试数据上的损失也越小。 这时的模型是欠拟合(underfit)的,即仍有改进的空间,网络还没有对训练数据中所有相关模式建模。但在训练数据上迭代一定次数之后,泛化不再提高,验证指标先是不变,然后开始变差, 即模型开始过拟合(overfit)。这时模型开始学习仅和训练数据有关的模式,但这种模式对新数据来说是错误的或无关紧要的。

防止过拟合的两大策略:

  • 获取更多的数据:模型的训练数据越多,泛化能力自然也越好。
  • 正则化:对模型允许存储的信息加以约束,迫使模型集中学习最重要的模式。
Keys: 减小模型大小

模型中可学习参数的个数通常被称为模型的容量 (capacity)。直观上来看,参数更多的模型拥有更大的记忆容量(memorization capacity),因此能够在训练样本和目标之间轻松地学会完美的字典式映射,这种映射没有任何泛化能力。与此相反,如果网络的记忆资源有限,则无法轻松学会这种映射。因此,为了让损失最小化, 网络必须学会对目标具有很强预测能力的压缩表示,这也正是我们感兴趣的数据表示。同时请记住,你使用的模型应该具有足够多的参数,以防欠拟合,即模型应避免记忆资源不足。在容量过大与容量不足之间要找到一个折中

Keys: 添加权重正则化

这里的简单模型(simple model)是指参数值分布的熵更小的模型(或参数更少的模型,比如上一节的例子)。因此,一种常见的降低过拟合的方法就是强制让模型权重只能取较小的值, 从而限制模型的复杂度,这使得权重值的分布更加规则(regular)。这种方法叫作权重正则化 (weight regularization),其实现方法是向网络损失函数中添加与较大权重值相关的成本(cost)。 这个成本有两种形式。

L1 正则化(L1 regularization):添加的成本与权重系数的绝对值[权重的 L1 范数(norm)] 成正比。

L2 正则化(L2 regularization):添加的成本与权重系数的平方(权重的 L2 范数)成正比。 神经网络的 L2 正则化也叫权重衰减(weight decay)。不要被不同的名称搞混,权重衰减 与 L2 正则化在数学上是完全相同的。

在 Keras 中,添加权重正则化的方法是向层传递权重正则化项实例(weight regularizer instance)作为关键字参数。下列代码将向电影评论分类网络中添加 L2 权重正则化。

from keras import regularizers

model = models.Sequential()
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001),
 		  activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001),
 		  activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

L2(0.001) 的意思是该层权重矩阵的每个系数都会使网络总损失增加 0.001 * weight_ coefficient_value。注意,由于这个惩罚项只在训练时添加,所以这个网络的训练损失会比测试损失大很多。

你还可以用 Keras 中以下这些权重正则化项来代替 L2 正则化。

from keras import regularizers
regularizers.l1(0.001)
regularizers.l1_l2(l1=0.001, l2=0.001)
Keys: 添加dropout正则化

dropout 是神经网络最有效也最常用的正则化方法之一,对某一层使用 dropout,就是在训练过程中随机将该层的一些输出特征舍 弃(设置为 0)。假设在训练过程中,某一层对给定输入样本的返回值应该是向量 [0.2, 0.5, 1.3, 0.8, 1.1]。使用 dropout 后,这个向量会有几个随机的元素变成 0,比如 [0, 0.5, 1.3, 0, 1.1]。dropout 比率(dropout rate)是被设为 0 的特征所占的比例,通常在 0.2~0.5 范围内。测试时没有单元被舍弃,而该层的输出值需要按 dropout 比率缩小,因为这时比训练时有更多的单元被激活,需要加以平衡。

在 Keras 中,你可以通过 Dropout 层向网络中引入 dropout,dropout 将被应用于前面一层 的输出。

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dropout(0.5))		# 0.5的概率的dropout
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dropout(0.5))		# 0.5的概率的dropout
model.add(layers.Dense(1, activation='sigmoid'))

总结一下,防止神经网络过拟合的常用方法包括:

  1. 获取更多的训练数据
  2. 减小网络容量
  3. 添加权重正则化
  4. 添加 dropout

4 计算机视觉

4.1 简介

下列代码将会展示一个简单的卷积神经网络。它是 Conv2D 层和 MaxPooling2D 层的堆叠。

from keras import layers
from keras import models

model = models.Sequential()

model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2)))

model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))

model.add(layers.Conv2D(64, (3, 3), activation='relu'))

model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

重要的是,卷积神经网络接收形状为 (image_height, image_width, image_channels) 的输入张量(不包括批量维度)。

将最后的输出张量[大小为 (3, 3, 64)]输入到一个密集连接分类器网络中,分类器可以处理 1D 向量,而当前的输出是 3D 张量。 首先,我们需要将 3D 输出展平为 1D,然后在上面添加几个 Dense 层。

Keys: 平移不变性与空间层次结构

卷积神经网络具有以下两个有趣的性质。

  • 平移不变性(translation invariant):卷积神经网络在图像 右下角学到某个模式之后,它可以在任何地方识别这个模式,比如左上角。对于密集连接网络来说,如果模式出现在新的位置,它只能重新学习这个模式。这使得卷积神经网络在处理图像时可以高效利用数据(因为视觉世界从根本上具有平移不变性),它只需 要更少的训练样本就可以学到具有泛化能力的数据表示
  • 空间层次结构(spatial hierarchies of patterns):第一个卷积层将学习较小的局部模式(比如边缘),第二个卷积层将学习由第一层特征组成的更大的模式,以此类推。这使得卷积神经网络可以有效地学习越来越复杂、越来越抽象的视觉概念(因为视觉世界从根本上具有空间层次结构)。
Keys: 通道与过滤器

过滤器 (filter):输出深度是层的参数,深度轴的不同通道不再像 RGB 输入那样代表特定颜色,而是代表过滤器 (filter)。过滤器对输入数据的某一方面进行编码,比如,单个过滤器可以从更高层次编码这样 一个概念:“输入中包含一张脸。”

Keys: 卷积层与池化层

**卷积(Conv2D)**由以下两个关键参数所定义。Conv2D(output_depth, (window_height, window_width))。

从输入中提取的图块尺寸:这些图块的大小通常是 3×3 或 5×5。

输出特征图的深度:卷积所计算的过滤器的数量。

卷积的工作原理:在 3D 输入特征图上滑动(slide)这些 3×3 或 5×5 的窗口,在每个可能 的位置停止并提取周围特征的 3D 图块[形状为 (window_height, window_width, input_ depth)]。然后每个 3D 图块与学到的同一个权重矩阵[叫作卷积核(convolution kernel)]做张量积,转换成形状为 (output_depth,) 的 1D 向量。然后对所有这些向量进行空间重组, 使其转换为形状为 (height, width, output_depth) 的 3D 输出特征图。输出特征图中的每个空间位置都对应于输入特征图中的相同位置(比如输出的右下角包含了输入右下角的信 息)。

Keys: 边界、填充与步幅

边界效应与填充:假设有一个 5×5 的特征图(共 25 个方块)。其中只有 9 个方块可以作为中心放入一个 3×3 的窗口,这 9 个方块形成一个 3×3 的网格(见图 5-5)。因此,输出特征图的尺寸是 3×3。 它比输入尺寸小了一点。如果你希望输出特征图的空间维度与输入相同,那么可以使用填充(padding)。填充是在 输入特征图的每一边添加适当数目的行和列,使得每个输入方块都能作为卷积窗口的中心。对于 Conv2D 层,可以通过 padding 参数来设置填充,这个参数有两个取值:“valid” 表 示不使用填充(只使用有效的窗口位置);“same” 表示“填充后输出的宽度和高度与输入相同”。 padding 参数的默认值为 “valid”。

步幅:两个连续窗口的距离是卷积的一个参数,叫作步幅,默认值为 1。步幅为 2 意味着特征图的宽度和高度都被做了 2 倍下采样(除了边界效应引起的变化)。虽 然步进卷积对某些类型的模型可能有用,但在实践中很少使用。

池化:最大池化是从输入特征图中提取窗口,并输出每个通道的最大值。它的概念与卷积类似, 但是最大池化使用硬编码的 max 张量运算对局部图块进行变换,而不是使用学到的线性变换(卷 积核)。最大池化与卷积的最大不同之处在于,最大池化通常使用 2×2 的窗口和步幅 2,其目 的是将特征图下采样 2 倍。与此相对的是,卷积通常使用 3×3 窗口和步幅 1。在卷积神经网络示例中,你可能注意到,在每个 MaxPooling2D 层之后,特征图的尺寸都会减半。例如,在第一个 MaxPooling2D 层之前,特征图的尺寸是 26×26,但最大池化运算将其减半为 13×13。这就是最大池化的作用:对特征图进行下采样,与步进卷积类似。

使用下采样的原因,一是减少需要处理的特征图的元素个数,二是通过让连续卷积层的观察窗口越来越大(即窗口覆盖原始输入的比例越来越大),从而引入空间过滤器的层级结构。 注意,最大池化不是实现这种下采样的唯一方法。你已经知道,还可以在前一个卷积层中使用步幅来实现。此外,你还可以使用平均池化来代替最大池化,其方法是将每个局部输入图 块变换为取该图块各通道的平均值,而不是最大值。但最大池化的效果往往比这些替代方法更好。

4.2 卷积神经网络构建

Keys: Kaggle图像数据集处理
import os, shutil

original_dataset_dir = 'D:\\datasets\\dogs-vs-cats\\train'	# 原始数据集解压目录的路径

base_dir = 'D:\datasets\cats_and_dogs_small'	# 保存较小数据集的目录
os.mkdir(base_dir)

train_dir = os.path.join(base_dir, 'train')		# 分别对应划分后的训练
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')	# 分别对应划分后的验证
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')		# 分别对应划分后的测试
os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir, 'cats')	# 猫的训练图像目录
os.mkdir(train_cats_dir)

train_dogs_dir = os.path.join(train_dir, 'dogs')	# 狗的训练图像目录
os.mkdir(train_dogs_dir)

validation_cats_dir = os.path.join(validation_dir, 'cats')		# 猫的验证图像目录
os.mkdir(validation_cats_dir)

validation_dogs_dir = os.path.join(validation_dir, 'dogs')		# 狗的验证图像目录
os.mkdir(validation_dogs_dir)

test_cats_dir = os.path.join(test_dir, 'cats')		# 猫的测试图像目录
os.mkdir(test_cats_dir)

test_dogs_dir = os.path.join(test_dir, 'dogs')		# 狗的测试图像目录
os.mkdir(test_dogs_dir)

fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]		# 将前 1000 张猫的图像复制到 train_cats_dir
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_cats_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]		# 将接下来 500 张猫的图像复制到 validation_cats_dir
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_cats_dir, fname)
    shutil.copyfile(src, dst)
    
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]		# 将接下来的 500 张猫的图像复制到 test_cats_dir
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_cats_dir, fname)
    shutil.copyfile(src, dst)
    
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]		# 将前 1000 张狗的图像复制到 train_dogs_dir
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_dogs_dir, fname)
    shutil.copyfile(src, dst)
    
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]		# 将接下来 500 张狗的图像复制到 validation_dogs_dir
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_dogs_dir, fname)
    shutil.copyfile(src, dst)
    
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]		# 将接下来 500 张狗的图像复制到 test_dogs_dir
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_dogs_dir, fname)
    shutil.copyfile(src, dst)
Keys: CNN构建
from keras import layers
from keras import models
# 构建模型
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
                        input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))

model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))

model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))

model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))

model.add(layers.Flatten())
model.add(layers.Dropout(0.5))	# dropout正则化,降低过拟合
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])

from keras import optimizers
# 编译模型
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])
Keys: 图像数据预处理Generator
from keras.preprocessing.image import ImageDataGenerator

# All images will be rescaled by 1./255
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(        
        train_dir,					# This is the target directory     
        target_size=(150, 150),		# All images will be resized to 150x150
        batch_size=20,    			# batch_size, how many samples are put into the model to train for each time
        class_mode='binary')		# Since we use binary_crossentropy loss, we need binary labels

validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

# 基于生成器的模型训练
history = model.fit(
    train_generator,
    steps_per_epoch=100,
    epochs=30,
    validation_data=validation_generator,
    validation_steps=50)
Keys: 模型的保存
model.save('cats_and_dogs_small_1.h5')
Keys: 模型的加载
from keras.models import load_model

model = load_model('cats_and_dogs_small_1.h5')
model.summary() # 作为提醒

绘制训练过程中模型在训练数据和验证数据上的损失和精度(见图 5-9 和图 5-10)。

import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GEvJwYIM-1672461065805)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221226154515472.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ZRwUfeE-1672461065805)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221226154521751.png)]

Keys: 数据增强

利用多种能够生成可信图像的随机变换来增加 (augment)样本,在 Keras 中,这可以通过对 ImageDataGenerator 实例读取的图像执行多次随机变换来实现。

datagen = ImageDataGenerator(
              rotation_range=40,
              width_shift_range=0.2,
              height_shift_range=0.2,
              shear_range=0.2,
              zoom_range=0.2,
              horizontal_flip=True,
              fill_mode='nearest')

利用数据增强生成器训练卷积神经网络

train_datagen = ImageDataGenerator(
    rescale=1./255,
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,)

# 注意,不能增强验证数据
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
        # 目标目录 
        train_dir,
        # 将所有图像的大小调整为 150×150
        target_size=(150, 150),
        batch_size=32,
        # 因为使用了 binary_crossentropy损失,所以需要用二进制标签
        class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')

history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=100,
      validation_data=validation_generator,
      validation_steps=50)

model.save('cats_and_dogs_small_2.h5')

4.3 预训练模型

**预训练网络(pretrained network)**是一个保存好的网络,之前已在大型数据集(通常是大规模图 像分类任务)上训练好。如果这个原始数据集足够大且足够通用,那么预训练网络学到的特征 的空间层次结构可以有效地作为视觉世界的通用模型,因此这些特征可用于各种不同的计算机 视觉问题,即使这些新问题涉及的类别和原始任务完全不同。使用预训练网络有两种方法:特征提取(feature extraction)微调模型(fine-tuning)

Keys: 特征提取

特征提取是使用之前网络学到的表示来从新样本中提取出有趣的特征。然后将这些特征输入一个新的分类器,分类器开始训练,之前负责特征提取的层则冻结。对于卷积神经网络而言,特征提取就是取出之前训练好的网络的卷积基,在上面运行新数据,然后在输出上面训练一个新的分类器

某个卷积层提取的表示的通用性(以及可复用性)取决于该层在模型中的深度。模型中更靠近底部的层提取的是局部的、高度通用的特征图(比如视觉边缘、颜色和纹理),而更靠近顶部的层提取的是更加抽象的概念(比如“猫耳朵”或“狗眼睛”)。因此,如果你的新数据集与原始模型训练的数据集有很大差异,那么最好只使用模型的前几层来做特征提取,而不是使用整个卷积基。

将 VGG16 卷积基实例化

from tensorflow.keras.applications import VGG16

conv_base = VGG16(weights='imagenet',	# 指定模型初始化的权重检查点。
                  include_top=False,	# 不要最后一层的分类器
                  input_shape=(150, 150, 3))	# 输入到网络中的图像张量的形状(可选),如果不传入,那么网络能够处理任意形状的输入。

最后的特征图形状为 (4, 4, 512)。我们将在这个特征上添加一个密集连接分类器。 接下来,下一步有两种方法可供选择。

  • 在你的数据集上运行卷积基,将输出保存成硬盘中的 Numpy 数组,然后用这个数据作为输入,输入到独立的密集连接分类器中(与本书第一部分介绍的分类器类似)。这种方法速度快,计算代价低,因为对于每个输入图像只需运行一次卷积基,而卷积基是目前流程中计算代价最高的。但出于同样的原因,这种方法不允许你使用数据增强。
  • 在顶部添加 Dense 层来扩展已有模型(即 conv_base),并在输入数据上端到端地运行整个模型。这样你可以使用数据增强,因为每个输入图像进入模型时都会经过卷积基。 但出于同样的原因,这种方法的计算代价比第一种要高很多。

1.不使用数据增强的快速特征提取

import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'

train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20

def extract_features(directory, sample_count):
    features = np.zeros(shape=(sample_count, 4, 4, 512))
    labels = np.zeros(shape=(sample_count))
    generator = datagen.flow_from_directory(
        directory,
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode='binary')
    i = 0
    for inputs_batch, labels_batch in generator:
        features_batch = conv_base.predict(inputs_batch)
        features[i * batch_size : (i + 1) * batch_size] = features_batch
        labels[i * batch_size : (i + 1) * batch_size] = labels_batch
        i += 1
        if i * batch_size >= sample_count:
            # Note that since generators yield data indefinitely in a loop,
            # we must `break` after every image has been seen once.
            break
    return features, labels

train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)

目前,提取的特征形状为 (samples, 4, 4, 512)。我们要将其输入到密集连接分类器中, 所以首先必须将其形状展平为 (samples, 8192)。

train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))

现在你可以定义你的密集连接分类器(注意要使用 dropout 正则化),并在刚刚保存的数据和标签上训练这个分类器。

from keras import models
from keras import layers
from keras import optimizers

model = models.Sequential()	# 只需要构建一个分类器就行
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
              loss='binary_crossentropy',
              metrics=['acc'])

history = model.fit(train_features, train_labels,
                    epochs=30,
                    batch_size=20,
                    validation_data=(validation_features, validation_labels))

2.使用数据增强的特征提取

扩展 conv_base 模型,然后在输入数据上端到端地运行模型。模型的行为和层类似,所以你可以向 Sequential 模型中添加一个模型(比如 conv_base), 就像添加一个层一样。

from keras import models
from keras import layers
from tensorflow.keras.applications import VGG16

conv_base = VGG16(weights='imagenet',
                  include_top=False,
                  input_shape=(150, 150, 3))

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

在编译和训练模型之前,一定要“冻结”卷积基。冻结(freeze)一个或多个层是指在训练过程中保持其权重不变。如果不这么做,那么卷积基之前学到的表示将会在训练过程中被修改。 因为其上添加的 Dense 层是随机初始化的,所以非常大的权重更新将会在网络中传播,对之前学到的表示造成很大破坏。

conv_base.trainable = False	# 编译之前先冻结conv_base

利用冻结的卷积基端到端地训练模型

from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(
      rescale=1./255,
      rotation_range=40,
      width_shift_range=0.2,
      height_shift_range=0.2,
      shear_range=0.2,
      zoom_range=0.2,
      horizontal_flip=True,
      fill_mode='nearest')

# Note that the validation data should not be augmented! 注意,不能增强验证数据
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
        # This is the target directory
        train_dir,
        # All images will be resized to 150x150 将所有图像的大小调整为 150×150
        target_size=(150, 150),
        batch_size=20,
        # Since we use binary_crossentropy loss, we need binary labels 因为使用了binary_crossentropy损失,所以需要用二进制标签
        class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=2e-5),
              metrics=['acc'])

history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=30,
      validation_data=validation_generator,
      validation_steps=50,
      verbose=2)
Keys: 微调(fine-tuning)

微调(fine-tuning),与特征提取互为补充。对于用于特征提取的冻结的模型基,微调是指将其顶部的几层“解冻”,并将这解冻的几层和新增加的部分联合训练。

微调是在分类器已经训练好的基础上,解冻基网络的一些层一起训练,而不是直接微调。因此, 微调网络的步骤如下。

  1. 在已经训练好的基网络(base network)上添加自定义网络。
  2. 冻结基网络。
  3. 训练所添加的部分。
  4. 解冻基网络的一些层。
  5. 联合训练解冻的这些层和添加的部分。

冻结直到某一层的所有层

conv_base.trainable = True

set_trainable = False
for layer in conv_base.layers:
    if layer.name == 'block5_conv1':	# block5_conv1以后的层都可以训练
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False

使用学习率非常小的 RMSProp 优化器来实现,因为希望其变化范围不要太大。太大的权重更新可能会破坏这些表示。

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-5),
              metrics=['acc'])

history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=100,
      validation_data=validation_generator,
      validation_steps=50)

在测试集进行评估。

test_generator = test_datagen.flow_from_directory(
        test_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc:', test_acc)

4.4 卷积神经网络的可视化

Keys: 可视化中间激活

可视化中间激活,是指对于给定输入,展示网络中各个卷积层和池化层输出的特征图(层的输出通常被称为该层的激活,即激活函数的输出)。这让我们可以看到输入如何被分解为网络学到的不同过滤器。

预处理单个数据

img_path = '/Users/fchollet/Downloads/cats_and_dogs_small/test/cats/cat.1700.jpg'

from keras.preprocessing import image	# 将图像预处理为一个 4D 张量
import numpy as np

img = image.load_img(img_path, target_size=(150, 150))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.	# 请记住,训练模型的输入数据都用这种方法预处理
print(img_tensor.shape)		# 其形状为 (1, 150, 150, 3)

用一个输入张量和一个输出张量列表将模型实例化

from keras import models
layer_outputs = [layer.output for layer in model.layers[:8]]		# 提取前 8 层的输出
activation_model = models.Model(inputs=model.input, outputs=layer_outputs)		# 创建一个模型,给定模型输入,可以返回这些输出

以预测模式运行模型

activations = activation_model.predict(img_tensor)		# 返回8个Numpy数组组成的列表,每个层激活对应一个 Numpy 数组

例如,对于输入的猫图像,第一个卷积层的激活如下所示。

first_layer_activation = activations[0]
print(first_layer_activation.shape)		# (1, 148, 148, 32)

它是大小为 148×148 的特征图,有 32 个通道。我们来绘制原始模型第一层激活的第 4 个 通道(见图 5-25)。

将第 4 个通道可视化

import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WeUefi8f-1672461065806)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221226205645434.png)]

将每个中间激活的所有通道可视化

import keras

# 层的名称,这样你可以将这些名称画到图中
layer_names = []
for layer in model.layers[:8]:
    layer_names.append(layer.name)

images_per_row = 16

# 显示特征图
for layer_name, layer_activation in zip(layer_names, activations):
    # 特征图中的特征个数
    n_features = layer_activation.shape[-1]

    # 特征图的形状为 (1, size, size, n_features)
    size = layer_activation.shape[1]

    # 在这个矩阵中将激活通道平铺
    n_cols = n_features // images_per_row
    display_grid = np.zeros((size * n_cols, images_per_row * size))

    # 将每个过滤器平铺到一个大的水平网格中
    for col in range(n_cols):
        for row in range(images_per_row):
            channel_image = layer_activation[0,
                                             :, :,
                                             col * images_per_row + row]
            # 对特征进行后处理,使其看起来更美观
            channel_image -= channel_image.mean()
            channel_image /= channel_image.std()
            channel_image *= 64
            channel_image += 128
            channel_image = np.clip(channel_image, 0, 255).astype('uint8')
            display_grid[col * size : (col + 1) * size,
                         row * size : (row + 1) * size] = channel_image

    # 显示网格
    scale = 1. / size
    plt.figure(figsize=(scale * display_grid.shape[1],
                        scale * display_grid.shape[0]))
    plt.title(layer_name)
    plt.grid(False)
    plt.imshow(display_grid, aspect='auto', cmap='viridis')
    
plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ACYb4dro-1672461065806)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221226205922111.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nLaj7RkS-1672461065806)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221226205931523.png)]

5 文本和序列

用于处理序列的两种基本的深度学习算法分别是循环神经网络(recurrent neural network)和一维卷积神经网络(1D convnet)。

5.1 文本向量化

  • 将文本分割为单词,并将每个单词转换为一个向量。
  • 将文本分割为字符,并将每个字符转换为一个向量。
  • 提取单词或字符的 n-gram,并将每个 n-gram 转换为一个向量。n-gram 是多个连续单词或字符的集合(n-gram 之间可重叠)。

将文本分解而成的单元(单词、字符或 n-gram)叫作标记(token),将文本分解成标记的 过程叫作分词(tokenization)。将向 量与标记相关联的方法有很多种。本节将介绍两种主要方法:对标记做 one-hot 编码(one-hot encoding)与标记嵌入[token embedding,通常只用于单词,叫作词嵌入(word embedding)]。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ITTALCB-1672461065807)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230120158614.png)]

Keys: one-hot encoding

one-hot 编码将每个单词与一个唯一的整数索引相关联, 然后将这个整数索引 i 转换为长度为 N 的二进制向量(N 是词表大小),这个向量只有第 i 个元 素是 1,其余元素都为 0。

import numpy as np

# 初始数据:每个样本是列表的一个元素(本例中的样本是一个句子,但也可以是一整篇文档)
samples = ['The cat sat on the mat.', 'The dog ate my homework.']

# 构建数据中所有标记的索引
token_index = {}
for sample in samples:
	# 利用 split 方法对样本进行分词。在实际应用中,还需要从样本中去掉标点和特殊字符
    for word in sample.split():
        if word not in token_index:
            # 为每个唯一单词指定一个唯一索引。注意,没有为索引编号 0 指定单词
            token_index[word] = len(token_index) + 1
print(token_index)	# {'The': 1, 'cat': 2, 'sat': 3, 'on': 4, 'the': 5, 'mat.': 6, 'dog': 7, 'ate': 8, 'my': 9, 'homework.': 10}           
# 对样本进行分词。只考虑每个样本前 max_length 个单词
max_length = 10

# 将结果保存在 results 中
results = np.zeros((len(samples), max_length, max(token_index.values()) + 1))
for i, sample in enumerate(samples):
    for j, word in list(enumerate(sample.split()))[:max_length]:
        index = token_index.get(word)
        results[i, j, index] = 1.
print(results)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wM1wjkYO-1672461065807)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230121551728.png)]

用 Keras 实现单词级的 one-hot 编码

from keras.preprocessing.text import Tokenizer

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

# 创建一个分词器(tokenizer),设置为只考虑前 1000 个最常见的单词
tokenizer = Tokenizer(num_words=1000)

# 构建单词索引
tokenizer.fit_on_texts(samples)

# 将字符串转换为整数索引组成的列表
sequences = tokenizer.texts_to_sequences(samples)
print(sequences)	# [[1, 2, 3, 4, 1, 5], [1, 6, 7, 8, 9]]

# 也可以直接得到 one-hot 二进制表示。这个分词器也支持除 one-hot 编码外的其他向量化模式
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')
print(one_hot_results)	#[[0. 1. 1. ... 0. 0. 0.]
						# [0. 1. 0. ... 0. 0. 0.]]
# 找回单词索引
word_index = tokenizer.word_index
print(word_index)	# {'the': 1, 'cat': 2, 'sat': 3, 'on': 4, 'mat': 5, 'dog': 6, 'ate': 7, 'my': 8, 'homework': 9}
Keys: word Embedding

要将一个词与一个密集向量相关联,最简单的方法就是随机选择向量。这种方法的问题在于,得到的嵌入空间没有任何结构。说得更抽象一点,词向量之间的几何关系应该表示这些词之间的语义关系。词嵌入的作用应该是将人类的语言映射到几何空间中。

一般来说,任意两个词向量之间的几何距离(比如 L2 距离)应该和这两个词的语义距离有关(表示不同事物的词被嵌入到相隔很远的点,而相关的词则更加靠近)。除了距离,你可能还希望嵌入空间中的特定方向也是有意义的。

例如,从 cat 到 tiger 的向量与从 dog 到 wolf 的向量相等,这个向量可以被解释为“从宠物到野生动物”向量。同样,从 dog 到 cat 的向量与从 wolf 到 tiger 的向量也相等,它可以被解释为“从犬科到猫科”向量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ydVjnDXr-1672461065807)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230201317923.png)]

获取词嵌入有两种方法。

  • 在完成主任务的同时学习词嵌入。在这种情况下,一开始是随机的词向量,然后对这些词向量进行学习,其学习方式与学习神经网络的权重相同。

  • 在不同于待解决问题的机器学习任务上预计算好词嵌入,然后将其加载到模型中。这些词嵌入叫作预训练词嵌入(pretrained word embedding)。

利用 Embedding 层学习词嵌入

from keras.layers import Embedding
embedding_layer = Embedding(1000, 64)	# Embedding 层至少需要两个参数:标记的个数(这里是 1000,即最大单词索引 +1)和嵌入的维度(这里是64)

最好将 Embedding 层理解为一个字典,将整数索引(表示特定单词)映射为密集向量。它 接收整数作为输入,并在内部字典中查找这些整数,然后返回相关联的向量。Embedding 层实 际上是一种字典查找。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gENBq8ZT-1672461065807)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230202003170.png)]

Embedding 层的输入是一个二维整数张量,其形状为 (samples, sequence_length), 每个元素是一个整数序列。它能够嵌入长度可变的序列,例如,对于前一个例子中的 Embedding 层,你可以输入形状为 (32, 10)(32 个长度为 10 的序列组成的批量)或 (64, 15)(64 个长度为 15 的序列组成的批量)的批量。不过一批数据中的所有序列必须具有相同的长度(因为需要将它们打包成一个张量),所以较短的序列应该用 0 填充,较长的序列应该被截断。

这个 Embedding 层返回一个形状为 (samples, sequence_length, embedding_ dimensionality) 的三维浮点数张量。然后可以用 RNN 层或一维卷积层来处理这个三维张量 (二者都会在后面介绍)。

加载 IMDB 数据,准备用于 Embedding 层

from keras.datasets import imdb
from keras.layers import preprocessing
max_features = 10000		# 作为特征的单词个数
maxlen = 20		# 在这么多单词后截断文本(这些单词都属于前 max_features 个最常见的单词)
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)	# 将数据加载为整数列表
x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)	# 将整数列表转换成形状为(samples, maxlen) 的二维整数张量
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)	
# 就是原始的是一个一个的list,每个list中的数量不一样。然后转成一个二维list,每个list20个整数值

在 IMDB 数据上使用 Embedding 层和分类器

from keras.models import Sequential
from keras.layers import Flatten, Dense

model = Sequential()
# 指定 Embedding 层的最大输入长度,以便后面将嵌入输入展平。Embedding 层激活的形状为 (samples, maxlen, 8)
model.add(Embedding(10000, 8, input_length=maxlen))	

# 将三维的嵌入张量展平成形状为 (samples, maxlen * 8) 的二维张量
model.add(Flatten())

# 在上面添加分类器
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
model.summary()

history = model.fit(x_train, y_train,
                    epochs=10,
                    batch_size=32,
                    validation_split=0.2)

使用预训练的词嵌入

  1. 下载 IMDB 数据的原始文本

首先,打开 http://mng.bz/0tIo,下载原始 IMDB 数据集并解压。

接下来,我们将训练评论转换成字符串列表,每个字符串对应一条评论。你也可以将评论 标签(正面 / 负面)转换成 labels 列表。

处理 IMDB 原始数据的标签

import os

imdb_dir = '/home/ubuntu/data/aclImdb'
train_dir = os.path.join(imdb_dir, 'train')

labels = []
texts = []

for label_type in ['neg', 'pos']:
    dir_name = os.path.join(train_dir, label_type)
    for fname in os.listdir(dir_name):
        if fname[-4:] == '.txt':
            f = open(os.path.join(dir_name, fname))
            texts.append(f.read())
            f.close()
            if label_type == 'neg':
                labels.append(0)
            else:
                labels.append(1)
  1. 对数据进行分词

利用本节前面介绍过的概念,我们对文本进行分词,并将其划分为训练集和验证集。因为预训练的词嵌入对训练数据很少的问题特别有用(否则,针对于具体任务的嵌入可能效果更好), 所以我们又添加了以下限制:将训练数据限定为前 200 个样本。因此,你需要在读取 200 个样本之后学习对电影评论进行分类。

对 IMDB 原始数据的文本进行分词

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np

maxlen = 100  # 在 100 个单词后截断评论
training_samples = 200  # 在 200 个样本上训练
validation_samples = 10000  # 在 10 000 个样本上验证
max_words = 10000  # 只考虑数据集中前 10 000 个最常见的单词

tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)

word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))

data = pad_sequences(sequences, maxlen=maxlen)

labels = np.asarray(labels)
print('Shape of data tensor:', data.shape)
print('Shape of label tensor:', labels.shape)

# 将数据划分为训练集和验证集,但首先要打乱数据,因为一开始数据中的样本是排好序的(所有负面评论都在前面,然后是所有正面评论)
indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]

x_train = data[:training_samples]
y_train = labels[:training_samples]
x_val = data[training_samples: training_samples + validation_samples]
y_val = labels[training_samples: training_samples + validation_samples]
  1. 下载 GloVe 词嵌入

打开 https://nlp.stanford.edu/projects/glove,下载 2014 年英文维基百科的预计算嵌入。这是 一个 822 MB 的压缩文件,文件名是 glove.6B.zip,里面包含 400 000 个单词(或非单词的标记) 的 100 维嵌入向量。解压文件。

  1. 对嵌入进行预处理

我们对解压后的文件(一个 .txt 文件)进行解析,构建一个将单词(字符串)映射为其向量表示(数值向量)的索引。

glove_dir = '/home/ubuntu/data/'

embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'))
for line in f:
    values = line.split()
    word = values[0]
    coefs = np.asarray(values[1:], dtype='float32')
    embeddings_index[word] = coefs
f.close()

print('Found %s word vectors.' % len(embeddings_index))

接下来,需要构建一个可以加载到 Embedding 层中的嵌入矩阵。它必须是一个形状为 (max_words, embedding_dim) 的矩阵,对于单词索引(在分词时构建)中索引为 i 的单词, 这个矩阵的元素 i 就是这个单词对应的 embedding_dim 维向量。注意,索引 0 不应该代表任何 单词或标记,它只是一个占位符。

准备 GloVe 词嵌入矩阵

embedding_dim = 100

embedding_matrix = np.zeros((max_words, embedding_dim))
for word, i in word_index.items():
    embedding_vector = embeddings_index.get(word)
    if i < max_words:
        if embedding_vector is not None:
            # 嵌入索引(embeddings_index)中找不到的词,其嵌入向量全为 0
            embedding_matrix[i] = embedding_vector
  1. 定义模型

我们将使用与前面相同的模型架构。

from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense

model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
  1. 在模型中加载 GloVe 嵌入

Embedding 层只有一个权重矩阵,是一个二维的浮点数矩阵,其中每个元素 i 是与索引 i 相关联的词向量。够简单。将准备好的 GloVe 矩阵加载到 Embedding 层中,即模型的第一层。

将预训练的词嵌入加载到 Embedding 层中

model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False

此外,需要冻结 Embedding 层(即将其 trainable 属性设为 False),其原理和预训练的卷积神经网络特征相同,你已经很熟悉了。如果一个模型的一部分是经过预训练的(如 Embedding 层),而另一部分是随机初始化的(如分类器),那么在训练期间不应该更新预训练的部分,以 避免丢失它们所保存的信息。随机初始化的层会引起较大的梯度更新,会破坏已经学到的特征。

  1. 训练模型与评估模型
model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=10,
                    batch_size=32,
                    validation_data=(x_val, y_val))
model.save_weights('pre_trained_glove_model.h5')

5.2 RNN

准备 IMDB 数据

from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000	# 作为特征的单词个数
maxlen = 500			# 在这么多单词之后截断文本(这些单词都属于前 max_features 个最常见的单词)
batch_size = 32

print('Loading data...')
(input_train, y_train), (input_test, y_test) = imdb.load_data(num_words=max_features)

print(len(input_train), 'train sequences')
print(len(input_test), 'test sequences')
print('Pad sequences (samples x time)')

input_train = sequence.pad_sequences(input_train, maxlen=maxlen)
input_test = sequence.pad_sequences(input_test, maxlen=maxlen)

print('input_train shape:', input_train.shape)
print('input_test shape:', input_test.shape)

我们用一个 Embedding 层和一个 SimpleRNN 层来训练一个简单的循环网络。

from keras.layers import Dense
# 构建模型
model = Sequential()
model.add(Embedding(max_features, 32))
model.add(SimpleRNN(32))
model.add(Dense(1, activation='sigmoid'))
# 编译模型
model.compile(optimizer='rmsprop', 
              loss='binary_crossentropy', 
              metrics=['acc'])
# 训练模型
history = model.fit(input_train, y_train,
                    epochs=10,
                    batch_size=128,
                    validation_split=0.2)
Keys: LSTM构建
from keras.layers import LSTM

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(LSTM(32))		# 和simpleRNN一样。
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['acc'])

history = model.fit(input_train, y_train,
                    epochs=10,
                    batch_size=128,
                    validation_split=0.2)
Keys: 序列数据生成器
def generator(data, lookback, delay, min_index, max_index,
              shuffle=False, batch_size=128, step=6):
    if max_index is None:
        max_index = len(data) - delay - 1
    i = min_index + lookback
    while 1:
        if shuffle:
            rows = np.random.randint(
                min_index + lookback, max_index, size=batch_size)
        else:
            if i + batch_size >= max_index:
                i = min_index + lookback
            rows = np.arange(i, min(i + batch_size, max_index))
            i += len(rows)

        samples = np.zeros((len(rows),
                           lookback // step,
                           data.shape[-1]))
        targets = np.zeros((len(rows),))
        for j, row in enumerate(rows):
            indices = range(rows[j] - lookback, rows[j], step)
            samples[j] = data[indices]
            targets[j] = data[rows[j] + delay][1]
        yield samples, targets

准备训练生成器、验证生成器和测试生成器

lookback = 1440
step = 6
delay = 144
batch_size = 128

train_gen = generator(float_data,
                      lookback=lookback,
                      delay=delay,
                      min_index=0,
                      max_index=200000,
                      shuffle=True,
                      step=step, 
                      batch_size=batch_size)
val_gen = generator(float_data,
                    lookback=lookback,
                    delay=delay,
                    min_index=200001,
                    max_index=300000,
                    step=step,
                    batch_size=batch_size)
test_gen = generator(float_data,
                     lookback=lookback,
                     delay=delay,
                     min_index=300001,
                     max_index=None,
                     step=step,
                     batch_size=batch_size)

# 为了查看整个验证集,需要从 val_gen 中抽取多少次
val_steps = (300000 - 200001 - lookback) // batch_size

# 为了查看整个测试集,需要从 test_gen 中抽取多少次
test_steps = (len(float_data) - 300001 - lookback) // batch_size
Keys: GRU构建
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen,
                              steps_per_epoch=500,
                              epochs=20,
                              validation_data=val_gen,
                              validation_steps=val_steps)
Keys: 循环 dropout

在循环网络中如何正确地使用 dropout,这并不是一个简单的问题。人们早就知道,在循环层前面应用 dropout,这种正则化会妨碍学习过程,而不是有所帮助。

2015 年,在关于贝叶斯深度学习的博士论文中,Yarin Gal 确定了在循环网络中使用 dropout 的正确方法:对每个时间步应该使用相同的 dropout 掩码(dropout mask,相同模式的舍弃单元),而不是让 dropout 掩码随着时间步的增加而随机变化。此外,为 了对 GRU、LSTM 等循环层得到的表示做正则化,应该将不随时间变化的 dropout 掩码应用于层的内部循环激活(叫作循环 dropout 掩码)。对每个时间步使用相同的 dropout 掩码,可以让网络沿着时间正确地传播其学习误差,而随时间随机变化的 dropout 掩码则会破坏这个误差信号,并且不利于学习过程。

Yarin Gal 使用 Keras 开展这项研究,并帮助将这种机制直接内置到 Keras 循环层中。Keras 的每个循环层都有两个与 dropout 相关的参数:一个是 dropout,它是一个浮点数,指定该层输入单元的 dropout 比率;另一个是 recurrent_dropout,指定循环单元的 dropout 比率。我 们向 GRU 层中添加 dropout 和循环 dropout,看一下这么做对过拟合的影响。因为使用 dropout 正则化的网络总是需要更长的时间才能完全收敛,所以网络训练轮次增加为原来的 2 倍。

from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32,
                     dropout=0.2,
                     recurrent_dropout=0.2,
                     input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
                              steps_per_epoch=500,
                              epochs=40,
                              validation_data=val_gen,
                              validation_steps=val_steps)
Keys: 循环层堆叠
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32,
                     dropout=0.1,
                     recurrent_dropout=0.5,
                     return_sequences=True,
                     input_shape=(None, float_data.shape[-1])))
model.add(layers.GRU(64, activation='relu',
                     dropout=0.1, 
                     recurrent_dropout=0.5))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
                              steps_per_epoch=500,
                              epochs=40,
                              validation_data=val_gen,
                              validation_steps=val_steps)
Keys: 双向RNN
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Bidirectional(layers.GRU(32), input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
                              steps_per_epoch=500,
                              epochs=40,
                              validation_data=val_gen,
                              validation_steps=val_steps)

5.3 一维卷积

from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Embedding(max_features, 128, input_length=max_len))
model.add(layers.Conv1D(32, 7, activation='relu'))	# 窗口大小为7,过滤器(通道数)为32。
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))

model.summary()

model.compile(optimizer=RMSprop(lr=1e-4),
              loss='binary_crossentropy',
              metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=10,
                    batch_size=128,
                    validation_split=0.2)
Keys: 一维卷积与RNN结合

要想结合卷积神经网络的速度和轻量与 RNN 的顺序敏感性,一种方法是在 RNN 前面使用 一维卷积神经网络作为预处理步骤。对于那些非常长,以至于 RNN 无法处理的序列 (比如包含上千个时间步的序列),这种方法尤其有用。卷积神经网络可以将长的输入序列转换为 高级特征组成的更短序列(下采样)。然后,提取的特征组成的这些序列成为网络中 RNN 的输入。

model = Sequential()
model.add(layers.Conv1D(32, 5, activation='relu',
                        input_shape=(None, float_data.shape[-1])))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.GRU(32, dropout=0.1, recurrent_dropout=0.5))
model.add(layers.Dense(1))

model.summary()

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
                              steps_per_epoch=500,
                              epochs=20,
                              validation_data=val_gen,
                              validation_steps=val_steps)

6 高级的深度学习

6.1 函数式 API

多模态(multimodal)输入:这些任务合并来自不同输入源的数据,并使用不同类型的神经层处理不同类型的数据。

假设有一个深度学习模型,试图利用下列输入来预测一件二手衣服最可能的市场价格:用户提供的元数据(比如商品品牌、已使用年限等)、用 户提供的文本描述与商品照片。如果你只有元数据,那么可以使用 one-hot 编码,然后用密集 连接网络来预测价格。如果你只有文本描述,那么可以使用循环神经网络或一维卷积神经网络。 如果你只有图像,那么可以使用二维卷积神经网络。但怎么才能同时使用这三种数据呢?一种 朴素的方法是训练三个独立的模型,然后对三者的预测做加权平均。但这种方法可能不是最优的, 因为模型提取的信息可能存在冗余。更好的方法是使用一个可以同时查看所有可用的输入模态 的模型,从而联合学习一个更加精确的数据模型——这个模型具有三个输入分支(见图 7-2)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lxc4ck8F-1672461065808)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230214620845.png)]

多模态(multimodal)输出:有些任务需要预测输入数据的多个目标属性。

给定一部小说的文本,你可能希望将 它按类别自动分类(比如爱情小说或惊悚小说),同时还希望预测其大致的写作日期。当然,你 可以训练两个独立的模型:一个用于划分类别,一个用于预测日期。但由于这些属性并不是统 计无关的,你可以构建一个更好的模型,用这个模型来学习同时预测类别和日期。这种联合模 型将有两个输出,或者说两个头(head,见图 7-3)。因为类别和日期之间具有相关性,所以知 道小说的写作日期有助于模型在小说类别的空间中学到丰富而又准确的表示,反之亦然。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52KtGxVV-1672461065808)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230214907652.png)]

此外,许多最新开发的神经架构要求非线性的网络拓扑结构,即网络结构为有向无环图。 比如,Inception 系列网络(由 Google 的 Szegedy 等人开发)a 依赖于 Inception 模块,其输入被 多个并行的卷积分支所处理,然后将这些分支的输出合并为单个张量(见图 7-4)。最近还有一 种趋势是向模型中添加残差连接(residual connection),它最早出现于 ResNet 系列网络(由微 软的何恺明等人开发)。b 残差连接是将前面的输出张量与后面的输出张量相加,从而将前面的 表示重新注入下游数据流中(见图 7-5),这有助于防止信息处理流程中的信息损失。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SQfMffXF-1672461065808)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230214940877.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZpRzLGB-1672461065808)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230214949556.png)]

使用函数式 API,你可以直接操作张量,也可以把层当作函数来使用,接收张量并返回张 量(因此得名函数式 API)。

Keys: API简易实现
from keras.models import Sequential, Model
from keras import layers
from keras import Input

# 前面学过的 Sequential 模型
seq_model = Sequential()
seq_model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
seq_model.add(layers.Dense(32, activation='relu'))
seq_model.add(layers.Dense(10, activation='softmax'))

# 对应的函数式 API 实现
input_tensor = Input(shape=(64,))
x = layers.Dense(32, activation='relu')(input_tensor)
x = layers.Dense(32, activation='relu')(x)
output_tensor = layers.Dense(10, activation='softmax')(x)

# Model 类将输入张量和输出张量转换为一个模型
model = Model(input_tensor, output_tensor)
model.summary() 

Model 对象实例化只用了一个输入张量和 一个输出张量。Keras 会在后台检索从 input_tensor 到 output_tensor 所包含的每一层, 并将这些层组合成一个类图的数据结构,即一个 Model。

Keys: 多输入模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndxxbHQy-1672461065809)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230215320107.png)]

from keras.models import Model
from keras import layers
from keras import Input

text_vocabulary_size = 10000
question_vocabulary_size = 10000
answer_vocabulary_size = 500

# 文本输入是一个长度可变的整数序列。注意,你可以选择对输入进行命名
text_input = Input(shape=(None,), dtype='int32', name='text')

# 将输入嵌入长度为 64 的向量
embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_input)

# 利用 LSTM 将向量编码为单个向量
encoded_text = layers.LSTM(32)(embedded_text)

# 对问题进行相同的处理(使用不同的层实例)
question_input = Input(shape=(None,), dtype='int32', name='question')

embedded_question = layers.Embedding(question_vocabulary_size, 32)(question_input)

encoded_question = layers.LSTM(16)(embedded_question)

# 将编码后的问题和文本连接起来
concatenated = layers.concatenate([encoded_text, encoded_question],axis=-1)

# 在上面添加一个softmax 分类器
answer = layers.Dense(answer_vocabulary_size, activation='softmax')(concatenated)

# 在模型实例化时,指定两个输入和输出
model = Model([text_input, question_input], answer)

model.compile(optimizer='rmsprop',
			  loss='categorical_crossentropy',
			  metrics=['acc'])

将数据输入到多输入模型中

import numpy as np

num_samples = 1000
max_length = 100

# 生成虚构的 Numpy数据
text = np.random.randint(1, text_vocabulary_size, size=(num_samples, max_length))

question = np.random.randint(1, question_vocabulary_size, size=(num_samples, max_length))

# 回答是 one-hot 编码的,不是整数
answers = np.random.randint(answer_vocabulary_size, size=(num_samples))
answers = keras.utils.to_categorical(answers, answer_vocabulary_size)

# 使用输入组成的列表来拟合
model.fit([text, question], answers, epochs=10, batch_size=128)

# 使用输入组成的字典来拟合(只有对输入进行命名之后才能用这种方法)
model.fit({'text': text, 'question': question}, answers, epochs=10, batch_size=128)
Keys: 多输出模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kJppo8DW-1672461065809)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230215513575.png)]

from keras import layers
from keras import Input
from keras.models import Model

vocabulary_size = 50000
num_income_groups = 10

posts_input = Input(shape=(None,), dtype='int32', name='posts')
embedded_posts = layers.Embedding(256, vocabulary_size)(posts_input)

x = layers.Conv1D(128, 5, activation='relu')(embedded_posts)
x = layers.MaxPooling1D(5)(x)

x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D(5)(x)

x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.GlobalMaxPooling1D()(x)

x = layers.Dense(128, activation='relu')(x)

# 注意,输出层都具有名称
age_prediction = layers.Dense(1, name='age')(x)

income_prediction = layers.Dense(num_income_groups,
                                 activation='softmax',
                                 name='income')(x)

gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)

model = Model(posts_input, [age_prediction, income_prediction, gender_prediction])

重要的是,训练这种模型需要能够对网络的各个头指定不同的损失函数,例如,年龄预测是标量回归任务,而性别预测是二分类任务,二者需要不同的训练过程。但是,梯度下降要求将一个标量最小化,所以为了能够训练模型,我们必须将这些损失合并为单个标量。合并不同损失最简单的方法就是对所有损失求和。在 Keras 中,你可以在编译时使用损失组成的列表或字典来为不同输出指定不同损失,然后将得到的损失值相加得到一个全局损失,并在训练过程中将这个损失最小化。

多输出模型的编译选项:多重损失

model.compile(optimizer='rmsprop',
			  loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'])

# 与上述写法等效(只有输出层具有名称时才能采用这种写法)
model.compile(optimizer='rmsprop',
			  loss={'age': 'mse', 'income': 'categorical_crossentropy', 'gender': 'binary_crossentropy'})

注意,严重不平衡的损失贡献会导致模型表示针对单个损失值最大的任务优先进行优化, 而不考虑其他任务的优化。为了解决这个问题,我们可以为每个损失值对最终损失的贡献分配不同大小的重要性。如果不同的损失值具有不同的取值范围,那么这一方法尤其有用。比如, 用于年龄回归任务的均方误差(MSE)损失值通常在 3~5 左右,而用于性别分类任务的交叉熵损失值可能低至 0.1。在这种情况下,为了平衡不同损失的贡献,我们可以让交叉熵损失的权重取 10,而 MSE 损失的权重取 0.5。

多输出模型的编译选项:损失加权

model.compile(optimizer='rmsprop',
              loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'],
              loss_weights=[0.25, 1., 10.])

# 与上述写法等效(只有输出层具有名称时才能采用这种写法)
model.compile(optimizer='rmsprop',
              loss={'age': 'mse',
            		'income': 'categorical_crossentropy',
            		'gender': 'binary_crossentropy'},
              loss_weights={'age': 0.25,
            				'income': 1.,
            				'gender': 10.})

将数据输入到多输出模型中

model.fit(posts, [age_targets, income_targets, gender_targets],
		  epochs=10, batch_size=64)

# 以下写法等效
model.fit(posts, {'age': age_targets,
                  'income': income_targets,
                  'gender': gender_targets},
          epochs=10, batch_size=64)
Keys: Inception模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QgLMNTmX-1672461065810)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230215951349.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6z2YBRru-1672461065810)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230220049072.png)]

from keras import layers

# 每个分支都有相同的步幅值(2),这对于保持所有分支输出具有相同的尺寸是很有必要的,这样你才能将它们连接在一起
branch_a = layers.Conv2D(128, 1, activation='relu', strides=2)(x)

# 在这个分支中,空间卷积层用到了步幅
branch_b = layers.Conv2D(128, 1, activation='relu')(x)
branch_b = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_b)

# 在这个分支中,平均池化层用到了步幅
branch_c = layers.AveragePooling2D(3, strides=2)(x)
branch_c = layers.Conv2D(128, 3, activation='relu')(branch_c)

# 在这个分支中,空间卷积层用到了步幅
branch_d = layers.Conv2D(128, 1, activation='relu')(x)
branch_d = layers.Conv2D(128, 3, activation='relu')(branch_d)
branch_d = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_d)

# 将分支输出连接在一起,得到模块输出
output = layers.concatenate([branch_a, branch_b, branch_c, branch_d], axis=-1) 
Keys: 残差链接

残差连接解决了困扰所有大规模深度学习模型的两个共性问题: 梯度消失和表示瓶颈。通常来说,向任何多于 10 层的模型中添加残差连接,都可能会有所帮助。

残差连接是让前面某层的输出作为后面某层的输入,从而在序列网络中有效地创造了一条捷径。前面层的输出没有与后面层的激活连接在一起,而是与后面层的激活相加(这里假设两 个激活的形状相同)。如果它们的形状不同,我们可以用一个线性变换将前面层的激活改变成目标形状(例如,这个线性变换可以是不带激活的 Dense 层;对于卷积特征图,可以是不带激活 1×1 卷积)。

如果特征图的尺寸相同,实现残差连接的方法如下,用的是恒等残差连接(identity residual connection)。这个例子假设我们有一个四维输入张量 x。

from keras import layers

x = ...
# 对 x 进行变换
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x) 	
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
# 将原始 x 与输出特征相加
y = layers.add([y, x]) 		

如果特征图的尺寸不同,实现残差连接的方法如下,用的是线性残差连接(linear residual connection)。同样,假设我们有一个四维输入张量 x。

from keras import layers
x = ...
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.MaxPooling2D(2, strides=2)(y)

# 使用 1×1 卷积,将原始 x 张量线性下采样为与 y 具有相同的形状
residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)

# 将残差张量与输出特征相加
y = layers.add([y, residual])
Keys: 共享层权重

函数式 API 还有一个重要特性,那就是能够多次重复使用一个层实例。如果你对一个层实例调用两次,而不是每次调用都实例化一个新层,那么每次调用可以重复使用相同的权重。这样你可以构建具有共享分支的模型,即几个分支全都共享相同的知识并执行相同的运算。也就是说,这些分支共享相同的表示,并同时对不同的输入集合学习这些表示。

举个例子,假设一个模型想要评估两个句子之间的语义相似度。这个模型有两个输入(需要比较的两个句子),并输出一个范围在 0~1 的分数,0 表示两个句子毫不相关,1 表示两个句子完全相同或只是换一种表述。这种模型在许多应用中都很有用,其中包括在对话系统中删除重复的自然语言查询。

在这种设置下,两个输入句子是可以互换的,因为语义相似度是一种对称关系,A 相对于 B 的相似度等于 B 相对于 A 的相似度。因此,学习两个单独的模型来分别处理两个输入句子是没有道理的。相反,你需要用一个 LSTM 层来处理两个句子。这个 LSTM 层的表示(即它的权重)是同时基于两个输入来学习的。我们将其称为连体 LSTM(Siamese LSTM)或共享 LSTM(shared LSTM)模型。

使用 Keras 函数式 API 中的层共享(层重复使用)可以实现这样的模型,其代码如下所示。

from keras import layers
from keras import Input
from keras.models import Model

lstm = layers.LSTM(32)	# 将一个 LSTM 层实例化一次

left_input = Input(shape=(None, 128))	# 构建模型的左分支:输入是长度 128 的向量组成的变长序列
left_output = lstm(left_input)

right_input = Input(shape=(None, 128))	# 构建模型的右分支:如果调用已有的层实例,那么就会重复使用它的权重
right_output = lstm(right_input)

merged = layers.concatenate([left_output, right_output], axis=-1)	# 在上面构建一个分类器
predictions = layers.Dense(1, activation='sigmoid')(merged)

model = Model([left_input, right_input], predictions)	# 将模型实例化并训练:训练这种模型时,基于两个输入对LSTM层的权重进行更新
model.fit([left_data, right_data], targets)

自然地,一个层实例可能被多次重复使用,它可以被调用任意多次,每次都重复使用一组相同的权重。

Keys: 将模型作为层

重要的是,在函数式 API 中,可以像使用层一样使用模型。实际上,你可以将模型看作“更大的层”。Sequential 类和 Model 类都是如此。这意味着你可以在一个输入张量上调用模型, 并得到一个输出张量。

y = model(x)

如果模型具有多个输入张量和多个输出张量,那么应该用张量列表来调用模型。

y1, y2 = model([x1, x2])

在调用模型实例时,就是在重复使用模型的权重,正如在调用层实例时,就是在重复使用层的权重。调用一个实例,无论是层实例还是模型实例,都会重复使用这个实例已经学到的表示, 这很直观。

通过重复使用模型实例可以构建一个简单的例子,就是一个使用双摄像头作为输入的视觉模型:两个平行的摄像头,相距几厘米(一英寸)。这样的模型可以感知深度,这在很多应用中都很有用。你不需要两个单独的模型从左右两个摄像头中分别提取视觉特征,然后再将二者合并。 这样的底层处理可以在两个输入之间共享,即通过共享层(使用相同的权重,从而共享相同的 表示)来实现。在 Keras 中实现连体视觉模型(共享卷积基)的代码如下所示。

from keras import layers
from keras import applications
from keras import Input

xception_base = applications.Xception(weights=None, include_top=False)	# 图像处理基础模型是Xception 网络(只包括卷积基)

# 输入是 250×250 的 RGB 图像
left_input = Input(shape=(250, 250, 3))
right_input = Input(shape=(250, 250, 3))

# 对相同的视觉模型调用两次
left_features = xception_base(left_input)
right_features = xception_base(right_input)

# 合并后的特征包含来自左右两个视觉输入中的信息
merged_features = layers.concatenate([left_features, right_features], axis=-1)

6.2 回调函数

当观测到验证损失不再改善时就停止训练。这可以使用 Keras 回调函数来实现。

回调函数(callback)是在调用 fit 时传入模型的一个对象(即实现特定方法的类实例),它在训练过程中的不同时间点都会被模型调用。它可以访问关于模型状态与性能的所有可用数据,还可以采取行动:中断训练、保存模型、加载一组不同的权重或改变模型的状态。

Keys: ModelCheckpoint 与 EarlyStopping

如果监控的目标指标在设定的轮数内不再改善,可以用 EarlyStopping 回调函数来中断训练。比如,这个回调函数可以在刚开始过拟合的时候就中断训练,从而避免用更少的轮次重新训练模型。这个回调函数通常与 ModelCheckpoint 结合使用,后者可以在训练过程中持续不断地保存模型(你也可以选择只保存目前的最佳模型,即一轮结束后具有最佳性能的模型)。

import keras

# 通过 fit 的 callbacks 参数将回调函数传入模型中,这个参数接收一个回调函数的列表。你可以传入任意个数的回调函数
callbacks_list = [
    keras.callbacks.EarlyStopping(	# 如果不再改善,就中断训练
    monitor='acc',	# 监控模型的验证精度
    patience=1,		# 如果精度在多于一轮的时间(即两轮)内不再改善,中断训练
    ),
    keras.callbacks.ModelCheckpoint(	# 在每轮过后保存当前权重
    filepath='my_model.h5',	# 目标模型文件的保存路径
    monitor='val_loss',		# 这两个参数的含义是,如果 val_loss 没有改善,那么不需要覆盖模型文件。这就可以始终保存在训练过程中见到的最佳模型
    save_best_only=True,
    )
]

model.compile(optimizer='rmsprop',
             loss='binary_crossentropy',
             metrics=['acc'])	# 你监控精度,所以它应该是模型指标的一部分

# 注意,由于回调函数要监控验证损失和验证精度,所以在调用 fit 时需要传入 validation_data(验证数据)
model.fit(x, y,
         epochs=10,
         batch_size=32,
         callbacks=callbacks_list,
         validation_data=(x_val, y_val))
Keys: ReduceLROnPlateau

如果验证损失不再改善,你可以使用这个回调函数来降低学习率。在训练过程中如果出现 了损失平台(loss plateau),那么增大或减小学习率都是跳出局部最小值的有效策略。下面这个示例使用了 ReduceLROnPlateau 回调函数。

callbacks_list = [
	keras.callbacks.ReduceLROnPlateau(
		monitor='val_loss'	# 监控模型的验证损失
		factor=0.1,			# 触发时将学习率除以 10
		patience=10,		# 如果验证损失在 10 轮内都没有改善,那么就触发这个回调函数
	)	
]

# 注意,因为回调函数要监控验证损失,所以你需要在调用 fit 时传入 validation_data(验证数据)
model.fit(x, y,
          epochs=10,
          batch_size=32,
          callbacks=callbacks_list,
          validation_data=(x_val, y_val)) 
Keys: 自定义回调函数

如果你需要在训练过程中采取特定行动,而这项行动又没有包含在内置回调函数中,那么 可以编写你自己的回调函数。回调函数的实现方式是创建 keras.callbacks.Callback 类的子类。然后你可以实现下面这些方法(从名称中即可看出这些方法的作用),它们分别在训练过程中的不同时间点被调用。

on_epoch_begin	# 在每轮开始时被调用
on_epoch_end	# 在每轮结束时被调用

on_batch_begin	# 在处理每个批量之前被调用
on_batch_end	# 在处理每个批量之后被调用

on_train_begin	# 在训练开始时被调用
on_train_end 	# 在训练结束时被调用

这些方法被调用时都有一个 logs 参数,这个参数是一个字典,里面包含前一个批量、前 一个轮次或前一次训练的信息,即训练指标和验证指标等。此外,回调函数还可以访问下列属性。

self.model:调用回调函数的模型实例。

self.validation_data:传入 fit 作为验证数据的值。

下面是一个自定义回调函数的简单示例,它可以在每轮结束后将模型每层的激活保存到硬盘(格式为 Numpy 数组),这个激活是对验证集的第一个样本计算得到的。

import keras
import numpy as np

class ActivationLogger(keras.callbacks.Callback):
     def set_model(self, model):
         self.model = model	# 在训练之前由父模型调用,告诉回调函数是哪个模型在调用它
         layer_outputs = [layer.output for layer in model.layers]
         self.activations_model = keras.models.Model(model.input,	# 模型实例,返回每层的激活
         											layer_outputs)
        
     def on_epoch_end(self, epoch, logs=None):
         if self.validation_data is None:
         	raise RuntimeError('Requires validation_data.')
         validation_sample = self.validation_data[0][0:1]	# 获取验证数据的第一个输入样本
         activations = self.activations_model.predict(validation_sample)	
         f = open('activations_at_epoch_' + str(epoch) + '.npz', 'w')	# 将数组保存到硬盘
         np.savez(f, activations)
         f.close()

关于回调函数你只需要知道这么多,其他的都是技术细节,很容易就能查到。现在,你已 经可以在训练过程中对一个 Keras 模型执行任何类型的日志记录或预定程序的干预。

6.3 TensorBoard

TensorBoard,一个内置于 TensorFlow 中的基于浏览器的可视化工具。TensorBoard 的主要用途是,在训练过程中帮助你以可视化的方法监控模型内部发生的一切。

  • 在训练过程中以可视化的方式监控指标
  • 将模型架构可视化
  • 将激活和梯度的直方图可视化
  • 以三维的形式研究嵌入

6.4 让模型性能发挥到极致

Keys: BatchNormalization

标准化(normalization)是一大类方法,用于让机器学习模型看到的不同样本彼此之间更加相似,这有助于模型的学习与对新数据的泛化。最常见的数据标准化形式就是你已经在本书中多次见到的那种形式:将数据减去其平均值使其中心为 0,然后将数据除以其标准差使其标准差为 1。实际上,这种做法假设数据服从正态分布(也叫高斯分布),并确保让该分布的中心为 0, 同时缩放到方差为 1。

normalized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)

前面的示例都是在将数据输入模型之前对数据做标准化。但在网络的每一次变换之后都应该考虑数据标准化。即使输入 Dense 或 Conv2D 网络的数据均值为 0、方差为 1,也没有理由假定网络输出的数据也是这样。

批标准化的工作原理是,训练过程中在内部保存已读取每批数据均值和方差的指数移动平均值。批标准化的主要效果是,它有助于梯度传播(这一点和残差连接很像),因此允许更深的网络。对于有些特别深的网络,只有包含多个 BatchNormalization 层时才能进行训练。例如,BatchNormalization 广泛用于 Keras 内置的许多高级卷积神经网络 架构,比如 ResNet50、Inception V3 和 Xception。

BatchNormalization 层通常在卷积层或密集连接层之后使用。

conv_model.add(layers.Conv2D(32, 3, activation='relu'))
conv_model.add(layers.BatchNormalization())		# 在卷积层之后使用

dense_model.add(layers.Dense(32, activation='relu'))
dense_model.add(layers.BatchNormalization())	# 在 Dense 层之后使用

BatchNormalization 层接收一个 axis 参数,它指定应该对哪个特征轴做标准化。这 个参数的默认值是 -1,即输入张量的最后一个轴。对于 Dense 层、Conv1D 层、RNN 层和将 data_format 设为 “channels_last”(通道在后)的 Conv2D 层,这个默认值都是正确的。 但有少数人使用将 data_format 设为 “channels_first”(通道在前)的 Conv2D 层,这时 特征轴是编号为 1 的轴,因此 BatchNormalization 的 axis 参数应该相应地设为 1。

Keys: 深度可分离卷积

深度可分离卷积层(SeparableConv2D)对输入的每个通道分别执行空间卷积,然后通过逐点卷积(1×1 卷积)将输出通道混合,如图 7-16 所示。这相当于将空间特征学习和通道特征学习分开,如果你假设输入中的空间位置高度相关,但不同的通道之间相对独立,那么这么做是很有意义的。它需要的参数要少很多,计算量也更小,因此可以得到更小、更快的模型。因为它是一种执行卷积更高效的方法, 所以往往能够使用更少的数据学到更好的表示,从而得到性能更好的模型。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NMEJDPWb-1672461065810)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221230224048181.png)]

如果只用有限的数据从头开始训练小型模型,这些优点就变得尤为重要。例如,下面这个示例是在小型数据集上构建一个轻量的深度可分离卷积神经网络,用于图像分类任务(softmax 多分类)。

from keras.models import Sequential, Model
from keras import layers

height = 64
width = 64
channels = 3
num_classes = 10

model = Sequential()
model.add(layers.SeparableConv2D(32, 3,
                                 activation='relu',
                                 input_shape=(height, width, channels,)))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.GlobalAveragePooling2D())

model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

对于规模更大的模型,深度可分离卷积是 Xception 架构的基础,Xception 是一个高性能的 卷积神经网络,内置于 Keras 中。在我的论文“Xception: deep learning with depthwise separable convolutions”中,你可以进一步了解深度可分离卷积和 Xception 的理论基础。

Keys: 超参数优化

构建深度学习模型时,你必须做出许多看似随意的决定:应该堆叠多少层?每层应该 包含多少个单元或过滤器?激活应该使用 relu 还是其他函数?在某一层之后是否应该使用 BatchNormalization ?应该使用多大的 dropout 比率?还有很多。这些在架构层面的参数叫作超参数(hyperparameter),以便将其与模型参数区分开来,后者通过反向传播进行训练。

你可以手动调节你的选择、重新训练模型,如此不停重复来改进你的选择,这也是机器学习工程师和研究人员大部分时间都在做的事情。但是,整天调节超参数不应该是人类的工作,最好留给机器去做。

因此,你需要制定一个原则,系统性地自动探索可能的决策空间。你需要搜索架构空间, 并根据经验找到性能最佳的架构。这正是超参数自动优化领域的内容。这个领域是一个完整的研究领域,而且很重要。AutoMachineLearning

超参数优化的过程通常如下所示。

  1. 选择一组超参数(自动选择)。
  2. 构建相应的模型。
  3. 将模型在训练数据上拟合,并衡量其在验证数据上的最终性能。
  4. 选择要尝试的下一组超参数(自动选择)。
  5. 重复上述过程。
  6. 最后,衡量模型在测试数据上的性能。

这个过程的关键在于,给定许多组超参数,使用验证性能的历史来选择下一组需要评估的超参数的算法。有多种不同的技术可供选择:贝叶斯优化、遗传算法、简单随机搜索等。

训练模型权重相对简单:在小批量数据上计算损失函数,然后用反向传播算法让权重向正确的方向移动。与此相反,更新超参数则非常具有挑战性。我们来考虑以下两点。

计算反馈信号(这组超参数在这个任务上是否得到了一个高性能的模型)的计算代价可能非常高,它需要在数据集上创建一个新模型并从头开始训练。

超参数空间通常由许多离散的决定组成,因而既不是连续的,也不是可微的。因此,你通常不能在超参数空间中做梯度下降。相反,你必须依赖不使用梯度的优化方法,而这些方法的效率比梯度下降要低很多。

这些挑战非常困难,而这个领域还很年轻,因此我们目前只能使用非常有限的工具来优化模型。通常情况下,随机搜索(随机选择需要评估的超参数,并重复这一过程)就是最好的解决方案,虽然这也是最简单的解决方案。但我发现有一种工具确实比随机搜索更好,它就是 Hyperopt。它是一个用于超参数优化的 Python 库,其内部使用 Parzen 估计器的树来预测哪组超参数可能会得到好的结果。另一个叫作 Hyperas 的库将 Hyperopt 与 Keras 模型集成在一起。一定要试试。

在进行大规模超参数自动优化时,有一个重要的问题需要牢记,那就是验证集过拟合。 因为你是使用验证数据计算出一个信号,然后根据这个信号更新超参数,所以你实际上是在验证数据上训练超参数,很快会对验证数据过拟合。请始终记住这一点。

总之,超参数优化是一项强大的技术,想要在任何任务上获得最先进的模型或者赢得机器学习竞赛,这项技术都必不可少。思考一下:曾经人们手动设计特征,然后输入到浅层机器学习模型中,这肯定不是最优的。现在,深度学习能够自动完成分层特征工程的任务,这些特征都是利用反馈信号学到的,而不是手动调节的,事情本来就应该如此。同样,你也不应该手动设计模型架构,而是应该按照某种原则对其进行最优化。在写作本书时,超参数自动优化还是一个非常年轻且不成熟的领域,正如几年前的深度学习,但我预计这一领域会在未来数年内蓬勃发展。

Keys: 模型集成

想要在一项任务上获得最佳结果,另一种强大的技术是模型集成(model ensembling)。集成是指将一系列不同模型的预测结果汇集到一起,从而得到更好的预测结果。观察机器学习竞赛, 特别是 Kaggle 上的竞赛,你会发现优胜者都是将很多模型集成到一起,它必然可以打败任何单个模型,无论这个模型的表现多么好。

集成依赖于这样的假设,即对于独立训练的不同良好模型,它们表现良好可能是因为不同的原因:每个模型都从略有不同的角度观察数据来做出预测,得到了“真相”的一部分,但不是全部真相。你可能听说过盲人摸象的古代寓言:一群盲人第一次遇到大象,想要通过触摸来 解大象。每个人都摸到了大象身体的不同部位,但只摸到了一部分,比如鼻子或一条腿。这些人描述的大象是这样的,“它像一条蛇” “像一根柱子或一棵树”,等等。这些盲人就好比机器学习模型,每个人都试图根据自己的假设(这些假设就是模型的独特架构和独特的随机权重初始化)并从自己的角度来理解训练数据的多面性。每个人都得到了数据真相的一部分,但不是 全部真相。将他们的观点汇集在一起,你可以得到对数据更加准确的描述。大象是多个部分的组合,每个盲人说的都不完全准确,但综合起来就成了一个相当准确的故事。

我们以分类问题为例。想要将一组分类器的预测结果汇集在一起[即分类器集成(ensemble the classifiers)],最简单的方法就是将它们的预测结果取平均值作为预测结果。

# 使用 4 个不同的模型来计算初始预测
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

# 这个新的预测数组应该比任何一个初始预测都更加准确
final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_d)

只有这组分类器中每一个的性能差不多一样好时,这种方法才奏效。如果其中一个分类器 性能比其他的差很多,那么最终预测结果可能不如这一组中的最佳分类器那么好。

将分类器集成有一个更聪明的做法,即加权平均,其权重在验证数据上学习得到。通常来说,更好的分类器被赋予更大的权重,而较差的分类器则被赋予较小的权重。为了找到一组好的集成权重,你可以使用随机搜索或简单的优化算法(比如 Nelder-Mead 方法)。

preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

# 假设 (0.5, 0.25, 0.1, 0.15)这些权重是根据经验学到的
final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d

还有许多其他变体,比如你可以对预测结果先取指数再做平均。一般来说,简单的加权平均, 其权重在验证数据上进行最优化,这是一个很强大的基准方法。

想要保证集成方法有效,关键在于这组分类器的多样性(diversity)。多样性就是力量。如 果所有盲人都只摸到大象的鼻子,那么他们会一致认为大象像蛇,并且永远不会知道大象的真 实模样。是多样性让集成方法能够取得良好效果。用机器学习的术语来说,如果所有模型的偏 差都在同一个方向上,那么集成也会保留同样的偏差。如果各个模型的偏差在不同方向上,那 么这些偏差会彼此抵消,集成结果会更加稳定、更加准确。

因此,集成的模型应该尽可能好,同时尽可能不同。这通常意味着使用非常不同的架构, 甚至使用不同类型的机器学习方法。有一件事情基本上是不值得做的,就是对相同的网络,使用不同的随机初始化多次独立训练,然后集成。如果模型之间的唯一区别是随机初始化和训练数据的读取顺序,那么集成的多样性很小,与单一模型相比只会有微小的改进。

我发现有一种方法在实践中非常有效(但这一方法还没有推广到所有问题领域),就是将基于树的方法(比如随机森林或梯度提升树)和深度神经网络进行集成。2014 年,合作者 Andrei Kolev 和我使用多种树模型和深度神经网络的集成,在 Kaggle 希格斯玻色子衰变探测挑战赛中获得第四名。值得一提的是,集成中的某一个模型来源于与其他模型都不相同的方法(它是正则化的贪婪森林),并且得分也远远低于其他模型。不出所料,它在集成中被赋予了一个很小的权重。但出乎我们的意料,它极大地改进了总体的集成结果,因为它和其他所有模型都完全不同, 提供了其他模型都无法获得的信息。这正是集成方法的关键之处。集成不在于你的最佳模型有多好,而在于候选模型集合的多样性。

7 生成式深度学习

机器学习模型能够对图像、 音乐和故事的统计潜在空间(latent space)进行学习,然后从这个空间中采样(sample),创造 出与模型在训练数据中所见到的艺术作品具有相似特征的新作品。

7.1 使用LSTM生成文本

用深度学习生成序列数据的通用方法,就是使用前面的标记作为输入,训练一个网络(通 常是循环神经网络或卷积神经网络)来预测序列中接下来的一个或多个标记。例如,给定输入 the cat is on the ma,训练网络来预测目标 t,即下一个字符。与前面处理文本数据时一样,标记 (token)通常是单词或字符,给定前面的标记,能够对下一个标记的概率进行建模的任何网络 都叫作语言模型(language model)。语言模型能够捕捉到语言的潜在空间(latent space),即语 言的统计结构。

一旦训练好了这样一个语言模型,就可以从中采样(sample,即生成新序列)。向模型中输 入一个初始文本字符串[即条件数据(conditioning data)],要求模型生成下一个字符或下一个 单词(甚至可以同时生成多个标记),然后将生成的输出添加到输入数据中,并多次重复这一过 程(见图 8-1)。这个循环可以生成任意长度的序列,这些序列反映了模型训练数据的结构,它 们与人类书写的句子几乎相同。在本节的示例中,我们将会用到一个 LSTM 层,向其输入从文 本语料中提取的 N 个字符组成的字符串,然后训练模型来生成第 N+1 个字符。模型的输出是对 所有可能的字符做 softmax,得到下一个字符的概率分布。这个 LSTM 叫作字符级的神经语言模 型(character-level neural language model)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7TGvaFE7-1672461065810)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221231104758291.png)]

采样策略:生成文本时,如何选择下一个字符至关重要。一种简单的方法是贪婪采样(greedy sampling), 就是始终选择可能性最大的下一个字符。但这种方法会得到重复的、可预测的字符串,看起来 不像是连贯的语言。一种更有趣的方法是做出稍显意外的选择:在采样过程中引入随机性,即 从下一个字符的概率分布中进行采样。这叫作随机采样(stochastic sampling,stochasticity 在这 个领域中就是“随机”的意思)。在这种情况下,根据模型结果,如果下一个字符是 e 的概率为 0.3,那么你会有 30% 的概率选择它。注意,贪婪采样也可以被看作从一个概率分布中进行采样, 即某个字符的概率为 1,其他所有字符的概率都是 0。

为了在采样过程中控制随机性的大小,我们引入一个叫作 softmax 温度(softmax temperature) 的参数,用于表示采样概率分布的熵,即表示所选择的下一个字符会有多么出人意料或多么可 预测。给定一个 temperature 值,将按照下列方法对原始概率分布(即模型的 softmax 输出)进行重新加权,计算得到一个新的概率分布。

构建模型

from keras import layers

model = keras.models.Sequential()
model.add(layers.LSTM(128, input_shape=(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation='softmax'))

optimizer = keras.optimizers.RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)

训练语言模型并从中采样

给定一个训练好的模型和一个种子文本片段,我们可以通过重复以下操作来生成新的文本。

  1. 给定目前已生成的文本,从模型中得到下一个字符的概率分布。
  2. 根据某个温度对分布进行重新加权。
  3. 根据重新加权后的分布对下一个字符进行随机采样。
  4. 将新字符添加到文本末尾。

下列代码将对模型得到的原始概率分布进行重新加权,并从中抽取一个字符索引[采样函 数(sampling function)]。

def sample(preds, temperature=1.0):
     preds = np.asarray(preds).astype('float64')
     preds = np.log(preds) / temperature
     exp_preds = np.exp(preds)
     preds = exp_preds / np.sum(exp_preds)
     probas = np.random.multinomial(1, preds, 1)
     return np.argmax(probas)

最后,下面这个循环将反复训练并生成文本。在每轮过后都使用一系列不同的温度值来生成 文本。这样我们可以看到,随着模型收敛,生成的文本如何变化,以及温度对采样策略的影响。

import random
import sys

for epoch in range(1, 60):
    print('epoch', epoch)
    # Fit the model for 1 epoch on the available training data
    model.fit(x, y,
              batch_size=128,
              epochs=1)

    # Select a text seed at random
    start_index = random.randint(0, len(text) - maxlen - 1)
    generated_text = text[start_index: start_index + maxlen]
    print('--- Generating with seed: "' + generated_text + '"')

    for temperature in [0.2, 0.5, 1.0, 1.2]:
        print('------ temperature:', temperature)
        sys.stdout.write(generated_text)

        # We generate 400 characters
        for i in range(400):
            sampled = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(generated_text):
                sampled[0, t, char_indices[char]] = 1.

            preds = model.predict(sampled, verbose=0)[0]
            next_index = sample(preds, temperature)
            next_char = chars[next_index]

            generated_text += next_char
            generated_text = generated_text[1:]

            sys.stdout.write(next_char)
            sys.stdout.flush()
        print()

7.2 DeepDream

DeepDream 算法与第 5 章介绍的卷积神经网络过滤器可视化技术几乎相同,都是反向运行 一个卷积神经网络:对卷积神经网络的输入做梯度上升,以便将卷积神经网络靠顶部的某一层 的某个过滤器激活最大化。DeepDream 使用了相同的想法,但有以下这几个简单的区别。

使用 DeepDream,我们尝试将所有层的激活最大化,而不是将某一层的激活最大化,因此需要同时将大量特征的可视化混合在一起。

不是从空白的、略微带有噪声的输入开始,而是从现有的图像开始,因此所产生的效果能够抓住已经存在的视觉模式,并以某种艺术性的方式将图像元素扭曲。

输入图像是在不同的尺度上[叫作八度(octave)]进行处理的,这可以提高可视化的质量。

7.3 神经风格迁移

除 DeepDream 之外,深度学习驱动图像修改的另一项重大进展是神经风格迁移(neural style transfer),它由 Leon Gatys 等人于 2015 年夏天提出。自首次提出以来,神经风格迁移算法已经做了许多改进,并衍生出许多变体,而且还成功转化成许多智能手机图片应用。为了简单起见,本节将重点介绍原始论文中描述的方法。 神经风格迁移是指将参考图像的风格应用于目标图像,同时保留目标图像的内容。图 8-7 给出了一个示例。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v2Z2Eswt-1672461065811)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228123807767.png)]

在当前语境下,风格(style)是指图像中不同空间尺度的纹理、颜色和视觉图案,内容 (content)是指图像的高级宏观结构。举个例子,在图 8-7 中(用到的参考图像是文森特 • 梵高 的《星夜》),蓝黄色圆形笔划被看作风格,而 Tübingen(图宾根)照片中的建筑则被看作内容。

风格迁移这一想法与纹理生成的想法密切相关,在 2015 年开发出神经风格迁移之前,这一 想法就已经在图像处理领域有着悠久的历史。但事实证明,与之前经典的计算机视觉技术实现相比,基于深度学习的风格迁移实现得到的结果是无与伦比的,并且还在计算机视觉的创造性应用中引发了惊人的复兴。

实现风格迁移背后的关键概念与所有深度学习算法的核心思想是一样的:定义一个损失函数来指定想要实现的目标,然后将这个损失最小化。你知道想要实现的目标是什么,就是保存原始图像的内容,同时采用参考图像的风格。如果我们能够在数学上给出内容和风格的定义, 那么就有一个适当的损失函数(如下所示),我们将对其进行最小化。

loss = distance(style(reference_image) - style(generated_image)) +
 	   distance(content(original_image) - content(generated_image))

这里的 distance 是一个范数函数,比如 L2 范数;content 是一个函数,输入一张图像,并计算出其内容的表示;style 是一个函数,输入一张图像,并计算出其风格的表示。将 这个损失最小化,会使得 style(generated_image) 接近于 style(reference_image)、 content(generated_image) 接近于 content(generated_image),从而实现我们定义的风格迁移。

Gatys 等人发现了一个很重要的观察结果,就是深度卷积神经网络能够从数学上定义 style 和 content 两个函数。

7.4 VAE生成图像

从图像的潜在空间中采样,并创建全新图像或编辑现有图像,这是目前最流行也是最成 功的创造性人工智能应用。在本节和下一节中,我们将会介绍一些与图像生成有关的高级概 念,还会介绍该领域中两种主要技术的实现细节,这两种技术分别是变分自编码器(VAE, variational autoencoder)生成式对抗网络(GAN,generative adversarial network)。我们这里介绍的技术不仅适用于图像,使用 GAN 和 VAE 还可以探索声音、音乐甚至文本的潜在空间,但 在实践中,最有趣的结果都是利用图像获得的,这也是我们这里介绍的重点。

7.4.1 从图像的潜在空间中采样

图像生成的关键思想就是找到一个低维的表示潜在空间(latent space,也是一个向量空间), 其中任意点都可以被映射为一张逼真的图像。能够实现这种映射的模块,即以潜在点作为输入并输出一张图像(像素网格),叫作生成器generator,对于 GAN 而言)或解码器decoder, 对于 VAE 而言)。一旦找到了这样的潜在空间,就可以从中有意地或随机地对点进行采样,并 将其映射到图像空间,从而生成前所未见的图像(见图 8-9)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1qqLYsc-1672461065811)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228144242407.png)]

想要学习图像表示的这种潜在空间,GAN 和 VAE 是两种不同的策略,每种策略都有各自的特点。VAE 非常适合用于学习具有良好结构的潜在空间,其中特定方向表示数据中有意义的变化轴(见图 8-10)。GAN 生成的图像可能非常逼真,但它的潜在空间可能没有良好结构,也没有足够的连续性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9t3XIcNX-1672461065811)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228144500042.png)]

7.4.2 图像编辑的概念向量

第 6 章介绍词嵌入时,我们已经暗示了概念向量(concept vector)的想法:给定一个表示的潜在空间或一个嵌入空间,空间中的特定方向可能表示原始数据中有趣的变化轴。比如在人 脸图像的潜在空间中,可能存在一个微笑向量(smile vector)s,它满足:如果潜在点 z 是某张 人脸的嵌入表示,那么潜在点 z+s 就是同一张人脸面带微笑的嵌入表示。一旦找到了这样的向量, 就可以用这种方法来编辑图像:将图像投射到潜在空间中,用一种有意义的方式来移动其表示, 然后再将其解码到图像空间。在图像空间中任意独立的变化维度都有概念向量,对于人脸而言, 你可能会发现向人脸添加墨镜的向量、去掉墨镜的向量。将男性面孔变成女性面孔的向量等。 图 8-11 是一个微笑向量的例子,它是由新西兰维多利亚大学设计学院的 Tom White 发现的概念 向量,使用的是在名人人脸数据集(CelebA 数据集)上训练的 VAE。

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wYFzKHI6-1672461065811)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228145641733.png)]

7.4.3 变分自编码器

自编码器由 Kingma 和 Welling 于 2013 年 12 月 a 与 Rezende、Mohamed 和 Wierstra 于 2014 年 1 月 b 同时发现,它是一种生成式模型,特别适用于利用概念向量进行图像编辑的任务。它是 一种现代化的自编码器,将深度学习的想法与贝叶斯推断结合在一起。自编码器是一种网络类型, 其目的是将输入编码到低维潜在空间,然后再解码回来。

经典的图像自编码器接收一张图像,通过一个编码器模块将其映射到潜在向量空间,然后再通过一个解码器模块将其解码为与原始图像具有相同尺寸的输出(见图 8-12)。然后,使用与输入图像相同的图像作为目标数据来训练这个自编码器,也就是说,自编码器学习对原始输入 进行重新构建。通过对代码(编码器的输出)施加各种限制,我们可以让自编码器学到比较有趣的数据潜在表示。最常见的情况是将代码限制为低维的并且是稀疏的(即大部分元素为 0), 在这种情况下,编码器的作用是将输入数据压缩为更少二进制位的信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZR3oaa9b-1672461065812)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228150007464.png)]

在实践中,这种经典的自编码器不会得到特别有用或具有良好结构的潜在空间。它们也没有对数据做多少压缩。因此,它们已经基本上过时了。但是,VAE 向自编码器添加了一点统计魔法,迫使其学习连续的、高度结构化的潜在空间。这使得 VAE 已成为图像生成的强大工具。

VAE 不是将输入图像压缩成潜在空间中的固定编码,而是将图像转换为统计分布的参数, 即平均值和方差。本质上来说,这意味着我们假设输入图像是由统计过程生成的,在编码和解码过程中应该考虑这一过程的随机性。然后,VAE 使用平均值和方差这两个参数来从分布中随机采样一个元素,并将这个元素解码到原始输入(见图 8-13)。这个过程的随机性提高了其稳健性,并迫使潜在空间的任何位置都对应有意义的表示,即潜在空间采样的每个点都能解码为有效的输出。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXqg088g-1672461065812)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228150250524.png)]

从技术角度来说,VAE 的工作原理如下。

  1. 一个编码器模块将输入样本 input_img 转换为表示潜在空间中的两个参数 z_mean 和 z_log_variance。
  2. 我们假定潜在正态分布能够生成输入图像,并从这个分布中随机采样一个点 z:z = z_mean + exp(z_log_variance) * epsilon,其中 epsilon 是取值很小的随机 张量。
  3. 一个解码器模块将潜在空间的这个点映射回原始输入图像。

因为 epsilon 是随机的,所以这个过程可以确保,与 input_img 编码的潜在位置(即 z-mean)靠近的每个点都能被解码为与 input_img 类似的图像,从而迫使潜在空间能够连续地有意义。潜在空间中任意两个相邻的点都会被解码为高度相似的图像。连续性以及潜在空间 的低维度,将迫使潜在空间中的每个方向都表示数据中一个有意义的变化轴,这使得潜在空间具有非常良好的结构,因此非常适合通过概念向量来进行操作。

VAE 的参数通过两个损失函数来进行训练:一个是重构损失(reconstruction loss),它迫使解码后的样本匹配初始输入;另一个是正则化损失(regularization loss),它有助于学习具有良好结构的潜在空间,并可以降低在训练数据上的过拟合。我们来快速浏览一下 Keras 实现的 VAE。其大致代码如下所示。

z_mean, z_log_variance = encoder(input_img)	# 将输入编码为平均值和方差两个参数
z = z_mean + exp(z_log_variance) * epsilon	# 使用小随机数 epsilon 来抽取一个潜在点
reconstructed_img = decoder(z)				# 将 z 解码为一张图像
model = Model(input_img, reconstructed_img)	# 将自编码器模型实例化,它将一张输入图像映射为它的重构

然后,你可以使用重构损失和正则化损失来训练模型。下列代码给出了我们将使用的编码器网络,它将图像映射为潜在空间中概率分布的参数。 它是一个简单的卷积神经网络,将输入图像 x 映射为两个向量 z_mean 和 z_log_var。

VAE 编码器网络

import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np

img_shape = (28, 28, 1)
batch_size = 16
latent_dim = 2  # 潜在空间的维度:一个二维平面

input_img = keras.Input(shape=img_shape)

x = layers.Conv2D(32, 3,
                  padding='same', activation='relu')(input_img)
x = layers.Conv2D(64, 3,
                  padding='same', activation='relu',
                  strides=(2, 2))(x)
x = layers.Conv2D(64, 3,
                  padding='same', activation='relu')(x)
x = layers.Conv2D(64, 3,
                  padding='same', activation='relu')(x)
shape_before_flattening = K.int_shape(x)

x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)

# 输入图像最终被编码为这两个参数
z_mean = layers.Dense(latent_dim)(x)
z_log_var = layers.Dense(latent_dim)(x)

接下来的代码将使用 z_mean 和 z_log_var 来生成一个潜在空间点 z,z_mean 和 z_log_ var 是统计分布的参数,我们假设这个分布能够生成 input_img。这里,我们将一些随意的代 码(这些代码构建于 Keras 后端之上)包装到 Lambda 层中。在 Keras 中,任何对象都应该是一 个层,所以如果代码不是内置层的一部分,我们应该将其包装到一个 Lambda 层(或自定义层)中。

潜在空间采样的函数

def sampling(args):
    z_mean, z_log_var = args
    epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim),
                              mean=0., stddev=1.)
    return z_mean + K.exp(z_log_var) * epsilon

z = layers.Lambda(sampling)([z_mean, z_log_var])

下列代码给出了解码器的实现。我们将向量 z 的尺寸调整为图像大小,然后使用几个卷积层来得到最终的图像输出,它和原始图像 input_img 具有相同的大小。

VAE 解码器网络,将潜在空间点映射为图像

# 需要将 z 输入到这里
decoder_input = layers.Input(K.int_shape(z)[1:])

# 对输入进行上采样
x = layers.Dense(np.prod(shape_before_flattening[1:]),
                 activation='relu')(decoder_input)

# 将 z 转换为特征图,使其形状与编码器模型最后一个 Flatten 层之前的特征图的形状相同
x = layers.Reshape(shape_before_flattening[1:])(x)

# 使用一个 Conv2DTranspose 层和一个 Conv2D 层,将 z 解码为与原始输入图像具有相同尺寸的特征图
x = layers.Conv2DTranspose(32, 3,
                           padding='same', activation='relu',
                           strides=(2, 2))(x)
x = layers.Conv2D(1, 3,
                  padding='same', activation='sigmoid')(x)
# We end up with a feature map of the same size as the original input.

# 将解码器模型实例化,它将 decoder_input转换为解码后的图像
decoder = Model(decoder_input, x)

# 将这个实例应用于 z,以得到解码后的 z
z_decoded = decoder(z)

我们一般认为采样函数的形式为 loss(input, target),VAE 的双重损失不符合这种形 式。因此,损失的设置方法为:编写一个自定义层,并在其内部使用内置的 add_loss 层方法 来创建一个你想要的损失。

用于计算 VAE 损失的自定义层

class CustomVariationalLayer(keras.layers.Layer):

    def vae_loss(self, x, z_decoded):
        x = K.flatten(x)
        z_decoded = K.flatten(z_decoded)
        xent_loss = keras.metrics.binary_crossentropy(x, z_decoded)
        kl_loss = -5e-4 * K.mean(
            1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
        return K.mean(xent_loss + kl_loss)

    def call(self, inputs):		# 通过编写一个 call 方法来实现自定义层
        x = inputs[0]
        z_decoded = inputs[1]
        loss = self.vae_loss(x, z_decoded)
        self.add_loss(loss, inputs=inputs)
        # We don't use this output.
        return x	# 我们不使用这个输出,但层必须要有返回值

# 对输入和解码后的输出调用自定义层,以得到最终的模型输出
y = CustomVariationalLayer()([input_img, z_decoded])

最后,将模型实例化并开始训练。因为损失包含在自定义层中,所以在编译时无须指定外部损失(即 loss=None),这意味着在训练过程中不需要传入目标数据。(如你所见,我们在调 用 fit 时只向模型传入了 x_train。)

训练 VAE

from keras.datasets import mnist

vae = Model(input_img, y)
vae.compile(optimizer='rmsprop', loss=None)
vae.summary()

# Train the VAE on MNIST digits
(x_train, _), (x_test, y_test) = mnist.load_data()

x_train = x_train.astype('float32') / 255.
x_train = x_train.reshape(x_train.shape + (1,))
x_test = x_test.astype('float32') / 255.
x_test = x_test.reshape(x_test.shape + (1,))

vae.fit(x=x_train, y=None,
        shuffle=True,
        epochs=10,
        batch_size=batch_size,
        validation_data=(x_test, None))

一旦训练好了这样的模型(本例中是在 MNIST 上训练),我们就可以使用 decoder 网络将任意潜在空间向量转换为图像。

从二维潜在空间中采样一组点的网格,并将其解码为图像

import matplotlib.pyplot as plt
from scipy.stats import norm

# 我们将显示 15×15 的数字网格(共 255 个数字)
n = 15  
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
# 使用 SciPy 的 ppf 函数对线性分隔的坐标进行变换,以生成潜在变量 z 的值(因为潜在空间的先验分布是高斯分布)
grid_x = norm.ppf(np.linspace(0.05, 0.95, n))
grid_y = norm.ppf(np.linspace(0.05, 0.95, n))

for i, yi in enumerate(grid_x):
    for j, xi in enumerate(grid_y):
        z_sample = np.array([[xi, yi]])
        z_sample = np.tile(z_sample, batch_size).reshape(batch_size, 2)	# 将 z 多次重复,以构建一个完整的批量
        x_decoded = decoder.predict(z_sample, batch_size=batch_size)	# 将批量解码为数字图像
        digit = x_decoded[0].reshape(digit_size, digit_size)			# 将批量第一个数字的形状从 28×28×1 转变为 28×28
        figure[i * digit_size: (i + 1) * digit_size,
               j * digit_size: (j + 1) * digit_size] = digit

plt.figure(figsize=(10, 10))
plt.imshow(figure, cmap='Greys_r')
plt.show()

采样数字的网格(见图 8-14)展示了不同数字类别的完全连续分布:当你沿着潜在空间的一条路径观察时,你会观察到一个数字逐渐变形为另一个数字。这个空间的特定方向具有一定 的意义,比如,有一个方向表示“逐渐变为 4”、有一个方向表示“逐渐变为 1”等。

下一节我们将会详细介绍生成人造图像的另一个重要工具,即生成式对抗网络(GAN)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UsJkW8bI-1672461065812)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228154914561.png)]

7.4.4 小结
  • 用深度学习进行图像生成,就是通过对潜在空间进行学习来实现的,这个潜在空间能够捕捉到关于图像数据集的统计信息。通过对潜在空间中的点进行采样和解码,我们可以生成前所未见的图像。这种方法有两种重要工具:变分自编码器(VAE)和生成式对抗网络(GAN)。
  • VAE 得到的是高度结构化的、连续的潜在表示。因此,它在潜在空间中进行各种图像编 辑的效果很好,比如换脸、将皱眉脸换成微笑脸等。它制作基于潜在空间的动画效果也很好,比如沿着潜在空间的一个横截面移动,从而以连续的方式显示从一张起始图像缓慢变化为不同图像的效果。
  • GAN 可以生成逼真的单幅图像,但得到的潜在空间可能没有良好的结构,也没有很好的连续性。

对于图像,我见过的大多数成功的实际应用都是依赖于 VAE 的,但 GAN 在学术研究领域非常流行,至少在 2016—2017 年左右是这样。下一节将会介绍 GAN 的工作原理以及实现。

如果你想进一步研究图像生成,我建议你使用大规模名人人脸属性(CelebA)数据集。 它是一个可以免费下载的图像数据集,里面包含超过 20 万张名人肖像,特别适合用概念向量进行实验,其结果肯定能打败 MNIST。

7.5 GAN

生成式对抗网络(GAN,generative adversarial network)由 Goodfellow 等人于 2014 年提出 ,它可以替代 VAE 来学习图像的潜在空间。它能够迫使生成图像与真实图像在统计上几乎无法区分,从而生成相当逼真的合成图像。

对 GAN 的一种直观理解是,想象一名伪造者试图伪造一副毕加索的画作。一开始,伪造者 非常不擅长这项任务。他将自己的一些赝品与毕加索真迹混在一起,并将其展示给一位艺术商人。 艺术商人对每幅画进行真实性评估,并向伪造者给出反馈,告诉他是什么让毕加索作品看起来像一幅毕加索作品。伪造者回到自己的工作室,并准备一些新的赝品。随着时间的推移,伪造者变得越来越擅长模仿毕加索的风格,艺术商人也变得越来越擅长找出赝品。最后,他们手上拥有了一些优秀的毕加索赝品。

这就是 GAN 的工作原理:一个伪造者网络和一个专家网络,二者训练的目的都是为了打败彼此。因此,GAN 由以下两部分组成。

生成器网络(generator network):它以一个随机向量(潜在空间中的一个随机点)作为输入,并将其解码为一张合成图像。

判别器网络(discriminator network):以一张图像(真实的或合成的均可)作为输入,并预测该图像是来自训练集还是由生成器网络创建。

训练生成器网络的目的是使其能够欺骗判别器网络,因此随着训练的进行,它能够逐渐生成越来越逼真的图像,即看起来与真实图像无法区分的人造图像,以至于判别器网络无法区分二 者(见图 8-15)。与此同时,判别器也在不断适应生成器逐渐提高的能力,为生成图像的真实性 设置了很高的标准。一旦训练结束,生成器就能够将其输入空间中的任何点转换为一张可信图像 (见图 8-16)。与 VAE 不同,这个潜在空间无法保证具有有意义的结构,而且它还是不连续的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pTH0bksb-1672461065812)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228160402960.png)]

值得注意的是,GAN 这个系统与本书中其他任何训练方法都不同,它的优化最小值是不固 定的。通常来说,梯度下降是沿着静态的损失地形滚下山坡。但对于 GAN 而言,每下山一步, 都会对整个地形造成一点改变。它是一个动态的系统,其最优化过程寻找的不是一个最小值, 而是两股力量之间的平衡。因此,GAN 的训练极其困难,想要让 GAN 正常运行,需要对模型 架构和训练参数进行大量的仔细调整。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qy2hdBWL-1672461065813)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228160437667.png)]

7.5.1 GAN 的简要实现流程

本节将会介绍如何用 Keras 来实现形式最简单的 GAN。GAN 属于高级应用,所以本书 不会深入介绍其技术细节。我们具体实现的是一个深度卷积生成式对抗网络(DCGAN,deep convolutional GAN),即生成器和判别器都是深度卷积神经网络的 GAN。特别地,它在生成器中 使用 Conv2DTranspose 层进行图像上采样。

我们将在 CIFAR10 数据集的图像上训练 GAN,这个数据集包含 50 000 张 32×32 的 RGB图像,这些图像属于 10 个类别(每个类别 5000 张图像)。为了简化,我们只使用属于“frog”(青蛙)类别的图像。

GAN 的简要实现流程如下所示。

  1. generator网络将形状为(latent_dim,)的向量映射到形状为(32, 32, 3)的图像。
  2. discriminator 网络将形状为 (32, 32, 3) 的图像映射到一个二进制分数,用于评估图像为真的概率。
  3. gan 网络将 generator 网络和 discriminator 网络连接在一起:gan(x) = discriminator (generator(x))。生成器将潜在空间向量解码为图像,判别器对这些图像的真实性进行评估,因此这个 gan 网络是将这些潜在向量映射到判别器的评估结果。
  4. 我们使用带有“真”/“假”标签的真假图像样本来训练判别器,就和训练普通的图像分类模型一样。
  5. 为了训练生成器,我们要使用 gan 模型的损失相对于生成器权重的梯度。这意味着,在每一步都要移动生成器的权重,其移动方向是让判别器更有可能将生成器解码的图像划分为“真”。换句话说,我们训练生成器来欺骗判别器。
7.5.2 大量技巧

训练 GAN 和调节 GAN 实现的过程非常困难。你应该记住一些公认的技巧。与深度学习中 的大部分内容一样,这些技巧更像是炼金术而不是科学,它们是启发式的指南,并没有理论上 的支持。这些技巧得到了一定程度的来自对现象的直观理解的支持,经验告诉我们,它们的效 果都很好,但不一定适用于所有情况。

下面是本节实现 GAN 生成器和判别器时用到的一些技巧。这里并没有列出与 GAN 相关的 全部技巧,更多技巧可查阅关于 GAN 的文献。

  • 我们使用 tanh 作为生成器最后一层的激活,而不用 sigmoid,后者在其他类型的模型中更加常见。
  • 我们使用正态分布(高斯分布)对潜在空间中的点进行采样,而不用均匀分布。
  • 随机性能够提高稳健性。训练GAN得到的是一个动态平衡,所以GAN可能以各种方式“卡住”。在训练过程中引入随机性有助于防止出现这种情况。我们通过两种方式引入随机性: 一种是在判别器中使用 dropout,另一种是向判别器的标签添加随机噪声。
  • 稀疏的梯度会妨碍 GAN 的训练。在深度学习中,稀疏性通常是我们需要的属性,但在 GAN 中并非如此。有两件事情可能导致梯度稀疏:最大池化运算和 ReLU 激活。我们推荐使用步进卷积代替最大池化来进行下采样,还推荐使用 LeakyReLU 层来代替 ReLU 激 活。LeakyReLU 和 ReLU 类似,但它允许较小的负数激活值,从而放宽了稀疏性限制。
  • 在生成的图像中,经常会见到棋盘状伪影,这是由生成器中像素空间的不均匀覆盖导致的 (见图 8-17)。为了解决这个问题,每当在生成器和判别器中都使用步进的 Conv2DTranpose 或 Conv2D 时,使用的内核大小要能够被步幅大小整除。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pqlvyhQ1-1672461065813)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228161026282.png)]

7.5.3 生成器

首先,我们来开发 generator 模型,它将一个向量(来自潜在空间,训练过程中对其随机 采样)转换为一张候选图像。GAN 常见的诸多问题之一,就是生成器“卡在”看似噪声的生成 图像上。一种可行的解决方案是在判别器和生成器中都使用 dropout。

GAN 生成器网络

import keras
from keras import layers
import numpy as np

latent_dim = 32
height = 32
width = 32
channels = 3

generator_input = keras.Input(shape=(latent_dim,))

# 将输入转换为大小为 16×16 的 128 个通道的特征图
x = layers.Dense(128 * 16 * 16)(generator_input)
x = layers.LeakyReLU()(x)
x = layers.Reshape((16, 16, 128))(x)

# Then, add a convolution layer
x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)

# 上采样为 32×32
x = layers.Conv2DTranspose(256, 4, strides=2, padding='same')(x)
x = layers.LeakyReLU()(x)

# Few more conv layers
x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(256, 5, padding='same')(x)
x = layers.LeakyReLU()(x)

# 生成一个大小为 32×32 的单通道特征图(即 CIFAR10 图像的形状)
x = layers.Conv2D(channels, 7, activation='tanh', padding='same')(x)
generator = keras.models.Model(generator_input, x)	# 将生成器模型实例化,它将形状为 (latent_dim,)的输入映射到形状为 (32, 32, 3) 的图像
generator.summary()
7.5.4 判别器

接下来,我们来开发 discriminator 模型,它接收一张候选图像(真实的或合成的)作为输入,并将其划分到这两个类别之一:“生成图像”或“来自训练集的真实图像”。

GAN 判别器网络

discriminator_input = layers.Input(shape=(height, width, channels))
x = layers.Conv2D(128, 3)(discriminator_input)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Conv2D(128, 4, strides=2)(x)
x = layers.LeakyReLU()(x)
x = layers.Flatten()(x)

# 一个 dropout 层:这是很重要的技巧
x = layers.Dropout(0.4)(x)

# 分类层
x = layers.Dense(1, activation='sigmoid')(x)

discriminator = keras.models.Model(discriminator_input, x)
discriminator.summary()

# 将判别器模型实例化,它将形状为 (32, 32, 3)的输入转换为一个二进制分类决策(真 / 假)
discriminator_optimizer = keras.optimizers.RMSprop(lr=0.0008, 
                                                   clipvalue=1.0, 	# 在优化器中使用梯度裁剪(限制梯度值的范围)
                                                   decay=1e-8)		# 为了稳定训练过程,使用学习率衰减

discriminator.compile(optimizer=discriminator_optimizer, loss='binary_crossentropy')
7.5.5 对抗网络

最后,我们要设置 GAN,将生成器和判别器连接在一起。训练时,这个模型将让生成器向 某个方向移动,从而提高它欺骗判别器的能力。这个模型将潜在空间的点转换为一个分类决策(即 “真”或“假”),它训练的标签都是“真实图像”。因此,训练 gan 将会更新 generator 的权重, 使得 discriminator 在观察假图像时更有可能预测为“真”。请注意,有一点很重要,就是在 训练过程中需要将判别器设置为冻结(即不可训练),这样在训练 gan 时它的权重才不会更新。 如果在此过程中可以对判别器的权重进行更新,那么我们就是在训练判别器始终预测“真”,但这并不是我们想要的!

# 将判别器权重设置为不可训练(仅应用于 gan 模型)
discriminator.trainable = False

gan_input = keras.Input(shape=(latent_dim,))
gan_output = discriminator(generator(gan_input))
gan = keras.models.Model(gan_input, gan_output)

gan_optimizer = keras.optimizers.RMSprop(lr=0.0004, clipvalue=1.0, decay=1e-8)
gan.compile(optimizer=gan_optimizer, loss='binary_crossentropy')
7.5.6 如何训练 DCGAN

现在开始训练。再次强调一下,训练循环的大致流程如下所示。每轮都进行以下操作。

  1. 从潜在空间中抽取随机的点(随机噪声)。
  2. 利用这个随机噪声用 generator 生成图像。
  3. 将生成图像与真实图像混合。
  4. 使用这些混合后的图像以及相应的标签(真实图像为“真”,生成图像为“假”)来训练 discriminator,如图 8-18 所示。
  5. 在潜在空间中随机抽取新的点。
  6. 使用这些随机向量以及全部是“真实图像”的标签来训练 gan。这会更新生成器的权重 (只更新生成器的权重,因为判别器在 gan 中被冻结),其更新方向是使得判别器能够将生成图像预测为“真实图像”。这个过程是训练生成器去欺骗判别器。

我们来实现这一流程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IfGMSi26-1672461065813)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221228164645584.png)]

实现 GAN 的训练

import os
from keras.preprocessing import image

# 加载 CIFAR10 数据
(x_train, y_train), (_, _) = keras.datasets.cifar10.load_data()

# 选择青蛙图像(类别编号为 6)
x_train = x_train[y_train.flatten() == 6]

# 数据标准化
x_train = x_train.reshape(
    (x_train.shape[0],) + (height, width, channels)).astype('float32') / 255.

iterations = 10000
batch_size = 20
save_dir = '/home/ubuntu/gan_images/'	# 指定保存生成图像的目录

# Start training loop
start = 0
for step in range(iterations):
    # 在潜在空间中采样随机点
    random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))

    # 将这些点解码为虚假图像
    generated_images = generator.predict(random_latent_vectors)

    # 将这些虚假图像与真实图像合在一起
    stop = start + batch_size
    real_images = x_train[start: stop]
    combined_images = np.concatenate([generated_images, real_images])

    # 合并标签,区分真实和虚假的图像
    labels = np.concatenate([np.ones((batch_size, 1)),
                             np.zeros((batch_size, 1))])
    
    # 向标签中添加随机噪声,这是一个很重要的技巧
    labels += 0.05 * np.random.random(labels.shape)

    # 训练判别器
    d_loss = discriminator.train_on_batch(combined_images, labels)

    # 在潜在空间中采样随机点
    random_latent_vectors = np.random.normal(size=(batch_size, latent_dim))

    # 合并标签,全部是“真实图像”(这是在撒谎)
    misleading_targets = np.zeros((batch_size, 1))

    # 通过 gan 模型来训练生成器(此时冻结判别器权重)
    a_loss = gan.train_on_batch(random_latent_vectors, misleading_targets)
    
    start += batch_size
    if start > len(x_train) - batch_size:
      start = 0

    # 每 100 步保存并绘图
    if step % 100 == 0:
        # 保存模型权重
        gan.save_weights('gan.h5')

        # 将指标打印出来
        print('discriminator loss at step %s: %s' % (step, d_loss))
        print('adversarial loss at step %s: %s' % (step, a_loss))

        # 保存一张生成图像
        img = image.array_to_img(generated_images[0] * 255., scale=False)
        img.save(os.path.join(save_dir, 'generated_frog' + str(step) + '.png'))

        # 保存一张真实图像,用于对比
        img = image.array_to_img(real_images[0] * 255., scale=False)
        img.save(os.path.join(save_dir, 'real_frog' + str(step) + '.png'))

训练时你可能会看到,对抗损失开始大幅增加,而判别损失则趋向于零,即判别器最终支配 了生成器。如果出现了这种情况,你可以尝试减小判别器的学习率,并增大判别器的 dropout 比率。

7.5.7 小结
  • GAN 由一个生成器网络和一个判别器网络组成。判别器的训练目的是能够区分生成器的输出与来自训练集的真实图像,生成器的训练目的是欺骗判别器。值得注意的是,生成器从未直接见过训练集中的图像,它所知道的关于数据的信息都来自于判别器。
  • GAN 很难训练,因为训练 GAN 是一个动态过程,而不是具有固定损失的简单梯度下降过程。想要正确地训练 GAN,需要使用一些启发式技巧,还需要大量的调节。
  • GAN 可能会生成非常逼真的图像。但与 VAE 不同,GAN 学习的潜在空间没有整齐的连续结构,因此可能不适用于某些实际应用,比如通过潜在空间概念向量进行图像编辑。

8 深度学习的局限性

深度学习模型只是将一个向量空间映射到另一个向量空间的简单而又连续的几何 变换链。它能做的只是将一个数据流形 X 映射到另一个流形 Y,前提是从 X 到 Y 存在可学习的 连续变换。深度学习模型可以被看作一种程序,但反过来说,大多数程序都不能被表示为深度 学习模型。对于大多数任务而言,要么不存在相应的深度神经网络能够解决任务,要么即使存 在这样的网络,它也可能是不可学习的(learnable)。后一种情况的原因可能是相应的几何变换 过于复杂,也可能是没有合适的数据用于学习。 通过堆叠更多的层并使用更多训练数据来扩展当前的深度学习技术,只能在表面上缓解一 些问题,无法解决更根本的问题,比如深度学习模型可以表示的内容非常有限,比如大多数你 想要学习的程序都不能被表示为数据流形的连续几何变形。

8.1 拟人化

我们 希望教神经网络学会某项任务,但它们是在一个不同的、更加狭窄的任务上进行训练,这个任务就是将训练输入逐点映射到训练目标。如果向神经网络展示与训练数据不一样的数据,它们可能会给出荒谬的结果。

简而言之,深度学习模型并不理解它们的输入,至少不是人类所说的理解。我们自己对图像、声音和语言的理解是基于我们作为人类的感觉运动体验。机器学习模型无法获得这些体验, 因此也就无法用与人类相似的方式来理解它们的输入。通过对输入模型的大量训练样本进行标记,我们可以让模型学会一个简单几何变换,这个变换在一组特定样本上将数据映射到人类概念, 但这种映射只是我们头脑中原始模型的简化。我们头脑中的原始模型是从我们作为具身主体的体验发展而来的。机器学习模型就像是镜子中的模糊图像(见图 9-3)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vE87d3qO-1672461065814)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221231110346770.png)]

8.2 局部泛化和极端泛化

尽管我们在机器感知方面取得了进展,但离达到人类水平的人工智能仍然很遥远。 我们的模型只能进行局部泛化,只能适应与过去数据类似的新情况,而人类的认知能够进行极端泛化,能够快速适应全新情况并为长期的未来情况做出规划。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-htNt2Lcz-1672461065814)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221231110043063.png)]

深度学习唯一的真正成功之处就是,给定大量的人 工标注数据,它能够使用连续的几何变换将空间 X 映射到空间 Y。做好这一点已经可以引起基 本上所有行业的变革,但离达到人类水平的人工智能还有很长的路要走。 想要突破我们这里所讨论的一些局限性,并创造出能够与人类大脑匹敌的人工智能,我们 需要抛弃简单的从输入到输出的映射,转而研究推理和抽象。对各种情况和概念进行抽象建模, 一个合适的基础可能是计算机程序。我们之前说过,机器学习模型可以被定义为可学习的程序。目前我们能学习的程序只是所有程序一个狭小的特定子集。但如果我们能够以一种模块化的、 可重复使用的方式来学习任何程序呢?

9 深度学习的未来

从较高层面来看,我认为下面这些重要方向很有前途。

  • 与通用的计算机程序更加接近的模型,它建立在比当前可微层要更加丰富的原语之上。 这也是我们实现推理抽象的方法,当前模型的致命弱点正是缺少推理和抽象。
  • 使上一点成为可能的新学习形式,这种学习形式能够让模型抛弃可微变换。
  • 需要更少人类工程师参与的模型。不停地调节模型不应该是我们的工作。
  • 更好地、系统性地重复使用之前学到的特征和架构,比如使用可复用和模块化子程序的元学习系统。

9.1 模型即程序

可以预测机器学习领域的一个必要转型是,抛弃只能进行纯模式识别并且只能实现局部泛化的模型,转而研究能够进行抽象和推理并且能够实现极端泛化的模型。

我认为人工智能有一个与此相关的子领域即将迎来春天,它就是程序合成,特别是神经程序合成。程序合成是指利用搜索算法(在遗传编程中也可能是遗传搜索)来探索可能程序的巨 大空间,从而自动生成简单的程序。如果找到了满足规格要求的程序(规格要求通常由一组输 入 / 输出对提供),那么搜索就会停止。这很容易让人联想到机器学习:给定输入 / 输出对作为 训练数据,我们找到一个程序,它能够将输入映射到输出,还能够泛化到新的输入。区别在于, 我们通过离散的搜索过程来生成源代码,而不是在硬编码的程序(神经网络)中学习参数值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2kBHS5Av-1672461065814)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221231114157415.png)]

9.2 超越反向传播和可微层

如果机器学习模型变得更像程序,那么通常就不再是可微的了。这些程序仍然使用连续的 几何层作为子程序,这些子程序是可微的,但整个模型是不可微的。因此,使用反向传播在固 定的硬编码的网络中调节权重值,可能不是未来训练模型的首选方法,至少不会只用这种方法。 我们需要找到能够有效地训练不可微系统的方法。目前的方法包括遗传算法、进化策略、某些 强化学习方法和交替方向乘子法(ADMM)。当然,梯度下降也不会被淘汰,梯度信息对于可微 的参数化函数的最优化总是很有用的。但我们的模型会变得越来越不满足于可微的参数化函数, 因此模型的自动开发(即机器学习中的学习)需要的也不仅仅是反向传播。

此外,反向传播是端到端的,这对于学习良好的链式变换是很有用的,但它没有充分利用 深度网络的模块化,所以计算效率很低。为了提高效率,有一个通用的策略:引入模块化和层 次结构。因此,我们可以引入解耦的训练模块以及训练模块之间的同步机制,并用一种层次化 的方式来组织,从而使反向传播更加高效。DeepMind 最近关于合成梯度的工作就稍稍反映了 这种策略。我希望在不远的将来,人们在这一方向上能走得更远。我可以设想的一个未来就是, 模型在全局上是不可微的(但部分是可微的),我们使用一种有效的搜索过程(不使用梯度)来 训练(生长)模型,而可微的部分则利用更高效版本的反向传播得到的梯度进行训练,其训练 速度更快。

9.3 自动化机器学习

未来,模型架构将是通过学习得到的,而不是由工程师人为设计的。学习架构与使用更丰 富的原语、类似程序的机器学习模型是密切相关的。

目前,深度学习工程师的大部分工作都是用 Python 脚本整理数据,然后花很长时间调节深 度网络的架构和超参数,以得到一个有效模型。如果这名工程师有野心,他可能还想得到一个 最先进的模型。毫无疑问,这种方法肯定不是最佳的,但人工智能可以提供帮助。只是数据整 理很难实现自动化,因为这一步通常需要领域知识,还需要对工程师想要实现的目标有一个清晰、 深刻的理解。但是,超参数调节是一个简单的搜索过程,我们也知道在这种情况下工程师想要 实现的目标,它由所调节网络的损失函数来定义。建立基本的自动化机器学习(AutoML)系统 已经是很常见的做法。

在最基本的层面上,这样的自动化机器学习系统可以调节堆叠的层数、层的顺序以及每一层中单元或过滤器的个数。这通常使用 Hyperopt 等库来实现,我们在第 7 章介绍过。但我们还可以更有野心,尝试从头开始学习合适的架构,让约束尽可能少,比如可以通过强化学习或遗传算法来实现。

另一个重要的自动化机器学习方向是联合学习模型架构和模型权重。我们每次尝试一个略有不同的架构,都要从头训练一个新模型,这种方法是极其低效的,因此,真正强大的自动化机器学习系统,在训练数据上进行反向传播来调节模型特征的同时,还能够不断调节其模型架构。 在我写到本节内容时,这种方法已经开始出现了。

9.4 终身学习与模块化子程序复用

如果模型变得更加复杂,并且构建于更加丰富的算法原语之上,对于这种增加的复杂度, 需要在不同的任务之间实现更多的复用,而不是每次面对一个新任务或新数据集时,都从头开 始训练一个新模型。许多数据集包含的信息都不足以让我们从头开发一个复杂的新模型,利用以往数据集中包含的信息是很必要的(就像你每次打开一本新书时,也不会从头开始学习英语—— 那是不可能的)。每开始一个新任务都从头训练模型也是非常低效的,因为当前任务与之前遇到 的任务有很多重复之处。

**训练同一个模型同时完成几个几乎没有联系的 任务,这样得到的模型在每个任务上的效果都更好。**例如,训练同一个神经机器翻译模型来实现 英语到德语的翻译和法语到意大利语的翻译,这样得到的模型在两组语言上的表现都变得更好。 同样,联合训练一个图像分类模型和一个图像分割模型,二者共享相同的卷积基,这样得到的模 型在两个任务上的表现都变得更好。这是很符合直觉的:看似无关的任务之间总是存在一些信息 重叠,与仅在特定任务上训练的模型相比,联合模型可以获取关于每项任务的更多信息。

目前,对于不同任务之间的模型复用,我们使用执行通用功能(比如视觉特征提取)的模 型的预训练权重。第 5 章介绍过这种用法。未来我希望这种方法的更一般的版本能够更加常见: 我们不仅重复使用之前学到的特征(子模型权重),还会重复使用模型架构和训练过程。随着模 型变得越来越像程序,我们将开始重复使用程序的子程序(program subroutine),就像重复使用人类编程语言中的函数和类那样。

想想如今的软件开发过程:每当工程师解决了一个具体问题(比如 Python 中的 HTTP 查询), 他们就会将其打包成一个抽象的、可复用的库。日后面临类似问题的工程师可以搜索现有的库, 然后下载,并在自己的项目中使用。同样,在未来,元学习系统能够在高级可复用模块的全局 库中筛选,从而组合成新程序。如果系统发现自己对几个不同的任务都开发出了类似的子程序, 那么它可以对这个子程序提出一个抽象的、可复用的版本,并将其存储在全局库中(见图 9-6)。 这一过程可以实现抽象,抽象是实现极端泛化的必要组件。如果一个子程序在不同任务和不同 领域中都很有用,我们可以说它对解决问题的某些方面进行了抽象化(abstract)。这个抽象的定 义与软件工程中的抽象概念类似。这些子程序可能是几何子程序(带有预训练表示的深度学习 模块),也可能是算法子程序(更接近于当代软件工程师所操作的库)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6IEuyBNO-1672461065814)(C:\Users\10555\AppData\Roaming\Typora\typora-user-images\image-20221231114932143.png)]

9.5 长期愿景

简而言之,以下是我对机器学习的长期愿景。

  • 模型将变得更像程序,其能力将远远超出我们目前对输入数据所做的连续几何变换。这些程序可以说是更加接近于人类关于周围环境和自身的抽象心智模型。因为它们具有丰富的算法特性,所以还具有更强的泛化能力
  • 具体而言,**模型将会融合算法模块与几何模块,前者提供正式的推理、搜索和抽象能力, 后者提供非正式的直觉和模式识别能力。**AlphaGo(这个系统需要大量的手动软件工程 和人为设计决策)就是这种符号人工智能和几何人工智能融合的一个早期例子。
  • 通过使用存储在可复用子程序的全局库(这个库随着在数千个先前任务和数据集上学习高性能模型而不断进化)中的模块化部件,这种模型可以自动成长(grow),而不需要人类工程师对其硬编码。随着元学习系统识别出经常出现的问题解决模式,这些模式将会被转化为可复用的子程序(正如软件工程中的函数和类),并被添加到全局库中。这 样就可以实现抽象。
  • 这个全局库和相关的模型成长系统能够实现某种形式的与人类类似的极端泛化:**给定一个新任务或新情况,系统使用很少的数据就能组合出一个适用于该任务的新的有效模型, 这要归功于丰富的类似程序的原语,它具有很好的泛化能力,还要归功于在类似任务上 的大量经验。**按照同样的方法,如果一个人具有很多以前的游戏经验,那么他可以很快学会玩一个复杂的新视频游戏,因为从先前经验得到的模型是抽象的、类似程序的,而不是刺激与行动之间的简单映射。
  • 因此,这种**永久学习的模型生长系统可以被看作一种通用人工智能(AGI,artificial general intelligence)。**但是,不要指望会出现奇点式的机器人灾难,那纯粹只是幻想,来自于人们对智能和技术的一系列深刻误解。不过对这种观点的批判不属于本书的范畴。

序,这些子程序是可微的,但整个模型是不可微的。因此,使用反向传播在固 定的硬编码的网络中调节权重值,可能不是未来训练模型的首选方法,至少不会只用这种方法。 我们需要找到能够有效地训练不可微系统的方法。目前的方法包括遗传算法、进化策略、某些 强化学习方法和交替方向乘子法(ADMM)。当然,梯度下降也不会被淘汰,梯度信息对于可微 的参数化函数的最优化总是很有用的。但我们的模型会变得越来越不满足于可微的参数化函数, 因此模型的自动开发(即机器学习中的学习)需要的也不仅仅是反向传播。

此外,反向传播是端到端的,这对于学习良好的链式变换是很有用的,但它没有充分利用 深度网络的模块化,所以计算效率很低。为了提高效率,有一个通用的策略:引入模块化和层 次结构。因此,我们可以引入解耦的训练模块以及训练模块之间的同步机制,并用一种层次化 的方式来组织,从而使反向传播更加高效。DeepMind 最近关于合成梯度的工作就稍稍反映了 这种策略。我希望在不远的将来,人们在这一方向上能走得更远。我可以设想的一个未来就是, 模型在全局上是不可微的(但部分是可微的),我们使用一种有效的搜索过程(不使用梯度)来 训练(生长)模型,而可微的部分则利用更高效版本的反向传播得到的梯度进行训练,其训练 速度更快。

9.3 自动化机器学习

未来,模型架构将是通过学习得到的,而不是由工程师人为设计的。学习架构与使用更丰 富的原语、类似程序的机器学习模型是密切相关的。

目前,深度学习工程师的大部分工作都是用 Python 脚本整理数据,然后花很长时间调节深 度网络的架构和超参数,以得到一个有效模型。如果这名工程师有野心,他可能还想得到一个 最先进的模型。毫无疑问,这种方法肯定不是最佳的,但人工智能可以提供帮助。只是数据整 理很难实现自动化,因为这一步通常需要领域知识,还需要对工程师想要实现的目标有一个清晰、 深刻的理解。但是,超参数调节是一个简单的搜索过程,我们也知道在这种情况下工程师想要 实现的目标,它由所调节网络的损失函数来定义。建立基本的自动化机器学习(AutoML)系统 已经是很常见的做法。

在最基本的层面上,这样的自动化机器学习系统可以调节堆叠的层数、层的顺序以及每一层中单元或过滤器的个数。这通常使用 Hyperopt 等库来实现,我们在第 7 章介绍过。但我们还可以更有野心,尝试从头开始学习合适的架构,让约束尽可能少,比如可以通过强化学习或遗传算法来实现。

另一个重要的自动化机器学习方向是联合学习模型架构和模型权重。我们每次尝试一个略有不同的架构,都要从头训练一个新模型,这种方法是极其低效的,因此,真正强大的自动化机器学习系统,在训练数据上进行反向传播来调节模型特征的同时,还能够不断调节其模型架构。 在我写到本节内容时,这种方法已经开始出现了。

9.4 终身学习与模块化子程序复用

如果模型变得更加复杂,并且构建于更加丰富的算法原语之上,对于这种增加的复杂度, 需要在不同的任务之间实现更多的复用,而不是每次面对一个新任务或新数据集时,都从头开 始训练一个新模型。许多数据集包含的信息都不足以让我们从头开发一个复杂的新模型,利用以往数据集中包含的信息是很必要的(就像你每次打开一本新书时,也不会从头开始学习英语—— 那是不可能的)。每开始一个新任务都从头训练模型也是非常低效的,因为当前任务与之前遇到 的任务有很多重复之处。

**训练同一个模型同时完成几个几乎没有联系的 任务,这样得到的模型在每个任务上的效果都更好。**例如,训练同一个神经机器翻译模型来实现 英语到德语的翻译和法语到意大利语的翻译,这样得到的模型在两组语言上的表现都变得更好。 同样,联合训练一个图像分类模型和一个图像分割模型,二者共享相同的卷积基,这样得到的模 型在两个任务上的表现都变得更好。这是很符合直觉的:看似无关的任务之间总是存在一些信息 重叠,与仅在特定任务上训练的模型相比,联合模型可以获取关于每项任务的更多信息。

目前,对于不同任务之间的模型复用,我们使用执行通用功能(比如视觉特征提取)的模 型的预训练权重。第 5 章介绍过这种用法。未来我希望这种方法的更一般的版本能够更加常见: 我们不仅重复使用之前学到的特征(子模型权重),还会重复使用模型架构和训练过程。随着模 型变得越来越像程序,我们将开始重复使用程序的子程序(program subroutine),就像重复使用人类编程语言中的函数和类那样。

想想如今的软件开发过程:每当工程师解决了一个具体问题(比如 Python 中的 HTTP 查询), 他们就会将其打包成一个抽象的、可复用的库。日后面临类似问题的工程师可以搜索现有的库, 然后下载,并在自己的项目中使用。同样,在未来,元学习系统能够在高级可复用模块的全局 库中筛选,从而组合成新程序。如果系统发现自己对几个不同的任务都开发出了类似的子程序, 那么它可以对这个子程序提出一个抽象的、可复用的版本,并将其存储在全局库中(见图 9-6)。 这一过程可以实现抽象,抽象是实现极端泛化的必要组件。如果一个子程序在不同任务和不同 领域中都很有用,我们可以说它对解决问题的某些方面进行了抽象化(abstract)。这个抽象的定 义与软件工程中的抽象概念类似。这些子程序可能是几何子程序(带有预训练表示的深度学习 模块),也可能是算法子程序(更接近于当代软件工程师所操作的库)。

[外链图片转存中…(img-6IEuyBNO-1672461065814)]

9.5 长期愿景

简而言之,以下是我对机器学习的长期愿景。

  • 模型将变得更像程序,其能力将远远超出我们目前对输入数据所做的连续几何变换。这些程序可以说是更加接近于人类关于周围环境和自身的抽象心智模型。因为它们具有丰富的算法特性,所以还具有更强的泛化能力
  • 具体而言,**模型将会融合算法模块与几何模块,前者提供正式的推理、搜索和抽象能力, 后者提供非正式的直觉和模式识别能力。**AlphaGo(这个系统需要大量的手动软件工程 和人为设计决策)就是这种符号人工智能和几何人工智能融合的一个早期例子。
  • 通过使用存储在可复用子程序的全局库(这个库随着在数千个先前任务和数据集上学习高性能模型而不断进化)中的模块化部件,这种模型可以自动成长(grow),而不需要人类工程师对其硬编码。随着元学习系统识别出经常出现的问题解决模式,这些模式将会被转化为可复用的子程序(正如软件工程中的函数和类),并被添加到全局库中。这 样就可以实现抽象。
  • 这个全局库和相关的模型成长系统能够实现某种形式的与人类类似的极端泛化:**给定一个新任务或新情况,系统使用很少的数据就能组合出一个适用于该任务的新的有效模型, 这要归功于丰富的类似程序的原语,它具有很好的泛化能力,还要归功于在类似任务上 的大量经验。**按照同样的方法,如果一个人具有很多以前的游戏经验,那么他可以很快学会玩一个复杂的新视频游戏,因为从先前经验得到的模型是抽象的、类似程序的,而不是刺激与行动之间的简单映射。
  • 因此,这种**永久学习的模型生长系统可以被看作一种通用人工智能(AGI,artificial general intelligence)。**但是,不要指望会出现奇点式的机器人灾难,那纯粹只是幻想,来自于人们对智能和技术的一系列深刻误解。不过对这种观点的批判不属于本书的范畴。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值