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

https://duongnt.com/ml-net

Trong năm 2020, tôi đảm nhiệm phần kỹ thuật cho một project deep learning sử dụng C#. Model khi đó được train bằng Python và sử dụng Tensorflow/Keras, tuy nhiên ứng dụng sử dụng model lại được viết bằng C# .NET Framework. Tôi sử dụng một thư viện với tên gọi TensorFlowSharp để chạy được model bằng C#. Lúc này model cần được chuyển sang dạng frozen graph (format .pb). Thư viện này tương đối ổn và là đủ để đáp ứng nhu cầu của tôi vào thời điểm đó.

Nhưng các công nghệ liên quan tới AI/ML thay đổi từng ngày. Tại thời điểm viết bài này, TensorFlow đã release bản 2.5.0, còn TensorFlowSharp thì vẫn chỉ hỗ trợ tối đa là TensorFlow 1.15.0 và có vẻ là không còn ai phát triển thư viện đó nữa. Khi nhảy từ v1 lên v2, TensorFlow có nhiều cải thiện về cả tính năng lẫn hiệu năng, tuy nhiên thay đổi này cũng tạo ra nhiều vấn đề về tương thích. Trong tương lai gần, chúng ta sẽ không thể chuyển các model được train bởi Tensorflow 2.x sang format mà TensorFlowSharp có thể sử dụng.

Tôi cũng đã thử tìm hiểu về thư viện Tensorflow.NET, và dùng nó để tạo một ứng dụng dự đoán tuổi và giới tính. Tuy nhiên tôi vẫn cảm thấy có điều gì đó không ổn.

Vì thế, tôi quyết định chuyển sang sử dụng ML.NET. Framework này được khởi tạo bởi Microsoft và bây giờ đang được phát triển bởi .NET Foundation. ML.NET có một số lợi thế sau.

  • Tất cả version của TensorFlow đều được hỗ trợ, kể cả các bản mới nhất.
  • ML.NET sử dụng format onnx, đây là một tiêu chuẩn đang được ứng dụng một cách rộng rãi. Ta có thể sử dụng tf2onnx để chuyển model từ các format như frozen graph/TensorFlow checkpoint/Keras h5 sang onnx một cách dễ dàng.
  • ML.NET được xây dựng bằng .NET Core/.NET Standard nên có thể chạy đa nền tảng.

Trong bài hôm nay, ta sẽ sử dụng ML.NET để chạy một model đã được train bằng Keras với backend là TensorFlow và thực hiện một số dự đoán.

Model để chạy thử

Model mà ta sẽ chạy thử là một mạng lưới CNN đơn giản, được train trên bộ dữ liệu FashionMNIST, các bạn có thể download model từ đây. Model này chỉ được train trong 20 epoch và có độ chính xác trên dưới 92%, nhưng thế là đủ cho thử nghiệm của ta. Phần code để train model nằm trong repository này. Còn đây là kiến trúc của model.

Ta có thể thấy là node đầu vào có tên là conv2d_input còn node đầu ra có tên là dense_1, ta sẽ dùng những cái tên này khi chạy model. Nếu chưa biết tên đầu vào và đầu ra, ta có thể dùng Netron để vẽ graph cho model rồi dựa vào đó để tìm đầu vào/đầu ra.

FashionMNIST là một bộ dữ liệu thường được dùng trong các thử nghiệm vì nó đủ nhỏ để cho phép ta nhanh chóng train model, nhưng lại đủ phức tạp để việc đạt độ chính xác trên 95% là không đơn giản. Bộ dữ liệu này bao gồm 60.000 mẫu, mỗi mẫu là một ảnh đơn màu với kích cỡ 28×28 pixel và thuộc một trong 10 nhóm trang phục. Ví dụ dưới đây là ảnh của áo chui đầu.

Load model và thực hiện dự đoán với ML.NET

Load model

Đầu tiên ta cần cài cả ML.NET và bộ runtime cho format onnx.

dotnet add package Microsoft.ML --version 1.6.0
dotnet add package Microsoft.ML.OnnxRuntime --version 1.8.1
dotnet add package Microsoft.ML.OnnxTransformer --version 1.6.0

Vì dữ liệu chạy thử của ta được lưu trong file Numpy với format npz nên ta cần cài cả NumSharp.

dotnet add package NumSharp --version 0.30.0

Ta ánh xạ đầu vào và đầu ra của model vào 2 lớp sau trong C#.

public class Input
{
    [VectorType(28*28*1)] // Đầu vào của model là tensor 3D, nhưng đầu vào của ML.NET là vector 1D
    [ColumnName("conv2d_input")] // Tên này phải trùng tên của đầu vào
    public float[] Data { get; set; }
}

public class Output
{
    [VectorType(10)] // Vì shape của đầu ra là (10)
    [ColumnName("dense_1")] // Tên này phải trùng tên của đầu ra
    public float[] Data { get; set; }
}

Chú ý là đầu vào và đầu ra của ML.NET luôn là vector 1D cho dù đầu vào/đầu ra của model có shape là gì đi chăng nữa. Ví dụ như model của ta có đầu vào với shape là (28, 28, 1) nhưng đầu vào của lớp Input lại là vector với kích cỡ là 784 = 28 * 28 * 1

Sau đó ta cần tạo một context rồi dùng context đó để tạo pipeline để load model. Có thể thấy là tên của đầu vào và đầu ra được truyền vào cùng với đường dẫn tới model. Các đầu vào và đầu ra này được lưu trong 2 array với kiểu là string (kể cả khi chỉ có 1 input hay output ta vẫn phải dùng array).

using Microsoft.ML;

var modelPath = "fashionmnist_cnn_model.onnx";
var outputColumnNames = new[] { "dense_1" };
var inputColumnNames = new[] { "conv2d_input" };
var mlContext = new MLContext();
var pipeline = mlContext.Transforms.ApplyOnnxModel(outputColumnNames, inputColumnNames, modelPath);

Thực hiện dự đoán

Ta sẽ thực hiện phân lớp cho bức ảnh áo chui đầu ở phần trước, các bạn có thể tải dữ liệu ảnh dưới dạng npz từ link này. Đoạn code để load dữ liệu ảnh và lưu vào object thuộc class Input là như sau.

var content = np.Load_Npz<byte[,]>("test_image.npz");
var data = np.array(content["test_image.npy"])
    .astype(NPTypeCode.Single)
    .flatten()  // remember to flatten the tensor
    .ToArray<float>();
var input = new Input { Data = data };

Như đã thấy, tensor dữ liệu được chuyển thành một mảng 1 chiều với kiểu là float, và mảng này được lưu vào property Data của object thuộc lớp Input. Sau đó ta dùng code dưới đây để thực hiện phân lớp.

var dataView = mlContext.Data.LoadFromEnumerable(input);
var transformedValues = pipeline.Fit(dataView).Transform(dataView);
var output = mlContext.Data.CreateEnumerable<Output>(transformedValues, reuseRowObject: false);

Kết quả sẽ có dạng sau.

output.Single().Data
// [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] 

Giá trị tại điểm index==2 là lớn nhất, có nghĩa là model của ta đã gán nhãn 2 cho ảnh. Từ đường link này, ta có thể thấy rằng nhãn 2 đúng là áo chui đầu.

Một số vấn đề thường gặp

Chuyển model dạng Keras h5 sang onnx

Đầu tiên ta cần chuyển model từ format h5 sang một format có tên là saved model.

from tensorflow.keras.models import saved_model, load_model

h5_path = 'my_model.h5'
saved_model_path = 'my_model' # đường dẫn tới folder đầu ra, không phải là tới file

model = load_model(h5_path)
save_model(model, saved_model_path)

Đoạn code trên sẽ tạo một folder mới với tên gọi là my_model với nội dung như sau.

.
└── assets
└── variables
    └── variables.data-00000-of-00001
    └── variables.index
└── keras_metadata.pb
└── saved_model.pb

Sau đó ta có thể dùng tf2onnx để chuyển format saved_model sang onnx. Chạy lệnh sau trong cửa sổ terminal.

pip install tf2onnx
python -m tf2onnx.convert --saved-model "my_model" --output "my_model.onnx"

Chuyển model từ dạng frozen graph sang onnx

Cả frozen graph và saved model đều sử dụng đuôi .pb. Nếu model của ta có 2 file với tên gọi là keras_metadata.pb and saved_model.pb thì nó có format là save model, ta có thể dùng cách ở phần trên để chuyển model sang dạng onnx.

Còn kể cả khi model đúng là có dạng frozen graph thì ta vẫn có thể chuyển nó sang onnx bằng tf2onnx với lệnh sau.

python -m tf2onnx.convert --input frozen_graph.pb --inputs <tên đầu vào, phân cách bằng dấu phẩy> --outputs <tên đầu ra, phân cách bằng dấu phẩy> --output frozen_graph.onnx

Như đã nói ở phần trước, ta có thể dùng Netron để tìm tên của đầu vào và đầu ra.

Model của ta có nhiều đầu vào hoặc nhiều đầu ra

Trong các model phức tạp, ta có thể có nhiều đầu vào hoặc nhiều đầu ra. Ví dụ như model để dự đoán tuổi và giới tính mà tôi đã nhắc đến ở phần trước có 2 đầu ra; một đầu ra cho tuổi và 1 đầu ra cho giới tính. Hơn nữa, kết quả đoán giới tính lại được dùng để đoán tuổi.

Lúc này, ta cần tạo 2 property trong class Output.

public class Output
{
    [VectorType(1)]
    [ColumnName("gender_output/Sigmoid:0")]
    public float[] Gender { get; set; }

    [VectorType(1)]
    [ColumnName("age_output/BiasAdd:0")]
    public float[] Age { get; set; }
}

Nếu như model có nhiều đầu vào thì ta cũng chỉ cần tạo nhiều property trong class Input.

Kết thúc

Mặc dù tôi vẫn thích dùng Python để train models, ML.NET đã mở ra nhiều cơ hội để ta áp dụng deep learning vào các dự án dùng C# .NET. Và vì thư viện này dùng chuẩn onnx, ta có thể dễ dàng sử dụng phần lớn các model đã được train bằng Python mà không gặp phải nhiều trở ngại.

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

14 Thoughts on “Dùng ML.NET và C# để chạy model TensorFlow”

  • xin chào a. em có làm theo hướng dẫn của a. nhưng tới đoạn var dataView = mlContext.Data.LoadFromEnumerable(Input); bị lỗi

    • À đúng rồi đoạn ấy mình copy lỗi, bạn sửa lại là như sau nhé.

       var dataView = mlContext.Data.LoadFromEnumerable(new[] { input })
      

      Mình cũng edit lại cả bài blog bản tiếng Anh và tiếng Việt rồi, cảm ơn bạn.

  • var output = mlContext.Data.CreateEnumerable(transformedValues, reuseRowObject: false); cũng bĩ lỗi chỗ TResult a hướng dẫn giúp e fix

    • Uh, chỗ ấy bạn sửa TResult thành Output nhé.

      var output = mlContext.Data.CreateEnumerable<Output>(transformedValues, reuseRowObject: false);
      
  • Nếu em có inut đầu vào là 1 ảnh được lưu thành file jpg. Vd “01.jpg” thì phải chuyển đổi thế nào anh

    • Về cơ bản thì bạn load file ảnh đó rồi đọc từng pixel một. Với ảnh màu thì mỗi 1 pixel sẽ có 3 giá trị cho 3 màu R, G, B. Giả sử file ảnh của bạn kích cỡ 256×256 pixel thì bạn sẽ có tổng cộng 256x256x3 giá trị cần đọc, tương ứng với 1 tensor với shape là (256, 256, 3).
      Mình xem email bạn thì thấy bạn rành python, chắc bạn biết thư viện opencv trong Python. Trong C# có bản port là OpenCvSharp với interface gần tương tự. Nếu không muốn tự viết code đọc ảnh thì bạn dùng luôn thư viện đó. Bạn tham khảo repo dưới mình dùng OpenCvSharp để đọc ảnh.

      https://github.com/duongntbk/AgeEstimatorSharp/blob/master/AgeEstimatorSharp/ImageProcessing/Resizer/FaceResizer.cs#L16
      
  • Nếu mình train model nhận diện hành động cử chỉ tay thì nó có hoạt động không?

    • Nếu model của bạn chạy ổn trong Python và bạn convert nó sang onnx thì model nào cũng dùng ML.NET được hết.

  • var mlContext = new MLContext();
    var pipeline = _mlContext.Transforms.ApplyOnnxModel(outputColumnNames, inputColumnNames, modelPath);
    

    Đoạn này _mlContext ở dòng dưới khác mlContext vừa tạo ở dòng trên như thế nào ạ.

    E đổi thành

    var mlContext = new MLContext();
    var pipeline = mlContext.Transforms.ApplyOnnxModel(outputColumnNames, inputColumnNames, modelPath);
    

    thì bị lỗi
    ‘TransformsCatalog’ does not contain a definition for ‘ApplyOnnxModel’ and no accessible extension method ‘ApplyOnnxModel’ accepting a first argument of type ‘TransformsCatalog’ could be found (are you missing a using directive or an assembly reference?)

    • Đoạn ấy mình copy lỗi thừa dấu underscore, bạn sửa thành var pipeline = mlContext.Transforms.ApplyOnnxModel(...) là đúng rồi.

      Hàm ApplyOnnxModel là extension method được định nghĩa trong static class Microsoft.ML.OnnxCatalog. Bạn kiểm tra xem đã có dòng using Microsoft.ML chưa, hoặc gọi trực tiếp hàm ApplyOnnxModel thông qua class OnnxCatalog thay vì gọi extension method.

Leave a Reply