我在 Julia 和机器学习方面还是新手,但我非常渴望学习。在我目前的项目中,我遇到了一个关于维度不匹配的问题,并且不知道该怎么办。
我有以下两个数组:
x_array: 9-element Array{Array{Int64,N} where N,1}: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 72, 73] [11, 12, 13, 14, 15, 16, 17, 72, 73] [18, 12, 19, 20, 21, 22, 72, 74] [23, 24, 12, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 72, 74] [36, 37, 38, 39, 40, 38, 41, 42, 72, 73] [43, 44, 45, 46, 47, 48, 72, 74] [49, 50, 51, 52, 14, 53, 72, 74] [54, 55, 41, 56, 57, 58, 59, 60, 61, 62, 63, 62, 64, 72, 74] [65, 66, 67, 68, 32, 69, 70, 71, 72, 74]y_array:9-element Array{Int64,1} 75 76 77 78 79 80 81 82 83
接下来是使用 Flux 的模型:
model = Chain( LSTM(10, 256), LSTM(256, 128), LSTM(128, 128), Dense(128, 9), softmax)
我将这两个数组打包,然后使用 Flux.train! 将它们输入到模型中
data = zip(x_array, y_array)Flux.train!(loss, Flux.params(model), data, opt)
然后立即抛出以下错误:
ERROR: DimensionMismatch("matrix A has dimensions (1024,10), vector B has length 9")
现在,我知道矩阵 A 的第一维是隐藏层的总和(256 + 256 + 128 + 128 + 128 + 128),第二维是输入层,为 10。我做的第一件事是将 10 改为 9,但之后它只抛出以下错误:
ERROR: DimensionMismatch("dimensions must match")
有人能解释一下哪些维度不匹配,以及如何使它们匹配吗?
回答:
引言
首先,您应该知道,从架构的角度来看,您对网络的要求非常高;softmax
将输出重新归一化为 0 到 1 之间的值(像概率分布一样加权),这意味着让您的网络输出像 77
这样的值来匹配 y
是不可行的。这并不是导致维度不匹配的原因,但这是需要注意的一点。我将删除末尾的 softmax()
以给网络一个机会,尤其是因为它并不是问题所在。
调试形状不匹配
让我们来看看 Flux.train!()
内部实际发生了什么。定义 实际上非常简单。忽略对我们不重要的一切,我们剩下的是:
for d in data gs = gradient(ps) do loss(d...) endend
因此,让我们从您的 data
中提取第一个元素,并将其分解到您的 loss
函数中。您在问题中没有指定损失函数或优化器。尽管 softmax
通常意味着您应该使用 crossentropy
损失,但您的 y
值绝对不是概率,所以如果我们去掉 softmax
,我们可以使用非常简单的 mse()
损失。对于优化器,我们将默认使用老式的 ADAM:
model = Chain( LSTM(10, 256), LSTM(256, 128), LSTM(128, 128), Dense(128, 9), #softmax, # 现在暂时注释掉)loss(x, y) = Flux.mse(model(x), y)opt = ADAM(0.001)data = zip(x_array, y_array)
现在,为了模拟 Flux.train!()
的第一次运行,我们取 first(data)
并将其分解到 loss()
中:
loss(first(data)...)
这会给我们之前看到的错误消息;ERROR: DimensionMismatch("matrix A has dimensions (1024,10), vector B has length 12")
。查看我们的数据,我们看到,确实,我们数据集的第一个元素长度为 12。因此,我们将模型更改为期望 12 个值而不是 10:
model = Chain( LSTM(12, 256), LSTM(256, 128), LSTM(128, 128), Dense(128, 9),)
现在我们重新运行:
julia> loss(first(data)...) 50595.52542674723 (tracked)
成功!它工作了!我们可以再次运行:
julia> loss(first(data)...) 50578.01417593167 (tracked)
值发生变化是因为 RNN 在内部保存了记忆,每次运行网络时都会更新,否则我们会期望网络对相同的输入给出相同的答案!
然而,当我们尝试运行第二个训练实例通过我们的网络时,问题出现了:
julia> loss([d for d in data][2]...)ERROR: DimensionMismatch("matrix A has dimensions (1024,12), vector B has length 9")
理解 LSTM
这是我们遇到更多机器学习问题而不是编程问题的地方;这里的问题是我们承诺给第一个 LSTM
网络提供一个长度为 10
的向量(现在是 12
),而我们违背了这个承诺。这是深度学习的一般规则;您总是必须遵守关于流经模型的张量形状的合同。
现在,您使用 LSTM 的原因可能是您想要输入不规则的数据,处理它们,然后对结果做一些事情。也许您在处理长度可变的句子,并且想要进行情感分析之类的。像 LSTM 这样的循环架构的美妙之处在于它们能够将信息从一个执行传递到另一个执行,因此它们能够在应用于一个时间点之后建立一个序列的内部表示。
在 Flux 中构建 LSTM
层时,您实际上声明的是您将输入的序列的维度,而不是序列的长度;想象一下,如果您有一个加速度计读数,长度为 1000 点,每个时间点给出 X、Y、Z 值;为了读取它,您将创建一个 LSTM
,其输入维度为 3
,然后喂入 1000
次。
编写我们自己的训练循环
我发现编写我们自己的训练循环和模型执行函数非常有指导意义,这样我们就可以完全控制一切。在处理时间序列时,很容易对如何调用 LSTM 和 Dense 层等感到困惑,所以我提供这些简单的经验法则:
-
当从一个时间序列映射到另一个时间序列时(例如,不断从之前的运动预测未来的运动),您可以使用一个
Chain
并在循环中调用它;对于每个输入时间点,您输出另一个时间点。 -
当从一个时间序列映射到一个“输出”时(例如,将句子归纳为“快乐情绪”或“悲伤情绪”),您必须首先将所有数据嚼碎并将其减少到固定大小;您输入许多东西,但最后只出来一个。
我们将重新架构我们的模型分为两部分;首先是循环的“吃豆人”部分,我们将可变长度的时间序列嚼碎成预定长度的内部状态向量,然后是前馈部分,它接收内部状态向量并将其减少到单一输出:
pacman = Chain( LSTM(1, 128), # 从时间点大小 1 映射到 128 LSTM(128, 256), # 进一步扩展到 256 LSTM(256, 128), # 瓶颈回落到 128)reducer = Chain( Dense(128, 9), #softmax, # 现在暂时保持注释)
我们将其分成两部分的原因是问题陈述希望我们将一个可变长度的输入序列减少到一个单一数字;我们处于上面的第二个要点。因此,我们的代码自然必须考虑到这一点;我们将编写我们的 loss(x, y)
函数,而不是调用 model(x)
,它将执行吃豆人的舞蹈,然后在输出上调用 reducer。注意,我们还必须 reset!()
RNN 状态,以便每个独立的训练示例的内部状态被清除:
function loss(x, y) # 重置内部 RNN 状态,以便它不会从 # 上次调用 `loss()` 时“携带过来”。 Flux.reset!(pacman) # 迭代 `x` 中的每个时间点 for x_t in x y_hat = pacman(x_t) end # 取循环部分的最后一个输出,减少它 y_hat = reducer(y_hat) # 计算减少后的输出与 `y` 的差异 return Flux.mse(y_hat, y)end
将此输入到 Flux.train!()
实际上会进行训练,尽管效果不佳。;)
最后的观察
-
尽管您的数据都是
Int64
的,但使用浮点数处理除嵌入之外的所有内容是很常见的(嵌入是一种将非数字数据如字符或单词赋值的方法,有点像 ASCII);如果您处理的是文本,您几乎肯定会使用某种嵌入,而该嵌入将决定您的第一个 LSTM 的维度,此时您的输入都将是“独热”编码的。 -
softmax
用于预测概率;它将确保对于每个输入,输出都在[0...1]
之间,并且它们总和为1.0
,就像一个好的概率分布应该的那样。这在进行分类时最有用,当您想将网络的野生输出值[-2, 5, 0.101]
整理成可以说“我们有99.1%
的把握认为第二个类别是正确的,以及0.7%
的把握认为是第三个类别”。 -
在训练这些网络时,您通常希望一次通过网络批处理多个时间序列,以提高硬件效率;这既简单又复杂,因为一方面它只意味着您不是通过一个
Sx1
向量(其中S
是您的嵌入大小),而是通过一个SxN
矩阵,但另一方面它也意味着批处理内所有东西的时间步数必须匹配(因为SxN
在所有时间步中必须保持不变,所以如果批处理中的一个时间序列在其他任何序列之前结束,您不能只删除它,从而在批处理进行到一半时减少N
)。所以大多数人做的就是将他们的时间序列填充到相同长度。
祝您在机器学习之旅中好运!