深度学习 | 斯坦福cs231n编程作业#3 --- Softmax

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

目录

1.实验综述

2.导入必要的包

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

3.Softmax分类器

4.mini-batch梯度下降

5.在验证集上调试超参数

6.在测试集上测试


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

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

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

1.实验综述

2.导入必要的包

import random
import numpy as np
from cs231n.data_utils import load_CIFAR10 #/cs231n/data_utils.py 加载数据集的函数
import matplotlib.pyplot as plt

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

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

首先进入项目目录下的cs231n/datasets目录,有一个get_datasets.sh脚本文件:

# Get CIFAR10
wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
tar -xzvf cifar-10-python.tar.gz
rm cifar-10-python.tar.gz 

该脚本用于下载cifar-10数据集并解压,然后删除压缩包。Mac用户可能会报错,找不到wget命令,此时可以使用Mac包管理工具homebrew,在命令行输入 brew install wget 安装wget即可(如果没有安装homebrew的话自行百度安装)。

在当前目录下,打开命令行,执行该脚本文件./get_datasets.sh,得到解压后的数据集:

def get_CIFAR10_data(num_training=49000, num_validation=1000, num_test=1000, num_dev=500):
    """
    从硬盘中加载cifar-10数据集并预处理,为线性分类器作准备. 采取的步骤和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]
    
    #我们还设置了开发集 它是训练集中的一个小子集 从49000个训练样本中随机不放回取500个
    #开发集用于测试我们实现的代码 运行速度快
    mask = np.random.choice(num_training, num_dev, replace=False)
    X_dev = X_train[mask]
    y_dev = y_train[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))
    X_dev = np.reshape(X_dev, (X_dev.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
    X_dev -= mean_image
    
    #为各个数据集的特征矩阵加一列1  方便矩阵运算  
    #将偏置参数加到权重参数中  只需要优化一个权重矩阵W即可
    X_train = np.hstack([X_train, np.ones((X_train.shape[0], 1))])
    X_val = np.hstack([X_val, np.ones((X_val.shape[0], 1))])
    X_test = np.hstack([X_test, np.ones((X_test.shape[0], 1))])
    X_dev = np.hstack([X_dev, np.ones((X_dev.shape[0], 1))])
    
    return X_train, y_train, X_val, y_val, X_test, y_test, X_dev, y_dev


# 运行上述函数 得到各个数据集
X_train, y_train, X_val, y_val, X_test, y_test, X_dev, y_dev = 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)
print('dev data shape: ', X_dev.shape)
print('dev labels shape: ', y_dev.shape)

查看#cs231n/data_utils.py 中的load_CIFAR10函数,它用于加载CIFAR-10数据集:

def load_CIFAR10(ROOT):
  """ 
  加载所有的cifar batch
  
  input:
  ROOT:解压后cifar数据集的路径
  
  output:
  Xtr:训练集 四维数组(50000,32,32,3)
  Ytr:训练集图像的标签 一维数组 (50000,)  取值0-9 10个类别
  Xte:测试集 四维数组(10000,32,32,3)
  Yte:测试集图像的标签 一维数组 (10000,) 取值0-9 10个类别
  """
  
  xs = []  #列表 用于存储cifar训练集 各个batch的数据(四维数组)
  ys = []  #列表 用于存储cifar训练集 各个batch的标签数据(一维数组)
  for b in range(1,6):#b是 batch的编号
    f = os.path.join(ROOT, 'data_batch_%d' % (b, )) #得到各个bacth数据的完整路径
    X, Y = load_CIFAR_batch(f) #得到各个batch中的图片和标签
    xs.append(X) #将每个batch的图片 四维数组 追加到xs中
    ys.append(Y) #将每个batch的图片标签 一维数组 追加到ys中   
  Xtr = np.concatenate(xs)  #将列表中所有的四维数组拼接起来  得到完整的训练集图片
  Ytr = np.concatenate(ys)  #将列表中所有的一维数组拼接起来 得到完整的训练集标签
  del X, Y    #删除中间变量X,Y
  Xte, Yte = load_CIFAR_batch(os.path.join(ROOT, 'test_batch')) #得到测试集图片和标签
  return Xtr, Ytr, Xte, Yte

查看load_CIFAR_batch函数:

def load_CIFAR_batch(filename):
  """ 
  加载cifar一个batch的数据
  
  input:
  filename:batch的完整路径
  
  output:
  X:batch中的所有图片 四维数组(10000,32,32,3)
  Y:batch中所有图片标签 一维数组(10000,)
  """
  with open(filename, 'rb') as f: #打开文件 以二进制读取
    datadict = load_pickle(f)  #得到数据字典
    X = datadict['data']     #得到batch中的所有图片 
    Y = datadict['labels']   #得到batch中图片的标签 
    X = X.reshape(10000, 3, 32, 32).transpose(0,2,3,1).astype("float") #将X转型为(10000,3,32,32)四维数组,并调换一下各个轴  得到(10000,32,32,3)四维数组  数值类型为float(np.float64)
    Y = np.array(Y) #将Y变为一维数组
    return X, Y

查看load_pickle函数:

import platform
from six.moves import cPickle as pickle

def load_pickle(f):
    version = platform.python_version_tuple()#得到Python版本  
    if version[0] == '2': #针对Python2
        return  pickle.load(f)
    elif version[0] == '3':  #针对Python3
        return  pickle.load(f, encoding='latin1')
    raise ValueError("invalid python version: {}".format(version))

3.Softmax分类器

cs231n/classifiers/softmax.py中编写代码。

 在上述基础上加上正则化惩罚项即可。

非向量化版本:

def softmax_loss_naive(W, X, y, reg):
  """
  使用循环实现softmax损失函数和梯度

  输入有D维,有C个类别,在包含N个样本的mini-batch上执行.
  
   Inputs:
  - W: 2维数组 (D, C) 包含权重.
  - X: 2维数组(N, D) 一个mini-batch的N个训练样本特征矩阵 每一行代表一个样本的特征向量.
  - y: 一维数组 (N,) 包含训练样本的标签; y[i] = c 意味着
    训练样本 X[i]的类别标签是 c, 0 <= c < C.
  - reg: (float)正则化惩罚系数/正则化强度

  Returns a tuple of:
  - loss: 损失
  - dW: 权重W的梯度 与W同维
  """

  # 初始化loss和梯度为0
  loss = 0.0  #实数
  dW = np.zeros_like(W)  #dW是W的梯度  与W同维

  num_classes = W.shape[1]  #类别数
  num_train = X.shape[0]   #mini-batch中的训练样本数
  
  scores = X.dot(W)  #(N,C) 每一行是样本的得分向量
  logC = -np.max(scores,axis=1).reshape((num_train,1)) #求每行最大值 必须使用reshape  把(N,)的一维数组转换成(N,1)2维数组  这样才能使用广播
  #每个样本的得分向量 减去向量中的最大元素  使其最大元素为0  再进行指数运算
  expScores = np.exp(scores - logC)  #取e为低的指数 括号内操作避免出现指数爆炸 广播(N,C)-(N,1)  (N,C)
  
  for i in range(num_train):
    esum = sum(expScores[i]) 
    eyi = expScores[i,y[i]]
    li = -np.log(eyi/esum)
    loss += li
    
    for j in range(num_classes):
      dW[:,j]+=(expScores[i,j]/esum)*X[i]

    dW[:,y[i]] -= X[i]
    
  
  loss /= num_train #数据损失的均值
  # 加上正则化损失 得到完整损失
  #注意我们一般只惩罚权重参数  之前我们把偏置参数加到了权重参数中 所以统一当作权重处理
  #在神经网络中 我们会把他们分开
  #可以把W中的权重参数单独拿出来进行惩罚  当然我们统一对W权重+偏置参数进行惩罚也是可以的 对最终结果几乎没有影响
  loss += 0.5*reg * np.sum(W * W)  #使用L2惩罚 前面乘以0.5,可以约掉对平方求导时产生的2(笔记中没有加)

  #每个训练样本都会产生一个梯度  求平均梯度
  dW /= num_train
  dW += reg*W  #加上正则化项产生的梯度
   
  return loss, dW
# 首先使用循环实现softmax损失函数
# 打开文件 cs231n/classifiers/softmax.py 
# 实现softmax_loss_naive 函数.

from cs231n.classifiers.softmax import softmax_loss_naive
import time

# 随机初始化权重矩阵的元素为很小的服从正态分布的数
W = np.random.randn(3073, 10) * 0.0001
#计算损失
loss, grad = softmax_loss_naive(W, X_dev, y_dev, 0.0)

# 计算的loss和 -log(0.1)接近.
print('loss: %f' % loss)
print('sanity check: %f' % (-np.log(0.1)))

# 使用循环实现softmax分类器的梯度计算
loss, grad = softmax_loss_naive(W, X_dev, y_dev, 0.0)

#在W中随机选几个位置 计算数值梯度 然后和该位置的解析梯度进行比较 
#梯度值应该是匹配的
#导入cs231n/gradient_check.py中的梯度检查函数 grad_check_sparse
from cs231n.gradient_check import grad_check_sparse
#定义匿名函数f  他的输入是不同的权重W,输出在当前W下的损失值  内部调用了之前计算loss的函数
f = lambda w: softmax_loss_naive(w, X_dev, y_dev, 0.0)[0]
grad_numerical = grad_check_sparse(f, W, grad, 10)

# 在包含正则化的情况下 reg参数不为0  进行梯度检查 因为正则化部分也会产生梯度
loss, grad = softmax_loss_naive(W, X_dev, y_dev, 5e1)
f = lambda w: softmax_loss_naive(w, X_dev, y_dev, 5e1)[0]
grad_numerical = grad_check_sparse(f, W, grad, 10)

向量化版本:

def softmax_loss_vectorized(W, X, y, reg):
  """
  Softmax 损失函数和梯度  向量化版本

  输入输出同 softmax_loss_naive.
  """
  
  # 初始化loss和梯度为0
  loss = 0.0  #实数
  dW = np.zeros_like(W)  #dW是W的梯度  与W同维

  num_classes = W.shape[1]  #类别数
  num_train = X.shape[0]   #mini-batch中的训练样本数

  scores = X.dot(W)  #(N,C) 每一行是样本的得分向量
  logC = -np.max(scores,axis=1).reshape((num_train,1)) #求每行最大值 必须使用reshape  把(N,)的一维数组转换成(N,1)2维数组  这样才能使用广播
  #每个样本的得分向量 减去向量中的最大元素  使其最大元素为0  再进行指数运算
  expScores = np.exp(scores - logC)  #取e为低的指数 括号内操作避免出现指数爆炸 广播(N,C)-(N,1)  (N,C)
  
  loss = np.sum(-np.log(expScores[np.arange(num_train),y]/np.sum(expScores,axis=1)))
  loss /= num_train #数据损失的均值
  # 加上正则化损失 得到完整损失
  #注意我们一般只惩罚权重参数  之前我们把偏置参数加到了权重参数中 所以统一当作权重处理
  #在神经网络中 我们会把他们分开
  #可以把W中的权重参数单独拿出来进行惩罚  当然我们统一对W权重+偏置参数进行惩罚也是可以的 对最终结果几乎没有影响
  loss += 0.5*reg * np.sum(W * W)  #使用L2惩罚 前面乘以0.5,可以约掉对平方求导时产生的2(笔记中没有加)
   
  expScoresSumRow = np.sum(expScores,axis=1).reshape((num_train,1))  #(N,)->(N,1) 利用广播运算
  gradientMatrix = expScores/expScoresSumRow  #(N,C)/(N,1)  (N,C)
   #对于yi要-1
  gradientMatrix[np.arange(num_train),y] -= 1
                 
  dW = X.T.dot(gradientMatrix)
   #每个训练样本都会产生一个梯度  求平均梯度
  dW /= num_train
  dW += reg*W  #加上正则化项产生的梯度
    
  return loss, dW
# 接下来实现计算softmax损失函数和梯度的向量化版本
# 两个版本答案一样  计算速度不同
tic = time.time()
loss_naive, grad_naive = softmax_loss_naive(W, X_dev, y_dev, 0.000005)
toc = time.time()
print('naive loss: %e computed in %fs' % (loss_naive, toc - tic))

from cs231n.classifiers.softmax import softmax_loss_vectorized
tic = time.time()
loss_vectorized, grad_vectorized = softmax_loss_vectorized(W, X_dev, y_dev, 0.000005)
toc = time.time()
print('vectorized loss: %e computed in %fs' % (loss_vectorized, toc - tic))

#loss是一个实数所以很好比较 直接用减法
#W的梯度和W同维是一个矩阵  所以我们使用F范数对其进行比较
grad_difference = np.linalg.norm(grad_naive - grad_vectorized, ord='fro')
print('Loss difference: %f' % np.abs(loss_naive - loss_vectorized))
print('Gradient difference: %f' % grad_difference)

4.mini-batch梯度下降

#在cs231n/classifiers/linear_classifier.py中实现mini-batch梯度下降
#实现并调用Softmax类中的train()方法(父类中的方法)
#从cs231n/classifiers/linear_classifier.py中导入Softmax类
from cs231n.classifiers import Softmax
soft = Softmax() #实例化类对象
tic = time.time()
#调用train方法
loss_hist = soft.train(X_train, y_train, learning_rate=1e-7, reg=2.5e4,
                      num_iters=1500, verbose=True)
toc = time.time()
print('That took %fs' % (toc - tic))

编写父类train方法:

def train(self, X, y, learning_rate=1e-3, reg=1e-5, num_iters=100,
            batch_size=200, verbose=False):
    """
    使用mini-batch梯度下降 训练线性分类器.

    Inputs:
    - X: 完整训练集特征矩阵,2维数组 (N, D) 包含N个训练样本,每个样本是D维的 每行代表一个训练样本的特征向量
    - y: 一维数组 (N,) 包含训练样本的标签; y[i] = c
      意味着训练样本 X[i] 的类别标签是c 0 <= c < C .
    - learning_rate: (float) 学习率.
    - reg: (float) 正则化惩罚系数/正则化强度.
    - num_iters: (integer) 优化迭代次数 每次使用一个mini-batch的数据
    - batch_size: (integer) mini-batch大小/包含的训练样本数.
    - verbose: (boolean) true的话 每100次迭代打印一次损失.

    Outputs:
    列表,包含每次迭代的损失
    """
    num_train, dim = X.shape
    num_classes = np.max(y) + 1 #类别数 y:0~C-1  C
    if self.W is None:
      # 随机初始化权重W  (D,C)  很小的符合标准正态分布的随机数
      self.W = 0.001 * np.random.randn(dim, num_classes)

    # 运行mini-batch梯度下降来优化权重W
    loss_history = []
    for it in range(num_iters):  #num_iters次梯度下降迭代 优化loss,更新权重
      X_batch = None
      y_batch = None
      #每次从训练集X中随机有放回的取样batch_size个样本 作为一个mini-batch
      random_Index = np.random.choice(len(X),batch_size,replace=True)
      X_batch = X[random_Index]
      y_batch = y[random_Index]
      #基于该mini-batch的训练样本 计算loss 和梯度
      loss,grad = self.loss(X_batch,y_batch,reg)
      loss_history.append(loss) #保存每个mini-batch上的损失
      
      #使用梯度下降法更新权重
      self.W += (-learning_rate*grad)
      
      if verbose and it % 100 == 0:
        print('iteration %d / %d: loss %f' % (it, num_iters, loss))
    return loss_history

查看父类的loss方法:

  def loss(self, X_batch, y_batch, reg):
    """
    计算损失和梯度. 
    子类会重写这个方法.

    Inputs:
    - X_batch: 一个mini-batch训练数据的特征矩阵,2维数组 (N, D) 包含N个训练样本,每个样本是D维的 每行代表一个训练样本的特征向量
    - y_batch: 一维数组 (N,) 包含mini-batch训练样本的标签; y[i] = c
      意味着训练样本 X[i] 的类别标签是c 0 <= c < C .
    - reg: (float) 正则化惩罚系数/正则化强度.

    Returns: A tuple containing:
    - loss 实数损失值
    - gradient:self.W的梯度 与W同维
    """
    pass

查看Softmax子类: 

class Softmax(LinearClassifier):
    
  """ Softmax类继承LinearClassifier类
  """
  #重写父类的loss方法  计算Softmax的损失和梯度
  def loss(self, X_batch, y_batch, reg):
    return softmax_loss_vectorized(self.W, X_batch, y_batch, reg)
# 绘制loss随迭代次数的变化曲线  每次迭代使用一个mini-batch的数据
plt.plot(loss_hist)
plt.xlabel('Iteration number')
plt.ylabel('Loss value')
plt.show()

#实现并调用Softmax父类中的predict()方法
#计算训练好的模型在训练集和验证集上的准确率
y_train_pred = soft.predict(X_train)
print('training accuracy: %f' % (np.mean(y_train == y_train_pred), ))
y_val_pred = soft.predict(X_val)
print('validation accuracy: %f' % (np.mean(y_val == y_val_pred), ))

编写父类的predict方法:

def predict(self, X):
    """
    使用线性分类器训练好的权重 预测样本的标签

    Inputs:
    - X: 2维数组 (N, D) 待预测数据的特征矩阵 有N个样本,每个样本的特征向量是D维的  

    Returns:
    - y_pred: 为X中的样本预测的标签. 1维数组(N,), 其中的每一项是为该样本预测的类别标签索引 0~C-1
      
    """
    y_pred = np.zeros(X.shape[0])
    scores = X.dot(self.W) #(N,C)
    y_pred = np.argmax(scores,axis=1)
    
    return y_pred

5.在验证集上调试超参数

# 使用验证集调试超参数(学习率和正则化强度)/进行模型选择 
#接下来会尝试 不同的学习率和正则化强度的选择 如果顺利的话 应该可以在验证集上达到40%的准确率
learning_rates = [1.4e-7, 1.5e-7, 1.6e-7]
regularization_strengths = [8000.0, 9000.0, 10000.0, 11000.0, 18000.0, 19000.0, 20000.0, 21000.0]


# results 是一个字典
# 键:(learning_rate, regularization_strength) 
# 值:(training_accuracy, validation_accuracy). 
results = {}
best_val = -1   # 存储最高的验证集准确率
best_soft = None # 实现最高验证集准确率的Softmax对象

'''
编写通过验证集调试超参数的代码,选择一组最好的超参数。 对于超参数的每个组合,在训练集上训练softmax分类器,计算训练和验证集的准确性,将这些数据存储在结果字典中。 另外,在best_val存储最好的验证集准确率,
在best_soft存储实现最高验证集准确率的Softmax对象
'''

'''
运行验证代码时可以先使用一个小的num_iters值,这样Softmax分类器不会花很多时间去训练, 当你确信验证代码
正确工作(在当前超参数设置下,刚开始每次迭代后的loss在减小)时,再用一个大的num_iters值来运行验证代码。
'''
#当前共有3*8=24种组合
for lr in learning_rates:
    for reg in regularization_strengths:
        soft = Softmax() #实例化类对象
        #调用train方法
        loss_hist = soft.train(X_train, y_train, learning_rate=lr, reg=reg,
                      num_iters=1500, verbose=True)
        y_train_pred = soft.predict(X_train)
        train_accuracy = np.mean(y_train == y_train_pred)
        y_val_pred = soft.predict(X_val)
        val_accuracy = np.mean(y_val == y_val_pred)
        if val_accuracy > best_val:
            best_val = val_accuracy
            best_soft = soft
        results[(lr,reg)] = (train_accuracy,val_accuracy)
    
# 打印结果
for lr, reg in sorted(results):
    train_accuracy, val_accuracy = results[(lr, reg)]
    print('lr %e reg %e train accuracy: %f val accuracy: %f' % (
                lr, reg, train_accuracy, val_accuracy))
    
print('best validation accuracy achieved during cross-validation: %f' % best_val)

# 可视化验证/交叉验证结果
import math
x_scatter = [math.log10(x[0]) for x in results] 
y_scatter = [math.log10(x[1]) for x in results]

# 可视化训练集准确率
marker_size = 100
colors = [results[x][0] for x in results]
plt.subplot(2, 1, 1)
plt.scatter(x_scatter, y_scatter, marker_size, c=colors)
plt.colorbar()
plt.xlabel('log learning rate')
plt.ylabel('log regularization strength')
plt.title('CIFAR-10 training accuracy')


# 可视化验证集准确率
colors = [results[x][1] for x in results] 
plt.subplot(2, 1, 2)
plt.scatter(x_scatter, y_scatter, marker_size, c=colors)
plt.colorbar()
plt.xlabel('log learning rate')
plt.ylabel('log regularization strength')
plt.title('CIFAR-10 validation accuracy')
plt.show()

6.在测试集上测试

# 使用最好的Softmax模型在测试集上进行测试
y_test_pred = best_soft.predict(X_test)
test_accuracy = np.mean(y_test == y_test_pred)
print('Softmax on raw pixels final test set accuracy: %f' % test_accuracy)

# 可视化每个类别学到的权重

w = best_soft.W[:-1,:] # 从W中取出权重参数 去掉偏置参数 (D=32*32*3,C=10)  每一列对应一个类别
w = w.reshape(32, 32, 3, 10) #对w进行转型 把每一列转化为图像维度
w_min, w_max = np.min(w), np.max(w)
classes = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
for i in range(10):
    plt.subplot(2, 5, i + 1)
      
    # 把权重值变换到0-255取值范围   变成图像,进行可视化
    wimg = 255.0 * (w[:, :, :, i].squeeze() - w_min) / (w_max - w_min)
    plt.imshow(wimg.astype('uint8'))
    plt.axis('off')
    plt.title(classes[i])

猜你喜欢

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