线性二分类的神经网络实现
提出问题
回忆历史,公元前206年,楚汉相争,当时刘邦项羽麾下的城池地理位置如下:
0.红色圆点,项羽的城池
1.绿色叉子,刘邦的城池
其中,在边界处有一些红色和绿色重合的城池,表示双方激烈争夺的拉锯战。
样本序号 | 1 | 2 | 3 | … | 119 |
---|---|---|---|---|---|
经度相对值 | 0 | .025 | 4.109 | … | 7.767 |
纬度相对值 | 3 | .408 | 8.012 | … | 1.872 |
1=汉, 0=楚 | 1 | 1 | 0 | … | 1 |
问题:
经纬度相对值为(5,1)时,属于楚还是汉?
经纬度相对值为(6,9)时,属于楚还是汉?
经纬度相对值为(5,5)时,属于楚还是汉?
你可能会觉得这个太简单了,这不是有图吗?定位坐标值后一下子就找到对应的区域了。但是我们要求你用机器学习的方法来解决这个看似简单的问题,以便将来的预测行为是快速准确的,而不是拿个尺子在图上去比划。再说了,我们用这个例子,主要是想让大家对问题和解决方法都有一个视觉上的清晰认识,而这类可以可视化的问题,在实际生产环境中并不多见,绝大多数都是属于乐山大佛——一头雾水。
问题分析
从图示来看,在两个颜色区间之间似乎存在一条直线,即线性可分的。我们如何通过神经网络精确地找到这条分界线呢?
从视觉上判断是线性可分的,所以我们使用单层神经网络即可
输入特征是经度和纬度,所以我们在输入层设置两个输入X1=经度,X2=维度
最后输出的是两个分类,分别是楚汉地盘,可以看成非0即1的二分类问题,所以我们只用一个输出单元就可以了
定义神经网络结构
根据前一节学习的二分类原理,我们只需要一个二入一出的神经元就可以搞定。这个网络只有输入层和输出层,由于输入层不算在内,所以是一层网络。
这次我们第一次使用了分类函数,所以有个A的输出,而不是以往的Z的输出。
输入层
输入经度(x1)和纬度(x2)两个特征:
X = ( x 1 , 1 x 2 , 1 ) X=\begin{pmatrix} x_{1,1} \ x_{2,1} \end{pmatrix} X=(x1,1 x2,1)
权重矩阵W1/B1
输入层是2个特征,则W的尺寸就是1x2:
W = ( w 1 , 1 w 1 , 2 ) W=\begin{pmatrix} w_{1,1} & w_{1,2} \end{pmatrix} W=(w1,1w1,2)
B的尺寸是1x1,行数永远和W一样,列数永远是1。
B = ( b 1 , 1 ) B=\begin{pmatrix} b_{1,1} \end{pmatrix} B=(b1,1)
输出层
Z = W ⋅ X + B Z=W \cdot X+B Z=W⋅X+B A = S i g m o i d ( Z ) A = Sigmoid(Z) A=Sigmoid(Z)
损失函数
二分类交叉熵函损失数 Cross Entropy
J = − [ Y l n A + ( 1 − Y ) l n ( 1 − A ) ] J = -[YlnA+(1-Y)ln(1-A)] J=−[YlnA+(1−Y)ln(1−A)]
分类的方式是,可以指定当A > 0.5时是正例,A <= 0.5时就是反例。或者根据实际情况指定别的阈值比如0.3,0.8等等。
此时反向传播矩阵运算的公式推导结果是:
(交叉熵函数求导) ∂ J ∂ A = − [ Y A − 1 − Y 1 − A ] = A − Y A ( 1 − A ) \frac{\partial{J}}{\partial{A}}=-[\frac{Y}{A}-\frac{1-Y}{1-A}]=\frac{A-Y}{A(1-A)} \tag{交叉熵函数求导} ∂A∂J=−[AY−1−A1−Y]=A(1−A)A−Y(交叉熵函数求导) (Sigmoid激活函数求导) ∂ A ∂ Z = A ( 1 − A ) \frac{\partial{A}}{\partial{Z}}=A(1-A) \tag{Sigmoid激活函数求导} ∂Z∂A=A(1−A)(Sigmoid激活函数求导) (矩阵运算求导) ∂ Z ∂ W = X T , ∂ Z ∂ B = 1 \frac{\partial{Z}}{\partial{W}}=X^T , \frac{\partial{Z}}{\partial{B}}=1 \tag{矩阵运算求导} ∂W∂Z=XT,∂B∂Z=1(矩阵运算求导)
所以W的梯度:
∂ J ∂ W = ∂ J ∂ A ∂ A ∂ Z ∂ Z ∂ W \frac{\partial{J}}{\partial{W}} = \frac{\partial{J}}{\partial{A}} \frac{\partial{A}}{\partial{Z}} \frac{\partial{Z}}{\partial{W}} ∂W∂J=∂A∂J∂Z∂A∂W∂Z = A − Y A ( 1 − A ) ⋅ A ( 1 − A ) ⋅ X T =\frac{A-Y}{A(1-A)} \cdot A(1-A) \cdot X^T =A(1−A)A−Y⋅A(1−A)⋅XT = ( A − Y ) X T =(A-Y)X^T =(A−Y)XT
加粗样式B的梯度:
∂ J ∂ B = ∂ J ∂ A ∂ A ∂ Z ∂ Z ∂ B \frac{\partial{J}}{\partial{B}}=\frac{\partial{J}}{\partial{A}}\frac{\partial{A}}{\partial{Z}}\frac{\partial{Z}}{\partial{B}} ∂B∂J=∂A∂J∂Z∂A∂B∂Z = A − Y A ( 1 − A ) A ( 1 − A ) =\frac{A-Y}{A(1-A)}A(1-A) =A(1−A)A−YA(1−A) = A − Y =A-Y =A−Y
样本数据
下载后拷贝到您要运行的Python文件所在的文件夹。
样本特征值
X
m
X_m
Xm表示第m个样本值,
x
m
,
n
x_{m,n}
xm,n表示第m个样本的第n个特征值。样本数据集中一共有200个数据,每个数据有两个特征:经度和纬度。所以定义矩阵如下:
X
=
(
X
1
X
2
…
X
200
)
=
(
x
1
,
1
x
2
,
1
…
x
200
,
1
x
1
,
2
x
2
,
2
…
x
200
,
2
)
X = \begin{pmatrix} X_1 & X_2 \dots X_{200} \end{pmatrix}= \begin{pmatrix} x_{1,1} & x_{2,1} \dots x_{200,1} \ x_{1,2} & x_{2,2} \dots x_{200,2} \end{pmatrix}
X=(X1X2…X200)=(x1,1x2,1…x200,1 x1,2x2,2…x200,2)
=
(
0.025
4.109
7.767
…
2.762
3.408
8.012
1.872
…
2.653
)
=\begin{pmatrix} 0.025 & 4.109 & 7.767 & \dots & 2.762 \ 3.408 & 8.012 & 1.872 & \dots & 2.653 \ \end{pmatrix}
=(0.0254.1097.767…2.762 3.4088.0121.872…2.653 )
样本标签值
一般来说,在标记样本时,我们会用1,2,3这样的标记,来指明是哪一类。在本例的二分类情况下,我们只需要把正例标记为1,负例标记为0。这个需要检查原始样本数据的格式,在自己的code中做相应的转化。如果你认为刘邦是“好人”,你就把汉标记为正例。对于一般的疾病分类来说,我们习惯于把阳性(有疾病嫌疑)标记为正例。
Y = ( Y 1 Y 2 … Y m ) = ( 1 1 0 … 1 ) Y = \begin{pmatrix} Y_1 & Y_2 \dots Y_m \end{pmatrix}= \begin{pmatrix} 1 & 1 & 0 \dots 1 \end{pmatrix} Y=(Y1Y2…Ym)=(110…1)
代码实现
我们先无耻地从第5章的代码库ch05中,把一些已经写好的函数copy过来,形成一个BaseClassification.py文件,其中会包括神经网络训练的基本过程函数,加载数据,数据的归一化函数,结果显示函数等等。
加载数据
基本的加载数据和对样本数据的归一化工作,都可以用前一章的代码来完成,统一集成在BaseClassification.py中了,下面代码中的from BaseClassification import *就是完成了代码引入的工作。但是对于标签数据,需要一个特殊处理。
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
import math
from BaseClassification import *
x_data_name = "X2.dat"
y_data_name = "Y2.dat"
def ToBool(YData):
num_example = YData.shape[1]
Y = np.zeros((1, num_example))
for i in range(num_example):
if YData[0,i] == 1: # 第一类的标签设为0
Y[0,i] = 0
elif YData[0,i] == 2: # 第二类的标签设为1
Y[0,i] = 1
# end if
# end for
return Y
遍历标签数据YData中所有记录,设置类别为1的标签为负例0,设置类别为2的标签为正例1。下载的数据中,被标记为1和2,表示第1类和第2类,需要转换成0/1。
上述函数在本例中并没有用,因为样本本身就是0/1标记的。
前向计算
前向计算需要增加分类函数调用:
def Sigmoid(x):
s=1/(1+np.exp(-x))
return s
前向计算
def ForwardCalculationBatch(W, B, batch_X):
Z = np.dot(W, batch_X) + B
A = Sigmoid(Z)
return A
计算损失函数值
损失函数不再是均方差了,而是交叉熵函数对于二分类的形式。
def CheckLoss(W, B, X, Y):
m = X.shape[1]
A = ForwardCalculationBatch(W,B,X)
p1 = 1 - Y
p2 = np.log(1-A)
p3 = np.log(A)
p4 = np.multiply(p1 ,p2)
p5 = np.multiply(Y, p3)
LOSS = np.sum(-(p4 + p5)) #binary classification
loss = LOSS / m
return loss
推理函数
def Inference(W,B,X_norm,xt):
xt_normalized = NormalizePredicateData(xt, X_norm)
A = ForwardCalculationBatch(W,B,xt_normalized)
return A, xt_normalized
主程序
if __name__ == '__main__':
# SGD, MiniBatch, FullBatch
method = "SGD"
# read data
XData,YData = ReadData(x_data_name, y_data_name)
X, X_norm = NormalizeData(XData)
Y = ToBool(YData)
W, B = train(method, X, Y, ForwardCalculationBatch, CheckLoss)
print("W=",W)
print("B=",B)
xt = np.array([5,1,6,9,5,5]).reshape(2,3,order='F')
result, xt_norm = Inference(W,B,X_norm,xt)
print(result)
print(np.around(result))
运行结果
epoch=99, iteration=199, loss=0.093750
W= [[-18.18569771 6.49279869]]
B= [[7.77920305]]
result=
[[0.33483134 0.93729121 0.87242717]]
[[0. 1. 1.]]
打印出来的W,B的值对我们来说是几个很神秘的数字,下一节再解释。result值是返回结果,
经纬度相对值为(5,1)时,概率为0.33,属于楚
经纬度相对值为(6,9)时,概率为0.93,属于汉
经纬度相对值为(5,5)时,概率为0.87,属于汉
损失函数值记录
PS:
Sigmoid的输出值域是(0,1)。从前面讲过的二分类原理看,Sigmoid是假设所有正类的标签值都是,负类的标签值都是0。而Tanh要求的是-1和1,所以如果要用tanh做分类函数的话需要将标签归一化到[-1, 1]之间。
https://github.com/microsoft/ai-edu/blob/master/B-教学案例与实践/B6-神经网络基本原理简明教程/06.2-线性二分类实现.md