上一节使用了最简单的网络来处理了 MNIST 数据集,但只有 92% 的正确率,接下来我们使用卷积神经网络来实现更高的正确率。
权重初始化
在上一节初始化 w 和 b 的时候,我们使用了置零初始化。但在卷积神经网络中,我们需要在初始化的时候权重加入少量噪声来打破对称性和避免零梯度,偏置项直接使用一个较小的正数来避免节点输出恒为零的问题。 所以权重我们可以使用截尾正态分布函数 truncated_normal() 来生成初始化张量,我们可以给它指定均值或标准差,均值默认是 0, 标准差默认是 1,例如我们生成一个 [10] 的张量,代码如下:
1 |
import tensorflow as tf |
结果如下:
1 |
[-0.13058113 0.03201858 -0.19349943 -0.06061752 -0.10267895 -0.11079147 |
另外 constant() 方法是用于生成常量的方法,例如生成一个 [10] 的常量张量,代码如下:
1 |
import tensorflow as tf |
结果如下:
1 |
[ 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2] |
所以这里我们可以将这两个方法封装成一个函数来尝试:
1 |
def weight(shape, stddev=0.1, mean=0): |
卷积
这次我们需要使用卷积神经网络来处理图片,所以这里的核心部分就是卷积和池化了,首先我们来了解一下卷积和池化。 卷积常用的方法为 conv2d() ,它的 API 如下:
1 |
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None) |
这个方法是 TensorFlow 实现卷积常用的方法,也是搭建卷积神经网络的核心方法,参数介绍如下:
- input,指需要做卷积的输入图像,它要求是一个 Tensor,具有 [batch_size, in_height, in_width, in_channels] 这样的 shape,具体含义是 [batch_size 的图片数量, 图片高度, 图片宽度, 输入图像通道数],注意这是一个 4 维的 Tensor,要求类型为 float32 和 float64 其中之一。
- filter,相当于 CNN 中的卷积核,它要求是一个 Tensor,具有 [filter_height, filter_width, in_channels, out_channels] 这样的shape,具体含义是 [卷积核的高度,卷积核的宽度,输入图像通道数,输出通道数(即卷积核个数)],要求类型与参数 input 相同,有一个地方需要注意,第三维 in_channels,就是参数 input 的第四维。
- strides,卷积时在图像每一维的步长,这是一个一维的向量,长度 4,具有 [stride_batch_size, stride_in_height, stride_in_width, stride_in_channels] 这样的 shape,第一个元素代表在一个样本的特征图上移动,第二三个元素代表在特征图上的高、宽上移动,第四个元素代表在通道上移动。
- padding,string 类型的量,只能是 SAME、VALID 其中之一,这个值决定了不同的卷积方式。
- use_cudnn_on_gpu,布尔类型,是否使用 cudnn 加速,默认为true。
返回的结果是 [batch_size, out_height, out_width, out_channels] 维度的结果。 我们这里拿一张 3x3 的图片,单通道(通道为1)的图片,拿一个 1x1 的卷积核进行卷积:
1 |
input = tf.Variable(tf.random_normal([1, 3, 3, 1])) |
结果如下:
1 |
(1, 3, 3, 1) |
很清晰,一张图片,拿一个 1x1 的核去做卷积,得到的结果输出是 3x3 的,输出通道为 1,batch_size 照旧。 再将卷积核扩大,用一个 3x3 的卷积核:
1 |
input = tf.Variable(tf.random_normal([1, 3, 3, 1])) |
结果如下:
1 |
(1, 1, 1, 1) |
最后输出的是一个 1x1 的值。 将图片扩大为 7x7,卷积核仍然使用 3x3:
1 |
input = tf.Variable(tf.random_normal([1, 7, 7, 1])) |
结果如下:
1 |
(1, 5, 5, 1) |
最后输出的是一个 5x5 的值。 这时如果我们把 padding 模式改为 SAME,表示卷积核可以停留在图像边缘:
1 |
input = tf.Variable(tf.random_normal([1, 7, 7, 1])) |
结果如下:
1 |
(1, 7, 7, 1) |
则输出的内容和原图像是相同的。 这时如果更改 batch_size 和 out_channels,比如 batch_size 修改为 3,out_channels 修改为 6:
1 |
input = tf.Variable(tf.random_normal([3, 7, 7, 1])) |
结果如下:
1 |
(3, 7, 7, 6) |
输出结果的 batch_size 和 out_channels 会随之变化。 当 strides 的步长不为 1 的时候,我们将 stride_in_height 和 stride_in_width 修改为 2,相当于每次移动两步:
1 |
input = tf.Variable(tf.random_normal([3, 7, 7, 1])) |
结果如下:
1 |
(3, 3, 3, 6) |
最后我们用一个例子来感受一下:
1 |
import tensorflow as tf |
这里 input、filter 通过指定 shape 的方式调用 random_normal() 方法进行随机初始化,input 的维度为 [2, 4, 4, 5],即 batch_size 为 2,图片是 4x4,输入通道数为 5,卷积核大小为 2x2,输入通道 5,输出通道 2,步长为 1,padding 方式选用 VALID,最后输出得到输出的 shape 和结果。 结果如下:
1 |
(2, 3, 3, 2) |
可以看到 input 维度为 [2, 4, 4, 5],filter 维度为 [2, 2, 5, 2] 时,生成的结果维度为 [2, 3, 3, 2]。
池化
池化层往往在卷积层后面,通过池化来降低卷积层输出的特征向量,同时改善结果。 在这里介绍一个常用的最大值池化 max_pool() 方法,其 API 如下:
1 |
tf.nn.max_pool(value, ksize, strides, padding, name=None) |
是CNN当中的最大值池化操作,其实用法和卷积很类似。 参数介绍如下:
- value,需要池化的输入,一般池化层接在卷积层后面,所以输入通常是 feature map,依然是 [batch_size, height, width, channels] 这样的shape。
- ksize,池化窗口的大小,取一个四维向量,一般是 [batch_size, height, width, channels],因为我们不想在 batch 和 channels 上做池化,所以这两个维度设为了1。
- strides,和卷积类似,窗口在每一个维度上滑动的步长,一般也是 [stride_batch_size, stride_height, stride_width, stride_channels]。
- padding,和卷积类似,可以取 VALID、SAME,返回一个 Tensor,类型不变,shape 仍然是 [batch_size, height, width, channels] 这种形式。
在这里输入为 [3, 7, 7, 2],池化窗口设置为 [1, 2, 2, 1],步长为 [1, 1, 1, 1],padding 模式设置为 VALID。
1 |
input = tf.Variable(tf.random_normal([3, 7, 7, 2])) |
结果如下:
1 |
(3, 6, 6, 2) |
类似的原理,我们可以得到这样的的结果。
卷积和池化
所以了解了以上卷积和池化方法的用法,我们可以定义如下两个工具方法:
1 |
def conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME'): |
这两个方法分别实现了卷积和池化,并设置了默认步长和核大小。 接下来就让我们开始神经网络的构建吧。
初始化
首先我们需要初始化一些数据,包括输入的 x 和对一个的标注 y_label:
1 |
x = tf.placeholder(tf.float32, shape=[None, 784]) |
第一层卷积
现在我们可以开始实现第一层了。它由一个卷积接一个 max pooling 完成。卷积在每个 5x5 的 patch 中算出 32 个特征。卷积的权重张量形状是 [5, 5, 1, 32],前两个维度是 patch 的大小,接着是输入的通道数目,最后是输出的通道数目,而对于每一个输出通道都有一个对应的偏置量,我们首先初始化 w 和 b
1 |
w_conv1 = weight([5, 5, 1, 32]) |
为了用这一层,我们把 x 变成一个四维向量,其第 2、3 维对应图片的宽、高,最后一维代表图片的颜色通道数,因为是灰度图所以这里的通道数为 1,如果是彩色图,则为 3。 随后我们需要对图片做 reshape 操作,将其
1 |
x_reshape = tf.reshape(x, [-1, 28, 28, 1]) |
我们把 x_reshape 和权值向量进行卷积,加上偏置项,然后应用 ReLU 激活函数,最后进行 max pooling:
1 |
h_conv1 = tf.nn.relu(conv2d(x_reshape, w_conv1) + b_conv1) |
第二层卷积
现在我们已经实现了一层卷积,为了构建一个更深的网络,我们再继续增加一层卷积,将通道数变成 64,所以这里的初始化权重和偏置为:
1 |
w_conv2 = weight([5, 5, 32, 64]) |
随后我们把上一层池化结果 h_pool1 和权值向量进行卷积,加上偏置项,然后应用 ReLU 激活函数,最后进行 max pooling:
1 |
h_conv2 = tf.nn.relu(conv2d(h_pool1, w_conv2) + b_conv2) |
密集连接层
现在,图片尺寸减小到7x7,我们再加入一个有 1024 个神经元的全连接层,用于处理整个图片。我们把池化层输出的张量 reshape 成一些向量,乘上权重矩阵,加上偏置,然后对其使用 ReLU。
1 |
w_fc1 = weight([7 * 7 * 64, 1024]) |
Dropout
为了减少过拟合,我们在输出层之前加入 dropout。我们用一个 placeholder 来代表一个神经元的输出在 dropout 中保持不变的概率。这样我们可以在训练过程中启用 dropout,在测试过程中关闭 dropout。 TensorFlow 的 tf.nn.dropout 操作除了可以屏蔽神经元的输出外,还会自动处理神经元输出值的 scale,所以用 dropout 的时候可以不用考虑 scale。
1 |
keep_prob = tf.placeholder(tf.float32) |
输出层
最后,我们添加一个 Softmax 输出层,这里我们需要将 1024 维转为 10 维,所以需要声明一个 [1024, 10] 的权重和 [10] 的偏置:
1 |
w_fc2 = weight([1024, 10]) |
训练和评估模型
为了进行训练和评估,我们使用与之前简单的单层 Softmax 神经网络模型几乎相同的一套代码,只是我们会用更加复杂的 Adam 优化器来做梯度最速下降,在 feed_dict 中加入额外的参数 keep_prob 来控制 dropout 比例,然后每 100次 迭代输出一次日志:
1 |
# Loss |
运行
以上代码,在最终测试集上的准确率大概是99.2%。 运行结果:
1 |
Training Accuracy 0 0.05 |
结语
本节我们实现了卷积神经网络来处理图像相关问题,将准确率大大提高,可见神经网络是非常强大的。




数据集中包含了图片和对应的标注,在 TensorFlow 中提供了这个数据集,我们可以用如下方法进行导入:
正如前面提到的一样,每一个 MNIST 数据单元有两部分组成:一张包含手写数字的图片和一个对应的标签。我们把这些图片设为 xs,把这些标签设为 ys。训练数据集和测试数据集都包含 xs 和 ys,比如训练数据集的图片是 mnist.train.images ,训练数据集的标签是 mnist.train.labels,每张图片是 28 x 28 像素,即 784 个像素点,我们可以把它展开形成一个向量,即长度为 784 的向量。 所以训练集我们可以转化为 [55000, 784] 的向量,第一维就是训练集中包含的图片个数,第二维是图片的像素点表示的向量。
展开等式右边的子式,可以得到:
比如判断一张图片中的动物是什么,可能的结果有三种,猫、狗、鸡,假如我们可以经过计算得出它们分别的得分为 3.2、5.1、-1.7,Softmax 的过程首先会对各个值进行次幂计算,分别为 24.5、164.0、0.18,然后计算各个次幂结果占总次幂结果的比重,这样就可以得到 0.13、0.87、0.00 这三个数值,所以这样我们就可以实现差别的放缩,即好的更好、差的更差。 如果要进一步求损失值可以进一步求对数然后取负值,这样 Softmax 后的值如果值越接近 1,那么得到的值越小,即损失越小,如果越远离 1,那么得到的值越大。
这里实际上是对输入的 x 乘以 w 权重,然后加上一个偏置项作为输出,而这两个变量实际是在训练的过程中动态调优的,所以我们需要指定它们的类型为 Variable,代码如下:
y 是我们预测的概率分布, y_label 是实际的分布,比较粗糙的理解是,交叉熵是用来衡量我们的预测用于描述真相的低效性。 我们可以首先定义 y_label,它的表达式是:










另外我们也可以单独添加单个 Node 或 Relationship,实例如下:
另外还可以利用 data() 方法来获取查询结果:
在这里我们用 NodeSelector 来筛选 age 为 21 的 Person Node,实例如下:

























在这里推荐安装CentOS7系统。 然后找到远程管理面板找到远程连接的用户名和密码,也就是SSH远程连接服务器的信息。 比如我这边的IP端口分别是 153.36.65.214:20063,用户名是root。 命令行下输入:
都提示成功之后就可以进行拨号了。 在拨号之前如果我们测试ping任何网站都是不通的,因为当前网络还没联通,输入拨号命令:
所以断线重播的命令就是二者组合起来,先执行
所以,到这里我们就可以知道它作为代理服务器的巨大优势了,如果将这台主机作为代理服务器,如果我们一直拨号换IP,就不怕遇到IP被封的情况了,即使某个IP被封了,重新拨一次号就好了。 所以接下来我们要做的就有两件事,一是怎样将主机设置为代理服务器,二是怎样实时获取拨号主机的IP。
如果有正常的结果输出并且origin的值为代理IP的地址,就证明TinyProxy配置成功了。 好,那到现在,我们接下来要做的就是需要动态实时获取主机的IP了。
获取最新代理:
获取所有代理:
请求接口获取可用代理即可,比如获取一个随机代理:















我们可以看到这里就是他的一些基本信息,我们需要抓取的就是这些,比如名字、签名、职业、关注数、赞同数等等。 接下来我们需要探索一下关注列表接口在哪里,我们点击关注选项卡,然后下拉,点击翻页,我们会在下面的请求中发现出现了 followees开头的Ajax请求。这个就是获取关注列表的接口。
我们观察一下这个请求结构
首先它是一个Get类型的请求,请求的URL是
可以看到有data和paging两个字段,data就是数据,包含20个内容,这些就是用户的基本信息,也就是关注列表的用户信息。 paging里面又有几个字段,is_end表示当前翻页是否结束,next是下一页的链接,所以在判读分页的时候,我们可以先利用is_end判断翻页是否结束,然后再获取next链接,请求下一页。 这样我们的关注列表就可以通过接口获取到了。 接下来我们再看下用户详情接口在哪里,我们将鼠标放到关注列表任意一个头像上面,观察下网络请求,可以发现又会出现一个Ajax请求。
可以看到这次的请求链接为
所以综上所述:
可以看到返回的结果非常全,在这里我们直接声明一个Item全保存下就好了。 在items里新声明一个UserItem
看下MongoDB,里面我们爬取的用户详情结果。
到现在为止,整个爬虫就基本完结了,我们主要通过递归的方式实现了这个逻辑。存储结果也通过适当的方法实现了去重。
































































