卡卷网
当前位置:卡卷网 / 每日看点 / 正文

为什么我的神经网络输出永远一样?

作者:卡卷网发布时间:2025-02-22 17:48浏览数量:59次评论数量:0次

看了一下代码,确实有点东西嗷,自己写神经网络,自己写MSE损失函数,自己写梯度回传。(甚至连矩阵计算都不用,直接手写矩阵行元素的计算,好险不是矩阵元素两重循环计算。)

简单对代码梳理了一下,整个模型可以分为两个部分:

前向传播部分:

  • 压缩: 为什么我的神经网络输出永远一样?  第1张 ,具体为 为什么我的神经网络输出永远一样?  第1张
  • 解压: 为什么我的神经网络输出永远一样?  第1张 ,具体为 为什么我的神经网络输出永远一样?  第1张
  • 损失函数:为什么我的神经网络输出永远一样?  第1张

反向传播/.参数更新部分:

  • 梯度计算:为什么我的神经网络输出永远一样?  第1张 等计算,通过导数的链式法则等。
  • 参数更新: 为什么我的神经网络输出永远一样?  第7张 ,对相关参数进行更新。

输出的矩阵为什么是大都趋于0.5,我猜测,主要是因为数据生成采用的是np.random.random函数,这个函数会以均匀分布的方式生成(0,1)之间的随机数,所以矩阵中每一个元素的均值就是0.5,与此同时,在实现的代码中,是计算完所有的样本再进行梯度下降,很大概率是朝着0.5的方向下降。

那么这个问题就来了在修改好下降方向以后,为什么模型仍然不会学习得到输入 为什么我的神经网络输出永远一样?  第1张 是什么,输出 为什么我的神经网络输出永远一样?  第1张 就是什么的额能力呢?大概是当前模型的能力不足以学会更多的东西(参数较小)。


关于代码的一些吐槽

整个的计算效率很低,运行题主源代码都快运行了一个小时了。

可以加速计算的一个点是:当前选择的是对每一个数据进行计算

for i in range(len(data)): pass

这里可以使用batch加速计算一下,不然整个计算速度就太慢了。但是为了能够适配batch,又需要将相关的计算从for计算改成矩阵计算,比如

原始的前向传播,对每一行进行计算:

alpha=np.zeros(8) belta=np.zeros(8) delta=np.zeros(8) for i in range(8): alpha += x_input[i] * w_matrix_input[0, i] belta += x_input[i] * w_matrix_input[1, i] delta += x_input[i] * w_matrix_input[2, i] alpha += b_array_input[0] belta += b_array_input[1] delta += b_array_input[2] sigmoid_alpha = sigmoid(alpha) sigmoid_belta = sigmoid(belta) sigmoid_delta = sigmoid(delta) hidden_layer_output=np.array([sigmoid_alpha,sigmoid_belta,sigmoid_delta]) output = np.zeros((8,8)) line = np.zeros(8) for i in range(8): line=sigmoid_alpha*w_matrix_output[i,0]+sigmoid_belta*w_matrix_output[i,1]+sigmoid_delta*w_matrix_output[i, 2]+b_array_output[i] output[i]=line output=sigmoid(output) return output,hidden_layer_output

新的前向传播:

C = w_matrix_input@x_input + b_array_input.reshape(-1,1) hidden_layer_output = sigmoid(C) D = w_matrix_output@hidden_layer_output + b_array_output.reshape(-1,1) output=sigmoid(D) return output,hidden_layer_output

总之先把整个代码能够精简一点,可能才能看出更加多的问题吧

PS:可以先整个深度学习框架,不然这看着不太容易理解。


Numpy下的矩阵计算

由于原代码采用了大量地for循环和向量计算,这实际上造成了时间上的浪费,毕竟现代的计算方法矩阵计算已经很方便了,所以在这里,对原来的代码进行改进,使其能够利用矩阵的方式来计算。

对于前向传播来说,很简单,在上面也介绍了,可以直接在代码使用 A@B 做矩阵计算。那么接下来的任务就是将梯度求导这一块也改成矩阵计算,为了方便看客们的理解,在这里,做一个跟上述任务类似的求导过程计算就完事了:

首先,我们的预测是 为什么我的神经网络输出永远一样?  第1张 其中, 为什么我的神经网络输出永远一样?  第1张 就是我们的输出,假设维度是 为什么我的神经网络输出永远一样?  第1张 ,而 为什么我的神经网络输出永远一样?  第1张 就是我们的输入,维度是 为什么我的神经网络输出永远一样?  第1张为什么我的神经网络输出永远一样?  第1张 是我们的矩阵参数,维度是 为什么我的神经网络输出永远一样?  第1张为什么我的神经网络输出永远一样?  第1张 是我们的偏置,维度是 为什么我的神经网络输出永远一样?  第1张 ,相当于每一行的每一个元素都加上同一个偏置。

我们的损失函数有 为什么我的神经网络输出永远一样?  第1张 ,其实就是预测和真实值之间每一个元素做一个平方再求和,所以也可以写成 为什么我的神经网络输出永远一样?  第1张

那么,接下来,我们想知道,当前的损失函数数值,如何得到我们的参数 为什么我的神经网络输出永远一样?  第1张 的梯度呢?在这里就只讨论可微分的情况。因为直接计算矩阵的微分比较困难,但是我们可以先计算其中一个,比如 为什么我的神经网络输出永远一样?  第1张 ,即计算 为什么我的神经网络输出永远一样?  第1张 有:

【数学数字比较多,防止伤害眼睛,可跳过】

为什么我的神经网络输出永远一样?  第1张 其中为什么我的神经网络输出永远一样?  第1张 ,所以拆开来有:

为什么我的神经网络输出永远一样?  第1张

其中,当 为什么我的神经网络输出永远一样?  第1张 的时候,该导数为 为什么我的神经网络输出永远一样?  第1张 ,其余为0。

所以原式可转化为 为什么我的神经网络输出永远一样?  第1张 ,其中 为什么我的神经网络输出永远一样?  第1张为什么我的神经网络输出永远一样?  第1张 为1,其余为0。

所以就有 为什么我的神经网络输出永远一样?  第1张 ,最后矩阵形式为 为什么我的神经网络输出永远一样?  第1张

类似地,如果对参数 为什么我的神经网络输出永远一样?  第1张 ,也有上述的关系有 为什么我的神经网络输出永远一样?  第1张 ,实际上就是对列求和,所向量形式有 为什么我的神经网络输出永远一样?  第1张

【结束】

总之,通过上述的方式,我们能够将梯度计算通过矩阵的形式进行计算,对于原问题来说,我们就有:

为什么我的神经网络输出永远一样?  第37张代码如下:

# 矩阵梯度更新 def gradiengt_decent(output, input, w2, hidden_output): Derror_Dw2 = ((output - input) * output *(1-output)) @ hidden_output.T Derror_Db2 = np.sum((output - input) * output *(1-output), axis=1) Derror_Dw1 = ((w2.T @ (((output - input) * output *(1-output)))) * hidden_output * (1 - hidden_output)) @ input.T Derror_Db1 = np.sum(((w2.T @ (((output - input) * output *(1-output)))) * hidden_output * (1 - hidden_output)), axis=1) return Derror_Dw1,Derror_Dw2,Derror_Db1,Derror_Db2

PS:因为原代码没有用到W2,大概率是有问题的。

至此,基于numpy下的矩阵计算就修改完了。

深度学习框架

通过上述的矩阵导数计算可以知道,手动计算一个参数的梯度求导很简单,但是计算效率低;计算一个矩阵的梯度就有难度,但是计算效率高。如果能够有一个自动求导微分的工具,那该多好啊。这也是当前的深度学习发展很关键的一部分——自动微分机制

接下来,将采用当前主流的深度学习框架Pytorch来实现,同时验证文章上述梯度计算的正确性。

首先是定义网络以及前向传播:

import torch from torch import nn class SAE(nn.Module): def __init__(self) -> None: super().__init__() # 参数初始化 self.W1 = nn.parameter.Parameter(torch.zeros(3, 8, dtype=torch.double)) self.b1 = nn.parameter.Parameter(torch.zeros(3, dtype=torch.double)) self.sigmoid = nn.Sigmoid() self.W2 = nn.parameter.Parameter(torch.zeros(8, 3, dtype=torch.double)) self.b2 = nn.parameter.Parameter(torch.zeros(8, dtype=torch.double)) def forward(self, X): hidden_matrix = self.sigmoid(self.W1.matmul(X) + self.b1.reshape(-1, 1)) output_matrix = self.sigmoid(self.W2.matmul(hidden_matrix) + self.b2.reshape(-1, 1)) return output_matrix

然后有训练过程:

from torch import optim Net = SAE() # Net(torch.tensor(data[0], dtype=torch.float)) # 开始训练 optimizer = optim.SGD(Net.parameters(), lr=0.05) Loss_func = nn.MSELoss(reduction="sum") epochs = 128 for _ in range(epochs): lenght = data.shape[0] Loss = 0 for i in range(lenght): X = torch.tensor(data[i]) X_hat = Net(X) loss = Loss_func(X_hat, X)/2 optimizer.zero_grad() loss.backward() optimizer.step() Loss += loss print(f"{_+1} loss:{Loss/lenght}")

为了验证上述梯度回传计算的正确性,使用这样的代码来验证,已经将矩阵都以全0的方式初始化:

input_data = data[0] # 基于Numpy的方法 w1, b1, w2, b2= W1, B1, W2, B2 output, hidden_layer_output=forward_propagation(input_data,w1,b1,w2,b2) dw1, dw2, db1, db2 = gradiengt_decent(output,input_data,w2,hidden_layer_output) # 基于Pytoch的方法 Net = SAE() X = torch.DoubleTensor(data[0]) X_hat = Net(X) loss = Loss_func(X_hat, X)/2 optimizer.zero_grad() loss.backward() # 比较 print(Net.W1.grad - dw1) print(Net.W2.grad - dw2) print(Net.b1.grad - db1) print(Net.b2.grad - db2)

结果有:

tensor([[0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0.]], dtype=torch.float64) tensor([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.], [0., 0., 0.], [0., 0., 0.], [0., 0., 0.], [0., 0., 0.], [0., 0., 0.]], dtype=torch.float64) tensor([0., 0., 0.], dtype=torch.float64) tensor([ 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, -2.7756e-17, 2.7756e-17, 0.0000e+00, 0.0000e+00], dtype=torch.float64)

说明上述的计算基本正确,可能存在一些精度上的误差。

至此,我们就可以开始分析,题主所说的“神经网络输出都一样这个问题”。

首先先看题主的任务,可能是某种矩阵压缩的任务,将矩阵从(8,8)的形式压缩到(3,8)的形式,甚至可能是8个8维度的行向量,压缩成3个8维度的行向量以探求向量之间的关系。

如果是后者,从线性代数的秩来看,是比较难做到的,一般来说,随机出来的矩阵是一个满秩矩阵,很难存在可以压缩的信息,即从8x8压缩到3x8的形式。

但是至少,通过实验结果,以及可能的分析,可以看到,题主的任务,首先在正确的实现下并不是0.5附近徘徊,在训练了1000次以后有:

1000 loss:2.3343 Net(torch.tensor(data[0]).cuda()).cpu().detach().numpy() array([[0.51172373, 0.56241952, 0.55800087, 0.47174427, 0.56678227, 0.40300673, 0.44959767, 0.52739377], [0.48013683, 0.40617486, 0.41255567, 0.53892418, 0.39989175, 0.63824329, 0.57131044, 0.45713635], [0.52591279, 0.61318884, 0.60572179, 0.45605942, 0.62052586, 0.33946687, 0.41771344, 0.553189 ], [0.50419805, 0.51642133, 0.51534773, 0.49460804, 0.51748356, 0.47785564, 0.48926727, 0.50795945], [0.52419569, 0.5990492 , 0.5926021 , 0.46454659, 0.60539538, 0.36360818, 0.43164683, 0.54750019], [0.52763252, 0.60983392, 0.6027865 , 0.46186592, 0.61676286, 0.35126886, 0.42565517, 0.55329581], [0.51289696, 0.5519445 , 0.54853156, 0.48214023, 0.55531696, 0.42882915, 0.46505109, 0.52494809], [0.39894411, 0.12901825, 0.14456692, 0.68272197, 0.1150574 , 0.94388547, 0.80557175, 0.29499562]])

可以看到部分数值有0.9和0.29不在0.5附近的存在,但是损失函数此时已经不随着训练而继续下降了,一定程度说明当前的模型的性能比较差,可能无法达到题主的目的。

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码