我们先看看如何用神经网络在两组不同标签的样本之间画一条明显的分界线。这条分界线可以是直线,也可以是曲线。这就是二分类问题。如果只画一条分界线的话,无论是直线还是曲线,我们可以用一支假想的笔(即一个神经元),就可以达到目的,也就是说笔的走向,完全依赖于这一个神经元根据输入信号的判断。

再看楚汉城池示意图,在两个颜色区域之间似乎存在一条分割的直线,即线性可分的。

  1. 从视觉上判断是线性可分的,所以我们使用单层神经网络即可;
  2. 输入特征是经度和纬度,所以我们在输入层设置两个输入X1=经度,X2=维度;
  3. 最后输出的是一个二分类,分别是楚汉地盘,可以看成非0即1的二分类问题,所以我们只用一个输出单元就可以了。

定义神经网络结构

根据前面的猜测,看来我们只需要一个二入一出的神经元就可以搞定。这个网络只有输入层和输出层,由于输入层不算在内,所以是一层网络,见图6-3。

用神经网络实现线性二分类 - 图1

与上一章的网络结构图的区别是,这次我们在神经元输出时使用了分类函数,所以有个A的输出,而不是以往的Z的直接输出。

输入层

输入经度x_1和纬度x_2两个特征:

$$
X=\begin{pmatrix} x{1} & x{2} \end{pmatrix}
$$

权重矩阵

输入是2个特征,输出一个数,则W的尺寸就是2x1:

$$
w=\left(\begin{array}{l}
w{1} \
w
{2}
\end{array}\right)
$$

B的尺寸是1x1,行数永远是1,列数永远和W一样。

$$
B=\begin{pmatrix} b_{1} \end{pmatrix}
$$

输出层

$$
\begin{array}{l}
z=x \cdot w+b=\left(\begin{array}{ll}
x{1} & x{2}
\end{array}\right)\left(\begin{array}{l}
w{1} \
w
{2}
\end{array}\right) \
\quad=x{1} \cdot w{1}+x{2} \cdot w{2}+b
\end{array} \tag{1}
$$

a = Logistic(z) \tag{2}

损失函数

二分类交叉熵损失函数:

loss(w,b) = -[yln a+(1-y)ln(1-a)] \tag{3}

反向传播

我们在上一节已经推导了loss对z的偏导数,结论为A-Y。接下来,我们求loss对w的导数。本例中,w的形式是一个2行1列的向量,所以求w的偏导时,要对向量求导:

$$
\begin{array}{c}
\frac{\partial \operatorname{loss}}{\partial w}=\left(\begin{array}{c}
\partial \operatorname{loss} / \partial w{1} \
\partial \operatorname{loss} / \partial w
{2}
\end{array}\right) \
=\left(\begin{array}{c}
\frac{\partial \operatorname{loss}}{\partial z} \frac{\partial z}{\partial w{1}} \
\frac{\partial \operatorname{loss}}{\partial z} \frac{\partial z}{\partial w
{2}}
\end{array}\right)=\left(\begin{array}{c}
(a-y) x{1} \
(a-y) x
{2}
\end{array}\right) \
=\left(x{1} x{2}\right)^{T}(a-y)
\end{array} \tag{4}
$$

上式中x_1,x_2是一个样本的两个特征值。如果是多样本的话,公式4将会变成其矩阵形式,以3个样本为例:

$$
\begin{array}{c}
\frac{\partial J(w, b)}{\partial w}=\left(\begin{array}{cc}
x{11} & x{12} \
x{21} & x{22} \
x{31} & x{32}
\end{array}\right)^{T}\left(\begin{array}{c}
a{1}-y{1} \
a{2}-y{2} \
a{3}-y{3}
\end{array}\right) \
=X^{T}(A-Y)
\end{array} \tag{5}
$$

代码实现

由于以前我们的神经网络只会做线性回归,现在多了一个做分类的技能,所以我们加一个枚举类型,可以让调用者通过指定参数来控制神经网络的功能。

  1. class NetType(Enum):
  2. Fitting = 1,
  3. BinaryClassifier = 2,
  4. MultipleClassifier = 3,

然后在超参类里把这个新参数加在初始化函数里:

  1. class HyperParameters(object):
  2. def __init__(self, eta=0.1, max_epoch=1000, batch_size=5, eps=0.1, net_type=NetType.Fitting):
  3. self.eta = eta
  4. self.max_epoch = max_epoch
  5. self.batch_size = batch_size
  6. self.eps = eps
  7. self.net_type = net_type

再增加一个Logistic分类函数:

  1. class Logistic(object):
  2. def forward(self, z):
  3. a = 1.0 / (1.0 + np.exp(-z))
  4. return a

以前只有均方差函数,现在我们增加了交叉熵函数,所以新建一个类便于管理:

  1. class LossFunction(object):
  2. def __init__(self, net_type):
  3. self.net_type = net_type
  4. # end def
  5. def MSE(self, A, Y, count):
  6. ...
  7. # for binary classifier
  8. def CE2(self, A, Y, count):
  9. ...

上面的类是通过初始化时的网络类型来决定何时调用均方差函数(MSE),何时调用交叉熵函数(CE2)的。

下面修改一下NeuralNet类的前向计算函数,通过判断当前的网络类型,来决定是否要在线性变换后再调用sigmoid分类函数:

  1. class NeuralNet(object):
  2. def __init__(self, params, input_size, output_size):
  3. self.params = params
  4. self.W = np.zeros((input_size, output_size))
  5. self.B = np.zeros((1, output_size))
  6. def __forwardBatch(self, batch_x):
  7. Z = np.dot(batch_x, self.W) + self.B
  8. if self.params.net_type == NetType.BinaryClassifier:
  9. A = Sigmoid().forward(Z)
  10. return A
  11. else:
  12. return Z

最后是主过程:

  1. if __name__ == '__main__':
  2. ......
  3. params = HyperParameters(eta=0.1, max_epoch=100, batch_size=10, eps=1e-3, net_type=NetType.BinaryClassifier)
  4. ......

与以往不同的是,我们设定了超参中的网络类型是BinaryClassifier。

运行结果

图6-4所示的损失函数值记录很平稳地下降,说明网络收敛了。

用神经网络实现线性二分类 - 图2

最后几行的打印输出:

  1. ......
  2. 99 19 0.20742586902509108
  3. W= [[-7.66469954]
  4. [ 3.15772116]]
  5. B= [[2.19442993]]
  6. A= [[0.65791301]
  7. [0.30556477]
  8. [0.53019727]]

打印出来的W,B的值对我们来说是几个很神秘的数字,下一节再解释。A值是返回的预测结果:

  1. 经纬度相对值为(0.58,0.92)时,概率为0.65,属于汉;
  2. 经纬度相对值为(0.62,0.55)时,概率为0.30,属于楚;
  3. 经纬度相对值为(0.39,0.29)时,概率为0.53,属于汉。

分类的方式是,可以指定当A > 0.5时是正例,A <= 0.5时就是反例。有时候正例反例的比例不一样或者有特殊要求时,也可以用不是0.5的数来当阈值。

代码实现

原代码位置:ch6, Level2

个人代码:BinaryClassification**

keras实现

  1. from keras.layers import Dense
  2. from keras.models import Sequential
  3. from keras.callbacks import EarlyStopping
  4. from sklearn.preprocessing import StandardScaler
  5. from HelperClass.DataReader_1_1 import *
  6. import matplotlib.pyplot as plt
  7. def get_data():
  8. sdr = DataReader_1_1("../data/ch06.npz")
  9. sdr.ReadData()
  10. X, Y = sdr.GetWholeTrainSamples()
  11. ss = StandardScaler()
  12. X = ss.fit_transform(X)
  13. # Y = ss.fit_transform(Y)
  14. return X, Y
  15. def build_model():
  16. model = Sequential()
  17. model.add(Dense(1, activation='sigmoid', input_shape=(2, )))
  18. model.compile(optimizer='SGD', loss='binary_crossentropy')
  19. return model
  20. def plt_loss(history):
  21. loss = history.history['loss']
  22. epochs = range(1, len(loss) + 1)
  23. plt.plot(epochs, loss, 'b', label='Training loss')
  24. plt.title('Training loss')
  25. plt.xlabel('Epochs')
  26. plt.ylabel('Loss')
  27. plt.legend()
  28. plt.show()
  29. if __name__ == '__main__':
  30. X, Y = get_data()
  31. x = np.array(X)
  32. y = np.array(Y)
  33. print(x.shape)
  34. print(y.shape)
  35. print(x)
  36. # print(y)
  37. model = build_model()
  38. early_stopping = EarlyStopping(monitor='loss', patience=100)
  39. history = model.fit(x, y, epochs=500, batch_size=10, callbacks=[early_stopping])
  40. w, b = model.layers[0].get_weights()
  41. print(w)
  42. print(b)
  43. plt_loss(history)
  44. # inference
  45. x_predicate = np.array([0.58, 0.92, 0.62, 0.55, 0.39, 0.29]).reshape(3, 2)
  46. a = model.predict(x_predicate)
  47. print("A=", a)

输出结果

  1. W=[[-4.683383 ]
  2. [ 1.9734153]]
  3. B=[0.22875357]
  4. A= [[0.3380343 ]
  5. [0.16944502]
  6. [0.26396227]]

用神经网络实现线性二分类 - 图3