TOP PAGE

このリンク先で示している「Unityで行うシンプルなディープニューラルネットワーク」で行った学習済みデータを使って、
MicroPythonでAI判定するNeural2Netクラスの実装例と、その検証内容を紹介しているページです。
Neural2Netでは、NumArrayクラス(Numpyの機能などを絞った自作クラス)を利用して作られています。
なお、このコードはNumpyが使えないMicroPython用に作ったコードですが、通常のPythonでも使えます。

このリンク先で作った学習済みデータで、MNISTの手書き画像を判定する。

Neural2Netクラス(判定器)のソース(neural2net.py)とその実行結果

このリンク先でUnityでMNISTの手書き画像を学習して、判定するまでのコードがあります。
そこでは、128個の画像入力を、2層に50個のニューロンに繋ぎ、それを10個の出力で判定する2層のニューラルネットワークをTwoLayerNetクラスで実装し、学習させています。
そこでは、次の3つファイルを生成しています。
以下では、上記学習済みデータで画僧の予測をソフトマックスで返すpredictメソッドを定義したNeural2Netクラスを定義して、動作を検証しています。

下記が2層ニューラルネットのNeural2Netクラスを定義したneural2net.pyです。
# import server # 「https://manabu.quu.cc/up/ume/ume_esp32_python.html」のサーバーで動作させる場合、この2行を使う
# print=server.send # サーバーで動作させる場合で、printの出力をTCPクライアントに送信する

# neural2net.pyのファイル内容
import numarray
from numarray import NumArray

# neural2net.pyのファイル内容

import numarray
from numarray import NumArray

class Neural2Net: # 判定器(ニューラル2層ネット)
    def __init__(self): # create 相当
        self.naW1 =  NumArray()  # 1層目の重みパラメタ
        self.naB1 =  NumArray()  # 1層目のバイアスパラメタ
        self.naW2 =  NumArray()  # 2層目の重みパラメタ
        self.naB2 =  NumArray()  # 2層目のバイアスパラメタ
    #
    # 20250805追加
    def load_params( self, path:str = "weight_bias_params_0.bin" ):
        try:
            with open(path, mode='rb') as fr:
                self.naW1.read(fr)  # 学習済みの重み、バイアス情報を読み取る。
                self.naB1.read(fr)
                self.naW2.read(fr)
                self.naB2.read(fr)
        except Exception as e:
            raise RuntimeError(f"[Neural2Net create_load_params('{path})']:{e}")
    #
    @classmethod # 20250805変更
    def create_load_params( cls, path:str = "weight_bias_params_0.bin" )->'Neural2Net':
        my = Neural2Net() 
        my.load_params( str  )
        return my
    #
    #
    def predict(self, x: 'NumArray' )->'NumArray': # 引数
        # 予測メソッド x のグレー画像(28×28)の判定をソフトマックスを介して返す。
        #print(f"x:{x}")
        #print(f"naW1:{self.naW1}, naW1.shapeR{self.naW1.shapeR}")
        #print(f"naB1:{self.naB1}")      

        self.naA1 = NumArray.dot(x, self.naW1)
        #print(f"naA1:{self.naA1}")

        self.naA1.add_matrix(self.naB1)
        #print(f"naA1:{self.naA1}")
        self.naZ1 = self.naA1.sigmoid()
        #print(f"naZ1:{self.naZ1}")

        naA2 = NumArray.dot(self.naZ1, self.naW2)
        naA2.add_matrix(self.naB2)
        #print(f"naA2:{naA2}")

        naY = NumArray.softmax(naA2)
        return naY # 確率分布を返す。
    #
    #
上記クラスを利用して 下記の n2n_test.pyでは、画像("x_train.bin")とその解答情報("x_train.bin")をデシリアライスして、先頭10枚の画像で予測判定を行っています。
なお Neural2Netクラスはcreate_load_paramsメソッドで、学習済み("weight_bias_params_0.bin")をロードして生成して、 これをn2nの変数に管理して、predictで10枚の画像の予測を行っています。
# import server # 「https://manabu.quu.cc/up/ume/ume_esp32_python.html」のサーバーで動作させる場合、この2行を使う
# print=server.send # サーバーで動作させる場合で、printの出力をTCPクライアントに送信する

# n2n_test.pyの判定器用ファイル

from numarray import NumArray
from neural2net import Neural2Net

n2n=Neural2Net.create_load_params("weight_bias_params_0.bin" ) 
print(f"n2n.naW1.shapeR:{n2n.naW1.shapeR}") # ロードした学習済みの1層目の重みパラメタ
print(f"n2n.naB1.shapeR:{n2n.naB1.shapeR}") # ロードした学習済みの1層目のバイアスパラメタ
print(f"n2n.naW2.shapeR:{n2n.naW2.shapeR}") # ロードした学習済みの2層目の重みパラメタ
print(f"n2n.naB2.shapeR:{n2n.naB2.shapeR}") # ロードした学習済みの2層目のバイアスパラメタ

x_train = NumArray() # 手書き画像
with open("x_train.bin", mode='rb') as fr:# 学習や検証素材を、前処理してシリアラスした画像ファイル(28*28の6万個)
    x_train.read(fr) # デシリアライズ


print(f"load x_train.bin:{x_train.shapeR}") # 画像データの構造表示

t_train = NumArray() # One-Hot ベクトル表現 上記の解答情報
with open("t_train_a.bin", mode='rb') as fr: # 上記の正解ファイルで、One-Hot ベクトル表現に前処理してファイル
    t_train.read(fr) # デシリアライズ

print(f"load t_train_a.bin:{t_train.shapeR}") # 正解データの構造表示

# load x_train10.binに存在する画像(28×28)の判定を行う。
for i in range(x_train.shapeR[1]):
    testImage = NumArray.createLineAt(x_train, i) # 入力変数 (Input Variable)で、画像(28×28)
    print(f"x_train[{i}].shapeR:{testImage.shapeR}") 
    one_hot = NumArray.createLineAt(t_train, i) # 正解ラベル (Ground Truth Label)
    print(f"t_train[{i}].shapeR:{one_hot}") 
    prediction=n2n.predict(testImage) # # 画像(28×28)の予測対象変数 (Target Variable) 
    print(f"         prediction:{prediction}\n") 

この結果は下記のようにsoftmaxにより確率データで、その表示の上にあるOne-Hotの正解ラベルと比較すると、正しく判定していることが分かります。
n2n.naW1.shapeR:[50, 784, 1]
n2n.naB1.shapeR:[50, 1, 0]
n2n.naW2.shapeR:[10, 50, 1]
n2n.naB2.shapeR:[10, 1, 0]
load x_train.bin:[784, 60000, 0]
load t_train_a.bin:[10, 60000, 0]
x_train[0].shapeR:[784, 0, 0]
t_train[0].shapeR:10, 0, 0
[   0.0000    0.0000    0.0000    0.0000    0.0000    1.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0234    0.0033    0.0052    0.0250    0.0000    0.9398    0.0011    0.0006    0.0013    0.0003 ]

x_train[1].shapeR:[784, 0, 0]
t_train[1].shapeR:10, 0, 0
[   1.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.9971    0.0000    0.0000    0.0000    0.0000    0.0021    0.0001    0.0006    0.0000    0.0000 ]

x_train[2].shapeR:[784, 0, 0]
t_train[2].shapeR:10, 0, 0
[   0.0000    0.0000    0.0000    0.0000    1.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0000    0.0003    0.0008    0.0053    0.9910    0.0002    0.0013    0.0002    0.0001    0.0009 ]

x_train[3].shapeR:[784, 0, 0]
t_train[3].shapeR:10, 0, 0
[   0.0000    1.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0000    0.9910    0.0020    0.0000    0.0000    0.0001    0.0000    0.0000    0.0069    0.0000 ]

x_train[4].shapeR:[784, 0, 0]
t_train[4].shapeR:10, 0, 0
[   0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    1.0000 ]
         prediction:10, 0, 0
[   0.0000    0.0003    0.0000    0.0000    0.0065    0.0000    0.0000    0.0018    0.0005    0.9909 ]

x_train[5].shapeR:[784, 0, 0]
t_train[5].shapeR:10, 0, 0
[   0.0000    0.0000    1.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0003    0.0003    0.9935    0.0003    0.0000    0.0004    0.0003    0.0001    0.0046    0.0001 ]

x_train[6].shapeR:[784, 0, 0]
t_train[6].shapeR:10, 0, 0
[   0.0000    1.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0000    0.9975    0.0001    0.0001    0.0000    0.0002    0.0000    0.0001    0.0020    0.0000 ]

x_train[7].shapeR:[784, 0, 0]
t_train[7].shapeR:10, 0, 0
[   0.0000    0.0000    0.0000    1.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0000    0.0001    0.0001    0.9991    0.0000    0.0003    0.0000    0.0000    0.0004    0.0001 ]

x_train[8].shapeR:[784, 0, 0]
t_train[8].shapeR:10, 0, 0
[   0.0000    1.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0000    0.9945    0.0001    0.0000    0.0000    0.0006    0.0000    0.0025    0.0022    0.0001 ]

x_train[9].shapeR:[784, 0, 0]
t_train[9].shapeR:10, 0, 0
[   0.0000    0.0000    0.0000    0.0000    1.0000    0.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0000    0.0000    0.0000    0.0000    0.9984    0.0001    0.0005    0.0000    0.0001    0.0008 ]

上記は、MicroPythonの環境ではなく、通常のPython(Python 3.6.8)の環境で実行した結果です。

UMEHOSHI ITA基板に取り付けたESP32のサーバーで、上記の判定を行う。

このリンク先で示しているUMEHOSHI ITA基板に取り付けたESP32のMicroPthonの環境で、 こちらのリンク先のUnityで行ったMNISTの手書き画像の学習データを使って、 判定処理を行った時の情報を下記で紹介します。

さて、上記のPythonの実行結果はPCで実行しているが、ESP32の環境では実行できません。
それは、[1,60000,28*28]の手書き画像をシリアライスした"x_train.bin"を記憶する容量がないからです。
そこで、画像ファイル("x_train.bin")とその正解ファイル("t_train_a.bin")から先頭の10個分だけのデータに減らしたファイルを、 それぞれ"x_train10.bin"と"t_train_a10.bin"のファイルを次のコードで作成して、それをESP32に転送して使います。
from numarray import NumArray

x_train = NumArray() # [1,60000,28*28]の手書き画像
with open("x_train.bin", mode='rb') as fr:# 学習や検証素材を、前処理してシリアラスした画像ファイル(28*28の6万個)
    x_train.read(fr) # デシリアライズ

print(f"load x_train.bin:{x_train.shapeR}") # 画像データの構造表示

x_train.shapeR[1]=10
with open("x_train10.bin", mode='wb') as fw:
    x_train.write(fw)       # 先頭10個を保存

t_train = NumArray() # One-Hot ベクトル表現 上記の解答情報
with open("t_train_a.bin", mode='rb') as fr: # 上記の正解ファイルで、One-Hot ベクトル表現に前処理してファイル
    t_train.read(fr) # デシリアライズ

t_train.shapeR[1]=10
with open("t_train_a10.bin", mode='wb') as fw:
    t_train.write(fw)       # 先頭10個を保存


また、UMEHOSHI ITA基板に取り付けたESP32のMicroPthonの環境で使うためには、 printで表示先をTCPで繋がるPCにするため、print関数をTCPの出力へ変更する必要があります。
そのために、numarray.pyと、上記eural2Netクラス(判定器)のソース(neural2net.py)と、 メインの n2n_test.pyの先頭2行のコメントを次のように外して、 次のようにしてサーバに置きます。
import server # 「https://manabu.quu.cc/up/ume/ume_esp32_python.html」のサーバーで動作させる場合、この2行を使う
print=server.send # サーバーで動作させる場合で、printの出力をTCPクライアントに送信する


"x_train10.bin"と"t_train_a10.bin"のファイルで判定する上記の修正を行ったn2n_test.pyのコードを示します。
import server # 「https://manabu.quu.cc/up/ume/ume_esp32_python.html」のサーバーで動作させる場合、この2行を使う
print=server.send # サーバーで動作させる場合で、printの出力をTCPクライアントに送信する

# n2n_test.pyの判定器用ファイル

from numarray import NumArray
from neural2net import Neural2Net

n2n=Neural2Net.create_load_params("weight_bias_params_0.bin" ) 
print(f"n2n.naW1.shapeR:{n2n.naW1.shapeR}") # ロードした学習済みの1層目の重みパラメタ
print(f"n2n.naB1.shapeR:{n2n.naB1.shapeR}") # ロードした学習済みの1層目のバイアスパラメタ
print(f"n2n.naW2.shapeR:{n2n.naW2.shapeR}") # ロードした学習済みの2層目の重みパラメタ
print(f"n2n.naB2.shapeR:{n2n.naB2.shapeR}") # ロードした学習済みの2層目のバイアスパラメタ

x_train = NumArray() # 手書き画像
with open("x_train10.bin", mode='rb') as fr:# 学習や検証素材を、前処理してシリアラスした画像ファイル(28*28の10個)
    x_train.read(fr) # デシリアライズ


print(f"load x_train10.bin:{x_train.shapeR}") # 画像データの構造表示

t_train = NumArray() # One-Hot ベクトル表現 上記の解答情報
with open("t_train_a10.bin", mode='rb') as fr: # 上記の正解ファイルで、One-Hot ベクトル表現に前処理してファイル
    t_train.read(fr) # デシリアライズ

print(f"load t_train_a10.bin:{t_train.shapeR}") # 正解データの構造表示

# load x_train10.binに存在する画像(28×28)の判定を行う。
for i in range(x_train.shapeR[1]):
    testImage = NumArray.createLineAt(x_train, i) # 入力変数 (Input Variable)で、画像(28×28)
    print(f"x_train[{i}].shapeR:{testImage.shapeR}") 
    one_hot = NumArray.createLineAt(t_train, i) # 正解ラベル (Ground Truth Label)
    print(f"t_train[{i}].shapeR:{one_hot}") 
    prediction=n2n.predict(testImage) # # 画像(28×28)の予測対象変数 (Target Variable) 
    print(f"         prediction:{prediction}\n") 

この修正したファイルを UMEHOSHI ITA基板に取り付けたESP32のMicroPthonの環境への転送や実行は、client_ume_esp32.pyで行います。
この方法で、numarray.pyと上記のneural2net.pyと n2n_test.pyを送信して実行した結果は次のようになりました。
D:\esp32ロボット1>python client_ume_esp32.py
IP (defualt:192.168.222.1)Address>

Connect!
入力:M/F/'quit'/w/a/s/d/b/?>F
['boot.py', 'client_ume_esp32.py', 'flow_off.umh', 'log.txt', 'neural2net.py', 'numarray.py', 'server.py', 'setap.py', 'uEsp32Init.umh',
 't_train_a10.bin',  'weight_bias_params_0.bin', 'x_train10.bin', '_df.py']
送信したいファイル名入力>n2n_test.py
Server:'n2n_test.py' 2053bytes received.
入力:M/F/'quit'/w/a/s/d/b/?>M
UME HEX Command /exc /ls -l/cat /get /del /import >import n2n_test
Send:import n2n_test
import_name:n2n_test
import_name Error:memory allocation failed, allocating 156800 bytes
入力:M/F/'quit'/w/a/s/d/b/?>
上記では、 'neural2net.py', 'numarray.py', 't_train_a10.bin', 'weight_bias_params_0.bin', 'x_train10.bin'のファイルを転送済みの環境で、 client_ume_esp32.pyを実行して、n2n_test.pyの送信と、その実行を行った場合の結果を示しています。)

残念ながら上記のように、メモリアロケーションで失敗しました。
それは、次の箇所です。
n2n=Neural2Net.create_load_params("weight_bias_params_0.bin" )

これは、学習済みデータファイル"weight_bias_params_0.bin" の155 KB (159,088 バイト)の読み込みで失敗したと予想できます。
ここで使っているesp32は、SP32-WROOM-32Dです。これはRAMが520kBしか存在しないためと予想しています。

現在(2025-04)では、ESP32-S3-WROOM-1-N16R8が入手できるようですが、これを使えばRAMが8MBなので、上記が実行できるのではないかと予想しています。

現在(2025-07)「ESP32-S3-DEV-KIT-N16R8-M」のこのリンク先で示した環境で、 上記n2n_test.pyのコードの判定動作が正しく動作しました。

ESP32-WROOM-32D(RAMが520kB)で、動作可能な範囲を模索する。

上述の実行の実行エラー(allocating 156800 bytes)で、失敗しましたが、どの程度の容量であれば可能を次のコードで確認しました。
import server # 「https://manabu.quu.cc/up/ume/ume_esp32_python.html」のサーバーで動作させる場合、この2行を使う
print=server.send # サーバーで動作させる場合で、printの出力をTCPクライアントに送信する

import gc
print(gc.mem_free()) # 今どのくらいヒープが空いているかのサイズ(byte)確認
上記コードをUMEHOSHI ITA基板に取り付けたESP32-WROOM-32Dのサーバーで動作させると、「69520」byteの値が得られました。
これから前述の学習済みデータファイルの"weight_bias_params_0.bin" の159,088 バイトのloadは無理があると分かりました。
そこで、「Unityで行うシンプルなディープニューラルネットワーク」で行ったこの学習済みデータサイズを1/3程度に減らしてみることにしました。
具体的には、「Unityで作ったTrrainCanvasクラス」において、次のコードを変更します。
this.twoLayerNet = new TwoLayerNet(28 * 28, 50, 10, 0.01f);
上記の50が中間層のサイズで、これを8に変更してLearnを5回クリック(5000回)の学習を行いました。
(この時のcross entropy loss は0.088で、それなりに学習できている学習データと考える)
この終了で得られた学習済みファイルは、"weight_bias_params_0.bin"は、25528bytesでした。
この結果もアロケーションエラーでした。(実行によってエラー表示無しで実行しない場合もあるような挙動)
実行しない範囲をコメント化するなどして調べると、判定器のNeural2Netのcreate_load_paramsメソッドの my.naW1.read(fr)で失敗している様子でした。

その後、中間層のサイズを5にした学習(5000回 cross entropy loss :0.088)で 得られたweight_bias_params_0.bin(5988bytes)で実験すると、 my.naW1.read(fr)が実行できましたが、my.naW2.read(fr)の 読み込みでアロケーションエラーでした。
(読み込む"weight_bias_params_0.bin"のファイルサイズを残りのヒープサイズに合わせて減らしても、単純には対応できないことが分かりました。 内部で生成するオブジェクト1つのサイズに対して、再利用可能な大きいヒープ領域が割り当てられるためと予想されます。)

その後は中間層のニューロン数を減らして、アロケーションが起きない値を調べてみました。
その結果、中間層のニューロン数を1まで減らすことで、ようやく次のように動作できました。
(なお、"x_train10.bin"と、"t_train_a.bin"のファイルも、容量減らしのため先頭1枚の5の手書き画像に変更しています)
D:\esp32ロボット1>python client_ume_esp32.py
IP (defualt:192.168.222.1)Address>

Connect!
入力:M/F/'quit'/w/a/s/d/b/?>F
['boot.py', 'client_ume_esp32.py', 'flow_off.umh', 'log.txt', 'neural2net.py', 'numarray.py', 'server.py', 'setap.py', 'uEsp32Init.umh',
 't_train_a10.bin',  'weight_bias_params_0.bin', 'x_train10.bin', '_df.py']
送信したいファイル名入力>weight_bias_params_0.bin
Server:'weight_bias_params_0.bin' 3268bytes received.
入力:M/F/'quit'/w/a/s/d/b/?>M
UME HEX Command /exc /ls -l/cat /get /del /import >import n2n_test
Send:import n2n_test.py
import_name:n2n_test.py
n2n.naW1.shapeR:[1, 784, 1]
n2n.naB1.shapeR:[1, 1, 0]
n2n.naW2.shapeR:[10, 1, 1]
n2n.naB2.shapeR:[10, 1, 0]
load x_train10.bin:[784, 1, 0]
load t_train_a10.bin:[10, 1, 0]
x_train[0].shapeR:[784, 0, 0]
t_train[0].shapeR:10, 0, 0
[   0.0000    0.0000    0.0000    0.0000    0.0000    1.0000    0.0000    0.0000    0.0000    0.0000 ]
         prediction:10, 0, 0
[   0.0613    0.0480    0.1218    0.0448    0.0831    0.2893    0.0778    0.1020    0.0767    0.0952 ]
入力:M/F/'quit'/w/a/s/d/b/?>
上記で使っている学習済みのファイル(weight_bias_params_0.bin 3268bytes)は、 中間層のニューロン数が1で、41000回の学習を行ったデータです。
cross entropy loss が 2.17で、とても学習できたという結果ではありません。
ですが、
行列処理用のNumArrayクラスと、これを使った判定器のNeural2Netクラスの 検証が、ある程度できたと判断しています。