Python numpy 를 Javascript 또는 NodeJS 측에서 읽어들이는 방법 - 1 - Whitmem
Python numpy 를 Javascript 또는 NodeJS 측에서 읽어들이는 방법 - 1
Python Programming
2025-04-25 22:21 게시 da5256930645370f93eb

0
0
32
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
Python numpy 를 Javascript 또는 NodeJS 측에서 읽어들이는 방법 - 1
이 게시글은 Python numpy 데이터를 Javascript 나 NodeJS 에서 읽어들이는 방법에 관한 것이다. 특히 이 게시글은 1차원 배열, 2차원 배열에 대해서만 정리한 것으로 더 많은 다차원의 경우 별도로 계산 및 연산이 필요할 수 있다. 이 게시글에서는 Python 에서 numpy 데이터를 저장하는 방법에 대해서 설명하고, 다음 게시글은 Javscript 에서 그 데이터를 읽어들여 로드하는 방법을 설명한다.
기본적으로 numpy 데이터 역시 메모리 위에서 저수준으로 관리되기 때문에 다른 언어나 라이브러리로 이식하기 위해서는 그 구조를 제대로 이해하고 있어야 한다. numpy 는 대용량의 수치 처리도 매우 빠르게 처리해주는데, 이는 모두 별도의 클래스나 인스턴스 없이 C/C++ 내부적으로 메모리 배열에 직접 접근하여 값을 조작하기 때문이다. 그렇기에 우리는 빅 데이터 또는 대용량 데이터 베이스를 numpy로 처리하더라도 빠르게 사용할 수 있는 것이다.
본인이 직접 구현해봤으면 알겠지만... 연산 속도를 최적화한다는 건 생각보다 쉽지 않다.
아무튼 본론으로 넘어와서, numpy 에서 어떤식으로 배열 정보를 관리하는지 바이너리 데이터를 분석해보자. 먼저 numpy 에서는 바이너리 데이터로 내보낼 수 있는 메서드를 제공하고 있는데, 원하는 배열안에서 tobytes() 를 호출하면 된다.
예를 들어 array 배열을 [[1, 2]]로 만들고 arr.tobytes() 를 호출하면 위와 같이 바이너리 데이터로 출력되는 것을 확인할 수 있다. 이 때 arr.dtype 을 출력해보면 int64로 할당된 것을 확인할 수 있다. int 64는 Integer 정수 타입인데, 한 정수당 64비트를 차지한다는 의미이다. 즉 64비트는 8바이트이므로 한 정수당 8바이트를 차지하는 메모리 공간을 사용한다고 볼 수 있다. 차원 정보인 shape에서 (1, 2)라면 1행 2열을 표현하기 위해 각 1행 안의 1 열 값이 8바이트를 차지하는 것이다.
결과적으로 dtype과 shape, 두 정보는 사실상 매우 중요한 정보이다. 메모리 배열이 존재하더라도 메모리가 어떻게 어떤 크기로 저장되는지에 대한 정보가 없으면 배열을 불러올 수 없기 때문이다. 또 바이트 배열을 쓰고 읽을 때 주의해야 할 사항이 하나 있는데 바로 바이트 정렬 순서이다. 바이트 정렬 순서는 말 그대로 바이트를 어떤 순서로 정렬할 것인지, 바이너리 값을 쓸 때 바이트 메모리 위치 순서를 의미한다.
위 array [[1,2]] 를 bytes 데이터로 내보냈을 때 "b'\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00'" 라는 값이 나오는데, 한 정수는 int64로 저장되기에 8개의 바이너리 데이터를 조각내면 된다.
앞에서(즉 0 byte offset 부터) 8 byte offset까지는 정수 1을 의미하고, 8 byte offset 부터 16 byte offset 까지는 정수 2를 의미한다. 흔히 우리는 정수 1이라는 값을 0x00000001 로, 정수 2라는 값을 0x00000002 로 알고 있는데, 바이트 순서가 정 반대로 0x10000000, 0x20000000 이다. 이 이유는 위에서 말한 바이트 정렬 순서에서 기인한다.
바이트 정렬 방식은 보통 빅 엔디안 리틀 엔디안 두 가지로 나뉜다.
빅 엔디안은 흔히 우리가 아는 형태인 0x12345678 사람이 읽는 순서대로 메모리 주소 공간에 저장하는 것을 의미한다. 반면 리틀 엔디안은 각 바이트를 낮은 메모리 주소부터 채워나가는 것을 의미한다. 이를 헥스 진수로 나타내면 0x87654321이 된다. 보통 네트워크 통신 과정에서는 빅 엔디안을 사용하고, 시스템은 보통 리틀 엔디안을 사용하는 경우가 많다. (이는 시스템이 사용하는 CPU 처리 장치에 따라 권장되는 바이트 정렬 방법이 다른데, 보통 x86 시스템은 리틀 엔디안이 기본 값이라고 한다.)
numpy 라이브러리은 시스템 기본 값을 따르기 때문에 여기서는 리틀 엔디안으로 지정되어 0x10000000 형태로 저장된 것이다. 이를 확인하기 위해서 array 의 dtype 내 byteorder 속성을 조회하면 된다.
arr.dtype.byteorder 을 출력해보면 '='가 표시되는데, 이는 시스템 기본 값을 따른다는 의미이다. 바이트 순서가 >인 경우 빅 엔디안이고, <인 경우 리틀 엔디안이다. 현재는 직접 정의한 것이 아닌, 시스템 기본 값을 따르기 때문에 '='로 표시된다. 임의로 빅 엔디안으로 강제하기 위해서는 dtype 을 지정해주면 된다.
dtype 을 >i8 로 명시함으로써 빅 엔디안이면서 Integer 8Byte 를 차지하도록 강제하였다. 이 상황에서 to bytes()로 메모리 복사본을 띄워보면 우리가 원하는대로 0x00000001, 0x00000002 가 표시된 것을 확인할 수 있다.
그러면 대량의 데이터를 빅엔디안 Float 32 바이너리 데이터 정보로 표시해보자.
위 arr는 난수 100개를 생성하여 2차원 배열에 저장하고 byte 로 저장하여 byte_result 에 기록한 예시이다. byte_result 에서 위 숫자 정보를 뽑아내기 위해서, 4바이트씩 나누고 빅엔디안으로 읽여들여 float로 불러오면 된다.
제대로 출력되는 것을 확인할 수 있다. 한편 64비트(8바이트)의 float로 저장한 경우는 iter_unpack 을 통해 가져올 때 double 형태 (64비트 소수)로 가져와야 한다.
제대로 불러와지는 것을 알 수 있다. 이로써 1차원 배열이 저장되는 방법과 이를 직접 바이너리 데이터로부터 불러오는 방법에 대해 알아보았는데, 여기서 끝은 아니다. 바로 행렬 정렬 방법이 남아있기 때문이다. 1차원 배열을 저장하고 불러올 때는 큰 문제가 되지 않지만 2차원 배열을 저장하고 불러올 때 순서 정보가 또 다르기 때문이다. 행을 먼저 저장할 것인지, 열을 먼저 저장할 것인지에 대한 순서 정보가 없기 때문이다.
예를 들어, 위 [1, 2] [3, 4] 배열을 저장할 때 행 우선으로 저장하면 1, 2, 3, 4 가 될 수도 있고 열 우선으로 저장하면 1, 3, 2, 4 가 될 수도 있다. numpy 라이브러리의 기본 값은 행 우선이므로 1, 2, 3, 4로 저장된다. 행 우선이라함은, 행을 고정해두고 안의 열을 우선 작성한다는 것이다. 즉 하나의 행을 먼저 메모리에 삽입한다고 보면된다.
좌표 (1차원, 2차원)이라고 가정했을 때 (0,0) (0,1) (1,0) (1,1) 와 같이 행을 먼저 고정해두고, 열로 들어가는 작업, 행 고정을 '행 우선'이라고 표현한다. 영 단어로는 row-major, column major 인데 이는 array의 flags 속성을 통해 확인할 수 있다.
C_CONTIGUOUS 가 활성 상태라면 행 우선이라고 볼 수 있다. F_CONTIGUOUS 가 활성 상태라면 열 우선이라고 볼 수 있다. 이 값 역시 array를 생성할 때 지정할 수 있는데 order 필드를 변경하면 된다. 다만 내보낼 때도 다시 별도로 order 을 지정해야 하는 것으로 보인다.
행 우선 (C)로 저장한 경우 1, 2, 3, 4 차곡 차곡 저장된 것을 확인할 수 있고, 열 우선 (F)로 저장한 경우 1, 3, 2, 4 순서대로 저장된 것을 확인할 수 있다. 그러면 2차원 배열을 생성한 뒤 저장하고, 이를 행 우선으로 가져와보자.
즉 원하는 데이터 형태로 저장한 뒤 헥스 데이터를 가져올 때,
'행 우선'의 경우 열 개수에 도달하면 새로운 열 공간에 삽입하면 된다.
위 생성한 arr 배열과 직접 불러온 loaded_arr는 100% 동일한 것을 확인할 수 있다. 이를 이제 Javascript 로 불러오기 위해서는 위 numpy 의 바이트 바이너리 데이터를 파일로 저장해야 한다. 이를 저장하기 위해서는 바이너리 데이터를 직접 file IO 에 쓸 수 있다.
그런데 굳이 이렇게 저장할 필요 없이, numpy 에서는 파일로 저장하는 메서드를 제공한다.
하지만 위 두 코드는 실제 로드하기에는 정보가 부족하다. 바로 배열이 어떤 차원을 가지고 있는지, 어떤 정렬 순서인지, 어떤 데이터형을 가지고 있는지를 포함하지 않기 때문이다. 단순히 데이터 영역만을 파일에 작성한다. 실제 4바이트 정수 2개를 파일로 읽어들여보면 'b'\x00\x00\x00\x01\x00\x00\x00\x02''와 같이 실제 데이터만 포함되어 있다. 이를 어느 단위로 조각내고 불러올지 확인하기 위해서는 shape, order, data type 정보는 필수이다. 따라서 이 정보를 같이 저장해주기 위해 우리만의 플래그를 만들어 파일의 최상단에 갖다 붙여야 한다. 물론 필요에 따라서 이러한 작업 없이 하드코딩 해도 된다. 단순히 하나의 파일만 가져오는 경우 굳이 바이너리 데이터 파일에 shape, order, data type 정보를 기입할 필요는 없다. 코드를 직접 작성하면서 형태, 순서, 데이터 타입을 써 넣어도 된다. 다만 다른 파일을 불러올 때 문제가 발생할 수 있다.
아무튼 필자의 경우 1) 차원 정보, 2) 메모리 개당 바이트 크기, 3) 메모리 정렬 순서, 4) 행 열 순서를 차례로 파일의 헤더에 작성하고자 한다. 다시 한번 말하지만 이 사항은 필수가 아니다. 하드 코딩하는 경우 필요 없고, 또는 텍스트 파일, 데이터 베이스에 저장하는 경우에도 필요 없다. 필자는 바이너리 데이터 공간에 같이 헤더로 작성하려고 하기 때문에 이 같은 작업을 진행하는 것이다. 이러한 파일 헤더 구조는 다음 게시글에서 작성하는 것으로 한다.
댓글 0개
댓글은 일회용 패스워드가 발급되며 사이트 이용 약관에 동의로 간주됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.