忍者ブログ

Memeplexes

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

[Deep Learningシリーズ] かんたんHopfieldネットワーク

Deep Learning

最近Deep Learningというものが話題になっています。
Deep Learningはコンピュータの学習手法の一つです。
一年ほど前(2012年)GoogleがコンピュータにたくさんのYoutube画像を見せネコの顔を自発的に学ばせたということで有名ですね。
人工知能学会誌には現在ディープラーニングの解説が連載中です。
Deep Learningはコンピュータ関係で現在最もホットな話題の一つといえるでしょう。

というわけでこのブログでもDeep Learningを取り上げてみたいと思います。
しかしいきなりDeep Learningの全容に迫るのは難しいのでまずは簡単な話から始めましょう。
今回はHopfield(ホップフィールド)ネットワークです。

なぜHopfieldネットワーク?

Deep Learningはたくさんの部品を積み重ねて高い性能を出しました。
たとえば「制限付きボルツマンマシン(Restricted Boltzmann Machine, RBM)」がその部品として使われます。
制限付きボルツマンマシンを何層も積み重ねるのです。
だから「Deep(深い)」なわけですね。

制限付きボルツマンマシンは相互結合型ニューラルネットワークと言うタイプのもので、そういう意味ではHopfieldネットワークに似ています。
似ている、より単純なものからやっていこうというわけです。

ちなみに制限付きボルツマンマシンというのは次のようなニューラルネットワークです。




そして今回扱うHopfieldネットワークは次のようなニューラルネットワークです。



「見た目ぜんぜん違うじゃないか!」という苦情の声が聞こえてきそうです。

そのとおり!
制限付きボルツマンマシンはニューロンが2種類(青と灰色)に分かれています。
一方でこのHopfieldネットワークはニューロンは1種類(青)しかありません。
そういう意味でこの2つは違うのですが(そして他にも違いはあるのですが)、どちらとも結合が対称であるという意味で同じです(結合が対称であるとはどういうことかというと、結合の重みがどちらの方向にも同じであるということです)。

そんなんで大丈夫かと思われる方もいらっしゃるはずですが、この方針で行きます!
まずはHopfieldネットワークを解説し、その後で制限付きボルツマンマシンを解説します。

Hopfieldネットワーク

ではHopfieldネットワークについて解説します。
Hopfieldネットワークはニューラルネットワークの一つです。
脳のニューロンをモデル化したシミュレーションです。
池谷さんのこのページで解説されているやつです。



上の図は一種のHopfieldネットワークです。
5つの青い丸がありますね。
これがニューロンです。
そしてニューロンの間を結ぶ線が信号の通信路です。

ニューロンは2つの状態のいずれかを取ります。
通常状態(-1)か興奮した状態(1)です。
そしてその興奮パターンによって何をコードしているかを表します。
たとえば(1, 1, -1, 1, 1)なら人、(1, -1, 1, -1, 1)ならネコといった調子です。

Hopfieldネットワークは物を覚え、思い出すことができます。
過去に学習したパターンに似たパターンを与えられると、その学習したパターンを想起するのです。
たとえば(1, 1, -1, 1, 1)を学習したとすると、(1, 1, 1, 1, 1)の状態からニューロンを動かせば、そのうち(1, 1, -1, 1, 1)に状態が遷移します。
しかし状態があまりに学習したパターンからかけ離れていると学習したパターンを思い出すことができません(例えば(-1,-1,1,-1,-1)では無理)。

Hopfieldネットワークの学習方法

ホップフィールドネットワークの学習ルールはとても単純です。
  1. 2つのニューロンが両方とも興奮していればその間のシナプスは強化される。
  2. 2つのニューロンが「両方とも興奮していない」状態だったらその間のシナプスは強化される。
  3. 2つのニューロンの片方しか興奮していなければその間のシナプスは弱体化する。l

いわゆるヘッブの法則というやつです。
2つのニューロンが同じように動くのならその間のシナプスは強化され、
違うように動くのならシナプスは弱体化するのです。

  • (ニューロン1の状態, ニューロン2の状態) => シナプスの強化ぐあい
  • (1, 1) => +1
  • (-1, -1) => +1
  • (1, -1) => -1
  • (-1, 1) => -1
以下はTypeScriptで書かれた学習するコードです。
class HopfieldNetwork {
    neurons: Array<Neuron> = new Array<Neuron>();
..
    learn() {
        for (var i in this.neurons)
        {
            this.neurons[i].learnByHebbian();
        }
    }
..
}

class Neuron {
    value: number = -1;
..
    synapses: Array<Synapse> = new Array<Synapse>();
..
    learnByHebbian() {
        for (var i in this.synapses)
        {
            this.synapses[i].learnByHebbian(this.value);
        }
    }
}

class Synapse {
..
    learnByHebbian(destinationValue: number) {
        this.weight += destinationValue * this.source.value;
    }
}

Hopfieldネットワークの想起方法

Hopfieldネットワークは以上のようにして学習したパターンを、後で思い出すことができます。
思い出すには条件があって、あらかじめ思い出すパターンに似たパターンをセットされていなければなりません。
あまりにかけ離れていると想起失敗です。

どのようにして思い出すのかというと、単にネットワークを何度か動かすだけで良いです。
つまり、自分につながっているニューロンがたくさん興奮していれば自分も興奮するというようなあれを何度か繰り返すのです。
そうすれば、学習したパターンに似たパターンから、学習したパターンそのものを思い出すことが出来る、かもしれません。
かもしれません?ふざけるな!という声が聞こえてきそうですが、そういうものなのです…。
Hopfieldネットワークは想起に失敗することもあるのです。

以下はTypeScriptで書かれた想起するコードです。
class HopfieldNetwork {
    neurons: Array<Neuron> = new Array<Neuron>();
..
    update() {
        for (var i in this.neurons)
        {
            this.neurons[i].prepareUpdate();
        }

        for (var i in this.neurons)
        {
            this.neurons[i].update();
        }
    }
}

class Neuron {
    value: number = -1;
    previousValue: number = 0;
    synapses: Array<Synapse> = new Array<Synapse>();
..

    prepareUpdate() {
        this.previousValue = this.value;
    }

    update() {
        this.value = this.transferFunction(sum(this.synapses, s=> s.getOutput()));
    }

    private transferFunction(sum: number): number {
        return (sum > 0) ? 1 :
            (sum == 0) ? this.previousValue :
            -1;
    }
}

function sum<T>(items : Array < T>, toNumber : (item: T) => number) : number {
    return items.map(item => toNumber(item)).reduce((a, b) => a + b);
}

class Synapse {
    weight: number = 0;
    source: Neuron = null;
..
    getOutput(): number {
        return this.weight * this.source.previousValue;
    }
..
}

Hopfieldネットワークのエネルギー


Hopfieldネットワークにはエネルギーという数字を定義することができます。
何を言っているのかというと、ネットワークを動かした時必ず下がる数字です。
これは別にシミュレーションには必要無いような気もするのですが、扱っているサイトが多いので載せることにします。(ふざけているのではありません)
どれくらい想起が進んでいるのかの目安になるようなならないようなよくわからない数字です。

function sum<T>(items : Array < T>, toNumber : (item: T) => number) : number {
    return items.map(item => toNumber(item)).reduce((a, b) => a + b);
}

class HopfieldNetwork {
    neurons: Array<Neuron> = new Array<Neuron>();
..
    getEnergy(): number {
        return -sum(this.neurons, n => sum(n.synapses, s => s.weight * s.source.value * n.value)) / 2;
    }
}

class Neuron {
    value: number = -1;
..
    synapses: Array<Synapse> = new Array<Synapse>();
..
}

class Synapse {
    weight: number = 0;
    source: Neuron = null;
..
}

デモ

ソースコード

hopfieldNetwork.ts
function sum<T>(items : Array < T>, toNumber : (item: T) => number) : number {
    return items.map(item => toNumber(item)).reduce((a, b) => a + b);
}

class HopfieldNetwork {
    neurons: Array<Neuron> = new Array<Neuron>();

    constructor(neuronCount: number) {
        for (var i = 0; i < neuronCount; i++)
        {
            this.neurons.push(new Neuron());
        }

        for (var i in this.neurons)
        {
            var neuron = this.neurons[i];

            for (var j in this.neurons)
            {
                var from = this.neurons[j];

                if (neuron == from) { continue; }

                neuron.synapses.push(new Synapse(from));
            }
        }
    }

    learn() {
        for (var i in this.neurons)
        {
            this.neurons[i].learnByHebbian();
        }
    }

    update() {
        for (var i in this.neurons)
        {
            this.neurons[i].prepareUpdate();
        }

        for (var i in this.neurons)
        {
            this.neurons[i].update();
        }
    }

    getEnergy(): number {
        return -sum(this.neurons, n => sum(n.synapses, s => s.weight * s.source.value * n.value)) / 2;
    }
}

class Neuron {
    value: number = -1;
    previousValue: number = 0;
    synapses: Array<Synapse> = new Array<Synapse>();
    tag: any = null;

    prepareUpdate() {
        this.previousValue = this.value;
    }

    update() {
        this.value = this.transferFunction(sum(this.synapses, s=> s.getOutput()));
    }

    private transferFunction(sum: number): number {
        return (sum > 0) ? 1 :
            (sum == 0) ? this.previousValue :
            -1;
    }

    learnByHebbian() {
        for (var i in this.synapses)
        {
            this.synapses[i].learnByHebbian(this.value);
        }
    }
}

class Synapse {
    weight: number = 0;
    source: Neuron = null;

    constructor(source: Neuron) {
        this.source = source;
    }

    getOutput(): number {
        return this.weight * this.source.previousValue;
    }

    learnByHebbian(destinationValue: number) {
        this.weight += destinationValue * this.source.value;
    }
}

app.ts
/// <reference path="hopfieldNetwork.ts"/>

class Vector2 {
    x: number;
    y: number;

    static add(a: Vector2, b: Vector2): Vector2 {
        return { x: a.x + b.x, y: a.y + b.y };
    }

    static mul(a: Vector2, scale: number): Vector2 {
        return { x: a.x * scale, y: a.y * scale };
    }

    static createFromAngle(angle: number): Vector2 {
        return { x: Math.sin(angle), y: -Math.cos(angle) };
    }
}

class NeuronView {
    neuron: Neuron;
    position: Vector2;
    isMouseOver: boolean;
    isMouseDown: boolean;
    isMouseClicked: boolean;
    context: CanvasRenderingContext2D;

    constructor(context: CanvasRenderingContext2D) {
        this.context = context;
    }

    initPath() {
        this.context.beginPath();
        this.context.arc(this.position.x, this.position.y, 20, 0, Math.PI * 2);
    }

    update() {
        this.updateMouseInfo();
        
        if (this.isMouseClicked) {
            this.neuron.value = -this.neuron.value;
        }
    }

    private updateMouseInfo() {
        this.initPath();
        this.isMouseOver = false;

        if (context.isPointInPath(mouse.position.x, mouse.position.y)) {
            this.isMouseOver = true;
            this.isMouseClicked = mouse.isClicked;
            this.isMouseDown = mouse.isDown;
        }
    }

    draw() {
        this.initPath();
        this.context.fillStyle = this.getNeuronStyle();
        this.context.fill();
        this.context.stroke();
    }

    private getNeuronStyle() {
        if (this.isMouseOver) {
            if (this.isMouseDown) {
                return "orange";
            } else
            {
                return this.neuron.value > 0 ? "rgb(240, 240, 240)" : "rgb(100, 100, 100)";
            }
        } else {
            return this.neuron.value > 0 ? "white" : "black";
        }
    }

    static getSynapseStyle(synapse: Synapse) {
        if (synapse.weight > 0) {
            return "red";
        }
        else if (synapse.weight == 0) {
            return "lightGray";
        }
        else {
            return "blue";
        }
    }

    drawSynapse(synapse: Synapse) {
        var sourceNeuron = synapse.source;
        var sourceView = <NeuronView>sourceNeuron.tag;

        context.save();
        context.beginPath();
        context.strokeStyle = NeuronView.getSynapseStyle(synapse);
        context.lineWidth = Math.abs(synapse.weight);
        context.moveTo(this.position.x, this.position.y);
        context.lineTo(sourceView.position.x, sourceView.position.y);
        context.stroke();
        context.restore();
    }
}

var canvas = <HTMLCanvasElement>document.getElementById("hopfieldNetworkDemoCanvas");
var context = canvas.getContext("2d");

var hopfieldNetwork = new HopfieldNetwork(5);
var neuronViews = new Array<NeuronView>();

for (var i in hopfieldNetwork.neurons) {
    var angle = 2 * Math.PI * i / hopfieldNetwork.neurons.length;
    var neuronPosition = Vector2.add(
        { x: 200, y: 200 },
        Vector2.mul(Vector2.createFromAngle(angle), 150)
        );
    var view = new NeuronView(context);
    view.neuron = hopfieldNetwork.neurons[i];
    view.position = neuronPosition;
    view.neuron.tag = view;
    neuronViews.push(view);
}

class MouseState {
    position: Vector2 = {x:  10, y :10};
    isDown: boolean;
    previousIsDown: boolean;
    isClicked: boolean;

    constructor(element: HTMLElement) {
        element.onmousemove = function (e) {
            var offset = element.getBoundingClientRect();
            this.position = { x: e.pageX - offset.left, y: e.pageY - offset.top };
        }.bind(this);

        element.onmousedown = function (e) {
            this.isDown = true;
        }.bind(this);

        element.onmouseup = function (e) {
            this.isDown = false;
        }.bind(this);
    }

    update() {
        this.isClicked = this.previousIsDown && !this.isDown;
        this.previousIsDown = this.isDown;
    }
}

var mouse = new MouseState(canvas);

class LearnedPattern {
    values: Array<number>;
    count: number = 0;
    tag: any;
}

class LearnedPatternCollection {
    patterns: Array<LearnedPattern> = new Array<LearnedPattern>();
    onPatternCreated;
    onPatternCountChanged;

    add(pattern: Array<number>) {
        if (!this.contains(pattern)) {
            this.create(pattern);
        }
        this.get(pattern).count++;
        this.onPatternCountChanged(this.get(pattern));
    }

    private contains(pattern: Array<number>) {
        return this.patterns.some(p => LearnedPatternCollection.areEqual(p.values, pattern));
    }

    private create(pattern: Array<number>) {
        var newInstance = new LearnedPattern();
        newInstance.values = pattern;
        this.patterns.push(newInstance);
        this.onPatternCreated(newInstance);
    }

    private get(pattern: Array<number>) {
        for (var i in this.patterns) {
            if (LearnedPatternCollection.areEqual(this.patterns[i].values, pattern)) {
                return this.patterns[i];
            }
        }

        return null;
    }
        
    private static areEqual(p1: Array <number>, p2: Array < number>):boolean{
        if (p1.length != p2.length) {
            return false;
        }

        for (var i = 0; i < p1.length; i++) {
            if (p1[i] != p2[i]) {
                return false;
            }
        }

        return true;
    }

}

var patterns = new LearnedPatternCollection();
patterns.onPatternCreated = function (pattern: LearnedPattern) {
    var listElement = <HTMLTableRowElement>document.getElementById("learnedPatternList");
    var learnedImageCanvas = document.createElement("canvas");
    learnedImageCanvas.width = 120;
    learnedImageCanvas.height = 100;
    var learnedImageContext = learnedImageCanvas.getContext("2d");

    for (var i in pattern.values) {
        var angle = 2 * Math.PI * i / pattern.values.length;
        var neuronPosition = Vector2.add(
            { x: 50, y: 50 },
            Vector2.mul(Vector2.createFromAngle(angle), 38)
            );
        learnedImageContext.beginPath();
        learnedImageContext.arc(neuronPosition.x, neuronPosition.y, 5, 0, Math.PI * 2);
        learnedImageContext.fillStyle = pattern.values[i] > 0 ? "white" : "black";
        learnedImageContext.fill();
        learnedImageContext.stroke();
    }

    var table = <HTMLTableElement>document.createElement("table");
    table.border = "1";
    table.style.styleFloat = "left";
    var cell1 = (<HTMLTableRowElement>table.insertRow(0)).insertCell(0);
    cell1.appendChild(learnedImageCanvas);
    var cell2 = (<HTMLTableRowElement>table.insertRow(1)).insertCell(0);
    cell2.innerHTML = pattern.count + "";
    pattern.tag = cell2;
    
    listElement.insertCell(0).appendChild(table);
}
patterns.onPatternCountChanged = function (pattern: LearnedPattern) {
    var cell = <HTMLTableCellElement>pattern.tag;
    cell.innerHTML = pattern.count + "";
}

document.getElementById("learnButton").onclick = function (e) {
    hopfieldNetwork.learn();
    patterns.add(hopfieldNetwork.neurons.map(n=> n.value));
};

document.getElementById("rememberButton").onclick = function (e) {
    hopfieldNetwork.update();
};

function update() {
    mouse.update();
    
    for (var i in neuronViews)
    {
        neuronViews[i].update();
    }
}

function updateEnergyText() {
    document.getElementById("energyText").innerText = "energy : " + hopfieldNetwork.getEnergy();
}

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < neuronViews.length; i++)
    {
        var neuronView = neuronViews[i];

        for (var j in neuronView.neuron.synapses) {
            var synapse = neuronView.neuron.synapses[j];
            neuronView.drawSynapse(synapse);
        }
    }

    for (var i in neuronViews)
    {
        neuronViews[i].draw();
    }

    updateEnergyText();
}

setInterval(function () {
    update();
    draw();
    }, 1000 / 60.0);

プログラム


黒い丸が5つありますね。
これはそれぞれニューロンを表しています。
その間の線はシナプスを表しています。

最初は全てのニューロンは発火しておらず、すべてのシナプスは重み0です。

適当な黒い丸をクリックしてみて下さい。
色が反転し白になります。
これはニューロンが発火したと言う意味です。

Learnボタンを押してみましょう。
そうすると現在の発火パターンを記憶します。
(記憶したパターンは下に表示されます)
シナプスは太くなり色がつきます。
赤は正、青は負という意味です。

発火パターンを変え、またLearnボタンを押すと、別のパターンも記憶できます。
やってみて下さい。

いくつか発火パターンを記憶したら、一つニューロンをどれでもいいのでクリックし、Remember Stepボタンを押しましょう。
(「一つニューロンをどれでもいいのでクリックし」というのはどういうことかというと、学習した発火パターンと似たパターンを人工的に作り出せ、と言う意味です)
そうすると(記憶したパターンと似て非なるパターンから)記憶したパターンに戻るはずです。
学習したパターンを思い出したのです!

なお、Remember Stepボタンを押す前にクリックするニューロンが多すぎたら、元のパターンに戻りません。
想起に失敗するのです。

拍手[10回]

PR