单层神经网络,又叫做感知机,它可以轻松实现逻辑与、或、非门。由于逻辑与、或门,需要有两个变量输入,而逻辑非门只有一个变量输入。但是它们共同的特点是输入为0或1,可以看作是正负两个类别。

所以,在学习了二分类知识后,我们可以用分类的思想来实现下列5个逻辑门:

  • 与门 AND
  • 与非门 NAND
  • 或门 OR
  • 或非门 NOR
  • 非门 NOT

以逻辑AND为例,图6-12中的4个点,分别是4个样本数据,蓝色圆点表示负例(y=0),红色三角表示正例(y=1)。

实现逻辑与或非门 - 图1

如果用分类思想的话,根据前面学到的知识,应该在红色点和蓝色点之间划出一条分割线来,可以正好把正例和负例完全分开。由于样本数据稀疏,所以这条分割线的角度和位置可以比较自由,比如图中的三条直线,都可以是这个问题的解。让我们一起看看神经网络能否给我们带来惊喜。

实现逻辑非门

很多阅读材料上会这样介绍:有公式 y=wx+b,令w=-1,b=1,则:

  • 当$$x=0$$时,$$y = -1 \times 0 + 1 = 1$$
  • 当$$x=1$$时,$$y = -1 \times 1 + 1 = 0$$

于是有如图6-13所示的神经元结构。

实现逻辑与或非门 - 图2

但是,这变成了一个拟合问题,而不是分类问题。比如,令x=0.5,带入公式中有:

y=wx+b = -1 \times 0.5 + 1 = 0.5

即,当x=0.5时,y=0.5,且其结果xy的值并没有丝毫“非”的意思。所以,应该定义如图6-14所示的神经元来解决问题,而其样本数据也很简单,如表6-6所示,一共只有两行数据。

实现逻辑与或非门 - 图3

表6-6 逻辑非问题的样本数据

样本序号 样本值x 标签值y
1 0 1
2 1 0

建立样本数据的代码如下:

  1. def Read_Logic_NOT_Data(self):
  2. X = np.array([0,1]).reshape(2,1)
  3. Y = np.array([1,0]).reshape(2,1)
  4. self.XTrain = self.XRaw = X
  5. self.YTrain = self.YRaw = Y
  6. self.num_train = self.XRaw.shape[0]

在主程序中,令:

  1. num_input = 1
  2. num_output = 1

执行训练过程,最终得到图6-16所示的分类结果和下面的打印输出结果。

  1. ......
  2. 2514 1 0.0020001369266925305
  3. 2515 1 0.0019993382569061806
  4. W= [[-12.46886021]]
  5. B= [[6.03109791]]
  6. [[0.99760291]
  7. [0.00159743]]

实现逻辑与或非门 - 图4

从图6-15中,可以理解神经网络在左右两类样本点之间画了一条直线,来分开两类样本,该直线的方程就是打印输出中的W和B值所代表的直线:

y = -12.468x + 6.031

但是,为什么不是一条垂直于x轴的直线呢,而是稍微有些“歪”?

这体现了神经网络的能力的局限性,它只是“模拟”出一个结果来,而不能精确地得到完美的数学公式。这个问题的精确的数学公式是一条垂直线,相当于w=\infty,这是不可能训练得出来的。

实现逻辑与或门

神经元模型

依然使用之前的神经元模型,如图6-16。

实现逻辑与或非门 - 图5

因为输入特征值只有两个,输出一个二分类,所以模型和前一节的一样。

训练样本

每个类型的逻辑门都只有4个训练样本,如表6-7所示。

表6-7 四种逻辑门的样本和标签数据

样本 x1 x2 逻辑与y 逻辑与非y 逻辑或y 逻辑或非y
1 0 0 0 1 0 1
2 0 1 0 1 1 0
3 1 0 0 1 1 0
4 1 1 1 0 1 0

读取数据

  1. class LogicDataReader(SimpleDataReader):
  2. def Read_Logic_AND_Data(self):
  3. X = np.array([0,0,0,1,1,0,1,1]).reshape(4,2)
  4. Y = np.array([0,0,0,1]).reshape(4,1)
  5. self.XTrain = self.XRaw = X
  6. self.YTrain = self.YRaw = Y
  7. self.num_train = self.XRaw.shape[0]
  8. def Read_Logic_NAND_Data(self):
  9. ......
  10. def Read_Logic_OR_Data(self):
  11. ......
  12. def Read_Logic_NOR_Data(self):
  13. ......

以逻辑AND为例,我们从SimpleDataReader派生出自己的类LogicDataReader,并加入特定的数据读取方法Read_Logic_AND_Data(),其它几个逻辑门的方法类似,在此只列出方法名称。

测试函数

  1. def Test(net, reader):
  2. X,Y = reader.GetWholeTrainSamples()
  3. A = net.inference(X)
  4. print(A)
  5. diff = np.abs(A-Y)
  6. result = np.where(diff < 1e-2, True, False)
  7. if result.sum() == 4:
  8. return True
  9. else:
  10. return False

我们知道了神经网络只能给出近似解,但是这个“近似”能到什么程度,是需要我们在训练时自己指定的。相应地,我们要有测试手段,比如当输入为(1,1)时,AND的结果是1,但是神经网络只能给出一个0.721的概率值,这是不满足精度要求的,必须让4个样本的误差都小于1e-2。

训练函数

  1. def train(reader, title):
  2. ...
  3. params = HyperParameters(eta=0.5, max_epoch=10000, batch_size=1, eps=2e-3, net_type=NetType.BinaryClassifier)
  4. num_input = 2
  5. num_output = 1
  6. net = NeuralNet(params, num_input, num_output)
  7. net.train(reader, checkpoint=1)
  8. # test
  9. print(Test(net, reader))
  10. ......

在超参中指定了最多10000次的epoch,0.5的学习率,停止条件为损失函数值低至2e-3时。在训练结束后,要先调用测试函数,需要返回True才能算满足要求,然后用图形显示分类结果。

运行结果

逻辑AND的运行结果的打印输出如下:

  1. ......
  2. epoch=4236
  3. 4236 3 0.0019998012999365928
  4. W= [[11.75750515]
  5. [11.75780362]]
  6. B= [[-17.80473354]]
  7. [[9.96700157e-01]
  8. [2.35953140e-03]
  9. [1.85140939e-08]
  10. [2.35882891e-03]]
  11. True

迭代了4236次,达到精度loss<1e-2。当输入(1,1)、(1,0)、(0,1)、(0,0)四种组合时,输出全都满足精度要求。

结果比较

把5组数据放入表6-8中做一个比较。

表6-8 五种逻辑门的结果比较

逻辑门 分类结果 参数值
实现逻辑与或非门 - 图6 W=-12.468 B=6.031
实现逻辑与或非门 - 图7 W1=11.757 W2=11.757 B=-17.804
与非 实现逻辑与或非门 - 图8 W1=-11.763 W2=-11.763 B=17.812
实现逻辑与或非门 - 图9 W1=11.743 W2=11.743 B=-11.738
或非 实现逻辑与或非门 - 图10 W1=-11.738 W2=-11.738 B=5.409

我们从数值和图形可以得到两个结论:

  1. W1和W2的值基本相同而且符号相同,说明分割线一定是135°斜率
  2. 精度越高,则分割线的起点和终点越接近四边的中点0.5的位置

以上两点说明神经网络还是很聪明的,它会尽可能优美而鲁棒地找出那条分割线。

代码位置

原代码位置:ch06, Level4

个人代码:LogicGates

思考与练习

  1. 减小max_epoch的数值,观察神经网络的训练结果。

将max_epoch调整为5000

实现逻辑与或非门 - 图11

  1. 2. 为什么达到相同的精度,逻辑ORNOR只用2000次左右的epoch,而逻辑ANDNAND却需要4000次以上?

因为逻辑OR和NOR的数据使得分类线在右上角就可以达到很好的效果,而逻辑AND和NAND在输入[0,0]时不满足要求,输入[0,1]和[1,0]时不满足要求,所以需要继续迭代,使得逻辑AND与NAND的训练次数更多。

下图分别是epoch=1000与epoch等于5000的情况

实现逻辑与或非门 - 图12

实现逻辑与或非门 - 图13

keras实现

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from keras.models import Sequential
  4. from keras.layers import Dense
  5. from HelperClass.NeuralNet_1_2 import *
  6. from HelperClass.Visualizer_1_0 import *
  7. class LogicDataReader(DataReader_1_1):
  8. def __init__(self):
  9. pass
  10. def Read_Logic_NOT_Data(self):
  11. X = np.array([0, 1]).reshape(2, 1)
  12. Y = np.array([1, 0]).reshape(2, 1)
  13. self.XTrain = self.XRaw = X
  14. self.YTrain = self.YRaw = Y
  15. self.num_train = self.XRaw.shape[0]
  16. def Read_Logic_AND_Data(self):
  17. X = np.array([0, 0, 0, 1, 1, 0, 1, 1]).reshape(4, 2)
  18. Y = np.array([0, 0, 0, 1]).reshape(4, 1)
  19. self.XTrain = self.XRaw = X
  20. self.YTrain = self.YRaw = Y
  21. self.num_train = self.XRaw.shape[0]
  22. def Read_Logic_NAND_Data(self):
  23. X = np.array([0, 0, 0, 1, 1, 0, 1, 1]).reshape(4, 2)
  24. Y = np.array([1, 1, 1, 0]).reshape(4, 1)
  25. self.XTrain = self.XRaw = X
  26. self.YTrain = self.YRaw = Y
  27. self.num_train = self.XRaw.shape[0]
  28. def Read_Logic_OR_Data(self):
  29. X = np.array([0, 0, 0, 1, 1, 0, 1, 1]).reshape(4, 2)
  30. Y = np.array([0, 1, 1, 1]).reshape(4, 1)
  31. self.XTrain = self.XRaw = X
  32. self.YTrain = self.YRaw = Y
  33. self.num_train = self.XRaw.shape[0]
  34. def Read_Logic_NOR_Data(self):
  35. X = np.array([0, 0, 0, 1, 1, 0, 1, 1]).reshape(4, 2)
  36. Y = np.array([1, 0, 0, 0]).reshape(4, 1)
  37. self.XTrain = self.XRaw = X
  38. self.YTrain = self.YRaw = Y
  39. self.num_train = self.XRaw.shape[0]
  40. def build_model():
  41. model = Sequential()
  42. model.add(Dense(1, activation='sigmoid', input_shape=(2,)))
  43. model.compile(optimizer='SGD', loss='binary_crossentropy')
  44. return model
  45. def draw_source_data(reader, title, show=False):
  46. fig = plt.figure(figsize=(5, 5))
  47. plt.grid()
  48. plt.axis([-0.1, 1.1, -0.1, 1.1])
  49. plt.title(title)
  50. X, Y = reader.GetWholeTrainSamples()
  51. if title == "Logic NOT operator":
  52. DrawTwoCategoryPoints(X[:, 0], np.zeros_like(X[:, 0]), Y[:, 0], title=title, show=show)
  53. else:
  54. DrawTwoCategoryPoints(X[:, 0], X[:, 1], Y[:, 0], title=title, show=show)
  55. def draw_split_line(w, b):
  56. x = np.array([-0.1, 1.1])
  57. old_w = w
  58. w = -w[0,0]/old_w[1,0]
  59. b = -b[0]/old_w[1,0]
  60. y = w * x + b
  61. plt.plot(x, y)
  62. if __name__ == '__main__':
  63. reader = LogicDataReader()
  64. reader.Read_Logic_AND_Data()
  65. x, y = reader.XTrain, reader.YTrain
  66. print("x", x)
  67. print("y", y)
  68. model = build_model()
  69. model.fit(x, y, epochs=1000, batch_size=1)
  70. # 获得权重
  71. w, b = model.layers[0].get_weights()
  72. print(w)
  73. print(b)
  74. draw_source_data(reader, "Logic AND operator")
  75. draw_split_line(w, b)
  76. plt.show()