关于ReLU的问题&思考

今天看完🐟书后回头捋了一下,突然对ReLU激活函数产生了疑问。

引例

试想一下有X1,X2两个输入,对应三个输出的简易网络:

eg net

根据对应把权值W设为W11,W12,W13…W23
显然通过矩阵运算:

Y1=W11·X1+W21·X2
Y2=W12·X1+W22·X2
Y3=W13·X1+W23·X2

所得到的表达式依旧是X的线性表达式
之后通过ReLU层(这里假设都>0)
因为Y1,Y2,Y3>0,故输出不变:

Y1=W11·X1+W21·X2
Y2=W12·X1+W22·X2
Y3=W13·X1+W23·X2

问题提出

我们知道引入激活函数的目的是为了使层神经网络表达能力就更加强大(不再是输入的线性组合,而是几乎可以逼近任意函数,详见上篇Blog),而通过小实验发现,ReLU好像并没有改变原式的线性结构,反而是原式输出。那么是否有悖机器学习的原理呢?

问题解决

首先肯定一点:上面的实验确实反映了ReLU没有改变实验对象的线性结构。

下面细说:

首先观察实验,实验的基础是Y1,Y2,Y3>0这个条件,在这个条件下显然是等价于恒等函数的。也就是说Y被原样加工。因为原来的表达式是线性结构,所以激活之后依旧是线性结构。但是,这并不能说明ReLU对增强神经网络的表现力无用。

事实是,参与训练的数据首先不止2个(参考MNIST数据集,训练输入就6万个),其次对权值w的初始化是按标准差为0.01的高斯分布来进行的,这就肯定输入ReLU层的数据不恒正
高斯分布
而小于0的则被赋值0,这意味着什么呢?线性结构被破坏!

矛盾推理法来想一下这个问题(假设ReLU不改变线性结构):

  • 那么应改满足F(X)=M·X(矩阵乘法)——>ReLU ——>M·X——>G(F(X))=N·M·X——>ReLU ——>N·M·X…
  • 可事实是:权值的正态性不保证每次输入都是正值,所以中间会出现等于0的情况
  • 也就是F(X)=M·X(矩阵乘法)——>ReLU ——>0——>G(0)!=N·M·X——>…
  • 显然线性传递被中断
  • 或者说形成了一个新的线性空间,新空间与恒等线性空间互相无法表示,使整个空间呈非线性结构

🐟书踩坑(一)

废话不多说,俺先讲一下来龙去脉~

今天看鱼书P108页,遇到一个想不通的地方,我先放一波源码:
(不了解的小伙伴看上一篇博文,有源码连接)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
def __init__(self):
self.W = np.random.randn(2,3)

def predict(self, x):
return np.dot(x, self.W)

def loss(self, x, t):
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)

return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)

没错,这个是一个求神经网络梯度 w 的简单脚本。

如你所见,文件头也import了许多外部函数,下面贴其中一个比较重要的外部函数:

numerical_gradient函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# coding: utf-8
import numpy as np

def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)

it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)

x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)

x[idx] = tmp_val # 还原值
it.iternext()

return grad

下面讲讲这个坑

小伙子们注意看第一段代码28,29两行:

1
2
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

函数f中lambda表达式没什么问题,注意里面的参数 w ,仔细看,你会发现……卧槽!这不没jb卵用吗…
如果你这么想,那你跟年轻的我有的一拼,too young,too simple

接着看下一行:dW = numerical_gradient(f, net.W)很显然调用了numerical_gradient函数,没毛病。但是,兄弟萌,请仔细看看numerical_gradient函数实现机制,你会发现:

第13行: fxh1 = f(x)
第16行: fxh2 = f(x)
??????????

看不出问题吗?
注意:这里的f是:

1
f = lambda w: net.loss(x, t)

w是伪参数,没卵用,而x是net.W——神经网络的权重组成的数组,这尼玛驴头不对马嘴怎么就传给f了???而且f也不需要参数啊!

我是左思右想一下午,后来有了一个猜想来解释这段代码。

猜想

我猜想fxh1 = f(x) ,fxh2 = f(x)括号里的x对程序根本没卵用,于是我大胆的删掉了x,变成:

第13行: fxh1 = f()
第16行: fxh2 = f()

当然为了保持兼容,也得把lambda表达式那个伪参数删了:

f = lambda : net.loss(x, t)

然后编译运行,结果…tmd就对了,你肯定觉得这是个坑,没错,我开始也觉得是坑,坑死我了。
后来浏览了后面的代码,发现了一些用numerical_gradient函数做测试脚本,其中测试的是一些简单函数的梯度问题,涉及到变量x所以numerical_gradient函数的x要保留。。。。。。相应的,神经网络求梯度就要顺着numerical_gradient函数,没必要再写一个,于是就加了伪参数w保证兼容。

最后俺深刻的明白:实践是检验真理的唯一标准(早试试就不会浪费那么多时间了)

END


关于激活函数的理解

什么是激活函数:

下午学完了神经网络误差的反向传播,心满意足看着满屏幕刚撸出来的Affine,Sigmoid,ReLU,Softmax……突然,心里发出一声疑问:激活函数有什么用???我慌了,我说不上来,但是隐隐约约能知道这玩意是干嘛的,本着一贯刨根问底跑个稀烂的作风,我决定彻底搞清楚!

引例

首先回到机器学习最初始的那个地方—–逻辑门(感知机表示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#与门感知机

def AND(x1,x2):
w1,w2,theta=0.5,0.5,0.7
tmp=x1*w1+x2*w2
if tmp<=theta:
return 0
elif tmp>theta:
return 1

#Test
AND(0,0) #输出0
AND(1,0) #输出0
AND(0,1) #输出0
AND(1,1) #输出1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#或门感知机

def OR(x1,x2):
w1,w2,theta=0.5,0.5,0.2
tmp=x1*w1+x2*w2
if tmp<=theta:
return 0
elif tmp>theta:
return 1

#Test
OR(0,0) #输出0
OR(1,0) #输出1
OR(0,1) #输出1
OR(1,1) #输出1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#与非门感知机

def NAND(x1,x2):
w1,w2,theta=0.5,0.5,0.7
tmp=x1*w1+x2*w2
if tmp<=theta:
return 1
elif tmp>theta:
return 0

#Test
XOR(0,0) #输出1
XOR(1,0) #输出1
XOR(0,1) #输出1
XOR(1,1) #输出0

毫无技术含量,所以这里就不细讲代码。

接下来看看如何表示异或门:

1
2
3
4
5
6
7
8
9
10
def XOR(x1,x2):
s1=NAND(x1,x2)
s2=OR(x1,x2)
y=AND(s1,s2)
return y

XOR(0,0) #输出0
XOR(1,0) #输出1
XOR(0,1) #输出1
XOR(1,1) #输出0

显然异或门的实现借助了另外三个门!
下面从几何角度上看看异或门:
异或门区域
显然:异或门划分的空间是非线性的!而另外三个门划分都是线性的!

现在再回头思考,发现通过对简单门的叠加,实现了非线性划分空间的复杂门!

回到神经网络中

神经网络中,我们知道,隐藏层中主要是Affine层和各种激活函数层:
net
现在假设把激活函数都删掉,也就是说经过一层仿射变换后不加处理继续下一层仿射变换。

比如:x->w·x=y->w’·y=z…(矩阵点乘)

这样会有什么问题呢?很显然。

注意: x->w·x=y->w’·y=z就相当于z=cx,其中c=w·w’

这说明两层仿射变换后的表达式结构还是线性的!!同理n层之后还是线性!这简直是无用功!要记住,神经网络那么多层的目的是尽可能学习如何逼近数据,如果数据是非线性结构排布的,那叠加再多也毫无卵用!
所以现在再来看看这个问题:为什么需要激活函数?

总结一下:

如果不用激励函数(其实相当于激励函数是f(x) = x),在这种情况下你每一层节点的输入都是上层输出的线性函数,很容易验证,无论你神经网络有多少层,输出都是输入的线性组合,与没有隐藏层效果相当,这种情况就是最原始的感知机(Perceptron)了,那么网络的逼近能力就相当有限。正因为上面的原因,我们决定引入非线性函数作为激励函数,这样深层神经网络表达能力就更加强大(不再是输入的线性组合,而是几乎可以逼近任意函数