191214 说明:
很抱歉,突然发现图中第三行多画了一列叉,事实上,生成 output(0,0) 数据只用到了input[:,0] 以及 weights[0,:]。比较懒,就不再画了,图中第三行的第一个矩阵应该和第二行的第一个矩阵相同。
此外至于评论区中有人提到得到的结果一样。为此我做了一个小实验,验证经过一步简单优化后,模型参数之间的差异。使用的代码如下:
import torch
import torch.nn as nn
from copy import deepcopy
SMALL = 1e-7
class Model(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv1d(3, 2, 1)
self.linear = nn.Linear(3, 2)
params = deepcopy(self.conv.state_dict())
params['weight'] = params['weight'].reshape(2, 3)
self.linear.load_state_dict(params)
self.optim0 = torch.optim.Adam(lr=0.5, params=self.conv.parameters())
self.optim1 = torch.optim.Adam(lr=0.5, params=self.linear.parameters())
def test(self, tensor):
assert (self.conv.weight.flatten().eq(self.linear.weight.flatten())).all(), 'Initial weights are different'
assert (self.conv.bias.flatten().eq(self.linear.bias.flatten())).all(), 'Initial biases are different'
# get out tensor
c_out = self.conv(tensor.transpose(1, 2)).transpose(1, 2)
l_out = self.linear(tensor)
# set optim target
c_target = c_out.sum(2).mean()
l_target = l_out.sum(2).mean()
# apply one step optimization
self.optim0.zero_grad()
c_target.backward()
self.optim0.step()
self.optim1.zero_grad()
l_target.backward()
self.optim1.step()
# if conv1d(kernel=1) behaves the same as linear,
# their parameters should be the same after applying
# one-step optimization
a = self.conv.weight.data.flatten()
b = self.linear.weight.data.flatten()
c = self.conv.bias.data.flatten()
d = self.linear.bias.data.flatten()
assert torch.add(
a, -b).abs().lt(SMALL).all(), 'After a step, weights are different'
assert torch.add(
c, -d).abs().lt(SMALL).all(), 'After a step, biases are different'
if __name__ == "__main__":
gen = torch.Generator()
for seed in range(1000):
gen.manual_seed(seed)
tensor = torch.rand((10, 3, 3), generator=gen)
model = Model()
model.test(tensor)
代码做的事情主要是:1)给定随机种子,生成随机的 tensor,2)建模并使得两个矩阵的初始参数相等。3)用 Adam 进行一次简单的优化,并比较优化后的参数。
这里做最后的参数比较的时候,用 torch.eq 是肯定无法通过的,经过观察,发现最后的weights 确实很接近。经过几次测试发现,两者在一步优化后,在 1e-6 误差内可以通过测试,但是无法通过 1e-7 误差的测试。
因此,这里有两种可能,第一,这个weights的误差仅仅是因为计算误差导致的,pytorch 在计算两者的时候,本质上是一样。第二,pytorch 在计算 conv1d 的时候确实如文档说的使用了cross-relation operation,但是这个operation在简单的case中,带来的gradients和linear确实存在微小的区别,从而使得其行为不一样。但是得说明的是,经过一个大 N 级别的优化过程,conv1d 和 linear 带来的区别会是显着的。因此,最起码的,在使用 pytorch 进行计算时,不可将两者视为等同。
通过本次实验以及基于自己在复现 VRP-RL 的经验,我偏向认为 conv1d 的行为和 linear 是不同的。欢迎进一步讨论。
-------
最近在复现VRP下的DRL算法,当考虑C个顾客的问题,以及batch的大小为N,相应的地图数据的shape是(N, C, 2),其中第三维分别存储物理坐标(x,y)信息。
原文使用Conv1d with kernel_size=1来作为encoder,将原始数据映射到embedding_size=M的维度上去,得到数据形状为(N, C, M)。
作为一个调包侠,从来都只在乎输入和输出的形状,怎么方便怎么来。因为pytorch的Conv1d的API的输入数据需要将1维和2维调换,即(N, 2, C),觉得麻烦,而且误以为kernel=1的时候的Conv1d和Linear是完全一样的,然后就顺手用了一个Linear Layer 去做为embedding。唯一的区别仅仅在于这个encoder的选择,结果就是和benchmark对比,花费时间更长且效果更差。
然后去Stack Overflow上面去找找看答案,发现遇到这种问题的不仅我一个(见帖子),这里就根据pytorch的API一起探索一下conv1d(kernel=1)和linear分别究竟做了什么,以及产生区别的原因。
首先我们看最简单的Linear layer如下,
然后我们看Conv1d的API如下
有兴趣的小伙伴可以推导一下公式,不难发现,假设考虑都是叉乘操作,结合轴变化,当kernel=1的时候,Conv1d和Linear的output中各元素是共源的,也就是说,对于entry(i,j),生成他们数据的原始数据来源是一样的,结果应该没什么区别。神级画手只能帮到这里了(1为Linear,2为假设的Conv1d,3为实际的Conv1d):
如此这般,那问题很可能就出在集结方式了!Linear这边,确实就是普通的叉乘操作。这时候,Conv1d中红色的这个cross-correlation很可能就是问题的关键了。wiki和百度链接如下,具体计算公式可以在链接里面找到:
wikiwww.wikiwand.comInsignal processing, cross-correlationis ameasure of similarityof two series as a function of the displacement of one relative to the other. This is also known as a slidingdot productor sliding inner-product.互相关函数_百度百科baike.baidu.com
互相关函数是信号分析里的概念,表示的是两个时间序列之间的相关程度,即描述信号 x (t),y (t) 在任意两个不同时刻 t1,t2 的取值之间的相关程度。描述两个不同的信号之间的相关性时,这两个信号可以是随机信号,也可以是确知信号。
这不就是明摆着这个操作会体现两个序列间的相关性吗?在VRP问题中,相当于提前集结了x和y的信息,并通过学习weights与历史信息中其他点作对比,大致估计当前点所在的位置,对于VRP问题,各个物理点x以及y之间的相关程度对于问题的求解是一个很重要的信息,当两个位置的坐标更相近的时候,更有可能考虑这两点相连,因此,使用Conv1d layer对问题的求解有更好的帮助。
小结,当同组数据中,各数据在每一维度的相关性比较重要的时候,Conv1d能提取这些数据并反映出来,这是普通的Linear layer做不到的。同时,做一个naive的调包侠是不可取的,仔细研究每一个API的内部运行机制才能避免写低效甚至错误的代码。