機械知能ワタナベ（仮）

## 機械知能ワタナベ（仮）

ジェフ・ホーキンス著『[考える脳考えるコンピューター](https://www.amazon.co.jp/%E8%80%83%E3%81%88%E3%82%8B%E8%84%B3-%E8%80%83%E3%81%88%E3%82%8B%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF%E3%83%BC-%E3%82%B8%E3%82%A7%E3%83%95%E3%83%BB%E3%83%9B%E3%83%BC%E3%82%AD%E3%83%B3%E3%82%B9/dp/4270000600/ref=as_li_ss_il?ie=UTF8&linkCode=li3&tag=pondadamazon-22&linkId=bf97f44843067d5fe22506ede9d4806f&language=ja_JP)』を読んで以来、人間の大脳新皮質をモデルにした人工知能にずっと興味を持っていましたが、なにしろ著作が書かれたのは15年前のことで、その後の情報もほとんど耳にすることはありませんでした。

今年に入ってからふと気になり調べてみたところ、想像したよりも研究が進んでおり、新皮質型のアルゴリズムを実際に使えるライブラリも公開されていることがわかりました。

しかしながら、日本語のドキュメントなどは皆無な状況で途方に暮れたのですが、機械翻訳を頼りになんとかアルゴリズムを実行することができました。愛着も湧いてきたので、作成したものに『機械知能ワタナベ（仮）』と名付けてみました。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/05e82c3b-b224-2b69-faf8-c0f72530f199.png)

『ワタナベ』の目的は、音楽のフレーズをきっかけに、その続きのフレーズを思い出すというものです。『考える脳考えるコンピューター』のなかでシーケンス学習・予測の例として音楽がよく使われていたので、今回アルゴリズムをつかってそれを再現しようと試みたというわけです。

音楽をデータで抽出するために今回は 『[music21](http://web.mit.edu/music21/)』 を使いました。これはMidi形式で保存された音楽のデータを分析できるPythonライブラリで、最近は機械学習でよく使われています。

この『Music21』をつかい、『[ノルウェーの森](https://ja.wikipedia.org/wiki/%E3%83%8E%E3%83%AB%E3%82%A6%E3%82%A7%E3%83%BC%E3%81%AE%E6%A3%AE)』を『ワタナベ』に学習させ、音階と拍数を微妙にはずしたMidiファイルに対して予測が正しくおこなえるか検証してみます。

**入力フレーズ**

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/5a530bb5-3510-3dc3-94a4-b2748e950680.png)

**出力フレーズ**

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/5c5dc520-6d48-8e82-b16d-6d9414016822.png)

『ノルウェーの森』の導入部分「ターンタ・タタタ・ターン」のあとに「タンタ・タタター・タラッタター」と正しく出力されました。（脳内再生してください）

『ワタナベ』は一部欠けた入力に対して、時間的なパターンをよびだしています。言い換えればこれは新皮質のおこなう「自己連想記憶」を再現しています。さて、これらはどの様な処理をおこなっているのでしょうか。ライブラリの使い方を中心にもう少し詳しくみていきましょう。

## 仕組み

『ワタナベ』がつかうアルゴリズム『**階層的一時記憶（HTM/Hierarchical temporal memory）**』はジェフ・ホーキンスが中心となり立ち上げたNumenta社によって開発されました。ライセンスはAGPL Version 3（商用利用可、ソフトウェア改変可）であり、制約を受けることなく使えます。

コア部はC ++ですが、APIによりPython、JAVA 、JavaScriptなど様々な言語で利用することができます。今回は有志がPython3系で動作できるように開発したラッパーライブラリ [htm.core](https://github.com/htm-community/htm.core) を使用します。

![watanabe-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/041327aa-d65c-0352-b1b1-0ab255323b1b.png)

HTMは時系列をともなう分類や異常検知に効果を発揮します。『ワタナベ』は最終的に分類予測をおこなうので、上の図のような仕組みで動作します。

HTMの特徴として、新皮質のニューロンがまばらに活性化されることにヒントを得た、**スパース分散表現（SDR/Sparse Distributed Representations）**により情報を処理することがあげられます。最初におこなう処理は入力データをSDRに**エンコード（Encode）**することです。

HTMの入力に対する学習と推論は、**空間プーリング（SP/Spatial Pooling）**アルゴリズムによっておこなわれます。また、シーケンス学習と予測は、**一時記憶（TM/Temporal Memory）**アルゴリズムによっておこなわれます。

SPとTMの学習・予測は、新皮質のニューロンがシナプスの発火により伝達効率が増強・減少すること（ヘッブの法則）をモデルにおこなわれます。

時系列をともなう分類は、最尤推定（さいゆうすいてい/maximum likelihood estimation）によっておこなわれます。入力データのラベルとTMで学習したアクティブ状態のセルをもとに分類器を学習し、予測します。

HTMは脳科学用語・機械学習用語・プログラミング言語が入り乱れ難解に感じるのですが、仕組み自体はシンプルです。『ワタナベ』もPythonコードにするとたった75行で動作します。

[Machine-Intelligence-Watanebe/mi-watanebe-1.ipynb](https://github.com/PonDad/Machine-Intelligence-Watanebe/blob/master/mi-watanebe-1.ipynb)

レポジトリにノートブックを用意しました。よかったら一緒にやってみましょう。

## 必要なライブラリ

『ワタナベ』はPython3系で動作します。以下のライブラリが必要になります。

- [htm.core](https://github.com/htm-community/htm.core) 
- [numpy](https://numpy.org/) 
- [matplotlib](https://matplotlib.org/users/installing.html)
- [music21](http://web.mit.edu/music21/)
    - [musescore](https://musescore.org/ja)
    - [pygame](https://www.pygame.org/news)

『htm.core』は前述の通りコア部分がC++ですので、インストールにC言語のコンパイラが必要になります。64BitのMac、PC、Linuxで動作します。実行にはPython3系のラッパーライブラリを使用するので、データ処理の大半はNumpyによっておこないます。

『music21』は、楽譜の生成とサウンド再生を外部アプリに依存します。そのため、楽譜生成アプリ『musescore』とゲーム作成エンジン『pygame』が必要になります。

## データの前処理

『ワタナベ』の最終目的は音符のシーケンス予測をおこなうことです。そのため時系列順にラベル付けされたデータが必要になります。

『Music21』をつかってMidiデータの音符を「音階」と「拍数」にパースし、それらを入力データとしてつかいます。学習用データのもとになるMidiファイルは、以下のサイトよりダウンロードしました。

[freemidi.org/Norwegian Wood Midi](https://freemidi.org/download3-1118-norwegian-wood-beatles)

データの前処理は以下のようにおこないました。

```py:python
from music21 import *

# Midiファイルを読み込み
inputMidi = converter.parse('./data/NorwegianWood.mid')
# 2番目の楽器を使用
parseMidi = inputMidi.parts[1].recurse()
# 音階（Midi番号）と拍数を配列に格納
notes = []
for element in parseMidi:
    if isinstance(element, note.Note):
        notes.append([int(element.pitch.ps),float(round(element.quarterLength,6))])

# 辞書型に格納
notesDict={}
for i in range(len(notes)):
    notesDict[i]=notes[i] 
print(notesDict)
```

複数の楽器で構成されているMidiファイルから `recurse()` メソッドをつかい主旋律を抽出します。この場合は2番目の楽器です。音符は `note.Note` メソッドで格納されているので、そこから「音階」と「拍数」をパースし、それぞれ配列に格納します。各音符のラベルが必要になるため、配列を辞書型に格納します。

```bash:terminal
{0: [71, 1.5], 1: [71, 0.333333], 2: [73, 0.25], 3: [71, 0.5], 4: [69, 0.333333],(略),131: [64, 1.5], 132: [62, 0.5], 133: [69, 0.5], 134: [61, 0.5], 135: [59, 1.5]}
```

ラベル数135の辞書型が取得できました。せっかくなので楽譜を生成し、サウンドを再生してみましょう。

```python:python
# ストリーミングデータを定義
s1 = stream.Stream()

# 辞書型から音階（Midi番号）と拍数を音符に代入し、ストリーミングデータへ入力する
for n in range(len(list(notesDict))):
    noteMidi = notesDict[n][0]
    noteLength=notesDict[n][1]
    n = note.Note(midi=noteMidi,quarterLength = noteLength)

    s1.append(n)

# ストリーミングデータをもとに楽譜を生成    
s1.show()
# ストリーミングデータをサウンド再生
StreamPlayer=midi.realtime.StreamPlayer(s1)
StreamPlayer.play()
```
『Music21』と連携させた楽譜生成アプリ『musescore』により出力された楽譜は以下のようになります。

![watanabe-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/b73b9a55-954b-54af-c345-d4fe48795ba6.png)

同時に『Music21』と連携させたゲームエンジン『pygame』によってサウンドが再生されます。

## エンコード（Encode）

辞書型のバリュー要素は「音階」と「拍数」の配列が格納されています。これらをSDR形式に変換します。『htm.core』の`SDR()` メソッドと `ScalarEncoder()` メソッドを利用します。

```py:python
import numpy as np

from htm.bindings.sdr import SDR
from htm.bindings.encoders import ScalarEncoder, ScalarEncoderParameters
from htm.algorithms import SpatialPooler as SP
from htm.algorithms import TemporalMemory as TM
from htm.bindings.algorithms import Predictor
```
必要なライブラリをインポートします。

```py:python
# 「音階」をサイズ24、オンビット3のSDRに変換するエンコーダーを定義
pitchParams = ScalarEncoderParameters()
pitchParams.minimum = 48
pitchParams.maximum = 83
pitchParams.activeBits = 3
pitchParams.size = 24
pitchParams.clipInput  = True

encPitch = ScalarEncoder(pitchParams)

# 「拍数」をサイズ24、オンビット3のSDRに変換するエンコーダーを定義
lengthParams = ScalarEncoderParameters()
lengthParams.minimum = 0
lengthParams.maximum = 1
lengthParams.activeBits = 3
lengthParams.size = 24
lengthParams.clipInput  = True

encLength = ScalarEncoder(lengthParams)
```

SDRのサイズとオンビットの数、入力データの最大値・最小値を指定します。ここでは音階48度〜83度、拍数0〜1（4/4拍子を1とする）の2つのエンコーダーを作成しました。SDRサイズは24、オンビット3と指定しています。

エンコードのメソッドは`ScalarEncoder.encode(入力データ)`でSDR形式にエンコードされます。今回は2種類エンコードしますので、2つのSDRはnumpyの `concatenate()` メソッドをつかい結合させます。データをストリーミング状に入力したものを可視化してみます。

![watanabe-4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/5bddea7b-84d2-7ade-8a53-69782de40baf.gif)


可視化するとこのようになります。可視化のためにSDRを8x6サイズで表示していますが、実際は48ビット、オンビット6の1次元配列であることに注意してください。可視化した図でいうと上半分が音階、下半分が拍数をあらわしています。この様にSDRはひとつのデータで複数の表現を含めることができます。

## 空間プーリング（SP/Spatial Pooling）

エンコードされたSDRは、空間プーリング（SP/Spatial Pooling）と呼ばれるアルゴリズムによって意味を学習し、推論をおこないます。

```py:python
# 入力SDRの次元を定義
inputSDR  = SDR( dimensions = (48, ) )
# 出力SDRの次元を定義
activeSDR = SDR( dimensions = (576,) )
# 空間プーラーのパラメーターを定義
sp = SP(inputDimensions  = inputSDR.dimensions,
        columnDimensions = activeSDR.dimensions,
        localAreaDensity = 0.02,
        globalInhibition = True,
        seed             = 1,
        synPermActiveInc   = 0.01,
        synPermInactiveDec = 0.008)
# 空間プーラーと入力の接続状態を表示
print(sp)
```

ここでは入力サイズ48に対して出力576、スパース値0.02で指定しています。SP層と入力層の接続状態を確認してみましょう。

```bash:terminal
Spatial Pooler Connections:
    Inputs (48) ~> Outputs (576) via Segments (576)
    Segments on Cell Min/Mean/Max 1 / 1 / 1
    Potential Synapses on Segment Min/Mean/Max 17 / 17 / 17
    Connected Synapses on Segment Min/Mean/Max 3 / 8.3941 / 15
    Synapses Dead (0%) Saturated (0%)
    Synapses pruned (0%) Segments pruned (0%)
```

初期値では興奮したなシナプスが0となっていることが確認できます。SPの学習・推論は `SP.compute(入力SDR, True, 出力SDR)`メソッドでおこないます。データを入力した空間プーリングを可視化してみましょう。

![watanabe-5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/393a8b02-e84b-2a7a-60fe-b5133e6f0d02.gif)


入力データと同じく可視化のため24x24に変形していますが、実際はサイズ576の１次元データであることに注意してください。興奮したシナプスの強度により空間プーリング層と入力層の接続が更新されていることに注目してください。

## 一時記憶（TM/Temporal Memory）

空間プーリングにより疎状態に変換されたSDRは、一時記憶（TM/Temporal Memory）と呼ばれるアルゴリズムによって時間ごとのセル同士の接続を学習し、予測をおこないます。

TM層のアクティブ状態のセルは、次にどのセルが興奮するかを表現します。

```py:python
# 一時記憶のパラメーターを定義
tm = TM(
    columnDimensions = (576,),
    cellsPerColumn=8,
    initialPermanence=0.5,
    connectedPermanence=0.5,
    minThreshold=8,
    maxNewSynapseCount=20,
    permanenceIncrement=0.1,
    permanenceDecrement=0.0,
    activationThreshold=8,
)
# 一時記憶のセル同士の接続状態を表示
print(tm)
```

ここでは空間プーリングの出力576ビットを入力空間とし、重ねる層を8と設定しています。これにより8x576ビットの一時記憶空間を設定します。重ねる層は8以上でないと時間記憶が正しく動作しないようです。TM層のセル間の接続状態を確認してみましょう。

```bash:terminal
Temporal Memory Connections:
    Inputs (0) ~> Outputs (4608) via Segments (0)
    Segments on Cell Min/Mean/Max 0 / 0 / 0
    Potential Synapses on Segment Min/Mean/Max 4294967295 / nan / 0
    Connected Synapses on Segment Min/Mean/Max 65535 / nan / 0
    Synapses Dead (nan%) Saturated (nan%)
    Synapses pruned (nan%) Segments pruned (nan%)
```

ここではオンビットの可能性があるシナプスが６万5千個あるのが見て取れます。

TMの学習・予測は `TM.compute(入力SDR, True)`メソッドでおこないます。空間プーリングから入力されたデータをもとに学習・予測する様子を可視化してみます。

![watanabe-6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/08ff4487-4f9a-4867-c86a-d52b9b33878f.gif)

可視化のため8x24x24で表示していますが、実際は8x526の２次元データであることに注意してください。

一時記憶は学習後、2つのステップに分かれてアルゴリズムを実行します。学習時、ミニカラム（縦に並んだセル。ここでは８個）がはじめてのデータを参照する際、それらは予測にないためバースト（Burst）が発生します。つまり、ミニカラムのすべてのセルがそのシーケンスを学習しようとします。

学習後、ひとつめのステップが実行されます。バーストしたミニカラムでは、特定のセルのみがアクティブになります。ふたつめのステップでは、特定のセルがアクティブになったと識別されると、次に発火する複数のセルが予測されます。単純化した例は以下のようになります。

- 「A→B→C→D→X→B→C→Y」で学習後
- 「A→B'→C'」ならば「D」と予測
- 「X→B"→C"」ならば「Y」と予測
- TM層のアクティブセルは入力「C」に対し過去の学習から「C'」と「C"」どちらも表現できる

これによってTM層のアクティブカラムを時系列に沿って分類すれば、そのセルが過去にアクティブになった記憶をもとに、将来どのセルが興奮するかを予測できます。

## 予測（Predictor）

入力データをラベルをつけた辞書型に格納したのはここで分類予測をおこなうためです。辞書型のキーをラベルとし、TM層のアクティブカラムを分類器で学習させることで、数ステップ先のラベルを予測できます。

```py:python
predictor = Predictor( steps=[1,2,3,4,5,6,7], alpha=0.1)
```

『htm.core』では、`Predictor()` メソッドが用意されており、分類器が何ステップ先まで予測するか指定できます。ここでは7ステップ先まで予測を得るように指定しました。

これによって、前述の例でいえば以下のような分類予測がおこなえます。

- 「E→B→C」の入力があったならば
- 出力されたアクティブセルが「A→B'→C'」に近ければ「D」と予測
- 出力されたアクティブセルが「X→B"→C"」に近ければ「Y」と予測

この「近さ」＝「尤（もっと）もらしさ」を分類するのに尤度（ゆうど）を使うというわけです。どのような形式で出力されるかみてみましょう。

```py:python
predictions = {1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: []}
for i in range(len(notesDict)):
    pitchBits        = encPitch.encode(notesDict[i][0])
    lengthBits = encLength.encode(notesDict[i][1])

    inputSDR = SDR( dimensions = (48, ) ).concatenate([pitchBits, lengthBits])
    sp.compute(inputSDR, True, activeSDR)

    tm.compute( activeSDR, learn=True)

    predictor.learn(i, tm.getActiveCells() ,list(notesDict)[i])

    pdf = predictor.infer( tm.getActiveCells() )
    for n in (1,2,3,4,5,6,7):
        if pdf[n]:
            predictions[n]=notesDict[list(notesDict)[np.argmax( pdf[n] )] ] 
        else:
            predictions[n]=float('nan')
                
print(predictions)
```
`predictor()`メソッドで定義する分類器の学習は `predictor.learn(ステップ数, アクティブセル,分類ラベル)` でおこない、推測（尤もらしさ）は`predictor.infer(アクティブセル)` で得られます。

この推測は学習のステップ数に対して、入力となるアクティブセルの尤度（ゆうど）で示されます。その中の最大値を予測としてみなします。グラフで出力しますので確認してみましょう。

![watanabe-7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/dee34f8b-912a-287a-5a43-99fd0c0ae32c.gif)

これは最初の50ステップまでの尤度（ゆうど）を表すグラフです。X軸がステップ数でY軸がnステップ先の予測尤度（ゆうど）をあらわします。

![watanabe-8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/a1eaf0c3-fbe3-d66f-4ffb-4f19f10343db.gif)

続いて100ステップ以降の尤度（ゆうど）を表すグラフです。楽譜と比較すると似たフレーズの部分の山が高くなっているのがわかると思います。

## テスト

テスト用に音階と拍数を少し外したデータを用意します。

```py:python
testNotesDict ={0:[71, 1.5], 1:[71, 0.5], 2:[74, 0.25], 3:[71, 0.25], 4:[69, 0.25], 5:[68, 1.5], 6:[66, 0.5]}
```

『Music21』でフレーズを確認してみましょう。

```py:python
s2 = stream.Stream()

for n in range(len(list(testNotesDict))):
    noteMidi = testNotesDict[n][0]
    noteLength=testNotesDict[n][1]
    n = note.Note(midi=noteMidi,quarterLength = noteLength)

    s2.append(n)

print("Score of testMidi.mid")
s2.show()
StreamPlayer=midi.realtime.StreamPlayer(s2)
StreamPlayer.play()
```

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/5a530bb5-3510-3dc3-94a4-b2748e950680.png)

冒頭で添付した入力データです。これを学習済みの『ワタナベ』に入力します。

```py:python
testPredictions = {1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: []}

for i in range(len(list(testNotesDict))):
    pitchBits        = encPitch.encode(testNotesDict[i][0])
    lengthBits = encLength.encode(testNotesDict[i][1])

    inputSDR = SDR( dimensions = (48, ) ).concatenate([pitchBits, lengthBits])
    sp.compute(inputSDR, True, activeSDR)

    tm.compute( activeSDR, learn=True)

    pdf = predictor.infer( tm.getActiveCells() )
    for n in (1,2,3,4,5,6,7):
        if pdf[n]:
            testPredictions[n]=notesDict[list(notesDict)[np.argmax( pdf[n] )] ] 
        else:
            testPredictions[n]=float('nan')
                
print(testPredictions)
```

分類器の学習`predictor.learn()`はおこないません、推測の最大値から予測を得る際、学習時に使った辞書型をつかいますので注意してください。

得られた予測を『Music21』で確認してみます。

```py:python
s3 = stream.Stream()
    
for n in range(len(predictions)):
    noteMidi = testPredictions[n+1][0]
    noteLength=testPredictions[n+1][1]
    n = note.Note(midi=noteMidi,quarterLength = noteLength)

    s3.append(n)

s3.show()
StreamPlayer=midi.realtime.StreamPlayer(s3)
StreamPlayer.play()
```

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47128/5c5dc520-6d48-8e82-b16d-6d9414016822.png)

『ワタナベ』は、音楽のフレーズをきっかけに、その続きのフレーズを思い出すことができました。

## まとめ

自己連想的に記憶を呼び起こすことができるようになった『ワタナベ』ですが、課題がまだ残っています。『考える脳考えるコンピューター』では、以下のように「知能（Intelligence）」を定義していました。

> **知能とは、記憶に基づく階層的なモデルを使って、予測することである。**

今回時系列に沿ったモデルをもとに予測はできましたが、階層的なモデルではありません。これを再現しなければ「機械知能」を名乗れないと思い、アルゴリズムの名前は『機械知能ワタナベ（仮）』としました。

階層的なモデルをつくるには、複数のSP/TM層の出力を入力として、さらにSP/TM層で学習させれば良いと思うのですが... 。新皮質の仕組みをできる限り再現しようと思えば処理は並列におこなわなければなりません。（ラズパイで並列処理するとか?）

また、ホーキンスは「階層的一時記憶」は古いモデルであるとして、新たな新皮質モデルを提案しています。（『千の脳理論』）単純に層を重ねるだけでは新皮質を再現した階層的なモデルの構築は難しそうです。

まだまだ課題は尽きないですが、少しづつ『ワタナベ』をバージョンアップできればと思います。そうすればいつか『ワタナベ』は『ノルウェーの森』を聴いて過去の記憶を一瞬で全て思い出すことができるかもしれません。ではまた。

## 引用

Hawkins, J. et al. 2016-2020. Biological and Machine Intelligence.
Release 0.4. Accessed at [numenta](https://numenta.com/resources/biological-and-machine-intelligence/).