使用numpy构建神经网络实现softmax多分类问题

Posted by huanrong on 2019-05-06

此篇文章为计算机与智能硬件体系结构课程的第三次作业报告。
完整源码

0 思路


  1. 使用二层神经网络,输入为8个变量,hidden layer有9个神经元(经实际测试,9个神经元效果较好,太多神经元容易过拟合),输出为维度为4的onehot编码
  2. 由输入到hidden layer先通过一个线性函数,再用relu函数进行激活
  3. 由hidden layer到输出使用softmax函数
  4. 使用梯度下降法迭代一定次数,使得Cost达到最低

1 实现


总的来说,实现步骤可分为:

  1. 导入数据
  2. 初始化模型参数
  3. 前向传播(Linear-Activation)
  4. 计算损失(Cost)
  5. 后向传播(Linear-Activation)
  6. 更新参数
  7. 循环执行2-5步

1.1 导入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dataset = pandas.read_csv('dataset.txt', sep='\s+')

trainset = dataset[0:400]
y_train = trainset.pop('y')
train_x = trainset.values
train_y = y_train.values.reshape(1, -1) - 1

testset = dataset[400:500]
y_test = testset.pop('y')
test_x = testset.values
test_y = y_test.values.reshape(1, -1) - 1

enc = OneHotEncoder()
train_y_oh = enc.fit_transform(train_y.T).toarray().T
  • 这里使用python中的pandas库进行数据的导入。
  1. 使用read_csv函数导入数据。由于原数据用txt保存,这里需要用正则表达式 \s+对数据进行分割;
  2. 划分训练集与测试集,前400条数据为训练集,后100条数据为测试集;
  3. y从训练集、测试集中分离需要注意的是,使用pop方法得到的dataframe经过values方法,得到的是一个没有维度的matrix,因此需要将其reshape;
  4. 将训练集的y用转换为one-hot编码,以便于损失函数和softmax的计算。使用sklearn.preprocessing中的OneHotEncoder方法。特别只需要注意维度。

1.2 初始化模型参数

1
2
3
4
5
6
7
8
9
10
11
12
def initialize_parameters(n_x, n_h, n_y):    
W1 = np.random.randn(n_h, n_x) * 0.01 # weight matrix随机初始化
b1 = np.zeros((n_h, 1)) # bias vector零初始化
W2 = np.random.randn(n_y, n_h) * 0.01
b2 = np.zeros((n_y, 1))

parameters = {"W1": W1,
"b1": b1,
"W2": W2,
"b2": b2}

return parameters
  • 对于参数W,使用np.random.randn()函数将其随机初始化,此函数将返回一组服从标准正态分布的随机样本值,区间为(0,1),经实验发现将函数返回的值乘以0.01得到的效果更好(也许是最终得到的参数普遍较小,乘以0.01可以使得遍历次数降低)。
  • 对于参数b,使用np.zeros()函数将其零初始化。

1.3 前向传播

此步需要定义线性前馈函数(linear_forward)以及非线性前馈函数(activation, relu & softmax)。线性函数以及relu函数较为简单,这里略而不谈。

1.3.1 softmax是什么?

softmax函数是逻辑函数的一种推广,它能够将一个含任意实数的K维向量映射到另一个维度的实向量中,使得每一个元素的范围都落在区间(0,1),并且使所有元素的和为1,以此来表示各分类的概率。结果的值越大,概率也就越大,属于某个分类的可能性就越大。
softmax广泛应用于机器学习和深度学习中的多分类问题。在深度学习中,softmax常用于多分类问题最后一层的激活函数,用于输出某样本属于某个分类的概率值。

1.3.2 softmax实现

  1. 定义
    $$softmax(x)i = \frac{e^{x_i}}{\displaystyle\sum{j=1}^{n}e^{x_j}}$$

  2. 代码

1
2
3
4
5
6
def softmax(Z):
Z_shift = Z - np.max(Z, axis = 0)
A = np.exp(Z_shift)/ np.sum(np.exp(Z_shift), axis=0)
cache = Z_shift

return A, cache

注意返回cache以便于后向传播中梯度的计算。

  1. 为什么这里要减去一个max值呢?
    从需求上来说,如果x的值没有限制的情况下,当x线性增长,e指数函数下的x就呈现指数增长,一个较大的x(比如1000)就会导致程序的数值溢出,导致程序error。所以需求上来说,如果能够将所有的x数值控制在0及0以下,则不会出现这样的情况,这也是为什么不用min而采用max的原因。

1.4 计算损失

1
2
3
4
5
6
7
def compute_cost(AL, Y):
m = Y.shape[1]
cost = -(np.sum(Y * np.log(AL))) / float(m)
#cost = np.squeeze(cost)
assert(cost.shape == ())

return cost

由于最后一层使用softmax激活,损失函数定义较为简单,只要把不为0的probability相加除以样本个数即可。

1.5 后向传播

对于Softmax求导的推导过程可以参考这篇博文
简单来说$\frac{\partial C}{\partial z_i} = a_i - y_i$。

这里要特别注意dW、db以及dA_prev的求导,维度一定要对的上。

1.6 更新参数

1
2
3
4
5
6
7
8
9
def update_parameters(parameters, grads, learning_rate):
L = len(parameters) // 2 # number of layers in the neural network

# Update rule for each parameter. Use a for loop.
for l in range(1,L+1):
parameters['W'+str(l)] -= learning_rate * grads['dW'+str(l)]
parameters['b'+str(l)] -= learning_rate * grads['db'+str(l)]

return parameters

这里使用的是最简单的参数更新方式,由于数据集较小,没有使用其他的参数更新策略(如SGD、Adam等等)。

2 结果


370d43402aa4c935b2a6117ae7023122.png

经测试发现,当n_h=9, learning_rate=0.05, num_iteration=15000时,得到的准确率较好(两个准确率分别为训练集、测试集上的准确率)。
当增大hidden layer层数、神经元数时,发现不仅训练速度变慢了,准确率也降低了,这可能就是过拟合了,因为这个数据集本身就不大。

3 踩坑总结


一开始在训练模型的时候,反复测试之下,发现无论是训练集还是测试集,Accuracy都非常低,只有24%左右。这可能表示模型已经完全失效了,因为本身四选一猜对的概率就是25%。一开始猜想是不是网络结构的问题,无法做分类任务,但是这一个网络已经够简单了,网络结构出错的可能性不大。然后把其预测出来的结果打出来看了一下,发现有两个原因:

  1. 传入的y没有用one-hot表示,这点错的十分离谱,直接导致损失极大;
  2. 数据集中的y范围是(1, 4),而最终得到的y范围是(0, 3),二者不统一。

e3da7c0b40e9cec997200d9d39635fa7.png


References

  1. Deep Learning and Neural Network on Coursera(Andrew Ng)
  2. Numpy学习—np.random.randn()、np.random.rand()和np.random.randint()
  3. 为什么softmax函数需要减去一个max值