上一节学习了感知机,对于感知机来说,即便对于复杂的函数,感知机也隐含着能够表示它的可能性。可以将计算机进行的复杂处理表示出来。但设定权重的工作,即确定合适的、能符合预期的输入与输出的权重,现在还是由人工进行的,上一节结合与门、或门的真值表人工决定了合适的权重。
上图中的感知机接收x1和x2两个输入信号,输出y。如果用数学式来表示图中的感知机,则如下式所示:
y = { 0 ( w 1 x 1 + w 2 x 2 ≤ θ ) 1 ( w 1 x 1 + w 2 x 2 > θ ) y=\begin{cases} 0 \text{ } (w_{1}x_{1}+w_{2}x_{2}\leθ) \\ 1 \text{ } (w_{1}x_{1}+w_{2}x_{2}>θ) \end{cases} y={ 0 (w1x1+w2x2≤θ)1 (w1x1+w2x2>θ)
上节也提到,若用-b代替θ,则公式表示如下:
y = { 0 ( w 1 x 1 + w 2 x 2 + b ≤ 0 ) 1 ( w 1 x 1 + w 2 x 2 + b > 0 ) y=\begin{cases} 0 \text{ } (w_{1}x_{1}+w_{2}x_{2}+b\le0) \\ 1 \text{ } (w_{1}x_{1}+w_{2}x_{2}+b>0) \end{cases} y={ 0 (w1x1+w2x2+b≤0)1 (w1x1+w2x2+b>0)
相对应的,示意图如下:
这个感知机将x1、x2、1 三个信号作为神经元的输入,将其和各自的权重相乘后,传送至下一个神经元。在下一个神经元中,计算这些加权信号的总和。如果这个总和超过0,则输出1,否则输出0。
现在可以考虑将上式改写成更加简洁的形式。为了简化上式,我们用一个函数来表示这种分情况的动作(超过0则输出1,否则输出0)。引入新函数h(x),将上式进行改写:
y = h ( w 1 x 1 + w 2 x 2 + b ) h ( x ) = { 0 x ≤ 0 1 x > 0 y=h(w_{1}x_{1}+w_{2}x_{2}+b) \\ h(x)=\begin{cases} 0 \text{ } x\le0\\ 1 \text{ } x>0 \end{cases} y=h(w1x1+w2x2+b)h(x)={ 0 x≤01 x>0
刚才登场的h(x)函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数(activation function)
。如“激活”一词所示,激活函数的作用在于决定如何来激活输入信号的总和。
现在来进一步改写:先计算输入信号的加权总和,然后用激活函数转换这一总和。因此,可以分成下面两个式子:
a = b + w 1 x 1 + w 2 x 2 y = h ( a ) a = b + w_{1}x_{1} + w_{2}x_{2}\\ y = h(a) a=b+w1x1+w2x2y=h(a)
首先,计算加权输入信号和偏置的总和,记为a。然后,用h()函数将a转换为输出y。
之前的神经元都是用一个○表示的,如果要在图中明确表示出式上式,则可以像下图这样做,表示神经元的○中明确显示了激活函数的计算过程:
上式表示的激活函数以阈值为界,一旦输入超过阈值,就切换输出。
这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为激活函数。也就是说,在激活函数的众多候选函数中,感知机使用了阶跃函数。
那么,如果感知机使用其他函数作为激活函数的话会怎么样呢?实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。下面我们就来介绍一下神经网络使用的激活函数。
神经网络中经常使用的一个激活函数就是下式表示的sigmoid 函数 (sigmoid function)
:
h ( x ) = 1 1 + e − x h(x)=\frac{1}{1+e^{-x}} h(x)=1+e−x1
式中 e是纳皮尔常数2.7182 . . .神经网络中用sigmoid 函数作为激活函数,进行信号的转换,转换后的信号被传送给下一个神经元。
实际上,上一章介绍的感知机和接下来要介绍的神经网络的主要区别就在于这个激活函数。其他方面,比如神经元的多层连接的构造、信号的传递方法等,基本上和感知机是一样的。
下面,我们编程,来直观感受激活函数与sigmoid 函数的区别。
# 普通版
def step_function(x):
if x > 0:
return 1
else:
return 0
# NumPy版
def step_function(x):
y = x > 0
return y.astype(np.int)
对NumPy数组进行不等号运算后,数组的各个元素都会进行不等号运算,生成一个布尔型数组。这里,数组x中大于0 的元素被转换为True,小于等于0 的元素被转换为False,从而生成一个新的数组y。
数组y是一个布尔型数组,但是我们想要的阶跃函数是会输出int型的0或1 的函数。因此,需要把数组y的元素类型从布尔型转换为int型。可以用astype()方法转换NumPy数组的类型,将布尔型转换为int型后,True会转换为1,False会转换为0。
def sigmoid(x):
# np.exp(-x)对应e的x次方
return 1 / (1 + np.exp(-x))
首先对上述两个函数进行可视化:
import numpy as np
import matplotlib.pylab as plt
def step_function(x):
y = x > 0
return y.astype(np.int)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y, label="step_function")
y = sigmoid(x)
plt.plot(x, y, label="sigmoid_function")
plt.legend() # 在图中标出图线含义
plt.ylim(-0.1, 1.1) # 指定y轴的范围
plt.show()
运行结果如下图所示:
观察图像,首先注意到的是“平滑性”的不同。sigmoid 函数是一条平滑的曲线,输出随着输入发生连续性的变化。而阶跃函数以0为界,输出发生急剧性的变化。sigmoid 函数的平滑性对神经网络的学习具有重要意义。
另一个不同点是,相对于阶跃函数只能返回0或1,sigmoid 函数可以返回0.731 . . .、0.880 . . . 等实数,这一点和刚才的平滑性有关。也就是说,感知机中神经元之间流动的是0 或1 的二元信号,而神经网络中流动的是连续的实数值信号。
阶跃函数和sigmoid函数虽然在平滑性上有差异,但是如果从宏观视角来看,可以发现它们具有相似的形状。实际上,两者的结构均是“输入小时,输出接近0(为0);随着输入增大,输出向1 靠近(变成1)”。也就是说,当输入信号为重要信息时,阶跃函数和sigmoid函数都会输出较大的值;当输入信号为不重要的信息时,两者都输出较小的值。
还有一个共同点是,不管输入信号有多小,或者有多大,输出信号的值都在0到1之间。
在神经网络发展的历史上,sigmoid 函数很早就开始被使用了,而最近则主要使用ReLU(Rectified Linear Unit)函数。ReLU函数在输入大于0 时,直接输出该值;在输入小于等于0 时,输出0:
h ( x ) = { x ( x > 0 ) 0 ( x ≤ 0 ) h(x)=\begin{cases} x \text{ } (x>0)\\ 0 \text{ } (x\le0) \end{cases} h(x)={ x (x>0)0 (x≤0)
ReLU 函数是一个非常简单的函数。因此,ReLU函数的实现也很简单,可以写成如下形式:
def relu(x):
return np.maximum(0, x)
这里使用了NumPy的maximum函数。maximum函数会从输入的数值中选择较大的那个值进行输出。
图像如下所示:
这里使用了NumPy的maximum函数。maximum函数会从输入的数值中选择较大的那个值进行输出。所以,ReLU将大于0的值按原样输出,小于等于0的值转变为0输出。
阶跃函数和sigmoid函数以及ReLU函数有一个共同点,就是三者均为非线性函数。
神经网络的激活函数必须使用非线性函数。换句话说,激活函数不能使用线性函数。为什么不能使用线性函数呢?因为使用线性函数的话,加深神经网络的层数就没有意义了。
线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐藏层的神经网络”。为了具体地(稍微直观地)理解这一点,我们来思考下面这个简单的例子:
这里我们考虑把线性函数 h(x) = cx 作为激活函数,把 y(x) = h(h(h(x))) 的运算对应3层神经网络A。这个运算会进行 y(x) = c × c × c × x 的乘法运算,但是同样的处理可以由 y(x) = ax(注意,a = c3)这一次乘法运算(即没有隐藏层的神经网络)来表示。如本例所示,使用线性函数时,无法发挥多层网络带来的优势。因此,为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题用恒等函数
,分类问题用softmax
函数。
机器学习的问题大致可以分为分类问题和回归问题。
分类问题是数据属于哪一个类别的问题。比如,区分图像中的人是男性还是女性的问题就是分类问题。
回归问题是根据某个输入预测一个(连续的)数值的问题。比如,根据一个人的图像预测这个人的体重的问题就是回归问题(类似“57.4kg”这样的预测)。
恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直接输出。因此,在输出层使用恒等函数时,输入信号会原封不动地被输出,如下图:
和前面介绍的隐藏层的激活函数一样,恒等函数进行的转换处理可以用一根箭头来表示。
分类问题中使用的softmax函数可以用下面的式表示:
y k = e a k ∑ i = 1 n e a i y_{k}=\frac{e^{a_{k}}}{\sum_{i=1}^{n} e^{a_{i}}} yk=∑i=1neaieak
上式表示假设输出层共有n个神经元,计算第k个神经元的输出yk。softmax 函数的分子是输入信号ak的指数函数,分母是所有输入信号的指数函数的和。
用图表示softmax函数的话,如下图所示:
上图中,softmax函数的输出通过箭头与所有的输入信号相连。这是因为输出层的各个神经元都受到所有输入信号的影响。
现在我们来实现softmax函数。在这个过程中,我们将使用Python逐一确认结果。
a = np.array([0.3, 2.9, 4.0])
exp_a = np.exp(a) # 指数函数
print(exp_a)
sum_exp_a = np.sum(exp_a) # 指数函数的和
print(sum_exp_a)
y = exp_a / sum_exp_a
print(y)
# 运行结果
[1.34985881 18.17414537 54.59815003]
74.1221542102
[0.01821127 0.24519181 0.73659691]
考虑到后面还要使用softmax函数,这里我们把它定义成如下的Python函数:
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
计算机处理数时,数值必须在4字节或8字节等有限数据宽度内。这意味着数存在有效位数,也就是说,可以表示的数值范围是有限的。因此,会出现超大值无法表示的问题。这个问题称为溢出,在进行计算机的运算时必须注意。
上面函数的实现虽然正确描述了softmax函数,但在计算机的运算上有一定的缺陷。这个缺陷就是溢出问题。softmax函数的实现中要进行指数函数的运算,但是此时指数函数的值很容易变得非常大。比如,e10的值会超过20000,e100会变成一个后面有40多个0 的超大值,e1000的结果会返回一个表示无穷大的inf。如果在这些超大值之间进行除法运算,结果会出现不确定的情况。
softmax 函数的实现可以像下式这样进行改进:
y k = e a k ∑ i = 1 n e a i = C e a k C ∑ i = 1 n e a i = e a k + l o g C ∑ i = 1 n e a i + l o g C = e a k + C ′ ∑ i = 1 n e a i + C ′ y_{k}=\frac{e^{a_{k}}}{\sum_{i=1}^{n} e^{a_{i}}} =\frac{Ce^{a_{k}}}{C\sum_{i=1}^{n} e^{a_{i}}}\\ =\frac{e^{a_{k}+logC}}{\sum_{i=1}^{n} e^{a_{i}+logC}} =\frac{e^{a_{k}+C^{'}}}{\sum_{i=1}^{n} e^{a_{i}+C^{'}}} yk=∑i=1neaieak=C∑i=1neaiCeak=∑i=1neai+logCeak+logC=∑i=1neai+C′eak+C′
首先,在分子和分母上都乘上C这个任意的常数(因为同时对分母和分子乘以相同的常数,所以计算结果不变)。然后,把这个C移动到指数函数中,记为logC。最后,把logC替换为另一个符号C’。
上式说明,在进行softmax的指数函数的运算时,加上(或者减去)某个常数并不会改变运算的结果。这里的C’可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值。我们来看一个具体的例子:
>>> a = np.array([1010, 1000, 990])
>>> np.exp(a) / np.sum(np.exp(a)) # softmax函数的运算
array([ nan, nan, nan]) # 没有被正确计算
>>>
>>> c = np.max(a) # 1010
>>> a - c
array([ 0, -10, -20])
>>>
>>> np.exp(a - c) / np.sum(np.exp(a - c))
array([ 9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
如上例所示,通过减去输入信号中的最大值(上例中的C),我们发现原本为nan(not a number,不确定)的地方,现在被正确计算了。综上,我们可以像下面这样实现softmax函数:
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
使用softmax()函数,可以按如下方式计算神经网络的输出:
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
print(y)
print(np.sum(y))
# 运行结果
[0.01821127 0.24519181 0.73659691]
1.0
如上所示,softmax函数的输出是0.0 到1.0之间的实数。并且,softmax函数的输出值的总和是1。输出总和为1 是softmax 函数的一个重要性质。正因为有了这个性质,我们才可以把softmax 函数的输出解释为概率
。
比如,上面的例子可以解释成y[0]的概率是0.018(1.8%),y[1]的概率是0.245(24.5%),y[2]的概率是0.737(73.7%)。从概率的结果来看,可以说“因为第2 个元素的概率最高,所以答案是第2 个类别”。而且,还可以回答“有74%的概率是第2个类别,有25%的概率是第1个类别,有1%的概率是第0个类别”。也就是说,通过使用softmax函数,我们可以用概率统计的方法处理问题。
这里需要注意的是,即便使用了softmax 函数,各个元素之间的大小关系也不会改变。这是因为指数函数(y = exp(x))是单调递增函数。实际上,上例中a的各元素的大小关系和y的各元素的大小关系并没有改变。比如,a的最大值是第2个元素,y的最大值也仍是第2个元素。
一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。并且,即便使用softmax函数,输出值最大的神经元的位置也不会变。因此,神经网络在进行分类时,输出层的softmax函数可以省略。在实际的问题中,由于指数函数的运算需要一定的计算机运算量,因此输出层的softmax函数一般会被省略。
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测是图中的数字0到9中的哪一个的问题(10 类别分类问题),可以像下图这样,将输出层的神经元设定为10个:
在这个例子中,输出层的神经元从上往下依次对应数字0, 1, . . ., 9。此外,图中输出层的神经元的值用不同的灰度表示。这个例子中神经元y2颜色最深,输出的值最大。这表明这个神经网络预测的是y2对应的类别,也就是“2”。
求解机器学习问题的步骤可以分为学习和推理两个阶段。首先,在学习阶段进行模型的学习,然后,在推理阶段,用学到的模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省略输出层的softmax函数。在输出层使用softmax函数是因为它和神经网络的学习有关系。