学深度学习也有一年多了,学的东西较为多且杂,最近打算从一些经典论文阅读与复现入手,一方面是给自己留个笔记,另一方面也给一些学深度学习的小白或者一些光学了理论不知道如何实践的朋友一个指引。本篇文章由于是面向小白的,所以后面代码讲解得会比较详细,希望初学者看完之后能理解并自己复现。
概述论文之前笔者啰嗦多几句,一些初学者可能喜欢上网看一些网上大牛讲解的一些网络结构进行学习,不是说这样不好,一些大牛会把一些晦涩的理论知识讲解的比较生动,但一些论文的细节可能就不会讲解到,笔者认为学习深度学习还是配合论文学习,要自己咬着牙去看论文,能收获到很多看别人讲解收获不到的知识,以下是英语不好的我我第一次看这篇文章配合欧路字典的惨烈结果,当然最后还是坚持啃了几遍,然后把这篇论文给复现了。
AlexNet的论文的创新点有以下几点:
接下来我们就这些创新点一点点说,再说一下AlexNet的网络结构,其他论文提到的细节的比如采取随机失活和数据增强策略来防止过拟合等我就不一一赘述了。
在此之前,一般选择tanh
作为激活函数,当使用梯度下降法训练的时候,这类饱和非线性激活函数,相对于非饱和非线性激活函数,会耗费更长的训练时间。(因为输入值处于饱和区时,梯度几乎为0,收敛极慢)
由图可知:采用ReLU的四层CNN(实线)在CIFAR-10数据集达到25%训练错误率是采用tanh的CNN(虚线)的六倍。每个网络的学习了都是独立选取以使其训练速度最大化,且都没有经过正则化处理。
单个GTX580 GPU 只有3GB显存,限制了能训练的神经网络的最大规模,于是作者将神经网络搭建于两个GPU上,使他们能够直接互相读取和写入显存而不需要经过主机内存,而且作者设置了一个机制,即两个GPU只在特定的层级中互相“交流”,就是某层接受前一层所有神经元的输出,而另一层只接受搭载于同一GPU上神经元的输出结果。具体的连接模式可以通过交叉验证来调整,但这一机制使得我们可以精准地调整连接数,使得其占总计算量的比例达到我们可以接受的程度。
有一种生物学的现象叫lateral inhibition
,指的是相近的神经元彼此之间发生的抑制作用,即在某个神经元受刺激而产生兴奋的时候,再刺激相近的神经元,则后者所发生的兴奋对前者产生的抑制作用,LRN就是利用了这一性质,其实LRN核心思想就是用相邻的数据进行归一化,公式如下:
其中:ax,yi表示第i个卷积核,作用于位置(x,y),N表示卷积核总数目,其他参数都是超参数,通过验证集确定具体的值。 LRN使得Top1和Top5错误率分别降低了1.4%和1.2%.但是现在LRN逐渐都被Batch Normalization取代了。
在普通的池化中,池化窗口z与滑动步长s相等。而重叠池化指的就是s<z的池化,此时相邻的滑动窗口之间就会有重叠,本文使用z=3,s=2,使得Top-1和Top-5误差率分别降低0.4%和0.3%,作者也发现使用这种形式可以降低过拟合。
AlexNet的网络结构如下图:
如上图所示,AlexNet包含8个层,前五层为卷积层,后面两层是全连接层,最后接上一个1000-way的softmax
层进行最后的分类。
模型中第二、四和五卷积层的卷积核只接收位于同一GPU上前一层的卷积核输出结果。而第三卷积层的卷积核接收前一层所有的卷积核(两个GPU)。全连接层的神经元也是接收前一层所有的输出结果。第一和第二卷积层后面各附带一个响应归一层。前面介绍的重叠池化层位于每一个响应归一层之后以及第五个卷积层之后。每一个卷积层和全连接层的输出结果都会经过ReLU非线性化处理。
AlexNet各个层的参数和结构如下:
输入层:227x227x3
C1:96x11x11x3
(卷积核个数/高/宽/深度)
C2:256x5x5x48
(卷积核个数/高/宽/深度)
C3:384x3x3x256
(卷积核个数/高/宽/深度)
C4:384x3x3x192
(卷积核个数/高/宽/深度)
C5:256x3x3x192
(卷积核个数/高/宽/深度)
224x224x3
。在数据增强部分作者有提到:作者是从原本大小256x256的图象中提取所有大小为224x224的子图像(以及他们的水平翻转图象)作为输入,但是我们可以从生成的卷积特征图倒推回去,生成的卷积特征图单元数为55x55x48x2,每个特征图尺度为55x55,由此可知输入图像尺度为227x227(55(单向尺度)x4(步长) + (11(卷积核尺度)-4(步长)) = 227)。因此输入图像单元数为227x227x3x1
。我们定义AlexNet的主结构之前,先定义一下各层。
channel/groups
,如果某层需要分开卷积再合并则传groups=2参数,将w和b用split进行切分,即把各自卷积核[Kheight, Kwidth, chanel/groups, num_filter]的第四个维度分为两半,分别进行卷积计算,然后使用tf.concat进行合并,合并之后加上偏置再激活。def maxPoolLayer(x, kHeight, kWidth, strideX, strideY, name, padding = "SAME"):
'''max-pooling Layer'''
return tf.nn.max_pool(x, ksize=[1, kHeight, kWidth, 1],
strides=[1, strideX, strideY, 1], padding=padding, name=name)
def dropout(x, keepPro, name = None):
'''dropout'''
return tf.nn.dropout(x, keepPro, name)
def LRN(x, R, alpha, beta, name = None, bias = 1.0):
'''LRN'''
return tf.nn.local_response_normalization(x, depth_radius= R, alpha = alpha,
beta = beta, bias = bias, name = name)
def fcLayer(x, inputD, outputD, reluFlage, name):
'''fully-connect Layer'''
with tf.variable_scope(name) as scoup:
w = tf.get_variable("w", shape = [inputD,outputD], dtype = "float")
b = tf.get_variable("b", shape = [outputD], dtype = "float")
out = tf.nn.xw_plus_b(x, w, b, name = scoup.name)
if reluFlage:
return tf.nn.relu(out)
else:
return out
def convLayer(x, kHeight, kWidth, strideX, strideY,featureNum, name,
padding = "SAME", groups = 1):
'''convolution Layer '''
channel = int(x.get_shape()[-1])
conv = lambda a,b: tf.nn.conv2d(a, b, strides = [1, strideX, strideY, 1], padding = padding)
with tf.variable_scope(name) as scope :
w = tf.get_variable("w", shape=[kHeight, kWidth, channel / groups, featureNum])
b = tf.get_variable("b", shape = [featureNum])
xNew = tf.split(value = x, num_or_size_splits = groups, axis = 3)
wNew = tf.split(value = w, num_or_size_splits = groups, axis = 3)
featureMap = [conv(t1,t2) for t1, t2 in zip(xNew, wNew)]
mergeFeatureMap = tf.concat(axis = 3, values = featureMap)
out = tf.nn.bias_add(mergeFeatureMap, b)
return tf.nn.relu(tf.reshape(out, mergeFeatureMap.get_shape().as_list(), name = scope.name))
定义好每一层后,我们就可以来定义AlexNet的主结构了。
bvlc_alexnet.npy
在http://www.cs.toronto.edu/~guerzhoy/tf_alexnet/这里下载finetune
操作class alexNet(object):
'''alexNet model'''
def __init__(self, x, keepPro, classNum, skip, modelPath = "bvlc_alexnet.npy"):
self.X = x
self.KEEPPRO = keepPro
self.CLASSNUM = classNum
self.SKIP = skip
self.MODELPATH = modelPath
self.bulidNet()
def bulidNet(self):
'''bulid model'''
conv1 = convLayer(self.X, 11, 11, 4, 4, 96, "conv1", "VALID")
lrn1 = LRN(conv1, 2, 2e-05, 0.75, "norm1")
pool1 = maxPoolLayer(lrn1, 3, 3, 2, 2, "pool", "VALID")
conv2 = convLayer(pool1, 5, 5, 1, 1, 256, "conv2", groups= 2)
lrn2 = LRN(conv2, 2, 2e-05, 0.75, "lrn2")
pool2 = maxPoolLayer(lrn2, 3, 3, 2, 2, "pool2", "VALID")
conv3 = convLayer(pool2, 3, 3, 1, 1, 384, "conv3")
conv4 = convLayer(conv3, 3, 3, 1, 1, 384, "conv4", groups = 2)
conv5 = convLayer(conv4, 3, 3, 1, 1, 256, "conv5", groups = 2)
pool5 = maxPoolLayer(conv5, 3, 3, 2, 2, "pool5", "VALID")
fcIn = tf.reshape(pool5, [-1,256 * 6 * 6])
fc1 = fcLayer(fcIn, 256 * 6 * 6, 4096, True, "fc6")
dropout1 = dropout(fc1, self.KEEPPRO)
fc2 = fcLayer(dropout1, 4096, 4096, True, "fc7")
dropout2 = dropout(fc2, self.KEEPPRO)
self.fc3 = fcLayer(dropout2, 4096, self.CLASSNUM, True, "fc8")
接着我们来定义读入参数文件的函数:
assign
函数进行赋值 def loadModel(self,sess):
'''load model'''
wDict = np.load(self.MODELPATH, encoding = "bytes").item()
for name in wDict:
if name not in self.SKIP:
with tf.variable_scope(name, reuse = True):
for p in wDict[name]:
if len(p.shape) == 1:
sess.run(tf.get_variable("b", trainable = False).assign(p))
else:
sess.run(tf.get_variable("w", trainable = False).assign(p))
定义好这些后我们就可以来定义主循环了:
argpase.ArgumentParser()
构建输入参数,参数分为图片本地途径和网络url地址。lamda
构建路径后,确定路径里确实存在文件,使用字典将读取结果进行组合;如果是url地址,用urllib.request.urlopen()
打开图片路径后,将读取图片转换为二进制,再转换为uint8类型的彩色图片。score
得分值,通过softmax
层获得概率值,传入预处理后的图片(减去均值)。caffe_classes
文件中获取类别名,最后将图片显示出来,标题改为类别名。'''add some argument'''
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description='Classify some images.',
epilog= "And that's the category")
parser.add_argument('-m', '--mode', choices=['folder', 'url'], default='folder')
parser.add_argument('-p', '--path', help='Specify a path [e.g. testModel]', default = 'testModel')
args = parser.parse_args(sys.argv[1:])
if args.mode == 'folder':
'''get image from folder'''
withPath = lambda f: '{}/{}'.format(args.path,f)
testImg = dict((f,cv2.imread(withPath(f))) for f in os.listdir(args.path) if os.path.isfile(withPath(f)))
elif args.mode == 'url':
def url2img(url):
'''get image from url'''
resp = urllib.request.urlopen(url)
image = np.asarray(bytearray(resp.read()), dtype="uint8")
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
return image
testImg = {args.path:url2img(args.path)}
if testImg.values():
#define some alexNet's params
dropoutPro = 1
classNum = 1000
skip = []
imgMean = np.array([104, 117, 124], np.float)
x = tf.placeholder("float", [1, 227, 227, 3])
model = alexnet.alexNet(x, dropoutPro, classNum, skip)
score = model.fc3
softmax = tf.nn.softmax(score)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
model.loadModel(sess)
for key,img in testImg.items():
'''img preprocess'''
resized = cv2.resize(img.astype(np.float), (227, 227)) - imgMean
maxx = np.argmax(sess.run(softmax, feed_dict = {x: resized.reshape((1, 227, 227, 3))}))
res = caffe_classes.class_names[maxx]
print("{}: {}\n----".format(key,res))
cv2.imshow("{}".format(res), img)
cv2.waitKey(0)
我们在终端输入python3 test.py -m folder -p testModel
,就可以看到结果(espresso:浓缩咖啡):
paper:http://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf
完整代码已上传到github:https://github.com/cristianoc20/AlexNet