具有单隐藏层的二类分类神经网络
一、目的
建立一个神经网络,它有一个隐藏层,用于对特定的数据进行二分类
二、训练集与测试集
百度网盘,提取码:1234
三、编程
本次编程所需的所有模块
import numpy as np
import matplotlib.pyplot as plt
import sklearn
import sklearn.datasets
import sklearn.linear_model
from testCases import *
from planar_utils import *
numpy:是用Python进行科学计算的基本软件包
sklearn:为数据挖掘和数据分析提供的简单高效的工具
matplotlib :是一个用于在Python中绘制图表的库
testCases:提供了一些测试示例来评估函数的正确性,参见下载的资料
planar_utils :提供了在这个任务中使用的各种有用的功能,参见下载的资料
3.1 导入原始数据集进行剖析
从planar_utils.py中load_planar_dataset()的加载数据集进来,使用X存放位置信息,Y存放颜色信息,红色:0 , 蓝色 :1
# 导入数据集
X, Y = load_planar_dataset()
我们看一看数据的维度
print(X.shape, Y.shape)
再看一下具体都存放了写什么,以第一个点为例
print(X[:, 0], Y[:, 0])
X为两行200列的矩阵,第一列存放x坐标,第二列存放y坐标,共有400个点;Y为1行400列的矩阵,存放每个点对应的颜色信息;我们使用matplotlib可视化数据集,看看我们的数据集是什么样的
# 绘制散点图
plt.scatter(X[0, :], X[1, :], c=np.squeeze(Y), s=40, cmap=plt.cm.Spectral)
plt.show()
3.2 查看简单的Logistic回归的分类效果
在构建完整的神经网络之前,先让我们看看逻辑回归在这个问题上的表现如何,我们可以使用sklearn的内置函数来做到这一点
# 先看看在逻辑回归上面的表现如何
clf = sklearn.linear_model.LogisticRegressionCV()
clf.fit(X.T, Y.T)
把逻辑回归分类器的分类绘制出来
# 将逻辑回归分类器的分类绘制出来
plot_decision_boundary(lambda x: clf.predict(x), X, Y) # 绘制决策边界
plt.title("Logistic Regression") # 图标题
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) +
"% " + "(正确标记的数据点所占的百分比)")
注1:plot_decision_boundary是planar_utils提供的绘制决策边界的函数
注2:这里这个准确性的计算,是先算预测到1的正确率,再加上预测0的准确率,最后处理总数据量
准确性只有47%的原因是数据集不是线性可分的,所以逻辑回归表现不佳
3.3 搭建神经网络
我们要搭建的神经网络模型如下:
当然还有我们的理论基础:
对于
x
(
i
)
x^{(i)}
x(i)而言:
z
[
1
]
(
i
)
=
W
[
1
]
(
i
)
x
(
i
)
+
b
[
1
]
(
i
)
z^{[1](i)}=W^{[1](i)}x^{(i)}+b^{[1](i)}
z[1](i)=W[1](i)x(i)+b[1](i)
a
[
1
]
(
i
)
=
σ
(
z
[
1
]
(
i
)
)
a^{[1](i)} = \sigma(z^{[1](i)})
a[1](i)=σ(z[1](i))
z
[
2
]
(
i
)
=
W
[
2
]
(
i
)
a
[
1
]
(
i
)
+
b
[
2
]
(
i
)
z^{[2](i)}=W^{[2](i)a^{[1](i)}}+b^{[2](i)}
z[2](i)=W[2](i)a[1](i)+b[2](i)
a
[
2
]
(
i
)
=
σ
(
z
[
2
]
(
i
)
a^{[2](i)}=\sigma(z^{[2](i)}
a[2](i)=σ(z[2](i)
成本函数J:
J
=
−
1
m
∑
i
=
0
m
[
y
(
i
)
∗
log
(
a
[
2
]
(
i
)
)
+
(
1
−
y
(
i
)
)
∗
log
(
1
−
a
[
2
]
(
i
)
)
]
J=-\frac{1}{m}\sum^{m}_{i=0}[y^{(i)}*\log(a^{[2](i)})+(1-y^{(i)})*\log(1-a^{[2](i)})]
J=−m1i=0∑m[y(i)∗log(a[2](i))+(1−y(i))∗log(1−a[2](i))]
构建神经网络的一般方法是:
- 定义神经网络结构(输入单元的数量,隐藏单元的数量等)
- 初始化模型的参数
- 循环:
3.1 实施前向传播
3.2 计算损失
3.3 实现向后传播
3.4 更新参数(梯度下降)
最后把它们合并到一个model函数中,当我们构建好了model并学习了正确的参数,我们就可以预测新的数据
3.4 定义神经网络结构
在构建之前,我们要先把神经网络的结构给定义好:
n_x: 输入层的数量
n_h: 隐藏层的数量(这里暂时设置为4)
n_y: 输出层的数量
def layer_sizes(X, Y):
"""
定义神经网络结构
:param X: 输入数据集,维度为(输入的数量,训练/测试的数量)
:param Y: 标签,维度为(输出的数量,训练/测试的数量)
:return: 返回n_x,n_h,n_y,分别是输入层数量,隐藏层数量,输出层数量
"""
n_x = X.shape[0]
n_h = 4
n_y = Y.shape[0]
return n_x, n_h, n_y
测试一下
# 测试layer_sizes
print("=========================测试layer_sizes=========================")
X_asses, Y_asses = layer_sizes_test_case()
(n_x, n_h, n_y) = layer_sizes(X_asses, Y_asses)
print("输入层的节点数量为: n_x = " + str(n_x))
print("隐藏层的节点数量为: n_h = " + str(n_h))
print("输出层的节点数量为: n_y = " + str(n_y))
3.5 初始化模型的参数
是用随机值(高斯分布)初始化权重W矩阵,将偏向量b置为0
def initialize_parameters(n_x, n_h, n_y):
"""
初始化模型的参数
:param n_x: 输出层数量
:param n_h: 隐藏层数量
:param n_y: 输出层数量
:return: 字典parameters:权重矩阵W1(n_h, n_x),W2(n_y, n_h);偏向量b1(n_h, 1),b2(n_y, 1);
"""
# 定义随机种子确保我们的值一样
np.random.seed(2)
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,
"W2": W2,
"b1": b1,
"b2": b2
}
return parameters
测试一下
# 测试initialize_parameters
print("=========================测试initialize_parameters=========================")
n_x, n_h, n_y = initialize_parameters_test_case()
parameters = initialize_parameters(n_x, n_h, n_y)
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))
3.6 循环
3.6.1 前向传播
使用字典类型的parameters(它是initialize_parameters() 的输出)检索每个参数
实现向前传播,计算
Z
1
,
A
1
,
Z
2
,
A
2
Z1,A1,Z2,A2
Z1,A1,Z2,A2(
A
2
A2
A2为训练集里面所有例子的预测向量)
反向传播所需的值存储在cache中,cache将作为反向传播函数的输入
def forward_propagation(X, parameters):
"""
前向传播
:param X: 维度为(n_x, m)的输入数据
:param parameters: 包含初始化W1,W2,b1,b2的字典,是initialize_parameters函数的输出
:return:A2:通过sigmoid函数计算的第二次激活的数值
cache:包含Z1,A1,Z2,A2的字典值,后面用于反向传播
"""
W1 = parameters["W1"]
W2 = parameters["W2"]
b1 = parameters["b1"]
b2 = parameters["b2"]
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
测试一下
# 测试forward_propagation
print("=========================测试forward_propagation=========================")
X_assess, parameters = forward_propagation_test_case()
A2, cache = forward_propagation(X_assess, parameters)
print(np.mean(cache["Z1"]), np.mean(cache["A1"]), np.mean(cache["Z2"]), np.mean(cache["A2"]))
现在我们已经计算了
A
2
,
a
[
2
]
(
i
)
A2,a^{[2](i)}
A2,a[2](i)包含了训练集里每个数值,现在我们就可以构建成本函数了
3.6.2 损失函数
计算成本的函数如下:
J
=
−
1
m
∑
i
=
0
m
[
y
(
i
)
∗
log
(
a
[
2
]
(
i
)
)
+
(
1
−
y
(
i
)
)
∗
log
(
1
−
a
[
2
]
(
i
)
)
]
J=-\frac{1}{m}\sum^{m}_{i=0}[y^{(i)}*\log(a^{[2](i)})+(1-y^{(i)})*\log(1-a^{[2](i)})]
J=−m1i=0∑m[y(i)∗log(a[2](i))+(1−y(i))∗log(1−a[2](i))]
def compute_cost(A2, Y):
"""
损失函数
:param A2: 通过sigmoid函数计算的第二次激活的数值
:param Y: 标签,维度(1, m)
:return: 交叉熵成本函数
"""
m = Y.shape[1]
cost = - 1 / m * np.sum(Y * np.log(A2) + (1 - Y) * np.log(1 - A2))
cost = float(np.squeeze(cost))
return cost
测试一下
# 测试compute_cost
print("=========================测试compute_cost=========================")
A2, Y_assess, parameters= compute_cost_test_case()
print("cost = " + str(compute_cost(A2, Y_assess)))
3.6.3 反向传播
计算dz2,dw2,db2,dz1,dw1,db1,使用以下公式计算:
其中
g
[
1
]
(
.
.
.
)
s
g^{[1]}(...)s
g[1](...)s是tanh函数:
g
[
1
]
(
z
)
=
a
=
t
a
n
h
(
z
)
=
e
z
−
e
−
z
e
z
+
e
−
z
g^{[1]}(z)=a=tanh(z)=\frac{e^z-e^{-z}}{e^z+e^{-z}}
g[1](z)=a=tanh(z)=ez+e−zez−e−z
故:
g
[
1
]
′
(
Z
[
1
]
)
=
1
−
a
2
g^{[1]'}(Z^{[1]})=1-a^2
g[1]′(Z[1])=1−a2
def backward_propagation(X, Y, cache, parameters):
"""
反向传播
:param X: 维度为(n_x, m)的输入数据
:param Y: 标签,维度(1, m)
:param cache: 包含Z1,A1,Z2,A2的字典值
:param parameters: 包含初始化W1,W2,b1,b2的字典值
:return: 字典grads包含W和b的导数
"""
m = Y.shape[1]
A1 = cache["A1"]
A2 = cache["A2"]
W2 = parameters["W2"]
dz2 = A2 - Y
dW2 = 1 / m * np.dot(dz2, A1.T)
db2 = 1 / m * np.sum(dz2, axis=1, keepdims=True)
dz1 = 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 = {
"dz2": dz2,
"dW2": dW2,
"db2": db2,
"dz1": dz1,
"dW1": dW1,
"db1": db1
}
return grads
测试一下
# 测试backward_propagation
print("=========================测试backward_propagation=========================")
parameters, cache, X_assess, Y_assess = backward_propagation_test_case()
grads = backward_propagation(X_assess, Y_assess, cache, parameters)
print("dW1 = " + str(grads["dW1"]))
print("db1 = " + str(grads["db1"]))
print("dW2 = " + str(grads["dW2"]))
print("db2 = " + str(grads["db2"]))
反向传播完成,,开始对参数进行更新
3.6.4 更新参数
我们需要使用(dW1, db1, dW2, db2)来更新(W1, b1, W2, b2)。
更新算法如下:
θ
=
θ
−
α
∂
J
∂
θ
\theta = \theta - \alpha \frac{\partial J }{ \partial \theta }
θ=θ−α∂θ∂J
其中
θ
\theta
θ为参数,
α
\alpha
α为学习率
def update_parameters(parameters, grads, alpha):
"""
更新参数
:param parameters: 包含初始化W1,W2,b1,b2的字典值
:param grads: 包含W和b的导数的字典值
:param alpha: 学习率
:return: 字典parameters包含更新后的W1,W2,b1,b2
"""
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 - alpha*dW1
b1 = b1 - alpha * db1
W2 = W2 - alpha*dW2
b2 = b2 - alpha*db2
parameters = {
"W1": W1,
"W2": W2,
"b1": b1,
"b2": b2
}
return parameters
测试一下:
# 测试update_parameters
print("=========================测试update_parameters=========================")
parameters, grads = update_parameters_test_case()
parameters = update_parameters(parameters, grads, alpha=1.2)
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))
3.7 模型整合
我们现在把上面的东西整合到model中,神经网络模型必须以正确的顺序使用先前的函数
def model(X, Y, n_h, n_iters, alpha, print_cost):
"""
模型整合
:param X: 维度为(n_x, m)的输入数据
:param Y: 标签,维度(1, m)
:param n_h: 隐藏层数量
:param n_iters: 迭代次数
:param alpha: 学习率
:param print_cost: 控制打印的布尔值
:return: 字典parameters包含更新后的最佳参数W和b,列表costs包含每一步的迭代成本数据,后面用于绘图
"""
costs = []
n_x, n_y = layer_sizes(X, Y)[0], 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(n_iters):
A2, cache = forward_propagation(X, parameters)
cost = compute_cost(A2, Y)
costs.append(cost)
grads = backward_propagation(X, Y, cache, parameters)
parameters = update_parameters(parameters, grads, alpha)
if i % 1000 == 0:
if print_cost:
print("第", i,"次循环,成本为", cost)
return parameters, costs
测试一下
# 测试model
print("=========================测试nn_model=========================")
X_assess, Y_assess = nn_model_test_case()
parameters, costs = model(X_assess, Y_assess, n_h=4, n_iters=10000, alpha=1.2, print_cost=False)
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))
注:这里的警告是因为sigmoid函数进行exp(-z)运算时,因为输入得z值太大(正值)或太小(负值),产生了内存溢出,最终得到的结果是nan,所以在cost函数中的log计算引发此警告,但是该警告不影响梯度的计算
3.8 预测
构建predict来使用模型进行预测, 使用向前传播来预测结果
A
2
=
{
<
0.5
,
y
=
0
⩾
0.5
,
y
=
1
A2=\left\{\begin{matrix} <0.5,y=0\\ \geqslant 0.5,y=1\\ \end{matrix}\right.
A2={<0.5,y=0⩾0.5,y=1
def predict(X, parameters):
"""
预测函数
:param X: 维度为(n_x, m)的输入数据
:param parameters: 包含更新后的最佳参数W和b的字典值
:return: 预测值
"""
A2, cache = forward_propagation(X, parameters)
predictions = np.round(A2)
return predictions
测试一下
# 测试predict
print("=========================测试predict=========================")
parameters, X_assess = predict_test_case()
predictions = predict(X_assess, parameters)
print("预测的平均值 = " + str(np.mean(predictions)))
3.9 运行与结果分析
parameters, costs = model(X, Y, n_h=4, n_iters=10000, alpha=1.2, print_cost=True)
# 绘制边界
plt.figure(1)
plot_decision_boundary(lambda x: predict(x.T, parameters), X, Y)
plt.title("Decision Boundary for hidden layer size " + str(4))
predictions = predict(X, parameters)
print('准确率: %d' % float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100) + '%')
plt.figure(2)
plt.plot(costs)
plt.xlabel("per thousand iters")
plt.ylabel("cost")
plt.legend()
plt.show()
结果:
在此模型中我们可以改变的因素有:
- 激活函数tanh
- 隐藏层神经元数量
- 迭代次数n_iters
- 学习率 α \alpha α
- 更换一个数据集
3.9.1 改变隐藏层神经元数量
n_h = [1, 2, 3, 4, 5, 10, 20, 50]
for n_h in n_h:
parameters, costs = model(X, Y, n_h=n_h, n_iters=10000, alpha=1.2, print_cost=False)
# 绘制边界
plt.figure(n_h)
plot_decision_boundary(lambda x: predict(x.T, parameters), X, Y)
plt.title("Decision Boundary for hidden layer size " + str(n_h))
predictions = predict(X, parameters)
print('隐藏层神经元为', n_h, '准确率: %d' % float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100) + '%')
plt.show()
测试一下
可以看到在隐藏层神经元数量为3-5之间,数据分类的效果不错,往后随着神经元的增多,数据过度拟合了
3.9.2 改变学习速率
此处可以参照我的另一篇文章:003_wz_wed_DL_课程一第二周编程题
3.9.3 更换数据集
在planner_utils中还提供了额外的4个数据集load_extra_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 = "noisy_circles"
X, Y = datasets[dataset]
X, Y = X.T, Y.reshape(1, Y.shape[0])
if dataset == "blobs":
Y = Y % 2
其余大家自行测试
3.10 源码
四、总结
整个神经网络架构如下:
我们需要注意的是
- 正确认识到原始数据集的维度、数据等信息
- 把控好每一层的数据维度
- 梯度下降法的正确应用
- 对结果的分析与思考