概述
之前已经讲述了不少理论知识了,现在是时候开始实战了。让我们尝试从零开始打造一个神经网络并训练它,把整个过程串起来。
为了更直观、更容易理解,我们遵循以下原则:
- 不使用第三方库,让逻辑更加简单;
- 不做性能优化:避免引入额外的概念和技巧,增加复杂度;
数据集
首先,我们需要一个数据集。为了方便可视化,我们使用一个二元函数作为目标函数,然后基于它的采样来生成数据集。
注:实际工程项目中,目标函数是未知的,但是可以基于它进行采样。
虚构目标函数
代码如下:
def o(x, y):
return 1.0 if x*x + y*y < 1 else 0.0
生成数据集
sample_density = 10
xs = [
[-2.0 + 4 * x/sample_density, -2.0 + 4 * y/sample_density]
for x in range(sample_density+1)
for y in range(sample_density+1)
]
dataset = [
(x, y, o(x, y))
for x, y in xs
]
生成的数据为:[[-2.0, -2.0, 0.0], [-2.0, -1.6, 0.0], ...]
图像如下:
构造神经网络
激活函数
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
神经元
from random import seed, random
seed(0)
class Neuron:
def __init__(self, num_inputs):
self.weights = [random()-0.5 for _ in range(num_inputs)]
self.bias = 0.0
def forward(self, inputs):
# z = wx + b
z = sum([
i * w
for i, w in zip(inputs, self.weights)
]) + self.bias
return sigmoid(z)
神经元表达式为:
- :向量,对应代码中的weights数组
- : 对应代码中的bias
注:神经元中的参数都是随机初始化的。但是为了确保可复现实验,一般都固定一个随机种子(seed(0))
神经网络
class MyNet:
def __init__(self, num_inputs, hidden_shapes):
layer_shapes = hidden_shapes + [1]
input_shapes = [num_inputs] + hidden_shapes
self.layers = [
[
Neuron(pre_layer_size)
for _ in range(layer_size)
]
for layer_size, pre_layer_size in zip(layer_shapes, input_shapes)
]
def forward(self, inputs):
for layer in self.layers:
inputs = [
neuron.forward(inputs)
for neuron in layer
]
# return the output of the last neuron
return inputs[0]
构造一个如下神经网络:
net = MyNet(2, [4])
到这里,我们就得到了一个神经网络(net),可以调用其代表的神经网络函数:
print(net.forward([0, 0]))
得到函数值0.55...,此时的神经网络的是一个未经训练的网络。
训练神经网络
损失函数
首先定义一个损失函数:
def square_loss(predict, target):
return (predict-target)**2
计算梯度
梯度的计算是比较复杂的,特别是对于深层神经网络。反向传播算法是一个专门为计算神经网络梯度而生的算法。
由于其比较复杂,这里不展开描述,感兴趣的可以参考下面详细代码。而且现在的深度学习框架中都有自动求梯度的功能。
定义导函数:
def sigmoid_derivative(x):
_output = sigmoid(x)
return _output * (1 - _output)
def square_loss_derivative(predict, target):
return 2 * (predict-target)
求偏导数(在forward函数中缓存了部分数据,以方便求导):
class Neuron:
...
def forward(self, inputs):
self.inputs_cache = inputs
# z = wx + b
self.z_cache = sum([
i * w
for i, w in zip(inputs, self.weights)
]) + self.bias
return sigmoid(self.z_cache)
def zero_grad(self):
self.d_weights = [0.0 for w in self.weights]
self.d_bias = 0.0
def backward(self, d_a):
d_loss_z = d_a * sigmoid_derivative(self.z_cache)
self.d_bias += d_loss_z
for i in range(len(self.inputs_cache)):
self.d_weights[i] += d_loss_z * self.inputs_cache[i]
return [d_loss_z * w for w in self.weights]
class MyNet:
...
def zero_grad(self):
for layer in self.layers:
for neuron in layer:
neuron.zero_grad()
def backward(self, d_loss):
d_as = [d_loss]
for layer in reversed(self.layers):
da_list = [
neuron.backward(d_a)
for neuron, d_a in zip(layer, d_as)
]
d_as = [sum(da) for da in zip(*da_list)]
- 偏导数分别存储于d_weights和d_bias中
- zero_grad函数用于清空梯度,包括各个偏导数
- backward函数用于计算偏导数,并将其值累加存储
更新参数
使用梯度下降法更新参数:
class Neuron:
...
def update_params(self, learning_rate):
self.bias -= learning_rate * self.d_bias
for i in range(len(self.weights)):
self.weights[i] -= learning_rate * self.d_weights[i]
class MyNet:
...
def update_params(self, learning_rate):
for layer in self.layers:
for neuron in layer:
neuron.update_params(learning_rate)
执行训练
def one_step(learning_rate):
net.zero_grad()
loss = 0.0
num_samples = len(dataset)
for x, y, z in dataset:
predict = net.forward([x, y])
loss += square_loss(predict, z)
net.backward(square_loss_derivative(predict, z) / num_samples)
net.update_params(learning_rate)
return loss / num_samples
def train(epoch, learning_rate):
for i in range(epoch):
loss = one_step(learning_rate)
if i == 0 or (i+1) % 100 == 0:
print(f"{i+1} {loss:.4f}")
训练2000步:
train(2000, learning_rate=10)
注:此处使用了一个比较大的学习率,这个跟项目情况有关。在实际项目中的学习率通常都很小
总结
本次实战的步骤如下:
- 构造了一个虚拟的目标函数: ;
- 基于进行抽样,得到数据集,即数据集函数:
- 构造了包含一个隐藏层的全连接神经网络,即神经网络函数:
- 使用梯度下降法训练神经网络,让近似
其中最复杂的部分在于求梯度,其中用到了反向传播算法。在实际项目中,使用主流的深度学习框架进行开发,可以省掉求梯度的代码,门槛更低。
实验室的"3D分类"实验中,第二个数据集跟本实战非常相似,可以进去实际操作一下。
参考软件
更多内容及可交互版本,请参考App:
可从App Store, Mac App Store, Google Play下载。