【VBA編】(順伝播)隠れ層の作成
レイヤークラス
前回はユニットクラスを作成しました。今回からはユニットを管理するレイヤークラスを作成していきます。
入力層は以前こちらで作成しました。
celaeno42.hatenablog.com
隠れ層と出力層がありますが、若干機能が異なるので今回は別々に実装します。
隠れ層
今回のニューラルネットワークでは隠れ層は2層ありますが、機能は同じです。
今回は隠れ層を実装していきましょう。
隠れ層の初期化
まずは初期化処理を作っていきます。コードは「classHiddenLayer」に記述します。
レイヤークラスは自分の層のユニットを管理するので、ユニット数、ユニットへの入力データ数、活性化関数の種類を知っておく必要があります。
例えば、隠れ層1層めでは、ユニット数 = 3, 入力データ数 = 4, 活性化関数 = ReLU とします。
これらは引数として呼び出し元から渡せるようにしておきます。
クラスモジュール【classHiddenLayer】
'[classHiddenLayer - 隠れ層クラス] Option Explicit Option Base 1 Dim mUnitList() As classUnit 'ユニット格納用リスト Dim mUnitCount As Long '自レイヤーのユニット数 Dim mAct As Long '活性化関数の種類 '自レイヤーのユニットを作成する '[引数] <- aUnitCount : Long / ユニットの数, aInputCount : Long / 入力データの数, aActivationFunction : Long / 活性化関数の種類 Public Sub Initialize(ByRef aUnitCount As Long, ByRef aInputCount As Long, ByRef aActivationFunction As Long) Dim i As Long ReDim mUnitList(aUnitCount) '必要な数だけユニットを作成しユニット格納用リストに格納する For i = 1 To aUnitCount Set mUnitList(i) = New classUnit 'Newしたユニットを初期化する Call mUnitList(i).Initialize(aInputCount) Next mUnitCount = aUnitCount mAct = aActivationFunction End Sub
「classUnit」型の配列「mUnitList()」にNewしたユニット(インスタンス化したユニット)を格納していきます。
Newしたユニットは初期化したいので「Initialize()」を呼び出して初期化処理をします。ユニットは各入力データに対応する重みを持つ必要があるため、引数として入力データの数を渡しています。
最後に、ユニット数と活性化関数の種類をモジュールレベル変数に格納してこのモジュールの他のプロシージャでも使えるようにしています。
順伝播
自レイヤーの管理するユニットに入力データを渡してユニットの出力値を計算します。入力データは引数として受け取るようにします。
ユニットで u を計算したら、活性化関数を適用して z を計算します。「activateU()」はこの後で実装します。
クラスモジュール【classHiddenLayer】
'順伝播:各ユニットのuとzを求める '[引数] <- aInputDataList() : Double / 入力データのリスト Public Sub Forward(ByRef aInputDataList() As Double) Dim i As Long 'ユニットに入力値を渡して u を計算する For i = 1 To mUnitCount Call mUnitList(i).CalcU(aInputDataList) Next '活性化関数を適用して z を計算する Call activateU End Sub
活性化関数
標準モジュール【G】に活性化関数の種類を列挙型で宣言しておきましょう。こうすることで、他の活性化関数を実装する際の拡張性を確保します。
標準モジュール【G】
Public Enum ACT ReLU = 1 Softmax = 2 End Enum
活性化関数の計算はマシンラーニング演算用モジュール【ML】に実装します。
なので、以下のように実装しています。
標準モジュール【ML】
'活性化関数 ReLU '[引数] <- aU : Double / ユニットの u '[戻り値] -> actReLU : Double / ReLU適用後の値 Public Function actReLU(ByRef aU As Double) As Double actReLU = WorksheetFunction.Max(aU, 0) End Function
【classHiddenLayer】に戻って、いま実装した「actReLU()」を呼び出す処理を書きましょう。
クラスモジュール【classHiddenLayer】
'ユニットの u に活性化関数を適用する Private Sub activateU() If mAct = ACT.ReLU Then Call activationReLU End If End Sub 'ユニットの u にReLUを適用する Private Sub activationReLU() Dim i As Long For i = 1 To mUnitCount mUnitList(i).Z = ML.actReLU(mUnitList(i).U) Next End Sub
「activateU()」プロシージャでいったん受けたのち、その層の活性化関数によって処理を分けるようにしています。今回は ReLU だけなのでありがたみはありませんが、例えばシグモイド関数を実装したい場合「activationSigmoid()」のようなプロシージャを作成し「activateU()」に [ ElseIf mACT = ACT.Sigmoid Then ] を追加して呼び出すようにすることで、活性化関数を適用したい呼び出し元(今回は「Forward()」プロシージャ)が、活性化関数によって呼び出すプロシージャを変える必要がなくなります。
「activationReLU()」では、ユニットごとに先ほど実装した【ML】モジュールの「actReLU()」を呼び出して、ユニットの z の値(活性化関数適用後の値)を計算しています。
出力値リスト
各ユニットの z (最終出力値 = 活性化関数適用後の出力値)が求まったので、次の層に渡すためにリスト化(配列化)します。なお、次の層ではこの出力値のリストを入力値として受け取ります。
コードは各ユニットの z を戻り値用の配列に格納しているだけです。
'次の層に渡すための出力リスト(配列)を準備する '[戻り値] -> OutputDataList() : Double / 自層の各ユニットの出力値の配列 Public Function OutputDataList() As Double() Dim i As Long Dim outputDatas() As Double ReDim outputDatas(mUnitCount) For i = 1 To mUnitCount outputDatas(i) = mUnitList(i).z Next OutputDataList = outputDatas() End Function
さて、これで入力から出力までを求めることができました。
自層の出力値が次の層の入力値になるので「classHiddenLayer」を複数インスタンス化すれば、複数の隠れ層をつなげていくことができます。
次回は出力層の実装をしていく予定です。
【VBA編】(順伝播)ユニットの作成
ユニット
ニューラルネットワークの基本となる単位です。隠れ層、出力層のユニットは基本的には同じ機能を持つので、共通して使える「classUnit(ユニットクラス)」として定義します。まずは順伝播の機能を作ります。
各ユニットは次のような機能を持つように設計します。
【順伝播】
- 入力値に重みを掛けて足し合わせる
- 上記の値にバイアス値を足す
- (上記の値に活性化関数を適用する)←これはレイヤークラスに実装します
- 上記の値を出力する
【逆伝播】
- 重みとバイアスを更新する
重みとバイアス
各ユニットはそれぞれに重みとバイアスのパラメータを持ちます。重みは入力データの数と同じだけ必要です。また、学習が正しく行えるように、それぞれの重みは 0 以外のランダムな数値で初期化されている必要があります。(ただし、バイアスの初期値は 0 でかまいません。)
実装
以上を踏まえユニットクラスを実装しましょう。クラスとして定義することで、インスタンス化で必要なだけパラメータの異なる複製が作れるようになります。
重みの初期値に使うランダム値の生成
重みは 0 以外のランダムな数値で初期化する必要があるので、ランダム値を生成するプロシージャを作成します。ランダム値の範囲としては -1より大きく、1より小さい範囲としています。
コードは「ML」モジュールに記述しています。
標準モジュール【ML】
'[ML - マシンラーニング演算用モジュール] Option Explicit '-1 < num < 1 で0以外のランダム値を返す '[戻り値] -> getRandom() : Double / -1 より大きく 1 より小さいランダム値(0以外) Public Function getRandom() As Double Dim i As Long Dim num As Double Randomize Do num = (Rnd * 2) - 1 Loop Until num <> 0 getRandom = num End Function
ユニットの初期化
ユニットの持つパラメータ(重みとバイアス)を初期化します。ユニットを管理するレイヤークラス(classHiddenLayer, classOutputLayer)からそのユニットが持つ重みの個数を引数として受け取ります。そして、先ほど作成した「getRandom()」プロシージャを呼び出して、各重みをランダム値で初期化しています。
ここで、ユニットクラスで必要となるモジュールレベル変数も宣言しておきましょう。
「Option Base 1」の宣言も忘れずにしておいてください。
クラスモジュール【classUnit】
'[classUnit - ユニットクラス] Option Explicit Option Base 1 Dim mWeightList() As Double '重みのリスト Dim mBias As Double 'バイアス Dim mU As Double '活性化関数適用前の合計値 Dim mZ As Double '出力値 'パラメータ(重みとバイアス)を初期化する '[引数] <- aWeightCount : Long / 重みの数 Public Sub Initialize(ByRef aWeightCount As Long) Dim i As Long ReDim mWeightList(aWeightCount) '各重みをランダム値で初期化 For i = 1 To aWeightCount mWeightList(i) = ML.getRandom() Next 'バイアスを0で初期化 mBias = 0 End Sub
合計値の計算
入力データとパラメータをもとに計算した合計値(活性化関数適用前の値)を求めます。上の図ではユニットの「U」となっている値です。
引数としてレイヤークラスから入力値のリストをもらい、リストの各データに対応する重みを掛けて足し合わせています。
(いままでの)説明では、それで計算した値にバイアス値を加えるとしています(していました)が、合計値に後で足すのと結果は変わらないので、今回は「mU」の初期値としてバイアス値をセットしています。
また、レイヤークラスで計算結果を取得できるように Getプロパティ で計算結果「mU」を返すようにしています。
クラスモジュール【classUnit】
'[classUnit - ユニットクラス] '活性化関数適用前の合計値を計算する '入力値にそれぞれの重みを掛けてバイアス値を加える '[引数] <- aInputDataList() : Double / 入力値のリスト Public Sub CalcU(ByRef aInputDataList() As Double) Dim i As Long mU = mBias For i = 1 To UBound(aInputDataList) mU = mU + (mWeightList(i) * aInputDataList(i)) Next End Sub '活性化関数適用前の合計値を返す '[戻り値] -> U : Double / 活性化関数適用前の合計値 Public Property Get U() As Double U = mU End Property
出力値の計算
重みとパラメータから計算された値がわかったので、これに活性化関数を適用してユニットの出力値とします。上の図では「Z」となっている部分です。
ただし、(今回の設計では)活性化関数の適用計算はレイヤークラスが受け持つため、計算自体はレイヤークラスで行います。
ここでは、レイヤークラスから活性化関数適用後の値を受け取って次の層に渡すために、Let / Get プロパティとして実装します。
クラスモジュール【classUnit】
'[classUnit - ユニットクラス] '出力値を格納する '[引数] <- aActivatedU : Double / 活性化関数適用後の合計値 Public Property Let Z(ByRef aActivatedU As Double) mZ = aActivatedU End Property '出力値を返す '[戻り値] -> Z : Double / 出力値 Public Property Get Z() As Double Z = mZ End Property
ユニットクラスの実装はいったんここまでにして次はレイヤークラス(隠れ層クラス)を実装していきます。
逆伝播の処理については、逆伝播の実装で行っていきます。
【VBA編】入力層の作成
入力層の作成?
まず手始めに入力層のコードを書いていきます。
が、、実は入力層はデータを受け取って隠れ層1層めに渡しているだけなので、なくても大丈夫です。
もし、入力層の作成が面倒だったり、計算資源を無駄にしたくない>< 場合などは、入力層の作成は省略していただいて構いません。
入力層の作成
では、気を取り直して入力層を作成していきましょう。
入力層は層の管理として「classInputLayer」を、ユニットとして「classInputUnit」を使用します。
入力層は単純にデータを隠れ層1層めに渡しているだけで、重みやバイアスといったパラメータもないので、
他の層とは異なる設計にしています。
入力用ユニットクラス
入力用のユニットクラスです。
1つのユニットにつき、1つの入力しかありません。出力も1つです。
そのため、プロパティのみを定義しています。
'[classInputUnit - 入力用ユニットクラス] Option Explicit Dim mX As Double '入力データをセットする '[引数] <- inputData : Double / 入力データ Public Property Let X(ByRef aInputData As Double) mX = aInputData End Property '入力データを返す '[戻り値] -> X : Double / 入力データ Public Property Get X() As Double X = mX End Property
入力層クラス
入力用ユニットを管理する入力層のクラスです。
入力用ユニットのインスタンスを生成し、渡されてきた入力データを入力用ユニットにセットします。
また、隠れ層1層めに渡すために、入力用ユニットの出力を配列としてまとめます。
'[classInputLayer - 入力用ユニットを管理するためのクラス] Option Explicit Option Base 1 Dim mInUnitList() As classInputUnit 'ユニット格納用リスト Dim mInUnitCount As Long '入力層のユニット数 '入力層のユニットに入力値を格納する '[引数] <- aInputDataList() : Double / 入力値のリスト Public Sub DataInput(ByRef aInputDataList() As Double) Dim i As Long Dim cInUnit As classInputUnit mInUnitCount = UBound(aInputDataList) ReDim mInUnitList(mInUnitCount) For i = 1 To mInUnitCount Set mInUnitList(i) = New classInputUnit mInUnitList(i).X = aInputDataList(i) Next End Sub '入力層のユニットの出力値を得る '[戻り値] -> OutputDataList() : Double / 出力値のリスト Public Function OutputDataList() As Double() Dim i As Long Dim outputDatas() As Double ReDim outputDatas(mInUnitCount) For i = 1 To mInUnitCount outputDatas(i) = mInUnitList(i).X Next OutputDataList = outputDatas() End Function
(他の層でも)コードが書きやすいように、[ Option Base 1 ] で配列のインデックスを 「1 始まり」にしています。
「DataInput()」プロシージャでは、
入力データ数(配列のサイズ)を元に、入力用ユニットの個数を設定し、
[ mInUnitCount = UBound(aInputDataList) ] で入力用ユニットを格納する配列を作成しています。
作成した配列に入力用ユニットのインスタンスを格納していき、その際にプロパティ「 X 」に入力値をセットしています。
「OutputDataList()」プロシージャでは、
入力用ユニットの個数サイズの配列を作成し、各ユニットのプロパティ「 X 」の値を取得しています。
入力層については、重みやバイアスといった更新するべきパラメータもないので、これで完成です。
次回からはユニットについてみていこうと思います。
【VBA編】設計
前回まででデータの読み込み処理の作成が完了しました。
今回からは実際にニューラルネットワークを作るフェーズに移っていきます。
その前に、どのように実装するかを簡単に見ていきましょう。
以前の記事もあわせて参考にしてください。
celaeno42.hatenablog.com
作成するニューラルネットワークのイメージ
今回作成するニューラルネットワークは、入力層1層、隠れ層2層、出力層1層からなり、それぞれを「classInputLayer(入力層クラス)」「classHiddenLayer(隠れ層クラス)」「classOutputLayer(出力層クラス)」(のインスタンス)として実装します。
入力層では「がくの長さ」「がくの幅」「花弁の長さ」「花弁の幅」の4つの入力を受け取る必要があるため、「classInputUnit(入力ユニットクラス)」を4つ定義します。
隠れ層1層めと2層めのユニット数は任意ですが、ここではそれぞれ3つと4つにしており、それぞれを「classUnit(ユニットクラス)」で定義します。
出力層では「Iris-setosa」「Iris-versicolor」「Iris-virginica」の3種類に分類するので、ユニットを3つ、「classUnit(ユニットクラス)」で定義しています。
入力層のユニットと隠れ層、出力層のユニットの実装クラスが異なるのは、入力層では入力値をそのまま次の層(隠れ層)に渡すだけで、特に演算等が不要なためです。
また、全体を通しての制御などは標準モジュールに「mdlSupervisor」モジュールを作成して行います。
他に、活性化関数の演算などは標準モジュールに「ML」モジュール(マシンラーニング用モジュール)を作成して行います。
では、必要な準備を整えておきましょう。
モジュールとシートの追加
上記であげた標準モジュール、クラスモジュールを追加しましょう。
今回追加するモジュールは以下の通りです。
【標準モジュール】
オブジェクト名 | 用途 |
---|---|
mdlSupervisor | 全体の制御に使用 |
ML | マシンラーニングの演算に使用 |
【クラスモジュール】
オブジェクト名 | 用途など |
---|---|
classInputLayer | 入力層の管理に使用 |
classHiddenLayer | 隠れ層の管理に使用 |
classOutputLayer | 出力層の管理に使用 |
classInputUnit | 入力用ユニット |
classUnit | 隠れ層、出力層用ユニット |
また、今後必要となるシートもあわせて追加しておきましょう。
シートはオブジェクト名も変えておくのを忘れないでください。
今回追加するシートは以下の通りです。
オブジェクト名 | シート名 | 用途 |
---|---|---|
ws_Test_Result | テスト結果 | テスト結果の出力に使用 |
ws_Train_Result | 訓練結果 | 訓練結果の出力に使用 |
ws_W_H1 | w_h1 | 隠れ層1層めの重みの出力に使用 |
ws_W_H2 | w_h2 | 隠れ層2層めの重みの出力に使用 |
ws_W_Out | w_out | 出力層の重みの出力に使用 |
ws_WI_H1 | wi_h1 | 隠れ層1層めの重みの初期値の出力に使用 |
ws_WI_H2 | wi_h2 | 隠れ層2層めの重みの初期値の出力に使用 |
ws_WI_Out | wi_out | 出力層の重みの初期値の出力に使用 |
すべて追加するとこのようになります。
いよいよ次回からはニューラルネットワークのコードを書いていきます。
【VBA】VBAからPythonのコードを実行してみる(2)【Python】
VBAにないなら Python のを使えばいいじゃない…の続き
前回は VBA から Python のコードを呼び出して、セルの値を取得→加工→書き込むという処理をみてみました。
今回は os.walk を使ってファイル一覧を取得する処理を実装してみましょう。
まずはPythonだけで実行するコード
Python でファイル一覧を取得する処理を記述します。
まずは Python だけで実行できるコードを書いてみましょう。
【xl_getFolderFileList.py】
import os def getFolderFileList(path): for folder, subfolders, files in os.walk(path): for file in files: filepath = os.path.join(folder, file) print(filepath) getFolderFileList(r'C:\Users\xxxx\Desktop')
実行すると、デスクトップにあるファイルやサブフォルダのファイル一覧を表示します。
さて、これを VBA から実行するように書き換えてみましょう。
VBAから実行する Pythonコード
【xl_getFolderFileList.py】
import os import xlwings as xw def getFolderFileList_VBA(): path = xw.Range('A1').value row = 2 for folder, subfolders, files in os.walk(path): for file in files: filepath = os.path.join(folder, file) rng = 'A' + str(row) xw.Range(rng).value = filepath row += 1
まずは、xlwings をインポートして xw で参照できるようにしています。
また、[ path = xw.Range('A1').value ] で、A1セルからパスを取得するようにしています。
ファイルリストの表示は 2行目から行うので、row の初期値に 2 を設定し、os.walk の処理に入ります。
filepath が取得できたら、表示先セルを指定して filepath を表示し、次に備えて row をインクリメントします。
「Pythonだけで実行するコード」と見比べると、Excel のセルとのやり取り部分が増えていますが、基本的な部分は同じことがわかると思います。
VBA から Python を呼び出すコード
【VBA - 標準モジュール】
Public Sub getFolderFileList() Call RunPython("import xl_getFolderFileList; xl_getFolderFileList.getFolderFileList_VBA()") End Sub
書き方は前回のコードと同様です。
ファイル名と呼び出すプロシージャ名が変わったくらいですね。
では、A1セルに対象フォルダのパスを記述して実行してみましょう。
今回はデスクトップに作った「無限不可能性ドライブ」フォルダを対象にしてみました。
実行結果はこのようになります。
サブフォルダまで検索したファイル一覧が作成されました。
さて、今回のサンプルでは、対象となるフォルダはA1セルに設定されたパスのフォルダのみです。
これを例えば、A列にはA1セルに設定されたパスのフォルダ、B列にはB1セルに設定されたパスのフォルダ…
というようにするにはどうしたらよいでしょう。
引数を受け取る Pythonコード
今回は引数として Excel の列番号(col)を受け取るようにしてみました。
前回のコードとの違いは Range() のセルの指定部分だけです。
今回は行番号と列番号で指定しています。
かっこが2重になっている(タプルになっている)ことに注意してください。
【xl_getFolderFileList.py】
import os import xlwings as xw def getFolderFileList_VBA2(col): path = xw.Range((1, col)).value row = 2 for folder, subfolders, files in os.walk(path): for file in files: filepath = os.path.join(folder, file) xw.Range((row, col)).value = filepath row += 1
では、VBAから呼び出してみましょう。
VBA から Python を呼び出すコード(引数あり)
【VBA - 標準モジュール】
Public Sub getList() Call getFolderFileList2(1) Call getFolderFileList2(2) End Sub Private Sub getFolderFileList2(ByRef col As Long) Call RunPython("import xl_getFolderFileList; xl_getFolderFileList.getFolderFileList_VBA2(" & col & ")") End Sub
引数の指定部分は、まぁ、よくある文字列との連結の書き方ですね。
では、A1セル、B1セルに対象フォルダのパスを記述し、「getList()」プロシージャを実行してみましょう。
A1セルには先ほどと同じフォルダ、B1セルには「Documents」フォルダ内の「vba」フォルダを指定しています。
実行結果はこのようになりました。
A列とB列でファイルの一覧が取得されています。
まとめ
サブフォルダ内も含むファイル一覧の取得も、Python の関数を使うことで比較的簡単に実現できたのではないでしょうか。
他にも Python でやったほうが簡単な処理があると思いますので、VBAだとちょっと…という場合には、このような方法も選択肢としてはありかなと思います。
ただし…わざわざ Python を呼び出しているためか、VBAのみで記述するよりは遅いのでまぁ、それを許容できるかというのもあるかもしれませんね。
おまけ
Excelとの直接の連携を考えないなら単純にこのような方法もあります。
コマンドプロンプトから [ >python ファイル名.py ] で実行するのと同じなので、
xlwings や pywin32 は不要です。
Pythonのコードはメッセージボックスを表示するだけの簡単なものです。
【helloworld.py】
from tkinter import messagebox messagebox.showinfo('vba2py', 'Hello World! from VBA')
【VBA - 標準モジュール】
Option Explicit Public Sub pythonコードを実行() Dim obj As Object Dim pyPath As String Set obj = CreateObject("WScript.Shell") 'Pythonファイルのパスはフルパスを設定するようにしたほうがいいかも pyPath = "C:\Users\xxxx\xxxxxxxx\helloworld.py" Call obj.Run("Python " & pyPath, WaitOnReturn:=True) Set obj = Nothing End Sub
【実行結果】
※※ ノンプロ研アドベントカレンダー7日目の記事です ※※
adventar.org
【VBA】VBAからPythonのコードを実行してみる(1)【Python】
VBAにないなら Python のを使えばいいじゃない
Excel で VBAを書いていて「この処理 Python で書けば楽なのに…」とか思うことってないでしょうか。
例えば、
サブフォルダを含めてファイルの一覧を取得したいけどVBAでどう書いたっけ?
Python なら os.walk で簡単に書けるのになぁ…VBAにも os.walk みたいな関数があればいいのに。。
とか。
今回はそんなときのために Excel VBA から Python のコードを実行する方法についてみていきたいと思います。
(Windows環境でPythonの環境構築が終わっている前提です。)
(準備1)xlwings と pywin32 のインストール
まずは必要なものをインストールします。
xlwings
コマンドプロンプトを起動してpip インストールでインストールしましょう。
pip install xlwings
Pythonの対話モードなどでインストールの確認をします。
>>> import xlwings
エラーにならなければOKです。
pywin32
こちらも同様に。
pip install pywin32
インストールの確認(pywin32 でなく win32 です)
>>> import win32
ちなみに、Anaconda の入っている私の環境では両方ともインストール済みでした。
(準備2)xlwings.bas ファイルを探して Excel VBA にインポートする
xlwings のインストールが正常に完了していれば、xlwings.bas ファイルがどこかにあるはずです。
Windows の検索機能を利用するなどしてファイルの場所を特定しましょう。
なお、私の環境では以下のフォルダにありました。
C:\Users\xxxx\Anaconda3\pkgs\xlwings-0.10.2-py36_0\Lib\site-packages\xlwings
ファイルのありかがわかったら、Excel VBA にインポートします。
VBE を開いて「ファイルのインポート」で xlwings.bas をインポートしてください。
インポートすると標準モジュールに「xlwings」というモジュールが読み込まれます。
これでひととおりの準備が完了です。
ここまでできたら、ファイルをいったん保存しておきましょう。
今回は「xl2py.xlsm」というファイル名にしてみました。
ついでに、今後のコードを書くための標準モジュールも追加しておきましょう。
(お試し)簡単なコードで動作確認
まずは簡単なコードで動作の確認をしてみましょう。
A1セルのテキストをコピーしてA3セルに貼り付ける処理を作ってみます。
貼り付ける前に若干の加工もしてみましょう。
(お試し)Pythonコードの準備
VBAから呼び出す Python のコードを準備しましょう。
ファイル名は「copy_text.py」とし、先ほど作成した Excel ファイル(xl2py.xlsm)と同じフォルダに作成します。
【copy_text.py】
import xlwings as xw def copyAddText(): txt = xw.Range('A1').value txt += ', I am the Doctor.' xw.Range('B3').value = txt
xlwings を import し xw でアクセスできるようにしています。
copyAddText 関数の内容はそれほど説明することはないかと思います。
txt = xw.Range('A1').value で A1 セルの値を変数 txt に格納しています。
その後加工して B3 セルに格納しなおしています。
(お試し)VBAからの呼び出し
上記で作成した Pyhon コードを VBA から呼び出す処理を記述します。
今回は標準モジュールにコードを書いています。
【VBA - 標準モジュール】
Option Explicit Public Sub copyText() Call RunPython("import copy_text; copy_text.copyAddText()") End Sub
RunPythonプロシージャ の引数に呼び出すPythonファイルと関数の情報を渡します。
引数の書き方は、[ "import pythonファイル名; pythonファイル名.関数名()" ] というような感じになっています。
拡張子の .py は不要です。
ちなみに RunPython プロシージャは先ほど Excel にインポートした xlwingsモジュール内に記述されています。
(お試し)VBAの実行
では、VBAを実行してみましょう。
Python のコードでは、A1 セルの値を取得していたので、実行前に A1 セルに「Hello」と入力しておきます。
VBAの「copyText()」を実行すると…
このようになります。
無事 Python のコードが実行されて、その結果が Excel のセルに反映されました!
次回はいよいよ「Pythonコードでのファイル一覧取得」についてみていきます。
※※ 私が10月から参加させていただいているノンプロ研のアドベントカレンダー3日目の記事です。 ※※
※※ ノンプロ研はこの12月で開始からちょうど1年だそうです。おめでたいですね! ※※
adventar.org
【VBA編】データの標準化
前回はそれぞれのシートにCSVデータを読み込む処理を作成しました。
今回は読み込んだデータを標準化する処理を書いていきます。
標準化
「標準化」とは、データを「平均が0、分散が1になるように変換する」ことです。
標準化を行うことで、学習の効率がよくなるそうなので、今回使用するデータについても標準化することにします。
グラフで見るとこんな感じ。
がくの長さを x 軸、がくの幅を y 軸にとってデータをプロットしています。
青い点が標準化前のデータ(元のデータ)、オレンジの点が標準化後のデータです。
標準化後のデータは原点 O の周辺に集まっているのがわかると思います。
標準化の計算
標準化は個々のデータについておこなっていくのですが、Excelの関数では、
=STANDARDIZE(x, 平均, 標準偏差)
を使って求めることができます。
なお、引数の x は 元の値ですが、
平均は「=AVERAGE(データ)」
標準偏差は「=STDEV.P(データ)」
でそれぞれ求めることができます。
標準化後の値を z とすると、数式では以下のようにあらわされます。
(今回のコードではワークシート関数を使うのでこの式は利用しません。)
では、標準化のコードを書いていきましょう。
必要なシートの追加
訓練データとテストデータそれぞれの標準化後のデータを格納するシートを追加します。
標準化後のデータを入力データとして利用するので、それぞれのシート名には「(入力)」とつけています。
オブジェクト名はそれぞれ「ws_Train_Data_Input」、「ws_Test_Data_Input」としました。
定数の宣言
標準モジュール「G」に以下の列挙型の定数を宣言しておきます。
「ラベル列」というのは、データの正解(値)が格納されている列です。
今回であれば「アヤメの種類」が格納されている列です。
'[G - 標準モジュール] Public Enum RW DATA_TITLE = 1 'データのタイトル行 DATA_START = 2 'データの開始行 End Enum Public Enum CL DATA_LABEL = 1 'データのラベル列 DATA_START = 2 'データの開始列 End Enum
標準化のコード
「mdlReadData」に以下のコードを書きましょう。
引数として、元データのあるシート、書き込み先のシートを取っています。
元データのあるシートは、「ws_Train_Data」もしくは「ws_Test_Data」
書き込み先のシートは、「ws_Train_Data_Input」もしくは「ws_Test_Data_Input」
が呼び出し元でセットされます。
'[mdlReadData - 標準モジュール] 'ワークシートのデータを標準化して書き込み先のワークシートに書き込む '[引数] <- aWsData : Worksheet / 元データがあるシート, aWsStandardized : Worksheet / 標準化したデータを書き込む先のシート '[戻り値] -> なし Private Sub standardizeData(ByRef aWsData As Worksheet, ByRef aWsStandardized As Worksheet) Dim r As Long Dim c As Long Dim i As Long Dim eRow As Long Dim eCol As Long Dim mean As Double '平均値 Dim std As Double '標準偏差 aWsStandardized.Cells.Clear With aWsData '最終行と最終列を取得する eRow = .Cells(Rows.Count, 1).End(xlUp).Row eCol = .Cells(1, Columns.Count).End(xlToLeft).Column '列ごとに処理 For c = CL.DATA_START To eCol '平均値と標準偏差を求める mean = WorksheetFunction.Average(.Range(.Cells(RW.DATA_START, c), .Cells(eRow, c)).Value) std = WorksheetFunction.StDev_P(.Range(.Cells(RW.DATA_START, c), .Cells(eRow, c)).Value) '標準化を行う For r = RW.DATA_START To eRow aWsStandardized.Cells(r, c).Value = WorksheetFunction.Standardize(CDbl(.Cells(r, c).Value), mean, std) Next Next 'タイトル行とラベル列をコピー貼り付け .Rows(RW.DATA_TITLE).Copy Destination:=aWsStandardized.Rows(RW.DATA_TITLE) .Columns(CL.DATA_LABEL).Copy Destination:=aWsStandardized.Columns(CL.DATA_LABEL) End With End Sub
1列目はラベルなので、2列目から処理を開始します。(CL.DATA_START は 2です。)
列ごとにワークシート関数で平均「mean」と標準偏差「std」を求めています。
平均と標準偏差が求まったら、元データの値を「=STANDARDIZE(x, 平均, 標準偏差)」のワークシート関数を使って標準化し、標準化後の値を書き込み先のシートに書き込みます。
すべての列について上記処理を行い、最後にタイトル行とラベル列を書き込み先シートにコピーして終了です。
標準化処理の呼び出し
では、上記の標準化処理を呼び出してデータを標準化しましょう。
前回作成した「Click_データ読み込み()」プロシージャにコードを追加します。
追加するのは、
[ Call standardizeData(ws_Train_Data, ws_Train_Data_Input) ]
[ Call standardizeData(ws_Test_Data, ws_Test_Data_Input) ]
の2行です。
それ以外の変更点はありません(コメントを除く)。
'[mdlReadData - 標準モジュール] 'データの読み込み Public Sub Click_データ読み込み() Dim filePath As String Application.ScreenUpdating = False '訓練データの読み込み filePath = Range(G.RNG_TRAIN_DATA_PATH).Value Call readData(ws_Train_Data, filePath) '訓練データを標準化する Call standardizeData(ws_Train_Data, ws_Train_Data_Input) 'テストデータの読み込み filePath = Range(G.RNG_TEST_DATA_PATH).Value Call readData(ws_Test_Data, filePath) 'テストデータを標準化する Call standardizeData(ws_Test_Data, ws_Test_Data_Input) Application.ScreenUpdating = True MsgBox "データを読み込みました。", vbOKOnly + vbInformation End Sub
処理の確認
では、メインシートの「データ読み込み」ボタンをクリックしてデータを読み込んでみましょう。
「訓練データ(入力)」シート、「テストデータ(入力)」シートにそれぞれ標準化後の値が書き込まれていると思います。
【訓練データ(入力)】シート(ws_Train_Data_Input)
【テストデータ(入力)】シート(ws_Test_Data_Input)