Thinking with FunctionInferer [0] – 从零开始的机器学习

/ 0评 / 0

一起探索新的编程范式。

机器学习一直是一个蛮热门的话题——尤其是随着 GPT 的大规模可用,让「机器学习」这个概念出现在了几乎所有(非计算机从业者的)人的视野。机器学习在我读大学的时候其实身边就已经有不少的人在搞了,但是当时的我限于(néng)力不足,所以不能去凑这个热闹。现在,随着业界的发展和积淀,我们逐渐有了一些标准的框架和工具用于辅助我们设计针对现实世界问题的解决方案。我的机器也有了足够的算力支持我做一些有趣的事情。

在之后的内容中,我们将从一个传统软件开发的角度去理解机器学习:它的理念、它的范式,以及它底部的数理逻辑。我们会尽可能地使用一些有趣的例子以及容易复现的逻辑来进行解决和演示。

当然,我们也可以先从 3Blue1Brown 那里学习一些基础的知识。同时,我将假设读者已经有一定的程序设计能力,至少不需要再额外介绍什么是 Python 以及它的具体语法。我们还将会遇到非常多的关于线性代数和概率统计的内容,所以这部分可能也需要补补课。

工具链

首先,我们需要使用 Python. 虽然我个人不是很喜欢这门语言,但是由于它是业界标准的一部分,所以我们其实也没有太多其他的选择——通过 C/C++ 直接调用底层 binding 虽然也是一种做法,但是一般这个方法用于最终部署;当我们设计和训练模型时,快速地迭代和验证想法是必要的。

在 Python 之上,也有许多框架可以选择。此处选择 Keras, 使用 Tensorflow 后端。一方面是因为 Tensorflow 更适合部署;另一方面是因为 Keras 帮我们隐藏了许多比较繁复的细节,可以让我们在一开始培养直觉时不必特别纠结于底层的具体实现。

至于在框架之上,是否使用 Jupyter 或者 PyCharm 之类的工具,我们并不关心。

要安装 Keras 和 Tensorflow, 可以通过 pip 安装;最好在 venv 下:

(.venv) $ pip install tensorflow keras

如果你的系统有显卡,那么你需要先配置好合适的 GPGPU 库(比如 CUDA)。Tensorflow 会自动检测系统的显卡配置并调用对应的 GPGPU 库。网上的对应各个平台的安装方法已经有很详细的描述,此处不再赘述。

FunctionInferer

在正式开始之前,我们用一个小例子来演示机器学习究竟是怎样一个概念。

考虑这样一对输入输出:

\( x \)\( y \)
13
26
515
824
1236
表 1 输入输出对

可以容易地猜到,\(x\) 和 \(y\) 的关系为 \( y = 3x \). 但我们更感兴趣的问题是:如何让机器「学会」这里的关系是 \( y = 3x \) 呢?

一个比较通用的方法是使用神经网络。我们需要使用的是类似这样的一个(十分简单)的神经网络,其结构很简单:一个输入、一个输出,中间有 1 个参数对应着 \( y = kx \) 的结构:

Input Layer ∈ ℝ¹Output Layer ∈ ℝ¹

我们把上面这个结构转化为程序代码:

# in file 00-hello.py

import tensorflow as tf
import numpy as np
from tensorflow import keras

model = tf.keras.Sequential() # 我们需要一个顺序的(简单图的)网络
model.add(keras.layers.Input(shape=(1,))) # 有一个输入层,其中有 1 个节点
model.add(keras.layers.Dense(1)) # 有一个全连接层,其中有 1 个节点

为了能够评估神经网络的能力,我们需要一个计算网络输出的结果和我们预期的结果「差的多远」的方式。这个方式就是「损失函数 (Loss function)」。一个比较常用的损失函数是均方差函数 (MSE),即对每一个输入样本,计算网络输出和预期输出的平方差,然后求平均值;在我们这个具体的问题中很容易联想到最小二乘法。

有了评估能力,就需要有根据评估调整网络参数的能力。很容易通过 3b1b 的介绍想到使用梯度下降法 (Gradient Decend)——这确实是一个经典的方案。Keras 为我们提供了随机梯度下降 (Stochastic Gradient Descent) 函数用于调整网络参数,我们可以直接使用。

把上面两个决定转换成代码:

model.compile(optimizer='sgd', loss='mean_squared_error')

然后,我们就可以开始「训练」这个网络了:

xs = np.array([1.0, 2.0, 5.0, 8.0, 12.0], dtype=float) # 我们的输入数据
ys = np.array([3.0, 6.0, 15.0, 24.0, 36.0], dtype=float) # 对应输出的数据
model.fit(xs, ys, epochs=500) # 迭代 500 步

网络完成训练之后,便可以测试网络关于新输入的输出:

print(model.predict([15.0]))

容易观察到,网络的输出并不是 45.00000, 而是某个距离 45 很近的数字,比如 44.98867 或者 45.01325. 这是因为神经网络的训练是呈现边界递减的,靠近「答案」的地方梯度很小。可以想象小球滚落山坡,在谷底较为平坦的地方,小球可能几乎不会移动。对于我们这种用例而言,我们期待 3.00000 这样一个绝对精确的值可能永远不会达到。

这种不精确性使得神经网络并不适合进行绝对精准的计算。我们的实际用例则更倾向于让神经网络去做不定项选择题或者可以带约等于号的填空题。在下一篇文章中,我们将讨论单层感知机和多层感知机,以及如何通过感知机解决简单的分类问题。

新的范式

泛化地讲,如果我们可以把问题转化成 \( \vec{y} = f(\vec{x}) \) 的形式,那么我们的目标就是通过有限对 \( (\vec{x}, \vec{y}) \) 去逼近 \( f \). 通常情况下,这个 \( f \) 的核心会是一系列线性代数运算,而逼近 \( f \) 的方法则是猜出其中各个矩阵的值。

在之后的文章中,我们将讨论如何通过向量和矩阵表述我们要解决的问题、如何根据我们的表述准备 \( (\vec{x}, \vec{y}) \) 对,以及如何通过尽可能少对 \( (\vec{x}, \vec{y}) \) 训练网络。


本期代码文件可以在 http://dousha99.ysepan.com/ 获取。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

Your comments will be submitted to a human moderator and will only be shown publicly after approval. The moderator reserves the full right to not approve any comment without reason. Please be civil.