深度学习中模块组合

论文:FACTS: Fully Automated Correlated Time Series Forecasting in Minutes

在FACTS这篇论文中,有这么一个逻辑:给定一定数量的模块,随机排列组合,用一定的操作连接。

代码中操作是这样定义的:

python 复制代码
PRIMITIVES = [
    # 'none',
    'skip_connect',  # 0
    'NLinear',  # 1

    'cnn',  # 2
    'dcc_1',  # 3
    'inception',  # 4

    'gru',  # 5
    'lstm',  # 6

    'diff_gcn',  # 7
    'mix_hop',  # 8

    'trans',  # 9
    'informer',  # 10
    'convformer',  # 11

    's_trans',  # 12
    's_informer',  # 13
    'masked_trans',  # 14
]

那么具体如何把模块与操作组织起来呢?

这里用到了拓扑排序。

(以下是对论文代码的个人理解,代码见 exps/collect_seeds.py)

思路分三步:把操作排序,把节点(模块)排序,然后把操作插入节点

1.把操作排序

为操作定义一个数组 comb = [0,3,1,2]

这里表示第0种操作有0个,第1种操作有3个,第2种操作有1个,第三种操作有2个

但是这些操作不一定按这种顺序来,这里就用到了不重复的全排列。

比如comb = [2,1,1],第0种操作有2个,第1种操作有1个,第2种操作有1个,排列后为

0,1,2,0\] \[0,2,1,0\] \[0,0,1,2\] \[0,0,2,1\] \[0,1,0,2\] \[0,2,0,1

具体算法分析可以看这篇文章:python实现简单去重全排列

这就是操作的所有可能

代码

python 复制代码
def permuteUnique(nums):#去重排列
    ans = [[]]
    for n in nums:
        ans = [l[:i] + [n] + l[i:]
               for l in ans
               for i in range((l + [n]).index(n) + 1)]
    return ans



def get_combs(num_ops, splits):

    all_combs = []
    for split in splits:
        if num_ops < len(split):
            continue
        fill_length = num_ops - len(split)
        filled_split = [0] * fill_length + split
        perm_split = permuteUnique(filled_split)
        all_combs.extend(perm_split)
    return all_combs  #所有操作数组

2.把节点排序(找到前驱节点)

论文代码中用了get_all_tops(4),表明默认4个节点

对这四个节点进行排列,这里默认节点都是按顺序编好号的,因此这里用数字0,1,2...分别代表第0个,第1个,第2个节点,相当于已经有了顺序。整个神经网络是有输入输出的,没有环,因此要保证后面的节点前面连着的,一定是编号较小的节点,这里称为前驱节点。

比如:3的前驱节点可以是0,1,2

现在如果有四个节点[0,1,2,3],那么排列可能是这样的:

python 复制代码
topos = [
    [0,0,0],
    [0,0,1],
    [0,1,0],
    [0,1,1],
    [0,1,2]
]

注意这里的理解,是指的节点所在的下标要大于在这个下标的节点的编号

然后我们对这个节点前后连接的情况进行保存,保存哪些内容呢?节点、前驱节点和它们之间的操作。我们可以这样表示:(前驱节点,操作)(自身节点,操作)

这两个信息来表达一个节点,比如:

python 复制代码
geno = [
  (0,0), (0,0), 
  (1,0), (1,0), 
  (2,0), (2,0), 
  (3,0)
]

实际上的节点排序就是编号 0 1 2 3 ,其中(0,0)

这里操作先用0来占位,后续是会替换的

代码

python 复制代码
def get_all_topos(num_nodes):
    def get_topos(arr1, arrlist):
        if arrlist:
            string = []
            for x in arr1:
                for y in arrlist[0]:
                    string.append(x + [y])
            result = get_topos(string, arrlist[1:])
            return result
        else:
            return arr1

    buckets = [list(range(i)) for i in range(1, num_nodes)]
    topos = get_topos([buckets[0]], buckets[1:])
    return topos #获得所有符合前驱节点的排列


def get_base_genos(topos):
    genos = []
    for topo in topos:
        geno = [(0, 0)]
        for i in range(len(topo)):
            geno.append((topo[i], 0))
            geno.append((i + 1, 0)) #增加占位
        genos.append(geno)
    return genos  

关于为什么要插入占位,gpt这样解释的:

DARTS 风格规则:

  • 每个中间节点必须有两条输入边。

  • 即使拓扑只给了一个前驱,也要补齐第二条边。

(i+1, 0) 不是节点连自己,而是一个占位符, 保证每个节点统一有两个输入槽位。

既然只是占位,那为什么用i+1呢?其实是无所谓的,因为后面没有用到这个地方,只是在prenode1 = base_geno[2*i-1][0]这里,用2*i-1表示下标与上面的op1 = arch[2*i-1]对应起来(具体见下面的代码)

3.把操作插入节点

也就是把上述的操作0转化为实际的操作序号,并为节点补充好其他的输入节点

代码

python 复制代码
def get_archs(comb):

    op_list = []
    for i in range(len(PRIMITIVES)):
        op_list += [i] * comb[i]
    all_archs = permuteUnique(op_list)

    topos = get_all_topos(4)
    base_genos = get_base_genos(topos)

    all_genos = []
    for arch in all_archs:          # 一种"操作序列"的候选
        for base_geno in base_genos:# 一种"拓扑骨架"的候选
            geno = []
            for i in range(args.steps):   # i=0..steps-1,对应目标节点 1..steps
                if i == 0:
                    op = arch[i]                         # 取第0个操作
                    prenode = base_geno[i][0]            # 取第0个目标节点的"拓扑前驱"
                    geno.extend([(prenode, op)])         # 节点1只有一条输入
                else:
                    op1 = arch[2 * i - 1]                # 节点(i+1)的第1条边的操作
                    op2 = arch[2 * i]                    # 节点(i+1)的第2条边的操作
                    prenode1 = base_geno[2 * i - 1][0]   # 从"拓扑骨架"里取到这一节点的前驱
                    prenode2 = i                         # 另一路固定来自"前一个节点" i
                    geno.extend([(prenode1, op1), (prenode2, op2)])
            all_genos.append(geno)

    random.shuffle(all_genos)
    return all_genos

op_list 是通过将每个操作索引 i 重复 comb[i] 次生成的列表,例如如果 comb = [2, 1],则 op_list = [0, 0, 1]

steps表示中间节点的数目,整个模块包含: 输入节点-中间节点-输出节点。

对于输入节点,是i==0的情况,只有一个前驱输入

对于中间节点,会有两个前驱输入,一个是上文提到的拓扑前驱,一个是来自前一个节点i。这里我理解的是,这里节点的顺序是123,不是从0开始,因此第一个中间节点时,i==1,1对应的第一个节点,也就是刚刚的输入节点。

这里似乎没有单独处理输出节点

最后打乱all_genos,消除顺序偏差,确保评估公平性

整体流程

get_combs(num_ops, splits)

└── permuteUnique() # 生成操作数量分配的所有排列

comb (某个操作分配方案)

get_archs(comb)

├── permuteUnique(op_list) # 枚举操作顺序

└── get_all_topos(num_nodes) # 枚举拓扑连接方式

get_base_genos(topos) # 把拓扑转成占位基因

组合 arch + base_geno

输出 all_genos(完整基因)

这里都是查过gpt后自己的理解,如有错误,欢迎指正!

相关推荐
IMA小队长5 小时前
VS2022运行openCV报错:应用程序无法正常启动(0xc000279)
人工智能·opencv·计算机视觉
wan5555cn5 小时前
AI生成内容的版权问题解析与实操指南
人工智能·笔记·深度学习·算法·音视频
catcfm5 小时前
MiniDrive:面向自动驾驶的更高效的视觉语言模型
人工智能·深度学习·语言模型·自动驾驶
腾讯云大数据5 小时前
IDC MarketScape:腾讯云位居国内生成式AI数据基础设施“领导者”象限
人工智能·云计算·腾讯云
我有一颗五叶草5 小时前
告别 “无效阅读”!2025 开学季超赞科技书单,带孩子解锁 AI、编程新技能
人工智能·科技
地平线开发者6 小时前
理想汽车智驾方案介绍 4 World model + 强化学习重建自动驾驶交互环境
人工智能·自动驾驶·汽车
whaosoft-1436 小时前
51c自动驾驶~合集20
人工智能
年年测试6 小时前
在LangChain中无缝接入MCP服务器扩展AI智能体能力
服务器·人工智能·langchain
飞哥数智坊6 小时前
一个 TRAE 巨好用的隐藏功能:任务完成通知
人工智能·trae