MATLAB中如何动态更改for循环

8

当设置for循环时,我希望能够循环遍历未知数量的参数。

通过输入文件,用户可以设置尽可能多或尽可能少的循环参数,而我想要能够无论他们设置了多少个参数都能循环遍历它们。循环输入示例:(请注意,我的输入也可以是字符串和数字组合的列表)

案例1:

  • 重量 45000:5000:75000
  • 高度 10000
  • 速度 0.2:0.1:0.9

案例2:

  • 重量 30000
  • 高度 1000:1000:10000
  • 襟翼 10,20
  • 温度 -10:1:10

列表的长度可能不同,并且可以包含从0到15个变量。我知道解决办法,但使用一堆嵌套的for循环非常混乱。我正在寻找一种方法,也许是用递归来设置一个适当的for循环系统,无论涉及多少个参数,仍然能跟踪这些变量。


2
你是否正在寻找类似于这个链接所提供内容:http://stackoverflow.com/a/34840222/2732801? - Daniel
2
我会解析输入文件并将结果作为参数传递给函数。 - sco1
不需要进行任何解析,只需编写代码以使用最大数量的循环(例如,在重量、高度、速度、温度、襟翼等方面进行循环)。在某个参数保持不变的情况下,它将有效地不进行循环,而只是设置索引一次并移动到下一个循环。 - transversality condition
@横截面条件 我会适当地解析我的数据并将其作为结构发送到循环中。我的当前设置基本上就是您所描述的,但我正在寻找更优雅和灵活的解决方案 - 如果添加了我没有预见到的新变量怎么办?我希望代码能够无缝处理它。 - arthream
@Daniel,乍一看,该解决方案似乎非常适合我的情况,但我仍然需要通过各种可能出现的情况进行更多测试。非常感谢! - arthream
显示剩余2条评论
4个回答

2

代码生成的解决方案

嗯,你已经有了许多相当好的解决方案。我将介绍一种涉及代码生成的方法。MATLAB并没有太多这样的工具,但你可以通过几个循环和 fprintf 来模拟它。下面是我的代码生成脚本:

s = struct() ;
s.weight = 45000:5000:75000 ;
s.altitude = 10000 ;
s.engine = {'ge','rolsroyce'} ;

h = fopen('thisrun.m','w+') ;

mydisp = @(varargin)disp(transpose(varargin(:))) ; % dummy function body

vars = fields(s) ;
nv = numel(vars) ;

for ii = 1:nv
    if isnumeric(s.(vars{ii}))
        lb = '(' ;
        rb = ')' ;
    else
        lb = '{' ;
        rb = '}' ;
    end
    fprintf(h,'for i%g = 1:numel(s.(vars{%g})) \n',ii,ii) ;
    fprintf(h,'i%gval = s.(vars{%g})%si%g%s ; \n',ii,ii,lb,ii,rb) ;
end

fprintf(h,'mydisp(') ;
for ii = 1:numel(vars)
    fprintf(h,'i%gval',ii) ;
    if ii<nv
        fprintf(h,',') ;
    end
end
fprintf(h,') ; \n') ;

for ii = 1:nv
    fprintf(h,'end \n') ;
end

fclose(h) ;
run thisrun.m

生成的代码(thisrun.m):


for i1 = 1:numel(s.(vars{1}))
    i1val = s.(vars{1})(i1) ;
    for i2 = 1:numel(s.(vars{2}))
        i2val = s.(vars{2})(i2) ;
        for i3 = 1:numel(s.(vars{3}))
            i3val = s.(vars{3}){i3} ;
            mydisp(i1val,i2val,i3val) ;
        end
    end
end

运行生成代码的结果:

>>
    [45000]    [10000]    'ge'

    [45000]    [10000]    'rolsroyce'

    [50000]    [10000]    'ge'

    [50000]    [10000]    'rolsroyce'

    [55000]    [10000]    'ge'

    [55000]    [10000]    'rolsroyce'

    [60000]    [10000]    'ge'

    [60000]    [10000]    'rolsroyce'

    [65000]    [10000]    'ge'

    [65000]    [10000]    'rolsroyce'

    [70000]    [10000]    'ge'

    [70000]    [10000]    'rolsroyce'

    [75000]    [10000]    'ge'

    [75000]    [10000]    'rolsroyce'

代码生成需要时间,但如果您需要多次运行该文件,它仍可能是一个高效的解决方案。

1

递归。

你没有对速度有任何要求。这个答案很慢,内存使用不佳,但是它是这个想法的最简单的实现。存在许多更好的实现,它们更加复杂,使用的内存更少,并且显着更快。这取决于你需要内部循环有多紧密...

parms.weight = [45000:5000:75000];
parms.alt    = 10000;
parms.Speed  = 0.2:0.1:0.9;

然后,定义你的模拟器,例如:
function result = simulation(parms)
    fieldNames = fieldnames(parms)
    result = [];
    for ix = 1 : numel(fieldNames)
       if 1 < numel(parms.(fieldNames{ix})) 
           list = parms.(fieldNames{ix});
           for jx = 1 : numel(list)
                tmpParms = parms;
                tmpParms.(fieldNames{ix}) = list(jx);
                tmpResult = simulation(tmpParms);
                result = [result; tmpResult];
           end
           return;
        end
     end

     if 0 == numel(result)
         % Do the real simulation here.
     end

2
我经常面临这个问题,但出于某种原因,你的问题激发了我全新的思考方式。非常好的问题。 - John
2
你的代码有问题,会生成过多的组合。请尝试输入 struct('a',[1 2],'b',[3],'c',[4,5],'e',[6,7])。本应只产生 6 次模拟调用,但实际上却产生了更多的调用。 - Daniel
1
@Daniel,谢谢。你说得对,有一个bug。已经修复了。顺便说一下,根据你的输入,最终答案中会有8个结果。注意:调用simulation的不同次数将大于8。 - John

1
这是一个递归函数,用于将所有可能的参数集作为数组的列向量生成:
function idx=dynloop(varargin)
    if nargin==1
        idx=varargin{1};
    else
        idx=dynloop(varargin{2:end});
        idx=[repelem(varargin{1},size(idx,2));
             repmat(idx,[1,length(varargin{1})])];
    end
return
示例输出:循环遍历5:7,然后是8,最后是[2,3,5,7]:
 >> idx = dynloop(5:7,8,[2,3,5,7])

idx =

     5     5     5     5     6     6     6     6     7     7     7     7
     8     8     8     8     8     8     8     8     8     8     8     8
     2     3     5     7     2     3     5     7     2     3     5     7

输出与ndgrid相当(除了在我的观点中更直观地形成)。 (此外,一些初步的基准测试显示这可能比ndgrid稍微快一点...?待确定) 主循环:现在只需使用一个循环来迭代idx的列。请注意,正如@Daniel在OP上评论的那样,此解决方案概念类似于this solution
Weight   = 45000:5000:75000;
Altitude = 10000;
Speed    = 0.2:0.1:0.9;

idx = dynloop(Weight,Altitude,Speed);

for ii=1:size(idx,2)
    params = idx(:,ii);

    %// do stuff with params! 
    %// ...

end

编辑:处理字符串和数字输入

对我之前发布的递归函数进行简单修改,使其可以返回索引而不是实际元素,因此字符串单元格也可以正常处理:

function idx=dynloop(varargin)
    if nargin==1
        idx=1:length(varargin{1});
    else
        idx=dynloop(varargin{2:end});
        idx=[repelem(1:length(varargin{1}),size(idx,2));
             repmat(idx,[1,length(varargin{1})]);];
    end
return

因此,您的函数应该按照以下方式运作:
function yourMainFcn(varargin)
    idx=dynloop(varargin{:});
    for ii=1:size(idx,2)
        params = cellfun(@(x,k) numORcell(x,k),...
                 varargin,num2cell(idx(:,ii).'),... %//'
                 'UniformOutput',0);

        %// do stuff with params! 
        %// ...
        disp(params)

    end
end

函数 numORcell 适当地解析数值和单元格数据:

function y=numORcell(x,k)
    if iscell(x)
        y=x{k};
    else
        y=x(k);
    end
end

字符串示例:

Weight   = 45000:5000:75000;
Altitude = 10000;
Speed    = 0.2:0.1:0.9;
Names    = {'Foo','Bar'};

>> yourMainFcn(Names,Altitude,Weight)
'Foo'    [10000]    [45000]

'Foo'    [10000]    [50000]

'Foo'    [10000]    [55000]

'Foo'    [10000]    [60000]

'Foo'    [10000]    [65000]

'Foo'    [10000]    [70000]

'Foo'    [10000]    [75000]

'Bar'    [10000]    [45000]

'Bar'    [10000]    [50000]

'Bar'    [10000]    [55000]

'Bar'    [10000]    [60000]

'Bar'    [10000]    [65000]

'Bar'    [10000]    [70000]

'Bar'    [10000]    [75000]

或者使用以下代码:
>> yourMainFcn(Names,Names,Speed,Names)
'Foo'    'Foo'    [0.2]    'Foo'

'Foo'    'Foo'    [0.2]    'Bar'

'Foo'    'Foo'    [0.3]    'Foo'

'Foo'    'Foo'    [0.3]    'Bar'

'Foo'    'Foo'    [0.4]    'Foo'

...

'Foo'    'Foo'    [0.9]    'Bar'

'Foo'    'Bar'    [0.2]    'Foo'

...

'Foo'    'Bar'    [0.9]    'Bar'

'Bar'    'Foo'    [0.2]    'Foo'

...

'Bar'    'Foo'    [0.9]    'Bar'

...

'Bar'    'Bar'    [0.8]    'Foo'

'Bar'    'Bar'    [0.8]    'Bar'

'Bar'    'Bar'    [0.9]    'Foo'

'Bar'    'Bar'    [0.9]    'Bar'
读者需要自己完成的练习:如果将所有下标存储在idx中会导致内存问题,那么你肯定正在进行一些相当复杂的循环。不过,你可以创建一个函数来确定从当前索引集到下一个字典序索引集的方法,这意味着你只需要存储一个索引集,并在每次循环结束时进行迭代即可。

有趣。我还应该指定我的输入参数可以包括字符串和字母数字值,而ndgrid不是最好的方法,对于字符串输入..除非我想出一个复杂的方案,在大型列表中索引字符串选项。但关键是复杂的。 让未来的用户理解代码是优先考虑的。 - arthream
@arthream我的解决方案已经被修改以处理字符串输入。它不需要你自己索引字符串选项。希望对你有所帮助。 - Geoff
非常感谢!这个编辑应该可以很好地工作,除了我正在运行MATLAB 2014b版本(repelem是在2015a引入的),而我的代码应该兼容至少到2012a。这绝对是有见地的,我会记住的! - arthream
@arthream 你可以很容易地解决 repelem 的问题:reshape(repmat(1:length(varargin{1}),[size(idx,2),1]),[1,length(varargin{1})*size(idx,2)]) ... 看起来很复杂,但如果你从内到外理解它,希望你能明白它的意义。 - Geoff

1

对于所有参数进行嵌套的for循环是简单的解决方案,常量参数将仅导致特定循环的单个迭代。

您还可以使用ndgrid创建一组多维数组,这些数组是每次调用computeFunction(...)的参数,并且迭代ngrid的输出而不是为每个潜在可变参数构建嵌套的for循环,如果您期望更改参数数量。

但是,请注意,它可能会带来性能成本,因为根据您在哪个matlab版本中执行的操作,它可能无法从内置优化中受益于“简单”for循环版本。


1
你提出了一个非常有用的关于性能的观点。非常感谢您的建议! - arthream
1
此外,如有错误请指正,ndgrid似乎仅限于数值类型,这对我的情况没有帮助。我可以为字符串输入构建索引查找,但这会让代码变得更加复杂,而我并不想这样做。 - arthream
1
@arthream:这种解决方案的主要缺点是,它会建立一个包含完整参数空间的矩阵(每个组合),可能会使用太多内存。 - Daniel
1
@arthream:使用字符串的要求是你应该在问题中提到的!请更新你的问题。 - Daniel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接