前言

本文教程均来自b站【小白也能听懂的人工智能原理】,感兴趣的可自行到b站观看。

代码及工具箱

本专栏的代码和工具函数已经上传到GitHub:1571859588/xiaobai_AI: 零基础入门人工智能 (github.com),可以找到对应课程的代码

正文

面对豆豆毒性的新变化,我们需要一种新的预测模型来适应这种非单调的变化。传统的线性模型或简单的激活函数模型可能无法捕捉这种复杂的模式,因为它们通常只能表达单调增加或减少的关系。

为了使预测模型能够产生类似山丘的曲线,我们需要引入非线性激活函数,这些函数可以模拟复杂的、非单调的数据关系。

通过这些方法,我们可以构建一个能够适应豆豆新毒性模式的预测模型,使其能够准确地预测出不同大小豆豆的毒性,无论是在有毒还是无毒的范围内。这种模型将能够更好地模拟自然界的复杂性和不可预测性。


是时候将单个神经元扩展成网络,以处理更复杂的任务了。通过添加更多的神经元并让它们相互连接,我们可以构建一个能够学习和模拟复杂函数的神经网络。

当我们在网络中增加了两个额外的神经元,并将输入数据分别送入这两个神经元,我们实质上创建了一个小型的隐藏层。这两个神经元会对输入数据进行初步的处理和特征提取,然后它们各自的输出被送入到第三个神经元,这个神经元通常称为输出层神经元。

第三个神经元结合前两个神经元的输出,通过某种方式(这取决于它的激活函数和权重配置)综合这些信息,最终产生预测结果。通过这种方式,神经网络能够学习输入数据与输出之间的非线性关系。

我们可以使用各种可视化工具来观察和调整这些神经元的参数(例如权重和偏置),直到我们找到合适的参数配置,使得网络能够正确分类输入数据。这个过程涉及到监督学习,其中我们提供给网络带有标签的训练数据,并允许网络通过梯度下降等优化算法来调整其参数。

为什么这样做有效呢?原因在于神经网络能够通过其结构化的学习过程捕捉到数据中的复杂模式和非线性特征。每个神经元可以学习数据的不同方面,而网络的层级结构允许更高层次的特征提取和决策制定。通过这种方式,即使是非常复杂的决策边界也可以被有效地近似和学习。

总之,将神经元组织成网络是处理复杂问题的关键步骤,它使得机器学习模型能够模拟和学习现实世界数据的多样性和复杂性。通过不断的迭代和优化,神经网络可以逐渐提高其预测的准确性,最终实现成功的分类。

构建神经网络的步骤

构建神经网络来解决复杂问题的过程可以简化为以下几个步骤:

  1. 初步计算:每个神经元首先接收输入,然后通过一个线性函数(涉及权重和偏置)进行计算。

  2. 激活函数处理:线性函数的结果经过激活函数处理,得到神经元的最终输出。激活函数引入非线性,使得网络能够学习和模拟更加复杂的函数映射。

  3. 梯度下降优化:通过梯度下降算法,不断调整每个神经元的权重和偏置,使得输出结果逐渐逼近期望的目标。

  4. 多神经元协作:当网络中有两个或以上的神经元时,每个神经元可以学习输入数据的不同方面或特征。这些神经元的输出随后被组合起来,形成更复杂的特征表示。

  5. 最终输出调整:多个神经元的输出被送入最后一个神经元(或另一个神经元层),通过进一步的线性计算和激活函数处理,形成最终的预测结果。

  6. 复杂曲线拟合:通过这种方式,神经网络可以形成具有起伏的、非单调的决策边界,这样的曲线能够适应更复杂的数据分布。

  7. 解决复杂问题:通过调节和组合不同神经元的输出,神经网络能够形成一个具有多样性和复杂性的函数,这使得它能够解决更加复杂的分类或回归问题。

隐藏层的作用

当然这里由于每个神经元都有一个偏执向b也就是线性函数的截距,一般认为这是大家默认的共识,所以为了在画网络结构图的时候不那么的拖沓,大家一般都会直接省略这些b让我们的网络结构图更加的精简。

这就是数学上的解释很无趣,对吧?

当然这里还有一个解释,虽然严谨性并不是那么好,但似乎更加有序,我们添加一个神经元后,相当于增加了一个抽象的维度,把输入放进这些不同的维度中,每个维度通过不断的调整权重并进行激活,从而产生对输入的不同理解。最后再把这些抽象维度中的输出进行合并,降为得到输出。这个输入数据由于在多个抽象维度中被产生了不同的解读,从而让输出得到了更多的可能。

同样当环境中的豆豆的毒性发生更多可能的时候,我们同样可以采用类似的方法,通过增加对输入的更多抽象纬度产生更多的解读,而实现更加复杂的分的效果,而中间这些新添加的神经元节点也称之为隐藏层。可以看出来正是隐藏层的存在才让神经网络能够在复杂的情况下仍旧working,显而易见的是隐藏层的神经元数量越多,就可以产生越复杂的组合,解决越复杂的问题,当然计算量也随之越来越大,我们已经横向的在神经网络上增加了神经员,形成了一层隐藏层,而在之后的课程中,我们会纵向的不断添加神经元,产生更深的隐藏层输入,通过这些隐藏层被一层又一层的不断抽象和理解,提取出微妙的特征,从而让神经网络变得更加强大和智能。


而这些隐藏层也就是神经网络为什么working的本质,当我们建立一个复杂程度恰当的神经网络,经过充分的训练之后,网络中的各个神经元的参数被调节成为不同的值,这些功能单一的神经元连接组合出来的整体就可以近似出一个相当复杂的函数,可能是这样这样或者是这样,最终变成什么样,则是根据我们采集的训练数据决定,如果训练数据只有这样的两个,那么最终的预测模型可能是这样,如果是这样的数据集最后可能会是这样

如果是更多的这样的数据集则结果可能会是这样


而我们采集的训练数据越充足,那么最后训练得到的模型也就能够越好的去预测新的问题,因为越充足的训练数据就在越大程度上蕴藏了问题的规律和特征,新的问题数据也就越难以逃脱这些规律的约束。

所以我们总是说机器学习神经网络的根基是海量的,一个训练之后拟合适当的模型,进而在遇到新的问题数据的时候,也能大概率产生正确的预测。我们把这个现象称之为模型的泛化,模型的泛化能力,也就是神经网络追求的核心问题。​

深度学习与炼丹

我们经常听到深度学习,其中深度二字其实并没有什么特别的奥义,只是指一个神经网络中纵向的隐藏层比较多,换句话说很深,我们一般把隐藏层超过三层的网络也就称之为深度神经网络,这也是深度学习中广受诟病的地方,隐藏层的神经元在理解什么提取什么都太过微妙,虽然我们对大致的结果有所把握,但却很难用精确的数学去进行描述,我们能做的也只有设计一个网络送入数据,然后充分的训练,如果得到的预测效果好,我们就会说它起作用,如果不好,那也只能说搞错了,调校参数在再来一遍。


所以很多人戏称深度学习是炼丹,确实道士把原材料放入八卦炉开火炼丹,最后得到的仙丹可能让人长生不老,也可能让人一命呜呼。炼丹过程中八卦炉里发生的微妙事情,道士也是不得而知的,虽然是他设计了炼丹的一切。

编程实验

好的,同学们,我们开始做本节课的编程实验,那提前说一下这节课的编码呢可能会有一点繁琐,但并不是说多么难,大家要分清楚繁琐和难是两回事儿,不过不必慌张,我们沉下心来慢慢的梳理。​​

初始化数据集

## Create a dataset
n = 100
xs, ys = dataset.get_beans4(n)

初始化参数,查看效果

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def init_network():

    # 第一层(隐藏层)
    ## 第一个神经元参数
    w11_1 = np.random.rand()
    b1_1 = np.random.rand()
    ## 第二个神经元参数
    w12_1 = np.random.rand()
    b2_1 = np.random.rand()

    # 第二层(输出层)
    # 只有一个输出神经元
    w11_2 = np.random.rand()
    w21_2 = np.random.rand()
    b1_2 = np.random.rand()
    return w11_1,b1_1,w12_1,b2_1,w11_2,w21_2,b1_2

# 前向传播
def foward_progation(xs):
    z1_1 = w11_1 * xs + b1_1
    a1_1 = sigmoid(z1_1)

    z2_1 = w12_1 * xs + b2_1
    a2_1 = sigmoid(z2_1)

    z1_2 = w11_2 * a1_1 + w21_2*a2_1+b1_2
    a1_2 =  sigmoid(z1_2)
    return a1_2,z1_2,a2_1,z2_1,a1_1,z1_1


w11_1,b1_1,w12_1,b2_1,w11_2,w21_2,b1_2 = init_network()
a1_2, z1_2, a2_1, z2_1, a1_1, z1_1 = foward_progation(xs)

plt.title('Size-Toxicity Function', fontsize=12)
plt.xlabel('Bean Size')
plt.ylabel('Toxicity')
plt.scatter(xs, ys)
plt.plot(xs,a1_2)

训练:梯度下降、反向传播

w11_1, b1_1, w12_1, b2_1, w11_2, w21_2, b1_2 = init_network()
alpha = 0.03

for _ in range(5000):
    for i in range(n):
        x = xs[i]
        y = ys[i]
        # 先来一次前向传播
        a1_2, z1_2, a2_1, z2_1, a1_1, z1_1 = foward_progation(x)

        # 反向传播
        e = (y-a1_2)**2
        deda1_2 = -2*(y-a1_2)
        da1_2dz1_2 = a1_2*(1-a1_2)
        dz1_2dw11_2 = a1_1
        dz1_2dw21_2 = a2_1
        dz1_2db1_2 = 1

        dedw11_2 = deda1_2*da1_2dz1_2*dz1_2dw11_2
        dedw21_2 = deda1_2*da1_2dz1_2*dz1_2dw21_2
        dedb1_2 = deda1_2*da1_2dz1_2*dz1_2db1_2

        dz1_2da1_1 = w11_2
        da1_1dz1_1 = a1_1 * (1 - a1_1)
        dz1_1dw11_1 = x
        dz1_1db1_1 = 1
        dedw11_1 = deda1_2*da1_2dz1_2*dz1_2da1_1*da1_1dz1_1*dz1_1dw11_1
        dedb1_1 = deda1_2 * da1_2dz1_2 * dz1_2da1_1 * da1_1dz1_1 * dz1_1db1_1

        dz1_2d2_1 = w21_2
        da2_1dz2_1 = a2_1 * (1 - a2_1)
        dz2_1dw12_1 = x
        dz2_1db2_1 = 1
        dedw12_1 = deda1_2*da1_2dz1_2*dz1_2da1_1*da2_1dz2_1*dz2_1dw12_1
        dedb2_1 = deda1_2 * da1_2dz1_2 * dz1_2da1_1 * da2_1dz2_1 * dz2_1db2_1

        w11_1 = w11_1 - alpha * dedw11_1
        w12_1 = w12_1 - alpha * dedw12_1
        b1_1 = b1_1 - alpha * dedb1_1
        b2_1 = b2_1 - alpha * dedb2_1
        w11_2 = w11_2 - alpha * dedw11_2
        w21_2 = w21_2 - alpha * dedw21_2
        b1_2 = b1_2 - alpha * dedb1_2