本文是笔者看完博主何宽的博文和github上Kulbear的文章Logistic Regression with a Neural Network mindset后写下,代码内容非原创。笔者在动手做的过程中因为基础太差遇到了一些问题,后来回头想想这些内容很基础,但有时不知道某个点的确容易浪费掉一些时间,故写下这篇文章详细记录我在学习过程中的一些理解和我觉得容易引起误解的地方。文章篇幅较长,如果已经对深度学习有些基础但又想看吴恩达的作业或者只是想随便看看神经网络的搭建过程建议去看博主何宽的博文,如果因为基础太差在做的过程中反复查资料,我想这篇文章可以帮你解答很多疑惑。本文力求帮助一个只会矩阵相乘的小白使用Logistic 回归实现一个识别猫的神经网络,如果看完吴恩达第二周的课程最好,没看也没关系,需要的地方我都会做解释说明。文中会穿插许多有关定义的讲解,还会写下笔者在编码过程中混淆过的问题,希望能够想要入门深度学习神经网络的朋友引以为鉴。文章以交流的方式表述,难免出现一些不严谨的地方,欢迎大家批评指正。
开始前先把资料准备好,放入一个空的文件夹下,点此下载 提取码:ga0x
数据预处理
直接看代码,在讲解过程中我们先用后讲。
lr_utils.py
文件用于加载数据集,不是我们的重点,只需要搞懂加载后图片的数学表示,其中我也对代码进行了一些解释,可以看一下,不会耽误太多时间。
import numpy as np
import h5py # 与H5文件中存储的数据集进行交互的常用软件包
def load_dataset():
train_dataset = h5py.File('datasets/train_catvnoncat.h5', "r")
# print(train_dataset) # <HDF5 file "train_catvnoncat.h5" (mode r)>
# print(list(train_dataset)) # ['list_classes', 'train_set_x', 'train_set_y']
# print(np.array(train_dataset)) # ['list_classes' 'train_set_x' 'train_set_y']
"""
train_dataset中包含三项,分别为classes列表,训练集输入train_set_x,训练集输出标签 train_set_y。这里我使用列表和numpy数组两种形式打印是为了先让读者从打印结果上看一下numpy数组与普通列表数组的区别。
print([1, 2, 3])
print(np.array([1, 2, 3]))
[1, 2, 3]
[1 2 3]
"""
# print(train_dataset["train_set_x"]) # <HDF5 dataset "train_set_x": shape (209, 64, 64, 3), type "|u1">
train_set_x_orig = np.array(train_dataset["train_set_x"][:])
# print(train_set_x_orig) # [:]是切片操作选出指定数量,我们选全部,[:]可以不加
"""
[
[[[ 17 31 56]
[ 22 33 59]
[ 25 35 62]
... 64个一层
[ 1 28 57]
[ 1 26 56]
[ 1 22 51]]
... 64个两层
[[ 25 36 62]
[ 28 38 64]
[ 30 40 67]
...
[ 1 27 56]
[ 1 25 55]
[ 2 21 51]]]
...209个三层
[[[196 192 190]
[193 186 182]
[188 179 174]
...
[ 90 142 200]
[ 90 142 201]
[ 90 142 201]]]
]
"""
train_set_y = np.array(train_dataset["train_set_y"][:]) # your train set labels
# print(train_set_y)
"""
[0 0 1 0 0 0 0 1 0 0 0 1 0 1 1 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 0 0
0 1 0 0 1 1 0 0 0 0 1 0 0 1 0 0 0 1 0 1 1 0 1 1 1 0 0 0 0 0 0 1 0 0 1 0 0
0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 0 0 1 1 1 0 0 1 0 0 0 0 1 0 1 0 1 1 1 1 1
1 0 0 0 0 0 1 0 0 0 1 0 0 1 0 1 0 1 1 0 0 0 1 1 1 1 1 0 0 0 0 1 0 1 1 1 0
1 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0 1 0 1 0 0 1 1 1 0 0 1 1 0 1 0 1 0 0 0 0 0
1 0 0 1 0 0 0 1 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0]
内容为一个numpy类型的一维数组,输出train_set_y.shape时会发现结果为(209,),并不是(1,209)。
"""
test_dataset = h5py.File('datasets/test_catvnoncat.h5', "r")
test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
test_set_y = np.array(test_dataset["test_set_y"][:]) # your test set labels
# 上三行代码不再过多赘述
classes = np.array(test_dataset["list_classes"][:]) # the list of classes
# print(classes) # [b'non-cat' b'cat']
# 下两行代码就是填前面维度的坑,由数组变为矩阵,(209,) -> (1, 209)
train_set_y = train_set_y_orig.reshape(1, train_set_y.shape[0]) # (209,),train_set_y_orig.shape[0] = 209
test_set_y = test_set_y_orig.reshape(1, test_set_y.shape[0])
return train_set_x_orig, train_set_y, test_set_x_orig, test_set_y, classes
现在调用load_dataset()
可以得到5个numpy类型的数组,分别是:训练集输入数组(209, 64, 64, 3),测试集输入数组(50, 64, 64, 3),训练集标签数组(1, 209),测试集标签数组(1, 209),分类结果标签“词典”(2,)。
题外话:[1]是列表,不具有shape属性,不支持矩阵运算的。通过
np.array([1])
将其转换为numpy类型一维数组,输出的shape为(1,)。通过np.array([[1]])
将其转换为numpy类型二维数组,输出的shape为(1,1)。
起一个名为main.py
的文件,导入必要的包
import numpy as np # 进行科学计算的基本软件包
import matplotlib.pyplot as plt # 用于在Python中绘制图表
from lr_utils import load_dataset # 我们之前做的函数
将图像数据加载到主程序:
train_set_x_orig, train_set_y, test_set_x_orig, test_set_y, classes = load_dataset()
我们可以尝试绘制图像,使用plt.imshow()
题外话:
plt.imshow()
的输入可以是以下几种类型:二维数组:如果输入是一个二维数组,它会被解释为灰度图像,其中每个元素表示图像中的一个像素的灰度值。
三维数组:如果输入是一个三维数组,通常是一个形状为 (height, width, channels) 的数组,其中 height 和 width 分别表示图像的高度和宽度,channels 表示图像的通道数(例如,RGB图像有3个通道),则
plt.imshow()
会将其解释为彩色图像。
也就是说现在我们只需要给plt.imshow()
一个三维数组参数即可,通过加载图像,我们获得了train_set_x_orig
(209, 64, 64, 3),暂且把它叫做包含209张图片的三维数组。所以可以通过下标访问每个图片对应的三维数组。
plt.imshow(train_set_x_orig[25])
# print(train_set_x_orig[25])
"""
[[[15 15 5]
[17 17 6]
[17 17 7]
...64个
[ 1 1 0]
[ 1 1 1]
[ 1 1 1]]
...64个
[[14 19 14]
[21 29 25]
[31 37 33]
...
[ 2 4 2]
[ 1 2 1]
[ 0 0 0]]]
"""
假设我们不知道他是猫,我们可以通过加载的数据train_set_y和classes查看
print("y=" + str(train_set_y[:,25]) + ", it's a " + classes[np.squeeze(train_set_y[:,25])].decode("utf-8") + "' picture")
打印结果:
y=[1], it's a cat' picture
题外话:我们加载后得到的train_set_y是一个二维数组,这里说二维是因为如果打印train_set_y.shape结果是(1,209),而不是(209,),所以使用train_set_y[:,25]访问得到的是由train_set_y的所有行的第26列组成的一维数组(),因为这里只有一行,打印的结果便为第26列中的“1”组成的一维数组
[1]
,要取出这个1也很简单,train_set_y[: index] [0]
。上面代码使用了一个压缩函数np.squeeze()
,np.squeeze()
函数用于从数组中移除长度为1的维度。[1]去掉一维结果也就是1了,再拿这个下标索引去classes
中找他的“翻译”即可。
继续,我们要做预测至少要知道图像的大小,训练集和测试集的数量,这些数据还没有给出,其实已经可以通过加载函数获得的那5个矩阵来得出了,而且那5个矩阵也不只一个隐含了数据的数量大小等信息。
num_pxnum_pxm_trainm_trainm_train = train_set_x_orig.shape[0] # 训练集里图片的数量train_set_x_orig:(209,64,64,3),当然也可以使用train_set_y.shape[1] (1,209)
m_test = test_set_x_orig.shape[0] # 测试集里样本的数量。
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))
"""
训练集的数量: m_train = 209
测试集的数量 : m_test = 50
每张图片的宽/高 : num_px = 64
每张图片的大小 : (64, 64, 3)
训练集_图片的维数 : (209, 64, 64, 3)
训练集_标签的维数 : (1, 209)
测试集_图片的维数: (50, 64, 64, 3)
测试集_标签的维数: (1, 50)
"""
想象一下,现在我们知道了训练集和测试集的数量,也知道了每张图片对应的像素点,每个像素点(共64x64个)由RGB三原色组成,所以我们可以把每个图像归为由64x64x3 = 12288个特征共同决定,也就是每个图像有12288个输入特征,每个输入特征值在0~255之间。现在每个图片的特征是三维数组,形状是(64,64,3),为了便于计算,我们将这个三维数组通过拉伸变为一维,批量处理这209+50张图像。看代码:
# X_flatten = X.reshape(X.shape[0],-1).T #X.T是X的转置
# 将训练集的维度降低并转置。
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))
"""
训练集降维最后的维度: (12288, 209)
训练集_标签的维数 : (1, 209)
测试集降维之后的维度: (12288, 50)
测试集_标签的维数 : (1, 50)
"""
题外话:train_set_x_orig.reshape(209,-1),reshape函数可以重新帮我们改变矩阵的维度及每维的长度,这里将train_set_x_orig由(209,64,64,3)变为(209,12288),参数-1是让函数自己计算12288。我们习惯把输入样本表示为列索引 [ x ( 0 ) x ( 1 ) x ( 2 ) . . . . x ( 208 ) ] [x^{(0)}x^{(1)}x^{(2)}....x^{(208)}] [x(0)x(1)x(2)....x(208)],所以也就有了代码最后的转置操作。这里注意区分,此处的x上标并不是每个图像的输入特征x,而是每一个图像全部的输入特征组成的一列,x为12288行,1列的矩阵。
不知道有没有读者和我有相同的疑惑,为什么不把代码
train_set_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T
替换为
train_set_x_flatten = train_set_x_orig.reshape(-1, train_set_x_orig.shape[0])
还要搞一个转置。我的理解是第一种方法得到那12288个数据的排列方式会保持原来的排列,保持与四维(209,64,64,3)以209所在的维度去遍历 209 个的三维数组(64,64,3)的顺序,第二种方法会打乱这个顺序,使得输入图像无法与标签对应起来,所以在测试集上的测试结果也就变的很糟糕了。
图像处理中的一种常见预处理步骤是将图像的像素值归一化(缩放)到一个范围内,通常是 [0, 1] 范围内。有助于提高深度学习模型的性能和稳定性。这个操作通常在训练前对数据进行,以确保输入数据具有一致的数值范围。
这里我们对输入数据进行归一化,因为每张图像的输入特征值都在0~255之间,我们只需要对每个输入元素除以255即可。
train_set_x = train_set_x_flatten / 255
test_set_x = test_set_x_flatten / 255
总结下上面的工作成果,我们得到了什么?
数据项 | 解释 |
---|---|
m_train | 训练集图像数量:209 |
m_test | 测试集图像数量:50 |
num_px | 训练集和测试级图像高度和宽度 64 |
train_set_x | 训练集所有图像特征,大小为(12288,209),理解为 [ x ( 0 ) x ( 1 ) x ( 2 ) . . . . x ( 208 ) ] [x^{(0)}x^{(1)}x^{(2)}....x^{(208)}] [x(0)x(1)x(2)....x(208)]每一列代表一个图像 |
train_set_y | 训练集图像标签,大小为(1,209) |
test_set_x | 测试集所有图像特征,大小为(12288,50) |
test_set_y | 测试集图像标签,大小为(1,50) |
classes | 分类“词典”,长这样[b’non-cat’ b’cat’],大小为 (2,) |
表格中涉及到的数组都为numpy类型,都要满足广播机制,接下来我们只需要记住表格内容。
题外话:说一下广播机制。看一下官方解释:In order to broadcast, the size of the trailing axes for both arrays in an operation must either be the same size or one of them must be one.(DeepL给出的翻译:要进行广播,操作中两个数组的尾轴大小必须相同,或者其中一个数组的尾轴大小必须为 1)
满足广播机制需要两个条件:
- 最后维度长度大小相等。
- 其中一个维度的大小为1 。
如a(4,3),b(3,1,2),求ab元素级相乘的结果(既不是点乘也不是叉乘)。
从后往前看,a:3,b:2,不满足,所以不能元素级相乘。将b改为(3,1,3)。从后往前,a:3,b:3,满足条件1,a:4,b:1,满足条件2,再往前,a没有值,默认取1,b:3,满足条件2,所以可以元素级相乘。广播机制在数组的加减乘除中都能使用,一个多维数组加一个标量也是可以的。
一句话总结广播机制:数组在不同维度上的复制拉伸。
网络搭建
题外话:来个概念,logistic回归
logistic回归是一种广义线性回归(generalized linear model),因此与多重线性回归分析有很多相同之处。它们的模型形式基本上相同,都具有 w‘x+b,其中w和b是待求参数,其区别在于他们的因变量不同,多重线性回归直接将w‘x+b作为因变量,即y =w‘x+b,而logistic回归则通过函数L将w‘x+b对应一个隐状态p,p =L(w‘x+b),然后根据p 与1-p的大小决定因变量的值。如果L是logistic函数(本文为Sigmoid函数),就是logistic回归,如果L是多项式函数就是多项式回归。
先看整体计算过程
注意区分一下,这里的
x
\mathrm x
x的上标为第几个图像,下标为我们每个图像的那12288个特征。我们把
X
X
X记为
X
=
[
x
0
(
0
)
x
0
(
1
)
.
.
.
x
0
(
208
)
x
1
(
0
)
x
1
(
1
)
.
.
.
x
1
(
208
)
.
.
.
x
12287
(
0
)
x
12287
(
1
)
.
.
.
x
12287
(
208
)
]
=
[
x
(
0
)
x
(
1
)
x
(
2
)
.
.
.
.
x
(
208
)
]
X=\begin{bmatrix} x_{0}^{(0)}& x_{0}^{(1)}& ... & x_{0}^{(208)}\\x_{1}^{(0)}& x_{1}^{(1)}& ... & x_{1}^{(208)}\\...\\ x_{12287}^{(0)} & x_{12287}^{(1)} & ... & x_{12287}^{(208)}\end{bmatrix}=[x^{(0)}x^{(1)}x^{(2)}....x^{(208)}]
X=
x0(0)x1(0)...x12287(0)x0(1)x1(1)x12287(1).........x0(208)x1(208)x12287(208)
=[x(0)x(1)x(2)....x(208)]
吴恩达视频中的公式如下
z
(
i
)
=
w
T
x
(
i
)
+
b
(1)
\mathrm {z^{(i)}=w^T x^{(i)}+b} \tag{1}
z(i)=wTx(i)+b(1)
y ^ ( i ) = a ( i ) = s i g m o i d ( z ( i ) ) (2) \mathrm{\hat y^{(i)}=a^{(i)}=sigmoid(z^{(i)})} \tag{2} y^(i)=a(i)=sigmoid(z(i))(2)
L ( a ( i ) , y ( i ) ) = − y ( i ) log ( a ( i ) ) − ( 1 − y ( i ) ) log ( 1 − a ( i ) ) (3) \mathrm{\mathcal{L}(a^{(i)},y^{(i)})=-y^{(i)}\log(a^{(i)})-(1-y^{(i)})\log(1-a^{(i)})} \tag{3} L(a(i),y(i))=−y(i)log(a(i))−(1−y(i))log(1−a(i))(3)
J = 1 m ∑ i = 1 m L ( a ( i ) , y ( i ) ) (4) \mathrm J=\dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m}\mathcal{L}(\mathrm a^{(\mathrm i)},\mathrm y^{(\mathrm i)}) \tag{4} J=m1i=1∑mL(a(i),y(i))(4)
题外话:这里我们跟随吴恩达老师的课,将公式3叫损失函数,将公式4叫做成本函数,在有些时候我们也会直接将公式4叫做损失函数。
一个个解释。先从大的的视角,我们要做什么?我们要找到一个函数,这个函数能够满足输入一个 x \mathrm x x,它能给我们输出一个 y \mathrm y y,这个 y \mathrm y y就是 x \mathrm x x是什么东西的可能性,或置信度。在这里可能性大于0.5我们就说图像是猫。
那我们现在开始试着构造这个函数,使用Logistic回归,它和线性回归很相似,看下线性回归公式1,要想求
w
\mathrm w
w和
b
\mathrm b
b就需要多个已知的
x
\mathrm x
x和
z
\mathrm z
z来构造出这个线性回归,而Logistic回归就是将
z
\mathrm z
z再进行一次转换,如公式2所示,对
z
\mathrm z
z使用Sigmoid函数(如下图),将
z
\mathrm z
z映射为一个在0到1之间的数
y
\mathrm y
y。
σ
(
z
)
=
1
1
+
e
−
z
\sigma(z) = \frac{1}{1 + e^{-\mathrm z}}
σ(z)=1+e−z1
sigmoid函数代码:
def sigmoid(z):
"""
参数:
z - 标量或numpy数组。
返回:
s - sigmoid(z)
"""
s = 1 / (1 + np.exp(-z))
return s
在一个简单的线性回归中
w
\mathrm w
w,
b
\mathrm b
b只是一个数,我们这不一样,细心的朋友应该发现了,公式1中的
w
\mathrm w
w加了转置符号。还记得我们输入的
x
(
i
)
\mathrm x^{(i)}
x(i)吗,一个列向量作为一个单独的图像输入,大小为(12288,1),我们要把这一个图像的12288个输入特征都假设为一个单独的输入,那就需要12288个
w
\mathrm w
w去匹配这些输入。我想你应该可以想象出
w
\mathrm w
w的样子了。
w
=
[
w
0
w
1
.
.
.
w
12287
]
\mathrm w=\begin{bmatrix} \mathrm w_{0}\\\mathrm w_{1}\\...\\ \mathrm w_{12287}\end{bmatrix}
w=
w0w1...w12287
根据公式1求
z
(
i
)
\mathrm z^{(i)}
z(i),公式1中
b
\mathrm b
b是一个标量。
w
\mathrm w
w(12288,1)转置以后为
w
T
\mathrm w^T
wT(1,12288),
x
(
i
)
\mathrm x^{(i)}
x(i)形状为(12288,1),所以
z
(
i
)
\mathrm z^{(i)}
z(i)最后也为一个数。
将计算后的
z
\mathrm z
z带入sigmoid
函数我们就可以一个
y
\mathrm y
y的“雏形”(这样叫是因为我还未初始化
w
\mathrm w
w,
b
\mathrm b
b,也没有进行任何的优化,这也是第一轮前向传播的过程)。
初始化参数 w \mathrm w w, b \mathrm b b,这里都将他们初始化为0。
def initialize_with_zeros(dim):
"""
此函数为w创建一个维度为(dim,1)的0 向量,并将b初始化为0。
参数:
dim - 在这里是传入的特征数目
返回:
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
现在我们就可以根据公式3和公式4通过正向传播计算损失函数了,正向传播可以理解为已知 w \mathrm w w和 b \mathrm b b求解 y ^ ( i ) \mathrm {\hat y^{(i)}} y^(i)的过程。
题外话:这里是最难理解的地方,有些朋友觉得很莫名奇妙就出来损失函数的概念。什么是损失函数?为什么损失函数是那个样子?我为什么要让损失函数最小,我的出发点是什么?
下面我在公式3基础上解答上面的问题。
- 什么是损失函数?
损失函数(Loss Function),也称为成本函数(Cost Function)或目标函数(Objective Function),是在机器学习和深度学习中用来度量模型预测值与实际观测值之间差异的数学函数。损失函数的定义取决于任务类型(例如回归、分类、聚类等)和问题的性质。损失函数的目标通常是最小化它的值。
- 为什么损失函数是那个样子?为什么要让损失函数最小,出发点是什么?
我们先不看损失函数的样子,还记得一开始我们的目的吗,找一个函数,通过修整它的w和b来使得我们输入一个 x \mathrm x x获得它是某个东西的概率 y ^ \mathrm {\hat y} y^。这里我们做的是二分类,所以P>0.5我们就说 x \mathrm x x是那个东西。
If y = 1 : p ( y ∣ x ) = y ^ If y = 0 : p ( y ∣ x ) = 1 − y ^ \begin{array}{ccc}\text{If}&\mathrm y=1{:}&p(\mathrm y|\mathrm x)=\hat{\mathrm y}\\\text{If}&\mathrm y=0{:}& p(\mathrm y|\mathrm x)=1-\hat{\mathrm y}\end{array} IfIfy=1:y=0:p(y∣x)=y^p(y∣x)=1−y^
看下这两个判断,如果 y = 1 \mathrm y=1 y=1(图像是猫),当我们对函数输入一个 x \mathrm x x得到 y = 1 \mathrm y=1 y=1(判断它是猫)的概率是 y ^ \mathrm{\hat y} y^。相反如果 y = 0 \mathrm y=0 y=0(图像不是猫),那我么对函数输入一个 x \mathrm x x得到 y = 0 \mathrm y=0 y=0(判断它不是猫)的概率就应为 1 − y ^ 1-\mathrm{\hat y} 1−y^,这里不要想复杂,只看第一个判断理解,第二个判断是对立的(从“是”的概率中扣除“不是”的概率)。所以我们接下来要做的工作就是提高预测概率P也就是 y ^ \mathrm{\hat y} y^的大小。很巧,对于上面那两个判断中的概率P,我们有这样一个公式可以同时包含两个判断
p ( y ∣ x ) = y ^ y ( 1 − y ^ ) ( 1 − y ) p(\mathrm y|\mathrm x)=\widehat{\mathrm y}^\mathrm y(1-\widehat{\mathrm y})^{(1-\mathrm y)} p(y∣x)=y y(1−y )(1−y)
当 y = 1 \mathrm y=1 y=1时, p ( y ∣ x ) = y ^ p(\mathrm y|\mathrm x)=\widehat{\mathrm y} p(y∣x)=y ,当 y = 0 \mathrm y=0 y=0时, p ( y ∣ x ) = 1 − y ^ p(\mathrm y|\mathrm x)=1-\widehat{\mathrm y} p(y∣x)=1−y ,所以我们只要想办法让 p ( y ∣ x ) p(\mathrm y|\mathrm x) p(y∣x)尽可能大就好了。为了简化计算我们对 p ( y ∣ x ) p(\mathrm y|\mathrm x) p(y∣x)取对数,化简后可以得到
l o g p ( y ∣ x ) = y l o g y ^ + ( 1 − y ) l o g ( 1 − y ^ ) = − L ( a ( i ) , y ( i ) ) logp(\mathrm y|\mathrm x)=\mathrm ylog\widehat{\mathrm y}+(1-y)log(1-\widehat{\mathrm y})=-\mathcal{L}(\mathrm a^{(i)},\mathrm y^{(i)}) logp(y∣x)=ylogy +(1−y)log(1−y )=−L(a(i),y(i))
这里 l o g p ( y ∣ x ) logp(\mathrm y|\mathrm x) logp(y∣x)的结果正是损失函数 L ( a ( i ) , y ( i ) ) \mathcal{L}(\mathrm a^{(i)},\mathrm y^{(i)}) L(a(i),y(i))的相反数,所以我们要想让 p ( y ∣ x ) p(\mathrm y|\mathrm x) p(y∣x)尽可能大,就得让 l o g p ( y ∣ x ) logp(\mathrm y|\mathrm x) logp(y∣x)尽可能大,就得让 L ( a ( i ) , y ( i ) ) \mathcal{L}(\mathrm a^{(i)},\mathrm y^{(i)}) L(a(i),y(i))尽可能小,也就是损失函数尽可能小。而上面我们的讨论全都是基于一个样本而言,也就是最后我们算出的 L ( a ( i ) , y ( i ) ) \mathcal{L}(\mathrm a^{(i)},\mathrm y^{(i)}) L(a(i),y(i))是一个形状为(1,209)的二维数组,它的209列的每一列都为一个样本预测的损失值,我们需要计算的整个训练集的损失。所以成本函数需要除以m_train,也就是公式中的 m m m。这样我们就得到了一个 J J J(成本函数)关于 w \mathrm w w和 b \mathrm b b的函数。之后的操作就是通过反向传播来减小这个成本函数的值。
在提反向传播前我想先说一下关于梯度下降的直观理解。
这里假设我们只有一个
J
J
J关于
w
\mathrm w
w的函数。给
w
\mathrm w
w一个初值,可以是图中的v1(蓝色)或v2(绿色)。假设取v1,我们要计算在
J
J
J取最小时
w
\mathrm w
w的值是多少,也就是红色点T的
w
\mathrm w
w值。先不考虑公式,只看函数图像思考一下应该怎么做?我们应该增大
w
\mathrm w
w的值让它与T处的
w
\mathrm w
w值一样或者近似相等。但计算机可不会看图,怎么从函数
J
J
J上提取到
w
\mathrm w
w需要增大的信息呢?这里需要用到函数
J
J
J在v1处导数正负的信息,导数大家应该都不会陌生,在T的左侧
J
J
J的导数是负值,而在T的右侧
J
J
J的导数是正值。导数的正负就为我们提供了
w
\mathrm w
w需要向哪移动的信息。我想你大致知道更新
w
\mathrm w
w的公式的样子了,
w
:
=
w
−
d
J
(
w
)
d
w
\mathrm w:=\mathrm w-\frac{dJ(\mathrm w)}{d\mathrm w}
w:=w−dwdJ(w),想一下,在v1处
d
J
(
w
)
d
w
\frac{dJ(\mathrm w)}{d\mathrm w}
dwdJ(w)小于0,
w
\mathrm w
w减去一个小于0的数会增大(右移),在V2处
d
J
(
w
)
d
w
\frac{dJ(\mathrm w)}{d\mathrm w}
dwdJ(w)大于0,
w
\mathrm w
w减去一个大于0的数会减小(左移),在这个过程中初值
w
\mathrm w
w会朝着T处
w
\mathrm w
w值靠近。那么问题又来了,万一
d
J
(
w
)
d
w
\frac{dJ(\mathrm w)}{d\mathrm w}
dwdJ(w)的绝对值很大,那更新一次岂不是直接越过了T处的
w
\mathrm w
w值?其实我们只需要
d
J
(
w
)
d
w
\frac{dJ(\mathrm w)}{d\mathrm w}
dwdJ(w)给我们提供的方向信息,不需要关注它具体的数值,所以在求出
d
J
(
w
)
d
w
\frac{dJ(\mathrm w)}{d\mathrm w}
dwdJ(w)后我们对它乘一个
α
\alpha
α,
α
\alpha
α取值一般会很小,它要减小由
d
J
(
w
)
d
w
\frac{dJ(\mathrm w)}{d\mathrm w}
dwdJ(w)数值大小在一次更新中的影响,这个
α
\alpha
α就是学习率,所以完整公式:
w
:
=
w
−
α
d
J
(
w
)
d
w
\mathrm w:=\mathrm w-\alpha\frac{dJ(\mathrm w)}{d\mathrm w}
w:=w−αdwdJ(w)
上面只是一个简单的
J
J
J关于
w
\mathrm w
w的函数,在我们的例子里,
J
J
J是关于
w
\mathrm w
w和
b
\mathrm b
b的函数,我们需要同时更新
w
\mathrm w
w和
b
\mathrm b
b,
b
\mathrm b
b的更新公式和
w
\mathrm w
w一样:
b
:
=
b
−
α
d
J
(
b
)
d
b
b:=b-\alpha\frac{dJ(b)}{db}
b:=b−αdbdJ(b)
接下来问题明确了,求
d
J
(
w
,
b
)
d
w
\frac{dJ(w,b)}{dw}
dwdJ(w,b)和
d
J
(
w
,
b
)
d
b
\frac{dJ(w,b)}{db}
dbdJ(w,b),为了方便我把
J
J
J的公式再放到下面
J
=
1
m
∑
i
=
1
m
L
(
a
(
i
)
,
y
(
i
)
)
\mathrm J=\dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m}\mathcal{L}(\mathrm a^{(\mathrm i)},\mathrm y^{(\mathrm i)})
J=m1i=1∑mL(a(i),y(i))
观察一下可以推出
d
J
(
w
,
b
)
d
w
=
1
m
∑
i
=
1
m
d
L
(
a
(
i
)
,
y
(
i
)
)
d
w
(5)
\frac{dJ(\mathrm w,b)}{d\mathrm w}=\dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m}\frac{d\mathcal{L}(\mathrm a^{(\mathrm i)},\mathrm y^{(\mathrm i)}) }{d\mathrm w} \tag{5}
dwdJ(w,b)=m1i=1∑mdwdL(a(i),y(i))(5)
继续求
d
L
(
a
,
y
)
d
w
\frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w}
dwdL(a,y),这里需要用到链式求导法则,根据公式123,公式写在下面:
d
L
(
a
,
y
)
d
w
=
d
L
(
a
,
y
)
d
a
⋅
d
a
d
z
⋅
d
z
d
w
(6)
\frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w}=\frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm a} \cdot \frac{d\mathrm a}{dz}\cdot \frac{dz}{d\mathrm w} \tag{6}
dwdL(a,y)=dadL(a,y)⋅dzda⋅dwdz(6)
其中
d
L
(
a
,
y
)
d
a
=
−
y
a
+
1
−
y
1
−
a
\frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm a}=-\frac{\mathrm y}{\mathrm a}+\frac{1-\mathrm y}{1-\mathrm a}
dadL(a,y)=−ay+1−a1−y,
d
a
d
z
=
a
(
1
−
a
)
\frac{d\mathrm a}{dz}=\mathrm a(1-\mathrm a)
dzda=a(1−a),
d
z
d
w
=
x
\frac{dz}{d\mathrm w}= \mathrm x
dwdz=x,带入可得:
d
L
(
a
,
y
)
d
w
=
x
(
a
−
y
)
(7)
\frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w}=\mathrm x(\mathrm a-\mathrm y) \tag{7}
dwdL(a,y)=x(a−y)(7)
同理可得:
d
L
(
a
,
y
)
d
b
=
a
−
y
(8)
\frac{d\mathcal{L}(\mathrm a,\mathrm y)}{db}=\mathrm a-\mathrm y \tag{8}
dbdL(a,y)=a−y(8)
题外话:在推导计算过程中为了减少误解从公式6开始我省略掉了 a \mathrm a a和 y \mathrm y y上标 i i i, i i i本来是为了说明对一张图像的操作,而从我们计算 d L ( a , y ) d w \frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w} dwdL(a,y)开始都是基于一张图像。还有一个点,注意区分 y \mathrm y y和 y ^ \mathrm{\hat y} y^, y \mathrm y y是真实标签, y ^ \mathrm {\hat y} y^是我们自己的函数计算的标签。
到这我们已经可以得到
w
\mathrm w
w和
b
\mathrm b
b具体的更新公式了:
w
:
=
w
−
α
⋅
1
m
∑
i
=
1
m
x
(
i
)
(
a
(
i
)
−
y
(
i
)
)
(9)
\mathrm w:=\mathrm w-\alpha \cdot \dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m} \mathrm x^{(\mathrm i)}(\mathrm a^{(\mathrm i)}-\mathrm y^{(\mathrm i)}) \tag{9}
w:=w−α⋅m1i=1∑mx(i)(a(i)−y(i))(9)
b : = b − α ⋅ 1 m ∑ i = 1 m ( a ( i ) − y ( i ) ) (10) \mathrm b:=\mathrm b-\alpha \cdot \dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m} (\mathrm a^{(\mathrm i)}-\mathrm y^{(\mathrm i)}) \tag{10} b:=b−α⋅m1i=1∑m(a(i)−y(i))(10)
接下来要做的无非就是根据新的 w \mathrm w w和 b \mathrm b b重复进行前向传播和反向传播,直到函数在测试集预测准确率上具有最好的表现。我们可以根据实际情况来选择合适的学习率 α \alpha α和重复次数。
题外话:其实到这里我们已经说完了构建神经网络的过程,剩下的只有编码工作了。我想在编码前再梳理一下,可能会有些啰嗦,但还是想说一下,因为编码中涉及到矩阵相乘,矩阵元素级相乘,如果公式理不清会越做越乱,尤其是什么时候使用转置,什么时候对矩阵使用列或行求和,如果前面讲的都能理解,上面公式中我随便提一个参数你能直接说出它的形状那直接去看代码就好了。
为方便观看我将公式1-4再放到下面,同时根据我们的实例标记上公式中大于一维的参数的形状。
z ( i ) = w T ( 1 , 12288 ) x ( i ) ( 12288 , 1 ) + b (1) \mathrm{z^{(i)}=w^T(1,12288)x^{(i)}(12288,1) +b} \tag{1} z(i)=wT(1,12288)x(i)(12288,1)+b(1)y ^ ( i ) = a ( i ) = s i g m o i d ( z ( i ) ) (2) \mathrm{\hat y^{(i)}=a^{(i)}=sigmoid(z^{(i)})} \tag{2} y^(i)=a(i)=sigmoid(z(i))(2)
L ( a ( i ) , y ( i ) ) = − y ( i ) log ( a ( i ) ) − ( 1 − y ( i ) ) log ( 1 − a ( i ) ) (3) \mathrm{\mathcal{L}(a^{(i)},y^{(i)})=-y^{(i)}\log(a^{(i)})-(1-y^{(i)})\log(1-a^{(i)})} \tag{3} L(a(i),y(i))=−y(i)log(a(i))−(1−y(i))log(1−a(i))(3)
J = 1 m ∑ i = 1 m L ( a ( i ) , y ( i ) ) (4) \mathrm J=\dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m}\mathcal{L}(\mathrm a^{(\mathrm i)},\mathrm y^{(\mathrm i)}) \tag{4} J=m1i=1∑mL(a(i),y(i))(4)
你会发现公式1-3都是针对一个图像的操作,只有在公式1中出现了大于一维的参数,公式4是对整个数据集的操作,即使这样公式4中的参数也全部都是标量。在实际计算中我们不会使用
for
循环来逐一遍历数据集的每一个样本,而是借助numpy包同时对所有样本进行计算。所以我们需要在上面公式的基础上将对单个样本的计算变为对整个数据集的运算。还是原来的公式,换种写法,同时标记参数大小。
Z ( 1 , 209 ) = w T ( 1 , 12288 ) X ( 12288 , 209 ) + b ( 1 , 209 ) (1’) \mathrm{Z(1,209)=w^T(1,12288)X(12288,209) +b(1,209)} \tag{1'} Z(1,209)=wT(1,12288)X(12288,209)+b(1,209)(1’)Y ^ ( 1 , 209 ) = A ( 1 , 209 ) = s i g m o i d ( Z ( 1 , 209 ) ) (2’) \mathrm{\hat Y(1,209)=A(1,209)=sigmoid(Z(1,209))} \tag{2'} Y^(1,209)=A(1,209)=sigmoid(Z(1,209))(2’)
L ( A , Y ) ( 1 , 209 ) = − Y ( 1 , 209 ) log ( A ( 1 , 209 ) ) − ( 1 − Y ( 1 , 209 ) ) log ( 1 − A ( 1 , 209 ) ) (3’) \mathrm{\mathcal{L}(A,Y)(1,209 )=-Y(1,209)\log(A(1,209))-(1-Y(1,209))\log(1-A(1,209))} \tag{3'} L(A,Y)(1,209)=−Y(1,209)log(A(1,209))−(1−Y(1,209))log(1−A(1,209))(3’)
J = 1 m ∑ L ( A , Y ) ( 1 , 209 ) (4’) \mathrm J=\dfrac{1}{\mathrm m}\sum\mathcal{L}(A,Y)(1,209 ) \tag{4'} J=m1∑L(A,Y)(1,209)(4’)
这里需要特别注意公式3‘中涉及到的矩阵相乘都是元素级相乘,因为我们需要同时计算这209个图像样本的损失值。这里出现的 ∑ \sum ∑代表矩阵行或列相加。下面继续写一下 w \mathrm w w和 b \mathrm b b中涉及到的矩阵形状。
w ( 12288 , 1 ) : = w ( 12288 , 1 ) − α ⋅ 1 m X ( 12288 , 209 ) ⋅ ( A ( 1 , 209 ) − Y ( 1 , 209 ) ) T ( 209 , 1 ) (9’) \mathrm w(12288,1):=\mathrm w(12288,1)-\alpha \cdot \dfrac{1}{\mathrm m}\mathrm X(12288,209)\cdot(\mathrm A(1,209)-\mathrm Y(1,209))^T(209,1) \tag{9'} w(12288,1):=w(12288,1)−α⋅m1X(12288,209)⋅(A(1,209)−Y(1,209))T(209,1)(9’)b : = b − α ⋅ 1 m ∑ ( A ( 1 , 209 ) − Y ( 1 , 209 ) ) ( 209 , 1 ) (10’) \mathrm b:=\mathrm b-\alpha \cdot \dfrac{1}{\mathrm m}\sum\mathrm (\mathrm A(1,209)-\mathrm Y(1,209))(209,1) \tag{10'} b:=b−α⋅m1∑(A(1,209)−Y(1,209))(209,1)(10’)
注意公式9‘中的矩阵相乘不是元素级相乘,这里我们可以借助公式7( d L ( a , y ) d w = x ( a − y ) \frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w}=\mathrm x(\mathrm a-\mathrm y) dwdL(a,y)=x(a−y))推导一下。假设只有一个样本,那 d L ( a , y ) d w \frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w} dwdL(a,y)可以写为 d L ( a , y ) d w = [ x ( 0 ) ] ⋅ [ a ( 0 ) − y ( 0 ) ] \frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w}=[x^{(0)}] \cdot[\mathrm a^{(0)}-\mathrm y^{(0)}] dwdL(a,y)=[x(0)]⋅[a(0)−y(0)],当有两个样本时 d L ( a , y ) d w = [ x ( 0 ) , x ( 1 ) ] ⋅ [ a ( 0 ) − y ( 0 ) a ( 1 ) − y ( 1 ) ] \frac{d\mathcal{L}(\mathrm a,\mathrm y)}{d\mathrm w}=[x^{(0)},x^{(1)}] \cdot \begin{bmatrix} \mathrm a^{(0)}-\mathrm y^{(0)} \\\ \mathrm a^{(1)}-\mathrm y^{(1)} \end{bmatrix} dwdL(a,y)=[x(0),x(1)]⋅[a(0)−y(0) a(1)−y(1)],而公式中的 ( A − Y ) (A-Y) (A−Y)(1,209)的是一行,不是一列,所以需要转置操作。还有要清楚这里我们要求的 w \mathrm w w形状为(12288,1)。
我想到这你应该清楚每一步计算过程中的细节了,至少能说出公式中每个参数的意义及形状了,如果现在还是一头雾水,我希望你停一停,不要再继续往下看了,看下去只会更乱,回头多看几遍推导过程。
Ok,我们继续编码。定义一个函数用于实现前向传播和反向传播的计算,函数返回每一轮传播后的成本值和 w \mathrm w w和 b \mathrm b b的修正梯度。
题外话:这里还得啰嗦两句, w \mathrm w w的更新函数为 w : = w − α ⋅ 1 m ∑ i = 1 m x ( i ) ( a ( i ) − y ( i ) ) \mathrm w:=\mathrm w-\alpha \cdot \dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m} \mathrm x^{(\mathrm i)}(\mathrm a^{(\mathrm i)}-\mathrm y^{(\mathrm i)}) w:=w−α⋅m1∑i=1mx(i)(a(i)−y(i)),在程序中我们将 1 m ∑ i = 1 m x ( i ) ( a ( i ) − y ( i ) ) \dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m} \mathrm x^{(\mathrm i)}(\mathrm a^{(\mathrm i)}-\mathrm y^{(\mathrm i)}) m1∑i=1mx(i)(a(i)−y(i))记为
"dw"
,同理将 b \mathrm b b更新函数中的$\dfrac{1}{\mathrm m}\sum_{\mathrm i=1}^{\mathrm m} (\mathrm a^{(\mathrm i)}-\mathrm y^{(\mathrm i)}) 记为 ‘ " d b " ‘ ,函数变为 记为`"db"`,函数变为 记为‘"db"‘,函数变为\mathrm w:=\mathrm w-\alpha d\mathrm w ,并不是微分上的 ,并不是微分上的 ,并不是微分上的d\mathrm x$概念,注意区分。还有个关于矩阵运算的事,假设有两个矩阵a和b(numpy数组),不知道有没朋友在python编码中将矩阵相乘写为
a * b
,实际上这在python中是元素级相乘(既不是点乘也不是叉乘),是需要满足广播机制的。而矩阵点乘需要写为a @ b
,NumPy 3.5及更高版本中,可以使用a @ b
和np.dot(a, b)
作用相同。
def propagate(w, b, X, Y):
"""
实现前向和后向传播,返回成本函数及其梯度。
参数:
w - 权重,大小不等的数组(num_px * num_px * 3,1)=>(12288,1)
b - 偏差
X - 矩阵类型为(num_px * num_px * 3,训练数量)=>(12288,1)
Y - 正确样本标签(0、1组成),维度为(1,训练数据数量)=>(1,209)
返回:
cost- 逻辑回归的负对数似然成本,这里已经是取平均后的损失
dw - 相对于w的梯度,dw只是编码写法,不是微分中的"dw",与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
测试及打印结果:
w, b, X, Y = np.array([[1], [2]]), 2, np.array([[1,2], [3,4]]), np.array([[1, 0]])
grads, cost = propagate(w, b, X, Y)
print ("dw = " + str(grads["dw"]))
print ("db = " + str(grads["db"]))
print ("cost = " + str(cost))
"""
dw = [[ 0.99993216]
[ 1.99980262]]
db = 0.499935230625
cost = 6.00006477319
"""
现在我们需要一个循环来将我们前边的代码串起来,求循环后我们更新得到的 w \mathrm w w和 b \mathrm b b,这里为了绘制曲线还做了一个列表costs用于记录训练过程中的损失。
def optimize(w, b, X, Y, num_iterations, learning_rate, print_cost=False):
"""
此函数通过运行梯度下降算法来优化w和b,返回优化后的w,b
参数:
w - 权重,numpy数组(num_px * num_px * 3,1)=>(12288,209)
b - 偏差,标量
X - 维度为(num_px * num_px * 3,训练数据的数量)的numpy数组。
Y - 正确样本标签(0、1组成),维度为(1,训练数据数量)=>(1,209)
num_iterations - 优化循环的迭代次数
learning_rate - 学习率
print_cost - 控制每100步打印一次损失值
返回:
params - 包含权重w和偏差b的字典
grads - 包含权重和偏差相对于成本函数的梯度的字典
成本 - 优化期间计算的所有成本列表,将用于绘制学习曲线。
提示:
我们需要写下两个步骤并遍历它们:
1)计算当前参数的成本和梯度,使用propagate()。
2)使用w和b的梯度下降法则更新参数。
"""
costs = []
dw = ''
db = ''
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
}
return params, costs
测试及打印结果:
w, b, X, Y = np.array([[1], [2]]), 2, np.array([[1,2], [3,4]]), np.array([[1, 0]])
params , costs = optimize(w , b , X , Y , num_iterations=100 , learning_rate = 0.009 , print_cost = False)
print ("w = " + str(params["w"]))
print ("b = " + str(params["b"]))
"""
w = [[ 0.1124579 ]
[ 0.23106775]]
b = 1.55930492484
"""
通过optimize函数我们已经可以得到
w
\mathrm w
w和
b
\mathrm b
b,准备工作全部做完,现在可以通过公式2来直接对数据集X进行预测了。实现预测函数predict
,将
a
\mathrm a
a的值变为0(激活值<= 0.5)或者为1(激活值> 0.5)并将预测值存储在向量Y_prediction
中。
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, 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_prediction_train": Y_prediction_train,
"w": w,
"b": b,
"learning_rate": learning_rate,
"num_iterations": num_iterations}
return d_
测试一下模型:
print("====================测试model====================")
d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations=2000, learning_rate=0.005, print_cost=True)
打印结果:
====================测试model====================
迭代的次数: 0 , 误差值: 0.693147
迭代的次数: 100 , 误差值: 0.584508
迭代的次数: 200 , 误差值: 0.466949
迭代的次数: 300 , 误差值: 0.376007
迭代的次数: 400 , 误差值: 0.331463
迭代的次数: 500 , 误差值: 0.303273
迭代的次数: 600 , 误差值: 0.279880
迭代的次数: 700 , 误差值: 0.260042
迭代的次数: 800 , 误差值: 0.242941
迭代的次数: 900 , 误差值: 0.228004
迭代的次数: 1000 , 误差值: 0.214820
迭代的次数: 1100 , 误差值: 0.203078
迭代的次数: 1200 , 误差值: 0.192544
迭代的次数: 1300 , 误差值: 0.183033
迭代的次数: 1400 , 误差值: 0.174399
迭代的次数: 1500 , 误差值: 0.166521
迭代的次数: 1600 , 误差值: 0.159305
迭代的次数: 1700 , 误差值: 0.152667
迭代的次数: 1800 , 误差值: 0.146542
迭代的次数: 1900 , 误差值: 0.140872
训练集准确性: 99.04306220095694 %
测试集准确性: 70.0 %
为了直观的看出成本函数值下降的过程,我们将costs
绘制出来。
#绘制图
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()
效果图:
目前为止我们已经做完了大部分工作,剩下的就是优化我们的模型,也是调整超参数(学习率、迭代次数等)的过程。总结一下构建神经网络的主要步骤:
-
确定网络架构:
- 定义神经网络的层数和每一层的神经元数量。这包括输入层、隐藏层和输出层的规模。
- 选择激活函数,用于在每一层的神经元之间传递信息。
- 确定损失函数,用于度量模型的性能。
-
初始化参数:
- 对网络的权重和偏差进行初始化。通常使用随机初始化的方法,以打破对称性。
-
前向传播:
- 实现前向传播算法,将输入数据从输入层传递到输出层。
- 在每一层中进行线性组合和激活函数操作,计算每个神经元的输出。
- 通过整个网络计算损失函数的值。
-
计算损失:
- 根据模型的预测值和实际标签,计算损失函数的值。常见的损失函数包括均方误差(MSE)和交叉熵损失等,具体取决于问题类型。
-
反向传播:
- 实现反向传播算法,计算损失函数相对于网络参数(权重和偏差)的梯度。
- 使用梯度信息来更新网络参数,以减小损失函数的值。这是通过优化算法(如梯度下降)来完成的。
-
训练模型:
- 重复执行前向传播、损失计算、反向传播和参数更新的步骤,直到模型的性能达到满意的水平,或者达到预定的停止条件(例如,最大迭代次数)。
-
评估模型:
- 使用独立的验证集或测试集来评估训练后的模型的性能。
- 计算准确率、精确度、召回率、F1 分数等指标,以评估模型的表现。
-
调整超参数:
- 根据模型性能对超参数(如学习率、隐藏层的数量和神经元数量等)进行调整,以改善模型性能。
-
部署模型:
- 一旦满足要求,可以将训练好的模型部署到实际应用中进行预测。
-
持续监控和维护:
-
定期监控模型的性能,以确保它在生产环境中的稳定性和准确性。
-
可能需要定期重新训练模型,以适应新的数据和情境。
这些是构建神经网络的基本步骤,具体实施可能会根据问题类型和需求有所不同。深度学习领域有各种不同类型的神经网络和技术,因此在实际应用中可能会有更复杂的网络结构和训练技巧。
-
结语
写这篇文章是为了记录我在学习神经网络过程中遇到的一些问题,之前跟着视频做过几个简单的神经网络,当时也看了视频讲解,也确实把网络结构搭建了出来,似乎会了,但回头一想好像只学会了拼凑代码而已。也许在未来的学习中大家也不会去细推每一步计算,但我希望这篇文章可以为我和其他读者在需要进行推导计算时提供一点帮助,最后希望大家在探索深度学习的路上都能顺顺利利,一路生花。
附完整代码
import numpy as np
import matplotlib.pyplot as plt
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_x_orig.shape[0] # 训练集里图片的数量。
m_test = test_set_x_orig.shape[0] # 测试集里图片的数量。
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
# train_set_x_flatten = train_set_x_orig.reshape(-1, train_set_x_orig.shape[0])
# 将测试集的维度降低并转置。
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 - 维度为(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)=>(12288,1)
b - 偏差
X - 矩阵类型为(num_px * num_px * 3,训练数量)=>(12288,1)
Y - 正确样本标签(0、1组成),维度为(1,训练数据数量)=>(1,209)
返回:
cost- 逻辑回归的负对数似然成本,这里已经是取平均后的损失
dw - 相对于w的梯度,dw只是编码写法,不是微分中的"dw",与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,b
参数:
w - 权重,numpy数组(num_px * num_px * 3,1)=>(12288,209)
b - 偏差,标量
X - 维度为(num_px * num_px * 3,训练数据的数量)的numpy数组。
Y - 正确样本标签(0、1组成),维度为(1,训练数据数量)=>(1,209)
num_iterations - 优化循环的迭代次数
learning_rate - 学习率
print_cost - 控制每100步打印一次损失值
返回:
params - 包含权重w和偏差b的字典
grads - 包含权重和偏差相对于成本函数的梯度的字典
成本 - 优化期间计算的所有成本列表,将用于绘制学习曲线。
提示:
我们需要写下两个步骤并遍历它们:
1)计算当前参数的成本和梯度,使用propagate()。
2)使用w和b的梯度下降法则更新参数。
"""
costs = []
dw = ''
db = ''
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
}
return params, 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, 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_prediction_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()