跳转至

06.5 实现逻辑与或非门

6.5 实现逻辑与或非门⚓︎

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

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

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

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

图6-12 可以解决逻辑与问题的多条分割线

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

6.5.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所示的神经元结构。

图6-13 不正确的逻辑非门的神经元实现

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

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

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

图6-14 正确的逻辑非门的神经元实现

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

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

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

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

在主程序中,令:

num_input = 1
num_output = 1
执行训练过程,最终得到图6-16所示的分类结果和下面的打印输出结果。
......
2514 1 0.0020001369266925305
2515 1 0.0019993382569061806
W= [[-12.46886021]]
B= [[6.03109791]]
[[0.99760291]
 [0.00159743]]

图6-15 逻辑非门的分类结果

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

y = -12.468x + 6.031

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

6.5.2 实现逻辑与或门⚓︎

神经元模型⚓︎

依然使用第6.2节中的神经元模型,如图6-16。

图6-16 逻辑与或门的神经元实现

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

训练样本⚓︎

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

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

样本 x_1 x_2 逻辑与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

读取数据⚓︎

class LogicDataReader(SimpleDataReader):
    def Read_Logic_AND_Data(self):
        X = np.array([0,0,0,1,1,0,1,1]).reshape(4,2)
        Y = np.array([0,0,0,1]).reshape(4,1)
        self.XTrain = self.XRaw = X
        self.YTrain = self.YRaw = Y
        self.num_train = self.XRaw.shape[0]

    def Read_Logic_NAND_Data(self):
        ......

    def Read_Logic_OR_Data(self):
        ......

    def Read_Logic_NOR_Data(self):        
        ......

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

测试函数⚓︎

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

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

训练函数⚓︎

def train(reader, title):
    ...
    params = HyperParameters(eta=0.5, max_epoch=10000, batch_size=1, eps=2e-3, net_type=NetType.BinaryClassifier)
    num_input = 2
    num_output = 1
    net = NeuralNet(params, num_input, num_output)
    net.train(reader, checkpoint=1)
    # test
    print(Test(net, reader))
    ......
在超参中指定了最多10000次的epoch,0.5的学习率,停止条件为损失函数值低至2e-3时。在训练结束后,要先调用测试函数,需要返回True才能算满足要求,然后用图形显示分类结果。

运行结果⚓︎

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

......
epoch=4236
4236 3 0.0019998012999365928
W= [[11.75750515]
 [11.75780362]]
B= [[-17.80473354]]
[[9.96700157e-01]
 [2.35953140e-03]
 [1.85140939e-08]
 [2.35882891e-03]]
True
迭代了4236次,达到精度loss<1e-2。当输入(1,1)、(1,0)、(0,1)、(0,0)四种组合时,输出全都满足精度要求。

6.5.3 结果比较⚓︎

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

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

逻辑门 分类结果 参数值
W=-12.468
B=6.031
W1=11.757
W2=11.757
B=-17.804
与非 W1=-11.763
W2=-11.763
B=17.812
W1=11.743
W2=11.743
B=-11.738
或非 W1=-11.738
W2=-11.738
B=5.409

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

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

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

代码位置⚓︎

ch06, Level4

思考与练习⚓︎

  1. 减小max_epoch的数值,观察神经网络的训练结果。
  2. 为什么达到相同的精度,逻辑OR和NOR只用2000次左右的epoch,而逻辑AND和NAND却需要4000次以上?