利用神经网络拟合一元二次方程

我们这次要拟合的关系是一个一元二次方程

目标

  • 理解参数初始化对于模型效果的重要影响。
  • 学习模型训练过程中,如何通过观察损失值相应的调整学习率、epoch大小。

数据构建

这次我们在准备数据的时候,就将数据进行划分好,总训练集100个,训练集90个,测试集10个。

# 生成准备y = x^2 + 2x + 1数据,共100个点,并按9:1的比例划分为训练集和测试集,并保存到CSV文件
​
import numpy as np
​
from sklearn.model_selection import train_test_split
​
# 生成输入数据
x = np.linspace(-10, 10, 100) # 在[-10, 10]范围内生成100个均匀分布的点
​
# 生成目标输出数据
y = x**2 + 2 * x + 1
​
# 划分为训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.1, random_state=42)
​
# 保存训练集数据到CSV文件
train_data = np.column_stack((x_train, y_train))
​
np.savetxt('train.csv', train_data, delimiter=',', header='x,y', comments='')
​
# 保存测试集数据到CSV文件
test_data = np.column_stack((x_test, y_test))
​
np.savetxt('test.csv', test_data, delimiter=',', header='x,y', comments='')
​
print("数据保存成功!")

训练集准备好之后,得到train.csv和test.csv两个数据集,分布做训练和测试验证用。

现在开始构建神经网络,以拟合数据集中未知的关系。尽管我们清楚这个数据集是由一元二次方程生成的,但为了模拟实际情况,我们要有意地“忘掉”这一点。假设我们面对的是一个完全未知的数据集,不清楚其中的模式或潜在规律,这就要求我们不能直接进入模型构建环节,而是先对数据进行全面的探索和分析。

现在开始实验,此时我们面对的是(x,y)的数据点,但是并不知道他们的关系,目标是构建一个神经网络来拟合潜在关系。

数据探索与可视化

老规矩,先进行数据探索和数据可视化。

import matplotlib.pyplot as plt
​
# 读取data.csv文件到本地变量x和y中
import numpy as np
​
data = np.loadtxt('train.csv', delimiter=',', skiprows=1)
x = data[:, 0]
y = data[:, 1]
​
# 可视化数据
plt.scatter(x, y, color='green', label='data-exploit')
plt.xlabel('x')
plt.ylabel('y')
plt.title('data exploit')
plt.legend()
plt.show()

可以看到,(x,y)的关系并不是一个线性或近线性关系,因此用在构建模型的时候,不能直接用线性模型进行构建,而需要引入非线性部件,也就是隐含层和激活函数。

模型构造

# 定义模型方法,简单线性模型,无隐含层
def build_linear_model():
    model = keras.Sequential([
        layers.Dense(1, input_shape=(2,))  # 只有输出层
    ])
    return model

模型训练

为了能够清晰看到每轮epoch误差的走势,我们将误差进行可视化,按照理想的情况,误差应该是逐步降低,并且趋于一个较小的值。

在图中可以看到,轮次在1000左右,误差开始区域平稳,并没有下降了,而误差大小却还是900多,这也验证了在数据探索时看到的,这个数据集并不是一个简单的线性关系,线性模型无法很好的拟合,表现出来也是误差偏大

既然简单的线性模型误差偏大,那么就需要增加模型复杂度,增加隐藏层和激活函数,来拟合非线性关系

# 定义模型方法,包含隐含层,用pytorch的模型方法来定义
class LinearModel(nn.Module):
  def __init__(self):
    super(LinearModel, self).__init__()
    self.layer = nn.Sequential(
      nn.Linear(1, 16),
      nn.ReLU(),
      nn.Linear(16, 1)
    )
​
    def forward(self, x):
      return self.layer(x)
​
# 固定初始权重和偏置,例如权重为 0.5,偏置为 0.1
with torch.no_grad():
  model.layer[0].weight.fill_(0.5)
  model.layer[0].bias.fill_(0.1)
  model.layer[2].weight.fill_(0.5)
  model.layer[2].bias.fill_(0.1)

选择隐藏层中的单元(神经元)数量确实是一个重要的超参数,直接影响模型的表达能力和训练效果。没有固定的公式来选择最优的神经元数量,但可以考虑以下几方面来做出合理的选择:

  • 简单任务:通常从 8、16 或 32 开始测试。如果在这些较小的神经元数量上表现良好,就不需要再增加。
  • 非线性关系且复杂任务:可以尝试 32 或 64,但一般来说超过 64 会显得多余,尤其是在单层隐藏层的情况下。

逐步调试和验证

  • 选择一个小的神经元数量(如 8 或 16),观察模型的表现。
  • 更少的神经元数量意味着更少的参数,这会降低计算复杂度,提高训练速度,同时也能减少过拟合的风险。
  • 在神经元数量过多时,模型可能学到数据中的噪声和细节,导致过拟合。

因为在数据探索可视化步骤中已经看到了数据的非线性关系,因此我们从16个神经元数量开始

激活函数Relu、学习率0.001、轮次100开始训练,并观测成本的走势

可以看到Loss一直是下降趋势,并没有到达一个平缓值,也没出现抖动,我们继续增加训练轮次到500

成本在300 epoch后下降趋势变得平缓,无法继续收敛,此时的损失输出。

可以看到模型Loss已经到达瓶颈,在300多处无法再下降了,此时模型还是处于误差偏大的情况,应该增加模型复杂度,我们先考虑增加隐藏层单元数,试试效果,将16增加到32

self.layer = nn.Sequential(
  nn.Linear(1, 32),
  nn.ReLU(),
  nn.Linear(32, 1)
)

损失快速下降,并且未出现瓶颈,继续增加epoch大小到1000。

epoch增加到1000看到下降趋势仍然明显,并且毫无平缓趋势,表示还未到达最小值附近,轮次虽然增加了一倍,但损失值没有显著降低,收敛速度较慢,因此将学习率从0.001调整为0.01,加速收敛过程,看看效果。

整体变化不大,继续调整0.01到0.1,加速收敛过程,看看效果。

效果非常明显,epoch在200左右开始,损失曲线进入平滑,但是此时损失值还在300以上,虽然效果并不是很好,但是以上对于学习率的尝试,可以感受到对收敛速度的影响。

面对这个损失值趋于平缓,且值较大,会猜测是否是学习率过大,导致未能很好的收敛至最小值,或者是模型复杂度不够,无法表达出数据的复杂关系。

先来验证第一个猜想,降低学习率,并且增大epoch次数,让模型在较小的学习率上,足够轮次去学习,看看能收敛到什么效果。尝试多轮试验,在0.001的学习率,epoch为30000时,足够小的学习率和足够多的轮次,损失曲线走向平缓,但是可以看到,损失值还是在300以上,足以验证并非时学习率导致未能收敛至最小值。

数据详情

再来验证第二个猜想,增加模型复杂度,增加一个隐藏层。

self.layer = nn.Sequential(
    nn.Linear(1, 64),
    nn.ReLU(),
    nn.Linear(64, 1)
)

Loss值依然很大,单纯通过增加隐藏层单元数作用不大。

考虑继续增加模型复杂度,增加隐藏层数量,从一个隐藏层增加到两个隐藏层

self.layer = nn.Sequential(
    nn.Linear(1, 32),
    nn.ReLU(),
    nn.Linear(32, 16),
    nn.ReLU(),
    nn.Linear(16, 1)
)

损失值依然还是处于高水位。

我们通过改变学习率、神经单元数,对Loss值的改变都没什么作用,陷入沉思。

仔细思考我们的模型,会发现,模型的初始化参数时手动设置的。

# 固定初始权重和偏置,例如权重为 0.5,偏置为 0.1
with torch.no_grad():
    model.layer[0].weight.fill_(0.5)
    model.layer[0].bias.fill_(0.1)
    model.layer[2].weight.fill_(0.5)
    model.layer[2].bias.fill_(0.1)

仔细思考,会发现手动初始化参数会有一个问题,每层所有的神经单元权重和更新都是一样的,只是一个单元的复制,因此无论单元数是8,还是64,本质都只是1,模型的表达能力大大退化,无法展示出复杂的表达能力。

恍然大悟,通过随机初始权重来替代手动初始化,用最初最简单的模型结构训练,看看效果

# 使用随机初始化的方法,替代 np.full 的固定值
self.layer = nn.Sequential(
    nn.Linear(1, 64),
    nn.ReLU(),
    nn.Linear(64, 1)
)
# 注释掉手动设置参数的逻辑
# with torch.no_grad():
#     model.layer[0].weight.fill_(0.5)
#     model.layer[0].bias.fill_(0.1)
#     model.layer[2].weight.fill_(0.5)
#     model.layer[2].bias.fill_(0.1)

Final Loss效果十分明显,证实我们的猜想是正确的。我们通过自动初始化,即如He初始化或Xavier初始化

之前设置的偏重和权重数是随意设置的,偏重0.5、0.1,现在不自己设置,改为由自动生成,看看效果

很神奇的一幕出现了,Loss居然一下子到达了从323多下降到0.24,效果惊人。

正常的梯度值应该是:

某一层的平均梯度可能从最初的较大值(比如 0.1 - 1)逐渐减小到 0.01 - 0.1 左右,随着训练的进行波动幅度也逐渐缩小。

注意一个情况,即每次运行可能得到的Final Loss值会不一样,原因是模型中存在几个随机给变量,分别是:Adam动量、随机初始化权重、偏重。这些都是影响模型效果的变量,面对随机变量,只要设定一个目标值,达到目标值之后就可以将模型保存。

面对当前目标,我们认为final loss在1以下就是可以接受的范围,这里没有一个定论,而是要与你实际的场景相结合,当目前final loss = 0.2,由于我损失函数用的是mean_squared_error,0.2开根号= 0.45,即预测误差会在大约0.45范围,这个是我可以接受的范围。

测试验证

接下来就是在测试环境进行模型验证:

# 测试集验证
import torch
from nonlinear_train_pt import NonLinearModel
import numpy as np
import torch.nn as nn

# 从data.csv文件中读取数据
def load_data(file_path):
    data = np.loadtxt(file_path, delimiter=',', skiprows=1)
    x = data[:, 0]
    y = data[:, 1]
    return x, y

if __name__ == "__main__":
    # 加载数据
    x, y = load_data('test.csv')
    x = torch.tensor(x, dtype=torch.float32).view(-1, 1)
    y = torch.tensor(y, dtype=torch.float32).view(-1, 1)

    # 加载模型
    model = NonLinearModel()
    model.load_state_dict(torch.load("nonlinear_model.pth", weights_only=True))
    criterion = nn.MSELoss()
    # 训练后评估模型
    model.eval()
    final_loss = criterion(model(x), y).item()
    print(f"Test final loss: {final_loss}")

Final Loss一下退后了好几个小数点,效果十分显著。

在测试集上进行验证,注意,模型是在标准后的数据上进行训练得到的,因此测试验证时,也需要利用训练数据的标准化参数进行处理。

运行之后得到结果发现,在标准化后的数据上效果很好,但是在真实数据集上去衡量,效果却远达不到。从下图可以看到,利用标准化数据进行预测,Loss在0.00138、而在真实值上预测,却在1.22,差了很多

总结

整体思路(从简单到复杂,更简单的模型部署、推理成本更低):

数据探索可视化 -> 非线性模型 -> 2层 -> 调整轮次、学习率 ->效果不佳 -> 自动初始化 -> 效果变好 -> 继续优化,数据标准化 ->标准化测试效果好,但真实值效果不好 -> 不用标准化,增加模型层数到3层 ->效果达标。

Read more

利用神经网络拟合一元线性方程

在现代人工智能技术中,神经网络通常被应用于复杂的非线性问题,如图像分类、语音识别和自然语言处理。然而,即便是简单的数学关系,像一元线性方程(y = ax + b) 的拟合,对于理解神经网络的基本原理、训练过程及其局限性会是很好的学习案例。 本文将探索如何利用神经网络来拟合这样一个简单的线性方程。我们将从零开始搭建一个基础网络,通过逐步训练,观察网络如何学习到输入与输出之间的线性关系。通过这个简单的示例,我们不仅可以验证神经网络的学习能力,还可以揭示一些训练过程中的常见挑战和优化技巧。 选择拟合的目标方程是: 目标 * 对可视化探索、模型构建、模型训练、模型测试有一个全程直观了解。 * 观察学习率、epoch调整对结果训练结果带来的影响。 数据构建 先来手动制作一批训练数据,以便为神经网络提供可用于拟合的样本数据。可以快速编写一个小脚本,生成 100 对 (x, y) 数据点,并将其保存在 train.csv 文件中。我们选择了一元线性方程 y = ax + b 的形式,后续构建神经网络及训练的目标是,通过训练逐步学习到系数 a

By 李浩

企业挣钱跟小区店铺有类似逻辑

今天在填明年海外部门预算,以往都是敷衍了事,没有在意,这次专门看了具体条目,以及金额数量,才发现预算金额如此庞大,再加上idc成本,人员成本,场地费,税务,剩下的就没多少来。一直以为挣了好多好多,结果算上来净利润还是在百分之三十左右,虽然说对比其他行业来说,已经是一个非常不错的水平了,但对于之前没有详细算过账的感觉来说,一下就打破模糊的偏差认知了。 这个利润率,也是公司坚持投入了好多年,才逐步打平以至营收的。 具体办企业与小商店在底层逻辑上有哪些共同点呢。 一,最后的利润,都是钱经过层层漏斗,漏到最后能留下来的,才是最后到口袋的。哪一个环节没有做好控制,都容易入不敷出。每个环节的成本控制,都不可忽视。对于负责人或老板来说,对每个环节的成本支出必须做到了然于胸。 二,外部合作必不可少,不管是企业还是小商店,都只是经济环节中的某一环,都会有上下游合作方,对于软件公司来说,云平台、支付渠道、投流平台等都会涉及到与其他企业的利益分配。对于小商店来说,进货商、场地租金、营收工具等都需要花钱解决,商店本身只负责末端售卖。大家都是整条产业链中的一环,参与进来才是最重要的。 三,都会有成本,

By 李浩

好的故事创意是否不会枯竭

似乎好久没有看到十分精彩的电影了,新出的电影观影过程中几乎就能才到后续的发展,总会感觉似曾相识或过于老套,引发了一个思考 - 好的故事创意是否不会枯竭。 如果不会枯竭,说明创意是一个永不会灌满的大水池,想不出创意只是能力问题,创意的点还无穷无尽。如何论证呢,无法论证,但是能计算,何解? 创意也可以分解为创意点的组合,将人类有史以来所有的故事放在一起,进行分解,可以提取出不同维度的要点,并且每个要点都会是可枚举的,即便是还未被创作出来的,其要点数量也会是可枚举的,可枚举的要点排列组合在一起,就会是一个有限数量大小。 如此看来,整个池子是一个固定数量,前面的创意点相对容易挖掘,越到后面,越难挖掘出来,想必这就是电影越来越难以创新的原因了,已挖掘的创意点多了,后续创意需要避开现有点才能被称为创意。 站在人的维度,看的电影越多,后续能触动到你的点就越少了。

By 李浩

是什么在支撑这个民族

经常在一些局上,听公司上位者侃侃而谈,他们在社会发展的浪潮中,随着企业上市,成功的实现财富自由,应该来说,会对国家、社会、民族会心存感激,但从他们的话语中,鲜有如此论调,更多的是冷嘲热讽,谈话内容多是滑雪、潜水、旅游、大餐、出入境携款、移民等话题,对于国家经济、政策、环境,鲜有积极话语,更别说感激之类,这些多少令我有些差异。 他们对国内信息,多半是不信,而是从自认为更客观的外媒中去获取。发出的声音也多半对国内抨击的。 给我一种强烈的感觉,一群精致的利己主义精英。 改革开放四十多年,整个社会价值观都被绑在以金钱唯一锚定物的评价体系中,所有人一致的向钱看,没钱的自惭形秽,物质生活不如人,谈家国、谈情怀、谈文化会缺少底气,有钱的更关心如何赚更多钱。 成长的过程中,都会被教得圆滑、世故,有错吗,也并不是,做事情避免不了与人打交道,就需要尽可能团结一切能团结的人,避免树敌,在不违背大原则的前提下,圆滑世故换个角度来看,也是照顾对方的感情,

By 李浩