人工智能原理基础——仅用中学数学知识就能看懂(上篇)

源码文件
链接: https://pan.baidu.com/s/1j4ny5voep7tUpz7u2gSl_Q?pwd=2hmg 提取码: 2hmg
系列合集
人工智能原理基础——仅用中学数学知识就能看懂(上篇)

  • 上篇:代价函数、梯度下降、前向/反向传播、激活函数、keras、深度学习入门

人工智能原理基础——仅用中学数学知识就能看懂(中篇)

  • 中篇:卷积神经网络及实战

人工智能原理基础——仅用中学数学知识就能看懂(下篇)

  • 下篇:循环神经网络及实战

一、一元一次函数感知器

原理

1943年神经学家麦卡洛克和数学家皮兹在他们合作的论文中提出一种神经元模型McCulloch-Pitts模型,使用一次函数来对生物神经元做模仿

1958年,心理学家Frank Rosenblatt在上述模型基础上提出Rosenblatt感知器模型,使得神经元有了自主调整参数的能力

img

二、方差代价函数

原理

1、预测问题的函数是y=wx,x是自变量,y是因变量

2、代价函数的自变量是w,因变量是误差代价e,这是用来分析并且改进预测函数的辅助函数

image-20250225155020034

我们需要让机器找到e最小的时候对应的w,那么如何让机器自动从不合适的w值自动向最低点移动,只需要初中的公式即可,求抛物线的对称轴,进而求出最低点

image-20250225155306230

3、正规方程:这种一次性求接触让误差最小的w的方法

image-20250225155430211

w_min = np.sum(xs*ys)/np.sum(xs*xs)
print(w_min)

4、numpy中array的两个数组做运算,直接就是把数组对应位置做运算

python中array数组两个数组相加,是把第二个数组拼接到第一个数组后面

import numpy

a = numpy.array([1,2,3,4])
b = numpy.array([5,6,7,8])
a+b # array([ 6,  8, 10, 12])
print(a+b) # [ 6  8 10 12]

c = [1,2,3,4]
d = [5,6,7,8]
print(c+d) # [1, 2, 3, 4, 5, 6, 7, 8]

5、python中

ws = np.arange(0, 3, 0.1) #生成从0到3步长为0.1不包括3的数组
ys = [1.2*x+np.random.rand()/10 for x in xs] #列表推导式,“1.2*x+np.random.rand()/10”是对x做的变化,for x in xs对于x,x是xs中的值

三、梯度下降和反向传播(上)

原理

1、寻找代价函数最低点的过程中,点的运动方向是通过斜率来表征的,通过斜率是否大于0来调整w的行为,那么到底每次调整多少合适呢,如果每次调整的x值固定,那么势必会过快或者过于慢,所以需要找到一种方法使其可以在距离最低点比较远的地方调整比较快,在近的地方调整比较慢,那么这一目的的实现方法就是梯度下降(根据曲线不同处”斜率“去调整w的方式称之为梯度下降),我们可以让w-点的斜率,可以做到斜率为正的时候,让w变小,斜率为负数的时候,斜率增大,这样就会接近最低点;同时,我们可以给式子乘一个学习率调和一下

image-20250225175958718

所以为什么是梯度下降而不说斜率下降呢,因为这是一种二维的情况,如果是三维的情况,斜率就不合适了

image-20250225193459383

批量梯度下降(标准梯度下降),指的是直接用全部样本(合成代价函数)进行梯度下降

随机梯度下降,指的是每次取一个样本进行梯度下降,因为其收敛的过程是一个随机震荡的曲线

迷你批量梯度下降(mini batch),综合考虑上面两种情况,每次选取样本中的一小批,比如一百个两百个,进行梯度下降

2、单个样本的代价函数

image-20250225195055559

编程实验

3、代码如下:

# dataset.py
import numpy as np

def get_beans(counts):
	xs = np.random.rand(counts)
	xs = np.sort(xs)
	ys = [1.2*x+np.random.rand()/10 for x in xs]
	return xs,ys
#随机梯度下降
import numpy as np
import matplotlib.pyplot as plt
import dataset

xs,ys = dataset.get_beans(100)
plt.title("Size-Toxicity")
plt.xlabel("Bean Size")
plt.ylabel("Toxicity")
plt.scatter(xs,ys)

w=0.1
y_pre = w*xs
plt.plot(xs,y_pre)
plt.show()

# 使用随机梯度下降算法进行调整
for i in range(100):
    for i in range(100):
        x = xs[i]
        y = ys[i]
        #a = x^2
        #b = -2*x*y
        #c = y^2
        #斜率k=2aw+b
        k = 2*(x**2)*w + (-2*x*y)
        alpha = 0.1
        w = w - alpha * k
        plt.clf()
        plt.scatter(xs,ys)
        y_pre = w*xs
        plt.xlim(0,1)
        plt.ylim(0,1.2)
        plt.plot(xs,y_pre)
        plt.pause(0.01)



#重新绘制预测曲线
# plt.scatter(xs,ys)
# y_pre = w*xs
# plt.plot(xs,y_pre)
# plt.show()

#批量梯度下降
import numpy as np
import matplotlib.pyplot as plt
import dataset

xs,ys = dataset.get_beans(100)
plt.title("Size-Toxicity")
plt.xlabel("Bean Size")
plt.ylabel("Toxicity")
plt.scatter(xs,ys)

w=0.1
y_pre = w*xs
plt.plot(xs,y_pre)
plt.show()

alpha = 0.1
for _ in range(100):
    k = (2*np.sum(xs**2)*w + np.sum(-2*xs*ys))/100
    w = w - alpha * k
    y_pre = w*xs

    #预测函数图像
    plt.clf()
    plt.scatter(xs, ys)
    plt.xlim(0, 1)
    plt.ylim(0, 1.2)
    plt.plot(xs, y_pre)
    plt.pause(0.01)

# 固定步长的梯度下降
import numpy as np
import matplotlib.pyplot as plt
import dataset

xs,ys = dataset.get_beans(100)
plt.title("Size-Toxicity")
plt.xlabel("Bean Size")
plt.ylabel("Toxicity")
plt.scatter(xs,ys)

w=0.1
y_pre = w*xs
plt.plot(xs,y_pre)
plt.show()

alpha = 0.1
step = 0.01
for _ in range(100):
    k = (2*np.sum(xs**2)*w + np.sum(-2*xs*ys))/100
    if k>0:
        w = w - step
    else:
        w = w + step
    y_pre = w*xs


    #预测函数图像
    plt.clf()
    plt.scatter(xs, ys)
    plt.xlim(0, 1)
    plt.ylim(0, 1.2)
    plt.plot(xs, y_pre)
    plt.pause(0.01)

四、梯度下降和反向传播:(下)

原理

1、如果把w、b同时考虑在内,寻找代价函数最低点,那么就变成在曲面上找最低点,这个曲面其实看起来并不是一个碗,而是一个很扁很扁的碗,梯度其实就是把对w和对b的偏导数看成是向量,把两个向量合在一起,形成一个新的合向量,沿着这个和向量进行下降,是这个曲面在该点下降最快的方式,合向量就是梯度

2、按照麦卡洛克-皮兹神经元模型,我们使用一个一元一次线性函数模拟神经元的树突、轴突的行为,这就是预测函数模型,而把我们统计观测而来的数据送入预测函数进行预测的过程,称之为前向传播

image-20250225202939232

3、数据通过预测函数完成一次前向传播就会得到一个预测值,预测值和统计而来的真实值之间存在误差,我们选择平方误差作为误差的评估手段,而误差和预测函数中的参数又会形成一种函数关系,我们把这个函数称之为代价函数,因为采用平方误差去评估预测误差,所以也称之为方差代价函数,描述了预测函数中的参数取不同值的时候预测的不同的误差代价,而用这个代价函数去修正预测函数参数的过程也称之为反向传播,因为计算从后往前,而这个反向传播的参数式修正方法,我们使用**“梯度下降”算法**。

image-20250225203549460在调整过程中用来调和下降幅度的alpha,称之为学习率,他的选择影响调整的速度,太大了容易反复横跳,过大则会不收敛甚至发散,太小又容易磨磨唧唧,是设计者根据经验选择出来的。

image-20250225204042480

而不断经历前向传播和反向传播最后到达代价函数最低点的过程,称之为训练或者学习,这就是所谓的机器学习中的神经网络

image-20250225204217285

编程实验

4、编程实现:除了w存在,b也存在的预测函数和方差代价函数:

# dataset.py
import numpy as np

def get_beans(counts):
	xs = np.random.rand(counts)
	xs = np.sort(xs)
	ys = np.array([(0.7*x+(0.5-np.random.rand())/5+0.5) for x in xs])
	return xs,ys
#使用mpl_toolkits.mplot3d来实现三维图的绘制,这是matplotlib中的一个插件,如果三维图不显示,那么需要卸载当前版本的matplotlib,安装3.5版本的

import matplotlib.pyplot as plt
import numpy as np
import dataset
from mpl_toolkits.mplot3d import Axes3D

m=100
xs,ys = dataset.get_beans(100)

#配置图像
plt.title("Size-Toxicity Function",fontsize=12)
plt.xlabel("Bean Size")
plt.ylabel("Toxicity")
plt.xlim(0,1)
plt.ylim(0,1.5)

plt.scatter(xs,ys)

w = 0.1
b = 0.1
y_pre = w*xs + b
plt.plot(xs,y_pre)
plt.show()

# 下面的代码简单理解就是把Axes3D和matplotlib做了一个链接
fig = plt.figure()
ax = Axes3D(fig)


ws = np.arange(-1,2,0.1)
bs = np.arange(-2,2,0.1)

for b in bs:
    es = []
    for w in ws:
        y_pre = w*xs+b
        e = np.sum((ys-y_pre)**2)*(1/m)
        es.append(e)

    # plt.plot(ws,es)
    ax.plot(ws,es,b,zdir='y')

plt.show()

五、激活函数

给机器注入灵魂

原理

1、人类思考问题的方式大多是分类而不是拟合,说白了就是人类擅长贴标签,比如一种食物随着大小不同毒性也不同,我们更愿意对其进行分类,分成有毒和无毒,对于下图,我们把毒性用概率表示,1表示有毒,0表示无毒,不存在中间的值,所以对于这个两极分化的分类问题,之前的神经元模型必然失效,我们更期望输出变成下图这样,在一元一次函数的输出大于某个阈值的时候固定输出1,小于某个阈值的时候固定输出0,很明显这是一个分段函数。

image-20250225214605248

但是使用阶跃函数作为激活函数并不美好,大括号就难以处理,并且在分界点处的导数为不可求,其他地方的导数也为0。这很不利于梯度下降的进行image-20250225215837979

我们更倾向与使用s型函数——Logistic函数,标准的Logistic函数如图右侧所示

image-20250225215104107

image-20250225215707724

注意下方提示

image-20250301193130699

对于Logistic激活函数,a和z形成的曲线不会随着w,b的变化而变化,但是a和x形成的曲线会随着w,b的变化而变化

image-20250225220928763

2、复合函数求导等于内导乘以外导,方差代价函数如下所示,为了求最小的e,我们需要对w求导,探究w取什么时候e最小

image-20250225222853612

e对a求导结果如下:
d e d w = d e d a ⋅ d a d z ⋅ d z d w = − 2 ( y − a ) a ( 1 − a ) x \frac{de}{dw}=\frac{de}{da}\cdot\frac{da}{dz}\cdot\frac{dz}{dw}=-2(y-a)a(1-a)x dwde=dadedzdadwdz=2(ya)a(1a)x
我们在前向传播的时候只需要保存好计算的结果a,那么把a带入这个式子就很容易计算出具体的结果,然后利用梯度下降去调整w,同样,还可以求出e对b的导数值:
d e d b = d e d a ⋅ d a d z ⋅ d z d b = − 2 ( y − a ) a ( 1 − a ) 1 \frac{de}{db}=\frac{de}{da}\cdot\frac{da}{dz}\cdot\frac{dz}{db}=-2(y-a)a(1-a)1 dbde=dadedzdadbdz=2(ya)a(1a)1
3、实际上,利用复合函数的链式求导法则,我们可以很容易计算出更加复杂的预测模型的代价函数在各个参数上的导数,比如之后学习的多层感知器或者说多层神经网络,本质上就是一个嵌套的非常非常深的复合函数

image-20250225223526064

当然可以横向增加神经元的个数,也可以纵向增加神经元的层数,而利用复合函数求导的链式法则,把梯度下降和反向传播完美结合后,不论网络有多深多宽,我们都可以使用同样的套路计算出代价函数在每一个神经元上权值参数的导数,然后利用梯度下降算法去调整这些参数

image-20250225223921085

激活函数是非线性的,他让我们开始具备处理复杂问题的能力,激活函数如果是线性的,即使有一个很复杂的网络,他们在一起仍然是线性系统,因为线性函数不论怎么叠加,结果都是线性的

加入激活函数神经元扩展到多个节点的网络,这时候也就可以说是更一般情况下真正的反向传播算法。把代价函数在神经网络中反向传播到各个节点,计算参数对代价函数的影响,然后调整参数,正是深度学习的开山鼻祖乔弗里辛顿和戴维娜姆哈特等人在1986年开始在神经网络的研究中引入的反向传播算法成为了现代神经网络的基石。

编程实验

4、编程训练:对w和b进行复合函数求导之后,利用sigmoid研究梯度下降

# dataset.py
import numpy as np

def get_beans(counts):
	xs = np.random.rand(counts)
	xs = np.sort(xs)
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		yi = 0.7*x+(0.5-np.random.rand())/50+0.5
		if yi > 0.8:
			ys[i] = 1
	return xs,ys
import dataset
import matplotlib.pyplot as plt
import numpy as np

m = 100
xs,ys = dataset.get_beans(m)

#配置图像
plt.title("Size-Toxicity")
plt.xlabel("Bean Size")
plt.ylabel("Toxicity")
plt.scatter(xs,ys)

w = 0.1
b = 0.1
z = w * xs + b
a = 1/(1+np.exp(-z))#exp函数以自然底数e作为底数,把传入的参数作为幂进行计算
plt.plot(xs,a)
plt.show()

# 在全部样本上做了五百次梯度下降,也就是训练了五百次
for _ in range(5000):
    # 随机梯度下降(在每个点上都进行梯度下降)
    # 下面就相当于在每一个点上求导数
    for i in range(100):
        x = xs[i]
        y = ys[i]
        # 分别对w和b直接进行求导,不借助第三个变量求偏导,求偏导的过程在下面
        # dw = 2*x**2*w + 2*x*b - 2*x*y
        # db = 2*b + 2*x*w - 2*y
        # alpha = 0.01
        # w = w - alpha*dw
        # b = b - alpha*db

        # 对w和b分别求偏导的目的是为了接下来的梯度下降
        # 对w和b求偏导
        z = w * x + b
        a = 1/(1+np.exp(-z))
        e = (y-a)**2
        # 使用复合函数求导法则
        deda = -2 * (y-a)
        dadz = a * (1-a)
        dzdw = x

        dedw = deda * dadz * dzdw

        dzdb = 1

        dedb = deda * dadz * dzdb

        alpha = 0.05

        # 进行梯度下降
        w = w - alpha * dedw
        b = b - alpha * dedb

    # 训练太慢,需要加快速度,我们可以利用外层循环的变量
    if _ % 100 == 0:
        plt.clf()
        plt.scatter(xs,ys)
        z = w * xs + b
        a = 1/(1+np.exp(-z))
        # y_pre = w * xs + b
        plt.xlim(0,1)
        plt.ylim(0,1.2)
        plt.plot(xs,a)
        plt.pause(0.01)

六、隐藏层

神经网络为什么working

原理

1、在研究不是简单的具有单调性的数据的分类问题时候,用之前的sigmoid函数就行不通了,这个时候需要我们使用不单调的预测函数,如下图,那么这个时候,就需要神经元形成一个网络了。

image-20250226094812715

在原本单一的神经元基础上,我们需要多添加两个神经元,并把输入分别送到第一层两个神经元结合每条边的权重进行计算,再把结果送入第二层的一个神经元进行计算,最后输出。

image-20250226095234310

经过调参之后让其差不多拟合

image-20250226095729501

也就是说把输入分为两个部分,然后分别对这两个部分进行调节,然后再送入最后一个神经元,让整体的神经网络形成一个单调性不唯一的更多变函数,从而具备了解决更复杂问题的能力。因为上图中的神经网络,每个神经元都有一个偏置项b,也就是线性函数的截距,一般认为是大家默认的共识,所以为了在画网络结构时候不那么拖沓,就会直接省略b,让网络结构图更精简。

从结果输入到最后输出的神经元之间的新添加的神经元结点也称之为隐藏层,正是隐藏层的存在,才让神经网络在复杂情况下仍然working,隐藏层的神经元越多,就可以产生越复杂的组合,能解决越复杂的问题,当然计算量也越大。当然也可以增加隐藏层的层数,使之变深

image-20250226100424508

image-20250226100845050

2、网络中各个神经元的参数被调节成为不同的值,这些功能单一的神经元联结组合出来的整体,就可以近似出一个相当复杂的函数,最终变成什么样子,则是根据我们采集的训练数据而定,而我们采集的训练数据越充足,那么最后训练得到的模型也就能够越好的预测新的问题,因为越充足的训练数据,也就越大程度上蕴藏了问题的规律特征,新的问题数据也就越难以逃脱这些规律的约束。

image-20250226101527747

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

我们经常听到深度学习,其实“深度”二字并没有什么特别的奥义,只是指一个神经网络中纵向的隐藏层比较多,换句话说,很深,我们一般吧隐藏层超过三层的网络就称之为深度神经网络

image-20250226102159735

而这也是深度学习中广受诟病的地方,隐藏层的神经元在理解什么,提取什么都太过于微秒,虽然我们对大致结果有所把握,但是很难用精准的数学去进行描述,我们能做的也只有设计一个网络,送入数据,然后进行充分的训练,如果得到的预测效果好,我们就会说,他“起作用了”,如果不好,那也只能说,“调调参数,再来一遍”。所以很多人戏称深度学习是“炼丹”,确实,道士把原材料(数据)放入八卦炉(神经网络)开火炼丹,最后得到的仙丹可能让人长生不老,也可能让人一命呜呼,炼丹过程中炉子里发生的微妙的事情,道士也是不得而知的,虽然是他设计了炼丹的一切。

image-20250226102808261

编程实验

3、编程训练:

神经网络的搭建过程中,最重要的是梳理清楚参数的名字。我们需要知道神经元是和哪一个输入进行连接的,比如第一个输入和第一个神经元连接,并且这个神经元在第一层,我们就给这根连接上的权重起名字为w11_1;同样,我们使用b1_1表示第一层的第一个神经元的偏置项(截距);再看第二层,我们使用w11_2表示第二层的第一个神经元在第一个输入上的权重,用w21_2表示第二层的第一个神经元在第二个输入上的权重,当然不能忘了每个神经元上默认的共识偏置项(截距),我们用b1_2表示第二层的第一个神经元的偏置项

image-20250226111234973

# 因为隐藏层不止一个神经元,所以最重要的是梳理清楚参数的名字
# 我们需要知道神经元是和哪一个输入进行连接的,比如第一个输入和第一个神经元连接,并且这个神经元在第一层,我们就起名字为w11_1
# 第一层
# 第一个神经元
w11_1 = np.random.rand()
b1_1 = np.random.rand()
# 第二个神经元
w12_1 = np.random.rand()
w2_1 = np.random.rand()
# 第二层
w11_2 = np.random.rand()
w21_2 = np.random.rand()
b1_2 = np.random.rand()
# dataset.py
import numpy as np

def get_beans(counts):
	xs = np.random.rand(counts)*2
	xs = np.sort(xs)
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		yi = 0.7*x+(0.5-np.random.rand())/50+0.5
		if yi > 0.8 and yi < 1.4:
			ys[i] = 1

	return xs,ys
import dataset
import matplotlib.pyplot as plt
import numpy as np

m = 100
xs,ys = dataset.get_beans(m)


#配置图像
plt.title("Size-Toxicity")
plt.xlabel("Bean Size")
plt.ylabel("Toxicity")
plt.scatter(xs,ys)


# 激活函数
def sigmoid(x):
    return 1/(1+np.exp(-x))


# 因为隐藏层不止一个神经元,所以最重要的是梳理清楚参数的名字
# 我们需要知道神经元是和哪一个输入进行连接的,比如第一个输入和第一个神经元连接,并且这个神经元在第一层,我们就起名字为w11_1
# 第一层
    # 第一个神经元
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()
def forward_propgation(xs):
    # 表示好参数之后实现前向传播
    # 第一层
    # 第一个神经元的前向传播就是,先算线性函数:
    z1_1 = w11_1 * xs + b1_1
    # 再用激活函数激活一下, a1_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

a1_2,z1_2,a2_1,z2_1,a1_1,z1_1 = forward_propgation(xs)

# 绘制xs和a1_2的关系
plt.plot(xs,a1_2)
plt.show()

#下面是反向传播(反向传播和梯度下降需要反复进行)

# 在全部样本上做了五百次梯度下降,也就是训练了五百次
for _ in range(5000):
    # 随机梯度下降(在每个点上都进行梯度下降)
    # 下面就相当于在每一个点上求导数
    for i in range(100):
        x = xs[i]
        y = ys[i]
        # 因为这里每次求误差e进行反向传播的时候都要先进行一次前向传播,所以我们需要把前向传播的代码封装成一个函数
        a1_2, z1_2, a2_1, z2_1, a1_1, z1_1 = forward_propgation(xs)
        # 误差代价
        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

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

        dz1_2db1_2 = 1
        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
        dedw11_1 = deda1_2 * da1_2dz1_2 * dz1_2da1_1 * da1_1dz1_1 * dz1_1dw11_1
        dz1_1db1_1 = 1
        dedb1_1 = deda1_2 * da1_2dz1_2 * dz1_2da1_1 * da1_1dz1_1 * dz1_1db1_1

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

        # alpha为学习率
        alpha = 0.03
        w11_2 = w11_2 - alpha * dedw11_2
        w21_2 = w21_2 - alpha * dedw21_2
        b1_2 = b1_2 - alpha * dedb1_2

        w12_1 = w12_1 - alpha * dedw12_1
        b2_1 = b2_1 - alpha * dedb2_1

        w11_1 = w11_1 - alpha * dedw11_1
        b1_1 = b1_1 - alpha * dedb1_1

    # 训练太慢,需要加快速度,我们可以利用外层循环的变量
	if _ % 100 == 0:
        plt.clf()  ## 清空窗口
        plt.scatter(xs, ys)
        a1_2, z1_2, a2_1, z2_1, a1_1, z1_1 = forward_propgation(xs)
        plt.plot(xs, a1_2)
        plt.pause(0.01)  

image-20250226112755386

image-20250226112916665

七、高维空间

机器如何面对越来越复杂的问题

原理

1、为了更加清晰的了解神经网络的工作原理,一直仅仅在用豆豆大小这个特征去预测它的毒性,但现实世界往往并非如此简单。比如豆豆的毒性不仅和它的大小有关系,也和它的颜色深浅有关,当毒性只和大小有关系的时候,我们可以用一个二维的坐标系来描述这个关系。此刻加入一个颜色深浅的特征,我们就需要一个三维的坐标系给颜色的深浅也指定一个表达的维度,当输入多了一个颜色深浅数据之后,我们的神经元的树突也就需要接收两个输入预测函数的线性部分需要从一元一次函数变成二元一次函数,颜色深浅成为了另外一个元,很明显这是一个有两个自变量,一个因变量的二元一次函数,如果我们把这个函数在三维坐标系中画出来,那么就是一个平面,这个线性函数的平面经过神经元的第二部分非线性激活函数之后,就被扭曲成为一个s型的曲面,正如一元一次函数的直线被激活函数扭曲成为一个s型曲线一样,那么这个曲面就表示当大小和颜色深浅取某个值的时候,对有毒概率的预测结果,而当我们找到这个预测曲面上预测值为0.5的点连接在一起,用地理里的概念来说就是高度为0.5的等高线,你会发现是一条直线,这条直线的一边的预测结果大于0.5也就是有毒,而另一边的预测结果小于0.5。这个0.5的等高线也就是类型分割线

image-20250226115156694

但如果环境中豆豆的大小、颜色、深度和毒性分布是这样一个情况,那就无能为力了,这就是线性不可分问题,罗森布拉特感知器便止步于此

image-20250226115416813

原因就是单个神经元无法做到这一点,一个二元一次函数在三维空间中将形成一个面,而且必然是平面再套上一层Sigmoid激活函数之后,这个面被扭曲成为曲面,然而这个曲面的0.5等高线仍旧是一个直线,而一个直线并不能分割这种弯曲的边界,我们可能需要一个曲线来实现这一点。而解决的办法就是再添加隐藏层神经元,类比之前单输入的二维函数,从数学上来看,我们引入多个神经元再通过梯度下降进行调整,就可以组合出不同的形状。在这里对于二元输入,同样在引入隐藏层神经元之后,我们也就可以扭曲三维空间中的面,让它们组合出不同的曲面,而0.5的分割线也就随之被扭曲成为一个曲线。

image-20250226115956707

而通过调整这些神经元的参数,最终把曲面扭曲成为这个样子,0.5的等高线也被扭曲成为下图样子,如此就解决了这种弯曲数据分布的分类问题。

image-20250226120111300

可以自行打开谷歌的tensorflow游乐场,选择圆圈形状的数据集,手动添加神经元,看看神经元需要添加到几个才可以把分割线扭曲成一个圈而实现类型分割,地址:http://playground.tensorflow.org/,答案是三个。为什么是三个不是两个,一个神经元的等高线是一条直线,两个是两条直线,但遗憾的是平面中的两条直线是无法完成闭合的,但是三个神经元是三条直线可以形成一个闭合的形状,把这三个神经元的计算结果再通过一个输出层的神经元进行汇合,通过Sigmoid的激活函数修饰一下之后就可以了,

image-20250226125521502

2、输入数据有多少元素也就是所谓的特征维度,也叫数据维度

image-20250226130826082

到此当输入数据的维度越来越多的时候,我们会发现权重参数也越来越多,如果一个个的去编写它们的函数表达式未免有点麻烦和拖沓。所以需要使用专门用来处理多维度数据的数学工具:向量和矩阵。之后我们一般是直接采用Keras框架实现神经网络的搭建,彼时我们不再需要处理前向传播,也不再需要手写反向传播,甚至梯度下降的细节也会被框架隐藏起来。但我们唯一逃不脱的就是对矩阵和向量的使用。

编程实验

3、编程实验:本次编程实验我们主要来实现一个接收两个输入的神经元,也就是豆豆的输入数据特征维度为二。其实很简单,就是把我们的预测模型中的前向传播的线性函数部分,从一元一次函数改成二元一次函数,然后再反向传播的时候,多考虑一个权重的参数

image-20250226133001112

张量是向量和矩阵向更高维度的推广,所以对于张量的数学含义我们不必多虑,就是多维的数据而已,我们也可以把一维数组称之为一维张量,二维的矩阵称之为二维张量,三维的就是三维张量,当然还可以有四维五维,我们画不出来就是了。而在编码的时候我们又回到了计算机领域,在计算机领域并没有这么多花里胡哨的名称,那就一个概念,一维数组、二维数组、三维数组、四维数组等等。

编程实验:

python中xs[:0]代表在xs这个矩阵的所有上,把第0切割下来形成一个数组,也叫切片

#dataset.py 用来制造数据
import numpy as np

def get_beans(counts):
	xs = np.random.rand(counts,2)*2
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		if (x[0]-0.5*x[1]-0.1)>0:
			ys[i] = 1
	return xs,ys

def get_beans2(counts):
	xs = np.random.rand(counts,2)*2
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		if (np.power(x[0]-1,2)+np.power(x[1]-0.3,2))<0.5:
			ys[i] = 1


	return xs,ys
# 封装好的绘图函数,我们只需要关注原理即可,绘图不是我们需要关心的事情,所以这里直接进行封装,便于其他py文件直接调用绘图函数
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

def show_scatter(xs,y):
	x = xs[:,0]
	z = xs[:,1]
	fig = plt.figure()
	ax = Axes3D(fig)
	ax.scatter(x, z, y)
	plt.show()

def show_surface(x,z,forward_propgation):
	x = np.arange(np.min(x),np.max(x),0.1)
	z = np.arange(np.min(z),np.max(z),0.1)
	x,z = np.meshgrid(x,z)
	y = forward_propgation(x,z)
	fig = plt.figure()
	ax = Axes3D(fig)
	ax.plot_surface(x, z, y, cmap='rainbow')
	plt.show()



def show_scatter_surface(xs,y,forward_propgation):
	x = xs[:,0]
	z = xs[:,1]
	fig = plt.figure()
	ax = Axes3D(fig)
	ax.scatter(x, z, y)

	x = np.arange(np.min(x),np.max(x),0.01)
	z = np.arange(np.min(z),np.max(z),0.01)
	x,z = np.meshgrid(x,z)
	y = forward_propgation(x,z)
	
	ax.plot_surface(x, z, y, cmap='rainbow')
	plt.show()

下面是_inputs_model.py,用来绘制一个具有两个输入x1,x2的神经元模型,前向传播公式为z=w1x1+w2x2+b, a=sigmoid(z)

# _inputs_model.py
import numpy as np
import dataset
import plot_utils

m = 100
xs,ys = dataset.get_beans(m)
print(xs)
print(ys)
plot_utils.show_scatter(xs,ys)

w1 = 0.1
w2 = 0.2
b = 0.1

# 切片操作
x1s = xs[:0]
x2s = xs[:1]
def forward_propgation(x1s,x2s):
    z = w1*x1s + w2*x2s + b
    a = 1/(1++np.exp(-z))
    return a

plot_utils.show_scatter_surface(xs,ys,forward_propgation)

第一个图展示数据的分布:

image-20250227094717225

第二个图展示分割的曲面,因为w和b都是定值,所以是平面,只是先看一看样子,所以只是示意图看看效果:

image-20250227094817423

下面进行前向传播和梯度下降是指成为曲面:

# _input_model.py
# 接上代码

# 训练500次
for _ in range(500):
    # 取出每一个数据进行随机梯度下降
    for i in range(m):
        x = xs[i] # 这里的x不是一个数字了,而是一个有两个元素的数组,第一个元素表示豆豆大小,第二个元素表示豆豆颜色深浅,可以通过打印xs矩阵看到,矩阵是一个100*2的矩阵
        y = ys[i]
        x1 = x[0]
        x2 = x[1]

        a = forward_propgation(x1,x2)

        # 下面开始计算误差代价然后反向传播,通过梯度下降调整参数
        e = (y-a)**2

        deda = -2*(y-a)
        dadz = a*(1-a)
        dzdw1 = x1
        dzdw2 = x2
        dzdb = 1

        # 通过链式法则得到误差e对w1和w2的导数
        dedw1 = deda * dadz * dzdw1
        dedw2 = deda * dadz * dzdw2
        dedb = deda * dadz * dzdb

        # 根据梯度下降算法调整参数
        alpha = 0.01
        w1 = w1 - alpha*dedw1
        w2 = w2 - alpha*dedw2
        b = b - alpha*dedb

plot_utils.show_scatter_surface(xs,ys,forward_propgation)

效果如图,一个曲面将两类豆豆分成了有毒无毒,并且在看俯视图的时候有一条0.5的等高线确实分隔开了豆豆

image-20250227100424621

image-20250227100500704

八、初识Keras

轻松完成神经网络模型搭建

向量和矩阵原理

1、使用向量来进行数学运算和多元一次函数的计算,这样就可以使用向量运算来表示线性函数了

image-20250227101102125

  • X是输入向量
  • W是权重系数向量
  • B是偏置系数向量
  • Z是结果向量

向量的元素数量也叫维度,一个具有100个输入的输入向量X就是一个100维的向量,比如中学时候我们学过,我们使用二维向量(x,y)来表示二维平面上的一个矢量,用(x,y,z)来表示三维空间中的一个矢量

2、如果输入的数据输入的是一组函数进行运算,比如我们加入两个隐藏层神经元的神经网络,那输入实际上就是分别输入到两个线性函数进行计算,这时候单靠向量似乎也不是很酸爽。

image-20250227101933423

每一个函数都有一个权重系数向量和偏置系数向量,为什么不把它们放在一起呢?

image-20250227101859574

于是矩阵出现了,同样这个矩阵也有转置操作,其实大可以把矩阵看作是由多个向量并在一起形成的特殊向量,所以矩阵的转置就可以简单的看作是对其中每一个向量进行转置操作,第一行竖起来变成第一列,第二行竖起来变成第二列。转置以后我们再让输入向量x点成系数矩阵,而输入向量x和权重系数矩阵的点乘运算也可以看作是分别让输入向量x点乘第一列计算出第一个计算结果,再让输入向量x点成第二列,计算出第二个结果,这两个计算的结果组成一个结果向量,再让结果向量加上偏置向量b你看最终的结果向量中的两个元素分别就是这两个函数的运算结果,所以会发现此时对于多个函数,我们仍然可以使用z等于x乘以w的转置加b来表示,只不过我们现在要明确w是一个2×3的矩阵。

image-20250227102517463

Keras原理

​ 我们可以做一个类比进行理解,我们用汇编语言写程序也是可以的,可是那太麻烦了,你必须完全了解你所面对的机器有多少个寄存器,ROM有多少,怎么分布的,如何响应中断等等。所以人们发明的c语言一下子就让我们很大程度上摆脱了计算机底层硬件的琐碎问题,而能专注于编写我们想要的功能。当然在使用c语言的时候,我们必要时还需要考虑一些点层的琐碎问题,人们希望编程更简单,更注重业务,而不是机器,所以出现了Java、c#等等这些更加上层的语言。

​ 直到Python、JS这种脚本语言的出现,我们几乎不太需要关心底层到底发生了什么。Keras框架,就像机器学习里的高级语言实现了对机器学习神经网络底层复杂的数据运算的封装,我们可以轻松的通过它提供的各种上层接口搭建模型,当然除了Keras以外,还有很多其他的框架,比如最耳熟能详的Tensorflow,还是用编程语言来举例子,Tensorflow更像是c语言对底层的封装并不是那么的完全,但是更加的强大和灵活,而Keras更像是Python语言就两个字简单。

​ 我们可以类比一下,一个同样的神经元模型,用Keras和Tensorflow框架实现的代码量,我们会很直观的看见它们的区别。

image-20250227112150958

​ 但不论Keras如何简单,我们也不能在完全不懂底层原理的情况下去使用它。就像不论你使用c语言还是Python语言编程,都断然不可完全不懂计算机基本原理一样,或许你能做出一点东西,但最终会很虚无缥缈,也很难向更高的地方前进。之前即使为了实现一个简单的神经元,也需要编写各种繁琐的代码,首先是前向传播,然后是计算代价函数,再然后利用反向传播计算误差在每个神经元上的权重和偏置参数的导数进行梯度下降,这些过程想想就很繁琐,而利用Keras框架事情就变成了下面这个样子:

image-20250227112502642

​ 现在如果我们想要把神经元变成两个,你只需要把units等于一改成units等于二就可以了。

image-20250227112653411

​ 当然我们的豆豆分类作为一个分类问题,只有一个0或1的单输出,两个神经元有两个输出,所以我们需要在后面再加一个神经元,把这两个神经元汇合一下。如果你想让这个模型更加复杂也更强大一点,比如把第一层的神经元数量改成4,同样也只需要简单的把units设置为4即可。

image-20250227112801898

​ 我们也简单的介绍一下Keras存在的一些问题,首先,Keras并不是一个独立的框架,而是通过调用诸如Tensorflow、CNTK或Theano独立的框架实现的,你可以简单地类比编程语言中,Python语言的运行环境是用更底层一点的c或c++写成的。第二就因为它太简单了,封装的太好了,所以有时候并没有像更加底层的Tensorflow那样灵活,虽然他说高度模块化可扩展性,但众所周知,往往高度封装带来的简单会造成对具体细节控制的流失。

​ 当然经过这么多年的发展,Keras在灵活性上也在不断的增强,而作为同出谷歌之手的两大框架,目前也已经把Keras并入到了Tensorflow之中,作为Tensorflow的高级API,但问题是如果我使用Keras去做这么细致的事情,为什么不使用Tensorflow?正如我们在面对谁是最好的编程语言,这个问题一样,答案是一致的,没有最好,只有最合适。

编程实验

实验一:使用向量和矩阵运算的方式来构建神经元

一般我们用大写的字母来表示一个向量或矩阵

前置代码(下面的实验二也会用到):

# dataset.py
import numpy as np

def get_beans(counts):
	xs = np.random.rand(counts,2)*2
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		if (x[0]-0.5*x[1]-0.1)>0:
			ys[i] = 1
	return xs,ys

def get_beans1(counts):
	xs = np.random.rand(counts)
	xs = np.sort(xs)
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		yi = 0.7*x+(0.5-np.random.rand())/50+0.5
		if yi > 0.8:
			ys[i] = 1
		else:
			ys[i] = 0
	return xs,ys

def get_beans2(counts):
	xs = np.random.rand(counts)*2
	xs = np.sort(xs)
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		yi = 0.7*x+(0.5-np.random.rand())/50+0.5
		if yi > 0.8 and yi < 1.4:
			ys[i] = 1


	return xs,ys

def get_beans3(counts):
	xs = np.random.rand(counts)*2
	xs = np.sort(xs)
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		yi = 0.7*x+(0.5-np.random.rand())/50+0.5
		if yi > 0.8 and yi < 1.4:
			ys[i] = 1

		if yi > 1.6 and yi < 1.8:
			ys[i] = 1
	return xs,ys

def get_beans4(counts):
	xs = np.random.rand(counts,2)*2
	ys = np.zeros(counts)
	for i in range(counts):
		x = xs[i]
		if (np.power(x[0]-1,2)+np.power(x[1]-0.3,2))<0.5:
			ys[i] = 1


	return xs,ys

# 封装好的绘图函数,我们只需要关注原理即可,绘图不是我们需要关心的事情,所以这里直接进行封装,便于其他py文件直接调用绘图函数
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from keras.models import Sequential#导入keras


def show_scatter_curve(X,Y,pres):
	plt.scatter(X, Y) 
	plt.plot(X, pres) 
	plt.show()

def show_scatter(X,Y):
	if X.ndim>1:
		show_3d_scatter(X,Y)
	else:
		plt.scatter(X, Y) 
		plt.show()


def show_3d_scatter(X,Y):
	x = X[:,0]
	z = X[:,1]
	fig = plt.figure()
	ax = Axes3D(fig)
	ax.scatter(x, z, Y)
	plt.show()

def show_surface(x,z,forward_propgation):
	x = np.arange(np.min(x),np.max(x),0.1)
	z = np.arange(np.min(z),np.max(z),0.1)
	x,z = np.meshgrid(x,z)
	y = forward_propgation(X)
	fig = plt.figure()
	ax = Axes3D(fig)
	ax.plot_surface(x, z, y, cmap='rainbow')
	plt.show()



def show_scatter_surface(X,Y,forward_propgation):
	if type(forward_propgation) == Sequential:
		show_scatter_surface_with_model(X,Y,forward_propgation)
		return
	x = X[:,0]
	z = X[:,1]
	y = Y

	fig = plt.figure()
	ax = Axes3D(fig)
	ax.scatter(x, z, y)

	x = np.arange(np.min(x),np.max(x),0.1)
	z = np.arange(np.min(z),np.max(z),0.1)
	x,z = np.meshgrid(x,z)

	X = np.column_stack((x[0],z[0]))
	for j in range(z.shape[0]):
		if j == 0:
			continue
		X = np.vstack((X,np.column_stack((x[0],z[j]))))

	r = forward_propgation(X)
	y = r[0]
	if type(r) == np.ndarray:
		y = r

	
	y = np.array([y])
	y = y.reshape(x.shape[0],z.shape[1])
	ax.plot_surface(x, z, y, cmap='rainbow')
	plt.show()

def show_scatter_surface_with_model(X,Y,model):
	#model.predict(X)

	x = X[:,0]
	z = X[:,1]
	y = Y

	fig = plt.figure()
	ax = Axes3D(fig)
	ax.scatter(x, z, y)

	x = np.arange(np.min(x),np.max(x),0.1)
	z = np.arange(np.min(z),np.max(z),0.1)
	x,z = np.meshgrid(x,z)



	X = np.column_stack((x[0],z[0]))

	for j in range(z.shape[0]):
		if j == 0:
			continue
		X = np.vstack((X,np.column_stack((x[0],z[j]))))

	y = model.predict(X)
	
	# return
	# y = model.predcit(X)
	y = np.array([y])
	y = y.reshape(x.shape[0],z.shape[1])
	ax.plot_surface(x, z, y, cmap='rainbow')
	plt.show()

def pre(X,Y,model):
	model.predict(X)

下面是vec_calculate.py:

其中向量的点乘运算和转置运算需要特别说明:

def forward_propgation(X): # X和W:numpy的ndarray类型数组
    Z = X.dot(W.T) + b # ndarray的dot函数:点乘运算   ndarray的T属性:转置运算
    A = 1/(1++np.exp(-Z)) # exp函数仍然有广播机制,Z是一个向量,exp仍然会对向量中的每一个元素进行运算,+1也可以广播
    return A

image-20250227103725837

如果输入数据是一个豆豆的数据,那么Z是一个单元素的向量,结果A也是一个单元素的向量

Keras报错说明

keras报错说明还没有安装tensorflow或者keras版本与tensorflow对应不上,需要自己去查找keras和tensorflow的对应表来安装,我的是2.4.1版本的tensorflow,所以安装的是Keras-2.4.0

以下是Keras版本与TensorFlow版本的对应关系:

  • 对于TensorFlow 1.x,应使用Keras 2.1.6及以前版本。
  • 对于TensorFlow 1.13-1.15,应使用Keras版本2.2.4-2.3.1。
  • 对于TensorFlow 2.0及以上版本,应使用Keras 2.3.0及更高版本。Keras 2.4.0及以上版本兼容TensorFlow 2.4。
  • 对于TensorFlow 2.5版本,应使用Keras的2.5.0或更高版本。
import numpy as np
import dataset
import plot_utils

m = 100
X,Y = dataset.get_beans(m)
print(X)
print(Y)
plot_utils.show_scatter(X,Y)

# w1 = 0.1
# w2 = 0.2
W = np.array([0.1,0.1])
# b = 0.1
B = np.array([0.1])

def forward_propgation(X):
    Z = X.dot(W.T) + B
    A = 1/(1++np.exp(-Z))
    return A

plot_utils.show_scatter_surface(X,Y,forward_propgation)

# ------------------------------------------------------

# 训练500次
for _ in range(500):
    # 取出每一个数据进行随机梯度下降
    for i in range(m):
        Xi = X[i]
        Yi = Y[i] # 这个时候Xi是一个一行两列的数组,表示一个豆豆的大小和颜色深浅的数据特征
        A = forward_propgation(Xi)

        # 下面开始计算误差代价然后反向传播,通过梯度下降调整参数
        E = (Yi-A)**2

        dEdA = -2*(Yi-A)
        dAdZ = A*(1-A)
        dZdW = Xi
        dZdB = 1

        # 通过链式法则得到误差e对w1和w2的导数
        dEdW = dEdA * dAdZ * dZdW
        dEdB = dEdA * dAdZ * dZdB


        # 根据梯度下降算法调整参数
        alpha = 0.01
        W = W - alpha*dEdW
        B = B - alpha*dEdB

plot_utils.show_scatter_surface(X,Y,forward_propgation)

image-20250227111242066

当然我们也可以用矩阵和向量的运算去很容易的构建有隐藏层的神经网络,然后去解决稍微复杂一点的分类问题,但是在反向传播中对矩阵和向量的求导理解起来其实还是比较有一定的难度的。代码如下:

# vec_one_hidden_layer.py
import dataset
import plot_utils

import numpy as np
#从数据中获取随机豆豆
m=100
X,Y = dataset.get_beans4(m)
print(X)
print(Y)

plot_utils.show_scatter(X, Y)

W1 = np.random.rand(2,2)
B1 = np.random.rand(1,2)
W2 = np.random.rand(1,2)
B2 = np.random.rand(1,1)


def forward_propgation(X):

	#Z1:(m,2)
	Z1 = X.dot(W1.T) + B1

	#A1:(m,2)
	A1 = 1/(1+np.exp(-Z1))
	#Z2:(m,1)
	Z2 = A1.dot(W2.T) + B2
	#A2:(m,1)
	A2 = 1/(1+np.exp(-Z2))
	return A2,Z2,A1,Z1

plot_utils.show_scatter_surface(X, Y,forward_propgation)


for _ in range(5000):
	for i in range(m):
		Xi = X[i]

		Yi = Y[i]
		A2,Z2,A1,Z1 = forward_propgation(Xi)


		E = (Yi - A2)**2


		#(1,1)
		dEdA2 = -2*(Yi-A2)
		#(1,1)
		dEdZ2 = dEdA2*A2*(1-A2)

		

		#(1,2)
		dEdW2 = dEdZ2*A1
		#(1,1)
		dEdB2 = dEdZ2*1

		#(1,2)
		dEdA1 = dEdZ2*W2

		#(1,2)
		dEdZ1 = dEdA1*A1*(1-A1)


        
		dEdW1 = (dEdZ1.T).dot(np.array([Xi]))

        
        
		dEdB1 = dEdZ1*1

        
        
		alpha = 0.05
		W2 = W2 - alpha*dEdW2
		B2 = B2 - alpha*dEdB2

		W1 = W1 - alpha*dEdW1

		B1 = B1 - alpha*dEdB1
	#计算准确率

	A2,Z2,A1,Z1 = forward_propgation(X)
	A2 = np.around(A2)#四舍五入取出0.5分割线左右的分类结果
	A2 = A2.reshape(1,m)[0]
	accuracy = np.mean(np.equal(A2,Y))
	print("准确率:"+str(accuracy))

plot_utils.show_scatter_surface(X, Y,forward_propgation)

image-20250227111514602

实验二:使用Keras搭建神经网络模型

使用Keras框架实现之前所有的实验,也就是以下四种网络:

image-20250227113523648

  • Sequential就是用来堆叠神经网络的序列,深度神经网络其实就是一层一层堆叠出来的,所以可以认为Sequential就是用来堆叠神经网络的载体,可以理解为是一张画布

  • Dense简单来说就是一层神经网络(比如两个隐藏神经元的隐藏层就是一个dense,单个神经元的输出层也是一个dense)

  • 全连接层就是字面意思:这一层的每一个神经元的输入和输出和上一层全都连接着,一个都不少,目前搭建的都是这种全连接的神经网络

image-20250227114312296

  • units代表这一层有几个神经元
  • activation表示指定的激活函数的类型。
  • input_dim输入的数据的特征维度,在我们的第一个数据集里其实只有豆豆的大小这一个特征,所以input的定我们设置为1

image-20250227132725386

  • 损失函数=代价函数,这里使用均方误差
  • 优化器optimizer用来优化或者调整参数的算法,我们指定使用随机梯度下降算法sgd
  • metrics衡量指标,表示训练时我们希望得到的评估标准,这是一个数组类型,我们就指定一个accuracy准确度就好了,这样在训练的时候就会得到预测的准确度

image-20250227132957673

  • 配置好之后使用model的fit函数开始训练,传入样本特征数据X作为输入,Y做为标准的答案数据。然后我们再指定一下训练的回合数epochs=5000和每一次训练使用的样本数量batch_size=10开始训练。

image-20250227133318173

  • 我们把在全部样本上完成一次训练称为一个回合。如果我们的样本有100个,batch_size等于10,每次训练的时候就拿出10个数据进行训练,那么需要经过10次训练才能完成在整个样本上的一个回合,如果我们的回合数量epochs等于5,那么就需要这个过程经历5次,一共就相当于训练了10×5=50次。

image-20250227133408071

  • 训练好之后,我们再用model的predict的函数做一次预测。pres就是我们最后训练好的模型对豆豆数据集的预测结果。

image-20250227133812116

  • 前置代码在实验一开头部分:dataset.py和plot_utils.py

  • 完整代码keras_for_blue.py

第一种数据集

import dataset
import numpy as np
import plot_utils
from keras.models import Sequential
from keras.layers import Dense

m = 100
# 先获取第一种数据集
X,Y = dataset.get_beans1(m)
plot_utils.show_scatter(X,Y)

# 搭建神经网络
# 创建一个画布
model = Sequential()
# 添加神经元
model.add(Dense(units=1, activation='sigmoid', input_dim=1))
# 指定参数调整方式
model.compile(loss='mean_squared_error', optimizer='sgd', metrics=['accuracy'])
# 指定训练次数
model.fit(X, Y, epochs=5000, batch_size=10)
# 接受预测结果
pres = model.predict(X)

plot_utils.show_scatter_curve(X, Y, pres) # 绘制数据和预测曲线
...
Epoch 4990/5000
100/100 [==============================] - 0s 125us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4991/5000
100/100 [==============================] - 0s 115us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4992/5000
100/100 [==============================] - 0s 120us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4993/5000
100/100 [==============================] - 0s 127us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4994/5000
100/100 [==============================] - 0s 134us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4995/5000
100/100 [==============================] - 0s 133us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4996/5000
100/100 [==============================] - 0s 126us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4997/5000
100/100 [==============================] - 0s 117us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4998/5000
100/100 [==============================] - 0s 132us/sample - loss: 0.0542 - acc: 1.0000
Epoch 4999/5000
100/100 [==============================] - 0s 140us/sample - loss: 0.0542 - acc: 1.0000
Epoch 5000/5000
100/100 [==============================] - 0s 120us/sample - loss: 0.0542 - acc: 1.0000

image-20250227135156175

第二种数据集,我们加一层神经元,并且把第二层的input_dim删掉,因为keras自动汇合输入

import dataset
import numpy as np
import plot_utils
from keras.models import Sequential
from keras.layers import Dense

m = 100
# 获取第二种数据集
X,Y = dataset.get_beans2(m)
plot_utils.show_scatter(X,Y)

# 两层神经元,最后一层不用指定input_dim,keras自动汇合
model = Sequential()
model.add(Dense(units=2, activation='sigmoid', input_dim=1))
model.add(Dense(units=1, activation='sigmoid'))
model.compile(loss='mean_squared_error', optimizer='sgd', metrics=['accuracy'])
model.fit(X, Y, epochs=5000, batch_size=10)
pres = model.predict(X)

plot_utils.show_scatter_curve(X, Y, pres)

image-20250227140419020

我们发现效果不是很好,所以我们需要化身“调参侠”,按照经验,这里是我们的学习率太小了,在使用compile进行配置的时候,我们使用一个字符串告诉模型使用sgd优化器,但是并没有指定学习率,实际上这种没有指定学习率的配置方式是一种默认配置方式,在keras源码中我们可以看到,SGD优化器默认情况下学习率是0.01。我们手动配置一下,这里我们不仅可以使用字符串,也可以使用SGD对象,这个SGD对象创建的时候,我们把学习率lr设置为比如0.05,当然这个SGD在Keras的optimizer中,所以我们需要在使用前导入一下。

image-20250227140717334

修改为下面的代码:

from tensorflow.keras.optimizers import SGD

...
model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.05), metrics=['accuracy'])
...

输出结果可观:

image-20250227141156486

数据三:两个特征维度的输入数据

首先是简单的分类情况,也就是线性可分的情况

import dataset
import numpy as np
import plot_utils
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD


m = 100
# 获取第三种数据集
X,Y = dataset.get_beans(m)
plot_utils.show_scatter(X,Y)

# 两层神经元,最后一层不用指定input_dim,keras自动汇合
model = Sequential()
model.add(Dense(units=1, activation='sigmoid', input_dim=2))# 因为是两个特征维度(大小和颜色),所以改成2
model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.05), metrics=['accuracy'])
model.fit(X, Y, epochs=5000, batch_size=10)
pres = model.predict(X)

plot_utils.show_scatter_surface(X, Y, model)

image-20250227141949373

数据四:输入数据有两个特征维度,而且是一个复杂的弯曲的数据分布

加入隐藏层后能够扭曲三维空间中的面,也能扭曲0.5的等高线的分割线,所以你会发现我们在用keras框架搭建神经网络的时候,主要就是留意一下输入数据的形状,指定好input_dim,然后我们再决定每一层有多少个神经元,指定就units就好了。

import dataset
import plot_utils
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"  # 禁用GPU

m = 100
X, Y = dataset.get_beans4(m)
plot_utils.show_scatter(X, Y)

model = Sequential()
# 当前层神经元的数量为2,激活函数类型:sigmoid,输入数据特征维度:2
model.add(Dense(units=2, activation='sigmoid', input_dim=2))
model.add(Dense(units=1, activation='sigmoid'))
# loss(损失函数、代价函数):mean_squared_error均方误差;
# optimizer(优化器):sgd(随机梯度下降算法);
# metrics(评估标准):accuracy(准确度);
model.compile(loss='mean_squared_error', optimizer=SGD(lr=0.05), metrics=['accuracy'])
# epochs:回合数(全部样本完成一次训练)、batch_size:批数量(一次训练使用多少个样本)
model.fit(X, Y, epochs=5000, batch_size=10)

pres = model.predict(X)

plot_utils.show_scatter_surface(X, Y, model)

image-20250227175400371

九、深度学习

神奇的DeepLearning

原理

1、深度学习就是不断的增加一个神经网络的隐藏层神经元,让输入的数据被这些神经元不断的抽象和理解,最后得到一个具有泛化能力的预测网络模型,而我们一般把隐藏层超过三层的网络也就称之为深度神经网络。网络中每个节点到底在理解什么,很难用精确的数学手段去分析,我们唯一能做的事情就是收集数据送入数据进行训练,然后期待结果。当然也不是说我们对一个深度神经网络完全不可把控,起码我们能在比如学习率、激活函数、神经元的层数和数量等等方面,调节神经网络的大致工作行为,俗称调参

2、下面我们使用谷歌的tensorflow游乐场来体验调参侠的快乐,激活函数选择Sigmoid

我们来进行四类分割

  • 线性可分问题,使用单个神经元就可以完成分类

image-20250228161334996

  • 圆形区域:需要两个输入,三个隐藏层神经元,两个输出神经元。一个隐藏层神经元是一条直线,两个是两条直线,但是遗憾的是,两条直线是没办法围成一个封闭的区域

image-20250227193435000

  • 异或问题:这实际上就是在感知器最初发展阶段困扰研究人员很多年的一个问题。当时为了证明感知器模型并没有太大的卵用,于是人们提出了一种在计算机领域非常常见的问题,异或。运算是这样的,如果两个数一样,结果就是0,比如(0,0)(1,1)如果两个数不一样,结果就是一比如(0,1)(1,0)想要分割出这种分布数据的类型,单个神经元的感知器是无法做到这一点的,如此简单的问题都无法分类,所以证明感知器并没有什么卵用。这曾是感知器发展最初阶段大家普遍持有的悲观态度,彼时人工智能神经网络陷入了持久的低谷期,但现在我们知道实际上同样使用具有包含三个神经元的隐藏层的神经网络,就可以顺利的做出分类。

image-20250227193200365

image-20250227193549159

  • 蚊香螺旋形数据:当使用Sigmoid激活函数和深度神经网络,我们发现并没有什么效果,这是因为激活函数的问题

image-20250227193736297

我们之前说之所以选择Sigmoid函数作为激活函数,那是因为相比于阶跃函数,Sigmoid函数处处可导,而且导数处处不为0,这样在反向传播的时候,我们知道使用梯度下降算法计算函数的导数,然后利用导数去修正参数,但是我们并没有说Sigmoid其实有一个很严重的问题,在Sigmoid的中心位置附近,这没有啥问题,但如果一旦进入了Sigmoid函数,远离中心点的位置,比如下图这里,虽然仍旧可导数仍旧不为0,但是导数却极其的小,这样梯度下降就很难进行。距离中心越远的位置导数越小,也就是所谓的梯度消失的问题(梯度消失问题是指在训练深度神经网络(尤其是具有多隐藏层的网络)时,梯度(通过反向传播计算的权重更新量)在从输出层向输入层传播的过程中逐渐变小,甚至接近于零。这会导致靠近输入层的权重几乎不更新,从而使网络难以学习或收敛,尤其是在训练早期阶段或深层网络中。),越深的神经网络就越容易出现这种梯度消失的问题,所以网络变得很难训练。

image-20250227194807282

所以目前人们普遍不再使用Sigmoid结构函数,而经常使用一种叫做ReLU的激活函数,这是一个分段函数,在线性结果z大于0的时候,输出值为z在z小于0的时候固定为0,这样在z大于0的时候处处可导,而且不会出现梯度消失的情况。

image-20250227195120137

在z等于0的时候,从数学上来看,这个折点确实不可导,但一般很难恰好遇到这个点,真要是遇到了那就特殊处理一下,比如认为这一点的导数是0或者1,或者是个很小的值,比如0.001,当然如果陷入了z小于0的部分,很有可能导致神经元死亡,也就是所谓的Dead ReLU Problem,死亡ReLU问题。因为根据反向传播中的链式求导法则(导数连乘,有一个是0,整个结果都是0),如果激活函数的导数是0,那么就锁定了神经元上的参数梯度为0,权重无法更新。

image-20250227195357347

所以人们又提出了一种改进版的ReLU函数,leaky-ReLU,在z小于0的时候输出不再是0,而是一个缓缓倾斜的直线,如此就可以避免死亡的问题。

image-20250227195455323

但有趣的是当神经网络比较复杂隐藏层比较深的时候,人们通过实践得到的经验往往是直接使用ReLU激活函数,往往也有很好的效果。关于梯度消失的问题,激活函数只是一个方面。当然关于梯度消失的问题,激活函数只是一个方面,还有很多因素会导致这个现象,目前也有很独锗关的研究,但按照经验大多数时候使用瑞露函数都会有不错的效果,所以现在最为流行的激活函数还是ReLU

image-20250227194659441

当然在最后的输出层,因为Sigmoid的上下限刚好在0~1之间很适合做分类,所以输出层的激活函数还是选择Sigmoid。

到此我们从为了分类而引入Sigmoid激活函数,然后发现激活函数的作用不仅是为了分类,而且给神经网络注入了灵魂,让它成为了能够预测复杂问题的非线性系统,而到现在我们又发现Sigmoid似乎又指适合用来做分类而已,当然我们已经有了其他更好的激活函数,给神经网络注入了更有趣的灵魂,兜兜转转之间Sigmoid的函数也算完成了它的使命。

而现在对于螺旋形的数据集经过一番训练之后,似乎也有一定的效果,但并不十分的好。我们就给隐藏层增加更多的神经元,每1层都改成8个,如下图结果。终于我们在螺旋形分布的数据集上取得了非常不错的分类结果,不过网络中,每个神经元上的参数都被大大小小的调节成为了不同的数值,每个神经元也产生了对输入不同的中间抽象和理解,到此我们似乎已经没有什么精确的数学手段去分析每个神经元到底是在抽象什么,在理解什么了。所以,欢迎开始炼丹(编程实验)。

image-20250228163544253

编程实验

1、本次实验我们使用keras框架搭建一个深度神经网络,拟合一下下面这个长得像蚊香一样的螺旋形数据集

image-20250228163914909image-20250228163956165

继续使用第八节的代码,将数据集开成这次的螺旋形数据集,我们会发现如果继续使用Sigmoid函数作为激活函数,那么训练出来的效果是这样的

# dataset.py
import numpy as np
import random

def get_beans(counts):
	posX,posY = genSpiral(int(counts/2),0,1)
	negX,negY = genSpiral(int(counts/2),np.pi,0)
	X = np.vstack((posX,negX))
	Y = np.hstack((posY,negY))
	return X,Y

def genSpiral(counts,deltaT, label):
	X = np.zeros((counts,2))
	Y = np.zeros(counts)
	for i in range(counts):
		r = i / counts * 5
		t = 1.75 * i / counts * 2 * np.pi + deltaT;
		x1 = r * np.sin(t) + random.uniform(-0.1,0.1)
		x2 = r * np.cos(t) + random.uniform(-0.1,0.1)
		X[i] = np.array([x1,x2])
		Y[i] = label
	return X,Y 

def dist(a, b):
	dx = a['x'] - b['x'];
	dy = a['y']- b['y'];
	return np.sqrt(dx * dx + dy * dy);
def getCircleLabel(p, center):
	radius = 1;
	if dist(p, center) < (radius * 0.5):
		return 1
	else:
		return 0

def randUniform(a=-1, b=1):
  return np.random.rand() * (b - a) + a;

def classifyCircleData(numSamples=100, noise=0):
	points = [];
	Y = []
	X = []
	radius = 1;
	num = int(numSamples/2)
	for i in range(num):
		r = randUniform(0, radius * 0.5);
		angle = randUniform(0, 2 * np.pi);
		x = r * np.sin(angle);
		y = r * np.cos(angle);
		noiseX = randUniform(-radius, radius) * noise;
		noiseY = randUniform(-radius, radius) * noise;
		label = getCircleLabel({'x': x + noiseX, 'y': y + noiseY}, {'x': 0, 'y': 0});
		X.append([x+1,y+1])
		Y.append(label)
  

	for i in range(num):
		r = randUniform(radius * 0.7, radius);
		angle = randUniform(0, 2 * np.pi);
		x = r * np.sin(angle);
		y = r * np.cos(angle);
		noiseX = randUniform(-radius, radius) * noise;
		noiseY = randUniform(-radius, radius) * noise;
		label = getCircleLabel({'x': x + noiseX, 'y': y + noiseY}, {'x': 0, 'y': 0});
		X.append([x+1,y+1])
		Y.append(label)

	X = np.array(X)
	Y = np.array(Y)

	return X,Y
# plot_utils.py
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from keras.models import Sequential#导入keras


def show_scatter_curve(X,Y,pres):
    plt.scatter(X, Y) 
    plt.plot(X, pres) 
    plt.show()

def show_scatter(X,Y):
    if X.ndim>1:
       show_3d_scatter(X,Y)
    else:
       plt.scatter(X, Y) 
       plt.show()


def show_3d_scatter(X,Y):
    x = X[:,0]
    z = X[:,1]
    fig = plt.figure()
    ax = Axes3D(fig)
    ax.scatter(x, z, Y)
    plt.show()

def show_surface(x,z,forward_propgation):
    x = np.arange(np.min(x),np.max(x),0.1)
    z = np.arange(np.min(z),np.max(z),0.1)
    x,z = np.meshgrid(x,z)
    y = forward_propgation(X)
    fig = plt.figure()
    ax = Axes3D(fig)
    ax.plot_surface(x, z, y, cmap='rainbow')
    plt.show()



def show_scatter_surface(X,Y,forward_propgation):
    if type(forward_propgation) == Sequential:
       show_scatter_surface_with_model(X,Y,forward_propgation)
       return
    x = X[:,0]
    z = X[:,1]
    y = Y

    fig = plt.figure()
    ax = Axes3D(fig)
    ax.scatter(x, z, y)

    x = np.arange(np.min(x),np.max(x),0.1)
    z = np.arange(np.min(z),np.max(z),0.1)
    x,z = np.meshgrid(x,z)

    X = np.column_stack((x[0],z[0]))
    for j in range(z.shape[0]):
       if j == 0:
          continue
       X = np.vstack((X,np.column_stack((x[0],z[j]))))

    print(X.shape)
    r = forward_propgation(X)
    y = r[0]
    if type(r) == np.ndarray:
       y = r

    
    y = np.array([y])
    y = y.reshape(x.shape[0],z.shape[1])
    ax.plot_surface(x, z, y, cmap='rainbow')
    plt.show()

def show_scatter_surface_with_model(X,Y,model):
    #model.predict(X)

    x = X[:,0]
    z = X[:,1]
    y = Y

    fig = plt.figure()
    ax = Axes3D(fig)
    ax.scatter(x, z, y)

    x = np.arange(np.min(x),np.max(x),0.1)
    z = np.arange(np.min(z),np.max(z),0.1)
    x,z = np.meshgrid(x,z)



    X = np.column_stack((x[0],z[0]))

    for j in range(z.shape[0]):
       if j == 0:
          continue
       X = np.vstack((X,np.column_stack((x[0],z[j]))))

    print(X.shape)
    y = model.predict(X)
    
    # return
    # y = model.predcit(X)
    y = np.array([y])
    y = y.reshape(x.shape[0],z.shape[1])
    ax.plot_surface(x, z, y, cmap='rainbow')
    plt.show()

def pre(X,Y,model):
    model.predict(X)
# beans_predict.py
import dataset
import plot_utils
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"  # Disable GPU

m = 100
X, Y = dataset.get_beans(m)
plot_utils.show_scatter(X, Y)

model = Sequential()
# 当前层神经元的数量为2,激活函数类型:sigmoid,输入数据特征维度:2
model.add(Dense(units=2, activation='sigmoid', input_dim=2))
model.add(Dense(units=1, activation='sigmoid'))
# loss(损失函数、代价函数):mean_squared_error均方误差;
# optimizer(优化器):sgd(随机梯度下降算法);
# metrics(评估标准):accuracy(准确度);
model.compile(loss='mean_squared_error', optimizer=SGD(lr=0.05), metrics=['accuracy'])
# epochs:回合数(全部样本完成一次训练)、batch_size:批数量(一次训练使用多少个样本)
model.fit(X, Y, epochs=5000, batch_size=10)

pres = model.predict(X)

plot_utils.show_scatter_surface(X, Y, model)

image-20250228164459697

keras有一个函数叫做get_weights()函数,这个函数可以得到模型的各个参数,我们在训练完成之后把他打印出来

image-20250228165304267

我们知道输入X是一个1×2的数组,那么输入数据x和隐藏层之间的权重参数矩阵,W1也就是一个2×2的数组,我们用X点乘W1的转置W^T当然W1的转置也就是一个2×2的数组,也就是我们这里第一个array中的2×2的数组。

image-20250228165646509

X点乘W1的转置之后得到一个1×2的数组,第一个array中的1×2数组是偏置数组B1,那么利用numpy数组的广播机制,让X点乘W转置的结果加上偏置数组B1,最后也就得到一个1×2的数组[w11_1X1+W21_1X2, W12_1X1+W22_1X2],这也就是隐藏层的两个神经元的线性输出。

image-20250228170039946

image-20250228165934052

当然这个线性输出还需要经过激活函数做非线性运算,最后得到最终的输出数组[a1_1, a2_1]。

image-20250228170345092

隐藏层的输出作为最后一层的输入,那么和输出层的神经元之间就有两个权重参数,也就是说是一个1×2的权重参数矩阵W2,当然还是要转置一下变成2×1,这也就是我们这里的第二个array中的2×1的数组。

image-20250228170518735

隐藏层1×2的输出点成2×1的权重系数矩阵之后,就变成一个1×1的数组,同样需要加上偏置项B2,也就是我们这里第二个array中的1×1的数组,最后的结果就是一个1×1的数组,再经过激活函数进行非线性运算,这也就是神经网络最后的运算测结果[a1_2]。

image-20250228170647500

image-20250228170936891

所以通过get_weights()函数,我们也就能够确定用Keras搭建的神经网络底层逻辑,其实就是我们之前说的那些**用线性函数组合,用激活函数输出,用代价函数评估,用梯度下降化,**只不过Keras采用向量矩阵以及张量的方式进行运算,并帮我们封装得很好。好到如果我们不注意几乎看不到这些东西的存在,而不管怎样,我们终于可以放心的使用Keras来搭建神经网络模型了。

image-20250228172647567

2、分析完上节的代码之后,我们回到蚊香数据集上面,按照之前在tensorflow游乐场的实验,我们使用了三层隐藏层,每一层使用8个神经元,所以我们把每一层的神经元添加到8个,使用三层隐藏层。而且除了输出层的激活函数,还是使用Sigmoid做分类以外,隐藏层的激活函数我们都采用Relu比较合适,代码如下:

# beans_predict.py
import dataset
import plot_utils
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"  # Disable GPU

m = 100
X, Y = dataset.get_beans(m)
plot_utils.show_scatter(X, Y)

model = Sequential()
# 当前层神经元的数量为2,激活函数类型:sigmoid,输入数据特征维度:2
model.add(Dense(units=8, activation='relu', input_dim=2))
model.add(Dense(units=8, activation='relu', input_dim=2))
model.add(Dense(units=8, activation='relu', input_dim=2))
model.add(Dense(units=1, activation='sigmoid'))
# loss(损失函数、代价函数):mean_squared_error均方误差;
# optimizer(优化器):sgd(随机梯度下降算法);
# metrics(评估标准):accuracy(准确度);
model.compile(loss='mean_squared_error', optimizer=SGD(lr=0.05), metrics=['accuracy'])
# epochs:回合数(全部样本完成一次训练)、batch_size:批数量(一次训练使用多少个样本)
model.fit(X, Y, epochs=5000, batch_size=10)

pres = model.predict(X)

plot_utils.show_scatter_surface(X, Y, model)

print(model.get_weights())

结果如下:
image-20250301194405963
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值