テキストファイルから2D配列をmmapする方法

2020-05-18 python numpy mmap

正の整数の2D配列を含む非常に大きなファイルがあります

  • 各ファイルにはマトリックスが含まれています

ファイルをメモリに読み込まずに処理したいと思います。幸い、入力ファイルの左から右に値を確認するだけで済みます。各ファイルをmmapして、実際にファイルをメモリに読み込むことなく、メモリ内にあるかのように処理できるようにしたいと思っていました。

小さいバージョンの例:

[[2, 2, 6, 10, 2, 6, 7, 15, 14, 10, 17, 14, 7, 14, 15, 7, 17], 
 [3, 3, 7, 11, 3, 7, 0, 11, 7, 16, 0, 17, 17, 7, 16, 0, 0], 
 [4, 4, 8, 7, 4, 13, 0, 0, 15, 7, 8, 7, 0, 7, 0, 15, 13], 
 [5, 5, 9, 12, 5, 14, 7, 13, 9, 14, 16, 12, 13, 14, 7, 16, 7]]

そのようなファイルをmmapnp.int64値を次のように処理できるようにすることは可能ですか?

for i in range(rownumber):
    for j in range(rowlength):
        process(M[i, j])

明確にするために、入力ファイルが収まらないため、すべての入力ファイルをメモリに保持したくありません。

Answers

これは、Pythonでmmapモジュールが行うこととまったく同じです。参照: https : //docs.python.org/3/library/mmap.html

ドキュメントの例

import mmap

# write a simple example file
with open("hello.txt", "wb") as f:
    f.write(b"Hello Python!\n")

with open("hello.txt", "r+b") as f:
    # memory-map the file, size 0 means whole file
    mm = mmap.mmap(f.fileno(), 0)
    # read content via standard file methods
    print(mm.readline())  # prints b"Hello Python!\n"
    # read content via slice notation
    print(mm[:5])  # prints b"Hello"
    # update content using slice notation;
    # note that new content must have same size
    mm[6:] = b" world!\n"
    # ... and read again using standard file methods
    mm.seek(0)
    print(mm.readline())  # prints b"Hello  world!\n"
    # close the map
    mm.close()

更新された回答

コメントと説明に基づいて、角かっこがたくさん入ったテキストファイルがあり、その長さは約4行で、1行あたり1,000,000,000のASCII整数がコンマで区切られているようです。あまり効率的な形式ではありません!ファイルを前処理して角かっこ、改行、スペースをすべて削除し、コンマを改行に変換して、1行あたり1つの値を簡単に処理できるようにすることをお勧めします。

trコマンドを使用して音訳すると、次のようになります。

# Delete all square brackets, newlines and spaces, change commas into newlines
tr -d '[] \n' < YourFile.txt | tr , '\n' > preprocessed.txt

ファイルは次のようになり、Pythonで一度に1つの値を簡単に処理できます。

2
2
6
10
2
6
...
...

Windowsを使用している場合は、 GNUWin32 LinuxツールとLinux用のWindowsサブシステム(git bash?)でtrツールを使用できます。

さらに進んで、私の答えの2番目の部分のようにmemmap()ができるファイルを作成すると、ファイル内の任意のバイトをランダムに見つけることができます。したがって、上記で作成したpreprocessed.txtを使用して、次のようなバイナリバージョンを作成できます。

import struct

# Make binary memmapable version
with open('preprocessed.txt', 'r') as ifile, open('preprocessed.bin', 'wb') as ofile:
    for line in ifile:
        ofile.write(struct.pack('q',int(line)))

元の回答

このようにできます。最初の部分はセットアップです:

#!/usr/bin/env python3

import numpy as np

# Create 2,4 Numpy array of int64
a = np.arange(8, dtype=np.int64).reshape(2,4)

# Write to file as binary
a.tofile('a.dat')

シェルで16進ダンプしてファイルを確認します。

xxd a.dat

00000000: 0000 0000 0000 0000 0100 0000 0000 0000  ................
00000010: 0200 0000 0000 0000 0300 0000 0000 0000  ................
00000020: 0400 0000 0000 0000 0500 0000 0000 0000  ................
00000030: 0600 0000 0000 0000 0700 0000 0000 0000  ................

これですべての設定がmemmap() 、ファイルをmemmap()ましょう。

# Memmap file and access values via 'mm'
mm = np.memmap('a.dat', dtype=np.int64, mode='r', shape=(2,4))

print(mm[1,2])      # prints 6

主な問題は、ファイルが大きすぎて、行に分割されていないように見えることです。 (参考までに、array.txtは指定した例であり、arr_map.datは空のファイルです)

import re
import numpy as np 

N = [str(i) for i in range(10)]
arrayfile = 'array.txt'
mmapfile = 'arr_map.dat'
R = 4
C = 17
CHUNK = 20

def read_by_chunk(file, chunk_size=CHUNK):
    return file.read(chunk_size)

fp = np.memmap(mmapfile, dtype=np.uint8, mode='w+', shape=(R,C)) 

with open(arrayfile,'r') as f:
    curr_row = curr_col = 0
    while True:
        data = read_by_chunk(f)
        if not data:
            break

        # Make sure that chunk reading does not break a number
        while data[-1] in N:
            data += read_by_chunk(f,1)

        # Convert chunk into numpy array
        nums = np.array(re.findall(r'[0-9]+', data)).astype(np.uint8)
        num_len = len(nums)

        if num_len == 0:
            break

        # CASE 1: Number chunk can fit into current row
        if curr_col + num_len <= C: 
            fp[curr_row, curr_col : curr_col + num_len] = nums

            curr_col = curr_col + num_len

        # CASE 2: Number chunk has to be split into current and next row
        else: 
            col_remaining = C-curr_col
            fp[curr_row, curr_col : C] = nums[:col_remaining] # Fill in row i

            curr_row, curr_col = curr_row+1, 0                # Move to row i+1 and fill the rest
            fp[curr_row, :num_len-col_remaining] = nums[col_remaining:]

            curr_col = num_len-col_remaining

        if curr_col>=C:
            curr_col = curr_col%C
            curr_row += 1

        #print('\n--debug--\n',fp,'\n--debug--\n')

基本的に、一度に配列ファイルの小さな部分を読み取り(数字を壊さないように注意してください)、正規表現を使用してカンマや角括弧などのジャンク文字から数字を見つけ、その数字をメモリマップに挿入します。

入力マトリックスで実行する操作によって異なります。それがマトリックス操作の場合は、 部分マトリックスを使用できます。ほとんどの場合、入力ファイルの小さなバッチを部分マトリックスとして部分的に処理できます。このようにして、ファイルを非常に効率的に処理できます。入力を読み取って部分的に処理し、結果をキャッシュするアルゴリズムを開発する必要があります。一部の操作では、入力行列の最適な表現を決定する必要がある場合があります(つまり、 行メジャーまたは列メジャー )。

部分行列アプローチを使用する主な利点は、 CまたはC ++に精通している場合、たとえばCUDA GPUを使用して、各反復でn 部分行列を処理するために並列処理技術を適用することで、 Python Cを使用できることです。 APIを使用すると、部分行列演算の時間の複雑さが大幅に改善される可能性がありますが、 Numpyを使用して部分行列を処理するだけでよいため、 Pythonを使用してもそれほど悪くはありません。

あなたが説明する状況は、ファイルから次の整数または次の行をフェッチして、それを処理できるジェネレーターに適しているようです。

def sanify(s):
    while s.startswith('['):
        s = s[1:]
    while s.endswith(']'):
        s = s[:-1]
    return int(s)


def get_numbers(file_obj):
    file_obj.seek(0)
    i = j = 0
    for line in file_obj:
        for item in line.split(', '):
            if item and not item.isspace():
                yield sanify(item), i, j
                j += 1
        i += 1
        j = 0

これにより、メモリに常駐するのは一度に1行だけになります。

これは次のように使用できます。

import io


s = '''[[2, 2, 6, 10, 2, 6, 7, 15, 14, 10, 17, 14, 7, 14, 15, 7, 17], 
[3, 3, 7, 11, 3, 7, 0, 11, 7, 16, 0, 17, 17, 7, 16, 0, 0], 
[4, 4, 8, 7, 4, 13, 0, 0, 15, 7, 8, 7, 0, 7, 0, 15, 13], 
[5, 5, 9, 12, 5, 14, 7, 13, 9, 14, 16, 12, 13, 14, 7, 16, 7]]'''


items = get_numbers(io.StringIO(s))
for item, i, j in items:
    print(item, i, j)

マトリックスの任意の要素にアクセスできるようにしたい場合は、上記のロジックを__getitem__を実装するクラスに適応させることができ、各行の先頭の位置を追跡するだけで済みます。 コードでは、これは次のようになります。

class MatrixData(object):
    def __init__(self, file_obj):
        self._file_obj = file_obj
        self._line_offsets = list(self._get_line_offsets(file_obj))[:-1]
        file_obj.seek(0)
        row = list(self._read_row(file_obj.readline()))
        self.shape = len(self._line_offsets), len(row)
        self.length = self.shape[0] * self.shape[1]


    def __len__(self):
        return self.length


    def __iter__(self):
        self._file_obj.seek(0)
        i = j = 0
        for line in self._file_obj:
            for item in _read_row(line):
                    yield item, i, j
                    j += 1
            i += 1
            j = 0


    def __getitem__(self, indices):
        i, j = indices
        self._file_obj.seek(self._line_offsets[i])
        line = self._file_obj.readline()
        row = self._read_row(line)
        return row[j]


    @staticmethod
    def _get_line_offsets(file_obj):
        file_obj.seek(0)
        yield file_obj.tell()
        for line in file_obj:
            yield file_obj.tell()


    @staticmethod
    def _read_row(line):
        for item in line.split(', '):
            if item and not item.isspace():
                yield MatrixData._sanify(item)


    @staticmethod
    def _sanify(item, dtype=int):
        while item.startswith('['):
            item = item[1:]
        while item.endswith(']'):
            item = item[:-1]
        return dtype(item)


class MatrixData(object):
    def __init__(self, file_obj):
        self._file_obj = file_obj
        self._line_offsets = list(self._get_line_offsets(file_obj))[:-1]
        file_obj.seek(0)
        row = list(self._read_row(file_obj.readline()))
        self.shape = len(self._line_offsets), len(row)
        self.length = self.shape[0] * self.shape[1]


    def __len__(self):
        return self.length


    def __iter__(self):
        self._file_obj.seek(0)
        i = j = 0
        for line in self._file_obj:
            for item in self._read_row(line):
                    yield item, i, j
                    j += 1
            i += 1
            j = 0


    def __getitem__(self, indices):
        i, j = indices
        self._file_obj.seek(self._line_offsets[i])
        line = self._file_obj.readline()
        row = list(self._read_row(line))
        return row[j]


    @staticmethod
    def _get_line_offsets(file_obj):
        file_obj.seek(0)
        yield file_obj.tell()
        for line in file_obj:
            yield file_obj.tell()


    @staticmethod
    def _read_row(line):
        for item in line.split(', '):
            if item and not item.isspace():
                yield MatrixData._sanify(item)


    @staticmethod
    def _sanify(item, dtype=int):
        while item.startswith('['):
            item = item[1:]
        while item.endswith(']'):
            item = item[:-1]
        return dtype(item)

として使用する:

m = MatrixData(io.StringIO(s))

# get total number of elements
len(m)

# get number of row and col
m.shape

# access a specific element
m[3, 12]

# iterate through
for x, i, j in m:
    ...

Related