关于神经网络最引人注目的事实之一是它们可以计算任何函数。也就是说,假设有人给你一些复杂的、不稳定的函数F(x):
一定会存在一个神经网络,对于任意可能的输入x,都有输出f(x)。比如这个神经网络是输入、输出层都是1个神经元的简单网络:
且当输入、输出层有多个神经元时(如下图),上面结论仍然成立。
不仅如此,这个结论对只有一层隐含层的神经网络也适用。这些是我们知道的或默认的,不然我们不会用它解决各种各样的问题。下面就分别用几何直观的方式和代数的方式,并结合代码解释下神经网络为什么会这么强大。
几何直观证明
为了理解为什么神经网络是通用的,让我们首先了解如何构建一个神经网络,该网络近似只有一个输入和一个输出的函数,输出结果为:σ(wx+b)。其中,σ(z)为激活函数,
σ(z)=
但为了证明普遍性,我们将通过完全忽略代数,而是操纵和观察图中所示的形状来获得更多的见解。
1、单神经元输入、单神经元输出、单个隐含层的情况
上图中,右面曲线表示隐含层第一个神经元的输出-输入关系,输出是σ(wx+b)。曲线形状有下面特性:
(1)w增大,曲线会变得更陡;w减小,曲线更平缓;
(2)b增大,曲线会左移;b减小,曲线右移。
我们可以让w很大,b调整到合适的值,使曲线如下图那样陡峭,近似阶跃函数(step function)。
下面也用阶跃函数这个词来近似描述该函数。阶跃点表示图中竖线在横轴的位置,其中竖线与曲线的中间点相交。阶跃点的位置跟w、b都有关系,关系是s=-b/w。
为了方便表示,我们可以用s代替w、b,调整s大小就能调节阶跃点位置。
如果我们考虑隐含层的两个神经元,就可以输出下面曲线:
曲线的高矮用h参数来表示,h跟s类似(是w和b的函数),h能任意调高矮,s能任意调宽窄。
至此,我们得到了一个可灵活变化的矩形。我们可以让隐含层的神经元变成两对,得到如下曲线:
同理可得,多对神经元就能粗略模拟一个曲线了!
至此,你get到了神经网络一般性的本质!任何函数都可以被一个神经网络近似计算。上图中的神经元越多、越窄,计算结果越精确。
代数证明
神经网络中最关键的是引入了激活函数。激活函数其中一个重要的作用是加入非线性因素的,解决线性模型所不能解决的问题,下面我们从解释一下激活函数的作用——特征的充分组合。
首先抛出一个问题,现实解决复杂问题用的神经网络模型中如果没有激活函数可以吗?答案是不可以的。激活函数的作用如下:
- 加入非线性因素,解决线性模型不能解决的问题;
- 激活函数可以用来组合训练的数据特征,让特征进行充分组合。
下面我分别对激活函数的两个作用进行解释。
(1)加入非线性因素,解决非线性问题
好吧,很容易能够看出,我给出的样本点根本不是线性可分的,一个感知器无论得到的直线怎么动,都不可能完全正确的将三角形与圆形区分出来,那么我们很容易想到用多个感知器来进行组合,以便获得更大的分类问题,好的,下面我们上图,看是否可行。
好的,我们已经得到了多感知器分类器了,那么它的分类能力是否强大到能将非线性数据点正确分类开呢~我们来分析一下:
如果我们的每一个结点加入了阶跃函数作为激活函数的话,就是上图描述的
那么随着不断训练优化,我们也就能够解决非线性的问题了~
所以到这里为止,我们就解释了这个观点,加入激活函数是用来加入非线性因素的,解决线性模型所不能解决的问题。
(2)激活函数可以用来组合训练数据的特征,特征的充分组合
我们可以通过上图可以看出,立方激活函数已经将输入的特征进行相互组合了。
通过泰勒展开,我们可以看到,我们已经构造出立方激活函数的形式了。
于是我们可以总结如下:
(3)总结
这就把原来需要领域知识的专家对特征进行组合的情况,在激活函数运算后,其实也能够起到特征组合的作用。(只要激活函数中有能够泰勒展开的函数,就可能起到特征组合的作用)
代码验证
以上我们从几何直观以及代数方面针对神经网络可以模拟任意复杂函数进行了理论证明,接下来我们通过代码进行验证说明。
以下是一个简单的例子,说明神经网络如何逼近一个非线性函数。
考虑一个非线性函数 y = sin(x) + 0.5x,我们将尝试使用神经网络来逼近这个函数。
首先,让我们生成一些训练数据,并创建一个包含单个隐藏层的前馈神经网络来逼近这个函数:
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
# 创建一个包含单个隐藏层的前馈神经网络
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
self.hidden_layer = nn.Linear(1, 10) # 1输入特征,10隐藏神经元
self.output_layer = nn.Linear(10, 1) # 10隐藏神经元,1输出
def forward(self, x):
x = torch.relu(self.hidden_layer(x))
x = self.output_layer(x)
return x
# 生成训练数据(函数 y = sin(x) + 0.5x)
x_train = np.random.rand(100, 1) * 4 * np.pi - 2 * np.pi # 100个随机的数据点在[-2*pi, 2*pi]范围内
y_train = np.sin(x_train) + 0.5 * x_train
# 转换为PyTorch张量
x_train = torch.tensor(x_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)
# 创建神经网络实例和损失函数
net = NeuralNetwork()
criterion = nn.MSELoss()
optimizer = optim.Adam(net.parameters(), lr=0.01)
# 训练神经网络
num_epochs = 10000
for epoch in range(num_epochs):
optimizer.zero_grad()
output = net(x_train)
loss = criterion(output, y_train)
loss.backward()
optimizer.step()
if (epoch + 1) % 1000 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
# 使用训练好的模型进行预测
x_test = np.linspace(-2 * np.pi, 2 * np.pi, 200).reshape(-1, 1)
x_test = torch.tensor(x_test, dtype=torch.float32)
y_pred = net(x_test).detach().numpy()
# 可视化训练结果和原始函数
plt.figure(figsize=(10, 6))
plt.scatter(x_train.numpy(), y_train.numpy(), label='Training Data', c='r', marker='o')
plt.plot(x_test.numpy(), y_pred, label='Neural Network Approximation', linewidth=2)
plt.plot(x_test.numpy(), np.sin(x_test) + 0.5 * x_test, label='True Function', linestyle='--', linewidth=2)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Approximating y = sin(x) + 0.5x with a Neural Network')
plt.legend()
plt.grid(True)
plt.show()
在这个示例中,我们使用神经网络来逼近非线性函数 y = sin(x) + 0.5x。训练网络后,我们可以看到神经网络成功地逼近了原始函数,这表明神经网络能够在逼近各种函数方面具有很强的灵活性。这个例子突显了神经网络的强大功能,可以用来逼近各种复杂的函数关系。
如果要进一步提升上述逼近原函数的精度,应该如何修改原程序?
要提高神经网络逼近原函数的准确度,您可以尝试以下修改和改进:
- 增加隐藏层和神经元数量:增加神经网络的复杂性,例如增加隐藏层的数量或每层的神经元数量,以增加模型的容量。
- 增加训练周期:增加训练周期,以便模型更充分地学习数据的模式。但要小心不要过拟合,可以使用验证数据来监控模型的泛化性能。
- 尝试不同的激活函数:尝试使用不同的激活函数,例如LeakyReLU、Tanh或Sigmoid,以查看哪种激活函数对这个任务更适合。
- 调整学习率:尝试不同的学习率,从较小的值开始,并逐渐增加学习率,以找到一个合适的学习率。
- 尝试不同的优化器:除了Adam,尝试使用其他优化器,如SGD、RMSprop等,看看哪个在这个任务上表现更好。
- 正则化:添加正则化项,如L1或L2正则化,以减少过拟合。
- 批量归一化:在隐藏层之间添加批量归一化层,有助于加速训练并提高模型的稳定性。
- 自适应学习率调整:使用学习率调度器,如学习率衰减或学习率计划,以自适应地调整学习率。
以下是修改后的代码示例,其中包括了上述建议的改进:
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
# 创建一个包含多个隐藏层的前馈神经网络
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
self.hidden_layer1 = nn.Linear(1, 50) # 1输入特征,50隐藏神经元
self.hidden_layer2 = nn.Linear(50, 50) # 50输入特征,50隐藏神经元
self.output_layer = nn.Linear(50, 1) # 50隐藏神经元,1输出
def forward(self, x):
x = torch.relu(self.hidden_layer1(x))
x = torch.relu(self.hidden_layer2(x))
x = self.output_layer(x)
return x
# 创建神经网络实例
net = NeuralNetwork()
# 打印神经网络结构
print("Neural Network Structure:")
print(net)
# 打印神经网络的权重和偏差
print("\nNeural Network Parameters:")
for name, param in net.named_parameters():
if param.requires_grad:
print(f"{name}: {param.data}")
# 打印每一层的输入和输出尺寸
print("\nInput and Output Sizes for Each Layer:")
for name, layer in net.named_children():
if isinstance(layer, nn.Linear):
input_size = layer.in_features
output_size = layer.out_features
print(f"{name}: Input Size = {input_size}, Output Size = {output_size}")
# 打印每一层的参数数量
print("\nNumber of Parameters in Each Layer:")
for name, param in net.named_parameters():
if param.requires_grad:
print(f"{name}: {param.numel()}")
# 打印总参数数量
total_params = sum(p.numel() for p in net.parameters() if p.requires_grad)
print("\nTotal Number of Trainable Parameters:", total_params)
# 生成训练数据(函数 y = sin(x) + 0.5x),在区间 [-6.6, 6.6] 内
x_train = np.random.rand(1000, 1) * 13.2 - 6.6 # 1000个随机的数据点在[-6.6, 6.6]范围内
y_train = np.sin(x_train) + 0.5 * x_train
# 转换为PyTorch张量
x_train = torch.tensor(x_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)
# 创建神经网络实例和损失函数
net = NeuralNetwork()
criterion = nn.MSELoss()
optimizer = optim.Adam(net.parameters(), lr=0.001) # 调整学习率
# 训练神经网络
num_epochs = 20000 # 增加训练周期
for epoch in range(num_epochs):
optimizer.zero_grad()
output = net(x_train)
loss = criterion(output, y_train)
loss.backward()
optimizer.step()
if (epoch + 1) % 1000 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
# 使用训练好的模型进行预测
x_test = np.linspace(-6.6, 6.6, 200).reshape(-1, 1)
x_test = torch.tensor(x_test, dtype=torch.float32)
y_pred = net(x_test).detach().numpy()
# 可视化训练结果和原始函数
plt.figure(figsize=(10, 6))
plt.scatter(x_train.numpy(), y_train.numpy(), label='Training Data', c='r', marker='o')
plt.plot(x_test.numpy(), y_pred, label='Neural Network Approximation', linewidth=2)
plt.plot(x_test.numpy(), np.sin(x_test) + 0.5 * x_test, label='True Function', linestyle='--', linewidth=2)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Improved Approximation of y = sin(x) + 0.5x with a Neural Network')
plt.legend()
plt.grid(True)
plt.show()
在这个修改后的代码中,我们增加了神经网络的复杂性、训练周期,并调整了学习率。这些改进有助于提高神经网络逼近原函数的准确度。您可以根据需要进一步调整超参数以获得更好的性能。
Neural Network Structure (After Training):
NeuralNetwork(
(hidden_layer1): Linear(in_features=1, out_features=50, bias=True)
(hidden_layer2): Linear(in_features=50, out_features=50, bias=True)
(output_layer): Linear(in_features=50, out_features=1, bias=True)
)
Neural Network Parameters (After Training):
hidden_layer1.weight: tensor([[ 0.0825],
[ 0.7044],
[-0.3375],
[-0.2671],
[-0.4813],
[ 0.6558],
[ 0.2793],
[ 0.3227],
[ 0.2140],
[ 0.5008],
[ 0.3725],
[ 0.9927],
[-0.8503],
[-0.2389],
[-0.3335],
[-0.7489],
[-0.5674],
[ 0.2670],
[ 0.4114],
[ 0.7258],
[ 0.9355],
[-0.8015],
[ 0.6182],
[ 0.3130],
[-0.2162],
[ 0.4598],
[ 0.2892],
[-0.5683],
[-0.6120],
[-0.5978],
[-0.6970],
[ 0.1208],
[ 0.4862],
[ 0.8421],
[ 0.5850],
[ 0.1100],
[ 0.0455],
[-0.2747],
[ 0.4374],
[ 0.3533],
[ 0.7137],
[-0.8125],
[-0.3950],
[ 0.8710],
[ 0.7468],
[ 0.4855],
[ 0.1949],
[-0.2547],
[ 0.9097],
[ 0.7534]])
hidden_layer1.bias: tensor([ 4.4674e-01, 5.4609e-01, -1.5498e+00, -7.8404e-02, 2.9593e-02,
-1.1261e+00, 1.1457e+00, -6.0130e-01, -1.1856e+00, 7.1336e-01,
-5.6380e-01, -3.0472e-01, 6.9052e-01, 9.6752e-02, -9.7798e-01,
-2.1741e-01, -5.9287e-01, 9.8672e-01, -8.8389e-01, -3.0103e-01,
-4.5305e-02, -1.1050e+00, 4.1906e-04, -1.5105e+00, 1.5826e-01,
-5.6970e-01, -5.6258e-01, -3.5113e-01, -4.8965e-01, 1.5346e-01,
-1.3311e+00, -8.3694e-01, 1.0095e+00, 5.1924e-01, -7.9395e-01,
6.5024e-01, -8.6198e-01, -7.5367e-01, 3.7591e-01, -2.9368e-01,
6.0570e-03, 3.4727e-02, 6.9381e-01, 2.4336e-01, 2.1229e-01,
6.5567e-01, 1.5767e-01, 9.7992e-01, 2.6099e-01, 7.8849e-01])
hidden_layer2.weight: tensor([[ 0.2374, 0.0302, 0.3109, ..., -0.0719, 0.0751, -0.0359],
[-0.0228, 0.0493, -0.0298, ..., -0.0481, -0.0728, -0.0408],
[-0.1225, 0.0192, -0.0970, ..., -0.1036, -0.0098, 0.0010],
...,
[ 0.1195, -0.1071, 0.0545, ..., 0.0434, -0.0668, -0.0541],
[ 0.6838, -0.2234, 0.2676, ..., 0.3907, -0.1174, -0.2583],
[ 0.0356, 0.1164, -0.0567, ..., -0.1317, 0.0338, 0.0335]])
hidden_layer2.bias: tensor([-0.3827, -0.0810, -0.0334, -0.0394, 0.0522, 0.1076, 0.0140, -0.1231,
-0.0695, -0.1611, 0.1111, -0.3494, -0.2981, 0.0385, -0.2837, -0.4027,
-0.1075, 0.3430, 0.0015, 0.0015, -0.0333, -0.0666, -0.0516, 0.0278,
-0.1814, -0.2531, 0.1020, -0.0110, -0.0753, -0.0058, -0.0629, -0.0411,
-0.0534, 0.0453, -0.0044, 0.0709, -0.2969, -0.0448, -0.0362, -0.0971,
0.1248, -0.1103, -0.0189, -0.0807, -0.0937, -0.0787, 0.1291, 0.0399,
0.3934, -0.1087])
output_layer.weight: tensor([[-0.1301, -0.0562, -0.0303, -0.0658, 0.0995, -0.1212, 0.0019, -0.1240,
0.0579, -0.2233, 0.1451, -0.1807, -0.2540, 0.1468, -0.3866, -0.5546,
-0.1173, 0.5713, -0.1438, -0.0490, -0.0422, 0.1370, -0.1795, 0.0840,
-0.4097, 0.0875, 0.1765, 0.0689, 0.0159, 0.1222, -0.0978, 0.3691,
0.1910, -0.0181, -0.0789, 0.1321, -0.2032, -0.0645, 0.0875, 0.1084,
-0.0642, -0.1513, 0.3670, 0.0797, 0.0685, 0.2305, 0.2901, 0.0727,
-0.4801, -0.0257]])
output_layer.bias: tensor([0.0420])
我们通过geoGebra作图,发现两者的图像几乎一致。这充分说明了神经网络对任意给定的函数进行模拟。