忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

かんたん!制限付きボルツマンマシン実装 (C#) バイナリ(0と1)バージョン [Deep Learningシリーズ]

Implementing Restricted Boltzmann Machine(RBM) in C#

YUSUKE SUGOMORIさんのjavaコードやその他のコードいろいろを参考にしてC#で制限付きボルツマンマシンを実装してみました。
私好みに書き換えていくとだいぶプログラムの行数が無駄に膨れ上がってしまいましたが、まあよしとしましょう。




C#で書いた制限付きボルツマンマシン

全部で4ファイルあります。
  • Program.cs : 制限付きボルツマンマシンを動かします。
  • RestrictedBoltzmannMachine.cs : 制限付きボルツマンマシンです。
  • Neuron.cs : ニューロンです。
  • Synapse.cs : ニューロン間の結合、シナプスです。

Program.cs

このProgramクラスは制限付きボルツマンマシンを動かします。
制限付きボルツマンマシンは学習したデータを思い出すことが出来るのですが、それのデモを行なっているのです。

3つのトレーニングデータを用意し、それを制限付きボルツマンマシンに学習させます。
ここで制限付きボルツマンマシンの可視ニューロンは5つ、隠れニューロンは3つです。
学習は一度だけ行うのではなく、3つのデータを1000回繰り返し学習させます。

学習が終わったら、想起を行います。
制限付きボルツマンマシンは、学習したパターンを思い出します。
学習後にあるパターン(これは学習パターンに似ている、でもちょっと違うパターン)をセットし動かすと、学習したパターンを思い出すのです。
ここでは思い出したパターンを出力します。
using System;

namespace RestrictedBoltzmannMachines.Binary
{
    class Program
    {
        static void Main(string[] args)
        {
            double[][] trainingData = {
                new double[]{1, 1, 1, 0, 0},
                new double[]{0, 0, 1, 1, 1},
                new double[]{0, 1, 1, 1, 0}
		    };

            var hiddenNeuronCount = 3;
            var visibleNeuronCount = trainingData[0].Length;

            var restrictedBoltzmannMachine = new RestrictedBoltzmannMachine(
                visibleNeuronCount,
                hiddenNeuronCount,
                new Random(0)
                );

            var trainingEpochCount = 1000;
            var basicLearningRate = 0.1;

            // train
            for (int epoch = 0; epoch < trainingEpochCount; epoch++)
            {
                foreach (var data in trainingData)
                {
                    restrictedBoltzmannMachine.SetVisibleNeuronValues(data);
                    restrictedBoltzmannMachine.LearnFromData(basicLearningRate / trainingData.Length);
                }
            }

            double[][] testInputData = {
			    new double[]{1, 1, 0, 0, 0},
			    new double[]{0, 0, 0, 1, 1}
		    };

            foreach (var input in testInputData)
            {
                restrictedBoltzmannMachine.SetVisibleNeuronValues(input);
                restrictedBoltzmannMachine.Associate();

                foreach (var output in restrictedBoltzmannMachine.VisibleNeurons)
                {
                    Console.Write(output.Value + " ");
                }

                Console.WriteLine();
            }
        }
    }
}

RestrictedBoltzmannMachine.cs

制限付きボルツマンマシンのクラスです。
このクラスの機能は主に2つ、学習と想起です。

ここでは、コントラスティブ・ダイバージェンス(Contrastive Divergence : CD)法というものを使って学習しています。
なんだか一見難しそうですが、要は「"起きてる時に(複数ではなく)一つだけパターンを見て学習し、そのあとその状態のまま眠りシナプスの重みを調節する"、ということを繰り返して色々なパターンを覚える」ということを言っているだけです。

面白いことに、制限付きボルツマンマシンは夢をみる(かもしれない!)のです。
起きている時にデータを受け取って学習するのは普通ですが、寝ている時は自由にネットワークが動き(つまり、可視ニューロンが外部からの入力によって更新されるのではなく、隠れニューロンの状態を元に更新され)、反学習というものをするのです。
反学習は学習係数の符号がマイナスの学習です。
制限付きボルツマンマシンの覚醒時における学習は、シナプスの重みやニューロンのバイアスを大きくする一方ですが、睡眠時の反学習はそれらを小さくすることができます。
調度良い重みやバイアスにするために睡眠の反学習が必要なのかもしれませんね。
これは故クリックさん(ワトソンとクリックのクリックです)が提唱した「人は忘れるために夢を見る」という説に似ていますね。
なんだか本物の生き物のようですが、関連があるかどうか私は知りません…

制限付きボルツマンマシンの、コントラスティブ・ダイバージェンスによる学習は、次のような流れとなります:
  1. 外部からの入力パターンを学習する(目が覚めている状態:Positive Phase)
    1. 入力パターンを可視ニューロンにセットする(つまり可視ニューロンのOn/Offを環境からセットする)。
    2. 隠れニューロンの発火状態(On/Off、1か0)を更新する。
    3. 結合の重みを、ヘブ則に従って更新する準備をする。
      つまり、
      d重み += 学習係数 × 可視ニューロンの発火状態 × 隠れニューロンの発火確率;
      (「d重み」は重みの変化を表す変数です。
      最後に「重み += d重み;」します。)
    4. ニューロンのバイアスを更新する準備をする。
      ここで、可視ニューロンと隠れニューロンでなぜか学習式が異なることに注意。
      可視ニューロン : dバイアス += 学習係数 × 発火状態;
      隠れニューロン : dバイアス += 学習係数 × 発火確率;
      (「dバイアス」はバイアスの変化を表す変数です。
      最後に「バイアス += dバイアス;」します)
  2. 外部からの入力をなくし、自由連想する(夢を見ている状態 : Negative Phase)
    1. 可視ニューロンの発火状態を更新する。
    2. 隠れニューロンの発火状態を更新する。(場合によってはここから1に戻り、1、2を何度か繰り返すが、戻らなくてもそこそこのいい結果が出ることが知られている)
    3. 結合の重みを、ヘブ則の逆で更新する準備をする。
      つまり、
      d重み += -学習係数 × 可視ニューロンの発火状態 × 隠れニューロンの発火確率;
      I.3では重みを増やしていたが、ここでは重みを減らしていることに注目。
      (学習係数の前にマイナス符号あり)
    4. ニューロンのバイアスを更新する準備をする。
      ここで、可視ニューロンと隠れニューロンでなぜか学習式が異なることに注意。
      可視ニューロン : dバイアス += -学習係数 × 発火状態;
      隠れニューロン : dバイアス += -学習係数 × 発火確率;
      I.4ではバイアスを増やしていたが、ここではバイアスを減らしていることに注意。
      (学習係数の前にマイナス符号有り)
  3. 結合の重み、ニューロンのバイアスを更新する。
    重み += d重み;
    バイアス += dバイアス;
    その後、
    d重み = 0;
    dバイアス = 0;
I、II、IIIを繰り返します。
(1000回くらい?)

想起のメカニズムは簡単です。
確率的に動くHopfieldネットワークのようなものです。
つまり、シナプスからの入力が大きければ大きいほど発火する確率が上がるだけのニューラルネットワークです。
using System;

namespace RestrictedBoltzmannMachines.Binary
{
    public class RestrictedBoltzmannMachine
    {
        public SymmetricConnection[][] Connections;
        public Neuron[] HiddenNeurons;
        public Neuron[] VisibleNeurons;
        public Random Random;

        public RestrictedBoltzmannMachine(int visibleNeuronCount, int hiddenNeuronCount, Random random) :
            this(SymmetricConnection.CreateRandomWeights(random, visibleNeuronCount, hiddenNeuronCount), new double[visibleNeuronCount], new double[hiddenNeuronCount], random)
        {
        }

        public RestrictedBoltzmannMachine(double[][] weights, double[] visibleBiases, double[] hiddenBiases, Random random)
        {
            this.Random = random;
            this.VisibleNeurons = Neuron.CreateNeurons(visibleBiases);
            this.HiddenNeurons = Neuron.CreateNeurons(hiddenBiases);
            this.Connections = SymmetricConnection.CreateConnections(weights, VisibleNeurons, HiddenNeurons);
            Neuron.WireConnections(this.Connections);
        }

        public void SetVisibleNeuronValues(double[] visibleValues)
        {
            for (int i = 0; i < this.VisibleNeurons.Length; i++)
            {
                this.VisibleNeurons[i].Value = visibleValues[i];
            }
        }

        public void LearnFromData(double learningRate, int freeAssociationStepCount = 1)
        {
            Wake(learningRate);
            Sleep(learningRate, freeAssociationStepCount);
            EndLearning();
        }

        public void Wake(double learningRate)
        {
            updateNeurons(this.HiddenNeurons);
            learn(learningRate);
        }

        private void updateNeurons(Neuron[] neurons)
        {
            foreach (var neuron in neurons)
            {
                neuron.Update(Random);
            }
        }

        private void learn(double learningRate)
        {
            foreach (var connectionRow in this.Connections)
            {
                foreach (var connection in connectionRow)
                {
                    connection.Learn(learningRate);
                }
            }

            foreach (var hidden in this.HiddenNeurons)
            {
                hidden.LearnAsHidden(learningRate);
            }

            foreach (var visible in this.VisibleNeurons)
            {
                visible.LearnAsVisible(learningRate);
            }
        }

        public void Sleep(double learningRate, int freeAssociationStepCount)
        {
            doFreeAssociation(freeAssociationStepCount);
            learn(-learningRate);
        }

        //Gibbs sampling
        private void doFreeAssociation(int freeAssociationStepCount)
        {
            for (int step = 0; step < freeAssociationStepCount; step++)
            {
                updateNeurons(this.VisibleNeurons);
                updateNeurons(this.HiddenNeurons);
            }
        }

        public void EndLearning()
        {
            foreach (var connectionRow in this.Connections)
            {
                foreach (var connection in connectionRow)
                {
                    connection.EndLearning();
                }
            }

            foreach (var hidden in this.HiddenNeurons)
            {
                hidden.EndLearning();
            }

            foreach (var visible in this.VisibleNeurons)
            {
                visible.EndLearning();
            }
        }

        public void Associate()
        {
            updateNeurons(this.HiddenNeurons);
            updateNeurons(this.VisibleNeurons);
        }
    }
}

Neuron.cs

ニューロンのクラスです。
このクラスの機能は主に2つで、バイアスの学習と発火状態の更新です。

バイアスとはそのニューロン特有の興奮しやすさです。
この値が大きければ大きいほどニューロンは興奮しやすく、小さければ興奮しにくいです。
学習の際にはこの値を微調整します。

発火状態の更新をするには、まず発火する確率を求めます。
シナプスからの入力が大きければ大きいほど発火する確率は大きくなります。
逆にシナプスからの入力が小さければ小さいほど発火する確率は小さくなります。
つぎに、発火する確率から疑似乱数を使って発火状態を更新するのです。
たとえば発火確率が0.1なら発火状態が1になる確率は0.1、0になる確率は0.9です。

ところで、可視ニューロンと隠れニューロンで学習の式が違っているのは何故でしょう?
可視ニューロンはValueを使った式で、隠れニューロンはProbabilityを使った式です
私にはわかりませんが、もしかしたら本来両方共Probabilityを使って学習したいのかも知れません。
で、隠れニューロンはProbabilityを使って学習できるのですが、可視ニューロンの方には最初パターンを覚えさせるときにProbability(発火する確率)がありません。
可視ニューロンは学習の覚醒フェイズ(Positive Phase)の際、否応なしに発火のOn,Offを調節されるからです。
発火の元になるProbabilityがないのです。
…と思ったのですが、数式の時点ですでに可視ニューロンと隠れニューロンの式は違っているのでこの推測は間違っているような気もします
鵜呑みにしないで下さい!
using System;
using System.Collections.Generic;
using System.Linq;

namespace RestrictedBoltzmannMachines.Binary
{
    public class Neuron
    {
        public double Value;
        public double Probability;
        public double Bias;
        public double DeltaBias;
        public List<Synapse> Synapses = new List<Synapse>();

        public void Update(Random random)
        {
            this.Probability = sigmoid(Synapses.Sum(s => s.Connection.Weight * s.SourceNeuron.Value) + Bias);
            this.Value = nextBool(random, this.Probability) ? 1 : 0;
        }

        public void LearnAsVisible(double learningRate)
        {
            this.DeltaBias += learningRate * this.Value;
        }

        public void LearnAsHidden(double learningRate)
        {
            this.DeltaBias += learningRate * this.Probability;
        }

        public void EndLearning()
        {
            this.Bias += this.DeltaBias;
            this.DeltaBias = 0;
        }

        public static Neuron[] CreateNeurons(double[] biases)
        {
            Neuron[] result = new Neuron[biases.Length];

            for (int i = 0; i < result.Length; i++)
            {
                result[i] = new Neuron { Bias = biases[i] };
            }

            return result;
        }

        public static void WireConnections(SymmetricConnection[][] connections)
        {
            foreach (var connectionRow in connections)
            {
                foreach (var connection in connectionRow)
                {
                    Synapse hiddenConnection = new Synapse();
                    hiddenConnection.Connection = connection;
                    hiddenConnection.SourceNeuron = connection.VisibleNeuron;
                    connection.HiddenNeuron.Synapses.Add(hiddenConnection);

                    Synapse visibleConnection = new Synapse();
                    visibleConnection.Connection = connection;
                    visibleConnection.SourceNeuron = connection.HiddenNeuron;
                    connection.VisibleNeuron.Synapses.Add(visibleConnection);
                }
            }
        }

        private static double sigmoid(double x)
        {
            return 1.0 / (1.0 + Math.Exp(-x));
        }

        private static bool nextBool(Random random, double rate)
        {
            if (rate < 0 || 1 < rate) return false;
            return random.NextDouble() < rate;
        }
    }
}

Synapse.cs

シナプスのクラス、および結合のクラスです。
シナプスはみてのとおり、ほとんど空っぽです。
結合クラスの機能は主に一つ、学習です。

学習はWeight(重み)という変数を更新します。
ご存じの方も多いと思いますが重みについて解説します。
重みは2つのニューロン間の発火させやすさを表します。
重みが大きければ片方のニューロンはもう片方を発火させやすいです。
重みが小さければ発火させにくくなります。

学習の方法はHopfieldネットワークでもそうだったように、ヘブ則です。
つまり、両方のニューロンが興奮していたら、それだけ結合を強めます。
ただし、普通のHopfieldネットワークと違って、ニューロンの値は-1, 1ではなく0, 1であるということに注意して下さい。
つまり学習係数が正であれば重みは大きくなる一方です。

ニューロンクラスでも触れましたが、学習に使われる変数が可視ニューロンと隠れニューロンで違っていますね。
可視ニューロンではValueですが、隠れニューロンではProbabilityです。
一体何故なのでしょうか…?
using System;

namespace RestrictedBoltzmannMachines.Binary
{
    public class Synapse
    {
        public Neuron SourceNeuron;
        public SymmetricConnection Connection;
    }

    public class SymmetricConnection
    {
        public double Weight;
        public double DeltaWeight;
        public Neuron VisibleNeuron;
        public Neuron HiddenNeuron;

        public void Learn(double learningRate)
        {
            this.DeltaWeight += 
                learningRate * VisibleNeuron.Value * HiddenNeuron.Probability;
        }

        public void EndLearning()
        {
            this.Weight += this.DeltaWeight;
            this.DeltaWeight = 0;
        }

        public static double[][] CreateRandomWeights(Random random, int visibleNeuronCount, int hiddenNeuronCount)
        {
            var result = createJaggedArray<double>(visibleNeuronCount, hiddenNeuronCount);

            double a = 1.0 / visibleNeuronCount;

            for (int i = 0; i < visibleNeuronCount; i++)
            {
                for (int j = 0; j < hiddenNeuronCount; j++)
                {
                    result[i][j] = uniform(random, -a, a);
                }
            }

            return result;
        }

        private static T[][] createJaggedArray<T>(int visibleNeuronCount, int hiddenNeuronCount)
        {
            var result = new T[visibleNeuronCount][];

            for (int i = 0; i < visibleNeuronCount; i++)
            {
                result[i] = new T[hiddenNeuronCount];
            }

            return result;
        }

        private static double uniform(Random random, double min, double max)
        {
            return random.NextDouble() * (max - min) + min;
        }

        public static SymmetricConnection[][] CreateConnections(double[][] weights, Neuron[] visibleNeurons, Neuron[] hiddenNeurons)
        {
            var result = createJaggedArray<SymmetricConnection>(visibleNeurons.Length, hiddenNeurons.Length);

            for (int i = 0; i < visibleNeurons.Length; i++)
            {
                for (int j = 0; j < hiddenNeurons.Length; j++)
                {
                    SymmetricConnection connection = new SymmetricConnection();
                    connection.Weight = weights[i][j];
                    connection.VisibleNeuron = visibleNeurons[i];
                    connection.HiddenNeuron = hiddenNeurons[j];
                    result[i][j] = connection;
                }
            }

            return result;
        }
    }
}

実行結果

Program.csを実行すると次のように出力されます:
1 1 1 0 0
0 0 1 1 1

これは何を意味しているのでしょう?
2つの行が表示されていますね。
答えは、思い出したパターンです。

制限付きボルツマンマシンは学習したパターンと似たパターンを与えると学習したパターンを思い出すニューラルネットワークだということを思い出して下さい。
ここでは学習したパターンと言うのはtrainingDataに入っている3つの配列です。
次の3つのパターンを学習させたのです。(0 : ■、1 : □)
□□□■■
■■□□□
■□□□■

そして学習が終わった後、新たにパターンを与え動かすと、それに似た学習したパターンを思い出すのです。 入力したパターン → 思い出したパターン
□□■■■ → □□□■■
■■■□□ → ■■□□□

拍手[3回]

PR