作业1用神经网络思想实现逻辑回归

用神经网络思想实现逻辑回归

欢迎来到你的第一个编程作业! 你将学习如何建立逻辑回归分类器用来识别猫。 这项作业将引导你逐步了解神经网络的思维方式,同时磨练你对深度学习的直觉。

说明:
除非指令中明确要求使用,否则请勿在代码中使用循环(for / while)。

你将学习以下内容:

  • 建立学习算法的一般架构,包括:
    • 初始化参数
    • 计算损失函数及其梯度
    • 使用优化算法(梯度下降)
  • 按正确的顺序将以上所有三个功能集成到一个主模型上。

安装包

首先,请使用程序导入本次作业需要的包。

问题概述

问题说明:你将获得一个包含以下内容的数据集(”datasets”):

  • 标记为cat(y = 1)或非cat(y = 0)的train_catvnoncat.h5训练图像集
  • 标记为cat或non-cat的test_catvnoncat.h5测试图像集
  • 图像维度为(num_px,num_px,3),其中3表示3个通道(RGB)。 因此,每个图像都是正方形(高度= num_px)和(宽度= num_px)。

你将构建一个简单的图像识别算法,该算法可以将图片正确分类为猫和非猫。现在你无须关心数据集的内容,只需要使用
lr_utils.py文件中的load_dataset函数直接获得数据集中的内容即可 返回值有:
train_set_x_orig :保存的是训练集里面的图像数据(本训练集有209张64x64的图像)。
train_set_y_orig :保存的是训练集的图像对应的分类值(【0 | 1】,0表示不是猫,1表示是猫)。
test_set_x_orig :保存的是测试集里面的图像数据(本训练集有50张64x64的图像)。
test_set_y_orig : 保存的是测试集的图像对应的分类值(【0 | 1】,0表示不是猫,1表示是猫)。
classes : 保存的是以bytes类型保存的两个字符串数据,数据为:[b’non-cat’ b’cat’] classes[0] =’non-cat’ classes[1] =’cat’
由于是bytes类型,会显示报错,所以需要.decode(“utf-8”)

让我们熟悉一下数据集吧, 首先请你通过load_dataset来加载数据。要求变量分别为train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes


说明:自变量在图像数据集(训练和测试)的末尾添加了”_orig”,以便对其进行预处理。 预处理后,我们将得到train_set_x和test_set_x(标签train_set_y和test_set_y不需要任何预处理)。
1
2
train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = load_dataset()

train_set_x_orig和test_set_x_orig的每一行都是代表图像的数组。

现在请通过编写代码来可视化示例查看训练集第26张图像图片。 设计变量`index`,随意更改`index`值可查看其他图像。


提示:用matplotlib中pyplot的imshow函数
请记住,“ train_set_x_orig”是一个维度为(训练集图片数量,图片高度,图片宽度,3)的numpy数组。 例如,你可以通过编写“ train_set_x_orig.shape [0]”来访问训练集的的第一张图片。
1
2
index = 25
plt.imshow(train_set_x_orig[index])

深度学习中的许多报错都来自于矩阵/向量尺寸不匹配。 如果你可以保持矩阵/向量的尺寸不变,那么将消除大多错误。

练习: 查找以下各项的值:

  • m_train(训练集示例数量)
  • m_test(测试集示例数量)
  • num_px(=训练图像的高度=训练图像的宽度)

请记住,“ train_set_x_orig”是一个维度为(m_train,num_px,num_px,3)的numpy数组。 例如,你可以通过编写“ train_set_x_orig.shape [0]”来访问“ m_train”。

现在请你设计m_train、m_test、num_px三个变量来获得训练集示例数量,测试集示例数量,图像的高度


1
2
3
m_train = train_set_y.shape[1] #训练集里图片的数量。
m_test = test_set_y.shape[1] #测试集里图片的数量。
num_px = train_set_x_orig.shape[1] #训练、测试集里面的图片的宽度和高度(均为64x64)。

非常棒,现在请你利用上面已经拥有的变量来编写程序输出以下的内容,格式如下


预期输出
训练集的数量: m_train = 209
测试集的数量 : m_test = 50
每张图片的宽/高 : num_px = 64
每张图片的大小 : (64, 64, 3)
训练集_图片的维数 : (209, 64, 64, 3)
训练集_标签的维数 : (1, 209)
测试集_图片的维数: (50, 64, 64, 3)
测试集_标签的维数: (1, 50)

1
2
3
4
5
6
7
8
print ("训练集的数量: m_train = " + str(m_train))
print ("测试集的数量 : m_test = " + str(m_test))
print ("每张图片的宽/高 : num_px = " + str(num_px))
print ("每张图片的大小 : (" + str(num_px) + ", " + str(num_px) + ", 3)")
print ("训练集_图片的维数 : " + str(train_set_x_orig.shape))
print ("训练集_标签的维数 : " + str(train_set_y.shape))
print ("测试集_图片的维数: " + str(test_set_x_orig.shape))
print ("测试集_标签的维数: " + str(test_set_y.shape))

为了方便起见,你现在应该以维度(num_px * num_px * 3, 1)的numpy数组重塑维度(num_px,num_px,3)的图像。 此后,我们的训练(和测试)数据集是一个numpy数组,其中每列代表一个展平的图像。 应该有m_train(和m_test)列。

练习: 重塑训练和测试数据集,以便将大小(num_px,num_px,3)的图像展平为单个形状的向量(num_px * num_px * 3, 1)。

当你想将维度为(a,b,c,d)的矩阵X展平为形状为(b*c*d, a)的矩阵X_flatten时的一个技巧是:
X_flatten = X.reshape(X.shape [0],-1).T # 其中X.T是X的转置矩阵

现在请你将训练集图片train_set_x_orig以及测试集图片test_set_x_orig重塑为每一列代表一张图片 要求:使用train_set_x_flatten变量代表train_set_x_orig重塑后 使用test_set_x_flatten变量代表test_set_x_orig重塑后


1
2
3
4
#将训练集的维度降低并转置。
train_set_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0],-1).T
#将测试集的维度降低并转置。
test_set_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T

做的很棒,现在请你利用上面已经拥有的变量来编写程序输出以下的内容,格式如下


预期输出
训练集降维最后的维度: (12288, 209)
训练集_标签的维数 : (1, 209)
测试集降维之后的维度: (12288, 50)
测试集_标签的维数 : (1, 50)
1
2
3
4
5
print ("训练集降维最后的维度: " + str(train_set_x_flatten.shape))
print ("训练集_标签的维数 : " + str(train_set_y.shape))
print ("测试集降维之后的维度: " + str(test_set_x_flatten.shape))
print ("测试集_标签的维数 : " + str(test_set_y.shape))

为了表示彩色图像,必须为每个像素指定红、绿、蓝色通道(RGB),因此像素值实际上是一个从0到255的三个数字的向量。

机器学习中一个常见的预处理步骤是对数据集进行居中和标准化,这意味着你要从每个示例中减去整个numpy数组的均值,然后除以整个numpy数组的标准差。但是图片数据集则更为简单方便,并且只要将数据集的每一行除以255(像素通道的最大值),效果也差不多。

在训练模型期间,你将要乘以权重并向一些初始输入添加偏差以观察神经元的激活。然后,使用反向梯度传播以训练模型。但是,让特征具有相似的范围以至渐变不会爆炸是非常重要的。具体内容我们将在后面的教程中详细学习!

开始标准化我们的数据集吧!

你需要记住的内容:

预处理数据集的常见步骤是:

  • 找出数据的尺寸和维度(m_train,m_test,num_px等)
  • 重塑数据集,以使每个示例都是大小为(num_px * num_px * 3,1)的向量
  • “标准化”数据

    现在标准化我们的训练集图片train_set_x_flatten和测试集图片test_set_x_flatten,通过除以255进行标准化,让数据位于[0,1]之间


    要求:使用train_set_x变量代表train_set_x_flatten标准化后


    使用test_set_x变量代表test_set_x_flatten标准化后

    1
    2
    train_set_x = train_set_x_flatten / 255
    test_set_x = test_set_x_flatten / 255

学习算法的一般架构

现在是时候设计一种简单的算法来区分猫图像和非猫图像了。

你将使用神经网络思维方式建立Logistic回归。 下图说明了为什么“逻辑回归实际上是一个非常简单的神经网络!”

算法的数学表达式

For one example $x^{(i)}$:
$z^{(i)} = w^T x^{(i)} + b \tag{1}$
$\hat{y}^{(i)} = a^{(i)} = sigmoid(z^{(i)})\tag{2}$

The cost is then computed by summing over all training examples:

关键步骤
在本练习中,你将执行以下步骤:

  • 初始化模型参数
  • 通过最小化损失来学习模型的参数
  • 使用学习到的参数进行预测(在测试集上)
  • 分析结果并得出结论

构建算法的各个部分

建立神经网络的主要步骤是:
1.定义模型结构(例如输入特征的数量)
2.初始化模型的参数
3.循环:

  • 计算当前损失(正向传播)
  • 计算当前梯度(反向传播)
  • 更新参数(梯度下降)

你通常会分别构建1-3,然后将它们集成到一个称为“ model()”的函数中。

激活函数

练习:使用“Python基础”中的代码,实现sigmoid()。 如上图所示,你需要计算$sigmoid( w^T x + b) = \frac{1}{1 + e^{-(w^T x + b)}}$ 去预测。 使用np.exp()。

编写实现sigmoid函数,实现输入z,输出s


$s = \frac{1}{1 + e^{-z}}$
1
2
3
4
5
6
7
8
9
10
def sigmoid(z):
"""
参数:
z - 任何大小的标量或numpy数组。

返回:
s - sigmoid(z)
"""
s = 1 / (1 + np.exp(-z))
return s

初始化参数

练习: 在下面将实现参数初始化。 你必须将w初始化为零的向量。 如果你不知道要使用什么numpy函数,请在Numpy库的文档中查找np.zeros()。

请你编写initialize_with_zeros(dim)函数,将w初始化一个维度为(dim,1)的0向量,并将b初始化为0。返回w和b的初始值


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def initialize_with_zeros(dim):
"""
此函数为w创建一个维度为(dim,1)的0向量,并将b初始化为0。

参数:
dim - 我们想要的w矢量的大小(或者这种情况下的参数数量)

返回:
w - 维度为(dim,1)的初始化向量。
b - 初始化的标量(对应于偏差)
"""
w = np.zeros(shape = (dim,1))
b = 0
#使用断言来确保我要的数据是正确的
assert(w.shape == (dim, 1)) #w的维度是(dim,1)
assert(isinstance(b, float) or isinstance(b, int)) #b的类型是float或者是int

return (w , b)

对于图像输入,w的维度为(num_px * num_px * 3, 1)。

前向和后向传播

现在,你的参数已初始化,你可以执行“向前”和“向后”传播步骤来学习参数。

练习: 实现函数propagate()来计算损失函数(正向传播)及其梯度(反向传播)。

坚持,马上就要成功了,现在请你编写propagate(w,b, X, Y)函数,这是一个传播函数,作用是实现

正向传播计算代价值cost

反向传播计算梯度dw以及db

返回值grads ,cost


提示

正向传播:

  • 得到X
  • 计算$A = \sigma(w^T X + b) = (a^{(0)}, a^{(1)}, …, a^{(m-1)}, a^{(m)})$
  • 计算损失函数:$J = -\frac{1}{m}\sum_{i=1}^{m}y^{(i)}\log(a^{(i)})+(1-y^{(i)})\log(1-a^{(i)})$

反向传播:

  • $\frac{\partial J}{\partial w} = \frac{1}{m}X(A-Y)^T\tag{7}$
  • $\frac{\partial J}{\partial b} = \frac{1}{m} \sum_{i=1}^m (a^{(i)}-y^{(i)})\tag{8}$
  • 请使用cost = np.squeeze(cost) 用于去掉成本值的多余维度,使其变为标量
  • 请使用字典把dw和db保存起来,字典的键是 “dw” 和 “db”,对应的值分别是 dw 和 db 的数据,你可以通过键来访问这些数据,例如 grads[“dw”]来获得变量dw的值 和 grads[“db”]来获得db的值
    grads = {
    “dw”: dw,
    “db”: db
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def propagate(w, b, X, Y):
"""
实现前向和后向传播的成本函数及其梯度。
参数:
w - 权重,大小不等的数组(num_px * num_px * 3,1)
b - 偏差,一个标量
X - 矩阵类型为(num_px * num_px * 3,训练数量)
Y - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据数量)

返回:
cost- 逻辑回归的负对数似然成本
dw - 相对于w的损失梯度,因此与w相同的形状
db - 相对于b的损失梯度,因此与b的形状相同
"""
m = X.shape[1]

#正向传播
A = sigmoid(np.dot(w.T,X) + b) #计算激活值,请参考公式2。
cost = (- 1 / m) * np.sum(Y * np.log(A) + (1 - Y) * (np.log(1 - A))) #计算成本,请参考公式3和4。

#反向传播
dw = (1 / m) * np.dot(X, (A - Y).T) #请参考视频中的偏导公式。
db = (1 / m) * np.sum(A - Y) #请参考视频中的偏导公式。

#使用断言确保我的数据是正确的
assert(dw.shape == w.shape)
assert(db.dtype == float)
cost = np.squeeze(cost)
assert(cost.shape == ())

#创建一个字典,把dw和db保存起来。
grads = {
"dw": dw,
"db": db
}
return (grads , cost)

优化函数

  • 初始化参数。
  • 计算损失函数及其梯度。
  • 使用梯度下降来更新参数。

练习: 写下优化函数。 目标是通过最小化损失函数 J 来学习 w 和 b。 对于参数$\theta$,更新规则为$\theta = \theta - \alpha \text{ } d\theta$,其中$\alpha$是学习率。

现在请你编写optimize(w , b , X , Y , num_iterations , learning_rate , print_cost = False)函数

  • 函数主要作用更新w,b的值并储存在字典params中
  • params = {“w” : w,”b” : b }
  • 每次迭代需要的数据来自grads, cost = propagate(w, b, X, Y)
  • 我们要求将costs做出空列表,每迭代100次向里面追加一个cost值
  • 如果print_cost为true则每迭代100次打印当前的cost值
  • 请你设置返回值为params , grads , costs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    def optimize(w , b , X , Y , num_iterations , learning_rate , print_cost = False):
    """
    此函数通过运行梯度下降算法来优化w和b

    参数:
    w - 权重,大小不等的数组(num_px * num_px * 3,1)
    b - 偏差,一个标量
    X - 维度为(num_px * num_px * 3,训练数据的数量)的数组。
    Y - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据的数量)
    num_iterations - 优化循环的迭代次数
    learning_rate - 梯度下降更新规则的学习率
    print_cost - 每100步打印一次损失值

    返回:
    params - 包含权重w和偏差b的字典
    grads - 包含权重和偏差相对于成本函数的梯度的字典
    成本 - 优化期间计算的所有成本列表,将用于绘制学习曲线。

    提示:
    我们需要写下两个步骤并遍历它们:
    1)计算当前参数的成本和梯度,使用propagate()。
    2)使用w和b的梯度下降法则更新参数。
    """

    costs = []

    for i in range(num_iterations):

    grads, cost = propagate(w, b, X, Y)

    dw = grads["dw"]
    db = grads["db"]

    w = w - learning_rate * dw
    b = b - learning_rate * db

    #记录成本
    if i % 100 == 0:
    costs.append(cost)
    #打印成本数据
    if (print_cost) and (i % 100 == 0):
    print("迭代的次数: %i , 误差值: %f" % (i,cost))

    params = {
    "w" : w,
    "b" : b }
    grads = {
    "dw": dw,
    "db": db }
    return (params , grads , costs)

预测函数

  • 计算 $\hat{Y} = A $。
  • 将a的项转换为0(如果激活<= 0.5)或1(如果激活> 0.5)。
    现在请你编写predict(w ,b , X )函数,返回Y_prediction参数代表m个样本的预测值,形状为(1,m),预测值只有0,1
    练习: 上一个函数将输出学习到的w和b。 我们能够使用w和b来预测数据集X的标签。实现predict()函数。 预测分类有两个步骤:
    1.计算$\hat{Y} = A = \sigma(w^T X + b)$
    2.将a的项转换为0(如果激活<= 0.5)或1(如果激活> 0.5),并将预测结果存储在向量“ Y_prediction”中。 如果愿意,可以在for循环中使用if / else语句。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    def predict(w , b , X ):
    """
    使用学习逻辑回归参数logistic (w,b)预测标签是0还是1,

    参数:
    w - 权重,大小不等的数组(num_px * num_px * 3,1)
    b - 偏差,一个标量
    X - 维度为(num_px * num_px * 3,训练数据的数量)的数据

    返回:
    Y_prediction - 包含X中所有图片的所有预测【0 | 1】的一个numpy数组(向量)

    """

    m = X.shape[1] #图片的数量
    Y_prediction = np.zeros((1,m))
    w = w.reshape(X.shape[0],1)

    #计预测猫在图片中出现的概率
    A = sigmoid(np.dot(w.T , X) + b)
    for i in range(A.shape[1]):
    #将概率a [0,i]转换为实际预测p [0,i]
    Y_prediction[0,i] = 1 if A[0,i] > 0.5 else 0
    #使用断言
    assert(Y_prediction.shape == (1,m))

    return Y_prediction
    你需要记住以下几点:
    你已经实现了以下几个函数:
  • 初始化(w,b)
  • 迭代优化损失以学习参数(w,b):
    • 计算损失及其梯度
    • 使用梯度下降更新参数
  • 使用学到的(w,b)来预测给定示例集的标签

将所有功能合并到模型中

现在,将所有构件(在上一部分中实现的功能)以正确的顺序放在一起,从而得到整体的模型结构。

请实现model(X_train ,Y_train , X_test ,Y_test ,num_iterations = 2000 , learning_rate = 0.5 , print_cost = False) 函数返回一个字典d

要求打印训练集准确性以及测试集准确性


提示:
$ \text{Absolute Error} = |Y_{\text{prediction}} - Y| $
$ \text{Mean Absolute Error (MAE)} = \frac{1}{m} \sum_{i=1}^{m} |Y_{\text{prediction}}^{(i)} - Y^{(i)}| $
$ \text{Percentage Error} = \text{MAE} \times 100 $
$ \text{Accuracy} = 100 - \text{Percentage Error} $
print(“训练集准确性:” , format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) 100) ,”%”)
print(“测试集准确性:” , format(100 - np.mean(np.abs(Y_prediction_test - Y_test))
100) ,”%”)

构建的顺序如下

  • initialize_with_zeros、optimize、predict
    练习: 实现模型功能,使用以下符号:
  • Y_prediction_test对测试集的预测
  • Y_prediction_train对训练集的预测
  • w,b,costs,learning_rate,num_iterations
  • 返回值d为一个字典包括以下内容
    d = {
    “costs” : costs,
    “Y_prediction_test” : Y_prediction_test,
    “Y_prediciton_train” : Y_prediction_train,
    “w” : w,
    “b” : b,
    “learning_rate” : learning_rate,
    “num_iterations” : num_iterations
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def model(X_train , Y_train , X_test , Y_test , num_iterations = 2000 , learning_rate = 0.5 , print_cost = False):
"""
通过调用之前实现的函数来构建逻辑回归模型

参数:
X_train - numpy的数组,维度为(num_px * num_px * 3,m_train)的训练集
Y_train - numpy的数组,维度为(1,m_train)(矢量)的训练标签集
X_test - numpy的数组,维度为(num_px * num_px * 3,m_test)的测试集
Y_test - numpy的数组,维度为(1,m_test)的(向量)的测试标签集
num_iterations - 表示用于优化参数的迭代次数的超参数
learning_rate - 表示optimize()更新规则中使用的学习速率的超参数
print_cost - 设置为true以每100次迭代打印成本

返回:
d - 包含有关模型信息的字典。
"""
w , b = initialize_with_zeros(X_train.shape[0])

parameters , grads , costs = optimize(w , b , X_train , Y_train,num_iterations , learning_rate , print_cost)

#从字典“参数”中检索参数w和b
w , b = parameters["w"] , parameters["b"]

#预测测试/训练集的例子
Y_prediction_test = predict(w , b, X_test)
Y_prediction_train = predict(w , b, X_train)

#打印训练后的准确性
print("训练集准确性:" , format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100) ,"%")
print("测试集准确性:" , format(100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100) ,"%")

d = {
"costs" : costs,
"Y_prediction_test" : Y_prediction_test,
"Y_prediciton_train" : Y_prediction_train,
"w" : w,
"b" : b,
"learning_rate" : learning_rate,
"num_iterations" : num_iterations }
return d

现在请你调用model函数使用d来接收返回值,由此将得到需要的各项参数信息

1
d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 2000, learning_rate = 0.005, print_cost = True)

预期输出:
训练集准确性: 99.04306220095694 %
测试集准确性: 70.0 %

评价:训练准确性接近100%。 这是一个很好的情况:你的模型正在运行,并且具有足够的容量来适合训练数据。 测试误差为68%。 考虑到我们使用的数据集很小,并且逻辑回归是线性分类器,对于这个简单的模型来说,这实际上还不错。 但请放心,下周你将建立一个更好的分类器!

此外,你会看到该模型明显适合训练数据。 在本专业的稍后部分,你将学习如何减少过度拟合,例如通过使用正则化。 使用下面的代码(并更改index变量),你可以查看测试集图片上的预测。

绘制损失函数

让我们绘制损失函数。
绘制损失函数,使用d字典的costs值,但是请使用squeeze把它变为标量,使用plot函数绘制,show函数展示

1
2
3
4
5
6
7
#绘制图
costs = np.squeeze(d['costs'])
plt.plot(costs)
plt.ylabel('cost')
plt.xlabel('iterations (per hundreds)')
plt.title("Learning rate =" + str(d["learning_rate"]))
plt.show()

看样子很不错,现在请你同时绘制三个损失函数曲线分别对应三个不同学习率,可以使用for循环,绘制图表外请你输出以下内容


learning rate is: 0.01
训练集准确性: 99.52153110047847 %
测试集准确性: 68.0 %


learning rate is: 0.001
训练集准确性: 88.99521531100478 %
测试集准确性: 64.0 %


learning rate is: 0.0001
训练集准确性: 68.42105263157895 %
测试集准确性: 36.0 %
提示

  • 使用plt.plot(y, label=”” ))
  • 创建字典models = {}来接受不同学习率对应的模型输出d,使用学习率的字符串表示作为键,models[str(i)] ,模型输出d为值
  • 使用models[str(i)][“costs”]来找到学习率为i时候的costs值
  • 在你展示之前请使用下面的程序为你的figure添加图列,添加后会让labe与y绑定,不同的label对应不同函数
    legend = plt.legend(loc=’upper center’, shadow=True)
    frame = legend.get_frame()
    frame.set_facecolor(‘0.90’)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
learning_rates = [0.01, 0.001, 0.0001]
models = {}
for i in learning_rates:
print ("learning rate is: " + str(i))
models[str(i)] = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 1500, learning_rate = i, print_cost = False)
print ('\n' + "-------------------------------------------------------" + '\n')

for i in learning_rates:
plt.plot(np.squeeze(models[str(i)]["costs"]), label= str(models[str(i)]["learning_rate"]))

plt.ylabel('cost')
plt.xlabel('iterations')

legend = plt.legend(loc='upper center', shadow=True)
frame = legend.get_frame()
frame.set_facecolor('0.90')
plt.show()

输出展示:

解释
损失下降表明正在学习参数。 但是,你看到可以在训练集上训练更多模型。 尝试增加迭代次数,然后重新运行。 你可能会看到训练集准确性提高了,但是测试集准确性却降低了。 这称为过度拟合。

使用自己的图像进行测试(可选练习)

祝贺你完成此作业。 你可以使用自己的图像并查看模型的预测输出。 要做到这一点:
1.将图像添加到项目目录中,在”images”文件夹中
2.在以下代码中更改图像的名称为cat.jpg
3.运行代码,检查算法是否正确(1 = cat,0 = non-cat)
引入自己的猫猫图片,导入后把图片改变形状为(64, 64, 3)并且转换为数组形式,用数组展示图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from PIL import Image
import numpy as np

# Step 1: 打开图片
image_path = 'images/cat.jpg' # 替换为您的图片路径
image = Image.open(image_path)

# Step 2: 调整图片尺寸为 (64, 64)
image_resized = image.resize((64, 64))

# Step 3: 转换为 NumPy 数组
image_array = np.array(image_resized)

# 验证数组形状
print(image_array.shape) # 应输出 (64, 64, 3)

#数组来展示图像
plt.imshow(image_array)
plt.show()

很好,现在对数据进行预处理包括两步骤
重塑数据集,以使每个示例都是大小为(num_px * num_px * 3,1)的向量
1
my_image_flatten  = image_array.reshape(1,-1).T

“标准化”数据
1
my_image_flatten = my_image_flatten / 255

使用训练好的模型获得较好的w和b值,然后用predict(w ,b , X )函数来预测
1
2
3
4
5
6
d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 2000, learning_rate = 0.005, print_cost = True)

y = predict(d["w"], d["b"], my_image_flatten)
print("y = " + str(np.squeeze(y)))
或者使用
print("y = " + str(np.squeeze(y)) + ", 你的算法预测为 \"" + classes[int(np.squeeze(y))].decode("utf-8") + "\" picture.")

此作业要记住的内容:

  1. 预处理数据集很重要。
  2. 如何实现每个函数:initialize(),propagation(),optimize(),并用此构建一个model()。
  3. 调整学习速率(这是“超参数”的一个示例)可以对算法产生很大的影响。 你将在本课程的稍后部分看到更多示例!

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

import numpy as np
import matplotlib.pyplot as plt
import h5py
from lr_utils import load_dataset

train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = load_dataset()

m_train = train_set_y.shape[1] #训练集里图片的数量。
m_test = test_set_y.shape[1] #测试集里图片的数量。
num_px = train_set_x_orig.shape[1] #训练、测试集里面的图片的宽度和高度(均为64x64)。

#现在看一看我们加载的东西的具体情况
print ("训练集的数量: m_train = " + str(m_train))
print ("测试集的数量 : m_test = " + str(m_test))
print ("每张图片的宽/高 : num_px = " + str(num_px))
print ("每张图片的大小 : (" + str(num_px) + ", " + str(num_px) + ", 3)")
print ("训练集_图片的维数 : " + str(train_set_x_orig.shape))
print ("训练集_标签的维数 : " + str(train_set_y.shape))
print ("测试集_图片的维数: " + str(test_set_x_orig.shape))
print ("测试集_标签的维数: " + str(test_set_y.shape))

#将训练集的维度降低并转置。
train_set_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0],-1).T
#将测试集的维度降低并转置。
test_set_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T

print ("训练集降维最后的维度: " + str(train_set_x_flatten.shape))
print ("训练集_标签的维数 : " + str(train_set_y.shape))
print ("测试集降维之后的维度: " + str(test_set_x_flatten.shape))
print ("测试集_标签的维数 : " + str(test_set_y.shape))

train_set_x = train_set_x_flatten / 255
test_set_x = test_set_x_flatten / 255

def sigmoid(z):
"""
参数:
z - 任何大小的标量或numpy数组。

返回:
s - sigmoid(z)
"""
s = 1 / (1 + np.exp(-z))
return s

def initialize_with_zeros(dim):
"""
此函数为w创建一个维度为(dim,1)的0向量,并将b初始化为0。

参数:
dim - 我们想要的w矢量的大小(或者这种情况下的参数数量)

返回:
w - 维度为(dim,1)的初始化向量。
b - 初始化的标量(对应于偏差)
"""
w = np.zeros(shape = (dim,1))
b = 0
#使用断言来确保我要的数据是正确的
assert(w.shape == (dim, 1)) #w的维度是(dim,1)
assert(isinstance(b, float) or isinstance(b, int)) #b的类型是float或者是int

return (w , b)

def propagate(w, b, X, Y):
"""
实现前向和后向传播的成本函数及其梯度。
参数:
w - 权重,大小不等的数组(num_px * num_px * 3,1)
b - 偏差,一个标量
X - 矩阵类型为(num_px * num_px * 3,训练数量)
Y - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据数量)

返回:
cost- 逻辑回归的负对数似然成本
dw - 相对于w的损失梯度,因此与w相同的形状
db - 相对于b的损失梯度,因此与b的形状相同
"""
m = X.shape[1]

#正向传播
A = sigmoid(np.dot(w.T,X) + b) #计算激活值,请参考公式2。
cost = (- 1 / m) * np.sum(Y * np.log(A) + (1 - Y) * (np.log(1 - A))) #计算成本,请参考公式3和4。

#反向传播
dw = (1 / m) * np.dot(X, (A - Y).T) #请参考视频中的偏导公式。
db = (1 / m) * np.sum(A - Y) #请参考视频中的偏导公式。

#使用断言确保我的数据是正确的
assert(dw.shape == w.shape)
assert(db.dtype == float)
cost = np.squeeze(cost)
assert(cost.shape == ())

#创建一个字典,把dw和db保存起来。
grads = {
"dw": dw,
"db": db
}
return (grads , cost)

def optimize(w , b , X , Y , num_iterations , learning_rate , print_cost = False):
"""
此函数通过运行梯度下降算法来优化w和b

参数:
w - 权重,大小不等的数组(num_px * num_px * 3,1)
b - 偏差,一个标量
X - 维度为(num_px * num_px * 3,训练数据的数量)的数组。
Y - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据的数量)
num_iterations - 优化循环的迭代次数
learning_rate - 梯度下降更新规则的学习率
print_cost - 每100步打印一次损失值

返回:
params - 包含权重w和偏差b的字典
grads - 包含权重和偏差相对于成本函数的梯度的字典
成本 - 优化期间计算的所有成本列表,将用于绘制学习曲线。

提示:
我们需要写下两个步骤并遍历它们:
1)计算当前参数的成本和梯度,使用propagate()。
2)使用w和b的梯度下降法则更新参数。
"""

costs = []

for i in range(num_iterations):

grads, cost = propagate(w, b, X, Y)

dw = grads["dw"]
db = grads["db"]

w = w - learning_rate * dw
b = b - learning_rate * db

#记录成本
if i % 100 == 0:
costs.append(cost)
#打印成本数据
if (print_cost) and (i % 100 == 0):
print("迭代的次数: %i , 误差值: %f" % (i,cost))

params = {
"w" : w,
"b" : b }
grads = {
"dw": dw,
"db": db }
return (params , grads , costs)

def predict(w , b , X ):
"""
使用学习逻辑回归参数logistic (w,b)预测标签是0还是1,

参数:
w - 权重,大小不等的数组(num_px * num_px * 3,1)
b - 偏差,一个标量
X - 维度为(num_px * num_px * 3,训练数据的数量)的数据

返回:
Y_prediction - 包含X中所有图片的所有预测【0 | 1】的一个numpy数组(向量)

"""

m = X.shape[1] #图片的数量
Y_prediction = np.zeros((1,m))
w = w.reshape(X.shape[0],1)

#计预测猫在图片中出现的概率
A = sigmoid(np.dot(w.T , X) + b)
for i in range(A.shape[1]):
#将概率a [0,i]转换为实际预测p [0,i]
Y_prediction[0,i] = 1 if A[0,i] > 0.5 else 0
#使用断言
assert(Y_prediction.shape == (1,m))

return Y_prediction

def model(X_train , Y_train , X_test , Y_test , num_iterations = 2000 , learning_rate = 0.5 , print_cost = False):
"""
通过调用之前实现的函数来构建逻辑回归模型

参数:
X_train - numpy的数组,维度为(num_px * num_px * 3,m_train)的训练集
Y_train - numpy的数组,维度为(1,m_train)(矢量)的训练标签集
X_test - numpy的数组,维度为(num_px * num_px * 3,m_test)的测试集
Y_test - numpy的数组,维度为(1,m_test)的(向量)的测试标签集
num_iterations - 表示用于优化参数的迭代次数的超参数
learning_rate - 表示optimize()更新规则中使用的学习速率的超参数
print_cost - 设置为true以每100次迭代打印成本

返回:
d - 包含有关模型信息的字典。
"""
w , b = initialize_with_zeros(X_train.shape[0])

parameters , grads , costs = optimize(w , b , X_train , Y_train,num_iterations , learning_rate , print_cost)

#从字典“参数”中检索参数w和b
w , b = parameters["w"] , parameters["b"]

#预测测试/训练集的例子
Y_prediction_test = predict(w , b, X_test)
Y_prediction_train = predict(w , b, X_train)

#打印训练后的准确性
print("训练集准确性:" , format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100) ,"%")
print("测试集准确性:" , format(100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100) ,"%")

d = {
"costs" : costs,
"Y_prediction_test" : Y_prediction_test,
"Y_prediciton_train" : Y_prediction_train,
"w" : w,
"b" : b,
"learning_rate" : learning_rate,
"num_iterations" : num_iterations }
return d

d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 2000, learning_rate = 0.005, print_cost = True)

#绘制图
costs = np.squeeze(d['costs'])
plt.plot(costs)
plt.ylabel('cost')
plt.xlabel('iterations (per hundreds)')
plt.title("Learning rate =" + str(d["learning_rate"]))
plt.show()

作业2 一层隐藏层的神经网络

用1层隐藏层的神经网络分类二维数据

欢迎来到第二次的编程作业。 现在是时候建立你的第一个神经网络了,它将具有一层隐藏层。 你将看到此模型与你使用逻辑回归实现的模型之间的巨大差异。

你将学到如何:

  • 实现具有单个隐藏层的2分类神经网络
  • 使用具有非线性激活函数的神经元,例如tanh
  • 计算交叉熵损失
  • 实现前向和后向传播

安装包

首先,请使用程序导入本次作业需要的包同时。请你设定随机种子保证和我的结果一致

  • numpy是Python科学计算的基本包。
  • sklearn提供了用于数据挖掘和分析的简单有效的工具。
  • matplotlib 是在Python中常用的绘制图形的库。
  • testCases提供了一些测试示例用以评估函数的正确性
  • planar_utils提供了此作业中使用的各种函数

提示:
seed(number) 用于指定随机数生成时所用算法开始的整数值。

  • 如果使用相同的number值,则每次生成的随即数都相同;
  • 如果不设置这个值,则系统根据时间来自己选择这个值,此时每次生成的随机数因时间差异而不同。
  • 设置的seed(number)值仅一次有效,每次使用新的seed(number)更换不同的number值
1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
import matplotlib.pyplot as plt
from testCases import *
import sklearn
import sklearn.datasets
import sklearn.linear_model
from planar_utils import plot_decision_boundary, sigmoid, load_planar_dataset, load_extra_datasets

#%matplotlib inline #如果你使用用的是Jupyter Notebook的话请取消注释。

np.random.seed(1) #设置一个固定的随机种子,以保证接下来的步骤中我们的结果是一致的。


数据集

首先,让我们获取处理的数据集。 请编写代码使用load_planar_dataset将“flower” 2分类数据集加载到变量 X 和 Y中。

提示:
您无需关注数据的加载和处理,我们将数据的处理和加载集成在planar_utils.py文件中,当你需要用到相关函数时,我们会给您提示,所以不必害怕。
在这里使用load_planar_dataset加载数据,X为数据,数据包含了每个点横坐标和纵坐标,Y为每个点的标签集合。
X有两行,第一行是点的横坐标,第二行是点的纵坐标

1
2
X, Y = load_planar_dataset()

很好现在请你使用matplotlib可视化数据集散点图

提示:plt.scatter(x,y,c=color,cmap=plt.cm.Spectral)
注意为了防止出错请你使用np.squeeze()把数据变成标量

1
2
3
plt.scatter(X[0, :], X[1, :], c=np.squeeze(Y), s=40, cmap=plt.cm.Spectral) #绘制散点图
plt.show()

数据看起来像是带有一些红色(标签y = 0)和一些蓝色(y = 1)点的“花”。 我们的目标是建立一个适合该数据的分类模型。

预期输出:

现在你有:

  • 包含特征(x1,x2)的numpy数组(矩阵)X
  • 包含标签(红色:0,蓝色:1)的numpy数组(向量)Y。

首先让我们深入地了解一下我们的数据。

练习:数据集中有多少个训练示例? 另外,变量“ X”和“ Y”的“shape”是什么?

现在请你使用编程输出数据集获得的X和标签Y的形状,以及确定训练集样本的个数
要求你在确定训练样本数量时用Y以及函数shape

提示:如何获得numpy数组的shape维度? (help)

1
2
3
4
5
6
7
8
shape_X = X.shape
shape_Y = Y.shape
m = Y.shape[1] # 训练集里面的数量

print ("X的维度为: " + str(shape_X))
print ("Y的维度为: " + str(shape_Y))
print ("数据集里面的数据有:" + str(m) + " 个")

预期输出:
X的维度为: (2, 400)
Y的维度为: (1, 400)
数据集里面的数据有:400个

简单Logistic回归

在构建完整的神经网络之前,首先让我们看看逻辑回归在此问题上的表现。在上一次编程中你已经手动编写了简单的逻辑回归 现在你可以使用sklearn的内置函数就可以实现这一功能。

请你编写代码以在数据集上训练逻辑回归分类器请使用sklearn的内置函数
首先请你使用sklearn.linear_model.LogisticRegressionCV()创建模型,模型命名为clf
自带的模型参数都是行向量,即clf以后在输入和输出都是一行代表一个样本
然后请你使用模型内的拟合函数fit训练模型,参数为训练集的输入和标签,参数要求每一行代表一个样本

1
2
3
clf = sklearn.linear_model.LogisticRegressionCV()
clf.fit(X.T,Y.T)

不要担心,现在请你编写程序绘制绘制决策边界,使用我们提供的函数
plot_decision_boundary(model, X, y) 您只需要提供模型的预测函数,样本数据,样本标签
plot_decision_boundary(model, X, y) 函数给模型的预测函数传入的是行向量即一行代表一个样本
请你对预测函数使用匿名,形式为lambda x: 匿名函数(x),匿名函数传入参数调用才能使用,您不必了解细致的过程,我们在plot_decision_boundary中会对该匿名函数进行调用

1
2
3
plot_decision_boundary(lambda x: clf.predict(x), X, Y) #绘制决策边界
plt.title("Logistic Regression") #图标题
plt.show()

预期输出:

请使用程序计算本次逻辑回归的准确性,预测使用LR_predictions接收结果 记得输入需要是行向量

1
2
3
4
LR_predictions  = clf.predict(X.T) #预测结果
print ("逻辑回归的准确性: %d " % float((np.dot(Y, LR_predictions) +
np.dot(1 - Y,1 - LR_predictions)) / float(Y.size) * 100) +
"% " + "(正确标记的数据点所占的百分比)")

预期输出:
逻辑回归的准确性: 47 % (正确标记的数据点所占的百分比)

说明:由于数据集不是线性可分类的,因此逻辑回归效果不佳。 让我们试试是否神经网络会做得更好吧!

神经网络模型

从上面我们可以得知Logistic回归不适用于“flower数据集”。现在你将训练带有单个隐藏层的神经网络。

这是我们的模型:

数学原理:

例如$x^{(i)}$:

根据所有的预测数据,你还可以如下计算损失$J$:

提示
建立神经网络的一般方法是:
1.定义神经网络结构(输入单元数,隐藏单元数等)。
2.初始化模型的参数
3.循环:

  • 实现前向传播
  • 计算损失
  • 后向传播以获得梯度
  • 更新参数(梯度下降)

我们通常会构建辅助函数来计算第1-3步,然后将它们合并为nn_model()函数。一旦构建了nn_model()并学习了正确的参数,就可以对新数据进行预测。

定义神经网络结构

现在请你定义一个函数layer_sizes(X , Y)用来计算各层的单元数并作为返回值

要求:定义三个变量:

  • n_x:输入层的大小
  • n_h:隐藏层的大小(将其设置为4)
  • n_y:输出层的大小

提示:使用shape来找到n_x和n_y。 另外,将隐藏层大小硬编码为4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def layer_sizes(X , Y):
"""
参数:
X - 输入数据集,维度为(输入的数量,训练/测试的数量)
Y - 标签,维度为(输出的数量,训练/测试数量)

返回:
n_x - 输入层的数量
n_h - 隐藏层的数量
n_y - 输出层的数量
"""
n_x = X.shape[0] #输入层
n_h = 4 #,隐藏层,硬编码为4
n_y = Y.shape[0] #输出层

return (n_x,n_h,n_y)

初始化模型的参数

现在请你定义一个函数initialize_parameters( n_x , n_h ,n_y)随机值初始化权重矩阵

要求

  • 请确保参数大小正确。 如果需要,也可参考上面的神经网络图。提示W1维度为(n_h,n_x)
  • 使用随机值初始化权重矩阵。
    • 使用:np.random.randn(a,b)* 0.01随机初始化维度为(a,b)的矩阵。
  • 将偏差向量初始化为零。

    • 使用:np.zeros((a,b)) 初始化维度为(a,b)零的矩阵。
  • 返回一个字典parameters保存W1,b1,W2,b2
    parameters = {“W1” : W1,
    “b1” : b1,
    “W2” : W2,
    “b2” : b2 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def initialize_parameters( n_x , n_h ,n_y):
"""
参数:
n_x - 输入层节点的数量
n_h - 隐藏层节点的数量
n_y - 输出层节点的数量

返回:
parameters - 包含参数的字典:
W1 - 权重矩阵,维度为(n_h,n_x)
b1 - 偏向量,维度为(n_h,1)
W2 - 权重矩阵,维度为(n_y,n_h)
b2 - 偏向量,维度为(n_y,1)

"""
np.random.seed(2) #指定一个随机种子,以便你的输出与我们的一样。
W1 = np.random.randn(n_h,n_x) * 0.01
b1 = np.zeros(shape=(n_h, 1))
W2 = np.random.randn(n_y,n_h) * 0.01
b2 = np.zeros(shape=(n_y, 1))

#使用断言确保我的数据格式是正确的
assert(W1.shape == ( n_h , n_x ))
assert(b1.shape == ( n_h , 1 ))
assert(W2.shape == ( n_y , n_h ))
assert(b2.shape == ( n_y , 1 ))

parameters = {"W1" : W1,
"b1" : b1,
"W2" : W2,
"b2" : b2 }

return parameters

循环

现在请你定义一个函数forward_propagation( X , parameters )实现前向传播

说明

  • 需要的输出A2以及cache
    cache = {“Z1”: Z1,
    “A1”: A1,
    “Z2”: Z2,
    “A2”: A2}
  • 在上方查看分类器的数学表示形式。
  • 你可以使用内置在笔记本中的sigmoid()函数以及numpy库中的np.tanh()函数。
  • 你也可以使用numpy库中的np.tanh()函数。
  • 必须执行以下步骤:
    1.使用parameters [“ ..”]从字典“ parameters”(这是initialize_parameters()的输出)中检索出每个参数。
    2.实现正向传播,计算$Z^{[1]}, A^{[1]}, Z^{[2]}$ 和 $A^{[2]}$ (所有训练数据的预测结果向量)。
  • 向后传播所需的值存储在cache中, cache将作为反向传播函数的输入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def forward_propagation( X , parameters ):
"""
参数:
X - 维度为(n_x,m)的输入数据。
parameters - 初始化函数(initialize_parameters)的输出

返回:
A2 - 使用sigmoid()函数计算的第二次激活后的数值
cache - 包含“Z1”,“A1”,“Z2”和“A2”的字典类型变量
"""
W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]
#前向传播计算A2
Z1 = np.dot(W1 , X) + b1
A1 = np.tanh(Z1)
Z2 = np.dot(W2 , A1) + b2
A2 = sigmoid(Z2)
#使用断言确保我的数据格式是正确的
assert(A2.shape == (1,X.shape[1]))
cache = {"Z1": Z1,
"A1": A1,
"Z2": Z2,
"A2": A2}

return (A2, cache)

现在,你已经计算了包含每个示例的 的 $\hat{A}^{[2]}$ (在Python变量“A2”中),其中,你可以计算损失函数 如下:

现在请你定义一个函数compute_cost(A2,Y,parameters)实现计算损失$J$

要求

  • 有很多种方法可以实现交叉熵损失。 我们为你提供了实现方法:
    :

实现代码为:
logprobs = np.multiply(np.log(A2),Y)
cost = - np.sum(logprobs) # 不需要使用循环就可以直接算出来。
你可以使用np.multiply()然后使用np.sum()或直接使用np.dot()。

  • 输出用cost变量代表损失$J$,注意将cost变为标量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def compute_cost(A2,Y,parameters):
"""
计算方程(6)中给出的交叉熵成本,

参数:
A2 - 使用sigmoid()函数计算的第二次激活后的数值
Y - "True"标签向量,维度为(1,数量)
parameters - 一个包含W1,B1,W2和B2的字典类型的变量

返回:
成本 - 交叉熵成本给出方程(13)
"""

m = Y.shape[1]
W1 = parameters["W1"]
W2 = parameters["W2"]

#计算成本
logprobs = logprobs = np.multiply(np.log(A2), Y) + np.multiply((1 - Y), np.log(1 - A2))
cost = - np.sum(logprobs) / m
cost = float(np.squeeze(cost))

assert(isinstance(cost,float))

return cost

现在,通过使用在正向传播期间计算的缓存,你可以实现后向传播。

请你编写反向传播函数backward_propagation(parameters,cache,X,Y) 获得梯度grads字典

grads =
{“dW1”: dW1,
“db1”: db1,
“dW2”: dW2,
“db2”: db2 }

你可以通过下面这张幻灯片来求梯度

说明
反向传播通常是深度学习中最难(最数学)的部分。为了帮助你更好地了解,我们提供了反向传播课程的幻灯片。你将要使用此幻灯片右侧的六个方程式以构建向量化实现。

  • 请注意,$*$ 表示元素乘法。
  • 你将使用在深度学习中很常见的编码表示方法:
    • dW1 = $\frac{\partial \mathcal{J} }{ \partial W_1 }$
    • db1 = $\frac{\partial \mathcal{J} }{ \partial b_1 }$
    • dW2 = $\frac{\partial \mathcal{J} }{ \partial W_2 }$
    • db2 = $\frac{\partial \mathcal{J} }{ \partial b_2 }$
  • 提示:
    • 要计算dZ1,你首先需要计算$g^{[1]’}(Z^{[1]})$。由于$g^{[1]}(.)$ 是tanh激活函数,因此如果$a = g^{[1]}(z)$ 则$g^{[1]’}(z) = 1-a^2$。所以你可以使用(1 - np.power(A1, 2))计算$g^{[1]’}(Z^{[1]})$。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def backward_propagation(parameters,cache,X,Y):
"""
使用上述说明搭建反向传播函数。

参数:
parameters - 包含我们的参数的一个字典类型的变量。
cache - 包含“Z1”,“A1”,“Z2”和“A2”的字典类型的变量。
X - 输入数据,维度为(2,数量)
Y - “True”标签,维度为(1,数量)

返回:
grads - 包含W和b的导数一个字典类型的变量。
"""
m = X.shape[1]

W1 = parameters["W1"]
W2 = parameters["W2"]

A1 = cache["A1"]
A2 = cache["A2"]

dZ2= A2 - Y
dW2 = (1 / m) * np.dot(dZ2, A1.T)
db2 = (1 / m) * np.sum(dZ2, axis=1, keepdims=True)
dZ1 = np.multiply(np.dot(W2.T, dZ2), 1 - np.power(A1, 2))
dW1 = (1 / m) * np.dot(dZ1, X.T)
db1 = (1 / m) * np.sum(dZ1, axis=1, keepdims=True)
grads = {"dW1": dW1,
"db1": db1,
"dW2": dW2,
"db2": db2 }

return grads

请你编写update_parameters(parameters,grads,learning_rate=1.2)来更新参数w和b
提示
必须使用(dW1,db1,dW2,db2)才能更新(W1,b1,W2,b2)。

$\theta = \theta - \alpha \frac{\partial J }{ \partial \theta }$其中$\alpha$是学习率,而$\theta$ 代表一个参数。

图示:具有良好的学习速率(收敛)和较差的学习速率(发散)的梯度下降算法。 图片由Adam Harley提供。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def update_parameters(parameters,grads,learning_rate=1.2):
"""
使用上面给出的梯度下降更新规则更新参数

参数:
parameters - 包含参数的字典类型的变量。
grads - 包含导数值的字典类型的变量。
learning_rate - 学习速率

返回:
parameters - 包含更新参数的字典类型的变量。
"""
W1,W2 = parameters["W1"],parameters["W2"]
b1,b2 = parameters["b1"],parameters["b2"]

dW1,dW2 = grads["dW1"],grads["dW2"]
db1,db2 = grads["db1"],grads["db2"]

W1 = W1 - learning_rate * dW1
b1 = b1 - learning_rate * db1
W2 = W2 - learning_rate * dW2
b2 = b2 - learning_rate * db2

parameters = {"W1": W1,
"b1": b1,
"W2": W2,
"b2": b2}

return parameters

在nn_model()中集成函数

使用已经编写的函数在nn_model()中组建神经网络模型,函数为nn_model(X,Y,n_h,num_iterations,print_cost=False)

提示:神经网络模型必须以正确的顺序组合先前构建的函数。

  • 使用layer_sizes(X, Y)获得n_x,n_y得到各单元数
  • 初始化参数initialize_parameters()得到字典parameters,包含了w和b
  • 循环:
    • 前向传播
    • 计算损失
    • 后向传播
    • 更新参数
  • 返回字典parameters,包含了w和b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def nn_model(X,Y,n_h,num_iterations,print_cost=False):
"""
参数:
X - 数据集,维度为(2,示例数)
Y - 标签,维度为(1,示例数)
n_h - 隐藏层的数量
num_iterations - 梯度下降循环中的迭代次数
print_cost - 如果为True,则每1000次迭代打印一次成本数值

返回:
parameters - 模型学习的参数,它们可以用来进行预测。
"""

np.random.seed(3) #指定随机种子
n_x = layer_sizes(X, Y)[0]
n_y = layer_sizes(X, Y)[2]

parameters = initialize_parameters(n_x,n_h,n_y)
W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]

for i in range(num_iterations):
A2 , cache = forward_propagation(X,parameters)
cost = compute_cost(A2,Y,parameters)
grads = backward_propagation(parameters,cache,X,Y)
parameters = update_parameters(parameters,grads,learning_rate = 0.5)

if print_cost:
if i%1000 == 0:
print("第 ",i," 次循环,成本为:"+str(cost))
return parameters

预测

现在你已经有了训练的模型,训练参数得到好的权重值就可以使用前向传播来预测了
现在请你编写predict(parameters,X)来预测,返回值predictions

要求
使用正向传播来预测结果。
使用np.round()对结果四舍五入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def predict(parameters,X):
"""
使用学习的参数,为X中的每个示例预测一个类

参数:
parameters - 包含参数的字典类型的变量。
X - 输入数据(n_x,m)

返回
predictions - 我们模型预测的向量(红色:0 /蓝色:1)

"""
A2 , cache = forward_propagation(X,parameters)
predictions = np.round(A2)

return predictions

现在你可以开始使用给模型传入参数训练模型,来获得参数parameters,然后进行预测,计算准确率

要求:

  • 训练模型获得parameters
  • 绘制边界plot_decision_boundary()
  • 计算预测值predictions并计算准确率
    提示:

  • plot_decision_boundary(model, X, y) 函数给模型的匿名预测函数predict(parameters,X)传入的是行向量即一行代表一个样本

  • 而predict(parameters,X)本身接受的X是列向量即一列代表一个样本
    现在运行模型以查看其如何在二维数据集上运行。 运行以下代码以使用含有$n_h$隐藏单元的单个隐藏层测试模型。
1
2
3
4
5
6
7
8
9
parameters = nn_model(X, Y, n_h = 4, num_iterations=10000, print_cost=True)

#绘制边界
plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
plt.title("Decision Boundary for hidden layer size " + str(4))
plt.show()
predictions = predict(parameters, X)
print ('准确率: %d' % float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100) + '%')

预期输出:
第 0 次循环,成本为:0.6930480201239823
第 1000 次循环,成本为:0.28808329356901835
第 2000 次循环,成本为:0.25438549407324496
第 3000 次循环,成本为:0.23386415038952196
第 4000 次循环,成本为:0.22679248744854008
第 5000 次循环,成本为:0.22264427549299015
第 6000 次循环,成本为:0.21973140404281316
第 7000 次循环,成本为:0.21750365405131294
第 8000 次循环,成本为:0.21950396469467315
第 9000 次循环,成本为:0.2185709575018246
准确率: 90%

与Logistic回归相比,准确性确实更高。 该模型学习了flower的叶子图案! 与逻辑回归不同,神经网络甚至能够学习非线性的决策边界。

现在,让我们尝试几种不同的隐藏层大小。

调整隐藏层大小(可选练习)

运行以下代码(可能需要1-2分钟), 你将观察到不同大小隐藏层的模型的不同表现。

要求

  • 创建一个新的图形窗口,设置图形的尺寸为 16 x 32 英寸。这会提供足够的空间来绘制多个子图。
  • 定义一个列表 hidden_layer_sizes,来设置隐藏层的不同数量为1, 2, 3, 4, 5, 20, 50
  • 使用 enumerate 函数遍历 hidden_layer_sizes 列表给i和n_h。i 是当前索引,n_h 是当前的隐藏层节点数量。
  • 循环
    • 使用plt.subplot(Rows,columns, index)创建一个 5 行 2 列的子图布局,并在第 i + 1 个位置添加当前的子图。
    • 使用plt.title为当前子图设置标题,标题显示当前隐藏层的节点数量
    • 调用 nn_model 函数来训练神经网络模型迭代5000次
    • 调用 plot_decision_boundary 函数来绘制决策边界
    • 使用训练得到的参数 parameters 对输入特征 X 进行预测,得到模型的预测结果,结果存储在 predictions 中
    • 计算模型的准确率
  • 显示图像
1
2
3
4
5
6
7
8
9
10
11
12
plt.figure(figsize=(16, 32))
hidden_layer_sizes = [1, 2, 3, 4, 5, 20, 50] #隐藏层数量
for i, n_h in enumerate(hidden_layer_sizes):
plt.subplot(5, 2, i + 1)
plt.title('Hidden Layer of size %d' % n_h)
parameters = nn_model(X, Y, n_h, num_iterations=5000)
plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
predictions = predict(parameters, X)
accuracy = float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100)
print ("隐藏层的节点数量: {} ,准确率: {} %".format(n_h, accuracy))
plt.show()

预期输出

隐藏层的节点数量: 1 ,准确率: 67.25 %
隐藏层的节点数量: 2 ,准确率: 66.5 %
隐藏层的节点数量: 3 ,准确率: 89.25 %
隐藏层的节点数量: 4 ,准确率: 90.0 %
隐藏层的节点数量: 5 ,准确率: 89.75 %
隐藏层的节点数量: 20 ,准确率: 90.0 %
隐藏层的节点数量: 50 ,准确率: 89.75 %

说明

  • 较大的模型(具有更多隐藏的单元)能够更好地拟合训练集,直到最终最大的模型过拟合数据为止。
  • 隐藏层的最佳大小似乎在n_h = 5左右。的确,此值似乎很好地拟合了数据,而又不会引起明显的过度拟合。
  • 稍后你还将学习正则化,帮助构建更大的模型(例如n_h = 50)而不会过度拟合。

可选问题

注意

如果你愿意,可以探索一些可选的问题:

  • 将tanh激活函数更改为sigmoid或ReLU会发生什么?
  • 调整学习率会发生什么?
  • 如果我们更改数据集该怎么办? (请参阅下面的第5部分!)

你学习了以下几点:

  • 建立具有隐藏层的完整神经网络
  • 善用非线性单位
  • 实现正向传播和反向传播,并训练神经网络
  • 了解不同隐藏层大小(包括过度拟合)的影响。

模型在其他数据集上的性能

如果需要,你可以使用其他的数据集,以下是对其他五个数据集的加载,你可以不用考虑处理过程
使用: 你只需要将程序最前面的加载数据部分X, Y = load_planar_dataset()删除替换为下面的代码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Datasets
noisy_circles, noisy_moons, blobs, gaussian_quantiles, no_structure = load_extra_datasets()

datasets = {"noisy_circles": noisy_circles,
"noisy_moons": noisy_moons,
"blobs": blobs,
"gaussian_quantiles": gaussian_quantiles}

# 你可以更改dataset的值选择你喜欢的数据集
dataset = "gaussian_quantiles"
# 提取输入X和标签Y

X, Y = datasets[dataset]
X, Y = X.T, Y.reshape(1, Y.shape[0])

# 确保标签Y为二进制即只要0和1
if dataset == "blobs":
Y = Y%2

# 可视化数据使用散点图
plt.scatter(X[0, :], X[1, :], c=Y.reshape(X[0,:].shape), s=40, cmap=plt.cm.Spectral);

预期输出:
隐藏层的节点数量: 1 ,准确率: 65.0 %
隐藏层的节点数量: 2 ,准确率: 78.5 %
隐藏层的节点数量: 3 ,准确率: 99.0 %
隐藏层的节点数量: 4 ,准确率: 99.0 %
隐藏层的节点数量: 5 ,准确率: 98.5 %
隐藏层的节点数量: 20 ,准确率: 99.0 %
隐藏层的节点数量: 50 ,准确率: 99.0 %

恭喜你完成了此编程作业!

参考:

作业3深度神经网络的实现,初始化,正向传播,反向传播

逐步构建你的深度神经网络

欢迎来到第4周作业的第1部分!在此之前你已经训练了一个2层的神经网络(只有一个隐藏层)。本周,你将学会构建一个任意层数的深度神经网络!

  • 在此作业中,你将实现构建深度神经网络所需的所有函数。
  • 在下一个作业中,你将使用这些函数来构建一个用于图像分类的深度神经网络。

完成此任务后,你将能够:

  • 使用ReLU等非线性单位来改善模型
  • 建立更深的神经网络(具有1个以上的隐藏层)
  • 实现一个易于使用的神经网络类

符号说明

  • 上标$[l]$ 表示与$l^{th}$ 层相关的数量。
    • 示例:$a^{[L]}$ 是$L^{th}$ 层的激活。 $W^{[L]}$ 和$b^{[L]}$是$L^{th}$层参数。
  • 上标$(i)$ 表示与$i^{th}$示例相关的数量。
    • 示例:$x^{(i)}$是第$i^{th}$ 的训练数据。
  • 下标$i$ 表示$i^{th}$的向量。
    • 示例:$a^{[l]}_i$ 表示$l^{th}$ 层激活的$i^{th}$ 输入。

让我们开始吧!

安装包

让我们首先导入作业过程中需要的所有包。

  • numpy是Python科学计算的基本包。
  • matplotlib是在Python中常用的绘制图形的库。
  • dnn_utils提供了一些必要的函数。(作业提供)
  • testCases提供了一些测试用例来评估函数的正确性(作业提供)
  • np.random.seed(1)使所有随机函数调用保持一致
1
2
3
4
5
6
7
import numpy as np
import h5py
import matplotlib.pyplot as plt
import testCases #参见资料包,或者在文章底部copy
from dnn_utils import sigmoid, sigmoid_backward, relu, relu_backward #参见资料包
import lr_utils #参见资料包,或者在文章底部copy

作业大纲

为了构建你的神经网络,你将实现几个“辅助函数”。这些辅助函数将在下一个作业中使用,用来构建一个两层神经网络和一个L层的神经网络。你将实现的每个函数都有详细的说明,这些说明将指导你完成必要的步骤。此作业的大纲如下:

  • 初始化两层的网络和$L$层的神经网络的参数。
  • 实现正向传播模块(在下图中以紫色显示)。
    • 完成模型正向传播步骤的LINEAR部分($Z^{[l]}$)。
    • 提供使用的ACTIVATION函数(relu / Sigmoid)。
    • 将前两个步骤合并为新的[LINEAR-> ACTIVATION]前向函数。
    • 堆叠[LINEAR-> RELU]正向函数L-1次(第1到L-1层),并在末尾添加[LINEAR-> SIGMOID](最后的$L$层)。这合成了一个新的L_model_forward函数。
  • 计算损失。
  • 实现反向传播模块(在下图中以红色表示)。
    • 完成模型反向传播步骤的LINEAR部分。
    • 提供的ACTIVATE函数的梯度(relu_backward / sigmoid_backward)
    • 将前两个步骤组合成新的[LINEAR-> ACTIVATION]反向函数。
    • 将[LINEAR-> RELU]向后堆叠L-1次,并在新的L_model_backward函数中后向添加[LINEAR-> SIGMOID]
  • 最后更新参数。

图1

注意:对于每个正向函数,都有一个对应的反向函数。 这也是为什么在正向传播模块的每一步都将一些值存储在缓存中的原因。缓存的值可用于计算梯度。 然后,在反向传导模块中,你将使用缓存的值来计算梯度。 此作业将指导说明如何执行这些步骤。

初始化

首先编写两个辅助函数用来初始化模型的参数。 第一个函数将用于初始化两层模型的参数。 第二个将把初始化过程推广到$L$层模型上。

2层神经网络

编写函数initialize_parameters(n_x,n_h,n_y)创建并初始化2层神经网络的参数。

提示

  • 模型的结构为:LINEAR -> RELU -> LINEAR -> SIGMOID
  • 随机初始化权重矩阵。 确保准确的维度,使用np.random.randn(shape)* 0.01
  • 将偏差初始化为0。 使用np.zeros(shape)
  • 返回字典parameters = {“W1”: W1,
    “b1”: b1,
    “W2”: W2,
    “b2”: b2}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    def initialize_parameters(n_x,n_h,n_y):
    """
    此函数是为了初始化两层网络参数而使用的函数。
    参数:
    n_x - 输入层节点数量
    n_h - 隐藏层节点数量
    n_y - 输出层节点数量

    返回:
    parameters - 包含你的参数的python字典:
    W1 - 权重矩阵,维度为(n_h,n_x)
    b1 - 偏向量,维度为(n_h,1)
    W2 - 权重矩阵,维度为(n_y,n_h)
    b2 - 偏向量,维度为(n_y,1)

    """
    W1 = np.random.randn(n_h, n_x) * 0.01
    b1 = np.zeros((n_h, 1))
    W2 = np.random.randn(n_y, n_h) * 0.01
    b2 = np.zeros((n_y, 1))

    #使用断言确保我的数据格式是正确的
    assert(W1.shape == (n_h, n_x))
    assert(b1.shape == (n_h, 1))
    assert(W2.shape == (n_y, n_h))
    assert(b2.shape == (n_y, 1))

    parameters = {"W1": W1,
    "b1": b1,
    "W2": W2,
    "b2": b2}

    return parameters

L层神经网络

更深的L层神经网络的初始化更加复杂,因为存在更多的权重矩阵和偏差向量。 完成 initialize_parameters_deep后,应确保各层之间的维度匹配。 回想一下,$n^{[l]}$是$l$层中的神经元数量。 因此,如果我们输入的 $X$ 的大小为$(12288, 209)$(以$m=209$为例),则:

Shape of WShape of bActivationShape of Activation
Layer 1$(n^{[1]},12288)$$(n^{[1]},1)$$Z^{[1]} = W^{[1]} X + b^{[1]} $$(n^{[1]},209)$
Layer 2$(n^{[2]}, n^{[1]})$$(n^{[2]},1)$$Z^{[2]} = W^{[2]} A^{[1]} + b^{[2]}$$(n^{[2]}, 209)$
$\vdots$$\vdots$$\vdots$$\vdots$$\vdots$
Layer L-1$(n^{[L-1]}, n^{[L-2]})$$(n^{[L-1]}, 1)$$Z^{[L-1]} = W^{[L-1]} A^{[L-2]} + b^{[L-1]}$$(n^{[L-1]}, 209)$
Layer L$(n^{[L]}, n^{[L-1]})$$(n^{[L]}, 1)$$Z^{[L]} = W^{[L]} A^{[L-1]} + b^{[L]}$$(n^{[L]}, 209)$

规律:
A - 来自上一层(或输入数据)的激活,维度为(上一层的节点数量,示例的数量)
W - 权重矩阵,numpy数组,维度为(当前图层的节点数量,前一图层的节点数量)
b - 偏向量,numpy向量,维度为(当前图层节点数量,1)
当我们在python中计算$W X + b$时,使用广播,比如:

Then $WX + b$ will be:

编写函数initialize_parameters_deep(layers_dims)实现L层神经网络的初始化。

提示

  • 模型的结构为 [LINEAR -> RELU] $ \times$ (L-1) -> LINEAR -> SIGMOID。也就是说,$L-1$层使用ReLU作为激活函数,最后一层采用sigmoid激活函数输出。
  • 随机初始化权重矩阵。使用np.random.rand(shape)* 0.01
  • 零初始化偏差。使用np.zeros(shape)
  • 我们将在不同的layer_dims变量中存储$n^{[l]}$,即不同层中的神经元数。例如,上周“二维数据分类模型”的layer_dims为[2,4,1]:即有两个输入,一个隐藏层包含4个隐藏单元,一个输出层包含1个输出单元。因此,W1的维度为(4,2),b1的维度为(4,1),W2的维度为(1,4),而b2的维度为(1,1)。现在你将把它应用到$L$层!
  • 这是$L=1$ (一层神经网络)的实现。以启发你如何实现通用的神经网络(L层神经网络)。
  • layers_dims是一个列表,第一个值代表输入层,第二个值代表第一层隐藏层…第L个值代表输出层
  • 返回字典parameters
    parameters - 包含参数“W1”,“b1”,…,“WL”,“bL”的字典:
    W1 - 权重矩阵,维度为(layers_dims [1],layers_dims [1-1])
    bl - 偏向量,维度为(layers_dims [1],1)
    W2 - 权重矩阵,维度为(layers_dims [2],layers_dims [2-1])
    b2 - 偏向量,维度为(layers_dims [2],1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def initialize_parameters_deep(layers_dims):
"""
此函数是为了初始化多层网络参数而使用的函数。
参数:
layers_dims - 包含我们网络中每个图层的节点数量的列表

返回:
parameters - 包含参数“W1”,“b1”,...,“WL”,“bL”的字典:
W1 - 权重矩阵,维度为(layers_dims [1],layers_dims [1-1])
bl - 偏向量,维度为(layers_dims [1],1)
"""
np.random.seed(3)
parameters = {}
L = len(layers_dims)

for l in range(1,L):
parameters["W" + str(l)] = np.random.randn(layers_dims[l], layers_dims[l - 1]) / np.sqrt(layers_dims[l - 1])

parameters["b" + str(l)] = np.zeros((layers_dims[l], 1))

#确保我要的数据的格式是正确的
assert(parameters["W" + str(l)].shape == (layers_dims[l], layers_dims[l-1]))
assert(parameters["b" + str(l)].shape == (layers_dims[l], 1))

return parameters

正向传播模块

线性正向

现在,你已经初始化了参数,接下来将执行正向传播模块。 首先实现一些基本函数,用于稍后的模型实现。按以下顺序完成三个函数:

  • LINEAR
  • LINEAR -> ACTIVATION,其中激活函数采用ReLU或Sigmoid。
  • [LINEAR -> RELU] $\times$ (L-1) -> LINEAR -> SIGMOID(整个模型)

线性正向模块(在所有数据中均进行向量化)的计算按照以下公式:

$Z^{[l]} = W^{[l]}A^{[l-1]} +b^{[l]}\tag{4}$

其中 $A^{[0]} = X$.

编写函数linear_forward(A,W,b)建立正向传播的线性部分

提示

  • 该单元的数学表示为 $Z^{[l]} = W^{[l]}A^{[l-1]} +b^{[l]}$,你可能会发现np.dot()有用。 如果维度不匹配,则可以printW.shape查看修改。
  • 返回值为Z,cache 其中cache = (A, W, b)是一个元组将三个值绑定,你可以通过cache[0]访问A,cache[1]访问W,cache[2]访问b
    A - 上一层(或输入数据)的激活(当前层的输入),维度为(上一层的节点数量,示例的数量)
    W - 权重矩阵,numpy数组,维度为(当前图层的节点数量,前一图层的节点数量)
    b - 偏向量,numpy向量,维度为(当前图层节点数量,1)
    Z - 当前层的未激活值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    def linear_forward(A,W,b):
    """
    实现前向传播的线性部分。

    参数:
    A - 来自上一层(或输入数据)的激活,维度为(上一层的节点数量,示例的数量)
    W - 权重矩阵,numpy数组,维度为(当前图层的节点数量,前一图层的节点数量)
    b - 偏向量,numpy向量,维度为(当前图层节点数量,1)

    返回:
    Z - 激活功能的输入,也称为预激活参数
    cache - 一个包含“A”,“W”和“b”的元组,存储这些变量以有效地计算后向传递
    """
    Z = np.dot(W,A) + b
    assert(Z.shape == (W.shape[0],A.shape[1]))
    cache = (A,W,b)

    return Z,cache

正向线性激活

你将使用两个激活函数:

  • Sigmoid:$\sigma(Z) = \sigma(W A + b) = \frac{1}{ 1 + e^{-(W A + b)}}$。 我们为你提供了“ Sigmoid”函数。 该函数返回两项值:激活值”a“和包含”Z“的”cache“(这是我们将馈入到相应的反向函数的内容)。 你可以按下述方式得到两项值:
  • ReLU:ReLu的数学公式为$A = RELU(Z) = max(0, Z)$。我们为你提供了relu函数。 该函数返回两项值:激活值“A”和包含“Z”的“cache”(这是我们将馈入到相应的反向函数的内容)。 你可以按下述方式得到两项值:

为了更加方便,我们把两个函数(线性和激活)组合为一个函数(LINEAR-> ACTIVATION)。 因此,我们将实现一个函数用以执行LINEAR正向步骤和ACTIVATION正向步骤。

编写函数linear_activation_forward(A_prev,W,b,activation)实现 LINEAR->ACTIVATION 层的正向传播

提示

  • 实现 LINEAR->ACTIVATION 层的正向传播。 数学表达式为:$A^{[l]} = g(Z^{[l]}) = g(W^{[l]}A^{[l-1]} +b^{[l]})$,其中激活”g” 可以是sigmoid()或relu()。 使用linear_forward()和正确的激活函数。
  • 我们为您提供的激活函数sigmoid(Z)和relu(Z)返回值是A和cache,A:Z的激活值,cache:(Z) 元组
  • 参数:

    • A_prev - 来自上一层(或输入层)的激活,维度为(上一层的节点数量,示例数)
    • W - 权重矩阵,numpy数组,维度为(当前层的节点数量,前一层的大小)
    • b - 偏向量,numpy阵列,维度为(当前层的节点数量,1)
    • activation - 选择在此层中使用的激活函数名,字符串类型,【”sigmoid” | “relu”】
  • 返回值:

    • A - 激活函数的输出,也称为激活后的值
    • cache - 一个包含“linear_cache”和“activation_cache”的元组,我们需要存储它以有效地计算后向传递
      • 其中 linear_cache为linear_forward函数的返回linear_cache = (A_prev, W, b)
      • 其中 activation_cache为sigmoid或relu的返回activation_cache = (Z)
      • 所以cache 为一个元组cache = (linear_cache,activation_cache)
        • linear_cache = (A_prev, W, b,) activation_cache = (Z) A_prev是前一层的激活,W, b,Z是当前层的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def linear_activation_forward(A_prev,W,b,activation):
"""
实现LINEAR-> ACTIVATION 这一层的前向传播

参数:
A_prev - 来自上一层(或输入层)的激活,维度为(上一层的节点数量,示例数)
W - 权重矩阵,numpy数组,维度为(当前层的节点数量,前一层的大小)
b - 偏向量,numpy阵列,维度为(当前层的节点数量,1)
activation - 选择在此层中使用的激活函数名,字符串类型,【"sigmoid" | "relu"】

返回:
A - 激活函数的输出,也称为激活后的值
cache - 一个包含“linear_cache”和“activation_cache”的元组,我们需要存储它以有效地计算后向传递
"""

if activation == "sigmoid":
Z, linear_cache = linear_forward(A_prev, W, b)
A, activation_cache = sigmoid(Z)
elif activation == "relu":
Z, linear_cache = linear_forward(A_prev, W, b)
A, activation_cache = relu(Z)

assert(A.shape == (W.shape[0],A_prev.shape[1]))
cache = (linear_cache,activation_cache)

return A,cache

注意:在深度学习中,”[LINEAR->ACTIVATION]”计算被视为神经网络中的单个层,而不是两个层。

L层模型

为了方便实现$L$层神经网络,你将需要一个函数来复制前一个函数(使用RELU的linear_activation_forward)$L-1$次,以及复制带有SIGMOID的linear_activation_forward

图2 : [LINEAR -> RELU] $\times$ (L-1) -> LINEAR -> SIGMOID 模型

编写函数L_model_forward(X,parameters)实现上述模型L层的正向传播

说明:在下面的代码中,变量AL表示$A^{[L]} = \sigma(Z^{[L]}) = \sigma(W^{[L]} A^{[L-1]} + b^{[L]})$(有时也称为Yhat,即$\hat{Y}$)

提示

  • 使用你先前编写的函数
  • 使用for循环复制[LINEAR-> RELU](L-1)次
  • 不要忘记在“cache”列表中更新缓存。 要将新值 c添加到list中,可以使用list.append(c)使用caches接收。
  • parameters是包含参数“W1”,“b1”,…,“WL”,“bL”的字典
  • 层数可以用L = len(parameters) // 2 获得,L-1个层用RELU,一层用SIGMOID
  • 返回AL,caches
    • AL:第L层的激活即预测值
    • caches:包含以下内容的cache列表: (linear_cache,activation_cache)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def L_model_forward(X,parameters):
"""
实现[LINEAR-> RELU] *(L-1) - > LINEAR-> SIGMOID计算前向传播,也就是多层网络的前向传播,为后面每一层都执行LINEAR和ACTIVATION

参数:
X - 数据,numpy数组,维度为(输入节点数量,示例数)
parameters - initialize_parameters_deep()的输出

返回:
AL - 最后的激活值
caches - 包含以下内容的缓存列表:
linear_relu_forward()的每个cache(有L-1个,索引为从0到L-2)
linear_sigmoid_forward()的cache(只有一个,索引为L-1)
"""
caches = []
A = X
L = len(parameters) // 2
for l in range(1,L):
A_prev = A
A, cache = linear_activation_forward(A_prev, parameters['W' + str(l)], parameters['b' + str(l)], "relu")
caches.append(cache)

AL, cache = linear_activation_forward(A, parameters['W' + str(L)], parameters['b' + str(L)], "sigmoid")
caches.append(cache)

assert(AL.shape == (1,X.shape[1]))

return AL,caches

现在,你有了一个完整的正向传播模块,它接受输入X并输出包含预测的行向量$A^{[L]}$。 它还将所有中间值记录在”caches”中以计算预测的损失值。

损失函数

现在,你将实现模型的正向和反向传播。 你需要计算损失,以检查模型是否在学习。

编写函数compute_cost(AL,Y)计算交叉熵损失$J$
提示

  • 使用以下公式计算交叉熵损失$J$:
  • 提示函数np.multiply(np.log(AL),Y)
  • 返回cost值,记得标量化cost
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def compute_cost(AL,Y):
"""
实施等式(4)定义的成本函数。

参数:
AL - 与标签预测相对应的概率向量,维度为(1,示例数量)
Y - 标签向量(例如:如果不是猫,则为0,如果是猫则为1),维度为(1,数量)

返回:
cost - 交叉熵成本
"""
m = Y.shape[1]
cost = -np.sum(np.multiply(np.log(AL),Y) + np.multiply(np.log(1 - AL), 1 - Y)) / m

cost = np.squeeze(cost)
assert(cost.shape == ())

return cost

反向传播模块

就像正向传播一样,你将实现辅助函数以进行反向传播。 请记住,反向传播用于计算损失函数相对于参数的梯度。

提醒

图3
LINEAR->RELU->LINEAR->SIGMOID 的正向和反向传播,紫色块代表正向传播,红色块代表反向传播。

对于那些精通微积分的人(不必进行此作业),可以使用微积分的链式规则来得出2层网络中的损失 $\mathcal{L}$相对于 $z^{[1]}$的导数,如下所示:

为了计算梯度$dW^{[1]} = \frac{\partial L}{\partial W^{[1]}}$,请使用链规则,然后执行$dW^{[1]} = dz^{[1]} \times \frac{\partial z^{[1]} }{\partial W^{[1]}}$。 在反向传播的每个步骤中,你都将当前梯度乘以对应层的梯度,以获得所需的梯度。

同样地,为了计算梯度$db^{[1]} = \frac{\partial L}{\partial b^{[1]}}$,你使用前一个链规则,然后执行$db^{[1]} = dz^{[1]} \times \frac{\partial z^{[1]} }{\partial b^{[1]}}$。

这也是为什么我们称之为反向传播

现在,类似于正向传播,你将分三个步骤构建反向传播:

  • LINEAR backward
  • LINEAR -> ACTIVATION backward,其中激活函数使用ReLU或sigmoid 的导数计算
  • [LINEAR -> RELU] $\times$ (L-1) -> LINEAR -> SIGMOID backward(整个模型)

线性反向

对于层$l$,线性部分为:$Z^{[l]} = W^{[l]} A^{[l-1]} + b^{[l]}$(之后是激活)。

假设你已经计算出导数$dZ^{[l]} = \frac{\partial \mathcal{L} }{\partial Z^{[l]}}$。 你想获得$(dW^{[l]}, db^{[l]} dA^{[l-1]})$。

编写函数linear_backward(dZ,cache)实现单层的线性反向传播,返回值dA_prev,dW, db

提示

  • cache - 来自当前层前向传播的值的元组cache =(A_prev,W,b)
  • dZ - 相对于(当前第l层的)线性输出的成本梯度Z
  • dA_prev - 相对于激活(前一层l-1)的成本梯度,与A_prev维度相同
  • dW - 相对于W(当前层l)的成本梯度,与W的维度相同
  • db - 相对于b(当前层l)的成本梯度,与b维度相同
  • 使用输入$dZ^{[l]}$计算三个输出$(dW^{[l]}, db^{[l]}, dA^{[l]})$。以下是所需的公式:

图4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def linear_backward(dZ,cache):
"""
为单层实现反向传播的线性部分(第L层)

参数:
dZ - 相对于(当前第l层的)线性输出的成本梯度
cache - 来自当前层前向传播的值的元组(A_prev,W,b)

返回:
dA_prev - 相对于激活(前一层l-1)的成本梯度,与A_prev维度相同
dW - 相对于W(当前层l)的成本梯度,与W的维度相同
db - 相对于b(当前层l)的成本梯度,与b维度相同
"""
A_prev, W, b = cache
m = A_prev.shape[1]
dW = np.dot(dZ, A_prev.T) / m
db = np.sum(dZ, axis=1, keepdims=True) / m
dA_prev = np.dot(W.T, dZ)

assert (dA_prev.shape == A_prev.shape)
assert (dW.shape == W.shape)
assert (db.shape == b.shape)

return dA_prev, dW, db

反向线性激活

接下来,创建一个合并两个辅助函数的函数:linear_backward 和反向步骤的激活 linear_activation_backward

为了帮助你实现linear_activation_backward,我们提供了两个反向函数:

  • sigmoid_backward:实现SIGMOID单元的反向传播。 你可以这样使用:

    • dZ = sigmoid_backward(dA, activation_cache)
  • relu_backward:实现RELU单元的反向传播。 你可以这样使用:

    • dZ = relu_backward(dA, activation_cache)

如果$g(.)$是激活函数,
sigmoid_backwardrelu_backward计算

接下来请编写函数linear_activation_backward(dA,cache,activation=relu)实现单层的激活反向传播
提示

  • 参数:
    • dA - 当前层l的激活后的梯度值
    • cache - 我们存储的用于有效计算反向传播的值的元组值为(linear_cache,activation_cache)
      • cache 为一个元组cache = (linear_cache,activation_cache)
      • linear_cache = (A_prev, W, b,) activation_cache = (Z) A_prev是前一层的激活,W, b,Z是当前层的
    • activation - 要在此层中使用的激活函数名,字符串类型,【”sigmoid” | “relu”】
  • 返回:
    • dA_prev - 相对于激活(前一层l-1)的成本梯度值,与A_prev维度相同
    • dW - 相对于W(当前层l)的成本梯度值,与W的维度相同
    • db - 相对于b(当前层l)的成本梯度值,与b的维度相同
  • dZ通过relu_backward(dA, cache)函数获得或者sigmoid_backward(dA,cache)获得。 其中cache应该是正向激活的缓存即Z
  • dA_prev, dW, db通过linear_backward(dZ, cache)。其中cache应该是正向线性的缓存即(A_prev, W, b,)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def linear_activation_backward(dA,cache,activation="relu"):
    """
    实现LINEAR-> ACTIVATION层的后向传播。

    参数:
    dA - 当前层l的激活后的梯度值
    cache - 我们存储的用于有效计算反向传播的值的元组(值为linear_cache,activation_cache)
    activation - 要在此层中使用的激活函数名,字符串类型,【"sigmoid" | "relu"】
    返回:
    dA_prev - 相对于激活(前一层l-1)的成本梯度值,与A_prev维度相同
    dW - 相对于W(当前层l)的成本梯度值,与W的维度相同
    db - 相对于b(当前层l)的成本梯度值,与b的维度相同
    """
    linear_cache, activation_cache = cache
    if activation == "relu":
    dZ = relu_backward(dA, activation_cache)
    dA_prev, dW, db = linear_backward(dZ, linear_cache)
    elif activation == "sigmoid":
    dZ = sigmoid_backward(dA, activation_cache)
    dA_prev, dW, db = linear_backward(dZ, linear_cache)

    return dA_prev,dW,db

反向L层模型

现在,你将为整个网络实现反向传播函数。 回想一下,当你实现L_model_forward函数时,在每次迭代中,你都存储了一个包含(A,W,b和Z)的缓存,A是前一层的。 在反向传播模块中,你将使用这些变量来计算梯度。 因此,在L_model_backward函数中,你将从$L$层开始向后遍历所有隐藏层。在每个步骤中,你都将使用$l$层的缓存值反向传播到层$l$。 图5展示了反向传播过程。

图5:反向流程

初始化反向传播
为了使网络反向传播,我们知道输出是
$A^{[L]} = \sigma(Z^{[L]})$。因此,你的代码需要计算dAL $= \frac{\partial \mathcal{L}}{\partial A^{[L]}}$。
为此,请使用以下公式(不需要深入的微积分知识):

  • dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL)) # derivative of cost with respect to AL

然后,你可以使用此激活后的梯度dAL继续反向传播。如图5所示,你现在可以将dAL输入到你实现的LINEAR-> SIGMOID反向函数中(它将使用L_model_forward函数存储的缓存值)。之后,你得通过for循环,使用LINEAR-> RELU反向函数迭代所有其他层。同时将每个dA,dW和db存储在grads词典中。为此,请使用以下公式:

例如,当$l=3$时,它将在grads["dW3"]中存储 $dW^{[l]}$。

编写函数L_model_backward(AL,Y,caches)实现上述模型L层的反向传播并返回字典grads
要求

  • 实现 [LINEAR->RELU] $\times$ (L-1) -> LINEAR -> SIGMOID 模型的反向传播。
  • dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
  • 参数:
    • AL - 概率向量,正向传播的输出(L_model_forward())
    • Y - 标签向量(例如:如果不是猫,则为0,如果是猫则为1),维度为(1,数量)
    • caches - 包含以下内容的cache列表:(linear_cache,activation_cache)
  • 返回:
    • grads - 具有梯度值的字典
      • grads [“dA”+ str(l)] = …
      • grads [“dW”+ str(l)] = …
      • grads [“db”+ str(l)] = …
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def L_model_backward(AL,Y,caches):
"""
对[LINEAR-> RELU] *(L-1) - > LINEAR - > SIGMOID组执行反向传播,就是多层网络的向后传播

参数:
AL - 概率向量,正向传播的输出(L_model_forward())
Y - 标签向量(例如:如果不是猫,则为0,如果是猫则为1),维度为(1,数量)
caches - 包含以下内容的cache列表:
linear_activation_forward("relu")的cache,不包含输出层
linear_activation_forward("sigmoid")的cache

返回:
grads - 具有梯度值的字典
grads [“dA”+ str(l)] = ...
grads [“dW”+ str(l)] = ...
grads [“db”+ str(l)] = ...
"""
grads = {}
L = len(caches)
m = AL.shape[1]
Y = Y.reshape(AL.shape)
dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))

current_cache = caches[L-1]
grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL, current_cache, "sigmoid")

for l in reversed(range(L-1)):
current_cache = caches[l]
dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA" + str(l + 2)], current_cache, "relu")
grads["dA" + str(l + 1)] = dA_prev_temp
grads["dW" + str(l + 1)] = dW_temp
grads["db" + str(l + 1)] = db_temp

return grads

更新参数

在本节中,你将使用梯度下降来更新模型的参数:

编写函数update_parameters(parameters,grads,learning_rate)更新模型的参数

其中 $\alpha$ 是学习率。 在计算更新的参数后,将它们存储在参数字典中。

提示

  • 参数:

    • parameters - 包含你的参数的字典
    • grads - 包含梯度值的字典,是L_model_backward的输出
  • 返回:

    • parameters - 包含更新参数的字典
      • parameters[“W”+ str(l)] = …
      • parameters[“b”+ str(l)] = …
  • 层数L = len(parameters) // 2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    def update_parameters(parameters, grads, learning_rate):
    """
    使用梯度下降更新参数

    参数:
    parameters - 包含你的参数的字典
    grads - 包含梯度值的字典,是L_model_backward的输出

    返回:
    parameters - 包含更新参数的字典
    参数[“W”+ str(l)] = ...
    参数[“b”+ str(l)] = ...
    """
    L = len(parameters) // 2 #整除
    for l in range(L):
    parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l + 1)]
    parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]

    return parameters

结论

恭喜你实现了构建深度神经网络所需的所有函数!

我们知道这是一项艰巨的任务,但是继续前进将变得更好。 下一部分的作业相对容易。

在下一个作业中,你将使用这些函数构建两个模型用于分类猫图像和非猫图像:

  • 两层神经网络
  • L层神经网络
作业3 深度神经网络的应用 图像分类

深度神经网络应用—图像分类

完成此作业后,你将完成第4周最后的编程任务,也是本课程最后的编程任务!

你将使用在上一个作业中实现的函数来构建深层网络,并将其应用于分类cat图像和非cat图像。 希望你会看到相对于先前的逻辑回归实现的分类,准确性有所提高。

完成此任务后,你将能够:

  • 建立深度神经网络并将其应用于监督学习。

让我们开始吧!

安装包

让我们首先导入在作业过程中需要的所有软件包。

  • numpy是Python科学计算的基本包。
  • matplotlib 是在Python中常用的绘制图形的库。
  • h5py是一个常用的包,可以处理存储为H5文件格式的数据集
  • 用你自己的图片去测试模型效果。
  • np.random.seed(1)使所有随机函数调用保持一致。

数据集

你将使用与“用神经网络思想实现Logistic回归”(作业2)中相同的“cats vs non-cats”数据集。 此前你建立的模型在对猫和非猫图像进行分类时只有70%的准确率。 希望你的新模型会更好!

问题说明:你将获得一个包含以下内容的数据集(”data.h5”):

  • 标记为cat(1)和非cat(0)图像的训练集m_train
  • 标记为cat或non-cat图像的测试集m_test
  • 每个图像的维度都为(num_px,num_px,3),其中3表示3个通道(RGB)。

Number of training examples: 209
Number of testing examples: 50
Each image is of size: (64, 64, 3)
train_x_orig shape: (209, 64, 64, 3)
train_y shape: (1, 209)
test_x_orig shape: (50, 64, 64, 3)
test_y shape: (1, 50)

与往常一样,在将图像输入到网络之前,需要对图像进行重塑和标准化。

图1:图像转换为向量。

train_x’s shape: (12288, 209)
test_x’s shape: (12288, 50)

$12,288$ 等于 $64 \times 64 \times 3$,这是图像重塑为向量的大小。

模型的结构

现在你已经熟悉了数据集,是时候建立一个深度神经网络来区分猫图像和非猫图像了。

你将建立两个不同的模型:

  • 2层神经网络
  • L层深度神经网络

然后,你将比较这些模型的性能,并尝试不同的$L$值。

让我们看一下两种架构。

2层神经网络

图2:2层神经网络。

该模型可以总结为:INPUT -> LINEAR -> RELU -> LINEAR -> SIGMOID -> OUTPUT

图2的详细架构:

  • 输入维度为(64,64,3)的图像,将其展平为大小为$(12288,1)$的向量。
  • 相应的向量:$[x_0,x_1,…,x_{12287}]^T$乘以大小为$(n^{[1]}, 12288)$的权重矩阵$W^{[1]}$。
  • 然后添加一个偏差项并按照公式获得以下向量:$[a_0^{[1]}, a_1^{[1]},…, a_{n^{[1]}-1}^{[1]}]^T$。
  • 然后,重复相同的过程。
  • 将所得向量乘以$W^{[2]}$并加上截距(偏差)。
  • 最后,采用结果的sigmoid值。 如果大于0.5,则将其分类为猫。

对数据的加载,以下代码之前在第一课写过了不再写了

1
2
3
4
5
6
7
8
9
10
11
train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = lr_utils.load_dataset()

train_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T
test_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T

train_x = train_x_flatten / 255
train_y = train_set_y
test_x = test_x_flatten / 255
test_y = test_set_y


L层深度神经网络

用上面的方式很难表示一个L层的深度神经网络。 这是一个简化的网络表示形式:

图3:L层神经网络。
该模型可以总结为:[LINEAR -> RELU] $\times$ (L-1) -> LINEAR -> SIGMOID

图3的详细结构:

  • 输入维度为(64,64,3)的图像,将其展平为大小为$(12288,1)$的向量。
  • 相应的向量:$[x_0,x_1,…,x_{12287}]^T$乘以权重矩阵$W^{[1]}$,然后加上截距$b^{[1]}$,结果为线性单位。
  • 接下来计算获得的线性单元。对于每个$(W^{[l]}, b^{[l]})$,可以重复数次,具体取决于模型体系结构。
  • 最后,采用最终线性单位的sigmoid值。如果大于0.5,则将其分类为猫。

通用步骤

与往常一样,你将遵循深度学习步骤来构建模型:
1.初始化参数/定义超参数
2.循环num_iterations次:
a. 正向传播
b. 计算损失函数
C. 反向传播
d. 更新参数(使用参数和反向传播的梯度)
4.使用训练好的参数来预测标签

现在让我们实现这两个模型!

两层神经网络

构建two_layer_model函数建立两层网络模型

  • two_layer_model(X,Y,layers_dims,learning_rate=0.0075,num_iterations=3000,print_cost=False,isPlot=True)
    提示:
  • 初始化参数获得字典parameters
  • 开始进行迭代
    • 前向传播获得A1, cache1;A1, cache1
    • 计算成本获得cost
    • 后向传播
      • 初始化后向传播获得dA2
      • 获得dA1, dW2, db2; dA0, dW1, db1
      • 向后传播完成后的数据保存到grads字典
    • 更新参数返回字典parameters

要求: 每迭代100次打印当前的cost值,绘制cost曲线,cost是一个列表,你可以用append追加元素,注意将cost标量化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def two_layer_model(X,Y,layers_dims,learning_rate=0.0075,num_iterations=3000,print_cost=False,isPlot=True):
"""
实现一个两层的神经网络,【LINEAR->RELU】 -> 【LINEAR->SIGMOID】
参数:
X - 输入的数据,维度为(n_x,例子数)
Y - 标签,向量,0为非猫,1为猫,维度为(1,数量)
layers_dims - 层数的向量,维度为(n_y,n_h,n_y)
learning_rate - 学习率
num_iterations - 迭代的次数
print_cost - 是否打印成本值,每100次打印一次
isPlot - 是否绘制出误差值的图谱
返回:
parameters - 一个包含W1,b1,W2,b2的字典变量
"""
np.random.seed(1)
grads = {}
costs = []
(n_x,n_h,n_y) = layers_dims

"""
初始化参数
"""
parameters = initialize_parameters(n_x, n_h, n_y)

W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]

"""
开始进行迭代
"""
for i in range(0,num_iterations):
#前向传播
A1, cache1 = linear_activation_forward(X, W1, b1, "relu")
A2, cache2 = linear_activation_forward(A1, W2, b2, "sigmoid")

#计算成本
cost = compute_cost(A2,Y)

#后向传播
##初始化后向传播
dA2 = - (np.divide(Y, A2) - np.divide(1 - Y, 1 - A2))

##向后传播,输入:“dA2,cache2,cache1”。 输出:“dA1,dW2,db2;还有dA0(未使用),dW1,db1”。
dA1, dW2, db2 = linear_activation_backward(dA2, cache2, "sigmoid")
dA0, dW1, db1 = linear_activation_backward(dA1, cache1, "relu")

##向后传播完成后的数据保存到grads
grads["dW1"] = dW1
grads["db1"] = db1
grads["dW2"] = dW2
grads["db2"] = db2

#更新参数
parameters = update_parameters(parameters,grads,learning_rate)
W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]

#打印成本值,如果print_cost=False则忽略
if i % 100 == 0:
#记录成本
costs.append(cost)
#是否打印成本值
if print_cost:
print("第", i ,"次迭代,成本值为:" ,np.squeeze(cost))
#迭代完成,根据条件绘制图
if isPlot:
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()

#返回parameters
return parameters

现在请你编写程序实现训练两层神经网络模型

1
2
3
4
5
6
7
8
9
n_x = 12288
n_h = 7
n_y = 1
layers_dims = (n_x,n_h,n_y)

parameters = two_layer_model(train_x, train_set_y, layers_dims = (n_x, n_h, n_y), num_iterations = 2500, print_cost=True,isPlot=True)



预期输出:
0次迭代后的损失
0.6930497356599888
100次迭代后的损失
0.6464320953428849

2400次迭代后的损失
0.048554785628770206

你构建了向量化的实现! 否则,可能需要花费10倍的时间来训练它。

你可以使用训练好的参数对数据集中的图像进行分类。 要查看训练和测试集的预测结果。
请你定义预测函数predict(X, y, parameters)返回预测结果p并打印准确度

要求:

  • 打印准确度float(np.sum((p == y))/m))
    • (p == y)解释:p的值与标签y相等则为1,否则为0
  • 使用前向传播L_model_forward(X, parameters)预测
  • 预测大于0.5的置为1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    def predict(X, y, parameters):
    """
    该函数用于预测L层神经网络的结果,当然也包含两层

    参数:
    X - 测试集
    y - 标签
    parameters - 训练模型的参数

    返回:
    p - 给定数据集X的预测
    """

    m = X.shape[1]
    n = len(parameters) // 2 # 神经网络的层数
    p = np.zeros((1,m))

    #根据参数前向传播
    probas, caches = L_model_forward(X, parameters)

    for i in range(0, probas.shape[1]):
    if probas[0,i] > 0.5:
    p[0,i] = 1
    else:
    p[0,i] = 0

    print("准确度为: " + str(float(np.sum((p == y))/m)))

    return p

现在请你编写程序开始对训练集和测试集预测

提示:你需要传的数据为

  • train_x, train_y, parameters 数据、标签、训练好的权值
  • test_x, test_y, parameters
1
2
3
predictions_train = predict(train_x, train_y, parameters) #训练集
predictions_test = predict(test_x, test_y, parameters) #测试集

注意:你可能会注意到,以较少的迭代次数(例如1500)运行模型可以使测试集具有更高的准确性。 这称为“尽早停止”,我们将在下一课程中讨论。 提前停止是防止过拟合的一种方法。

恭喜你!看来你的2层神经网络的性能(72%)比逻辑回归实现(70%,第1周的作业)更好。 让我们看看使用$L$层模型是否可以做得更好。

L层神经网络

问题:使用之前实现的辅助函数来构建具有以下结构的$L$层神经网络:[LINEAR -> RELU]$\times$(L-1) -> LINEAR -> SIGMOID。 你可能需要的函数及其输入为:

请你编写L_layer_model函数建立多层网络模型

  • L_layer_model(X, Y, layers_dims, learning_rate=0.0075, num_iterations=3000, print_cost=False,isPlot=True)

提示:

  • 初始化参数获得字典parameters
  • 开始进行迭代
    • 前向传播获得AL , caches
    • 计算成本获得cost
    • 后向传播使用L_model_backward获得grads字典
    • 更新参数返回字典parameters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def L_layer_model(X, Y, layers_dims, learning_rate=0.0075, num_iterations=3000, print_cost=False,isPlot=True):
"""
实现一个L层神经网络:[LINEAR-> RELU] *(L-1) - > LINEAR-> SIGMOID。

参数:
X - 输入的数据,维度为(n_x,例子数)
Y - 标签,向量,0为非猫,1为猫,维度为(1,数量)
layers_dims - 层数的向量,维度为(n_y,n_h,···,n_h,n_y)
learning_rate - 学习率
num_iterations - 迭代的次数
print_cost - 是否打印成本值,每100次打印一次
isPlot - 是否绘制出误差值的图谱

返回:
parameters - 模型学习的参数。 然后他们可以用来预测。
"""
np.random.seed(1)
costs = []

parameters = initialize_parameters_deep(layers_dims)

for i in range(0,num_iterations):
AL , caches = L_model_forward(X,parameters)

cost = compute_cost(AL,Y)

grads = L_model_backward(AL,Y,caches)

parameters = update_parameters(parameters,grads,learning_rate)

#打印成本值,如果print_cost=False则忽略
if i % 100 == 0:
#记录成本
costs.append(cost)
#是否打印成本值
if print_cost:
print("第", i ,"次迭代,成本值为:" ,np.squeeze(cost))
#迭代完成,根据条件绘制图
if isPlot:
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()
return parameters

现在加载数据集已经写过了不必再写

1
2
3
4
5
6
7
8
9
10
11
train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = lr_utils.load_dataset()

train_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T
test_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T

train_x = train_x_flatten / 255
train_y = train_set_y
test_x = test_x_flatten / 255
test_y = test_set_y


请编写程序开始正式训练5层的网络并迭代2500次

提示:layers_dims = [12288, 20, 7, 5, 1] 分别代表每层的单元数

  • 第一层不能变需要和单样本的特征个数一致,最后一个不能变需要和单样本的标签Y的值数量一致
1
2
3
4
layers_dims = [12288, 20, 7, 5, 1] #  5-layer model
parameters = L_layer_model(train_x, train_y, layers_dims, num_iterations = 2500, print_cost = True,isPlot=True)


现在请你编写程序开始对训练集和测试集预测

提示:你需要传的数据为

  • train_x, train_y, parameters 数据、标签、训练好的权值
  • test_x, test_y, parameters
1
2
3
predictions_train = predict(train_x, train_y, parameters) #训练集
predictions_test = predict(test_x, test_y, parameters) #测试集

预期输出:
准确度为: 0.9952153110047847
准确度为: 0.78

Good!在相同的测试集上,你的5层的神经网络似乎比2层神经网络具有更好的性能(80%)。做的好!

在下一作业教程“改善深度神经网络”中,你将学习如何通过系统地匹配更好的超参数(学习率,层数,迭代次数以及下一门课程中还将学习到的其他参数)来获得更高的准确性。

结果分析

首先,让我们看一下L层模型标记错误的一些图像。 这将显示一些分类错误的图像。您无须编写只需要运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def print_mislabeled_images(classes, X, y, p):
"""
绘制预测和实际不同的图像。
X - 数据集
y - 实际的标签
p - 预测
"""
a = p + y
mislabeled_indices = np.asarray(np.where(a == 1))
plt.rcParams['figure.figsize'] = (40.0, 40.0) # set default size of plots
num_images = len(mislabeled_indices[0])
for i in range(num_images):
index = mislabeled_indices[1][i]

plt.subplot(2, num_images, i + 1)
plt.imshow(X[:,index].reshape(64,64,3), interpolation='nearest')
plt.axis('off')
plt.title("Prediction: " + classes[int(p[0,index])].decode("utf-8") + " \n Class: " + classes[y[0,index]].decode("utf-8"))
plt.show()

print_mislabeled_images(classes, test_x, test_y, predictions_test)



该模型在表现效果较差的的图像包括:

  • 猫身处于异常位置
  • 图片背景与猫颜色类似
  • 猫的种类和颜色稀有
  • 相机角度
  • 图片的亮度
  • 比例变化(猫的图像很大或很小)

使用你自己的图像进行测试(可选练习)

祝贺你完成此作业。 你可以使用自己的图像测试并查看模型的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

## START CODE HERE ##
my_image = "my_image.jpg" # change this to the name of your image file
my_label_y = [1] # the true class of your image (1 -> cat, 0 -> non-cat)
## END CODE HERE ##

fname = "images/" + my_image
image = np.array(ndimage.imread(fname, flatten=False))
my_image = scipy.misc.imresize(image, size=(num_px,num_px)).reshape((num_px*num_px*3,1))
my_predicted_image = predict(my_image, my_label_y, parameters)

plt.imshow(image)
print ("y = " + str(np.squeeze(my_predicted_image)) + ", your L-layer model predicts a \"" + classes[int(np.squeeze(my_predicted_image)),].decode("utf-8") + "\" picture.")


参考

主程序:
1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
import numpy as np
import h5py
import matplotlib.pyplot as plt
import testCases #参见资料包,或者在文章底部copy
from dnn_utils import sigmoid, sigmoid_backward, relu, relu_backward #参见资料包
import lr_utils #参见资料包,或者在文章底部copy


def initialize_parameters(n_x, n_h, n_y):
"""
此函数是为了初始化两层网络参数而使用的函数。
参数:
n_x - 输入层节点数量
n_h - 隐藏层节点数量
n_y - 输出层节点数量

返回:
parameters - 包含你的参数的python字典:
W1 - 权重矩阵,维度为(n_h,n_x)
b1 - 偏向量,维度为(n_h,1)
W2 - 权重矩阵,维度为(n_y,n_h)
b2 - 偏向量,维度为(n_y,1)

"""
W1 = np.random.randn(n_h, n_x) * 0.01
b1 = np.zeros((n_h, 1))
W2 = np.random.randn(n_y, n_h) * 0.01
b2 = np.zeros((n_y, 1))

# 使用断言确保我的数据格式是正确的
assert (W1.shape == (n_h, n_x))
assert (b1.shape == (n_h, 1))
assert (W2.shape == (n_y, n_h))
assert (b2.shape == (n_y, 1))

parameters = {"W1": W1,
"b1": b1,
"W2": W2,
"b2": b2}

return parameters


def initialize_parameters_deep(layers_dims):
"""
此函数是为了初始化多层网络参数而使用的函数。
参数:
layers_dims - 包含我们网络中每个图层的节点数量的列表

返回:
parameters - 包含参数“W1”,“b1”,...,“WL”,“bL”的字典:
W1 - 权重矩阵,维度为(layers_dims [1],layers_dims [1-1])
bl - 偏向量,维度为(layers_dims [1],1)
"""
np.random.seed(3)
parameters = {}
L = len(layers_dims)

for l in range(1, L):
parameters["W" + str(l)] = np.random.randn(layers_dims[l], layers_dims[l - 1]) / np.sqrt(layers_dims[l - 1])
parameters["b" + str(l)] = np.zeros((layers_dims[l], 1))

# 确保我要的数据的格式是正确的
assert (parameters["W" + str(l)].shape == (layers_dims[l], layers_dims[l - 1]))
assert (parameters["b" + str(l)].shape == (layers_dims[l], 1))


return parameters


def linear_forward(A, W, b):
"""
实现前向传播的线性部分。

参数:
A - 来自上一层(或输入数据)的激活,维度为(上一层的节点数量,示例的数量)
W - 权重矩阵,numpy数组,维度为(当前图层的节点数量,前一图层的节点数量)
b - 偏向量,numpy向量,维度为(当前图层节点数量,1)

返回:
Z - 激活功能的输入,也称为预激活参数
cache - 一个包含“A”,“W”和“b”的元组,存储这些变量以有效地计算后向传递
"""
Z = np.dot(W, A) + b
assert (Z.shape == (W.shape[0], A.shape[1]))
cache = (A, W, b)

return Z, cache


def linear_activation_forward(A_prev, W, b, activation):
"""
实现LINEAR-> ACTIVATION 这一层的前向传播

参数:
A_prev - 来自上一层(或输入层)的激活,维度为(上一层的节点数量,示例数)
W - 权重矩阵,numpy数组,维度为(当前层的节点数量,前一层的大小)
b - 偏向量,numpy阵列,维度为(当前层的节点数量,1)
activation - 选择在此层中使用的激活函数名,字符串类型,【"sigmoid" | "relu"】

返回:
A - 激活函数的输出,也称为激活后的值
cache - 一个包含“linear_cache”和“activation_cache”的元组,我们需要存储它以有效地计算后向传递
"""

if activation == "sigmoid":
Z, linear_cache = linear_forward(A_prev, W, b)
A, activation_cache = sigmoid(Z)
elif activation == "relu":
Z, linear_cache = linear_forward(A_prev, W, b)
A, activation_cache = relu(Z)

assert (A.shape == (W.shape[0], A_prev.shape[1]))
cache = (linear_cache, activation_cache)

return A, cache


def L_model_forward(X, parameters):
"""
实现[LINEAR-> RELU] *(L-1) - > LINEAR-> SIGMOID计算前向传播,也就是多层网络的前向传播,为后面每一层都执行LINEAR和ACTIVATION

参数:
X - 数据,numpy数组,维度为(输入节点数量,示例数)
parameters - initialize_parameters_deep()的输出

返回:
AL - 最后的激活值
caches - 包含以下内容的缓存列表:
linear_relu_forward()的每个cache(有L-1个,索引为从0到L-2)
linear_sigmoid_forward()的cache(只有一个,索引为L-1)
"""
caches = []
A = X
L = len(parameters) // 2
for l in range(1, L):
A_prev = A
A, cache = linear_activation_forward(A_prev, parameters['W' + str(l)], parameters['b' + str(l)], "relu")
caches.append(cache)

AL, cache = linear_activation_forward(A, parameters['W' + str(L)], parameters['b' + str(L)], "sigmoid")
caches.append(cache)

assert (AL.shape == (1, X.shape[1]))

return AL, caches


def compute_cost(AL, Y):
"""
实施等式(4)定义的成本函数。

参数:
AL - 与标签预测相对应的概率向量,维度为(1,示例数量)
Y - 标签向量(例如:如果不是猫,则为0,如果是猫则为1),维度为(1,数量)

返回:
cost - 交叉熵成本
"""
m = Y.shape[1]
cost = -np.sum(np.multiply(np.log(AL), Y) + np.multiply(np.log(1 - AL), 1 - Y)) / m

cost = np.squeeze(cost)
assert (cost.shape == ())

return cost


def linear_backward(dZ, cache):
"""
为单层实现反向传播的线性部分(第L层)

参数:
dZ - 相对于(当前第l层的)线性输出的成本梯度
cache - 来自当前层前向传播的值的元组(A_prev,W,b)

返回:
dA_prev - 相对于激活(前一层l-1)的成本梯度,与A_prev维度相同
dW - 相对于W(当前层l)的成本梯度,与W的维度相同
db - 相对于b(当前层l)的成本梯度,与b维度相同
"""
A_prev, W, b = cache
m = A_prev.shape[1]
dW = np.dot(dZ, A_prev.T) / m
db = np.sum(dZ, axis=1, keepdims=True) / m
dA_prev = np.dot(W.T, dZ)

assert (dA_prev.shape == A_prev.shape)
assert (dW.shape == W.shape)
assert (db.shape == b.shape)

return dA_prev, dW, db


def linear_activation_backward(dA, cache, activation="relu"):
"""
实现LINEAR-> ACTIVATION层的后向传播。

参数:
dA - 当前层l的激活后的梯度值
cache - 我们存储的用于有效计算反向传播的值的元组(值为linear_cache,activation_cache)
activation - 要在此层中使用的激活函数名,字符串类型,【"sigmoid" | "relu"】
返回:
dA_prev - 相对于激活(前一层l-1)的成本梯度值,与A_prev维度相同
dW - 相对于W(当前层l)的成本梯度值,与W的维度相同
db - 相对于b(当前层l)的成本梯度值,与b的维度相同
"""
linear_cache, activation_cache = cache
if activation == "relu":
dZ = relu_backward(dA, activation_cache)
dA_prev, dW, db = linear_backward(dZ, linear_cache)
elif activation == "sigmoid":
dZ = sigmoid_backward(dA, activation_cache)
dA_prev, dW, db = linear_backward(dZ, linear_cache)

return dA_prev, dW, db


def L_model_backward(AL, Y, caches):
"""
对[LINEAR-> RELU] *(L-1) - > LINEAR - > SIGMOID组执行反向传播,就是多层网络的向后传播

参数:
AL - 概率向量,正向传播的输出(L_model_forward())
Y - 标签向量(例如:如果不是猫,则为0,如果是猫则为1),维度为(1,数量)
caches - 包含以下内容的cache列表:
linear_activation_forward("relu")的cache,不包含输出层
linear_activation_forward("sigmoid")的cache

返回:
grads - 具有梯度值的字典
grads [“dA”+ str(l)] = ...
grads [“dW”+ str(l)] = ...
grads [“db”+ str(l)] = ...
"""
grads = {}
L = len(caches)
m = AL.shape[1]
Y = Y.reshape(AL.shape)
dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))

current_cache = caches[L - 1]
grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL, current_cache,
"sigmoid")

for l in reversed(range(L - 1)):
current_cache = caches[l]
dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA" + str(l + 2)], current_cache, "relu")
grads["dA" + str(l + 1)] = dA_prev_temp
grads["dW" + str(l + 1)] = dW_temp
grads["db" + str(l + 1)] = db_temp

return grads


def update_parameters(parameters, grads, learning_rate):
"""
使用梯度下降更新参数

参数:
parameters - 包含你的参数的字典
grads - 包含梯度值的字典,是L_model_backward的输出

返回:
parameters - 包含更新参数的字典
参数[“W”+ str(l)] = ...
参数[“b”+ str(l)] = ...
"""
L = len(parameters) // 2 # 整除
for l in range(L):
parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l + 1)]
parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]

return parameters

train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = lr_utils.load_dataset()

train_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T
test_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T

train_x = train_x_flatten / 255
train_y = train_set_y
test_x = test_x_flatten / 255
test_y = test_set_y


def two_layer_model(X, Y, layers_dims, learning_rate=0.0075, num_iterations=3000, print_cost=False, isPlot=True):
"""
实现一个两层的神经网络,【LINEAR->RELU】 -> 【LINEAR->SIGMOID】
参数:
X - 输入的数据,维度为(n_x,例子数)
Y - 标签,向量,0为非猫,1为猫,维度为(1,数量)
layers_dims - 层数的向量,维度为(n_y,n_h,n_y)
learning_rate - 学习率
num_iterations - 迭代的次数
print_cost - 是否打印成本值,每100次打印一次
isPlot - 是否绘制出误差值的图谱
返回:
parameters - 一个包含W1,b1,W2,b2的字典变量
"""
np.random.seed(1)
grads = {}
costs = []
(n_x, n_h, n_y) = layers_dims

"""
初始化参数
"""
parameters = initialize_parameters(n_x, n_h, n_y)

W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]

"""
开始进行迭代
"""
for i in range(0, num_iterations):
# 前向传播
A1, cache1 = linear_activation_forward(X, W1, b1, "relu")
A2, cache2 = linear_activation_forward(A1, W2, b2, "sigmoid")

# 计算成本
cost = compute_cost(A2, Y)

# 后向传播
##初始化后向传播
dA2 = - (np.divide(Y, A2) - np.divide(1 - Y, 1 - A2))

##向后传播,输入:“dA2,cache2,cache1”。 输出:“dA1,dW2,db2;还有dA0(未使用),dW1,db1”。
dA1, dW2, db2 = linear_activation_backward(dA2, cache2, "sigmoid")
dA0, dW1, db1 = linear_activation_backward(dA1, cache1, "relu")

##向后传播完成后的数据保存到grads
grads["dW1"] = dW1
grads["db1"] = db1
grads["dW2"] = dW2
grads["db2"] = db2

# 更新参数
parameters = update_parameters(parameters, grads, learning_rate)
W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]

# 打印成本值,如果print_cost=False则忽略
if i % 100 == 0:
# 记录成本
costs.append(cost)
# 是否打印成本值
if print_cost:
print("第", i, "次迭代,成本值为:", np.squeeze(cost))
# 迭代完成,根据条件绘制图
if isPlot:
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()

# 返回parameters
return parameters


def predict(X, y, parameters):
"""
该函数用于预测L层神经网络的结果,当然也包含两层

参数:
X - 测试集
y - 标签
parameters - 训练模型的参数

返回:
p - 给定数据集X的预测
"""

m = X.shape[1]
n = len(parameters) // 2 # 神经网络的层数
p = np.zeros((1, m))

# 根据参数前向传播
probas, caches = L_model_forward(X, parameters)

for i in range(0, probas.shape[1]):
if probas[0, i] > 0.5:
p[0, i] = 1
else:
p[0, i] = 0

print("准确度为: " + str(float(np.sum((p == y)) / m)))

return p
n_x = 12288
n_h = 7
n_y = 1
layers_dims = (n_x,n_h,n_y)

parameters = two_layer_model(train_x, train_set_y, layers_dims = (n_x, n_h, n_y), num_iterations = 2500, print_cost=True,isPlot=True)
predictions_train = predict(train_x, train_y, parameters) #训练集
predictions_test = predict(test_x, test_y, parameters) #测试集


def L_layer_model(X, Y, layers_dims, learning_rate=0.0075, num_iterations=3000, print_cost=False, isPlot=True):
"""
实现一个L层神经网络:[LINEAR-> RELU] *(L-1) - > LINEAR-> SIGMOID。

参数:
X - 输入的数据,维度为(n_x,例子数)
Y - 标签,向量,0为非猫,1为猫,维度为(1,数量)
layers_dims - 层数的向量,维度为(n_y,n_h,···,n_h,n_y)
learning_rate - 学习率
num_iterations - 迭代的次数
print_cost - 是否打印成本值,每100次打印一次
isPlot - 是否绘制出误差值的图谱

返回:
parameters - 模型学习的参数。 然后他们可以用来预测。
"""
np.random.seed(1)
costs = []

parameters = initialize_parameters_deep(layers_dims)

for i in range(0, num_iterations):
AL, caches = L_model_forward(X, parameters)

cost = compute_cost(AL, Y)

grads = L_model_backward(AL, Y, caches)

parameters = update_parameters(parameters, grads, learning_rate)

# 打印成本值,如果print_cost=False则忽略
if i % 100 == 0:
# 记录成本
costs.append(cost)
# 是否打印成本值
if print_cost:
print("第", i, "次迭代,成本值为:", np.squeeze(cost))
# 迭代完成,根据条件绘制图
if isPlot:
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()
return parameters
train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = lr_utils.load_dataset()

train_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T
test_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T

train_x = train_x_flatten / 255
train_y = train_set_y
test_x = test_x_flatten / 255
test_y = test_set_y

layers_dims = [12288, 20, 7, 5, 1] # 5-layer model
parameters = L_layer_model(train_x, train_y, layers_dims, num_iterations = 2500, print_cost = True,isPlot=True)

predictions_train = predict(train_x, train_y, parameters) #训练集
predictions_test = predict(test_x, test_y, parameters) #测试集


def print_mislabeled_images(classes, X, y, p):
"""
绘制预测和实际不同的图像。
X - 数据集
y - 实际的标签
p - 预测
"""
a = p + y
mislabeled_indices = np.asarray(np.where(a == 1))
plt.rcParams['figure.figsize'] = (40.0, 40.0) # set default size of plots
num_images = len(mislabeled_indices[0])
for i in range(num_images):
index = mislabeled_indices[1][i]

plt.subplot(2, num_images, i + 1)
plt.imshow(X[:, index].reshape(64, 64, 3), interpolation='nearest')
plt.axis('off')
plt.title(
"Prediction: " + classes[int(p[0, index])].decode("utf-8") + " \n Class: " + classes[y[0, index]].decode(
"utf-8"))
plt.show()

print_mislabeled_images(classes, test_x, test_y, predictions_test)






作业4 深度神经网络的初始化

深度神经网络的初始化

欢迎来到“改善深度神经网络”的第一项作业。

训练神经网络需要指定权重的初始值,而一个好的初始化方法将有助于网络学习。

如果你完成了本系列的上一课程,则可能已经按照我们的说明完成了权重初始化。但是,如何为新的神经网络选择初始化?在本笔记本中,你能学习看到不同的初始化导致的不同结果。

好的初始化可以:

  • 加快梯度下降、模型收敛
  • 减小梯度下降收敛过程中训练(和泛化)出现误差的几率

首先,运行以下代码以加载包和用于分类的二维数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
import matplotlib.pyplot as plt
import sklearn
import sklearn.datasets
import init_utils #第一部分,初始化
import reg_utils #第二部分,正则化
import gc_utils #第三部分,梯度校验
#%matplotlib inline #如果你使用的是Jupyter Notebook,请取消注释。
plt.rcParams['figure.figsize'] = (7.0, 4.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

train_X, train_Y, test_X, test_Y = init_utils.load_dataset(is_plot=True)

我们希望分类器将蓝点和红点分开。

神经网络模型

你将使用已经实现了的3层神经网络。 下面是你将尝试的初始化方法:

  • 零初始化 :在输入参数中设置initialization = "zeros"
  • 随机初始化 :在输入参数中设置initialization = "random",这会将权重初始化为较大的随机值。
  • He初始化 :在输入参数中设置initialization = "he",这会根据He等人(2015)的论文将权重初始化为按比例缩放的随机值。

说明:请快速阅读并运行以下代码,在下一部分中,你将实现此model()调用的三种初始化方法。

下面是一个三层网络模型,扩展时内部可修改

  • m = X.shape[1]
  • layers_dims = [X.shape[0],10,5,1]
  • cost = init_utils.compute_loss(a3,Y)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    def model(X,Y,learning_rate=0.01,num_iterations=15000,print_cost=True,initialization="he",is_polt=True):
    """
    实现一个三层的神经网络:LINEAR ->RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID

    参数:
    X - 输入的数据,维度为(2, 要训练/测试的数量)
    Y - 标签,【0 | 1】,维度为(1,对应的是输入的数据的标签)
    learning_rate - 学习速率
    num_iterations - 迭代的次数
    print_cost - 是否打印成本值,每迭代1000次打印一次
    initialization - 字符串类型,初始化的类型【"zeros" | "random" | "he"】
    is_polt - 是否绘制梯度下降的曲线图
    返回
    parameters - 学习后的参数
    """
    grads = {}
    costs = []
    m = X.shape[1]
    layers_dims = [X.shape[0],10,5,1]

    #选择初始化参数的类型
    if initialization == "zeros":
    parameters = initialize_parameters_zeros(layers_dims)
    elif initialization == "random":
    parameters = initialize_parameters_random(layers_dims)
    elif initialization == "he":
    parameters = initialize_parameters_he(layers_dims)
    else :
    print("错误的初始化参数!程序退出")
    exit

    #开始学习
    for i in range(0,num_iterations):
    #前向传播
    a3 , cache = init_utils.forward_propagation(X,parameters)

    #计算成本
    cost = init_utils.compute_loss(a3,Y)

    #反向传播
    grads = init_utils.backward_propagation(X,Y,cache)

    #更新参数
    parameters = init_utils.update_parameters(parameters,grads,learning_rate)

    #记录成本
    if i % 1000 == 0:
    costs.append(cost)
    #打印成本
    if print_cost:
    print("第" + str(i) + "次迭代,成本值为:" + str(cost))


    #学习完毕,绘制成本曲线
    if is_polt:
    plt.plot(costs)
    plt.ylabel('cost')
    plt.xlabel('iterations (per hundreds)')
    plt.title("Learning rate =" + str(learning_rate))
    plt.show()

    #返回学习完毕后的参数
    return parameters


零初始化

在神经网络中有两种类型的参数要初始化:

  • 权重矩阵 $(W^{[1]}, W^{[2]}, W^{[3]}, …, W^{[L-1]}, W^{[L]})$
  • 偏差向量 $(b^{[1]}, b^{[2]}, b^{[3]}, …, b^{[L-1]}, b^{[L]})$

练习:实现以下函数以将所有参数初始化为零。 稍后你会看到此方法会报错,因为它无法“打破对称性”。总之先尝试一下,看看会发生什么。确保使用正确维度的np.zeros((..,..))。

下面是函数initialize_parameters_zeros(layers_dims)将参数初始化为0,扩展时内部无需修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def initialize_parameters_zeros(layers_dims):
"""
将模型的参数全部设置为0

参数:
layers_dims - 列表,模型的层数和对应每一层的节点的数量
返回
parameters - 包含了所有W和b的字典
W1 - 权重矩阵,维度为(layers_dims[1], layers_dims[0])
b1 - 偏置向量,维度为(layers_dims[1],1)
···
WL - 权重矩阵,维度为(layers_dims[L], layers_dims[L -1])
bL - 偏置向量,维度为(layers_dims[L],1)
"""
parameters = {}

L = len(layers_dims) #网络层数

for l in range(1,L):
parameters["W" + str(l)] = np.zeros((layers_dims[l],layers_dims[l-1]))
parameters["b" + str(l)] = np.zeros((layers_dims[l],1))

#使用断言确保我的数据格式是正确的
assert(parameters["W" + str(l)].shape == (layers_dims[l],layers_dims[l-1]))
assert(parameters["b" + str(l)].shape == (layers_dims[l],1))

return parameters


测试

1
2
3
4
5
6
parameters = initialize_parameters_zeros([3,2,1])
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

预期输出:
W1 = [[0. 0. 0.]
[0. 0. 0.]]
b1 = [[0.]
[0.]]
W2 = [[0. 0.]]
b2 = [[0.]]

运行以下代码使用零初始化并迭代15

1
2
parameters = model(train_X, train_Y, initialization = "zeros",is_polt=True)

init_utils文件中的预测函数predict打印准确率,扩展时内部可修改

  • m = X.shape[1]
  • p = np.zeros((1,m), dtype = np.int)
  • a3

predict(X, y, parameters)函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def predict(X, y, parameters):
"""
This function is used to predict the results of a n-layer neural network.

Arguments:
X -- data set of examples you would like to label
parameters -- parameters of the trained model

Returns:
p -- predictions for the given dataset X
"""

m = X.shape[1]
p = np.zeros((1,m), dtype = np.int)

# Forward propagation
a3, caches = forward_propagation(X, parameters)

# convert probas to 0/1 predictions
for i in range(0, a3.shape[1]):
if a3[0,i] > 0.5:
p[0,i] = 1
else:
p[0,i] = 0

# print results
print("Accuracy: " + str(np.mean((p[0,:] == y[0,:]))))

return p
1
2
3
4
5
print ("训练集:")
predictions_train = init_utils.predict(train_X, train_Y, parameters)
print ("测试集:")
predictions_test = init_utils.predict(test_X, test_Y, parameters)

On the train set:
Accuracy: 0.5
On the test set:
Accuracy: 0.5

性能确实很差,损失也没有真正降低,该算法的性能甚至不如随机猜测。为什么呢?让我们看一下预测的详细信息和决策边界:

1
2
3
4
5
6
7
8
9
10
11
print("predictions_train = " + str(predictions_train))
print("predictions_test = " + str(predictions_test))

plt.title("Model with Zeros initialization")
# 获取当前坐标轴
axes = plt.gca()
# 设置 x 轴的显示范围
axes.set_xlim([-1.5, 1.5])
axes.set_ylim([-1.5, 1.5])
init_utils.plot_decision_boundary(lambda x: init_utils.predict_dec(parameters, x.T), train_X, train_Y)

predictions_train = [[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0]]
predictions_test = [[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]]

该模型预测的每个示例都为0。

通常,将所有权重初始化为零会导致网络无法打破对称性。 这意味着每一层中的每个神经元都将学习相同的东西,并且你不妨训练每一层$n^{[l]}=1$的神经网络,且该网络的性能不如线性分类器,例如逻辑回归。

你应该记住

  • 权重$W^{[l]}$ 应该随机初始化以打破对称性。
  • 将偏差$b^{[l]}$初始化为零是可以的。只要随机初始化了$W^{[l]}$,对称性仍然会破坏。

随机初始化

为了打破对称性,让我们随机设置权重。 在随机初始化之后,每个神经元可以继续学习其输入的不同特征。 在本练习中,你将看到如果将权重随机初始化为非常大的值会发生什么。

练习:实现以下函数,将权重初始化为较大的随机值(按*10缩放),并将偏差设为0。 将 np.random.randn(..,..) * 10用于权重,将np.zeros((.., ..))用于偏差。我们使用固定的np.random.seed(..),以确保你的“随机”权重与我们的权重匹配。因此,如果运行几次代码后参数初始值始终相同,也请不要疑惑。

下面是函数initialize_parameters_random(layers_dims)将参数随机初始化,扩展时内部无需修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def initialize_parameters_random(layers_dims):
"""
参数:
layers_dims - 列表,模型的层数和对应每一层的节点的数量
返回
parameters - 包含了所有W和b的字典
W1 - 权重矩阵,维度为(layers_dims[1], layers_dims[0])
b1 - 偏置向量,维度为(layers_dims[1],1)
···
WL - 权重矩阵,维度为(layers_dims[L], layers_dims[L -1])
b1 - 偏置向量,维度为(layers_dims[L],1)
"""

np.random.seed(3) # 指定随机种子
parameters = {}
L = len(layers_dims) # 层数

for l in range(1, L):
parameters['W' + str(l)] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * 10 #使用10倍缩放
parameters['b' + str(l)] = np.zeros((layers_dims[l], 1))

#使用断言确保我的数据格式是正确的
assert(parameters["W" + str(l)].shape == (layers_dims[l],layers_dims[l-1]))
assert(parameters["b" + str(l)].shape == (layers_dims[l],1))

return parameters


测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
parameters = initialize_parameters_random([3, 2, 1])
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

```

**预期输出**:
W1 = [[ 17.88628473 4.36509851 0.96497468]
[-18.63492703 -2.77388203 -3.54758979]]
b1 = [[0.]
[0.]]
W2 = [[-0.82741481 -6.27000677]]
b2 = [[0.]]
{% span red, 运行以下代码使用随机初始化迭代15,000次以训练模型。 %}

```py
parameters = model(train_X, train_Y, initialization = "random",is_polt=True)
print("训练集:")
predictions_train = init_utils.predict(train_X, train_Y, parameters)
print("测试集:")
predictions_test = init_utils.predict(test_X, test_Y, parameters)

print(predictions_train)
print(predictions_test)

预期输出:
训练集:
Accuracy: 0.83
测试集:
Accuracy: 0.86

因为数值舍入,你可能在0迭代之后看到损失为”inf”,我们会在之后用更复杂的数字实现解决此问题。

总之,看起来你的对称性已打破,这会带来更好的结果。 相比之前,模型不再输出全0的结果了。

1
2
print (predictions_train)
print (predictions_test)

[[1 0 1 1 0 0 1 1 1 1 1 0 1 0 0 1 0 1 1 0 0 0 1 0 1 1 1 1 1 1 0 1 1 0 0 1
1 1 1 1 1 1 1 0 1 1 1 1 0 1 0 1 1 1 1 0 0 1 1 1 1 0 1 1 0 1 0 1 1 1 1 0
0 0 0 0 1 0 1 0 1 1 1 0 0 1 1 1 1 1 1 0 0 1 1 1 0 1 1 0 1 0 1 1 0 1 1 0
1 0 1 1 0 0 1 0 0 1 1 0 1 1 1 0 1 0 0 1 0 1 1 1 1 1 1 1 0 1 1 0 0 1 1 0
0 0 1 0 1 0 1 0 1 1 1 0 0 1 1 1 1 0 1 1 0 1 0 1 1 0 1 0 1 1 1 1 0 1 1 1
1 0 1 0 1 0 1 1 1 1 0 1 1 0 1 1 0 1 1 0 1 0 1 1 1 0 1 1 1 0 1 0 1 0 0 1
0 1 1 0 1 1 0 1 1 0 1 1 1 0 1 1 1 1 0 1 0 0 1 1 0 1 1 1 0 0 0 1 1 0 1 1
1 1 0 1 1 0 1 1 1 0 0 1 0 0 0 1 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 1 1 1
1 1 1 1 0 0 0 1 1 1 1 0]]
[[1 1 1 1 0 1 0 1 1 0 1 1 1 0 0 0 0 1 0 1 0 0 1 0 1 0 1 1 1 1 1 0 0 0 0 1
0 1 1 0 0 1 1 1 1 1 0 1 1 1 0 1 0 1 1 0 1 0 1 0 1 1 1 1 1 1 1 1 1 0 1 0
1 1 1 1 1 0 1 0 0 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0]]

使用以下代码打印边界

1
2
3
4
5
6
plt.title("Model with large random initialization")
axes = plt.gca()
axes.set_xlim([-1.5, 1.5])
axes.set_ylim([-1.5, 1.5])
init_utils.plot_decision_boundary(lambda x: init_utils.predict_dec(parameters, x.T), train_X, train_Y)

观察

  • 损失一开始很高是因为较大的随机权重值,对于某些数据,最后一层激活函数sigmoid输出的结果非常接近0或1,并且当该示例数据预测错误时,将导致非常高的损失。当$\log(a^{[3]}) = \log(0)$时,损失达到无穷大。
  • 初始化不当会导致梯度消失/爆炸,同时也会减慢优化算法的速度。
  • 训练较长时间的网络,将会看到更好的结果,但是使用太大的随机数进行初始化会降低优化速度。

总结

  • 将权重初始化为非常大的随机值效果不佳。
  • 初始化为较小的随机值会更好。重要的问题是:这些随机值应为多小?让我们在下一部分中找到答案!

He初始化

最后,让我们尝试一下“He 初始化”,该名称以He等人的名字命名(类似于“Xavier初始化”,但Xavier初始化使用比例因子 sqrt(1./layers_dims[l-1])来表示权重$W^{[l]}$ ,而He初始化使用sqrt(2./layers_dims[l-1]))。

练习:实现以下函数,以He初始化来初始化参数。

提示:此函数类似于先前的initialize_parameters_random(...)。 唯一的不同是,无需将np.random.randn(..,..)乘以10,而是将其乘以$\sqrt{\frac{2}{\text{上一层的维度}}}$,这是He初始化建议使用的ReLU激活层。

下面是函数initialize_parameters_he(layers_dims)将参数使用He初始化,扩展时内部无需修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def initialize_parameters_he(layers_dims):
"""
参数:
layers_dims - 列表,模型的层数和对应每一层的节点的数量
返回
parameters - 包含了所有W和b的字典
W1 - 权重矩阵,维度为(layers_dims[1], layers_dims[0])
b1 - 偏置向量,维度为(layers_dims[1],1)
···
WL - 权重矩阵,维度为(layers_dims[L], layers_dims[L -1])
b1 - 偏置向量,维度为(layers_dims[L],1)
"""

np.random.seed(3) # 指定随机种子
parameters = {}
L = len(layers_dims) # 层数

for l in range(1, L):
parameters['W' + str(l)] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * np.sqrt(2 / layers_dims[l - 1])
parameters['b' + str(l)] = np.zeros((layers_dims[l], 1))

#使用断言确保我的数据格式是正确的
assert(parameters["W" + str(l)].shape == (layers_dims[l],layers_dims[l-1]))
assert(parameters["b" + str(l)].shape == (layers_dims[l],1))

return parameters


测试

1
2
3
4
5
parameters = initialize_parameters_he([2, 4, 1])
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

预期输出:
W1 = [[ 1.78862847 0.43650985]
[ 0.09649747 -1.8634927 ]
[-0.2773882 -0.35475898]
[-0.08274148 -0.62700068]]
b1 = [[0.]
[0.]
[0.]
[0.]]
W2 = [[-0.03098412 -0.33744411 -0.92904268 0.62552248]]
b2 = [[0.]]

运行以下代码,使用He初始化并迭代15

1
2
3
4
5
6
parameters = model(train_X, train_Y, initialization = "he",is_polt=True)
print("训练集:")
predictions_train = init_utils.predict(train_X, train_Y, parameters)
print("测试集:")
init_utils.predictions_test = init_utils.predict(test_X, test_Y, parameters)

预期输出:
第0次迭代,成本值为:0.883053746342
第1000次迭代,成本值为:0.687982591973
第2000次迭代,成本值为:0.675128626452
第3000次迭代,成本值为:0.652611776889
第4000次迭代,成本值为:0.608295897057
第5000次迭代,成本值为:0.530494449172
第6000次迭代,成本值为:0.413864581707
第7000次迭代,成本值为:0.311780346484
第8000次迭代,成本值为:0.236962153303
第9000次迭代,成本值为:0.185972872092
第10000次迭代,成本值为:0.150155562804
第11000次迭代,成本值为:0.123250792923
第12000次迭代,成本值为:0.0991774654653
第13000次迭代,成本值为:0.0845705595402
第14000次迭代,成本值为:0.0735789596268
训练集:
Accuracy: 0.993333333333
测试集:
Accuracy: 0.96

使用下面的代码绘制预测情况和边界

1
2
3
4
5
plt.title("Model with He initialization")
axes = plt.gca()
axes.set_xlim([-1.5, 1.5])
axes.set_ylim([-1.5, 1.5])
init_utils.plot_decision_boundary(lambda x: init_utils.predict_dec(parameters, x.T), train_X, train_Y)

观察

  • 使用He初始化的模型可以在少量迭代中很好地分离蓝色点和红色点。

总结

我们已经学习了三种不同类型的初始化方法。对于相同的迭代次数和超参数,三种结果比较为:

Model测试准确率评价
零初始化的3层NN50%未能打破对称性
随机初始化的3层NN83%权重太大
He初始化的3层NN99%推荐方法

此作业中应记住的内容

  • 不同的初始化会导致不同的结果
  • 随机初始化用于打破对称性,并确保不同的隐藏单元可以学习不同的东西
  • 不要初始化为太大的值
  • 初始化对于带有ReLU激活的网络非常有效。
作业4 深度神经网络的正则化

深度神经网络的正则化

欢迎来到本周的第二次作业。
深度学习模型具有很高的灵活性和能力,如果训练数据集不够大,将会造成一个严重的问题—过拟合。尽管它在训练集上效果很好,但是学到的网络不能应用到测试集中!

你将学习: 在深度学习模型中使用正则化。

首先导入要使用的包。

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
import matplotlib.pyplot as plt
import sklearn
import sklearn.datasets
import init_utils #第一部分,初始化
import reg_utils #第二部分,正则化
import gc_utils #第三部分,梯度校验
#%matplotlib inline #如果你使用的是Jupyter Notebook,请取消注释。
plt.rcParams['figure.figsize'] = (7.0, 4.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

问题陈述:你刚刚被法国足球公司聘为AI专家。他们希望你推荐预测法国守门员将球踢出的位置,以便法国队的球员可以用头将球击中。

图1
足球场
守门员将球踢到空中,每支球队的球员都在尽力用头击球

他们为你提供了法国过去10场比赛的二维数据集。
使用load_2D_dataset()加载数据集

1
train_X, train_Y, test_X, test_Y = reg_utils.load_2D_dataset(is_plot=True)

数据展示

数据中每个点对应于足球场上的位置,在该位置上,法国守门员从足球场左侧射出球后,足球运动员用他/她的头部击中了球。

  • 如果圆点为蓝色,则表示法国球员设法用头部将球击中
  • 如果圆点为红色,则表示另一支球队的球员用头撞球

你的目标:运用深度学习模型预测守门员应将球踢到球场上的位置。

数据集分析:该数据集含有噪声,但看起来一条将左上半部分(蓝色)与右下半部分(红色)分开的对角线会很比较有效。

你将首先尝试非正则化模型。然后学习如何对其进行正则化,并决定选择哪种模型来解决法国足球公司的问题。

非正则化模型

你将使用以下神经网络(已为你实现),可以如下使用此模型:

  • regularization mode中,通过lambd将输入设置为非零值。我们使用lambd代替lambda,因为lambda是Python中的保留关键字。
  • dropout mode中,将keep_prob设置为小于1的值

首先,你将尝试不进行任何正则化的模型。然后,你将实现:

  • L2 正则化 函数:compute_cost_with_regularization()backward_propagation_with_regularization()
  • Dropout 函数:forward_propagation_with_dropout()backward_propagation_with_dropout()

在每个部分中,你都将使用正确的输入来运行此模型,以便它调用已实现的函数。查看以下代码以熟悉该模型。

没有正则化的三层模型如下令lambd = 0,keep_prob = 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def model(X, Y, learning_rate = 0.3, num_iterations = 30000, print_cost = True, lambd = 0, keep_prob = 1):
"""
Implements a three-layer neural network: LINEAR->RELU->LINEAR->RELU->LINEAR->SIGMOID.

Arguments:
X -- input data, of shape (input size, number of examples)
Y -- true "label" vector (1 for blue dot / 0 for red dot), of shape (output size, number of examples)
learning_rate -- learning rate of the optimization
num_iterations -- number of iterations of the optimization loop
print_cost -- If True, print the cost every 10000 iterations
lambd -- regularization hyperparameter, scalar
keep_prob - probability of keeping a neuron active during drop-out, scalar.

Returns:
parameters -- parameters learned by the model. They can then be used to predict.
"""

grads = {}
costs = [] # to keep track of the cost
m = X.shape[1] # number of examples
layers_dims = [X.shape[0], 20, 3, 1]

# Initialize parameters dictionary.
parameters = initialize_parameters(layers_dims)

# Loop (gradient descent)

for i in range(0, num_iterations):

# Forward propagation: LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID.
if keep_prob == 1:
a3, cache = forward_propagation(X, parameters)
elif keep_prob < 1:
a3, cache = forward_propagation_with_dropout(X, parameters, keep_prob)

# Cost function
if lambd == 0:
cost = compute_cost(a3, Y)
else:
cost = compute_cost_with_regularization(a3, Y, parameters, lambd)

# Backward propagation.
assert(lambd==0 or keep_prob==1) # it is possible to use both L2 regularization and dropout,
# but this assignment will only explore one at a time
if lambd == 0 and keep_prob == 1:
grads = backward_propagation(X, Y, cache)
elif lambd != 0:
grads = backward_propagation_with_regularization(X, Y, cache, lambd)
elif keep_prob < 1:
grads = backward_propagation_with_dropout(X, Y, cache, keep_prob)

# Update parameters.
parameters = update_parameters(parameters, grads, learning_rate)

# Print the loss every 10000 iterations
if print_cost and i % 10000 == 0:
print("Cost after iteration {}: {}".format(i, cost))
if print_cost and i % 1000 == 0:
costs.append(cost)

# plot the cost
plt.plot(costs)
plt.ylabel('cost')
plt.xlabel('iterations (x1,000)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()

return parameters

让我们在不进行任何正则化的情况下训练模型,并观察训练/测试集的准确性。

1
2
3
4
5
6
parameters = model(train_X, train_Y,is_plot=True)
print("训练集:")
predictions_train = reg_utils.predict(train_X, train_Y, parameters)
print("测试集:")
predictions_test = reg_utils.predict(test_X, test_Y, parameters)

预测函数predict(X,y,parameters)如下,注意这是三层的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def predict(X, y, parameters):
"""
This function is used to predict the results of a n-layer neural network.

Arguments:
X -- data set of examples you would like to label
parameters -- parameters of the trained model

Returns:
p -- predictions for the given dataset X
"""

m = X.shape[1]
p = np.zeros((1, m), dtype=np.int)

# Forward propagation
a3, caches = forward_propagation(X, parameters)

# convert probas to 0/1 predictions
for i in range(0, a3.shape[1]):
if a3[0, i] > 0.5:
p[0, i] = 1
else:
p[0, i] = 0

# print results
print("Accuracy: " + str(np.mean((p[0, :] == y[0, :]))))

return p

预期输出
第0次迭代,成本值为:0.655741252348
第10000次迭代,成本值为:0.163299875257
第20000次迭代,成本值为:0.138516424233
训练集:
Accuracy: 0.947867298578
测试集:
Accuracy: 0.915

训练精度为94.8%,而测试精度为91.5%。这是基准模型的表现(你将观察到正则化对该模型的影响)。

运行以下代码绘制模型的决策边界

1
2
3
4
5
6
plt.title("Model without regularization")
axes = plt.gca()
axes.set_xlim([-0.75,0.40])
axes.set_ylim([-0.75,0.65])
reg_utils.plot_decision_boundary(lambda x: reg_utils.predict_dec(parameters, x.T), train_X, train_Y)

非正则化模型显然过度拟合了训练集,拟合了一些噪声点!现在让我们看一下减少过拟合的两种手段。

L2正则化

避免过拟合的标准方法称为 L2正则化,它将损失函数从:

修改到:

让我们修改损失并观察结果。

练习:实现compute_cost_with_regularization(),以计算公式(2)的损失。要计算$\sum\limits_k\sum\limits_j W_{k,j}^{[l]2}$ ,请使用:

1
np.sum(np.square(Wl))

请注意,你必须对$W^{[1]}$,$W^{[2]}$和$W^{[3]}$执行此操作,然后将三个项相加并乘以$\frac{1}{m}\frac{\lambda}{2}$。

编写函数计算正则化后的损失函数三层compute_cost_with_regularization(A3,Y,parameters,lambd)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def compute_cost_with_regularization(A3,Y,parameters,lambd):
"""
实现公式2的L2正则化计算成本

参数:
A3 - 正向传播的输出结果,维度为(输出节点数量,训练/测试的数量)
Y - 标签向量,与数据一一对应,维度为(输出节点数量,训练/测试的数量)
parameters - 包含模型学习后的参数的字典
返回:
cost - 使用公式2计算出来的正则化损失的值

"""
m = Y.shape[1]
W1 = parameters["W1"]
W2 = parameters["W2"]
W3 = parameters["W3"]

cross_entropy_cost = reg_utils.compute_cost(A3,Y)

L2_regularization_cost = lambd * (np.sum(np.square(W1)) + np.sum(np.square(W2)) + np.sum(np.square(W3))) / (2 * m)

cost = cross_entropy_cost + L2_regularization_cost

return cost

当然,因为你更改了损失,所以还必须更改反向传播! 必须针对新损失函数计算所有梯度。

练习:实现正则化后的反向传播。更改仅涉及dW1,dW2和dW3。对于每一个,你必须添加正则化项的梯度($\frac{d}{dW} ( \frac{1}{2}\frac{\lambda}{m} W^2) = \frac{\lambda}{m} W$)。

编写函数计算正则化后的反向传播三层backward_propagation_with_regularization(X,Y, cache, lambd)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def backward_propagation_with_regularization(X, Y, cache, lambd):
"""
实现我们添加了L2正则化的模型的后向传播。

参数:
X - 输入数据集,维度为(输入节点数量,数据集里面的数量)
Y - 标签,维度为(输出节点数量,数据集里面的数量)
cache - 来自forward_propagation()的cache输出
lambda - regularization超参数,实数

返回:
gradients - 一个包含了每个参数、激活值和预激活值变量的梯度的字典
"""

m = X.shape[1]

(Z1, A1, W1, b1, Z2, A2, W2, b2, Z3, A3, W3, b3) = cache

dZ3 = A3 - Y

dW3 = (1 / m) * np.dot(dZ3,A2.T) + ((lambd * W3) / m )
db3 = (1 / m) * np.sum(dZ3,axis=1,keepdims=True)

dA2 = np.dot(W3.T,dZ3)
dZ2 = np.multiply(dA2,np.int64(A2 > 0))
dW2 = (1 / m) * np.dot(dZ2,A1.T) + ((lambd * W2) / m)
db2 = (1 / m) * np.sum(dZ2,axis=1,keepdims=True)

dA1 = np.dot(W2.T,dZ2)
dZ1 = np.multiply(dA1,np.int64(A1 > 0))
dW1 = (1 / m) * np.dot(dZ1,X.T) + ((lambd * W1) / m)
db1 = (1 / m) * np.sum(dZ1,axis=1,keepdims=True)

gradients = {"dZ3": dZ3, "dW3": dW3, "db3": db3, "dA2": dA2,
"dZ2": dZ2, "dW2": dW2, "db2": db2, "dA1": dA1,
"dZ1": dZ1, "dW1": dW1, "db1": db1}

return gradients

现在让我们使用L2正则化$(\lambda = 0.7)$运行的模型。model()函数将调用:

  • compute_cost_with_regularization代替compute_cost
  • backward_propagation_with_regularization代替backward_propagation

编写函数运行正则化的模型,只需要修改lambd的值不为0

1
2
3
4
5
6
parameters = model(train_X, train_Y, lambd=0.7,is_plot=True)
print("使用正则化,训练集:")
predictions_train = reg_utils.predict(train_X, train_Y, parameters)
print("使用正则化,测试集:")
predictions_test = reg_utils.predict(test_X, test_Y, parameters)

预期输出
第0次迭代,成本值为:0.697448449313
第10000次迭代,成本值为:0.268491887328
第20000次迭代,成本值为:0.268091633713
使用正则化,训练集:
Accuracy: 0.938388625592
使用正则化,测试集:
Accuracy: 0.93

Nice!测试集的准确性提高到93%。你成功拯救了法国足球队!

模型不再过拟合训练数据了。让我们绘制决策边界看一下。

运行下面的代码让我们绘制决策边界

1
2
3
4
5
6
plt.title("Model with L2-regularization")
axes = plt.gca()
axes.set_xlim([-0.75,0.40])
axes.set_ylim([-0.75,0.65])
reg_utils.plot_decision_boundary(lambda x: reg_utils.predict_dec(parameters, x.T), train_X, train_Y)

观察

  • $\lambda$ 的值是你可以调整开发集的超参数。
  • L2正则化使决策边界更平滑。如果$\lambda$ 太大,则也可能“过度平滑”,从而使模型偏差较高。

L2正则化的原理

L2正则化基于以下假设:权重较小的模型比权重较大的模型更简单。因此,通过对损失函数中权重的平方值进行惩罚,可以将所有权重驱动为较小的值。比重太大会使损失过高!这将导致模型更平滑,输出随着输入的变化而变化得更慢。

你应该记住 L2正则化的影响:

  • 损失计算:
    • 正则化条件会添加到损失函数中
  • 反向传播函数:
    • 有关权重矩阵的渐变中还有其他术语
  • 权重最终变小(“权重衰减”):
    • 权重被推到较小的值。

Dropout

最后,Dropout是广泛用于深度学习的正则化技术。
它会在每次迭代中随机关闭一些神经元。
观看这两个video,看看这它是什么意思!

要了解Dropout,可以思考与朋友进行以下对话:

  • 朋友:“为什么你需要所有神经元来训练你的网络以分类图像?”。
  • 你:“因为每个神经元都有权重,并且可以学习图像的特定特征/细节/形状。我拥有的神经元越多,模型学习的特征就越丰富!”
  • 朋友:“我知道了,但是你确定你的神经元学习的是不同的特征而不是全部相同的特征吗?”
  • 你:“这是个好问题……同一层中的神经元实际上并不关联。应该绝对有可能让他们学习相同的图像特征/形状/形式/细节…这是多余的。为此应该有一个解决方案。”

图2:Dropout第二个隐藏层。
在每次迭代中,你以概率$1 - keep_prob$或以概率$keep_prob$(此处为50%)关闭此层的每个神经元。关闭的神经元对迭代的正向和反向传播均无助于训练。

图3:Dropout第一和第三隐藏层。
$1^{st}$层:我们平均关闭了40%的神经元。$3^{rd}$ 层:我们平均关闭了20%的神经元。

当你关闭某些神经元时,实际上是在修改模型。Dropout背后的想法是,在每次迭代中,你将训练仅使用神经元子集的不同模型。通过Dropout,你的神经元对另一种特定神经元的激活变得不那么敏感,因为另一神经元可能随时关闭。

带有Dropout的正向传播

练习:实现带有Dropout的正向传播。你正在使用3层的神经网络,并将为第一和第二隐藏层添加Dropout。我们不会将Dropout应用于输入层或输出层。

编写函数forward_propagation_with_dropout(X,parameters,keep_prob=0.5)

要求
关闭第一层和第二层中的某些神经元。为此,将执行4个步骤:

  1. 在讲座中,我们讨论了使用np.random.rand()创建与$a^{[1]}$形状相同的变量$d^{[1]}$的方法,以随机获取0到1之间的数。在这里,你将使用向量化的实现,创建一个与$A^{[1]}$的矩阵维度相同的随机矩阵$D^{[1]} = [d^{1} d^{1} … d^{1}] $。
  2. 通过对$D^{[1]}$中的值进行阈值设置,将$D^{[1]}$的每个条目设置为0(概率为1-keep_prob)或1(概率为keep_prob)。提示:将矩阵X的所有条目设置为0(如果概率小于0.5)或1(如果概率大于0.5),则可以执行:X = (X < 0.5)。注意0和1分别对应False和True。
  3. 将$A^{[1]}$设置为$A^{[1]} * D^{[1]}$(关闭一些神经元)。你可以将$D^{[1]}$ 视为掩码,这样当它与另一个矩阵相乘时,关闭某些值。
  4. 将$A^{[1]}$除以keep_prob。通过这样做,你可以确保损失结果仍具有与dropout相同的期望值。(此技术也称为反向dropout)
  • 步骤:
    • 初始化矩阵D2 = np.random.rand(…, …)
    • 将D2的值转换为0或1(使​​用keep_prob作为阈值)
    • 步骤3:舍弃A1的一些节点(将它的值变为0或False)
    • 步骤4:缩放未舍弃的节点(不为0)的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def forward_propagation_with_dropout(X,parameters,keep_prob=0.5):
"""
实现具有随机舍弃节点的前向传播。
LINEAR -> RELU + DROPOUT -> LINEAR -> RELU + DROPOUT -> LINEAR -> SIGMOID.

参数:
X - 输入数据集,维度为(2,示例数)
parameters - 包含参数“W1”,“b1”,“W2”,“b2”,“W3”,“b3”的python字典:
W1 - 权重矩阵,维度为(20,2)
b1 - 偏向量,维度为(20,1)
W2 - 权重矩阵,维度为(3,20)
b2 - 偏向量,维度为(3,1)
W3 - 权重矩阵,维度为(1,3)
b3 - 偏向量,维度为(1,1)
keep_prob - 随机删除的概率,实数
返回:
A3 - 最后的激活值,维度为(1,1),正向传播的输出
cache - 存储了一些用于计算反向传播的数值的元组
"""
np.random.seed(1)

W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]
W3 = parameters["W3"]
b3 = parameters["b3"]

#LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID
Z1 = np.dot(W1,X) + b1
A1 = reg_utils.relu(Z1)

#下面的步骤1-4对应于上述的步骤1-4。
D1 = np.random.rand(A1.shape[0],A1.shape[1]) #步骤1:初始化矩阵D1 = np.random.rand(..., ...)
D1 = D1 < keep_prob #步骤2:将D1的值转换为0或1(使​​用keep_prob作为阈值)
A1 = A1 * D1 #步骤3:舍弃A1的一些节点(将它的值变为0或False)
A1 = A1 / keep_prob #步骤4:缩放未舍弃的节点(不为0)的值
"""
#不理解的同学运行一下下面代码就知道了。
import numpy as np
np.random.seed(1)
A1 = np.random.randn(1,3)

D1 = np.random.rand(A1.shape[0],A1.shape[1])
keep_prob=0.5
D1 = D1 < keep_prob
print(D1)

A1 = 0.01
A1 = A1 * D1
A1 = A1 / keep_prob
print(A1)
"""

Z2 = np.dot(W2,A1) + b2
A2 = reg_utils.relu(Z2)

#下面的步骤1-4对应于上述的步骤1-4。
D2 = np.random.rand(A2.shape[0],A2.shape[1]) #步骤1:初始化矩阵D2 = np.random.rand(..., ...)
D2 = D2 < keep_prob #步骤2:将D2的值转换为0或1(使​​用keep_prob作为阈值)
A2 = A2 * D2 #步骤3:舍弃A1的一些节点(将它的值变为0或False)
A2 = A2 / keep_prob #步骤4:缩放未舍弃的节点(不为0)的值

Z3 = np.dot(W3, A2) + b3
A3 = reg_utils.sigmoid(Z3)

cache = (Z1, D1, A1, W1, b1, Z2, D2, A2, W2, b2, Z3, A3, W3, b3)

return A3, cache


带有dropout的反向传播

练习:实现带有dropout的反向传播。和之前一样,训练一个3层的网络。使用存储在缓存中的掩码$D^{[1]}$和$D^{[2]}$,添加dropout到第一和第二个隐藏层。

说明
带有dropout的反向传播实现上非常容易。你将必须执行2个步骤:
1.你先前通过在A1上应用掩码$D^{[1]}$来关闭正向传播过程中的某些神经元。在反向传播中,你将必须将相同的掩码$D^{[1]}$重新应用于dA1来关闭相同的神经元。
2.在正向传播过程中,你已将A1除以keep_prob。 因此,在反向传播中,必须再次将dA1除以keep_prob(计算的解释是,如果$A^{[1]}$被keep_prob缩放,则其派生的$dA^{[1]}$也由相同的keep_prob缩放)。

编写函数backward_propagation_with_dropout(X,Y,cache,keep_prob)实现Dropout的反向传播

  • 步骤:
    • 步骤1:使用正向传播期间相同的节点,舍弃那些关闭的节点(因为任何数乘以0或者False都为0或者False)
    • 步骤2:缩放未舍弃的节点(不为0)的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def backward_propagation_with_dropout(X,Y,cache,keep_prob):
"""
实现我们随机删除的模型的后向传播。
参数:
X - 输入数据集,维度为(2,示例数)
Y - 标签,维度为(输出节点数量,示例数量)
cache - 来自forward_propagation_with_dropout()的cache输出
keep_prob - 随机删除的概率,实数

返回:
gradients - 一个关于每个参数、激活值和预激活变量的梯度值的字典
"""
m = X.shape[1]
(Z1, D1, A1, W1, b1, Z2, D2, A2, W2, b2, Z3, A3, W3, b3) = cache

dZ3 = A3 - Y
dW3 = (1 / m) * np.dot(dZ3,A2.T)
db3 = 1. / m * np.sum(dZ3, axis=1, keepdims=True)
dA2 = np.dot(W3.T, dZ3)

dA2 = dA2 * D2 # 步骤1:使用正向传播期间相同的节点,舍弃那些关闭的节点(因为任何数乘以0或者False都为0或者False)
dA2 = dA2 / keep_prob # 步骤2:缩放未舍弃的节点(不为0)的值

dZ2 = np.multiply(dA2, np.int64(A2 > 0))
dW2 = 1. / m * np.dot(dZ2, A1.T)
db2 = 1. / m * np.sum(dZ2, axis=1, keepdims=True)

dA1 = np.dot(W2.T, dZ2)

dA1 = dA1 * D1 # 步骤1:使用正向传播期间相同的节点,舍弃那些关闭的节点(因为任何数乘以0或者False都为0或者False)
dA1 = dA1 / keep_prob # 步骤2:缩放未舍弃的节点(不为0)的值

dZ1 = np.multiply(dA1, np.int64(A1 > 0))
dW1 = 1. / m * np.dot(dZ1, X.T)
db1 = 1. / m * np.sum(dZ1, axis=1, keepdims=True)

gradients = {"dZ3": dZ3, "dW3": dW3, "db3": db3,"dA2": dA2,
"dZ2": dZ2, "dW2": dW2, "db2": db2, "dA1": dA1,
"dZ1": dZ1, "dW1": dW1, "db1": db1}

return gradients

现在让我们使用dropout(keep_prob = 0.86)运行模型。 这意味着在每次迭代中,你都以24%的概率关闭第1层和第2层的每个神经元。 函数model()将调用:

  • forward_propagation_with_dropout而不是forward_propagation
  • backward_propagation_with_dropout,而不是backward_propagation

调用模型进行训练,使用dropout

1
2
3
4
5
6
7
parameters = model(train_X, train_Y, keep_prob=0.86, learning_rate=0.3,is_plot=True)

print("使用随机删除节点,训练集:")
predictions_train = reg_utils.predict(train_X, train_Y, parameters)
print("使用随机删除节点,测试集:")
reg_utils.predictions_test = reg_utils.predict(test_X, test_Y, parameters)

预期结果

第0次迭代,成本值为:0.654391240515
第10000次迭代,成本值为:0.0610169865749
第20000次迭代,成本值为:0.0605824357985

使用随机删除节点,训练集:
Accuracy: 0.928909952607
使用随机删除节点,测试集:
Accuracy: 0.95

Dropout效果很好!测试精度再次提高(达到95%)!模型并未过拟合训练集,并且在测试集上表现很好。法国足球队将永远感激你!

运行以下代码以绘制决策边界。

1
2
3
4
5
6
plt.title("Model with dropout")
axes = plt.gca()
axes.set_xlim([-0.75, 0.40])
axes.set_ylim([-0.75, 0.65])
reg_utils.plot_decision_boundary(lambda x: reg_utils.predict_dec(parameters, x.T), train_X, train_Y)

注意

  • 使用dropout时的常见错误是在训练和测试中都使用。你只能在训练中使用dropout(随机删除节点)。
  • 深度学习框架,例如tensorflow, PaddlePaddle, keras或者 caffe 附带dropout层的实现。不需强调-相信你很快就会学习到其中的一些框架。

关dropout你应该记住的事情:

  • dropout是一种正则化技术。
  • 仅在训练期间使用dropout,在测试期间不要使用。
  • 在正向和反向传播期间均应用dropout。
  • 在训练期间,将每个dropout层除以keep_prob,以保持激活的期望值相同。例如,如果keep_prob为0.5,则平均而言,我们将关闭一半的节点,因此输出将按0.5缩放,因为只有剩余的一半对解决方案有所贡献。除以0.5等于乘以2,因此输出现在具有相同的期望值。你可以检查此方法是否有效,即使keep_prob的值不是0.5。

结论

这是我们三个模型的结果

模型训练精度测试精度
三层神经网络,无正则化95%91.50%
具有L2正则化的3层NN94%93%
具有dropout的3层NN93%95%

请注意,正则化会损害训练集的性能! 这是因为它限制了网络过拟合训练集的能力。 但是,由于它最终可以提供更好的测试准确性,因此可以为你的系统提供帮助。

恭喜你完成此作业! 同时也帮助了法国足球。 :-)

我们希望你从此笔记本中记住的内容

  • 正则化将帮助减少过拟合。
  • 正则化将使权重降低到较低的值。
  • L2正则化和Dropout是两种非常有效的正则化技术。

代码:
init_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# -*- coding: utf-8 -*-

#init_utils.py

import numpy as np
import matplotlib.pyplot as plt
import sklearn
import sklearn.datasets


def sigmoid(x):
"""
Compute the sigmoid of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- sigmoid(x)
"""
s = 1/(1+np.exp(-x))
return s

def relu(x):
"""
Compute the relu of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- relu(x)
"""
s = np.maximum(0,x)

return s

def compute_loss(a3, Y):

"""
Implement the loss function

Arguments:
a3 -- post-activation, output of forward propagation
Y -- "true" labels vector, same shape as a3

Returns:
loss - value of the loss function
"""

m = Y.shape[1]
logprobs = np.multiply(-np.log(a3),Y) + np.multiply(-np.log(1 - a3), 1 - Y)
loss = 1./m * np.nansum(logprobs)

return loss

def forward_propagation(X, parameters):
"""
Implements the forward propagation (and computes the loss) presented in Figure 2.

Arguments:
X -- input dataset, of shape (input size, number of examples)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat)
parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3":
W1 -- weight matrix of shape ()
b1 -- bias vector of shape ()
W2 -- weight matrix of shape ()
b2 -- bias vector of shape ()
W3 -- weight matrix of shape ()
b3 -- bias vector of shape ()

Returns:
loss -- the loss function (vanilla logistic loss)
"""

# retrieve parameters
W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]
W3 = parameters["W3"]
b3 = parameters["b3"]

# LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID
z1 = np.dot(W1, X) + b1
a1 = relu(z1)
z2 = np.dot(W2, a1) + b2
a2 = relu(z2)
z3 = np.dot(W3, a2) + b3
a3 = sigmoid(z3)

cache = (z1, a1, W1, b1, z2, a2, W2, b2, z3, a3, W3, b3)

return a3, cache

def backward_propagation(X, Y, cache):
"""
Implement the backward propagation presented in figure 2.

Arguments:
X -- input dataset, of shape (input size, number of examples)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat)
cache -- cache output from forward_propagation()

Returns:
gradients -- A dictionary with the gradients with respect to each parameter, activation and pre-activation variables
"""
m = X.shape[1]
(z1, a1, W1, b1, z2, a2, W2, b2, z3, a3, W3, b3) = cache

dz3 = 1./m * (a3 - Y)
dW3 = np.dot(dz3, a2.T)
db3 = np.sum(dz3, axis=1, keepdims = True)

da2 = np.dot(W3.T, dz3)
dz2 = np.multiply(da2, np.int64(a2 > 0))
dW2 = np.dot(dz2, a1.T)
db2 = np.sum(dz2, axis=1, keepdims = True)

da1 = np.dot(W2.T, dz2)
dz1 = np.multiply(da1, np.int64(a1 > 0))
dW1 = np.dot(dz1, X.T)
db1 = np.sum(dz1, axis=1, keepdims = True)

gradients = {"dz3": dz3, "dW3": dW3, "db3": db3,
"da2": da2, "dz2": dz2, "dW2": dW2, "db2": db2,
"da1": da1, "dz1": dz1, "dW1": dW1, "db1": db1}

return gradients

def update_parameters(parameters, grads, learning_rate):
"""
Update parameters using gradient descent

Arguments:
parameters -- python dictionary containing your parameters
grads -- python dictionary containing your gradients, output of n_model_backward

Returns:
parameters -- python dictionary containing your updated parameters
parameters['W' + str(i)] = ...
parameters['b' + str(i)] = ...
"""

L = len(parameters) // 2 # number of layers in the neural networks

# Update rule for each parameter
for k in range(L):
parameters["W" + str(k+1)] = parameters["W" + str(k+1)] - learning_rate * grads["dW" + str(k+1)]
parameters["b" + str(k+1)] = parameters["b" + str(k+1)] - learning_rate * grads["db" + str(k+1)]

return parameters

def predict(X, y, parameters):
"""
This function is used to predict the results of a n-layer neural network.

Arguments:
X -- data set of examples you would like to label
parameters -- parameters of the trained model

Returns:
p -- predictions for the given dataset X
"""

m = X.shape[1]
p = np.zeros((1,m), dtype = np.int)

# Forward propagation
a3, caches = forward_propagation(X, parameters)

# convert probas to 0/1 predictions
for i in range(0, a3.shape[1]):
if a3[0,i] > 0.5:
p[0,i] = 1
else:
p[0,i] = 0

# print results
print("Accuracy: " + str(np.mean((p[0,:] == y[0,:]))))

return p

def load_dataset(is_plot=True):
np.random.seed(1)
train_X, train_Y = sklearn.datasets.make_circles(n_samples=300, noise=.05)
np.random.seed(2)
test_X, test_Y = sklearn.datasets.make_circles(n_samples=100, noise=.05)
# Visualize the data
if is_plot:
plt.scatter(train_X[:, 0], train_X[:, 1], c=train_Y, s=40, cmap=plt.cm.Spectral);
train_X = train_X.T
train_Y = train_Y.reshape((1, train_Y.shape[0]))
test_X = test_X.T
test_Y = test_Y.reshape((1, test_Y.shape[0]))
return train_X, train_Y, test_X, test_Y

def plot_decision_boundary(model, X, y):
# Set min and max values and give it some padding
x_min, x_max = X[0, :].min() - 1, X[0, :].max() + 1
y_min, y_max = X[1, :].min() - 1, X[1, :].max() + 1
h = 0.01
# Generate a grid of points with distance h between them
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
# Predict the function value for the whole grid
Z = model(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot the contour and training examples
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(X[0, :], X[1, :], c=y, cmap=plt.cm.Spectral)
plt.show()

def predict_dec(parameters, X):
"""
Used for plotting decision boundary.

Arguments:
parameters -- python dictionary containing your parameters
X -- input data of size (m, K)

Returns
predictions -- vector of predictions of our model (red: 0 / blue: 1)
"""

# Predict using forward propagation and a classification threshold of 0.5
a3, cache = forward_propagation(X, parameters)
predictions = (a3>0.5)
return predictions



reg_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# -*- coding: utf-8 -*-

#reg_utils.py

import numpy as np
import matplotlib.pyplot as plt
import scipy.io as sio

def sigmoid(x):
"""
Compute the sigmoid of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- sigmoid(x)
"""
s = 1/(1+np.exp(-x))
return s

def relu(x):
"""
Compute the relu of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- relu(x)
"""
s = np.maximum(0,x)

return s


def initialize_parameters(layer_dims):
"""
Arguments:
layer_dims -- python array (list) containing the dimensions of each layer in our network

Returns:
parameters -- python dictionary containing your parameters "W1", "b1", ..., "WL", "bL":
W1 -- weight matrix of shape (layer_dims[l], layer_dims[l-1])
b1 -- bias vector of shape (layer_dims[l], 1)
Wl -- weight matrix of shape (layer_dims[l-1], layer_dims[l])
bl -- bias vector of shape (1, layer_dims[l])

Tips:
- For example: the layer_dims for the "Planar Data classification model" would have been [2,2,1].
This means W1's shape was (2,2), b1 was (1,2), W2 was (2,1) and b2 was (1,1). Now you have to generalize it!
- In the for loop, use parameters['W' + str(l)] to access Wl, where l is the iterative integer.
"""

np.random.seed(3)
parameters = {}
L = len(layer_dims) # number of layers in the network

for l in range(1, L):
parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1]) / np.sqrt(layer_dims[l-1])
parameters['b' + str(l)] = np.zeros((layer_dims[l], 1))

assert(parameters['W' + str(l)].shape == layer_dims[l], layer_dims[l-1])
assert(parameters['W' + str(l)].shape == layer_dims[l], 1)


return parameters

def forward_propagation(X, parameters):
"""
Implements the forward propagation (and computes the loss) presented in Figure 2.

Arguments:
X -- input dataset, of shape (input size, number of examples)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat)
parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3":
W1 -- weight matrix of shape ()
b1 -- bias vector of shape ()
W2 -- weight matrix of shape ()
b2 -- bias vector of shape ()
W3 -- weight matrix of shape ()
b3 -- bias vector of shape ()

Returns:
loss -- the loss function (vanilla logistic loss)
"""

# retrieve parameters
W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]
W3 = parameters["W3"]
b3 = parameters["b3"]

# LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID
z1 = np.dot(W1, X) + b1
a1 = relu(z1)
z2 = np.dot(W2, a1) + b2
a2 = relu(z2)
z3 = np.dot(W3, a2) + b3
a3 = sigmoid(z3)

cache = (z1, a1, W1, b1, z2, a2, W2, b2, z3, a3, W3, b3)

return a3, cache



def compute_cost(a3, Y):
"""
Implement the cost function

Arguments:
a3 -- post-activation, output of forward propagation
Y -- "true" labels vector, same shape as a3

Returns:
cost - value of the cost function
"""
m = Y.shape[1]

logprobs = np.multiply(-np.log(a3),Y) + np.multiply(-np.log(1 - a3), 1 - Y)
cost = 1./m * np.nansum(logprobs)

return cost

def backward_propagation(X, Y, cache):
"""
Implement the backward propagation presented in figure 2.

Arguments:
X -- input dataset, of shape (input size, number of examples)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat)
cache -- cache output from forward_propagation()

Returns:
gradients -- A dictionary with the gradients with respect to each parameter, activation and pre-activation variables
"""
m = X.shape[1]
(z1, a1, W1, b1, z2, a2, W2, b2, z3, a3, W3, b3) = cache

dz3 = 1./m * (a3 - Y)
dW3 = np.dot(dz3, a2.T)
db3 = np.sum(dz3, axis=1, keepdims = True)

da2 = np.dot(W3.T, dz3)
dz2 = np.multiply(da2, np.int64(a2 > 0))
dW2 = np.dot(dz2, a1.T)
db2 = np.sum(dz2, axis=1, keepdims = True)

da1 = np.dot(W2.T, dz2)
dz1 = np.multiply(da1, np.int64(a1 > 0))
dW1 = np.dot(dz1, X.T)
db1 = np.sum(dz1, axis=1, keepdims = True)

gradients = {"dz3": dz3, "dW3": dW3, "db3": db3,
"da2": da2, "dz2": dz2, "dW2": dW2, "db2": db2,
"da1": da1, "dz1": dz1, "dW1": dW1, "db1": db1}

return gradients

def update_parameters(parameters, grads, learning_rate):
"""
Update parameters using gradient descent

Arguments:
parameters -- python dictionary containing your parameters
grads -- python dictionary containing your gradients, output of n_model_backward

Returns:
parameters -- python dictionary containing your updated parameters
parameters['W' + str(i)] = ...
parameters['b' + str(i)] = ...
"""

L = len(parameters) // 2 # number of layers in the neural networks

# Update rule for each parameter
for k in range(L):
parameters["W" + str(k+1)] = parameters["W" + str(k+1)] - learning_rate * grads["dW" + str(k+1)]
parameters["b" + str(k+1)] = parameters["b" + str(k+1)] - learning_rate * grads["db" + str(k+1)]

return parameters




def load_2D_dataset(is_plot=True):
data = sio.loadmat('datasets/data.mat')
train_X = data['X'].T
train_Y = data['y'].T
test_X = data['Xval'].T
test_Y = data['yval'].T
if is_plot:
plt.scatter(train_X[0, :], train_X[1, :], c=train_Y, s=40, cmap=plt.cm.Spectral);

return train_X, train_Y, test_X, test_Y

def predict(X, y, parameters):
"""
This function is used to predict the results of a n-layer neural network.

Arguments:
X -- data set of examples you would like to label
parameters -- parameters of the trained model

Returns:
p -- predictions for the given dataset X
"""

m = X.shape[1]
p = np.zeros((1,m), dtype = np.int)

# Forward propagation
a3, caches = forward_propagation(X, parameters)

# convert probas to 0/1 predictions
for i in range(0, a3.shape[1]):
if a3[0,i] > 0.5:
p[0,i] = 1
else:
p[0,i] = 0

# print results
print("Accuracy: " + str(np.mean((p[0,:] == y[0,:]))))

return p

def plot_decision_boundary(model, X, y):
# Set min and max values and give it some padding
x_min, x_max = X[0, :].min() - 1, X[0, :].max() + 1
y_min, y_max = X[1, :].min() - 1, X[1, :].max() + 1
h = 0.01
# Generate a grid of points with distance h between them
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
# Predict the function value for the whole grid
Z = model(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot the contour and training examples
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(X[0, :], X[1, :], c=y, cmap=plt.cm.Spectral)
plt.show()

def predict_dec(parameters, X):
"""
Used for plotting decision boundary.

Arguments:
parameters -- python dictionary containing your parameters
X -- input data of size (m, K)

Returns
predictions -- vector of predictions of our model (red: 0 / blue: 1)
"""

# Predict using forward propagation and a classification threshold of 0.5
a3, cache = forward_propagation(X, parameters)
predictions = (a3>0.5)
return predictions



gc_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# -*- coding: utf-8 -*-

#gc_utils.py

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
"""
Compute the sigmoid of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- sigmoid(x)
"""
s = 1/(1+np.exp(-x))
return s

def relu(x):
"""
Compute the relu of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- relu(x)
"""
s = np.maximum(0,x)

return s



def dictionary_to_vector(parameters):
"""
Roll all our parameters dictionary into a single vector satisfying our specific required shape.
"""
keys = []
count = 0
for key in ["W1", "b1", "W2", "b2", "W3", "b3"]:

# flatten parameter
new_vector = np.reshape(parameters[key], (-1,1))
keys = keys + [key]*new_vector.shape[0]

if count == 0:
theta = new_vector
else:
theta = np.concatenate((theta, new_vector), axis=0)
count = count + 1

return theta, keys

def vector_to_dictionary(theta):
"""
Unroll all our parameters dictionary from a single vector satisfying our specific required shape.
"""
parameters = {}
parameters["W1"] = theta[:20].reshape((5,4))
parameters["b1"] = theta[20:25].reshape((5,1))
parameters["W2"] = theta[25:40].reshape((3,5))
parameters["b2"] = theta[40:43].reshape((3,1))
parameters["W3"] = theta[43:46].reshape((1,3))
parameters["b3"] = theta[46:47].reshape((1,1))

return parameters

def gradients_to_vector(gradients):
"""
Roll all our gradients dictionary into a single vector satisfying our specific required shape.
"""

count = 0
for key in ["dW1", "db1", "dW2", "db2", "dW3", "db3"]:
# flatten parameter
new_vector = np.reshape(gradients[key], (-1,1))

if count == 0:
theta = new_vector
else:
theta = np.concatenate((theta, new_vector), axis=0)
count = count + 1

return theta


作业5 梯度下降的优化

优化算法

到目前为止,你一直使用梯度下降来更新参数并使损失降至最低。 在本笔记本中,你将学习更多高级的优化方法,以加快学习速度,甚至可以使你的损失函数的获得更低的最终值。 一个好的优化算法可以使需要训练几天的网络,训练仅仅几个小时就能获得良好的结果。
梯度下降好比在损失函数 $J$上“下坡”。就像下图:

图1 损失最小化好比在丘陵景观中寻找最低点

在训练的每个步骤中,你都按照一定的方向更新参数,以尝试到达最低点。

符号:与往常一样,$\frac{\partial J}{\partial a } =$ da适用于任何变量a

首先,请运行以下代码以导入所需的库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding: utf-8 -*-  # 指定文件编码为UTF-8,以支持中文字符

import numpy as np # 导入NumPy库,用于数值计算和数组操作
import matplotlib.pyplot as plt # 导入Matplotlib库中的pyplot模块,用于绘图
import scipy.io # 导入SciPy库,用于处理输入输出,尤其是MATLAB文件
import math # 导入数学库,提供基本数学功能
import sklearn # 导入Scikit-learn库,常用于机器学习任务
import sklearn.datasets # 导入Scikit-learn中的数据集模块,提供常用数据集

import opt_utils # 导入自定义工具模块opt_utils,通常包含优化相关的函数
import testCase # 导入自定义测试用例模块testCase,可能用于测试和验证功能

# %matplotlib inline # 如果使用Jupyter Notebook,请取消注释以在Notebook中直接显示图像

# 设置Matplotlib绘图的默认图像大小
plt.rcParams['figure.figsize'] = (7.0, 4.0) # 图像宽7.0,高4.0
plt.rcParams['image.interpolation'] = 'nearest' # 设置图像插值方式为“最近邻”
plt.rcParams['image.cmap'] = 'gray' # 设置图像的颜色映射为灰度

梯度下降

机器学习中一种简单的优化方法是梯度下降(gradient descent,GD)。当你对每个step中的所有$m$示例执行梯度计算步骤时,它也叫做“批量梯度下降”。

热身练习:实现梯度下降更新方法。 对于$l = 1, …, L$,梯度下降规则为:

其中L是层数,$\alpha$是学习率。所有参数都应存储在 parameters字典中。请注意,迭代器lfor 循环中从0开始,而第一个参数是$W^{[1]}$和$b^{[1]}$。编码时需要将l 转换为l+1

编写函数update_parameters_with_gd(parameters,grads,learning_rate)实现梯度更新
要求:

  • 参数:

    • parameters - 字典,包含了要更新的参数:
      • parameters[‘W’ + str(l)] = Wl
      • parameters[‘b’ + str(l)] = bl
    • grads - 字典,包含了每一个梯度值用以更新参数
      • grads[‘dW’ + str(l)] = dWl
      • grads[‘db’ + str(l)] = dbl
    • learning_rate - 学习率
  • 返回值:

    • parameters - 字典,包含了更新后的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def update_parameters_with_gd(parameters,grads,learning_rate):
"""
使用梯度下降更新参数

参数:
parameters - 字典,包含了要更新的参数:
parameters['W' + str(l)] = Wl
parameters['b' + str(l)] = bl
grads - 字典,包含了每一个梯度值用以更新参数
grads['dW' + str(l)] = dWl
grads['db' + str(l)] = dbl
learning_rate - 学习率

返回值:
parameters - 字典,包含了更新后的参数
"""

L = len(parameters) // 2 #神经网络的层数

#更新每个参数
for l in range(L):
parameters["W" + str(l +1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l + 1)]
parameters["b" + str(l +1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]

return parameters

运行代码测试函数update_parameters_with_gd(parameters,grads,learning_rate)

1
2
3
4
5
6
7
8
9
#测试update_parameters_with_gd
print("-------------测试update_parameters_with_gd-------------")
parameters , grads , learning_rate = testCase.update_parameters_with_gd_test_case()
parameters = update_parameters_with_gd(parameters,grads,learning_rate)
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

预期输出:
W1 = [[ 1.63535156 -0.62320365 -0.53718766]
[-1.07799357 0.85639907 -2.29470142]]
b1 = [[ 1.74604067]
[-0.75184921]]
W2 = [[ 0.32171798 -0.25467393 1.46902454]
[-2.05617317 -0.31554548 -0.3756023 ]
[ 1.1404819 -1.09976462 -0.1612551 ]]
b2 = [[-0.88020257]
[ 0.02561572]
[ 0.57539477]]

它的一种变体是随机梯度下降(SGD),它相当于mini版的批次梯度下降,其中每个mini-batch只有一个数据示例。刚刚实现的更新规则不会更改。不同的是,SGD一次仅在一个训练数据上计算梯度,而不是在整个训练集合上计算梯度。下面的代码示例说明了随机梯度下降和(批量)梯度下降之间的区别。
下面的代码不要运行,只需查看两者区别

  • (Batch) Gradient Descent:批量梯度下降,又叫梯度下降
1
2
3
4
5
6
7
8
9
10
parameters = initialize_parameters(layers_dims)
for i in range(0,num_iterations):
#前向传播
A,cache = forward_propagation(X,parameters)
#计算损失
cost = compute_cost(A,Y)
#反向传播
grads = backward_propagation(X,Y,cache)
#更新参数
parameters = update_parameters(parameters,grads)
  • Stochastic Gradient Descent: 随机梯度下降算法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    parameters = initialize_parameters(layers_dims)
    for i in (0,num_iterations):
    for j in m:
    #前向传播
    A,cache = forward_propagation(X,parameters)
    #计算成本
    cost = compute_cost(A,Y)
    #后向传播
    grads = backward_propagation(X,Y,cache)
    #更新参数
    parameters = update_parameters(parameters,grads)

对于随机梯度下降,在更新梯度之前,只使用1个训练样例。当训练集大时,SGD可以更新的更快。但是这些参数会向最小值“摆动”而不是平稳地收敛。下图是一个演示例子:

图 1: SGD vs GD
“+”表示损失的最小值。 SGD造成许多振荡以达到收敛。但是每个step中,计算SGD比使用GD更快,因为它仅使用一个训练示例(相对于GD的整个批次)。

注意:实现SGD总共需要3个for循环:
1.迭代次数
2.$m$个训练数据
3.各层上(要更新所有参数,从$(W^{[1]},b^{[1]})$到$(W^{[L]},b^{[L]})$)

实际上,如果你既不使用整个训练集也不使用一个训练示例来执行每次更新,则通常会得到更快的结果。小批量梯度下降法在每个步骤中使用中间数量的示例。通过小批量梯度下降,你可以遍历小批量,而不是遍历各个训练示例。

图 2SGD vs Mini-Batch GD
“+”表示损失的最小值。在优化算法中使用mini-batch批处理通常可以加快优化速度。

你应该记住

  • 梯度下降,小批量梯度下降和随机梯度下降之间的差异是用于执行一个更新步骤的数据数量。
  • 必须调整超参数学习率$\alpha$。
  • 在小批量的情况下,通常它会胜过梯度下降或随机梯度下降(尤其是训练集较大时)。

Mini-Batch 梯度下降

让我们学习如何从训练集(X,Y)中构建小批次数据。

分两个步骤:

  • Shuffle:如下所示,创建训练集(X,Y)的随机打乱版本。X和Y中的每一列代表一个训练示例。注意,随机打乱是在X和Y之间同步完成的。这样,在随机打乱之后,X的$i^{th}$列就是对应于Y中$i^{th}$标签的示例。打乱步骤可确保该示例将随机分为不同小批。
  • Partition:将打乱后的(X,Y)划分为大小为mini_batch_size(此处为64)的小批处理。请注意,训练示例的数量并不总是可以被mini_batch_size整除。最后的小批量可能较小,但是你不必担心,当最终的迷你批处理小于完整的mini_batch_size时,它将如下图所示:

练习:实现random_mini_batches
请注意,最后一个小批次的结果可能小于mini_batch_size=64。令$\lfloor s \rfloor$代表$s$向下舍入到最接近的整数(在Python中为math.floor(s))。如果示例总数不是mini_batch_size = 64的倍数,则将有$\lfloor \frac{m}{mini_batch_size}\rfloor$个带有完整示例的小批次,数量为64最终的一批次中的示例将是()。

编写函数random_mini_batches(X,Y,mini_batch_size=64,seed=0)实现构建小批次数据
要求

  • 从(X,Y)中创建一个随机的mini-batch列表

  • 参数:

    • X - 输入数据,维度为(输入节点数量,样本的数量)
    • Y - 对应的是X的标签,【1 | 0】(蓝|红),维度为(1,样本的数量)
    • mini_batch_size - 每个mini-batch的样本数量
  • 返回:

    • mini-bacthes - 一个同步列表,维度为(mini_batch_X,mini_batch_Y)
    • mini_batches[][]第一个索引是mini_batch_X的列表的索引号,第二个索引是mini_batch_Y的列表的索引号
      提示
  • permutation = list(np.random.permutation(m))返回一个长度为m的随机数组,且里面的数是0到m-1

  • 使用获得的列表permutation当索引打乱样本数据
  • 使用向下取整函数math.floor()来获得minibatch的数量num_complete_minibatches
  • 切片操作获得a的第4到6列(索引为3-5) a[:, 13:(1+1)3],切片操作左闭右开
  • 切片操作获得mini_batch_X可以用
    • for k in range(0,num_complete_minibatches):
      • mini_batch_X = shuffled_X[:,k mini_batch_size:(k+1)mini_batch_size]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def random_mini_batches(X,Y,mini_batch_size=64,seed=0):
"""
从(X,Y)中创建一个随机的mini-batch列表

参数:
X - 输入数据,维度为(输入节点数量,样本的数量)
Y - 对应的是X的标签,【1 | 0】(蓝|红),维度为(1,样本的数量)
mini_batch_size - 每个mini-batch的样本数量

返回:
mini-bacthes - 一个同步列表,维度为(mini_batch_X,mini_batch_Y)

"""

np.random.seed(seed) #指定随机种子
m = X.shape[1]
mini_batches = []

#第一步:打乱顺序
permutation = list(np.random.permutation(m)) #它会返回一个长度为m的随机数组,且里面的数是0到m-1
shuffled_X = X[:,permutation] #将每一列的数据按permutation的顺序来重新排列。
shuffled_Y = Y[:,permutation].reshape((1,m))

"""
#博主注:
#如果你不好理解的话请看一下下面的伪代码,看看X和Y是如何根据permutation来打乱顺序的。
x = np.array([[1,2,3,4,5,6,7,8,9],
[9,8,7,6,5,4,3,2,1]])
y = np.array([[1,0,1,0,1,0,1,0,1]])

random_mini_batches(x,y)
permutation= [7, 2, 1, 4, 8, 6, 3, 0, 5]
shuffled_X= [[8 3 2 5 9 7 4 1 6]
[2 7 8 5 1 3 6 9 4]]
shuffled_Y= [[0 1 0 1 1 1 0 1 0]]
"""

#第二步,分割
num_complete_minibatches = math.floor(m / mini_batch_size) #把你的训练集分割成多少份,请注意,如果值是99.99,那么返回值是99,剩下的0.99会被舍弃
for k in range(0,num_complete_minibatches):
mini_batch_X = shuffled_X[:,k * mini_batch_size:(k+1)*mini_batch_size]
mini_batch_Y = shuffled_Y[:,k * mini_batch_size:(k+1)*mini_batch_size]
"""
#博主注:
#如果你不好理解的话请单独执行下面的代码,它可以帮你理解一些。
a = np.array([[1,2,3,4,5,6,7,8,9],
[9,8,7,6,5,4,3,2,1],
[1,2,3,4,5,6,7,8,9]])
k=1
mini_batch_size=3
print(a[:,1*3:(1+1)*3]) #从第4列到第6列
'''
[[4 5 6]
[6 5 4]
[4 5 6]]
'''
k=2
print(a[:,2*3:(2+1)*3]) #从第7列到第9列
'''
[[7 8 9]
[3 2 1]
[7 8 9]]
'''

#看一下每一列的数据你可能就会好理解一些
"""
mini_batch = (mini_batch_X,mini_batch_Y)
mini_batches.append(mini_batch)

#如果训练集的大小刚好是mini_batch_size的整数倍,那么这里已经处理完了
#如果训练集的大小不是mini_batch_size的整数倍,那么最后肯定会剩下一些,我们要把它处理了
if m % mini_batch_size != 0:
#获取最后剩余的部分
mini_batch_X = shuffled_X[:,mini_batch_size * num_complete_minibatches:]
mini_batch_Y = shuffled_Y[:,mini_batch_size * num_complete_minibatches:]

mini_batch = (mini_batch_X,mini_batch_Y)
mini_batches.append(mini_batch)

return mini_batches

现在测试random_mini_batches

1
2
3
4
5
6
7
8
9
10
11
12
13
#测试random_mini_batches
print("-------------测试random_mini_batches-------------")
X_assess,Y_assess,mini_batch_size = testCase.random_mini_batches_test_case()
mini_batches = random_mini_batches(X_assess,Y_assess,mini_batch_size)

print("第1个mini_batch_X 的维度为:",mini_batches[0][0].shape)
print("第1个mini_batch_Y 的维度为:",mini_batches[0][1].shape)
print("第2个mini_batch_X 的维度为:",mini_batches[1][0].shape)
print("第2个mini_batch_Y 的维度为:",mini_batches[1][1].shape)
print("第3个mini_batch_X 的维度为:",mini_batches[2][0].shape)
print("第3个mini_batch_Y 的维度为:",mini_batches[2][1].shape)


预期输出:
shape of the 1st mini_batch_X: (12288, 64)
shape of the 2nd mini_batch_X: (12288, 64)
shape of the 3rd mini_batch_X: (12288, 20)
shape of the 1st mini_batch_Y: (1, 64)
shape of the 2nd mini_batch_Y: (1, 64)
shape of the 3rd mini_batch_Y: (1, 20)
mini batch sanity check: [ 0.90085595 -0.7612069 0.2344157 ]

分析:
每一个mini_batch_X都有64个样本,每个样本是一列
每一个mini_batch_Y都有64个样本标签,每个样本是一列

你应该记住

  • 通常选择2的幂作为最小批量大小,例如16、32、64、128。

Momentum

因为小批量梯度下降仅在看到示例的子集后才进行参数更新,所以更新的方向具有一定的差异,因此小批量梯度下降所采取的路径将“朝着收敛”振荡。利用冲量则可以减少这些振荡。

冲量考虑了过去的梯度以平滑更新。我们将先前梯度的“方向”存储在变量$v$中。这将是先前步骤中梯度的指数加权平均值,你也可以将$v$看作是下坡滚动的球的“速度”,根据山坡的坡度/坡度的方向来提高速度(和冲量)。

图 3:红色箭头显示了带冲量的小批次梯度下降步骤所采取的方向。蓝点表示每一步的梯度方向(相对于当前的小批量)。让梯度影响$v$而不是仅遵循梯度,然后朝$v$的方向迈出一步。

练习:初始化速度。速度$v$是一个Python字典,需要使用零数组进行初始化。它的键与grads词典中的键相同,即:
为$l =1,…,L$:

1
2
v["dW" + str(l+1)] = ... #(numpy array of zeros with the same shape as parameters["W" + str(l+1)])
v["db" + str(l+1)] = ... #(numpy array of zeros with the same shape as parameters["b" + str(l+1)])

注意:迭代器l在for循环中从0开始,而第一个参数是v[“dW1”]和v[“db1”](在上标中为“1”)。这就是为什么我们在“for”循环中将l转换为l+1的原因。
提示

  • np.zeros_like()用于创建一个与给定数组具有相同形状和数据类型的全零数组。

创建函数initialize_velocity(parameters)初始化速度,返回字典v是与parameters相同的全0字典

要求

  • 初始化速度,velocity是一个字典:
  • keys: “dW1”, “db1”, …, “dWL”, “dbL”
  • values:与相应的梯度/参数维度相同的值为零的矩阵。
  • 参数:
    • parameters - 一个字典,包含了以下参数:
    • parameters[“W” + str(l)] = Wl
    • parameters[“b” + str(l)] = bl
  • 返回:
    • v - 一个字典变量,包含了以下参数:
    • v[“dW” + str(l)] = dWl的速度
    • v[“db” + str(l)] = dbl的速度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def initialize_velocity(parameters):
"""
初始化速度,velocity是一个字典:
- keys: "dW1", "db1", ..., "dWL", "dbL"
- values:与相应的梯度/参数维度相同的值为零的矩阵。
参数:
parameters - 一个字典,包含了以下参数:
parameters["W" + str(l)] = Wl
parameters["b" + str(l)] = bl
返回:
v - 一个字典变量,包含了以下参数:
v["dW" + str(l)] = dWl的速度
v["db" + str(l)] = dbl的速度

"""
L = len(parameters) // 2 #神经网络的层数
v = {}

for l in range(L):
v["dW" + str(l + 1)] = np.zeros_like(parameters["W" + str(l + 1)])
v["db" + str(l + 1)] = np.zeros_like(parameters["b" + str(l + 1)])

return v

测试initialize_velocity

1
2
3
4
5
6
7
8
9
10
#测试initialize_velocity
print("-------------测试initialize_velocity-------------")
parameters = testCase.initialize_velocity_test_case()
v = initialize_velocity(parameters)

print('v["dW1"] = ' + str(v["dW1"]))
print('v["db1"] = ' + str(v["db1"]))
print('v["dW2"] = ' + str(v["dW2"]))
print('v["db2"] = ' + str(v["db2"]))

预期输出:
v[“dW1”] = [[0. 0. 0.]
[0. 0. 0.]]
v[“db1”] = [[0.]
[0.]]
v[“dW2”] = [[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
v[“db2”] = [[0.]
[0.]
[0.]]

练习:实现带冲量的参数更新。冲量更新规则是,对于$l = 1, …, L$:

其中L是层数,$\beta$是动量,$\alpha$是学习率。所有参数都应存储在parameters字典中。请注意,迭代器lfor循环中从0开始,而第一个参数是$W^{[1]}$和$b^{[1]}$(在上标中为“1”)。因此,编码时需要将l转化至l+1

编写函数update_parameters_with_momentun(parameters,grads,v,beta,learning_rate使用动量更新参数

要求

  • 参数:
    • parameters - 一个字典类型的变量,包含了以下字段:
    • parameters[“W” + str(l)] = Wl
    • parameters[“b” + str(l)] = bl
    • grads - 一个包含梯度值的字典变量,具有以下字段:
      • grads[“dW” + str(l)] = dWl
      • grads[“db” + str(l)] = dbl
    • v - 包含当前速度的字典变量,具有以下字段:
      • v[“dW” + str(l)] = …
      • v[“db” + str(l)] = …
    • beta - 超参数,动量,实数
    • learning_rate - 学习率,实数
  • 返回:
    • parameters - 更新后的参数字典
    • v - 包含了更新后的速度变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def update_parameters_with_momentun(parameters,grads,v,beta,learning_rate):
"""
使用动量更新参数
参数:
parameters - 一个字典类型的变量,包含了以下字段:
parameters["W" + str(l)] = Wl
parameters["b" + str(l)] = bl
grads - 一个包含梯度值的字典变量,具有以下字段:
grads["dW" + str(l)] = dWl
grads["db" + str(l)] = dbl
v - 包含当前速度的字典变量,具有以下字段:
v["dW" + str(l)] = ...
v["db" + str(l)] = ...
beta - 超参数,动量,实数
learning_rate - 学习率,实数
返回:
parameters - 更新后的参数字典
v - 包含了更新后的速度变量
"""
L = len(parameters) // 2
for l in range(L):
#计算速度
v["dW" + str(l + 1)] = beta * v["dW" + str(l + 1)] + (1 - beta) * grads["dW" + str(l + 1)]
v["db" + str(l + 1)] = beta * v["db" + str(l + 1)] + (1 - beta) * grads["db" + str(l + 1)]

#更新参数
parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * v["dW" + str(l + 1)]
parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * v["db" + str(l + 1)]

return parameters,v

运行代码测试update_parameters_with_momentun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#测试update_parameters_with_momentun
print("-------------测试update_parameters_with_momentun-------------")
parameters,grads,v = testCase.update_parameters_with_momentum_test_case()
update_parameters_with_momentun(parameters,grads,v,beta=0.9,learning_rate=0.01)

print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))
print('v["dW1"] = ' + str(v["dW1"]))
print('v["db1"] = ' + str(v["db1"]))
print('v["dW2"] = ' + str(v["dW2"]))
print('v["db2"] = ' + str(v["db2"]))

预期输出:
W1 = [[ 1.62544598 -0.61290114 -0.52907334]
[-1.07347112 0.86450677 -2.30085497]]
b1 = [[ 1.74493465]
[-0.76027113]]
W2 = [[ 0.31930698 -0.24990073 1.4627996 ]
[-2.05974396 -0.32173003 -0.38320915]
[ 1.13444069 -1.0998786 -0.1713109 ]]
b2 = [[-0.87809283]
[ 0.04055394]
[ 0.58207317]]
v[“dW1”] = [[-0.11006192 0.11447237 0.09015907]
[ 0.05024943 0.09008559 -0.06837279]]
v[“db1”] = [[-0.01228902]
[-0.09357694]]
v[“dW2”] = [[-0.02678881 0.05303555 -0.06916608]
[-0.03967535 -0.06871727 -0.08452056]
[-0.06712461 -0.00126646 -0.11173103]]
v[“db2”] = [[0.02344157]
[0.16598022]
[0.07420442]]

注意

  • 速度用零初始化。因此,该算法将花费一些迭代来“提高”速度并开始采取更大的步骤。
  • 如果$\beta = 0$,则它变为没有冲量的标准梯度下降。

怎样选择$\beta$?

  • 冲量$\beta$越大,更新越平滑,因为我们对过去的梯度的考虑也更多。但是,如果$\beta$太大,也可能使更新变得过于平滑。
  • $\beta$的常用值范围是0.8到0.999。如果你不想调整它,则$\beta = 0.9$通常是一个合理的默认值。
  • 调整模型的最佳$\beta$可能需要尝试几个值,以了解在降低损失函数$J$的值方面最有效的方法。

你应该记住

  • 冲量将过去的梯度考虑在内,以平滑梯度下降的步骤。它可以应用于批量梯度下降,小批次梯度下降或随机梯度下降。
  • 必须调整冲量超参数$\beta$和学习率$\alpha$。

Adam

Adam是训练神经网络最有效的优化算法之一。它结合了RMSProp和Momentum的优点。

Adam原理
1.计算过去梯度的指数加权平均值,并将其存储在变量$v$(使用偏差校正之前)和$v^{corrected}$ (使用偏差校正)中。
2.计算过去梯度的平方的指数加权平均值,并将其存储在变量$s$(偏差校正之前)和$s^{corrected}$(偏差校正中)中。
3.组合“1”和“2”的信息,在一个方向上更新参数。

对于$l = 1, …, L$,更新规则为:

其中:

  • t计算出Adam采取的步骤数
  • L是层数
  • $\beta_1$和$\beta_2$是控制两个指数加权平均值的超参数。
  • $\alpha$是学习率
  • $\varepsilon$是一个很小的数字,以避免被零除

编写函数initialize_adam(parameters)初始化v和s,它们的key与grads的key相同

要求
初始化v和s,它们都是字典类型的变量,都包含了以下字段:

  • keys: “dW1”, “db1”, …, “dWL”, “dbL”
  • values:与对应的梯度/参数相同维度的值为零的numpy矩阵

  • 参数:

    • parameters - 包含了以下参数的字典变量:
    • parameters[“W” + str(l)] = Wl
    • parameters[“b” + str(l)] = bl
  • 返回:
    • v - 包含梯度的指数加权平均值,字段如下:
      • v[“dW” + str(l)] = …
      • v[“db” + str(l)] = …
    • s - 包含平方梯度的指数加权平均值,字段如下:
      • s[“dW” + str(l)] = …
      • s[“db” + str(l)] = …
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def initialize_adam(parameters):
"""
初始化v和s,它们都是字典类型的变量,都包含了以下字段:
- keys: "dW1", "db1", ..., "dWL", "dbL"
- values:与对应的梯度/参数相同维度的值为零的numpy矩阵

参数:
parameters - 包含了以下参数的字典变量:
parameters["W" + str(l)] = Wl
parameters["b" + str(l)] = bl
返回:
v - 包含梯度的指数加权平均值,字段如下:
v["dW" + str(l)] = ...
v["db" + str(l)] = ...
s - 包含平方梯度的指数加权平均值,字段如下:
s["dW" + str(l)] = ...
s["db" + str(l)] = ...

"""

L = len(parameters) // 2
v = {}
s = {}

for l in range(L):
v["dW" + str(l + 1)] = np.zeros_like(parameters["W" + str(l + 1)])
v["db" + str(l + 1)] = np.zeros_like(parameters["b" + str(l + 1)])

s["dW" + str(l + 1)] = np.zeros_like(parameters["W" + str(l + 1)])
s["db" + str(l + 1)] = np.zeros_like(parameters["b" + str(l + 1)])

return (v,s)


和之前一样,我们将所有参数存储在parameters字典中

练习:初始化跟踪过去信息的Adam变量$v, s$

说明:变量$v, s$是需要用零数组初始化的python字典。它们的key与grads的key相同,即:
对于$l = 1, …, L$:

运行代码测试initialize_adam

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#测试initialize_adam
print("-------------测试initialize_adam-------------")
parameters = testCase.initialize_adam_test_case()
v,s = initialize_adam(parameters)

print('v["dW1"] = ' + str(v["dW1"]))
print('v["db1"] = ' + str(v["db1"]))
print('v["dW2"] = ' + str(v["dW2"]))
print('v["db2"] = ' + str(v["db2"]))
print('s["dW1"] = ' + str(s["dW1"]))
print('s["db1"] = ' + str(s["db1"]))
print('s["dW2"] = ' + str(s["dW2"]))
print('s["db2"] = ' + str(s["db2"]))

预期输出:
v[“dW1”] = [[0. 0. 0.]
[0. 0. 0.]]
v[“db1”] = [[0.]
[0.]]
v[“dW2”] = [[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
v[“db2”] = [[0.]
[0.]
[0.]]
s[“dW1”] = [[0. 0. 0.]
[0. 0. 0.]]
s[“db1”] = [[0.]
[0.]]
s[“dW2”] = [[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
s[“db2”] = [[0.]
[0.]
[0.]]

练习:用Adam实现参数更新。回想一下一般的更新规则是,对于$l = 1, …, L$:

注意:迭代器 lfor 循环中从0开始,而第一个参数是$W^{[1]}$和$b^{[1]}$。编码时需要将l转换为 l+1

编写函数update_parameters_with_adam(parameters,grads,v,s,t,learning_rate=0.01,beta1=0.9,beta2=0.999,epsilon=1e-8)用Adam实现参数更新

要求

  • 参数:

    • parameters - 包含了以下字段的字典:
      • parameters[‘W’ + str(l)] = Wl
      • parameters[‘b’ + str(l)] = bl
    • grads - 包含了梯度值的字典,有以下key值:
      • grads[‘dW’ + str(l)] = dWl
      • grads[‘db’ + str(l)] = dbl
    • v - Adam的变量,第一个梯度的移动平均值,是一个字典类型的变量
    • s - Adam的变量,平方梯度的移动平均值,是一个字典类型的变量
    • t - 当前迭代的次数
    • learning_rate - 学习率
    • beta1 - 动量,超参数,用于第一阶段,使得曲线的Y值不从0开始(参见天气数据的那个图)
    • beta2 - RMSprop的一个参数,超参数
    • epsilon - 防止除零操作(分母为0)
  • 返回:

    • parameters - 更新后的参数
    • v - 第一个梯度的移动平均值,是一个字典类型的变量
    • s - 平方梯度的移动平均值,是一个字典类型的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def update_parameters_with_adam(parameters,grads,v,s,t,learning_rate=0.01,beta1=0.9,beta2=0.999,epsilon=1e-8):
"""
使用Adam更新参数

参数:
parameters - 包含了以下字段的字典:
parameters['W' + str(l)] = Wl
parameters['b' + str(l)] = bl
grads - 包含了梯度值的字典,有以下key值:
grads['dW' + str(l)] = dWl
grads['db' + str(l)] = dbl
v - Adam的变量,第一个梯度的移动平均值,是一个字典类型的变量
s - Adam的变量,平方梯度的移动平均值,是一个字典类型的变量
t - 当前迭代的次数
learning_rate - 学习率
beta1 - 动量,超参数,用于第一阶段,使得曲线的Y值不从0开始(参见天气数据的那个图)
beta2 - RMSprop的一个参数,超参数
epsilon - 防止除零操作(分母为0)

返回:
parameters - 更新后的参数
v - 第一个梯度的移动平均值,是一个字典类型的变量
s - 平方梯度的移动平均值,是一个字典类型的变量
"""
L = len(parameters) // 2
v_corrected = {} #偏差修正后的值
s_corrected = {} #偏差修正后的值

for l in range(L):
#梯度的移动平均值,输入:"v , grads , beta1",输出:" v "
v["dW" + str(l + 1)] = beta1 * v["dW" + str(l + 1)] + (1 - beta1) * grads["dW" + str(l + 1)]
v["db" + str(l + 1)] = beta1 * v["db" + str(l + 1)] + (1 - beta1) * grads["db" + str(l + 1)]

#计算第一阶段的偏差修正后的估计值,输入"v , beta1 , t" , 输出:"v_corrected"
v_corrected["dW" + str(l + 1)] = v["dW" + str(l + 1)] / (1 - np.power(beta1,t))
v_corrected["db" + str(l + 1)] = v["db" + str(l + 1)] / (1 - np.power(beta1,t))

#计算平方梯度的移动平均值,输入:"s, grads , beta2",输出:"s"
s["dW" + str(l + 1)] = beta2 * s["dW" + str(l + 1)] + (1 - beta2) * np.square(grads["dW" + str(l + 1)])
s["db" + str(l + 1)] = beta2 * s["db" + str(l + 1)] + (1 - beta2) * np.square(grads["db" + str(l + 1)])

#计算第二阶段的偏差修正后的估计值,输入:"s , beta2 , t",输出:"s_corrected"
s_corrected["dW" + str(l + 1)] = s["dW" + str(l + 1)] / (1 - np.power(beta2,t))
s_corrected["db" + str(l + 1)] = s["db" + str(l + 1)] / (1 - np.power(beta2,t))

#更新参数,输入: "parameters, learning_rate, v_corrected, s_corrected, epsilon". 输出: "parameters".
parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * (v_corrected["dW" + str(l + 1)] / np.sqrt(s_corrected["dW" + str(l + 1)] + epsilon))
parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * (v_corrected["db" + str(l + 1)] / np.sqrt(s_corrected["db" + str(l + 1)] + epsilon))

return (parameters,v,s)

运行代码测试update_with_parameters_with_adam

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#测试update_with_parameters_with_adam
print("-------------测试update_with_parameters_with_adam-------------")
parameters , grads , v , s = testCase.update_parameters_with_adam_test_case()
update_parameters_with_adam(parameters,grads,v,s,t=2)

print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))
print('v["dW1"] = ' + str(v["dW1"]))
print('v["db1"] = ' + str(v["db1"]))
print('v["dW2"] = ' + str(v["dW2"]))
print('v["db2"] = ' + str(v["db2"]))
print('s["dW1"] = ' + str(s["dW1"]))
print('s["db1"] = ' + str(s["db1"]))
print('s["dW2"] = ' + str(s["dW2"]))
print('s["db2"] = ' + str(s["db2"]))

预期输出:

W1 = [[ 1.63178673 -0.61919778 -0.53561312]
[-1.08040999 0.85796626 -2.29409733]]
b1 = [[ 1.75225313]
[-0.75376553]]
W2 = [[ 0.32648046 -0.25681174 1.46954931]
[-2.05269934 -0.31497584 -0.37661299]
[ 1.14121081 -1.09245036 -0.16498684]]
b2 = [[-0.88529978]
[ 0.03477238]
[ 0.57537385]]
v[“dW1”] = [[-0.11006192 0.11447237 0.09015907]
[ 0.05024943 0.09008559 -0.06837279]]
v[“db1”] = [[-0.01228902]
[-0.09357694]]
v[“dW2”] = [[-0.02678881 0.05303555 -0.06916608]
[-0.03967535 -0.06871727 -0.08452056]
[-0.06712461 -0.00126646 -0.11173103]]
v[“db2”] = [[0.02344157]
[0.16598022]
[0.07420442]]
s[“dW1”] = [[0.00121136 0.00131039 0.00081287]
[0.0002525 0.00081154 0.00046748]]
s[“db1”] = [[1.51020075e-05]
[8.75664434e-04]]
s[“dW2”] = [[7.17640232e-05 2.81276921e-04 4.78394595e-04]
[1.57413361e-04 4.72206320e-04 7.14372576e-04]
[4.50571368e-04 1.60392066e-07 1.24838242e-03]]
s[“db2”] = [[5.49507194e-05]
[2.75494327e-03]
[5.50629536e-04]]

现在,你学习了三种有效的优化算法(小批次梯度下降,冲量,Adam)。让我们使用每个优化器来实现一个模型,并观察其中的差异。

不同优化算法的模型

我们使用“moons”数据集来测试不同的优化方法。(该数据集被命名为“月亮”,因为两个类别的数据看起来有点像月牙。)

1
train_X, train_Y = load_dataset()

我们已经实现了一个三层的神经网络。你将使用以下方法进行训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def model(X,Y,layers_dims,optimizer,learning_rate=0.0007,
mini_batch_size=64,beta=0.9,beta1=0.9,beta2=0.999,
epsilon=1e-8,num_epochs=10000,print_cost=True,is_plot=True):

"""
可以运行在不同优化器模式下的3层神经网络模型。

参数:
X - 输入数据,维度为(2,输入的数据集里面样本数量)
Y - 与X对应的标签
layers_dims - 包含层数和节点数量的列表
optimizer - 字符串类型的参数,用于选择优化类型,【 "gd" | "momentum" | "adam" 】
learning_rate - 学习率
mini_batch_size - 每个小批量数据集的大小
beta - 用于动量优化的一个超参数
beta1 - 用于计算梯度后的指数衰减的估计的超参数
beta1 - 用于计算平方梯度后的指数衰减的估计的超参数
epsilon - 用于在Adam中避免除零操作的超参数,一般不更改
num_epochs - 整个训练集的遍历次数,(视频2.9学习率衰减,1分55秒处,视频中称作“代”),相当于之前的num_iteration
print_cost - 是否打印误差值,每遍历1000次数据集打印一次,但是每100次记录一个误差值,又称每1000代打印一次
is_plot - 是否绘制出曲线图

返回:
parameters - 包含了学习后的参数

"""
L = len(layers_dims)
costs = []
t = 0 #每学习完一个minibatch就增加1
seed = 10 #随机种子

#初始化参数
parameters = opt_utils.initialize_parameters(layers_dims)

#选择优化器
if optimizer == "gd":
pass #不使用任何优化器,直接使用梯度下降法
elif optimizer == "momentum":
v = initialize_velocity(parameters) #使用动量
elif optimizer == "adam":
v, s = initialize_adam(parameters)#使用Adam优化
else:
print("optimizer参数错误,程序退出。")
exit(1)

#开始学习
for i in range(num_epochs):
#定义随机 minibatches,我们在每次遍历数据集之后增加种子以重新排列数据集,使每次数据的顺序都不同
seed = seed + 1
minibatches = random_mini_batches(X,Y,mini_batch_size,seed)

for minibatch in minibatches:
#选择一个minibatch
(minibatch_X,minibatch_Y) = minibatch

#前向传播
A3 , cache = opt_utils.forward_propagation(minibatch_X,parameters)

#计算误差
cost = opt_utils.compute_cost(A3 , minibatch_Y)

#反向传播
grads = opt_utils.backward_propagation(minibatch_X,minibatch_Y,cache)

#更新参数
if optimizer == "gd":
parameters = update_parameters_with_gd(parameters,grads,learning_rate)
elif optimizer == "momentum":
parameters, v = update_parameters_with_momentun(parameters,grads,v,beta,learning_rate)
elif optimizer == "adam":
t = t + 1
parameters , v , s = update_parameters_with_adam(parameters,grads,v,s,t,learning_rate,beta1,beta2,epsilon)
#记录误差值
if i % 100 == 0:
costs.append(cost)
#是否打印误差值
if print_cost and i % 1000 == 0:
print("第" + str(i) + "次遍历整个数据集,当前误差值:" + str(cost))
#是否绘制曲线图
if is_plot:
plt.plot(costs)
plt.ylabel('cost')
plt.xlabel('epochs (per 100)')
plt.title("Learning rate = " + str(learning_rate))
plt.show()

return parameters

  • 小批次 Gradient Descent:它将调用你的函数:
    • update_parameters_with_gd()
  • 小批次 冲量:它将调用你的函数:
    • initialize_velocity()update_parameters_with_momentum()
  • 小批次 Adam:它将调用你的函数:
    • initialize_adam()update_parameters_with_adam()

现在,你将依次使用3种优化方法来运行此神经网络。

小批量梯度下降

运行以下代码以查看模型如何进行小批量梯度下降。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#使用普通的梯度下降
layers_dims = [train_X.shape[0],5,2,1]
parameters = model(train_X, train_Y, layers_dims, optimizer="gd",is_plot=True)
#预测
preditions = opt_utils.predict(train_X,train_Y,parameters)

#绘制分类图
plt.title("Model with Gradient Descent optimization")
axes = plt.gca()
axes.set_xlim([-1.5, 2.5])
axes.set_ylim([-1, 1.5])
opt_utils.plot_decision_boundary(lambda x: opt_utils.predict_dec(parameters, x.T), train_X, train_Y)



Accuracy: 0.7966666666666666

带冲量的小批量梯度下降

运行以下代码,以查看模型如何使用冲量。因为此示例相对简单,所以使用冲量的收益很小。但是对于更复杂的问题,你可能会看到更大的收获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
layers_dims = [train_X.shape[0],5,2,1]
#使用动量的梯度下降
parameters = model(train_X, train_Y, layers_dims, beta=0.9,optimizer="momentum",is_plot=True)
#预测
preditions = opt_utils.predict(train_X,train_Y,parameters)

#绘制分类图
plt.title("Model with Momentum optimization")
axes = plt.gca()
axes.set_xlim([-1.5, 2.5])
axes.set_ylim([-1, 1.5])
opt_utils.plot_decision_boundary(lambda x: opt_utils.predict_dec(parameters, x.T), train_X, train_Y)


预期输出:
第0次遍历整个数据集,当前误差值:0.690741298835
第1000次遍历整个数据集,当前误差值:0.685340526127
第2000次遍历整个数据集,当前误差值:0.64714483701
第3000次遍历整个数据集,当前误差值:0.619594303208
第4000次遍历整个数据集,当前误差值:0.576665034407
第5000次遍历整个数据集,当前误差值:0.607323821901
第6000次遍历整个数据集,当前误差值:0.529476175879
第7000次遍历整个数据集,当前误差值:0.460936190049
第8000次遍历整个数据集,当前误差值:0.465780093701
第9000次遍历整个数据集,当前误差值:0.464739596792

Accuracy: 0.7966666666666666

Adam的小批量梯度下降

运行以下代码以查看使用Adam的模型表现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
layers_dims = [train_X.shape[0], 5, 2, 1]
#使用Adam优化的梯度下降
parameters = model(train_X, train_Y, layers_dims, optimizer="adam",is_plot=True)

#预测
preditions = opt_utils.predict(train_X,train_Y,parameters)

#绘制分类图
plt.title("Model with Adam optimization")
axes = plt.gca()
axes.set_xlim([-1.5, 2.5])
axes.set_ylim([-1, 1.5])
opt_utils.plot_decision_boundary(lambda x: opt_utils.predict_dec(parameters, x.T), train_X, train_Y)

第0次遍历整个数据集,当前误差值:0.690552244611
第1000次遍历整个数据集,当前误差值:0.185501364386
第2000次遍历整个数据集,当前误差值:0.150830465753
第3000次遍历整个数据集,当前误差值:0.07445438571
第4000次遍历整个数据集,当前误差值:0.125959156513
第5000次遍历整个数据集,当前误差值:0.104344435342
第6000次遍历整个数据集,当前误差值:0.100676375041
第7000次遍历整个数据集,当前误差值:0.0316520301351
第8000次遍历整个数据集,当前误差值:0.111972731312
第9000次遍历整个数据集,当前误差值:0.197940071525

Accuracy: 0.94

总结

优化方法准确度模型损失
Gradient descent79.70%振荡
Momentum79.70%振荡
Adam94%更光滑

冲量通常会有所帮助,但是鉴于学习率低和数据集过于简单,其影响几乎可以忽略不计。同样,你看到损失的巨大波动是因为对于优化算法,某些小批处理比其他小批处理更为困难。

另一方面,Adam明显胜过小批次梯度下降和冲量。如果你在此简单数据集上运行更多epoch,则这三种方法都将产生非常好的结果。但是,Adam收敛得更快。

Adam的优势包括:

  • 相对较低的内存要求(尽管高于梯度下降和带冲量的梯度下降)
  • 即使很少调整超参数,通常也能很好地工作($\alpha$除外)

参考

相关代码
opt_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# -*- coding: utf-8 -*-

#opt_utils.py

import numpy as np
import matplotlib.pyplot as plt
import sklearn
import sklearn.datasets

def sigmoid(x):
"""
Compute the sigmoid of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- sigmoid(x)
"""
s = 1/(1+np.exp(-x))
return s

def relu(x):
"""
Compute the relu of x

Arguments:
x -- A scalar or numpy array of any size.

Return:
s -- relu(x)
"""
s = np.maximum(0,x)

return s


def load_params_and_grads(seed=1):
np.random.seed(seed)
W1 = np.random.randn(2,3)
b1 = np.random.randn(2,1)
W2 = np.random.randn(3,3)
b2 = np.random.randn(3,1)

dW1 = np.random.randn(2,3)
db1 = np.random.randn(2,1)
dW2 = np.random.randn(3,3)
db2 = np.random.randn(3,1)

return W1, b1, W2, b2, dW1, db1, dW2, db2

def initialize_parameters(layer_dims):
"""
Arguments:
layer_dims -- python array (list) containing the dimensions of each layer in our network

Returns:
parameters -- python dictionary containing your parameters "W1", "b1", ..., "WL", "bL":
W1 -- weight matrix of shape (layer_dims[l], layer_dims[l-1])
b1 -- bias vector of shape (layer_dims[l], 1)
Wl -- weight matrix of shape (layer_dims[l-1], layer_dims[l])
bl -- bias vector of shape (1, layer_dims[l])

Tips:
- For example: the layer_dims for the "Planar Data classification model" would have been [2,2,1].
This means W1's shape was (2,2), b1 was (1,2), W2 was (2,1) and b2 was (1,1). Now you have to generalize it!
- In the for loop, use parameters['W' + str(l)] to access Wl, where l is the iterative integer.
"""

np.random.seed(3)
parameters = {}
L = len(layer_dims) # number of layers in the network

for l in range(1, L):
parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1])* np.sqrt(2 / layer_dims[l-1])
parameters['b' + str(l)] = np.zeros((layer_dims[l], 1))

assert(parameters['W' + str(l)].shape == layer_dims[l], layer_dims[l-1])
assert(parameters['W' + str(l)].shape == layer_dims[l], 1)

return parameters

def forward_propagation(X, parameters):
"""
Implements the forward propagation (and computes the loss) presented in Figure 2.

Arguments:
X -- input dataset, of shape (input size, number of examples)
parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3":
W1 -- weight matrix of shape ()
b1 -- bias vector of shape ()
W2 -- weight matrix of shape ()
b2 -- bias vector of shape ()
W3 -- weight matrix of shape ()
b3 -- bias vector of shape ()

Returns:
loss -- the loss function (vanilla logistic loss)
"""

# retrieve parameters
W1 = parameters["W1"]
b1 = parameters["b1"]
W2 = parameters["W2"]
b2 = parameters["b2"]
W3 = parameters["W3"]
b3 = parameters["b3"]

# LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID
z1 = np.dot(W1, X) + b1
a1 = relu(z1)
z2 = np.dot(W2, a1) + b2
a2 = relu(z2)
z3 = np.dot(W3, a2) + b3
a3 = sigmoid(z3)

cache = (z1, a1, W1, b1, z2, a2, W2, b2, z3, a3, W3, b3)

return a3, cache

def backward_propagation(X, Y, cache):
"""
Implement the backward propagation presented in figure 2.

Arguments:
X -- input dataset, of shape (input size, number of examples)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat)
cache -- cache output from forward_propagation()

Returns:
gradients -- A dictionary with the gradients with respect to each parameter, activation and pre-activation variables
"""
m = X.shape[1]
(z1, a1, W1, b1, z2, a2, W2, b2, z3, a3, W3, b3) = cache

dz3 = 1./m * (a3 - Y)
dW3 = np.dot(dz3, a2.T)
db3 = np.sum(dz3, axis=1, keepdims = True)

da2 = np.dot(W3.T, dz3)
dz2 = np.multiply(da2, np.int64(a2 > 0))
dW2 = np.dot(dz2, a1.T)
db2 = np.sum(dz2, axis=1, keepdims = True)

da1 = np.dot(W2.T, dz2)
dz1 = np.multiply(da1, np.int64(a1 > 0))
dW1 = np.dot(dz1, X.T)
db1 = np.sum(dz1, axis=1, keepdims = True)

gradients = {"dz3": dz3, "dW3": dW3, "db3": db3,
"da2": da2, "dz2": dz2, "dW2": dW2, "db2": db2,
"da1": da1, "dz1": dz1, "dW1": dW1, "db1": db1}

return gradients

def compute_cost(a3, Y):

"""
Implement the cost function

Arguments:
a3 -- post-activation, output of forward propagation
Y -- "true" labels vector, same shape as a3

Returns:
cost - value of the cost function
"""
m = Y.shape[1]

logprobs = np.multiply(-np.log(a3),Y) + np.multiply(-np.log(1 - a3), 1 - Y)
cost = 1./m * np.sum(logprobs)

return cost

def predict(X, y, parameters):
"""
This function is used to predict the results of a n-layer neural network.

Arguments:
X -- data set of examples you would like to label
parameters -- parameters of the trained model

Returns:
p -- predictions for the given dataset X
"""

m = X.shape[1]
p = np.zeros((1,m), dtype = np.int)

# Forward propagation
a3, caches = forward_propagation(X, parameters)

# convert probas to 0/1 predictions
for i in range(0, a3.shape[1]):
if a3[0,i] > 0.5:
p[0,i] = 1
else:
p[0,i] = 0

# print results

#print ("predictions: " + str(p[0,:]))
#print ("true labels: " + str(y[0,:]))
print("Accuracy: " + str(np.mean((p[0,:] == y[0,:]))))

return p

def predict_dec(parameters, X):
"""
Used for plotting decision boundary.

Arguments:
parameters -- python dictionary containing your parameters
X -- input data of size (m, K)

Returns
predictions -- vector of predictions of our model (red: 0 / blue: 1)
"""

# Predict using forward propagation and a classification threshold of 0.5
a3, cache = forward_propagation(X, parameters)
predictions = (a3 > 0.5)
return predictions

def plot_decision_boundary(model, X, y):
# Set min and max values and give it some padding
x_min, x_max = X[0, :].min() - 1, X[0, :].max() + 1
y_min, y_max = X[1, :].min() - 1, X[1, :].max() + 1
h = 0.01
# Generate a grid of points with distance h between them
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
# Predict the function value for the whole grid
Z = model(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot the contour and training examples
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(X[0, :], X[1, :], c=y, cmap=plt.cm.Spectral)
plt.show()

def load_dataset(is_plot = True):
np.random.seed(3)
train_X, train_Y = sklearn.datasets.make_moons(n_samples=300, noise=.2) #300 #0.2
# Visualize the data
if is_plot:
plt.scatter(train_X[:, 0], train_X[:, 1], c=train_Y, s=40, cmap=plt.cm.Spectral);
train_X = train_X.T
train_Y = train_Y.reshape((1, train_Y.shape[0]))

return train_X, train_Y


testCase.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# -*- coding: utf-8 -*-

#testCase.py

import numpy as np

def update_parameters_with_gd_test_case():
np.random.seed(1)
learning_rate = 0.01
W1 = np.random.randn(2,3)
b1 = np.random.randn(2,1)
W2 = np.random.randn(3,3)
b2 = np.random.randn(3,1)

dW1 = np.random.randn(2,3)
db1 = np.random.randn(2,1)
dW2 = np.random.randn(3,3)
db2 = np.random.randn(3,1)

parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2}
grads = {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2}

return parameters, grads, learning_rate

"""
def update_parameters_with_sgd_checker(function, inputs, outputs):
if function(inputs) == outputs:
print("Correct")
else:
print("Incorrect")
"""

def random_mini_batches_test_case():
np.random.seed(1)
mini_batch_size = 64
X = np.random.randn(12288, 148)
Y = np.random.randn(1, 148) < 0.5
return X, Y, mini_batch_size

def initialize_velocity_test_case():
np.random.seed(1)
W1 = np.random.randn(2,3)
b1 = np.random.randn(2,1)
W2 = np.random.randn(3,3)
b2 = np.random.randn(3,1)
parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2}
return parameters

def update_parameters_with_momentum_test_case():
np.random.seed(1)
W1 = np.random.randn(2,3)
b1 = np.random.randn(2,1)
W2 = np.random.randn(3,3)
b2 = np.random.randn(3,1)

dW1 = np.random.randn(2,3)
db1 = np.random.randn(2,1)
dW2 = np.random.randn(3,3)
db2 = np.random.randn(3,1)
parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2}
grads = {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2}
v = {'dW1': np.array([[ 0., 0., 0.],
[ 0., 0., 0.]]), 'dW2': np.array([[ 0., 0., 0.],
[ 0., 0., 0.],
[ 0., 0., 0.]]), 'db1': np.array([[ 0.],
[ 0.]]), 'db2': np.array([[ 0.],
[ 0.],
[ 0.]])}
return parameters, grads, v

def initialize_adam_test_case():
np.random.seed(1)
W1 = np.random.randn(2,3)
b1 = np.random.randn(2,1)
W2 = np.random.randn(3,3)
b2 = np.random.randn(3,1)
parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2}
return parameters

def update_parameters_with_adam_test_case():
np.random.seed(1)
v, s = ({'dW1': np.array([[ 0., 0., 0.],
[ 0., 0., 0.]]), 'dW2': np.array([[ 0., 0., 0.],
[ 0., 0., 0.],
[ 0., 0., 0.]]), 'db1': np.array([[ 0.],
[ 0.]]), 'db2': np.array([[ 0.],
[ 0.],
[ 0.]])}, {'dW1': np.array([[ 0., 0., 0.],
[ 0., 0., 0.]]), 'dW2': np.array([[ 0., 0., 0.],
[ 0., 0., 0.],
[ 0., 0., 0.]]), 'db1': np.array([[ 0.],
[ 0.]]), 'db2': np.array([[ 0.],
[ 0.],
[ 0.]])})
W1 = np.random.randn(2,3)
b1 = np.random.randn(2,1)
W2 = np.random.randn(3,3)
b2 = np.random.randn(3,1)

dW1 = np.random.randn(2,3)
db1 = np.random.randn(2,1)
dW2 = np.random.randn(3,3)
db2 = np.random.randn(3,1)

parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2}
grads = {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2}

return parameters, grads, v, s


作业6 TensorFlow入门

TensorFlow教程

欢迎来到本周的编程作业。 到目前为止,你一直使用numpy来构建神经网络。现在,我们将引导你使用深度学习框架,该框架将使你可以更轻松地构建神经网络。TensorFlow,PaddlePaddle,Torch,Caffe,Keras等机器学习框架可以极大地加快你的机器学习开发速度。所有这些框架也都有很多文档,你应该随时阅读学习。你将学习在TensorFlow中执行以下操作:

  • 初始化变量
  • 创建自己的会话(session)
  • 训练算法
  • 实现神经网络

编程框架不仅可以缩短编码时间,而且有时还可以进行优化以加快代码速度。

探索Tensorflow库

首先,导入库:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
import h5py
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.python.framework import ops
import tf_utils
import time

#%matplotlib inline #如果你使用的是jupyter notebook取消注释
np.random.seed(1)

现在,你已经导入了库,我们将引导你完成其不同的应用程序。你将从一个示例开始:计算一个训练数据的损失。

1
2
3
4
5
6
7
8
9
10
11
y_hat = tf.constant(36,name="y_hat")            #定义y_hat为固定值36
y = tf.constant(39,name="y") #定义y为固定值39

loss = tf.Variable((y-y_hat)**2,name="loss" ) #为损失函数创建一个变量

init = tf.global_variables_initializer() #运行之后的初始化(ession.run(init))
#损失变量将被初始化并准备计算
with tf.Session() as session: #创建一个session并打印输出
session.run(init) #初始化变量
print(session.run(loss)) #打印损失值

预期输出:
9

在TensorFlow中编写和运行程序包含以下步骤:

  1. 创建尚未执行的张量(变量)。
  2. 在这些张量之间编写操作。
  3. 初始化张量。
  4. 创建一个会话。
  5. 运行会话,这将运行你上面编写的操作。

因此,当我们为损失创建变量时,我们仅将损失定义为其他数量的函数,但没有验证其值。为了验证它,我们必须运行init = tf.global_variables_initializer()初始化损失变量,在最后一行中,我们终于能够验证loss的值并打印它。

现在让我们看一个简单的例子。运行下面的单元格:

1
2
3
4
5
6
a = tf.constant(2)
b = tf.constant(10)
c = tf.multiply(a,b)

print(c)

预期输出:
Tensor(“Mul:0”, shape=(), dtype=int32)

不出所料,看不到结果20!而是得到一个张量,是一个不具有shape属性且类型为“int32”的张量。你所做的所有操作都已放入“计算图”中,但你尚未运行此计算。为了实际将两个数字相乘,必须创建一个会话并运行它。

1
2
3
4
sess = tf.Session()

print(sess.run(c))

预期输出:
20

Great! 总而言之,记住要初始化变量,创建一个会话并在该会话中运行操作

接下来,你还必须了解 placeholders(占位符)。占位符是一个对象,你只能稍后指定其值。
要为占位符指定值,你可以使用”feed dictionary”(feed_dict变量)传入值。在下面,我们为x创建了一个占位符,以允许我们稍后在运行会话时传递数字。

1
2
3
4
5
6
#利用feed_dict来改变x的值

x = tf.placeholder(tf.int64,name="x")
print(sess.run(2 * x,feed_dict={x:3}))
sess.close()

预期输出:
6

当你首次定义x时,不必为其指定值。占位符只是一个变量,你在运行会话时才将数据分配给该变量。也就是说你在运行会话时向这些占位符“提供数据”。

当你指定计算所需的操作时,你在告诉TensorFlow如何构造计算图。计算图可以具有一些占位符,你将在稍后指定它们的值。最后,在运行会话时,你要告诉TensorFlow执行计算图。

线性函数

让我们开始此编程练习,计算以下方程式:$Y = WX + b$,其中$W$和$X$是随机矩阵,b是随机向量。

练习:计算$WX + b$ ,其中$W, X$和$b$是从随机正态分布中得到的,W的维度为(4,3),X的维度为(3,1),b的维度为(4,1)。例如,下面是定义维度为(3,1)的常量X的方法:

1
2
X = tf.constant(np.random.randn(3,1), name = "X")


你可能会发现以下函数很有用:

  • tf.matmul(…, …)进行矩阵乘法
  • tf.add(…, …)进行加法
  • np.random.randn(…)随机初始化

编写函数linear_function()实现线性功能
要求

  • 实现一个线性功能:
    初始化W,类型为tensor的随机变量,维度为(4,3)
    初始化X,类型为tensor的随机变量,维度为(3,1)
    初始化b,类型为tensor的随机变量,维度为(4,1)
  • 返回:
    • result - 运行了session后的结果,运行的是Y = WX + b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def linear_function():
"""
实现一个线性功能:
初始化W,类型为tensor的随机变量,维度为(4,3)
初始化X,类型为tensor的随机变量,维度为(3,1)
初始化b,类型为tensor的随机变量,维度为(4,1)
返回:
result - 运行了session后的结果,运行的是Y = WX + b

"""

np.random.seed(1) #指定随机种子

X = np.random.randn(3,1)
W = np.random.randn(4,3)
b = np.random.randn(4,1)

Y = tf.add(tf.matmul(W,X),b) #tf.matmul是矩阵乘法
#Y = tf.matmul(W,X) + b #也可以以写成这样子

#创建一个session并运行它
sess = tf.Session()
result = sess.run(Y)

#session使用完毕,关闭它
sess.close()

return result

运行代码测试函数linear_function()

1
2
print("result = " +  str(linear_function()))

预期输出:
result = [[-2.15657382]
[ 2.95891446]
[-1.08926781]
[-0.84538042]]

计算Sigmoid

Great!你刚刚实现了线性函数。Tensorflow提供了各种常用的神经网络函数,例如tf.sigmoidtf.softmax。对于本练习,让我们计算输入的sigmoid函数值。

你将使用占位符变量x进行此练习。在运行会话时,应该使用feed字典传入输入z。在本练习中,你必须:
(i)创建一个占位符x
(ii)使用tf.sigmoid定义计算Sigmoid所需的操作;
(iii)然后运行该会话

练习:实现下面的Sigmoid函数。你应该使用以下内容:

  • tf.placeholder(tf.float32, name = "...")
  • tf.sigmoid(...)
  • sess.run(..., feed_dict = {x: z})

注意,在tensorflow中创建和使用会话有两种典型的方法:

Method 1:

1
2
3
4
sess = tf.Session()
result = sess.run(...,feed_dict = {...})
sess.close()


Method 2:
1
2
with tf.Session as sess:
result = sess.run(...,feed_dict = {...})

使用tf编写函数sigmoid(z)
要求

  • 实现使用sigmoid函数计算z

  • 参数:

    • z - 输入的值,标量或矢量
  • 返回:

    • result - 用sigmoid计算z的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def sigmoid(z):
"""
实现使用sigmoid函数计算z

参数:
z - 输入的值,标量或矢量

返回:
result - 用sigmoid计算z的值

"""

#创建一个占位符x,名字叫“x”
x = tf.placeholder(tf.float32,name="x")

#计算sigmoid(z)
sigmoid = tf.sigmoid(x)

#创建一个会话,使用方法二
with tf.Session() as sess:
result = sess.run(sigmoid,feed_dict={x:z})

return result

运行代码测试函数sigmoid(z)

1
2
3
print ("sigmoid(0) = " + str(sigmoid(0)))
print ("sigmoid(12) = " + str(sigmoid(12)))

预期输出:
sigmoid(0) = 0.5
sigmoid(12) = 0.999994

总而言之,你知道如何
1.创建占位符
2.指定运算相对应的计算图
3.创建会话
4.如果需要指定占位符变量的值,使用feed字典运行会话。

计算损失

你还可以使用内置函数来计算神经网络的损失。因此,对于i=1…m,无需编写代码来将其作为$a^{2}$和$y^{(i)}$的函数来计算:

你可以使用tensorflow的一行代码中做到这一点!

练习:实现交叉熵损失。你将使用的函数是:

  • tf.nn.sigmoid_cross_entropy_with_logits(logits = ..., labels = ...)

你的代码应输入z,计算出sigmoid(得到a),然后计算出交叉熵损失$J$,所有这些操作都可以通过调用tf.nn.sigmoid_cross_entropy_with_logits来完成:

编写函数cost(logits,labels)计算cost

要求

  • 计算使用sigmoid交叉熵的成本

  • 参数:

    • logits — 向量,包含最后线性单元的输出(在最终sigmoid激活之前)
    • labels — 向量,标签y(1或0)
  • 注意:在本课程中,我们称之为“z”和“y”的内容分别在TensorFlow文档中称为“logits”和“labels”。
    因此logits将输入z,labels将输入y。

  • 返回:

    • cost — 运行成本的会话(公式(2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def cost(logits, labels):
"""
计算使用sigmoid交叉熵的成本

参数:
logits -- 向量,包含最后线性单元的输出(在最终sigmoid激活之前)
labels -- 向量,标签y(1或0)

注意:在本课程中,我们称之为“z”和“y”的内容分别在TensorFlow文档中称为“logits”和“labels”。
因此logits将输入z,labels将输入y。

返回:
cost -- 运行成本的会话(公式(2))
"""

### 在这里开始代码 ###

# 创建“logits”(z)和“labels”(y)的占位符
z = tf.placeholder(tf.float32, name = "z") # 创建一个用于logits的占位符
y = tf.placeholder(tf.float32, name = "y") # 创建一个用于labels的占位符

# 使用损失函数
cost = tf.nn.sigmoid_cross_entropy_with_logits(logits=z, labels=y) # 计算sigmoid交叉熵损失

# 创建一个会话。请参见上面的方法1。
sess = tf.Session() # 初始化一个TensorFlow会话

# 运行会话
cost = sess.run(cost, feed_dict={z: logits, y: labels}) # 运行计算,并传入logits和labels

# 关闭会话。请参见上面的方法1。
sess.close() # 关闭TensorFlow会话

return cost # 返回计算得到的成本

运行代码测试函数cost(logits,labels)

1
2
3
logits = sigmoid(np.array([0.2,0.4,0.7,0.9]))
cost = cost(logits, np.array([0,0,1,1]))
print ("cost = " + str(cost))

预期输出 :
cost = [1.0053872 1.0366409 0.41385433 0.39956614]

使用独热(One Hot)编码

在深度学习中,很多时候你会得到一个y向量,其数字范围从0到C-1,其中C是类的数量。例如C是4,那么你可能具有以下y向量,你将需要按以下方式对其进行转换:

这称为独热编码,因为在转换后的表示形式中,每一列中的一个元素正好是“hot”(设为1)。要以numpy格式进行此转换,你可能需要编写几行代码。在tensorflow中,你可以只使用一行代码:

  • tf.one_hot(labels, depth, axis)

练习:实现以下函数,以获取一个标签向量和$C$类的总数,并返回一个独热编码。使用tf.one_hot()来做到这一点。

编写函数one_hot_matrix(lables,C)

要求

  • 创建一个矩阵,其中第i行对应第i个类号,第j列对应第j个训练样本
    所以如果第j个样本对应着第i个标签,那么entry (i,j)将会是1

  • 参数:

    • lables - 标签向量
    • C - 分类数
  • 返回:

    • one_hot - 独热矩阵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def one_hot_matrix(lables,C):
"""
创建一个矩阵,其中第i行对应第i个类号,第j列对应第j个训练样本
所以如果第j个样本对应着第i个标签,那么entry (i,j)将会是1

参数:
lables - 标签向量
C - 分类数

返回:
one_hot - 独热矩阵

"""

#创建一个tf.constant,赋值为C,名字叫C
C = tf.constant(C,name="C")

#使用tf.one_hot,注意一下axis
one_hot_matrix = tf.one_hot(indices=lables , depth=C , axis=0)

#创建一个session
sess = tf.Session()

#运行session
one_hot = sess.run(one_hot_matrix)

#关闭session
sess.close()

return one_hot

运行代码测试函数one_hot_matrix(lables,C)

1
2
3
4
labels = np.array([1,2,3,0,2,1])
one_hot = one_hot_matrix(labels,C=4)
print(str(one_hot))

预期输出:
[[ 0. 0. 0. 1. 0. 0.]
[ 1. 0. 0. 0. 0. 1.]
[ 0. 1. 0. 0. 1. 0.]
[ 0. 0. 1. 0. 0. 0.]]

使用0和1初始化

现在,你将学习如何初始化0和1的向量。 你将要调用的函数是tf.ones()。要使用零初始化,可以改用tf.zeros()。这些函数采用一个维度,并分别返回一个包含0和1的维度数组。

练习:实现以下函数以获取维度并返回维度数组。

  • tf.ones(shape)

编写函数ones(shape)创建一个维度为shape的变量,其值全为1
要求

  • 创建一个维度为shape的变量,其值全为1

  • 参数:

    • shape - 你要创建的数组的维度
  • 返回:

    • ones - 只包含1的数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def ones(shape):
"""
创建一个维度为shape的变量,其值全为1

参数:
shape - 你要创建的数组的维度

返回:
ones - 只包含1的数组
"""

#使用tf.ones()
ones = tf.ones(shape)

#创建会话
sess = tf.Session()

#运行会话
ones = sess.run(ones)

#关闭会话
sess.close()

return ones

运行代码测试函数函数ones(shape)

1
2
print ("ones = " + str(ones([3])))

预期输出:
ones = [1. 1. 1.]

使用Tensorflow构建神经网络

在这一部分作业中,你将使用tensorflow构建神经网络。请记住,实现tensorflow模型包含两个部分:

  • 创建计算图
  • 运行计算图

让我们深入研究你要解决的问题!

问题陈述:SIGNS 数据集

一个下午,我们决定和一些朋友一起用计算机来解密手语。我们花了几个小时在白墙前拍照,并提出了以下数据集。现在,你的工作就是构建一种算法,以帮助语音障碍者和不懂手语的人的交流。

  • 训练集:1080张图片(64 x 64像素)的手势表示从0到5的数字(每个数字180张图片)。
  • 测试集:120张图片(64 x 64像素)的手势表示从0到5的数字(每个数字20张图片)。

请注意,这是SIGNS数据集的子集。完整的数据集包含更多的手势。

这是每个数字的示例,以及如何解释标签的方式。这些是原始图片,然后我们将图像分辨率降低到64 x 64像素。

图 1:SIGNS数据集

运行以下代码以加载数据集。

1
2
3
# Loading the dataset
X_train_orig , Y_train_orig , X_test_orig , Y_test_orig , classes = tf_utils.load_dataset()

更改下面的索引并运行以可视化数据集中的一些示例。

1
2
3
4
index = 11
plt.imshow(X_train_orig[index])
print("Y = " + str(np.squeeze(Y_train_orig[:,index])))

预期输出:
Y = 1

通常先将图像数据集展平,然后除以255以对其进行归一化。最重要的是将每个标签转换为一个独热向量,如图1所示。运行下面的程序即可转化。
运行以下代码将图像数据集展平归一化并将每个标签转换为一个独热向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
X_train_flatten = X_train_orig.reshape(X_train_orig.shape[0],-1).T #每一列就是一个样本
X_test_flatten = X_test_orig.reshape(X_test_orig.shape[0],-1).T

#归一化数据
X_train = X_train_flatten / 255
X_test = X_test_flatten / 255

#转换为独热矩阵
Y_train = tf_utils.convert_to_one_hot(Y_train_orig,6)
Y_test = tf_utils.convert_to_one_hot(Y_test_orig,6)

print("训练集样本数 = " + str(X_train.shape[1]))
print("测试集样本数 = " + str(X_test.shape[1]))
print("X_train.shape: " + str(X_train.shape))
print("Y_train.shape: " + str(Y_train.shape))
print("X_test.shape: " + str(X_test.shape))
print("Y_test.shape: " + str(Y_test.shape))

预期输出:
训练集样本数 = 1080
测试集样本数 = 120
X_train.shape: (12288, 1080)
Y_train.shape: (6, 1080)
X_test.shape: (12288, 120)
Y_test.shape: (6, 120)

分析:
每一列代表一个样本数据,12288为每张图片的像素,6是每张图片有6个可能的标签0-5

注意 12288 = $64 \times 64 \times 3$,每个图像均为正方形,64 x 64像素,其中3为RGB颜色。请确保理解这些数据的维度意义,然后再继续。

你的目标是建立一种能够高精度识别符号的算法。为此,你将构建一个tensorflow模型,该模型与你先前在numpy中为猫识别构建的tensorflow模型几乎相同(但现在使用softmax输出)。这是将numpy实现的模型与tensorflow进行比较的好机会。

模型LINEAR-> RELU-> LINEAR-> RELU-> LINEAR-> SOFTMAX 。 SIGMOID输出层已转换为SOFTMAX。SOFTMAX层将SIGMOID应用到两个以上的类。

创建占位符

你的第一个任务是为XX创建占位符,方便你以后在运行会话时传递训练数据。

练习:实现以下函数以在tensorflow中创建占位符。

编写函数create_placeholders(n_x,n_y)为训练数据创建占位符
要求

  • 为TensorFlow会话创建占位符
  • 参数:

    • n_x - 一个实数,图片向量的大小(64643 = 12288)
    • n_y - 一个实数,分类数(从0到5,所以n_y = 6)
  • 返回:

    • X - 一个数据输入的占位符,维度为[n_x, None],dtype = “float”
    • Y - 一个对应输入的标签的占位符,维度为[n_Y,None],dtype = “float”

提示:
使用None,因为它让我们可以灵活处理占位符提供的样本数量。事实上,测试/训练期间的样本数量是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def create_placeholders(n_x,n_y):
"""
为TensorFlow会话创建占位符
参数:
n_x - 一个实数,图片向量的大小(64*64*3 = 12288)
n_y - 一个实数,分类数(从0到5,所以n_y = 6)

返回:
X - 一个数据输入的占位符,维度为[n_x, None],dtype = "float"
Y - 一个对应输入的标签的占位符,维度为[n_Y,None],dtype = "float"

提示:
使用None,因为它让我们可以灵活处理占位符提供的样本数量。事实上,测试/训练期间的样本数量是不同的。

"""

X = tf.placeholder(tf.float32, [n_x, None], name="X")
Y = tf.placeholder(tf.float32, [n_y, None], name="Y")

return X, Y

运行代码测试函数create_placeholders(n_x,n_y)

1
2
3
4
X, Y = create_placeholders(12288, 6)
print("X = " + str(X))
print("Y = " + str(Y))

预期输出:
X = Tensor(“X:0”, shape=(12288, ?), dtype=float32)
Y = Tensor(“Y:0”, shape=(6, ?), dtype=float32)

初始化参数

你的第二个任务是初始化tensorflow中的参数。

练习:实现以下函数以初始化tensorflow中的参数。使用权重的Xavier初始化和偏差的零初始化。维度如下,对于W1和b1,你可以使用:

1
2
3
4
W1 = tf.get_variable("W1", [25,12288], initializer = tf.contrib.layers.xavier_initializer(seed = 1))
b1 = tf.get_variable("b1", [25,1], initializer = tf.zeros_initializer())


  • tf.Variable() 每次都在创建新对象,对于get_variable()来说,对于已经创建的变量对象,就把那个对象返回,如果没有创建变量对象的话,就创建一个新的

请使用seed = 1来确保你的结果与我们的结果相符。

编写函数initialize_parameters()初始化神经网络的参数

要求

  • 初始化神经网络的参数,
  • 参数的维度:

    • W1 : [25, 12288]
    • b1 : [25, 1]
    • W2 : [12, 25]
    • b2 : [12, 1]
    • W3 : [6, 12]
    • b3 : [6, 1]
  • 返回:

    • parameters - 包含了W和b的字典
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def initialize_parameters():
"""
初始化神经网络的参数,参数的维度如下:
W1 : [25, 12288]
b1 : [25, 1]
W2 : [12, 25]
b2 : [12, 1]
W3 : [6, 12]
b3 : [6, 1]

返回:
parameters - 包含了W和b的字典


"""

tf.set_random_seed(1) #指定随机种子

W1 = tf.get_variable("W1",[25,12288],initializer=tf.contrib.layers.xavier_initializer(seed=1))
b1 = tf.get_variable("b1",[25,1],initializer=tf.zeros_initializer())
W2 = tf.get_variable("W2", [12, 25], initializer = tf.contrib.layers.xavier_initializer(seed=1))
b2 = tf.get_variable("b2", [12, 1], initializer = tf.zeros_initializer())
W3 = tf.get_variable("W3", [6, 12], initializer = tf.contrib.layers.xavier_initializer(seed=1))
b3 = tf.get_variable("b3", [6, 1], initializer = tf.zeros_initializer())

parameters = {"W1": W1,
"b1": b1,
"W2": W2,
"b2": b2,
"W3": W3,
"b3": b3}

return parameters

运行代码测试函数initialize_parameters()

1
2
3
4
5
6
7
8
9
tf.reset_default_graph() #用于清除默认图形堆栈并重置全局默认图形。 

with tf.Session() as sess:
parameters = initialize_parameters()
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

预期输出:

1
2
3
4
W1 = <tf.Variable 'W1:0' shape=(25, 12288) dtype=float32_ref>
b1 = <tf.Variable 'b1:0' shape=(25, 1) dtype=float32_ref>
W2 = <tf.Variable 'W2:0' shape=(12, 25) dtype=float32_ref>
b2 = <tf.Variable 'b2:0' shape=(12, 1) dtype=float32_ref>

如预期的那样,这些参数只有物理空间,但是还没有被赋值,这是因为没有通过session执行

Tensorflow中的正向传播

你现在将在tensorflow中实现正向传播模块。该函数将接收参数字典,并将完成正向传递。你将使用的函数是:

  • tf.add(...,...)进行加法
  • tf.matmul(...,...)进行矩阵乘法
  • tf.nn.relu(...)以应用ReLU激活

问题:实现神经网络的正向传递。我们为你注释了numpy等式,以便你可以将tensorflow实现与numpy实现进行比较。重要的是要注意,前向传播在z3处停止。原因是在tensorflow中,最后的线性层输出作为计算损失函数的输入。因此,你不需要a3

编写函数forward_propagation(X,parameters)实现一个模型的前向传播

要求

  • 实现一个模型的前向传播,模型结构为LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SOFTMAX

  • 参数:

    • X - 输入数据的占位符,维度为(输入节点数量,样本数量)
    • parameters - 包含了W和b的参数的字典
  • 返回:

    • Z3 - 最后一个LINEAR节点的输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def forward_propagation(X,parameters):
"""
实现一个模型的前向传播,模型结构为LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SOFTMAX

参数:
X - 输入数据的占位符,维度为(输入节点数量,样本数量)
parameters - 包含了W和b的参数的字典

返回:
Z3 - 最后一个LINEAR节点的输出

"""

W1 = parameters['W1']
b1 = parameters['b1']
W2 = parameters['W2']
b2 = parameters['b2']
W3 = parameters['W3']
b3 = parameters['b3']

Z1 = tf.add(tf.matmul(W1,X),b1) # Z1 = np.dot(W1, X) + b1
#Z1 = tf.matmul(W1,X) + b1 #也可以这样写
A1 = tf.nn.relu(Z1) # A1 = relu(Z1)
Z2 = tf.add(tf.matmul(W2, A1), b2) # Z2 = np.dot(W2, a1) + b2
A2 = tf.nn.relu(Z2) # A2 = relu(Z2)
Z3 = tf.add(tf.matmul(W3, A2), b3) # Z3 = np.dot(W3,Z2) + b3


return Z3


运行代码测试函数forward_propagation(X,parameters)

1
2
3
4
5
6
7
tf.reset_default_graph() #用于清除默认图形堆栈并重置全局默认图形。 
with tf.Session() as sess:
X,Y = create_placeholders(12288,6)
parameters = initialize_parameters()
Z3 = forward_propagation(X,parameters)
print("Z3 = " + str(Z3))

预期输出:
Z3 = Tensor(“Add_2:0”, shape=(6, ?), dtype=float32)

你可能已经注意到,正向传播不会输出任何cache。当我们开始进行传播时,你将在下面理解为什么。

计算损失

如前所述,使用以下方法很容易计算损失:

1
2
tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = ..., labels = ...))


问题:实现以下损失函数。

  • 重要的是要知道tf.nn.softmax_cross_entropy_with_logits的”logits“和”labels“输入应具有一样的维度(数据数,类别数)。 因此,我们为你转换了Z3和Y。
  • 此外,tf.reduce_mean是对所有数据求平均。

编写函数compute_cost(Z3,Y) 计算成本
注意

  • 函数tf.nn.softmax_cross_entropy_with_logits期望的输入格式是 [batch_size, num_classes],即每一行代表一个样本的预测结果和对应的真实标签。
    要求
  • 计算成本

  • 参数:

    • Z3 - 前向传播的结果
    • Y - 标签,一个占位符,和Z3的维度相同
  • 返回:

    • cost - 成本值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def compute_cost(Z3,Y):
"""
计算成本

参数:
Z3 - 前向传播的结果
Y - 标签,一个占位符,和Z3的维度相同

返回:
cost - 成本值


"""
logits = tf.transpose(Z3) #转置
labels = tf.transpose(Y) #转置

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits,labels=labels))

return cost

测试函数compute_cost(Z3,Y)

1
2
3
4
5
6
7
8
9
10
tf.reset_default_graph()

with tf.Session() as sess:
X,Y = create_placeholders(12288,6)
parameters = initialize_parameters()
Z3 = forward_propagation(X,parameters)
cost = compute_cost(Z3,Y)
print("cost = " + str(cost))


预期输出:
cost = Tensor(“Mean:0”, shape=(), dtype=float32)

反向传播和参数更新

所有反向传播和参数更新均可使用1行代码完成,将这部分合并到模型中非常容易。

计算损失函数之后,你将创建一个”optimizer“对象。运行tf.session时,必须与损失一起调用此对象。调用时,它将使用所选方法和学习率对给定的损失执行优化。

例如,对于梯度下降,优化器将是:

1
2
3
4

optimizer = tf.train.GradientDescentOptimizer(learning_rate = learning_rate).minimize(cost)


要进行优化,你可以执行以下操作:

1
2
_ , c = sess.run([optimizer,cost],feed_dict={X:mini_batch_X,Y:mini_batch_Y})

通过相反顺序的tensorflow图来计算反向传播。从损失到输入。

注意编码时,我们经常使用_作为“throwaway”变量来存储以后不再需要使用的值。这里_代表了我们不需要的optimizer的评估值(而 c 代表了 cost变量的值)。

建立模型

现在,将它们组合在一起!

练习:调用之前实现的函数构建完整模型。
编写函数model(X_train,Y_train,X_test,Y_test, learning_rate=0.0001,num_epochs=1500,minibatch_size=32, print_cost=True,is_plot=True)
要求

  • 实现一个三层的TensorFlow神经网络:LINEAR->RELU->LINEAR->RELU->LINEAR->SOFTMAX

  • 参数:

    • X_train - 训练集,维度为(输入大小(输入节点数量) = 12288, 样本数量 = 1080)
    • Y_train - 训练集分类数量,维度为(输出大小(输出节点数量) = 6, 样本数量 = 1080)
    • X_test - 测试集,维度为(输入大小(输入节点数量) = 12288, 样本数量 = 120)
    • Y_test - 测试集分类数量,维度为(输出大小(输出节点数量) = 6, 样本数量 = 120)
    • learning_rate - 学习速率
    • num_epochs - 整个训练集的遍历次数
    • mini_batch_size - 每个小批量数据集的大小
    • print_cost - 是否打印成本,每100代打印一次
    • is_plot - 是否绘制曲线图
  • 返回:

    • parameters - 学习后的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
def model(X_train,Y_train,X_test,Y_test,
learning_rate=0.0001,num_epochs=1500,minibatch_size=32,
print_cost=True,is_plot=True):
"""
实现一个三层的TensorFlow神经网络:LINEAR->RELU->LINEAR->RELU->LINEAR->SOFTMAX

参数:
X_train - 训练集,维度为(输入大小(输入节点数量) = 12288, 样本数量 = 1080)
Y_train - 训练集分类数量,维度为(输出大小(输出节点数量) = 6, 样本数量 = 1080)
X_test - 测试集,维度为(输入大小(输入节点数量) = 12288, 样本数量 = 120)
Y_test - 测试集分类数量,维度为(输出大小(输出节点数量) = 6, 样本数量 = 120)
learning_rate - 学习速率
num_epochs - 整个训练集的遍历次数
mini_batch_size - 每个小批量数据集的大小
print_cost - 是否打印成本,每100代打印一次
is_plot - 是否绘制曲线图

返回:
parameters - 学习后的参数

"""
ops.reset_default_graph() #能够重新运行模型而不覆盖tf变量
tf.set_random_seed(1)
seed = 3
(n_x , m) = X_train.shape #获取输入节点数量和样本数
n_y = Y_train.shape[0] #获取输出节点数量
costs = [] #成本集

#给X和Y创建placeholder
X,Y = create_placeholders(n_x,n_y)

#初始化参数
parameters = initialize_parameters()

#前向传播
Z3 = forward_propagation(X,parameters)

#计算成本
cost = compute_cost(Z3,Y)

#反向传播,使用Adam优化
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

#初始化所有的变量
init = tf.global_variables_initializer()

#开始会话并计算
with tf.Session() as sess:
#初始化
sess.run(init)

#正常训练的循环
for epoch in range(num_epochs):

epoch_cost = 0 #每代的成本
num_minibatches = int(m / minibatch_size) #minibatch的总数量
seed = seed + 1
minibatches = tf_utils.random_mini_batches(X_train,Y_train,minibatch_size,seed)

for minibatch in minibatches:

#选择一个minibatch
(minibatch_X,minibatch_Y) = minibatch

#数据已经准备好了,开始运行session
_ , minibatch_cost = sess.run([optimizer,cost],feed_dict={X:minibatch_X,Y:minibatch_Y})

#计算这个minibatch在这一代中所占的误差
epoch_cost = epoch_cost + minibatch_cost / num_minibatches

#记录并打印成本
## 记录成本
if epoch % 5 == 0:
costs.append(epoch_cost)
#是否打印:
if print_cost and epoch % 100 == 0:
print("epoch = " + str(epoch) + " epoch_cost = " + str(epoch_cost))

#是否绘制图谱
if is_plot:
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()

#保存学习后的参数
parameters = sess.run(parameters)
print("参数已经保存到session。")

#计算当前的预测结果
correct_prediction = tf.equal(tf.argmax(Z3),tf.argmax(Y))

#计算准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction,"float"))

print("训练集的准确率:", accuracy.eval({X: X_train, Y: Y_train}))
print("测试集的准确率:", accuracy.eval({X: X_test, Y: Y_test}))

return parameters

运行以下单元格来训练你的模型!在我们的机器上大约需要5分钟。 你的“100epoch后的损失”应为1.016458。如果不是,请不要浪费时间。中断训练,然后尝试更正你的代码。如果损失正确,请稍等片刻,然后在5分钟内回来!

1
2
3
4
5
6
7
8
9
10
parameters = model(X_train, Y_train, X_test, Y_test)
#开始时间
start_time = time.clock()
#开始训练
parameters = model(X_train, Y_train, X_test, Y_test)
#结束时间
end_time = time.clock()
#计算时差
print("CPU的执行时间 = " + str(end_time - start_time) + " 秒" )

预期输出:
poch = 0 epoch_cost = 1.85570189447
epoch = 100 epoch_cost = 1.01645776539
epoch = 200 epoch_cost = 0.733102379423
epoch = 300 epoch_cost = 0.572938936226
epoch = 400 epoch_cost = 0.468773578604
epoch = 500 epoch_cost = 0.3810211113
epoch = 600 epoch_cost = 0.313826778621
epoch = 700 epoch_cost = 0.254280460603
epoch = 800 epoch_cost = 0.203799342567
epoch = 900 epoch_cost = 0.166511993291
epoch = 1000 epoch_cost = 0.140936921718
epoch = 1100 epoch_cost = 0.107750129745
epoch = 1200 epoch_cost = 0.0862994250475
epoch = 1300 epoch_cost = 0.0609485416137
epoch = 1400 epoch_cost = 0.0509344103436
参数已经保存到session。
训练集的准确率: 0.999074
测试集的准确率: 0.725
CPU的执行时间 = 482.19651398680486 秒

Nice!你的算法可以识别出表示0到5之间数字的手势,准确度达到了72.5%。

评价

  • 你的模型足够强大,可以很好地拟合训练集。但是,鉴于训练和测试精度之间的差异,你可以尝试添加L2或dropout正则化以减少过拟合。
  • 将会话视为训练模型的代码块。每次你在小批次上运行会话时,它都会训练参数。总的来说,你已经运行了该会话多次(1500个epoch),直到获得训练有素的参数为止。

使用自己的图像进行测试(可选练习)

  • 把jpg转化为png,因为mpimg只能读取png的图片
1
2
3
4
5
6
7
8
9
10
11
12
13
import matplotlib.pyplot as plt # plt 用于显示图片
import matplotlib.image as mpimg # mpimg 用于读取图片
import numpy as np

# 自己拍的图片
my_image1 = "5.png" #定义图片名称
fileName1 = "images/fingers/" + my_image1 #图片地址
image1 = mpimg.imread(fileName1) #读取图片
plt.imshow(image1) #显示图片
my_image1 = image1.reshape(1,64 * 64 * 3).T #重构图片
my_image_prediction = tf_utils.predict(my_image1, parameters) #开始预测
print("预测结果: y = " + str(np.squeeze(my_image_prediction)))

预期输出: y = 5 #预测正确

尽管你看到算法似乎对它进行了错误分类,但你确实值得“竖起大拇指”。原因是训练集不包含任何“竖起大拇指”,因此模型不知道如何处理! 我们称其为“数据不平衡”,它是下一章“构建机器学习项目”中的学习课程之一。

你应该记住

  • Tensorflow是深度学习中经常使用的编程框架
  • Tensorflow中的两个主要对象类别是张量和运算符。
  • 在Tensorflow中进行编码时,你必须执行以下步骤:
    • 创建一个包含张量(变量,占位符…)和操作(tf.matmul,tf.add,…)的计算图
    • 创建会话
    • 初始化会话
    • 运行会话以执行计算图
  • 你可以像在model()中看到的那样多次执行计算图
  • 在“优化器”对象上运行会话时,将自动完成反向传播和优化。
作业7 搭建卷积神经网络模型

逐步实现卷积神经网络

欢迎来到课程4的第一个作业!在此作业中,你将使用numpy实现卷积(CONV)和池化(POOL)层,包括正向传播和反向传播(可选)。

符号

  • 上标$[l]$表示第$l^{th}$层的对象。

    • 例如:$a^{[4]}$是$4^{th}$层的激活。 $W^{[5]}$和$b^{[5]}$是$5^{th}$层的参数。
  • 上标$(i)$表示第$i^{th}$个示例中的对象。

    • 示例:$x^{(i)}$是$i^{th}$个训练数据的输入。
  • 下标$i$表示$i^{th}$的向量输入。

    • 示例:$a^{[l]}_i$表示$l$层中的$i^{th}$个激活,假设这是全连接层(FC)。
  • $n_H$, $n_W$和$n_C$分别表示给定层的通道的高度,宽度和数量。如果要引用特定层$l$,则还可以写入 $n_H^{[l]}$, $n_W^{[l]}$, $n_C^{[l]}$。
  • $n_{H_{prev}}$, $n_{W_{prev}}$和$n_{C_{prev}}$ 分别表示前一层的高度,宽度和通道数。如果引用特定层$l$,则也可以表示为$n_H^{[l-1]}$, $n_W^{[l-1]}$, $n_C^{[l-1]}$。

我们假设你已经熟悉numpy或者已经完成了之前的专业课程。那就开始吧!

安装包

让我们首先导入在作业过程中需要用到的包:

  • numpy 是Python科学计算的基本包。
  • matplotlib 是在Python中常用的绘制图形的库。
  • np.random.seed(1)使所有随机函数调用保持一致。这将帮助我们为你的作品评分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
import h5py
import matplotlib.pyplot as plt

%matplotlib inline
plt.rcParams['figure.figsize'] = (5.0, 4.0)
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

#ipython很好用,但是如果在ipython里已经import过的模块修改后需要重新reload就需要这样
#在执行用户代码前,重新装入软件的扩展和模块。
%load_ext autoreload
#autoreload 2:装入所有 %aimport 不包含的模块。
%autoreload 2

np.random.seed(1) #指定随机种子

作业大纲

你将实现构建卷积神经网络的需要的模块!要求实现的每个函数都有详细的说明,以帮助你完成所需的步骤:

  • 卷积函数,包括:
    • 零填充
    • 卷积窗口
    • 正向卷积
    • 反向卷积(可选)
  • 池化函数,包括:
    • 正向池化
    • 创建mask
    • 分配值
    • 反向池化(可选)

本次将要求你使用 numpy从头开始实现这些函数。

注意,对于每个正向函数,都有其对应的反向等式。因此,在正向传播模块的每一步中,都将一些参数存储在缓存中。这些参数用于在反向传播时计算梯度。

卷积神经网络

尽管编程框架可以方便使用卷积,但它们仍然是深度学习中最难理解的概念之一。卷积层将输入体积转换为不同大小的输出体积,如下所示。

在这一部分,你将构建卷积层的每一步。首先实现两个辅助函数:一个用于零填充,另一个用于计算卷积函数本身。

零填充

零填充将在图像的边界周围添加零:

图1 零填充
图像(3个通道,RGB),填充2次。

填充的主要好处有:

  • 允许使用CONV层而不必缩小其高度和宽度。这对于构建更深的网络很重要,因为高度/宽度会随着更深的层而缩小。一个重要、特殊的例子是”same”卷积,其中高度/宽度在一层之后被精确保留。
  • 有助于我们将更多信息保留在图像边缘。如果不进行填充,下一层的一部分值将会受到图像边缘像素的干扰。

练习:实现以下函数,该功能将使用零填充处理一个批次X的所有图像数据。Use np.pad。注意,如果要填充维度为$(5,5,5,5,5)$的数组“a”,则第二维的填充为pad = 1,第四维的填充为pad = 3,其余为pad = 0,你可以这样做:

1
a = np.pad(a, ((0,0), (1,1), (0,0), (3,3), (0,0)), 'constant', constant_values = (..,..))

解读
np.pad(arr3D, ((0, 0), (1, 1), (2, 2)), ‘constant’)
arr3D 是待填充的三维数组。假设它的形状是 (d1, d2, d3)
第二个参数 ((0, 0), (1, 1), (2, 2)) 定义了在各个维度上填充的数量。

第一个元组 (0, 0) 表示在第一个维度(通常是深度或页的方向)上不填充,即保持这个维度的大小不变。
第二个元组 (1, 1) 表示在第二个维度(通常是行的方向)上在前后各填充 1 个单位。
第三个元组 (2, 2) 表示在第三个维度(通常是列的方向)上在前后各填充 2 个单位。
这样填充后,arr3D 的新形状将变为 (d1, d2 + 2, d3 + 4)

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

#constant连续一样的值填充,有constant_values=(x, y)时前面用x填充,后面用y填充。缺省参数是为constant_values=(0,0)

a = np.pad(a,( (0,0),(1,1),(0,0),(3,3),(0,0)),'constant',constant_values = (..,..))

#比如:
import numpy as np
arr3D = np.array([[[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4]],

[[0, 1, 2, 3, 4, 5],
[0, 1, 2, 3, 4, 5],
[0, 1, 2, 3, 4, 5]],

[[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4]]])

print 'constant: \n' + str(np.pad(arr3D, ((0, 0), (1, 1), (2, 2)), 'constant'))

"""
constant:
[[[0 0 0 0 0 0 0 0 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 0 0 0 0 0 0 0 0]]

[[0 0 0 0 0 0 0 0 0 0]
[0 0 0 1 2 3 4 5 0 0]
[0 0 0 1 2 3 4 5 0 0]
[0 0 0 1 2 3 4 5 0 0]
[0 0 0 0 0 0 0 0 0 0]]

[[0 0 0 0 0 0 0 0 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 0 0 0 0 0 0 0 0]]]
"""


编写函数zero_pad(X,pad)把数据集X的图像边界全部使用0来扩充pad个宽度和高度
要求

  • 把数据集X的图像边界全部使用0来扩充pad个宽度和高度。

  • 参数:

    • X - 图像数据集,维度为(样本数,图像高度,图像宽度,图像通道数)
    • pad - 整数,每个图像在垂直和水平维度上的填充量
  • 返回:
    • X_paded - 扩充后的图像数据集,维度为(样本数,图像高度 + 2pad,图像宽度 + 2pad,图像通道数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# GRADED FUNCTION: zero_pad

def zero_pad(X,pad):
"""
把数据集X的图像边界全部使用0来扩充pad个宽度和高度。

参数:
X - 图像数据集,维度为(样本数,图像高度,图像宽度,图像通道数)
pad - 整数,每个图像在垂直和水平维度上的填充量
返回:
X_paded - 扩充后的图像数据集,维度为(样本数,图像高度 + 2*pad,图像宽度 + 2*pad,图像通道数)

"""

X_paded = np.pad(X,(
(0,0), #样本数,不填充
(pad,pad), #图像高度,你可以视为上面填充x个,下面填充y个(x,y)
(pad,pad), #图像宽度,你可以视为左边填充x个,右边填充y个(x,y)
(0,0)), #通道数,不填充
'constant', constant_values=0) #连续一样的值填充

return X_paded


运行代码测试zero_pad(X,pad)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
np.random.seed(1)
x = np.random.randn(4,3,3,2)
x_paded = zero_pad(x,2)
#查看信息
print ("x.shape =", x.shape)
print ("x_paded.shape =", x_paded.shape)
print ("x[1, 1] =", x[1, 1])
print ("x_paded[1, 1] =", x_paded[1, 1])

#绘制图
fig , axarr = plt.subplots(1,2) #一行两列
axarr[0].set_title('x')
axarr[0].imshow(x[0,:,:,0])
axarr[1].set_title('x_paded')
axarr[1].imshow(x_paded[0,:,:,0])

预期输出:
x.shape = (4, 3, 3, 2)
x_pad.shape = (4, 7, 7, 2)
x[1,1] = [[ 0.90085595 -0.68372786]
[-0.12289023 -0.93576943]
[-0.26788808 0.53035547]]
x_pad[1,1] = [[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]]

卷积的单个步骤

在这一部分中,实现卷积的单个步骤,其中将滤波器(卷积核)应用于输入的单个位置。这将用于构建卷积单元,该卷积单元:

  • 占用输入体积
  • 在输入的每个位置都应用滤波器
  • 输出另一个体积(通常大小不同)

图2 卷积操作
滤波器大小为2x2,步幅为1(步幅=每次滑动时移动窗口的数量)

在计算机视觉应用中,左侧矩阵中的每个值都对应一个像素值,我们将3x3滤波器与图像进行卷积操作,首先将滤波器元素的值与原始矩阵相乘,然后将它们相加。在练习的第一步中,你将实现卷积的单个步骤,相当于仅对一个位置应用滤波器以获得单个实值输出。

在后面,你将应用此函数于输入的多个位置以实现完整的卷积运算。

练习:实现conv_single_step()。 提示.

编写函数conv_single_step(a_slice_prev,W,b)实现卷积的单个步骤仅对一个位置应用滤波器

要求

  • 在前一层的激活输出的一个片段上应用一个由参数W定义的过滤器。
  • 这里切片大小和过滤器大小相同

  • 参数:

    • a_slice_prev - 输入数据的一个片段,维度为(过滤器大小,过滤器大小,上一通道数),通常是上一层的激活输出。
    • W - 权重参数,包含在了一个矩阵中,维度为(过滤器大小,过滤器大小,上一通道数)定义了滤波器(卷积核)。
    • b - 偏置参数,包含在了一个矩阵中,维度为(1,1,1)通常是一个标量

返回:

  • Z - 在输入数据的片X上卷积滑动窗口(w,b)的结果。对输入数据 a_slice_prev 经过权重 W 和偏置 b 处理后的单个卷积输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def conv_single_step(a_slice_prev,W,b):
"""
在前一层的激活输出的一个片段上应用一个由参数W定义的过滤器。
这里切片大小和过滤器大小相同

参数:
a_slice_prev - 输入数据的一个片段,维度为(过滤器大小,过滤器大小,上一通道数)
W - 权重参数,包含在了一个矩阵中,维度为(过滤器大小,过滤器大小,上一通道数)
b - 偏置参数,包含在了一个矩阵中,维度为(1,1,1)

返回:
Z - 在输入数据的片X上卷积滑动窗口(w,b)的结果。
"""
# np.multiply(a_slice_prev, W) 进行逐元素相乘,这将 a_slice_prev 和权重矩阵 W 中的每个元素相乘。
# 对整个 s 数组的每个元素加上偏置 b
s = np.multiply(a_slice_prev,W) + b

Z = np.sum(s)

return Z

运行代码测试函数conv_single_step(a_slice_prev,W,b)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
np.random.seed(1)

#这里切片大小和过滤器大小相同
a_slice_prev = np.random.randn(4,4,3)
W = np.random.randn(4,4,3)
b = np.random.randn(1,1,1)

Z = conv_single_step(a_slice_prev,W,b)

print("Z = " + str(Z))

```

**预期输出**:
Z = -23.16021220252078


### 卷积神经网络--正向传递

在正向传递中,你将使用多个滤波器对输入进行卷积。每个“卷积”都会输出一个2D矩阵。然后,你将堆叠这些输出以获得3

{% image /media/深度学习基础编程作业/60.gif %}

**练习**:实现以下函数,使用滤波器W卷积输入A_prev。此函数将上一层的激活输出(对于一批m个输入)A_prev作为输入,F表示滤波器/权重(W)和偏置向量(b),其中每个滤波器都有自己的(单个)偏置。最后,你还可以访问包含stride和padding的超参数字典。

**提示**:
1.要在矩阵“a_prev”(5,5,3)的左上角选择一个2x2切片,对三个维度都进行切片操作,请执行以下操作:
- 提取三维数据的前两行前两列所有通道(左闭右开)

```py
a_slice_prev = a_prev[0:2,0:2,:]

使用定义的start/end索引定义a_slice_prev时将非常有用。
2.要定义a_slice,你需要首先定义其角点 vert_start, vert_end, horiz_starthoriz_end。该图可能有助于你找到如何在下面的代码中使用h,w,f和s定义每个角。

图3 使用垂直和水平的start/end(2x2滤波器)定义切片

该图仅显示一个通道。

提醒
卷积的输出维度与输入维度相关公式为:

编写函数conv_forward(A_prev, W, b, hparameters)实现卷积函数的前向传播

要求

  • 对于此作业,我们不必考虑向量化,只使用for循环实现所有函数。
  • 参数:

    • A_prev - 上一层的激活输出矩阵,维度为(m, n_H_prev, n_W_prev, n_C_prev),(样本数量,上一层图像的高度,上一层图像的宽度,上一层过滤器数量)
    • W - 权重矩阵,维度为(f, f, n_C_prev, n_C),(过滤器大小,过滤器大小,上一层的过滤器数量,这一层的过滤器数量)
    • b - 偏置矩阵,维度为(1, 1, 1, n_C),(1,1,1,这一层的过滤器数量)
    • hparameters - 包含了”stride”与 “pad”的超参数字典。
  • 返回:

    • Z - 卷积输出,维度为(m, n_H, n_W, n_C),(样本数,图像的高度,图像的宽度,过滤器数量)
    • cache - 缓存了一些反向传播函数conv_backward()需要的一些数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def conv_forward(A_prev, W, b, hparameters):
"""
实现卷积函数的前向传播

参数:
A_prev - 上一层的激活输出矩阵,维度为(m, n_H_prev, n_W_prev, n_C_prev),(样本数量,上一层图像的高度,上一层图像的宽度,上一层过滤器数量)
W - 权重矩阵,维度为(f, f, n_C_prev, n_C),(过滤器大小,过滤器大小,上一层的过滤器数量,这一层的过滤器数量)
b - 偏置矩阵,维度为(1, 1, 1, n_C),(1,1,1,这一层的过滤器数量)
hparameters - 包含了"stride"与 "pad"的超参数字典。

返回:
Z - 卷积输出,维度为(m, n_H, n_W, n_C),(样本数,图像的高度,图像的宽度,过滤器数量)
cache - 缓存了一些反向传播函数conv_backward()需要的一些数据
"""

#获取来自上一层数据的基本信息
(m , n_H_prev , n_W_prev , n_C_prev) = A_prev.shape

#获取权重矩阵的基本信息
( f , f ,n_C_prev , n_C ) = W.shape

#获取超参数hparameters的值
stride = hparameters["stride"]
pad = hparameters["pad"]

#计算卷积后的图像的宽度高度,参考上面的公式,使用int()来进行板除
n_H = int(( n_H_prev - f + 2 * pad )/ stride) + 1
n_W = int(( n_W_prev - f + 2 * pad )/ stride) + 1

#使用0来初始化卷积输出Z
Z = np.zeros((m,n_H,n_W,n_C))

#通过A_prev创建填充过了的A_prev_pad
A_prev_pad = zero_pad(A_prev,pad)

for i in range(m): #遍历样本
a_prev_pad = A_prev_pad[i] #选择第i个样本的扩充后的激活矩阵
for h in range(n_H): #在输出的垂直轴上循环
for w in range(n_W): #在输出的水平轴上循环
for c in range(n_C): #循环遍历输出的通道
#定位当前的切片位置
vert_start = h * stride #竖向,开始的位置
vert_end = vert_start + f #竖向,结束的位置
horiz_start = w * stride #横向,开始的位置
horiz_end = horiz_start + f #横向,结束的位置
#切片位置定位好了我们就把它取出来,需要注意的是我们是“穿透”取出来的,
#自行脑补一下吸管插入一层层的橡皮泥就明白了
a_slice_prev = a_prev_pad[vert_start:vert_end,horiz_start:horiz_end,:]
#执行单步卷积
Z[i,h,w,c] = conv_single_step(a_slice_prev,W[: ,: ,: ,c],b[0,0,0,c])

#数据处理完毕,验证数据格式是否正确
assert(Z.shape == (m , n_H , n_W , n_C ))

#存储一些缓存值,以便于反向传播使用
cache = (A_prev,W,b,hparameters)

return (Z , cache)

测试conv_forward(A_prev, W, b, hparameters)函数

1
2
3
4
5
6
7
8
9
10
11
12
13
np.random.seed(1)

A_prev = np.random.randn(10,4,4,3)
W = np.random.randn(2,2,3,8)
b = np.random.randn(1,1,1,8)

hparameters = {"pad" : 2, "stride": 1}

Z , cache_conv = conv_forward(A_prev,W,b,hparameters)

print("np.mean(Z) = ", np.mean(Z))
print("cache_conv[0][1][2][3] =", cache_conv[0][1][2][3])

预期输出:
Z’s mean = 0.15585932488906465
cache_conv[0][1][2][3] = [-0.20075807 0.18656139 0.41005165]

最后,CONV层还应包含一个激活,此情况下,我们将添加以下代码行:

1
2
3
4
5
6
#获取输出
Z[i, h, w, c] = ...
#计算激活
A[i, h, w, c] = activation(Z[i, h, w, c])


在这里你不需要做这个。

池化层

池化(POOL)层减少了输入的高度和宽度。它有助于减少计算量,而且可以使特征检测器在输入中的位置保持不变。池化层有两种:

  • 最大池化:在输入上滑动 ($f, f$)窗口,并将窗口的最大值存储在输出中。

  • 平均池化:在输入上滑动 ($f, f$)窗口,并将该窗口的平均值存储在输出中。

这些池化层没有用于反向传播训练的参数。但是,它们具有超参数,例如窗口大小$f$,它指定了你要计算最大值或平均值的窗口的高度和宽度。

正向池化

现在,你将在同一函数中实现最大池化和平均池化。

练习:实现池化层的正向传播。请遵循下述提示。

提示
由于没有填充,因此将池化的输出维度绑定到输入维度的公式为:

编写函数pool_forward(A_prev,hparameters,mode = max)实现池化层的前向传播

要求

  • 参数:

    • A_prev - 输入数据,维度为(m, n_H_prev, n_W_prev, n_C_prev)
    • hparameters - 包含了 “f” 和 “stride”的超参数字典
    • mode - 模式选择【”max” | “average”】
  • 返回:

    • A - 池化层的输出,维度为 (m, n_H, n_W, n_C)
    • cache - 存储了一些反向传播需要用到的值,包含了输入(数组)和超参数(字典)的元组。
      • cache = (A_prev,hparameters)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def pool_forward(A_prev,hparameters,mode="max"):
"""
实现池化层的前向传播

参数:
A_prev - 输入数据,维度为(m, n_H_prev, n_W_prev, n_C_prev)
hparameters - 包含了 "f" 和 "stride"的超参数字典
mode - 模式选择【"max" | "average"】

返回:
A - 池化层的输出,维度为 (m, n_H, n_W, n_C)
cache - 存储了一些反向传播需要用到的值,包含了输入和超参数的字典。
"""

#获取输入数据的基本信息
(m , n_H_prev , n_W_prev , n_C_prev) = A_prev.shape

#获取超参数的信息
f = hparameters["f"]
stride = hparameters["stride"]

#计算输出维度
n_H = int((n_H_prev - f) / stride ) + 1
n_W = int((n_W_prev - f) / stride ) + 1
n_C = n_C_prev

#初始化输出矩阵
A = np.zeros((m , n_H , n_W , n_C))

for i in range(m): #遍历样本
for h in range(n_H): #在输出的垂直轴上循环
for w in range(n_W): #在输出的水平轴上循环
for c in range(n_C): #循环遍历输出的通道
#定位当前的切片位置
vert_start = h * stride #竖向,开始的位置
vert_end = vert_start + f #竖向,结束的位置
horiz_start = w * stride #横向,开始的位置
horiz_end = horiz_start + f #横向,结束的位置
#定位完毕,开始切割
a_slice_prev = A_prev[i,vert_start:vert_end,horiz_start:horiz_end,c]

#对切片进行池化操作
if mode == "max":
A[ i , h , w , c ] = np.max(a_slice_prev)
elif mode == "average":
A[ i , h , w , c ] = np.mean(a_slice_prev)

#池化完毕,校验数据格式
assert(A.shape == (m , n_H , n_W , n_C))

#校验完毕,开始存储用于反向传播的值
cache = (A_prev,hparameters)

return A,cache

测试一下函数pool_forward

1
2
3
4
5
6
7
8
9
10
11
12
13
np.random.seed(1)
A_prev = np.random.randn(2,4,4,3)
hparameters = {"f":4 , "stride":1}

A , cache = pool_forward(A_prev,hparameters,mode="max")
A, cache = pool_forward(A_prev, hparameters)
print("mode = max")
print("A =", A)
print("----------------------------")
A, cache = pool_forward(A_prev, hparameters, mode = "average")
print("mode = average")
print("A =", A)

预期输出:
mode = max
A = [[[[1.74481176 1.6924546 2.10025514]]]

[[[1.19891788 1.51981682 2.18557541]]]]

mode = average
A = [[[[-0.09498456 0.11180064 -0.14263511]]]

[[[-0.09525108 0.28325018 0.33035185]]]]

Nice!现在你已经实现了卷积网络所有层的正向传播。

卷积神经网络中的反向传播(可选练习)

在深度学习框架中,你只需要实现正向传播,该框架就可以处理反向传播,因此大多数深度学习工程师不需要理会反向传播的细节。卷积网络的反向传播很复杂。但是,如果你愿意,可以在笔记本的此可选部分中进行操作,以了解卷积网络中反向传播的原理。

在较早的课程中,当你实现了一个简单的(全连接)神经网络时,你就使用了反向传播来计算损失的导数以更新参数。类似地,在卷积神经网络中,你可以计算损失的导数以更新参数。反向传播方程并非不重要,即使我们在课程中并未导出它们,但下面简要介绍了过程。

卷积层的反向传播

让我们从实现CONV层的反向传播开始。

计算 dA:

这是用于针对特定滤波器$W_c$的损失和给定训练示例计算$dA$的公式:

其中$W_c$ 是一个滤波器,$dZ_{hw}$是一个标量,相对于第h行和第w列的conv层Z的输出的梯度的损失。请注意,每次更新dA时,我们都会将相同的滤波器$W_c$ 乘以不同的dZ。我们这样做主要是因为在计算正向传播时,每个滤波器都由不同的a_slice进行点乘和求和。因此,在为dA计算backprop时,我们只是加上所有a_slices的梯度。

在适当的for循环内,此公式转换为:

1
2
da_perv_pad[vert_start:vert_end,horiz_start:horiz_end,:] += W[:,:,:,c] * dZ[i,h,w,c]

计算 dW:

这是用于针对损失计算$dW_c$的公式($dW_c$是一个滤波器的导数):

其中$a_{slice}$对应于用于生成激活$Z_{ij}$的切片,最终我们得到$W$相对于该切片的梯度。由于它是相同的$W$,因此我们将所有这些梯度加起来即可得到$dW$。

在适当的for循环内,此公式转换为:

1
dW[:,:,:, c] += a_slice * dZ[i , h , w , c]

计算 db:

这是用于某个滤波器$W_c$的损失计算$db$的公式:

正如你先前在基本神经网络中所见,db是通过将$dZ$相加得出的。在这种情况下,你只需要对转换输出(Z)相对于损失的所有梯度求和。

在适当的for循环内,此公式转换为:

1
db[:,:,:,c] += dZ[ i, h, w, c]

练习:在下面实现conv_backward函数。你应该总结所有训练数据,滤波器,高度和宽度。然后,你应该使用上面的公式1、2和3计算导数。

编写函数conv_backward(dZ,cache)实现卷积层的反向传播

要求

  • 参数:

    • dZ - 卷积层的输出Z的 梯度,维度为(m, n_H, n_W, n_C)
    • cache - 反向传播所需要的参数,conv_forward()的输出之一
  • 返回:

    • dA_prev - 卷积层的输入(A_prev)的梯度值,维度为(m, n_H_prev, n_W_prev, n_C_prev)
    • dW - 卷积层的权值的梯度,维度为(f,f,n_C_prev,n_C)
    • db - 卷积层的偏置的梯度,维度为(1,1,1,n_C)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def conv_backward(dZ,cache):
"""
实现卷积层的反向传播

参数:
dZ - 卷积层的输出Z的 梯度,维度为(m, n_H, n_W, n_C)
cache - 反向传播所需要的参数,conv_forward()的输出之一

返回:
dA_prev - 卷积层的输入(A_prev)的梯度值,维度为(m, n_H_prev, n_W_prev, n_C_prev)
dW - 卷积层的权值的梯度,维度为(f,f,n_C_prev,n_C)
db - 卷积层的偏置的梯度,维度为(1,1,1,n_C)

"""
#获取cache的值
(A_prev, W, b, hparameters) = cache

#获取A_prev的基本信息
(m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape

#获取dZ的基本信息
(m,n_H,n_W,n_C) = dZ.shape

#获取权值的基本信息
(f, f, n_C_prev, n_C) = W.shape

#获取hparaeters的值
pad = hparameters["pad"]
stride = hparameters["stride"]

#初始化各个梯度的结构
dA_prev = np.zeros((m,n_H_prev,n_W_prev,n_C_prev))
dW = np.zeros((f,f,n_C_prev,n_C))
db = np.zeros((1,1,1,n_C))

#前向传播中我们使用了pad,反向传播也需要使用,这是为了保证数据结构一致
A_prev_pad = zero_pad(A_prev,pad)
dA_prev_pad = zero_pad(dA_prev,pad)

#现在处理数据
for i in range(m):
#选择第i个扩充了的数据的样本,降了一维。
a_prev_pad = A_prev_pad[i]
da_prev_pad = dA_prev_pad[i]

for h in range(n_H):
for w in range(n_W):
for c in range(n_C):
#定位切片位置
vert_start = h
vert_end = vert_start + f
horiz_start = w
horiz_end = horiz_start + f

#定位完毕,开始切片
a_slice = a_prev_pad[vert_start:vert_end,horiz_start:horiz_end,:]

#切片完毕,使用上面的公式计算梯度
da_prev_pad[vert_start:vert_end, horiz_start:horiz_end,:] += W[:,:,:,c] * dZ[i, h, w, c]
dW[:,:,:,c] += a_slice * dZ[i,h,w,c]
db[:,:,:,c] += dZ[i,h,w,c]
#设置第i个样本最终的dA_prev,即把非填充的数据取出来。
dA_prev[i,:,:,:] = da_prev_pad[pad:-pad, pad:-pad, :]

#数据处理完毕,验证数据格式是否正确
assert(dA_prev.shape == (m, n_H_prev, n_W_prev, n_C_prev))

return (dA_prev,dW,db)


编写函数测试conv_backward(dZ,cache)

1
2
3
4
5
np.random.seed(1)
dA, dW, db = conv_backward(Z, cache_conv)
print("dA_mean =", np.mean(dA))
print("dW_mean =", np.mean(dW))
print("db_mean =", np.mean(db))

预期输出:
dA_mean = 9.608990675868995
dW_mean = 10.581741275547563
db_mean = 76.37106919563735

池化层—反向传播

接下来,让我们从MAX-POOL层开始实现池化层的反向传播。即使池化层没有用于反向传播更新的参数,你仍需要通过池化层对梯度进行反向传播,以便计算池化层之前的层的梯度。

最大池化—反向传播

在进入池化层的反向传播之前,首先构建一个名为create_mask_from_window()的辅助函数,该函数将执行以下操作:

此函数创建一个“掩码”矩阵,该矩阵追踪矩阵的最大值。True(1)表示最大值在X中的位置,其他条目为False(0)。稍后你将看到,平均池的反向传播与此相似,但是使用了不同的掩码。

练习:实现create_mask_from_window()。此函数将有助于反向池化。
提示:

  • [np.max()]()可能会有所帮助。它计算一个数组的最大值。
  • 如果有一个矩阵X和一个标量x:A =(X == x)将返回与X大小相同的矩阵A,因此可以这么用:
1
2
A[i,j] = True if X[i,j] = x
A[i,j] = False if X[i,j] != x
  • 此处无需考虑矩阵中有多个最大值的情况。

编写函数create_mask_from_window(x)创建掩码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def create_mask_from_window(x):
"""
从输入矩阵中创建掩码,以保存最大值的矩阵的位置。

参数:
x - 一个维度为(f,f)的矩阵

返回:
mask - 包含x的最大值的位置的矩阵
"""
mask = x == np.max(x)

return mask

测试函数create_mask_from_window(x)

1
2
3
4
5
6
7
8
9
np.random.seed(1)

x = np.random.randn(2,3)

mask = create_mask_from_window(x)

print("x = " + str(x))
print("mask = " + str(mask))

预期输出:
x = [[ 1.62434536 -0.61175641 -0.52817175]
[-1.07296862 0.86540763 -2.3015387 ]]
mask = [[ True False False]
[False False False]]

为什么我们要追踪最大值的位置?因为这是最终影响输出的输入值,也影响了损失。 我们的正向传播首先是经过卷积层,然后滑动地取卷积层最大值构成了池化层,如果我们不记录最大值的位置,那么我们怎样才能反向传播到卷积层呢?
反向传播算法是根据损失计算梯度的,因此影响最终损失的任何事物都应具有非零的梯度。因此,反向传播将使梯度“传播”回影响损失的特定输入值。

平均池化—反向传播

在最大池化中,对于每个输入窗口,输出上的所有“影响”都来自单个输入值,即最大值。在平均池化中,输入窗口的每个元素对输出的影响均相同 因此,要实现反向传播,你现在将实现一个反映此点的辅助函数。

例如,如果我们使用2x2滤波器在正向传播中进行平均池化,那么用于反向传播的掩码将如下所示:

这意味着矩阵$dZ$中的每个位置对输出的贡献均等,因为在正向传播中,我们取平均值。

练习:实现以下函数,以通过维度矩阵平均分配值dz。 提示

编写函数distribute_value(dz,shape)平均分配dz的值

要求

  • 给定一个值,为按矩阵大小平均分配到每一个矩阵位置中。

  • 参数:

    • dz - 输入的实数
    • shape - 元组,两个值,分别为n_H , n_W
  • 返回:
    -a - 已经分配好了值的矩阵,里面的值全部一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def distribute_value(dz,shape):
"""
给定一个值,为按矩阵大小平均分配到每一个矩阵位置中。

参数:
dz - 输入的实数
shape - 元组,两个值,分别为n_H , n_W

返回:
a - 已经分配好了值的矩阵,里面的值全部一样。

"""
#获取矩阵的大小
(n_H , n_W) = shape

#计算平均值
average = dz / (n_H * n_W)

#填充入矩阵
a = np.ones(shape) * average

return a

编写函数测试函数distribute_value(dz,shape)

1
2
3
4
5
6
dz = 2
shape = (2,2)

a = distribute_value(dz,shape)
print("a = " + str(a))

预期输出:
a = [[ 0.5 0.5]
[ 0.5 0.5]]

组合:反向池化

现在,你准备好了在池化层上计算反向传播所需的一切。

练习:在两种模式("max""average")都实现“pool_backward”功能。再次使用4个for循环(遍历训练数据,高度,宽度和通道)。使用 if/elif语句来查看模式是否等于'max''average'。如果等于’average’ ,则应使用上面实现的distribute_value()函数创建与 a_slice维度相同的矩阵。此外,模式等于’max‘时,你将使用 create_mask_from_window()创建一个掩码,并将其乘以相应的dZ值。

编写函数pool_backward(dA,cache,mode = max)实现池化层的反向传播

要求

  • 参数:

    • dA - 池化层的输出的梯度,和池化层的输出的维度一样
    • cache - 池化层前向传播时所存储的参数。
    • mode - 模式选择,【”max” | “average”】
  • 返回:

    • dA_prev - 池化层的输入的梯度,和A_prev的维度相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def pool_backward(dA,cache,mode = "max"):
"""
实现池化层的反向传播

参数:
dA - 池化层的输出的梯度,和池化层的输出的维度一样
cache - 池化层前向传播时所存储的参数。
mode - 模式选择,【"max" | "average"】

返回:
dA_prev - 池化层的输入的梯度,和A_prev的维度相同

"""
#获取cache中的值
(A_prev , hparaeters) = cache

#获取hparaeters的值
f = hparaeters["f"]
stride = hparaeters["stride"]

#获取A_prev和dA的基本信息
(m , n_H_prev , n_W_prev , n_C_prev) = A_prev.shape
(m , n_H , n_W , n_C) = dA.shape

#初始化输出的结构
dA_prev = np.zeros_like(A_prev)

#开始处理数据
for i in range(m):
a_prev = A_prev[i]
for h in range(n_H):
for w in range(n_W):
for c in range(n_C):
#定位切片位置
vert_start = h
vert_end = vert_start + f
horiz_start = w
horiz_end = horiz_start + f

#选择反向传播的计算方式
if mode == "max":
#开始切片
a_prev_slice = a_prev[vert_start:vert_end,horiz_start:horiz_end,c]
#创建掩码
mask = create_mask_from_window(a_prev_slice)
#计算dA_prev
dA_prev[i,vert_start:vert_end,horiz_start:horiz_end,c] += np.multiply(mask,dA[i,h,w,c])

elif mode == "average":
#获取dA的值
da = dA[i,h,w,c]
#定义过滤器大小
shape = (f,f)
#平均分配
dA_prev[i,vert_start:vert_end, horiz_start:horiz_end ,c] += distribute_value(da,shape)
#数据处理完毕,开始验证格式
assert(dA_prev.shape == A_prev.shape)

return dA_prev

测试函数pool_backward(dA,cache,mode = max)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
np.random.seed(1)
A_prev = np.random.randn(5, 5, 3, 2)
hparameters = {"stride" : 1, "f": 2}
A, cache = pool_forward(A_prev, hparameters)
dA = np.random.randn(5, 4, 2, 2)

dA_prev = pool_backward(dA, cache, mode = "max")
print("mode = max")
print('mean of dA = ', np.mean(dA))
print('dA_prev[1,1] = ', dA_prev[1,1])
print()
dA_prev = pool_backward(dA, cache, mode = "average")
print("mode = average")
print('mean of dA = ', np.mean(dA))
print('dA_prev[1,1] = ', dA_prev[1,1])

预期输出:
mode = max
mean of dA = 0.14571390272918056
dA_prev[1,1] = [[ 0. 0. ]
[ 5.05844394 -1.68282702]
[ 0. 0. ]]

mode = average
mean of dA = 0.14571390272918056
dA_prev[1,1] = [[ 0.08485462 0.2787552 ]
[ 1.26461098 -0.25749373]
[ 1.17975636 -0.53624893]]

恭喜!

祝贺你完成此作业。你现在了解了卷积神经网络如何工作,实现了构建神经网络所需的所有模块。

作业7 卷积神经网络的应用

卷积神经网络的应用

欢迎来到课程4的第二项作业!你将:

  • 实现模型构建所需的辅助函数
  • 使用TensorFlow实现功能全面的ConvNet

完成此作业后,你将能够:

  • 用TensorFlow构建和训练ConvNet解决分类问题

我们在这里假设你已经熟悉TensorFlow。如果不是,请先学习课程2第三周的TensorFlow教程(“改善深度神经网络”)。

TensorFlow模型

在上一项作业中,你使用numpy构建了辅助函数,以了解卷积神经网络背后的机制。实际上现在大多数深度学习的应用都是使用编程框架构建的,框架具有许多内置函数,你可以轻松地调用它们。

和之前一样,我们将从加载包开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
import math
import numpy as np
import h5py
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import tensorflow as tf
from tensorflow.python.framework import ops

import cnn_utils

%matplotlib inline
np.random.seed(1)

运行以下单元格以加载要使用的“SIGNS”数据集。

1
2
# Loading the data (signs)
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset()

SIGNS数据集是6个手势符号的图片集,这些符号表示从0到5的数字。

以下单元格将显示数据集中标记图像的示例。随时更改index的值,然后重新运行以查看不同的示例。

1
2
3
4
# Example of a picture
index = 6
plt.imshow(X_train_orig[index])
print ("y = " + str(np.squeeze(Y_train_orig[:, index])))

y = 2

在课程2中,你已经为此数据集构建了一个全连接的网络。但是由于这是图像数据集,因此应用ConvNet将更自然。

首先,让我们检查数据的维度。

1
2
3
4
5
6
7
8
9
10
11
X_train = X_train_orig/255.
X_test = X_test_orig/255.
Y_train = convert_to_one_hot(Y_train_orig, 6).T
Y_test = convert_to_one_hot(Y_test_orig, 6).T
print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print ("Y_train shape: " + str(Y_train.shape))
print ("X_test shape: " + str(X_test.shape))
print ("Y_test shape: " + str(Y_test.shape))
conv_layers = {}

预期输出
number of training examples = 1080
number of test examples = 120
X_train shape: (1080, 64, 64, 3)
Y_train shape: (1080, 6)
X_test shape: (120, 64, 64, 3)
Y_test shape: (120, 6)

创建占位符

TensorFlow需要为运行会话时输入的数据创建占位符。

练习:实现以下函数为输入图像X和输出Y创建占位符。暂时不用定义训练数据的数量。为此,你可以使用 “None” 作为批次大小,稍后灵活地选择它。因此,X的维度应为 [None, n_H0, n_W0, n_C0],Y的尺寸应为 [None, n_y]提示

编写函数create_placeholders(n_H0,n_W0,n_C0,n_y)为输入图像X和输出Y创建占位符
要求

  • 为session创建占位符

  • 参数:

    • n_H0 - 实数,输入图像的高度
    • n_W0 - 实数,输入图像的宽度
    • n_C0 - 实数,输入的通道数
    • n_y - 实数,分类数
  • 返回值:

    • X - 输入数据的占位符,维度为[None, n_H0, n_W0, n_C0],类型为”float”
    • Y - 输入数据的标签的占位符,维度为[None, n_y],维度为”float”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def create_placeholders(n_H0, n_W0, n_C0, n_y):
"""
为session创建占位符

参数:
n_H0 - 实数,输入图像的高度
n_W0 - 实数,输入图像的宽度
n_C0 - 实数,输入的通道数
n_y - 实数,分类数

输出:
X - 输入数据的占位符,维度为[None, n_H0, n_W0, n_C0],类型为"float"
Y - 输入数据的标签的占位符,维度为[None, n_y],维度为"float"
"""
X = tf.placeholder(tf.float32,[None, n_H0, n_W0, n_C0])
Y = tf.placeholder(tf.float32,[None, n_y])

return X,Y

测试函数create_placeholders(n_H0,n_W0,n_C0,n_y)

1
2
3
4
5
X , Y = create_placeholders(64,64,3,6)
print ("X = " + str(X))
print ("Y = " + str(Y))


预期输出
X = Tensor(“Placeholder:0”, shape=(?, 64, 64, 3), dtype=float32)
Y = Tensor(“Placeholder_1:0”, shape=(?, 6), dtype=float32)

初始化参数

你将使用tf.contrib.layers.xavier_initializer(seed = 0)初始化权重/滤波器$W1$和$W2$。你无需担心偏差变量,因为TensorFlow函数可以处理偏差。还要注意你只会为conv2d函数初始化权重/滤波器,TensorFlow将自动初始化全连接部分的层。在本作业的后面,我们将详细讨论。

练习:实现initialize_parameters(),下面提供了每组过滤器的尺寸。
提示:在Tensorflow中初始化维度为[1,2,3,4]的参数$W$,使用:

1
W = tf.get_variable("W", [1,2,3,4], initializer = ...)

More Info

编写函数initialize_parameters()初始化权值矩阵
要求

  • 初始化权值矩阵,这里我们把权值矩阵硬编码:
  • 参数

    • W1 : [4, 4, 3, 8]
    • W2 : [2, 2, 8, 16]
  • 返回值:

    • 包含了tensor类型的W1、W2的字典
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def initialize_parameters():
"""
初始化权值矩阵,这里我们把权值矩阵硬编码:
W1 : [4, 4, 3, 8]
W2 : [2, 2, 8, 16]

返回:
包含了tensor类型的W1、W2的字典
"""
tf.set_random_seed(1)

W1 = tf.get_variable("W1",[4,4,3,8],initializer=tf.contrib.layers.xavier_initializer(seed=0))
W2 = tf.get_variable("W2",[2,2,8,16],initializer=tf.contrib.layers.xavier_initializer(seed=0))

parameters = {"W1": W1,
"W2": W2}

return parameters

运行代码测试函数initialize_parameters()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 重置默认的 TensorFlow 图
tf.reset_default_graph()
# 使用 with 语句可以确保会话在使用完后自动关闭,释放资源
# 创建一个 TensorFlow 会话
with tf.Session() as sess_test:
# 初始化网络的参数
parameters = initialize_parameters()
# 创建一个操作,用于初始化 TensorFlow 图中定义的所有全局变量
init = tf.global_variables_initializer()
sess_test.run(init)
# 获取张量中的特定索引的值
print("W1 = " + str(parameters["W1"].eval()[1,1,1]))
print("W2 = " + str(parameters["W2"].eval()[1,1,1]))
# 显式关闭会话,释放资源
sess_test.close()

预期输出:
W1 = [ 0.00131723 0.1417614 -0.04434952 0.09197326 0.14984085 -0.03514394
-0.06847463 0.05245192]
W2 = [-0.08566415 0.17750949 0.11974221 0.16773748 -0.0830943 -0.08058
-0.00577033 -0.14643836 0.24162132 -0.05857408 -0.19055021 0.1345228
-0.22779644 -0.1601823 -0.16117483 -0.10286498]

正向传播

在TensorFlow中,有内置函数为你执行卷积步骤。

  • tf.nn.conv2d(X,W1, strides = [1,s,s,1], padding = ‘SAME’): 给定输入$X$和一组滤波器$W1$,函数将使用$W1$的滤波器卷积X。第三个输入([1,s,s,1])表示对于输入的每个维度(m, n_H_prev, n_W_prev, n_C_prev)而言,每次滑动的步幅。你可以在here阅读完整的文档。

  • tf.nn.max_pool(A, ksize = [1,f,f,1], strides = [1,s,s,1], padding = ‘SAME’): 给定输入A,此函数使用大小为(f,f)的窗口和大小为(s,s)的步幅在每个窗口上进行最大池化。你可以在 here阅读完整的文档。

  • tf.nn.relu(Z1): 计算Z1的ReLU激活输出(可以是任何形状)。你可以在 here阅读完整的文档。

  • tf.contrib.layers.flatten(P): 给定输入P,此函数将每个示例展平为一维向量,同时保持批量大小。它返回维度为[batch_size,k]的展平张量。你可以在 here阅读完整的文档。

  • tf.contrib.layers.fully_connected(F, num_outputs): 给定展平的输入F,它将返回用全连接层计算出的输出。你可以在 here阅读完整的文档。

在上面的最后一个函数(tf.contrib.layers.fully_connected)中,全连接层会自动初始化图中的权重,并在训练模型时继续对其进行训练。因此,初始化参数时无需初始化这些权重。

练习

实现下面的forward_propagation函数以构建以下模型:CONV2D-> RELU-> MAXPOOL-> CONV2D-> RELU-> MAXPOOL-> FLATTEN-> FULLYCONNECTED。使用上面那些函数。

编写函数forward_propagation(X,parameters)实现前向传播

要求

  • 具体地,我们将在所有步骤中使用以下参数:
    • Conv2D:步幅为1,填充为“SAME”
    • ReLU
    • Max pool:使用8x8的滤波器和8x8的步幅,填充为“SAME”
    • Conv2D:步幅为1,填充为“SAME”
    • ReLU
    • Max pool:使用4x4的滤波器和4x4的步幅,填充为“SAME”
    • 展平之前的输出。
    • FULLYCONNECTED(FC)层:应用不含非线性激活函数的全连接层。输出6个特征 请勿在此处调用softmax。这将在输出层中产生6个神经元,然后将其传递给softmax。在TensorFlow中,softmax和cost函数被合并为一个函数,在计算损失时将调用另一个函数。
  • 实现前向传播
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

  • 参数:

    • X - 输入数据的placeholder,维度为(输入节点数量,样本数量)
    • parameters - 包含了“W1”和“W2”的python字典。
  • 返回:

    • Z3 - 最后一个LINEAR节点的输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def forward_propagation(X,parameters):
"""
实现前向传播
CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

参数:
X - 输入数据的placeholder,维度为(输入节点数量,样本数量)
parameters - 包含了“W1”和“W2”的python字典。

返回:
Z3 - 最后一个LINEAR节点的输出

"""
W1 = parameters['W1']
W2 = parameters['W2']

#Conv2d : 步伐:1,填充方式:“SAME”
Z1 = tf.nn.conv2d(X,W1,strides=[1,1,1,1],padding="SAME")
#ReLU :
A1 = tf.nn.relu(Z1)
#Max pool : 窗口大小:8x8,步伐:8x8,填充方式:“SAME”
P1 = tf.nn.max_pool(A1,ksize=[1,8,8,1],strides=[1,8,8,1],padding="SAME")

#Conv2d : 步伐:1,填充方式:“SAME”
Z2 = tf.nn.conv2d(P1,W2,strides=[1,1,1,1],padding="SAME")
#ReLU :
A2 = tf.nn.relu(Z2)
#Max pool : 过滤器大小:4x4,步伐:4x4,填充方式:“SAME”
P2 = tf.nn.max_pool(A2,ksize=[1,4,4,1],strides=[1,4,4,1],padding="SAME")

#一维化上一层的输出
P = tf.contrib.layers.flatten(P2)

#全连接层(FC):使用没有非线性激活函数的全连接层,输出6个特征
Z3 = tf.contrib.layers.fully_connected(P,6,activation_fn=None)

return Z3


测试函数forward_propagation(X,parameters)

提示
a = sess_test.run(Z3,{X: np.random.randn(2,64,64,3), Y: np.random.randn(2,6)})
生成随机输入数据,形状为 (2, 64, 64, 3),表示 2 个 64x64 的 RGB 图像;生成随机目标数据,形状为 (2, 6)。然后运行 Z3 的计算,得到输出值并存储在 a 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 重置默认计算图,清空之前的定义,确保环境干净
tf.reset_default_graph()

# 设置随机种子,以确保结果可重复
np.random.seed(1)

# 创建一个 TensorFlow 会话
with tf.Session() as sess_test:

# 创建占位符 X 和 Y,分别用于输入数据和目标数据
# 这里 X 的形状为 (64, 64, 3),表示输入为 64x64 的 RGB 图像;Y 的形状为 (6),表示有 6 个目标输出
X, Y = create_placeholders(64, 64, 3, 6)

# 初始化网络参数,返回一个包含权重和偏置的字典
parameters = initialize_parameters()

# 定义前向传播函数,计算输出 Z3
# 这里的 `forward_propagation` 定义了如何通过参数计算输出
Z3 = forward_propagation(X, parameters)

# 创建一个操作以初始化全局变量(即模型的参数)
init = tf.global_variables_initializer()

# 运行初始化操作
sess_test.run(init)

# 生成随机输入数据,输入形状为 (2, 64, 64, 3),目标数据形状为 (2, 6)
# 然后运行 Z3 的计算,得到输出值并存储在 a 中
a = sess_test.run(Z3, {X: np.random.randn(2, 64, 64, 3), Y: np.random.randn(2, 6)})

# 打印输出 Z3 的值
print("Z3 = " + str(a))

# 会话结束,自动关闭
sess_test.close()

预期输出:
Z3 = [[ 1.4416984 -0.24909666 5.450499 -0.2618962 -0.20669907 1.3654671 ]
[ 1.4070846 -0.02573211 5.08928 -0.48669922 -0.40940708 1.2624859 ]]

计算损失

在下面实现损失函数的计算,你可能会发现以下两个函数很有帮助:

  • tf.nn.softmax_cross_entropy_with_logits(logits = Z3, labels = Y): 计算softmax熵损失,该函数会计算softmax激活函数以及由此产生的损失。你可以在 here查看完整的文档。
  • tf.reduce_mean: 计算张量各维度上元素的均值,用它来对所有训练示例的损失求和,以获得总损失,你可以在here查看完整的文档。

练习:使用上面的函数计算下述损失。

编写函数compute_cost(Z3,Y)计算成本
要求

  • 计算成本
  • 参数:

    • Z3 - 正向传播最后一个LINEAR节点的输出,维度为(6,样本数)。
    • Y - 标签向量的placeholder,和Z3的维度相同
  • 返回:

    • cost - 计算后的成本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def compute_cost(Z3,Y):
"""
计算成本
参数:
Z3 - 正向传播最后一个LINEAR节点的输出,维度为(6,样本数)。
Y - 标签向量的placeholder,和Z3的维度相同

返回:
cost - 计算后的成本

"""

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=Z3,labels=Y))

return cost

运行代码测试函数compute_cost(Z3,Y)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tf.reset_default_graph()

with tf.Session() as sess_test:
np.random.seed(1)
X,Y = create_placeholders(64,64,3,6)
parameters = initialize_parameters()
Z3 = forward_propagation(X,parameters)
cost = compute_cost(Z3,Y)

init = tf.global_variables_initializer()
sess_test.run(init)
a = sess_test.run(cost,{X: np.random.randn(4,64,64,3), Y: np.random.randn(4,6)})
print("cost = " + str(a))

sess_test.close()


预期输出:
cost =2.91034

构建模型

最后,你将合并以上实现的辅助函数以构建模型并在SIGNS数据集上对其进行训练。

你已经在课程2的“优化算法”编程作业中实现了random_mini_batches(),记住此函数返回的是一个小批次的处理列表。

练习:完成以下函数:

以下模型应:

  • 创建占位符
  • 初始化参数
  • 正向传播
  • 计算损失
  • 创建优化函数

最后,你将创建一个会话并为num_epochs运行一个for循环,获取小批次处理,然后针对每个小批次运行优化函数。
Hint for initializing the variables

编写模型函数model(X_train,Y_train,X_test, Y_test, learning_rate=0.009, num_epochs=100,minibatch_size=64,print_cost=True,isPlot=True)实现三层的卷积神经网络

要求

  • 使用TensorFlow实现三层的卷积神经网络
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

  • 参数:

    • X_train - 训练数据,维度为(None, 64, 64, 3)
    • Y_train - 训练数据对应的标签,维度为(None, n_y = 6)
    • X_test - 测试数据,维度为(None, 64, 64, 3)
    • Y_test - 训练数据对应的标签,维度为(None, n_y = 6)
    • learning_rate - 学习率
    • num_epochs - 遍历整个数据集的次数
    • minibatch_size - 每个小批量数据块的大小
    • print_cost - 是否打印成本值,每遍历100次整个数据集打印一次
    • isPlot - 是否绘制图谱
  • 返回:

    • train_accuracy - 实数,训练集的准确度
    • test_accuracy - 实数,测试集的准确度
    • parameters - 学习后的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
def model(X_train, Y_train, X_test, Y_test, learning_rate=0.009, 
num_epochs=100,minibatch_size=64,print_cost=True,isPlot=True):
"""
使用TensorFlow实现三层的卷积神经网络
CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED

参数:
X_train - 训练数据,维度为(None, 64, 64, 3)
Y_train - 训练数据对应的标签,维度为(None, n_y = 6)
X_test - 测试数据,维度为(None, 64, 64, 3)
Y_test - 训练数据对应的标签,维度为(None, n_y = 6)
learning_rate - 学习率
num_epochs - 遍历整个数据集的次数
minibatch_size - 每个小批量数据块的大小
print_cost - 是否打印成本值,每遍历100次整个数据集打印一次
isPlot - 是否绘制图谱

返回:
train_accuracy - 实数,训练集的准确度
test_accuracy - 实数,测试集的准确度
parameters - 学习后的参数
"""
ops.reset_default_graph() #能够重新运行模型而不覆盖tf变量
tf.set_random_seed(1) #确保你的数据和我一样
seed = 3 #指定numpy的随机种子
(m , n_H0, n_W0, n_C0) = X_train.shape
n_y = Y_train.shape[1]
costs = []

#为当前维度创建占位符
X , Y = create_placeholders(n_H0, n_W0, n_C0, n_y)

#初始化参数
parameters = initialize_parameters()

#前向传播
Z3 = forward_propagation(X,parameters)

#计算成本
cost = compute_cost(Z3,Y)

#反向传播,由于框架已经实现了反向传播,我们只需要选择一个优化器就行了
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

#全局初始化所有变量
init = tf.global_variables_initializer()

#开始运行
with tf.Session() as sess:
#初始化参数
sess.run(init)
#开始遍历数据集
for epoch in range(num_epochs):
minibatch_cost = 0
num_minibatches = int(m / minibatch_size) #获取数据块的数量
seed = seed + 1
minibatches = cnn_utils.random_mini_batches(X_train,Y_train,minibatch_size,seed)

#对每个数据块进行处理
for minibatch in minibatches:
#选择一个数据块
(minibatch_X,minibatch_Y) = minibatch
#最小化这个数据块的成本
_ , temp_cost = sess.run([optimizer,cost],feed_dict={X:minibatch_X, Y:minibatch_Y})

#累加数据块的成本值
minibatch_cost += temp_cost / num_minibatches

#是否打印成本
if print_cost:
#每5代打印一次
if epoch % 5 == 0:
print("当前是第 " + str(epoch) + " 代,成本值为:" + str(minibatch_cost))

#记录成本
if epoch % 1 == 0:
costs.append(minibatch_cost)

#数据处理完毕,绘制成本曲线
if isPlot:
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()

#开始预测数据
## 计算当前的预测情况
"""
返回 Z3 中每一行的最大值索引,这代表模型对每个样本的预测类别。这里的 1 表示沿着列的方向(每一行)寻找最大值
"""
predict_op = tf.arg_max(Z3,1)
## 将模型的预测类别与真实类别进行比较,返回一个布尔值张量,其中正确预测的值为 True,错误的值为 False
corrent_prediction = tf.equal(predict_op , tf.arg_max(Y,1))

##计算准确度
###将布尔值(True/False)转换为浮点数(1.0/0.0)计算所有预测中正确预测的平均值
accuracy = tf.reduce_mean(tf.cast(corrent_prediction,"float"))
print("corrent_prediction accuracy= " + str(accuracy))
# 传入字典调用accuracy对应的函数操作
train_accuracy = accuracy.eval({X: X_train, Y: Y_train})
test_accuary = accuracy.eval({X: X_test, Y: Y_test})

print("训练集准确度:" + str(train_accuracy))
print("测试集准确度:" + str(test_accuary))

return (train_accuracy,test_accuary,parameters)

运行以下单元格以训练模型

1
2
_, _, parameters = model(X_train, Y_train, X_test, Y_test,num_epochs=150)

预期输出:
当前是第 0 代,成本值为:1.91791956872
当前是第 5 代,成本值为:1.53247521073
当前是第 10 代,成本值为:1.01480386034
当前是第 15 代,成本值为:0.885136671364
当前是第 20 代,成本值为:0.766963448375
当前是第 25 代,成本值为:0.651207884774
当前是第 30 代,成本值为:0.613355720416
当前是第 35 代,成本值为:0.605931192636
当前是第 40 代,成本值为:0.53471290879
当前是第 45 代,成本值为:0.551402201876
当前是第 50 代,成本值为:0.496976461262
当前是第 55 代,成本值为:0.454438356683
当前是第 60 代,成本值为:0.455495661125
当前是第 65 代,成本值为:0.458359180018
当前是第 70 代,成本值为:0.450039617717
当前是第 75 代,成本值为:0.41068668291
当前是第 80 代,成本值为:0.469005130231
当前是第 85 代,成本值为:0.389252956957
当前是第 90 代,成本值为:0.363807530142
当前是第 95 代,成本值为:0.376132288016
当前是第 100 代,成本值为:0.375231474638
当前是第 105 代,成本值为:0.349760836922
当前是第 110 代,成本值为:0.366967062466
当前是第 115 代,成本值为:0.325298860669
当前是第 120 代,成本值为:0.327003779821
当前是第 125 代,成本值为:0.374339781702
当前是第 130 代,成本值为:0.350489996374
当前是第 135 代,成本值为:0.318831278477
当前是第 140 代,成本值为:0.269049352966
当前是第 145 代,成本值为:0.342204230838
corrent_prediction accuracy= Tensor(“Mean_1:0”, shape=(), dtype=float32)
训练集准确度:0.908333
测试集准确度:0.791667

Nice!你已经完成了作业并建立了一个模型,该模型可以在测试集上以几乎80%的精度识别SIGN手势,如果你愿意,可以随时使用此数据集。实际上,你可以通过花费更多时间调整超参数或使用正则化来提高其准确性(因为该模型显然具有很高的方差)。

相关代码
cnn_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#cnn_utils.py

import math
import numpy as np
import h5py
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.python.framework import ops
def load_dataset():
train_dataset = h5py.File('datasets/train_signs.h5', "r")
train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features
train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels
test_dataset = h5py.File('datasets/test_signs.h5', "r")
test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels
classes = np.array(test_dataset["list_classes"][:]) # the list of classes
train_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0]))
test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0]))
return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes
def random_mini_batches(X, Y, mini_batch_size = 64, seed = 0):
"""
Creates a list of random minibatches from (X, Y)
Arguments:
X -- input data, of shape (input size, number of examples) (m, Hi, Wi, Ci)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat), of shape (1, number of examples) (m, n_y)
mini_batch_size - size of the mini-batches, integer
seed -- this is only for the purpose of grading, so that you're "random minibatches are the same as ours.
Returns:
mini_batches -- list of synchronous (mini_batch_X, mini_batch_Y)
"""
m = X.shape[0] # number of training examples
mini_batches = []
np.random.seed(seed)
# Step 1: Shuffle (X, Y)
permutation = list(np.random.permutation(m))
shuffled_X = X[permutation,:,:,:]
shuffled_Y = Y[permutation,:]
# Step 2: Partition (shuffled_X, shuffled_Y). Minus the end case.
num_complete_minibatches = math.floor(m/mini_batch_size) # number of mini batches of size mini_batch_size in your partitionning
for k in range(0, num_complete_minibatches):
mini_batch_X = shuffled_X[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:,:,:]
mini_batch_Y = shuffled_Y[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:]
mini_batch = (mini_batch_X, mini_batch_Y)
mini_batches.append(mini_batch)
# Handling the end case (last mini-batch &lt; mini_batch_size)
if m % mini_batch_size != 0:
mini_batch_X = shuffled_X[num_complete_minibatches * mini_batch_size : m,:,:,:]
mini_batch_Y = shuffled_Y[num_complete_minibatches * mini_batch_size : m,:]
mini_batch = (mini_batch_X, mini_batch_Y)
mini_batches.append(mini_batch)
return mini_batches
def convert_to_one_hot(Y, C):
Y = np.eye(C)[Y.reshape(-1)].T
return Y
def forward_propagation_for_predict(X, parameters):
"""
Implements the forward propagation for the model: LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SOFTMAX
Arguments:
X -- input dataset placeholder, of shape (input size, number of examples)
parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3"
the shapes are given in initialize_parameters
Returns:
Z3 -- the output of the last LINEAR unit
"""
# Retrieve the parameters from the dictionary "parameters"
W1 = parameters['W1']
b1 = parameters['b1']
W2 = parameters['W2']
b2 = parameters['b2']
W3 = parameters['W3']
b3 = parameters['b3']
# Numpy Equivalents:
Z1 = tf.add(tf.matmul(W1, X), b1) # Z1 = np.dot(W1, X) + b1
A1 = tf.nn.relu(Z1) # A1 = relu(Z1)
Z2 = tf.add(tf.matmul(W2, A1), b2) # Z2 = np.dot(W2, a1) + b2
A2 = tf.nn.relu(Z2) # A2 = relu(Z2)
Z3 = tf.add(tf.matmul(W3, A2), b3) # Z3 = np.dot(W3,Z2) + b3
return Z3
'''
def predict(X, parameters):
W1 = tf.convert_to_tensor(parameters["W1"])
b1 = tf.convert_to_tensor(parameters["b1"])
W2 = tf.convert_to_tensor(parameters["W2"])
b2 = tf.convert_to_tensor(parameters["b2"])
W3 = tf.convert_to_tensor(parameters["W3"])
b3 = tf.convert_to_tensor(parameters["b3"])
params = {"W1": W1,
"b1": b1,
"W2": W2,
"b2": b2,
"W3": W3,
"b3": b3}
x = tf.placeholder("float", [12288, 1])
z3 = forward_propagation_for_predict(x, params)
p = tf.argmax(z3)
sess = tf.Session()
prediction = sess.run(p, feed_dict = {x: X})
return prediction
'''

作业8 Keras入门

Keras教程

欢迎来到第2周的作业1。在此作业中,你将:

  1. 学习使用Keras,这是一种高级神经网络API(编程框架),采用Python编写,并且能够在包括TensorFlow和CNTK在内的几个较低级框架之上运行。
  2. 了解如何在几个小时内构建深度学习算法。

我们为什么要使用Keras?开发Keras的目的是使深度学习工程师能够快速构建和试验不同的模型。正如TensorFlow是一个比Python更高级的框架一样,Keras是一个甚至更高层次的框架,能够以最小的延迟将想法付诸实践是找到良好模型的关键。但是,Keras比低级框架更具限制性,因此可以在TensorFlow中实现一些非常复杂的模型,而在Keras中实现这些模型较为困难。话虽如此,Keras仍可以在许多常见模型上正常工作。

在本练习中,你将解决“the Happy House”问题,我们将在下面进行解释。首先让我们加载所需的软件包并开始解决问题吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
from keras import layers
from keras.layers import Input, Dense, Activation, ZeroPadding2D, BatchNormalization, Flatten, Conv2D
from keras.layers import AveragePooling2D, MaxPooling2D, Dropout, GlobalMaxPooling2D, GlobalAveragePooling2D
from keras.models import Model
from keras.preprocessing import image
from keras.utils import layer_utils
from keras.utils.data_utils import get_file
from keras.applications.imagenet_utils import preprocess_input
import pydot
from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot
from keras.utils import plot_model
import kt_utils

import keras.backend as K
K.set_image_data_format('channels_last')
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow

%matplotlib inline

注意:如你所见,我们从Keras导入了许多函数。 你可以在笔记本中直接调用它们;
例如:X = Input(...)X = ZeroPadding2D(...)

The Happy House 挑战

你决定与五个朋友在下个假期一起度过一星期。你们来到一个非常方便的房子,附近有很多事情可以做。但是最重要的好处是,每个人在家里时都承诺要快乐。因此,任何想要进入房屋的人都必须证明自己目前的幸福状态。
作为一名深度学习专家,要确保严格执行“Happy”规则,你将要构建一种算法,该算法使用前门摄像头中的图片来检查该人是否快乐。仅当该人感到高兴时,门才应打开。

你已经通过前门摄像头收集了你的朋友和你自己的照片。数据集是附带标签的。

运行以下代码以标准化数据集并查看其维度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = kt_utils load_dataset()

# Normalize image vectors
X_train = X_train_orig/255.
X_test = X_test_orig/255.

# Reshape
Y_train = Y_train_orig.T
Y_test = Y_test_orig.T

print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print ("Y_train shape: " + str(Y_train.shape))
print ("X_test shape: " + str(X_test.shape))
print ("Y_test shape: " + str(Y_test.shape))

预期输出:
number of training examples = 600
number of test examples = 150
X_train shape: (600, 64, 64, 3)
Y_train shape: (600, 1)
X_test shape: (150, 64, 64, 3)
Y_test shape: (150, 1)

数据集的详细信息

  • 图像维度(64、64、3)
  • 训练:600张图片
  • 测试:150张图片

现在该解决“Happy”挑战了。

在Keras中建立模型

Keras非常适合快速制作原型,你可以在很短的时间内,建立一个能够获得出色结果的模型。

这是Keras中的模型构建示例:

提示

  • Input(input_shape)可用来创建定义一个tensor的placeholder(占位符),维度为input_shape
  • ZeroPadding2D((3,3))(X_input)在输入的边缘添加零填充。(3,3)表示在高度和宽度的每一边都填充3个像素的零
  • Conv2D(32, (7, 7), strides = (1, 1), name = 'conv0')(X)这个层使用32个7x7的卷积核,步幅设置为(1,1)。卷积核的数量(32)决定了输出特征图的深度
  • BatchNormalization(axis = 3, name = 'bn0')(X)设置axis=3表示对每个通道独立进行归一化
  • Activation('relu')(X)应用ReLU(Rectified Linear Unit)激活函数
  • MaxPooling2D((2,2),name="max_pool")(X)最大池化操作,使用2x2的池化窗口。这个操作将特征图的尺寸减半,保留最重要的特征,减少计算量,同时防止过拟合。
  • Flatten()(X)层将多维的特征图展平为一维向量,为全连接层(Dense层)做准备
  • Dense(1, activation='sigmoid', name='fc')创建了一个全连接层,输出一个单一的数值0到1之间
  • Model类用于创建Keras模型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    def model(input_shape):
    """
    模型大纲
    """
    #定义一个tensor的placeholder,维度为input_shape
    X_input = Input(input_shape)

    #使用0填充:X_input的周围填充0
    X = ZeroPadding2D((3,3))(X_input)

    # 对X使用 CONV -> BN -> RELU 块
    X = Conv2D(32, (7, 7), strides = (1, 1), name = 'conv0')(X)
    X = BatchNormalization(axis = 3, name = 'bn0')(X)
    X = Activation('relu')(X)

    #最大值池化层
    X = MaxPooling2D((2,2),name="max_pool")(X)

    #降维,矩阵转化为向量 + 全连接层
    X = Flatten()(X)
    X = Dense(1, activation='sigmoid', name='fc')(X)

    #创建模型,讲话创建一个模型的实体,我们可以用它来训练、测试。
    model = Model(inputs = X_input, outputs = X, name='HappyModel')

    return model


请注意,Keras使用变量名与我们之前使用numpy和TensorFlow不同。不是在正向传播的每个步骤上创建和分配新变量,例如X, Z1, A1, Z2, A2等,以用于不同层的计算, Keras代码上面的每一行只是使用X = ...X重新分配给新值。换句话说,在正向传播的每个步骤中,我们只是将计算中的最新值写入相同的变量X。唯一的例外是X_input,我们将其分开并没有覆盖,因为我们最终需要它来创建Keras模型实例(上面的model = Model(inputs = X_input, ...))。

练习:实现一个HappyModel()。我们建议你首先使用我们建议的结构来实现模型,然后再使用该模型作为初始模型来完成本任务的其余部分。之后请返回并主动尝试其他模型架构。例如,你可能会从上面的模型中获得启发,但是随后根据需要更改网络体系结构和超参数。你还可以使用其他函数,例如AveragePooling2D(), GlobalMaxPooling2D(), Dropout()

注意:注意数据的维度。利用你在视频中学到的知识,确保卷积,池化和全连接层适用。

编写函数HappyModel(input_shape)实现一个检测笑容的模型

要求

  • 实现一个检测笑容的模型

  • 参数:

    • input_shape - 输入的数据的维度
  • 返回:
    • model - 创建的Keras的模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def HappyModel(input_shape):
"""
实现一个检测笑容的模型

参数:
input_shape - 输入的数据的维度
返回:
model - 创建的Keras的模型

"""

#你可以参考和上面的大纲
X_input = Input(input_shape)

#使用0填充:X_input的周围填充0
X = ZeroPadding2D((3, 3))(X_input)

#对X使用 CONV -> BN -> RELU 块
X = Conv2D(32, (7, 7), strides=(1, 1), name='conv0')(X)
X = BatchNormalization(axis=3, name='bn0')(X)
X = Activation('relu')(X)

#最大值池化层
X = MaxPooling2D((2, 2), name='max_pool')(X)

#降维,矩阵转化为向量 + 全连接层
X = Flatten()(X)
X = Dense(1, activation='sigmoid', name='fc')(X)

#创建模型,讲话创建一个模型的实体,我们可以用它来训练、测试。
model = Model(inputs=X_input, outputs=X, name='HappyModel')

return model

现在,你已经构建了一个描述模型的函数。为了训练和测试该模型,Keras中有四个步骤:

  1. 通过调用上面的函数创建模型
  2. 通过调用model.compile(optimizer = "...", loss = "...", metrics = ["accuracy"])编译模型
    • 参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    happy_model.compile(optimizer, loss, metrics=None, loss_weights=None, sample_weight_mode=None, weighted_metrics=None, target_tensors=None)

optimizer:
定义: 这是用于优化模型参数的算法。
类型: 可以是字符串或优化器实例。
可选的字符串:
"sgd": 随机梯度下降 (Stochastic Gradient Descent)。
"adam": Adam 优化器,结合了动量和自适应学习率调整。
"rmsprop": RMSprop 优化器,特别适合处理非平稳目标。
"adagrad": 自适应梯度算法,适用于稀疏数据。
你也可以使用 Keras 中的优化器类,如 keras.optimizers.Adam(learning_rate=0.001)。
loss:

定义: 这是用于衡量模型预测与真实标签之间差异的函数。
类型: 可以是字符串或损失函数实例。
可选的字符串:
"binary_crossentropy": 二元交叉熵,适用于二分类问题。
"categorical_crossentropy": 多分类交叉熵,适用于多分类问题,目标为独热编码形式。
"sparse_categorical_crossentropy": 稀疏多分类交叉熵,适用于多分类问题,目标为整数形式。
"mean_squared_error": 均方误差,适用于回归问题。
"mean_absolute_error": 平均绝对误差,适用于回归问题。
自定义损失函数的实例。
metrics:

定义: 这是用于评估模型性能的指标列表。
类型: 可以是字符串或指标实例的列表。
可选的字符串:
"accuracy": 准确率,适用于分类问题。
"mae": 平均绝对误差。
"mse": 均方误差。
"AUC": 曲线下面积,用于评估二分类模型。
可以使用 Keras 中的指标类,如 keras.metrics.AUC()。
例子: metrics=['accuracy', 'AUC']。
loss_weights:

定义: 用于指定不同输出的损失权重。
类型: 可以是浮点数或浮点数列表。
适用场景: 当模型有多个输出时,可以为每个输出分配不同的损失权重。
sample_weight_mode:

定义: 指定如何使用样本权重。
类型: 字符串或 None
可选值:
"temporal": 对于时间序列数据,可以将样本权重应用于每个时间步。
None: 不使用样本权重。
weighted_metrics:

定义: 用于指定加权的评估指标。
类型: 可以是指标实例的列表。
用途: 适用于存在样本权重的情况,评估指标也会根据样本权重进行计算。
target_tensors:

定义: 可以指定自定义的目标张量。
类型: Keras 张量。
用途: 适用于特定模型设计的需求。
  1. 通过调用model.fit(x = ..., y = ..., epochs = ..., batch_size = ...)训练模型
    • 参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
x:

定义: 输入数据,可以是 NumPy 数组、TensorFlow 张量或其他数据格式。
类型: ndarray 或 tensor。
示例: 在你的代码中,X_train 是作为 x 参数传递的。
y:

定义: 目标(标签)数据,与输入数据对应。
类型: ndarray 或 tensor。
示例: 在你的代码中,Y_train 是作为 y 参数传递的。
batch_size:

定义: 每次训练迭代使用的样本数量。
类型: 整数或 None(默认为 32)。
示例: 在你的代码中,batch_size=50 表示每个批次将使用 50 个样本进行训练。
epochs:

定义: 训练模型的完整轮数。每轮模型会遍历整个训练数据集。
类型: 整数。
示例: 在你的代码中,epochs=40 表示模型将训练 40 个轮次。
verbose:

定义: 控制训练过程的日志输出。
类型: 整数(0, 1, 2)。
可选值:
0: 不输出日志。
1: 输出进度条日志。
2: 输出每个epoch的日志。
示例: 默认值为 1
callbacks:

定义: 一系列回调函数,用于在训练过程中执行特定操作(如保存模型、调整学习率等)。
类型: 列表。
示例: 可以传入 ModelCheckpoint, EarlyStopping 等回调函数。
validation_split:

定义: 从训练数据中划分出一部分作为验证集。
类型: 浮点数,范围 [0.0, 1.0]。
示例: 如果设置为 0.2,则 20% 的训练数据将用于验证。
validation_data:

定义: 额外的验证数据,元组形式 (x_val, y_val)。
类型: 元组或 None
示例: 如果你有专门的验证集,可以传入,如 (X_val, Y_val)。
shuffle:

定义: 在每个epoch前是否打乱训练数据。
类型: 布尔值。
示例: 默认为 True,可以设置为 False
class_weight:

定义: 用于给不同类别分配权重,以处理类别不平衡。
类型: 字典。
示例: class_weight={0: 1., 1: 2.},表示类别 0 的权重为 1,类别 1 的权重为 2
sample_weight:

定义: 用于指定每个样本的权重。
类型: ndarray。
示例: 如果需要对某些样本加大权重,可以传入相应的权重数组。
initial_epoch:

定义: 从第几轮开始训练。
类型: 整数。
示例: 如果你在第 10 轮继续训练,可以设置为 initial_epoch=10
steps_per_epoch:

定义: 每个 epoch 的步数(批次数)。
类型: 整数或 None
示例: 如果使用生成器,可以设置为相应的步数。
validation_steps:

定义: 验证数据的步数。
类型: 整数或 None
示例: 与 steps_per_epoch 类似,通常在使用生成器时设置。
max_queue_size:

定义: 当使用生成器时,最大排队样本数。
类型: 整数。
示例: 默认为 10
workers:

定义: 训练时使用的并行进程数量。
类型: 整数。
示例: 默认为 1,增加此值可以提高数据加载速度。
use_multiprocessing:

定义: 是否使用多进程来加载数据。
类型: 布尔值。
示例: 默认为 False
  1. 通过调用model.evaluate(x = ..., y = ...)测试模型
    • 参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
x:

描述: 输入数据。可以是 NumPy 数组、TensorFlow 张量,或者是一个生成器(generator)。
类型: array-like 或者 tf.data.Dataset。
y:

描述: 输入数据对应的标签(目标值)。与输入数据 x 形状必须一致。
类型: array-like。
batch_size:

描述: 用于评估的批次大小。它决定了在评估过程中每次传递多少样本数据。
类型: int,默认值为 32
注意: 在使用生成器或数据集时,此参数将被忽略。
verbose:

描述: 控制评估过程的输出。
类型: int,可以取 012
0: 无输出。
1: 显示进度条。
2: 每个epoch结束后输出一行。
sample_weight:

描述: 用于加权评估的样本权重。如果某些样本对损失的贡献比其他样本更大,可以通过这个参数来调整。
类型: array-like,形状应与 y 一致。默认值为 None
steps:

描述: 在使用生成器或 tf.data.Dataset 时,指定要评估的步骤数量。如果未提供,系统将会自动计算步骤数量。
类型: int,默认值为 None
callbacks:

描述: 用于在评估过程中调用的回调函数列表。一般不用于评估,但可以用于记录等。
类型: list,默认值为 None
return_dict:

描述: 如果为 True,则返回一个字典,其中包含所有评估的指标。否则,将返回一个列表。
类型: bool,默认值为 False
返回值
evaluate 方法的返回值根据 return_dict 的设置有所不同:

如果 return_dict=False(默认值),将返回一个包含损失和其他指标值的列表。
如果 return_dict=True,将返回一个字典,字典的键为指标名称,值为对应的评估结果

如果你想进一步了解model.compile()model.fit()model.evaluate()及其参数,请参考官方 Keras documentation

下面跟着引导编写代码训练并测试模型

练习:第一步,创建模型

1
2
3
4
5

# 创建一个模型实体
happy_model = HappyModel(X_train.shape[1:])
# 如果 X_train 的形状是 (100, 32, 32, 3),那么 X_train.shape[1:] 将会返回 (32, 32, 3)

练习:实施第2步,编译模型以配置学习过程。 正确选择 compile()的3个参数。
提示:“快乐挑战”是一个二进制分类问题。

1
2
#编译模型
happy_model.compile("adam","binary_crossentropy", metrics=['accuracy'])

练习:实施第3步,训练模型。选择epoch和批次大小。

1
2
3
#训练模型
#请注意,此操作会花费你大约6-10分钟。
happy_model.fit(X_train, Y_train, epochs=40, batch_size=50)

请注意,如果再次运行fit()model将继续使用已经学习的参数进行训练,而不是重新初始化它们。

练习:实施第4步,测试/评估模型。

1
2
3
4
#评估模型
preds = happy_model.evaluate(X_test, Y_test, batch_size=32, verbose=1, sample_weight=None)
print ("误差值 = " + str(preds[0]))
print ("准确度 = " + str(preds[1]))

预期输出
Epoch 1/40
600/600 [==============================] - 12s 19ms/step - loss: 2.2593 - acc: 0.5667
Epoch 2/40
600/600 [==============================] - 9s 16ms/step - loss: 0.5355 - acc: 0.7917
Epoch 3/40
600/600 [==============================] - 10s 17ms/step - loss: 0.3252 - acc: 0.8650
Epoch 4/40
600/600 [==============================] - 10s 17ms/step - loss: 0.2038 - acc: 0.9250
Epoch 5/40
600/600 [==============================] - 10s 16ms/step - loss: 0.1664 - acc: 0.9333

Epoch 38/40
600/600 [==============================] - 10s 17ms/step - loss: 0.0173 - acc: 0.9950
Epoch 39/40
600/600 [==============================] - 14s 23ms/step - loss: 0.0365 - acc: 0.9883
Epoch 40/40
600/600 [==============================] - 12s 19ms/step - loss: 0.0291 - acc: 0.9900
150/150 [==============================] - 3s 21ms/step
误差值 = 0.407454126676
准确度 = 0.840000001589

如果你的happyModel()函数起作用,在训练和测试集上的测试结果应该比随机猜测(50%)更好。要通过此作业,你必须至少达到75%的准确性。

为了给你提供一个比较点,我们的模型在batch size为16个和adam优化器的情况下,在40个epoch内获得了95%的测试准确度(和99%的训练准确度)。但是我们的模型仅需2-5个epoch即可获得不错的准确性,因此,如果你要比较不同的模型,则还可以在几个epoch上训练各种模型,并观察比较它们。

如果你尚未达到75%的准确度,可以尝试以下方法来达到此目的:

  • 尝试使用CONV-> BATCHNORM-> RELU模块,例如:
1
2
3
4
X = Conv2D(32, (3, 3), strides = (1, 1), name = 'conv0')(X)
X = BatchNormalization(axis = 3, name = 'bn0')(X)
X = Activation('relu')(X)

  • 你可以在此模块之后使用MAXPOOL。这将帮助你降低高度和宽度尺寸。
  • 更改优化器。我们发现Adam行之有效。
  • 如果模型运行困难,并且遇到内存问题,请降低batch_size(例如12)
  • 运行更多epoch,直到看到训练精度达到稳定。

即使你已达到75%的准确性,也可以调整模型,尝试获得更好的结果。

注意:如果你调整模型超参数,则测试集实际上将成为开发集,并且你的模型最终可能会过拟合测试(开发)集。但仅出于此作业的目的,我们在此无需担心。

结论

Nice,你已经解决了快乐之家的挑战!

现在,你只需要将此模型连接到房屋的前门摄像头即可。

我们希望你从这项作业中记住什么:

  • Keras是我们建议用于快速制作模型的工具。它使你可以快速尝试不同的模型架构。你有想用Keras实现日常生活中任何深度学习的应用吗?
  • 记住如何在Keras中编码模型以及完成在测试集上评估模型的四个步骤。创建->编译->调整/训练->评估/测试。

使用你自己的图像进行测试(可选)

祝贺你完成此作业。现在,你可以拍张照片,看看是否可以进入快乐之家。要做到这一点:

训练/测试集非常相似。例如,所有图片都是在相同背景下拍摄的(因为前门摄像头始终安装在同一位置)。这使问题变得更容易,但是根据该数据训练的模型也可能无法在你自己的数据上工作。但是,可以尝试一下!

1
2
3
4
5
6
7
8
9
10
11
12
13
14

img_path = 'images/smile.jpeg'
# target_size=(64, 64) 参数指明在加载图像时要将其调整为 64x64 像素的大小
img = image.load_img(img_path, target_size=(64, 64))
imshow(img)
# 这行代码将图像 img 转换为数组形式,方便后续的处理。img_to_array 函数将图像数据转换为一个 NumPy 数组,数组的形状为 (高度, 宽度, 通道数),在这里将变为 (64, 64, 3)
x = image.img_to_array(img)
np.expand_dims(x, axis=0) 这一行代码在数组 x 的最前面添加一个新的维度,使其形状从 (64, 64, 3) 变为 (1, 64, 64, 3)。这一步是为了将图像数据调整为批处理格式
x = np.expand_dims(x, axis=0)
# preprocess_input(x) 是一个预处理函数,用于对输入的图像数组进行标准化处理,以便更好地适应模型的输入要求。这个函数通常会根据具体的模型(如 VGG16、ResNet 等)进行特定的像素值缩放、均值减去等处理,使输入数据在训练时能更有效地收敛。
x = preprocess_input(x)

print(happy_model.predict(x))

预期输出
[[ 1.]]

Keras中的其他有用函数(可选)

你会发现Keras中其他两个有用的基本功能是:

  • model.summary():以表格形式打印每层输入输出的详细信息
  • plot_model():绘制图形,如果你想在社交媒体上共享它,可以使用SVG()将其另存为“ .png”。

运行以下代码。

1
happyModel.summary()

预期输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 64, 64, 3) 0
_________________________________________________________________
zero_padding2d_1 (ZeroPaddin (None, 66, 66, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 64, 64, 8) 224
_________________________________________________________________
batch_normalization_1 (Batch (None, 64, 64, 8) 32
_________________________________________________________________
activation_1 (Activation) (None, 64, 64, 8) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 32, 32, 8) 0
_________________________________________________________________
zero_padding2d_2 (ZeroPaddin (None, 34, 34, 8) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 32, 32, 16) 1168
_________________________________________________________________
batch_normalization_2 (Batch (None, 32, 32, 16) 64
_________________________________________________________________
activation_2 (Activation) (None, 32, 32, 16) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 16, 16, 16) 0
_________________________________________________________________
zero_padding2d_3 (ZeroPaddin (None, 18, 18, 16) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 16, 16, 32) 4640
_________________________________________________________________
batch_normalization_3 (Batch (None, 16, 16, 32) 128
_________________________________________________________________
activation_3 (Activation) (None, 16, 16, 32) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 8, 8, 32) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 2048) 0
_________________________________________________________________
dense_1 (Dense) (None, 1) 2049
=================================================================
Total params: 8,305
Trainable params: 8,193
Non-trainable params: 112

1
2
3
4
5
6
7
8
%matplotlib inline
# 指定将绘制的模型结构保存为 PNG 格式的图像文件,文件名为 'happy_model.png'。这个文件将保存在当前工作目录中。
plot_model(happy_model, to_file='happy_model.png')
#model_to_dot 函数,用于将 Keras 模型转换为 Graphviz DOT 格式的对象。该格式是一种用于描述图形的文本格式,适用于绘制图表。
# .create(prog='dot', format='svg'):这部分调用了 create 方法,使用 prog='dot' 参数指定使用 Graphviz 的 dot 工具来生成图像。format='svg' 参数指定输出格式为 SVG(可缩放矢量图形),这是一种常见的图形文件格式,适合用于高质量图像和可缩放显示。
# SVG(...):这是一个函数,通常用于在 Jupyter Notebook 中直接显示 SVG 格式的图像。
SVG(model_to_dot(happy_model).create(prog='dot', format='svg'))

相关代码:
kt_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#kt_utils.py

import keras.backend as K
import math
import numpy as np
import h5py
import matplotlib.pyplot as plt


def mean_pred(y_true, y_pred):
return K.mean(y_pred)

def load_dataset():
train_dataset = h5py.File('datasets/train_happy.h5', "r")
train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features
train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels

test_dataset = h5py.File('datasets/test_happy.h5', "r")
test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels

classes = np.array(test_dataset["list_classes"][:]) # the list of classes

train_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0]))
test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0]))

return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes

作业8 残差网络的搭建

残差网络

欢迎来到本周的第二项作业!你将学习如何使用残差网络(ResNets)构建非常深的卷积网络。理论上讲,更深的网络可以表现更复杂的特征。但实际上,它们很难训练。He et al.引入的残差网络使你可以训练比以前实际可行的深层网络。

在此作业中,你将:

  • 实现ResNets的基本构建块。
  • 将这些模块放在一起,以实现和训练用于图像分类的最新神经网络。

这项作业将使用Keras完成。

在跳入问题之前,让我们运行下面的代码以加载所需的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import numpy as np
import tensorflow as tf

from keras import layers
from keras.layers import Input, Add, Dense, Activation, ZeroPadding2D, BatchNormalization, Flatten, Conv2D, AveragePooling2D, MaxPooling2D, GlobalMaxPooling2D
from keras.models import Model, load_model
from keras.preprocessing import image
from keras.utils import layer_utils
from keras.utils.data_utils import get_file
from keras.applications.imagenet_utils import preprocess_input
from keras.utils.vis_utils import model_to_dot
from keras.utils import plot_model
from keras.initializers import glorot_uniform

import pydot
from IPython.display import SVG
import scipy.misc
from matplotlib.pyplot import imshow
import keras.backend as K
K.set_image_data_format('channels_last')
K.set_learning_phase(1)

import resnets_utils

1 深层神经网络带来的问题

上周,你构建了第一个卷积神经网络。近年来,神经网络变得越来越深,网络已经从最初的几层(例如AlexNet)扩展到了一百多层。

深层网络的主要好处是可以表示非常复杂的特征。它还可以学习许多不同抽象级别的特征,从边缘(较低层)到非常复杂的特征(较深层)。但是,使用更深的网络并不总是好的。训练它们的一个巨大障碍是梯度的消失:非常深的网络通常具有迅速变为零的梯度信号,因此使梯度下降的速度令人难以忍受。更具体地说,在梯度下降过程中,当你从最后一层反向传播回第一层时,你需要在每一步上乘以权重矩阵,因此,梯度可以快速指数下降至零(或者在极少数情况下呈指数增长并“爆炸”为非常大的值)。

因此,在训练过程中,随着训练的进行,你可能会看到较早层的梯度的大小(或范数)非常快地减小到零:

图1 梯度消失

随着训练,网络学习的速度开始迅速下降
你现在将通过构建残差网络来解决此问题!

2 建立残差网络

在ResNets中,”shortcut” 或者 “skip connection”允许将梯度直接反向传播到较早的层:

图2:显示skip connection的残差块

左图显示了通过网络的“主要路径”。右图为主路径添加了shortcut。通过将这些ResNet块彼此堆叠,可以形成一个非常深的网络。

我们在教程中还看到,使用带有shortcut的ResNet块可以非常容易学习标识功能。这意味着你可以堆叠在其他ResNet块上,而几乎不会损害训练集性能。(还有一些证据表明,学习标识功能甚至比skip connections有助于解决梯度消失问题—也说明了ResNets的出色性能。)

ResNet中主要使用两种类型的块,这主要取决于输入/输出尺寸是相同还是不同。你将要在作业中实现两者。

2.1 The identity block恒等块

The identity block是ResNets中使用的标准块,它对应于输入激活(例如$a^{[l]}$)与输出激活(例如$a^{[l+2]}$)。为了充实ResNet标识块中发生的不同步骤,下面是显示各个步骤的替代图:

图3 标识块 skips over2层

上部路径是“shortcut path”。下部路径是“main path”。在此图中,我们还明确了每一层中的CONV2D和ReLU步骤。为了加快训练速度,我们还添加了BatchNorm步骤。不必担心实现起来很复杂-你会看到BatchNorm只需Keras中的一行代码!

在本练习中,你实际上将实现此识别块的功能稍强的版本,其中跳过连接将”skips over”3个隐藏层而不是2个。看起来像这样:

图4 标识块 skips over3层。

下面是各个步骤:

主路径的第一部分:

  • 第一个CONV2D具有形状为(1,1)和步幅为(1,1)的$F_1$个滤波器。其填充为“valid”,其名称应为conv_name_base + '2a'。使用0作为随机初始化的种子。
  • 第一个BatchNorm标准化通道轴。它的名字应该是bn_name_base + '2a'
  • 然后应用ReLU激活函数。

主路径的第二部分:

  • 第二个CONV2D具有形状为$(f,f)$ 的步幅为(1,1)的$F_2$个滤波器。其填充为“same”,其名称应为conv_name_base + '2b'。使用0作为随机初始化的种子。
  • 第二个BatchNorm标准化通道轴。它的名字应该是bn_name_base + '2b'
  • 然后应用ReLU激活函数。

主路径的第三部分:

  • 第三个CONV2D具有形状为(1,1)和步幅为(1,1)的$F_3$个滤波器。其填充为“valid”,其名称应为conv_name_base + '2c'。使用0作为随机初始化的种子。
  • 第三个BatchNorm标准化通道轴。它的名字应该是bn_name_base + '2c'。请注意,此组件中没有ReLU激活函数。

最后一步:

  • 将shortcut和输入添加在一起。
  • 然后应用ReLU激活函数。

练习:实现ResNet的identity block。我们已经实现了主路径的第一部分,请仔细阅读此内容,以确保你知道它在做什么。你应该执行其余的工作。

  • 要实现Conv2D步骤:See reference
  • 要实现BatchNorm: See reference(axis:整数,需要标准化的轴(通常是通道轴) )
  • 对于激活,请使用:Activation('relu')(X)
  • 要添加shortcut传递的值:See reference

编写函数identity_block(X,f, filters, stage, block)实现残差网络的恒等块三层
要求

  • 参数:

    • X - 输入的tensor类型的数据,维度为( m, n_H_prev, n_W_prev, n_H_prev )
    • f - 整数,指定主路径中间的CONV窗口的维度
    • filters - 整数列表,定义了主路径每层的卷积层的过滤器数量
    • stage - 整数,根据每层的位置来命名每一层,与block参数一起使用。
    • block - 字符串,据每层的位置来命名每一层,与stage参数一起使用。
  • 返回:

    • X - 恒等块的输出,tensor类型,维度为(n_H, n_W, n_C)

提示

  • X = Add()([X, X_shortcut])可以将跳跃信号X_shortcut与X相加获得新的信号X
  • 卷积:
    • X = Conv2D(filters=F1, kernel_size=(1,1), strides=(1,1) ,padding=”valid”,name=conv_name_base+”2a”, kernel_initializer=glorot_uniform(seed=0))(X)
  • 归一化:
    • X = BatchNormalization(axis=3,name=bn_name_base+”2a”)(X)
  • RELU:
    • X = Activation(“relu”)(X)
  • 添加快捷方式传递:
    • X = Add()([X,X_shortcut])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def identity_block(X, f, filters, stage, block):
"""
实现图3的恒等块

参数:
X - 输入的tensor类型的数据,维度为( m, n_H_prev, n_W_prev, n_H_prev )
f - 整数,指定主路径中间的CONV窗口的维度
filters - 整数列表,定义了主路径每层的卷积层的过滤器数量
stage - 整数,根据每层的位置来命名每一层,与block参数一起使用。
block - 字符串,据每层的位置来命名每一层,与stage参数一起使用。

返回:
X - 恒等块的输出,tensor类型,维度为(n_H, n_W, n_C)

"""

#定义命名规则
conv_name_base = "res" + str(stage) + block + "_branch"
bn_name_base = "bn" + str(stage) + block + "_branch"

#获取过滤器
F1, F2, F3 = filters

#保存输入数据,将会用于为主路径添加捷径
X_shortcut = X

#主路径的第一部分
##卷积层
X = Conv2D(filters=F1, kernel_size=(1,1), strides=(1,1) ,padding="valid",
name=conv_name_base+"2a", kernel_initializer=glorot_uniform(seed=0))(X)
##归一化
X = BatchNormalization(axis=3,name=bn_name_base+"2a")(X)
##使用ReLU激活函数
X = Activation("relu")(X)

#主路径的第二部分
##卷积层
X = Conv2D(filters=F2, kernel_size=(f,f),strides=(1,1), padding="same",
name=conv_name_base+"2b", kernel_initializer=glorot_uniform(seed=0))(X)
##归一化
X = BatchNormalization(axis=3,name=bn_name_base+"2b")(X)
##使用ReLU激活函数
X = Activation("relu")(X)


#主路径的第三部分
##卷积层
X = Conv2D(filters=F3, kernel_size=(1,1), strides=(1,1), padding="valid",
name=conv_name_base+"2c", kernel_initializer=glorot_uniform(seed=0))(X)
##归一化
X = BatchNormalization(axis=3,name=bn_name_base+"2c")(X)
##没有ReLU激活函数

#最后一步:
##将捷径与输入加在一起
X = Add()([X,X_shortcut])
##使用ReLU激活函数
X = Activation("relu")(X)

return X

运行下面的代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
tf.reset_default_graph()
with tf.Session() as test:
np.random.seed(1)
A_prev = tf.placeholder("float",[3,4,4,6])
X = np.random.randn(3,4,4,6)
A = identity_block(A_prev,f=2,filters=[2,4,6],stage=1,block="a")

test.run(tf.global_variables_initializer())
out = test.run([A],feed_dict={A_prev:X,K.learning_phase():0})
print("out = " + str(out[0][1][1][0]))

test.close()

预期输出:
out = [ 0.19716813 0. 1.35612273 2.17130733 0. 1.33249867]

2.2 The convolutional block卷积块

你已经实现了ResNet中的识别块。接下来,ResNet“卷积块”是另一种类型的块。当输入和输出尺寸不匹配时,可以使用这种类型的块。与标识块的区别在于,shortcut路径中有一个CONV2D层:

图4 卷积块

shortcut路径中的CONV2D层用于将输入$x$调整为另一个维度的大小,以便维度与最终需要添加到shortcut主路径所用的维度匹配。(这与讲座中讨论的矩阵$W_s$起到类似的作用。)例如,要将激活尺寸的高度和宽度减小2倍,可以使用步幅为2的1x1卷积。CONV2D层位于shortcut路径不使用任何非线性激活函数。它的主要作用是仅应用(学习的)线性函数来减小输入的维度,以使维度与之后的步骤匹配。

卷积块的细节如下:

主路径的第一部分:

  • 第一个CONV2D具有形状为(1,1)和步幅为(s,s)的$F_1$个滤波器。其填充为”valid”,其名称应为conv_name_base + '2a'
  • 第一个BatchNorm标准化通道轴。其名字是bn_name_base + '2a'
  • 然后应用ReLU激活函数。

主路径的第二部分:

  • 第二个CONV2D具有(f,f)的$F_2$滤波器和(1,1)的步幅。其填充为”same”,并且名称应为conv_name_base + '2b'
  • 第二个BatchNorm标准化通道轴。它的名字应该是bn_name_base + '2b'
  • 然后应用ReLU激活函数。

主路径的第三部分:

  • 第三个CONV2D的$F_3$滤波器为(1,1),步幅为(1,1)。其填充为”valid”,其名称应为conv_name_base + '2c'
  • 第三个BatchNorm标准化通道轴。它的名字应该是bn_name_base + '2c'。请注意,此组件中没有ReLU激活函数。

Shortcut path:

  • CONV2D具有形状为(1,1)和步幅为(s,s)的$F_3$个滤波器。其填充为”valid”,其名称应为conv_name_base + '1'
  • BatchNorm标准化通道轴。它的名字应该是bn_name_base + '1'

最后一步:

  • 将Shortcut路径和主路径添加在一起。
  • 然后应用ReLU激活函数。

练习:实现卷积模块。我们已经实现了主路径的第一部分;你应该执行其余的工作。和之前一样,使用0作为随机初始化的种子,以确保与评分器的一致性。

编写函数convolutional_block(X,f,filters, stage, block, s=2)实现卷积模块
要求

  • 参数:

    • X - 输入的tensor类型的变量,维度为( m, n_H_prev, n_W_prev, n_C_prev)
    • f - 整数,指定主路径中间的CONV窗口的维度
    • filters - 整数列表,定义了主路径每层的卷积层的过滤器数量
    • stage - 整数,根据每层的位置来命名每一层,与block参数一起使用。
    • block - 字符串,据每层的位置来命名每一层,与stage参数一起使用。
    • s - 整数,指定要使用的步幅
  • 返回:

    • X - 卷积块的输出,tensor类型,维度为(n_H, n_W, n_C)
      提示
      使用X = Add()([X,X_shortcut])将Shortcut路径和主路径添加在一起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def convolutional_block(X, f, filters, stage, block, s=2):
"""

参数:
X - 输入的tensor类型的变量,维度为( m, n_H_prev, n_W_prev, n_C_prev)
f - 整数,指定主路径中间的CONV窗口的维度
filters - 整数列表,定义了主路径每层的卷积层的过滤器数量
stage - 整数,根据每层的位置来命名每一层,与block参数一起使用。
block - 字符串,据每层的位置来命名每一层,与stage参数一起使用。
s - 整数,指定要使用的步幅

返回:
X - 卷积块的输出,tensor类型,维度为(n_H, n_W, n_C)
"""

#定义命名规则
conv_name_base = "res" + str(stage) + block + "_branch"
bn_name_base = "bn" + str(stage) + block + "_branch"

#获取过滤器数量
F1, F2, F3 = filters

#保存输入数据
X_shortcut = X

#主路径
##主路径第一部分
X = Conv2D(filters=F1, kernel_size=(1,1), strides=(s,s), padding="valid",
name=conv_name_base+"2a", kernel_initializer=glorot_uniform(seed=0))(X)
X = BatchNormalization(axis=3,name=bn_name_base+"2a")(X)
X = Activation("relu")(X)

##主路径第二部分
X = Conv2D(filters=F2, kernel_size=(f,f), strides=(1,1), padding="same",
name=conv_name_base+"2b", kernel_initializer=glorot_uniform(seed=0))(X)
X = BatchNormalization(axis=3,name=bn_name_base+"2b")(X)
X = Activation("relu")(X)

##主路径第三部分
X = Conv2D(filters=F3, kernel_size=(1,1), strides=(1,1), padding="valid",
name=conv_name_base+"2c", kernel_initializer=glorot_uniform(seed=0))(X)
X = BatchNormalization(axis=3,name=bn_name_base+"2c")(X)

#捷径
X_shortcut = Conv2D(filters=F3, kernel_size=(1,1), strides=(s,s), padding="valid",
name=conv_name_base+"1", kernel_initializer=glorot_uniform(seed=0))(X_shortcut)
X_shortcut = BatchNormalization(axis=3,name=bn_name_base+"1")(X_shortcut)

#最后一步
X = Add()([X,X_shortcut])
X = Activation("relu")(X)

return X

运行下面的代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tf.reset_default_graph()

with tf.Session() as test:
np.random.seed(1)
A_prev = tf.placeholder("float",[3,4,4,6])
X = np.random.randn(3,4,4,6)

A = convolutional_block(A_prev,f=2,filters=[2,4,6],stage=1,block="a")
test.run(tf.global_variables_initializer())

out = test.run([A],feed_dict={A_prev:X,K.learning_phase():0})
print("out = " + str(out[0][1][1][0]))

test.close()

预期输出:
out = [ 0.09018463 1.23489773 0.46822017 0.0367176 0. 0.65516603]

3 建立你的第一个ResNet模型(50层)

现在,你具有构建非常深的ResNet的必要块。下图详细描述了此神经网络的体系结构。图中的“ID BLOCK”代表“标准块”,“ID BLOCK x3”表示你应该将3个标准块堆叠在一起。

图5 ResNet50 模型

此ResNet-50模型的详细结构是:

  • 零填充填充(3,3)的输入
  • 阶段1:
    • 2D卷积具有64个形状为(7,7)的滤波器,并使用(2,2)步幅,名称是“conv1”。
    • BatchNorm应用于输入的通道轴。
    • MaxPooling使用(3,3)窗口和(2,2)步幅。
  • 阶段2:
    • 卷积块使用三组大小为[64,64,256]的滤波器,“f”为3,“s”为1,块为“a”。
    • 2个标识块使用三组大小为[64,64,256]的滤波器,“f”为3,块为“b”和“c”。
  • 阶段3:
    • 卷积块使用三组大小为[128,128,512]的滤波器,“f”为3,“s”为2,块为“a”。
    • 3个标识块使用三组大小为[128,128,512]的滤波器,“f”为3,块为“b”,“c”和“d”。
  • 阶段4:
    • 卷积块使用三组大小为[256、256、1024]的滤波器,“f”为3,“s”为2,块为“a”。
    • 5个标识块使用三组大小为[256、256、1024]的滤波器,“f”为3,块为“b”,“c”,“d”,“e”和“f”。
  • 阶段5:
    • 卷积块使用三组大小为[512、512、2048]的滤波器,“f”为3,“s”为2,块为“a”。
    • 2个标识块使用三组大小为[256、256、2048]的滤波器,“f”为3,块为“b”和“c”。
  • 2D平均池使用形状为(2,2)的窗口,其名称为“avg_pool”。
  • Flatten层没有任何超参数或名称。
  • 全连接(密集)层使用softmax激活将其输入减少为类数。名字是'fc' + str(classes)

练习:使用上图中的描述实现50层的ResNet。请确保遵循上面文本中的命名。

你需要使用以下函数:

这是我们在以下代码中使用的其他函数:

编写函数ResNet50(input_shape=(64,64,3),classes=6)实现50层残差网络
要求

  • 框架CONV2D -> BATCHNORM -> RELU -> MAXPOOL -> CONVBLOCK -> IDBLOCK2 -> CONVBLOCK -> IDBLOCK3
    -> CONVBLOCK -> IDBLOCK5 -> CONVBLOCK -> IDBLOCK2 -> AVGPOOL -> TOPLAYER
  • 参数:

    • input_shape - 图像数据集的维度
    • classes - 整数,分类数
  • 返回:

    • model - Keras框架的模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def ResNet50(input_shape=(64,64,3),classes=6):
"""
实现ResNet50
CONV2D -> BATCHNORM -> RELU -> MAXPOOL -> CONVBLOCK -> IDBLOCK*2 -> CONVBLOCK -> IDBLOCK*3
-> CONVBLOCK -> IDBLOCK*5 -> CONVBLOCK -> IDBLOCK*2 -> AVGPOOL -> TOPLAYER

参数:
input_shape - 图像数据集的维度
classes - 整数,分类数

返回:
model - Keras框架的模型

"""

#定义tensor类型的输入数据
X_input = Input(input_shape)

#0填充
X = ZeroPadding2D((3,3))(X_input)

#stage1
X = Conv2D(filters=64, kernel_size=(7,7), strides=(2,2), name="conv1",
kernel_initializer=glorot_uniform(seed=0))(X)
X = BatchNormalization(axis=3, name="bn_conv1")(X)
X = Activation("relu")(X)
X = MaxPooling2D(pool_size=(3,3), strides=(2,2))(X)

#stage2
X = convolutional_block(X, f=3, filters=[64,64,256], stage=2, block="a", s=1)
X = identity_block(X, f=3, filters=[64,64,256], stage=2, block="b")
X = identity_block(X, f=3, filters=[64,64,256], stage=2, block="c")

#stage3
X = convolutional_block(X, f=3, filters=[128,128,512], stage=3, block="a", s=2)
X = identity_block(X, f=3, filters=[128,128,512], stage=3, block="b")
X = identity_block(X, f=3, filters=[128,128,512], stage=3, block="c")
X = identity_block(X, f=3, filters=[128,128,512], stage=3, block="d")

#stage4
X = convolutional_block(X, f=3, filters=[256,256,1024], stage=4, block="a", s=2)
X = identity_block(X, f=3, filters=[256,256,1024], stage=4, block="b")
X = identity_block(X, f=3, filters=[256,256,1024], stage=4, block="c")
X = identity_block(X, f=3, filters=[256,256,1024], stage=4, block="d")
X = identity_block(X, f=3, filters=[256,256,1024], stage=4, block="e")
X = identity_block(X, f=3, filters=[256,256,1024], stage=4, block="f")

#stage5
X = convolutional_block(X, f=3, filters=[512,512,2048], stage=5, block="a", s=2)
X = identity_block(X, f=3, filters=[512,512,2048], stage=5, block="b")
X = identity_block(X, f=3, filters=[512,512,2048], stage=5, block="c")

#均值池化层
X = AveragePooling2D(pool_size=(2,2),padding="same")(X)

#输出层
X = Flatten()(X)
X = Dense(classes, activation="softmax", name="fc"+str(classes),
kernel_initializer=glorot_uniform(seed=0))(X)


#创建模型
model = Model(inputs=X_input, outputs=X, name="ResNet50")

return model

编写程序对模型实体化和编译
提示
使用loss=”categorical_crossentropy”适用于多分类问题目标为独热编码形式

1
2
model = ResNet50(input_shape=(64,64,3),classes=6)
model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])

现在可以训练模型了。 你唯一需要的就是数据集。

加载SIGNS数据集。

图6 SIGNS 数据集

运行下面代码加载数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = resnets_utils.load_dataset()

# Normalize image vectors
X_train = X_train_orig / 255.
X_test = X_test_orig / 255.

# Convert training and test labels to one hot matrices
Y_train = resnets_utils.convert_to_one_hot(Y_train_orig, 6).T
Y_test = resnets_utils.convert_to_one_hot(Y_test_orig, 6).T

print("number of training examples = " + str(X_train.shape[0]))
print("number of test examples = " + str(X_test.shape[0]))
print("X_train shape: " + str(X_train.shape))
print("Y_train shape: " + str(Y_train.shape))
print("X_test shape: " + str(X_test.shape))
print("Y_test shape: " + str(Y_test.shape))

number of training examples = 1080
number of test examples = 120
X_train shape: (1080, 64, 64, 3)
Y_train shape: (1080, 6)
X_test shape: (120, 64, 64, 3)
Y_test shape: (120, 6)

分析:本次使用的数据集变成了一行一样本,标签是独热形式

运行以下程序,训练模型(批处理大小为32)2个epoch。在CPU上,每个epoch大约需要5分钟。

1
2
model.fit(X_train,Y_train,epochs=2,batch_size=32)

预期输出:
Epoch 1/2
1080/1080 [==============================] - 200s 185ms/step - loss: 3.0667 - acc: 0.2593
Epoch 2/2
1080/1080 [==============================] - 186s 172ms/step - loss: 1.9755 - acc: 0.4093

编写函数评估一下模型用测试集

1
2
3
4
5
preds = model.evaluate(X_test,Y_test)

print("误差值 = " + str(preds[0]))
print("准确率 = " + str(preds[1]))

预期输出:
120/120 [==============================] - 5s 44ms/step
误差值 = 12.3403865178
准确率 = 0.175000000497

4 测试你自己的图片(可选练习)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt # plt 用于显示图片

%matplotlib inline

img_path = 'images/fingers_big/2.jpg'

my_image = image.load_img(img_path, target_size=(64, 64))
my_image = image.img_to_array(my_image)

my_image = np.expand_dims(my_image,axis=0)
my_image = preprocess_input(my_image)

print("my_image.shape = " + str(my_image.shape))

print("class prediction vector [p(0), p(1), p(2), p(3), p(4), p(5)] = ")
print(model.predict(my_image))

my_image = scipy.misc.imread(img_path)
plt.imshow(my_image)

预期输出
my_image.shape = (1, 64, 64, 3)
class prediction vector [p(0), p(1), p(2), p(3), p(4), p(5)] =
[[ 1. 0. 0. 0. 0. 0.]]

你还可以通过运行以下代码来打印模型的摘要。

1
model.summary()

最后,运行下面的代码以可视化你的ResNet50。你也可以通过转到”File -> Open…-> model.png”.下载模型的.png图片。

1
2
plot_model(model, to_file='model.png')
SVG(model_to_dot(model).create(prog='dot', format='svg'))

你应该记住的内容:

  • 非常深的”plain”网络在实践中不起作用,因为梯度消失而难以训练。
  • skip-connections有助于解决梯度消失问题。它们还使ResNet块易于学习识别功能。
  • 块有两种主要类型:标识块(标准块)和卷积块。
  • 通过将这些块堆叠在一起,可以构建非常深的残差网络。

参考

相关代码:
kt_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#kt_utils.py

import keras.backend as K
import math
import numpy as np
import h5py
import matplotlib.pyplot as plt


def mean_pred(y_true, y_pred):
return K.mean(y_pred)

def load_dataset():
train_dataset = h5py.File('datasets/train_happy.h5', "r")
train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features
train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels

test_dataset = h5py.File('datasets/test_happy.h5', "r")
test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels

classes = np.array(test_dataset["list_classes"][:]) # the list of classes

train_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0]))
test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0]))

return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes

resnets_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#resnets_utils.py

import os
import numpy as np
import tensorflow as tf
import h5py
import math

def load_dataset():
train_dataset = h5py.File('datasets/train_signs.h5', "r")
train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features
train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels

test_dataset = h5py.File('datasets/test_signs.h5', "r")
test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels

classes = np.array(test_dataset["list_classes"][:]) # the list of classes

train_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0]))
test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0]))

return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes


def random_mini_batches(X, Y, mini_batch_size = 64, seed = 0):
"""
Creates a list of random minibatches from (X, Y)

Arguments:
X -- input data, of shape (input size, number of examples) (m, Hi, Wi, Ci)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat), of shape (1, number of examples) (m, n_y)
mini_batch_size - size of the mini-batches, integer
seed -- this is only for the purpose of grading, so that you're "random minibatches are the same as ours.

Returns:
mini_batches -- list of synchronous (mini_batch_X, mini_batch_Y)
"""

m = X.shape[0] # number of training examples
mini_batches = []
np.random.seed(seed)

# Step 1: Shuffle (X, Y)
permutation = list(np.random.permutation(m))
shuffled_X = X[permutation,:,:,:]
shuffled_Y = Y[permutation,:]

# Step 2: Partition (shuffled_X, shuffled_Y). Minus the end case.
num_complete_minibatches = math.floor(m/mini_batch_size) # number of mini batches of size mini_batch_size in your partitionning
for k in range(0, num_complete_minibatches):
mini_batch_X = shuffled_X[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:,:,:]
mini_batch_Y = shuffled_Y[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:]
mini_batch = (mini_batch_X, mini_batch_Y)
mini_batches.append(mini_batch)

# Handling the end case (last mini-batch < mini_batch_size)
if m % mini_batch_size != 0:
mini_batch_X = shuffled_X[num_complete_minibatches * mini_batch_size : m,:,:,:]
mini_batch_Y = shuffled_Y[num_complete_minibatches * mini_batch_size : m,:]
mini_batch = (mini_batch_X, mini_batch_Y)
mini_batches.append(mini_batch)

return mini_batches


def convert_to_one_hot(Y, C):
Y = np.eye(C)[Y.reshape(-1)].T
return Y


def forward_propagation_for_predict(X, parameters):
"""
Implements the forward propagation for the model: LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SOFTMAX

Arguments:
X -- input dataset placeholder, of shape (input size, number of examples)
parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3"
the shapes are given in initialize_parameters
Returns:
Z3 -- the output of the last LINEAR unit
"""

# Retrieve the parameters from the dictionary "parameters"
W1 = parameters['W1']
b1 = parameters['b1']
W2 = parameters['W2']
b2 = parameters['b2']
W3 = parameters['W3']
b3 = parameters['b3']
# Numpy Equivalents:
Z1 = tf.add(tf.matmul(W1, X), b1) # Z1 = np.dot(W1, X) + b1
A1 = tf.nn.relu(Z1) # A1 = relu(Z1)
Z2 = tf.add(tf.matmul(W2, A1), b2) # Z2 = np.dot(W2, a1) + b2
A2 = tf.nn.relu(Z2) # A2 = relu(Z2)
Z3 = tf.add(tf.matmul(W3, A2), b3) # Z3 = np.dot(W3,Z2) + b3

return Z3

def predict(X, parameters):

W1 = tf.convert_to_tensor(parameters["W1"])
b1 = tf.convert_to_tensor(parameters["b1"])
W2 = tf.convert_to_tensor(parameters["W2"])
b2 = tf.convert_to_tensor(parameters["b2"])
W3 = tf.convert_to_tensor(parameters["W3"])
b3 = tf.convert_to_tensor(parameters["b3"])

params = {"W1": W1,
"b1": b1,
"W2": W2,
"b2": b2,
"W3": W3,
"b3": b3}

x = tf.placeholder("float", [12288, 1])

z3 = forward_propagation_for_predict(x, params)
p = tf.argmax(z3)

sess = tf.Session()
prediction = sess.run(p, feed_dict = {x: X})

return prediction

作业9 迁移学习-神经风格迁移

深度学习与艺术 - 神经风格迁移

欢迎来到本周的作业2。在本次作业中,你将学习神经风格迁移。该算法由Gatys等人在2015年创建(https://arxiv.org/abs/1508.06576)。

在此作业中,你将:

  • 实现神经风格迁移算法
  • 使用算法生成新颖的艺术图像

目前你研究的大多数算法都会优化损失函数以获得一组参数值。而在神经样式转换中,你将学习优化损失函数以获得像素值!

运行代码导入本次需要的包

1
2
3
4
5
6
7
8
9
10
11
12
import os
import sys
import scipy.io
import scipy.misc
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow
from PIL import Image
from nst_utils import *
import numpy as np
import tensorflow as tf

%matplotlib inline

问题陈述

神经风格迁移(NST)是深度学习中最有趣的技术之一。如下所示,它将“内容”图像(Content)和“风格”图像(Style)合并在一起,以创建“生成”图像(Generated)。生成的图像G将图像C的“内容”与图像S的“风格”组合在一起。

在此示例中,你将巴黎卢浮宫博物馆的图像(内容图像C)与印象派运动的领导者克劳德·莫奈的作品(风格图像S)混合在一起以生成新的图像。

让我们看看如何做到这一点。

迁移学习

神经风格迁移(NST)使用以前训练过的卷积网络,并以此为基础。将之前经过不同任务训练的网络应用于新任务的想法叫做迁移学习。

遵循原始的NST论文,我们将使用VGG网络。具体来说,我们将使用VGG-19,这是VGG网络的19层版本。该模型已经在非常大的ImageNet数据库上进行了训练,因此已经学会了识别各种低层特征和高层特征。

运行以下代码以从VGG模型加载参数。这可能需要几秒钟。

1
2
3
4
model = nst_utils.load_vgg_model("pretrained-model/imagenet-vgg-verydeep-19.mat")

print(model)

该模型存储在python字典中,其中每个变量名称都是键,而对应的值是包含该变量值的张量。要通过此网络测试图像,只需要将图像提供给模型。在TensorFlow中,你可以使用 tf.assign函数执行此操作。特别地,你将使用如下的assign函数:

1
2
3
#tf.assign函数用法
model["input"].assign(image)


这会将图像分配为模型的输入。此后,如果要访问特定层的激活函数,例如当网络在此图像上运行时说4_2 层,则可以在正确的张量conv4_2上运行TensorFlow会话,如下所示:
1
2
3
#访问 4_2 层的激活
sess.run(model["conv4_2"])

神经风格迁移

我们将分三步构建NST算法:

  • 建立内容损失函数 $J_{content}(C,G)$;
  • 建立风格损失函数$J_{style}(S,G)$;
  • 放在一起得出$J(G) = \alpha J_{content}(C,G) + \beta J_{style}(S,G)$。

计算内容损失

在我们的运行示例中,内容图像C是巴黎卢浮宫博物馆的图片。
运行下面的代码以查看卢浮宫的图片。

1
2
3
content_image = scipy.misc.imread("images/louvre.jpg")
imshow(content_image)

内容图像(C)显示了卢浮宫博物馆的金字塔,周围是古老的巴黎建筑,在晴朗的天空下只有几层云。

3.1.1 如何确保生成的图像G与图像C的内容匹配?

正如我们在课程中所讲述的,ConvNet的底层倾向于检测诸如边缘和简单纹理之类的低层特征,而深层则倾向于检测诸如纹理之类的更复杂的高层特征。

我们希望生成的图像G具有与输入图像C相似的内容。假设你已选择某些层的激活来表示图像的内容。在实践中,如果在网络中间选择一个层,既不会太浅也不会太深,你将获得视觉上令人满意的结果。(完成本练习后,请随时返回并尝试使用不同的图层,以查看结果变化。)

因此,假设你选择了一个特定的隐藏层使用。现在,将图像C设置为预训练的VGG网络的输入,并进行正向传播。假设$a^{(C)}$是你选择的层中的隐藏层激活。(在课程中,我们将其写为,但在这里我们将删除上标$[l]$以简化表示手法。)这将是张量。对图像G重复此过程:将G设置为输入,然后进行正向传播。令

为相应的隐藏层激活。我们将内容损失函数定义为:

在这里,$n_H, n_W$和$n_C$是你选择的隐藏层的高度,宽度和通道数,并以损失的归一化术语显示。请注意$a^{(C)}$和$a^{(G)}$是与隐藏层的激活对应的。为了计算损失$J_{content}(C,G)$,将这些3D体积展开为2D矩阵更方便,如下所示。(从技术上讲,此展开步骤不需要计算$J_{content}$,但是对于之后需要进行类似操作以计算样式$J_{style}$常数的情况来说,这将是一个很好的实践。)

练习:使用TensorFlow计算“内容损失”。

说明:实现此函数包含3个步骤:

  1. 从a_G检索尺寸:
    • 要从张量X检索尺寸,请使用: X.get_shape().as_list()
  2. 如上图所示展开a_C和a_G
  3. 计算内容损失:

编写函数compute_content_cost(a_C,a_G)计算内容代价$J_{content}(C,G)$

要求

  • 参数:

    • a_C — tensor类型,维度为(1, n_H, n_W, n_C),表示隐藏层中图像C的内容的激活值。
    • a_G — tensor类型,维度为(1, n_H, n_W, n_C),表示隐藏层中图像G的内容的激活值。
  • 返回:

    • J_content — 实数,用上面的公式计算的值
      提示
  • a_G.get_shape().as_list() 方法获取张量 a_G 的形状
  • tf.reshape(a_C, [n_H n_W, n_C]): 将 a_C 从 (1, n_H, n_W, n_C) 形状重新调整为 (n_H n_W, n_C),即将高度和宽度的维度合并
  • tf.transpose(…): 这个函数会将张量的维度进行转置,以便得到的张量形状为 (n_C, n_H * n_W)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def compute_content_cost(a_C, a_G):
"""
计算内容代价的函数

参数:
a_C -- tensor类型,维度为(1, n_H, n_W, n_C),表示隐藏层中图像C的内容的激活值。
a_G -- tensor类型,维度为(1, n_H, n_W, n_C),表示隐藏层中图像G的内容的激活值。

返回:
J_content -- 实数,用上面的公式1计算的值。

"""

#获取a_G的维度信息
m, n_H, n_W, n_C = a_G.get_shape().as_list()

#对a_C与a_G从3维降到2维
a_C_unrolled = tf.transpose(tf.reshape(a_C, [n_H * n_W, n_C]))
a_G_unrolled = tf.transpose(tf.reshape(a_G, [n_H * n_W, n_C]))

#计算内容代价
#J_content = (1 / (4 * n_H * n_W * n_C)) * tf.reduce_sum(tf.square(tf.subtract(a_C_unrolled, a_G_unrolled)))
J_content = 1/(4*n_H*n_W*n_C)*tf.reduce_sum(tf.square(tf.subtract(a_C_unrolled, a_G_unrolled)))
return J_content

运行代码测试

1
2
3
4
5
6
7
8
9
10
11
tf.reset_default_graph()

with tf.Session() as test:
tf.set_random_seed(1)
a_C = tf.random_normal([1, 4, 4, 3], mean=1, stddev=4)
a_G = tf.random_normal([1, 4, 4, 3], mean=1, stddev=4)
J_content = compute_content_cost(a_C, a_G)
print("J_content = " + str(J_content.eval()))

test.close()

预期输出:
J_content = 6.7655935

你应该记住

  • 内容损失需要对神经网络进行隐藏层激活,并计算$a^{(C)}$ 和 $a^{(G)}$之间的差异。
  • 当我们在最小化内容损失时,这将有助于确保$G$具有与$C$类似的内容。

计算风格损失

我们将使用以下样式图像作为示例运行:

运行代码查看风格图片

1
2
3
4
style_image = scipy.misc.imread("images/monet_800600.jpg")

imshow(style_image)

这幅画以印象派的风格绘制。

让我们看看如何定义“风格”常数函数$J_{style}(S,G)$。

风格矩阵

风格矩阵也称为“语法矩阵”。在线性代数中,向量$(v_{1},\dots ,v_{n})$的集合的Gram矩阵G是点积的矩阵,其项是${\displaystyle G_{ij} = v_{i}^T v_{j} = np.dot(v_{i}, v_{j}) }$。换句话说,$G_{ij}$ 比较$v_i$ 与$v_j$的相似度:如果它们非常相似,则它们会具有较大的点积,因此$G_{ij}$也会较大。

请注意,此处使用的变量名称存在冲突。我们遵循文献中使用的通用术语,但是$G$用于表示风格矩阵(或Gram矩阵)也表示生成的图像。我们将从上下文中确保清楚$G$的指代。

在NST中,可以通过将“展开的”滤波器矩阵与其转置相乘来计算风格矩阵

结果是维度为$(n_C,n_C)$的矩阵,其中$n_C$是滤波器的数量。值$G_{ij}$衡量滤波器$i$的激活与滤波器$j$的激活的相似度

语法矩阵的一个重要部分是对角元素(例如$G_{ii}$)也可以衡量滤波器$i$的活跃程度。例如,假设滤波器$i$正在检测图像中的垂直纹理。然后$G_{ii}$衡量整个图像中垂直纹理的普遍程度:如果$G_{ii}$大,则意味着图像具有很多垂直纹理。

通过捕获不同类型特征的普遍性($G_{ii}$)以及一起出现多少不同特征($G_{ii}$),风格矩阵G可以衡量图像的样式

练习
使用TensorFlow实现一个计算矩阵A的语法矩阵的函数。公式为:A的语法矩阵为$G_A = AA^T$。如果遇到问题,请查看Hint 1Hint 2

编写gram_matrix(A)计算矩阵的风格矩阵

要求

  • 计算输入矩阵 A 的 Gram 矩阵。

  • 参数:

    • A — 形状为 (n_C, n_H*n_W) 的矩阵,n_C 表示通道数,n_H 和 n_W 分别表示高和宽。
  • 返回:

    • GA — A 的 Gram 矩阵,形状为 (n_C, n_C)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tensorflow as tf

def gram_matrix(A):
"""
计算输入矩阵 A 的 Gram 矩阵。

参数:
A -- 形状为 (n_C, n_H*n_W) 的矩阵,n_C 表示通道数,n_H 和 n_W 分别表示高和宽。

返回:
GA -- A 的 Gram 矩阵,形状为 (n_C, n_C)。
"""

### START CODE HERE ### (≈1 line)
# 计算 Gram 矩阵,使用矩阵乘法将 A 与其转置相乘。
# 这里的 tf.matmul 用于计算矩阵乘法,tf.transpose(A) 用于得到 A 的转置。
GA = tf.matmul(A, tf.transpose(A))
### END CODE HERE ###

# 返回 Gram 矩阵 GA
return GA

运行代码测试

1
2
3
4
5
6
7
8
tf.reset_default_graph()

with tf.Session() as test:
tf.set_random_seed(1)
A = tf.random_normal([3, 2*1], mean=1, stddev=4)
GA = gram_matrix(A)

print("GA = " + str(GA.eval()))

预期输出:
GA = [[ 6.422305 -4.429122 -2.096682]
[-4.429122 19.465837 19.563871]
[-2.096682 19.563871 20.686462]]

风格损失

生成风格矩阵(Gram矩阵)后,你的目标是使”style”图像S的Gram矩阵和生成的图像G的Gram矩阵之间的距离最小。现在,我们仅使用单个隐藏层$a^{[l]}$,该层的相应的风格损失定义为:

其中$G^{(S)}$ 和 $G^{(G)}$分别是“风格”图像和“生成的”图像的语法矩阵,使用针对网络中特定的隐藏层的激活来计算。

练习:计算单层的风格损失。

说明:实现此函数的步骤是:

  1. 从隐藏层激活a_G中检索尺寸:
    • 要从张量X检索尺寸,请使用:X.get_shape().as_list()
  2. 如上图所示,将隐藏层激活a_S和a_G展开为2D矩阵。
  3. 计算图像S和G的风格矩阵。(使用以前编写的函数)
  4. 计算风格损失:

编写函数compute_layer_style_cost(a_S,a_G)计算单层的风格损失
要求

  • 计算两个图像在特定层的风格损失。

  • 参数:

    • a_S — 张量,维度为 (1, n_H, n_W, n_C),表示图像 S 的隐藏层激活,反映其风格。
    • a_G — 张量,维度为 (1, n_H, n_W, n_C),表示图像 G 的隐藏层激活,反映其风格。
  • 返回:

    • J_style_layer — 张量,表示一个标量值,风格损失,按照公式 (2) 定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def compute_layer_style_cost(a_S, a_G):
"""
计算两个图像在特定层的风格损失。

参数:
a_S -- 张量,维度为 (1, n_H, n_W, n_C),表示图像 S 的隐藏层激活,反映其风格。
a_G -- 张量,维度为 (1, n_H, n_W, n_C),表示图像 G 的隐藏层激活,反映其风格。

返回:
J_style_layer -- 张量,表示一个标量值,风格损失,按照公式 (2) 定义。
"""

### 开始代码 ###
# 从 a_G 中获取维度信息 (m, n_H, n_W, n_C)
# m 表示批次大小,这里应该是 1,n_H 表示高度,n_W 表示宽度,n_C 表示通道数
m, n_H, n_W, n_C = a_G.get_shape().as_list()

# 将 a_S 和 a_G 重塑为形状 (n_C, n_H*n_W),以便进行 Gram 矩阵计算
# 这里的重塑是为了将 (n_H, n_W) 维度展平,以便将每个像素的特征(通道)结合起来
a_S = tf.reshape(a_S, shape=(n_H * n_W, n_C)) # 将 S 的形状重塑为 (n_H*n_W, n_C)
a_G = tf.reshape(a_G, shape=(n_H * n_W, n_C)) # 将 G 的形状重塑为 (n_H*n_W, n_C)

# 计算图像 S 和 G 的 Gram 矩阵
# Gram 矩阵用于捕获图像的风格特征,它是激活的转置与自身的乘积
GS = gram_matrix(tf.transpose(a_S)) # 计算 S 的 Gram 矩阵
GG = gram_matrix(tf.transpose(a_G)) # 计算 G 的 Gram 矩阵

# 计算风格损失
# 使用公式计算损失,分母部分是一个常数,用于归一化
J_style_layer = tf.reduce_sum(tf.square(tf.subtract(GS, GG))) / (4 * (n_C * n_C) * (n_W * n_H) * (n_W * n_H))
# tf.subtract(GS, GG) 计算两个 Gram 矩阵之间的差异
# tf.square(...) 计算差异的平方
# tf.reduce_sum(...) 计算所有元素的和
# 最终结果除以一个归一化因子,保证损失值的合理范围

### 结束代码 ###

return J_style_layer # 返回计算得到的风格损失

运行代码测试

1
2
3
4
5
6
7
8
9
tf.reset_default_graph()

with tf.Session() as test:
tf.set_random_seed(1)
a_S = tf.random_normal([1, 4, 4, 3], mean=1, stddev=4)
a_G = tf.random_normal([1, 4, 4, 3], mean=1, stddev=4)
J_style_layer = compute_layer_style_cost(a_S, a_G)

print("J_style_layer = " + str(J_style_layer.eval()))

预期输出:
J_style_layer = 9.190278

风格权重

到目前为止,你仅从一层捕获了风格特征。如果我们从几个不同的层次“合并”风格损失,我们将获得更好的结果。完成此练习后,请随时返回并尝试不同的权重,以查看它如何更改生成的图像$G$。但现在而言,这是一个合理的默认值:

1
2
3
4
5
6
STYLE_LAYERS = [
('conv1_1', 0.2),
('conv2_1', 0.2),
('conv3_1', 0.2),
('conv4_1', 0.2),
('conv5_1', 0.2)]

你可以如下组合不同层的风格损失:

$\lambda^{[l]}$的值在STYLE_LAYERS中给出。

我们已经实现了compute_style_cost(…)函数。它只是简单地多次调用你的compute_layer_style_cost(…),并使用STYLE_LAYERS中的值对结果进行加权。请仔细阅读以确保你了解它在做什么。

1
2
3
4
5
6
7
2.从STYLE_LAYERS循环(layer_name,coeff):
         a. 选择当前层的输出张量 例如,要从层"conv1_1"中调用张量,你可以这样做:out = model["conv1_1"]
         b. 通过在张量"out"上运行会话,从当前层获取style图像的风格
         C. 获取一个表示当前层生成的图像风格的张量。 这只是"out"。
         d. 现在,你拥有两种风格。使用上面实现的函数计算当前层的style_cost
         e. 将当前层的(style_cost x coeff)添加到整体风格损失(J_style)中
3.返回J_style,它现在应该是每层的(style_cost x coeff)之和。

编写函数compute_style_cost(model, STYLE_LAYERS)计算从多个选择的层提取的整体风格成本
要求:

  • 计算从多个选择的层提取的整体风格成本

  • 参数:

    • model — 我们的 TensorFlow 模型
    • STYLE_LAYERS — 一个 Python 列表,包含:
      • 我们希望提取风格的层的名称
      • 每个层的系数
  • 返回:

    • J_style — 表示标量值的张量,风格成本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def compute_style_cost(model, STYLE_LAYERS):
"""
计算从多个选择的层提取的整体风格成本

参数:
model -- 我们的 TensorFlow 模型
STYLE_LAYERS -- 一个 Python 列表,包含:
- 我们希望提取风格的层的名称
- 每个层的系数

返回:
J_style -- 表示标量值的张量,风格成本
"""

# 初始化整体风格成本为0
J_style = 0

# 遍历每一层名称和对应的系数
for layer_name, coeff in STYLE_LAYERS:

# 选择当前选定层的输出张量
out = model[layer_name]

# 将 a_S 设置为我们选择的层的隐藏层激活值,通过会话执行 out
a_S = sess.run(out)

# 将 a_G 设置为同一层的隐藏层激活值。在这里,a_G 引用 model[layer_name]
# 但尚未计算。稍后在代码中,我们将把图像 G 作为模型输入,这样
# 当我们运行会话时,这将是从适当层提取的激活值,以 G 为输入。
a_G = out

# 计算当前层的风格成本
J_style_layer = compute_layer_style_cost(a_S, a_G)

# 将当前层的系数乘以该层的风格成本,并添加到整体风格成本中
J_style += coeff * J_style_layer

return J_style

注意:在上述for循环的内部循环中,a_G是张量,尚未进行求值。当我们在下面的model_nn()中运行TensorFlow计算图时,它将在每次迭代时进行评估和更新。

你如何选择每一层的系数?较深的层捕获更复杂的特征,并且较深的层中的特征在图像中相对于彼此而言定位较少。因此,如果希望生成的图像柔和地跟随风格图像,请尝试为较深的层选择较大的权重,为第一层选择较小的权重。相反,如果希望生成的图像强烈遵循风格图像,请尝试为较低的层选择较小的权重,为第一层选择较大的权重

你应该记住

  • 可以使用隐藏层激活的Gram矩阵表示图像的风格。但是,结合多个不同层的语法矩阵表示,我们可以获得更好的结果。这与内容表示法相反,后者通常仅使用一个隐藏层就足够了。
  • 最小化风格损失将导致图像$G$遵循图像$S$的风格。

定义优化的总损失

最后,让我们创建一个损失函数,以最小化风格和内容损失。公式为:

练习:实现总损失函数,其中包括内容损失和风格损失。

编写函数total_cost(J_content, J_style, alpha = 10, beta = 40)
要求

  • 计算总成本函数

  • 参数:

    • J_content — 内容成本,由外部计算得出
    • J_style — 风格成本,由外部计算得出
    • alpha — 超参数,权衡内容成本的重要性,默认为10
    • beta — 超参数,权衡风格成本的重要性,默认为40
  • 返回:

    • J — 根据上述公式定义的总成本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def total_cost(J_content, J_style, alpha=10, beta=40):
"""
计算总成本函数

参数:
J_content -- 内容成本,由外部计算得出
J_style -- 风格成本,由外部计算得出
alpha -- 超参数,权衡内容成本的重要性,默认为10
beta -- 超参数,权衡风格成本的重要性,默认为40

返回:
J -- 根据上述公式定义的总成本
"""

### START CODE HERE ### (≈1 line)
# 计算总成本,公式为:总成本 = alpha * 内容成本 + beta * 风格成本
J = alpha * J_content + beta * J_style
### END CODE HERE ###

# 返回计算得到的总成本J
return J

运行代码测试

1
2
3
4
5
6
7
8
tf.reset_default_graph()

with tf.Session() as test:
np.random.seed(3)
J_content = np.random.randn()
J_style = np.random.randn()
J = total_cost(J_content, J_style)
print("J = " + str(J))

预期输出:
J = 35.34667875478276

你应该记住

  • 总损失是内容损失$J_{content}(C,G)$和风格损失$J_{style}(S,G)$的线性组合
  • $\alpha$和$\beta$是控制内容和风格之间相对权重的超参数

解决优化问题

最后,让我们将所有内容组合在一起以实现神经风格迁移!

该程序必须执行以下操作:

  1. 创建一个交互式会话
  2. 加载内容图像
  3. 加载风格图像
  4. 随机初始化要生成的图像
  5. 加载VGG16模型
  6. 构建TensorFlow计算图:
    • 通过VGG16模型运行内容图像并计算内容损失
    • 通过VGG16模型运行风格图像并计算风格损失
    • 计算总损失
    • 定义优化器和学习率
  7. 初始化TensorFlow图,并运行大量迭代,然后在每个步骤更新生成的图像。

让我们详细介绍各个步骤。

你之前已经实现了总损失 $J(G)$,我们现在将设置TensorFlow来针对$G$进行优化。 为此,你的程序必须重置计算图并使用”Interactive Session“。与常规会话不同,交互式会话将启动自身作为默认会话以构建计算图。这使你可以运行变量而无需经常引用会话对象,从而简化了代码。

运行代码开始交互式会话
提示
通过创建一个交互式会话,你可以在命令行或 Jupyter Notebook 中逐步运行图中的操作,而不需要每次都创建新的会话

1
2
3
4
5
# 重置计算图
tf.reset_default_graph()

# 启动交互式会话
sess = tf.InteractiveSession()

让我们加载,重塑和标准化我们的“内容”图像(卢浮宫博物馆图片):

1
2
content_image = scipy.misc.imread("images/louvre_small.jpg")
content_image = reshape_and_normalize_image(content_image)

加载,重塑和标准化我们的“风格”图像(克劳德·莫奈的画):

1
2
style_image = scipy.misc.imread("images/monet.jpg")
style_image = reshape_and_normalize_image(style_image)

现在,我们将“生成的”图像初始化作为从content_image创建的噪声图像。通过将生成图像的像素初始化为主要是噪声但仍与内容图像稍微相关的像素,这将有助于生成的图像的内容更快速地匹配“内容”图像的内容。(可以在nst_utils.py中查看generate_noise_image(...)的详细信息;为此,请在此Jupyter笔记本的左上角单击”File—>Open…”)

将“生成的”图像初始化作为从content_image创建的噪声图像

1
2
3
4
5
6
# 生成噪声图像,基于给定的内容图像
generated_image = generate_noise_image(content_image)

# 显示生成的噪声图像(取第一个图像)
imshow(generated_image[0])

接下来,如第(2)部分所述,让我们加载VGG16模型。
运行代码加载VGG16模型

1
model = load_vgg_model("pretrained-model/imagenet-vgg-verydeep-19.mat")

为了获得计算内容损失的程序,我们现在将a_Ca_G分配为适当的隐藏层激活。我们将使用conv4_2层来计算内容损失。
运行下面的代码执行以下操作:

1.将内容图像分配为VGG模型的输入。
2.将a_C设置为张量,为层”conv4_2”提供隐藏层激活。
3.设置a_G为张量,为同一层提供隐藏层激活。
4.使用a_C和a_G计算内容损失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 将内容图像赋值为 VGG 模型的输入
sess.run(model['input'].assign(content_image))

# 选择 conv4_2 层的输出张量
out = model['conv4_2']

# 设置 a_C 为我们选择的层的隐藏层激活值
a_C = sess.run(out)

# 设置 a_G 为同一层的隐藏层激活值。这里,a_G 引用 model['conv4_2']
# 但还没有被计算。稍后的代码中,我们将把图像 G 赋值为模型输入,
# 这样当我们运行会话时,这将是从适当的层中提取的激活值,输入为 G。
a_G = out

# 计算内容损失
J_content = compute_content_cost(a_C, a_G)

注意:此时,a_G是张量,尚未验证。当我们在下面的model_nn()中运行Tensorflow计算图时,它将在每次迭代时进行确认和更新。

1
2
3
4
5
6
# 将模型的输入赋值为“风格”图像
sess.run(model['input'].assign(style_image))

# 计算风格损失
J_style = compute_style_cost(model, STYLE_LAYERS)

练习:现在你有了J_content和J_style,通过调用total_cost()计算总损失J。 使用alpha = 10beta = 40

编写函数计算总损失J

1
2
3
4
5

# 计算总损失 J,包括内容损失和风格损失,使用权重 alpha 和 beta
J = total_cost(J_content, J_style, alpha=10, beta=40)
### 在此处结束代码 ###

你之前已经学习了如何在TensorFlow中设置Adam优化器。我们在这里使用2.0的学习率。 See reference

1
2
3
4
5
6
# 定义优化器
optimizer = tf.train.AdamOptimizer(2.0)

# 定义训练步骤
train_step = optimizer.minimize(J)

练习:实现model_nn()函数,该函数初始化tensorflow计算图的变量,将输入图像(初始生成的图像)作为VGG16模型的输入,并运行train_step进行训练步骤。

实现model_nn()函数
要求

  • 初始化全局变量tf.global_variables_initializer()
  • 将噪声输入图像(初始生成的图像)传递给模型
  • 循环
    • 在train_step上运行会话以最小化总成本
    • 通过运行当前模型[‘input’]计算生成的图像
    • 每20次迭代打印一次Jt, Jc, Js,当前生成的图像保存在“/output”目录中
  • 保存最后生成的图像
  • 返回generated_image最后生成的图像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def model_nn(sess, input_image, num_iterations=200):

# 初始化全局变量(需要在会话中运行初始化器)

sess.run(tf.global_variables_initializer())


# 将噪声输入图像(初始生成的图像)传递给模型。使用assign()方法。

generated_image = sess.run(model['input'].assign(input_image))


for i in range(num_iterations):

# 在train_step上运行会话以最小化总成本

sess.run(train_step)


# 通过运行当前模型['input']计算生成的图像

generated_image = sess.run(model['input'])


# 每20次迭代打印一次。
if i % 20 == 0:
Jt, Jc, Js = sess.run([J, J_content, J_style])
print("Iteration " + str(i) + " :")
print("total cost = " + str(Jt))
print("content cost = " + str(Jc))
print("style cost = " + str(Js))

# 将当前生成的图像保存在“/output”目录中
save_image("output/" + str(i) + ".png", generated_image)

# 保存最后生成的图像
save_image('output/generated_image.jpg', generated_image)

return generated_image

运行以下代码以生成艺术图像。每运行20次迭代在CPU上大约需要3分钟,但是在大约140次迭代后你开始观察到好的结果。通常使用GPU训练神经风格迁移。

1
model_nn(sess, generated_image)

预期输出:
Iteration 0 :
total cost = 5050363000.0
content cost = 7877.685
style cost = 126257096.0

你应该看到下面显示的图像:

我们不想让你等待太久才能看到初始结果,因此已相应地设置了超参数。为了获得最佳效果,较长的优化算法(可能以较小的学习率)运行效果更好。完成并提交此作业后,我们建议你返回并使用此笔记本进行更多操作,看看是否可以生成外观更好的图像。

使用你自己的图像进行测试(可选练习)

最后,你还可以在自己的图像上重新运行算法!

为此,请回到第4部分,并使用你自己的图片更改内容图像和风格图像。以下是你应该执行的操作:

1
2
content_image = scipy.misc.imread("images/louvre.jpg")
style_image = scipy.misc.imread("images/claude-monet.jpg")

到:

1
2
content_image = scipy.misc.imread("images/my_content.jpg")
style_image = scipy.misc.imread("images/my_style.jpg")

你还可以试着调整一下超参数:

  • 哪一层负责表示风格? STYLE_LAYERS
  • 你要运行算法多少次迭代? num_iterations
  • 内容和风格之间的相对权重是多少? alpha / beta

总结

恭喜你出色地完成这项任务!现在,你可以使用“神经风格迁移”生成艺术图像。这也是你第一次建立模型,在该模型中,优化算法将更新像素值而不是神经网络的参数。深度学习有许多不同类型的模型,这只是其中之一!

你应该记住:

  • 神经风格迁移是一种算法,给定内容图像C和风格图像S可以生成艺术图像
  • 它使用基于预训练的ConvNet的特征(隐藏层激活)。
  • 使用一个隐藏层的激活来计算内容损失函数。
  • 使用该层激活的Gram矩阵计算一层的风格损失函数。使用几个隐藏层可以获得整体风格损失函数。
  • 优化总损失函数以合成新图像。

这是本课程的最后编程练习。 恭喜,你已经完成了本课程在卷积网络上的所有编程练习!我们希望能在课程5-序列模型中同样看到你的身影。

参考:

神经风格迁移算法源于Gatys et al. (2015)。 Harish Narayanan和Github用户”log0”也写了很多精湛的文章,我们从中汲取了灵感。此实现中使用的预训练网络是VGG网络,这是Simonyan和Zisserman(2015)的工作成果。预先训练的权重来自MathConvNet团队的工作。

作业10 手把手实现RNN

手把手实现循环神经网络

欢迎来到课程5的第一个作业!在此作业中,你将使用numpy实现你的第一个循环神经网络。
请注意,从本节开始将变得综合也变得极为重要,请你务必亲自敲写代码体会思路,当然我们也会不断的引导你,不必担心
红色部分将提示你手写代码

循环神经网络(RNN)在解决自然语言处理和其他序列任务上非常有效,因为它们具有“记忆”,可以一次读取一个输入 $x^{\langle t \rangle}$(例如单词),并通过从一个时间步传递到下一个时间步的隐藏层激活来记住一些信息/上下文。这使得单向RNN可以提取过去的信息以处理之后的输入。双向RNN则可以借鉴过去和未来的上下文信息。

符号

  • 上标$[l]$表示与$l^{th}$层关联的对象。

    • 例如:$a^{[4]}$是$4^{th}$层激活。 $W^{[5]}$和$b^{[5]}$是$5^{th}$层参数。
  • 上标$(i)$表示与$i^{th}$示例关联的对象。

    • 示例:$x^{(i)}$是$i^{th}$训练示例输入。
  • 上标$\langle t \rangle$表示在$t^{th}$时间步的对象。

    • 示例:$x^{\langle t \rangle}$是在$t^{th}$时间步的输入x。 $x^{(i)\langle t \rangle}$是示例$i$的$t^{th}$时间步的输入。
  • 下标$i$表示向量的$i^{th}$条目。

    • 示例:$a^{[l]}_i$表示层$l$中激活的$i^{th}$条目。

我们假设你已经熟悉numpy或者已经完成了之前的专业课程。让我们开始吧!

首先运行代码导入在作业过程中需要用到的所有软件包。

1
2
3
import numpy as np
import rnn_utils

循环神经网络的正向传播

在本周之后的作业,你将使用RNN生成音乐。你将实现的基本RNN具有以下结构。在此示例中,$T_x = T_y$。

图1 :基础RNN模型

这是实现RNN的方法:

步骤

  1. 实现RNN的一个时间步所需的计算。
  2. 在$T_x$个时间步上实现循环,以便一次处理所有输入。

让我们开始吧!

RNN单元

循环神经网络可以看作是单个cell的重复。你首先要在单个时间步上实现计算。下图描述了RNN单元的单个时间步的操作。

图2:基础RNN单元,将$x^{\langle t \rangle}$(当前输入)和$a^{\langle t - 1\rangle}$(包含过去信息的前隐藏状态)作为输入,并输出$a^{\langle t \rangle}$给下一个RNN单元,用于预测$y^{\langle t \rangle}$

练习:实现图(2)中描述的RNN单元。

说明

  1. 使用tanh激活计算隐藏状态:$a^{\langle t \rangle} = \tanh(W_{aa} a^{\langle t-1 \rangle} + W_{ax} x^{\langle t \rangle} + b_a)$。
  2. 使用新的隐藏状态$a^{\langle t \rangle}$,计算预测$\hat{y}^{\langle t \rangle} = softmax(W_{ya} a^{\langle t \rangle} + b_y)$。我们为你提供了一个函数:softmax
  3. 将$(a^{\langle t \rangle}, a^{\langle t-1 \rangle}, x^{\langle t \rangle}, parameters)$存储在缓存中
  4. 返回$a^{\langle t \rangle}$ , $y^{\langle t \rangle}$并缓存

我们将对$m$个示例进行向量化处理。因此,$x^{\langle t \rangle}$维度将是$(n_x,m)$,而$a^{\langle t \rangle}$维度将是$(n_a,m)$。

请编写函数rnn_cell_forward(xt, a_prev,parameters)实现RNN单元的单步前向传播

要求

  • 根据图2实现RNN单元的单步前向传播
  • 注意维度

  • 参数:

    • xt — 时间步“t”输入的数据,维度为(n_x, m)
    • a_prev — 时间步“t - 1”的隐藏隐藏状态,维度为(n_a, m)
    • parameters — 字典,包含了以下内容:
      • Wax — 矩阵,输入乘以权重,维度为(n_a, n_x)
      • Waa — 矩阵,隐藏状态乘以权重,维度为(n_a, n_a)
      • Wya — 矩阵,隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
      • ba — 偏置,维度为(n_a, 1)
      • by — 偏置,隐藏状态与输出相关的偏置,维度为(n_y, 1)
  • 返回:

    • a_next — 下一个隐藏状态,维度为(n_a, m)
    • yt_pred — 在时间步“t”的预测,维度为(n_y, m)
    • cache — 反向传播需要的元组,包含了(a_next, a_prev, xt, parameters)
  • 可以使用rnn_utils.softmax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def rnn_cell_forward(xt, a_prev, parameters):
"""
根据图2实现RNN单元的单步前向传播

参数:
xt -- 时间步“t”输入的数据,维度为(n_x, m)
a_prev -- 时间步“t - 1”的隐藏隐藏状态,维度为(n_a, m)
parameters -- 字典,包含了以下内容:
Wax -- 矩阵,输入乘以权重,维度为(n_a, n_x)
Waa -- 矩阵,隐藏状态乘以权重,维度为(n_a, n_a)
Wya -- 矩阵,隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
ba -- 偏置,维度为(n_a, 1)
by -- 偏置,隐藏状态与输出相关的偏置,维度为(n_y, 1)

返回:
a_next -- 下一个隐藏状态,维度为(n_a, m)
yt_pred -- 在时间步“t”的预测,维度为(n_y, m)
cache -- 反向传播需要的元组,包含了(a_next, a_prev, xt, parameters)
"""

# 从“parameters”获取参数
Wax = parameters["Wax"]
Waa = parameters["Waa"]
Wya = parameters["Wya"]
ba = parameters["ba"]
by = parameters["by"]

# 使用上面的公式计算下一个激活值
a_next = np.tanh(np.dot(Waa, a_prev) + np.dot(Wax, xt) + ba)

# 使用上面的公式计算当前单元的输出
yt_pred = rnn_utils.softmax(np.dot(Wya, a_next) + by)

# 保存反向传播需要的值
cache = (a_next, a_prev, xt, parameters)

return a_next, yt_pred, cache


运行下面代码进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
Waa = np.random.randn(5,5)
Wax = np.random.randn(5,3)
Wya = np.random.randn(2,5)
ba = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Waa": Waa, "Wax": Wax, "Wya": Wya, "ba": ba, "by": by}

a_next, yt_pred, cache = rnn_cell_forward(xt, a_prev, parameters)
print("a_next[4] = ", a_next[4])
print("a_next.shape = ", a_next.shape)
print("yt_pred[1] =", yt_pred[1])
print("yt_pred.shape = ", yt_pred.shape)

预期输出:
a_next[4] = [ 0.59584544 0.18141802 0.61311866 0.99808218 0.85016201 0.99980978
-0.18887155 0.99815551 0.6531151 0.82872037]
a_next.shape = (5, 10)
yt_pred[1] = [0.9888161 0.01682021 0.21140899 0.36817467 0.98988387 0.88945212
0.36920224 0.9966312 0.9982559 0.17746526]
yt_pred.shape = (2, 10)

RNN正向传播

你可以将RNN视为刚刚构建的单元的重复。如果输入的数据序列经过10个时间步长,则将复制RNN单元10次。每个单元格都将前一个单元格($a^{\langle t-1 \rangle}$)的隐藏状态和当前时间步的输入数据($x^{\langle t \rangle}$)作为输入,并为此时间步输出隐藏状态($a^{\langle t \rangle}$) 和预测($y^{\langle t \rangle}$)。

图3:基本RNN。输入序列$x = (x^{\langle 1 \rangle}, x^{\langle 2 \rangle}, …, x^{\langle T_x \rangle})$执行$T_x$个时间步。网络输出$y = (y^{\langle 1 \rangle}, y^{\langle 2 \rangle}, …, y^{\langle T_x \rangle})$。

练习:编码实现图(3)中描述的RNN的正向传播。
编写函数rnn_forward(x,a0,parameters)实现RNN的正向传播

提示

  1. 创建一个零向量($a$),该向量将存储RNN计算的所有隐藏状态。
  2. 将“下一个”隐藏状态初始化为$a_0$(初始隐藏状态)。
  3. 开始遍历每个时间步,增量索引为$t$:
    • 通过运行rnn_step_forward更新“下一个”隐藏状态和缓存。
    • 将“下一个”隐藏状态存储在$a$中($t^{th}$位置)
    • 将预测存储在y中
    • 将缓存添加到缓存列表中
  4. 返回$a$,$y$和缓存

要求

  • 根据图3来实现循环神经网络的前向传播

  • 参数:

    • x — 输入的全部数据,维度为(n_x, m, T_x)
    • a0 — 初始化隐藏状态,维度为 (n_a, m)
    • parameters — 字典,包含了以下内容:
      • Wax — 矩阵,输入乘以权重,维度为(n_a, n_x)
      • Waa — 矩阵,隐藏状态乘以权重,维度为(n_a, n_a)
      • Wya — 矩阵,隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
      • ba — 偏置,维度为(n_a, 1)
      • by — 偏置,隐藏状态与输出相关的偏置,维度为(n_y, 1)
  • 返回:

    • a — 所有时间步的隐藏状态,维度为(n_a, m, T_x)
    • y_pred — 所有时间步的预测,维度为(n_y, m, T_x)
    • caches — 为反向传播的保存的元组,维度为(【列表类型】cache, x))

步骤

  • 初始化“caches”,它将以列表类型包含所有的cache
  • 通过 x 与 Wya 的维度信息获取n_x, m, T_x, n_y, n_a
  • 使用0来初始化“a” 与“y”
  • 初始化“a_next”为a0作为每一层(时间步)的激活传递值
  • 遍历所有时间步
    • 使用rnn_cell_forward函数来更新“a_next”隐藏状态与cache
      • 注意你的输入是x[:, :, t]每个时间步的所有数据才是当前步的x,第一步的输入数据是x[:, :, 0]
      • 用a_next, yt_pred, cache接收
    • 使用 a 来保存“a_next”隐藏状态(第 t )步(层)的值。
      • 提示a[:, :, t]
    • 使用 y 来保存预测值第 t )步(层)的值
  • 保存反向传播所需要的参数caches = (caches, x)
  • 返回a, y_pred, caches
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def rnn_forward(x, a0, parameters):
"""
根据图3来实现循环神经网络的前向传播

参数:
x -- 输入的全部数据,维度为(n_x, m, T_x)
a0 -- 初始化隐藏状态,维度为 (n_a, m)
parameters -- 字典,包含了以下内容:
Wax -- 矩阵,输入乘以权重,维度为(n_a, n_x)
Waa -- 矩阵,隐藏状态乘以权重,维度为(n_a, n_a)
Wya -- 矩阵,隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
ba -- 偏置,维度为(n_a, 1)
by -- 偏置,隐藏状态与输出相关的偏置,维度为(n_y, 1)

返回:
a -- 所有时间步的隐藏状态,维度为(n_a, m, T_x)
y_pred -- 所有时间步的预测,维度为(n_y, m, T_x)
caches -- 为反向传播的保存的列表,维度为(【列表类型】cache, x))
"""

# 初始化“caches”,它将以列表类型包含所有的cache
caches = []

# 获取 x 与 Wya 的维度信息
n_x, m, T_x = x.shape
n_y, n_a = parameters["Wya"].shape

# 使用0来初始化“a” 与“y”
a = np.zeros([n_a, m, T_x])
y_pred = np.zeros([n_y, m, T_x])

# 初始化“next”
a_next = a0

# 遍历所有时间步
for t in range(T_x):
## 1.使用rnn_cell_forward函数来更新“next”隐藏状态与cache。
a_next, yt_pred, cache = rnn_cell_forward(x[:, :, t], a_next, parameters)

## 2.使用 a 来保存“next”隐藏状态(第 t )个位置。
a[:, :, t] = a_next

## 3.使用 y 来保存预测值。
y_pred[:, :, t] = yt_pred

## 4.把cache保存到“caches”列表中。
caches.append(cache)

# 保存反向传播所需要的参数
caches = (caches, x)

return a, y_pred, caches

运行代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
np.random.seed(1)
x = np.random.randn(3,10,4)
a0 = np.random.randn(5,10)
Waa = np.random.randn(5,5)
Wax = np.random.randn(5,3)
Wya = np.random.randn(2,5)
ba = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Waa": Waa, "Wax": Wax, "Wya": Wya, "ba": ba, "by": by}

a, y_pred, caches = rnn_forward(x, a0, parameters)
print("a[4][1] = ", a[4][1])
print("a.shape = ", a.shape)
print("y_pred[1][3] =", y_pred[1][3])
print("y_pred.shape = ", y_pred.shape)
print("caches[1][1][3] =", caches[1][1][3])
print("len(caches) = ", len(caches))

预期输出:
a[4][1] = [-0.99999375 0.77911235 -0.99861469 -0.99833267]
a.shape = (5, 10, 4)
y_pred[1][3] = [0.79560373 0.86224861 0.11118257 0.81515947]
y_pred.shape = (2, 10, 4)
caches[1][1][3] = [-1.1425182 -0.34934272 -0.20889423 0.58662319]
len(caches) = 2

Nice!你已经从头实现了循环神经网络的正向传播。对于某些应用来说,这已经足够好,但是会遇到梯度消失的问题。因此,当每个输出$y^{\langle t \rangle}$主要使用”local”上下文进行估算时它表现最好(即来自输入$x^{\langle t’ \rangle}$的信息,其中$t’$是距离$t$较近)。

在下一部分中,你将构建一个更复杂的LSTM模型,该模型更适合解决逐渐消失的梯度。LSTM将能够更好地记住一条信息并将其保存许多个时间步。

长短期记忆网络(LSTM)

下图显示了LSTM单元的运作。

图4:LSTM单元,这会在每个时间步上跟踪并更新“单元状态”或存储的变量$c^{\langle t \rangle}$,
与$a^{\langle t \rangle}$不同。
与上面的RNN示例类似,你将以单个时间步开始实现LSTM单元。然后,你可以从for循环内部迭代调用它,以使其具有$T_x$时间步长的输入。

关于门

下面将介绍LSTM中的几个门

遗忘门

为了便于说明,假设我们正在阅读一段文本中的单词,并希望使用LSTM跟踪语法结构,例如主体是单数还是复数。如果主体从单数变为复数,我们需要找到一种方法来摆脱以前存储的单/复数状态的内存值。在LSTM中,遗忘门可以实现这样的操作:

在这里,$W_f$是控制遗忘门行为的权重。我们将$[a^{\langle t-1 \rangle}, x^{\langle t \rangle}]$连接起来,然后乘以$W_f$。上面的等式使得向量$\Gamma_f^{\langle t \rangle}$的值介于0到1之间。该遗忘门向量将逐元素乘以先前的单元状态$c^{\langle t-1 \rangle}$。因此,如果$\Gamma_f^{\langle t \rangle}$的其中一个值为0(或接近于0),则表示LSTM应该移除$c^{\langle t-1 \rangle}$组件中的一部分信息(例如,单数主题),如果其中一个值为1,则它将保留信息。

更新门

一旦我们忘记了所讨论的主体是单数,就需要找到一种更新它的方式,以反映新主体现在是复数。这是更新门的公式:

类似于遗忘门,在这里$\Gamma_u^{\langle t \rangle}$也是值为0到1之间的向量。这将与$\tilde{c}^{\langle t \rangle}$逐元素相乘以计算$c^{\langle t \rangle}$。

更新单元格

要更新新主体,我们需要创建一个新的数字向量,可以将其添加到先前的单元格状态中。我们使用的等式是:

最后,新的单元状态为:

输出门

为了确定我们将使用哪些输出,我们将使用以下两个公式:

在等式5中,你决定使用sigmoid函数输出;在等式6中,将其乘以先前状态的$\tanh$。

LSTM单元

练习:实现图(4)中描述的LSTM单元。

提示

  1. 将$a^{\langle t-1 \rangle}$和$x^{\langle t \rangle}$连接在一个矩阵中:$concat = \begin{bmatrix} a^{\langle t-1 \rangle} \\ x^{\langle t \rangle} \end{bmatrix}$
  2. 计算公式1-6,你可以使用sigmoid()np.tanh()
  3. 计算预测$y^{\langle t \rangle}$,你可以使用softmax()

现在请编写函数lstm_cell_forward(xt,a_prev, c_prev, parameters)实现一个LSTM单元的前向传播

要求

  • 根据图4实现一个LSTM单元的前向传播。

  • 参数:

    • xt — 在时间步“t”输入的数据,维度为(n_x, m)
    • a_prev — 上一个时间步“t-1”的隐藏状态,维度为(n_a, m)
    • c_prev — 上一个时间步“t-1”的记忆状态,维度为(n_a, m)
    • parameters — 字典类型的变量,包含了:
      • Wf — 遗忘门的权值,维度为(n_a, n_a + n_x)
      • bf — 遗忘门的偏置,维度为(n_a, 1)
      • Wi — 更新门的权值,维度为(n_a, n_a + n_x)
      • bi — 更新门的偏置,维度为(n_a, 1)
      • Wc — 第一个“tanh”的权值,维度为(n_a, n_a + n_x)
      • bc — 第一个“tanh”的偏置,维度为(n_a, n_a + n_x)
      • Wo — 输出门的权值,维度为(n_a, n_a + n_x)
      • bo — 输出门的偏置,维度为(n_a, 1)
      • Wy — 隐藏状态与输出相关的权值,维度为(n_y, n_a)
      • by — 隐藏状态与输出相关的偏置,维度为(n_y, 1)
  • 返回:

    • a_next — 下一个隐藏状态,维度为(n_a, m)
    • c_next — 下一个记忆状态,维度为(n_a, m)
    • yt_pred — 在时间步“t”的预测,维度为(n_y, m)
    • cache — 包含了反向传播所需要的参数,包含了(a_next, c_next, a_prev, c_prev, xt, parameters)
  • 注意:

    • ft/it/ot表示遗忘/更新/输出门,cct表示候选值(c tilda),c表示记忆值。
    • 可以使用rnn_utils.sigmoid
      步骤
  • 从parameters中获取相关值
  • 通过 xt 与 Wy 的维度信息获得n_x, m, n_y, n_a
  • 连接 a_prev 与 xt
    • 创建连接矩阵时注意前n_a行时a_prev的值因为a_prev有n_a行,第n_a行后时xt的值
      • contact[: n_a, :] = a_prev
      • contact[n_a :, :] = xt
  • 根据公式计算ft、it、cct、c_next、ot、a_next
  • 计算LSTM单元的预测值
  • 保存包含反向传播所需要的参数cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def lstm_cell_forward(xt, a_prev, c_prev, parameters):
"""
根据图4实现一个LSTM单元的前向传播。

参数:
xt -- 在时间步“t”输入的数据,维度为(n_x, m)
a_prev -- 上一个时间步“t-1”的隐藏状态,维度为(n_a, m)
c_prev -- 上一个时间步“t-1”的记忆状态,维度为(n_a, m)
parameters -- 字典类型的变量,包含了:
Wf -- 遗忘门的权值,维度为(n_a, n_a + n_x)
bf -- 遗忘门的偏置,维度为(n_a, 1)
Wi -- 更新门的权值,维度为(n_a, n_a + n_x)
bi -- 更新门的偏置,维度为(n_a, 1)
Wc -- 第一个“tanh”的权值,维度为(n_a, n_a + n_x)
bc -- 第一个“tanh”的偏置,维度为(n_a, n_a + n_x)
Wo -- 输出门的权值,维度为(n_a, n_a + n_x)
bo -- 输出门的偏置,维度为(n_a, 1)
Wy -- 隐藏状态与输出相关的权值,维度为(n_y, n_a)
by -- 隐藏状态与输出相关的偏置,维度为(n_y, 1)
返回:
a_next -- 下一个隐藏状态,维度为(n_a, m)
c_next -- 下一个记忆状态,维度为(n_a, m)
yt_pred -- 在时间步“t”的预测,维度为(n_y, m)
cache -- 包含了反向传播所需要的参数,包含了(a_next, c_next, a_prev, c_prev, xt, parameters)

注意:
ft/it/ot 表示遗忘/更新/输出门,cct表示候选值(c tilda),c表示记忆值。
图中的Fi和Fu是一个东西
"""

# 从“parameters”中获取相关值
Wf = parameters["Wf"]
bf = parameters["bf"]
Wi = parameters["Wi"]
bi = parameters["bi"]
Wc = parameters["Wc"]
bc = parameters["bc"]
Wo = parameters["Wo"]
bo = parameters["bo"]
Wy = parameters["Wy"]
by = parameters["by"]

# 获取 xt 与 Wy 的维度信息
n_x, m = xt.shape
n_y, n_a = Wy.shape

# 1.连接 a_prev 与 xt
contact = np.zeros([n_a + n_x, m])
contact[: n_a, :] = a_prev
contact[n_a :, :] = xt

# 2.根据公式计算ft、it、cct、c_next、ot、a_next

## 遗忘门,公式1
ft = rnn_utils.sigmoid(np.dot(Wf, contact) + bf)

## 更新门,公式2
it = rnn_utils.sigmoid(np.dot(Wi, contact) + bi)

## 更新单元,公式3
cct = np.tanh(np.dot(Wc, contact) + bc)

## 更新单元,公式4
#c_next = np.multiply(ft, c_prev) + np.multiply(it, cct)
c_next = ft * c_prev + it * cct
## 输出门,公式5
ot = rnn_utils.sigmoid(np.dot(Wo, contact) + bo)

## 输出门,公式6
#a_next = np.multiply(ot, np.tan(c_next))
a_next = ot * np.tanh(c_next)
# 3.计算LSTM单元的预测值
yt_pred = rnn_utils.softmax(np.dot(Wy, a_next) + by)

# 保存包含了反向传播所需要的参数
cache = (a_next, c_next, a_prev, c_prev, ft, it, cct, ot, xt, parameters)

return a_next, c_next, yt_pred, cache

运行下面的代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
c_prev = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)
Wy = np.random.randn(2,5)
by = np.random.randn(2,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a_next, c_next, yt, cache = lstm_cell_forward(xt, a_prev, c_prev, parameters)
print("a_next[4] = ", a_next[4])
print("a_next.shape = ", c_next.shape)
print("c_next[2] = ", c_next[2])
print("c_next.shape = ", c_next.shape)
print("yt[1] =", yt[1])
print("yt.shape = ", yt.shape)
print("cache[1][3] =", cache[1][3])
print("len(cache) = ", len(cache))

预期输出:
a_next[4] = [-0.66408471 0.0036921 0.02088357 0.22834167 -0.85575339 0.00138482
0.76566531 0.34631421 -0.00215674 0.43827275]
a_next.shape = (5, 10)
c_next[2] = [ 0.63267805 1.00570849 0.35504474 0.20690913 -1.64566718 0.11832942
0.76449811 -0.0981561 -0.74348425 -0.26810932]
c_next.shape = (5, 10)
yt[1] = [0.79913913 0.15986619 0.22412122 0.15606108 0.97057211 0.31146381
0.00943007 0.12666353 0.39380172 0.07828381]
yt.shape = (2, 10)
cache[1][3] = [-0.16263996 1.03729328 0.72938082 -0.54101719 0.02752074 -0.30821874
0.07651101 -1.03752894 1.41219977 -0.37647422]
len(cache) = 10

LSTM的正向传播

既然你已经实现了LSTM的一个步骤,现在就可以使用for循环在$T_x$输入序列上对此进行迭代。

图4:多个时间步的LSTM。

练习:实现lstm_forward()以在$T_x$个时间步上运行LSTM。

注意:$c^{\langle 0 \rangle}$用零初始化。

编写函数lstm_forward(x,a0,parameters)实现LSTM单元组成的的循环神经网络
要求

  • 根据图5来实现LSTM单元组成的的循环神经网络(与RNN相似,写的时候联想)

  • 参数:

    • x — 所有时间步的输入数据,维度为(n_x, m, T_x)
    • a0 — 初始化隐藏状态,维度为(n_a, m)
    • parameters — python字典,包含了以下参数:
      • Wf — 遗忘门的权值,维度为(n_a, n_a + n_x)
      • bf — 遗忘门的偏置,维度为(n_a, 1)
      • Wi — 更新门的权值,维度为(n_a, n_a + n_x)
      • bi — 更新门的偏置,维度为(n_a, 1)
      • Wc — 第一个“tanh”的权值,维度为(n_a, n_a + n_x)
      • bc — 第一个“tanh”的偏置,维度为(n_a, n_a + n_x)
      • Wo — 输出门的权值,维度为(n_a, n_a + n_x)
      • bo — 输出门的偏置,维度为(n_a, 1)
      • Wy — 隐藏状态与输出相关的权值,维度为(n_y, n_a)
      • by — 隐藏状态与输出相关的偏置,维度为(n_y, 1)
  • 返回:

    • a — 所有时间步的隐藏状态,维度为(n_a, m, T_x)
    • y — 所有时间步的预测值,维度为(n_y, m, T_x)
    • caches — 为反向传播的保存的元组,维度为(【列表类型】cache, x))

步骤

  • 初始化“caches”列表
  • 通过 xt 与 Wy 的维度信息获得n_x, m, T_x, n_y, n_a
  • 使用0来初始化“a”、“c”、“y”
  • 初始化“a_next”、“c_next”
  • 遍历所有的时间步
    • 使用lstm_cell_forward更新下一个隐藏状态,下一个记忆状态,计算预测值,获取cache
    • 用a_next, c_next, yt_pred, cache接收
    • 保存新的下一个隐藏状态到变量a中
    • 保存预测值到变量y中
    • 保存下一个单元状态到变量c中
    • 把cache添加到caches中
  • return a, y, c, caches
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def lstm_forward(x, a0, parameters):
"""
根据图5来实现LSTM单元组成的的循环神经网络

参数:
x -- 所有时间步的输入数据,维度为(n_x, m, T_x)
a0 -- 初始化隐藏状态,维度为(n_a, m)
parameters -- python字典,包含了以下参数:
Wf -- 遗忘门的权值,维度为(n_a, n_a + n_x)
bf -- 遗忘门的偏置,维度为(n_a, 1)
Wi -- 更新门的权值,维度为(n_a, n_a + n_x)
bi -- 更新门的偏置,维度为(n_a, 1)
Wc -- 第一个“tanh”的权值,维度为(n_a, n_a + n_x)
bc -- 第一个“tanh”的偏置,维度为(n_a, n_a + n_x)
Wo -- 输出门的权值,维度为(n_a, n_a + n_x)
bo -- 输出门的偏置,维度为(n_a, 1)
Wy -- 隐藏状态与输出相关的权值,维度为(n_y, n_a)
by -- 隐藏状态与输出相关的偏置,维度为(n_y, 1)

返回:
a -- 所有时间步的隐藏状态,维度为(n_a, m, T_x)
y -- 所有时间步的预测值,维度为(n_y, m, T_x)
caches -- 为反向传播的保存的列表,维度为(【列表类型】cache, x))
"""

# 初始化“caches”
caches = []

# 获取 xt 与 Wy 的维度信息
n_x, m, T_x = x.shape
n_y, n_a = parameters["Wy"].shape

# 使用0来初始化“a”、“c”、“y”
a = np.zeros([n_a, m, T_x])
c = np.zeros([n_a, m, T_x])
y = np.zeros([n_y, m, T_x])

# 初始化“a_next”、“c_next”
a_next = a0
c_next = np.zeros([n_a, m])

# 遍历所有的时间步
for t in range(T_x):
# 更新下一个隐藏状态,下一个记忆状态,计算预测值,获取cache
a_next, c_next, yt_pred, cache = lstm_cell_forward(x[:,:,t], a_next, c_next, parameters)

# 保存新的下一个隐藏状态到变量a中
a[:, :, t] = a_next

# 保存预测值到变量y中
y[:, :, t] = yt_pred

# 保存下一个单元状态到变量c中
c[:, :, t] = c_next

# 把cache添加到caches中
caches.append(cache)

# 保存反向传播需要的参数
caches = (caches, x)

return a, y, c, caches

运行代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
np.random.seed(1)
x = np.random.randn(3,10,7)
a0 = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)
Wy = np.random.randn(2,5)
by = np.random.randn(2,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a, y, c, caches = lstm_forward(x, a0, parameters)
print("a[4][3][6] = ", a[4][3][6])
print("a.shape = ", a.shape)
print("y[1][4][3] =", y[1][4][3])
print("y.shape = ", y.shape)
print("caches[1][1[1]] =", caches[1][1][1])
print("c[1][2][1]", c[1][2][1])
print("len(caches) = ", len(caches))

预期输出:
a[4][3][6] = 0.17211776753291672
a.shape = (5, 10, 7)
y[1][4][3] = 0.9508734618501101
y.shape = (2, 10, 7)
caches[1][1[1]] = [ 0.82797464 0.23009474 0.76201118 -0.22232814 -0.20075807 0.18656139
0.41005165]
c[1][2][1] -0.8555449167181981
len(caches) = 2

Nice!现在,你已经为基础RNN和LSTM实现了正向传播。使用深度学习框架时,实现正向传播以足以构建性能出色的系统。

循环神经网络中的反向传播(可选练习)

不需要的话就不用看反向传播了,有的复杂
在现代深度学习框架中,你仅需实现正向传播,而框架将处理反向传播,因此大多数深度学习工程师无需理会反向传播的细节。但是,如果你是微积分专家并且想查看RNN中反向传播的详细信息,可以学习此笔记本的剩余部分。

在较早的课程中,当你实现了一个简单的(全连接的)神经网络时,你就使用了反向传播来计算用于更新参数的损失的导数。同样,在循环神经网络中,你可以计算损失的导数以更新参数。反向传播方程非常复杂,我们在讲座中没有导出它们。但是,我们将在下面简要介绍它们。

基础RNN的反向传播

我们将从计算基本RNN单元的反向传播开始。

图5:RNN单元的反向传播。就像在全连接的神经网络中一样,损失函数$J$的导数遵循链规则在RNN中计算反向传播。链规则还用于计算$(\frac{\partial J}{\partial W_{ax}},\frac{\partial J}{\partial W_{aa}},\frac{\partial J}{\partial b})$更新参数$(W_{ax}, W_{aa}, b_a)$。

反向求导函数:

要计算rnn_cell_backward,你需要计算以下方程式。手工导出它们是一个很好的练习。

$\tanh$的导数为$1-\tanh(x)^2$。你可以在here中找到完整的证明。请注意:$\sec(x)^2 = 1 - \tanh(x)^2$

同样,对于$\frac{ \partial a^{\langle t \rangle} } {\partial W_{ax}}, \frac{ \partial a^{\langle t \rangle} } {\partial W_{aa}}, \frac{ \partial a^{\langle t \rangle} } {\partial b}$,$\tanh(u)$导数为$(1-\tanh(u)^2)du$。

最后两个方程式也遵循相同的规则,并使用 $\tanh$ 导数导出。请注意,这种安排是为了获得相同的维度以方便匹配的。

rnn_cell_backward基本的RNN单元的单步反向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def rnn_cell_backward(da_next, cache):
"""
实现基本的RNN单元的单步反向传播

参数:
da_next -- 关于下一个隐藏状态的损失的梯度。
cache -- 字典类型,rnn_step_forward()的输出

返回:
gradients -- 字典,包含了以下参数:
dx -- 输入数据的梯度,维度为(n_x, m)
da_prev -- 上一隐藏层的隐藏状态,维度为(n_a, m)
dWax -- 输入到隐藏状态的权重的梯度,维度为(n_a, n_x)
dWaa -- 隐藏状态到隐藏状态的权重的梯度,维度为(n_a, n_a)
dba -- 偏置向量的梯度,维度为(n_a, 1)
"""
# 获取cache 的值
a_next, a_prev, xt, parameters = cache

# 从 parameters 中获取参数
Wax = parameters["Wax"]
Waa = parameters["Waa"]
Wya = parameters["Wya"]
ba = parameters["ba"]
by = parameters["by"]

# 计算tanh相对于a_next的梯度.
dtanh = (1 - np.square(a_next)) * da_next

# 计算关于Wax损失的梯度
dxt = np.dot(Wax.T,dtanh)
dWax = np.dot(dtanh, xt.T)

# 计算关于Waa损失的梯度
da_prev = np.dot(Waa.T,dtanh)
dWaa = np.dot(dtanh, a_prev.T)

# 计算关于b损失的梯度
dba = np.sum(dtanh, keepdims=True, axis=-1)

# 保存这些梯度到字典内
gradients = {"dxt": dxt, "da_prev": da_prev, "dWax": dWax, "dWaa": dWaa, "dba": dba}

return gradients

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
Wax = np.random.randn(5,3)
Waa = np.random.randn(5,5)
Wya = np.random.randn(2,5)
b = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "ba": ba, "by": by}

a_next, yt, cache = rnn_cell_forward(xt, a_prev, parameters)

da_next = np.random.randn(5,10)
gradients = rnn_cell_backward(da_next, cache)
print("gradients[\"dxt\"][1][2] =", gradients["dxt"][1][2])
print("gradients[\"dxt\"].shape =", gradients["dxt"].shape)
print("gradients[\"da_prev\"][2][3] =", gradients["da_prev"][2][3])
print("gradients[\"da_prev\"].shape =", gradients["da_prev"].shape)
print("gradients[\"dWax\"][3][1] =", gradients["dWax"][3][1])
print("gradients[\"dWax\"].shape =", gradients["dWax"].shape)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("gradients[\"dWaa\"].shape =", gradients["dWaa"].shape)
print("gradients[\"dba\"][4] =", gradients["dba"][4])
print("gradients[\"dba\"].shape =", gradients["dba"].shape)

预期输出:
gradients[“dxt”][1][2] = -0.4605641030588796
gradients[“dxt”].shape = (3, 10)
gradients[“da_prev”][2][3] = 0.08429686538067724
gradients[“da_prev”].shape = (5, 10)
gradients[“dWax”][3][1] = 0.39308187392193034
gradients[“dWax”].shape = (5, 3)
gradients[“dWaa”][1][2] = -0.28483955786960663
gradients[“dWaa”].shape = (5, 5)
gradients[“dba”][4] = [0.80517166]
gradients[“dba”].shape = (5, 1)

反向传播

在每个时间步长$t$上计算相对于$a^{\langle t \rangle}$的损失梯度非常有用,因为它有助于将梯度反向传播到先前的RNN单元。为此,你需要从头开始遍历所有时间步,并且在每一步中,增加总的$db_a$, $dW_{aa}$, $dW_{ax}$并存储$dx$ 。

说明

实现rnn_backward函数。首先用零初始化返回变量,然后循环遍历所有时间步,同时在每个时间步调用rnn_cell_backward,相应地更新其他变量。

rnn_backward整个输入数据序列上实现RNN的反向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def rnn_backward(da, caches):
"""
在整个输入数据序列上实现RNN的反向传播

参数:
da -- 所有隐藏状态的梯度,维度为(n_a, m, T_x)
caches -- 包含向前传播的信息的元组

返回:
gradients -- 包含了梯度的字典:
dx -- 关于输入数据的梯度,维度为(n_x, m, T_x)
da0 -- 关于初始化隐藏状态的梯度,维度为(n_a, m)
dWax -- 关于输入权重的梯度,维度为(n_a, n_x)
dWaa -- 关于隐藏状态的权值的梯度,维度为(n_a, n_a)
dba -- 关于偏置的梯度,维度为(n_a, 1)
"""
# 从caches中获取第一个cache(t=1)的值
caches, x = caches
a1, a0, x1, parameters = caches[0]

# 获取da与x1的维度信息
n_a, m, T_x = da.shape
n_x, m = x1.shape

# 初始化梯度
dx = np.zeros([n_x, m, T_x])
dWax = np.zeros([n_a, n_x])
dWaa = np.zeros([n_a, n_a])
dba = np.zeros([n_a, 1])
da0 = np.zeros([n_a, m])
da_prevt = np.zeros([n_a, m])

# 处理所有时间步
for t in reversed(range(T_x)):
# 计算时间步“t”时的梯度
gradients = rnn_cell_backward(da[:, :, t] + da_prevt, caches[t])

#从梯度中获取导数
dxt, da_prevt, dWaxt, dWaat, dbat = gradients["dxt"], gradients["da_prev"], gradients["dWax"], gradients["dWaa"], gradients["dba"]

# 通过在时间步t添加它们的导数来增加关于全局导数的参数
dx[:, :, t] = dxt
dWax += dWaxt
dWaa += dWaat
dba += dbat

#将 da0设置为a的梯度,该梯度已通过所有时间步骤进行反向传播
da0 = da_prevt

#保存这些梯度到字典内
gradients = {"dx": dx, "da0": da0, "dWax": dWax, "dWaa": dWaa,"dba": dba}

return gradients

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
np.random.seed(1)
x = np.random.randn(3,10,4)
a0 = np.random.randn(5,10)
Wax = np.random.randn(5,3)
Waa = np.random.randn(5,5)
Wya = np.random.randn(2,5)
ba = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "ba": ba, "by": by}
a, y, caches = rnn_forward(x, a0, parameters)
da = np.random.randn(5, 10, 4)
gradients = rnn_backward(da, caches)

print("gradients[\"dx\"][1][2] =", gradients["dx"][1][2])
print("gradients[\"dx\"].shape =", gradients["dx"].shape)
print("gradients[\"da0\"][2][3] =", gradients["da0"][2][3])
print("gradients[\"da0\"].shape =", gradients["da0"].shape)
print("gradients[\"dWax\"][3][1] =", gradients["dWax"][3][1])
print("gradients[\"dWax\"].shape =", gradients["dWax"].shape)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("gradients[\"dWaa\"].shape =", gradients["dWaa"].shape)
print("gradients[\"dba\"][4] =", gradients["dba"][4])
print("gradients[\"dba\"].shape =", gradients["dba"].shape)

预期输出:
gradients[“dx”][1][2] = [-2.07101689 -0.59255627 0.02466855 0.01483317]
gradients[“dx”].shape = (3, 10, 4)
gradients[“da0”][2][3] = -0.31494237512664996
gradients[“da0”].shape = (5, 10)
gradients[“dWax”][3][1] = 11.264104496527777
gradients[“dWax”].shape = (5, 3)
gradients[“dWaa”][1][2] = 2.303333126579893
gradients[“dWaa”].shape = (5, 5)
gradients[“dba”][4] = [-0.74747722]
gradients[“dba”].shape = (5, 1)

LSTM反向传播

反向传播一步

LSTM反向传播比正向传播要复杂得多。我们在下面为你提供了LSTM反向传播的所有方程式。(如果你喜欢微积分练习,可以尝试从头开始自己演算)

门求导

参数求导

要计算$db_f, db_u, db_c, db_o$,你只需要在$d\Gamma_f^{\langle t \rangle}, d\Gamma_u^{\langle t \rangle}, d\tilde c^{\langle t \rangle}, d\Gamma_o^{\langle t \rangle}$的水平(axis=1)轴上分别求和。注意,你应该有keep_dims = True选项。

最后,你将针对先前的隐藏状态,先前的记忆状态和输入计算导数。

在这里,等式13的权重是第n_a个(即$W_f = W_f[:n_a,:]$ 等…)

其中等式15的权重是从n_a到末尾(即$W_f = W_f[n_a:,:]$ etc…)

练习:通过实现下面的等式$7-17$来实现lstm_cell_backward。祝好运!

lstm_cell_backward实现LSTM的单步反向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def lstm_cell_backward(da_next, dc_next, cache):
"""
实现LSTM的单步反向传播

参数:
da_next -- 下一个隐藏状态的梯度,维度为(n_a, m)
dc_next -- 下一个单元状态的梯度,维度为(n_a, m)
cache -- 来自前向传播的一些参数

返回:
gradients -- 包含了梯度信息的字典:
dxt -- 输入数据的梯度,维度为(n_x, m)
da_prev -- 先前的隐藏状态的梯度,维度为(n_a, m)
dc_prev -- 前的记忆状态的梯度,维度为(n_a, m, T_x)
dWf -- 遗忘门的权值的梯度,维度为(n_a, n_a + n_x)
dbf -- 遗忘门的偏置的梯度,维度为(n_a, 1)
dWi -- 更新门的权值的梯度,维度为(n_a, n_a + n_x)
dbi -- 更新门的偏置的梯度,维度为(n_a, 1)
dWc -- 第一个“tanh”的权值的梯度,维度为(n_a, n_a + n_x)
dbc -- 第一个“tanh”的偏置的梯度,维度为(n_a, n_a + n_x)
dWo -- 输出门的权值的梯度,维度为(n_a, n_a + n_x)
dbo -- 输出门的偏置的梯度,维度为(n_a, 1)
"""
# 从cache中获取信息
(a_next, c_next, a_prev, c_prev, ft, it, cct, ot, xt, parameters) = cache

# 获取xt与a_next的维度信息
n_x, m = xt.shape
n_a, m = a_next.shape

# 根据公式7-10来计算门的导数
dot = da_next * np.tanh(c_next) * ot * (1 - ot)
dcct = (dc_next * it + ot * (1 - np.square(np.tanh(c_next))) * it * da_next) * (1 - np.square(cct))
dit = (dc_next * cct + ot * (1 - np.square(np.tanh(c_next))) * cct * da_next) * it * (1 - it)
dft = (dc_next * c_prev + ot * (1 - np.square(np.tanh(c_next))) * c_prev * da_next) * ft * (1 - ft)

# 根据公式11-14计算参数的导数
concat = np.concatenate((a_prev, xt), axis=0).T
dWf = np.dot(dft, concat)
dWi = np.dot(dit, concat)
dWc = np.dot(dcct, concat)
dWo = np.dot(dot, concat)
dbf = np.sum(dft,axis=1,keepdims=True)
dbi = np.sum(dit,axis=1,keepdims=True)
dbc = np.sum(dcct,axis=1,keepdims=True)
dbo = np.sum(dot,axis=1,keepdims=True)


# 使用公式15-17计算洗起来了隐藏状态、先前记忆状态、输入的导数。
da_prev = np.dot(parameters["Wf"][:, :n_a].T, dft) + np.dot(parameters["Wc"][:, :n_a].T, dcct) + np.dot(parameters["Wi"][:, :n_a].T, dit) + np.dot(parameters["Wo"][:, :n_a].T, dot)

dc_prev = dc_next * ft + ot * (1 - np.square(np.tanh(c_next))) * ft * da_next

dxt = np.dot(parameters["Wf"][:, n_a:].T, dft) + np.dot(parameters["Wc"][:, n_a:].T, dcct) + np.dot(parameters["Wi"][:, n_a:].T, dit) + np.dot(parameters["Wo"][:, n_a:].T, dot)

# 保存梯度信息到字典
gradients = {"dxt": dxt, "da_prev": da_prev, "dc_prev": dc_prev, "dWf": dWf,"dbf": dbf, "dWi": dWi,"dbi": dbi,
"dWc": dWc,"dbc": dbc, "dWo": dWo,"dbo": dbo}

return gradients

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
c_prev = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)
Wy = np.random.randn(2,5)
by = np.random.randn(2,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a_next, c_next, yt, cache = lstm_cell_forward(xt, a_prev, c_prev, parameters)

da_next = np.random.randn(5,10)
dc_next = np.random.randn(5,10)
gradients = lstm_cell_backward(da_next, dc_next, cache)
print("gradients[\"dxt\"][1][2] =", gradients["dxt"][1][2])
print("gradients[\"dxt\"].shape =", gradients["dxt"].shape)
print("gradients[\"da_prev\"][2][3] =", gradients["da_prev"][2][3])
print("gradients[\"da_prev\"].shape =", gradients["da_prev"].shape)
print("gradients[\"dc_prev\"][2][3] =", gradients["dc_prev"][2][3])
print("gradients[\"dc_prev\"].shape =", gradients["dc_prev"].shape)
print("gradients[\"dWf\"][3][1] =", gradients["dWf"][3][1])
print("gradients[\"dWf\"].shape =", gradients["dWf"].shape)
print("gradients[\"dWi\"][1][2] =", gradients["dWi"][1][2])
print("gradients[\"dWi\"].shape =", gradients["dWi"].shape)
print("gradients[\"dWc\"][3][1] =", gradients["dWc"][3][1])
print("gradients[\"dWc\"].shape =", gradients["dWc"].shape)
print("gradients[\"dWo\"][1][2] =", gradients["dWo"][1][2])
print("gradients[\"dWo\"].shape =", gradients["dWo"].shape)
print("gradients[\"dbf\"][4] =", gradients["dbf"][4])
print("gradients[\"dbf\"].shape =", gradients["dbf"].shape)
print("gradients[\"dbi\"][4] =", gradients["dbi"][4])
print("gradients[\"dbi\"].shape =", gradients["dbi"].shape)
print("gradients[\"dbc\"][4] =", gradients["dbc"][4])
print("gradients[\"dbc\"].shape =", gradients["dbc"].shape)
print("gradients[\"dbo\"][4] =", gradients["dbo"][4])
print("gradients[\"dbo\"].shape =", gradients["dbo"].shape)

预期输出:
gradients[“dxt”][1][2] = 3.2305591151091875
gradients[“dxt”].shape = (3, 10)
gradients[“da_prev”][2][3] = -0.06396214197109236
gradients[“da_prev”].shape = (5, 10)
gradients[“dc_prev”][2][3] = 0.7975220387970015
gradients[“dc_prev”].shape = (5, 10)
gradients[“dWf”][3][1] = -0.1479548381644968
gradients[“dWf”].shape = (5, 8)
gradients[“dWi”][1][2] = 1.0574980552259903
gradients[“dWi”].shape = (5, 8)
gradients[“dWc”][3][1] = 2.3045621636876668
gradients[“dWc”].shape = (5, 8)
gradients[“dWo”][1][2] = 0.3313115952892109
gradients[“dWo”].shape = (5, 8)
gradients[“dbf”][4] = [0.18864637]
gradients[“dbf”].shape = (5, 1)
gradients[“dbi”][4] = [-0.40142491]
gradients[“dbi”].shape = (5, 1)
gradients[“dbc”][4] = [0.25587763]
gradients[“dbc”].shape = (5, 1)
gradients[“dbo”][4] = [0.13893342]
gradients[“dbo”].shape = (5, 1)

反向传播LSTM RNN

这部分与你在上面实现的rnn_backward函数非常相似。首先将创建与返回变量相同维度的变量。然后,你将从头开始遍历所有时间步,并在每次迭代中调用为LSTM实现的一步函数。然后,你将通过分别汇总参数来更新参数。最后返回带有新梯度的字典。

说明:实现lstm_backward函数。创建一个从$T_x$开始并向后的for循环。对于每个步骤,请调用lstm_cell_backward并通过向其添加新梯度来更新旧梯度。请注意,dxt不会更新而是存储。

lstm_backward实现整个输入数据序列上LSTM网络的反向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def lstm_backward(da, caches):

"""
实现LSTM网络的反向传播

参数:
da -- 关于隐藏状态的梯度,维度为(n_a, m, T_x)
cachses -- 前向传播保存的信息

返回:
gradients -- 包含了梯度信息的字典:
dx -- 输入数据的梯度,维度为(n_x, m,T_x)
da0 -- 先前的隐藏状态的梯度,维度为(n_a, m)
dWf -- 遗忘门的权值的梯度,维度为(n_a, n_a + n_x)
dbf -- 遗忘门的偏置的梯度,维度为(n_a, 1)
dWi -- 更新门的权值的梯度,维度为(n_a, n_a + n_x)
dbi -- 更新门的偏置的梯度,维度为(n_a, 1)
dWc -- 第一个“tanh”的权值的梯度,维度为(n_a, n_a + n_x)
dbc -- 第一个“tanh”的偏置的梯度,维度为(n_a, n_a + n_x)
dWo -- 输出门的权值的梯度,维度为(n_a, n_a + n_x)
dbo -- 输出门的偏置的梯度,维度为(n_a, 1)

"""

# 从caches中获取第一个cache(t=1)的值
caches, x = caches
(a1, c1, a0, c0, f1, i1, cc1, o1, x1, parameters) = caches[0]

# 获取da与x1的维度信息
n_a, m, T_x = da.shape
n_x, m = x1.shape

# 初始化梯度
dx = np.zeros([n_x, m, T_x])
da0 = np.zeros([n_a, m])
da_prevt = np.zeros([n_a, m])
dc_prevt = np.zeros([n_a, m])
dWf = np.zeros([n_a, n_a + n_x])
dWi = np.zeros([n_a, n_a + n_x])
dWc = np.zeros([n_a, n_a + n_x])
dWo = np.zeros([n_a, n_a + n_x])
dbf = np.zeros([n_a, 1])
dbi = np.zeros([n_a, 1])
dbc = np.zeros([n_a, 1])
dbo = np.zeros([n_a, 1])

# 处理所有时间步
for t in reversed(range(T_x)):
# 使用lstm_cell_backward函数计算所有梯度
gradients = lstm_cell_backward(da[:,:,t],dc_prevt,caches[t])
# 保存相关参数
dx[:,:,t] = gradients['dxt']
dWf = dWf+gradients['dWf']
dWi = dWi+gradients['dWi']
dWc = dWc+gradients['dWc']
dWo = dWo+gradients['dWo']
dbf = dbf+gradients['dbf']
dbi = dbi+gradients['dbi']
dbc = dbc+gradients['dbc']
dbo = dbo+gradients['dbo']
# 将第一个激活的梯度设置为反向传播的梯度da_prev。
da0 = gradients['da_prev']

# 保存所有梯度到字典变量内
gradients = {"dx": dx, "da0": da0, "dWf": dWf,"dbf": dbf, "dWi": dWi,"dbi": dbi,
"dWc": dWc,"dbc": dbc, "dWo": dWo,"dbo": dbo}

return gradients

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
np.random.seed(1)
x = np.random.randn(3,10,7)
a0 = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a, y, c, caches = lstm_forward(x, a0, parameters)

da = np.random.randn(5, 10, 4)
gradients = lstm_backward(da, caches)

print("gradients[\"dx\"][1][2] =", gradients["dx"][1][2])
print("gradients[\"dx\"].shape =", gradients["dx"].shape)
print("gradients[\"da0\"][2][3] =", gradients["da0"][2][3])
print("gradients[\"da0\"].shape =", gradients["da0"].shape)
print("gradients[\"dWf\"][3][1] =", gradients["dWf"][3][1])
print("gradients[\"dWf\"].shape =", gradients["dWf"].shape)
print("gradients[\"dWi\"][1][2] =", gradients["dWi"][1][2])
print("gradients[\"dWi\"].shape =", gradients["dWi"].shape)
print("gradients[\"dWc\"][3][1] =", gradients["dWc"][3][1])
print("gradients[\"dWc\"].shape =", gradients["dWc"].shape)
print("gradients[\"dWo\"][1][2] =", gradients["dWo"][1][2])
print("gradients[\"dWo\"].shape =", gradients["dWo"].shape)
print("gradients[\"dbf\"][4] =", gradients["dbf"][4])
print("gradients[\"dbf\"].shape =", gradients["dbf"].shape)
print("gradients[\"dbi\"][4] =", gradients["dbi"][4])
print("gradients[\"dbi\"].shape =", gradients["dbi"].shape)
print("gradients[\"dbc\"][4] =", gradients["dbc"][4])
print("gradients[\"dbc\"].shape =", gradients["dbc"].shape)
print("gradients[\"dbo\"][4] =", gradients["dbo"][4])
print("gradients[\"dbo\"].shape =", gradients["dbo"].shape)

预期输出:
gradients[“dx”][1][2] = [-0.00173313 0.08287442 -0.30545663 -0.43281115]
gradients[“dx”].shape = (3, 10, 4)
gradients[“da0”][2][3] = -0.09591150195400468
gradients[“da0”].shape = (5, 10)
gradients[“dWf”][3][1] = -0.06981985612744011
gradients[“dWf”].shape = (5, 8)
gradients[“dWi”][1][2] = 0.10237182024854777
gradients[“dWi”].shape = (5, 8)
gradients[“dWc”][3][1] = -0.06249837949274524
gradients[“dWc”].shape = (5, 8)
gradients[“dWo”][1][2] = 0.04843891314443014
gradients[“dWo”].shape = (5, 8)
gradients[“dbf”][4] = [-0.0565788]
gradients[“dbf”].shape = (5, 1)
gradients[“dbi”][4] = [-0.15399065]
gradients[“dbi”].shape = (5, 1)
gradients[“dbc”][4] = [-0.29691142]
gradients[“dbc”].shape = (5, 1)
gradients[“dbo”][4] = [-0.29798344]
gradients[“dbo”].shape = (5, 1)

祝贺你完成此作业。你现在了解了循环神经网络的工作原理!

让我们继续下一个练习,在下一个练习中,你将学习使用RNN来构建字符级的语言模型。

作业10 字符级语言模型恐龙岛

字母级语言模型 - 恐龙岛

欢迎来到恐龙大陆! 6500万年前,恐龙就已经存在,并且在该作业下它们又回来了。假设你负责一项特殊任务,领先的生物学研究人员正在创造新的恐龙品种,并计划将它们带入地球,而你的工作就是为这些新恐龙起名字。如果恐龙不喜欢它的名字,它可能会变得疯狂,所以需要明智地选择!

幸运的是,你掌握了深度学习的一些知识,你将使用它来节省时间。你的助手已收集了他们可以找到的所有恐龙名称的列表,并将其编译到此dataset中。(请单击上一个链接查看)要创建新的恐龙名称,你将构建一个字母级语言模型来生成新名称。你的算法将学习不同的名称模式,并随机生成新名称。希望该算法可以使你和你的团队免受恐龙的愤怒!

完成此作业,你将学习:

  • 如何存储文本数据以供RNN使用
  • 如何在每个时间步采样预测并将其传递给下一个RNN单元以合成数据
  • 如何建立一个字母级的文本生成循环神经网络
  • 为什么梯度裁剪很重要

我们将从加载rnn_utils中为你提供的一些函数开始。具体来说,你可以访问诸如rnn_forwardrnn_backward之类的函数,这些函数与你在上一个作业中实现的函数等效。

现在请运行代码加载必要的包

1
2
3
4
5
import numpy as np
import random
import time
import cllm_utils

问题陈述

数据集和预处理

运行以下单元格以读取包含恐龙名称的数据集,创建唯一字符列表(例如a-z),并计算数据集和词汇量。

运行下面的代码处理数据变为无序且不重复的元素列表名为chars

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 以只读模式读取文件中的所有内容
data = open("dinos.txt", "r").read()

# 转化为小写字符
data = data.lower()

# 转化为无序且不重复的元素列表
chars = list(set(data))

# 获取大小信息
data_size, vocab_size = len(data), len(chars)

print(chars)
print("共计有%d个字符,唯一字符有%d个"%(data_size,vocab_size))

预期输出
[‘o’, ‘n’, ‘m’, ‘j’, ‘s’, ‘q’, ‘f’, ‘k’, ‘e’, ‘t’, ‘z’, ‘c’, ‘a’, ‘\n’, ‘i’, ‘u’, ‘l’, ‘h’, ‘b’, ‘w’, ‘v’, ‘y’, ‘r’, ‘p’, ‘x’, ‘g’, ‘d’]
共计有19909个字符,唯一字符有27个

这些字符是a-z(26个字符)加上“\n”(换行符),在此作业中,其作用类似于我们在讲座中讨论的<EOS>(句子结尾)标记,仅在此处表示恐龙名称的结尾,而不是句子的结尾。在下面的单元格中,我们创建一个python字典(即哈希表),以将每个字符映射为0-26之间的索引。我们还创建了第二个python字典,该字典将每个索引映射回对应的字符。这将帮助你找出softmax层的概率分布输出中哪个索引对应于哪个字符。下面的char_to_ixix_to_char是python字典。

下面请编写函数实现字符到索引和索引到字符的映射
提示

  • sorted(chars):对字符列表 chars 进行排序,以确保映射的一致性
  • enumerate(sorted(chars)) 的返回值是一个迭代器,生成一个包含索引和字符的元组
  • {ch: i}字典 char_to_ix,其中键是字符 ch,值是其对应的索引
    1
    2
    3
    4
    5
    6
    char_to_ix = {ch:i for i, ch in enumerate(sorted(chars))}
    ix_to_char = {i:ch for i, ch in enumerate(sorted(chars))}

    print(char_to_ix)
    print(ix_to_char)

    预期输出

{‘\n’: 0, ‘a’: 1, ‘b’: 2, ‘c’: 3, ‘d’: 4, ‘e’: 5, ‘f’: 6, ‘g’: 7, ‘h’: 8, ‘i’: 9, ‘j’: 10, ‘k’: 11, ‘l’: 12, ‘m’: 13, ‘n’: 14, ‘o’: 15, ‘p’: 16, ‘q’: 17, ‘r’: 18, ‘s’: 19, ‘t’: 20, ‘u’: 21, ‘v’: 22, ‘w’: 23, ‘x’: 24, ‘y’: 25, ‘z’: 26}

{0: ‘\n’, 1: ‘a’, 2: ‘b’, 3: ‘c’, 4: ‘d’, 5: ‘e’, 6: ‘f’, 7: ‘g’, 8: ‘h’, 9: ‘i’, 10: ‘j’, 11: ‘k’, 12: ‘l’, 13: ‘m’, 14: ‘n’, 15: ‘o’, 16: ‘p’, 17: ‘q’, 18: ‘r’, 19: ‘s’, 20: ‘t’, 21: ‘u’, 22: ‘v’, 23: ‘w’, 24: ‘x’, 25: ‘y’, 26: ‘z’}

模型概述

你的模型将具有以下结构:

  • 初始化参数
  • 运行优化循环
    • 正向传播以计算损失函数
    • 反向传播以计算相对于损失函数的梯度
    • 剪裁梯度以避免梯度爆炸
    • 使用梯度下降方法更新参数。
  • 返回学习的参数

图1:循环神经网络,类似于你在上一个笔记本“手把手实现循环神经网络”中构建的内容。

在每个时间步,RNN都会根据给定的先前字符来预测下一个字符。数据集$X = (x^{\langle 1 \rangle}, x^{\langle 2 \rangle}, …, x^{\langle T_x \rangle})$是训练集中的字符列表,而$Y = (y^{\langle 1 \rangle}, y^{\langle 2 \rangle}, …, y^{\langle T_x \rangle})$使得每个时间步$t$,我们有$y^{\langle t \rangle} = x^{\langle t+1 \rangle}$。

构建模型模块

在这一部分中,你将构建整个模型的两个重要模块:

  • 梯度裁剪:避免梯度爆炸
  • 采样:一种用于生成字符的技术

然后,你将应用这两个函数来构建模型。

在优化循环中裁剪梯度

在本节中,你将实现在优化循环中调用的clip函数。回想一下,你的总体循环结构通常由正向传播,损失计算,反向传播和参数更新组成。在更新参数之前,你将在需要时执行梯度裁剪,以确保你的梯度不会“爆炸”,这意味着要采用很大的值。

在下面的练习中,你将实现一个函数clip,该函数接收梯度字典,并在需要时返回裁剪后的梯度。梯度裁剪有多种方法。我们将使用简单的按元素裁剪程序,其中将梯度向量的每个元素裁剪为位于范围[-N,N]之间。通常,你将提供一个maxValue(例如10)。在此示例中,如果梯度向量的任何分量大于10,则将其设置为10;并且如果梯度向量的任何分量小于-10,则将其设置为-10。如果介于-10和10之间,则将其保留。

图2:在网络遇到轻微的“梯度爆炸”的情况下,使用与不使用梯度裁剪的梯度下降对比。

练习:实现以下函数以返回字典gradients的裁剪梯度。你的函数接受最大阈值,并返回裁剪后的梯度。你可以查看此hint,以获取有关如何裁剪numpy的示例。你将需要使用参数out = ...

编写函数clip(gradients, maxValue)修剪梯度

要求

  • 使用maxValue来修剪梯度

  • 参数:

    • gradients — 字典类型,包含了以下参数:”dWaa”, “dWax”, “dWya”, “db”, “dby”
    • maxValue — 阈值,把梯度值限制在[-maxValue, maxValue]内
  • 返回:

    • gradients — 修剪后的梯度
  • 从gradients中获得你想要的参数

提示

  • np.clip(gradient, -maxValue, maxValue, out=gradient)
    它将小于 -maxValue 的值替换为 -maxValue,将大于 maxValue 的值替换为 maxValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def clip(gradients, maxValue):
"""
使用maxValue来修剪梯度

参数:
gradients -- 字典类型,包含了以下参数:"dWaa", "dWax", "dWya", "db", "dby"
maxValue -- 阈值,把梯度值限制在[-maxValue, maxValue]内

返回:
gradients -- 修剪后的梯度
"""
# 获取参数
dWaa, dWax, dWya, db, dby = gradients['dWaa'], gradients['dWax'], gradients['dWya'], gradients['db'], gradients['dby']

# 梯度修剪
for gradient in [dWaa, dWax, dWya, db, dby]:
np.clip(gradient, -maxValue, maxValue, out=gradient)

gradients = {"dWaa": dWaa, "dWax": dWax, "dWya": dWya, "db": db, "dby": dby}

return gradients

运行代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
np.random.seed(3)
dWax = np.random.randn(5,3)*10
dWaa = np.random.randn(5,5)*10
dWya = np.random.randn(2,5)*10
db = np.random.randn(5,1)*10
dby = np.random.randn(2,1)*10
gradients = {"dWax": dWax, "dWaa": dWaa, "dWya": dWya, "db": db, "dby": dby}
gradients = clip(gradients, 10)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("gradients[\"dWax\"][3][1] =", gradients["dWax"][3][1])
print("gradients[\"dWya\"][1][2] =", gradients["dWya"][1][2])
print("gradients[\"db\"][4] =", gradients["db"][4])
print("gradients[\"dby\"][1] =", gradients["dby"][1])

预期输出:
gradients[“dWaa”][1][2] = 10.0
gradients[“dWax”][3][1] = -10.0
gradients[“dWya”][1][2] = 0.2971381536101662
gradients[“db”][4] = [10.]
gradients[“dby”][1] = [8.45833407]

采样

现在假设你的模型已经训练好。你想生成新文本(字符)。下图说明了生成过程:

图3:在此图中,我们假设模型已经训练好。我们在第一步中传入$x^{\langle 1\rangle} = \vec{0}$,然后让网络一次采样一个字符。

练习:实现以下的sample函数来采样字母(采样是随机的)。你需要执行4个步骤:

  • 步骤1:将第一个”dummy”虚输入$x^{\langle 1 \rangle} = \vec{0}$(零向量)传递给网络。这是我们生成任意字母之前的默认输入。我们还设置$a^{\langle 0 \rangle} = \vec{0}$。

  • 步骤2:执行向正向传播的步骤,即可获得$a^{\langle 1 \rangle}$ and $\hat{y}^{\langle 1 \rangle}$。以下是等式:

注意$\hat{y}^{\langle t+1 \rangle }$是一个(softmax)概率向量(其条目在0到1之间且总和为1)。$\hat{y}^{\langle t+1 \rangle}_i$ 表示由”i”索引的字符是下一个字符的概率。我们提供了一个softmax()函数供你使用。

  • 步骤3:执行采样:根据$\hat{y}^{\langle t+1 \rangle }$指定的概率分布,选择下一个字符的索引。这意味着,如果$\hat{y}^{\langle t+1 \rangle }_i = 0.16$,你将以16%的概率选择索引”i”。要实现它,你可以使用np.random.choice

以下是一个使用np.random.choice()的例子:

1
2
3
np.random.seed(0)
p = np.array([0.1, 0.0, 0.7, 0.2])
index = np.random.choice([0, 1, 2, 3], p = p.ravel())

这意味着你将根据分布选择index
$P(index = 0) = 0.1, P(index = 1) = 0.0, P(index = 2) = 0.7, P(index = 3) = 0.2$。

  • 步骤4:要在sample()中实现的最后一步是覆盖变量x,该变量当前存储$x^{\langle t \rangle }$,其值为$x^{\langle t + 1 \rangle }$。通过创建与预测字符相对应的独热向量以表示$x^{\langle t + 1 \rangle }$。然后,你将在步骤1中前向传播$x^{\langle t + 1 \rangle }$,并继续重复此过程,直到获得“\n”字符,表明你已经到达恐龙名称的末尾。

编写函数sample(parameters, char_to_is, seed)根据RNN输出的概率分布序列对字符序列进行采样
要求

  • 根据RNN输出的概率分布序列对字符序列进行采样

  • 参数:

    • parameters — 包含了Waa, Wax, Wya, by, b的字典
    • char_to_ix — 字符映射到索引的字典
    • seed — 随机种子
  • 返回:

    • indices — 包含采样字符索引的长度为n的列表。

注意:这里不要紧张,有点麻烦,但是我们会引导你,不要害怕
步骤:

  • 获取参数Waa, Wax, Wya, by, b
  • 从by获得词汇表大小
  • 从Waa获得隐藏层单元的数量n_a
  • 创建一个独热向量 x,初始化为全零,表示当前输入的字符(初始为空)
  • a_prev 初始化为零,用于存储上一个时间步的隐藏状态。
  • 创建索引的空列表indices,这是包含要生成的字符的索引的列表。
  • idx是检测换行符的标志,我们将其初始化为-1
  • 使用counter 用于计数生成的字符
  • 获取换行符的索引newline_character,作为终止条件
  • 循环直到生成换行符或达到 50 个字符
    • 使用公式1、2、3进行前向传播
    • 设定随机种子np.random.seed(counter + seed)
    • 从概率分布 y 中随机选择一个字符索引 idx:np.random.choice(list(range(vocab_size)), p=y.ravel())
    • 将选中的索引添加到 indices 列表使用append
    • 更新输入向量 x,将对应 idx 的位置置为 1其余全为0
    • 更新 a_prev 为当前时间步的隐藏状态 a
    • 增加计数器和随机种子,以便于下次采样
  • 如果生成了 50 个字符且没有遇到换行符,则将换行符的索引添加到 indices
  • 返回生成的字符索引列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def sample(parameters, char_to_is, seed):
"""
根据RNN输出的概率分布序列对字符序列进行采样

参数:
parameters -- 包含了Waa, Wax, Wya, by, b的字典
char_to_ix -- 字符映射到索引的字典
seed -- 随机种子

返回:
indices -- 包含采样字符索引的长度为n的列表。
"""

# 从parameters 中获取参数
Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
vocab_size = by.shape[0]
n_a = Waa.shape[1]

# 步骤1
## 创建独热向量x
x = np.zeros((vocab_size,1))

## 使用0初始化a_prev
a_prev = np.zeros((n_a,1))

# 创建索引的空列表,这是包含要生成的字符的索引的列表。
indices = []

# IDX是检测换行符的标志,我们将其初始化为-1。
idx = -1

# 循环遍历时间步骤t。在每个时间步中,从概率分布中抽取一个字符,
# 并将其索引附加到“indices”上,如果我们达到50个字符,
#(我们应该不太可能有一个训练好的模型),我们将停止循环,这有助于调试并防止进入无限循环
counter = 0
newline_character = char_to_ix["\n"]

while (idx != newline_character and counter < 50):
# 步骤2:使用公式1、2、3进行前向传播
a = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b)
z = np.dot(Wya, a) + by
y = cllm_utils.softmax(z)

# 设定随机种子
np.random.seed(counter + seed)

# 步骤3:从概率分布y中抽取词汇表中字符的索引
idx = np.random.choice(list(range(vocab_size)), p=y.ravel())

# 添加到索引中
indices.append(idx)

# 步骤4:将输入字符重写为与采样索引对应的字符。
x = np.zeros((vocab_size,1))
x[idx] = 1

# 更新a_prev为a
a_prev = a

# 累加器
seed += 1
counter +=1

if(counter == 50):
indices.append(char_to_ix["\n"])

return indices


测试代码

1
2
3
4
5
6
7
8
9
10
11
12
np.random.seed(2)
_, n_a = 20, 100
Wax, Waa, Wya = np.random.randn(n_a, vocab_size), np.random.randn(n_a, n_a), np.random.randn(vocab_size, n_a)
b, by = np.random.randn(n_a, 1), np.random.randn(vocab_size, 1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b, "by": by}


indices = sample(parameters, char_to_ix, 0)
print("Sampling:")
print("list of sampled indices:", indices)
print("list of sampled characters:", [ix_to_char[i] for i in indices])

预期输出:
Sampling:
list of sampled indices: [12, 17, 24, 14, 13, 9, 10, 22, 24, 6, 13, 11, 12, 6, 21, 15, 21, 14, 3, 2, 1, 21, 18, 24, 7, 25, 6, 25, 18, 10, 16, 2, 3, 8, 15, 12, 11, 7, 1, 12, 10, 2, 7, 7, 11, 17, 24, 12, 12, 0, 0]
list of sampled characters: [‘l’, ‘q’, ‘x’, ‘n’, ‘m’, ‘i’, ‘j’, ‘v’, ‘x’, ‘f’, ‘m’, ‘k’, ‘l’, ‘f’, ‘u’, ‘o’, ‘u’, ‘n’, ‘c’, ‘b’, ‘a’, ‘u’, ‘r’, ‘x’, ‘g’, ‘y’, ‘f’, ‘y’, ‘r’, ‘j’, ‘p’, ‘b’, ‘c’, ‘h’, ‘o’, ‘l’, ‘k’, ‘g’, ‘a’, ‘l’, ‘j’, ‘b’, ‘g’, ‘g’, ‘k’, ‘q’, ‘x’, ‘l’, ‘l’, ‘\n’, ‘\n’]

建立语言模型

现在是时候建立用于文字生成的字母级语言模型了。

梯度下降

在本部分中,你将实现一个函数,该函数执行随机梯度下降的一个步骤(梯度裁剪)。你将一次查看一个训练示例,因此优化算法将是随机梯度下降。提醒一下,以下是RNN常见的优化循环的步骤:

  • 通过RNN正向传播以计算损失
  • 随时间反向传播以计算相对于参数的损失梯度
  • 必要时裁剪梯度
  • 使用梯度下降更新参数

练习:实现此优化过程(随机梯度下降的一个步骤)。

我们为你提供了以下函数:

下面的代码请不要运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 示例,请勿执行。
def rnn_forward(X, Y, a_prev, parameters):
"""
通过RNN进行前向传播,计算交叉熵损失。

它返回损失的值以及存储在反向传播中使用的“缓存”值。
"""
....
return loss, cache

def rnn_backward(X, Y, parameters, cache):
"""
通过时间进行反向传播,计算相对于参数的梯度损失。它还返回所有隐藏的状态
"""
...
return gradients, a

def update_parameters(parameters, gradients, learning_rate):
"""
Updates parameters using the Gradient Descent Update Rule
"""
...
return parameters

下面请使用我们提供的函数编写函数optimize(X, Y, a_prev, parameters, learning_rate = 0.01) 执行训练模型的单步优化

要求

  • 执行训练模型的单步优化。

  • 参数:

    • X — 整数列表,其中每个整数映射到词汇表中的字符。
    • Y — 整数列表,与X完全相同,但向左移动了一个索引。
    • a_prev — 上一个隐藏状态
    • parameters — 字典,包含了以下参数:
      • Wax — 权重矩阵乘以输入,维度为(n_a, n_x)
      • Waa — 权重矩阵乘以隐藏状态,维度为(n_a, n_a)
      • Wya — 隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
      • b — 偏置,维度为(n_a, 1)
      • by — 隐藏状态与输出相关的权重偏置,维度为(n_y, 1)
    • learning_rate — 模型学习的速率
  • 返回:

    • loss — 损失函数的值(交叉熵损失)
    • gradients — 字典,包含了以下参数:
      • dWax — 输入到隐藏的权值的梯度,维度为(n_a, n_x)
      • dWaa — 隐藏到隐藏的权值的梯度,维度为(n_a, n_a)
      • dWya — 隐藏到输出的权值的梯度,维度为(n_y, n_a)
      • db — 偏置的梯度,维度为(n_a, 1)
      • dby — 输出偏置向量的梯度,维度为(n_y, 1)
    • a[len(X)-1] — 最后的隐藏状态,维度为(n_a, 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def optimize(X, Y, a_prev, parameters, learning_rate = 0.01):
"""
执行训练模型的单步优化。

参数:
X -- 整数列表,其中每个整数映射到词汇表中的字符。
Y -- 整数列表,与X完全相同,但向左移动了一个索引。
a_prev -- 上一个隐藏状态
parameters -- 字典,包含了以下参数:
Wax -- 权重矩阵乘以输入,维度为(n_a, n_x)
Waa -- 权重矩阵乘以隐藏状态,维度为(n_a, n_a)
Wya -- 隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
b -- 偏置,维度为(n_a, 1)
by -- 隐藏状态与输出相关的权重偏置,维度为(n_y, 1)
learning_rate -- 模型学习的速率

返回:
loss -- 损失函数的值(交叉熵损失)
gradients -- 字典,包含了以下参数:
dWax -- 输入到隐藏的权值的梯度,维度为(n_a, n_x)
dWaa -- 隐藏到隐藏的权值的梯度,维度为(n_a, n_a)
dWya -- 隐藏到输出的权值的梯度,维度为(n_y, n_a)
db -- 偏置的梯度,维度为(n_a, 1)
dby -- 输出偏置向量的梯度,维度为(n_y, 1)
a[len(X)-1] -- 最后的隐藏状态,维度为(n_a, 1)
"""

# 前向传播
loss, cache = cllm_utils.rnn_forward(X, Y, a_prev, parameters)

# 反向传播
gradients, a = cllm_utils.rnn_backward(X, Y, parameters, cache)

# 梯度修剪,[-5 , 5]
gradients = clip(gradients,5)

# 更新参数
parameters = cllm_utils.update_parameters(parameters,gradients,learning_rate)

return loss, gradients, a[len(X)-1]


运行代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
np.random.seed(1)
vocab_size, n_a = 27, 100
a_prev = np.random.randn(n_a, 1)
Wax, Waa, Wya = np.random.randn(n_a, vocab_size), np.random.randn(n_a, n_a), np.random.randn(vocab_size, n_a)
b, by = np.random.randn(n_a, 1), np.random.randn(vocab_size, 1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b, "by": by}
X = [12,3,5,11,22,3]
Y = [4,14,11,22,25, 26]

loss, gradients, a_last = optimize(X, Y, a_prev, parameters, learning_rate = 0.01)
print("Loss =", loss)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("np.argmax(gradients[\"dWax\"]) =", np.argmax(gradients["dWax"]))
print("gradients[\"dWya\"][1][2] =", gradients["dWya"][1][2])
print("gradients[\"db\"][4] =", gradients["db"][4])
print("gradients[\"dby\"][1] =", gradients["dby"][1])
print("a_last[4] =", a_last[4])

预期输出:
Loss = 126.50397572165363
gradients[“dWaa”][1][2] = 0.19470931534719205
np.argmax(gradients[“dWax”]) = 93
gradients[“dWya”][1][2] = -0.007773876032003275
gradients[“db”][4] = [-0.06809825]
gradients[“dby”][1] = [0.01538192]
a_last[4] = [-1.]

训练模型

给定恐龙名称数据集,我们将数据集的每一行(一个名称)用作一个训练示例。每100步随机梯度下降,你将抽样10个随机选择的名称,以查看算法的运行情况。请记住要对数据集进行混洗,以便随机梯度下降以随机顺序访问示例。

练习:按照说明进行操作并实现model()。当examples [index]包含一个恐龙名称(字符串)时,创建示例(X,Y),可以使用以下方法:

1
2
3
4
5
6
# 计算当前示例的索引,通过取模确保索引在有效范围内
index = j % len(examples)
# 创建输入数组X,将当前示例的每个字符转换为索引,并在开头加上None
X = [None] + [char_to_ix[ch] for ch in examples[index]]
# 创建输出数组Y,去掉X的第一个元素,然后在末尾添加换行符的索引
Y = X[1:] + [char_to_ix["\n"]]

注意,我们使用:index= j % len(examples),其中j = 1....num_iterations,以确保examples [index]始终是有效的语句(index小于len(examples))。
X的第一个条目为None将被rnn_forward()解释为设置$x^{\langle 0 \rangle} = \vec{0}$。此外,这确保了Y等于X,但向左移动了一步,并附加了“\n”以表示恐龙名称的结尾。

下面开始编写模型model(data, ix_to_char, char_to_ix, num_iterations=3500, n_a=50, dino_names=7,vocab_size=27)

要求

  • 训练模型并生成恐龙名字

  • 参数:

    • data — 语料库
    • ix_to_char — 索引映射字符字典
    • char_to_ix — 字符映射索引字典
    • num_iterations — 迭代次数
    • n_a — RNN单元数量
    • dino_names — 每次迭代中采样的数量
    • vocab_size — 在文本中的唯一字符的数量
  • 返回:

    • parameters — 学习后了的参数

步骤:

  • 从vocab_size中获取n_x、n_y
  • 初始化参数cllm_utils.initialize_parameters
  • 初始化损失
  • 构建恐龙名称列表
  • 随机打乱 examples 列表中的元素顺序使用np.random.shuffle函数
  • 初始化LSTM隐藏状态a_prev为0
  • 循环迭代
    • 定义一个训练样本
    • 执行单步优化:前向传播 -> 反向传播 -> 梯度修剪 -> 更新参数使用optimize函数
      • 返回curr_loss, gradients, a_prev
    • 使用延迟来保持损失平滑,这是为了加速训练使用cllm_utils.smooth函数
    • 每2000次迭代时:
      • 打印当前迭代次数和对应的损失值
      • 循环 dino_names 次调用采样函数从模型中生成字符,采样的索引转换回字符并打印出来
  • 返回参数parameters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def model(data,ix_to_char, char_to_ix, num_iterations=3500,n_a=50, dino_names=7,vocab_size=27):
"""
训练模型并生成恐龙名字

参数:
data -- 语料库
ix_to_char -- 索引映射字符字典
char_to_ix -- 字符映射索引字典
num_iterations -- 迭代次数
n_a -- RNN单元数量
dino_names -- 每次迭代中采样的数量
vocab_size -- 在文本中的唯一字符的数量

返回:
parameters -- 学习后了的参数
"""

# 从vocab_size中获取n_x、n_y
n_x, n_y = vocab_size, vocab_size

# 初始化参数
parameters = cllm_utils.initialize_parameters(n_a, n_x, n_y)

# 初始化损失
loss = cllm_utils.get_initial_loss(vocab_size, dino_names)

# 构建恐龙名称列表
"""
open 函数打开 "dinos.txt" 文件,并将文件对象赋值给变量 f
使用 f.readlines() 读取文件中的所有行,返回一个列表,每个元素是一行文本
列表推导式 [x.lower().strip() for x in examples] 对读取的每一行进行处理:将其转换为小写并去除首尾空白字符
"""
with open("dinos.txt") as f:
examples = f.readlines()
examples = [x.lower().strip() for x in examples]

# 打乱全部的恐龙名称
np.random.seed(0)
np.random.shuffle(examples)

# 初始化LSTM隐藏状态
a_prev = np.zeros((n_a,1))

# 循环
for j in range(num_iterations):
# 定义一个训练样本
index = j % len(examples)
X = [None] + [char_to_ix[ch] for ch in examples[index]]
Y = X[1:] + [char_to_ix["\n"]]

# 执行单步优化:前向传播 -> 反向传播 -> 梯度修剪 -> 更新参数
# 选择学习率为0.01
curr_loss, gradients, a_prev = optimize(X, Y, a_prev, parameters)

# 使用延迟来保持损失平滑,这是为了加速训练。
loss = cllm_utils.smooth(loss, curr_loss)

# 每2000次迭代,通过sample()生成“\n”字符,检查模型是否学习正确
if j % 2000 == 0:
print("第" + str(j+1) + "次迭代,损失值为:" + str(loss))

seed = 0
for name in range(dino_names):
# 采样
sampled_indices = sample(parameters, char_to_ix, seed)
cllm_utils.print_sample(sampled_indices, ix_to_char)

# 为了得到相同的效果,随机种子+1
seed += 1

print("\n")
return parameters

运行代码训练模型

1
parameters = model(data, ix_to_char, char_to_ix)

预期输出
第1次迭代,损失值为:23.0873360855
Nkzxwtdmfqoeyhsqwasjkjvu
Kneb
Kzxwtdmfqoeyhsqwasjkjvu
Neb
Zxwtdmfqoeyhsqwasjkjvu
Eb
Xwtdmfqoeyhsqwasjkjvu

第2001次迭代,损失值为:27.8841604914
Liusskeomnolxeros
Hmdaairus
Hytroligoraurus
Lecalosapaus
Xusicikoraurus
Abalpsamantisaurus
Tpraneronxeros

执行了:0分3秒

结论

你可以看到,在训练即将结束时,你的算法已开始生成合理的恐龙名称。刚开始时,它会生成随机字符,但是到最后,你会看到恐龙名字的结尾很酷。运行该算法更长时间,并调整超参数来看看是否可以获得更好的结果。我们的实现产生了一些非常酷的名称,例如“maconucon”,“marloralus”和“macingsersaurus”。你的模型还有望了解到恐龙名称往往以saurusdonaurator等结尾。

如果你的模型生成了一些不酷的名字,请不要完全怪罪模型-并非所有实际的恐龙名字听起来都很酷。(例如,dromaeosauroides是实际存在的恐龙名称,并且也在训练集中。)但是此模型应该给你提供了一组可以从中挑选的候选名字!

该作业使用了相对较小的数据集,因此你可以在CPU上快速训练RNN。训练英语模型需要更大的数据集,并且通常需要更多的计算,在GPU上也要运行多个小时。我们使用恐龙的名字已经有一段时间了,到目前为止,我们最喜欢的名字是great, undefeatable,且fierce的:Mangosaurus!

像莎士比亚一样创作

该笔记本的其余部分是可选的,但我们希望你都尝试做一下,因为它非常有趣且内容丰富。

一个类似(但更复杂)的任务是生成莎士比亚诗歌。无需从恐龙名称的数据集中学习,而是使用莎士比亚诗歌集。使用LSTM单元,你可以学习跨文本中许多字符的长期依赖关系。例如,某个字符出现在序列的某个地方可能会影响序列后面的其他字符。这些长期依赖关系对于恐龙名称来说不太重要,因为它们的名称很短。

让我们成为诗人!

我们已经用Keras实现了莎士比亚诗歌生成器。运行以下单元格以加载所需的软件包和模型。这可能需要几分钟的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#开始时间
start_time = time.clock()

from keras.callbacks import LambdaCallback
from keras.models import Model, load_model, Sequential
from keras.layers import Dense, Activation, Dropout, Input, Masking
from keras.layers import LSTM
from keras.utils.data_utils import get_file
from keras.preprocessing.sequence import pad_sequences
from shakespeare_utils import *
import sys
import io

#结束时间
end_time = time.clock()

#计算时差
minium = end_time - start_time

print("执行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒")

预期输出
Loading text data…
Creating training set…
number of training examples: 31412
Vectorizing training set…
Loading model…
执行了:0分8秒

为了节省你的时间,我们已经在莎士比亚的十四行诗“The Sonnets”诗歌集上训练了大约1000个epoch的模型。

让我们再训练模型完成一个新epoch,这也将花费几分钟。你可以运行generate_output,这将提示你输入小于40个字符的句子。这首诗将从你输入的句子开始,我们的RNN-Shakespeare将为你完成其余的部分!例如,尝试”Forsooth this maketh no sense “(不要输入引号)。根据是否在末尾加上空格,你的结果也可能会有所不同,两种方法都应尝试,也可以尝试其他输入法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""
LambdaCallback:这是 Keras 提供的一种回调机制,允许用户定义在训练过程中某些特定事件发生时要执行的代码。在这里,它用于在每个 epoch 结束时执行 on_epoch_end 函数。
on_epoch_end:这个函数需要事先定义,它将在每个 epoch 结束时被调用。通常,你可以在这个函数中添加用于监控训练过程的代码,例如打印损失值、更新图表等
"""
print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

"""
这是 Keras 中用于训练模型的方法。
x:训练数据(输入特征)。
y:对应的标签(目标值)。
batch_size=128:指定每个训练批次包含128个样本。这样可以控制每次更新模型参数时所用的数据量。
epochs=1:指定训练周期的数量,这里设置为1,意味着模型将训练一个完整的 epoch。
callbacks=[print_callback]:传入之前定义的回调函数 print_callback。这意味着在每个 epoch 结束时,on_epoch_end 函数将被调用
"""

model.fit(x, y, batch_size=128, epochs=1, callbacks=[print_callback])

预期输出
Epoch 1/1
31412/31412 [==============================] - 27s 846us/step - loss: 2.7274

运行下面的代码尝试生成输出

1
2
3
4
# 运行此代码尝试不同的输入,而不必重新训练模型。
generate_output() #博主在这里输入hello


预期输出:
Write the beginning of your poem, the Shakespeare machine will complete it. Your input is: hello

Here is your poem:

hello s ate.

how wile is seyse to vimither part,
no love and do pros of eyes for show,
thy bourt duse dore bes in his band
awharces pery arey apery denired,
hibfers beer bespeck new shome is dide in shill,
to ars’ thy loth le’s all my vide berend,
that delencly pap steloted and’ing chimthof shoud,
grot, with meauty’s whether on my epsiy to drase.
our the with make i prouy my drere.
for my canvere’s med

RNN-Shakespeare模型与你为恐龙名称构建的模型非常相似。唯一的区别是:

  • LSTM代替基本的RNN来捕获更远的依赖
  • 模型是更深的堆叠的LSTM模型(2层)
  • 使用Keras而不是python来简化代码

如果你想了解更多信息,还可以在GitHub上查看Keras Team的文本生成实现:https://github.com/keras-team/keras/blob/master/examples/lstm_text_generation.py

祝贺你完成本笔记本作业!

参考:

作业11 词向量运算

词向量的基本操作

欢迎来到本周的第一份作业!

因为训练单词嵌入在计算上非常耗时耗力,所以大多数ML练习者都会加载一组经过预先训练的嵌入。

完成此任务后,你将能够:

  • 加载预训练的词向量,并使用余弦相似度测量相似度
  • 使用单词嵌入来解决单词类比问题,例如“男人相对女人”,“国王相对____”。
  • 修改词嵌入以减少其性别偏见

让我们开始吧!

运行程序以加载所需的软件包。

1
2
3
import numpy as np
import w2v_utils

接下来,让我们加载单词向量。对于此作业,我们将使用50维GloVe向量表示单词。运行以下单元格以加载word_to_vec_map

运行代码加载单词向量

1
2
words, word_to_vec_map = w2v_utils.read_glove_vecs('data/glove.6B.50d.txt')

你已加载:

  • words:词汇表中的单词集。
  • word_to_vec_map:将单词映射到其GloVe向量表示的字典上。

你已经看到,单向向量不能很好地说明相似的单词。GloVe向量提供有关单个单词含义的更多有用信息。现在让我们看看如何使用GloVe向量确定两个单词的相似程度。

运行代码查看word_to_vec_map字典

1
2
3
4
5
# python 3.x
print(word_to_vec_map['hello'])

# python 2.x
print word_to_vec_map['hello']

预期输出
[-0.38497 0.80092 0.064106 -0.28355 -0.026759 -0.34532 -0.64253
-0.11729 -0.33257 0.55243 -0.087813 0.9035 0.47102 0.56657
0.6985 -0.35229 -0.86542 0.90573 0.03576 -0.071705 -0.12327
0.54923 0.47005 0.35572 1.2611 -0.67581 -0.94983 0.68666
0.3871 -1.3492 0.63512 0.46416 -0.48814 0.83827 -0.9246
-0.33722 0.53741 -1.0616 -0.081403 -0.67111 0.30923 -0.3923
-0.55002 -0.68827 0.58049 -0.11626 0.013139 -0.57654 0.048833
0.67204 ]

余弦相似度

要测量两个单词的相似程度,我们需要一种方法来测量两个单词的两个嵌入向量之间的相似度。给定两个向量$u$和$v$,余弦相似度定义如下:

其中 $u.v$ 是两个向量的点积(或内积),$||u||_2$ 是向量 $u$ 的范数(或长度),而$\theta$ 是$u$和$v$之间的夹角。这种相似性取决于$u$和$v$之间的角度。如果$u$和$v$非常相似,它们的余弦相似度将接近1;如果它们不相似,则余弦相似度将取较小的值。

图1:两个向量之间的夹角余弦表示它们的相似度

练习:实现函数cosine_similarity()以评估单词向量之间的相似性。

提醒:$u$的范数定义为$||u||_2 = \sqrt{\sum_{i=1}^{n} u_i^2}$

编写函数cosine_similarity(u,v)计算u与v的余弦相似度

要求

  • u与v的余弦相似度反映了u与v的相似程度

  • 参数:

    • u — 维度为(n,)的词向量
    • v — 维度为(n,)的词向量
  • 返回:

    • cosine_similarity — 由上面公式定义的u和v之间的余弦相似度。

提示:u的L2范数np.sqrt(np.sum(np.power(u, 2)))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def cosine_similarity(u, v):
"""
u与v的余弦相似度反映了u与v的相似程度

参数:
u -- 维度为(n,)的词向量
v -- 维度为(n,)的词向量

返回:
cosine_similarity -- 由上面公式定义的u和v之间的余弦相似度。
"""
distance = 0

# 计算u与v的内积
dot = np.dot(u, v)

#计算u的L2范数
norm_u = np.sqrt(np.sum(np.power(u, 2)))

#计算v的L2范数
norm_v = np.sqrt(np.sum(np.power(v, 2)))

# 根据公式1计算余弦相似度
cosine_similarity = np.divide(dot, norm_u * norm_v)

return cosine_similarity

运行代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
father = word_to_vec_map["father"]
mother = word_to_vec_map["mother"]
ball = word_to_vec_map["ball"]
crocodile = word_to_vec_map["crocodile"]
france = word_to_vec_map["france"]
italy = word_to_vec_map["italy"]
paris = word_to_vec_map["paris"]
rome = word_to_vec_map["rome"]

print("cosine_similarity(father, mother) = ", cosine_similarity(father, mother))
print("cosine_similarity(ball, crocodile) = ",cosine_similarity(ball, crocodile))
print("cosine_similarity(france - paris, rome - italy) = ",cosine_similarity(france - paris, rome - italy))

预期输出:
cosine_similarity(father, mother) = 0.8909038442893615
cosine_similarity(ball, crocodile) = 0.2743924626137942
cosine_similarity(france - paris, rome - italy) = -0.6751479308174202

获得正确的预期输出后,请随时修改输入并测量其他词对之间的余弦相似度!围绕其他输入的余弦相似性进行操作将使你对单词向量的表征有更好的了解。

单词类比任务

在类比任务中,我们完成句子”a is to b as c is to ____“。 一个例子是’man is to woman as king is to queen‘。 详细地说,我们试图找到一个单词d,以使关联的单词向量$e_a, e_b, e_c, e_d$通过以下方式相关: $e_b - e_a \approx e_d - e_c$。我们将使用余弦相似性来衡量$e_b - e_a$ 和 $e_d - e_c$之间的相似性。

练习:完成以下代码即可执行单词类比!

编写函数complete_analogy(word_a,word_b, word_c, word_to_vec_map)
要求

  • 解决“A与B相比就类似于C与____相比一样”之类的问题

  • 参数:

    • word_a — 一个字符串类型的词
    • word_b — 一个字符串类型的词
    • word_c — 一个字符串类型的词
    • word_to_vec_map — 字典类型,单词到GloVe向量的映射
  • 返回:

    • best_word — 满足(v_b - v_a) 最接近 (v_best_word - v_c) 的词
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def complete_analogy(word_a, word_b, word_c, word_to_vec_map):
"""
解决“A与B相比就类似于C与____相比一样”之类的问题

参数:
word_a -- 一个字符串类型的词
word_b -- 一个字符串类型的词
word_c -- 一个字符串类型的词
word_to_vec_map -- 字典类型,单词到GloVe向量的映射

返回:
best_word -- 满足(v_b - v_a) 最接近 (v_best_word - v_c) 的词
"""

# 把单词转换为小写
word_a, word_b, word_c = word_a.lower(), word_b.lower(), word_c.lower()

# 获取对应单词的词向量
e_a, e_b, e_c = word_to_vec_map[word_a], word_to_vec_map[word_b], word_to_vec_map[word_c]

# 获取全部的单词
words = word_to_vec_map.keys()

# 将max_cosine_sim初始化为一个比较大的负数,这是为了确保任何计算得到的余弦相似度都会比这个值更高
max_cosine_sim = -100
# best_word 被初始化为 None,用来存储找到的最佳单词
best_word = None

# 遍历整个数据集
for word in words:
# 要避免匹配到输入的数据
if word in [word_a, word_b, word_c]:
continue
# 计算余弦相似度
cosine_sim = cosine_similarity((e_b - e_a), (word_to_vec_map[word] - e_c))

if cosine_sim > max_cosine_sim:
max_cosine_sim = cosine_sim
best_word = word

return best_word

运行下面的程序以测试你的代码

1
2
3
4
triads_to_try = [('italy', 'italian', 'spain'), ('india', 'delhi', 'japan'), ('man', 'woman', 'boy'), ('small', 'smaller', 'large')]
for triad in triads_to_try:
print ('{} -> {} <====> {} -> {}'.format( *triad, complete_analogy(*triad,word_to_vec_map)))

预期输出:
italy -> italian :: spain -> spanish
india -> delhi :: japan -> tokyo
man -> woman :: boy -> girl
small -> smaller :: large -> larger

一旦获得正确的预期输出,请随时修改上面的输入单元以测试你自己的类比。尝试找到其他可行的类比对,但还要找到一些算法无法给出正确答案的类比对:例如,你可以尝试使用small-> smaller正如big->?。

恭喜!你到了本作业的结尾。以下是你应记住的要点:

  • 余弦相似度是比较单词向量对之间相似度的好方法。(尽管L2距离也适用。)
  • 对于NLP应用程序,使用互联网上经过预先训练的一组词向量通常是入门的好方法。

即使你已完成分级部分,我们也建议你也看一下本笔记本的其余部分。

恭喜你完成了笔记本的分级部分!

去偏见词向量(可选练习)

在下面的练习中,你将研究可嵌入词嵌入的性别偏见,并探索减少偏见的算法。除了了解去偏斜的主题外,本练习还将帮助你磨清直觉,了解单词向量在做什么。本节涉及一些线性代数,尽管即使你不擅长线性代数,你也可以完成它,我们鼓励你尝试一下。笔记本的此部分是可选的,未分级。

首先让我们看看GloVe词嵌入与性别之间的关系。你将首先计算向量$g = e_{woman}-e_{man}$,其中$e_ {woman}$表示与单词woman对应的单词向量,而$e_{man}$则与单词对应与单词man对应的向量。所得向量$g$大致编码“性别”的概念。(如果你计算$g_1 = e_{mother}-e_{father}$, $g_2 = e_{girl}-e_{boy}$等,并对其进行平均,则可能会得到更准确的表示。但是仅使用$e_{woman}-e_{man}$ 会给你足够好的结果。)

1
2
3
g = word_to_vec_map['woman'] - word_to_vec_map['man']
print(g)

预期输出
[-0.087144 0.2182 -0.40986 -0.03922 -0.1032 0.94165
-0.06042 0.32988 0.46144 -0.35962 0.31102 -0.86824
0.96006 0.01073 0.24337 0.08193 -1.02722 -0.21122
0.695044 -0.00222 0.29106 0.5053 -0.099454 0.40445
0.30181 0.1355 -0.0606 -0.07131 -0.19245 -0.06115
-0.3204 0.07165 -0.13337 -0.25068714 -0.14293 -0.224957
-0.149 0.048882 0.12191 -0.27362 -0.165476 -0.20426
0.54376 -0.271425 -0.10245 -0.32108 0.2516 -0.33455
-0.04371 0.01258 ]

现在,你将考虑一下不同单词$g$的余弦相似度。考虑相似性的正值与余弦相似性的负值意味着什么。

运行代码考虑一下不同单词g的余弦相似度

1
2
3
4
5
name_list = ['john', 'marie', 'sophie', 'ronaldo', 'priya', 'rahul', 'danielle', 'reza', 'katy', 'yasmin']

for w in name_list:
print (w, cosine_similarity(word_to_vec_map[w], g))

预期输出:

john -0.23163356146
marie 0.315597935396
sophie 0.318687898594
ronaldo -0.312447968503
priya 0.17632041839
rahul -0.169154710392
danielle 0.243932992163
reza -0.079304296722
katy 0.283106865957
yasmin 0.233138577679

如你所见,女性名字与我们构造的向量$g$的余弦相似度为正,而男性名字与余弦的相似度为负。这并不令人惊讶,结果似乎可以接受。

但是,让我们尝试其他一些词。

1
2
3
4
5
word_list = ['lipstick', 'guns', 'science', 'arts', 'literature', 'warrior','doctor', 'tree', 'receptionist', 
'technology', 'fashion', 'teacher', 'engineer', 'pilot', 'computer', 'singer']
for w in word_list:
print (w, cosine_similarity(word_to_vec_map[w], g))

预期输出

lipstick 0.276919162564
guns -0.18884855679
science -0.0608290654093
arts 0.00818931238588
literature 0.0647250443346
warrior -0.209201646411
doctor 0.118952894109
tree -0.0708939917548
receptionist 0.330779417506
technology -0.131937324476
fashion 0.0356389462577
teacher 0.179209234318
engineer -0.0803928049452
pilot 0.00107644989919
computer -0.103303588739
singer 0.185005181365

你有什么惊讶的地方吗?令人惊讶的是,这些结果如何反映出某些不健康的性别定型观念。例如,”computer”更接近 “man”,而”literature” 更接近”woman”。

我们将在下面看到如何使用Boliukbasi et al., 2016提出的算法来减少这些向量的偏差。注意,诸如 “actor”/“actress” 或者 “grandmother”/“grandfather”之类的词对应保持性别特定,而诸如”receptionist” 或者”technology”之类的其他词语应被中和,即与性别无关。消除偏见时,你将不得不区别对待这两种类型的单词。

消除非性别特定词的偏见

下图应帮助你直观地了解中和的作用。如果你使用的是50维词嵌入,则50维空间可以分为两部分:偏移方向$g$和其余49维,我们将其称为$g_{\perp}$。在线性代数中,我们说49维$g_{\perp}$与$g$垂直(或“正交”),这意味着它与$g$成90度。中和步骤采用向量,例如$e_{receptionist}$,并沿$g$的方向将分量清零,从而得到$e_{receptionist}^{debiased}$。

即使$g_{\perp}$是49维的,鉴于我们可以在屏幕上绘制的内容的局限性,我们还是使用下面的1维轴对其进行说明。

图2 :在应用中和操作之前和之后,代表”receptionist”的单词向量。

练习:实现neutralize()以消除诸如”receptionist” 或 “scientist”之类的词的偏见。给定嵌入$e$的输入,你可以使用以下公式来计算$e^{debiased}$:

如果你是线性代数方面的专家,则可以将$e^{bias_component}$识别为$e$在$g$方向上的投影。如果你不是线性代数方面的专家,请不必为此担心。

提醒:向量$u$可分为两部分:在向量轴$v_B$上的投影和在与$v$正交的轴上的投影:

其中:$u_B =$ and $u_{\perp} = u - u_B$

编写函数neutralize(word, g, word_to_vec_map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def neutralize(word, g, word_to_vec_map):
"""
通过将“word”投影到与偏置轴正交的空间上,消除了“word”的偏差。
该函数确保“word”在性别的子空间中的值为0

参数:
word -- 待消除偏差的字符串
g -- 维度为(50,),对应于偏置轴(如性别)
word_to_vec_map -- 字典类型,单词到GloVe向量的映射

返回:
e_debiased -- 消除了偏差的向量。
"""

# 根据word选择对应的词向量
e = word_to_vec_map[word]

# 根据公式2计算e_biascomponent
e_biascomponent = np.divide(np.dot(e, g), np.square(np.linalg.norm(g))) * g

# 根据公式3计算e_debiased
e_debiased = e - e_biascomponent

return e_debiased

1
2
3
4
5
6
e = "receptionist"
print("去偏差前{0}与g的余弦相似度为:{1}".format(e, cosine_similarity(word_to_vec_map["receptionist"], g)))

e_debiased = neutralize("receptionist", g, word_to_vec_map)
print("去偏差后{0}与g的余弦相似度为:{1}".format(e, cosine_similarity(e_debiased, g)))

预期输出: The second result is essentially 0, up to numerical roundof (on the order of $10^{-17}$).
cosine similarity between receptionist and g, before neutralizing: 0.3307794175059374
cosine similarity between receptionist and g, after neutralizing: -2.099120994400013e-17

性别专用词的均衡算法

接下来,让我们看一下如何将偏置也应用于单词对,例如”actress”和”actor”。均衡仅应用与你希望通过性别属性有所不同的单词对。作为具体示例,假设”actress”比”actor”更接近”babysit”。通过将中和应用于”babysit”,我们可以减少与”babysit”相关的性别刻板印象。但这仍然不能保证”actress”和”actor”与”babysit”等距,均衡算法负责这一点。

均衡背后的关键思想是确保一对特定单词与49维 $g_\perp$等距。均衡步骤还确保了两个均衡步骤现在与$e_{receptionist}^{debiased}$或与任何其他已中和的作品之间的距离相同。图片中展示了均衡的工作方式:

为此,线性代数的推导要复杂一些。(详细信息请参见Bolukbasi et al., 2016)但其关键方程式是:

练习:实现以下函数。使用上面的等式来获取单词对的最终均等化形式。Good luck!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def equalize(pair, bias_axis, word_to_vec_map):
"""
通过遵循上图中所描述的均衡方法来消除性别偏差。

参数:
pair -- 要消除性别偏差的词组,比如 ("actress", "actor")
bias_axis -- 维度为(50,),对应于偏置轴(如性别)
word_to_vec_map -- 字典类型,单词到GloVe向量的映射

返回:
e_1 -- 第一个词的词向量
e_2 -- 第二个词的词向量
"""
# 第1步:获取词向量
w1, w2 = pair
e_w1, e_w2 = word_to_vec_map[w1], word_to_vec_map[w2]

# 第2步:计算w1与w2的均值
mu = (e_w1 + e_w2) / 2.0

# 第3步:计算mu在偏置轴与正交轴上的投影
mu_B = np.divide(np.dot(mu, bias_axis), np.square(np.linalg.norm(bias_axis))) * bias_axis
mu_orth = mu - mu_B

# 第4步:使用公式7、8计算e_w1B 与 e_w2B
e_w1B = np.divide(np.dot(e_w1, bias_axis), np.square(np.linalg.norm(bias_axis))) * bias_axis
e_w2B = np.divide(np.dot(e_w2, bias_axis), np.square(np.linalg.norm(bias_axis))) * bias_axis

# 第5步:根据公式9、10调整e_w1B 与 e_w2B的偏置部分
corrected_e_w1B = np.sqrt(np.abs(1-np.square(np.linalg.norm(mu_orth)))) * np.divide(e_w1B-mu_B, np.abs(e_w1 - mu_orth - mu_B))
corrected_e_w2B = np.sqrt(np.abs(1-np.square(np.linalg.norm(mu_orth)))) * np.divide(e_w2B-mu_B, np.abs(e_w2 - mu_orth - mu_B))

# 第6步: 使e1和e2等于它们修正后的投影之和,从而消除偏差
e1 = corrected_e_w1B + mu_orth
e2 = corrected_e_w2B + mu_orth

return e1, e2

测试:

1
2
3
4
5
6
7
8
print("==========均衡校正前==========")
print("cosine_similarity(word_to_vec_map[\"man\"], gender) = ", cosine_similarity(word_to_vec_map["man"], g))
print("cosine_similarity(word_to_vec_map[\"woman\"], gender) = ", cosine_similarity(word_to_vec_map["woman"], g))
e1, e2 = equalize(("man", "woman"), g, word_to_vec_map)
print("\n==========均衡校正后==========")
print("cosine_similarity(e1, gender) = ", cosine_similarity(e1, g))
print("cosine_similarity(e2, gender) = ", cosine_similarity(e2, g))

预期输出:

==========均衡校正前==========
cosine_similarity(word_to_vec_map[“man”], gender) = -0.117110957653
cosine_similarity(word_to_vec_map[“woman”], gender) = 0.356666188463

==========均衡校正后==========
cosine_similarity(e1, gender) = -0.716572752584
cosine_similarity(e2, gender) = 0.739659647493

请随意使用上方单元格中的输入单词,以将均衡应用于其他单词对。

这些去偏置算法对于减少偏差非常有帮助,但并不完美,并且不能消除所有偏差痕迹。例如,该实现的一个缺点是仅使用单词 _woman_ 和 _man_ 来定义偏差方向$g$。如前所述,如果$g$是通过计算$g_1 = e_{woman} - e_{man}$; $g_2 = e_{mother} - e_{father}$; $g_3 = e_{girl} - e_{boy}$等等来定义的,然后对它们进行平均,可以更好地估计50维单词嵌入空间中的“性别”维度。也可以随意使用这些变体。

恭喜,你完成了本次作业,并且看到了许多使用和修改单词向量的方法。

congratulations!

参考:

作业12 机器翻译--使用注意力机制

这个项目需要配置环境 Faker==4.0.2

神经机器翻译

欢迎来到本周的第一份编程作业!

你将建立一个神经机器翻译(NMT)模型,以将人类可读的日期(”25th of June, 2009”)转换为机器可读的日期(”2009-06-25”)。 你将使用注意力模型来完成此任务,注意力模型是序列模型中最复杂的序列之一。

让我们加载此作业所需的所有软件包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from keras.layers import Bidirectional, Concatenate, Permute, Dot, Input, LSTM, Multiply
from keras.layers import RepeatVector, Dense, Activation, Lambda
from keras.optimizers import Adam
from keras.utils import to_categorical
from keras.models import load_model, Model
import keras.backend as K
import numpy as np

from faker import Faker
import random
from tqdm import tqdm
from babel.dates import format_date
from nmt_utils import *
import matplotlib.pyplot as plt

%matplotlib inline

将人类可读的日期转换成机器可读的日期

你在此处构建的模型可用于将一种语言翻译成另一种语言,例如从英语翻译成印地语。但是,语言翻译需要大量的数据集,通常需要花费数天时间在GPU上进行训练。为了给你提供一个即使不使用大量数据集也可以试验这些模型的地方,我们将使用更简单的“日期转换”任务。

网络将以各种可能的格式输入日期(例如 “the 29th of August 1958”, “03/30/1968”, “24 JUNE 1987”),并将其转换为标准化的机器可读日期(例如”1958-08-29”, “1968-03-30”, “1987-06-24”)。我们将让网络学习如何以通用的机器可读格式YYYY-MM-DD输出日期。

查看nmtutils.py以查看所有格式。计算并弄清楚格式如何工作,之后你将需要应用这些知识。

数据集

我们将在10000个人类可读日期及其等效的标准化机器可读日期的数据集上训练模型。让我们运行以下单元格以加载数据集并打印一些示例。

1
2
3
m = 10000
dataset, human_vocab, machine_vocab, inv_machine_vocab = load_dataset(m)

100%|██████████| 10000/10000 [00:00<00:00, 22610.25it/s]
1
dataset[:10]

预期输出

[(‘9 may 1998’, ‘1998-05-09’),
(‘10.11.19’, ‘2019-11-10’),
(‘9/10/70’, ‘1970-09-10’),
(‘saturday april 28 1990’, ‘1990-04-28’),
(‘thursday january 26 1995’, ‘1995-01-26’),
(‘monday march 7 1983’, ‘1983-03-07’),
(‘sunday may 22 1988’, ‘1988-05-22’),
(‘08 jul 2008’, ‘2008-07-08’),
(‘8 sep 1999’, ‘1999-09-08’),
(‘thursday january 1 1981’, ‘1981-01-01’)]

你已加载:

  • dataset :(人可读日期,机器可读日期)元组列表
  • human_vocab:python字典,将人类可读日期中使用的所有字符映射到整数索引
  • machine_vocab:python字典,将机器可读日期中使用的所有字符映射到整数索引。这些索引不一定与human_vocab一致。
  • inv_machine_vocabmachine_vocab的逆字典,从索引映射回字符。

让我们预处理数据并将原始文本数据映射到索引值。我们还将使用Tx = 30(我们假设这是人类可读日期的最大长度;如果输入的时间更长,则必须截断它)和Ty = 10(因为“YYYY-MM-DD”为10个长字符)。

1
2
3
4
5
6
7
8
9
Tx = 30
Ty = 10
X, Y, Xoh, Yoh = preprocess_data(dataset, human_vocab, machine_vocab, Tx, Ty)

print("X.shape:", X.shape)
print("Y.shape:", Y.shape)
print("Xoh.shape:", Xoh.shape)
print("Yoh.shape:", Yoh.shape)

预期输出

X.shape: (10000, 30)
Y.shape: (10000, 10)
Xoh.shape: (10000, 30, 37)
Yoh.shape: (10000, 10, 11)

你现在拥有:

  • X:训练集中人类可读日期的处理版本,其中每个字符都由通过human_vocab映射到该字符的索引替换。每个日期都用特殊字符()进一步填充为$T_x$值。X.shape = (m, Tx)
  • Y:训练集中机器可读日期的处理版本,其中每个字符都被映射为machine_vocab中映射到的索引替换。你应该具有Y.shape = (m, Ty)
  • XohX的一个独热版本,由于human_vocab,将“1”条目的索引映射到该字符。Xoh.shape = (m, Tx, len(human_vocab))
  • YohY的一个独热版本,由于使用machine_vocab,因此将“1”条目的索引映射到了该字符。Yoh.shape = (m, Tx, len(machine_vocab))在这里,因为有11个字符(“-”以及0-9),所以len(machine_vocab) = 11

我们再看一些预处理训练集的示例。你可以在下面的单元格中随意使用index来查看数据集,并查看如何对source/target日期进行预处理。

1
2
3
4
5
6
7
8
9
10
index = 0
print("Source date:", dataset[index][0])
print("Target date:", dataset[index][1])
print()
print("Source after preprocessing (indices):", X[index])
print("Target after preprocessing (indices):", Y[index])
print()
print("Source after preprocessing (one-hot):", Xoh[index])
print("Target after preprocessing (one-hot):", Yoh[index])

预期输出

Source date: 9 may 1998
Target date: 1998-05-09

Source after preprocessing (indices): [12 0 24 13 34 0 4 12 12 11 36 36 36 36 36 36 36 36 36 36 36 36 36 36
36 36 36 36 36 36]
Target after preprocessing (indices): [ 2 10 10 9 0 1 6 0 1 10]

Source after preprocessing (one-hot): [[0. 0. 0. … 0. 0. 0.]
[1. 0. 0. … 0. 0. 0.]
[0. 0. 0. … 0. 0. 0.]

[0. 0. 0. … 0. 0. 1.]
[0. 0. 0. … 0. 0. 1.]
[0. 0. 0. … 0. 0. 1.]]
Target after preprocessing (one-hot): [[0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
[1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]]

带注意力机制的神经机器翻译

如果你必须将一本书的段落从法语翻译为英语,则无需阅读整个段落然后关闭该书并进行翻译。即使在翻译过程中,你也会阅读/重新阅读并专注于与你所写下的英语部分相对应的法语段落部分。

注意机制告诉神经机器翻译模型在任何步骤都应该注意到的地方。

注意力机制

在这一部分中,你将实现讲座视频中介绍的注意力机制。这是一个提醒你该模型如何工作的图。左图显示了注意力模型。右图显示了“注意”步骤用于计算注意变量$\alpha^{\langle t, t’ \rangle}$,这些变量用于计算上下文变量$context^{\langle t \rangle}$输出中的每个时间步长($t=1, \ldots, T_y$)。

图1:带注意力机制的神经机器翻译

你可能会注意到以下一些模型属性:

  • 此模型中有两个单独的LSTM(请参见左侧的图)。因为图片底部的一个是双向LSTM,并且在“注意力机制”之前出现,所以我们将其称为pre-attention Bi-LSTM。图表顶部的LSTM在注意力机制之后,因此我们将其称为post-attention LSTM。pre-attention Bi-LSTM经过$T_x$个时间步长;post-attention LSTM经过$T_y$个时间步长。

  • post-attention LSTM 从一个时间步长传递到下一个步长,传递$s^{\langle t \rangle}, c^{\langle t \rangle}$ 。在讲座视频中,我们仅将基本RNN用于激活后序列模型,因此RNN输出激活捕获的状态$s^{\langle t\rangle}$。但是,由于我们在这里使用LSTM,因此LSTM既具有输出激活$s^{\langle t\rangle}$,也具有隐藏单元状态$c^{\langle t\rangle}$。但是,与先前的文本生成示例(例如第1周的恐龙)不同,在此模型中,时间$t$的激活后LSTM不会将特定的生成的$y^{\langle t-1 \rangle}$作为输入只需将$s^{\langle t\rangle}$ 和 $c^{\langle t\rangle}$ 作为输入。我们以这种方式设计了模型,因为(与相邻字符高度相关的语言生成不同),在YYYY-MM-DD日期中,上一个字符与下一个字符之间没有那么强的依赖性。

  • 我们使用$a^{\langle t \rangle} = [\overrightarrow{a}^{\langle t \rangle}; \overleftarrow{a}^{\langle t \rangle}]$ 表示pre-attention Bi-LSTM的正向和反向激活的串联。

  • 右图使用RepeatVector节点复制$s^{\langle t-1 \rangle}$的值$T_x$次,然后使用Concatenation来连接$s^{\langle t-1 \rangle}$ 和 $a^{\langle t \rangle}$来计算$e^{\langle t, t’}$,然后将其传递给softmax以计算$\alpha^{\langle t, t’ \rangle}$。我们将在下面的Keras中说明如何使用RepeatVectorConcatenation

让我们实现这个模型。你将从实现one_step_attention()model()两个函数开始:

1)one_step_attention():在步骤$t$中,给出Bi-LSTM的所有隐藏状态()和第二个LSTM的先前隐藏状态(),one_step_attention()将计算注意力权重() 并输出上下文向量(详细信息请参见图1(右)):

请注意,我们在此笔记本中将注意力表示为$context^{\langle t \rangle}$。在讲座视频中,上下文被表示为$c^{\langle t \rangle}$,但在这里我们将其称为$context^{\langle t \rangle}$,以避免与(post-attention)LSTM内部记忆单元变量混淆,有时也称为$c^{\langle t \rangle}$。

2)model():实现整个模型。它首先通过Bi-LSTM运行输入以获取。然后,它调用one_step_attention()$T_y$次(“for”循环)。在此循环的每次迭代中,它将计算出上下文向量提供给第二个LSTM,并通过具有softmax激活的密集层运行LSTM的输出,以生成预测

练习:实现one_step_attention()。函数 model()将使用for循环调用one_step_attention() $T_y$中的层,重要的是所有$T_y$副本具有相同的权重。即,它不应该每次都重新初始化权重。换句话说,所有$T_y$步骤均应具有权重。这是在Keras中实现可共享权重的层的方法:

  1. 定义层对象(例如,作为全局变量)。
  2. 在传播输入时调用这些对象。

我们已经将你需要的层定义为全局变量。请运行以下单元格以创建它们。请检查Keras文档以确保你了解这些层是什么:RepeatVector(), Concatenate(), Dense(), Activation(), Dot()

1
2
3
4
5
6
7
8
# 将共享层定义为全局变量 
repeator = RepeatVector(Tx)
concatenator = Concatenate(axis=-1)
densor1 = Dense(10, activation = "tanh")
densor2 = Dense(1, activation = "relu")
activator = Activation(softmax, name='attention_weights') # 在这个 notebook 我们正在使用自定义的 softmax(axis = 1)
dotor = Dot(axes = 1)

现在你可以使用这些层来实现one_step_attention()。 为了通过这些层之一传播Keras张量对象X,请使用layer(X)(如果需要多个输入则使用layer([X,Y]))。densor(X)将通过上面定义的 Dense(1) 层传播X。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# GRADED FUNCTION: one_step_attention

def one_step_attention(a, s_prev):
"""
执行一步 attention: 输出一个上下文向量,输出作为注意力权重的点积计算的上下文向量
"alphas" Bi-LSTM的 隐藏状态 "a"

参数:
a -- Bi-LSTM的输出隐藏状态 numpy-array 维度 (m, Tx, 2*n_a)
s_prev -- (post-attention) LSTM的前一个隐藏状态, numpy-array 维度(m, n_s)

返回:
context -- 上下文向量, 下一个(post-attetion) LSTM 单元的输入
"""

# 使用 repeator 重复 s_prev 维度 (m, Tx, n_s) 这样你就可以将它与所有隐藏状态"a" 连接起来。 (≈ 1 line)
s_prev = repeator(s_prev)
# 使用 concatenator 在最后一个轴上连接 a 和 s_prev (≈ 1 line)
concat = concatenator([a, s_prev])
# 使用 densor1 传入参数 concat, 通过一个小的全连接神经网络来计算“中间能量”变量 e。(≈1 lines)
e = densor1(concat)
# 使用 densor2 传入参数 e , 通过一个小的全连接神经网络来计算“能量”变量 energies。(≈1 lines)
energies = densor2(e)
# 使用 activator 传入参数 "energies" 计算注意力权重 "alphas" (≈ 1 line)
alphas = activator(energies)
# 使用 dotor 传入参数 "alphas" 和 "a" 计算下一个((post-attention) LSTM 单元的上下文向量 (≈ 1 line)
context = dotor([alphas, a])

return context

在对model()函数进行编码之后,你将能够检查one_step_attention()的预期输出。

练习:按照图2和上面的文字中的说明实现model()。再次,我们定义了全局层,这些全局层将共享将在model()中使用的权重。

1
2
3
4
5
n_a = 32
n_s = 64
post_activation_LSTM_cell = LSTM(n_s, return_state = True)
output_layer = Dense(len(machine_vocab), activation=softmax)

现在你可以在for循环中使用这些层$T_y$次来生成输出,并且它们的参数将不会重新初始化。你将必须执行以下步骤:

  1. 将输入传播到Bidirectional LSTM
  2. 迭代$t = 0, \dots, T_y-1$:

    1. 上调用 one_step_attention() 以获取上下文向量
    2. 分配给post-attention LSTM单元。请记住,使用initial_state= [previous hidden state, previous cell state]传递此LSTM的前一个隐藏状态$s^{\langle t-1\rangle}$和单元状态 $c^{\langle t-1\rangle}$。取回新的隐藏状态和新的单元状态
    3. 将softmax层应用于,获得输出。
    4. 通过将输出添加到输出列表中来保存输出。
  3. 创建你的Keras模型实例,它应该具有三个输入(“inputs”, $s^{<0>}$ and $c^{<0>}$)并输出”outputs”列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# GRADED FUNCTION: model

def model(Tx, Ty, n_a, n_s, human_vocab_size, machine_vocab_size):
"""
参数:
Tx -- 输入序列的长度
Ty -- 输出序列的长度
n_a -- Bi-LSTM的隐藏状态大小
n_s -- post-attention LSTM的隐藏状态大小
human_vocab_size -- python字典 "human_vocab" 的大小
machine_vocab_size -- python字典 "machine_vocab" 的大小

返回:
model -- Keras 模型实例
"""

# 定义模型的输入,维度 (Tx,)
# 定义 s0 和 c0, 初始化解码器 LSTM 的隐藏状态,维度 (n_s,)
X = Input(shape=(Tx, human_vocab_size))
s0 = Input(shape=(n_s,), name='s0')
c0 = Input(shape=(n_s,), name='c0')
s = s0
c = c0

# 初始化一个空的输出列表
outputs = []


# 第一步:定义 pre-attention Bi-LSTM。 记得使用 return_sequences=True. (≈ 1 line)
a = Bidirectional(LSTM(n_a, return_sequences=True), input_shape=(m, Tx, n_a * 2))(X)

# 第二步:迭代 Ty 步
for t in range(Ty):

# 第二步.A: 执行一步注意机制,得到在 t 步的上下文向量 (≈ 1 line)
context = one_step_attention(a, s)

# 第二步.B: 使用 post-attention LSTM 单元得到新的 "context"
# 别忘了使用: initial_state = [hidden state, cell state] (≈ 1 line)
s, _, c = post_activation_LSTM_cell(context, initial_state=[s, c])

# 第二步.C: 使用全连接层处理post-attention LSTM 的隐藏状态输出 (≈ 1 line)
out = output_layer(s)

# 第二步.D: 追加 "out" 到 "outputs" 列表 (≈ 1 line)
outputs.append(out)

# 第三步:创建模型实例,获取三个输入并返回输出列表。 (≈ 1 line)
model = Model(inputs=[X, s0, c0], outputs=outputs)

return model



运行以下单元格以创建模型。

1
2
model = model(Tx, Ty, n_a, n_s, len(human_vocab), len(machine_vocab))

让我们获得模型的总结,以检查其是否与预期输出匹配。

1
2
model.summary()

预期输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) (None, 30, 37) 0
__________________________________________________________________________________________________
s0 (InputLayer) (None, 64) 0
__________________________________________________________________________________________________
bidirectional_1 (Bidirectional) (None, 30, 64) 17920 input_1[0][0]
__________________________________________________________________________________________________
repeat_vector_1 (RepeatVector) (None, 30, 64) 0 s0[0][0]
lstm_1[0][0]
lstm_1[1][0]
lstm_1[2][0]
lstm_1[3][0]
lstm_1[4][0]
lstm_1[5][0]
lstm_1[6][0]
lstm_1[7][0]
lstm_1[8][0]
__________________________________________________________________________________________________
concatenate_1 (Concatenate) (None, 30, 128) 0 bidirectional_1[0][0]
repeat_vector_1[0][0]
bidirectional_1[0][0]
repeat_vector_1[1][0]
bidirectional_1[0][0]
repeat_vector_1[2][0]
bidirectional_1[0][0]
repeat_vector_1[3][0]
bidirectional_1[0][0]
repeat_vector_1[4][0]
bidirectional_1[0][0]
repeat_vector_1[5][0]
bidirectional_1[0][0]
repeat_vector_1[6][0]
bidirectional_1[0][0]
repeat_vector_1[7][0]
bidirectional_1[0][0]
repeat_vector_1[8][0]
bidirectional_1[0][0]
repeat_vector_1[9][0]
__________________________________________________________________________________________________
dense_1 (Dense) (None, 30, 10) 1290 concatenate_1[0][0]
concatenate_1[1][0]
concatenate_1[2][0]
concatenate_1[3][0]
concatenate_1[4][0]
concatenate_1[5][0]
concatenate_1[6][0]
concatenate_1[7][0]
concatenate_1[8][0]
concatenate_1[9][0]
__________________________________________________________________________________________________
dense_2 (Dense) (None, 30, 1) 11 dense_1[0][0]
dense_1[1][0]
dense_1[2][0]
dense_1[3][0]
dense_1[4][0]
dense_1[5][0]
dense_1[6][0]
dense_1[7][0]
dense_1[8][0]
dense_1[9][0]
__________________________________________________________________________________________________
attention_weights (Activation) (None, 30, 1) 0 dense_2[0][0]
dense_2[1][0]
dense_2[2][0]
dense_2[3][0]
dense_2[4][0]
dense_2[5][0]
dense_2[6][0]
dense_2[7][0]
dense_2[8][0]
dense_2[9][0]
__________________________________________________________________________________________________
dot_1 (Dot) (None, 1, 64) 0 attention_weights[0][0]
bidirectional_1[0][0]
attention_weights[1][0]
bidirectional_1[0][0]
attention_weights[2][0]
bidirectional_1[0][0]
attention_weights[3][0]
bidirectional_1[0][0]
attention_weights[4][0]
bidirectional_1[0][0]
attention_weights[5][0]
bidirectional_1[0][0]
attention_weights[6][0]
bidirectional_1[0][0]
attention_weights[7][0]
bidirectional_1[0][0]
attention_weights[8][0]
bidirectional_1[0][0]
attention_weights[9][0]
bidirectional_1[0][0]
__________________________________________________________________________________________________
c0 (InputLayer) (None, 64) 0
__________________________________________________________________________________________________
lstm_1 (LSTM) [(None, 64), (None, 33024 dot_1[0][0]
s0[0][0]
c0[0][0]
dot_1[1][0]
lstm_1[0][0]
lstm_1[0][2]
dot_1[2][0]
lstm_1[1][0]
lstm_1[1][2]
dot_1[3][0]
lstm_1[2][0]
lstm_1[2][2]
dot_1[4][0]
lstm_1[3][0]
lstm_1[3][2]
dot_1[5][0]
lstm_1[4][0]
lstm_1[4][2]
dot_1[6][0]
lstm_1[5][0]
lstm_1[5][2]
dot_1[7][0]
lstm_1[6][0]
lstm_1[6][2]
dot_1[8][0]
lstm_1[7][0]
lstm_1[7][2]
dot_1[9][0]
lstm_1[8][0]
lstm_1[8][2]
__________________________________________________________________________________________________
dense_3 (Dense) (None, 11) 715 lstm_1[0][0]
lstm_1[1][0]
lstm_1[2][0]
lstm_1[3][0]
lstm_1[4][0]
lstm_1[5][0]
lstm_1[6][0]
lstm_1[7][0]
lstm_1[8][0]
lstm_1[9][0]
==================================================================================================
Total params: 52,960
Trainable params: 52,960
Non-trainable params: 0
__________________________________________________________________________________________________


预期输出:

Here is the summary you should see
| Total params: | 185,484 |
| ——————————————————- | ———————- |
| Trainable params: | 185,484 |
| Non-trainable params: | 0 |
| bidirectional_1’s output shape | (None, 30, 128) |
| repeat_vector_1’s output shape | (None, 30, 128) |
| concatenate_1’s output shape | (None, 30, 256) |
| attention_weights’s output shape | (None, 30, 1) |
| dot_1’s output shape | (None, 1, 128) |
| dense_2’s output shape | (None, 11) |

与往常一样,在Keras中创建模型后,你需要对其进行编译并定义要使用的损失,优化器和评价指标。 使用categorical_crossentropy损失,自定义Adam optimizer(learning rate = 0.005, $\beta_1 = 0.9$, $\beta_2 = 0.999$, decay = 0.01)和['accuracy']指标:

1
2
3
4
5
### START CODE HERE ### (≈2 lines)
opt = Adam(lr=0.005, beta_1=0.9, beta_2=0.999, decay=0.01)
model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])
### END CODE HERE ###

最后一步是定义所有输入和输出以适合模型:

  • 你已经拥有包含训练示例的维度为$(m = 10000, T_x = 30)$的X。
  • 你需要创建s0c0以将你的post_activation_LSTM_cell初始化为0。
  • 根据你编码的model(),你需要”outputs”作为11个维度元素(m,T_y)的列表。因此:outputs[i][0], ..., outputs[i][Ty]代表与$i^{th}$训练示例(X[i])。更一般而言, outputs[i][j] 是$i^{th}$训练示例中$j^{th}$字符的真实标签。
1
2
3
4
s0 = np.zeros((m, n_s))
c0 = np.zeros((m, n_s))
outputs = list(Yoh.swapaxes(0,1))

现在让我们拟合模型并运行一个epoch。

1
2
model.fit([Xoh, s0, c0], outputs, epochs=1, batch_size=100)

训练时,你可以看到输出的10个位置中的每个位置的损失以及准确性。下表为你提供了一个示例,说明该批次有2个示例时的精确度:

因此,dense_2_acc_8: 0.89意味着你在当前数据批次中有89%的时间正确预测了输出的第7个字符。

我们对该模型运行了更长的时间,并节省了权重。运行下一个单元格以加载我们的体重。(通过训练模型几分钟,你应该可以获得准确度相似的模型,但是加载我们的模型可以节省你的时间。)

1
2
model.load_weights('models/model.h5')

现在,你可以在新示例中查看结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
EXAMPLES = ['3 May 1979', '5 April 09', '21th of August 2016', 'Tue 10 Jul 2007', 'Saturday May 9 2018', 'March 3 2001', 'March 3rd 2001', '1 March 2001']
for example in EXAMPLES:

source = string_to_int(example, Tx, human_vocab)
source = np.array(list(map(lambda x: to_categorical(x, num_classes=len(human_vocab)), source)))
source = np.expand_dims(source, axis=0)
prediction = model.predict([source, s0, c0])
prediction = np.argmax(prediction, axis = -1)
output = [inv_machine_vocab[int(i)] for i in prediction]

print("source:", example)
print("output:", ''.join(output))

预期输出
source: 3 May 1979
output: 1979-05-03
source: 5 April 09
output: 2009-05-05
source: 21th of August 2016
output: 2016-08-21
source: Tue 10 Jul 2007
output: 2007-07-10
source: Saturday May 9 2018
output: 2018-05-09
source: March 3 2001
output: 2001-03-03
source: March 3rd 2001
output: 2001-03-03
source: 1 March 2001
output: 2001-03-01

你也可以更改这些示例,以使用自己的示例进行测试。下一部分将使你更好地了解注意力机制的作用-即生成特定输出字符时网络要注意的输入部分。

可视化注意力(可选练习)

由于问题的输出长度固定为10,因此还可以使用10个不同的softmax单元来执行此任务,以生成10个字符的输出。但是注意力模型的一个优点是输出的每个部分(例如月份)都知道它只需要依赖输入的一小部分(输入中代表月份的字符)。我们可以可视化输出的哪一部分正在查看输入的哪一部分。

考虑将”Saturday 9 May 2018”转换为”2018-05-09”的任务。如果我们可视化计算出的$\alpha^{\langle t, t’ \rangle}$,我们将得到:

图8 :Full Attention Map

注意输出如何忽略输入的”Saturday”部分。没有一个输出时间步长关注输入的那部分。我们还看到9已被翻译为09,May已被正确翻译为05,而输出则注意进行翻译所需的部分输入。该年份主要要求它注意输入的“18”以生成“2018”。

从网络获取激活

现在让我们可视化你网络中的注意力值。我们将通过网络传播一个示例,然后可视化$\alpha^{\langle t, t’ \rangle}$的值。

为了弄清注意值的位置,让我们开始打印模型摘要。

1
2
model.summary()

预期输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) (None, 30, 37) 0
__________________________________________________________________________________________________
s0 (InputLayer) (None, 64) 0
__________________________________________________________________________________________________
bidirectional_1 (Bidirectional) (None, 30, 64) 17920 input_1[0][0]
__________________________________________________________________________________________________
repeat_vector_1 (RepeatVector) (None, 30, 64) 0 s0[0][0]
lstm_1[0][0]
lstm_1[1][0]
lstm_1[2][0]
lstm_1[3][0]
lstm_1[4][0]
lstm_1[5][0]
lstm_1[6][0]
lstm_1[7][0]
lstm_1[8][0]
__________________________________________________________________________________________________
concatenate_1 (Concatenate) (None, 30, 128) 0 bidirectional_1[0][0]
repeat_vector_1[0][0]
bidirectional_1[0][0]
repeat_vector_1[1][0]
bidirectional_1[0][0]
repeat_vector_1[2][0]
bidirectional_1[0][0]
repeat_vector_1[3][0]
bidirectional_1[0][0]
repeat_vector_1[4][0]
bidirectional_1[0][0]
repeat_vector_1[5][0]
bidirectional_1[0][0]
repeat_vector_1[6][0]
bidirectional_1[0][0]
repeat_vector_1[7][0]
bidirectional_1[0][0]
repeat_vector_1[8][0]
bidirectional_1[0][0]
repeat_vector_1[9][0]
__________________________________________________________________________________________________
dense_1 (Dense) (None, 30, 10) 1290 concatenate_1[0][0]
concatenate_1[1][0]
concatenate_1[2][0]
concatenate_1[3][0]
concatenate_1[4][0]
concatenate_1[5][0]
concatenate_1[6][0]
concatenate_1[7][0]
concatenate_1[8][0]
concatenate_1[9][0]
__________________________________________________________________________________________________
dense_2 (Dense) (None, 30, 1) 11 dense_1[0][0]
dense_1[1][0]
dense_1[2][0]
dense_1[3][0]
dense_1[4][0]
dense_1[5][0]
dense_1[6][0]
dense_1[7][0]
dense_1[8][0]
dense_1[9][0]
__________________________________________________________________________________________________
attention_weights (Activation) (None, 30, 1) 0 dense_2[0][0]
dense_2[1][0]
dense_2[2][0]
dense_2[3][0]
dense_2[4][0]
dense_2[5][0]
dense_2[6][0]
dense_2[7][0]
dense_2[8][0]
dense_2[9][0]
__________________________________________________________________________________________________
dot_1 (Dot) (None, 1, 64) 0 attention_weights[0][0]
bidirectional_1[0][0]
attention_weights[1][0]
bidirectional_1[0][0]
attention_weights[2][0]
bidirectional_1[0][0]
attention_weights[3][0]
bidirectional_1[0][0]
attention_weights[4][0]
bidirectional_1[0][0]
attention_weights[5][0]
bidirectional_1[0][0]
attention_weights[6][0]
bidirectional_1[0][0]
attention_weights[7][0]
bidirectional_1[0][0]
attention_weights[8][0]
bidirectional_1[0][0]
attention_weights[9][0]
bidirectional_1[0][0]
__________________________________________________________________________________________________
c0 (InputLayer) (None, 64) 0
__________________________________________________________________________________________________
lstm_1 (LSTM) [(None, 64), (None, 33024 dot_1[0][0]
s0[0][0]
c0[0][0]
dot_1[1][0]
lstm_1[0][0]
lstm_1[0][2]
dot_1[2][0]
lstm_1[1][0]
lstm_1[1][2]
dot_1[3][0]
lstm_1[2][0]
lstm_1[2][2]
dot_1[4][0]
lstm_1[3][0]
lstm_1[3][2]
dot_1[5][0]
lstm_1[4][0]
lstm_1[4][2]
dot_1[6][0]
lstm_1[5][0]
lstm_1[5][2]
dot_1[7][0]
lstm_1[6][0]
lstm_1[6][2]
dot_1[8][0]
lstm_1[7][0]
lstm_1[7][2]
dot_1[9][0]
lstm_1[8][0]
lstm_1[8][2]
__________________________________________________________________________________________________
dense_3 (Dense) (None, 11) 715 lstm_1[0][0]
lstm_1[1][0]
lstm_1[2][0]
lstm_1[3][0]
lstm_1[4][0]
lstm_1[5][0]
lstm_1[6][0]
lstm_1[7][0]
lstm_1[8][0]
lstm_1[9][0]
==================================================================================================
Total params: 52,960
Trainable params: 52,960
Non-trainable params: 0
__________________________________________________________________________________________________


浏览上面的model.summary()的输出。你可以看到,在每个时间步dot_2计算$t = 0, \ldots, T_y-1$上下文向量之前,名为attention_weights的层都会输出维度为(m, 30, 1)的alphas。让我们从该层获取激活。

函数attention_map()从模型中提取注意力值并绘制它们。

1
2
attention_map = plot_attention_map(model, human_vocab, inv_machine_vocab, "Tuesday 09 Oct 1993", num = 7, n_s = 64)

预期输出

在生成的图上,你可以观察预测输出的每个字符的注意权重值。检查此图,并检查网络对你的关注是否有意义。

在日期转换应用程序中,你会发现大部分时间的注意力都有助于预测年份,并且对预测日期/月份没有太大影响。

恭喜!你已结束本作业

应记住的内容

  • 机器翻译模型可用于从一个序列映射到另一个序列。它们不仅对翻译人类语言(如法语->英语)有用,而且对日期格式翻译等任务也很有用。
  • 注意机制允许网络在产生输出的特定部分时将注意力集中在输入的最相关部分。
  • 使用注意力机制的网络可以将长度为$T_x$的输入转换为长度为$T_y$的输出,其中$T_x$和$T_y$可以不同。
  • 你可以可视化注意权重$\alpha^{\langle t,t’ \rangle}$,以查看网络在生成每个输出时要注意的内容。

恭喜你完成此作业! 现在,你可以实现注意力模型,并使用它来学习从一个序列到另一个序列的复杂映射。

代码下载