提问人:René Chenard 提问时间:3/4/2023 最后编辑:René Chenard 更新时间:5/10/2023 访问量:283
SWI-Prolog中模式识别的滑动窗口方法
Sliding window method for pattern recognition in SWI-Prolog
问:
从算法上讲,我想找到最有说服力和最有效的方法,来计算SWI-Prolog中某些模式的出现次数。
目前,我的解决方案使用 DCG,如下所示:
count_occurrences(Pattern, List, Count) :-
phrase(Pattern, PatternList),
length(PatternList, PatternLength),
length(List, ListLength),
MaxIndex is ListLength - PatternLength,
findall(1, (
between(0, MaxIndex, Index),
sublist(List, Index, PatternLength, PatternList)
), Counts),
sum_list(Counts, Count).
sublist(List, Index, Length, Sublist) :-
length(Sublist, Length),
append(Prefix, Rest, List),
length(Prefix, Index),
append(Sublist, _, Rest).
rep(S, L) --> [], {L is 0} | [S], {L is 1} | [S], { L_1 is L - 1 }, rep(S, L_1), !.
open_rep(S, L) --> [v], rep(S, L), [v], !.
结果是这样的:
1 ?- count_occurrences(open_rep(n, 1), [v,n,v,n,v,v,v,v,v,v,n,v,v], Count).
Count = 3.
2 ?- count_occurrences(open_rep(n, 3), [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], Count).
Count = 1.
3 ?- count_occurrences(open_rep(n, 1), [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], Count).
Count = 2.
我对这个结果感到满意,但我认为这不是实现它的最有效方法,并希望得到一些帮助,使其计算速度更快。我认为使用 和 计算成本很高,但不知道没有它们如何做同样的事情......append
findall
这里显示的 DCG 只是一个示例,但我必须计算列表中模式的出现次数并给它们打分。这是在使用 Alpha-Beta 修剪为 Gomoku 实现 AI 的背景下进行的。由于经常对电路板进行评估,因此算法的复杂性很重要,以减少 AI 采取行动所需的时间。
我已经尝试了上面显示的代码的多种变体,但它们都使用谓词,我发现减少计算时间的最佳解决方案是实现早期失败。findall
答:
我认为,即使使用 谓词 ,您仍然可以有一个有效的解决方案(至少这是使用 swi-prolog 可以观察到的)。append/3
count(Pattern, List, Count) :-
phrase(Pattern, Sublist),
count(List, Sublist, 0, Count).
count([], _, Count, Count).
count([X|Xs], Sublist, Count0, Count) :-
( append(Sublist, _, [X|Xs])
-> Count1 is Count0 + 1
; Count1 is Count0 ),
count(Xs, Sublist, Count1, Count).
% To compare the two versions with large lists
random_list(0, []) :- !.
random_list(N, [X|Xs]) :-
random_member(X, [n,v]),
M is N-1,
random_list(M, Xs).
使用 swi-prolog 比较 和 :count/3
count_occurrences/3
?- random_list(1000,L), time(count(open_rep(n,1),L,C1)), time(count_occurrences(open_rep(n,1),L,C2)).
% 4,648 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
% 3,003,226 inferences, 1.578 CPU in 1.683 seconds (94% CPU, 1903034 Lips)
L = [v, n, v, v, n, n, n, n, v|...],
C1 = C2, C2 = 116.
?- random_list(2000,L), time(count(open_rep(n,1),L,C1)), time(count_occurrences(open_rep(n,1),L,C2)).
% 9,288 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
% 12,006,431 inferences, 11.812 CPU in 12.421 seconds (95% CPU, 1016417 Lips)
L = [n, n, v, v, n, n, n, v, n|...],
C1 = C2, C2 = 229.
?- random_list(1000000,L), time(count(open_rep(n,3),L,C1)).
% 4,905,979 inferences, 0.109 CPU in 0.146 seconds (75% CPU, 44854665 Lips)
L = [n, v, v, v, n, v, n, n, n|...],
C1 = 31246.
?- L = [v,n,v,n,v,v,v,v,v,v,n,v,v], time(count(open_rep(n,1),L,C1)), time(count_occurrences(open_rep(n,1),L,C2)).
% 77 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
% 571 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
L = [v, n, v, n, v, v, v, v, v|...],
C1 = C2, C2 = 3.
?- L = [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], time(count(open_rep(n,3),L,C1)), time(count_occurrences(open_rep(n,3),L,C2)).
% 99 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
% 641 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
L = [v, n, v, n, v, v, v, v, v|...],
C1 = C2, C2 = 1.
?- L = [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], time(count(open_rep(n,1),L,C1)), time(count_occurrences(open_rep(n,1),L,C2)).
% 86 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
% 739 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
L = [v, n, v, n, v, v, v, v, v|...],
C1 = C2, C2 = 2.
评论
IMO 您的方法过于具体,从(可重用性)的角度来看,它将是次优的。 SWI-Prolog 提供了一个执行 RLE(运行长度编码)的库谓词,正如我从这个有趣的主题中发现的那样,值得一试:在这里我将发布一个模块,我在其中复制了您的代码,以及一个使用 clumped/2 的替代方案:
/* File: x_so_sliding_window.pl
Author: Carlo
Created: Mar 4 2023
Purpose: https://stackoverflow.com/q/75630809/874024
*/
:- module(x_so_sliding_window,
[count_occurrences/3
,open_rep//2
,count_by_clumped/3
]).
count_occurrences(Pattern, List, Count) :-
phrase(Pattern, PatternList),
length(PatternList, PatternLength),
length(List, ListLength),
MaxIndex is ListLength - PatternLength,
findall(1, (
between(0, MaxIndex, Index),
sublist(List, Index, PatternLength, PatternList)
), Counts),
sum_list(Counts, Count).
sublist(List, Index, Length, Sublist) :-
length(Sublist, Length),
append(Prefix, Rest, List),
length(Prefix, Index),
append(Sublist, _, Rest).
rep(S, L) --> [], {L is 0} | [S], {L is 1} | [S], { L_1 is L - 1 }, rep(S, L_1), !.
open_rep(S, L) --> [v], rep(S, L), [v], !.
count_by_clumped(Pattern,List,Count) :-
clumped(List, R),
aggregate_all(count, member(Pattern,R), Count).
然后我在 x_so_sliding_window.plt 中有这个代码
:- [x_so_sliding_window].
t(Count) :- count_occurrences(open_rep(n, 1), [v,n,v,n,v,v,v,v,v,v,n,v,v], Count).
t(Count) :- count_occurrences(open_rep(n, 3), [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], Count).
t(Count) :- count_occurrences(open_rep(n, 1), [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], Count).
c(Count) :- count_by_clumped(n-1, [v,n,v,n,v,v,v,v,v,v,n,v,v], Count).
c(Count) :- count_by_clumped(n-3, [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], Count).
c(Count) :- count_by_clumped(n-1, [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v], Count).
乍一看,这显示了 T/1 和 C/1 调用之间的等价性:
?- t(Count).
Count = 3 ;
Count = 1 ;
Count = 2.
?- c(Count).
Count = 3 ;
Count = 1 ;
Count = 2.
最后,直接在 REPL 中编码的“基准”:
?- time((between(1,1000,_),(t(_),fail))).
% 1,953,001 inferences, 0.234 CPU in 0.234 seconds (100% CPU, 8332804 Lips)
false.
?- time((between(1,1000,_),(c(_),fail))).
% 123,001 inferences, 0.000 CPU in 0.014 seconds (0% CPU, Infinite Lips)
false.
评论
count_by_clumped(n-1, [v,n,v,n,v,v,v,v,v,v,n], 3)
count_occurrences(open_rep(n, 1), [v,n,v,n,v,v,v,v,v,v,n], 2)
下面是子列表的更好方法:
sublist1([H|T], Index, Length, Sublist) :-
sublist1_start_(T, H, 1, Index, Length, Sublist).
% P = previous
sublist1_start_(L, P, Ind, Index, Length, Sublist) :-
sublist1_loop_(L, P, Ind, Index, 1, Length, Sublist).
sublist1_start_([H|T], _, Ind, Index, Length, Sublist) :-
Ind1 is Ind + 1,
% Go to next element
sublist1_start_(T, H, Ind1, Index, Length, Sublist).
sublist1_loop_(_, H, Ind, Ind, Len, Len, [H]).
sublist1_loop_([H|T], P, Ind, Index, Len, Length, [P|Sublist]) :-
Len1 is Len + 1,
sublist1_loop_(T, H, Ind, Index, Len1, Length, Sublist).
swi-prolog 中的结果:
?- sublist1([a,b,c], Ind, Len, Sub).
Ind = Len, Len = 1,
Sub = [a] ;
Ind = 1,
Len = 2,
Sub = [a, b] ;
Ind = 1,
Len = 3,
Sub = [a, b, c] ;
Ind = 2,
Len = 1,
Sub = [b] ;
Ind = Len, Len = 2,
Sub = [b, c] ;
Ind = 3,
Len = 1,
Sub = [c].
如果希望索引从 0 而不是 1 开始,请将第 2 行中的 1 更改为 0。
我排除了空列表。
评论
你的问题中缺少一些东西,不知道是什么。我也对答案感到惊讶。
不确定是否必须使用列表,或者是否可以使用原子或字符串。原子和字符串有 sub_atom/5 和 sub_string/5 resp,可用于搜索。喜欢这个:
?- sub_atom(vnvnvvvvvvnvv, Before, Len, After, vnv).
Before = 0, Len = 3, After = 10 ;
Before = 2, Len = 3, After = 8 ;
Before = 9, Len = 3, After = 1 ;
false.
相同但带有字符串适用于 sub_string/5。
如果您已经在使用 SWI-Prolog,则可以使用 library(aggregate) 来计算解决方案:
?- aggregate_all(count, sub_atom(vnvnvvvvvvnvv, _,_,_, vnv), N).
N = 3.
?- aggregate_all(count, sub_atom(vnvnvvvvvvnnnvv, _,_,_, vnv), N).
N = 2.
到目前为止,您不需要编写任何程序。如果必须使用列表,则两个附加就足够了:
?- aggregate_all(
count,
( append(_, Back, [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v]),
append([v,n,n,n,v], _, Back)
),
N).
N = 1.
请记住,您还有正则表达式库库(pcre)。如果您使用文档中的示例代码:
:- use_module(library(pcre)).
re_match_count(Regex, String, N) :-
re_foldl(inc, Regex, String, 0, N, []).
inc(_, V0, V) :- V is V0 + 1.
您还可以使用原子或字符串进行计数:
?- re_match_count('vn{1}(?=v)', vnvnvvvvvvnvv, N).
N = 3.
?- re_match_count("vn{3}(?=v)", "vnvnvvvvvvnnnvv", N).
N = 1.
(注意:这些正则表达式使用“零宽度正展望”。他们最初被我厌烦,并在威尔·尼斯注意到错误后修复)。
在此示例中,有关匹配项本身的所有信息都将被丢弃,并且仅计算匹配项的数量。你可以用第一个参数做任何你觉得re_foldl/6的事情。
注意:您需要衡量什么对您最有效。如果有一种方法可以读取字符串并将其保存为 Prolog 字符串,并使用 library(pcre),这可能比其他方法更快。
最后,如果要使用 DCG 进行模式识别,可以使用以下代码“匹配任意位置”:
match_dcg(G) --> string(_), call(G), remainder(_).
现在,这完全忽略了之前和之后的内容,或者匹配在列表中的位置。你可以像这样使用它,有自己的open_rep//2:
?- aggregate_all(
count,
phrase(match_dcg(open_rep(n, 1)), [v,n,v,n,v,v,v,v,v,v,n,v,v]),
N).
N = 3.
此处定义的 match_dcg//1 与 phrase_from_file/2 文档中示例中的 match//1 之间的区别在于,match_dcg//1 将另一个 DCG 作为参数,而不是模式(子字符串)。
评论
re_match_count('vn{1}v', vnvnvvvvvvnvv, N), N=2
N=3
aggregate_all(count, sub_atom(vnvnvvvvvvnvv, _,_,_, vnv), N)
:- use_module(library(pcre)).
应用增长分析的经验顺序,您可以看到您的代码表现出二次行为,而各种答案的代码变体是线性的。
为什么?
这是因为您坚持强制中缀从已知索引开始,逐个增加索引。然后,中缀字符串由一对 s 找到,每次都从输入列表的开头重新开始。append
因此,该工作是以经典的三角形方式完成的(如链接条目中所示),导致有问题的二次行为。
但是两个 s 将自行执行此操作,此外,从上次处理的索引开始,而不是从索引 0 重新开始。他们不会从 0 开始并沿着输入列表到给定的索引,而是直接从继续;因此具有线性执行时间。append
i
i
为了用你的代码实现这一点,你只需要从中删除处理索引操作的每一位,它就会神奇地变成你在答案中看到的有效变体。
这让你
count_occurrences(Pattern, List, Count) :-
phrase(Pattern, PatternList),
/*length(PatternList, PatternLength),
length(List, ListLength),
MaxIndex is ListLength - PatternLength,*/
findall(1, (
/*between(0, MaxIndex, Index),*/
sublist(List, /*Index, PatternLength,*/ PatternList)
), Counts),
sum_list(Counts, Count).
sublist(List, /*Index, Length,*/ Sublist) :-
/*length(Sublist, Length),*/
append(_Prefix, Rest, List),
/*length(Prefix, Index),*/
append(Sublist, _, Rest).
正如预期的那样,它检查线性:
167 ?- random_list(1000,L),time(count_occurrences(open_rep(n,3),L,C)).
% 3,094 inferences, 0.000 CPU in 0.000 seconds (?% CPU, Infinite Lips)
L = [v, n, n, v, v, v, n, n, n|...],
C = 24.
168 ?- random_list(10000,L),time(count_occurrences(open_rep(n,3),L,C)).
% 30,514 inferences, 0.000 CPU in 0.003 seconds (0% CPU, Infinite Lips)
L = [n, n, v, v, v, v, v, n, v|...],
C = 280.
使用此处的另一个答案进行测试。random_list/2
现在,您可以将调用合并到一个谓词中,如该答案所示。不过,请务必使用实际数据测试这两种方法。使用库函数可能仍然更快。因此,也请尝试TA_intern答案中的方法。findall
sum_list
count
aggregate_all
少需求和控制,多成就!或
过早的优化实施是万恶之源。
评论
──◯─●─●─●─●───
──●─●───●─●───
──●─●─●─●─────
open_rep(n, 1), [v,n,v,n,v,v,v,v,v,v,n,n,n,v,v]
open_rep(n, 1)
[v,n,v,n,v,v,v,v,v,v,n,n,n,v,v]
[v,n,v]
[v,n,v,n,v,v,v,v,v,v,n,n,n,v,v]
[1,0,1,0,0,0,0,0,0,0,0,0,0,0]
2
5