很多人行业中的老人都在向刚刚入行的新手提出建议:一开始进入到机器学习之中,最需要涉足的便是工作原理,只有将整套的工作原理弄清楚之后,才可以正式开始动手实践,这才是机器学习的普遍规律,不过对于这些老人提出的建议,我却不这么认为。
我觉得实践高于理论,新手首先要做的是了解整个模型的工作流程,数据大致是怎样流动的,经过了哪些关键的结点,最后的结果在哪里获取,并立即开始动手实践,构建自己的机器学习模型。至于算法和函数内部的实现机制,可以等了解整个流程之后,在实践中进行更深入的学习和掌握。
在本文中,我们将利用 TensorFlow 实现一个基于深度神经网络(DNN)的文本分类模型,希望对各位初学者有所帮助。
关于 TensorFlow
TensorFlow 是谷歌旗下一个开源的机器学习框架。从它的名字就能看出这个框架基本的工作原理:由多维数组构成的张量(tensor)在图(graph)结点之间定向流动(flow),从输入走到输出。
在 TensorFlow 中,每次运算都可以用数据流图(dataflow graph)的方式表示。每个数据流图都有以下两个重要元素:
● 一组 tf.Operation 对象,代表运算本身;
● 一组 tf.Tensor 对象,代表被运算的数据。
如下图所示,这里我们以一个简单的例子说明数据流图具体是怎样运行的。
假设图中的 x=[1,3,6],y=[1,1,1]。由于 tf.Tensor 被用来表示运算数据,因此在 TensorFlow 中我们会首先定义两个 tf.Tensor 常量对象存放数据。然后再用 tf.Operation 对象定义图中的加法运算,具体代码如下:
import tensorflow as tf
x = tf.constant([1,3,6])
y = tf.constant([1,1,1])
op = tf.add(x,y)
现在,我们已经定义了数据流图的两个重要元素:tf.Operation 和 tf.Tensor,那么如何构建图本身呢,具体代码如下:
import tensorflow as tf
my_graph = tf.Graph()
with my_graph.as_default():
x = tf.constant([1,3,6])
y = tf.constant([1,1,1])
op = tf.add(x,y)
至此我们已经完成了数据流图的定义,在 TensorFlow 中,只有先定义了图,才能进行后续的计算操作(即驱动数据在图的结点间定向流动)。这里 TensorFlow 又规定,要进行后续的计算,必须通过 tf.Session 来统一管理,因此我们还要定义一个 tf.Session 对象,即会话。
在 TensorFlow 中,tf.Session 专门用来封装 tf.Operation 在 tf.Tensor 基础上执行的操作环境。因此,在定义 tf.Session 对象时,也需要传入相应的数据流图(可以通过 graph 参数传入),本例中具体的代码如下:
import tensorflow as tf
my_graph = tf.Graph()
with tf.Session(graph=my_graph) as sess:
x = tf.constant([1,3,6])
y = tf.constant([1,1,1])
op = tf.add(x,y)
定义好 tf.Session 之后,我们可以通过 tf.Session.run() 方法来执行对应的数据流图。run() 方法可以通过 fetches 参数传入相应 tf.Operation 对象,并导入与 tf.Operation 相关的所有 tf.Tensor 对象,然后递归执行与当前 tf.Operation 有依赖关系的所有操作。本例中具体执行的是求和操作,实现代码如下:
import tensorflow as tf
my_graph = tf.Graph()
with tf.Session(graph=my_graph) as sess:
x = tf.constant([1,3,6])
y = tf.constant([1,1,1])
op = tf.add(x,y)
result = sess.run(fetches=op)
print(result)
>>> [2 4 7]
可以看到运算结果是 [2 4 7]。
关于预测模型
了解 TensorFlow 的基本原理之后,下面的任务是如何构建一个预测模型。简单来说,机器学习算法 + 数据就等于预测模型。构建预测模型的流程如下图所示:
如图,经过数据训练的机器学习算法就是模型。训练好一个模型之后,输入待预测数据,就能得到相应的预测结果。大体流程如下图所示:
在本例中,我们将要构建的模型需要根据输入文本,输出相应的类别,即完成文本分类的工作。因此这里的输入应该是文本(text),输出是类别(category)。更具体地说,本例中我们已经事先获取了标记数据(即一些已经标明了类别的文本段),然后用这些数据对算法进行训练,最后再用训练好的模型对新文本分类。这一过程也就是通常所说的监督学习(supervised learning)。另外,由于我们的任务是对文本数据进行分类,所以也属于分类问题的范畴。
为了构建该文本分类模型,下面我们需要介绍一些神经网络的基础知识。
关于神经网络
从本质上说,神经网络是计算模型(computational model)的一种。(注:这里所谓计算模型是指通过数学语言和数学概念描述系统的方法)并且这种计算模型还能够自动完成学习和训练,不需要精确编程。
最原始也是最基础的一个神经网络算法模型是感知机模型(Perceptron),关于感知机模型的详细介绍请参见这篇博客:
https://t.cn/R5MphRp
由于神经网络模型是模拟人类大脑神经系统的组织结构而提出的,因此它与人类的脑神经网络具有相似的结构。
如上图所示,一般的神经网络结构可以分为三层:输入层、隐蔽层(hidden layer)和输出层。
为了深入理解神经网络究竟是如何工作的,我们需要利用 TensorFlow 自己亲手构建一个神经网络模型,下面介绍一个具体的实例。
本例中,我们有两个隐蔽层(关于隐蔽层层数的选择是另一个问题,详细内容https://stats.stackexchange.com/questions/181/how-to-choose-the-number-of-hidden-layers-and-nodes-in-a-feedforward-neural-netw
)。概括地说,隐蔽层的主要作用是将输入层的数据转换成一种输出层更便于利用的形式。
如图所示,本例中输入层的每个结点都代表了输入文本中的一个词,接下来是第一个隐蔽层。这里需要注意的是,第一层隐蔽层的结点个数选择也是一项重要的任务,通常被称为特征选择。
图中的每个结点(也被称为神经元),都会搭配一个权重。而我们下面所谓训练过程其实就是不断调整这些权重值,让模型的实际输出和预想输出更匹配的过程。当然,除了权重之外,整个网络还要加上一个偏差值。
对每个结点做加权和并加上一个偏差值之后,还需要经过激活函数(activation function)的处理才能输出到下一层 。
x为一个神经元的值,W为权重,b为偏差值,softmax()为激活函数,a即为输出值。
实际上,这里激活函数确定了每个结点的最终输出情况,同时为整个模型加入了非线性元素。如果用台灯来做比喻的话,激活函数的作用就相当于开关。实际研究中根据应用的具体场景和特点,有各种不同的激活函数可供选择,这里屏蔽层选择的是 ReLu 函数。
另外图中还显示了第二个隐蔽层,它的功能和第一层并没有本质区别,唯一的不同就是它的输入是第一层的输出,而第一层的输入则是原始数据。
最后是输出层,本例中应用了独热编码的方式来对结果进行分类。这里所谓独热编码是指每个向量中只有一个元素是 1,其他均为 0 的编码方式。例如我们要将文本数据分为三个类别(体育、航空和电脑绘图),则编码结果为:
这里独热编码的好处是:输出结点的个数恰好等于输出类别的个数。此外,输出层和前面的隐蔽层结构类似,我们也要为每个结点搭配一个权重值,加上恰当的偏差,最后通过激活函数的处理。
但本例中输出层的激活函数与隐蔽层的激活函数不同。由于本例的最终目的是输出每个文本对应的类别信息,而这里所有类别之间又是互斥的关系。基于这些特点,我们在输出层选择了 Softmax 函数作为激活函数。该函数的特点是可以将输出值转换为 0-1 之间的一个小数值,并且这些小数值的和为 1。于是正好可以用这些小数表示每个类别的可能性分布情况。假如刚才提到的三个类别原本的输出值为 1.2、0.9 和 0.4,则通过 Softmax 函数的处理后,得到的结果为:
可以看到这三个小数的和正好为 1。
到目前为止,我们已经明确了该神经网络的数据流图,下面为具体的代码实现:
# Network Parameters
n_hidden_1 = 10 # 1st layer number of features
n_hidden_2 = 5 # 2nd layer number of features
n_input = total_words # Words in vocab
n_classes = 3 # Categories: graphics, space and baseball
def multilayer_perceptron(input_tensor, weights, biases ):
layer_1_multiplication = tf.matmul(input_tensor, weights['h1'])
layer_1_addition = tf.add(layer_1_multiplication, biases['b1'])
layer_1_activation = tf.nn.relu(layer_1_addition)
# Hidden layer with RELU activation
layer_2_multiplication = tf.matmul(layer_1_activation, weights['h2'])
layer_2_addition = tf.add(layer_2_multiplication, biases['b2'])
layer_2_activation = tf.nn.relu(layer_2_addition)
# Output layer with linear activation
out_layer_multiplication = tf.matmul(layer_2_activation, weights['out'])
out_layer_addition = out_layer_multiplication + biases['out']
return out_layer_addition
神经网络的训练
如前所述,模型训练中一项非常重要的任务就是调整结点的权重。本节我们将介绍如何在 TensorFlow 中实现这一过程。
在 TensorFlow 中,结点权重和偏差值以变量的形式存储,即 tf.Variable 对象。在数据流图调用 run() 函数的时候,这些值将保持不变。在一般的机器学习场景中,权重值和偏差值的初始取值都通过正太分布确定。具体代码如下图所示:
weights = {
'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])),
'h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])),
'out': tf.Variable(tf.random_normal([n_hidden_2, n_classes]))
}
biases = {
'b1': tf.Variable(tf.random_normal([n_hidden_1])),
'b2': tf.Variable(tf.random_normal([n_hidden_2])),
'out': tf.Variable(tf.random_normal([n_classes]))
}
以初始值运行神经网络之后,会得到一个实际输出值 z,而我们的期望输出值是 expected,这时我们需要做的就是计算两者之间的误差,并通过调整权重等参数使之最小化。一般计算误差的方法有很多,这里因为我们处理的是分类问题,因此采用交叉熵误差。
在 TensorFlow 中,我们可以通过调用 tf.nn.softmax_cross_entropy_with_logits() 函数来计算交叉熵误差,因为这里我们的激活函数选择了 Softmax ,因此误差函数中出现了 softmax_ 前缀。具体代码如下(代码中我们同时调用了
tf.reduced_mean() 函数来计算平均误差):
# Construct model
prediction = multilayer_perceptron(input_tensor, weights, biases)
# Define loss
entropy_loss = tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=output_tensor)
loss = tf.reduce_mean(entropy_loss)
得到误差之后,下面的任务是如何使之最小化。这里我们选择的方法是最常用的随机梯度下降法,其直观的原理图如下所示:
同样,用来计算梯度下降的方法也有很多,这里我们采用了 Adaptive Moment Estimation (Adam) 优化法,即自适应矩估计的优化方法,具体在 TensorFlow 中的体现是 tf.train.AdamOptimizer(learning_rate).minimize(loss) 函数。这里我们需要传入 learning_rate 参数以决定计算梯度时的步进长度。
非常方便的一点是,AdamOptimizer() 函数封装了两种功能:一是计算梯度,二是更新梯度。换句话说,调用该函数不但能计算梯度值,还能将计算结果更新到所有 tf.Variables 对象中,这一点大大降低了编程复杂度。
具体模型训练部分的代码如下所示:
learning_rate = 0.001
# Construct model
prediction = multilayer_perceptron(input_tensor, weights, biases)
# Define loss
entropy_loss = tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=output_tensor)
loss = tf.reduce_mean(entropy_loss)
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)
数据处理
本例中,我们得到的原始数据是许多英文的文本片段,为了将这些数据导入模型中,我们需要对原始数据进行必要的预处理过程。这里具体包括两个部分:
● 为每个单词编码;
● 为每个文本片段创建对应的张量表示,其中以数字 1 代表出现了某个单词,0 表示没有该单词。
具体实现代码如下:
import numpy as np #numpy is a package for scientific computing
from collections import Counter
vocab = Counter()
text = "Hi from Brazil"
#Get all words
for word in text.split(' '):
vocab[word]+=1
#Convert words to indexes
def get_word_2_index(vocab):
word2index = {}
for i,word in enumerate(vocab):
word2index[word] = i
return word2index
#Now we have an index
word2index = get_word_2_index(vocab)
total_words = len(vocab)
#This is how we create a numpy array (our matrix)
matrix = np.zeros((total_words),dtype=float)
#Now we fill the values
for word in text.split():
matrix[word2index[word]] += 1
print(matrix)
>>> [ 1. 1. 1.]
从以上代码可以看到,当输入文本是“Hi from Brazil”时,输出矩阵是
[ 1. 1. 1.]。而当输入文本只有“Hi”时又会怎么样呢,具体代码和结果如下:
matrix = np.zeros((total_words),dtype=float)
text = "Hi"
for word in text.split():
matrix[word2index[word.lower()]] += 1
print(matrix)
>>> [ 1. 0. 0.]
可以看到,这时的输出是 [ 1. 0. 0.]。
相应的,我们也可以对类别信息进行编码,只不过这时使用的是独热编码:
y = np.zeros((3),dtype=float)
if category == 0:
y[0] = 1. # [ 1. 0. 0.]
elif category == 1:
y[1] = 1. # [ 0. 1. 0.]
else:
y[2] = 1. # [ 0. 0. 1.]
运行模型并预测
至此我们已经对 TensorFlow、神经网络模型、模型训练和数据预处理等方面有了初步的了解,下面我们将演示如何将这些知识应用于实际的数据。
这里我们的数据来源是 20 Newsgroups,其中包括了 18000 篇新闻稿,覆盖率 20 个类别,开源免费,下载地址为:
https://t.cn/zY6ssrE
首先,为了导入这些数据集,我们需要借助 scikit-learn 库。它也是个开源的函数库,基于 Python 语言,主要进行机器学习相关的数据处理任务。本例中我们只使用了其中的三个类:comp.graphics,sci.space 和 rec.sport.baseball。
最终数据会被分为两个子集,一个是数据训练集,一个是测试集。这里的建议是最好不要提前查看测试数据集。因为提前查看测试数据会影响我们对模型参数的选择,从而影响模型对其他未知数据的通用性。
具体的数据导入代码如下:
from sklearn.datasets import fetch_20newsgroups
categories = ["comp.graphics","sci.space","rec.sport.baseball"]
newsgroups_train = fetch_20newsgroups(subset='train', categories=categories)
newsgroups_test = fetch_20newsgroups(subset='test', categories=categories)
在神经网络术语中,一个 epoch 过程就是对所有训练数据的一个前向传递(forward pass)加后向传递(backward pass)的完整循环。这里前向是指根据现有权重得到实际输出值的过程,后向是指根据误差结果反过来调整权重的过程。下面我们重点介绍一下 tf.Session.run() 函数,实际上它的完整调用形式如下:
tf.Session.run(fetches, feed_dict=None, options=None, run_metadata=None)
在文章开头介绍该函数时,我们只通过 fetches 参数传入了加法操作,但其实它还支持一次传入多种操作的用法。在面向实际数据的模型训练环节,我们就传入了两种操作:一个是误差计算(即随机梯度下降),另一个是优化函数(即自适应矩估计)。
run() 函数中另一个重要的参数是 feed_dict,我们就是通过这个参数传入模型每次处理的输入数据。而为了输入数据,我们又必须先定义 tf.placeholders。
按照官方文档的解释,这里 placeholder 仅仅是一个空客,用于引用即将导入模型的数据,既不需要初始化,也不存放真实的数据。本例中定义 tf.placeholders 的代码如下:
n_input = total_words # Words in vocab
n_classes = 3 # Categories: graphics, sci.space and baseball
input_tensor = tf.placeholder(tf.float32,[None, n_input],name="input")
output_tensor = tf.placeholder(tf.float32,[None, n_classes],name="output")
在进行实际的模型训练之前,还需要将数据分成 batch,即一次计算处理数据的量。
这时就体现了之前定义 tf.placeholders 的好处,即可以通过 placeholders 定义中的“None”参数指定一个维度可变的 batch。也就是说,batch 的具体大小可以等后面使用时再确定。这里我们在模型训练阶段传入的 batch 更大,而测试阶段可能会做一些改变,因此需要使用可变 batch。随后在训练中,我们通过 get_batches() 函数来获取每次处理的真实文本数据。具体模型训练部分的代码如下:
training_epochs = 10
# Launch the graph
with tf.Session() as sess:
sess.run(init) #inits the variables (normal distribution, remember?)
# Training cycle
for epoch in range(training_epochs):
avg_cost = 0.
total_batch = int(len(newsgroups_train.data)/batch_size)
# Loop over all batches
for i in range(total_batch):
batch_x,batch_y = get_batch(newsgroups_train,i,batch_size)
# Run optimization op (backprop) and cost op (to get loss value)
c,_ = sess.run([loss,optimizer], feed_dict={input_tensor: batch_x, output_tensor:batch_y})
至此我们已经针对实际数据完成了模型训练,下面到了应用测试数据对模型进行测试的时候。在测试过程中,和之前的训练部分类似,我们同样要定义图元素,包括操作和数据两类。这里为了计算模型的精度,同时还因为我们对结果引入了独热编码,因此需要同时得到正确输出的索引,以及预测输出的索引,并检查它们是否相等,如果不等,要计算相应的平均误差。具体实现代码和结果如下:
# Test model
index_prediction = tf.argmax(prediction, 1)
index_correct = tf.argmax(output_tensor, 1)
correct_prediction = tf.equal(index_prediction, index_correct)
# Calculate accuracy
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
total_test_data = len(newsgroups_test.target)
batch_x_test,batch_y_test = get_batch(newsgroups_test,0,total_test_data)
print("Accuracy:", accuracy.eval({input_tensor: batch_x_test, output_tensor: batch_y_test}))
>>> Epoch: 0001 loss= 1133.908114347
Epoch: 0002 loss= 329.093700409
Epoch: 0003 loss= 111.876660109
Epoch: 0004 loss= 72.552971845
Epoch: 0005 loss= 16.673050320
Epoch: 0006 loss= 16.481995190
Epoch: 0007 loss= 4.848220565
Epoch: 0008 loss= 0.759822878
Epoch: 0009 loss= 0.000000000
Epoch: 0010 loss= 0.079848485
Optimization Finished!
Accuracy: 0.75
最终可以看到,我们的模型预测精度达到了 75%,对于初学者而言,这个成绩还是不错的。至此,我们已经通过 TensorFlow 实现了基于神经网络模型的文本分类任务。