神经网络是多个“神经元”(感知机)的带权级联,神经网络算法可以提供非线性的复杂模型,它有两个参数:权值矩阵Wl和偏置向量bl,不同于感知机的单一向量形式,Wl是复数个矩阵,bl是复数个向量,其中的元素分别属于单个层,而每个层的组成单元,就是神经元。
神经网络是由多个“神经元”(感知机)组成的,每个神经元图示如下:
这其实就是一个单层感知机,其输入是由x1,x2,x3和+1组成的向量,其输出为hW,b(x)=f(WTx)=f(∑3i=1Wixi+b),其中f是一个激活函数,模拟的是生物神经元在接受一定的刺激之后产生兴奋信号,否则刺激不够的话,神经元保持抑制状态这种现象。这种由一个阈值决定两个极端的函数有点像示性函数,然而这里采用的是Sigmoid函数,其优点是连续可导。
常用的Sigmoid有两种——
其图像如下
或者写成
f(x)=1−e−x1+e−x把第一个式子分子分母同时除以ex,令x=−2x就得到第二个式子了,换汤不换药。
其图像如下
从它们两个的值域来看,两者名称里的极性应该指的是正负号。从导数来看,它们的导数都非常便于计算:
对于f(x)=11+e−x
11+e−x求导的过程:
ddxσ=ddx(11+e−x)=e−x(1+e−x)2=(1+e−x)−1(1+e−x)2=(1+e−x)(1+e−x)2−1(1+e−x)2=σ(x)−σ(x)2σ′=σ(1−σ)
输入层+输出层
def sigmoid(x):
"""
sigmoid 函数,1/(1+e^-x)
:param x:
:return:
"""
return 1.0/(1.0+math.exp(-x))
def dsigmoid(y):
"""
sigmoid 函数的导数
:param y:
:return:
"""
return y * (1 - y)
也可以使用双曲正切函数tanh
def sigmoid(x):
"""
sigmoid 函数,tanh
:param x:
:return:
"""
return math.tanh(x)
def dsigmoid(y):
"""
sigmoid 函数的导数
:param y:
:return:
"""
return 1.0 - y ** 2
神经网络就是多个神经元的级联,上一级神经元的输出是下一级神经元的输入,而且信号在两级的两个神经元之间传播的时候需要乘上这两个神经元对应的权值。例如,下图就是一个简单的神经网络:
其中,一共有一个输入层,一个隐藏层和一个输出层。输入层有3个输入节点,标注为+1的那个节点是偏置节点,偏置节点不接受输入,输出总是+1。
这里的数学推导只需关注输入层到隐藏层
定义上标为层的标号,下标为节点的标号,则本神经网络模型的参数是:(W,b)=(W(1),b(1),W(2),b(2)),其中W(l)ij是第l层的第j个节点与第l+1层第i个节点之间的连接参数(或称权值);b(l)表示第l层第i个偏置节点。这些符号在接下来的前向传播将要用到。
如果后向传播对应训练的话,那么前向传播就对应预测(分类),并且训练的时候计算误差也要用到预测的输出值来计算误差。
定义a(l)i为第l层第i个节点的激活值(输出值)。当l=1时,a(1)i=xi。前向传播的目的就是在给定模型参数W,b的情况下,计算l=2,3,4...层的输出值,直到最后一层就得到最终的输出值。具体怎么算呢,以上图的神经网络模型为例:
a(2)1=f(W(1)11x1+W(1)12x2+W(1)13x3+b(1)1)a(2)2=f(W(1)21x1+W(1)22x2+W(1)23x3+b(1)2)a(2)3=f(W(1)31x1+W(1)32x2+W(1)33x3+b(1)3)hW,b(x)=f(a(3)1=f(W(2)11a(2)1+W(2)12a(2)2+W(2)13a(2)3+b(2)1)这没什么稀奇的,核心思想是这一层的输出乘上相应的权值加上偏置量代入激活函数等于下一层的输入,一句大白话,所谓中文伪码。
另外,追求好看的话可以把括号里面那个老长老长的加权和定义为一个参数:z(l)i表示第l层第i个节点的输入加权和,比如z(2)i=∑nj=1W(1)ijxj+b(1)i。那么该节点的输出可以写作a(l)i=f(z(l)i)。
于是就得到一个好看的形式:
z(2)=W(1)x+b(1)a(2)=f(z(2))z(3)=W(2)a(2)+b(2)hW,b(x)=a(3)=f(z(3))在这个好看的形式下,前向传播可以简明扼要地表示为:
z(l+1)=W(l)a(l)+b(l)a(l+1)=f(z(l+1))在Python实现中,对应如下方法:
def runNN(self, inputs):
"""
前向传播进行分类
:param inputs:输入
:return:类别
"""
if len(inputs) != self.ni - 1:
print 'incorrect number of inputs'
for i in range(self.ni - 1):
self.ai[i] = inputs[i]
for j in range(self.nh):
sum = 0.0
for i in range(self.ni):
sum += ( self.ai[i] * self.wi[i][j] )
self.ah[j] = sigmoid(sum)
for k in range(self.no):
sum = 0.0
for j in range(self.nh):
sum += ( self.ah[j] * self.wo[j][k] )
self.ao[k] = sigmoid(sum)
return self.ao
其中,ai、ah、ao分别是输入层、隐藏层、输出层,而wi、wo则分别是输入层到隐藏层、隐藏层到输出层的权值矩阵。在本Python实现中,将偏置量一并放入了矩阵,这样进行线性代数运算就会方便一些。
后向传播指的是在训练的时候,根据最终输出的误差来调整倒数第二层、倒数第三层……第一层的参数的过程。
xlj:第l层第j个节点的输入。
Wlij:从第l−1层第i个节点到第l层第j个节点的权值。
σ(x)=11+e−x:Sigmoid函数。
θlj:第l层第j个节点的偏置。
Olj:第l层第j个节点的输出。
tj:输出层第j个节点的目标值(Target value)。
给定训练集tk和模型输出Ok(这里没有上标l是因为这里在讨论输出层,l是固定的),输出层的输出误差(或称损失函数吧)定义为:
E=12∑k∈K(Ok−tk)2其实就是所有实例对应的误差的平方和的一半,训练的目标就是最小化该误差。怎么最小化呢?看损失函数对参数的导数∂E∂Wljk呗。
将E的定义代入该导数:
∂E∂Wjk=∂∂Wjk12∑k∈K(Ok−tk)2无关变量拿出来:
∂E∂Wjk=(Ok−tk)∂∂WjkOk看到这里大概明白为什么非要把误差定义为误差平方和的一半了吧,就是为了好看,数学家都是外貌协会的。
将Ok=σ(xk)(输出层的输出等于输入代入Sigmoid函数)这个关系代入有:
∂E∂Wjk=(Ok−tk)∂∂Wjkσ(xk)对Sigmoid求导有:
∂E∂Wjk=(Ok−tk)σ(xk)(1−σ(xk))∂∂Wjkxk要开始耍小把戏了,由于输出层第k个节点的输入xk等于上一层第j个节点的输出Oj乘上Wjk,即xk=OkWjk,而上一层的输出Oj是与到输出层的权值变量Wjk无关的,可以看做一个常量,是线性关系。所以对xk求权值变量Wjk的偏导数直接等于Oj,也就是说:∂∂Wjkxk=∂∂Wjk(OjWjk)=Oj。
然后将上面用过的σ(xk)=Ok代进去就得到最终的:
∂E∂Wjk=(Ok−tk)Ok(1−Ok)Oj为了表述方便将上式记作:
∂E∂Wjk=Okδk其中:
δ=(Ok−tk)Ok(1−Ok)Oj依然采用类似的方法求导,只不过求的是关于隐藏层和前一层的权值参数的偏导数:
∂E∂Wij=∂∂Wij12∑k∈K(Ok−tk)2老样子:
∂E∂Wij=∑k∈K(Ok−tk)∂∂WijOk还是老样子:
∂E∂Wij=∑k∈K(Ok−tk)∂∂Wijσ(xk)还是把Sigmoid弄进去:
∂E∂Wij=∑k∈K(Ok−tk)σ(xk)(1−σ(xk))∂xk∂Wij把σ(xk)=Ok代进去,并且将导数部分拆开:
∂E∂Wij=∑k∈K(Ok−tk)Ok(1−Ok)∂xk∂Oj⋅∂Oj∂Wij又要耍把戏了,输出层的输入等于上一层的输出乘以相应的权值,亦即xk=WjkOj,于是得到:
∂E∂Wij=∑k∈K(Ok−tk)Ok(1−Ok)Wjk∂Oj∂Wij把最后面的导数挪到前面去,接下来要对它动刀了:
∂E∂Wij=∂Oj∂Wij∑k∈K(Ok−tk)Ok(1−Ok)Wjk再次利用Ok=σ(xk),这对j也成立,代进去:
∂E∂Wij=Oj(1−Oj)∂xj∂Wij∑k∈K(Ok−tk)Ok(1−Ok)Wjk再次利用xk=WjkOj,j换成i,k换成j也成立,代进去:
∂E∂Wij=Oj(1−Oj)Oi∑k∈K(Ok−tk)Ok(1−Ok)Wjk利用刚才定义的δk,最终得到:
∂E∂Wij=OiOj(1−Oj)∑k∈KδkWjk其中:
δ=Ok(1−Ok)(Ok−tk)我们还可以仿照δk的定义来定义一个δj,得到:
∂E∂Wij=Oiδj其中
δj=Oj(1−Oj)∑k∈KδkWjk因为没有任何节点的输出流向偏置节点,所以偏置节点不存在上层节点到它所对应的权值参数,也就是说不存在关于权值变量的偏导数。虽然没有流入,但是偏置节点依然有输出(总是+1),该输出到下一层某个节点的时候还是会有权值的,对这个权值依然需要更新。
我们可以直接对偏置求导,发现:
∂O∂θ=O(1−O)∂θ∂θ原视频中说∂O∂θ=1,这是不对的,作者也在讲义中修正了这个错误,∂O∂θ=O(1–O)。
然后再求∂E∂θ,∂E∂θ=∑k∈K(Ok−tk)∂∂θOk,后面的导数等于∂O∂θ=O(1−O),代进去有
∂E∂θ=δl其中,
δk=Ok(1−Ok)(Ok−tk)随机初始化参数,对输入利用前向传播计算输出。
对每个输出节点按照下式计算δ:δk=Ok(1−Ok)(Ok−tk)
对每个隐藏节点按照下式计算δ:δj=Oj(1−Oj)∑k∈KδkWjk
计算梯度ΔW=−ηδlOl−1,Δθ=−ηδl,并更新权值参数和偏置参数:W+ΔW→W,θ+Δθ→θ。这里的η是学习率,影响训练速度。
def backPropagate(self, targets, N, M):
"""
后向传播算法
:param targets: 实例的类别
:param N: 本次学习率
:param M: 上次学习率
:return: 最终的误差平方和的一半
"""
# http://www.youtube.com/watch?v=aVId8KMsdUU&feature=BFa&list=LLldMCkmXl4j9_v0HeKdNcRA
# 计算输出层 deltas
# dE/dw[j][k] = (t[k] - ao[k]) * s'( SUM( w[j][k]*ah[j] ) ) * ah[j]
output_deltas = [0.0] * self.no
for k in range(self.no):
error = targets[k] - self.ao[k]
output_deltas[k] = error * dsigmoid(self.ao[k])
# 更新输出层权值
for j in range(self.nh):
for k in range(self.no):
# output_deltas[k] * self.ah[j] 才是 dError/dweight[j][k]
change = output_deltas[k] * self.ah[j]
self.wo[j][k] += N * change + M * self.co[j][k]
self.co[j][k] = change
# 计算隐藏层 deltas
hidden_deltas = [0.0] * self.nh
for j in range(self.nh):
error = 0.0
for k in range(self.no):
error += output_deltas[k] * self.wo[j][k]
hidden_deltas[j] = error * dsigmoid(self.ah[j])
# 更新输入层权值
for i in range(self.ni):
for j in range(self.nh):
change = hidden_deltas[j] * self.ai[i]
# print 'activation',self.ai[i],'synapse',i,j,'change',change
self.wi[i][j] += N * change + M * self.ci[i][j]
self.ci[i][j] = change
# 计算误差平方和
# 1/2 是为了好看,**2 是平方
error = 0.0
for k in range(len(targets)):
error = 0.5 * (targets[k] - self.ao[k]) ** 2
return error
注意不同于上文的单一学习率η,这里有两个学习率N和M。N相当于上文的η,而M则是在用上次训练的梯度更新权值时的学习率。这种同时考虑最近两次迭代得到的梯度的方法,可以看做是对单一学习率的改进。
这里并没有出现任何更新偏置的操作,为什么?
因为这里的偏置是单独作为一个偏置节点放到输入层里的,它的值(输出,没有输入)固定为1,它的权值已经自动包含在上述权值调整中了。
如果将偏置作为分别绑定到所有神经元的许多值,那么则需要进行偏置调整,而不需要权值调整(此时没有偏置节点)。 哪个方便,当然是前者了,这也导致了大部分神经网络实现都采用前一种做法。
直接运行bpnn.py即可得到输出:
Combined error 0.171204877501
Combined error 0.190866985872
Combined error 0.126126875154
Combined error 0.0658488960415
Combined error 0.0353249077599
Combined error 0.0214428399072
Combined error 0.0144886807614
Combined error 0.0105787745309
Combined error 0.00816264126944
Combined error 0.00655731212209
Combined error 0.00542964723539
Combined error 0.00460235328667
Combined error 0.00397407912435
Combined error 0.00348339081276
Combined error 0.00309120476889
Combined error 0.00277163178862
Combined error 0.00250692771135
Combined error 0.00228457151714
Combined error 0.00209550313514
Combined error 0.00193302192499
Inputs: [0, 0] --> [0.9982333356008245] Target [1]
Inputs: [0, 1] --> [0.9647325217906978] Target [1]
Inputs: [1, 0] --> [0.9627966274767186] Target [1]
Inputs: [1, 1] --> [0.05966109502803293] Target [0]
IBM利用Neil Schemenauer的这一模块(旧版)做了一个识别代码语言的例子,我将其更新到新版,已经整合到了项目中。
要运行测试的话,执行命令
code_recognizer.py testdata.200
即可得到输出:
ERROR_CUTOFF = 0.01
INPUTS = 20
ITERATIONS = 1000
MOMENTUM = 0.1
TESTSIZE = 500
OUTPUTS = 3
TRAINSIZE = 500
LEARNRATE = 0.5
HIDDEN = 8
Targets: [1, 0, 0] -- Errors: (0.000 OK) (0.001 OK) (0.000 OK) -- SUCCESS!
值得一提的是,这里的HIDDEN = 8指的是隐藏层的节点个数,不是层数,层数多了就变成DeepLearning了。
接下来秀下操作,比如,只用9行
尽管我们不直接用神经网络库,但还是要从Python数学库Numpy中导入4种方法:
from numpy import exp, array, random, dot
training_set_inputs = array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
training_set_outputs = array([[0, 1, 1, 0]]).T
random.seed(1)
synaptic_weights = 2 * random.random((3, 1)) - 1
for iteration in range(10000):
output = 1 / (1 + exp(-(dot(training_set_inputs, synaptic_weights))))
synaptic_weights += dot(training_set_inputs.T, (training_set_outputs - output) * output * (1 - output))
print("accurency:",1 / (1 + exp(-(dot(array([1, 0, 0]), synaptic_weights)))))
accurency: [ 0.99993704]
import numpy as np
X = np.array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
Y = np.array([[0, 1, 1, 0]]).T
np.random.seed(1)
w = 2 * np.random.random((3, 1)) - 1
b = 2 * np.random.random((4, 1)) - 1
for i in range(10000):
y = 1 / (1 + np.exp(-(np.dot(X, w)+b)))
w += np.dot(X.T, (Y - y) * y * (1 - y))
b += (Y - y) * y * (1 - y)
print("accurency:",1 / (1 + np.exp(-(np.dot(np.array([1, 0, 0]), w)))))
X = np.array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
Y = np.array([[0, 1, 1, 0]]).T
np.random.seed(1)
w1 = 2 * np.random.random((3, 2)) - 1
b1 = 2 * np.random.random((4, 2)) - 1
w2 = 2 * np.random.random((2, 1)) - 1
b2 = 2 * np.random.random((4, 1)) - 1
Sigmoid = lambda x: 1/(1+np.exp(-x))
dSigmoid_dx = lambda x: x*(1-x)
for i in range(10000):
y1 = Sigmoid(np.dot(X, w1)+b1) # (4,2)
y2 = Sigmoid(np.dot(y1,w2)+b2) # (4,1)
err2 = (Y - y2) * dSigmoid_dx(y2) # (4,1)
w2 += np.dot(y1.T, err2) # (2,1)
b2 += err2 # (4,1)
err1 = np.dot(err2, w2.T) * dSigmoid_dx(y1) # (4,2)
w1 += np.dot(X.T, err1) # (3,2)
b1 += err1 # (4,1)
print("accurency:",Sigmoid(np.dot(Sigmoid(np.dot(np.array([1, 0, 0]), w1)), w2)))
accurency: [ 0.99405311] accurency: [ 0.88478114]
from numpy import exp, array, random, dot
class NeuralNetwork():
def __init__(self):
# 随机数发生器种子,以保证每次获得相同结果
random.seed(1)
# 对单个神经元建模,含有3个输入连接和一个输出连接
# 对一个3 x 1的矩阵赋予随机权重值。范围-1~1,平均值为0
self.synaptic_weights = 2 * random.random((3, 1)) - 1
# Sigmoid函数,S形曲线
# 用这个函数对输入的加权总和做正规化,使其范围在0~1
def __sigmoid(self, x):
return 1 / (1 + exp(-x))
# Sigmoid函数的导数
# Sigmoid曲线的梯度
# 表示我们对当前权重的置信程度
def __sigmoid_derivative(self, x):
return x * (1 - x)
# 通过试错过程训练神经网络
# 每次都调整突触权重
def train(self, training_set_inputs, training_set_outputs, number_of_training_iterations):
for iteration in range(number_of_training_iterations):
# 将训练集导入神经网络
output = self.think(training_set_inputs)
# 计算误差(实际值与期望值之差)
error = training_set_outputs - output
# 将误差、输入和S曲线梯度相乘
# 对于置信程度低的权重,调整程度也大
# 为0的输入值不会影响权重
adjustment = dot(training_set_inputs.T, error * self.__sigmoid_derivative(output))
# 调整权重
self.synaptic_weights += adjustment
# 神经网络一思考
def think(self, inputs):
# 把输入传递给神经网络
return self.__sigmoid(dot(inputs, self.synaptic_weights))
if __name__ == "__main__":
# 初始化神经网络
neural_network = NeuralNetwork()
print("随机的初始突触权重:")
print(neural_network.synaptic_weights)
# 训练集。四个样本,每个有3个输入和1个输出
training_set_inputs = array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
training_set_outputs = array([[0, 1, 1, 0]]).T
# 用训练集训练神经网络
# 重复一万次,每次做微小的调整
neural_network.train(training_set_inputs, training_set_outputs, 10000)
print("训练后的突触权重:")
print(neural_network.synaptic_weights)
# 用新数据测试神经网络
print("考虑新的形势 [1, 0, 0] -> ?: ")
print(neural_network.think(array([1, 0, 0])))
随机的初始突触权重: [[-0.16595599] [ 0.44064899] [-0.99977125]] 训练后的突触权重: [[ 9.67299303] [-0.2078435 ] [-4.62963669]] 考虑新的形势 [1, 0, 0] -> ?: [ 0.99993704]
输入层+3∗隐藏层+输出层
from numpy import exp, array, random, dot
class NeuronLayer():
def __init__(self, number_of_neurons, number_of_inputs_per_neuron):
self.synaptic_weights = 2 * random.random((number_of_inputs_per_neuron, number_of_neurons)) - 1
class NeuralNetwork():
def __init__(self, layer1, layer2):
self.layer1 = layer1
self.layer2 = layer2
# Sigmoid函数,S形曲线
# 传递输入的加权和,正规化为0-1
def __sigmoid(self, x):
return 1 / (1 + exp(-x))
# Sigmoid函数的导数,Sigmoid曲线的梯度,表示对现有权重的置信程度
def __sigmoid_derivative(self, x):
return x * (1 - x)
# 通过试错训练神经网络,每次微调突触权重
def train(self, training_set_inputs, training_set_outputs, number_of_training_iterations):
for iteration in range(number_of_training_iterations):
# 将整个训练集传递给神经网络
output_from_layer_1, output_from_layer_2 = self.think(training_set_inputs)
# 计算第二层的误差
layer2_error = training_set_outputs - output_from_layer_2
layer2_delta = layer2_error * self.__sigmoid_derivative(output_from_layer_2)
# 计算第一层的误差,得到第一层对第二层的影响
layer1_error = layer2_delta.dot(self.layer2.synaptic_weights.T)
layer1_delta = layer1_error * self.__sigmoid_derivative(output_from_layer_1)
# 计算权重调整量
layer1_adjustment = training_set_inputs.T.dot(layer1_delta)
layer2_adjustment = output_from_layer_1.T.dot(layer2_delta)
# 调整权重
self.layer1.synaptic_weights += layer1_adjustment
self.layer2.synaptic_weights += layer2_adjustment
# 神经网络一思考
def think(self, inputs):
output_from_layer1 = self.__sigmoid(dot(inputs, self.layer1.synaptic_weights))
output_from_layer2 = self.__sigmoid(dot(output_from_layer1, self.layer2.synaptic_weights))
return output_from_layer1, output_from_layer2
# 输出权重
def print_weights(self):
print(" Layer 1 (4 neurons, each with 3 inputs): ")
print(self.layer1.synaptic_weights)
print(" Layer 2 (1 neuron, with 4 inputs):")
print(self.layer2.synaptic_weights)
if __name__ == "__main__":
# 设定随机数种子
random.seed(1)
# 创建第一层 (4神经元, 每个3输入)
layer1 = NeuronLayer(4, 3)
# 创建第二层 (单神经元,4输入)
layer2 = NeuronLayer(1, 4)
# 组合成神经网络
neural_network = NeuralNetwork(layer1, layer2)
print("Stage 1) 随机初始突触权重: ")
neural_network.print_weights()
# 训练集,7个样本,均有3输入1输出
training_set_inputs = array([[0, 0, 1], [0, 1, 1], [1, 0, 1], [0, 1, 0], [1, 0, 0], [1, 1, 1], [0, 0, 0]])
training_set_outputs = array([[0, 1, 1, 1, 1, 0, 0]]).T
# 用训练集训练神经网络
# 迭代60000次,每次微调权重值
neural_network.train(training_set_inputs, training_set_outputs, 60000)
print("Stage 2) 训练后的新权重值: ")
neural_network.print_weights()
# 用新数据测试神经网络
print("Stage 3) 思考新形势 [1, 1, 0] -> ?: ")
hidden_state, output = neural_network.think(array([1, 1, 0]))
print(output)
Stage 1) 随机初始突触权重: Layer 1 (4 neurons, each with 3 inputs): [[-0.16595599 0.44064899 -0.99977125 -0.39533485] [-0.70648822 -0.81532281 -0.62747958 -0.30887855] [-0.20646505 0.07763347 -0.16161097 0.370439 ]] Layer 2 (1 neuron, with 4 inputs): [[-0.5910955 ] [ 0.75623487] [-0.94522481] [ 0.34093502]] Stage 2) 训练后的新权重值: Layer 1 (4 neurons, each with 3 inputs): [[ 0.3122465 4.57704063 -6.15329916 -8.75834924] [ 0.19676933 -8.74975548 -6.1638187 4.40720501] [-0.03327074 -0.58272995 0.08319184 -0.39787635]] Layer 2 (1 neuron, with 4 inputs): [[ -8.18850925] [ 10.13210706] [-21.33532796] [ 9.90935111]] Stage 3) 思考新形势 [1, 1, 0] -> ?: [ 0.0078876]
数学家们经过严格的数学证明,双隐层神经网络能够解决任意复杂的分类问题。
对于一切分类问题我们都可以有一个统一的方法,只需要两层隐藏层。以下面这个3分类问题为例
我们通过取”半平面”、取交集生成精确包含每一个样本点的凸域(所以凸域的个数与训练集的样本的个数相等),再对同类的样本点的区域取并集,这样无论多复杂的分离界面我们可以考虑进去。
于是,我们设计双隐层神经网络结构如下,每一层的节点数依次为:3、25、7、3:
解释下每一层的作用:
第一层是输入层
,对应着每一个输入的二维样本点x(X1,X2)的2个输坐标和1个辅助偏移的常量“1”,总共3个节点。
第二层是第一层的输出层
,每个节点对应着输入样本点属于某个”半平面”的概率,该”半平面”的正向法向量就对应着输入层(输入样本点坐标)到该节点的权重w和偏移b。
为了精确定位总共这6个点,我们围绕每个点附近平行于坐标轴切4刀,用四个”半平面”相交产生的方形封闭凸域精确包裹定位该点,所以这一层对应着总共产生了的4*6=24个”半平面”,有24个节点。 第二层同时也是第三层的输入层,要考虑再加上求第三层节点时要用的1个辅助偏移的常量节点“1”,第二层总共有24+1=25个节点。
第二层的输出层
,每个节点对应着输入样本点属于第二层区分出来的某4个”半平面”取交集形成的某个方形封闭凸域的概率,故总共需要24/4=6个节点。因为采用我们的划分方式,只需要四个”半平面”就可以精确包裹某一个样本点了,我们认为其他的”半平面”贡献的权重应该为0,就只画了生成该凸域的四个”半平面”给予的权重对应的连线,其他连线不画。 第三层同时也是第四层的输入层,要考虑再加上求第四层节点时要用的1个辅助偏移的常量节点“1”,第三层总共有6+1=7个节点。
第三层的输出层
,每个节点对应着输入样本点属于某一类点所在的所有区域的概率。为找到该类的区分区域,对第三层属于同一类点的2个凸域的2个节点取并集,故总共需要6/2=3个节点。 (同样,我们不画其他类的凸域贡献的权重的连线)
小结一下就是先取”半平面”再取交集最后取并集。
一图胜千言
从上图我们可以看到,CNN的架构和基本的神经网络差不多(事实上,CNN的前面几层是特殊的卷积层,图中表示不明显)
具体一点,卷积神经网络各个层级结构如下:
上图中CNN要做的事情是:给定一张图片,是车还是马未知,是什么车也未知,现在需要模型判断这张图片里具体是一个什么东西,总之输出一个结果:如果是车 那是什么车
所以
最左边是
中间是
最右边是
这几个部分中,卷积计算层是CNN的核心,下文将重点阐述。
从上面的讨论我们知道,对一般的数据分类任务,几层的BPNN就可以搞定(不然再加层加神经元,总会成功的)
但是,如果我们分类的对象是图片呢?
在计算机中图片表达为矩阵,矩阵的数代表灰度
图像在计算机中是一堆按顺序排列的数字,数值为0到255。0表示最暗,255表示最亮。
你可以把这堆数字用一个长长的向量来表示,也就是tensorflow的mnist教程中784维向量的表示方式。 然而这样会失去平面结构的信息
,为保留该结构信息,通常选择矩阵的表示方式:28x28的矩阵。
上图是只有黑白颜色的灰度图,而更普遍的图片表达方式是RGB颜色模型,即红(Red)、绿(Green)、蓝(Blue)三原色的色光以不同的比例相加,以产生多种多样的色光。这样,RGB颜色模型中,单个矩阵就扩展成了有序排列的三个矩阵,也可以用三维张量去理解,其中的每一个矩阵又叫这个图片的一个channel。
在电脑中,一张图片是数字构成的“长方体”。可用 宽width, 高height, 深depth 来描述,如图。
画面识别的输入x是shape为(width, height, depth)的三维张量。
比如下面x.shape=(width=5,height=2,depth=3)
x=[
[[1,2,3,4,5],
[6,7,8,9,0]],
[[1,2,3,4,5],
[6,7,8,9,0]],
[[1,2,3,4,5],
[6,7,8,9,0]]
]
接下来要考虑的就是该如何处理这样的“数字长方体”。
当我们给定一个"X"的图案,计算机怎么识别这个图案就是“X”呢?一个可能的办法就是计算机存储一张标准的“X”图案,然后把需要识别的未知图案跟标准"X"图案进行比对,如果二者一致,则判定未知图案即是一个"X"图案。
而且即便未知图案可能有一些平移或稍稍变形,依然能辨别出它是一个X图案。如此,CNN是把未知图案和标准X图案一个局部一个局部的对比,如下图所示
而未知图案的局部和标准X图案的局部一个一个比对时的计算过程,便是卷积操作。卷积计算结果为1表示匹配,否则不匹配。
举个数学例子
x=[123456789],y=[123456789],x与y的卷积为[1∗1+2∗2+3∗3+4∗4+5∗5+6∗6+7∗7+8∗8+9∗9]=[1+4+9+16+25+36+49+64+81]=285两个矩阵的卷积是一个数!这个数非常大,所以匹配。更多时候我们会根据平均值把图象二值化,只留0和1,这样计算和判断更准确
接下来,我们来了解下什么是卷积操作。
对图像(不同的数据窗口数据)和滤波矩阵(一组固定的权重:因为每个神经元的多个权重固定,所以又可以看做一个恒定的滤波器filter)做内积(逐个元素相乘再求和)的操作就是所谓的『卷积』操作,也是卷积神经网络的名字来源。
非严格意义上来讲,下图中红框框起来的部分便可以理解为一个滤波器,即带着一组固定权重的神经元。多个滤波器叠加便成了卷积层。
换句话说,滤波器相当于平面状态的权重,训练CNN就是训练这些滤波器的权重
OK,举个具体的例子。比如下图中,图中左边部分是原始输入数据,图中中间部分是滤波器filter,图中右边是输出的新的二维数据。
中间滤波器filter与数据窗口做内积,其具体计算过程则是:
pic=[000011012],y=[−40000000−4],x与y的卷积为[4∗0+0∗0+0∗0+0∗0+0∗1+0∗1+0∗0+0∗1+−4∗2]=−8
在下图对应的计算过程中,输入是一定区域大小(width*height)的数据,和滤波器filter(带着一组固定权重的神经元)做内积后等到新的二维数据。
具体来说,左边是图像输入,中间部分就是滤波器filter(带着一组固定权重的神经元),不同的滤波器filter会得到不同的输出数据,比如颜色深浅、轮廓。相当于如果想提取图像的不同特征,则用不同的滤波器filter,提取想要的关于图像的特定信息:颜色深浅或轮廓。
如下图所示
在CNN中,滤波器filter(带着一组固定权重的神经元)对局部输入数据进行卷积计算。每计算完一个数据窗口内的局部数据后,数据窗口不断平移滑动,直到计算完所有数据。这个过程中,有这么几个参数
from IPython.core.display import HTML
HTML('<iframe src="cnn.html" width="100%" height="700px;" style="border:none;"></iframe>')
可以看到:
然后分别以两个滤波器filter为轴滑动数组进行卷积计算,得到两组不同的结果。
如果初看上图,可能不一定能立马理解啥意思,但结合上文的内容后,理解这个动图已经不是很困难的事情:
左边是输入(773中,7*7代表图像的像素/长宽,3代表R、G、B 三个颜色通道)
中间部分是两个不同的滤波器Filter w0、Filter w1
最右边则是两个不同的输出
随着左边数据窗口的平移滑动,滤波器Filter w0 / Filter w1对不同的局部数据进行卷积计算。
值得一提的是:
嗯,在这里可以停一下,去看上面的动图,仔细揣摩卷积操作到底怎么进行的
我们知道一个物体不管在画面左侧还是右侧,都会被识别为同一物体,这一特点就是不变性(invariance),如下图所示。
我们希望所建立的网络可以尽可能的满足这些不变性特点。
为了理解卷积神经网络对这些不变性特点的贡献,我们将用不具备这些不变性特点的前馈神经网络来进行比较。
方便起见,我们用depth只有1的灰度图来举例。
想要完成的任务是:在宽长为4x4的图片中识别是否有下图所示的“横折”。 图中,黄色圆点表示值为0的像素,深色圆点表示值为1的像素。 我们知道不管这个横折在图片中的什么位置,都会被认为是相同的横折。
若训练前馈神经网络来完成该任务,那么表达图像的三维张量将会被摊平成一个向量,作为网络的输入,即(width, height, depth)为(4, 4, 1)的图片会被展成维度为16的向量作为网络的输入层。再经过几层不同节点个数的隐藏层,最终输出两个节点,分别表示“有横折的概率”和“没有横折的概率”.
我们用数字(16进制)对图片中的每一个像素点(pixel)进行编号。
当使用右侧那种物体位于中间的训练数据来训练网络时,网络就只会对编号为5,6,9,a的节点的权重进行调节。
若让该网络识别位于右下角的“横折”时,则无法识别。换句话说,前馈神经网络很难满足平移不变性,对二维图象的感知有限。
卷积神经网络对二维图象的特征提取有2个策略:局部连接,空间共享
就是filter不和整个矩阵全连接而是通过局部卷积和滑动来提取特征,同时满足了平移不变性
有2个方面
个人觉得卷积神经网络克服这一不变性的主要手段还是靠大量的数据。 并没有明确加入“旋转和视角不变性”的先验特性。
与平移不变性不同,最初的卷积网络并没有明确照顾尺寸不变性这一特点。
我们知道filter的size是事先选择的,而不同的尺寸所寻找的形状(概念)范围不同。
从直观上思考,如果选择小范围,再一步步通过组合,仍然是可以得到大范围的形状。 如3x3尺寸的形状都是可以由2x2形状的图形组合而成。所以形状的尺寸不变性对卷积神经网络而言并不算问题。 这恐怕ZF Net让第一层的stride和filter size更小,VGGNet将所有filter size都设置成3x3仍可以得到优秀结果的一个原因。
但是,除了形状之外,很多概念的抓取通常需要考虑一个像素与周边更多像素之间的关系后得出。 也就是说5x5的filter也是有它的优点。 同时,小尺寸的堆叠需要很多个filters来共同完成,如果需要抓取的形状恰巧在5x5的范围,那么5x5会比3x3来的更有效率。 所以一次性使用多个不同filter size来抓取多个范围不同的概念是一种顺理成章的想法,而这个也就是Inception。 可以说Inception是为了尺寸不变性而引入的一个先验知识。
上图是Inception的结构,尽管也有不同的版本,但是其动机都是一样的:消除尺寸对于识别结果的影响,一次性使用多个不同filter size来抓取多个范围不同的概念,并让网络自己选择需要的特征。
你也一定注意到了蓝色的1x1卷积,撇开它,先看左边的这个结构。
输入(可以是被卷积完的长方体输出作为该层的输入)进来后,通常我们可以选择直接使用像素信息(1x1卷积)传递到下一层,可以选择3x3卷积,可以选择5x5卷积,还可以选择max pooling的方式downsample刚被卷积后的feature maps。 但在实际的网络设计中,究竟该如何选择需要大量的实验和经验的。 Inception就不用我们来选择,而是将4个选项给神经网络,让网络自己去选择最合适的解决方案。
接下来我们再看右边的这个结构,多了很多蓝色的1x1卷积。 这些1x1卷积的作用是为了让网络根据需要能够更灵活的控制数据的depth的。
如果卷积的输出输入都只是一个平面,那么1x1卷积核并没有什么意义,它是完全不考虑像素与周边其他像素关系。 但卷积的输出输入是长方体,所以1x1卷积实际上是对每个像素点,在不同的channels上进行线性组合(信息整合),且保留了图片的原有平面结构,调控depth,从而完成升维或降维的功能。
如下图所示,如果选择2个filters的1x1卷积层,那么数据就从原本的depth 3 降到了2。若用4个filters,则起到了升维的作用。
这就是为什么上面Inception的4个选择中都混合一个1x1卷积,如右侧所展示的那样。 其中,绿色的1x1卷积本身就1x1卷积,所以不需要再用另一个1x1卷积。 而max pooling用来去掉卷积得到的Feature Map中的冗余信息,所以出现在1x1卷积之前,紧随刚被卷积后的feature maps。(由于没做过实验,不清楚调换顺序会有什么影响。)
现在我们已经知道了depth维度只有1的灰度图是如何处理的。 但前文提过,图片的普遍表达方式是下图这样有3个channels的RGB颜色模型。 当depth为复数的时候,每个feature detector是如何卷积的?
在2D卷积中,filter在张量的width维, height维上是局部连接,在depth维上是贯串全部channels的。
下面这张图展示了,在depth为复数时,filter是如何连接输入节点到输出节点的。 图中红、绿、蓝颜色的节点表示3个channels。 黄色节点表示一个feature detector卷积后得到的Feature Map。 其中被透明黑框圈中的12个节点会被连接到黄黑色的节点上。
注意:三个channels的权重并不共享。 即当深度变为3后,权重也跟着扩增到了三组,如式子(3)所示,不同channels用的是自己的权重。 式子中增加的角标r,g,b分别表示red channel, green channel, blue channel的权重。 [wr1wr2wr3wr4],[wg1wg2wg3wg4],[wb1wb2wb3wb4](3)
当filter扫到其他位置计算输出节点y_i时,那12个权重在不同位置是共用的,如下面的动态图所展示。 透明黑框圈中的12个节点会连接到被白色边框选中的黄色节点上。
每个filter会在width维, height维上,以局部连接和空间共享,并贯串整个depth维的方式得到一个Feature Map。
细心的读者应该早就注意到了,4x4的图片被2x2的filter卷积后变成了3x3的图片,每次卷积后都会小一圈的话,经过若干层后岂不是变的越来越小? Zero padding就可以在这时帮助控制Feature Map的输出尺寸,同时避免了边缘信息被一步步舍弃的问题。
例如:下面4x4的图片在边缘Zero padding一圈后,再用3x3的filter卷积后,得到的Feature Map尺寸依然是4x4不变。
通常大家都想要在卷积时保持图片的原始尺寸。 选择3x3的filter和1的zero padding,或5x5的filter和2的zero padding可以保持图片的原始尺寸。 这也是为什么大家多选择3x3和5x5的filter的原因。 另一个原因是3x3的filter考虑到了像素与其距离为1以内的所有其他像素的关系,而5x5则是考虑像素与其距离为2以内的所有其他像素的关系。
尺寸:Feature Map的尺寸等于(input_size + 2 * padding_size − filter_size)/stride+1。
注意:上面的式子是计算width或height一维的。padding_size也表示的是单边补零的个数。例如(4+2-3)/1+1 = 4,保持原尺寸。
不用去背这个式子。其中(input_size + 2 * padding_size)是经过Zero padding扩充后真正要卷积的尺寸。 减去 filter_size后表示可以滑动的范围。 再除以可以一次滑动(stride)多少后得到滑动了多少次,也就意味着得到了多少个输出节点。 再加上第一个不需要滑动也存在的输出节点后就是最后的尺寸。
知道了每个filter在做什么之后,我们再来思考这样的一个filter会抓取到什么样的信息。
我们知道不同的形状都可由细小的“零件”组合而成的。比如下图中,用2x2的范围所形成的16种形状可以组合成格式各样的“更大”形状。
卷积的每个filter可以探测特定的形状。又由于Feature Map保持了抓取后的空间结构。若将探测到细小图形的Feature Map作为新的输入再次卷积后,则可以由此探测到“更大”的形状概念。 比如下图的第一个“大”形状可由2,3,4,5基础形状拼成。第二个可由2,4,5,6组成。第三个可由6,1组成。
除了基础形状之外,颜色、对比度等概念对画面的识别结果也有影响。卷积层也会根据需要去探测特定的概念。
可以从下面这张图中感受到不同数值的filters所卷积过后的Feature Map可以探测边缘,棱角,模糊,突出等概念。
如我们先前所提,图片被识别成什么不仅仅取决于图片本身,还取决于图片是如何被观察的。
而filter内的权重矩阵W是网络根据数据学习得到的,也就是说,我们让神经网络自己学习以什么样的方式去观察图片。
拿老妇与少女的那幅图片举例,当标签是少女时,卷积网络就会学习抓取可以成少女的形状、概念。 当标签是老妇时,卷积网络就会学习抓取可以成老妇的形状、概念。
下图展现了在人脸识别中经过层层的卷积后,所能够探测的形状、概念也变得越来越抽象和复杂。
卷积神经网络会尽可能寻找最能解释训练数据的抓取方式。
每个filter可以抓取探测特定的形状的存在。 假如我们要探测下图的长方框形状时,可以用4个filters去探测4个基础“零件”。
因此我们自然而然的会选择用多个不同的filters对同一个图片进行多次抓取。 如下图(动态图过大,如果显示不出,请看到该链接观看),同一个图片,经过两个(红色、绿色)不同的filters扫描过后可得到不同特点的Feature Maps。 每增加一个filter,就意味着你想让网络多抓取一个特征。
这样卷积层的输出也不再是depth为1的一个平面,而是和输入一样是depth为复数的长方体。
如下图所示,当我们增加一个filter(紫色表示)后,就又可以得到一个Feature Map。 将不同filters所卷积得到的Feature Maps按顺序堆叠后,就得到了一个卷积层的最终输出。
卷积层的输入是长方体,输出也是长方体。 这样卷积后输出的长方体可以作为新的输入送入另一个卷积层中处理。
和前馈神经网络一样,经过线性组合和偏移后,会加入非线性增强模型的拟合能力。
将卷积所得的Feature Map经过ReLU变换(elementwise)后所得到的output就如下图所展示。
现在我们知道了一个卷积层的输出也是一个长方体。 那么这个输出长方体的(width, height, depth)由哪些因素决定和控制。
这里直接用CS231n的Summary:
在卷积后还会有一个pooling的操作,尽管有其他的比如average pooling等,这里只提max pooling。
max pooling的操作如下图所示:整个图片被不重叠的分割成若干个同样大小的小块(pooling size)。每个小块内只取最大的数字,再舍弃其他节点后,保持原有的平面结构得出output。
max pooling在不同的depth上是分开执行的,且不需要参数控制。 那么问题就max pooling有什么作用?部分信息被舍弃后难道没有影响吗?
Max pooling的主要功能是downsamping,却不会损坏识别结果。 这意味着卷积后的Feature Map中有对于识别物体不必要的冗余信息。 那么我们就反过来思考,这些“冗余”信息是如何产生的。
直觉上,我们为了探测到某个特定形状的存在,用一个filter对整个图片进行逐步扫描。但只有出现了该特定形状的区域所卷积获得的输出才是真正有用的,用该filter卷积其他区域得出的数值就可能对该形状是否存在的判定影响较小。 比如下图中,我们还是考虑探测“横折”这个形状。 卷积后得到3x3的Feature Map中,真正有用的就是数字为3的那个节点,其余数值对于这个任务而言都是无关的。 所以用3x3的Max pooling后,并没有对“横折”的探测产生影响。 试想在这里例子中如果不使用Max pooling,而让网络自己去学习。 网络也会去学习与Max pooling近似效果的权重。因为是近似效果,增加了更多的parameters的代价,却还不如直接进行Max pooling。
Max pooling还有类似“选择句”的功能。假如有两个节点,其中第一个节点会在某些输入情况下最大,那么网络就只在这个节点上流通信息;而另一些输入又会让第二个节点的值最大,那么网络就转而走这个节点的分支。
但是Max pooling也有不好的地方。因为并非所有的抓取都像上图的例子。有些周边信息对某个概念是否存在的判定也有影响。 并且Max pooling是对所有的Feature Maps进行等价的操作。就好比用相同网孔的渔网打鱼,一定会有漏网之鱼。
当抓取到足以用来识别图片的特征后,接下来的就是如何进行分类。 全连接层(也叫前馈层)就可以用来将最后的输出映射到线性可分的空间。 通常卷积网络的最后会将末端得到的长方体平摊(flatten)成一个长长的向量,并送入全连接层配合输出层进行分类。
卷积神经网络大致就是covolutional layer, pooling layer, ReLu layer, fully-connected layer的组合,例如下图所示的结构。
这里也体现了深层神经网络或deep learning之所以称deep的一个原因:模型将特征抓取层和分类层合在了一起。 负责特征抓取的卷积层主要是用来学习“如何观察”。
下图简述了机器学习的发展,从最初的人工定义特征再放入分类器的方法,到让机器自己学习特征,再到如今尽量减少人为干涉的deep learning。
前馈神经网络也好,卷积神经网络也好,都是一层一层逐步变换的,不允许跳层组合。 但现实中是否有跳层组合的现象?
比如说我们在判断一个人的时候,很多时候我们并不是观察它的全部,或者给你的图片本身就是残缺的。 这时我们会靠单个五官,外加这个人的着装,再加他的身形来综合判断这个人,如下图所示。 这样,即便图片本身是残缺的也可以很好的判断它是什么。 这和前馈神经网络的先验知识不同,它允许不同层级之间的因素进行信息交互、综合判断。
残差网络就是拥有这种特点的神经网络。大家喜欢用identity mappings去解释为什么残差网络更优秀。 这里我只是提供了一个以先验知识的角度去理解的方式。 需要注意的是每一层并不会像我这里所展示的那样,会形成明确的五官层。 只是有这样的组合趋势,实际无法保证神经网络到底学到了什么内容。
用下图举一个更易思考的例子。 图形1,2,3,4,5,6是第一层卷积层抓取到的概念。 图形7,8,9是第二层卷积层抓取到的概念。 图形7,8,9是由1,2,3,4,5,6的基础上组合而成的。
但当我们想要探测的图形10并不是单纯的靠图形7,8,9组成,而是第一个卷积层的图形6和第二个卷积层的8,9组成的话,不允许跨层连接的卷积网络不得不用更多的filter来保持第一层已经抓取到的图形信息。并且每次传递到下一层都需要学习那个用于保留前一层图形概念的filter的权重。 当层数变深后,会越来越难以保持,还需要max pooling将冗余信息去掉。
一个合理的做法就是直接将上一层所抓取的概念也跳层传递给下下一层,不用让其每次都重新学习。 就好比在编程时构建了不同规模的functions。 每个function我们都是保留,而不是重新再写一遍。提高了重用性。
同时,因为ResNet使用了跳层连接的方式。也不需要max pooling对保留低层信息时所产生的冗余信息进行去除。
Inception中的第一个1x1的卷积通道也有类似的作用,但是1x1的卷积仍有权重需要学习。 并且Inception所使用的结合方式是concatenate的合并成一个更大的向量的方式,而ResNet的结合方式是sum。 两个结合方式各有优点。 concatenate当需要用不同的维度去组合成新观念的时候更有益。 而sum则更适用于并存的判断。
比如既有油头发,又有胖身躯,同时穿着常年不洗的牛仔裤,三个不同层面的概念并存时,该人会被判定为程序员的情况。
又比如双向LSTM中正向和逆向序列抓取的结合常用相加的方式结合。
在语音识别中,这表示既可以正向抓取某种特征,又可以反向抓取另一种特征。当两种特征同时存在时才会被识别成某个特定声音。
Group convolution 分组卷积,最早在AlexNet中出现,由于当时的硬件资源有限,训练AlexNet时卷积操作不能全部放在同一个GPU处理,因此作者把feature maps分给多个GPU分别进行处理,最后把多个GPU的结果进行融合。
分组卷积的思想影响比较深远,当前一些轻量级的SOTA(State Of The Art)网络,都用到了分组卷积的操作,以节省计算量。但题主有个疑问是,如果分组卷积是分在不同GPU上的话,每个GPU的计算量就降低到 1/groups,但如果依然在同一个GPU上计算,最终整体的计算量是否不变?
关于这个问题,知乎用户朋友 @蔡冠羽 提出了他的见解:
我感觉group conv本身应该就大大减少了参数,比如当input channel为256,output channel也为256,kernel size为33,不做group conv参数为25633256,若group为8,每个group的input channel和output channel均为32,参数为8323332,是原来的八分之一。这是我的理解。
我的理解是分组卷积最后每一组输出的feature maps应该是以concatenate的方式组合,而不是element-wise add,所以每组输出的channel是 input channels / #groups,这样参数量就大大减少了。
AlexNet中用到了一些非常大的卷积核,比如11×11、5×5卷积核,之前人们的观念是,卷积核越大,receptive field(感受野)越大,看到的图片信息越多,因此获得的特征越好。虽说如此,但是大的卷积核会导致计算量的暴增,不利于模型深度的增加,计算性能也会降低。于是在VGG(最早使用)、Inception网络中,利用2个3×3卷积核的组合比1个5×5卷积核的效果更佳,同时参数量(3×3×2+1 VS 5×5×1+1)被降低,因此后来3×3卷积核被广泛应用在各种模型中。
传统的层叠式网络,基本上都是一个个卷积层的堆叠,每层只用一个尺寸的卷积核,例如VGG结构中使用了大量的3×3卷积层。事实上,同一层feature map可以分别使用多个不同尺寸的卷积核,以获得不同尺度的特征,再把这些特征结合起来,得到的特征往往比使用单一卷积核的要好,谷歌的GoogleNet,或者说Inception系列的网络,就使用了多个卷积核的结构:
如上图所示,一个输入的feature map分别同时经过1×1、3×3、5×5的卷积核的处理,得出的特征再组合起来,获得更佳的特征。但这个结构会存在一个严重的问题:参数量比单个卷积核要多很多,如此庞大的计算量会使得模型效率低下。这就引出了一个新的结构:
发明GoogleNet的团队发现,如果仅仅引入多个尺寸的卷积核,会带来大量的额外的参数,受到Network In Network中1×1卷积核的启发,为了解决这个问题,他们往Inception结构中加入了一些1×1的卷积核,如图所示:
加入1×1卷积核的Inception结构
根据上图,我们来做个对比计算,假设输入feature map的维度为256维,要求输出维度也是256维。有以下两种操作:
1×1卷积核也被认为是影响深远的操作,往后大型的网络为了降低参数量都会应用上1×1卷积核。
传统的卷积层层叠网络会遇到一个问题,当层数加深时,网络的表现越来越差,很大程度上的原因是因为当层数加深时,梯度消散得越来越严重,以至于反向传播很难训练到浅层的网络。为了解决这个问题,何凯明大神想出了一个“残差网络”,使得梯度更容易地流动到浅层的网络当中去,而且这种“skip connection”能带来更多的好处,这里可以参考一个PPT:极深网络(ResNet/DenseNet): Skip Connection为何有效及其它 ,以及我的一篇文章:为什么ResNet和DenseNet可以这么深?一文详解残差块为何能解决梯度弥散问题。 ,大家可以结合下面的评论进行思考。
标准的卷积过程可以看上图,一个2×2的卷积核在卷积时,对应图像区域中的所有通道均被同时考虑,问题在于,为什么一定要同时考虑图像区域和通道?我们为什么不能把通道和空间区域分开考虑?
Xception网络就是基于以上的问题发明而来。我们首先对每一个通道进行各自的卷积操作,有多少个通道就有多少个过滤器。得到新的通道feature maps之后,这时再对这批新的通道feature maps进行标准的1×1跨通道卷积操作。这种操作被称为 “DepthWise convolution” ,缩写“DW”。
这种操作是相当有效的,在imagenet 1000类分类任务中已经超过了InceptionV3的表现,而且也同时减少了大量的参数,我们来算一算,假设输入通道数为3,要求输出通道数为256,两种做法:
直接接一个3×3×256的卷积核,参数量为:3×3×3×256 = 6,912
DW操作,分两步完成,参数量为:3×3×3 + 3×1×1×256 = 795,又把参数量降低到九分之一!
因此,一个depthwise操作比标准的卷积操作降低不少的参数量,同时论文中指出这个模型得到了更好的分类效果。
在AlexNet的Group Convolution当中,特征的通道被平均分到不同组里面,最后再通过两个全连接层来融合特征,这样一来,就只能在最后时刻才融合不同组之间的特征,对模型的泛化性是相当不利的。为了解决这个问题,ShuffleNet在每一次层叠这种Group conv层前,都进行一次channel shuffle,shuffle过的通道被分配到不同组当中。进行完一次group conv之后,再一次channel shuffle,然后分到下一层组卷积当中,以此循环。
经过channel shuffle之后,Group conv输出的特征能考虑到更多通道,输出的特征自然代表性就更高。另外,AlexNet的分组卷积,实际上是标准卷积操作,而在ShuffleNet里面的分组卷积操作是depthwise卷积,因此结合了通道洗牌和分组depthwise卷积的ShuffleNet,能得到超少量的参数以及超越mobilenet、媲美AlexNet的准确率!
另外值得一提的是,微软亚洲研究院MSRA最近也有类似的工作,他们提出了一个IGC单元(Interleaved Group Convolution),即通用卷积神经网络交错组卷积,形式上类似进行了两次组卷积,Xception 模块可以看作交错组卷积的一个特例,特别推荐看看这篇文章:王井东详解ICCV 2017入选论文:通用卷积神经网络交错组卷积
要注意的是,Group conv是一种channel分组的方式,Depthwise +Pointwise是卷积的方式,只是ShuffleNet里面把两者应用起来了。因此Group conv和Depthwise +Pointwise并不能划等号。
八、通道间的特征都是平等的吗? -- SEnet
无论是在Inception、DenseNet或者ShuffleNet里面,我们对所有通道产生的特征都是不分权重直接结合的,那为什么要认为所有通道的特征对模型的作用就是相等的呢? 这是一个好问题,于是,ImageNet2017 冠军SEnet就出来了。
一组特征在上一层被输出,这时候分两条路线,第一条直接通过,第二条首先进行Squeeze操作(Global Average Pooling),把每个通道2维的特征压缩成一个1维,从而得到一个特征通道向量(每个数字代表对应通道的特征)。然后进行Excitation操作,把这一列特征通道向量输入两个全连接层和sigmoid,建模出特征通道间的相关性,得到的输出其实就是每个通道对应的权重,把这些权重通过Scale乘法通道加权到原来的特征上(第一条路),这样就完成了特征通道的权重分配。作者详细解释可以看这篇文章:专栏 | Momenta详解ImageNet 2017夺冠架构SENet
九、能否让固定大小的卷积核看到更大范围的区域?-- Dilated convolution
标准的3×3卷积核只能看到对应区域3×3的大小,但是为了能让卷积核看到更大的范围,dilated conv使其成为了可能。dilated conv原论文中的结构如图所示:
上图b可以理解为卷积核大小依然是3×3,但是每个卷积点之间有1个空洞,也就是在绿色7×7区域里面,只有9个红色点位置作了卷积处理,其余点权重为0。这样即使卷积核大小不变,但它看到的区域变得更大了。详细解释可以看这个回答:如何理解空洞卷积(dilated convolution)?
传统的卷积核一般都是长方形或正方形,但MSRA提出了一个相当反直觉的见解,认为卷积核的形状可以是变化的,变形的卷积核能让它只看感兴趣的图像区域 ,这样识别出来的特征更佳。
要做到这个操作,可以直接在原来的过滤器前面再加一层过滤器,这层过滤器学习的是下一层卷积核的位置偏移量(offset),这样只是增加了一层过滤器,或者直接把原网络中的某一层过滤器当成学习offset的过滤器,这样实际增加的计算量是相当少的,但能实现可变形卷积核,识别特征的效果更好。详细MSRA的解读可以看这个链接:可变形卷积网络:计算机新“视”界。
现在越来越多的CNN模型从巨型网络到轻量化网络一步步演变,模型准确率也越来越高。现在工业界追求的重点已经不是准确率的提升(因为都已经很高了),都聚焦于速度与准确率的trade off,都希望模型又快又准。因此从原来AlexNet、VGGnet,到体积小一点的Inception、Resnet系列,到目前能移植到移动端的mobilenet、ShuffleNet(体积能降低到0.5mb!),我们可以看到这样一些趋势:
类比到通道加权操作,卷积层跨层连接能否也进行加权处理?
bottleneck + Group conv + channel shuffle + depthwise的结合会不会成为以后降低参数量的标准配置?
其实我们并不知道。因此我们需要更加科学客观的方法进行评估模型。
降低神经网络过拟合的方式有哪些?
我们凭什么知道我们的节点数和隐藏层个数的选择是合适的?
我们刚才只是直接是画出了分界线和相应的神经元组合。问题是,很多时候你并不知道样本真实的分布,并不知道需要多少层、多少个节点、之间的链接方式怎样。
我们怎么求出这些(超)参数呢?
假设我们已经知道了节点数和隐藏层数,我们怎么求这些神经元的权重和偏移呢?
最常用的最优化的方法是梯度下降法。求梯度就要求偏导数,但是你怎么求每一个权重和偏移的偏导数呢?
假设我们已经知道了传说中的BP算法,也就是反向传播算法来求偏导数。但是对于稍微深一点的神经网络,这种方法可能会求出一些很奇葩的偏导数,跟真实的偏导数完全不一样,导致整个模型崩溃。那么我们怎么处理、怎么预防这样的事情发生呢?
为什么神经网络这么难以训练?
除掉逻辑回归类型对应的sigmoid激励函数神经元,还有很多其他的激励函数,我们对不同的问题,怎么选择合适的激励函数?
神经网络还有其他的表示形式和可能性吗?
怎样选择不同的神经网络类型去解决不同的问题?
TensorBoard:TensorFlow集成可视化工具 GitHub官方项目:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/tensorboard
更多详细内容参考:
[ConvnetJS]http://cs.stanford.edu/people/karpathy/convnetjs/
文章来源:Towards Better Analysis of Deep Convolutional Neural Networks arxiv.org/abs/1604.07043
WEVI
官网:wevi: word embedding visual inspector
GitHub项目:https://github.com/ronxin/wevi