無限不可能性ドライブ

『ニューラルネットワーク自作入門』に刺激されてExcelVBAでニューラルネットワークを作ってみたものの、やっぱり数学やらなきゃと思い少しずつやってきたのもあって、自分の知識の整理とかそういった感じです。

【VBA編】データの読み込み

必要なシート、モジュールの追加

前回は読み込むデータのパスを取得するまでの処理を作りましたので、今回はそのデータを読み込む処理を作ります。
訓練データ、テストデータはそれぞれ別のシートに読み込ませたいので、「訓練データ」と「テストデータ」の2つのシートを追加します。
オブジェクト名はそれぞれ「ws_Train_Data」、「ws_Test_Data」としています。
また、読み込む処理を書くためのモジュールも追加しましょう。
標準モジュールを追加し、オブジェクト名を「mdlReadData」とします。

f:id:celaeno42:20181117230044p:plain

CSV読み込み処理

今回使用するデータはCSV形式のデータとなっています。
Windowsのメモ帳でそのまま開くとこんな感じ。
なお、訓練データとテストデータはデータの件数が異なるだけです。

f:id:celaeno42:20181117231036p:plain:w400

単にCSVファイルを読み込むだけの処理なのでどういう方法でも構いませんが、ここでは次のようなコードにしてみました。

'[mdlReadData - 標準モジュール]
Option Explicit

'データの読み込み
Public Sub Click_データ読み込み()
    Dim filePath As String
    
    Application.ScreenUpdating = False
    
    '訓練データの読み込み
    filePath = Range(G.RNG_TRAIN_DATA_PATH).Value
    Call readData(ws_Train_Data, filePath)

    
    'テストデータの読み込み
    filePath = Range(G.RNG_TEST_DATA_PATH).Value
    Call readData(ws_Test_Data, filePath)

    Application.ScreenUpdating = True
    
    MsgBox "データを読み込みました。", vbOKOnly + vbInformation
    
End Sub

'データを読み込んで書き込み先のワークシートに書き込む
'[引数] <- aWsData : Worksheet / 読み込んだデータを書き込む先のシート, aFilePath : String / CSVデータのファイル名
'[戻り値] -> なし
Private Sub readData(ByRef aWsData As Worksheet, ByRef aFilePath As String)
    Dim fileNo As Long
    Dim r As Long
    Dim i As Long
    Dim data As String
    Dim datas() As String
    Dim dataLength As Long
    Const DELIMITER As String = ","
    
    With aWsData
    
        .Cells.Clear
        
        r = 1
        fileNo = FreeFile
        Open aFilePath For Input As #fileNo
            Do Until EOF(1)
                Line Input #fileNo, data
                datas = Split(data, DELIMITER)
                dataLength = UBound(datas)
                
                'splitはoption base 1の影響を受けないのでインデックスは0からスタート
                For i = 0 To dataLength
                    .Cells(r, i + 1).Value = datas(i)
                Next
                
                r = r + 1
            Loop
        Close #fileNo
    
    End With
    
End Sub

「Click_データ読み込み()」はボタンがクリックされたときに呼ばれる処理、「readData()」が実際のCSV読み込み処理です。
ここでは特に難しいことはしていません。

コードが書けたら、メインシートの「データ読み込み」ボタンに「Click_データ読み込み()」プロシージャを登録して、実際にデータファイルが読み込めるか試してみましょう。
正しく書けていれば、訓練データシート、テストデータシートにこのようにデータが読み込まれます。
(並び順は読み込むファイルによって異なります。)

f:id:celaeno42:20181117232201p:plain:w400

【VBA編】メイン画面の作成

メイン画面の作成

メイン画面を下記のように作成します。

f:id:celaeno42:20181115232506p:plain

シート名は「ws_Main」としておきます。
また、「G」という名前で標準モジュールを作成しておきましょう。
ここにはグローバルで使用する定数や変数をまとめておきます。

f:id:celaeno42:20181116214312p:plain

定数の宣言

パスを格納するセルのアドレスを定数として宣言します。

'[G - 標準モジュール]
Option Explicit

Public Const RNG_TRAIN_DATA_PATH As String = "C2"   '訓練データのパスを格納するセル
Public Const RNG_TEST_DATA_PATH As String = "C3"    'テストデータのパスを格納するセル

データの選択

訓練データとテストデータのファイルを選択する処理を作ります。
「C2」セルか「C3」セルをダブルクリックするとファイル選択ダイアログが表示されるようにします。
ファイル選択ダイアログでファイルを選択すると、ダブルクリックしたセルにパスが表示されます。
ダブルクリック時の処理なので「ws_Main」シートの「Worksheet_BeforeDoubleClick()」プロシージャに記述します。

'[ws_Main - シートオブジェクト]
Option Explicit

Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
    Dim filePath As String
    
    If Target = Range(G.RNG_TRAIN_DATA_PATH) _
        Or Target = Range(G.RNG_TEST_DATA_PATH) Then
        
        Cancel = True
        
        filePath = Application.GetOpenFilename("CSV ファイル,*.csv")
        
        If filePath <> "False" Then
            Target.Value = filePath
        End If
        
    End If
    
End Sub


コードが書けたら正しく動作するか確認しておきましょう。

f:id:celaeno42:20181116221550p:plain


データのダウンロード先を再掲しておきます。
訓練用データ:https://1drv.ms/u/s!Akn_IZSOKLJ-1gKVR6dEZOntRsIh
テストデータ:https://1drv.ms/u/s!Akn_IZSOKLJ-1gHgfYyKccBTvuTX
(webで表示すると1行目が文字化けしてますが、ダウンロードすると正常に表示されます(Shift-JIS))

【VBA】文字列検索【GAS】

某所でのお題を若干改変して「スプレッドシートから特定の文字列を検索してそのアドレスを出力する処理」としてみました。
この表の中から「Excel」という文字列を探して、アドレスを出力します。
GASの勉強という側面もあるので、Findとかは使わずに地味に For で回す処理にしてあります。

f:id:celaeno42:20181109233420p:plain

VBA

Option Explicit

Public Sub textSearch()
    Dim vRange As Range
    Dim r As Long
    Dim c As Long
    Dim searchText As String
    Dim res As String
    
    searchText = "Excel"
    
    Set vRange = ActiveSheet.UsedRange
    
    res = ""
    For r = 1 To vRange.Rows.Count
        For c = 1 To vRange.Columns.Count
            If Cells(r, c).Value = searchText Then
                res = Cells(r, c).Address
                GoTo BREAK
            End If
        Next
    Next
    
BREAK:

    If res = "" Then
        Debug.Print "文字列「" & searchText & "」は見つかりません。"
    Else
        Debug.Print "文字列「" & searchText & "」は[" & res & "]にあります。"
    End If
    
End Sub

特段特別な書き方はしていません。
唯一、二重の For から一気に抜けるために GoTo を使っていることくらいでしょうか。
(他の言語の break 文をイメージ)


【GAS】

function textSearch() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var values = sheet.getDataRange().getValues();
  var res = '';
  
  var searchText = 'Excel';
  
  LABEL:
  for (var i=0; i<values.length; i++) {
    for (var j=0; j<values[i].length; j++) {
      if (searchText == values[i][j]) {
        res = sheet.getRange(i + 1, j + 1).getA1Notation();
        break LABEL;
      }
    }
  }

  if (res == '') {
    res = '文字列「' + searchText + '」は見つかりません。'
  } else {
    res = '文字列「' + searchText + '」は[' + res + ']にあります。'
  }
  
  Logger.log(res);
  
}

こちらも二重ループから一気に抜けるためにラベルを使っています。
Javaも同じ書き方ですが、どうもこのラベルの使い方は慣れないです…

【VBA編】(準備)クラスについて(2)

クラスについて その2

前回に引き続きクラスについてもう少し見ていきましょう。

インスタンス(オブジェクト)

クラスを使うにあたっての利点の一つは1つのクラスから複数のインスタンス(実体)を作れることではないでしょうか。
特に同じようで若干異なる処理をさせたいときなどに役に立つと思います。

次のような処理をしたい場合、クラスを使わないで書くとしたらどうなるでしょうか。
f:id:celaeno42:20181105230935p:plain

[処理結果]
f:id:celaeno42:20181105231040p:plain

ひとまずこのようにしてみました。
各種類の次の書き込み行をそれぞれの変数で制御しているのが煩わしい感じがします。

'[標準モジュール]
Option Explicit

Public Enum CL
    Shokuhin = 5
    Bunbougu = 7
    Kaden = 9
End Enum

Public Sub Main()
    Dim r As Long
    Dim eRow As Long
    Dim shurui As String
    Dim rwShokuhin As Long
    Dim rwBunbougu As Long
    Dim rwKaden As Long
    
    Cells(1, CL.Shokuhin).Value = "食品"
    Cells(1, CL.Bunbougu).Value = "文房具"
    Cells(1, CL.Kaden).Value = "家電"
    
    rwShokuhin = 2
    rwBunbougu = 2
    rwKaden = 2
    
    eRow = Cells(Rows.Count, 1).End(xlUp).Row

    For r = 2 To eRow
        shurui = Cells(r, 1).Value

        Select Case shurui
            Case "食品"
                Cells(rwShokuhin, CL.Shokuhin).Value = Cells(r, 2).Value
                rwShokuhin = rwShokuhin + 1
            Case "文房具"
                Cells(rwBunbougu, CL.Bunbougu).Value = Cells(r, 2).Value
                rwBunbougu = rwBunbougu + 1
            Case "家電"
                Cells(rwKaden, CL.Kaden).Value = Cells(r, 2).Value
                rwKaden = rwKaden + 1
        End Select

    Next

    MsgBox "終了しました。"    

End Sub

クラスを使って書いてみます。

'[クラスモジュール - classPlot]
Option Explicit

Dim mShurui As String
Dim mRow As Long
Dim mCol As Long

'初期化処理
'自分の担当する種類を覚えて、自分の列の1行目に表示する
'[引数] <- aShurui / String:自分の担当する種類, aCol / Long:表示先列
Public Sub Initialize(ByRef aShurui As String, ByRef aCol As Long)
    mShurui = aShurui
    mRow = 2
    mCol = aCol
    
    Cells(1, mCol).Value = mShurui
End Sub

'品目追加処理
'表示先セルに品目を表示して表示先行をインクリメントする
'[引数] <- aItem / String:品目
Public Sub AddItem(ByRef aItem As String)
    Cells(mRow, mCol).Value = aItem
    mRow = mRow + 1
End Sub

'自分の種類を返す
Public Property Get Shurui() As String
    Shurui = mShurui
End Property

「初期化処理」では、自分の担当する「種類」と「表示先の列」を引数として取っています。
「種類」と「列」を変数に格納したら、1行目に種類を表示します。
「品目」の表示は2行目からなので、mRow に 2 をセットしています。
なお、今回は 「Class_Initialize()」での処理は不要と判断して定義していません。

「品目追加処理」では、「品目」を引数として取り、表示先のセルに表示しています。
その後、行を更新し次の表示に備えています。

あと、呼び出し元で利用するために、「種類」を返す Property Get を定義しています。
今回は、「種類」の設定は「初期化処理」のみで行うので、Property Let は定義しませんでした。

では、呼び出し元を見てみましょう。

'[標準モジュール]
Option Explicit

Public Sub Main_class()
    Dim r As Long
    Dim eRow As Long
    Dim item As String
    
    Dim cShokuhin As classPlot
    Dim cBunbougu As classPlot
    Dim cKaden As classPlot
    
    Set cShokuhin = New classPlot
    Set cBunbougu = New classPlot
    Set cKaden = New classPlot
    
    '種類と表示先列を引数として振り分け担当(オブジェクト)を作る
    Call cShokuhin.Initialize("食品", 5)
    Call cBunbougu.Initialize("文房具", 7)
    Call cKaden.Initialize("家電", 9)
    
    eRow = Cells(Rows.Count, 1).End(xlUp).Row
    
    For r = 2 To eRow
        item = Cells(r, 2).Value
        Select Case Cells(r, 1).Value
            Case cShokuhin.Shurui
                Call cShokuhin.AddItem(item)
            Case cBunbougu.Shurui
                Call cBunbougu.AddItem(item)
            Case cKaden.Shurui
                Call cKaden.AddItem(item)
        End Select
    Next
    
    MsgBox "終了しました。"
    
End Sub

「食品」「文房具」「家電」を担当する「cShokuhin」「cBunbougu」「cKaden」を宣言し、
それぞれのインスタンスを生成しています(New しています)。
それぞれに該当の引数を与えて「初期化処理」をした後、A列を順番に見ていって
該当するインスタンスの「AddItem()」を呼び出して、「品目」を表示しています。

先ほどのコードと異なる点は、「品目」を表示する際に、呼び出し元では「何行目に表示するか」を一切気にしていないというところです。
また、列についても、「初期化処理」で設定してしまえば、以降は気にする必要がありません。

クラスを使うとコードが長くなる場合がありますが、必要な設定や処理はクラスに任せて、
呼び出し側では「あとはお願い」的にコードが書けるので考慮すべきことが少なくなります。

ということは、バグが混入する余地を減らすことにもつながります。

配列への格納

複数のインスタンスを作る際、上記の例のようにそれぞれで変数を宣言してもいいのですが、
宣言すべき変数が多くなる場合や、そもそもいくつ変数が必要になるかがわからない場合があるかと思います。
そういった場合はインスタンスを配列に格納すると便利です。

'[クラスモジュール - classPlayer]
Option Explicit

Dim mIndex As Long
Dim mColor As Long
Dim mBaseColor As Long
Dim mRow As Long
Dim mCol As Long

Private Sub Class_Initialize()
    mRow = 1
    mBaseColor = Cells(1, 1).Interior.Color
End Sub

Public Sub Initialize(ByRef aIndex As Long, ByRef aCol As Long)
    mIndex = aIndex
    mColor = Cells(1, aCol).Interior.Color
    mCol = aCol
End Sub

Public Property Get MyIndex() As Long
    MyIndex = mIndex
End Property

Public Property Get MyRow() As Long
    MyRow = mRow
End Property

Public Property Get MyColumn() As Long
    MyColumn = mCol
End Property

Public Function GoNext() As Boolean
    Dim res As Boolean
    Dim offsetR As Long
    Dim offsetC As Long
    
    res = True
    offsetR = 0
    offsetC = 0
    
    If Cells(mRow, mCol + 1).Interior.Color <> mBaseColor And Cells(mRow, mCol + 1).Interior.Color <> mColor Then
        offsetC = 1
    ElseIf Cells(mRow, mCol - 1).Interior.Color <> mBaseColor And Cells(mRow, mCol - 1).Interior.Color <> mColor Then
        offsetC = -1
    ElseIf Cells(mRow + 1, mCol).Interior.Color <> mBaseColor And Cells(mRow + 1, mCol).Interior.Color <> mColor Then
        offsetR = 1
    Else
        res = False
    End If
    
    If res Then
        mRow = mRow + offsetR
        mCol = mCol + offsetC
        
        Application.Wait [Now()+"00:00:00.1"]
        Cells(mRow, mCol).Interior.Color = mColor
    End If
    
    GoNext = res
End Function

今回はクラスモジュールのコード説明は省略します。

呼び出し側はこのようにしています。

'[標準モジュール]
Option Explicit
Option Base 1

Public Sub Main()
    Dim cPlayers() As classPlayer
    Dim i As Long
    Dim flagNext As Boolean
    
    ReDim cPlayers(4)
    
    For i = 1 To UBound(cPlayers)
        Set cPlayers(i) = New classPlayer
        Call cPlayers(i).Initialize(i, getColumn(i))
    Next
    
    For i = 1 To UBound(cPlayers)
        Do
            flagNext = cPlayers(i).GoNext
        Loop Until flagNext = False
        Cells(cPlayers(i).MyRow, cPlayers(i).MyColumn).Value = cPlayers(i).MyIndex
    Next
    
End Sub

Private Function getColumn(ByRef aIndex As Long) As Long
    Dim res As Long
    
    Select Case aIndex
        Case 1
            res = 2
        Case 2
            res = 6
        Case 3
            res = 10
        Case 4
            res = 14
    End Select
    
    getColumn = res
    
End Function

まずは、classPlayer 型の配列 cPlayers() を宣言しています。
素数はここで設定してもいいのですが、今回は ReDim で設定しています。

ひとつめの For - Next ですが、
[ Set cPlayers(i) = New classPlayer ] とすることによって配列の要素ごとにインスタンスを格納しています。
そのあとで [ Call cPlayers(i).Initialize() ] を実行して、インスタンスの初期化を行っています。

次の For - Next では、配列から取り出したインスタンスを利用して処理を行っています。

このように配列に格納することで、取り回しを楽にすることができます。
(実際、この後で書いていくニューラルネットワークのコードでは、この手法を利用しています。)

ちなみに [ Option Base 1 ] で配列のインデックスを 1 始まりにしているのは、
今回のコードに都合がいいようにそうしているだけです。


上記コードはExcelシートにこのようなあみだくじを作成して実行してみてください。
色は同じでなくても構いません。
(実行前に復帰用にシートをコピーしておくことをお勧めします。)
f:id:celaeno42:20181108230439p:plain


これでひとまずクラスの説明は終わります。

クラスを使えるようにしましょう

VBAはクラスを使わなくてもコードを書くことは可能です。
しかし、クラスを使うことで、より簡単に、便利に、安全にコードを書くことができるようになります。
もちろん、適材適所がありますので、なんでもかんでもクラスにすればいいというわけではありません。
ただ、必要な時にクラスが使えるようにしておくことで、手持ちの武器が増え、表現の幅が広がります。
クラスを使うコードは使わないコードとちょっと発想の転換が必要というか頭の使い方が異なると思うので、はじめはなかなか慣れないかもしれません。
そういった場合はちょっとVBAから離れて「オブジェクト指向」の考え方のほうに目を向けてみるのもいいかと思います。

【VBA編】(準備)クラスについて(1)

準備

VBAニューラルネットワークを実装するにあたり、(ここでの実装では)クラスの概念が必要になります。
とはいえ、VBAでクラスを使うのはあまりなじみがないと思いますので、ここでは準備段階として簡単に説明しようと思います。

オブジェクト指向

オブジェクトとかクラスとかいう言葉を聞くと拒否反応を起こしてしまう方もいるのではないでしょうか。
でも、大丈夫です。
ExcelVBAはオブジェクト指向を完全にはサポートしていないので、覚えることはそれほど多くありません。
なので、ひとまずはあまりオブジェクト指向を意識しなくてもよいと思います。
とはいえ、概念だけでも知っておく必要はありますので、該当する部分だけを簡単に説明します。

クラス

クラスとは一般的には設計図やひな型のようなものという説明がされることが多いかと思います。
オブジェクト指向では、その設計図・ひな型(クラス)をもとに実体(オブジェクト、インスタンス)を作って利用していきます。
同じクラスから作られたオブジェクトは同じ属性、機能を持ちますが、(基本的には)それぞれが独立しています。
ユーザフォームのコントロールのようなものと考えればよいでしょうか。
たとえば、コマンドボタンを配置すると、いろいろと初期値が設定されていますが、
キャプションや色などはそれぞれのコマンドボタンごとに自由に設定できます(独立しています)。
クラスを定義し、そこからオブジェクトを作ることで、同じ機能を持つただし若干異なるオブジェクトを(比較的簡単に)大量に作ることができます。
オブジェクト指向では、継承や多様性などいろいろな用語が出てきますが、VBAではこの程度の理解で大丈夫だと思います。
つまり、
  「設計図・ひな型(クラス)となるものを用意し、それをもとに実体(オブジェクト)を作る」
まずは、これを抑えておきましょう。

[ひな型をもとに実体を作る]
f:id:celaeno42:20181101233203p:plain:w400

3冊の本を作ってみる

例として3冊の本を作ってみましょう。
本はそれぞれ「コード」「タイトル」「著者」「(税抜き)価格」の情報を持っていて、
「コード」「タイトル」「著者」「税抜き価格」「税込み価格」を表示します。

クラスを使わないで書いてみる

まずはクラスを使わないで書いてみましょう。
本ごとに必要な変数を用意して、それぞれの変数に必要な情報を格納するようなコードにしています。
(ユーザ定義型を使えばもう少しスマートに書けるとは思いますが…)

'[標準モジュール]
Option Explicit

Public Sub クラス不使用()
    Dim book01Code As String
    Dim book01Title As String
    Dim book01Author As String
    Dim book01Price As Long

    Dim book02Code As String
    Dim book02Title As String
    Dim book02Author As String
    Dim book02Price As Long
    
    Dim book03Code As String
    Dim book03Title As String
    Dim book03Author As String
    Dim book03Price As Long
    
    book01Code = "bk-01"
    book01Title = "長時間の影"
    book01Author = "H.P.ラヴクラフト"
    book01Price = 800
    
    book02Code = "bk-02"
    book02Title = "異次元を覗く家"
    book02Author = "W.H.ホジスン"
    book02Price = 1000
    
    book03Code = ""
    book03Title = ""
    book03Author = ""
    book03Price = -100
    
    Call printBook(book01Code, book01Title, book01Author, book01Price)
    Call printBook(book02Code, book02Title, book02Author, book02Price)
    Call printBook(book03Code, book03Title, book03Author, book03Price)
    
End Sub

Private Sub printBook(ByRef aCode As String, ByRef aTitle As String, ByRef aAuthor As String, ByRef aPrice As Long)
    Const TAX As Double = 1.08
    
    Debug.Print "----" & aCode & "----"
    Debug.Print "タイトル = " & aTitle
    Debug.Print "著者 = " & aAuthor
    Debug.Print "税抜き価格 = " & aPrice
    Debug.Print "税込み価格 = " & aPrice * TAX
    Debug.Print "------------"
End Sub


[イミディエイトウィンドウ](出力結果)

----bk-01----
タイトル = 長時間の影
著者 = H.P.ラヴクラフト
税抜き価格 = 800
税込み価格 = 864
------------
----bk-02----
タイトル = 異次元を覗く家
著者 = W.H.ホジスン
税抜き価格 = 1000
税込み価格 = 1080
------------
--------
タイトル = 
著者 = 
税抜き価格 = -100
税込み価格 = -108
------------


クラスを使って書いてみる - 1(クラスモジュール)

では、これをクラスを使って書いてみましょう。
クラスはクラスモジュールに書きますので、VBEの[挿入]から「クラスモジュール」を選んでクラスモジュールを追加しましょう。
クラスモジュールのオブジェクト名は「classBook」としておきます。

VBEのプロジェクトはこのようになります。
f:id:celaeno42:20181103231252p:plain

以下のコードは classBook に書きます。

'[クラスモジュール - classBook]
Option Explicit

Public pCode As String
Public pTitle As String
Public pAuthor As String
Public pPrice As Long
Const TAX As Double = 1.08

Public Function exTaxPrice() As Long
    exTaxPrice = pPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = pPrice * TAX
End Function

「コード」「タイトル」「著者」「(税抜き)価格」はこのモジュールの外から呼ばれることを想定して、それぞれ Public変数として宣言しています。
これらの「クラスの持つ変数」をここでは他の言語のマネをして「メンバ変数」と呼ぶことにします。
また、定数として「税率」を持っていて、「税抜き価格」「税込み価格」は Function プロシージャで計算結果を戻すようにしています。
(もっとも、「税抜き価格」はそのまま「価格」を返しているだけですが、Functionプロシージャとすることで、呼び出し側で「税込み価格」と同じような書式で呼び出すことができます。)

では、呼び出し側の処理と、結果を出力する処理を見てみましょう。
これらは標準モジュールに書いています。

'[標準モジュール]
Option Explicit

Public Sub クラス使用()
    Dim cBook11 As classBook
    Dim cBook12 As classBook
    Dim cBook13 As classBook
    
    Set cBook11 = New classBook
    cBook11.pCode = "bk-11"
    cBook11.pTitle = "銀河ヒッチハイクガイド"
    cBook11.pAuthor = "ダグラス・アダムス"
    cBook11.pPrice = 420
    
    Set cBook12 = New classBook
    cBook12.pCode = "bk-12"
    cBook12.pTitle = "プロジェクトぴあの"
    cBook12.pAuthor = "山本弘"
    cBook12.pPrice = 1500
    
    Set cBook13 = New classBook
    cBook13.pTitle = ""
    cBook13.pPrice = -100
    
    Call printBookInfo(cBook11)
    Call printBookInfo(cBook12)
    Call printBookInfo(cBook13)

    Set cBook11 = Nothing
    Set cBook12 = Nothing
    Set cBook13 = Nothing

End Sub

Private Sub printBookInfo(ByRef aBook As classBook)
    Debug.Print "----" & aBook.pCode & "----"
    Debug.Print "タイトル = " & aBook.pTitle
    Debug.Print "著者 = " & aBook.pAuthor
    Debug.Print "税抜き価格 = " & aBook.exTaxPrice
    Debug.Print "税込み価格 = " & aBook.inTaxPrice
    Debug.Print "------------"
End Sub

クラスは変数として宣言できます。
通常の変数と同様に

 Dim 変数名 as クラス名

とすればOKです。

そのあとで

 Set 変数名 = New クラス名

とすることで、クラス(設計図)からインスタンス(オブジェクト / 実体)が作られます。
(一般的に 「Newする」といいます。)

クラスの持つ Public な変数やプロシージャなどにアクセスするには、「 . 」(ドット演算子)を用います。
上記コードでは、[ cBook11.pCode = "bk-01" ] などとしているところで、
これは、「cBook11 オブジェクト のメンバ変数 pCode に 文字列 "bk-01" を代入する」という意味です。
フォームコントロールの [ Text1.Text = "bk-01" ] などの書き方と同じですね。

また、クラスはプロシージャの引数として渡すこともできます。
上記コードでは、printBookInfoの引数として classBook型の変数 を渡しています。

最後の [ Set cBook11 = Nothing ] でオブジェクトを解放(破棄)しています。
ただ、VBAの場合、変数は所属するプロシージャが終了すると破棄されるので、あまり神経質にならなくてもいいのでは?との意見もあります。

では、実行して出力を見てみましょう。
設定した値が表示されているのがわかりますね。

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
--------
タイトル = 
著者 = 
税抜き価格 = -100
税込み価格 = -108
------------


クラスを使って書いてみる - 2(プロパティ)

さて、設定した値が表示されるのはいいのですが、例えばタイトルにブランクを設定したら「未定」と表示させたり、価格にマイナスの値が設定されたら 0 にしたりというようにしたい場合、上記のコードでは実現できません。
そこで、classBook をもう少し使い勝手の良いクラスに修正してみましょう。

'[クラスモジュール - classBook]
Option Explicit

Dim mCode As String
Dim mTitle As String
Dim mAuthor As String
Dim mPrice As Long
Const TAX As Double = 1.08

Public Property Let Code(ByRef aCode As String)
    If aCode = "" Then
        aCode = "bk-xx"
    Else
        mCode = aCode
    End If
End Property

Public Property Get Code() As String
    Code = mCode
End Property

Public Property Let Title(ByRef aTitle As String)
    If aTitle = "" Then
        mTitle = "未定"
    Else
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

Public Property Let Author(ByRef aAuthor As String)
    If aAuthor = "" Then
        mAuthor = "未定"
    Else
        mAuthor = aAuthor
    End If
End Property

Public Property Get Author() As String
    Author = mAuthor
End Property

Public Property Let Price(ByRef aPrice As Long)
    If aPrice < 0 Then
        mPrice = 0
    Else
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function


少し長くなりましたが解説していきましょう。
まず、前回 Public で宣言していたメンバ変数を Dim の宣言に変更しています。
そのため、前回のように呼び出し側(classBookの外)からは直接メンバ変数を操作できなくなっています。
そこで、メンバ変数を操作するために、Propertyプロシージャを利用します。
コードを見ると Property Let、Property Get でなんらかの同じ名前の定義をしていることがわかると思います。
これは、他の言語では セッター/ゲッター(Setter/Getter) と呼ばれるもので、クラスのメンバ変数の値を設定したり取得したりするプロシージャです。
VBAでは Property プロシージャと呼ばれ、通常は、Let(オブジェクトの場合はSet)と Get で同じ名前のプロシージャを定義して、値を設定したり取得したりする際に利用します。
Let(Set)は1つの引数をとり、Get は引数をとることができないという点も Property プロシージャ独特の仕様です。
これも次のようなフォームコントロールの使い方と同様です。

 Text1.Text = "Hello World"
 result = Text1.Text
 Debug.Print(result)

 [出力]
 Hello World

一部を抜粋しましょう。

Dim mTitle As String

Public Property Let Title(ByRef aTitle As String)
    If aTitle = "" Then
        mTitle = "未定"
    Else
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

これは Titleプロパティに Let で値(引数の aTitle) を設定、Get で値を取得するコードです。
引数 aTitle がブランクの場合は、Titleのデフォルト値として "未定" を設定するようにしています。
設定値はメンバ変数 mTitle に格納し、Get の戻り値としています。

これは例えば、

 cBook.Title = "アイの物語"  '値の設定(Let が呼ばれる)
 result = cBook.Title     '値の取得(Get が呼ばれる)
 Debug.Print(result)

 [出力]
 アイの物語

のように使います。

Dim mPrice As Long
Const TAX As Double = 1.08

Public Property Let Price(ByRef aPrice As Long)
    If aPrice < 0 Then
        mPrice = 0
    Else
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function

上記コードは、Let で価格(Price)を設定している個所です。
価格がマイナス(aPrice が 0 未満)なら、価格に 0 を設定するようにしています。
ただ、Price プロパティ については、Get がありません。
(代わりに exTaxPrice()、inTaxPrice() で値を返しています。)
このように、必ずしも Let(Set) と Get を対で利用しなくてもかまいません。

では、これを標準モジュールから呼び出してみましょう。

'[標準モジュール]
Option Explicit

Public Sub クラス使用()
    Dim cBook11 As classBook
    Dim cBook12 As classBook
    Dim cBook13 As classBook
    
    Set cBook11 = New classBook
    cBook11.Code = "bk-11"
    cBook11.Title = "銀河ヒッチハイクガイド"
    cBook11.Author = "ダグラス・アダムス"
    cBook11.Price = 420
    
    Set cBook12 = New classBook
    cBook12.Code = "bk-12"
    cBook12.Title = "プロジェクトぴあの"
    cBook12.Author = "山本弘"
    cBook12.Price = 1500
    
    Set cBook13 = New classBook
    cBook13.Title = ""
    cBook13.Price = -100
    
    Call printBookInfo(cBook11)
    Call printBookInfo(cBook12)
    Call printBookInfo(cBook13)

    Set cBook11 = Nothing
    Set cBook12 = Nothing
    Set cBook13 = Nothing

End Sub

Private Sub printBookInfo(ByRef aBook As classBook)
    Debug.Print "----" & aBook.Code & "----"
    Debug.Print "タイトル = " & aBook.Title
    Debug.Print "著者 = " & aBook.Author
    Debug.Print "税抜き価格 = " & aBook.exTaxPrice
    Debug.Print "税込み価格 = " & aBook.inTaxPrice
    Debug.Print "------------"
End Sub

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
--------
タイトル = 未定
著者 = 
税抜き価格 = 0
税込み価格 = 0
------------

呼び出し方は先ほどとそれほど変わっていません。
ただ、3冊めの出力結果が若干変わっていて、タイトル = 未定 となっていたり、価格が(マイナスでなく)0 になっています。
これは、プロパティの処理で引数がブランクだったりマイナスだったりしたときにデフォルト値を設定するようにしているためです。

このように、メンバ変数へのアクセスは必ず Property や Function から行うようにすることで、(価格にマイナスが設定されるような)不用意な操作を防ぐことができ、より安全性の高いコードになります(なるといわれてます)。
オブジェクト指向では「情報隠蔽」とか「カプセル化」といったりします。)

クラスを使って書いてみる - 3(初期化1)

もう一度3つめの出力結果をよく見てみると、「コード」と「著者名」がブランクになっています。
それぞれの Property プロシージャでは、「引数としてブランクが渡されたらデフォルト値に設定する」としていましたが、そもそも呼び出し側でこれらの項目については何の設定もしていないため、Code と Author については、Let プロシージャが呼ばれていないのです。そのため、Get プロシージャでは何の設定もされていない mCode, mAuthor が返されています。

そこで、classBook が New されたときに初期値を設定することで「何も設定しなくても初期値が設定されている」ように修正してみましょう。

'[クラスモジュール - classBook]
Option Explicit

Dim mCode As String
Dim mTitle As String
Dim mAuthor As String
Dim mPrice As Long
Const TAX As Double = 1.08

Private Sub Class_Initialize()
    mCode = "bk-xx"
    mTitle = "未定"
    mAuthor = "未定"
    mPrice = 0
End Sub

Public Property Let Code(ByRef aCode As String)
    mCode = aCode
End Property

Public Property Get Code() As String
    Code = mCode
End Property

Public Property Let Title(ByRef aTitle As String)
    If aTitle <> "" Then
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

Public Property Let Author(ByRef aAuthor As String)
    If aAuthor <> "" Then
        mAuthor = aAuthor
    End If
End Property

Public Property Get Author() As String
    Author = mAuthor
End Property

Public Property Let Price(ByRef aPrice As Long)
    If aPrice > 0 Then
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function

f:id:celaeno42:20181103233013p:plain:w600


VBEのオブジェクトボックス(①の箇所)で「Class」、プロシージャボックス(②の箇所)で「Initialize」を選択すると、「Class_Initialize()」プロシージャが挿入されます。
このプロシージャはクラスが New されたときに自動的に呼び出される(実行される)プロシージャで、オブジェクトの初期設定に利用することができます。
他の言語では「コンストラクタ」と呼ばれる機能に近いと思います。
ただし、VBAの「Class_Initialize()」プロシージャは(残念ながら)引数をとることができないので、New する際には同じ初期値(デフォルト値)でしか初期化できません。
今回は、「コード」は「bk-xx」、「タイトル」は「未定」、「著者」は「未定」、「価格」は「0」で初期化しています。
また、前のコードでは、Let プロシージャで引数にブランクや0が渡されたときは、それぞれのデフォルト値を設定するようにしていましたが、今回のコードでは無視するようにしています。
New したときに自動的に実行される「Class_Initialize()」で初期値を設定しているため、それぞれの Let プロシージャであらためてデフォルト値を設定する必要がないためです。

では、実際に呼び出して結果を見てみましょう。
呼び出し元のコードは前回と同じです。

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
----bk-xx----
タイトル = 未定
著者 = 未定
税抜き価格 = 0
税込み価格 = 0
------------

3冊目の「著者」がきちんと「未定」になっていますね。

クラスを使って書いてみる - 4(初期化2)

さて、いまの classBook では、New した後に「タイトル」プロパティには「タイトル」、「著者」プロパティには「著者名」など、それぞれのプロパティを代入する必要があります。
これはちょっと面倒なので、一気に設定できると便利ですね。
そこでプロパティを一気に設定できるプロシージャを作ることにしましょう。
先ほども書いたように、他の言語では New する際に初期値を引数で渡せますが、VBAではそれができないので、New した後にこの「初期値設定用のプロシージャ」を呼び出すことで、プロパティをひとつひとつ設定する手間を省くことができます。
今回、この「初期値設定用のプロシージャ」は「Initialize()」という名前にしましたが、「Class_Initialize()」と紛らわしいので別の名前にしても構いません。
(私は「Constructor()」とすることもあります。)

'[クラスモジュール - classBook]
Option Explicit

Dim mCode As String
Dim mTitle As String
Dim mAuthor As String
Dim mPrice As Long
Const TAX As Double = 1.08

Private Sub Class_Initialize()
    mCode = "bk-xx"
    mTitle = "未定"
    mAuthor = "未定"
    mPrice = 0
End Sub

'プロパティを一度に設定する
Public Sub Initialize(ByRef aCode As String, ByRef aTitle As String, ByRef aAuthor As String, ByRef aPrice As Long)
    Me.Code = aCode
    Me.Title = aTitle
    Me.Author = aAuthor
    Me.Price = aPrice
End Sub

Public Property Let Code(ByRef aCode As String)
    If aCode <> "" Then
        mCode = aCode
    End If
End Property

Public Property Get Code() As String
    Code = mCode
End Property

Public Property Let Title(ByRef aTitle As String)
    If aTitle <> "" Then
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

Public Property Let Author(ByRef aAuthor As String)
    If aAuthor <> "" Then
        mAuthor = aAuthor
    End If
End Property

Public Property Get Author() As String
    Author = mAuthor
End Property

Public Property Let Price(ByRef aPrice As Long)
    If aPrice > 0 Then
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function

「Initialize()」プロシージャでは、引数をそれぞれの Property プロシージャに渡しています。
これは、「Initialize()」 の引数として渡されてきた値が不用意にプロパティに格納されないようにするためです。
いったん Property プロシージャに渡すことで Property Let でのチェックが働くので、コードの安全性を高めることができます。

では、呼び出し側を見てみましょう。

'[標準モジュール]
Option Explicit

Public Sub クラス使用()
    Dim cBook11 As classBook
    Dim cBook12 As classBook
    Dim cBook13 As classBook
    
    Set cBook11 = New classBook
    cBook11.Code = "bk-11"
    cBook11.Title = "銀河ヒッチハイクガイド"
    cBook11.Author = "ダグラス・アダムス"
    cBook11.Price = 420
    
    Set cBook12 = New classBook
    Call cBook12.Initialize("bk-12", "プロジェクトぴあの", "山本弘", 1500)
    
    Set cBook13 = New classBook
    Call cBook13.Initialize("", "", "", -100)
    
    Call printBookInfo(cBook11)
    Call printBookInfo(cBook12)
    Call printBookInfo(cBook13)
    
    '価格改定!
    Debug.Print
    Debug.Print "--! 価格改定 !--"
    cBook12.Price = 1200
    Call printBookInfo(cBook12)

    Set cBook11 = Nothing
    Set cBook12 = Nothing
    Set cBook13 = Nothing

End Sub

Private Sub printBookInfo(ByRef aBook As classBook)
    Debug.Print "----" & aBook.Code & "----"
    Debug.Print "タイトル = " & aBook.Title
    Debug.Print "著者 = " & aBook.Author
    Debug.Print "税抜き価格 = " & aBook.exTaxPrice
    Debug.Print "税込み価格 = " & aBook.inTaxPrice
    Debug.Print "------------"
End Sub

「cBook12」と「cBook13」のプロパティ設定を今回作成した「Initialize()」プロシージャで行っています。
「cBook11」と比べるとスッキリしていますね。
なお、必要に応じて「Initialize()」プロシージャの引数に Optional キーワードを使うことで、呼び出し側で必要な引数のみを設定すればよくなるため、より使い勝手の良いクラスになると思います。

今回は、一度書籍情報を表示した後、「cBook12」の価格を変更してみました。
このようにいったんオブジェクトの初期値を一度に設定した後、一部のプロパティを変更することも可能です。

出力はこのようになります。

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
----bk-xx----
タイトル = 未定
著者 = 未定
税抜き価格 = 0
税込み価格 = 0
------------

--! 価格改定 !--
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1200
税込み価格 = 1296
------------

2冊目と3冊目が正しく初期値設定されていることと、きちんと2冊目の価格が変更されたことがわかりますね。

次回はクラスについてもうちょっと見ていきます。

【VBA編】どんなものを作っていくか

今回作るニューラルネットワーク


さんざん既出ですが、これを実装していきます。
f:id:celaeno42:20180922233518p:plain


完成のイメージはこんな感じです。(動画が再生されます)
https://1drv.ms/v/s!Akn_IZSOKLJ-1WwjfhdtoXOFKQv1

どう実装するか

ユニットクラスを作りそれで各層のユニットを作っていきます。
また、レイヤークラスを作り、各層のユニットを管理します。
基本的には、これまでで求めた計算式を実装していく形になります。
ニューラルネットワークの実装では、通常は行列計算を使った実装が行われると思いますが、
今回は行列計算は利用しません。
そのかわり、処理速度はあまり速くはありません。

アイリスデータセット

データセットはアイリスデータセットを使用し、アイリス(あやめ)の分類を行います。
f:id:celaeno42:20181102222129p:plain

実際のデータはこんな感じです。
f:id:celaeno42:20181201104704p:plain

がくの長さ、がくの幅、花弁の長さ、花弁の幅のデータから、それがどの種類のあやめかを推測します。
今回は、訓練用データとして129件、テストデータとして21件に分けたものを使用します。

訓練用データ:https://1drv.ms/u/s!Akn_IZSOKLJ-1lIFNhzFRmzawQF1
テストデータ:https://1drv.ms/u/s!Akn_IZSOKLJ-1lEDYSXiM_n2tniI
(webで表示すると1行目が文字化けしてますが、ダウンロードすると正常に表示されます(Shift-JIS))