メインモジュールの実装|Excel VBAでMNIST機械学習

本ページはゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装を参考にニューラルネットワークを作成していきます。参考書籍では「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」という3つのレイヤを作成し、それらを組み合わせることでニューラルネットワークを構築していきます。本サイトでも同じように3つのレイヤをVBAで作成し実装していきます。

これまでに「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」、それらを組み合わせたニューラルネットワーク本体である「TwoLayerNetクラス」をVBAで実装しました。

本ページではニューラルネットワークを呼び出し、学習を行うための機能をメインモジュールを実装していきます。また、別ページではこのメインモジュール内に推論を行う機能も追加します。つまり、マクロとして実行するのは今回実装するメインモジュールの関数のみとなっています。

 

メインモジュールの機能

メインモジュールでは「学習」「推論」の2つの機能を持ちます。
(本ページでは「学習」の機能のみを実装します)
 

ニューラルネットワークの学習

「学習」とは、順伝播と逆伝播を繰り返し、重みパラメータとバイアスパラメータの値を徐々に更新していき『適正な値』を見つけ出すことを意味しています。

例えば「2」と描かれた画像を入力したら「2」という値を出力、「3」と描かれた画像を入力したら「3」という値を出力するように、「0〜9」どの数字で"誰が描いた数字"だとしても正しく出力できるようなパラメータ値を見つけます。

適切なパラメータの値はこれまでに実装したクラス/レイヤの関数を順番に実行していくことで導き出すことができます。導き出す方法はいくつかありますが、今回実装している方法は「誤差逆伝播法」という1番シンプルで簡単な仕組みです。
とても極端な話をすれば「TwoLayerNetクラス」の関数「gradient」と関数「ParamsUpdate」をループ処理に入れるだけで学習が可能になっています。(引数を準備する必要がありますが)
 

ニューラルネットワークの推論

「推論」とは、入力された画像に何の数字が描かれているかをニューラルネットが自ら考え導き出すことです。

処理としては非常にシンプルで「学習」で求めた適切なパラメータの値をそのまま使い、順伝播を1回行うだけです。この順伝播によって出力された値がニューラルネットワークの最終的な答えとなります。

1度学習済みのパラメータさえ求めることができれば同じニューラルネットワークに入れることで何度でも使い回すことができます。つまり今回の場合、1度学習を終えパラメータ値が取得できればMNISTと同じ構成の画像データに描かれている「0~9」のいずれかの数字を当てるという処理は何度でも行ことができます。

ただ注意しないといけないのは、正解できる確率(精度)は学習の内容によるという点です。
正しく十分な学習ができていれば正解をうまく導き出すことができますし、逆に学習が不十分の場合はうまく導き出すことができません。

そのため学習中に一定間隔で精度を求める機能を追加していきます。
これにより現在の学習状態での学習の精度を随時確認することができるので、精度を確認しつつ自分の好きなタイミングで学習を終了させることができます。

 

メインモジュールの実装

まずは標準モジュールで「Main」というモジュールを作成します。
これまでに実装したクラス/レイヤと揃えるため、本モジュールでも「Option Base 1」を使い配列を「1」スタートにします。

以下はメインモジュール(学習のみ)の全コードです。

'VBA Main(module)
Option Explicit
Option Base 1
 
#If Win64 Then
    Declare PtrSafe Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Integer
#Else
    Declare Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Integer
#End If

Dim input_size As Long         '入力層のニューロン数
Dim hidden_size As Long        '隠れ層のニューロン数
Dim output_size As Long        '出力層のニューロン数

Dim MinLoss As Double          '最小Loss値
Dim MaxEpoc As Long            '最大エポック数

Dim LearningRate As Double      '学習率
Dim batch_size As Long          'ミニバッチサイズ

Dim train_size As Long          'mnist_trainの総データ数(default:60000)
Dim test_size As Long           'mnist_testの総データ数(default:10000)

Public ParamsSheet As Worksheet '書き出し用シート

Public ParamsCheck As Boolean   'シート確認用変数
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Sub Train()

'**********************************************************************
'
'        ユーザ-設定(ハイパーパラメータ設定)
'
'**********************************************************************

    
    'ユニット数の設定(Input:784 / Output:10は変更不可)
    
    input_size = 784   '28x28(px)
    hidden_size = 64   '学習結果に応じて変更可
    output_size = 10   '0~9
    
    
    '学習設定
    
    batch_size = 100     'バッチサイズ(Default:100)
    LearningRate = 0.01  '学習率(Default:0.01)
    
    train_size = ws_mnist_train.Cells(Rows.count, 1).End(xlUp).Row   'mnist_trainの総データ数(default:60000)
    test_size = ws_mnist_test.Cells(Rows.count, 1).End(xlUp).Row     'mnist_testの総データ数(default:10000)
    
    
    '学習終了条件
    
    MinLoss = 0.01    '目標Loss値
    MaxEpoc = 500     '最大エポック数(学習回数)


    
    
'**********************************************************************
'
'          学習開始
'
'**********************************************************************
    
    
'学習途中データの確認-----------------------------------------------------

    ParamsCheck = False  
    
    Dim ws As Worksheet
    Dim flag As Boolean
    Dim res As String
    Dim msg As String
    
    msg = "学習モデルのデータがあります。" & vbLf & _
          "続きから学習を始めますか?" & vbLf & vbLf & _
          "[いいえ]を押すと新規モデルとして学習を始めます。"
    
    
    For Each ws In Worksheets
        If ws.Name = "Parameters" Then flag = True
    Next ws
    
    If flag = True Then
    
        res = MsgBox(msg, vbYesNoCancel + vbInformation, "学習データの再利用")
        
        If res = vbYes Then
            ParamsCheck = True
            Set ParamsSheet = Worksheets.Item("Parameters")
            
        ElseIf res = vbNo Then
            ParamsCheck = False
            flag = False
                  
label1:
            Dim SheetName As String
            SheetName = InputBox("既存のモデル(シート)名を入力して下さい。", "モデル名変更")
            If StrPtr(SheetName) = 0 Then
                MsgBox "キャンセルします。"
                Exit Sub
            ElseIf SheetName = "" Then
                MsgBox "モデル名を入力して下さい。"
                GoTo label1
            End If
            
            For Each ws In Worksheets
                If ws.Name = SheetName Then flag = True
            Next ws
            If flag = True Then
                MsgBox "「" & SheetName & "」は既に存在します。" & vbLf & "別の名称を入力し直してください。"
                GoTo label1
            End If
            
            Worksheets.Item("Parameters").Name = SheetName
            
            Set ParamsSheet = Worksheets.add(After:=Worksheets(1))
            ParamsSheet.Name = "Parameters"
        ElseIf res = vbCancel Then
            MsgBox "キャンセルします。"
            Exit Sub
        End If
        
    Else
    
        Set ParamsSheet = Worksheets.add(After:=Worksheets(1))
        ParamsSheet.Name = "Parameters"
        
    End If

'----------------------------------------------------------------------    
    
    Dim network As TwoLayerNet
    Set network = New TwoLayerNet
    
    'ニューラルネットワークの初期化
    Call network.Initialize(input_size, hidden_size, output_size, ParamsSheet)
    
    
    Dim eRow As Long
    Dim eCol As Long
    Dim Datas() As Double
    Dim c As Long
    Dim r As Long
    Dim i As Long
    Dim epoc As Long
    Dim sumE As Double
    Dim sumAcc As Long
    Dim cnt As Long
    Dim label As Integer
    Dim t() As Long
    
    Dim DataRows() As Long
    
    
    With ws_mnist_train
        
        eRow = .Cells(Rows.count, 1).End(xlUp).Row
        eCol = .Cells(1, Columns.count).End(xlToLeft).Column
            
        ReDim Datas(eCol - 1)
        
        Do
        
            '最大エポック数に到達したら学習終了
            If epoc >= MaxEpoc Then Exit Do
            
            sumAcc = 0
            sumE = 0
            cnt = 0
        
            '学習データ行をランダム取得
            ReDim DataRows(batch_size)
            DataRows = Functions.GetRandomRow(batch_size, train_size)
        
        
            For r = 1 To UBound(DataRows)
                
                cnt = cnt + 1
                
                For c = 2 To eCol
                    Datas(c - 1) = .Cells(DataRows(r), c).Value / 255  '正規化
                Next c
                
                label = .Cells(DataRows(r), 1).Value
                t = Functions.one_hot_t(output_size, label)     'labelをone-hot化
                
                
                Call network.gradient(Datas, t)                 '重み/バイアスパラメータの勾配を求める
                
                sumE = sumE + network.E
                
                If network.accuracy = True Then                
                    sumAcc = sumAcc + 1
                End If
                
                Call network.ParamsUpdate(LearningRate)         'Affinレイヤの重み/バイアスパラメータを更新
                
                
                '[F7]キーが押下されたら学習終了
                If Functions.KeyPress = True Then
                    Call network.ExportParameters(ParamsSheet)
                    MsgBox "学習終了" & vbLf & "学習結果はシート(Parameters)に書き出しました。"
                    Exit Sub
                End If
                
                DoEvents
            Next r
            
            epoc = epoc + 1
            
      'テストデータ(未学習データ)で精度確認-------------------------------------------------------------
            
            Dim testData() As Double
            Dim testLabel As Long
            Dim testDataRows() As Long
            Dim test_acc() As Double
            Dim test_acc_size As Long
            Dim test_cnt As Long
            
            test_acc_size = 100
            ReDim testData(input_size)
            
            ReDim testDataRows(test_acc_size)
            testDataRows = Functions.GetRandomRow(test_acc_size, test_size)
            
            test_cnt = 0
            
            For r = 1 To UBound(testDataRows)
                For c = 2 To eCol
                    testData(c - 1) = ws_mnist_test.Cells(testDataRows(r), c).Value / 255  '正規化
                Next c
                
                testLabel = ws_mnist_test.Cells(testDataRows(r), 1).Value
                
                If network.test_accuracy(testData, testLabel) = True Then
                    test_cnt = test_cnt + 1
                End If
            Next r
            
      '--------------------------------------------------------------------------------------------
            
            '学習の途中経過をイミディエイトウィンドウに表示
            Debug.Print epoc & "回目: " & sumE / cnt & " 正解率: " & (sumAcc / cnt) * 100 & "%  テスト正解率" & (test_cnt / test_acc_size) * 100 & "% "
            
        Loop Until sumE / cnt < MinLoss
    
    End With
 

    '学習済みパラメータを書き出し
    Call network.ExportParameters(ParamsSheet)
        
    MsgBox "学習終了" & vbLf & "学習結果はシート(Parameters)に書き出しました。"

End Sub

 
以下では上記のコードでどのような処理を行っているかを解説していきます。
※メインモジュールではこれまでに実装したクラス/レイヤ/関数を全て呼び出します。
 適宜、各実装ページに戻り本モージュールとの繋がりやレイヤごとの繋がりを確認しましょう。

 

Sub Train

関数「Train」ではこれまでに実装したレイヤや関数を呼び出してニューラルネットワークの学習を行う関数です。

ここでは「この関数がどのような処理を行なっているのか」を部分ごとに分けて上から順に説明していきます。
 

Windows APIの宣言

まずはメインモジュールの設定を行います。
「Functions」モジュールで実装した関数「KeyPress」を実行するためにWindows APIの「GetAsyncKeyState関数」を宣言します。(詳しくは「GetAsyncKeyState関数」ページを参照ください)

Option Explicit
Option Base 1
 
#If Win64 Then
    Declare PtrSafe Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Integer
#Else
    Declare Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Integer
#End If

 

変数の宣言

次に変数の宣言を行います。
プロシージャ外で宣言する必要のない変数もありますが、ハイパーパラメータ類は1つに固めておきたかったのでここでまとめて宣言しています。中には「Public」を使ってグローバル変数として宣言しているものもあるので注意してください。

Option Explicit
Option Base 1
 
#If Win64 Then
    Declare PtrSafe Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Integer
#Else
    Declare Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Integer
#End If

Dim input_size As Long         '入力層のニューロン数
Dim hidden_size As Long        '隠れ層のニューロン数
Dim output_size As Long        '出力層のニューロン数

Dim MinLoss As Double          '最小Loss値
Dim MaxEpoc As Long            '最大エポック数

Dim LearningRate As Double      '学習率
Dim batch_size As Long          'ミニバッチサイズ

Dim train_size As Long          'mnist_trainの総データ数(default:60000)
Dim test_size As Long           'mnist_testの総データ数(default:10000)

Public ParamsSheet As Worksheet '書き出し用シート

Public ParamsCheck As Boolean   'シート確認用変数

 

ハイパーパラメータ設定

ここからは「Sub Train()」プロシージャ内のコードです。
はじめに、学習の初期設定ともいえるハイパーパラメータを設定します。

ハイパーパラメータとは各層のニューロン数や学習率などのニューラルネットワークの根源となるパラメータのことをいいます。このハイパーパラメータの値は学習前に変更することが可能で、学習結果に応じて変更したり自分の好きな設定で学習を行うことができます。

今回作成したニューラルネットワークではハイパーパラメータの値を変更できるものは少ないですが、コードを書き足したり、レイヤを増やすことで隠れ層の数を増やしたり、活性化関数を変更したりすることも可能になります。(ニューラルネットワークについて理解が深まったらぜひコードを書き換えてみてください)

    'ユニット数の設定(Input:784 / Output:10は変更不可)
    
    input_size = 784   '28x28(px)
    hidden_size = 64   '学習結果に応じて変更可
    output_size = 10   '0~9
    
    
    '学習設定
    
    batch_size = 100     'バッチサイズ(Default:100)
    LearningRate = 0.01  '学習率(Default:0.01)
    
    train_size = ws_mnist_train.Cells(Rows.count, 1).End(xlUp).Row   'mnist_trainの総データ数(default:60000)
    test_size = ws_mnist_test.Cells(Rows.count, 1).End(xlUp).Row     'mnist_testの総データ数(default:10000)
    
    
    '学習終了条件
    
    MinLoss = 0.01
    MaxEpoc = 500 

基本的に今回変更できるのは以下のハイパーパラメータだけです。

hidden_size:隠れ層のニューロン数
batch_size:バッチサイズ(まとめて何個のデータを学習するかを決める)
LearningRate:学習率(この値が小さいほど学習時間は長くなるが精度の高い学習ができる)
train_size:学習用データのデータ数
test_size:テスト用データのデータ数
MinLoss:最少Loss値(Loss値がこの値になったら学習を終了する)
MaxEpoc:最大エポック数(batch_size×MaxEpoc個のデータを学習したら学習を終了する)

batch_size、MaxEpocについては本来の意味とは少し違うので注意してください。
(詳しい説明は後ほど出てきます)
 

学習途中データの確認

次に学習途中データの確認を行います。
今回実装するニューラルネットワークは最終的に「Parameters」という名前のシートを作成しそこに結果を書き出します。ここでは既に「Parameters」というシートが存在するかを確認し、以降でどの処理を行うかをユーザーに選択させます。

    ParamsCheck = False  
    
    Dim ws As Worksheet
    Dim flag As Boolean
    Dim res As String
    Dim msg As String
    
    msg = "学習モデルのデータがあります。" & vbLf & _
          "続きから学習を始めますか?" & vbLf & vbLf & _
          "[いいえ]を押すと新規モデルとして学習を始めます。"
    
    
    For Each ws In Worksheets
        If ws.Name = "Parameters" Then flag = True
    Next ws
    
    If flag = True Then
    
        res = MsgBox(msg, vbYesNoCancel + vbInformation, "学習データの再利用")
        
        If res = vbYes Then
            ParamsCheck = True
            Set ParamsSheet = Worksheets.Item("Parameters")
            
        ElseIf res = vbNo Then
            ParamsCheck = False
            flag = False
                  
label1:
            Dim SheetName As String
            SheetName = InputBox("既存のモデル(シート)名を入力して下さい。", "モデル名変更")
            If StrPtr(SheetName) = 0 Then
                MsgBox "キャンセルします。"
                Exit Sub
            ElseIf SheetName = "" Then
                MsgBox "モデル名を入力して下さい。"
                GoTo label1
            End If
            
            For Each ws In Worksheets
                If ws.Name = SheetName Then flag = True
            Next ws
            If flag = True Then
                MsgBox "「" & SheetName & "」は既に存在します。" & vbLf & "別の名称を入力し直してください。"
                GoTo label1
            End If
            
            Worksheets.Item("Parameters").Name = SheetName
            
            Set ParamsSheet = Worksheets.add(After:=Worksheets(1))
            ParamsSheet.Name = "Parameters"
        ElseIf res = vbCancel Then
            MsgBox "キャンセルします。"
            Exit Sub
        End If
        
    Else
    
        Set ParamsSheet = Worksheets.add(After:=Worksheets(1))
        ParamsSheet.Name = "Parameters"
        
    End If

ユーザーの選択により処理は以下のように分岐します。

Parametersが存在しない場合 → 学習開始
Parametersが存在する場合    → 続きから学習をする(Parametersに学習結果を上書き)
                 → 新規で学習をする(現在のParametersを別名に変更する)
                 → キャンセル

Parametersが存在する場合はメッセージボックスでさらに分岐しますが、学習済みのシート(つまりはParameters)がない場合はすぐに学習が始まります。

ここでは上記の分岐を行うために「シートの設定」と変数「ParamsCheck」の設定を行います。(ParamsCheckはグローバル変数なのでこの値を使ってTwoLayerNetクラスなどの処理を分岐させます)
 

学習

次にTwoLayerNetのインスタンスを作成し、ループ分を使って学習を行なっていきます。

   Dim network As TwoLayerNet
    Set network = New TwoLayerNet
    
    'ニューラルネットワークの初期化
    Call network.Initialize(input_size, hidden_size, output_size, ParamsSheet)
    
    
    Dim eRow As Long
    Dim eCol As Long
    Dim Datas() As Double
    Dim c As Long
    Dim r As Long
    Dim i As Long
    Dim epoc As Long
    Dim sumE As Double
    Dim sumAcc As Long
    Dim cnt As Long
    Dim label As Integer
    Dim t() As Long
    
    Dim DataRows() As Long
    
    
    With ws_mnist_train
        
        eRow = .Cells(Rows.count, 1).End(xlUp).Row
        eCol = .Cells(1, Columns.count).End(xlToLeft).Column
            
        ReDim Datas(eCol - 1)
        
        Do
        
            '最大エポック数に到達したら学習終了
            If epoc >= MaxEpoc Then Exit Do
            
            sumAcc = 0
            sumE = 0
            cnt = 0
        
            '学習データ行をランダム取得
            ReDim DataRows(batch_size)
            DataRows = Functions.GetRandomRow(batch_size, train_size)
        
        
            For r = 1 To UBound(DataRows)
                
                cnt = cnt + 1
                
                For c = 2 To eCol
                    Datas(c - 1) = .Cells(DataRows(r), c).Value / 255  '正規化
                Next c
                
                label = .Cells(DataRows(r), 1).Value
                t = Functions.one_hot_t(output_size, label)     'labelをone-hot化
                
                
                Call network.gradient(Datas, t)                 '重み/バイアスパラメータの勾配を求める
                
                sumE = sumE + network.E
                
                If network.accuracy = True Then                
                    sumAcc = sumAcc + 1
                End If
                
                Call network.ParamsUpdate(LearningRate)         'Affinレイヤの重み/バイアスパラメータを更新
                
                
                '[F7]キーが押下されたら学習終了
                If Functions.KeyPress = True Then
                    Call network.ExportParameters(ParamsSheet)
                    MsgBox "学習終了" & vbLf & "学習結果はシート(Parameters)に書き出しました。"
                    Exit Sub
                End If
                
                DoEvents
            Next r
            
            epoc = epoc + 1
            
      'テストデータ(未学習データ)で精度確認-------------------------------------------------------------
            
            Dim testData() As Double
            Dim testLabel As Long
            Dim testDataRows() As Long
            Dim test_acc() As Double
            Dim test_acc_size As Long
            Dim test_cnt As Long
            
            test_acc_size = 100
            ReDim testData(input_size)
            
            ReDim testDataRows(test_acc_size)
            testDataRows = Functions.GetRandomRow(test_acc_size, test_size)
            
            test_cnt = 0
            
            For r = 1 To UBound(testDataRows)
                For c = 2 To eCol
                    testData(c - 1) = ws_mnist_test.Cells(testDataRows(r), c).Value / 255  '正規化
                Next c
                
                testLabel = ws_mnist_test.Cells(testDataRows(r), 1).Value
                
                If network.test_accuracy(testData, testLabel) = True Then
                    test_cnt = test_cnt + 1
                End If
            Next r
            
      '--------------------------------------------------------------------------------------------
            
            '学習の途中経過をイミディエイトウィンドウに表示
            Debug.Print epoc & "回目: " & sumE / cnt & " 正解率: " & (sumAcc / cnt) * 100 & "%  テスト正解率" & (test_cnt / test_acc_size) * 100 & "% "
            
        Loop Until sumE / cnt < MinLoss
    
    End With

複雑なように見えますが処理の流れを大雑把にみると以下の通りです。

① ランダムで学習データを取得(このとき学習データの値を0〜1の範囲で表現するため255で割る)
② ①の画像データの正解ラベルを取得しone-hot表現に変換する
③ ①と②を引数としてTwoLayerNetの関数「gradient」を実行
④ ③で取得した勾配を使い、各Affineレイヤのパラメータを更新
⑤ ①〜④の処理を「batch_size」回行う(「batch_size」回の学習結果の平均の精度を出力する)
⑥ ⑤の処理を学習終了条件を満たすまでループする

この処理の流れはミニバッチ学習から来ています。
本来のミニバッチ学習では③の処理の時点で複数の画像データをまとめてTwoLayerNetに渡し、まとめて計算を行います。(通常これを1エポックという)

しかしVBAでミニバッチ学習を行う場合、入力する画像データが増えることは配列の次元が増えることを意味します。ここでは機能の理解をメインとしているためあまり複雑な計算は避けています。そのため本来のミニバッチ学習とは少し変えた処理で、ミニバッチ学習に似せた処理をしています。(つまりミニバッチ学習自体は実装できていません)

上記の処理には書いていませんが、ループの中ではテストデータの精度確認も併せて行います。
処理の内容自体は上記の内容とほとんど同じなので割愛しますが、最終的にはこのテストデータの精度で学習の良し悪しを判断します。(詳しくは実際に学習をする際に解説します)

ここの処理は説明を聞くよりも1行1行コードを見ながら何をしているかを確認していった方が理解しやすいと思います。おそらくニューラルネットワークを学ぶ上で1番つまづきやすいのが「学習」の部分なのでここはじっくりとやっていきましょう。
 

学習終了処理

最後に学習終了条件を満たした際の処理を書いておきます。
ここではTwoLayerNetの関数「ExportParameters」を呼び出すだけです。
引数となる「ParamsSheet」は先の「学習途中データの確認」のコードで条件に合わせて中身のシートを変更しているため、引数自体を変える必要はありません。

    '学習済みパラメータを書き出し
    Call network.ExportParameters(ParamsSheet)
        
    MsgBox "学習終了" & vbLf & "学習結果はシート(Parameters)に書き出しました。"

 

まとめ

ここではゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装を参考に、ニューラルネットワークで学習を行うためのメインモジュールを作成しました。

今回実装したメインモジュールの機能をまとめると以下の通りです。

Sub Train:学習を行う

長いコードにはなっていますが機能としては「学習を行う」の1つだけです。
これまでに実装してきたレイヤやクラスの関数を呼び出して学習していくので、初めのうちは少しややこしく感じるかもしれません。ただ、コードが理解できればやっている内容が思ったよりはシンプルだということがわかります。
このシンプルな内容を理解することで、ニューラルネットワークの仕組みも理解できるのでじっくり理解していってください。

今回のメインモジュールで「学習」に関するコードはすべてです。
次回はこれらのコードを使いMNIST学習を行う方法を解説していきます。
※これまでのコードをコピペして実行してもエラーが発生します。
 詳しくは次回のMNISTデータセットを学習させてみようを参照下さい。

 

メインページ
 

 icon-book 参考書籍

 

2021年1月3日AI,Deep Learning,Excel,VBA