카테고리 보관물: .Net / C#

ONNX 파일 .Net framework C# 으로 활용하기

개요

요즘 기계 학습(Machine Learning, ML)을 이용한 프로그램이나 앱이 많이 등장하고 있습니다. 기계 학습의 결과를 .Net framework C# 환경에서 연동하는 방법을 알아보도록 하겠습니다.

ONNX

ONNX 는 (Open Neural Network Exchange) 를 줄인 것으로 여러가지 학습 플랫폼의 결과를 서로 사용할 수 있게 하는 일종의 프레임워크 같은 개념입니다.

ONNX 웹 사이트
ONNX 웹 사이트

기계 학습의 결과를 ONNX 파일로 저장하는 방법은 이 글의 범위를 넘어가게 됩니다. 좋은 글이 많이 있으므로 그것을 참조하시면 됩니다.

이 글에서는 직접 기계학습으로 저장한 것이나 다른 곳에서 구한 ONNX 파일을 사용하는 것으로 하겠습니다.

Netron

직접 기계 학습의 결과를 저장했다면 어떤 값을 입력했고 출력 되는 값을 이미 알고 있습니다. 그러나 다른 곳에서 전달 받았거나 구한 파일만 있다면 그것을 정확히 알 수 없습니다. 이때 사용하는 프로그램이 Netron 입니다.

웹사이트에 방문하여 파일을 올리면 구조를 시각화해서 보여주므로 매우 편리합니다. 파일을 올리는 것이 꺼려지는 분들은 Github 에서 설치본을 받아 설치한 후 사용하시면 됩니다.

필자는 타이타닉 생존자 데이터를 가지고 기계 학습을 통해서 예측하는 모델로 export 한 파일을 사용하였습니다.

ONNX 파일을 Netron 으로 열어보면 다음과 같습니다.

ONNX 파일을 Netron에서 열어 본 결과
ONNX 파일을 Netron에서 열어 본 결과

모델이 간단하여 복잡하지 않습니다. 기계 학습을 위해 입력한 값이 float 형으로 8개 이고 출력 값이 확률과 레이블(목표값)임을 알 수 있습니다.

C# 코드

위에 언급한 ONNX 파일을 이용하여 예측 값을 가져오는 코드는 다음과 같습니다.

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
...
string ONNX_MODEL_PATH = "D:\\yourPaht\\your.onnx";

InferenceSession session = new InferenceSession(ONNX_MODEL_PATH);
DenseTensor<float> denseTensor;

float[,] predictInput = new float[1, 8];
predictInput[0, 0] = 3f;
predictInput[0, 1] = 1f;
predictInput[0, 2] = 16.0f;
predictInput[0, 3] = 2f;
predictInput[0, 4] = 0f;
predictInput[0, 5] = 18.000f;
predictInput[0, 6] = 7f;
predictInput[0, 7] = 3f;

denseTensor = predictInput.ToTensor();
var inputMetaData = session.InputMetadata;
var outputMetaData = session.OutputMetadata;

var modelInput = new List<NamedOnnxValue>();

foreach (var name in inputMetaData.Keys)
{
    modelInput.Add(NamedOnnxValue.CreateFromTensor<float>(name, denseTensor));
}

try
{
    var results = session.Run(modelInput);
    var inferenceResult = results.ToList()[0];
    var inferenceResultValue = inferenceResult.Value;

    var modelOutput = results.ToList()[0].AsTensor<long>().ToArray<long>()[0].ToString();

    Console.WriteLine("Predict Survived : " + modelOutput.ToString());
    Console.ReadLine();
}
catch (Exception error)
{
    Console.WriteLine(error.Message);
    Console.ReadLine();
}

9~28 행은 입력 값으로 8개의 항목을 입력하는 코드입니다.

32~39 행은 입력 데이터를 적용해서 그 결과를 출력하는 부분 입니다.

위 결과를 수행해 보면 다음과 같습니다(1이 생존).

예측 결과
예측 결과

입력 값의 항목을 변경하면 예측한 값이 달라집니다.

이번 글에서는 ONNX 파일을 .Net framework C# 환경에서 사용하는 방법을 알아보았습니다.

OpenXML을 이용한 엑셀파일 읽기 성능 향상

개요

C#을 이용해서 엑셀파일을 읽어들이는 방법이 여러가지가 있습니다. 필자는 주로 OpenXML 을 이용합니다. 파일을 읽어 가공을 하던 중 자료의 양에 비해 속도가 지나치게 떨어지는 현상을 발견했습니다. 대상이 되는 파일이 많아서 아주 많은 시간이 소요될 것으로 예상되었습니다. 아무리 생각해도 정상적인 상황이 아니라는 판단이 들어 더 분석해 보기로 했습니다.

성능 저하 원인

결과부터 이야기하면 텍스트 형태의 셀 자료를 가져오는 부분때문에 발생한 문제였습니다. OpenXML 을 이용해서 엑셀을 읽어오는 글을 보면 셀의 자료를 가져오는 부분이 다음과 같은 형태로 구현되어 있습니다.

string value = cell.CellValue.InnerText;

if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString)
{
    return doc.WorkbookPart.SharedStringTablePart.SharedStringTable.ChildElements.GetItem(int.Parse(value)).InnerText;
    //return doc.WorkbookPart.SharedStringTablePart.SharedStringTable.ElementAt(int.Parse(value)).InnerText;
}

속도가 심각하게 저하되는 부분이 5, 6행 입니다. 엑셀을 읽을 때 행 단위로 반복하게 됩니다. 그 안에서 각 컬럼(셀)의 값을 읽어오게 됩니다. 셀이 텍스트 자료인 경우 1행의 결과가 숫자로 나타납니다. 그래서 3행의 조건을 통해 실제 텍스트 값을 가져오게 됩니다.

자료의 개수가 많지 않은 경우는 문제가 되지 않습니다. 그러나 많아지게 되면 심각한 속도저하가 발생합니다.

SharedStringTable

SharedStringTable 은 텍스트 값을 효율적으로 저장하기 위해 만들어진 객체입니다. 엑셀같은 형식의 자료 특성상 같은 텍스트가 반복될 여지가 많기 때문에 중복을 최소화 해서 관리합니다.

어찌보면 당연한 것인데 반복문 안에서 크기가 큰 SharedStringTable 객체를 계속 참조해서 속도 저하가 일어나는 것 입니다.

해결책(Dictionary 이용)

읽어들인 엑셀파일의 SharedStringTable 을 반복문이 시작되기 전 Dictionary 객체에 담아놓고 참조하면 성능이 크게 향상됩니다. 다음의 코드가 설명한 방식으로 구현된 것 입니다.

var dictionary = new Dictionary<int, string>();

var excelFile = @"your_excel_file_path";

using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(excelFile, false))
{
    var sharedStringTablePart = spreadsheetDocument.WorkbookPart.SharedStringTablePart;
    using (OpenXmlReader reader = OpenXmlReader.Create(sharedStringTablePart))
    {
        int i = 0;
        while (reader.Read())
        {
            if (reader.ElementType == typeof(SharedStringItem))
            {
                SharedStringItem ssi = (SharedStringItem)reader.LoadCurrentElement();
                dictionary.Add(i++, ssi.Text != null ? ssi.Text.Text : string.Empty);
            }
        }
    }

    .
    .
    .

    foreach (Row row in rows)
    {
        foreach (Cell cell in row.Descendants<Cell>())
        {
            string value = cell.CellValue.InnerText;

            if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString)
            {
                var cellValue = dictionary[int.Parse(value)];
                //your code
            }
        }
    }
}

1행에서 SharedStringTable 의 값을 담아둘 Dictionary 변수를 선언합니다. 7~19행 까지 SharedStringTable 객체를 읽어서 변수에 추가합니다. 33행에서 텍스트 값을 SharedStringTable 객체가 아닌 Dictionary 변수에서 참조해서 가져옵니다. 이렇게 하면 무거운 SharedStringTable 객체를 반복문 내에서 참조하지 않아 금방 처리됩니다.

OpenXML 로 엑셀처리 시 성능 문제로 어려움을 겪으신 분들 도움이 되시기를 바랍니다.