机器学习(十二):深度神经网络浅析

先阐述一些概念性的东西(也是对之前的东西的回顾,记性不好,老忘):

回归问题与分类问题:

回归:计算圆形面积的例子就属于回归问题,即我们的目的是对于一个输入x,预测其输出值y,且这个y值是根据x连续变化的值。

分类:分类问题则是事先给定若干个类别,对于一个输入x,判断其属于哪个类别,即输出一般是离散的

监督学习和无监督学习:

监督学习:通过训练让机器自己找到特征和标签之间的联系(注:也就是学习的训练集包含输入和输出,得到了最优参数模型之后 ,新来的数据集在面对只有特征没有标签的情况下时,可以判断出标签)

无监督学习:训练数据中只有特征没有标签,输入数据没有被标记,也没有确定的结果。样本数据类别未知,需要根据样本间的相似性对样本集进行分类。(注:不一定"分类",没有训练集,旨在寻找规律性,不予以某种预先分类标签对上号为目的)

损失函数,二元损失函数:

quadratic loss function通过计算hy之间差值的二次方,来表达一个神经网络的效果。具体形式如下:

J(Θ)=(h(Θ,X)−Y)2J(Θ)=(h(Θ,X)−Y)2

这个公式几乎就是我们在上面提到的对模型的误差求平方,只是稍微再复杂一点。其中的希腊字母theta代表网络中的所有参数,X代表向神经网络输入的数据,Y代表输入数据对应的预期正确输出值。

实际上,在这里theta才是自变量,因为我们一开始不知道让网络能够正确的工作的参数值是多少。我们的学习算法learn按照某种策略,通过不断的更新参数值来使损失函数J(theta, X, Y)的值减小。在这里,theta是不断变化的量。那XY就不是自变量了吗?对,它们确实不是自变量!因为对于我们要解决的一个具体的问题(比如图片字母识别),其训练数据中包含的信息其实代表的是这个问题本身的性质,我们的学习算法learn其实最终就是要从训练数据data中学习到这些性质,并通过网络模型model的结构和其中的参数把这些性质表达出来。对于一个特定的问题来说,可以认为它们是常量!

梯度下降算法:来帮助我们减小损失函数的值

参数和超参数:

我们将模型model中的参数theta称为参数,而学习算法(即梯度下降算法)里的参数alhpa被称为超参数。

叫法不同,是因为它们的作用及我们设置它们值的方式不一样。theta被称为 “参数”,是因为theta决定了我们的模型model的性质,并且theta的最终值是由我们的学习算法learn学习得到的,不需要我们手工设定。

alpha则不同,在这里alpha并不是模型model中的参数,而是在学习算法learn中决定我们的梯度下降算法每一步走多远的参数,它需要我们手工设定,且决定了得到最优theta的过程。即超参数决定如何得到最优参数

超参数alpha又被称为学习速率(learning rate)。因为alpha越大时,我们的参数theta更新的幅度越大,我们可能会更快的到达最低点。但是alpha不能设置的太大,否则有可能一次变化的太大导致 “步子太长”,会直接越过最低点,甚至导致损失函数值不降反升。

反向传播算法:

对于一个具体的参数值,我们只需要把每个节点的值代入求得的导函数公式就可以求得导数(偏导数),进而得到梯度。 这很简单,我们先从计算图的底部开始向上,逐个节点计算函数值并保存下来。这个步骤,叫做前向计算(forward)

然后,我们从计算图的顶部开始向下,逐步计算损失函数对每个子节点的导函数,代入前向计算过程中得到的节点值,得到导数值。这个步骤,叫做反向传播(backward)或者更明确一点叫做反向梯度传播

全连接层:

我们将层与层之间的每个点都有连接的层叫做全连接(fully connect)层

以sigmoid为例进行简易的反向传播代码:

import numpy as np
class FullyConnect:
    def __init__(self, l_x, l_y):  # 两个参数分别为输入层的长度和输出层的长度
        self.weights = np.random.randn(l_y, l_x)  # 使用随机数初始化参数
        print(self.weights)
        self.bias = np.random.randn(1)  # 使用随机数初始化参数
        print(self.bias)
    def forward(self, x):
        self.x = x  # 把中间结果保存下来,以备反向传播时使用
        self.y = np.dot(self.weights, x) + self.bias  # 计算w11*a1+w12*a2+bias1
        return self.y  # 将这一层计算的结果向前传递
    def backward(self, d):
        self.dw = d * self.x  # 根据链式法则,将反向传递回来的导数值乘以x,得到对参数的梯度
        self.db = d
        self.dx = d * self.weights
        return self.dw, self.db  # 返回求得的参数梯度,注意这里如果要继续反向传递梯度,应该返回self.db
class Sigmoid:
    def __init__(self):  # 无参数,不需初始化
        pass
    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))
    def forward(self, x):
        self.x = x
        self.y = self.sigmoid(x)
        return self.y
    def backward(self):  # 这里sigmoid是最后一层,所以从这里开始反向计算梯度
        sig = self.sigmoid(self.x)
        self.dx = sig * (1 - sig)
        return self.dx  # 反向传递梯度
def main():
    fc = FullyConnect(2, 1)
    sigmoid = Sigmoid()
    x = np.array([[1], [2]])
    print('weights:', fc.weights, ' bias:', fc.bias, ' input: ', x)
    # 执行前向计算
    y1 = fc.forward(x)
    y2 = sigmoid.forward(y1)
    print('forward result: ', y2)
    # 执行反向传播
    d1 = sigmoid.backward()
    dx = fc.backward(d1)
    print('backward result: ', dx)
if __name__ == '__main__':
    main()

激活函数:

在多层神经网络中,上层节点的输出和下层节点的输入之间具有一个函数关系,这个函数称为激活函数(又称激励函数)。

激活函数的意义也就是加入非线性因素,让神经网络具备非线性的表达能力(当然不是真正意义上的非线性,不过可以逼近任意的非线性函数)

epoch:我们称所有训练图片都已参与一遍训练的一个周期称为一个epoch。每个epoch结束时,我们会将训练数据重新打乱,这样可以获得更好的训练效果。我们通常会训练多个epoch

以浅层神经网络识别图片中的英文字母案例为例:

代码:

# encoding=utf-8
from scipy import misc
import numpy as np
def chuli(src, dst):
    with open(src, 'r') as f:  # 读取图片列表
        list = f.readlines()
    data = []
    labels = []
    for i in list:
        name, label = i.strip('\n').split(' ')  # 将图片列表中的每一行拆分成图片名和图片标签
        img = misc.imread(name)  # 将图片读取出来,存入一个矩阵
        img = img / 255  # 将图片转换为只有0、1值的矩阵
        img.resize((img.size, 1))  # 为了之后的运算方便,我们将图片存储到一个img.size*1的列向量里面
        data.append(img)
        labels.append(int(label))
    print('write to npy')
    np.save(dst, [data, labels])  # 将训练数据以npy的形式保存到成本地文件
    print('completed')
#数据层,读入数据的操作放到一个数据层
class Data:
    def __init__(self, name, batch_size):  # 数据所在的文件名name和batch中图片的数量batch_size
        with open(name, 'rb') as f:
            data = np.load(f)
        self.x = data[0]  # 输入x
        self.y = data[1]  # 预期正确输出y
        self.l = len(self.x)
        self.batch_size = batch_size
        self.pos = 0  # pos用来记录数据读取的位置
    def forward(self):
        pos = self.pos
        bat = self.batch_size
        l = self.l
        if pos + bat >= l:  # 已经是最后一个batch时,返回剩余的数据,并设置pos为开始位置0
            ret = (self.x[pos:l], self.y[pos:l])
            self.pos = 0
            index = range(l)
            np.random.shuffle(list(index))  # 将训练数据打乱
            self.x = self.x[index]
            self.y = self.y[index]
        else:  # 不是最后一个batch, pos直接加上batch_size
            ret = (self.x[pos:pos + bat], self.y[pos:pos + bat])
            self.pos += self.batch_size
        return ret, self.pos  # 返回的pos为0时代表一个epoch已经结束
    def backward(self, d):  # 数据层无backward操作
        pass
class FullyConnect:
    def __init__(self, l_x, l_y):  # 两个参数分别为输入层的长度和输出层的长度
        #l_x为输入单个数据向量的长度,在这里是17*17=289,l_y代表全连接层输出的节点数量,由于大写英文字母有26个,所以这里的l_y=26
        self.weights = np.random.randn(l_y, l_x) / np.sqrt(l_x)  # 使用随机数初始化参数,请暂时忽略这里为什么多了np.sqrt(l_x)
        self.bias = np.random.randn(l_y, 1)  # 使用随机数初始化参数
        self.lr = 0  # 先将学习速率初始化为0,最后统一设置学习速率
    def forward(self, x):
        self.x = x  # 把中间结果保存下来,以备反向传播时使用
        self.y = np.array([np.dot(self.weights, xx) + self.bias for xx in x])  # 计算全连接层的输出
        return self.y  # 将这一层计算的结果向前传递
    def backward(self, d):
        ddw = [np.dot(dd, xx.T) for dd, xx in zip(d, self.x)]  # 根据链式法则,将反向传递回来的导数值乘以x,得到对参数的梯度
        self.dw = np.sum(ddw, axis=0) / self.x.shape[0]
        self.db = np.sum(d, axis=0) / self.x.shape[0]
        self.dx = np.array([np.dot(self.weights.T, dd) for dd in d])
        # 更新参数
        self.weights -= self.lr * self.dw
        self.bias -= self.lr * self.db
        return self.dx  # 反向传播梯度
#激活函数层
class Sigmoid:
    def __init__(self):  # 无参数,不需初始化
        pass
    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))
    def forward(self, x):
        self.x = x
        self.y = self.sigmoid(x)
        return self.y
    def backward(self, d):
        sig = self.sigmoid(self.x)
        self.dx = d * sig * (1 - sig)
        return self.dx  # 反向传递梯度
#损失函数层
class QuadraticLoss:
    def __init__(self):
        pass
    def forward(self, x, label):
        self.x = x
        self.label = np.zeros_like(x)  # 由于我们的label本身只包含一个数字,我们需要将其转换成和模型输出值尺寸相匹配的向量形式
        for a, b in zip(self.label, label):
            a[b] = 1.0  # 只有正确标签所代表的位置概率为1,其他为0
        self.loss = np.sum(np.square(x - self.label)) / self.x.shape[0] / 2  # 求平均后再除以2是为了表示方便
        return self.loss
    def backward(self):
        self.dx = (self.x - self.label) / self.x.shape[0]  # 2被抵消掉了
        return self.dx
#准确率层
class Accuracy:
    def __init__(self):
        pass
    def forward(self, x, label):  # 只需forward
        self.accuracy = np.sum([np.argmax(xx) == ll for xx, ll in zip(x, label)])  # 对预测正确的实例数求和
        self.accuracy = 1.0 * self.accuracy / x.shape[0]
        return self.accuracy
#构建神经网络
def main():
    datalayer1 = Data('train.npy', 1024)  # 用于训练,batch_size设置为1024
    datalayer2 = Data('validate.npy', 10000)  # 用于验证,所以设置batch_size为10000,一次性计算所有的样例
    inner_layers = []
    inner_layers.append(FullyConnect(17 * 17, 26))
    inner_layers.append(Sigmoid())
    losslayer = QuadraticLoss()
    accuracy = Accuracy()
    for layer in inner_layers:
        layer.lr = 1000.0  # 为所有中间层设置学习速率
    epochs = 20
    for i in range(epochs):
        print('epochs:', i)
        losssum = 0
        iters = 0
        while True:
            data, pos = datalayer1.forward()  # 从数据层取出数据
            x, label = data
            for layer in inner_layers:  # 前向计算
                x = layer.forward(x)
            loss = losslayer.forward(x, label)  # 调用损失层forward函数计算损失函数值
            losssum += loss
            iters += 1
            d = losslayer.backward()  # 调用损失层backward函数层计算将要反向传播的梯度
            for layer in inner_layers[::-1]:  # 反向传播
                d = layer.backward(d)
            if pos == 0:  # 一个epoch完成后进行准确率测试
                data, _ = datalayer2.forward()
                x, label = data
                for layer in inner_layers:
                    x = layer.forward(x)
                accu = accuracy.forward(x, label)  # 调用准确率层forward()函数求出准确率
                print('loss:', losssum / iters)
                print('accuracy:', accu)
                break
if __name__ == '__main__':
    #chuli('train.txt', 'train.npy')
    #chuli('test.txt', 'test.npy')
    #chuli('validate.txt', 'validate.npy')
    main()

数据集下载:

http://labfile.oss.aliyuncs.com/courses/814/data.tar.gz

说明上面代码的大体处理步骤:

1:下载数据集,加载到项目里,然后规范好训练集验证集测试集所用图片都是哪些,从规范好的列表中读取图片,转化为只有0,1值的矩阵,存储到一个img.size*1的列向量,将转化结果输出到文件中。

2:编写数据层处理函数,生成一份用于训练,一份用于验证的数据。

3:将数据传入全连接层,以及激活函数层进行前向计算。

4:将前向计算返回的值代入损失函数层forward函数,计算损失函数值,用损失层backward函数层计算将要反向传播的梯度。

5:用训练好的模型带入验证数据前向计算,完成后数据加入准确率函数中进行计算。

6:查看损失值以及正确率的变化情况。

可以将平方损失函数层替换成交叉熵损失函数层加上隐层等优化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章