我有一个大约50,000个用户的数组引用。我想通过所有这些用户并将每个用户与其他所有用户进行比较,以便建立一个加权的匹配列表(如果名称是完全匹配,则值x,部分匹配值等)。
在完成列表并完成所有检查之后,我想要获得10个最高加权匹配。以下是我正在做的帮助解释的一个例子:
#!/usr/bin/perl
######################################################################
# Libraries
# ---------
use strict;
use warnings;
my $users = [];
$users->[0]{'Name'} = 'xxx';
$users->[0]{'Address'} = 'yyyy';
$users->[0]{'Phone'} = 'xxx';
$users->[1]{'Name'} = 'xxx';
$users->[1]{'Address'} = 'yyyy';
$users->[1]{'Phone'} = 'xxx';
$users->[2]{'Name'} = 'xxx';
$users->[3]{'Address'} = 'yyyy';
$users->[4]{'Phone'} = 'xxx';
foreach my $user_to_check (@$users) {
my $matched_users = [];
foreach my $user (@$users) {
$user_to_check->{'Weight'} = 0;
if (lc($user_to_check->{'Name'}) eq lc($user->{'Name'})) {
$user_to_check->{'Weight'} = ($user_to_check->{'Weight'} + 10);
} elsif ((length($user_to_check->{'Name'}) > 2) && (length($user->{'Name'}) > 2) && ($user_to_check->{'Name'} =~ /\Q$user->{'Name'}\E/i)) {
$user_to_check->{'Weight'} = ($user_to_check->{'Weight'} + 5);
}
if (lc($user_to_check->{'Address'}) eq lc($user->{'Address'})) {
.....
}
if ($user_to_check->{'Weight'} > 0) {
# We have matches, add to matched users
push (@$matched_users,$user);
}
}
# Now we want to get just the top 10 highest matching users
foreach my $m_user (sort { $b->{'Weight'} <=> $a->{'Weight'} } @$matched_users ) {
last if $counter == 10;
.... # Do stuff with the 10 we want
}
}
问题是,它太慢了。运行需要一天多的时间(我已经在多台机器上试过了)。我知道“排序”是一个杀手,但我也尝试将结果插入到tmp mysql表中,然后在最后而不是进行Perl排序,我只是通过选择做了一个订单,但时间的差异非常大次要的。
由于我正在浏览现有的数据结构并进行比较,因此我不确定我能做些什么(如果有的话)来加快速度。我很感激任何建议。
答案 0 :(得分:15)
您将@$users
中的每个元素与其中的每个元素进行比较。那是5E4²= 2.5E9的比较。例如,您不需要将元素与自身进行比较。您也不需要将元素与已比较的元素进行比较。即在这个比较表中
X Y Z
X - + +
Y - - +
Z - - -
只需将三个比较与每个元素进行比较。您正在进行的九项比较是66%的必要(渐近:50%不需要)。
您可以通过循环索引来实现此目的:
for my $i (0 .. $#$users) {
my $userA = $users->[$i];
for my $j ($i+1 .. $#$users) {
my $userB = $users->[$j];
...;
}
}
但这意味着在匹配时,您必须增加两个匹配用户的权重。
您将每个用户的名称小写1E5次。这是1E5 - 1倍!只需为每个元素执行一次,可能是在数据输入时。
作为旁注,你不应该执行小套,你应该做案例折叠。这可以通过fc
功能至少获得v16。当你有非英语数据时,只需小写就会出错。
use feature 'fc'; # needs v16
$user->[NAME] = fc $name;
或
use Unicode::CaseFold;
$user->[NAME] = fc $name;
哈希很快,因为查找需要不变的时间。但是单个哈希查找比数组访问更昂贵。由于您只有一组小的预定义字段,因此您可以使用以下技巧来使用类似哈希的数组:
使用映射到索引的字段名称声明一些常量,例如
use constant {
WEIGHT => 0,
NAME => 1,
ADDRESS => 2,
...;
};
然后将数据放入数组:
$users->[0][NAME] = $name; ...;
您可以访问
等字段$userA->[WEIGHT] += 10;
虽然这看起来像一个哈希,但这实际上是一种安全的方法,只能以最小的开销访问数组的某些字段。
嗯,它们非常快,但有一种更好的方法来确定字符串是否是另一个字符串的子字符串:使用index
。即。
$user_to_check->{'Name'} =~ /\Q$user->{'Name'}\E/i
可以写成
(-1 != index $user_to_check->{Name}, $user->{Name})
假设两者都已经小写案例折叠。
修改:您对问题的修改似乎无效。这假设您试图找到一些全局相似性,而不是为每个用户获得一组好的匹配
实现这些想法会使你的循环看起来像
for my $i (0 .. $#$users) {
my $userA = $users->[$i];
for my $j ($i+1 .. $#$users) {
my $userB = $users->[$j];
if ($userA->[NAME] eq $userB->[NAME]) {
$userA->[WEIGHT] += 10;
$userB->[WEIGHT] += 10;
} elsif ((length($userA->[NAME]) > 2) && (length($userB->[NAME]) > 2))
$userA->[WEIGHT] += 5 if -1 != index $userA->[NAME], $userB->[NAME];
$userB->[WEIGHT] += 5 if -1 != index $userB->[NAME], $userA->[NAME];
}
if ($userA->[ADDRESS] eq $userB->[ADDRESS]) {
..... # More checks
}
}
}
my (@top_ten) = (sort { $b->[WEIGHT] <=> $a->[WEIGHT] } @$users)[0 .. 9];
您展示的任务是高度可并行化的。如果你有内存,在这里使用线程很容易:
my $top10 = Thread::Queue->new;
my $users = ...; # each thread gets a copy of this data
my @threads = map threads->create(\&worker, $_), [0, int($#$users/2)], [int($#$users/2)+1, $#users];
# process output from the threads
while (defined(my $ret = $top10->dequeue)) {
my ($user, @top10) = @$ret;
...;
}
$_->join for @threads;
sub worker {
my ($from, $to) = @_;
for my $i ($from .. $to) {
my $userA = $users->[$i];
for $userB (@$users) {
...;
}
my @top10 = ...;
$top10->enqueue([ $userA, @top10 ]); # yield data to the main thread
}
}
您应该通过队列返回输出(如此处所示),但在线程内尽可能多地处理。通过更高级的工作负载分区,应该产生与可用处理器一样多的线程。
但是如果任何类型的流水线操作,过滤或缓存都可以减少嵌套循环中所需的迭代次数,那么你应该进行这样的优化(想想map-reduce风格的编程)。
我们基本上正在做的是计算我们的记录匹配的好的矩阵,例如
X Y Z
X 9 4 5
Y 3 9 2
Z 5 2 9
如果我们假设 X类似于Y 暗示 Y类似于X ,那么矩阵是对称的,我们只需要它的一半:
X Y Z
X \ 4 5
Y \ 2
Z \
这样的矩阵相当于加权的无向图:
4 X 5 | X – Y: 4
/ \ | X – Z: 5
Y---Z | Y – Z: 2
2 |
因此,我们可以优雅地将其表示为哈希的散列:
my %graph;
$graph{X}{Y} = 4;
$graph{X}{Z} = 5;
$graph{Y}{Z} = 2;
但是,这种散列结构意味着一个方向(从节点X
到节点Y
)。为了更容易查询数据,我们也可以包括另一个方向(由于哈希的实现,这不会导致大量内存增加)。
$graph{$x}{$y} = $graph{$y}{$x} += 2;
因为每个节点现在只连接到那些类似的节点,所以我们不必对50,000条记录进行排序。对于第100条记录,我们可以获得十个最相似的节点,如
my $node = 100;
my @top10 = (sort { $graph{$node}{$b} <=> $graph{$node}{$a} } keys %{ $graph{$node} })[0 .. 9];
这会将实现更改为
my %graph;
# build the graph, using the array indices as node ID
for my $i (0 .. $#$users) {
my $userA = $users->[$i];
for my $j ($i+1 .. $#$users) {
my $userB = $users->[$j];
if ($userA->[NAME] eq $userB->[NAME]) {
$graph{$j}{$i} = $graph{$i}{$j} += 10;
} elsif ((length($userA->[NAME]) > 2) && (length($userB->[NAME]) > 2))
$graph{$j}{$i} = $graph{$i}{$j} += 5
if -1 != index $userA->[NAME], $userB->[NAME]
or -1 != index $userB->[NAME], $userA->[NAME];
}
if ($userA->[ADDRESS] eq $userB->[ADDRESS]) {
..... # More checks
}
}
}
# the graph is now fully populated.
# do somethething with each top10
while (my ($node_id, $similar) = each %graph) {
my @most_similar_ids = (sort { $similar->{$b} <=> $similar->{$a} } keys %$similar)[0 .. 9];
my ($user, @top10) = @$users[ $node_id, @most_similar_ids ];
...;
}
以这种方式构建图形应该花费原始迭代的一半时间,并且如果每个节点的平均边缘数足够低,那么通过相似的节点应该会快得多。
并行化这有点困难,因为每个线程生成的图必须在查询数据之前进行组合。为此,每个线程最好执行上述代码,但迭代边界作为参数给出,并且只生成一个边。这对边缘将在组合阶段完成:
THREAD A [0 .. 2/3] partial
\ graph
=====> COMBINE -> full graph -> QUERY
/ partial
THREAD B [2/3 .. 1] graph
# note bounds recognizing the triangular distribution of workload
但是,如果给定节点的节点非常少,这只会很有用,因为组合很昂贵。