前面讲了LeNet、AlexNet和Vgg,这周来讲讲GoogLeNet。GoogLeNet是由google的Christian Szegedy等人在2014年的论文《Going Deeper with Convolutions》提出,其最大的亮点是提出一种叫Inception的结构,以此为基础构建GoogLeNet,并在当年的ImageNet分类和检测任务中获得第一,ps:GoogLeNet的取名是为了向YannLeCun的LeNet系列致敬。

(本系列所有代码均在github:https://github.com/huxiaoman7/PaddlePaddle_code)


关于深度网络的一些思考

  在本系列最开始的几篇文章我们讲到了卷积神经网络,设计的网络结构也非常简单,属于浅层神经网络,如三层的卷积神经网络等,但是在层数比较少的时候,有时候效果往往并没有那么好,在实验过程中发现,当我们尝试增加网络的层数,或者增加每一层网络的神经元个数的时候,对准确率有一定的提升,简单的说就是增加网络的深度与宽度,但这样做有两个明显的缺点:

  • 更深更宽的网络意味着更多的参数,提高了模型的复杂度,从而大大增加过拟合的风险,尤其在训练数据不是那么多或者某个label训练数据不足的情况下更容易发生;
  • 增加计算资源的消耗,实际情况下,不管是因为数据稀疏还是扩充的网络结构利用不充分(比如很多权重接近0),都会导致大量计算的浪费。

  解决以上两个问题的基本方法是将全连接或卷积连接改为稀疏连接。不管从生物的角度还是机器学习的角度,稀疏性都有良好的表现,回想一下在讲AlexNet这一节提出的Dropout网络以及ReLU激活函数,其本质就是利用稀疏性提高模型泛化性(但需要计算的参数没变少)。 
  简单解释下稀疏性,当整个特征空间是非线性甚至不连续时:

  • 学好局部空间的特征集更能提升性能,类似于Maxout网络中使用多个局部线性函数的组合来拟合非线性函数的思想;
  • 假设整个特征空间由N个不连续局部特征空间集合组成,任意一个样本会被映射到这N个空间中并激活/不激活相应特征维度,如果用C1表示某类样本被激活的特征维度集合,用C2表示另一类样本的特征维度集合,当数据量不够大时,要想增加特征区分度并很好的区分两类样本,就要降低C1和C2的重合度(比如可用Jaccard距离衡量),即缩小C1和C2的大小,意味着相应的特征维度集会变稀疏。

  不过尴尬的是,现在的计算机体系结构更善于稠密数据的计算,而在非均匀分布的稀疏数据上的计算效率极差,比如稀疏性会导致的缓存miss率极高,于是需要一种方法既能发挥稀疏网络的优势又能保证计算效率。好在前人做了大量实验(如《On Two-Dimensional Sparse Matrix Partitioning: Models, Methods, and a Recipe》),发现对稀疏矩阵做聚类得到相对稠密的子矩阵可以大幅提高稀疏矩阵乘法性能,借鉴这个思想,作者提出Inception的结构。

 图1 Inception结构

  • 把不同大小卷积核抽象得到的特征空间看做子特征空间,每个子特征空间都是稀疏的,把这些不同尺度特征做融合,相当于得到一个相对稠密的空间;
  • 采用1×1、3×3、5×5卷积核(不是必须的,也可以是其他大小),stride取1,利用padding可以方便的做输出特征维度对齐;
  • 大量事实表明pooling层能有效提高卷积网络的效果,所以加了一条max pooling路径;
  • 这个结构符合直观理解,视觉信息通过不同尺度的变换被聚合起来作为下一阶段的特征,比如:人的高矮、胖瘦、青老信息被聚合后做下一步判断。

这个网络的最大问题是5×5卷积带来了巨大计算负担,例如,假设上层输入为:28×28×192:

  • 直接经过96个5×5卷积层(stride=1,padding=2)后,输出为:28×28×96,卷积层参数量为:192×5×5×96=460800;
  • 借鉴NIN网络(Network in Network,后续会讲),在5×5卷积前使用32个1×1卷积核做维度缩减,变成28×28×32,之后经过96个5×5卷积层(stride=1,padding=2)后,输出为:28×28×96,但所有卷积层的参数量为:192×1×1×32+32×5×5×96=82944,可见整个参数量是原来的1/5.5,且效果上没有多少损失。 
    新网络结构为

 图2 新Inception结构


 GoogLeNet网络结构

  利用上述Inception模块构建GoogLeNet,实验表明Inception模块出现在高层特征抽象时会更加有效(我理解由于其结构特点,更适合提取高阶特征,让它提取低阶特征会导致特征信息丢失),所以在低层依然使用传统卷积层。整个网路结构如下:

图3 GoogLeNet网络结构

 图4 GoogLeNet详细网络结构示意图

网络说明:

  • 所有卷积层均使用ReLU激活函数,包括做了1×1卷积降维后的激活;
  • 移除全连接层,像NIN一样使用Global Average Pooling,使得Top 1准确率提高0.6%,但由于GAP与类别数目有关系,为了方便大家做模型fine-tuning,最后加了一个全连接层;
  • 与前面的ResNet类似,实验观察到,相对浅层的神经网络层对模型效果有较大的贡献,训练阶段通过对Inception(4a、4d)增加两个额外的分类器来增强反向传播时的梯度信号,但最重要的还是正则化作用,这一点在GoogLeNet v3中得到实验证实,并间接证实了GoogLeNet V2中BN的正则化作用,这两个分类器的loss会以0.3的权重加在整体loss上,在模型inference阶段,这两个分类器会被去掉;
  • 用于降维的1×1卷积核个数为128个;
  • 全连接层使用1024个神经元;
  • 使用丢弃概率为0.7的Dropout层;

网络结构详细说明:

  输入数据为224×224×3的RGB图像,图中"S"代表做same-padding,"V"代表不做。

  • C1卷积层:64个7×7卷积核(stride=2,padding=3),输出为:112×112×64;
  • P1抽样层:64个3×3卷积核(stride=2),输出为56×56×64,其中:56=(112-3+1)/2+1
  • C2卷积层:192个3×3卷积核(stride=1,padding=1),输出为:56×56×192;
  • P2抽样层:192个3×3卷积核(stride=2),输出为28×28×192,其中:28=(56-3+1)/2+1,接着数据被分出4个分支,进入Inception (3a)
  • Inception (3a):由4部分组成 
    • 64个1×1的卷积核,输出为28×28×64;
    • 96个1×1的卷积核做降维,输出为28×28×96,之后128个3×3卷积核(stride=1,padding=1),输出为:28×28×128
    • 16个1×1的卷积核做降维,输出为28×28×16,之后32个5×5卷积核(stride=1,padding=2),输出为:28×28×32
    • 192个3×3卷积核(stride=1,padding=1),输出为28×28×192,进行32个1×1卷积核,输出为:28×28×32 
      最后对4个分支的输出做“深度”方向组合,得到输出28×28×256,接着数据被分出4个分支,进入Inception (3b);
  • Inception (3b):由4部分组成 
    • 128个1×1的卷积核,输出为28×28×128;
    • 128个1×1的卷积核做降维,输出为28×28×128,进行192个3×3卷积核(stride=1,padding=1),输出为:28×28×192
    • 32个1×1的卷积核做降维,输出为28×28×32,进行96个5×5卷积核(stride=1,padding=2),输出为:28×28×96
    • 256个3×3卷积核(stride=1,padding=1),输出为28×28×256,进行64个1×1卷积核,输出为:28×28×64 
      最后对4个分支的输出做“深度”方向组合,得到输出28×28×480; 
      后面结构以此类推。

用PaddlePaddle实现GoogLeNet

  1.网络结构 googlenet.py

  在PaddlePaddle的models下面,有关于GoogLeNet的实现代码,大家可以直接学习拿来跑一下:

  1. import paddle.v2 as paddle
  2.  
  3. __all__ = ['googlenet']
  4.  
  5. def inception(name, input, channels, filter1, filter3R, filter3, filter5R,
  6. filter5, proj):
  7. cov1 = paddle.layer.img_conv(
  8. name=name + '_1',
  9. input=input,
  10. filter_size=1,
  11. num_channels=channels,
  12. num_filters=filter1,
  13. stride=1,
  14. padding=0)
  15.  
  16. cov3r = paddle.layer.img_conv(
  17. name=name + '_3r',
  18. input=input,
  19. filter_size=1,
  20. num_channels=channels,
  21. num_filters=filter3R,
  22. stride=1,
  23. padding=0)
  24. cov3 = paddle.layer.img_conv(
  25. name=name + '_3',
  26. input=cov3r,
  27. filter_size=3,
  28. num_filters=filter3,
  29. stride=1,
  30. padding=1)
  31.  
  32. cov5r = paddle.layer.img_conv(
  33. name=name + '_5r',
  34. input=input,
  35. filter_size=1,
  36. num_channels=channels,
  37. num_filters=filter5R,
  38. stride=1,
  39. padding=0)
  40. cov5 = paddle.layer.img_conv(
  41. name=name + '_5',
  42. input=cov5r,
  43. filter_size=5,
  44. num_filters=filter5,
  45. stride=1,
  46. padding=2)
  47.  
  48. pool1 = paddle.layer.img_pool(
  49. name=name + '_max',
  50. input=input,
  51. pool_size=3,
  52. num_channels=channels,
  53. stride=1,
  54. padding=1)
  55. covprj = paddle.layer.img_conv(
  56. name=name + '_proj',
  57. input=pool1,
  58. filter_size=1,
  59. num_filters=proj,
  60. stride=1,
  61. padding=0)
  62.  
  63. cat = paddle.layer.concat(name=name, input=[cov1, cov3, cov5, covprj])
  64. return cat
  65.  
  66. def googlenet(input, class_dim):
  67. # stage 1
  68. conv1 = paddle.layer.img_conv(
  69. name="conv1",
  70. input=input,
  71. filter_size=7,
  72. num_channels=3,
  73. num_filters=64,
  74. stride=2,
  75. padding=3)
  76. pool1 = paddle.layer.img_pool(
  77. name="pool1", input=conv1, pool_size=3, num_channels=64, stride=2)
  78.  
  79. # stage 2
  80. conv2_1 = paddle.layer.img_conv(
  81. name="conv2_1",
  82. input=pool1,
  83. filter_size=1,
  84. num_filters=64,
  85. stride=1,
  86. padding=0)
  87. conv2_2 = paddle.layer.img_conv(
  88. name="conv2_2",
  89. input=conv2_1,
  90. filter_size=3,
  91. num_filters=192,
  92. stride=1,
  93. padding=1)
  94. pool2 = paddle.layer.img_pool(
  95. name="pool2", input=conv2_2, pool_size=3, num_channels=192, stride=2)
  96.  
  97. # stage 3
  98. ince3a = inception("ince3a", pool2, 192, 64, 96, 128, 16, 32, 32)
  99. ince3b = inception("ince3b", ince3a, 256, 128, 128, 192, 32, 96, 64)
  100. pool3 = paddle.layer.img_pool(
  101. name="pool3", input=ince3b, num_channels=480, pool_size=3, stride=2)
  102.  
  103. # stage 4
  104. ince4a = inception("ince4a", pool3, 480, 192, 96, 208, 16, 48, 64)
  105. ince4b = inception("ince4b", ince4a, 512, 160, 112, 224, 24, 64, 64)
  106. ince4c = inception("ince4c", ince4b, 512, 128, 128, 256, 24, 64, 64)
  107. ince4d = inception("ince4d", ince4c, 512, 112, 144, 288, 32, 64, 64)
  108. ince4e = inception("ince4e", ince4d, 528, 256, 160, 320, 32, 128, 128)
  109. pool4 = paddle.layer.img_pool(
  110. name="pool4", input=ince4e, num_channels=832, pool_size=3, stride=2)
  111.  
  112. # stage 5
  113. ince5a = inception("ince5a", pool4, 832, 256, 160, 320, 32, 128, 128)
  114. ince5b = inception("ince5b", ince5a, 832, 384, 192, 384, 48, 128, 128)
  115. pool5 = paddle.layer.img_pool(
  116. name="pool5",
  117. input=ince5b,
  118. num_channels=1024,
  119. pool_size=7,
  120. stride=7,
  121. pool_type=paddle.pooling.Avg())
  122. dropout = paddle.layer.addto(
  123. input=pool5,
  124. layer_attr=paddle.attr.Extra(drop_rate=0.4),
  125. act=paddle.activation.Linear())
  126.  
  127. out = paddle.layer.fc(
  128. input=dropout, size=class_dim, act=paddle.activation.Softmax())
  129.  
  130. # fc for output 1
  131. pool_o1 = paddle.layer.img_pool(
  132. name="pool_o1",
  133. input=ince4a,
  134. num_channels=512,
  135. pool_size=5,
  136. stride=3,
  137. pool_type=paddle.pooling.Avg())
  138. conv_o1 = paddle.layer.img_conv(
  139. name="conv_o1",
  140. input=pool_o1,
  141. filter_size=1,
  142. num_filters=128,
  143. stride=1,
  144. padding=0)
  145. fc_o1 = paddle.layer.fc(
  146. name="fc_o1",
  147. input=conv_o1,
  148. size=1024,
  149. layer_attr=paddle.attr.Extra(drop_rate=0.7),
  150. act=paddle.activation.Relu())
  151. out1 = paddle.layer.fc(
  152. input=fc_o1, size=class_dim, act=paddle.activation.Softmax())
  153.  
  154. # fc for output 2
  155. pool_o2 = paddle.layer.img_pool(
  156. name="pool_o2",
  157. input=ince4d,
  158. num_channels=528,
  159. pool_size=5,
  160. stride=3,
  161. pool_type=paddle.pooling.Avg())
  162. conv_o2 = paddle.layer.img_conv(
  163. name="conv_o2",
  164. input=pool_o2,
  165. filter_size=1,
  166. num_filters=128,
  167. stride=1,
  168. padding=0)
  169. fc_o2 = paddle.layer.fc(
  170. name="fc_o2",
  171. input=conv_o2,
  172. size=1024,
  173. layer_attr=paddle.attr.Extra(drop_rate=0.7),
  174. act=paddle.activation.Relu())
  175. out2 = paddle.layer.fc(
  176. input=fc_o2, size=class_dim, act=paddle.activation.Softmax())
  177.  
  178. return out, out1, out2

  2.训练模型

  1. import gzip
  2. import paddle.v2.dataset.flowers as flowers
  3. import paddle.v2 as paddle
  4. import reader
  5. import vgg
  6. import resnet
  7. import alexnet
  8. import googlenet
  9. import argparse
  10.  
  11. DATA_DIM = 3 * 224 * 224
  12. CLASS_DIM = 102
  13. BATCH_SIZE = 128
  14.  
  15. def main():
  16. # parse the argument
  17. parser = argparse.ArgumentParser()
  18. parser.add_argument(
  19. 'model',
  20. help='The model for image classification',
  21. choices=['alexnet', 'vgg13', 'vgg16', 'vgg19', 'resnet', 'googlenet'])
  22. args = parser.parse_args()
  23.  
  24. # PaddlePaddle init
  25. paddle.init(use_gpu=True, trainer_count=7)
  26.  
  27. image = paddle.layer.data(
  28. name="image", type=paddle.data_type.dense_vector(DATA_DIM))
  29. lbl = paddle.layer.data(
  30. name="label", type=paddle.data_type.integer_value(CLASS_DIM))
  31.  
  32. extra_layers = None
  33. learning_rate = 0.01
  34. if args.model == 'alexnet':
  35. out = alexnet.alexnet(image, class_dim=CLASS_DIM)
  36. elif args.model == 'vgg13':
  37. out = vgg.vgg13(image, class_dim=CLASS_DIM)
  38. elif args.model == 'vgg16':
  39. out = vgg.vgg16(image, class_dim=CLASS_DIM)
  40. elif args.model == 'vgg19':
  41. out = vgg.vgg19(image, class_dim=CLASS_DIM)
  42. elif args.model == 'resnet':
  43. out = resnet.resnet_imagenet(image, class_dim=CLASS_DIM)
  44. learning_rate = 0.1
  45. elif args.model == 'googlenet':
  46. out, out1, out2 = googlenet.googlenet(image, class_dim=CLASS_DIM)
  47. loss1 = paddle.layer.cross_entropy_cost(
  48. input=out1, label=lbl, coeff=0.3)
  49. paddle.evaluator.classification_error(input=out1, label=lbl)
  50. loss2 = paddle.layer.cross_entropy_cost(
  51. input=out2, label=lbl, coeff=0.3)
  52. paddle.evaluator.classification_error(input=out2, label=lbl)
  53. extra_layers = [loss1, loss2]
  54.  
  55. cost = paddle.layer.classification_cost(input=out, label=lbl)
  56.  
  57. # Create parameters
  58. parameters = paddle.parameters.create(cost)
  59.  
  60. # Create optimizer
  61. optimizer = paddle.optimizer.Momentum(
  62. momentum=0.9,
  63. regularization=paddle.optimizer.L2Regularization(rate=0.0005 *
  64. BATCH_SIZE),
  65. learning_rate=learning_rate / BATCH_SIZE,
  66. learning_rate_decay_a=0.1,
  67. learning_rate_decay_b=128000 * 35,
  68. learning_rate_schedule="discexp", )
  69.  
  70. train_reader = paddle.batch(
  71. paddle.reader.shuffle(
  72. flowers.train(),
  73. # To use other data, replace the above line with:
  74. # reader.train_reader('train.list'),
  75. buf_size=1000),
  76. batch_size=BATCH_SIZE)
  77. test_reader = paddle.batch(
  78. flowers.valid(),
  79. # To use other data, replace the above line with:
  80. # reader.test_reader('val.list'),
  81. batch_size=BATCH_SIZE)
  82.  
  83. # Create trainer
  84. trainer = paddle.trainer.SGD(
  85. cost=cost,
  86. parameters=parameters,
  87. update_equation=optimizer,
  88. extra_layers=extra_layers)
  89.  
  90. # End batch and end pass event handler
  91. def event_handler(event):
  92. if isinstance(event, paddle.event.EndIteration):
  93. if event.batch_id % 1 == 0:
  94. print "\nPass %d, Batch %d, Cost %f, %s" % (
  95. event.pass_id, event.batch_id, event.cost, event.metrics)
  96. if isinstance(event, paddle.event.EndPass):
  97. with gzip.open('params_pass_%d.tar.gz' % event.pass_id, 'w') as f:
  98. trainer.save_parameter_to_tar(f)
  99.  
  100. result = trainer.test(reader=test_reader)
  101. print "\nTest with Pass %d, %s" % (event.pass_id, result.metrics)
  102.  
  103. trainer.train(
  104. reader=train_reader, num_passes=200, event_handler=event_handler)
  105.  
  106. if __name__ == '__main__':
  107. main()

  3.运行方式

  1. python train.py googlenet

  其中最后的googlenet是可选的网络模型,输入其他的网络模型,如alexnet、vgg3、vgg6等就可以用不同的网络结构来训练了。

 


 用Tensorflow实现GoogLeNet

  tensorflow的实现在models里有非常详细的代码,这里就不全部贴出来了,大家可以在models/research/slim/nets/里详细看看,关于InceptionV1~InceptionV4的实现都有。

ps:这里的slim不是tensorflow的contrib下的slim,是models下的slim,别弄混了,slim可以理解为Tensorflow的一个高阶api,在构建这些复杂的网络结构时,可以直接调用slim封装好的网络结构就可以了,而不需要从头开始写整个网络结构。关于slim的详细大家可以在网上搜索,非常方便。


 总结

  其实GoogLeNet的最关键的一点就是提出了Inception结构,这有个什么好处呢,原来你想要提高准确率,需要堆叠更深的层,增加神经元个数等,堆叠到一定层可能结果的准确率就提不上去了,因为参数更多了啊,模型更复杂,更容易过拟合了,但是在实验中转向了更稀疏但是更精密的结构同样可以达到很好的效果,说明我们可以照着这个思路走,继续做,所以后面会有InceptionV2 ,V3,V4等,它表现的结果也非常好。给我们传统的通过堆叠层提高准确率的想法提供了一个新的思路。

【深度学习系列】用PaddlePaddle和Tensorflow实现经典CNN网络GoogLeNet的更多相关文章

  1. 【深度学习系列】用PaddlePaddle和Tensorflow实现经典CNN网络Vgg

    上周我们讲了经典CNN网络AlexNet对图像分类的效果,2014年,在AlexNet出来的两年后,牛津大学提出了Vgg网络,并在ILSVRC 2014中的classification项目的比赛中取得 ...

  2. 【深度学习系列】用PaddlePaddle和Tensorflow实现经典CNN网络AlexNet

    上周我们用PaddlePaddle和Tensorflow实现了图像分类,分别用自己手写的一个简单的CNN网络simple_cnn和LeNet-5的CNN网络识别cifar-10数据集.在上周的实验表现 ...

  3. 【深度学习系列】PaddlePaddle垃圾邮件处理实战(二)

    PaddlePaddle垃圾邮件处理实战(二) 前文回顾   在上篇文章中我们讲了如何用支持向量机对垃圾邮件进行分类,auc为73.3%,本篇讲继续讲如何用PaddlePaddle实现邮件分类,将深度 ...

  4. 使用腾讯云 GPU 学习深度学习系列之二:Tensorflow 简明原理【转】

    转自:https://www.qcloud.com/community/article/598765?fromSource=gwzcw.117333.117333.117333 这是<使用腾讯云 ...

  5. 【深度学习系列】PaddlePaddle之手写数字识别

    上周在搜索关于深度学习分布式运行方式的资料时,无意间搜到了paddlepaddle,发现这个框架的分布式训练方案做的还挺不错的,想跟大家分享一下.不过呢,这块内容太复杂了,所以就简单的介绍一下padd ...

  6. 【深度学习系列】PaddlePaddle可视化之VisualDL

    上篇文章我们讲了如何对模型进行可视化,用的keras手动绘图输出CNN训练的中途结果,本篇文章将讲述如何用PaddlePaddle新开源的VisualDL来进行可视化.在讲VisualDL之前,我们先 ...

  7. 【深度学习系列】PaddlePaddle垃圾邮件处理实战(一)

    PaddlePaddle垃圾邮件处理实战(一) 背景介绍   在我们日常生活中,经常会受到各种垃圾邮件,譬如来自商家的广告.打折促销信息.澳门博彩邮件.理财推广信息等,一般来说邮件客户端都会设置一定的 ...

  8. 【深度学习系列】PaddlePaddle之数据预处理

    上篇文章讲了卷积神经网络的基本知识,本来这篇文章准备继续深入讲CNN的相关知识和手写CNN,但是有很多同学跟我发邮件或私信问我关于PaddlePaddle如何读取数据.做数据预处理相关的内容.网上看的 ...

  9. 【深度学习系列】关于PaddlePaddle的一些避“坑”技巧

    最近除了工作以外,业余在参加Paddle的AI比赛,在用Paddle训练的过程中遇到了一些问题,并找到了解决方法,跟大家分享一下: PaddlePaddle的Anaconda的兼容问题 之前我是在服务 ...

随机推荐

  1. 在找一份相对完整的Webpack项目配置指南么?这里有

    Webpack已经出来很久了,相关的文章也有很多,然而比较完整的例子却不是很多,让很多新手不知如何下脚,下脚了又遍地坑 说实话,官方文档是蛮乱的,而且有些还是错的错的..很多配置问题只有爬过坑才知道 ...

  2. 「Vue」起步 - vue-router路由与页面间导航

    vue-router 我们知道路由定义了一系列访问的地址规则,路由引擎根据这些规则匹配找到对应的处理页面,然后将请求转发给页进行处理.可以说所有的后端开发都是这样做的,而前端路由是不存在"请 ...

  3. Webpack 入门教程

    Webpack 是一个前端资源加载/打包工具.它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源. 本章节基于 Webpack3.0 测试通过. 从图中我们可以看出,W ...

  4. windows查看端口占用命令

    开始--运行--cmd 进入命令提示符 输入netstat -aon 即可看到所有连接的PID 之后在任务管理器中找到这个PID所对应的程序如果任务管理器中没有PID这一项,可以在任务管理器中选&qu ...

  5. layui数据表格以及传数据方式

    数据表格一: <div style="margin:0px; background-color: white; margin:0 10px;"> <blockqu ...

  6. vs2015添加T4模版

    <#@ template language="C#" debug="false" hostspecific="true"#> & ...

  7. sql 1.1 1.1.1 1.10.1 排序

    解决思路:计算每位的权重,得到序号完整的权重值,使用权重值进行排序! 创建sql 函数如下: ALTER FUNCTION [dbo].[SequenceToOrderNum] ( @Sequence ...

  8. ASP.NET没有魔法——ASP.NET Identity与授权

    一个完整的ASP.NET的请求中会存在身份验证(Authentication)阶段以及授权(Authorization)阶段,英文单词Authentication和Authorization非常相似, ...

  9. [转载] linux中文件描述符fd和文件指针flip的理解

    转载自http://www.cnblogs.com/Jezze/archive/2011/12/23/2299861.html 简单归纳:fd只是一个整数,在open时产生.起到一个索引的作用,进程通 ...

  10. ssh相关原理学习与常见错误总结

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...