Note: see the link below for the English version of this article.

https://duongnt.com/read-numpy-files-in-csharp

đọc file numpy bằng CSharp

Như đã giới thiệu trong bài trước, chúng ta có thể tải và chạy model Tensorflow bằng C#. Trong một số trường hợp, ta còn cần phải đọc dữ liệu từ file Numpy với định dạng npy hoặc npz. Ví dụ: trong một project xử lý tiếng nói, tôi cần đọc giá trị trung bình và phương sai của Mel Cepstrum Envelope từ file npz. Trong bài hôm nay, chúng ta sẽ tìm hiểu cách dùng thư viện NumSharp để đọc file Numpy với C#. Bài hôm này được viết dựa trên 2 câu trả lời của tôi trên Stackoverflow, các bạn có thể tìm đọc tại đâyđây.

Các bạn có thể cài NumSharp bằng lệnh dưới đây.

dotnet add package NumSharp --version 0.30.0

Đọc file Numpy với format npz

Đọc mảng một chiều chứa dữ liệu integer

Đầu tiên, chúng ta dùng Python để tạo file thử nghiệm.

import numpy as np

my_data = np.random.randint(0, 255, size=(250)) # Sinh một mảng với 250 phần tử; mỗi phần tử là một số nguyên trong khoảng 0~254
np.savez_compressed('data.npz', test_data=my_data) # Đặt tên mảng là 'test_data' và lưu nó vào file 'data.npz'

Code C# để đọc data.npz là như sau.

using NumSharp;

var dict = np.Load_Npz<int[]>("data.npz"); // Kiểu dữ liệu của 'dict' là NpzDictionary<int[]>
var myData = dict["test_data.npy"]; // Phần '.npy' ở đây là cần thiết, và ta cần dùng 'npy' thay vì 'npz'

Kiểu dữ liệu của myDataint[], ta có thể dùng index để đọc dữ liệu từ nó.

Console.WriteLine(myData[0]); // Xuất giá trị đầu tiên ra console

Ở đây, hàm Load_Npz đọc dữ liệu từ file, nhưng hàm này cũng có thể đọc dữ liệu từ stream hoặc byte array.

Đọc mảng một chiều chứa dữ liệu không phải kiểu integer

Nếu kiểu dữ liệu trong data.npz không phải là integer thì sao? Giả sử ta tạo file npz bằng đoạn code dưới đây.

my_data = np.random.rand(250) # Sinh mảng với 250 phần tử. Kiểu dữ liệu ở đây là np.double (float64)

Trong trường hợp này, code C# sẽ là như sau.

var dict = np.Load_Npz<double[]>("data.npz"); // Bây giờ T là 'double[]'
var myData = dict["test_data.npy"]; // Kiểu dữ liệu của myData là 'double[]'

Có thể thấy là ta chỉ cần thay đổi kiểu dữ liệu generic của Load_Npz từ int[] thành double[]. Bảng dưới đây chứa một số kiểu dữ liệu thường gặp trong Numpy và giá trị tương ứng của C#.

Kiểu dữ liệu của Numpy Kích thước Kiểu dữ liệu của C#
numpy.byte 8 bit byte
numpy.single 32 bit float
numpy.double 64 bit double

Đọc mảng đa chiều

Trong thực tế, ta thường phải làm việc với dữ liệu ở dạng mảng đa chiều. Ví dụ: đoạn code dưới đây sẽ tạo một mảng 3 chiều với định dạng là (250, 250, 3) và kiểu dữ liệu là double.

my_data = np.random.rand(250, 250, 3)
np.savez_compressed('data.npz', test_data=my_data)

Code để đọc file npz mới này thực chất rất giống với code đọc mảng một chiều.

var dict = np.Load_Npz<double[,,]>("data.npz"); // Kiểu của T bây giờ là 'double[,,]' (mảng 3 chiều với kiểu double)
var myData = dict["test_data.npy"]; // Kiểu dữ liệu của myData là 'double[,,]'

Ta cũng chỉ cần thay đổi kiểu dữ liệu generic của Load_Npz. Ở đây, dữ liệu của ta là mảng 3 chiều, vì thế ta dùng double[,,]. Nếu dữ liệu của ta là mảng 2 chiều thì ta sẽ dùng double[,],…

Console.WriteLine(myData[0, 0, 0]); // Xuất giá trị đầu tiên ra console

Có lẽ các bạn cũng đã đoán được cách đọc mảng 3 chiều với dữ liệu integer.

var dict = np.Load_Npz<integer[,,]>("data.npz"); // Kiểu của T là 'integer[,,]'
var myData = dict["test_data.npy"]; // Kiểu của myData là 'integer[,,]'

Đọc file npz chứa nhiều mảng

Ta có thể lưu nhiều mảng vảo trong cùng một file npz. Bởi vì mỗi mảng đều có tên riêng nên ta có thể đọc từng mảng thông qua tên của chúng.

my_data1 = np.random.rand(250, 250, 3)
my_data2 = np.random.rand(250, 250, 3)
my_data2 = np.random.rand(250, 250, 3)
np.savez_compressed('data.npz', test_data1=my_data1, test_data2=my_data2, test_data3=my_data3)
var dict = np.Load_Npz<integer[,,]>("data.npz");
var myData1 = dict["test_data1.npy"];
var myData2 = dict["test_data2.npy"];
var myData3 = dict["test_data3.npy"];

Nếu ta không đặt tên cho mảng trong file npz thì sao?

Như đã thấy ở trên, ta cần dùng tới tên của mảng khi đọc nó từ NpzDictionary. Nhưng ta có thể bỏ qua bước đặt tên khi lưu mảng vào trong file npz.

np.savez_compressed('data.npz', my_data)

Lúc này, Numpy sẽ tự động đặt tên cho mảng của ta là arr_0. Vì vậy, code C# của ta sẽ là như sau.

var dict = np.Load_Npz<integer[,,]>("data.npz");
var myData = dict["arr_0.npy"]; // Nhớ là phải có cả phần `.npy`

Nếu như file npz chứa nhiều mảng thì tên của chúng sẽ lần lượt là arr_0, arr_1, arr_2,…

np.savez_compressed('data.npz', my_data1, my_data2, my_data3)

Và code C# sẽ như sau.

var dict = np.Load_Npz<integer[,,]>("data.npz");
var myData1 = dict["arr_0.npy"];
var myData2 = dict["arr_1.npy"];
var myData3 = dict["arr_2.npy"];

Đọc file Numpy với format npy

Format npz rất hữu ích vì nó cho phép ta lưu nhiều mảng trong cùng một file; đồng thời nó nén nhỏ lưu lượng file. Nhưng cũng có lúc ta cần đọc file npy. NumSharp có hàm Load<T> để hỗ trợ việc này.

Dưới đây là code Python để tạo dữ liệu chạy thử.

my_data = np.random.rand(250)
np.save('data.npy', my_data)

Và đây là code C# để đọc data.npy.

var myData = np.Load<double[]>("data.npy"); // Kiểu dữ liệu của myData là 'double[]'

File npy chỉ chứa một array duy nhất và array đó không được đặt tên. Để đọc mảng đa chiều hoặc mảng với kiểu dữ liệu khác, ta chỉ cần thay đổi giá trị generic của hàm Load<T>, bước này cũng giống như khi ta gọi hàm Load_Npz.

Và cũng giống như hàm Load_Npz, hàm Load cũng hỗ trợ đọc dữ liệu từ stream hoặc byte array.

Giới hạn của NumSharp

Như đã thấy trong các phần trước, NumSharp đọc dữ liệu từ file Numpy và lưu chúng vào trong array của .NET. Trong .NET, kích cỡ của array không chỉ bị hạn chế bởi lượng RAM ta có. Tài liệu này liệt kê các giới hạn của array trong .NET Core.

  • Tổng số phần tử của một array không được vượt quá 4 tỷ (xét tất cả các chiều)
  • Với byte array, giá trị index tối đa trong một chiều không được vượt quá 0X7FEFFFFF/2,146,435,071 (nếu kiểu dữ liệu của array có kích thước 1 byte thì giới hạn này là 0X7FFFFFC7/2,147,483,591).

Khi sử dụng .NET Framework, ta có thêm một hạn chế nữa.

  • Kích thước của array không được vượt quá 2 GB. Với môi trường 64-bit, ta có thể gỡ bỏ giới hạn này bằng cách bật flag gcAllowVeryLargeObjects.

Vậy có phải là NumSharp có thể đọc file Numpy với tối đa 4 tỷ phần tử, trong đó mỗi chiều có không quá 2,146,435,071 phần tử? Đáng tiếc rằng câu trả lời ở đây là không. Và kiểu dữ liệu ta dùng có kích thước càng lớn thì mảng dữ liệu mà NumSharp có thể đọc lại càng nhỏ. Để hiểu lý do, ta cần đọc mã nguồn của NumSharp.

Cách thức hoạt động của NumSharp

Cả hàm Load_Npz<T>(string path) lẫn Load<T>(string path) cuối cùng đều gọi đến hàm Load<T>(Stream stream), và hàm đó sẽ gọi LoadMatrix(Stream stream). Các bạn có thể đọc mã nguồn của LoadMatrix tại đây.

int bytes;
Type type;
int[] shape;
if (!parseReader(reader, out bytes, out type, out shape))
    throw new FormatException();

Array matrix = Arrays.Create(type, shape);

if (type == typeof(String))
    return readStringMatrix(reader, matrix, bytes, type, shape);
return readValueMatrix(reader, matrix, bytes, type, shape);

Ở đây, bytes là kích thước của kiểu dữ liệu ta sử dụng (intfloat là 4 bytes, double là 8 bytes,…). Và shape là định dạng của mảng mà ta muốn đọc. Hai dữ liệu này được truyền vào hàm readValueMatrix.

int total = 1;
for (int i = 0; i < shape.Length; i++)
    total *= shape[i];
var buffer = new byte[bytes * total];
// lược bỏ phần còn lại

Như ta thấy, NumSharp tạo một array 1 chiều với kiểu dữ liệu là byte và kích thước là bytes * total. Trong đó total là tích của kích thước tất cả các chiều trong shape. Hay nói cách khác, đây là tổng số phần tử trong file Numpy ta muốn đọc. Ta biết rằng một array với kiểu dữ liệu là byte không thể có quá 2,147,483,591 trong một chiều. Từ đó, ta có thể dùng kích thước của từng kiểu dữ liệu để tính xem số phần tử tối đa mà NumSharp có thể xử lý là bao nhiêu.

Kiểu dữ liệu Kích thước Số phần tử tối đa có thể đọc
byte 1 byte 2,147,483,591
int/float 4 bytes 536,870,897
double 8 bytes 268,435,448

Kết thúc

Mặc dù có một số hạn chế, NumSharp vẫn là sự thay thế khả dĩ cho Numpy trong môi trường .NET. Thông thường, tôi thực hiện bước train model với Python và chỉ dùng C# để tải model và thực hiện dự đoán. Trong trường hợp đó, NumSharp luôn đáp ứng đủ yêu cầu.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Đọc file Numpy bằng C# và NumSharp”

Leave a Reply