目录
1.Inception-v1
1.1 Introduction
1.2 Inception结构
1.3 GoogLeNet
参考文献
2.Inception-v2
2.1 introduction
2.2 BN算法
参考文献
3.Inception-v3
3.1 General Design Principles
3.2 Factorizing Convolutions with Large Filter Size
3.3 其他思想
参考文献
4. Inception-v4
4.1 introduction
4.2 Inception-v4 结构
4.3 Inception-ResNet-v1 结构
4.4 Inception-ResNet-v2 结构
4.5 Inception-v4 TensorFlow实现源码(建议对照Inception-v4结构图食用):
总结
这里推荐一下这个GitHub,博主将常见的论文都做了翻译,大家可以参考中文来加深理解。
1.Inception-v1
1.1 Introduction
Inception V1是来源于《Going deeper with convolutions》,论文主要介绍了,如何在有限的计算资源内,进一步提升网络的性能。
提升网络的性能的方法有很多,例如硬件的升级,更大的数据集等。但一般而言,提升网络性能最直接的方法是增加网络的深度和宽度。其中,网络的深度只的是网络的层数,宽度指的是每层的通道数。但是,这种方法会带来两个不足:
a) 容易发生过拟合。当深度和宽度不断增加的时候,需要学习到的参数也不断增加,巨大的参数容易发生过拟合。
b) 均匀地增加网络的大小,会导致计算量的加大。
因此,解决上述不足的方法是引入稀疏特性和将全连接层转换成稀疏连接。这个思路的缘由来自于两方面:1)生物的神经系统连接是稀疏的;2)有文献指出:如果数据集的概率分布能够被大型且非常稀疏的DNN网络所描述的话,那么通过分析前面层的激活值的相关统计特性和将输出高度相关的神经元进行聚类,便可逐层构建出最优的网络拓扑结构。说明臃肿的网络可以被不失性能地简化。
但是,现在的计算框架对非均匀的稀疏数据进行计算是非常低效的,主要是因为查找和缓存的开销。因此,作者提出了一个想法,既能保持滤波器级别的稀疏特性,又能充分密集矩阵的高计算性能。有大量文献指出,将稀疏矩阵聚类成相对密集的子矩阵,能提高计算性能。根据此想法,提出了Inception结构。
1.2 Inception结构
inception结构的主要思路是:如何使用一个密集成分来近似或者代替最优的局部稀疏结构。inception V1的结构如下面两个图所示。
对于上图中的(a)做出几点解释:
a)采用不同大小的卷积核意味着不同大小的感受野,最后拼接意味着不同尺度特征融合;
b)之所以卷积核大小采用1、3和5,主要是为了方便对齐;
c)文章说很多地方都表明pooling挺有效,所以Inception里面也嵌入了;
d)网络越到后面,特征越抽象,而且每个特征所涉及的感受野也更大了,因此随着层数的增加,3x3和5x5卷积的比例也要增加。
上图为降维(dimension reductions)后的最终Inception-v1版本:优点:(1) 同时使用不同尺寸的卷积核可以提取到种类更加丰富的特征;(2) 使用稀疏矩阵分解为密集矩阵计算的原理,增加了收敛速度。但是,使用5x5的卷积核仍然会带来巨大的计算量。 为此,文章借鉴NIN,采用1x1卷积核来进行降维,如图中(b)所示。
例如:上一层的输出为100x100x128,经过具有256个输出的5x5卷积层之后(stride=1,pad=2),输出数据的大小为100x100x256。其中,卷积层的参数为5x5x128x256。假如上一层输出先经过具有32个输出的1x1卷积层,再经过具有256个输出的5x5卷积层,那么最终的输出数据的大小仍为100x100x256,但卷积参数量已经减少为1x1x128x32 + 5x5x32x256,大约减少了4倍。
在Inception结构中,大量采用了1x1的矩阵,主要是两点作用:
1)使用1*1的卷积核可以对模型进行降维,减少运算量。当一个卷积层输入了很多feature maps的时候,这个时候进行卷积运算计算量会非常大,如果先对输入进行降维操作,feature maps减少之后再进行卷积运算,运算量会大幅减少。
2)在大小相同的感受野上叠加更多的卷积核,可以让模型学习到更加丰富的特征。传统的卷积层的输入数据只和一种尺寸的卷积核进行运算,而Inception-v1结构是Network in Network(NIN),就是先进行一次普通的卷积运算(比如5*5),经过激活函数(比如ReLU)输出之后,然后再进行一次1*1的卷积运算,这个后面也跟着一个激活函数。1*1的卷积操作可以理解为feature maps个神经元都进行了一个全连接运算,引入更多的非线性,提高泛化能力。
Inception模块中,1*1、3*3、5*5的卷积核并不是固定的,可以根据实验进行调整。
1.3 GoogLeNet
GoogLeNet是由inception模块进行组成的,结构太大了,就不放出来了,这里做出几点说明:
a)GoogLeNet采用了模块化的结构,方便增添和修改;
b)网络最后采用了average pooling来代替全连接层,想法来自NIN,事实证明可以将TOP1 accuracy提高0.6%。但是,实际在最后还是加了一个全连接层,主要是为了方便以后大家finetune;
c)虽然移除了全连接,但是网络中依然使用了Dropout;
d)为了避免梯度消失,网络额外增加了2个辅助的softmax用于向前传导梯度。文章中说这两个辅助的分类器的loss应该加一个衰减系数,但看源码中的model也没有加任何衰减。此外,实际测试的时候,这两个额外的softmax会被去掉。
参考文献
[1] https://blog.csdn.net/qq_38906523/article/details/80061075
[2] Inception v1的TensorFlow源码:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/slim/python/slim/nets/inception_v1.py
2.Inception-v2
2.1 introduction
Inception v2来自于论文《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》训练DNN网络的一个难点是,在训练时每层输入数据的分布会发生改变,所以需要较低的学习率和精心设置初始化参数。只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。一旦网络某一层的输入数据的分布发生改变,那么这一层网络就需要去适应学习这个新的数据分布,所以如果训练过程中,训练数据的分布一直在发生变化,那么将会影响网络的训练速度。作者把网络中间层在训练过程中,数据分布的改变称之为:“Internal Covariate Shift”。因此,作者提出对数据做归一化的想法。
对数据进行了BN算法后,具有以下的优点:
a)可以设置较大的初始学习率,并且减少对参数初始化的依赖,提高了训练速度;
b)这是个正则化模型,因此可以去除dropout和降低L2正则约束参数;
c)不需要局部响应归一化层;
d)能防止网络陷入饱和,即消除梯度弥散。
Inception-v2:有三种形式,论文截图如下所示:
Figure 5:参考VGG,用两个3*3的卷积核代替5*5的大卷积核,这样在保持相同感受野的同时减少了参数,而且加强了非线性表达能力,还可以提升速度。
Figure 6:引入了factorization into asymmetric convolutions的思想,就是用两个 1*n 和 n*1 的卷积核替换一个较大的n*n卷积核。这种分解方法减少了大量参数,并且可以提高运算速度,减轻过拟合,同时给模型增加了一层非线性结构,提升了模型的表达能力,让模型可以处理更丰富的空间特征,增加了特征的多样性。但经过试验发现,在网络的前期用这种分解效果并不好,而且这种分解在中等大小的特征图上使用效果最好,如n=7。
Figure 7:模块中的滤波器组(filter banks)被扩展(使得更宽而不是更深),以消除representational bottleneck(降低representational bottleneck:其思路是,当卷积不会大幅改变输入尺寸,神经网络的性能会更好。减少维度会造成信息大量损失,也就是所说的 representational bottleneck)。如果模块变得更深,尺度将会过度缩小,从而导致信息的丢失。较适合于高维特征。
2.2 BN算法
BN算法通过下面公式,对某一层进行归一化处理,也叫近似白化预处理:
其中,由于我们是采用批量梯度下降法的,所以E[x(k)]是指在一批数据中,各神经元的平均值;Var(x(k))是指在一批训练数据时各神经元输入值的标准差。
如果是仅仅使用上面的归一化公式,对网络某一层A的输出数据做归一化,然后送入网络下一层B,这样是会影响到本层网络A所学习到的特征的。打个比方,比如我网络中间某一层学习到特征数据本身就分布在S型激活函数的两侧,你强制把它给我归一化处理、标准差也限制在了1,把数据变换成分布于s函数的中间部分,这样就相当于我这一层网络所学习到的特征分布被搞坏了。
于是,提出了“变换重构”,引入了可学习参数γ和β:
每一个神经元x(k)都会有这样的一对参数γ和β。当γ(k)=√Var(x(k))和β(k)=E[x(k)]时,是可以恢复出原始的某一层所学到的特征的。
通过上面的学习,我们知道BN层是对于每个神经元做归一化处理,甚至只需要对某一个神经元进行归一化,而不是对一整层网络的神经元进行归一化。既然BN是对单个神经元的运算,那么在CNN中卷积层上要怎么搞?假如某一层卷积层有6个特征图,每个特征图的大小是100*100,这样就相当于这一层网络有6*100*100个神经元,如果采用BN,就会有6*100*100个参数γ、β,这样岂不是太恐怖了。因此卷积层上的BN使用,其实也是使用了类似权值共享的策略,把一整张特征图当做一个神经元进行处理。
卷积神经网络经过卷积后得到的是一系列的特征图,如果min-batch sizes为m,那么网络某一层输入数据可以表示为四维矩阵(m,f,p,q),m为min-batch sizes,f为特征图个数,p、q分别为特征图的宽高。在cnn中我们可以把每个特征图看成是一个特征处理(一个神经元),因此在使用Batch Normalization,mini-batch size 的大小就是:m*p*q,于是对于每个特征图都只有一对可学习参数:γ、β。说白了吧,这就是相当于求取所有样本所对应的一个特征图的所有神经元的平均值、方差,然后对这个特征图神经元做归一化。
Inception v2模型相对于Inception v1模型的改进为:
加入了BN层,减少了Internal Covariate Shift(内部neuron的数据分布发生变化),通过一定的手段,把每层神经元的输入值分布拉到均值0方差1的正态分布,使其落入激活函数的敏感区,避免梯度消失,加快收敛;
学习VGG的模型架构,用2个3x3的conv替代inception模块中的5x5,既降低了参数数量,也加速计算,能够减少参数,另一方面相当于进行了更多的非线性映射,可以增加网络的拟合/表达能力,结构简洁,层数更深、特征图更宽;
参考文献
[1] https://blog.csdn.net/qq_26898461/article/details/51221166
[2] https://m.dandelioncloud.cn/article/details/1584890031158579202
3.Inception-v3
Inception v3是来源于论文《Rethinking the Inception Architecture for Computer Vision》,主要是引入了因子分解的思想。
3.1 General Design Principles
复杂的inception结构,使得我们很难对网络进行修改。如果盲目的扩大网络,反而会增大计算量。所以,本论文首先给出了几条通用的原则和优化的思想:
-
避免特征表示瓶颈,尤其是在网络的前面。要避免严重压缩导致的瓶颈。特征表示尺寸应该温和地减少,从输入端到输出端。特征表示的维度只是一个粗浅的信息量表示,它丢掉了一些重要的因素如相关性结构。
-
高维信息更适合在网络的局部处理。在卷积网络中逐步增加非线性激活响应可以解耦合更多的特征,那么网络就会训练的更快。
-
空间聚合可以通过低维嵌入,不会导致网络表示能力的降低。例如在进行大尺寸的卷积(如3*3)之前,我们可以在空间聚合前先对输入信息进行降维处理,如果这些信号是容易压缩的,那么降维甚至可以加快学习速度。
-
平衡好网络的深度和宽度。通过平衡网络每层滤波器的个数和网络的层数可以是网络达到最佳性能。增加网络的宽度和深度都会提升网络的性能,但是两者并行增加获得的性能提升是最大的。所以计算资源应该被合理的分配到网络的宽度和深度。
3.2 Factorizing Convolutions with Large Filter Size
大尺度滤波器的卷积(如5*5,7*7)的引入,会产生大量的计算。例如一个5*5的卷积比一个3*3卷积滤波器多25/9=2.78倍计算量。当然5*5滤波器可以学习到更多的信息。那么我们能不能使用一个多层感知器来代替这个5*5卷积滤波器。
因此,提出了使用两个级联的3*3的滤波器来代替一个5*5的滤波器,这样节省了(5*5)/(2*3*3)=1.39被计算量,如下图的左图所示。因此,inception v1中的模块可以被替换成下图的右图所示。
受到上面的启发,又进一步对卷进进一步分解,将3*3的卷积核分解为3*1+1*3,如下图所示。这样,又能进一步降低计算量。因此,一个n*n的卷积可以被1*n和n*1的卷积所代替。但实际上,在网络模型的前期使用这样的卷积分解,并不能达到一个很好的效果。通过在网络中期使用,在特征图的尺寸为12-20左右使用最佳。
3.3 其他思想
-
利用辅助决策分支,来加快收敛速度。在inception-v1中,引入了辅助决策分支。但是本文证明了,底层的辅助决策分支并不能起到很好的作用。如果辅助决策分支进行归一化或者dropout,效果会更好。
-
利用平行的池化与卷积,来进行特征图尺寸缩小,不仅能较少计算量,又能防止特征瓶颈,如下图所示。
- 提出了Label Smoothing
Inception v3模型相对于Inception v2模型的改进为:
a)RMSProp优化器(自适应学习率)。
b)分解为7*7卷积。
c)辅助分类器使用了 BatchNorm。
d)标签平滑(Label Smoothing,添加到损失公式的一种正则化项,旨在阻止网络对某一类别过分自信,即阻止过拟合)。
e)Inception V3一个最重要的改进是分解(Factorization),将7x7分解成两个一维的卷积(1x7,7x1),3x3也是一样(1x3,3x1),这样的好处,既可以加速计算,又可以将1个卷积拆成2个卷积,使得网络深度进一步增加,增加了网络的非线性(每增加一层都要进行ReLU)。
f)另外,网络输入从224x224变为了299x299。
参考文献
[1] https://www.cnblogs.com/vincentqliu/p/7467298.html
4. Inception-v4
Inception V4来自于论文《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》,主要提出了新的Inception结构,并且结合ResNet网络提出了Inception-ResNet-v1和Inception-ResNet-v2。
4.1 introduction
文章中提出了一个疑问:当网络更深更宽时,inception网络能否一样高效。基于这个想法,将inception和resnet两者进行融合,进一步改善网络。由于TensorFlow的出现,能大大简化训练,不需要将模型进行分割。因此,google采取了更加大胆的设计方法,提出了inception v4,其具有更加统一的inception结构。
4.2 Inception-v4 结构
4.3 Inception-ResNet-v1 结构
4.4 Inception-ResNet-v2 结构
可以看出,这3种结构还是相当复杂的,即使比之前的inception模块统一了很多。
4.5 Inception-v4 TensorFlow实现源码(建议对照Inception-v4结构图食用):
#python3
#modules.py for Inception-v4
import numpy as np
import tensorflow as tf
def stem(inputs,
scope='Stem'):
'''
Stem for Inception-v4 and Inception-ResNet-v2
Figure 3
'''
with tf.variable_scope(scope):
x = inputs
#conv1
with tf.variable_scope('conv1'):
x = tf.layers.conv2d(x, 32, [3,3], 2, padding='valid')
#conv2
with tf.variable_scope('conv2'):
x = tf.layers.conv2d(x, 32, [3,3], 1, padding='valid')
#conv3
with tf.variable_scope('conv3'):
x = tf.layers.conv2d(x, 64, [3,3], 1, padding='same')
#sub1
with tf.variable_scope('sub1'):
sub1 = tf.layers.max_pooling2d(x, [3,3], 2, padding='valid')
sub2 = tf.layers.conv2d(x, 96, [3,3], 2, padding='valid')
x = tf.concat([sub1,sub2], axis=-1)
#sub2
with tf.variable_scope('sub2'):
sub1 = tf.layers.conv2d(x, 64, [1,1], 1, padding='same')
sub1 = tf.layers.conv2d(sub1, 96, [3,3], 1, padding='valid')
sub2 = tf.layers.conv2d(x, 64, [1,1], 1, padding='same')
sub2 = tf.layers.conv2d(sub2, 64, [7,1], 1, padding='same')
sub2 = tf.layers.conv2d(sub2, 64, [1,7], 1, padding='same')
sub2 = tf.layers.conv2d(sub2, 96, [3,3], 1, padding='valid')
x = tf.concat([sub1,sub2], axis=-1)
#sub3
with tf.variable_scope('sub3'):
sub1 = tf.layers.conv2d(x, 192, [3,3], 2, padding='valid')
sub2 = tf.layers.max_pooling2d(x, [3,3], 2, padding='valid')
x = tf.concat([sub1,sub2], axis=-1)
return x
def inception_a(inputs,
scope='Inception-A'):
'''
Inception-A for Inception-v4
Figure 4
'''
with tf.variable_scope(scope):
x = inputs
sub1 = tf.layers.average_pooling2d(x, [3,3], 1, padding='same')
sub1 = tf.layers.conv2d(sub1, 96, [1,1], 1, padding='same')
sub2 = tf.layers.conv2d(x, 96, [1,1], 1, padding='same')
sub3 = tf.layers.conv2d(x, 64, [1,1], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, 96, [3,3], 1, padding='same')
sub4 = tf.layers.conv2d(x, 64, [1,1], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 96, [3,3], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 96, [3,3], 1, padding='same')
x = tf.concat([sub1,sub2,sub3,sub4], axis=-1)
return x
def inception_b(inputs,
scope='Inception-B'):
'''
Inception-B for Inception-v4
Figure 5
'''
with tf.variable_scope(scope):
x = inputs
sub1 = tf.layers.average_pooling2d(x, [3,3], 1, padding='same')
sub1 = tf.layers.conv2d(sub1, 128, [1,1], 1, padding='same')
sub2 = tf.layers.conv2d(x, 384, [1,1], 1, padding='same')
sub3 = tf.layers.conv2d(x, 192, [1,1], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, 224, [1,7], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, 256, [7,1], 1, padding='same')
sub4 = tf.layers.conv2d(x, 192, [1,1], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 192, [1,7], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 224, [7,1], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 224, [1,7], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 256, [7,1], 1, padding='same')
x = tf.concat([sub1,sub2,sub3,sub4], axis=-1)
return x
def inception_c(inputs,
scope='Inception-C'):
'''
Inception-C for Inception-v4
Figure 6
'''
sub = []
with tf.variable_scope(scope):
x = inputs
sub1 = tf.layers.average_pooling2d(x, [3,3], 1, padding='same')
sub1 = tf.layers.conv2d(sub1, 256, [1,1], 1, padding='same')
sub.append(sub1)
sub2 = tf.layers.conv2d(x, 256, [1,1], 1, padding='same')
sub.append(sub2)
sub3 = tf.layers.conv2d(x, 384, [1,1], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, 256, [1,3], 1, padding='same')
sub.append(sub3)
sub3 = tf.layers.conv2d(sub3, 256, [3,1], 1, padding='same')
sub.append(sub3)
sub4 = tf.layers.conv2d(x, 384, [1,1], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 448, [1,3], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 512, [3,1], 1, padding='same')
sub4 = tf.layers.conv2d(sub4, 256, [3,1], 1, padding='same')
sub.append(sub4)
sub4 = tf.layers.conv2d(sub4, 256, [1,3], 1, padding='same')
sub.append(sub4)
x = tf.concat(sub, axis=-1)
return x
def reduction_a(inputs,
params,
scope='Reduction-A'):
'''
Reduction-A
Figure 7
'''
[k,l,m,n] = params
with tf.variable_scope(scope):
x = inputs
sub1 = tf.layers.max_pooling2d(x, [3,3], 2, padding='valid')
sub2 = tf.layers.conv2d(x, n, [3,3], 2, padding='valid')
sub3 = tf.layers.conv2d(x, k, [1,1], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, l, [3,3], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, m, [3,3], 2, padding='valid')
x = tf.concat([sub1,sub2,sub3], axis=-1)
return x
def reduction_b(inputs,
scope='Reduction-B'):
'''
Reduction-B for Inception-v4
Figure 7
'''
with tf.variable_scope(scope):
x = inputs
sub1 = tf.layers.max_pooling2d(x, [3,3], 2, padding='valid')
sub2 = tf.layers.conv2d(x, 192, [1,1], 1, padding='same')
sub2 = tf.layers.conv2d(sub2, 192, [3,3], 2, padding='valid')
sub3 = tf.layers.conv2d(x, 256, [1,1], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, 256, [1,7], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, 320, [7,1], 1, padding='same')
sub3 = tf.layers.conv2d(sub3, 320, [3,3], 2, padding='valid')
x = tf.concat([sub1,sub2,sub3], axis=-1)
return x
#python3
#Inference for Inception-v4
import numpy as np
import tensorflow as tf
import modules as modules
def print_activation(x):
print(x.op.name, x.get_shape().as_list())
def inferene(inputs,
scope='inference'):
with tf.variable_scope(scope):
x = inputs
#Stem
with tf.variable_scope('Stem'):
x = modules.stem(x, scope='Stem')
#Inception-A-x
with tf.variable_scope('Inception-A-x'):
for i in range(4):
x = modules.inception_a(x, scope='Inception-A-'+str(i))
#Reduction-A
with tf.variable_scope('Reduction-A'):
x = modules.reduction_a(x, [192,224,256,384], scope='Reduction-A')
#Inception-B-x
with tf.variable_scope('Inception-B-x'):
for i in range(7):
x = modules.inception_b(x, scope='Inception-B-'+str(i))
#Reduction-B
with tf.variable_scope('Reduction-B'):
x = modules.reduction_b(x, scope='Reduction-B')
#Inception-C-x
with tf.variable_scope('Inception-C-x'):
for i in range(3):
x = modules.inception_c(x, scope='Inception-C-'+str(i))
#Average Pooling
with tf.variable_scope('Average_Pooling'):
x = tf.layers.average_pooling2d(x, [8,8], 1, padding='same')
#Dropout
with tf.variable_scope('Dropout'):
x = tf.layers.dropout(x, rate=0.2)
#Softmax
with tf.variable_scope('Softmax'):
logits = tf.layers.conv2d(x,1000,[1,1],1,padding='same')
return logits
inputs = tf.placeholder(tf.float32, [None,299,299,3])
y = inferene(inputs)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
tf.summary.FileWriter('log/', sess.graph)
总结
inception是通过增加网络的宽度来提高网络性能,在每个inception模块中,使用了不同大小的卷积核,可以理解成不同的感受野,然后将其concentrate起来,丰富了每层的信息。之后,使用了BN算法(BN使用在conv之后,relu之前),来加速网络的收敛速度。在V3版本中,还使用了卷积因子分解的思想,将大卷积核分解成小卷积,节省了参数,降低了模型大小。在V4版本中,使用了更加统一的inception模块,并结合了resnet的残差思想,能将网络做得更深。
本文详细呈现了三种新的网络结构:
Inception-ResNet-v1:混合Inception版本,它的计算效率约同Inception-v3;
Inception-ResNet-v2:更加昂贵的混合Inception版本,明显改善了识别性能;
Inception-v4:没有残差链接的纯净Inception变种,性能如同Inception-ResNet-v2
我们研究了引入残差连接如何显著的提高inception网络的训练速度。而且仅仅凭借增加的模型尺寸,我们的最新的模型(带和不带残差连接)都优于我们以前的网络。
参考:
(3条消息) Inception v4, Inception-ResNet 论文笔记_黑暗星球的博客-CSDN博客
Inception网络模型 - 啊顺 - 博客园 (cnblogs.com)