1.4 使用神经网络解决问题
到这里为止,我们的准备工作就做好了。现在,我们对一个简单的数据集进行神经网络的学习。
1.4.1 螺旋状数据集
本书在dataset目录中提供了几个便于处理数据集的类,本节将使用其中的dataset/spiral.py文件。这个文件中实现了读取螺旋(旋涡)状数据的类,其用法如下所示(ch01/show_spiral_dataset.py)。
import sys sys.path.append('..') # 为了引入父目录的文件而进行的设定 from dataset import spiral import matplotlib.pyplot as plt x, t = spiral.load_data() print ('x', x.shape) # (300, 2) print ('t', t.shape) # (300, 3)
在上面的例子中,要从ch01目录的dataset目录引入spiral.py。因此,上面的代码通过sys.path.append('..')将父目录添加到了import的检索路径中。
然后,使用spiral.load_data()进行数据的读入。此时,x是输入数据, t是监督标签。观察x和t的形状,可知它们各自有300笔样本数据,其中x是二维数据,t是三维数据。另外,t是one-hot向量,对应的正确解标签的类标记为1,其余的标记为0。下面,我们把这些数据绘制在图上,结果如图1-31所示。
图1-31 学习用的螺旋状数据集(用×▲●分别表示3个类)
如图1-31所示,输入是二维数据,类别数是3。观察这个数据集可知,它不能被直线分割。因此,我们需要学习非线性的分割线。那么,我们的神经网络(具有使用非线性的sigmoid激活函数的隐藏层的神经网络)能否正确学习这种非线性模式呢?让我们实验一下。
因为这个实验相对简单,所以我们不把数据集分成训练数据、验证数据和测试数据。不过,实际任务中会将数据集分为训练数据和测试数据(以及验证数据)来进行学习和评估。
1.4.2 神经网络的实现
现在,我们来实现一个具有一个隐藏层的神经网络。首先,import语句和初始化程序的__init__()如下所示(ch01/two_layer_net.py)。
import sys sys.path.append('..') import numpy as np from common.layers import Affine, Sigmoid, SoftmaxWithLoss class TwoLayerNet: def__init__(self, input_size, hidden_size, output_size): I, H, O = input_size, hidden_size, output_size # 初始化权重和偏置 W1 = 0.01 * np.random.randn(I, H) b1 = np.zeros(H) W2 = 0.01 * np.random.randn(H, O) b2 = np.zeros(O) # 生成层 self.layers = [ Affine(W1, b1), Sigmoid(), Affine(W2, b2) ] self.loss_layer = SoftmaxWithLoss() # 将所有的权重和梯度整理到列表中 self.params, self.grads = [], [] for layer in self.layers: self.params += layer.params self.grads += layer.grads
初始化程序接收3个参数。input_size是输入层的神经元数,hidden_size是隐藏层的神经元数,output_size是输出层的神经元数。在内部实现中,首先用零向量(np.zeros())初始化偏置,再用小的随机数(0.01 *np.random.randn())初始化权重。通过将权重设成小的随机数,学习可以更容易地进行。接着,生成必要的层,并将它们整理到实例变量layers列表中。最后,将这个模型使用到的参数和梯度归纳在一起。
因为Softmax with Loss层和其他层的处理方式不同,所以不将它放入layers列表中,而是单独存储在实例变量loss_layer中。
接着,我们为TwoLayerNet实现3个方法,即进行推理的predict()方法、正向传播的forward()方法和反向传播的backward()方法( ch01/two_layer_net.py)。
def predict(self, x): for layer in self.layers: x = layer.forward(x) return x def forward(self, x, t): score = self.predict(x) loss = self.loss_layer.forward(score, t) return loss def backward(self, dout=1): dout = self.loss_layer.backward(dout) for layer in reversed(self.layers): dout = layer.backward(dout) return dout
如上所示,这个实现非常清楚。因为我们已经将神经网络中要用的处理模块实现为了层,所以这里只需要以合理的顺序调用这些层的forward()方法和backward()方法即可。
1.4.3 学习用的代码
下面,我们来看一下学习用的代码。首先,读入学习数据,生成神经网络(模型)和优化器。然后,按照之前介绍的学习的4个步骤进行学习。另外,在机器学习领域,通常将针对具体问题设计的方法(神经网络、SVM等)称为模型。学习用的代码如下所示(ch01/train_custom_loop.py)。
import sys sys.path.append('..') import numpy as np from common.optimizer import SGD from dataset import spiral import matplotlib.pyplot as plt from two layer net import TwoLayerNet # ❶设定超参数 max_epoch = 300 batch_size = 30 hidden_size = 10 learning_rate = 1.0 # ❷读入数据,生成模型和优化器 x, t = spiral.load_data() model = TwoLayerNet(input_size=2, hidden_size=hidden_size, output_size=3) optimizer = SGD(lr=learning_rate) # 学习用的变量 data_size = len(x) max_iters = data_size // batch_size total_loss = 0 loss_count = 0 loss_list = [] for epoch in range(max_epoch): # ❸打乱数据 idx = np.random.permutation(data_size) x = x[idx] t = t[idx] for iters in range(max_iters): batch_x = x[iters*batch_size:(iters+1)*batch_size] batch_t = t[iters*batch_size:(iters+1)*batch_size] # ❹计算梯度,更新参数 loss = model.forward(batch_x, batch_t) model.backward() optimizer.update(model.params, model.grads) total_loss += loss loss_count += 1 # ❺定期输出学习过程 if (iters+1) % 10 == 0: avg_loss = total_loss / loss_count print ('| epoch %d | iter %d / %d | loss %.2f' % (epoch + 1, iters + 1, max_iters, avg_loss)) loss_list.append(avg_loss) total_loss, loss_count = 0, 0
首先,在代码❶的地方设定超参数。具体而言,就是设定学习的epoch数、mini-batch的大小、隐藏层的神经元数和学习率。接着,在代码❷的地方进行数据的读入,生成神经网络(模型)和优化器。我们已经将2层神经网络实现为了TwoLayerNet类,将优化器实现为了SGD类,这里直接使用它们就可以。
epoch表示学习的单位。1个epoch相当于模型“看过”一遍所有的学习数据(遍历数据集)。这里我们进行300个epoch的学习。
在进行学习时,需要随机选择数据作为mini-batch。这里,我们以epoch为单位打乱数据,对于打乱后的数据,按顺序从头开始抽取数据。数据的打乱(准确地说,是数据索引的打乱)使用np.random.permutation()方法。给定参数N,该方法可以返回0到N-1的随机序列,其实际的使用示例如下所示。
>>> import numpy as np >>> np.random.permutation(10) array([7, 6, 8, 3, 5, 0, 4, 1, 9, 2]) >>> np.random.permutation(10) array([1, 5, 7, 3, 9, 2, 8, 6, 0, 4])
像这样,调用np.random.permutation()可以随机打乱数据的索引。
接着,在代码❹的地方计算梯度,更新参数。最后,在代码❺的地方定期地输出学习结果。这里,每10次迭代计算1次平均损失,并将其添加到变量loss_list中。以上就是学习用的代码。
这里实现的神经网络的学习用的代码在本书其他地方也可以使用。因此,本书将这部分代码作为Trainer类提供出来。使用Trainer类,可以将神经网络的学习细节嵌入Trainer类。详细的用法将在1.4.4节说明。
运行一下上面的代码(ch01/train_custom_loop.py)就会发现,向终端输出的损失的值在平稳下降。我们将结果画出来,如图1-32所示。
由图1-32可知,随着学习的进行,损失在减小。我们的神经网络正在朝着正确的方向学习!接下来,我们将学习后的神经网络的区域划分(也称为决策边界)可视化,结果如图1-33所示。
由图1-33可知,学习后的神经网络可以正确地捕获“旋涡”这个模式。也就说,模型正确地学习了非线性的区域划分。像这样,神经网络通过隐藏层可以实现复杂的表现力。深度学习的特征之一就是叠加的层越多,表现力越丰富。
图1-32 损失的图形:横轴是学习的迭代次数(刻度值的10倍),竖轴是每10次迭代的平均损失
图1-33 学习后的神经网络的决策边界(用不同颜色描绘神经网络识别的各个类别的区域)
1.4.4 Trainer类
如前所述,本书中有很多机会执行神经网络的学习。为此,就需要编写前面那样的学习用的代码。然而,每次都写相同的代码太无聊了,因此我们将进行学习的类作为Trainer类提供出来。Trainer类的内部实现和刚才的源代码几乎相同,只是添加了一些新的功能而已,我们在需要的时候再详细说明其用法。
Trainer类的代码在common/trainer.py中。这个类的初始化程序接收神经网络(模型)和优化器,具体如下所示。
model = TwoLayerNet(...) optimizer = SGD(lr=1.0) trainer = Trainer(model, optimizer)
然后,调用fit()方法开始学习。fit()方法的参数如表1-1所示。
表1-1 Trainer类的fit()方法的参数。表中的"(=XX)"表示参数的默认值
另外,Trainer类有plot()方法,它将fit()方法记录的损失(准确地说,是按照eval_interval评价的平均损失)在图上画出来。使用Trainer类进行学习的代码如下所示(ch01/train.py)。
import sys sys.path.append('..') from common.optimizer import SGD from common.trainer import Trainer from dataset import spiral from two layer net import TwoLayerNet max_epoch = 300 batch_size = 30 hidden_size = 10 learning_rate = 1.0 x, t = spiral.load_data() model = TwoLayerNet(input_size=2, hidden_size=hidden_size, output_size=3) optimizer = SGD(lr=learning_rate) trainer = Trainer(model, optimizer) trainer.fit(x, t, max_epoch, batch_size, eval_interval=10) trainer.plot()
执行这段代码,会进行和之前一样的神经网络的学习。通过将之前展示的学习用的代码交给Trainer类负责,代码变简洁了。本书今后都将使用Trainer类进行学习。