先阐述一些概念性的东西(也是对之前的东西的回顾,记性不好,老忘):
回归问题与分类问题:
回归:计算圆形面积的例子就属于回归
问题,即我们的目的是对于一个输入x
,预测其输出值y
,且这个y
值是根据x
连续变化的值。
分类:分类
问题则是事先给定若干个类别,对于一个输入x
,判断其属于哪个类别,即输出一般是离散的
监督学习和无监督学习:
监督学习:通过训练让机器自己找到特征和标签之间的联系(注:也就是学习的训练集包含输入和输出,得到了最优参数模型之后 ,新来的数据集在面对只有特征没有标签的情况下时,可以判断出标签)
无监督学习:训练数据中只有特征没有标签,输入数据没有被标记,也没有确定的结果。样本数据类别未知,需要根据样本间的相似性对样本集进行分类。(注:不一定"分类",没有训练集,旨在寻找规律性,不予以某种预先分类标签对上号为目的)
损失函数,二元损失函数:
quadratic loss function
通过计算h
和y
之间差值的二次方,来表达一个神经网络的效果。具体形式如下:
J(Θ)=(h(Θ,X)−Y)2J(Θ)=(h(Θ,X)−Y)2
这个公式几乎就是我们在上面提到的对模型的误差求平方,只是稍微再复杂一点。其中的希腊字母theta
代表网络中的所有参数,X
代表向神经网络输入的数据,Y
代表输入数据对应的预期正确输出值。
实际上,在这里theta
才是自变量,因为我们一开始不知道让网络能够正确的工作的参数值是多少。我们的学习算法learn
按照某种策略,通过不断的更新参数值来使损失函数J(theta, X, Y)
的值减小。在这里,theta
是不断变化的量。那X
和Y
就不是自变量了吗?对,它们确实不是自变量!因为对于我们要解决的一个具体的问题(比如图片字母识别),其训练数据中包含的信息其实代表的是这个问题本身的性质,我们的学习算法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:查看损失值以及正确率的变化情况。
可以将平方损失函数层替换成交叉熵损失函数层加上隐层等优化。