很多介绍TF开发的书籍中都喜欢用逻辑回归拟合线性二维数据来开始介绍TF的开发过程,按照数据准备,模型搭建,反向损失函数定义和训练模型,使用模型的的顺序来介绍,并给出代码。但TF框架本身隐藏了其中的大部分流程,只暴露了一小部分参数供给用户程序调节,导致学习者知其然但不知道其所以然,可能一行剪短的tf函数调用,隐藏了大部分的实现细节。根据网上学习的资料,结合自己摸索,这里给出一种不依赖tensorflow用单神经元实现一次函数拟合的例子,也是用python实现。这样做一方面是因为自己也是个新手,写出来的过程本身就是学习的过程,可以加深印象,另一方面,如果当中有不对的地方,也好让别人指点,纠正。
单个神经元工作模型:
对于符合形如
此类线性函数分布的数据,理论上单个神经元即可实现对其进行拟合. 单神经元的工作模型如下:
网络完成的功能简要介绍如下,x作为样本输入端,w是权重,中间方形框表示x,w之间的操作是乘法操作,然后结果在加上后续的b,得到的结果y在进行一个叫做activate function函数的变换,得到最终输出o. l上述计算过程用方程表达如下:
在神经网络领域,专业的叫法称呼b为偏置,w为权重,f为激活函数(activate function),也叫非线性单元
拟合的过程不过就是通过不断的输入样本,根据输出值和预期值的误差变化趋势,不断调整权重w和偏置b,得到适合样本数据的最佳权重和置换值,这个过程就是训练的过程。
另外插一句题外话,我学的是自动化,自动化专业接触最多的一个词就是”负反馈",上面介绍的神经网络工作过程,实际上也是用了反馈的原理,将预期值和实际值之间的误差作为控制信号源,通过网络”反馈“给前面的神经元节点,产生控制w和b变化的控制信号,从而使输出和预期值拟合的过程。常用的BP神经网络,BP(back propagate,负向传播)也和负反馈的原理神合,负反馈对应的控制变量变成了负梯度。
下面根据以上网络模型,给出带噪声的符合y=6x规律的数据拟合实现。
数据准备:
下面的代码产生一组介于区间[-1,1]之间的100个样点,并将每个样点值乘以6之后加上随机噪声进行输出。
import numpy as np
import matplotlib.pyplot as plt
traing_x=np.linspace(-1, 1, 100)
traing_y=6*traing_x + np.random.randn(*traing_x.shape) * 0.3
plt.plot(traing_x,traing_y,'ro', label='Original data')
plt.legend()
plt.show()
数据图像为:
根据数据图像分布来看,总体上符合
的分布规律,噪声影响导致数据点在距离直线不远的范围浮动变化。
选择激活函数:
选择比较常用的sigmoid函数作为激活函数,其图像为:
选择将其作为激活函数的另一个原因是, 从数据分布图上可见,在区间 x的取值区间[-1,1],y 的取值范围为[-6,6], sigmoid函数在[-6,6]区间上具备比较良好的区分度。
将函数复核后,上述神经网络的传递方程式:
图像变成如下所示:
网络traing是根据负梯度进行的,负梯度的计算过程是损失函数对权重和偏置的偏导数,根据链式求导法则,对sigmoid函数求导是链式法则的中间一环,下面是计算sigmoid导数的过程
所以,sigmoid函数的导数为:
选择损失函数:
这里选择均值平方差(Mean Squared Error, MSE),也叫做”均方误差" 作为训练结果的评价指标,也就是loss函数,它主要表达的是预测值和真实值之间的差异,在数理统计中,均方误差是指参数估计值与参数真实值之差平方的期望值,它的定义如下:
其中预测值和真实值是经过sigmoid函数进行映射之后的输出。
以单次训练为例,本例中,每一期训练的每单笔数据均为一个样本:
训练优化的目的就是不断减少损失函数的值,我们知道,改变网络的权重和偏置可以影响预测值,但我们应该怎么做才能使结果朝着损失函数减少的方向优化呢?这里需要高等数学中的一些概念。
observde是样本真值,是属于模型外部输入的常数,不用对其求导,所以,对w, b分别求偏导为:
模型搭建:
定义激活函数, 损失函数,sigmoid求导函数等子程序:
# Sigmoid activation function: f(x) = 1 / (1 + e^(-x))
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# Derivative of sigmoid: f'(x) = f(x) * (1 - f(x))
def derivative_sigmoid(x):
f = sigmoid(x)
return f * (1 - f)
def mse_loss(expect, actual):
return ((expect - actual) ** 2).mean()
然后定义神经元,其中导数按照上面给出的公式计算,但是,根据偏导数更新新的w,b值呢? 这里用上了负梯度的概念。
根据上面的公式,损失函数的梯度为:
至于为什么选择负梯度,你可以这样想:
其中
为梯度下降算法的下降变化率,用来控制梯度算法的收敛速度的,在机器学习中有一个专门的名字,叫做学习率,可以取0~1之间的数字,当得到在局部最优的偏置和权重后,梯度向量变为0,递推公式得到稳定输出,此时
梯度前加一个负号,就意味着朝着梯度相反的方向前进!梯度的方向实际就是函数在此点上升最快的方向,而我们需要朝着下降最快的方向走,自然就是负的梯度的方向,所以此处需要加上负号;那么如果时上坡,也就是梯度上升算法,当然就不需要添加负号了,这也梯度下降方法,下降一词的由来。
算法达到最优解.
class one_neural_network:
def __init__(self):
# Weights
self.w = np.random.normal()
# Biases
self.b = np.random.normal()
def feedforward(self, x):
# x is a numpy array with 2 elements.
o= sigmoid(self.w * x + self.b)
return o
def train(self, input, expect):
learn_rate = 0.1
epochs = 600 # number of times to loop through the entire dataset
datasets = traing_x.shape[0];
result = np.zeros(epochs)
for epoch in range(epochs):
for counter in range(datasets):
x=traing_x[counter]
y=traing_y[counter]
o1 = self.w * x + self.b
o2 = sigmoid(o1)
exp = sigmoid(y)
d_L_d_input = -2 * (exp - o2)
d_input_d_w = x * derivative_sigmoid(o1)
d_input_d_b = derivative_sigmoid(o1)
self.w -= learn_rate * d_L_d_input * d_input_d_w
self.b -= learn_rate * d_L_d_input * d_input_d_b
y_predict = np.apply_along_axis(self.feedforward, 0, traing_x)
loss = mse_loss(sigmoid(traing_y), y_predict)
print("Epoch %d loss: %.3f" % (epoch, loss))
result[epoch] = loss;
print(self.w)
print(self.b)
plt.plot(result)
plt.grid(True)
plt.axis('tight')
#plt.ylim(0,0.1)
plt.show()
编写应用代码:
应用代码其实非常简单,都是对上面已经封装好的函数的直接调用,把模型数据喂给这些函数就好了.
# train our neural network!
network = one_neural_network()
network.train(traing_x, traing_y)
执行训练:
学习率是超参数,它并非由训练得到,而是由部署者的经验决定,程序中设置学习率为0.1,训练600轮,经过600轮的训练后,得到的结果如下:
Epoch 595 loss: 0.001
Epoch 596 loss: 0.001
Epoch 597 loss: 0.001
Epoch 598 loss: 0.001
Epoch 599 loss: 0.001
6.02222669476
0.0489193518788
损失曲线:
由数据可见,经过600轮训练后,loss平均值稳定再0.001,从损失曲线也可以明显看出,随着训练进行,MSE损失越来越小。
最终,训练出来的w=6.02222669476,b=0.0489193518788非常接近真实值6和0.
进一步,修改模型函数,调整初始数据的w和b值,看上述代码能否追踪新的变化,让b=4, w=2,调整训练1500轮。
traing_y=2*traing_x + np.random.randn(*traing_x.shape) * 0.3+4
损失函数变化图:
Epoch 1496 loss: 0.000
Epoch 1497 loss: 0.000
Epoch 1498 loss: 0.000
Epoch 1499 loss: 0.000
1.93756984637
3.91283159652
值得注意的是学习率的值对训练效果影响很大,不合适的学习率甚至有可能导致损失函数不收敛,在合适的学习率下,损失函数每一步迭代都会有所减小,表现在图像中就是损失函数随着迭代次数增加,呈现单调递减的形态。
数学家已经证明,只要学习率设置的足够小,损失函数一定会收敛,只是收敛的速度有快有慢,所以如果遇到不收敛的情况,最大可能是学习率设置的太大了。
可以看到w收敛到1.93756984637,b收敛到3.91283159652,非常接近2和4,loss值已经接近0,误差基本检测不到了,训练中由于每个训练样本执行完后都会更新权重和偏置,所以batch size为1,也可以设置其它的batch size,等多笔样本训练完成后,再对每个训练样本产生的梯度求和,算出总梯度更新权重和偏置。
以上就是TF框架中关于回归训练的原理展开,虽然不一定相同,但思想应该是一致的。
附图
执行完学习,就该进行推理了,输入一个新的x值,它会给你一个最符合实际的输出。全连接神经网络是一个通用的近似框架,只要有足够多的神经元,即使只有一层隐藏层的神经网络,利用常用的sigmoid,relu等激活函数,就可以无限逼近任何连续函数。在实际中,如果想使用浅层神经网络来拟合复杂非线性函数,就需要靠增加神经元的个数来实现,神经元过多意味着需要训练的参数过多,这会增加网络的学习难度,并影响网络的泛化能力,因此,在搭建网络结构是,一般倾向于使用更深的模型,来减少网络中需要的神经元数量,使网络有更好的泛化能力。
完整代码:
# -*- coding: utf-8 -*-
"""
Created on Tue Nov 3 21:52:30 2020
@author: caozilong
"""
import numpy as np
import matplotlib.pyplot as plt
saver=tf.train.Saver()
traing_x=np.linspace(-1, 1, 100)
traing_y=2*traing_x + np.random.randn(*traing_x.shape) * 0.3+4
plt.plot(traing_x,traing_y,'ro', label='Original data')
plt.legend()
plt.show()
# Sigmoid activation function: f(x) = 1 / (1 + e^(-x))
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# Derivative of sigmoid: f'(x) = f(x) * (1 - f(x))
def derivative_sigmoid(x):
f = sigmoid(x)
return f * (1 - f)
def mse_loss(expect, actual):
return ((expect - actual) ** 2).mean()
'''
A neural network with:
- 1 input
- no hidden layer
- 1 output
*** DISCLAIMER ***:
The code below is intended to be simple and educational, NOT optimal.
Real neural net code looks nothing like this. DO NOT use this code.
Instead, read/run it to understand how this specific network works.
'''
class one_neural_network:
def __init__(self):
# Weights
self.w = np.random.normal()
# Biases
self.b = np.random.normal()
def feedforward(self, x):
# x is a numpy array with 2 elements.
o= sigmoid(self.w * x + self.b)
return o
def train(self, input, expect):
learn_rate = 0.1
epochs = 1500 # number of times to loop through the entire dataset
datasets = traing_x.shape[0];
result = np.zeros(epochs)
for epoch in range(epochs):
for counter in range(datasets):
x=traing_x[counter]
y=traing_y[counter]
o1 = self.w * x + self.b
o2 = sigmoid(o1)
exp = sigmoid(y)
d_L_d_input = -2 * (exp - o2)
d_input_d_w = x * derivative_sigmoid(o1)
d_input_d_b = derivative_sigmoid(o1)
self.w -= learn_rate * d_L_d_input * d_input_d_w
self.b -= learn_rate * d_L_d_input * d_input_d_b
y_predict = np.apply_along_axis(self.feedforward, 0, traing_x)
loss = mse_loss(sigmoid(traing_y), y_predict)
print("Epoch %d loss: %.3f" % (epoch, loss))
result[epoch] = loss;
print(self.w)
print(self.b)
plt.plot(result)
plt.grid(True)
plt.axis('tight')
#plt.ylim(0,0.1)
plt.show()
# Train our neural network!
network = one_neural_network()
network.train(traing_x, traing_y)