斯坦福cs231n(2017年版)的所有编程作业均采用iPython Notebooks实现,不熟悉的朋友可以提前使用一下Notebooks。编程作业#4主要是手写实现一个两层的神经网络(单隐层)分类器来对cifar-10图像数据集进行分类。
目录
1.实验综述
2.导入必要的包
import numpy as np
import matplotlib.pyplot as plt
#从cs231n/classifiers/neural_net.py中导入TwoLayerNet类
from cs231n.classifiers.neural_net import TwoLayerNet
from __future__ import print_function
#绘图默认设置
%matplotlib inline
plt.rcParams['figure.figsize'] = (10.0, 8.0) # 默认大小
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'
# 更多重载外部Python模块的魔法命令查看 http://stackoverflow.com/questions/1907993/autoreload-of-modules-in-ipython
%load_ext autoreload
%autoreload 2
def rel_error(x, y):
""" 返回相对误差 """
return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))
#创建一个小的网络模型和一些小型数据来检查我们的实现的正确性
#最后把正确实现的模型在整个数据集上进行训练
#注意我们为可重复的实验设置随机种子(保证每次生成的随机数一致)
input_size = 4 #输入层单元数 样本的特征向量维数
hidden_size = 10 #隐层单元数
num_classes = 3 #输出层单元数 分类的类别数
num_inputs = 5 #输入的样本数
def init_toy_model():
np.random.seed(0)
return TwoLayerNet(input_size, hidden_size, num_classes, std=1e-1)
def init_toy_data():
np.random.seed(1)
X = 10 * np.random.randn(num_inputs, input_size)
y = np.array([0, 1, 2, 2, 1])
return X, y
net = init_toy_model()
X, y = init_toy_data()
3.前向传播
完整的loss方法:
def loss(self, X, y=None, reg=0.0):
"""
计算两层全联接网络的loss和梯度。
注意计算损失的公式和之前吴恩达专项课程中稍有差别,因为之前我们把样本真实标签转换成了one-hot形式,如进行3分类,标签为2(类别索引),则表示为[0,0,1];本实验中我们并没有把标签转换为one-hot形式,直接使用标签/类别索引[0,C)之间的一个整数,所以损失函数的定义稍有差别,损失函数部分的前向/反向传播稍有不同,原理是一致的。
Inputs:
- X: 数据集样本的特征矩阵 (N, D). 每一行代表一个样本的特征向量.
- y: 数据集样本的标签. y[i] 是样本 X[i]的标签, y[i] 是一个整数 0 <= y[i] < C. 这个参数是缺省的,如果没有传入默认为None,此时只返回类别得分, 如果传入了则返回loss和梯度。
- reg: 正则化强度,默认为0不进行正则化.
Returns:
如果没传入y,y=None, 返回一个得分矩阵 维度(N, C) 每一行代表一个样本的得分向量。通常对mini-batch中的N个样本同时进行计算(矢量化并行计算).
如果传入y,则返回一个元组。包括以下几部分:
- loss: mini-batch上N个样本的损失(数据损失和正则化损失)
- grads:字典形式,键和self.params一致,为权重和偏置参数的名称(字符串),值为该参数相对于损失函数的梯度。
"""
# 从参数字典中取出初始化的参数
W1, b1 = self.params['W1'], self.params['b1']
W2, b2 = self.params['W2'], self.params['b2']
N, D = X.shape
# 前向传播:计算得分
scores = None
z1 = X.dot(W1) + b1 #(N,H)
h1 = np.maximum(0,z1) #(N,H)
z2 = h1.dot(W2) + b2 #(N,C)
scores = z2
if y is None:
return scores
# 计算loss
loss = None
maxLogC = np.max(scores,axis=1).reshape((N,1)) #scores中每一行代表一个样本的得分向量 找到每一行中的最大值 rshape:(N,) -> (N,1) 满足广播规则
scores = scores - maxLogC #广播 (N,C)-(N,1) 每一行减去其所在行最大值 使其最大值为0,避免指数爆炸
expScores = np.exp(scores)
loss = np.sum(-np.log(expScores[np.arange(N),y]/np.sum(expScores,axis=1)))
loss /= N #计算N个样本的平均数据损失
loss += reg*(np.sum(W1*W1)+np.sum(W2*W2)) #加上正则化损失 一般只对权重进行惩罚,如果对偏置也进行惩罚对结果几乎没有影响,但习惯不那么做
#reg前可以乘以0.5 也可以不乘
# 反向传播:计算梯度
grads = {}
dh2 = expScores/np.sum(expScores,axis=1,keepdims=True) #keepdims保持维度 用2维数组表示结果 (N,C)
dh2[np.arange(N),y] -= 1 #(N,C)
dh2 /= N #计算N个样本的平均梯度 (N,C)
#前向传播分阶段计算 反向传播时可以使用其中间结果
dW2 = h1.T.dot(dh2) #(H,C) 数据梯度
dW2 += 2*reg*W2 #正则化梯度 如果之前正则化损失*0.5 就会约掉这个2 我们之前没有乘0.5 所以会有一个平方项求导产生的2
db2 = np.sum(dh2,axis=0) #(C,)
dh1 = dh2.dot(W2.T) #(N,H)
#ReLu Max函数
dz1 = dh1 #(N,H)
dz1[h1<=0] = 0 #(N,H)
dW1 = X.T.dot(dz1) #(D,H)数据梯度
dW1 += 2*reg*W1 #正则化梯度 如果之前正则化损失*0.5 就会约掉这个2 我们之前没有乘0.5 所以会有一个平方项求导产生的2
db1 = np.sum(dz1,axis=0) #(H,)
grads['W2']=dW2
grads['b2']=db2
grads['W1']=dW1
grads['b1'] = db1
return loss, grads
- loss方法第一部分计算得分
scores = net.loss(X)
print('Your scores:')
print(scores)
print()
print('correct scores:')
correct_scores = np.asarray([
[-0.81233741, -1.27654624, -0.70335995],
[-0.17129677, -1.18803311, -0.47310444],
[-0.51590475, -1.01354314, -0.8504215 ],
[-0.15419291, -0.48629638, -0.52901952],
[-0.00618733, -0.12435261, -0.15226949]])
print(correct_scores)
print()
# 2者的差别会非常小. 结果应该 < 1e-7
print('Difference between your scores and correct scores:')
print(np.sum(np.abs(scores - correct_scores)))
- loss方法第二部分计算loss
实现上述函数的第2部分,计算损失(数据损失和正则化损失)
loss,_ = net.loss(X,y,reg=0.05)
print(loss)
correct_loss = 1.30378789133
# 差距会非常小 应该< 1e-12
print('Difference between your loss and correct loss:')
print(np.sum(np.abs(loss - correct_loss)))
4.反向传播
- loss方法第三部分,计算梯度
from cs231n.gradient_check import eval_numerical_gradient
# 使用梯度检查来验证反向传播的实现
# 如果实现是正确的,对于每一个参数W1, W2, b1, 和 b2其解析梯度和数值梯度之间的差别小于1e-8 .
loss, grads = net.loss(X, y, reg=0.05)
# 差别应该小于1e-8
for param_name in grads: #对于梯度字典grads中的每一个梯度 遍历键名/参数名
#定义匿名函数f 他的输入是不同的参数W,输出在当前W下的损失值 内部调用了之前计算loss的函数
f = lambda W: net.loss(X, y, reg=0.05)[0]
#下面是定义在cs231n/gradient_check.py中的梯度检查函数
#比较每个参数(矩阵/向量)的数值梯度和解析梯度
param_grad_num = eval_numerical_gradient(f, net.params[param_name], verbose=False)
print('%s max relative error: %e' % (param_name, rel_error(param_grad_num, grads[param_name])))
查看梯度检查函数:
def eval_numerical_gradient(f, x, verbose=True, h=0.00001):
"""
计算f在x处数值梯度的朴素实现
- f是一个接受单一参数的函数
- x是一个数组(矩阵/向量,也可以看作是高维空间中的一个点) 计算f在x处的数值梯度
"""
fx = f(x) # 计算f在x处的值
grad = np.zeros_like(x) #x的梯度与x同维 初始化为0
# 遍历x中的每一个索引/遍历x中的每一项 x是一个矩阵/向量
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
ix = it.multi_index #得到某一项的索引
oldval = x[ix] #得到该索引处的值
x[ix] = oldval + h # 修改该处的值 +h
fxph = f(x) # 计算此时的f函数的值
x[ix] = oldval - h #该处的值 -h
fxmh = f(x) # 计算此时的f函数的值
x[ix] = oldval # 恢复该索引处原来的值
# 计算该索引/项的数值梯度
grad[ix] = (fxph - fxmh) / (2 * h)
if verbose:
print(ix, grad[ix])
it.iternext() # 得到下一个索引/下一项
return grad #返回x的数值梯度
5.训练网络
net = init_toy_model()
stats = net.train(X, y, X, y,
learning_rate=1e-1, reg=5e-6,
num_iters=100, verbose=False)
print('Final training loss: ', stats['loss_history'][-1])
# 绘制训练过程中loss的变化
plt.plot(stats['loss_history'])
plt.xlabel('iteration')
plt.ylabel('training loss')
plt.title('Training Loss history')
plt.show()
编写类中的train和predict方法:
def train(self, X, y, X_val, y_val,
learning_rate=1e-3, learning_rate_decay=0.95,
reg=5e-6, num_iters=100,
batch_size=200, verbose=False):
"""
使用mini-batch梯度下降训练神经网络.
Inputs:
- X:训练样本的特征矩阵(N,D) 每一行代表一个样本的特征向量
- y:训练样本的标签 维度(N,) y[i] = c 意味着样本
X[i] 的标签是c, 其中c是一个整数 0 <= c < C.
- X_val: 验证样本的特征矩阵(N_val,D) 每一行代表一个样本的特征向量
- y_val: 验证样本的标签 维度(N,) y_val[i] = c 意味着样本
X_val[i] 的标签是c, 其中c是一个整数 0 <= c < C.
- learning_rate: 实数,学习率
- learning_rate_decay: 实数 学习率衰减率 随着梯度下降迭代的进行学习率应该逐渐变小,每一个epoch(完整遍历一遍训练集)进行一次衰减
- reg: 实数 正则化强度.
- num_iters: 梯度下降迭代次数.
- batch_size: mini-batch中包含的样本数 每次梯度下降迭代使用的样本数.
- verbose: 为true 打印优化进程.
"""
num_train = X.shape[0] #训练样本数
iterations_per_epoch = max(num_train / batch_size, 1) #一个epoch包含的mini-batch数
# 使用mini-batch GD优化模型参数
loss_history = [] #存放训练过程中的损失
train_acc_history = [] #存放训练过程中模型在训练集上的准确率
val_acc_history = [] #存放模型在验证集上的准确率
for it in range(num_iters):
X_batch = None
y_batch = None
#每次从训练集X中随机有放回的取样batch_size个样本 作为一个mini-batch
randomIndex = np.random.choice(len(X),batch_size,replace=True)
X_batch = X[randomIndex]
y_batch = y[randomIndex]
# 使用当前的mini-batch计算loss和梯度
loss,grads = self.loss(X_batch,y_batch,reg=reg)
loss_history.append(loss) #保留每个mini-batch上的loss
#使用梯度下降法更新参数
for param_name in self.params: #遍历每一个参数
self.params[param_name] -= learning_rate*grads[param_name]
if verbose and it % 1000 == 0: #每1000次迭代(1000个mini-batch)打印一次loss
print('iteration %d / %d: loss %f' % (it, num_iters, loss))
# 每训练一个epoch检查一次模型在训练集和验证集上的准确率 进行一次学习率衰减.
#一个epoch包含 num_train // batch_size个mini-batch
if it % iterations_per_epoch == 0:
train_acc = (self.predict(X_batch) == y_batch).mean()
val_acc = (self.predict(X_val) == y_val).mean()
train_acc_history.append(train_acc)
val_acc_history.append(val_acc)
# 对学习率进行衰减
learning_rate *= learning_rate_decay
return {
'loss_history': loss_history,
'train_acc_history': train_acc_history,
'val_acc_history': val_acc_history,
}
def predict(self, X):
"""
使用两层神经网络训练的权重/参数来预测数据集样本的标签,对于每一个样本(数据点,可以看作高维空间中的一个点)我们预测出在各个类别上的得分,最高得分对应的类别索引就是该样本的标签。
Inputs:
- X: 数据集 (N, D) 每一行代表一个样本的特征向量 包含N个样本/数据点
Returns:
- y_pred: 一维数组 维度 (N,) 包含对X中的每个样本预测的标签. y_pred[i] = c意味着为样本 X[i] 预测的标签/类别索引是 c, c是一个整数 0 <= c < C.
"""
y_pred = None
#取出训练的模型参数
W1, b1 = self.params['W1'], self.params['b1']
W2, b2 = self.params['W2'], self.params['b2']
z1 = X.dot(W1) + b1 #(N,H)
h1 = np.maximum(0,z1) #(N,H)
z2 = h1.dot(W2) + b2 #(N,C) 每一行代表一个样本的得分向量
y_pred = np.argmax(z2,axis=1) #计算每行最大值的索引 (N,)
return y_pred
6.加载CIFAR-10数据集并预处理
from cs231n.data_utils import load_CIFAR10
def get_CIFAR10_data(num_training=49000, num_validation=1000, num_test=1000, num_dev=500):
"""
从硬盘中加载cifar-10数据集并预处理,为2层前联接网络分类器作准备. 采取的步骤和SVM
实验中相同,只不过这里把它压缩成了一个函数.
"""
# 加载原始cifar-10数据集
cifar10_dir = 'cs231n/datasets/cifar-10-batches-py'
X_train, y_train, X_test, y_test = load_CIFAR10(cifar10_dir)
# #验证集中的样本是原始训练集中的num_validation(1000)个样本
mask = list(range(num_training, num_training + num_validation))
X_val = X_train[mask]
y_val = y_train[mask]
#训练集是原始训练集中的前num_training(49000)个样本
mask = list(range(num_training))
X_train = X_train[mask]
y_train = y_train[mask]
#测试集是原始测试集中的前num_test(1000)个样本
mask = list(range(num_test))
X_test = X_test[mask]
y_test = y_test[mask]
# 预处理 将每张图像(32,32,3)拉伸为一维数组(3072,)
#各个数据集从四维数组(m,32,32,3) 转型为2维数组(m,3072)
X_train = np.reshape(X_train, (X_train.shape[0], -1))
X_val = np.reshape(X_val, (X_val.shape[0], -1))
X_test = np.reshape(X_test, (X_test.shape[0], -1))
#预处理 0均值化 减去图像每个像素/特征上的平均值
#基于训练数据 计算图像每个像素/特征上的平均值
mean_image = np.mean(X_train, axis = 0)
#各个数据集减去基于训练集计算的各个像素/特征的平均值
#(m,3072) - (3072,) 广播运算
X_train -= mean_image
X_val -= mean_image
X_test -= mean_image
return X_train, y_train, X_val, y_val, X_test, y_test
# 运行上述函数 得到各个数据集
X_train, y_train, X_val, y_val, X_test, y_test = get_CIFAR10_data()
print('Train data shape: ', X_train.shape)
print('Train labels shape: ', y_train.shape)
print('Validation data shape: ', X_val.shape)
print('Validation labels shape: ', y_val.shape)
print('Test data shape: ', X_test.shape)
print('Test labels shape: ', y_test.shape)
7.在CIFAR-10上训练神经网络
input_size = 32 * 32 * 3 #输入层单元数 样本的特征向量维数
hidden_size = 50 #隐层单元数
num_classes = 10 #输出层单元数 分类类别数
#实例化一个两层神经网络的对象
net = TwoLayerNet(input_size, hidden_size, num_classes)
# 训练这个神经网络
stats = net.train(X_train, y_train, X_val, y_val,
num_iters=1000, batch_size=200,
learning_rate=1e-4, learning_rate_decay=0.95,
reg=0.25, verbose=True)
# 训练好的模型在验证集上的准确率
val_acc = (net.predict(X_val) == y_val).mean()
print('Validation accuracy: ', val_acc)
# 绘制优化/迭代过程中损失函数或模型在训练和验证集上的准确度的变化曲线。
plt.subplot(2, 1, 1)
plt.plot(stats['loss_history'])
plt.title('Loss history')
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.subplot(2, 1, 2)
plt.plot(stats['train_acc_history'], label='train')
plt.plot(stats['val_acc_history'], label='val')
plt.title('Classification accuracy history')
plt.xlabel('Epoch')
plt.ylabel('Clasification accuracy')
plt.show()
#导入cs231n/vis_utils.py中的visualize_grid函数
from cs231n.vis_utils import visualize_grid
# 可视化神经网络第一层的权重
def show_net_weights(net):
W1 = net.params['W1']
W1 = W1.reshape(32, 32, 3, -1).transpose(3, 0, 1, 2)
plt.imshow(visualize_grid(W1, padding=3).astype('uint8'))
plt.gca().axis('off')
plt.show()
show_net_weights(net)
查看visualize_grid函数:
def visualize_grid(Xs, ubound=255.0, padding=1):
"""
Reshape a 4D tensor of image data to a grid for easy visualization.
Inputs:
- Xs: Data of shape (N, H, W, C)
- ubound: Output grid will have values scaled to the range [0, ubound]
- padding: The number of blank pixels between elements of the grid
"""
(N, H, W, C) = Xs.shape
grid_size = int(ceil(sqrt(N)))
grid_height = H * grid_size + padding * (grid_size - 1)
grid_width = W * grid_size + padding * (grid_size - 1)
grid = np.zeros((grid_height, grid_width, C))
next_idx = 0
y0, y1 = 0, H
for y in range(grid_size):
x0, x1 = 0, W
for x in range(grid_size):
if next_idx < N:
img = Xs[next_idx]
low, high = np.min(img), np.max(img)
grid[y0:y1, x0:x1] = ubound * (img - low) / (high - low)
# grid[y0:y1, x0:x1] = Xs[next_idx]
next_idx += 1
x0 += W + padding
x1 += W + padding
y0 += H + padding
y1 += H + padding
# grid_max = np.max(grid)
# grid_min = np.min(grid)
# grid = ubound * (grid - grid_min) / (grid_max - grid_min)
return grid
8.在验证集上调试超参数/进行模型选择
best_net = None # 存储最好的模型 对应一组最好的超参数配置
#################################################################################
# TODO:使用验证集调试超参数,把最好的模型存储在best_net中 #
# #
# To help debug your network, it may help to use visualizations similar to the #
# ones we used above; these visualizations will have significant qualitative #
# differences from the ones we saw above for the poorly tuned network. #
# #
# Tweaking hyperparameters by hand can be fun, but you might find it useful to #
# write code to sweep through possible combinations of hyperparameters #
# automatically like we did on the previous exercises. #
#################################################################################
import random
X_train, y_train, X_val, y_val, X_test, y_test = get_CIFAR10_data()
workers=10 #超参数调试的次数
input_size = 32 * 32 * 3
num_classes = 10
best_accuracy=0
#和之前不同,我们需要调试的超参数比较少。比如有2个,可以设置两重for循环,分别对
#两个超参数的各种合理取值进行组合
#现在需要调试的超参数比较多,我们可以只设置一个调试次数的循环,内部对需要调试的每个超参数
#每次在一个合理的范围内随机选择一个值 构成一组超参数设置 然后训练模型
#最终选择一个在验证集上准确率最好的模型 在测试集上进行最后一次评估
for worki in range(0,workers):
hidden_size=random.randint(110,130) #每次在110-130中随机选择一个隐层单元数(整数)
learning_ratei = 10 ** np.random.uniform(-5,-3) #每次在这个范围中随机选择一个学习率
#0.0000992918
numer_of_training_epochs =random.randint(500,2000)#每次在此范围内随机选择一个优化/迭代次数
reg =0.25 #正则化强度是0.25 也可以和之前一样多设置几个值 进行调试
#实例化一个网络对象
net = TwoLayerNet(input_size, hidden_size, num_classes)
# 训练网络
stats = net.train(X_train, y_train, X_val, y_val,
num_iters=numer_of_training_epochs, batch_size=200,
learning_rate=learning_ratei, learning_rate_decay=0.95,
reg=reg, verbose=True)
#验证集准确率
val_acc = (net.predict(X_val) == y_val).mean()
print("hidden_size:%d ,learning_rate:%.10lf ,numer_of_training_epochs:%d ,reg:%.10lf\n val_acc:%.10lf\n"
%(hidden_size,learning_ratei,numer_of_training_epochs,reg,val_acc))
#存储最好的模型和最好的验证集准确率
if val_acc>best_accuracy:
best_accuracy=val_acc
best_net=net
print("Best accuracy is: %.10lf\n"%best_accuracy)
# 可视化最好的模型/网络 第一层学习好的权重
show_net_weights(best_net)
此时的第一层权重的可视化结果,比之前结构信息更明显。因为准确率提高了,模型的超参数配置更优。
9.在测试集上进行测试¶
使用验证集选择的最好的模型,在测试集上进行测试,得到最终的评估结果。
test_acc = (best_net.predict(X_test) == y_test).mean()
print('Test accuracy: ', test_acc)