深度学习 | 斯坦福cs231n编程作业#4 --- TwoLayerNN

斯坦福cs231n(2017年版)的所有编程作业均采用iPython Notebooks实现,不熟悉的朋友可以提前使用一下Notebooks。编程作业#4主要是手写实现一个两层的神经网络(单隐层)分类器来对cifar-10图像数据集进行分类。

目录

1.实验综述

2.导入必要的包

3.前向传播

4.反向传播

5.训练网络

6.加载CIFAR-10数据集并预处理

7.在CIFAR-10上训练神经网络

8.在验证集上调试超参数/进行模型选择

9.在测试集上进行测试¶


cs231全部编程作业(英文原版带答案)

cs231n全部编程作业(英文原版不带答案)

编程作业#4(中文翻译版带答案)

 

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)

 

猜你喜欢

转载自blog.csdn.net/sdu_hao/article/details/86669731