C#でDeep Learning(Accord.NETによる方法)

流行りのDeep LearningをC#で試してみる.機械学習やコンピュータビジョン,信号処理等の.NET向けのオープンソースのライブラリであるAccord.NET FrameworkにDeep Learningが実装されているのでそれを使う.

Deep Belief Networks(DBN), Deep Neural Networks(DNNs)とおまけにRestricted Boltzmann Machine(RBM)を単体で動かしてみる.

Deep Learning自体については以下のページ等を参考に.
MIRU2014 tutorial deeplearning
Deep Learning技術の今
ディープラーニング チュートリアル(もしくは研究動向報告)

Accord.NETのインストール

NuGet経由でインストール可能.プロジェクトを右クリックして「NuGetパッケージの管理」を選択する.オンラインから「Accord.Neuro」を検索・インストール.

f:id:whoopsidaisies:20140819013518p:plain

ソースコード

を動かすサンプルコードを載せていくが,いずれのサンプルでも以下のusingを書いておくこと.

using Accord.Neuro;
using Accord.Neuro.Networks;
using Accord.Neuro.Learning;
using AForge.Neuro.Learning;
using Accord.Neuro.ActivationFunctions;
using Accord.Math;

Deep Belief Networks(DBN) - 教師あり学習(Back propagation)

// トレーニングデータ
double[][] inputs = {
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 1, 0, 1, 0, 0, 0 },
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
    new double[] { 0, 0, 1, 1, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
};
double[][] outputs = {
    new double[] { 1, 0 },
    new double[] { 1, 0 },
    new double[] { 1, 0 },
    new double[] { 0, 1 },
    new double[] { 0, 1 },
    new double[] { 0, 1 },
};

// DBNの生成
var network = new DeepBeliefNetwork(
    inputsCount: inputs.Length,         // 入力層の次元
    hiddenNeurons: new int[] { 4, 2 }); // 隠れ層と出力層の次元

// ネットワークの重みをガウス分布で初期化する
new GaussianWeights(network).Randomize();
network.UpdateVisibleWeights();

// DBNの学習アルゴリズムの生成  5000回繰り返し入力
var teacher = new BackPropagationLearning(network);                        
for (int i = 0; i < 5000; i++)
    teacher.RunEpoch(inputs, outputs);

// 重みの更新
network.UpdateVisibleWeights();

// 学習されたネットワークでテストデータが各クラスに属する確率を計算
double[] input = { 1, 1, 1, 1, 0, 0 };
var output = network.Compute(input);

//  一番確率の高いクラスのインデックスを得る
int imax; output.Max(out imax);

// 結果出力
Console.WriteLine("class : {0}", imax);
foreach (var o in output)
{
    Console.Write("{0} ", o);
}

実行結果

class : 0
0.883118949465337 0.115560850111682

Deep Belief Networks(DBN) - 教師なし学習(Contrastive Divergence)

// トレーニングデータ
double[][] inputs = {
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 1, 0, 1, 0, 0, 0 },
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
    new double[] { 0, 0, 1, 1, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
};

// DBNの生成
var network = new DeepBeliefNetwork(
    inputsCount: 6,
    hiddenNeurons: new int[] { 4, 2 });

// ネットワークの重みをガウス分布で初期化する
new GaussianWeights(network).Randomize();
network.UpdateVisibleWeights();

// DBNの学習アルゴリズムの生成  5000回繰り返し入力
var teacher = new DeepBeliefNetworkLearning(network)
{
    Algorithm = (h, v, i) => new ContrastiveDivergenceLearning(h, v)
};
var layerData = teacher.GetLayerInput(inputs);
for (int i = 0; i < 5000; i++)
    teacher.RunEpoch(layerData);
// 重みの更新
network.UpdateVisibleWeights();

// 学習されたネットワークでテストデータが各クラスに属する確率を計算
double[] input = { 1, 1, 1, 1, 0, 0 };
var output = network.Compute(input);

//  一番確率の高いクラスのインデックスを得る
int imax; output.Max(out imax);

// 結果出力
Console.WriteLine("class : {0}", imax);
foreach (var o in output)
{
    Console.Write("{0} ", o);
}

実行結果

class : 1
0.529437183967883 0.554114047128916

Deep Neural Networks(DNNs)

// トレーニングデータ
double[][] inputs = {
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 1, 0, 1, 0, 0, 0 },
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
    new double[] { 0, 0, 1, 1, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
};
double[][] outputs = {
    new double[] { 1, 0 },
    new double[] { 1, 0 },
    new double[] { 1, 0 },
    new double[] { 0, 1 },
    new double[] { 0, 1 },
    new double[] { 0, 1 },
};

// ネットワークの生成
var network = new DeepBeliefNetwork(
    inputsCount: inputs.Length,         // 入力層の次元
    hiddenNeurons: new int[] { 4, 2 }); // 隠れ層と出力層の次元

// DNNの学習アルゴリズムの生成
var teacher = new DeepNeuralNetworkLearning(network)
{
    Algorithm = (ann, i) => new ParallelResilientBackpropagationLearning(ann),
    LayerIndex = network.Machines.Count - 1,
};

// 5000回繰り返し学習
var layerData = teacher.GetLayerInput(inputs);
for (int i = 0; i < 5000; i++)
    teacher.RunEpoch(layerData, outputs);

// 重みの更新
network.UpdateVisibleWeights();

// 学習されたネットワークでテストデータが各クラスに属する確率を計算
double[] input = { 1, 1, 1, 1, 0, 0 };
var output = network.Compute(input);

//  一番確率の高いクラスのインデックスを得る
int imax; output.Max(out imax);

// 結果出力
Console.WriteLine("class : {0}", imax);
foreach (var o in output)
{
    Console.Write("{0} ", o);
}

実行結果

class : 0
0.999680477117216 0.00120961077917418

Restricted Boltzmann Machine(RBM)

// トレーニングデータ
double[][] inputs = {
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 1, 0, 1, 0, 0, 0 },
    new double[] { 1, 1, 1, 0, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
    new double[] { 0, 0, 1, 1, 0, 0 },
    new double[] { 0, 0, 1, 1, 1, 0 },
};
double[][] outputs = {
    new double[] { 1, 0 },
    new double[] { 1, 0 },
    new double[] { 1, 0 },
    new double[] { 0, 1 },
    new double[] { 0, 1 },
    new double[] { 0, 1 },
};

// RBMの生成
var rbm = new RestrictedBoltzmannMachine(
    inputsCount: 6, 
    hiddenNeurons: 2);

// トレーニングデータで学習
var teacher = new ContrastiveDivergenceLearning(rbm);
for (int i = 0; i < 5000; i++)
    teacher.RunEpoch(inputs);

// テストデータ
double[] input = { 1, 1, 1, 1, 0, 0 };

// 学習されたネットワークで各クラスに属する確率を計算
var output = rbm.Compute(input);

//  一番確率の高いクラスのインデックスを得る
int imax; output.Max(out imax);

// 結果出力
Console.WriteLine("class : {0}", imax);
foreach (var o in output)
{
    Console.Write("{0} ", o);
}

実行結果

class : 1
0.133832413712274 0.906343089146992

C#でSVM libsvm.netによる方法

SVMのライブラリであるLIBSVMを.NETプロジェクトで使えるようにするlibsvm.netを動かしてみる.

libsvm.netのインストール

NuGet経由でインストール可能.プロジェクトを右クリックして「NuGetパッケージの管理」を選択する.オンラインから「libsvm.net」を検索・インストール.

動作確認

libsvm.netでは,トレーニングデータやテストデータをLIBSVMと同じ形式でファイルから入力できる.データのフォーマットは以下の通りである.

<label> <index1>:<value1> <index2>:<value2> ... <indexN>:<valueN>
<label> <index1>:<value1> <index2>:<value2> ... <indexN>:<valueN>
<label> <index1>:<value1> <index2>:<value2> ... <indexN>:<valueN>
.
.
.
<label> <index1>:<value1> <index2>:<value2> ... <indexN>:<valueN>

LIBSVMの入力データ形式について

ここにLIBSVM用のデータセットが落ちていたので,fourclassというデータセットを使ってみる.

以下に,分類器を動かすソースコード乗せる.

// トレーニングデータの読み込み
var training = libsvm.ProblemHelper.ReadProblem("fourclass");
// RBFカーネルを生成
var gamma = 1;
var kernel = libsvm.KernelHelper.RadialBasisFunctionKernel(gamma);
// C-SVC用のクラスの生成
var c = 1;
var svm = new libsvm.C_SVC(training, kernel, c);

// テストデータの読み込み(面倒だったのでトレーニングデータをそのまま使ってる)
var test = libsvm.ProblemHelper.ReadProblem("fourclass");
for (int i = 0; i < test.l; i++)
{
    // 予測値
    var z = svm.Predict(test.x[i]);
    // 入力ファイルについているラベル
    var y = test.y[i];
    // 予測値と正解値を出力
    Console.WriteLine("{0}:{1} {2}", i, z, y);
}

トレーニングデータとテストデータは同じものを使った.実行すると,予測値と正解値が出力される.

上記コードではパラメータ選択とかまでは行っていないが,結構簡単に使えそう.他にも回帰 (ϵ-SVR)や外れ値検出 (One-Class SVM) が出来るようである.

C# LINQでFIRフィルタ

LINQを使った,FIRフィルタ演算を行う関数.

ソースコード

/// <summary>
/// FIRフィルタをかけたデータを返す
/// </summary>
/// <param name="data">入力データ</param>
/// <param name="coefficients">フィルタ係数</param>
/// <returns>FIRフィルタをかけたデータ</returns>
List<double> FiniteImpulseResponseFilter(
    List<double> data,
    List<double> coefficients)
{
    return Enumerable.Range(0, data.Count - coefficients.Count)
        .Select(i => data.Skip(i).Take(coefficients.Count)
            .Zip(coefficients, (d, c) => d * c).Sum())
        .ToList();
}

C# LINQで移動平均 - whoopsidaisies's diary
をベースにした.

.Zip(coefficients, (d, c) => d * c).Sum()

の部分で入力データとフィルタ係数の積和演算を行っている.

C# LINQで移動平均

LINQを使った,移動平均を計算する関数.

ソースコード

/// <summary>
/// 移動平均を格納した配列を返す
/// </summary>
/// <param name="data">入力データ</param>
/// <param name="num">何個分の平均をとるか</param>
/// <returns>移動平均をとったデータ</returns>
List<double> MovingAverage(
    List<double> data,
    int num)
{
    return Enumerable.Range(0, data.Count - num)
        .Select(i => data.Skip(i).Take(num).Average())
        .ToList();
}

LINQの練習用なので効率が良いかとかは知らない.

Enumerable.Range(0, data.Count - num)

で,0~総データ数-平均を求めるときのデータの個数までの整数のシーケンスを生成する.

data.Skip(i).Take(num).Average()

で,入力データのi番目のデータからnum個データを抽出して平均をとる.

その他

FIRフィルタは以下の記事.
C# LINQでFIRフィルタ - whoopsidaisies's diary

C#でExcelファイルを作成・グラフを挿入する NetOfficeによる方法

C#からExcelファイルを作って,グラフも挿入したい.

以前,以下の記事でClosedXmlというライブラリを使ってC#から簡単にExcelファイルを作る方法を紹介したが,グラフの挿入が出来ないのがグラフの挿入が出来ないのが気になっていた.
C#でExcelファイルを作成する ClosedXmlを使用 - whoopsidaisies's diary

NetOfficeというライブラリを使えば,ClosedXmlと同じくらい簡単にExcelファイルが作成でき,さらにグラフの挿入も行えるので,今回はその方法を紹介する.

NetOfficeのインストール

NetOfficeはMicrosoft Officeにアクセスするための.NETのライブラリで,今回紹介するExcel以外にも,Word, PowerPoint等にもアクセスできるらしい.

インストールはNuget経由で可能.プロジェクトを右クリックして「NuGetパッケージの管理」を選択する.オンラインから検索て「NetOffice.Excel」をインストールする.

f:id:whoopsidaisies:20140619095605p:plain

動作確認

以下,動作確認用のソースコードと生成されたExcelファイルの中身.実行環境ではExcel2010がインストール済み.

class Program
{
    static void Main(string[] args)
    {
        // Excelを開く
        using (var excelApplication = new NetOffice.ExcelApi.Application())
        {
            // ワークブックを追加
            var workBook = excelApplication.Workbooks.Add();
            // ワークシートを取得
            var workSheet = (NetOffice.ExcelApi.Worksheet)workBook.Worksheets[1];

            // データの入力
            workSheet.Cells[1, 2].Value = "ブラウザシェア";
            workSheet.Cells[2, 1].Value = "IE";
            workSheet.Cells[2, 2].Value = "52.56%";
            workSheet.Cells[3, 1].Value = "Chrome";
            workSheet.Cells[3, 2].Value = "24.7%";
            workSheet.Cells[4, 1].Value = "Firefox";
            workSheet.Cells[4, 2].Value = "12.92%";
            workSheet.Cells[5, 1].Value = "その他";
            workSheet.Cells[5, 2].Value = "=1-B2-B3-B4";
                
            // グラフオブジェクトの追加
            var chart = ((NetOffice.ExcelApi.ChartObjects)workSheet.ChartObjects()).Add(70, 100, 375, 225);
            // 円グラフに設定
            chart.Chart.ChartType = NetOffice.ExcelApi.Enums.XlChartType.xlPie;
            // データ範囲を指定
            chart.Chart.SetSourceData(workSheet.Range("A1:B5"));

            // 保存(環境に合わせて拡張子xlsとかでも大丈夫みたい)
            workBook.SaveAs(@"d:\test.xlsx");

            // Excelを終了する
            excelApplication.Quit();
        }
    }
}

出来上がったExcelファイルの中身はこんな感じ.

f:id:whoopsidaisies:20140619094620p:plain

その他

以下のページにグラフ以外のサンプルも多数ある.
NetOffice - MS Office in .NET - Home
【NetOffice】【Excel】NetOfficeのまとめ | 創造的プログラミングと粘土細工(こっちは日本語)

C#でグラフを描く OxyPlotのFunctionSeriesによる方法

.NETフレームワーク用のグラフ描画ライブラリであるOxyPlotを使ってグラフを描く.
その中でもFunctionSeriesクラスを使って与えられた関数のグラフを描く方法を記す.

OxyPlotはWindows Formアプリケーションだけでなく,WPF,Silverlight,Windows8等様々な環境で使える.

Chartコントロールとの比較

Chartコントロールによるグラフの描画方法は以下の別の記事で紹介したことがあるが,比較してOxyPlotの方が良いなと思っている点をあげる.

  • 関数を渡してこの範囲を表示するとかが楽(今回の記事のメイン.詳細は後述.たとえば

y=x^3+2x^2-3-2\le x\le 2の範囲のグラフを描画とか)

  • マウス操作によるグラフの拡大・縮小,移動等の機能が最初からついている
  • ベクター画像できれいにグラフを保存できる(ChartコントロールでもEMFで保存できるがグラフの種類によっては汚い)
  • 配列データ等にバインディングさせて,複数のグラフを描画するとき便利な気がする(今度記事にまとめようと思います)

OxyPlotのインストール

NuGet経由でインストール可能.プロジェクトを右クリックして「NuGetパッケージの管理」を選択する.オンラインから「OxyPlot」を検索.自分に合った環境のパッケージをインストール.(この記事ではOxyPlot.WindowsFormsを使った)

f:id:whoopsidaisies:20140210110630p:plain

Plotコントロールの追加

グラフ描画用のPlotというコントロールが用意されているので使う.

ツールボックスの適当なところで右クリックして「アイテムの選択(I)...」を選択.「.NET Framework コンポーネント」タブの「参照(B)...」ボタンを押して,「(ソリューションフォルダ)\packages\OxyPlot.WindowsForms.yyyy.x.xxx.x\lib\NET45\OxyPlot.WindowsForms.dll」を選択したらOK.

すると,ツールボックスに「Plot」ができるので,Formに配置する.

f:id:whoopsidaisies:20140210111336p:plain

FunctionSeries

OxyPlotには,Func<double, double>のメソッドのグラフを描画してくれるFunctionSeriesクラスがあるのでその使い方を記す.
Func<double, double>という書き方になじみがない人もいるかもしれないので,とりあえず標準正規分布
f\(x\)=\frac{1}{\sqrt{2\pi}}exp\(-\frac{x^{2}}{2}\)
を描くサンプルを以下に示す.

private static double StandardNormalDistribution(double x)
{
    return 1 / Math.Sqrt(2 * Math.PI) * Math.Exp(-x * x / 2);
}

private void Form1_Load(object sender, EventArgs e)
{
    plot1.Model = new OxyPlot.PlotModel { PlotType = OxyPlot.PlotType.XY };
    var series = new OxyPlot.Series.FunctionSeries(
        StandardNormalDistribution,     // 引数double,戻り値doubleの関数
        -3,                             // x座標の最小値
        3,                              // x座標の最大値
        0.01,                           // x座標の刻み幅
        "標準正規分布");                // グラフタイトル
    plot1.Model.Series.Add(series);
}

実行すると以下のようなグラフが表示される.

f:id:whoopsidaisies:20140210114639p:plain

StandardNormalDistributionという引数がdouble型,戻り値もdouble型の関数を定義している.これがFunc<double, double>のメソッドである.
これと,グラフ描画をしたい範囲をFunctionSeriesクラスのコンストラクタに渡してやるだけでグラフを描画してくれて非常に便利である.

ラムダ式による方法

メソッドの部分をラムダ式で表現することも当然出来るので,StandardNormalDistributionメソッドをわざわざ定義しないでも以下のように書くこともできる.(ここではラムダ式の説明は省略.わからない人はここらのへんのページを参照)

    var series = new OxyPlot.Series.FunctionSeries(
        x => 1 / Math.Sqrt(2 * Math.PI) * Math.Exp(-x * x / 2),     // 引数double,戻り値doubleの関数
        -3,                             // x座標の最小値
        3,                              // x座標の最大値
        0.01,                           // x座標の刻み幅
        "標準正規分布");                // グラフタイトル

媒介変数表示された関数のグラフ描画

これも簡単に出来るので例を示す.例として以下のカージオイドを表示させてみる.
x=a\(2\cos\theta - \cos2\theta\)
y=a\(2\sin\theta - \sin2\theta\)

ソースコードは以下のようになる.(ラムダ式使用)

= new OxyPlot.PlotModel { PlotType = OxyPlot.PlotType.XY };
var series = new OxyPlot.Series.FunctionSeries(
    t => 2 * Math.Cos(t) - Math.Cos(2 * t), // x座標の関数
    t => 2 * Math.Sin(t) - Math.Sin(2 * t), // y座標の関数
    0,                                      // tの最小値
    2 * Math.PI,                            // tの最大値
    Math.PI / 32,                           // tの刻み幅
    "カージオイド");                        // グラフタイトル
plot1.Model.Series.Add(series);

実行すると以下のようなグラフが表示される.

f:id:whoopsidaisies:20140210122606p:plain

その他

PlotTypeの設定によって極座標表示とかもできるがそれはまたの機会に.

C#で地球を表示する(vtkTexturedSphereSourceによる方法)

VTKのvtkTexturedSphereSourceで球に世界地図のテクスチャを貼り付けることで地球を表示させる.

テクスチャを使わずにvtkEarthSourceによって表示する方法は以下のページを参照.
C#で地球を表示する(VTKのEarthSourceによる方法) - whoopsidaisies's diary

ActiViz

ActiVizは,3Dコンピュータグラフィックスの可視化ライブラリVTK(The Visualization ToolKit)のC#ラッパである.NuGetからインストールできる.簡単な解説は以下のページを参照.
C#で3Dモデル表示(VTKのC#ラッパActiVizによる方法) - whoopsidaisies's diary

RenderWindowControl

NuGetからActiVizをインストールし,RenderWindowControlをフォームに追加する.

vtkTexturedSphereSource

VTKにはvtkTexturedSphereSourceという,テクスチャを貼り付けることが出来る球オブジェクトのクラスがあるためそれを利用する.ソースコードは以下のようになる.

sing (var sphere = new Kitware.VTK.vtkTexturedSphereSource())
using (var texture = new Kitware.VTK.vtkTexture())
using (var image = new Kitware.VTK.vtkPNGReader())
using (var mapper = new Kitware.VTK.vtkCompositePolyDataMapper())
using (var actor = new Kitware.VTK.vtkActor())
{
    // 球の滑らかさ(数字が多いほど分割数が多く滑らかになる)
    sphere.SetThetaResolution(64);
    sphere.SetPhiResolution(64);
    mapper.SetInputConnection(sphere.GetOutputPort());
    actor.SetMapper(mapper);

    // 世界地図画像の読み込みとテクスチャの設定
    image.SetFileName(@"WorldMap_ja.png");
    texture.SetInputConnection(image.GetOutputPort());                
    actor.SetTexture(texture);

    renderWindowControl1.RenderWindow.GetRenderers().GetFirstRenderer().AddActor(actor);
}

実行すると以下のように地球が表示される.世界地図画像はウィキペディアにあったものを使った.

f:id:whoopsidaisies:20140209202615p:plain

以下のように拡大表示もできるが,この方法だとベクターグラフィックの読み込みが(たぶん)できないので拡大表示する際は何かしら工夫が必要そう.

f:id:whoopsidaisies:20140209203055p:plain