我猜测可能会有很多人跟我一样,在学完cs231n后仍然对神经网络没有一个整体印象,所以这是一份为后人能更轻松地理解神经网络而编写的学习笔记,本文通过我自己的理解对一份神经网络代码进行解释,将所有的知识点串联起来,希望可以造福后人。
import numpy as np
def unpickle(file):
import pickle
with open(file, 'rb') as fo:
dict = pickle.load(fo, encoding='bytes')
return dict
def fetch_data():
train_data = []
train_label = []
test_data = []
testlabel = []
for i in range(5):
data = unpickle(
# datasets\cifar-10-batches-py\data_batch_1
f'E:/projects/datasets/cifar-10-batches-py/data_batch_{i+1}')
train_data.append(data[b'data'])
train_label.append(data[b'labels'])
test = unpickle('E:/projects/datasets/cifar-10-batches-py/test_batch')
test_data = test[b'data']
testlabel = test[b'labels']
train_data = np.concatenate(train_data)
train_label = np.concatenate(train_label)
train_data = train_data.reshape(
(50000, 3, 32, 32)).transpose(0, 2, 3, 1).astype("float32")
train_data = train_data.reshape(-1, 32*32*3)
test_data = test_data.reshape((10000, 3, 32, 32)).transpose(
0, 2, 3, 1).astype("float32")
test_data = test_data.reshape(-1, 32*32*3)
return train_data, train_label, test_data, testlabel
def preprocess_data(train_data, test_data):
mean = np.mean(train_data, axis=0)
train_data -= mean
test_data -= mean
return train_data, test_data
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size, std=1e-4):
self.params = {}
self.params['W1'] = std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
def relu(self, x):
return np.maximum(0, x)
def relu_backward(self, dout, cache):
dx, x = dout, cache
dx[x <= 0] = 0
return dx
def loss(self, X, y=None, reg=0.0):
W1, b1 = self.params['W1'], self.params['b1']
W2, b2 = self.params['W2'], self.params['b2']
Hidden_layer = self.relu(np.dot(X, W1) + b1)
scores = np.dot(Hidden_layer, W2) + b2
if y is None:
return scores
loss = np.sum(-np.log(np.exp(scores[range(len(scores)), y]) /
np.sum(np.exp(scores), axis=1)))
loss /= len(X)
loss += 0.5*reg*(np.sum(W1*W1)+np.sum(W2*W2))
grads = {}
dscores = np.exp(scores)/np.sum(np.exp(scores), axis=1, keepdims=True)
dscores[range(len(scores)), y] -= 1
dscores /= len(X)
grads['W2'] = np.dot(Hidden_layer.T, dscores) + reg*W2
grads['b2'] = np.sum(dscores, axis=0)
dhidden = np.dot(dscores, W2.T)
dhidden_relu = self.relu_backward(dhidden, Hidden_layer)
grads['W1'] = np.dot(X.T, dhidden_relu) + reg*W1
grads['b1'] = np.sum(dhidden, axis=0)
return loss, grads
def train(self, X, y,
learning_rate=1e-3, learning_rate_decay=0.95,
reg=5e-6, num_iters=100,
batch_size=200, verbose=False):
num_train = X.shape[0]
iterations_per_epoch = max(num_train // batch_size, 1)
for it in range(num_iters):
idx = np.random.choice(num_train, batch_size, replace=True)
X_batch = X[idx]
y_batch = y[idx]
loss, grads = self.loss(X_batch, y=y_batch, reg=reg)
self.params['W1'] -= learning_rate * grads['W1']
self.params['b1'] -= learning_rate * grads['b1']
self.params['W2'] -= learning_rate * grads['W2']
self.params['b2'] -= learning_rate * grads['b2']
# if verbose and it % 100 == 0:
# print('iteration %d / %d: loss %f' % (it, num_iters, loss))
if it % iterations_per_epoch == 0 and it != 0:
learning_rate *= learning_rate_decay
def predict(self, X):
Hidden_layer = self.relu(
np.dot(X, self.params['W1']) + self.params['b1'])
scores = np.dot(Hidden_layer, self.params['W2']) + self.params['b2']
return np.argmax(scores, axis=1)
def main():
train_data, train_label, test_data, testlabel = fetch_data()
train_data, test_data = preprocess_data(train_data, test_data)
net = NeuralNetwork(32*32*3, 100, 10)
net.train(train_data, train_label, num_iters=10000,
batch_size=200, verbose=True)
print(f'train accuracy: {np.mean(net.predict(train_data) == train_label)}')
return
if __name__ == '__main__':
main()
这里是本文所使用的代码。(训练内容为CIFAR-10数据集)
首先,
fetch_data和unpickle函数作为数据读入函数,在这里不做讲解,我们只需要知道train_data,train_label,test_data,testlabel这四个都分别是什么即可,train_data是一个n*3072的矩阵,它存储了n个训练需要的图像数据,train_label是一个列表,有n项,存储了train_data对应的答案,test_data,testlabel同理。之后我们需要对数据进行预处理,我们来看
preprocess_data函数,这份代码中只采用了均值减法初始化,这是最为常见的初始化方式,且在这份代码中效果显著,当然我们仍然可以在这个函数中添加更多的初始化方式,比如归一化等等,但是我们需要注意数据的类别,防止产生负优化,比如在CIFAR-10数据集上,归一化会显著降低它的准确率。现在我们马上就要进入最紧张刺激的神经网络环节了,但是在训练之前,我们仍然需要明确我们的神经网络格式,比如在这份代码中,我们使用的是一个两层全连接神经网络,所以我们有这个预处理函数
def __init__(self, input_size, hidden_size, output_size, std=1e-4): self.params = {} self.params['W1'] = std * np.random.randn(input_size, hidden_size) self.params['b1'] = np.zeros(hidden_size) self.params['W2'] = std * np.random.randn(hidden_size, output_size) self.params['b2'] = np.zeros(output_size)这里,我们对每一层分别定义了它的权重W和偏量b,这四个参数是我们需要训练的主要参数,并且我们注意到,他们都是根据神经元数量来定义的矩阵,那么我现在来解释一下为什么是矩阵。我们来看这个初始化函数的传入参数
input_size, hidden_size, output_size, std=1e-4,std先不用管,前面三个分别是输入的格式,第一层格式,输出格式,我们完全可以简单理解为我们需要将一个有若干个数字的集合,通过一系列的数学运算,先压缩为hidden_size的大小,最后再压缩为output_size的大小,而这里的output_size就是我们需要分类的10个标签。而这里对参数的初始化使用了随机数,因为全部为0是不正确的,因为我们后续还要对其进行求偏导以及计算梯度。初始化之后,我们就可以进行训练了,而训练的目的是迭代计算出最好的参数,也就是神经网络初始化所定义的四个参数。
def train(self, X, y, learning_rate=1e-3, learning_rate_decay=0.95, reg=5e-6, num_iters=100, batch_size=200, verbose=False): num_train = X.shape[0] iterations_per_epoch = max(num_train // batch_size, 1) for it in range(num_iters): idx = np.random.choice(num_train, batch_size, replace=True) X_batch = X[idx] y_batch = y[idx] loss, grads = self.loss(X_batch, y=y_batch, reg=reg) self.params['W1'] -= learning_rate * grads['W1'] self.params['b1'] -= learning_rate * grads['b1'] self.params['W2'] -= learning_rate * grads['W2'] self.params['b2'] -= learning_rate * grads['b2'] # if verbose and it % 100 == 0: # print('iteration %d / %d: loss %f' % (it, num_iters, loss)) if it % iterations_per_epoch == 0 and it != 0: learning_rate *= learning_rate_decay我们来看这个训练主函数,reg是我们定义的L2正则化超参数,我们直接来看迭代训练循环,我们每一次迭代随机抽取数据进行训练,用这份数据计算出四个参数的梯度,以此来更新我们的参数,使我们的参数不断优化。在这个函数中我们还使用了两个参数,
learning_rate和learning_rate_decay,这两个参数用来不断降低模型的学习率,使参数的迭代越来越精确。我们发现在训练迭代中使用了一个很重要的
loss函数来计算我们的参数梯度,这也是神经网络最重要的部分。def relu(self, x): return np.maximum(0, x) def relu_backward(self, dout, cache): dx, x = dout, cache dx[x <= 0] = 0 return dx def loss(self, X, y=None, reg=0.0): W1, b1 = self.params['W1'], self.params['b1'] W2, b2 = self.params['W2'], self.params['b2'] Hidden_layer = self.relu(np.dot(X, W1) + b1) scores = np.dot(Hidden_layer, W2) + b2 if y is None: return scores loss = np.sum(-np.log(np.exp(scores[range(len(scores)), y]) / np.sum(np.exp(scores), axis=1))) loss /= len(X) loss += 0.5*reg*(np.sum(W1*W1)+np.sum(W2*W2)) grads = {} dscores = np.exp(scores)/np.sum(np.exp(scores), axis=1, keepdims=True) dscores[range(len(scores)), y] -= 1 dscores /= len(X) grads['W2'] = np.dot(Hidden_layer.T, dscores) + reg*W2 grads['b2'] = np.sum(dscores, axis=0) dhidden = np.dot(dscores, W2.T) dhidden_relu = self.relu_backward(dhidden, Hidden_layer) grads['W1'] = np.dot(X.T, dhidden_relu) + reg*W1 grads['b1'] = np.sum(dhidden, axis=0) return loss, grads这里便是我们计算所需要的函数,我们来对
loss函数逐步分析。首先传入现有参数没有什么好说的,然后便是我们的正向传播环节,我们先用现有参数计算出10个类别所对应的得分,然后我们将这10个分数放进线性分类器里进行计算,最后得到的就是我们的损失loss。之后便是我们喜闻乐见的反向传播环节,所谓反向传播,其实就是按照线性求导法则对两层神经网络的所有参数求偏导,以此找到loss下降最快的点,才能以此更新参数。26-28行都是在对softmax线性分类器求梯度,到了29行我们发现我们在运算时对矩阵进行了转置,通俗地讲,我们明白矩阵是不服从乘法交换律的,所以如果我们想让它正确地乘,便需要给他转个方向,这样才可以进行矩阵乘法。还有一点需要注意,在32行我们对激活函数进行了求导,因为我们在前向传播的时候在第一层使用了激活函数,所以这一步也必不可少。至此,我们便可以完整地计算出参数梯度,并以此迭代训练我们的四个主要参数。