计算数百GB数据的子序列

时间:2016-03-24 13:54:52

标签: perl memory substring large-files

我试图处理一个非常大的文件并计算文件中某个长度的所有序列的频率。

为了说明我在做什么,请考虑一个包含序列abcdefabcgbacbdebdbbcaebfebfebfeb的小输入文件

下面,代码读取整个文件,并获取长度为n的第一个子字符串(低于我将其设置为5,尽管我希望能够更改它)并计算其频率:

abcde => 1

下一行,它会向右移动一个字符并执行相同操作:

bcdef => 1

然后继续其余的字符串并打印5个最常见的序列:

open my $in, '<', 'in.txt' or die $!; # 'abcdefabcgbacbdebdbbcaebfebfebfeb'

my $seq = <$in>; # read whole file into string
my $len = length($seq);

my $seq_length = 5; # set k-mer length
my %data;

for (my $i = 0; $i <= $len - $seq_length; $i++) {
     my $kmer = substr($seq, $i, $seq_length);
     $data{$kmer}++;
}

# print the hash, showing only the 5 most frequent k-mers
my $count = 0;
foreach my $kmer (sort { $data{$b} <=> $data{$a} } keys %data ){
    print "$kmer $data{$kmer}\n";
    $count++;
    last if $count >= 5;
}
ebfeb 3
febfe 2
bfebf 2
bcaeb 1
abcgb 1

但是,我想找到一种更有效的方法来实现这一目标。如果输入文件是10GB或1000GB,那么将整个内容读入字符串将非常昂贵。

我考虑过按字符块读取,一次说100个并按上述步骤进行,但是在这里,跨越2个块的序列将无法正确计算。

我的想法是,只读取字符串中的n个字符,然后移动到接下来的n个字符并执行相同的操作,如上所述在哈希中计算它们的频率。

  • 对于我如何做到这一点有什么建议吗?我使用偏移量看了read,但是我无法理解如何将其合并到此处
  • substr是此任务最有效的内存工具吗?

3 个答案:

答案 0 :(得分:5)

从您自己的代码看,您的数据文件看起来只有一行数据 - 不会被换行符分解 - 所以我假设在我的解决方案中。即使该行有可能在末尾有一个换行符,最后选择五个最常见的子序列也会将其抛出,因为它只发生一次

该程序使用sysread从文件中获取任意大小的数据块,并将其附加到内存中已有的数据

循环的主体与你自己的代码大致相似,但是我使用了for的列表版本而不是C风格的版本,因为它更清晰

在处理完每个块之后,在循环的下一个循环从文件中提取更多数据之前,内存中的数据被截断为最后SEQ_LENGTH-1个字节

我还使用常量来表示K-mer大小和块大小。毕竟它们是不变的!

输出数据是在CHUNK_SIZE设置为7的情况下生成的,因此会出现许多跨境子序列的实例。它匹配您自己所需的输出,但最后两个条目的计数为1.这是因为Perl的哈希键的固有随机顺序,如果您需要具有相同计数的特定顺序的序列,那么您必须指定它以便我可以改变排序

use strict;
use warnings 'all';

use constant SEQ_LENGTH => 5;           # K-mer length
use constant CHUNK_SIZE => 1024 * 1024; # Chunk size - say 1MB

my $in_file = shift // 'in.txt';

open my $in_fh, '<', $in_file or die qq{Unable to open "$in_file" for input: $!};

my %data;
my $chunk;
my $length = 0;

while ( my $size = sysread $in_fh, $chunk, CHUNK_SIZE, $length ) {

    $length += $size;

    for my $offset ( 0 .. $length - SEQ_LENGTH ) {
         my $kmer = substr $chunk, $offset, SEQ_LENGTH;
         ++$data{$kmer};
    }

    $chunk = substr $chunk, -(SEQ_LENGTH-1);
    $length = length $chunk;
}

my @kmers = sort { $data{$b} <=> $data{$a} } keys %data;
print "$_ $data{$_}\n" for @kmers[0..4];

输出

ebfeb 3
febfe 2
bfebf 2
gbacb 1
acbde 1

注意我们通过$chunk = substr $chunk, -(SEQ_LENGTH-1);循环时设置$chunk的行while。这可确保正确计算跨越2个块的字符串。

$chunk = substr $chunk, -4语句从当前块中删除除最后四个字符之外的所有字符,以便下一个读取从文件中追加CHUNK_SIZE个字节到剩余的字符。这样搜索将继续,但是除了下一个块之外,还会从前一个块的最后4个字符开始:数据不会落入块之间的“裂缝”。

答案 1 :(得分:4)

即使您在处理之前没有将整个文件读入内存,您仍然可能会耗尽内存。

10 GiB文件包含近11E9序列。

如果你的序列是从一组5个字符中选择的5个字符的序列,那么只有5个 5 = 3,125个独特序列,这很容易适合记忆。

如果您的序列是从一组5个字符中选择的20个字符的序列,则有5个 20 = 95E12个唯一序列,因此10 GiB文件的所有11E9序列都是唯一的。这不符合记忆。

在这种情况下,我建议您执行以下操作:

  1. 创建一个包含原始文件所有序列的文件。

    以下以块的形式读取文件,而不是一次读取所有文件。棘手的部分是处理跨越两个块的序列。以下程序使用RewriteEngine on ##IF host==subdomain.domain.com## RewriteCond %{HTTP_HOST} ^sudomain\.domain\.com$ ##And uri==/## RewriteCond %{REQUEST_URI} ^/$ ##redirect to http://subdomain.domain.com/Contreller/method## RewriteRule ^ http://subdomain.domain.com/Controller/method [NC,L,R] [1] 从文件中获取任意大小的数据块,并将其附加到先前读取的块的最后几个字符。最后一个细节允许计算跨越块的序列。

    sysread
  2. 对序列进行排序。

    perl -e'
       use strict;
       use warnings qw( all );
    
       use constant SEQ_LENGTH => 20;
       use constant CHUNK_SIZE => 1024 * 1024;
    
       my $buf = "";
       while (1) {
          my $size = sysread(\*STDIN, $buf, CHUNK_SIZE, length($buf));
          die($!) if !defined($size);
          last if !$size;
    
          for my $offset ( 0 .. length($buf) - SEQ_LENGTH ) {
             print(substr($buf, $offset, SEQ_LENGTH), "\n");
          }
    
          substr($buf, 0, -(SEQ_LENGTH-1), "");
       }
    ' <in.txt >sequences.txt
    
  3. 计算每个序列的实例数,并将计数与序列一起存储在另一个文件中。

    sort sequences.txt >sorted_sequences.txt
    
  4. 按计数对序列进行排序。

    perl -e'
       use strict;
       use warnings qw( all );
    
       my $last = "";           
       my $count;
       while (<>) {
          chomp;
          if ($_ eq $last) {
             ++$count;
          } else {
             print("$count $last\n") if $count;
             $last = $_;
             $count = 1;
          }
       }
    ' sorted_sequences.txt >counted_sequences.txt
    
  5. 提取结果。

    sort -rns counted_sequences.txt >sorted_counted_sequences.txt
    

    这也打印了第五名的领带。

  6. 这可以通过调整传递给perl -e' use strict; use warnings qw( all ); my $last_count; while (<>) { my ($count, $seq) = split; last if $. > 5 && $count != $last_count; print("$seq $count\n"); $last_count = $count; } ' sorted_counted_sequences.txt [2] 的参数来优化,但它应该提供不错的性能。

    1. sort比之前建议的sysread更快,因为后者在内部执行一系列4 KiB或8 KiB读取(取决于您的Perl版本)。

    2. 鉴于序列的固定长度性质,您还可以将序列压缩为ceil(log 256 (5 20 ))= 6字节然后base64-将它们编码为ceil(6 * 4/3)= 8个字节。这意味着每个序列需要12个字节,大大减少了读取和写入的数量。

    3. 此答案的部分内容已由用户改编自内容:622310在cc by-sa 3.0下获得许可。

答案 2 :(得分:2)

一般来说,Perl在逐个字符处理解决方案上的速度非常慢,就像上面发布的那样,它在正常表达式上更快很多,因为基本上你的开销主要是你执行的运算符数量

因此,如果你能把它变成一个更好的基于正则表达式的解决方案。

这是尝试这样做:

$ perl -wE 'my $str = "abcdefabcgbacbdebdbbcaebfebfebfeb"; for my $pos (0..4) { $str =~ s/^.// if $pos; say for $str =~ m/(.{5})/g }'|sort|uniq -c|sort -nr|head -n 5
  3 ebfeb
  2 febfe
  2 bfebf
  1 gbacb
  1 fabcg

即。我们在$ str中有我们的字符串,然后我们将它传递5次,生成5个字符的序列,在第一遍之后我们开始从字符串前面切掉一个字符。在很多语言中,由于你不得不重新分配整个字符串,所以真的慢,但perl会为这种特殊情况作弊而只是将字符串的索引设置为1+以上它在此之前。

我没有对此进行基准测试,但是我打赌这样的事情比上面的算法更可行,你也可以通过递增哈希(使用/ e正则表达式)在perl中进行uniq计数选项可能是最快的方法),但我只是在这个实现中卸载| sort | uniq -c,这可能更快。

略有改变的实现,在perl中执行此操作:

$ perl -wE 'my $str = "abcdefabcgbacbdebdbbcaebfebfebfeb"; my %occur; for my $pos (0..4) { substr($str, 0, 1) = "" if $pos; $occur{$_}++ for $str =~ m/(.{5})/gs }; for my $k (sort { $occur{$b} <=> $occur{$a} } keys %occur) { say "$occur{$k} $k" }'
3 ebfeb
2 bfebf
2 febfe
1 caebf
1 cgbac
1 bdbbc
1 acbde
1 efabc
1 aebfe
1 ebdbb
1 fabcg
1 bacbd
1 bcdef
1 cbdeb
1 defab
1 debdb
1 gbacb
1 bdebd
1 cdefa
1 bbcae
1 bcgba
1 bcaeb
1 abcgb
1 abcde
1 dbbca

背后代码的漂亮格式:

my $str = "abcdefabcgbacbdebdbbcaebfebfebfeb";
my %occur;
for my $pos (0..4) {
    substr($str, 0, 1) = "" if $pos;
    $occur{$_}++ for $str =~ m/(.{5})/gs;
}

for my $k (sort { $occur{$b} <=> $occur{$a} } keys %occur) {
    say "$occur{$k} $k";
}