全连接网络(fully connected network)是神经网络中非常重要的概念,他是由多层网络输入输出所组成的节点映射,每一层之间的节点都相互连接,对于一个 $R^m->R^n$ 的全连接网络,输入(m 个节点) 和输出(n 个节点)之间可能会有多个隐藏层,如图所示:

一个含有 3 个隐藏层的全连接网络

全连接网络也是一个经典的神经网络的结构,其最主要的好处就是“结构无关”,也就是说我们可以无需猜测输入数据是什么结构的,所以常用在图像或者视频分析的领域。

使用 BP 算法训练全连接网络

全神经网络使用 BP 算法为下降函数,计算网络的权重。其基本原理是将正向计算的整体网络的全体损失,反向平摊到网络的各节点权重上,作为一次权重调整,多次迭代后达到整体损失最低化的过程,大致可以理解为适应于神经网络的梯度下降法。更详细的解释,可以参考这篇文章: https://www.cnblogs.com/charlotte77/p/5629865.html

值得注意的是,bp 算法不是所有的函数都能用做下降算法,最主要的限制就是 bp 算法无法保证求得的解就是全连接网络的最优解。另外 bp 算法也不是神经网络的唯一下降算法,还有一些模拟自然过程的如退火算法,或者遗传算法,都可用与下降优化。

为什么要用深度网络

在实践中,大家发现越深的网络越能容易理解复杂的模型,能让更复杂的模型被理解的“更迅速”。在有同样节点数的各种神经网络中,深度越深的网络能够学习更复杂的网络结构。虽然没有明确的证明,但是大量的例证展示出,越深的神经网络能够达到越广连接网络的学习复杂度,甚至能超过我们数学家的理解范围。

训练全连接神经网络

数据变换

为什么全连接神经网络有这样的能力?一种简单的理解方式就是,认为全连接层其实就是数据空间的特征变换问题。比如通过复变函数,我们能将复杂的时域问题,转换成频域问题,把原来需要用积分的复杂问题,转换到了加减乘除就能解决的小学生问题,大大简化了问题处理难度,增加了数学处理问题的范围,经典的如傅利叶变换。在一些经典的图像处理算法中,也会有一些频域变换或者特征提取的办法,提高图像分类准确性。全连接神经网络其实起到的就是这个作用,他通过较深层次的节点变换,将难以找到规律的图像、视频等问题,转换成相对好收敛的数域中,加快了训练速度。只是,这种变换的本身是难以解释的,不像傅利叶变换这样的人为推倒出来的公式,他是通过算法学习的结果。从某种程度上来说,可能这个过程就是智能的过程。

所以有人认为,深度学习是第一种特征变换的算法,以后可能有基于更精妙的数学原理构建的特征变换算法,拓宽我们人工智能的研究领域。

激活函数

在传统的神经网络或者回归算法中,激活函数会采用 sigmoid 这样的非线性算法来完成。但是在现在的深度网络中,往往会使用更简单的线性激活函数 ReLU: $ \theta(x) = max(x,0) $,在实践效果中比 sigmoid 函数更有效。这是因为随着网络的深度增加,sigmoid 容易在较深的网络中将输入全转换成 0,不利于较深网络的构建。关于两者比较的进一步讨论,可以参考这篇文章:https://blog.csdn.net/Leo_Xu06/article/details/53708647

比较深度 sigmoid 函数和 ReLU

在后续的分类网络中,还有 softmax 等激活函数,他们与经典的 sigmoid 有各自的特点。

全连接网络的记忆性

全连接网络的一个大问题就是只要训练的时间够长,他就能记忆整个过程中的训练数据,也就是有一定的过拟合性,所以无法用收敛来指示一个网络的训练程度。一个大型的网络很可能会将训练的损失训练成 0,这就是全连接网络的全局模糊性的一个特点的体现。但是这并不意味着训练的成功。如何去除这种过拟合是决定模型训练成功的关键。

常用的处理办法是将数据正则化(Regulaziation)。但是深度学习中的正则化和统计学中的正则化又有一定的区别,统计学中非常符合直觉的规律,在深度学习中并不适用,比如 LASSO,在深度学习的建模过程中,并没有太大的意义。

深度学习中比较常用的正则化方法有 Dropout, Early Stopping 和权值正则化等。其中最有意思的就是 Dropout,他在训练的过程中,随机的去除一些节点,如果训练得到的是“真规律”,则这些去除的节点也并不会对结果产生较大影响,否则就是伪规律。Dropout 可以有效防止网络的记忆性,并在一定程度上算是模拟了网络对新数据的预测能力。不过注意,在网络训练完成,预测结果时,需要将 dropout 关闭。

实践训练全连接网络

全连接网络在训练大型数据库时,往往会采用使用多批次的数据集,分批计算下降梯度。本次实践我们使用 deepchem 的化合物数据集 Tox21,他是一个有多种化合物特征的数据库,我们的任务则是使用机器学习来判断某种化合物是否有毒。Deepchem 的安装方法在后续章节我们再做介绍。

我们首先载入数据集

import deepchem as dc

_, (train, valid, test), _ = dc.molnet.load_tox21()

train_X, train_y, train_w = train.X, train.y, train.w
valid_X, valid_y, valid_w = valid.X, valid.y, valid.w
test_X, test_y, test_w = test.X, test.y, test.w

# 删除一些多余的标签,以做简化

train_y = train_y[:, 0]
valid_y = valid_y[:, 0]
test_y = test_y[:, 0]
train_w = train_w[:, 0]
valid_w = valid_w[:, 0]

其中,x 是化合物的特征向量,y 是标签,使用 0/1 的二元数据表示化合物是否和受体反映。由于 Tox21 是一个不平衡的数据集,也就是说正样本远小于负样本,所以数据库再单独给了一个值 w,用于表示样本的权重,以增加某个特征对正样本的重要性。这是在非平衡样本中,常用的处理方式。

创建批次数据

由于样本中共有 947 个样本,50 个一个批次的话,最后一个批次只有 47 个样本。Tensorflow 中可以方便的处理这种情况,创建可变的占位符(Placeholder),就是设置 tesnor 的第一个维度为 None:

# 使用分批数据,处理所有的不同数据内容

d = 1024  # Tox21 特征向量维度数

with tf.name_scope("placeholdes"):
    x = tf.placeholder(tf.float32, (None, d))
    y = tf.placeholder(tf.float32, (None, ))

构建全连接网络的隐藏层

隐藏层的构建和逻辑回归中构建的方式比较类似,我们使用 n_hidden 代表隐藏层层数,这是一个超变量。

# 定义一个隐藏层

with tf.name_scope("hidden-layer"):
    W = tf.Variable(tf.random_normal((d, n_hidden)))
    b = tf.Variable(tf.random_normal((n_hidden, )))
    # 使用xW而不是Wx,为了方便处理多批次的数据

    # 使用ReLU的非线性整流结果

    # max(0, w^Tx + b)

    x_hidden = tf.nn.relu(tf.matmul(x, W) + b)

剩余部分和前几个章节是一致的,不再赘述。

# 使用分批数据,处理所有的不同数据内容

d = 1024  # 特征向量维度数

with tf.name_scope("placeholdes"):
    x = tf.placeholder(tf.float32, (None, d))
    y = tf.placeholder(tf.float32, (None, ))

# 定义一个隐藏层

with tf.name_scope("hidden-layer"):
    W = tf.Variable(tf.random_normal((d, n_hidden)))
    b = tf.Variable(tf.random_normal((n_hidden, )))
    # 使用xW而不是Wx,为了方便处理多批次的数据

    # 使用ReLU的非线性整流结果

    # max(0, w^Tx + b)

    x_hidden = tf.nn.relu(tf.matmul(x, W) + b)

# 定义输出层

with tf.name_scope("output"):
    W = tf.Variable(tf.random_normal((n_hidden, 1)))
    b = tf.Variable(tf.random_normal((1, )))
    y_logit = tf.matmul(x_hidden, W) + b
    y_one_prob = tf.sigmoid(y_logit)
    y_pred = tf.round(y_one_prob)

# 定义损失函数

with tf.name_scope("loss"):
    y_expand = tf.expand_dims(y, 1)
    # 交叉熵差异损失函数

    entropy = tf.nn.sigmoid_cross_entropy_with_logits(
        logits=y_logit, labels=y_expand)
    l = tf.reduce_sum(entropy)

# 定义下降函数

with tf.name_scope("optim"):
    train_op = tf.train.AdamOptimizer(learning_rate).minimize(l)

with tf.name_scope("summaries"):
    tf.summary.scalar("loss", l)
    merged = tf.summary.merge_all()

train_writer = tf.summary.FileWriter('logs/train', tf.get_default_graph())

在隐藏层中使用 Dropout

在隐藏层中加入 Dropout 比较简单,有内置函数即可完成。但 Dropout 的概率也是个超变量,并且在训练和预测的过程中使用不同的值,训练时我们采用 0.5,预测时我们使用 1.0。所以我们也把他加入到 placeholder 中去。

with tf.name_scope("placeholdes"):
    x = tf.placeholder(tf.float32, (None, d))
    y = tf.placeholder(tf.float32, (None, ))
    keep_prob = tf.placeholder(tf.float32)

# 定义一个隐藏层

with tf.name_scope("hidden-layer"):
    W = tf.Variable(tf.random_normal((d, n_hidden)))
    b = tf.Variable(tf.random_normal((n_hidden, )))
    # 使用xW而不是Wx,为了方便处理多批次的数据

    # 使用ReLU的非线性整流结果

    # max(0, w^Tx + b)

    x_hidden = tf.nn.relu(tf.matmul(x, W) + b)
    # add dropout

    x_hidden = tf.nn.dropout(x_hidden, keep_prob)

喂入批量数据

过程和之前的一样,只是多一个步分 batch 的过程。实验时我们就进行了 10 段数据的分隔

# 喂入批量数据

step = 0

N = train_X.shape[0]

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for epoch in range(n_epochs):
        pos = 0
        while pos < N:
            batch_x = train_X[pos:pos + batch_size]
            batch_y = train_y[pos:pos + batch_size]
            feed_dict = {x: batch_x, y: batch_y, keep_prob: dropout_prob}
            _, summary, loss = sess.run([train_op, merged, l],
                                        feed_dict=feed_dict)
            print("epoch %d, step %d, loss: %f" % (epoch, step, loss))

            train_writer.add_summary(summary, step)

            step += 1
            pos += batch_size

    # 得到预测结果

    train_y_pred = sess.run(y_pred, feed_dict={x: train_X, keep_prob: 1.0})
    valid_y_pred = sess.run(y_pred, feed_dict={x: valid_X, keep_prob: 1.0})
    test_y_pred = sess.run(y_pred, feed_dict={x: test_X, keep_prob: 1.0})

判断训练的准确性

由于 Tox21 的数据不平衡性,使用 accuracy_score 函数进行计算时,需要加入权重,以保证样本的平衡。实践中,正样本的权重为负样本的 19 倍左右,此时用 MolecureNet 分类类别可得到 50%的准确度,相对来说就比较合理了。

# 定义评价函数,由于0标签数量比1标签数量多的多,我们需要给1标签数量加权重

train_weighted_score = accuracy_score(
    train_y, train_y_pred, sample_weight=train_w)
print("Train Weighted Classfiication Accuracy: %f" % train_weighted_score)
valid_weighted_score = accuracy_score(
    valid_y, valid_y_pred, sample_weight=valid_w)
print("Valid Weighted Classfication Accuracy: %f" % valid_weighted_score)
test_weighted_score = accuracy_score(test_y, test_y_pred, sample_weight=test_w)
print("Test Weighted Classfication Accuracy: %f" % test_weighted_score)

执行脚本,得到训练结果:

训练结果

可以看到分 10 批,共 630 次训练后,在测试集上有了 53.8%的准确度,可以说并不算太高。

研究训练过程

使用命令

tensorboard  --logdir logs/train

打开 tensorboard,查看训练过程。首先查看可视化的计算网络,可以看到训练过程中加入了一个隐藏网络 hidden layer。

可视化训练网络

再点开隐藏层,可以看到其中的具体过程

隐藏层的具体计算过程

而损失函数的下降可视化如图

损失函数可视化

可见这个过程是极其不稳定的,这也是多批次数据训练的代价。其加快了训练过程,但也降低了稳定性。后续学习中,我们就要想办法来增加训练的稳定性,以及提升准确度。

遇到的问题和解决

实践过程中,由于在这之前我都是使用的 pipenv 代替 anaconda 的方式安装的依赖,本次过程中 deepchem 无法正常使用,pip 直接安装依赖无法满足所有依赖,特别是 rdkit,提示

> pip install rdkit
Collecting rdkit
ERROR: Could not find a version that satisfies the requirement rdkit (from versions: none)
ERROR: No matching distribution found for rdkit

看 deepchem 的官网,其官方支持的只有 linux 和 osx。索幸 windows 还有 WSL,于是我在 WSL 上安装了 anaconda,具体过程如资料:https://gist.github.com/kauffmanes/5e74916617f9993bc3479f401dfec7da

安装的时候直接安装也可能会有依赖无法全部满足的问题,提示

# gsj987 @ LOU-THINKPAD in /mnt/c/Users/gsj98/Projects/tensorflow_tutorial [16:02:44] C:1
$ ~/anaconda2/bin/conda install -c deepchem deepchem=2.1.0 python=3.5

Solving environment: failed
PackagesNotFoundError: The following packages are not available from current channels:

 - deepchem=2.1.0
 - pdbfixer==1.4
 - deepchem=2.1.0
 - xgboost==0.6a2
 - deepchem=2.1.0
 - rdkit==2017.09.1

Current channels:

 - [https://conda.anaconda.org/deepchem/linux-64](https://conda.anaconda.org/deepchem/linux-64)
 - [https://conda.anaconda.org/deepchem/noarch](https://conda.anaconda.org/deepchem/noarch)
 - [https://repo.anaconda.com/pkgs/main/linux-64](https://repo.anaconda.com/pkgs/main/linux-64)
 - [https://repo.anaconda.com/pkgs/main/noarch](https://repo.anaconda.com/pkgs/main/noarch)
 - [https://repo.anaconda.com/pkgs/free/linux-64](https://repo.anaconda.com/pkgs/free/linux-64)
 - [https://repo.anaconda.com/pkgs/free/noarch](https://repo.anaconda.com/pkgs/free/noarch)
 - [https://repo.anaconda.com/pkgs/r/linux-64](https://repo.anaconda.com/pkgs/r/linux-64)
 - [https://repo.anaconda.com/pkgs/r/noarch](https://repo.anaconda.com/pkgs/r/noarch)
 - [https://repo.anaconda.com/pkgs/pro/linux-64](https://repo.anaconda.com/pkgs/pro/linux-64)
 - [https://repo.anaconda.com/pkgs/pro/noarch](https://repo.anaconda.com/pkgs/pro/noarch)

To search for alternate channels that may provide the conda package you're
looking for, navigate to
 [https://anaconda.org](https://anaconda.org)

and use the search bar at the top of the page.

解决办法是指定更详细的依赖列表:

conda install -c deepchem -c rdkit -c conda-forge -c omnia deepchem=2.1.0

另外有个坑就是 conda 启用 virtualenv 后,直接使用的 python 代替的默认 python 版本,而不是系统的 python3,后者使用的仍旧是系统版本,需要注意。

总结

全连接网络是神经网络和深度学习中的重要概念。可以说特别是在图像的分类问题上,最后都会加上一个全连接网络作为最后的分类器。现代深度学习最大的突破也是将全连接网络的可计算深度大大增加,从而带来了更多的抽象理解能力。后续的工作就是如何提升网络的准确性,加快收敛速度等角度,但基本原理都来自全连接网络,需要重点理解。