ML.NET là một thư viện học sâu của Microsoft dành cho nền tảng .NET, cho phép xây dựng mô hình bằng công cụ ModelBuilder, sau đó sử dụng cho suy luận và xử lý dữ liệu. Mặc dù thiết kế tốt, nhưng do sự phát triển mạnh mẽ của AI hiện nay chủ yếu dựa trên Python, tương lai của ML.NET, đặc biệt là phần xây dựng mô hình, còn chưa chắc chắn.
Quan điểm cá nhân tôi cho rằng, trường hợp hữu ích nhất của ML.NET là khi các nhóm thuật toán xây dựng mô hình, xuất ra định dạng ONNX, sau đó ML.NET sẽ tải và áp dụng trong môi trường sản xuất. Quy trình này cho phép tích hợp và triển khai liên tục với hiệu suất tốt. Hơn nữa, đội ngũ backend không cần có kiến thức sâu về AI, chỉ cần hiểu cách xử lý kết quả, giúp giảm bớt rào cản triển khai.
Theo hướng này, gần đây khi sử dụng ML.NET để tải mô hình ONNX xuất từ PyTorch cho suy luận, tôi gặp phải vấn đề do thứ tự các hàng của đầu ra mô hình đã bị thay đổi. Tôi cần thực hiện một thao tác transpose trước khi sử dụng.
Lưu ý phân biệt rõ ràng giữa thao tác
reshapevàtranspose, chúng có bản chất khác nhau.
Tuy nhiên, các chức năng xử lý dữ liệu tích hợp sẵn của ML.NET rất hạn chế. Sau khi tìm hiểu, tôi nhận ra rằng thiết kế của ONNXRuntime trong ML.NET ban đầu không tính đến việc xử lý dữ liệu tiếp theo. Họ cho rằng việc xử lý này thuộc về các quy trình riêng biệt và cần sử dụng các phương tiện khác để thực hiện biến đổi dữ liệu.
Thú thực, quan điểm này có vẻ hợp lý, nhưng sự hiểu biết của tôi có lẽ còn nông cạn. Cuối cùng, tôi đã thử nhiều phương pháp, thậm chí tự triển khai một hàm, nhưng cảm thấy có chút vấn đề và không muốn bỏ phí công sức. Vì vậy, tôi xin chia sẻ đoạn mã tự triển khai này:
// Cách sử dụng
// TransposeHelper.TransposeDimensions(data, inputShape, outputShape, permutation);
/// <summary>
/// Lớp trợ giúp thực hiện transpose cho dữ liệu dạng mảng phẳng với thông tin về hình dạng và hoán vị các chiều.
/// </summary>
internal static class DataTransposer
{
/// <summary>
/// Thực hiện transpose cho một mảng dữ liệu 1 chiều dựa trên hình dạng đầu vào, đầu ra và phép hoán vị các chiều.
/// </summary>
/// <param name="rawData">Mảng dữ liệu gốc.</param>
/// <param name="originalShape">Hình dạng ban đầu của tensor.</param>
/// <param name="targetShape">Hình dạng mong muốn sau transpose.</param>
/// <param name="axisPermutation">Thứ tự các trục mới. Ví dụ: {1, 0} cho transpose 2 chiều.</param>
/// <returns>Mảng dữ liệu đã được transpose.</returns>
/// <exception cref="ArgumentException">Ném ra nếu các hình dạng hoặc phép hoán vị không hợp lệ.</exception>
public static float[] TransposeArray(float[] rawData, int[] originalShape, int[] targetShape, int[] axisPermutation)
{
var dimensions = originalShape.Length;
// Nếu không có phép hoán vị, mặc định đảo ngược thứ tự các trục.
var actualPermutation = axisPermutation ?? Enumerable.Range(0, dimensions).Reverse().ToArray();
if (originalShape.Length != actualPermutation.Length || originalShape.Length != targetShape.Length)
{
throw new ArgumentException("Các hình dạng đầu vào, đầu ra hoặc phép hoán vị không hợp lệ.");
}
var transposedData = new float[rawData.Length];
var currentIndex = new int[dimensions]; // Lưu trữ chỉ số hiện tại cho từng chiều
for (var i = 0; i < rawData.Length; i++)
{
// Chuyển đổi chỉ số tuyến tính 'i' thành chỉ số đa chiều dựa trên originalShape
currentIndex = GetMultiDimensionalIndex(i, currentIndex, originalShape);
// Áp dụng phép hoán vị để có được chỉ số đích
var targetIndex = ApplyPermutation(currentIndex, actualPermutation);
// Tính toán vị trí tuyến tính trong mảng đích từ chỉ số đa chiều đã hoán vị
var targetLinearIndex = GetLinearIndex(targetIndex, targetShape);
// Gán giá trị từ dữ liệu gốc vào vị trí đã tính toán trong mảng đích
transposedData[targetLinearIndex] = rawData[i];
}
return transposedData;
}
/// <summary>
/// Chuyển đổi một chỉ số tuyến tính thành chỉ số đa chiều.
/// </summary>
private static int[] GetMultiDimensionalIndex(int linearIndex, int[] indexArray, int[] shape)
{
for (var dim = shape.Length - 1; dim >= 0; dim--)
{
var stride = 1;
// Tính toán "stride" cho chiều hiện tại
for (var k = 0; k < dim; k++)
{
stride *= shape[k];
}
// Tính toán chỉ số cho chiều hiện tại
indexArray[dim] = (linearIndex / stride) % shape[dim];
}
return indexArray;
}
/// <summary>
/// Áp dụng phép hoán vị các trục cho một chỉ số đa chiều.
/// </summary>
private static int[] ApplyPermutation(int[] originalIndex, int[] permutation)
{
var permutedIndex = new int[originalIndex.Length];
for (var i = 0; i < originalIndex.Length; i++)
{
// Chỉ số mới tại vị trí 'i' là giá trị của chỉ số cũ tại vị trí 'permutation[i]'
permutedIndex[i] = originalIndex[permutation[i]];
}
return permutedIndex;
}
/// <summary>
/// Chuyển đổi một chỉ số đa chiều thành chỉ số tuyến tính.
/// </summary>
private static int GetLinearIndex(int[] index, int[] shape)
{
var linearIndex = 0;
var stride = 1; // Bắt đầu với stride cho chiều cuối cùng
// Tính toán chỉ số tuyến tính từ phải sang trái (tương ứng với cách lưu trữ row-major)
for (var dim = shape.Length - 1; dim >= 0; dim--)
{
linearIndex += index[dim] * stride;
stride *= shape[dim]; // Cập nhật stride cho chiều tiếp theo
}
return linearIndex;
}
}
Sau khi thực hiện, tôi nhận ra rằng việc tự triển khai có thể tiềm ẩn lỗi và không hiệu quả bằng các thư viện chuyên dụng. Sau khi tìm hiểu thêm, tôi phát hiện ra NumSharp hỗ trợ chức năng transpose tương tự như NumPy. Việc triển khai trở nên đơn giản hơn rất nhiều.
NumSharp cung cấp cú pháp tương tự NumPy trên nền tảng C#. Đoạn mã sau đây được thử nghiệm và chạy trên RoslynPad:
#r "nuget: NumSharp, 0.30.0"
#r "nuget: System.Numerics.Tensors, 0.1.0"
using System.Numerics.Tensors;
using NumSharp.Utilities;
using NumSharp;
// Khởi tạo một DenseTensor với dữ liệu và hình dạng ban đầu
var initialTensor = new DenseTensor<float>(new float[] { 1, 2, 3, 4, 5, 6 }, new int[] { 2, 3 });
Console.WriteLine("Tensor ban đầu:");
initialTensor.Dump(); // In ra console tensor ban đầu
// Thực hiện reshape cho tensor
Console.WriteLine("\nTensor sau khi reshape(3, 2):");
initialTensor.Reshape(new int[]{ 3, 2 }).Dump(); // In ra console tensor sau reshape
// Chuyển đổi DenseTensor sang NDArray của NumSharp
NDArray ndArray = new NDArray(initialTensor.ToArray(), new Shape(new int[]{ 2, 3 }));
Console.WriteLine("\nNDArray ban đầu:");
ndArray.Dump(); // In ra console NDArray ban đầu
// Thực hiện transpose trên NDArray với phép hoán vị trục (1, 0)
ndArray = ndArray.transpose(new int[]{1,0});
Console.WriteLine("\nNDArray sau khi transpose(1, 0):");
ndArray.Dump(); // In ra console NDArray sau transpose