一、神经网络的基本含义
根据蔡自兴所著的《人工智能原理及应用》的定义:人工神经网络是由大量的人工神经元互相连接,模拟人脑神经系统的结构和功能。
主要分为三层:输入层、隐藏层和输出层。
输入层是输入我们的样本数据,通过隐藏层的计算得到输出层的结果。
其中有几个很重要的函数需要了解:
(1)激活函数:
由于我们进行学习时很多场景并不一定是线性关系,很多是非线性的关系。但是我们人工神经网络利用的还是线性回归的思想。
比如:y = w1 * x1 + w2 *x2 + w3 * x3 。 y与x1、x2、x3三个自变量有关系,每个自变量对y的影响程度是由w1,w2,w3决定的。但是这个模型明显是一个线性的结构。但是很多场景并不是线性的,因此需要一个函数 p = f(y),将y这样一个线性的结果加入非线性的因素。
因此,这个函数就是激活函数。
常见的激活函数有:sigmod 函数:f(x) = 1 / (1 + e^-x) 这个函数主要用于二分类问题。
softmax函数:如果是多分类问题可以用softmax函数。
还有tanh、ReLU等激活函数。可以参考https://blog.csdn.net/kangyi411/article/details/78969642
(2)损失函数
损失函数(loss function)也叫代价函数(cost function),是神经网络优化的目标函数。
一定要注意这句话:神经网络训练或者优化的过程就是最小化损失函数的过程(损失函数值小了,对应预测的结果和真实结果的值就越接近)
其实损失函数就是对学习过程的一个评价。每次学习得到一个[w1, w2, w3],在该[w1, w2, w3]下的损失值越大其实离真实结果就越远。
因此,梯度下降法其实是对损失函数进行梯度下降的。
二、案例
比如我们现在有一个图片数据集,这些图片有三个类别,分别是猫、狗、鸡。我们使用独热码对猫、狗、鸡进行编码:比如猫就是(1,0,0),狗是(0,1,0),鸡是(0,0,1)
假如每个图片数据都是一个1行2列的结构(其实在实际的图片识别中并不会有这么简单的结构,比如28*28,28行28列一共有784个子数据)。假设有张图片X,肉眼判断是一只鸡,X = [0.6, 0.9](1行2列) 我们想通过输入一个X=[0.6, 0.9],得到一个输出,即y = [0,0,1](X是一只鸡),该怎么办呢?我们用梯度下降法来进行学习。
(1)先设置好X和y的数据,如下。
import numpy as np
X = np.array([[0.6, 0.9]]) #这个是输入层
y = np.array([0, 0, 1]) #这个是目标输出
(2)写个神经网络
接下来我们先写一个简单的神经网络,只有输入层和输出层,都没有隐藏层,主要是为了理解神经网络的基本工作原理。
具体每个代码的含义我都写在了注释上,可以看注释进行理解:
# Sigmoid 二分类激活函数
# Softmax 多分类激活函数
def _softmax(x):
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)
# 损失函数:指导参数进行调整的方向性指导
def cross_entropy_error(p, y):
delta = 1e-7 # 为了防止log(p)中的p为0导致函数报错,因此加了一个delta,这是一个很小的数
return np.sum(-y * np.log(p + delta))
# 这是一个简单的神经网络
class simpleNet:
def __init__(self):
np.random.seed(0)
# 定义权值
self.W = np.random.randn(2, 3)
# 设置初始的权值,由于我们输入一个1行2列的数据,想得到一个1行3列的数据结果,因此要生成1个2行3列的权值矩阵
def forward(self, x):
# 这是在做点乘,将输入的x与权值W进行点乘得到一个1行3列的输入值。
return np.dot(x, self.W)
def loss(self, x, y):
z = self.forward(x)
# 由于forward是点乘,是一个线性模型。需要加入激活函数来增加非线性的因素
# 由于本场景是一个多分类问题,采用了softmax函数作为激活函数
p = _softmax(z)
# 这是损失函数
loss = cross_entropy_error(p, y)
return loss
重点说一下cross_entropy_error(p,y)函数:这是损失函数,采用了交叉熵损失函数
常见的损失函数可以参考:https://zhuanlan.zhihu.com/p/58883095
损失函数用来评价模型的预测值和真实值不一样的程度,因此cross_entropy_error(p,y) 这个函数中的p 其实就是神经网络的输出值,y是正确答案,使用cross_entropy_error(交叉熵损失函数)来判断一下输出值与正确答案之间的差距。
(3)用梯度下降法来找到最合适的权值W
这里我们采用数值微分的梯度下降法来进行说明。还是先看代码。对着代码我们讲解思路。
def numerical_gradient(f, x):
h = 1e-4 #0.0001
grad = np.zeros_like(x) # 生成一个与X形状一样的 0 矩阵
# nditer 对象有另一个可选参数 op_flags。
# 默认情况下,nditer 将视待迭代遍历的数组为只读对象(read-only)
# 为了在遍历数组的同时,实现对数组元素值得修改,必须指定 read-write 或者 write-only 的模式。
# https://www.runoob.com/numpy/numpy-terating-over-array.html
it = np.nditer(x, flags = ['multi_index'], op_flags = ['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x)
x[idx] = float(tmp_val) - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = tmp_val
it.iternext()
return grad
第一个:numerical_gradient(f, x),需要传入两个参数,分别是f 和 x。其中 f 是损失函数, x为权重(2行3列)。
为了让一些初学者理解梯度下降就简单说一下梯度下降的原理哈
首先,我们的目标是什么?是不是希望我们的模型足够强大,分类识别率最高。那么就需要输出值与正确答案之间的差距越小越好,因此损失函数的值越小越好。为了让我们的损失函数值能尽快到最小,我们的学习网络要选择下降速度最快的方向进行学习。
那我们来解释一个numerical_gradient(f, x)函数:我们的思路就是要求出每个方向的梯度,也就是导数,才能知道在这个点到底哪个方向的下降速度最快
h = 1e-4 就是一个很小很小的值。
我们使用一个while循环来求x的梯度:其实就是一个求导的过程。大家看代码应该能看懂的。
grad就是存储导数的,它也是一个2行3列的形状。
def gradient_descent(f, init_x, lr=0.01, step_num=10000):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
第二个:gradient_descent(f, init_x, lr=0.01, step_num=10000) f就是损失函数,init_x就是初始的权值,lr是学习效率,stem_num是梯度下降的次数。
grad = numerical_gradient(f, x) 第一次循环的时候 传入损失函数f 和初始权值init_x
由于numerical_gradient函数返回的就是梯度。
更新权重 x = x - lr * grad。这个方法就是将x进行更新,再进入循环。
重复step_num次,最终得到一个权重x
三、进行学习
net = simpleNet()
f = lambda w : net.loss(X, y)
# 等价于:
# def f(w):
# return net.loss(X, y)
print(net.W)
dw = gradient_descent(f, net.W)
print(dw)
print(net.W)
Y_predict = net.forward(X)
print(Y_predict)
print(_softmax(Y_predict))
print(net.loss(X, y))
net 就是我们的简单神经网络。定义个f为损失函数,就是net.loss函数,参数是输入层X和正确答案y
dw = gradient_descent(f, net.W) 输入损失函数f和初始权值,在这个初始权值下,计算X得到预测值y_predict,通过y_predict 与正确答案y的比较得到损失值。 dw就是经过10000次梯度下降后的权重值
不断降低损失值,得到新的w,重复step_num次,得到的最终w。
step_num可以自己设置。
四、运行结果
我们先看看初始的net.W和学习完后的dw和net.W的值
这是初始的net.W
[[ 1.76405235 0.40015721 0.97873798]
[ 2.2408932 1.86755799 -0.97727788]]
这是dw
[[ 0.01517964 -0.79217197 3.91993987]
[-0.38241586 0.07906423 3.43452494]]
这是学习后的net.W
[[ 0.01517964 -0.79217197 3.91993987]
[-0.38241586 0.07906423 3.43452494]]
可以看出dw和net.W是相等的。
我们使用初始的net.W来学习X,我们看看Y_predict是多少。与正确答案(0,0,1)差多少
net = simpleNet()
f = lambda w : net.loss(X, y)
# 等价于:
# def f(w):
# return net.loss(X, y)
print(net.W)
Y_predict = net.forward(X)
print(Y_predict)
print(_softmax(Y_predict))
print(net.loss(X, y))
运行结果:
[[ 3.07523529 1.92089652 -0.2923073 ]] 这是预测值Y_predict
[[0.74088333 0.23357527 0.0255414 ]] 用激活函数softmax后可以看出,明显第一个概率高,因此这个结果是猫(1,0,0)。但是实际是鸡。分类错误
3.6674507891066104 损失值较大,达到了3.66
加入梯度下降法后计算出来的新的W,对X进行预测我们看一下结果:
[[-0.33506649 -0.40414538 5.44303637]] 这是预测值Y_predict
[[0.00307618 0.00287085 0.99405297]] 用激活函数softmax后可以看出,第三个概率极高,第一个第二个概率及较低,因此这个结果是鸡(0,0,1)
0.005964682257980716 损失值很小
上述结果是step_num 为10000时的结果。如果把step_num 改成3000是什么结果呢?
[[0.11696196 0.01044543 4.57641712]]
[[0.01131998 0.01017621 0.9785038 ]]
0.021730502167228764
分类虽然也正确,但是概率较10000次还是低了一点。但是也完全可以接受。