[学习笔记]神经网络之一:简单实现一个神经网络

这几天开始学习神经网络,本帖为我在读完《Python神经网络编程》后的一个总结,因为我是神经网络的初学者,当出现一些错误或者说法不当时,请多多指正。

本文的目的是使用二层神经网络(输入层、隐藏层和输出层,输入层一般只是负责输入)来实现对手写体数字的识别,这里分别采用《Python神经网络编程》中的代码和Pytorch来实现。

训练数据集:http://www.pjreddie.com/media/files/mnist_train.csv

测试数据集:http://www.pjreddie.com/media/files/mnist_test.csv

1.使用Python和numpy

第一段使用Python和numpy来实现一个二层的神经网络。

1.1 神经网络展示

最简单的二层神经网络的结构如下:

[学习笔记]神经网络之一:简单实现一个神经网络_第1张图片 图1-1 二层神经网络(摘自《Python神经网络编程》)

我所理解的神经网络就类似于一个函数,给定一个输入,然后神经网络返回一个输出,如此而已。

 

首先是输入层,这一层不做其他事情,仅仅表示输入信号,输入节点不对输入值应用激活函数

然后是隐藏层,第一层输入值在经过组合后作为隐藏层的输入,然后经过激活函数后传递;

最后是输出层,组合隐藏层的输入,然后经过激活函数后输出。

组合的意思就是按权重相加,可以理解为表达式:

y = Ax + b

 A表示链接权重矩阵,b表示偏移值(常数,这里为0),这里的组合方式就是pytorch中的nn.Linear()。

常用的激活函数有sigmoid、relu等,这里使用的是sigmoid函数。


为什么要使用激活函数?

如果不用激活函数的话,多层神经网络与单层神经网络等同。


 其表达式如下:y = \frac{1}{1 + e^{-x}},值域为(0, 1)

[学习笔记]神经网络之一:简单实现一个神经网络_第2张图片 图1-2 sigmoid函数

拿图1-1举例子,输入层在经过表达式y=Ax后作为隐藏层的输入,隐藏层输出的为sigmoid(y)的值。

1.2 初始化函数

接下来就开始编写python代码了,本示例使用的是二层神经网络,因此有两个链接权重矩阵,而链接权重的初始化,则也有一些讲究。大的初始权重会造成大的信号传递给激活函数,导致网络饱和,因此应该避免过大的初始权重。数学家所得到的经验规则是,我们可以在一个节点传入链接数量平方根倒数的大致范围内随机采样,初始化权重。

如果一个节点有三个输入,那么初始权重的范围应该在-1\sqrt{3}+1\sqrt{3}之间,如此类推。这一经验法则实际上讲的是从均值为0、标准方差等于节点传入链接数量平方根倒数的正态分布中进行采样。另外,应该避免设置相同的权重或0,相同的权重会导致同等的权重更新,从而达不到更新权重矩阵的效果;权重为0时同样也使得网络丧失了更新权重的能力

class NeuralNetwork(object):
    def __init__(self, input_nodes, hidden_nodes, output_nodes, learning_rate):
        # 输入层 隐藏层和输出层的节点个数
        self.inodes = input_nodes
        self.hnodes = hidden_nodes
        self.onodes = output_nodes
        # learning rate
        self.lr = learning_rate
        # 创建输入层和隐藏层的链接权重矩阵
        self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
        # 隐藏层和输出层的权重矩阵
        self.who = np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
        # 定义激活函数
        self.activation_func = lambda x: expit(x)

因为有隐藏层和输出层,所以有两个链接权重矩阵。 

self.wih为输入层(input layer)和隐藏层(hidden layer)的链接权重矩阵;self.who为隐藏层(hidden layer)和输出层(output layer)的链接权重矩阵。

1.3 正向传播

所谓的正向传播,指的是给神经网络输入,然后得到它的输出,此即为正向传播,在本示例中为NeuralNetwork类的query函数:

    def query(self, inputs_list):
        hidden_inputs = np.dot(self.wih, inputs_list)
        hidden_outputs = self.activation_func(hidden_inputs)

        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_func(final_inputs)

        return final_outputs

 query函数中输出层也调用了一次激活函数,因为使用的激活函数为sigmoid,所以输出值的值域为(0, 1),两端取不到。

1.4 反向传播

反向传播的目的是为了训练,训练神经网络的目的就是根据训练样本来获得一个“正确”的权重矩阵。通过把训练样本输入到神经网络得到输出值,然后神经网络输出值和训练样本输出值的差值来更新链接权重矩阵的值,进而达到学习的目的,此即为反向传播。

反向传播的流程如下:

  1. 得到输出层的误差;
  2. 根据输出层和相应的权重得到隐藏层的误差;
  3. 根据学习效率(learning rate)和斜率对矩阵进行更新;
  4. 重复1~3。
    def train(self, inputs_list, targets_list):
        # 转换到二维矩阵
        inputs = np.array(inputs_list, ndmin=2).T
        targets = np.array(targets_list, ndmin=2).T
        # 计算
        hidden_inputs = np.dot(self.wih, inputs)
        hidden_outputs = self.activation_func(hidden_inputs)

        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_func(final_inputs)
        # 计算误差
        output_errors = targets - final_outputs
        hidden_errors = np.dot(self.who.T, output_errors)
        # 更新权重
        self.who += self.lr * np.dot((output_errors * final_outputs * (1.0 - final_outputs)),
                                     np.transpose(hidden_outputs))
        self.wih += self.lr * np.dot((hidden_errors * hidden_outputs * (1 - hidden_outputs)),
                                     np.transpose(inputs))

 由于这些节点都不是简单的线性分类器。这些稍微复杂的节点,对加权后的信号进行求和,并运用了sigmoid函数,将所得到的的输出给下一层的节点。由于这里使用到的激活函数为sigmoid,因此更新权重的公式略微复杂。

数学家们发现了梯度下降(gradient descent),采用步进的方式接近答案。

[学习笔记]神经网络之一:简单实现一个神经网络_第3张图片 图1-3 梯度下降

步进时要避免超调,这也就是learning rate的作用。 

误差E的值是目标值减去实际值,即E=t-o,而由于误差不能相互抵消,因此应该去掉E的符号值;为了避免超调,因此使用平方的形式:E = (t-a)^{2}。接着就需要对上面的值求偏导即可。

详细参见《Python神经网络编程》P96-P102

1.5 样本训练

if __name__ == '__main__':
    input_nodes = 784
    hidden_nodes = 100
    output_nodes = 10
    learning_rate = 0.1

    net = NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
    # 加载训练数据 并 训练
    train_data = pd.read_csv('mnist_train.csv', header=None)
    for i, series in train_data.iterrows():
        # 输入转为[0.01, 1.0]
        scale_inputs = (np.array(series[1:]) / 255 * 0.99) + 0.01
        # 把第一行数据转为输出
        targets = np.zeros(output_nodes) + 0.01
        targets[series[0]] = 0.99
        # 训练
        net.train(scale_inputs, targets)
    print('训练完成,正在测试...')
    # 使用测试数据
    test_data = pd.read_csv('mnist_test.csv', header=None)

    correct_count = 0
    for index, series in test_data.iterrows():
        correct_label = series[0]
        inputs = (np.array(series[1:]) / 255 * 0.99) + 0.01
        # 预测
        outputs = net.query(inputs)
        label = np.argmax(outputs)
        if label == correct_label:
            correct_count += 1
    print('正确率', correct_count / len(test_data))

 这里要先说明几点:

1.5.1 输入

首先是输入,数据集为784维度,即28*28,每个值的大小是[0, 255],这里限定输入值的值域在[0.01, 1.0]之间。刻意选择0.01作为范围的最低点,是为了避免先前观察到的0值输入造成的权重更新失败(0 * 链接权重=0);选择1.0作为输入的上限,是因为不需要避免输入1.0造成这个问题,只需要避免输出值为1.0即可。

1.5.2 输出

接着是输出值,因为目前的输出值同样也调用了sigmoid激活函数,而sigmoid是取不到1的,从图1-2中可以看出,如果目标值为1, 则会导致大的权重和饱和网络,因此这里使用了0.99代替了1.0。

1.5.3 神经网络

最后则是神经网络,输入层是784个,是因为训练样本有784个维度,输出层为10个,是因为数字有0~9共10种情况:

[学习笔记]神经网络之一:简单实现一个神经网络_第4张图片 图1-4 输出

 输出层得到的长度为10的数组,然后从中选出一个最大的值作为标签。

注:类似于这种分类,其实更应该使用softmax。

2.使用Pytorch

首先是导入包

"""
使用pytorch进行分类
"""
import os
import torch
import pandas as pd
from torch import nn
from torch.utils.data import DataLoader, Dataset
import torch.optim as optim

2.1 网络结构

其实我个人觉得一层神经网络就够了,不过为了统一,这里仍然使用两层神经网络。使用类来组织:

class MnistNet(nn.Module):
    def __init__(self, input_nodes, hidden_nodes, output_nodes):
        super(MnistNet, self).__init__()
        self.hidden = nn.Linear(input_nodes, hidden_nodes, bias=False)
        self.output = nn.Linear(hidden_nodes, output_nodes, bias=False)

    def forward(self, x):
        y = torch.sigmoid(self.hidden(x))
        y = self.output(y)
        return y

 和第一部分有所不同的是,由于这里主要用于分类,所以这里的输出并没有使用激活函数(主要使用nn.CrossEntropyLoss类)。

2.2 获取数据

使用pytorch的DataLoader和自定义的Dataset类来完成数据的获取:

class MnistDataset(Dataset):
    def __init__(self, filename):
        self.df = pd.read_csv(filename, header=None)

    def __getitem__(self, idx):
        series = self.df.iloc[idx]
        features = (series[1:] / 255 * 0.99) + 0.01

        X = torch.tensor(features.to_numpy(), dtype=torch.float32)
        y = torch.tensor(series.iloc[0])

        return X, y

    def __len__(self):
        return len(self.df)

自定义的Dataset类必须要实现__getitem__和__len__方法。因为在隐藏层使用了sigmoid函数,因此仍然需要输入值在[0.01, 1]之间。

2.3 主函数

对于分类问题,一般可以使用softmax,在《动手学神经网络》3.7.3节中,“分开定义softmax运算和交叉熵损失函数可能会造成数值 不不稳定。因此,PyTorch提供了了⼀一个包括softmax运算和交叉熵损失计算的函数”。综上,这里使用的交叉熵损失函数。优化同样使用随机梯度下降作为优化算法:

if __name__ == '__main__':
    input_nodes = 784
    hidden_nodes = 100
    output_nodes = 10
    learning_rate = 0.1

    net = MnistNet(input_nodes, hidden_nodes, output_nodes)
    loss_func = nn.CrossEntropyLoss()
    optimizer = optim.SGD(net.parameters(), lr=learning_rate)

首先定义了网络、损失函数和优化器。

    train_data = MnistDataset('mnist_train.csv')
    test_data = MnistDataset('mnist_test.csv')
    train_iter = DataLoader(train_data, batch_size=100, shuffle=True)
    test_iter = DataLoader(test_data, batch_size=100, shuffle=True)

 接着加载数据,把数据作为迭代器,100为一个批次,并随机打乱数据。

    for epoch in range(10):
        train_loss_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            y_hat = net(X)
            loss = loss_func(y_hat, y)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            train_loss_sum += loss.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f' %
              (epoch + 1, train_loss_sum / n, train_acc_sum / n, test_acc))

 最后我们进行训练,这里训练了10代,并在每一代结束的时候都测试准确度,测试结果如下:

[学习笔记]神经网络之一:简单实现一个神经网络_第5张图片

可以看到,在训练了第7代的时候,测试准确度已经达到了92%。 

3.参考文献

  • 《Python神经网络编程》
  • 《动手学深度学习》

你可能感兴趣的:(机器学习)