神经网络的学习步骤
前提
神经网络存在合适的权重和偏置,调整权重和偏置的过程以便拟合训练数据的过程称为“学习”。神经网络的学习分成以下四个步骤。<!-- more -->
选出mini-batch
从训练数据中随机选出一部分数据作为mini-batch,我们的目的是尽量减少mini-batch的损失函数的值。
计算梯度
为了减少mini-batch损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减少最多的方向。
更新参数
将权重参数沿梯度方向进行更新
重复
重复前三个步骤
构建神经网络
接下来我们以两层神经网络为对象(隐藏层为一层),使用MNIST数据集进行学习。
import sys, os # 添加mnist.py所在的目录到Python路径 sys.path.append("E:\\deap_learning\\ORIGAINAL") """ # 确认common是个文件夹并包含__init__.py文件 if not os.path.exists("E:\\deap_learning\\ORIGAINAL\\common\\__init__.py"): open("E:\\deap_learning\\ORIGAINAL\\common\\__init__.py", 'a').close() """ from common.functions import * from common.gradient import numerical_gradient class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01): # 初始化权重 self.params = {} self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) self.params['b1'] = np.zeros(hidden_size) self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) self.params['b2'] = np.zeros(output_size) def predict(self, x): W1, W2 = self.params['W1'], self.params['W2'] b1, b2 = self.params['b1'], self.params['b2'] a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) a2 = np.dot(z1, W2) + b2 y = softmax(a2) return y # x:输入数据, t:监督数据 def loss(self, x, t): y = self.predict(x) return cross_entropy_error(y, t) def accuracy(self, x, t): y = self.predict(x) y = np.argmax(y, axis=1) t = np.argmax(t, axis=1) accuracy = np.sum(y == t) / float(x.shape[0]) return accuracy # x:输入数据, t:监督数据 def numerical_gradient(self, x, t): loss_W = lambda W: self.loss(x, t) grads = {} grads['W1'] = numerical_gradient(loss_W, self.params['W1']) grads['b1'] = numerical_gradient(loss_W, self.params['b1']) grads['W2'] = numerical_gradient(loss_W, self.params['W2']) grads['b2'] = numerical_gradient(loss_W, self.params['b2']) return grads def gradient(self, x, t): W1, W2 = self.params['W1'], self.params['W2'] b1, b2 = self.params['b1'], self.params['b2'] grads = {} batch_num = x.shape[0] # forward a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) a2 = np.dot(z1, W2) + b2 y = softmax(a2) # backward dy = (y - t) / batch_num grads['W2'] = np.dot(z1.T, dy) grads['b2'] = np.sum(dy, axis=0) da1 = np.dot(dy, W2.T) dz1 = sigmoid_grad(a1) * da1 grads['W1'] = np.dot(x.T, dz1) grads['b1'] = np.sum(dz1, axis=0) return grads
twolayernet类有两个字典型实列变量——params和grads。params内保存了权重参数,并且其中的权重参数会用在推理处理内。而grads内保存了各个参数的梯度。使用 numberical_gradient()
方法计算梯度后,梯度的信息会保存在grads变量中。随后我们来看下twolayernet方法的实现。
首先是init(self,input_size, hidden size,output_size)方法,它是类的初始化方法(所谓初始化方法,就是生成TwoLayerNet实例时被调用的方法)。从第1个参数开始,依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。另外,因为进行手写数字识别时,输入图像的大小是784(28×28),输出为10个类别,所以指定参数input_size=784、output_size=10,将隐藏层的个数hidden_size设置为一个合适的值即可。 此外,这个初始化方法会对权重参数进行初始化。如何设置权重参数的初始值这个问题是关系到神经网络能否成功学习的重要问题。后面我们会详细讨论权重参数的初始化,这里只需要知道,权重使用符合高斯分布的随机数进行初始化,偏置使用0进行初始化。predict(self,x)和accuracy(self,x,t)的实现和上一章的神经网络的推理处理基本一样。如果仍有不明白的地方,请再回顾一下上一章的内容。另外,loss(self,x,t)是计算损失函数值的方法。这个方法会基于predict()的结果和正确解标签 计算交叉熵误差。 剩下的numerical_gradient(self,x,t)方法会计算各个参数的梯度。根据数值微分,计算各个参数相对于损失函数的梯度。另外,gradient(self,x,t)是之后要实现的方法,该方法使用误差反向传播法高效地计算梯度。
mini-batch的实现
在这里的代码中,mini-batch的大小是100,每次需要从60000个训练数据中随机取出100个数据作为mini-batch求梯度,使用随机梯度算法更新参数。随着学习的进行,损失函数的值会逐渐减小,这说明神经网络的权重参数正在逐渐拟合数据,正在向最优参数靠近。
代码实现
import sys import os import numpy as np from mnist import load_mnist from two_layer_net import TwoLayerNet import matplotlib.pyplot as plt # 设置本地数据集路径 mnist_path = "E:\\deap_learning\\resource" # 读入数据 (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True) network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) iters_num = 10000 # 适当设定循环的次数 train_size = x_train.shape[0] batch_size = 100 learning_rate = 0.1 train_loss_list = [] train_acc_list = [] test_acc_list = [] iter_per_epoch = max(train_size / batch_size, 1) for i in range(iters_num): batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] # 计算梯度 #grad = network.numerical_gradient(x_batch, t_batch) grad = network.gradient(x_batch, t_batch) # 更新参数 for key in ('W1', 'b1', 'W2', 'b2'): network.params[key] -= learning_rate * grad[key] loss = network.loss(x_batch, t_batch) train_loss_list.append(loss) if i % iter_per_epoch == 0: train_acc = network.accuracy(x_train, t_train) test_acc = network.accuracy(x_test, t_test) train_acc_list.append(train_acc) test_acc_list.append(test_acc) print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc)) # 绘制图形 markers = {'train': 'o', 'test': 's'} x = np.arange(len(train_acc_list)) plt.plot(x, train_acc_list, label='train acc') plt.plot(x, test_acc_list, label='test acc', linestyle='--') plt.xlabel("epochs") plt.ylabel("accuracy") plt.ylim(0, 1.0) plt.legend(loc='lower right') plt.show()
附上mnist(本地数据):
import os import gzip import pickle import numpy as np def load_mnist(normalize=True, flatten=True, one_hot_label=False): """读入MNIST数据集 Parameters ---------- normalize : 将图像的像素值正规化为0.0~1.0 one_hot_label : one_hot_label为True的情况下,标签作为one-hot数组返回 one-hot数组是指[0,0,1,0,0,0,0,0,0,0]这样的数组 flatten : 是否将图像展开为一维数组 Returns ------- (训练图像, 训练标签), (测试图像, 测试标签) """ mnist_path = "E:\\deap_learning\\resource" dataset = { 'train_img': load_images(os.path.join(mnist_path, 'train-images-idx3-ubyte.gz')), 'train_label': load_labels(os.path.join(mnist_path, 'train-labels-idx1-ubyte.gz')), 'test_img': load_images(os.path.join(mnist_path, 't10k-images-idx3-ubyte.gz')), 'test_label': load_labels(os.path.join(mnist_path, 't10k-labels-idx1-ubyte.gz')) } if normalize: for key in ('train_img', 'test_img'): dataset[key] = dataset[key].astype(np.float32) dataset[key] /= 255.0 if one_hot_label: dataset['train_label'] = _change_one_hot_label(dataset['train_label']) dataset['test_label'] = _change_one_hot_label(dataset['test_label']) if not flatten: for key in ('train_img', 'test_img'): dataset[key] = dataset[key].reshape(-1, 1, 28, 28) return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label']) def load_images(file_path): with gzip.open(file_path, 'rb') as f: data = np.frombuffer(f.read(), np.uint8, offset=16) return data.reshape(-1, 784) def load_labels(file_path): with gzip.open(file_path, 'rb') as f: data = np.frombuffer(f.read(), np.uint8, offset=8) return data def _change_one_hot_label(X): T = np.zeros((X.size, 10)) for idx, row in enumerate(T): row[X[idx]] = 1 return T
在代码中出现了epoch作为学习时的次数单位。
什么是 epoch
?
在机器学习和深度学习中,epoch
是一个重要的概念。epoch
表示整个训练数据集通过神经网络一次。简单来说,一个 epoch
就是将所有的训练数据都用来更新模型参数一次。
epoch
在神经网络中的作用
在训练神经网络时,我们通常不会一次性将整个训练数据集都输入到模型中进行训练,而是将数据集分成多个小批次(batch),然后依次输入模型。这样做的原因包括:
-
内存限制:一次性处理整个数据集可能会导致内存不足,尤其是当数据集非常大时。
-
更稳定的梯度更新:使用小批次数据可以使梯度更新更加平稳,并能有效避免一些局部最优解。
在每个 epoch
中,模型会经历以下过程:
-
将训练数据集分成若干个批次(batches)。
-
对每个批次进行前向传播、计算损失、后向传播、更新模型参数。
-
一个
epoch
结束后,通常会进行一次验证,检查模型在验证集上的表现,以判断模型是否在训练中取得进展。
总结
我们以损失函数为基准,找出了使神经网络的值达到最小的权重参数,也就是神经网络学习的目标。为了尽可能找到小的损失函数值,我们使用了函数斜率的梯度法。