使用的数值化数据集:
链接:https://pan.baidu.com/s/110dhDKA8eXYV4-kfmevo0g
提取码:x3wu
整体思路
使用Numpy和pandas写一个神经网络,进行手写数字识别(MNIST),样本图如下:

总思路
整体神经网络代码只使用Numpy和pandas,根据额外需要使用time和matplotlib,设计思路如下
数据处理-> 建立模型 -> 进行训练 -> 进行测试 -> 评估时间和精度
数据处理
数据分为训练集和测试集,分别是785*6w和785*1w的数据,785列中前784列代表了28*28的图片的数值化数据,第785列为0~9的标签,并且第一行为0~784的索引
- 使用pandas读取训练和测试数据
- 分别把训练和测试数据中的数据和标签分离
- 将标签进行one-hot编码
import numpy as np
import pandas as pd
import time
# 训练集有6w, 785列,前784为28*28,最后一列为标签
train = pd.read_csv("MNIST_Training_60K.csv",index_col=False,header=0)
# 测试集有1w,784列,28*28的图片
test = pd.read_csv("MNIST_Test_10K.csv",index_col=False)
label = train[:]["784"]
train = train.drop(["784"],axis=1)
X = train.T
Y = np.zeros((10, X.shape[1]))
# 以下为成像代码
# import matplotlib.pyplot as plt
# x = np.array(X)
# X_train = x.reshape((60000,28,28))
# cur=X_train[0:1]
# plt.imshow(cur[0].reshape(h,w).T,cmap='gray')
# 将标签one_hot编码
for n in range(X.shape[1]):
Y[label[n]][n] = 1
# 处理test数据
label_test = test[:]["784"]
test1 = test.drop(["784"],axis=1)
X_test = test1.T
# Y_test = np.eye(10)[l]
建立模型
正向传播
参数初始化
采用”He”初始化,即使用标准化的服从高斯分布的随机数乘上 2/前一层网络单元数的根号幂初始化权重W,并且使用0初始化对偏差b进行初始化
def initialize_parameters(n_x, n_h, n_y): np.random.seed(1) W1 = np.random.randn(n_h, n_x) * np.sqrt(2. / n_x) b1 = np.zeros((n_h, 1)) W2 = np.random.randn(n_y, n_h) * np.sqrt(2. / n_h) b2 = np.zeros((n_y, 1)) # 断言保证这些参数的形状如下,否则报错 assert(W1.shape == (n_h, n_x)) assert(b1.shape == (n_h, 1)) assert(W2.shape == (n_y, n_h)) assert(b2.shape == (n_y, 1)) # 将参数封装 parameters = {"W1":W1, "b1":b1, "W2":W2, "b2":b2} return parameters注意:
权重w本来的维度应该是(前一层神经元数,本层神经元数),但是为了方便后续计算省略掉此处的转置操作,在一开始就选择将W的维度设置为(本层神经元数,前一层大小神经元数)
而偏差b由于需要整层网络共享,我们基于Python的广播机制,选择让它的大小设置为(本层神经元数,1)
正向线性传播
def linear_forward(A0, W1, b1): Z1 = np.dot(W1, A0) + b1 # 即Z1 = W1 * A0 + b1 assert(Z1.shape == (W1.shape[0], A0.shape[1])) cache = (A0, W1, b1) return Z1, cache无特别说明
正向激活
隐藏层使用ReLU(整流线性单元)函数,输出层使用softmax函数,期望它的值符合0~9十个分类。
def relu(Z1): A1 = np.maximum(0, Z1) cache = Z1 return A1, cache def softmax(Z2): A2 = np.exp(Z2) / sum(np.exp(Z2)) cache = Z2 return A2, cache
正向传播函数
将线性传播部分和激活函数合并
def linear_activation_forward_relu(A0, W1, b1): Z1, linear_cache = linear_forward(A0, W1, b1) A1, activation_cache = relu(Z1) assert(A1.shape == (W.shape[0], A0.shape[1])) cache = (linear_cache, activation_cache) return A1, cache def linear_activation_forward_softmax(A1, W2, b2): Z2, linear_cache = linear_forward(A1, W2, b2) A2, activation_cache = softmax(Z2) assert(A2.shape == (W2.shape[0], A1.shape[1])) cache = (linear_cache, activation_cache) return A2, cache
反向传播
计算成本
使用交叉熵作为成本函数,其函数的公式为:
\sum_{i}^{m} y_i\log{\hat{y_{i}}}def compute_cost(A2, Y): # 计算交叉熵 m = Y.shape[1] # 计算loss函数 logprobs = -1* sum(np.multiply(np.log(A2), Y)) # 成本函数就是将整个训练集上的损失相加取相反数 cost = -1/m * np.sum(logprobs) cost = np.squeeze(cost) return cost, logprobs
反向传播
计算各个参数基于成本的梯度,用于更新参数
def backward_propagation(parameters, cache, X, Y): m = X.shape[1] W1 = parameters['W1'] W2 = parameters['W2'] A1 = cache['A1'] A2 = cache['A2'] dZ2 = A2 - Y dW2 = 1/m * np.dot(dZ2, A1.T) db2 = 1/m * np.sum(dZ2, axis=1, keepdims=True) dA1 = np.dot(W2.T, dZ2) dZ1 = np.array(dA1, copy=True) dZ1[Z1 <=0 ] = 0 # 标记一下,操作十分神奇,牛逼! dW1 = 1/m * np.dot(dZ1, X.T) db1 = 1/m * np.sum(dZ1, axis=1, keepdims=True) grads={"dW1":dW1, "db1":db1, "dW2":dW2, "db2":db2} return grads各参数传播的顺序和算式如下:

softmax对输入求导的公式见[CS231n-Week3-Assignment4](https://1305936314.github.io/2019/12/09/CS231n-Week3-Assignment4/)更新参数
使用mini-batch梯度下降法更新参数,目前仅实现了梯度下降法,代码如下:
def update_parameters(parameters, grads, learning_rate=1.2): W1 = parameters['W1'] b1 = parameters['b1'] W2 = parameters['W2'] b2 = parameters['b2'] dW1 = grads['dW1'] db1 = grads['db1'] dW2 = grads['dW2'] db2 = grads['db2'] W1 = W1 - learning_rate* dW1 b1 = b1 - learning_rate* db1 W2 = W2 - learning_rate* dW2 b2 = b2 - learning_rate* db2 parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2} return parameters
整体模型
将上述函数等无缝合并,方便转换
def nn_model(X, Y, n_h=1000, learning_rate=0.12 ,num_iterations=10000, print_cost=False):
n_x = 784
n_h = 1000
n_y = 10
m = X.shape[1]
# 随机初始化参数
np.random.seed(3)
W1 = np.random.randn(n_h, n_x) * np.sqrt(2. / n_x)
b1 = np.zeros((n_h, 1))
W2 = np.random.randn(n_y, n_h) * np.sqrt(2. / n_h)
b2 = np.zeros((n_y, 1))
for i in range(0, num_iterations):
# 正向传播: 线性-> ReLU -> 线性-> softmax
Z1 = np.dot(W1, X) + b1
A1 = np.maximum(0, Z1)
Z2 = np.dot(W2, A1) + b2
A2 = np.exp(Z2) / sum(np.exp(Z2))
# 计算成本:计算交叉熵成本
logprobs = -1* sum(np.multiply(np.log(A2), Y))
cost = -1/m * np.sum(logprobs)
cost = np.squeeze(cost)
# 反向传播:梯度下降法
dZ2 = A2 - Y
dW2 = 1/m * np.dot(dZ2, A1.T)
db2 = 1/m * np.sum(dZ2, axis=1, keepdims=True)
dA1 = np.dot(W2.T, dZ2)
dZ1 = np.array(dA1, copy=True)
dZ1[Z1 <=0 ] = 0 # 标记一下,操作十分神奇,牛逼!
dW1 = 1/m * np.dot(dZ1, X.T)
db1 = 1/m * np.sum(dZ1, axis=1, keepdims=True)
# 更新参数
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
if print_cost and i % 1000 == 0:
print("Cost after iteration %i: %f" %(i, cost))
return W1, b1, W2, b2
此处只为保留一下未mini-batch版本
训练测试
预测函数如下,即正向传播过程
def predict(X, W1, b1, W2, b2):
Z1 = np.dot(W1, X) + b1
A1 = np.maximum(0, Z1)
Z2 = np.dot(W2, A1) + b2
A2 = np.exp(Z2) / sum(np.exp(Z2))
predictions = A2.argmax(axis=0)
# 使用argmax使得返回值predictions为和label相同的0~9标签
return predictions
# 训练
tic = time.process_time()
W1, b1, W2, b2= nn_model(X, Y,n_h=300, learning_rate=0.08, num_iterations=10000, print_cost=False)
# 测试
predictions = predict(X_test, W1, b1, W2, b2)
toc = time.process_time()
# 时间
print ("Computation time = " + str((toc - tic)) + "s")
# 评估精确度
correct_predictions = np.equal(predictions, label_test)
accuracy = np.mean(correct_predictions.astype(np.float32))
print('Test Accuracy:%f'%(accuracy*100) + '%')
所有代码合并
import numpy as np
import pandas as pd
# import matplotlib.pyplot as plt
import time
# 训练集有6w, 785列,前784为28*28,最后一列为标签
train = pd.read_csv("MNIST_Training_60K.csv",index_col=False,header=0)
# 测试集有1w,784列,28*28的图片
test = pd.read_csv("MNIST_Test_10K.csv",index_col=False)
label = train[:]["784"]
train = train.drop(["784"],axis=1)
X = train.T
Y = np.zeros((10, X.shape[1]))
for n in range(X.shape[1]):
Y[label[n]][n] = 1
def nn_model(X, Y, n_h=1000, learning_rate=0.12 ,num_iterations=10000, print_cost=False):
n_x = 784
n_y = 10
m = X.shape[1]
# 随机初始化参数
np.random.seed(1)
W1 = np.random.randn(n_h, n_x) * np.sqrt(2. / n_x)
b1 = np.zeros((n_h, 1))
W2 = np.random.randn(n_y, n_h) * np.sqrt(2. / n_h)
b2 = np.zeros((n_y, 1))
for i in range(0, num_iterations):
# 正向传播: 线性-> ReLU -> 线性-> softmax
Z1 = np.dot(W1, X) + b1
A1 = np.maximum(0, Z1)
Z2 = np.dot(W2, A1) + b2
A2 = np.exp(Z2) / sum(np.exp(Z2))
# 计算成本:计算交叉熵成本
logprobs = -1* sum(np.multiply(np.log(A2), Y))
cost = -1/m * np.sum(logprobs)
cost = np.squeeze(cost)
# 反向传播:梯度下降法
dZ2 = A2 - Y
dW2 = 1/m * np.dot(dZ2, A1.T)
db2 = 1/m * np.sum(dZ2, axis=1, keepdims=True)
dA1 = np.dot(W2.T, dZ2)
dZ1 = np.array(dA1, copy=True)
dZ1[Z1 <=0 ] = 0 # 标记一下,操作十分神奇,牛逼!
dW1 = 1/m * np.dot(dZ1, X.T)
db1 = 1/m * np.sum(dZ1, axis=1, keepdims=True)
# 更新参数
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
if print_cost and i % 50 == 0:
print("Cost after iteration %i: %f" %(i, cost))
return W1, b1, W2, b2
label_test = test[:]["784"]
test1 = test.drop(["784"],axis=1)
X_test = test1.T
# Y_test = np.eye(10)[l]
# 训练
tic = time.process_time()
W1, b1, W2, b2= nn_model(X, Y,n_h=300, learning_rate=0.08, num_iterations=10000, print_cost=False)
# 测试
Z1 = np.dot(W1, X_test) + b1
A1 = np.maximum(0, Z1)
Z2 = np.dot(W2, A1) + b2
A2 = np.exp(Z2) / sum(np.exp(Z2))
predictions = A2.argmax(axis=0)
toc = time.process_time()
print ("Computation time = " + str((toc - tic)) + "s")
correct_predictions = np.equal(predictions, label_test)
accuracy = np.mean(correct_predictions.astype(np.float32))
print('Test Accuracy:%f'%(accuracy*100) + '%')
添加mini-batch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import time
# 训练集有6w, 785列,前784为28*28,最后一列为标签
train = pd.read_csv("./MNIST_Training_60K.csv", index_col=False, header=0)
# 测试集有1w,784列,28*28的图片
test = pd.read_csv("./MNIST_Test_10K.csv", index_col=False)
label = train[:]["784"]
train = train.drop(["784"], axis=1)
X = train.T
Y = np.zeros((10, X.shape[1]))
for n in range(X.shape[1]):
Y[label[n]][n] = 1
def nn_model(X, Y, n_h=1000, mini_batch_size=100, learning_rate=0.08, epoch=1000, print_cost=False):
n_x = 784
n_y = 10
m = X.shape[1]
# 随机初始化参数
np.random.seed(3)
W1 = np.random.randn(n_h, n_x) * np.sqrt(2. / n_x)
b1 = np.zeros((n_h, 1))
W2 = np.random.randn(n_y, n_h) * np.sqrt(2. / n_h)
b2 = np.zeros((n_y, 1))
costs=[]
complete_numof_mini_batch = m // mini_batch_size
for i in range(0, epoch):
for j in range(0, complete_numof_mini_batch):
# 正向传播: 线性-> ReLU -> 线性-> softmax
mini_batch_X = X.values[:, (j * mini_batch_size): (j + 1) * mini_batch_size]
mini_batch_Y = Y[:, (j * mini_batch_size): (j + 1) * mini_batch_size]
Z1 = np.dot(W1, mini_batch_X) + b1
A1 = np.maximum(0, Z1)
Z2 = np.dot(W2, A1) + b2
A2 = np.exp(Z2) / sum(np.exp(Z2))
# 计算成本:计算交叉熵成本
logprobs = -1 * sum(np.multiply(np.log(A2), mini_batch_Y))
cost = -1 / mini_batch_size * np.sum(logprobs)
cost = np.squeeze(cost)
# 反向传播:梯度下降法
dZ2 = A2 - mini_batch_Y
dW2 = 1 / mini_batch_size * np.dot(dZ2, A1.T)
db2 = 1 / mini_batch_size * np.sum(dZ2, axis=1, keepdims=True)
dA1 = np.dot(W2.T, dZ2)
dZ1 = np.array(dA1, copy=True)
dZ1[Z1 <= 0] = 0 # 标记一下,操作十分神奇,牛逼!
dW1 = 1 / mini_batch_size * np.dot(dZ1, mini_batch_X.T)
db1 = 1 / mini_batch_size * np.sum(dZ1, axis=1, keepdims=True)
# 更新参数
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
if print_cost and (j+1) % 500:
costs.append(cost)
if print_cost and (i+1) % 5 == 0:
print("Cost after epoch [%3d/%3d]: %f" % (i+1, epoch, cost))
plt.plot(costs)
plt.ylabel('cost')
plt.xlabel('iterations (per 500)')
plt.title("Learning rate = " + str(learning_rate))
plt.show()
return W1, b1, W2, b2
label_test = test[:]["784"]
test1 = test.drop(["784"], axis=1)
X_test = test1.T
# Y_test = np.eye(10)[l]
# 训练
mini_batch_size = 100
tic = time.process_time()
W1, b1, W2, b2 = nn_model(X, Y, n_h=1000, mini_batch_size=mini_batch_size, learning_rate=0.1, epoch=100, print_cost=True)
# 测试
complete_numof_mini_batch_test = X_test.shape[1] // mini_batch_size
accuracy = []
for k in range(0, complete_numof_mini_batch_test):
mini_batch_X_test = X_test.values[:, (k * mini_batch_size):((k+1) * mini_batch_size)]
mini_batch_label = label_test.values[(k * mini_batch_size): (k+1) * mini_batch_size]
Z1 = np.dot(W1, mini_batch_X_test) + b1
A1 = np.maximum(0, Z1)
Z2 = np.dot(W2, A1) + b2
A2 = np.exp(Z2) / sum(np.exp(Z2))
predictions = A2.argmax(axis=0)
correct_predictions = np.equal(predictions, mini_batch_label)
accuracy.append(np.mean(correct_predictions.astype(np.float32)))
accuracy = np.mean(accuracy)
toc = time.process_time()
print("Computation time = " + str((toc - tic)) + "s")
print('Test Accuracy:%f' % (accuracy * 100) + '%')

代码与cost如上图,目前改进就是如此了,将X分成100样本一小份,然后逐份进行训练,由于参数共享,所以只有反向传播时涉及到了Y,别处都不需要修改太多。后续具体参数还需要继续调整,敬请期待!
最后调整好的准确率和时间如下:

说说我对神经网络的理解
再看一眼这个参数传播的图:

宏观理解
根据上面的参数传播图来宏观的理解一下就是:
我们所需要做的分类在数值化后,可以通过拟合几个函数(在这个全连接层里起码是这样的),通过它们得到最终的值并进行取舍和改进,慢慢的能够得出一个网络,包含了多个拟合好的函数进行计算和选择,以此来做到分类
从X开始,每层选择一个节点,一直到输出层,这样就是一个函数,全连接层就是每层节点与上一层都有关系,而通过上一层的权重与上上层的节点也保持一定的关联,依次类推。
正向传播
正向传播中,每次求Z都是一次线性运算,相当于将x放入一个逐次增高的函数中,让x位于的维度越来越高,而w就是其每层幂中对于x的权重,在图像中,相当于是w在决定该维度中的决策边线的角度,b是因为单纯的旋转不足以让需要拟合的曲线到达需要的地方,还需要进行上下调整,因为决策边线是无限长的,所以上下调整配合角度旋转就可以到达任何需要拟合的位置。
而随着网络层数的增加,这个输出会到达较高的维度,这样的维度中决策边线就是曲线了。
ReLU函数(A = max(0, Z))的修正作用一方面使得被激活的函数呈非对称(偶)结构,另一方面优化了计算的步骤,对于Z1是负数的情况不需要考虑,由别的地方来给它解答。
softmax()的作用不言而喻,对于最可能的情况给予他最大的权重值,但是别的情况也有相似值可以考虑,这就好像模拟了人的认识过程,随着你越来越懂怎么辨认,你就越来越不会觉得可能是别的情况,但在一开始还是不清楚的。softmax还有一个好处就是它自带标准化,得出的值的和为1,这样我们不妨可以将每个值认作对应类的概率,从而与one-hot后的Y值不谋而合。只要我们优化函数使得最终对应类的值尽可能的接近1即可~
计算成本
计算成本的函数为交叉熵函数(),其中N指的是需要分辨的类的总数,也是softmax中得出的结果数量。这个函数与KL散度密切相关,当且仅当分布P和Q在离散变量的情况下是相同的分布,或者连续性变量的情况下是几乎“处处相同”时,KL散度为0。最小化成本即最小化交叉熵的过程就是拟合神经网络中的分布与给定数据的分布的过程,使得它们尽量的相似。在数据足够多的情况下,基本可以认为它服从真实的分布。
(原公式中是,而在softmax的结果中我们基本可以认为)
每一批量的成本取值为该批量中的样本计算出的交叉熵的平均值,这样大体上可以模拟该批量中的样本所代表的分布与拟合出的分布之间的差距
反向传播
反向传播的意义在于什么呢,在于计算出成本函数关于各个参数(尤其是W,b)的梯度,而W和b的梯度需要借助其他参数得到(因为链式求导法则),因此,我们需要沿着成本函数反向的计算各个参数关于成本的梯度,从而借此更方便的计算出W和b的梯度
关于这个梯度的作用的话,大家都知道,梯度方向是函数在该店沿着梯度方向变化最快的方向,所以借助梯度我们能更快的改变参数的值,让网络的性能更好。
更新参数
更新参数的过程才是使用梯度下降法的关键,更新参数需要参数减去学习率*成本关于该参数的梯度。我们将梯度认作是一个让我们下降最快的方向的话,那么学习率就是我们下降的时候一步跨出的步长,比如在一个碗状的函数中,高度值代表着我们的误差值,那么沿着当前点的梯度方向跨出一步,理论上相对而言就是下降较快的一步,而不同的点的梯度不一样,所以并不能说明它是最快的。
并且随着我们越来越接近碗底,我们跨出的步长应该变得越来越小,从而使得我们能够更“小心地”接近最低点。所以学习率衰减是一个好办法。从物理上得出这样的理解,同样从物理上得到灵感的还有动量梯度下降法),取其加权平均数,一般取 = 0.9较多,即十个样本的加权平均值。
循环批量(batch),遍数(epoch)
我这里使用小批量(mini-batch)的是为了照顾后面对应使用x86汇编实现,暂时想的是mini-batch取20,因为10及以下容易在计算log的时候算到log0从而后面的值都是无效值。小批量循环至一遍结束后要继续遍历epoch次,我这里由于将第一层的神经元数调小了,遍历次数需要多一些,从100升到了120,有时150
并且,由于使用了小批量时每次向量化计算减少了,for循环需要的次数增多了,计算时间也会被明显拉长
测试
先训练后得出模型的重要参数W,b后,我们要进行测试,时间的评估以及准确度的评估
测试时将测试集放入参数中进行正向传播后得出A2,即模型的预测值,然后看预测值正确的数量来评估准确度。
调参收获

这个是我在调小了第一层的神经元数1000=>50后的成本和准确率,调小了神经元数之后呢,相应的这个需要遍历的次数变的高了不少,我觉得可能第一层的权重w初始化需要小一点比较好,本身初始化大小与节点数成反相关的。并且呢需要拟合的复杂度变高了,毕竟与每个节点的相关度变高了,这就相当于是反向正则化了(并且也确认了一个观点“增加网络的规模有一定正则化作用”是对的),然后的话mini-batch不能太小,而在cost图中如果阴影部分比较分散的话,我个人觉得可能是需要学习率小一点,如果是多片分开的阴影的话我觉得可能需要神经元数多一点更好。