从零开始打造并训练神经网络

概述


之前已经讲述了不少理论知识了,现在是时候开始实战了。让我们尝试从零开始打造一个神经网络并训练它,把整个过程串起来。

为了更直观、更容易理解,我们遵循以下原则:

  1. 不使用第三方库,让逻辑更加简单;
  2. 不做性能优化:避免引入额外的概念和技巧,增加复杂度;

数据集


首先,我们需要一个数据集。为了方便可视化,我们使用一个二元函数作为目标函数,然后基于它的采样来生成数据集。
注:实际工程项目中,目标函数是未知的,但是可以基于它进行采样。

虚构目标函数

o(x, y) = \begin{cases} 1 & x^2 + y^2 < 1 \\ 0 & \text{其它}\end{cases}

代码如下:

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)

神经元表达式为:\text{sigmoid}( \mathbf {w} \mathbf x + b)

  • \mathbf {w}:向量,对应代码中的weights数组
  • b: 对应代码中的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)

注:此处使用了一个比较大的学习率,这个跟项目情况有关。在实际项目中的学习率通常都很小

训练后的神经网络函数图像

总结

本次实战的步骤如下:

  1. 构造了一个虚拟的目标函数: o(x, y)
  2. 基于o(x, y)进行抽样,得到数据集,即数据集函数: d(x, y)
  3. 构造了包含一个隐藏层的全连接神经网络,即神经网络函数: f(x, y)
  4. 使用梯度下降法训练神经网络,让f(x, y)近似d(x, y)

其中最复杂的部分在于求梯度,其中用到了反向传播算法。在实际项目中,使用主流的深度学习框架进行开发,可以省掉求梯度的代码,门槛更低。

实验室的"3D分类"实验中,第二个数据集跟本实战非常相似,可以进去实际操作一下。

参考软件

更多内容及可交互版本,请参考App:

神经网络与深度学习

可从App Store, Mac App Store, Google Play下载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容