吴恩达机器学习课笔记, 实例神经网络. 以AICS中为例
1 Neural Network
神经网络可以理解成抽象的神经元连接, 如果不引入非线性的函数, 其本质和线性回归差别不大.
神经网络中, 多层网络是在自动提取特征. 逐渐从小特征到大特征.
这里直接举出实例来表现: 手写阿拉伯数字的识别.
1.1 手写数字识别网络结构
搭建神经网络如下图:
1.1.1 全连接层(FullyConnectedLayer)
class FullyConnectedLayer(object):
def __init__(self, num_input, num_output): # 全连接层初始化
self.num_input = num_input
self.num_output = num_output
print('\tFully connected layer with input %d, output %d.' % (self.num_input, self.num_output))
def init_param(self, std=0.01): # 参数初始化
self.weight = np.random.normal(loc=0.0, scale=std, size=(self.num_input, self.num_output))
self.bias = np.zeros([1, self.num_output])
def forward(self, input): # 前向传播计算
start_time = time.time()
self.input = input
# TODO:全连接层的前向传播,计算输出结果
self.output = np.mat
return self.output
def backward(self, top_diff): # 反向传播的计算
self.d_weight = np.mat(self.input.T, top_diff)
self.d_bias = np.matmul(np.ones([1, top_diff.shape[0]]), top_diff)
bottom_diff = np.matmul(top_diff, self.weight.T)
return bottom_diff
def update_param(self, lr): # 参数更新
self.weight = self.weight - lr * self.d_weight
self.bias = self.bias - lr * self.d_bias
def load_param(self, weight, bias): # 参数加载
assert self.weight.shape == weight.shape
assert self.bias.shape == bias.shape
self.weight = weight
self.bias = bias
def save_param(self): # 参数保存
return self.weight, self.bias
全连接层以一维向量作为输入,输入与权重相乘后再与偏置相加得到输出向量. 是计算的主要层.
假设全连接层的输入为一维向量$\vec{x}$,维度为m;输出为一维向量y,维度为 n;权重 $W$ 是二维矩阵,维度为 m × n,偏置 b 是一维向量(也可以整个层用一个值替代),维度为 n。前向传播(forward)时,全连接层的输出的计算公式为
\[\vec{y} = W^T\vec{x} + b\]这里和线性回归一致.
在计算全连接层的反向传播时,给定神经网络损失函数 L 对当前全连接层的输出 y 的偏导$\nabla_yL = \frac{\partial L}{\partial y}$,其维度与全连接层的输出 y 相同,均为 n。
实际应用中通常使用批量随机梯度下降算法进行反向传播计算,即选择若干个样本同时计算。假设选择的样本量为 p,此时输入变为二维矩阵 X,维度为 p×m,每行代表一个样本。输出也变为二维矩阵 Y,维度为 p×n。
1.1.2 ReLU激活函数层
就是一个$\max(0,x)$的非线性函数.
1.1.3 Softmax损失层
计算损失函数, 并且归一化. 可以理解为多维的sigmoid函数.
1.2 模型构建
我们按照构建顺序来讲解各层和NN构建.
1.2.1 主函数
if __name__ == '__main__':
mlp = build_mnist_mlp()
evaluate(mlp)
这里分成两部, 一部分是构建模型, 另一部分是评估模型.
评估有很多种方法, 比较简单的就是将数据分为训练集(train set)和测试集(test/validation set). 训练后从测试集上评估结构, 防止过拟合等情况出现.
具体的优化方法由于笔者还远未入门, 所以等以后在学习.
接下来看构建模型.
1.2.2 构建模型
def build_mnist_mlp(param_dir='weight.npy'):
h1, h2, e = 32, 16, 10
mlp = MNIST_MLP(hidden1=h1, hidden2=h2, max_epoch=e)
mlp.load_data()
mlp.build_model()
mlp.init_model()
mlp.train()
mlp.save_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
return mlp
其中h1
和h2
都是隐层神经元个数.
所谓隐层, 就是除开输出和输入两个层之间的所有层.
而e
为训练代数, 此处就为训练10次.
第二行构建一个mlp类返回, 接下来这个mlp会读入数据, 构建模型等操作. 我们一步一步来.
首先是参数初始化:
class MNIST_MLP(object):
def __init__(self, batch_size=100, input_size=784, hidden1=32, hidden2=16, out_classes=10, lr=0.01, max_epoch=1, print_iter=100):
self.batch_size = batch_size
self.input_size = input_size
self.hidden1 = hidden1
self.hidden2 = hidden2
self.out_classes = out_classes
self.lr = lr
self.max_epoch = max_epoch
self.print_iter = print_iter
各个参数具体含义之后再讲.
接下来读入数据:
1.2.3 MNIST
def load_data(self):
print('Loading MNIST data from files...')
train_images = self.load_mnist(os.path.join(MNIST_DIR, TRAIN_DATA), True)
train_labels = self.load_mnist(os.path.join(MNIST_DIR, TRAIN_LABEL), False)
test_images = self.load_mnist(os.path.join(MNIST_DIR, TEST_DATA), True)
test_labels = self.load_mnist(os.path.join(MNIST_DIR, TEST_LABEL), False)
self.train_data = np.append(train_images, train_labels, axis=1)
self.test_data = np.append(test_images, test_labels, axis=1)
读入需要按照MNIST数据集结构来读入:
MNIST数据集(Mixed National Institute of Standards and Technology database)是美国国家标准与技术研究院收集整理的大型手写数字数据库,包含60,000个示例的训练集以及10,000个示例的测试集.
其格式如下:
所以有读入函数:
def load_mnist(self, file_dir, is_images = 'True'):
# Read binary data
bin_file = open(file_dir, 'rb')
bin_data = bin_file.read()
bin_file.close()
# Analysis file header
if is_images:
# Read images
fmt_header = '>iiii'
magic, num_images, num_rows, num_cols = struct.unpack_from(fmt_header, bin_data, 0)
else:
# Read labels
fmt_header = '>ii'
magic, num_images = struct.unpack_from(fmt_header, bin_data, 0)
num_rows, num_cols = 1, 1
data_size = num_images * num_rows * num_cols
mat_data = struct.unpack_from('>' + str(data_size) + 'B', bin_data, struct.calcsize(fmt_header))
mat_data = np.reshape(mat_data, [num_images, num_rows * num_cols])
print('Load images from %s, number: %d, data shape: %s' % (file_dir, num_images, str(mat_data.shape)))
return mat_data
简单说一下
fmt_header
,>
表示大尾端,iiii
表示读出四个有符号整数; 后面的B
代表无符号字节(或者unsigned char); 更多请看FormatString
如果是图像信息, 则读出魔数, 图像个数, 单幅图像的行列共四个信息.
如果是标记信息, 则只需要读出魔数和图像个数, 接下来每个字节都是对应图像的数字值.
最后返回一个num_image
行, 每行长num_rows * num_cols
的二维矩阵.
回到load_data()
中, 最后使用np.append(... axis = 1)
将标记数据添加到行末位. 也就是数据变成: num_image行, 每行初始为num_rows*num_cols的图像, 和最后一个0-9的数字表示结果
并将这些存储到mlp中.
自此, 建立的模型的第一步, 读入数据完成. 接下来看搭建网络.
1.2.4 搭建网络
def build_model(self): # 建立网络结构
print('Building multi-layer perception model...')
self.fc1 = FullyConnectedLayer(self.input_size, self.hidden1)
self.relu1 = ReLULayer()
self.fc2 = FullyConnectedLayer(self.hidden1, self.hidden2)
self.relu2 = ReLULayer()
self.fc3 = FullyConnectedLayer(self.hidden2, self.out_classes)
self.softmax = SoftmaxLossLayer()
self.update_layer_list = [self.fc1, self.fc2, self.fc3]
设置各层如初始图一致.
随后初始化:
def init_model(self):
print('Initializing parameters of each layer in MLP...')
for layer in self.update_layer_list:
layer.init_param()
1.2.5 训练网络
def train(self):
max_batch = self.train_data.shape[0] / self.batch_size
print('Start training...')
for idx_epoch in range(self.max_epoch):
self.shuffle_data()
for idx_batch in range(max_batch):
batch_images = self.train_data[idx_batch*self.batch_size:(idx_batch+1)*self.batch_size, :-1]
batch_labels = self.train_data[idx_batch*self.batch_size:(idx_batch+1)*self.batch_size, -1]
prob = self.forward(batch_images)
loss = self.softmax.get_loss(batch_labels)
self.backward()
self.update(self.lr)
if idx_batch % self.print_iter == 0:
print('Epoch %d, iter %d, loss: %.6f' % (idx_epoch, idx_batch, loss))
确定训练批次, 为图像总数/每次大小. 每次一次完成的训练即为一次epoch.
随后利用np.random.shuffle
打乱数据顺序, 防止固定的训练顺序导致结果类似.
对每一次周期内的分批次训练. 由之前的数据分析可知, :-1
是取到label之前, 而-1
则是取最后的label, 随后开始前向传播-损失计算-后向传播.
1.2.6 保存参数
mlp.save_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
最后保存下参数, 防止每次都要训练.
接下来我们深入1.2.5中的训练过程.
1.3 训练过程
着重分析这一段:
prob = self.forward(batch_images)
loss = self.softmax.get_loss(batch_labels)
self.backward()
self.update(self.lr)
1.3.1 forward
前向传播如下:
def forward(self, input): # 神经网络的前向传播
h1 = self.fc1.forward(input)
h1 = self.relu1.forward(h1)
h2 = self.fc2.forward(h1)
h2 = self.relu2.forward(h2)
h3 = self.fc3.forward(h2)
prob = self.softmax.forward(h3)
return prob
先看全连接层的forward:
def forward(self, input): # 前向传播计算
self.input = input
# y = X * W + b
#self.output = np.matmul(self.input, self.weight) + self.bias
self.output = self.input.dot(self.weight) + self.bias
return self.output
直接相乘即可
relu则是 output = np.maximum(0, self.input)
输出即可;
softmax则有些不同:
def forward(self, input): # 前向传播的计算
input_max = np.max(input, axis=1, keepdims=True)
input_exp = np.exp(input - input_max)
""" prob = exp(input-input_max) / sum(exp(input-input_max))"""
self.prob = input_exp / np.sum(input_exp, axis=1, keepdims=True)
return self.prob
以上就是直接的公式实现即可.
1.3.2 loss
损失层是为了衡量当前结果和最终目标之间的差距, 可以说是训练的监督员:
def get_loss(self, label): # 计算损失
self.batch_size = self.prob.shape[0]
self.label_onehot = np.zeros_like(self.prob)
self.label_onehot[np.arange(self.batch_size), label] = 1.0
loss = -np.sum(np.log(self.prob) * self.label_onehot) / self.batch_size
return loss
softmax层利用交叉熵来计算损失. 实现如上.
1.3.3 backward
后向传播是神经网络的调整过程, 大致如下:
def backward(self): # 神经网络的反向传播
dloss = self.softmax.backward()
dh3 = self.fc3.backward(dloss)
dh2 = self.relu2.backward(dh3)
dh2 = self.fc2.backward(dh2)
dh1 = self.relu1.backward(dh2)
dh1 = self.fc1.backward(dh1)
首先是softmax计算出损失, 然后逐层后向传递.
def backward(self): # 反向传播的计算
""" diff = 1/p * (y_h - y) """
bottom_diff = (self.prob - self.label_onehot) / self.batch_size
return bottom_diff
全连接层后向传播:
def backward(self, top_diff): # 反向传播的计算
self.d_weight = np.matmul(self.input.T, top_diff)
self.d_bias = np.matmul(np.ones([1, top_diff.shape[0]]), top_diff)
bottom_diff = np.matmul(top_diff, self.weight.T)
return bottom_diff
ReLU层后向传播:
def backward(self, top_diff): # 反向传播的计算
bottom_diff = top_diff * (self.input >= 0.)
return bottom_diff
如此反复就可以调整出最佳的参数模式.
之后会更新一个更复杂一点的VGG19的神经网络.