【VBA編】(準備)クラスについて(2)
クラスについて その2
前回に引き続きクラスについてもう少し見ていきましょう。
インスタンス(オブジェクト)
クラスを使うにあたっての利点の一つは1つのクラスから複数のインスタンス(実体)を作れることではないでしょうか。
特に同じようで若干異なる処理をさせたいときなどに役に立つと思います。
次のような処理をしたい場合、クラスを使わないで書くとしたらどうなるでしょうか。
[処理結果]
ひとまずこのようにしてみました。
各種類の次の書き込み行をそれぞれの変数で制御しているのが煩わしい感じがします。
'[標準モジュール] 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シートにこのようなあみだくじを作成して実行してみてください。
色は同じでなくても構いません。
(実行前に復帰用にシートをコピーしておくことをお勧めします。)
これでひとまずクラスの説明は終わります。
クラスを使えるようにしましょう
VBAはクラスを使わなくてもコードを書くことは可能です。
しかし、クラスを使うことで、より簡単に、便利に、安全にコードを書くことができるようになります。
もちろん、適材適所がありますので、なんでもかんでもクラスにすればいいというわけではありません。
ただ、必要な時にクラスが使えるようにしておくことで、手持ちの武器が増え、表現の幅が広がります。
クラスを使うコードは使わないコードとちょっと発想の転換が必要というか頭の使い方が異なると思うので、はじめはなかなか慣れないかもしれません。
そういった場合はちょっとVBAから離れて「オブジェクト指向」の考え方のほうに目を向けてみるのもいいかと思います。
【VBA編】(準備)クラスについて(1)
準備
VBAでニューラルネットワークを実装するにあたり、(ここでの実装では)クラスの概念が必要になります。
とはいえ、VBAでクラスを使うのはあまりなじみがないと思いますので、ここでは準備段階として簡単に説明しようと思います。
オブジェクト指向
オブジェクトとかクラスとかいう言葉を聞くと拒否反応を起こしてしまう方もいるのではないでしょうか。
でも、大丈夫です。
ExcelVBAはオブジェクト指向を完全にはサポートしていないので、覚えることはそれほど多くありません。
なので、ひとまずはあまりオブジェクト指向を意識しなくてもよいと思います。
とはいえ、概念だけでも知っておく必要はありますので、該当する部分だけを簡単に説明します。
クラス
クラスとは一般的には設計図やひな型のようなものという説明がされることが多いかと思います。
オブジェクト指向では、その設計図・ひな型(クラス)をもとに実体(オブジェクト、インスタンス)を作って利用していきます。
同じクラスから作られたオブジェクトは同じ属性、機能を持ちますが、(基本的には)それぞれが独立しています。
ユーザフォームのコントロールのようなものと考えればよいでしょうか。
たとえば、コマンドボタンを配置すると、いろいろと初期値が設定されていますが、
キャプションや色などはそれぞれのコマンドボタンごとに自由に設定できます(独立しています)。
クラスを定義し、そこからオブジェクトを作ることで、同じ機能を持つただし若干異なるオブジェクトを(比較的簡単に)大量に作ることができます。
オブジェクト指向では、継承や多様性などいろいろな用語が出てきますが、VBAではこの程度の理解で大丈夫だと思います。
つまり、
「設計図・ひな型(クラス)となるものを用意し、それをもとに実体(オブジェクト)を作る」
まずは、これを抑えておきましょう。
[ひな型をもとに実体を作る]
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のプロジェクトはこのようになります。
以下のコードは 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
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編】どんなものを作っていくか
今回作るニューラルネットワーク
- 入力:4
- 出力:3
- 隠れ層の活性化関数:ReLU
- 出力層の活性化関数:Softmax
- 損失関数:クロスエントロピー
- 学習方法:教師あり学習
- 教師データ:one-hot 表現
- 扱う問題:分類問題
- データセット:アイリス データセット
さんざん既出ですが、これを実装していきます。
完成のイメージはこんな感じです。(動画が再生されます)
https://1drv.ms/v/s!Akn_IZSOKLJ-1WwjfhdtoXOFKQv1
どう実装するか
ユニットクラスを作りそれで各層のユニットを作っていきます。
また、レイヤークラスを作り、各層のユニットを管理します。
基本的には、これまでで求めた計算式を実装していく形になります。
ニューラルネットワークの実装では、通常は行列計算を使った実装が行われると思いますが、
今回は行列計算は利用しません。
そのかわり、処理速度はあまり速くはありません。
アイリスデータセット
データセットはアイリスデータセットを使用し、アイリス(あやめ)の分類を行います。
実際のデータはこんな感じです。
がくの長さ、がくの幅、花弁の長さ、花弁の幅のデータから、それがどの種類のあやめかを推測します。
今回は、訓練用データとして129件、テストデータとして21件に分けたものを使用します。
訓練用データ:https://1drv.ms/u/s!Akn_IZSOKLJ-1lIFNhzFRmzawQF1
テストデータ:https://1drv.ms/u/s!Akn_IZSOKLJ-1lEDYSXiM_n2tniI
(webで表示すると1行目が文字化けしてますが、ダウンロードすると正常に表示されます(Shift-JIS))
【数式編】(逆伝播)のまとめ 2
逆伝播の更新式を一覧でまとめたものを載せておきます。
VBA編ではこれを元に実装していきます。
【数式編】(逆伝播)のまとめ 1
逆伝播についてはそれぞれの重みとバイアスの (勾配)の部分を載せます。
重みやバイアスは、 (勾配)に学習率を掛け、現在の重みやバイアスの値から引くことで更新していきます。
出力層
出力層についても共通部分を で表すことにしましょう。
ユニットo11
ユニットo12
ユニットo13
隠れ層2層め
ユニットh21
ユニットh22
ユニットh23
ユニットh24
【数式編】(順伝播)のまとめ、あと(損失関数)
いままでに求めた順伝播の式をまとめておきます。
隠れ層1層め
ユニットh11
ユニットh12
ユニットh13
隠れ層2層め
ユニットh21
ユニットh22
ユニットh23
ユニットh24
出力層
ユニットo11
ユニットo12
ユニットo13
損失関数(クロスエントロピーエラー)
【数式編】(逆伝播)1つめの隠れ層の重みとバイアスを更新する 3-(3)
ユニットh12 の重みとバイアスの更新式
もう一度、前回までで求めた更新式を見てみましょう。
(再掲)
よく見ると、
という構造になっていることがわかります。
それを踏まえると以下のようになると思います。
今回の共通部は としましょう。
ユニットh13 の重みとバイアスの更新式
同じパターンなので以下のようになります。
共通部は とします。
これで必要な計算式がすべて求まりました。