回顾上一篇:深刻理解机器学习的: 目标函数,损失函数和代价函数
引言
前面我们讲了关于张量、张量运算、激活函数、代价函数相关的一系列文章,本篇将使用Python 3从头实现一个神经网络,用来逼近函数,有理论证明,神经网络可以逼近任何函数。本篇作为例子,我们使用神经网络逼近
f
(
x
)
=
s
i
n
(
x
)
f(x)=sin(x)
f(x)=sin(x)函数,让大家初步领略神经网络的魔力。
为了确保你能正确运行本章代码,确保你有:PyCharm IDE,安装好Python虚拟机,安装好Tensorflow以及相关Python包。
文章目录:
- 引出问题
- 定义神经网络结构
- 产生输入数据以及数据的组织
- 选择和实现激活函数
- 计算前馈网络的输出
- 选择和实现损失函数
- 计算BP网络梯度和更新权重
- 实现训练网络
- 运行自编写Python代码和展示结果
- 用TensorFlow实现以及结果对比
- 拓展本例
- 小结
引出问题
以上两张图,一张是加了噪点点数据集,一张是没有加点,对于神经网络来说,它并不管你是正弦或者余弦,它只知道使用神经网络的方法可以找到规律或者模型,以便你输入新点x值,它可以告诉你对应点y值。
定义神经网络结构
首先面临的第一个问题,使用什么样的神经网络结构?神经网络结构怎么定义?很显然,只需要使用浅层神经网络结构即可,是个简单线性回归问题,这里我们使用两层,一个隐藏层,一个输出层的网络来实现模型的训练,网络示意图如下:
x 1 , x 2 , x 3 x1,x2,x3 x1,x2,x3这层属于输入层; a 1 ~ a 10 a1~a10 a1~a10是隐藏层,图里面只画了6个,实际上有数据点个数相同多个神经元,这层的神经元要使用激活函数; z 1 z1 z1是输出层,1个神经元,这个神经元里不用激活函数,这层的输出就是结果。输入层和隐藏层之间有权重W1和偏置B1,隐藏层与输出层之间有权重W2和和偏置B2。
我们定义一个Python类 ApproachNetwork 来实现,根据网络结构定义,可以在__init__里定义:
def __init__(self, hidden_size=100, output_size=1):
self.params = {'W1': np.random.random((1, hidden_size)),
'B1': np.zeros(hidden_size),
'W2': np.random.random((hidden_size, output_size)),
'B2': np.zeros(output_size)}
产生输入数据以及数据的组织
怎么把数据输入到神经网络?以什么形式呢?这是需要好好思考的问题。
前面在这篇文章了解机器学习(深度学习)的几个特点里说过,输入数据的维度以及含义,这里我们把数据组织形状为 (samples, features)这样的形式, samples代表数据点个数,features代表每个数据点的特征数,在这里因为只有一个自变量,特征数为1,如果我们产生100个数据点,那么数据的形状是(100,1), 我们可以一次行把这个矩阵数据输入神经网络,即上面的输入层神经元个数为100.
为了产生这样的训练数据,可以利用文章np.random用法里面的Numpy方法,在类定义里增加产生数据的函数generate_data,函数的参数含义代码里有解释,这个函数具有一定的通用性,可以为不同函数产生训练数据集:
@staticmethod
def generate_data(fun, is_noise=True, axis=np.array([-1, 1, 100])):
"""
产生数据集
:param fun: 这个是你希望逼近的函数功能定义,在外面定义一个函数功能方法,把功能方法名传入即可
:param is_noise: 是否需要加上噪点,True是加,False表示不加
:param axis: 这个是产生数据的起点,终点,以及产生多少个数据
:return: 返回数据的x, y
"""
np.random.seed(0)
x = np.linspace(axis[0], axis[1], axis[2])[:, np.newaxis]
x_size = x.size
y = np.zeros((x_size, 1))
if is_noise:
noise = np.random.normal(0, 0.1, x_size)
else:
noise = None
for i in range(x_size):
if is_noise:
y[i] = fun(x[i]) + noise[i]
else:
y[i] = fun(x[i])
return x, y
对于函数 f ( x ) = x 2 f(x)=x^2 f(x)=x2 调用上面的方法产生数据并把它显示出来,如下:
# 逼近函数 f(x)=sin(x)
def fun_sin(x0):
return math.sin(x0)
x, y = network.generate_data(fun_sin, False, axis=np.array([-3, 3, 100]))
ax = plt.gca()
ax.set_title('data points')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.scatter(x, y)
plt.show()
图形如下:
选择和实现激活函数
激活函数选择 s i g m o i d sigmoid sigmoid或 r e l u relu relu都可以,这里选择 s i g m o i d sigmoid sigmoid,Python实现如下,注意还需要实现其导数以便在BP反向传播时使用,导数推导过程这里不讲,自行百度:
@staticmethod
def sigmoid(x_):
return 1 / (1 + np.exp(-x_))
def sigmoid_grad(self, x_):
return (1.0 - self.sigmoid(x_)) * self.sigmoid(x_)
计算前馈网络的输出
从输入到输出结果计算,隐藏层用激活函数 s i g m o i d sigmoid sigmoid,输出层不用激活函数,用Python实现predict方法,如下:
def predict(self, x_):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['B1'], self.params['B2']
a1 = np.dot(x_, W1) + b1
z1 = self.sigmoid(a1)
a2 = np.dot(z1, W2) + b2
return a2
选择和实现损失函数
对于本例,因为是对比输出值的偏差,所以选择均方差MSE作为损失函数或者叫优化函数,定义 l o s s loss loss函数定义实现。
def loss(self, x_, t):
y_ = self.predict(x_)
return y_, np.mean((t - y_) ** 2)
计算BP网络梯度和更新权重
神经网络是根据计算出来的 l o s s loss loss不断调整权重和偏置,以便不断减少 l o s s loss loss,达到逼近函数的目的。Python实现代码如下:
def gradient(self, x, t):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['B1'], self.params['B2']
grads = {}
batch_num = x.shape[0]
# forward
a1 = np.dot(x, W1) + b1
z1 = self.sigmoid(a1)
a2 = np.dot(z1, W2) + b2
# backward
dy = (a2 - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['B2'] = np.sum(dy, axis=0)
dz1 = np.dot(dy, W2.T)
da1 = self.sigmoid_grad(a1) * dz1
grads['W1'] = np.dot(x.T, da1)
grads['B1'] = np.sum(da1, axis=0)
return grads
# 根据上述计算的梯度,结合学习率,更新权重和偏置
for key in ('W1', 'B1', 'W2', 'B2'):
self.params[key] -= self.learning_rate * grad[key]
实现训练网络
有了上面的步骤,就可以定义训练主函数,主要代码:
def train_with_own(self, x_, y_, max_steps=100):
for k in range(max_steps):
grad = self.gradient(x_, y_)
for key in ('W1', 'B1', 'W2', 'B2'):
self.params[key] -= self.learning_rate * grad[key]
pred, loss = network.loss(x_, y_)
if k % 150 == 0:
# 动态绘制结果图,你可以看到训练过程如何慢慢的拟合数据点
plt.cla()
plt.scatter(x, y)
plt.plot(x, pred, 'r-', lw=5)
plt.text(0.5, 0, 'Loss=%.4f' % abs(loss), fontdict={'size': 20, 'color': 'red'})
plt.pause(0.1)
# 关闭动态绘制模式
plt.ioff()
plt.show()
运行自编写Python代码和展示结果
编写运行主函数,其中实例化类ApproachNetwork,调用产生数据和训练方法即可完成。
if __name__ == '__main__':
network = ApproachNetwork()
x, y = network.generate_data(fun_sin, False, axis=np.array([-3, 3, 100]))
# x, y = network.generate_data(fun_xx)
# 使用 自编代码 训练
network.train_with_own(x, y, 3500)
结果展示
以下图形是在设定最大循环步骤为3500,学习率为0.05下输出的,本例还采用了动态输出图形的办法,每150步,刷新输出当前图形,你可以直观看到训练过程中函数逼近的过程,本例是在PyCharm中开发、测试和运行。
用TensorFlow实现以及结果对比
函数逼近的问题,也可以使用Tensorflow这种深度学习框架来实现,在本例中在ApproachNetwork类中定义一个方法 train_with_tf,代码如下,解释在代码中
@staticmethod
def train_with_tf(x, y):
tf_x = tf.placeholder(tf.float32, x.shape) # input x
tf_y = tf.placeholder(tf.float32, y.shape) # input y
# 神经网络定义, 激活函数使用 ReLU 或 sigmoid 都可以
# l1 = tf.layers.dense(tf_x, 10, tf.nn.relu) # hidden layer
l1 = tf.layers.dense(tf_x, 10, tf.nn.sigmoid) # hidden layer
output = tf.layers.dense(l1, 1) # output layer
# 损失函数和优化器设置
loss = tf.losses.mean_squared_error(tf_y, output) # compute cost
train_op = tf.train.GradientDescentOptimizer(learning_rate=0.5).minimize(loss)
# 初始化
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# 设置动态绘制,不是训练完了才绘制结果
plt.ion()
for step in range(1500):
_, l, pred = sess.run([train_op, loss, output], {tf_x: x, tf_y: y})
if step % 100 == 0:
# 动态绘制结果图,你可以看到训练过程如何慢慢的拟合数据点
plt.cla()
plt.scatter(x, y)
plt.plot(x, pred, 'r-', lw=5)
plt.text(0.5, 0, 'Loss=%.4f' % l, fontdict={'size': 20, 'color': 'red'})
plt.pause(0.1)
# 关闭动态绘制模式
plt.ioff()
plt.show()
可以看出,使用Tensorflow框架代码简洁,步骤清楚。但是自己用Python动手开发神经网络能使你清楚各个环节,对你提高能力和水平是大有裨益的。
在同样的条件设置下,逼近的结果如下:
比较起来,自己编写代码精度似乎还要高些!Cheers!
扩展本例
这个类不仅可以逼近正弦函数,还可以逼近你喜欢试的函数,例如我想让它逼近 f ( x ) = x 2 f(x) = x^2 f(x)=x2,先定义该函数,然后在生成数据参数中传递函数名进去,第三个参数根据需要修改一下,保证你的数据集能够完整的表现改函数的形状特征。在训练过程中,其它参数可以适当调整以下,如学习率,最大迭代次数等,这种可以把函数作为参数传递的做法,也是Python作为动态语言的一个优势所在,自己可以去体会:
# 逼近函数 f(x)=x**2
def fun_xx(x0):
return np.power(x0, 2)
x, y = network.generate_data(fun_xx)
小结
读者可能的疑惑是,BP反馈网络计算梯度不太明白,继续阅读 用计算图理解和计算BP神经网络的梯度。
参考文献
利用BP神经网络逼近函数——Python实现
机器学习与神经网络(四):BP神经网络的介绍和Python代码实现
一个 11 行 Python 代码实现的神经网络