神经网络(四):应用示例之分类
一、 传统分类模型的局限
在之前的文章中(《神经网络(一)》、《神经网络(二)》和《神经网络(三)》),我们讨论的重点是神经网络的理论知识。现在来看一个实际的例子,如何利用神经网络解决分类问题。(为了更好地展示神经网络的特点,我们在这个示例中并不划分训练集和测试集)。
分类是机器学习最常见的应用之一,之前的章节也讨论过很多解决分类问题的机器学习模型,比如逻辑回归和支持向量学习机等。但这些模型最大的局限性是它们都有比较明确的适用范围,如果训练数据符合模型的假设,则分类效果很好。否则,分类的效果就会很差。
比如图1[1]中展示了4种不同分布类型的数据。具体来说,数据里有两个自变量,分别对应着坐标系的横纵轴;数据分为两类,在图中用三角形表示类别0,用圆点表示类别1。如果使用逻辑回归对数据进行分类,只有图中标记1中的模型效果较好(图中的灰色区域里,模型的预测结果是类别0;白色区域里,模型的预测结果是类别1),因为在已知类别的情况下,数据服从正态分布(不同类别,分布的中心不同),符合逻辑回归的模型假设。对于标记2、3、4中的数据,由于类别与自变量之间的关系是非线性的,如果想取得比较好的分类效果,则需要其他的建模技巧。比如先使用核函数对数据进行升维,再使用支持向量学习机进行分类。
图1二、 神经网络的优势
这样的建模方法是比较辛苦的,要求搭建模型的数据科学家对不同模型的假设以及优缺点有比较深刻的理解。但如果使用神经网络对数据进行分类,则整个建模过程就比较轻松了,只需设计神经网络的形状(包括神经网络的层数以及每一层里的神经元个数),然后将数据输入给模型即可。
在这个例子中,使用的神经网络如图2所示,是一个3-层的全连接神经网络。
使用这个神经网络对数据进行分类,得到的结果如图3所示,可以看到同一个神经网络(结构相同,但具体的模型参数是不同的)对4种不同分布类型的数据都能较好地进行分类。
图3三、 代码实现(完整的代码请见)
这一节节将讨论如何借助第三方库TensorFlow来实现神经网络,。
第一步是定义神经网络的结构,如程序清单1所示。
- 我们使用类(class)来实现神经网络,如第4行代码所示。在Python的类中可以定义相应的函数,但在类中,函数的定义与普通函数的定义有所不同,它的参数个数必须大于1,且第一个参数表示类本身,如第7行代码里的“self”变量。但在调用这个函数时,却不需要“手动”地传入这个参数,Python会自动地进行参数传递,比如defineANN函数的调用方式是“defineANN()”。
- 在ANN类中,“self.input”对应着训练数据里的自变量(它的类型是tf.placeholder),如第12行代码所示,“self.input.shape[1].value”表示输入层的神经元个数(针对如图2的神经网络,这个值等于2)。而“self.size”是表示神经网络结构的数组(针对如图2的神经网络,这个值等于[4, 4, 2])。在ANN类中,“self.input”对应着训练数据里的自变量(它的类型是tf.placeholder),如第12行代码所示,“self.input.shape[1].value”表示输入层的神经元个数(针对如图12-8的神经网络,这个值等于2)。而“self.size”是表示神经网络结构的数组(针对如图2的神经网络,这个值等于[4, 4, 2])。
- 接下来是定义网络的隐藏层。首先是神经元里的线性模型部分,如第18~21行代码所示,定义权重项“weights”和截距项“biases”。因此,权重项是一个的矩阵,而截距项是一个维度等于的行向量。值得注意的是,在定义权重项时,使用tf.truncated_normal函数(近似地对应着正态分布)来生成初始值,在生成初始值的过程中,我们用如下的命令来规定分布的标准差“stddev=1.0 / np.sqrt(float(prevSize))”,这样操作的原因是为了使神经网络更快收敛。定义好线性模型后,就需要定义神经元的激活函数,如第22行代码所示,使用的激活函数是tf.nn.sigmoid,它对应着sigmoid函数。
- 最后是定义神经网络的输出层,如第25~29行代码所示。具体的过程和隐藏层类似,唯一不同的是,输出层并没有激活函数,因此只需定义线性模型部分“tf.matmul(prevOut, weights) + biases”。
程序清单1 定义神经网络的结构
1 | import numpy as np
2 | import tensorflow as tf
3 |
4 | class ANN(object):
5 | # 省略掉其他部分
6 |
7 | def defineANN(self):
8 | """
9 | 定义神经网络的结构
10 | """
11 | # self.input是训练数据里自变量
12 | prevSize = self.input.shape[1].value
13 | prevOut = self.input
14 | # self.size是神经网络的结构,也就是每一层的神经元个数
15 | size = self.size
16 | # 定义隐藏层
17 | for currentSize in size[:-1]:
18 | weights = tf.Variable(
19 | tf.truncated_normal([prevSize, currentSize],
20 | stddev=1.0 / np.sqrt(float(prevSize))))
21 | biases = tf.Variable(tf.zeros([currentSize]))
22 | prevOut = tf.nn.sigmoid(tf.matmul(prevOut, weights) + biases)
23 | prevSize = currentSize
24 | # 定义输出层
25 | weights = tf.Variable(
26 | tf.truncated_normal([prevSize, size[-1]],
27 | stddev=1.0 / np.sqrt(float(prevSize))))
28 | biases = tf.Variable(tf.zeros([size[-1]]))
29 | self.out = tf.matmul(prevOut, weights) + biases
30 | return self
第二步是定义神经网络的损失函数,如程序清单2所示。
- 在ANN类中,“self.label”对应着训练数据里的标签变量(它的类型是tf.placeholder)。值得注意的是,这里用到的标签变量是使用One-Hot Encoding(独热编码)处理过的。比如针对图1中的数据,每个数据的标签变量是二维的行向量,用表示类别0,用表示类别1。
- 在ANN类中,“self.out”对应着神经网络的输出层,具体的定义如程序清单2中的第29行代码所示。
- 根据《神经网络(一)》、《神经网络(二)》和《神经网络(三)》中的讨论结果,神经网络的单点损失的实现如第9、10行代码所示,其中,“self.out”对应着公式里的变量。
- 模型的整体损失等于所有单点损失之和,相应的实现如第12行代码所示。
程序清单2 定义神经网络的结构
1 | class ANN(object):
2 | # 省略掉其他部分
3 |
4 | def defineLoss(self):
5 | """
6 | 定义神经网络的损失函数
7 | """
8 | # 定义单点损失,self.label是训练数据里的标签变量
9 | loss = tf.nn.softmax_cross_entropy_with_logits(
10 | labels=self.label, logits=self.out, name="loss")
11 | # 定义整体损失
12 | self.loss = tf.reduce_mean(loss, name="average_loss")
13 | return self
第三步是训练神经网络,如程序清单3所示。
-
从理论上来讲,训练神经网络的算法是之后将讨论的反向传播算法,这个算法的基础是随机梯度下降法(stochastic gradient descent)。由于TensorFlow已经将整个算法包装好了,如第8~23行代码所示。限于篇幅,实现的具体细节在此就不再重复了。
-
如果将训练过程的模型损失(随训练轮次的变化曲线)记录下来,可以得到如图4所示的图像,其中曲线的标记对应着训练数据的标记。从图中的结果可以看到,对于不同类型的数据,模型损失函数的变化曲线是不一样的。对于比较难训练的数据(标记3),模型的损失经历了一个很漫长的训练瓶颈期。也就是说,虽然模型并没有达到收敛状态,但在较长的训练周期里,模型效果几乎没有提升。这种现象其实是神经网络研究领域里最大的难点,它使得神经网络的训练(特别是层数较多深度神经网络)变得极其困难,一方面瓶颈期会使模型的训练变得非常漫长;另一方面,在实际应用中,当模型损失不再大幅变动时,我们很难判断这是因为模型到达了收敛状态还是因为模型进入了瓶颈期[2]。引起瓶颈期这种现象的原因有很多,我们将在后面的文章中重点讨论这部分内容。
程序清单3 训练模型
1 | class ANN(object):
2 | # 省略掉其他部分
3 |
4 | def SGD(self, X, Y, learningRate, miniBatchFraction, epoch):
5 | """
6 | 使用随机梯度下降法训练模型
7 | """
8 | method = tf.train.GradientDescentOptimizer(learningRate)
9 | optimizer= method.minimize(self.loss)
10 | batchSize = int(X.shape[0] * miniBatchFraction)
11 | batchNum = int(np.ceil(1 / miniBatchFraction))
12 | sess = tf.Session()
13 | init = tf.global_variables_initializer()
14 | sess.run(init)
15 | step = 0
16 | while (step < epoch):
17 | for i in range(batchNum):
18 | batchX = X[i * batchSize: (i + 1) * batchSize]
19 | batchY = Y[i * batchSize: (i + 1) * batchSize]
20 | sess.run([optimizer],
21 | feed_dict={self.input: batchX, self.label: batchY})
22 | step += 1
23 | self.sess = sess
24 | return self
神经网络训练好之后,就可以使用它对未知数据做预测,如程序清单4所示。根据前面的讨论,对神经网络的输出层使用softmax函数,就可以得到每个类别的预测概率,具体的实现如第9、10行代码所示。
程序清单4 对未知数据做预测
1 | class ANN(object):
2 | # 省略掉其他部分
3 |
4 | def predict_proba(self, X):
5 | """
6 | 使用神经网络对未知数据进行预测
7 | """
8 | sess = self.sess
9 | pred = tf.nn.softmax(logits=self.out, name="pred")
10 | prob = sess.run(pred, feed_dict={self.input: X})
11 | return prob
四、广告时间
这篇文章的大部分内容参考自我的新书《精通数据科学:从线性回归到深度学习》。
李国杰院士和韩家炜教授在读过此书后,亲自为其作序,欢迎大家购买。
另外,与之相关的免费视频课程请关注这个链接