04-20 10:35
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[Artificial Intelligence / CNTK] CNTK CPU Example 코드 작성하기 본문

Artificial Intelligence/CNTK

[Artificial Intelligence / CNTK] CNTK CPU Example 코드 작성하기

cinema4dr12 2018. 5. 13. 01:30

Written by Geol Choi | 


이번 포스팅에서는 Microsoft Research에서 개발한 CNTK(Cognitive ToolKit)의 간단한 C# 예제를 만드는 방법에 대하여 알아보도록 하겠습니다.


참고로, 개발환경은 다음과 같습니다:

  • OS : Windows 10 64bit

  • IDE : Visual Studio 2015(Ver. 14)

  • .NET Framework : 4.5 or higher

  • Runtime Env. : CPU


설명은 자세한 튜토리얼 형식으로 진행하도록 하겠습니다.

Visual Studio 프로젝트 생성

Visual Studio 메뉴에서 File > New > Project를 클릭하여 새로운 프로젝트를 생성합니다.


Template은 Visual C# > Console Application을 선택한 후, CntkExample이라는 이름으로 적당한 경로에 프로젝트를 새로 생성합니다.


CNTK C# NuGet 패키지 설치

새로운 프로젝트를 생성하였으면, CNTK NuGet 패키지 설치를 하여야 하는데 온라인으로 설치가 가능합니다.


아래 이미지와 같이, Visual Studio Solution Explorer로부터 프로젝트 이름(CntkExample)을 오른쪽 마우스 클릭하여 Manage NuGet Packages...를 NuGet 패키지 추가 창으로 이동합니다.



아래 이미지와 같이, NuGet Package Manager 창이 열리면 Browse 탭에서 "cntk" 입력하면, CNTK 문자열이 포함된 모든 패키지 검색결과를 얻을 수 있는데, 본 튜토리얼에서는 CNTK CPU 예제 코드를 작성할 것이므로 이 중  CNTK.CPUOnly를 선택하여 설치합니다.



설치 시, CNTK.CPUOnly 패키지의 타 의존성(Dependencies)으로 인해 아래 이미지와 같이 세 개의 패키지가 우선적으로 설치될 것입니다.



[I Accept] 버튼을 클릭하여 설치를 진행합니다. 설치가 모두 완료되면 Installed 탭으로 이동하여 설치된 NuGet 패키지 목록을 확인할 수 있습니다.


Solution Platforms 설정

현재 CNTK는 64bit 플랫폼만을 지원합니다. Visual Studio에서 생성된 프로젝트는 기본적으로 "Any CPU"로 플랫폼이 설정되어 있는데, Solution Platforms 버튼을 클릭하여 드롭다운 항목에서 Configuration Manager...를 클릭합니다.



Configuration Manager 창이 열리면, Platform 항목에서 <New...>를 클릭하고,



New Project Platform 창으로부터 New platform:x64를 선택합니다.


Example Code 작성

다음과 같이, Program.cs 코드에서 using 지시문으로 사용할 namespace를 정의합니다:


1
2
3
4
5
6
using CNTK;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
cs


위의 코드까지 작성한 후, 빌드했을 때 빌드가 성공되어야 합니다.


Program.csnamespaceCNTK.CntkExample으로 수정하도록 하겠습니다 (Line 8):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using CNTK;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace CNTK.CntkExample
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}
 
cs


이제 C# Class를 하나 추가하도록 하겠습니다. Visual Studio의 Solution Explorer에서 프로젝트 이름(CntkExample)을 오른쪽 마우스 버튼을 클릭하여 나오는 Context Menu에서 Add > New Item...을 클릭하여 새로운 아이템을 추가합니다.



Add New Item 창이 열리면, Visual C# Items에서 Class 항목을 선택 후, TestHelper.cs라는 이름으로 새로운 Class를 생성합니다.



TestHlelper.cs는 이름에서 알 수 있듯이 CNTK 코드를 테스트하는데 도움을 주는 유용한 코드이며, CNTK 공식 C# 예제 솔루션에 포함되어 있는 예제입니다.


아래와 같이, TestHelper.cs 코드를 작성합니다:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
//
// TestHelper.cs -- Help functions for CNTK Library C# model training tests.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
 
namespace CNTK.CntkExample
{
    public enum Activation
    {
        None,
        ReLU,
        Sigmoid,
        Tanh
    }
    public class TestHelper
    {
        public static Function Dense(Variable input, int outputDim, DeviceDescriptor device,
            Activation activation = Activation.None, string outputName = "")
        {
            if (input.Shape.Rank != 1)
            {
                // 
                int newDim = input.Shape.Dimensions.Aggregate((d1, d2) => d1 * d2);
                input = CNTKLib.Reshape(input, new int[] { newDim });
            }
 
            Function fullyConnected = FullyConnectedLinearLayer(input, outputDim, device, outputName);
            switch (activation)
            {
                default:
                case Activation.None:
                    return fullyConnected;
                case Activation.ReLU:
                    return CNTKLib.ReLU(fullyConnected);
                case Activation.Sigmoid:
                    return CNTKLib.Sigmoid(fullyConnected);
                case Activation.Tanh:
                    return CNTKLib.Tanh(fullyConnected);
            }
        }
 
        public static Function FullyConnectedLinearLayer(Variable input, int outputDim, DeviceDescriptor device,
            string outputName = "")
        {
            System.Diagnostics.Debug.Assert(input.Shape.Rank == 1);
            int inputDim = input.Shape[0];
 
            int[] s = { outputDim, inputDim };
            var timesParam = new Parameter((NDShape)s, DataType.Float,
                CNTKLib.GlorotUniformInitializer(
                    CNTKLib.DefaultParamInitScale,
                    CNTKLib.SentinelValueForInferParamInitRank,
                    CNTKLib.SentinelValueForInferParamInitRank, 1),
                device, "timesParam");
            var timesFunction = CNTKLib.Times(timesParam, input, "times");
 
            int[] s2 = { outputDim };
            var plusParam = new Parameter(s2, 0.0f, device, "plusParam");
            return CNTKLib.Plus(plusParam, timesFunction, outputName);
        }
 
        public static float ValidateModelWithMinibatchSource(
            string modelFile, MinibatchSource testMinibatchSource,
            int[] imageDim, int numClasses, string featureInputName, string labelInputName, string outputName,
            DeviceDescriptor device, int maxCount = 1000)
        {
            Function model = Function.Load(modelFile, device);
            var imageInput = model.Arguments[0];
            var labelOutput = model.Outputs.Single(o => o.Name == outputName);
 
            var featureStreamInfo = testMinibatchSource.StreamInfo(featureInputName);
            var labelStreamInfo = testMinibatchSource.StreamInfo(labelInputName);
 
            int batchSize = 50;
            int miscountTotal = 0, totalCount = 0;
            while (true)
            {
                var minibatchData = testMinibatchSource.GetNextMinibatch((uint)batchSize, device);
                if (minibatchData == null || minibatchData.Count == 0)
                    break;
                totalCount += (int)minibatchData[featureStreamInfo].numberOfSamples;
 
                // expected lables are in the minibatch data.
                var labelData = minibatchData[labelStreamInfo].data.GetDenseData<float>(labelOutput);
                var expectedLabels = labelData.Select(l => l.IndexOf(l.Max())).ToList();
 
                var inputDataMap = new Dictionary<Variable, Value>() {
                    { imageInput, minibatchData[featureStreamInfo].data }
                };
 
                var outputDataMap = new Dictionary<Variable, Value>() {
                    { labelOutput, null }
                };
 
                model.Evaluate(inputDataMap, outputDataMap, device);
                var outputData = outputDataMap[labelOutput].GetDenseData<float>(labelOutput);
                var actualLabels = outputData.Select(l => l.IndexOf(l.Max())).ToList();
 
                int misMatches = actualLabels.Zip(expectedLabels, (a, b) => a.Equals(b) ? 0 : 1).Sum();
 
                miscountTotal += misMatches;
                Console.WriteLine($"Validating Model: Total Samples = {totalCount}, Misclassify Count = {miscountTotal}");
 
                if (totalCount > maxCount)
                    break;
            }
 
            float errorRate = 1.0F * miscountTotal / totalCount;
            Console.WriteLine($"Model Validation Error = {errorRate}");
            return errorRate;
        }
 
        public static void SaveAndReloadModel(ref Function function, IList<Variable> variables, DeviceDescriptor device, uint rank = 0)
        {
            string tempModelPath = "feedForward.net" + rank;
            File.Delete(tempModelPath);
 
            IDictionary<string, Variable> inputVarUids = new Dictionary<string, Variable>();
            IDictionary<string, Variable> outputVarNames = new Dictionary<string, Variable>();
 
            foreach (var variable in variables)
            {
                if (variable.IsOutput)
                    outputVarNames.Add(variable.Owner.Name, variable);
                else
                    inputVarUids.Add(variable.Uid, variable);
            }
 
            function.Save(tempModelPath);
            function = Function.Load(tempModelPath, device);
 
            File.Delete(tempModelPath);
 
            var inputs = function.Inputs;
            foreach (var inputVarInfo in inputVarUids.ToList())
            {
                var newInputVar = inputs.First(v => v.Uid == inputVarInfo.Key);
                inputVarUids[inputVarInfo.Key] = newInputVar;
            }
 
            var outputs = function.Outputs;
            foreach (var outputVarInfo in outputVarNames.ToList())
            {
                var newOutputVar = outputs.First(v => v.Owner.Name == outputVarInfo.Key);
                outputVarNames[outputVarInfo.Key] = newOutputVar;
            }
        }
 
        public static bool MiniBatchDataIsSweepEnd(ICollection<MinibatchData> minibatchValues)
        {
            return minibatchValues.Any(a => a.sweepEnd);
        }
 
        public static void PrintTrainingProgress(Trainer trainer, int minibatchIdx, int outputFrequencyInMinibatches)
        {
            if ((minibatchIdx % outputFrequencyInMinibatches) == 0 && trainer.PreviousMinibatchSampleCount() != 0)
            {
                float trainLossValue = (float)trainer.PreviousMinibatchLossAverage();
                float evaluationValue = (float)trainer.PreviousMinibatchEvaluationAverage();
                Console.WriteLine($"Minibatch: {minibatchIdx} CrossEntropyLoss = {trainLossValue}, EvaluationCriterion = {evaluationValue}");
            }
        }
 
        public static void PrintOutputDims(Function function, string functionName)
        {
            NDShape shape = function.Output.Shape;
 
            if (shape.Rank == 3)
            {
                Console.WriteLine($"{functionName} dim0: {shape[0]}, dim1: {shape[1]}, dim2: {shape[2]}");
            }
            else
            {
                Console.WriteLine($"{functionName} dim0: {shape[0]}");
            }
        }
 
    }
}
cs


가장 간단한 예제 중 하나인 Logistic Regression 예제 코드를 작성하겠습니다. 참고로, 본 튜토리얼은 CNTK의 개발환경을 구성하고 간단한 예제를 실행해 보는 것에 초점을 두었기 때문에 코드 자체의 설명은 생략하도록 하겠습니다.


다음과 같이 Program.cs 전체 코드를 작성합니다:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
using CNTK;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace CNTK.CntkExample
{
    class Program
    {
        static int inputDim = 3;
        static int numOutputClasses = 2;
 
        static public void TrainAndEvaluate(DeviceDescriptor device)
        {
            // build a logistic regression model
            Variable featureVariable = Variable.InputVariable(new int[] { inputDim }, DataType.Float);
            Variable labelVariable = Variable.InputVariable(new int[] { numOutputClasses }, DataType.Float);
            var classifierOutput = CreateLinearModel(featureVariable, numOutputClasses, device);
            var loss = CNTKLib.CrossEntropyWithSoftmax(classifierOutput, labelVariable);
            var evalError = CNTKLib.ClassificationError(classifierOutput, labelVariable);
 
            // prepare for training
            CNTK.TrainingParameterScheduleDouble learningRatePerSample = new CNTK.TrainingParameterScheduleDouble(0.021);
            IList<Learner> parameterLearners =
                new List<Learner>() { Learner.SGDLearner(classifierOutput.Parameters(), learningRatePerSample) };
            var trainer = Trainer.CreateTrainer(classifierOutput, loss, evalError, parameterLearners);
 
            int minibatchSize = 64;
            int numMinibatchesToTrain = 1000;
            int updatePerMinibatches = 50;
 
            // train the model
            for (int minibatchCount = 0; minibatchCount < numMinibatchesToTrain; minibatchCount++)
            {
                Value features, labels;
                GenerateValueData(minibatchSize, inputDim, numOutputClasses, out features, out labels, device);
                //TODO: sweepEnd should be set properly instead of false.
#pragma warning disable 618
                trainer.TrainMinibatch(
                    new Dictionary<Variable, Value>() { { featureVariable, features }, { labelVariable, labels } }, device);
#pragma warning restore 618
                TestHelper.PrintTrainingProgress(trainer, minibatchCount, updatePerMinibatches);
            }
 
            // test and validate the model
            int testSize = 100;
            Value testFeatureValue, expectedLabelValue;
            GenerateValueData(testSize, inputDim, numOutputClasses, out testFeatureValue, out expectedLabelValue, device);
 
            // GetDenseData just needs the variable's shape
            IList<IList<float>> expectedOneHot = expectedLabelValue.GetDenseData<float>(labelVariable);
            IList<int> expectedLabels = expectedOneHot.Select(l => l.IndexOf(1.0F)).ToList();
 
            var inputDataMap = new Dictionary<Variable, Value>() { { featureVariable, testFeatureValue } };
            var outputDataMap = new Dictionary<Variable, Value>() { { classifierOutput.Output, null } };
            classifierOutput.Evaluate(inputDataMap, outputDataMap, device);
            var outputValue = outputDataMap[classifierOutput.Output];
            IList<IList<float>> actualLabelSoftMax = outputValue.GetDenseData<float>(classifierOutput.Output);
            var actualLabels = actualLabelSoftMax.Select((IList<float> l) => l.IndexOf(l.Max())).ToList();
            int misMatches = actualLabels.Zip(expectedLabels, (a, b) => a.Equals(b) ? 0 : 1).Sum();
 
            Console.WriteLine($"Validating Model: Total Samples = {testSize}, Misclassify Count = {misMatches}");
        }
 
        private static void GenerateValueData(int sampleSize, int inputDim, int numOutputClasses,
            out Value featureValue, out Value labelValue, DeviceDescriptor device)
        {
            float[] features;
            float[] oneHotLabels;
            GenerateRawDataSamples(sampleSize, inputDim, numOutputClasses, out features, out oneHotLabels);
 
            featureValue = Value.CreateBatch<float>(new int[] { inputDim }, features, device);
            labelValue = Value.CreateBatch<float>(new int[] { numOutputClasses }, oneHotLabels, device);
        }
 
        private static void GenerateRawDataSamples(int sampleSize, int inputDim, int numOutputClasses,
            out float[] features, out float[] oneHotLabels)
        {
            Random random = new Random(0);
 
            features = new float[sampleSize * inputDim];
            oneHotLabels = new float[sampleSize * numOutputClasses];
 
            for (int sample = 0; sample < sampleSize; sample++)
            {
                int label = random.Next(numOutputClasses);
                for (int i = 0; i < numOutputClasses; i++)
                {
                    oneHotLabels[sample * numOutputClasses + i] = label == i ? 1 : 0;
                }
 
                for (int i = 0; i < inputDim; i++)
                {
                    features[sample * inputDim + i] = (float)GenerateGaussianNoise(31, random) * (label + 1);
                }
            }
        }
 
        /// <summary>
        /// https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
        /// https://stackoverflow.com/questions/218060/random-gaussian-variables
        /// </summary>
        /// <returns></returns>
        static double GenerateGaussianNoise(double mean, double stdDev, Random random)
        {
            double u1 = 1.0 - random.NextDouble();
            double u2 = 1.0 - random.NextDouble();
            double stdNormalRandomValue = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
            return mean + stdDev * stdNormalRandomValue;
        }
 
        private static Function CreateLinearModel(Variable input, int outputDim, DeviceDescriptor device)
        {
            int inputDim = input.Shape[0];
            var weightParam = new Parameter(new int[] { outputDim, inputDim }, DataType.Float, 1, device, "w");
            var biasParam = new Parameter(new int[] { outputDim }, DataType.Float, 0, device, "b");
 
            return CNTKLib.Times(weightParam, input) + biasParam;
        }
 
        static void Main(string[] args)
        {
            var device = DeviceDescriptor.CPUDevice;
 
            Console.WriteLine($"======== running LogisticRegression.TrainAndEvaluate using {device.Type} ========");
            TrainAndEvaluate(device);
        }
    }
}
cs


위의 Logistic Regression 코드는 CNTK의 공식 C# Example의 LogisticRegression.cs 소스를 참고한 것입니다.


이제 빌드 후 실행하면, 다음과 같이 Command Line 창이 열리고 Logistic Regression의 Training이 진행되는 것을 확인하실 수 있습니다.


Visual Studio Template으로부터 프로젝트 생성

CNTK CPU의 새로운 프로젝트 생성하여 매번 개발환경을 구축하는 것은 매우 번거롭습니다. 따라서, 편의를 위하여 Visual Studio Template을 배포하고자 합니다.


우선, 다음 첨부된 파일을 다운로드를 하시기 바랍니다: CntkCpuExample.zip


다운로드한 파일을 (압축 해제하지 않고) 다음 경로에 복사합니다:

C:\Users\{USERNAME}\Documents\Visual Studio 2015\Templates\ProjectTemplates\


Visual Studio를 닫은 후 재실행하여 새로운 프로젝트를 생성하면 아래 이미지와 같이 Templates > Visual C# > CntkCpuExample 템플릿이 등록되어 있음을 확인할 수 있습니다.



원하는 경로에 원하는 이름으로 프로젝트를 생성한 후, Solution Platformx64로 설정하여 빌드하면 자동으로 CNTK NuGet 패키지가 로딩되고 빌드가 될 것입니다.


이상으로, CNTK CPU 예제 코드를 작성하는 방법에 대하여 알아보았고, 다음 튜토리얼에서는 CNTK GPU 실행환경을 구축하는 방법에 대하여 알아보도록 하겠습니다.

Comments