BayesServerをプログラムから使っていて、ちょっとこのデータはスパースだからスパースデータ形式で読み取りたいというあります。
標準の方法
標準のAPIではデータベースに対するDataReaderである
BayesServer.Data.DatabaseDataReaderCommand
クラスがスパース列をサポートするようなので、DBを介せば大丈夫のようです。
https://docs.microsoft.com/ja-jp/sql/relational-databases/tables/use-sparse-columns?view=sql-server-2017
Sparse対応のEvidence Readerを実装
IEvidenceReaderというインターフェイスでカスタムの事例リーダーの例がサンプルにあります。
Custom evidence reader (Json) C#
これはJSON形式のデータを読むためのものですが、IEvidenceReaderは
public bool Read(IEvidence evidence, IReadOptions readOptions)
を実装しさえすれば良いので、中はいかようにでもなります。このReadで事例の属性を要素にもつevidenceにデータをセットすれば良いわけです。
このサンプルの場合ReadはJSONデータを受け取ってEvidenceにマップするという以下の関数(Action)を呼んでいるだけです。
Action<IEvidence, T> setEvidence
この関数はEvidenceReaderのコンストラクタで、指定されていますが、 JsonEvidenceMapper というクラスに実装されています。中身はJSONからデコードされてオブジェクトにマッピングされたデータから、Evidenceへの代入が行われています。
public void SetEvidence(IEvidence evidence, Person person)
{
evidence.SetState(this.hairLength.States[person.HairLength, true]);
evidence.Set(this.height, person.Height);
}
JSONですとオブジェクトへのマッピングから、行への展開まで一筋縄ではいけないので、このようなややまどろっこしい実装になっているのか知りませんが、真似するには少々しんどい。
やりたいのは、他のサンプルのように一度メモリに読み込まれたDataTable(通常は別途CSVから読み込んでいます)のあるカラムにスパースな列名の列挙(ある/なしのデータboolean)を入れているという仮定です。
sparseVarialbeリストに該当する列に達した場合は、要素を再パースして、分割して、それぞれを列として認識させるということをやっています。(ここでは;が内部の区切り記号)
Booleanに限定しており、カラムの番号などを指定する必要がないのですが、そうすると、出来上がりの(密な)データフレームの列構成がわかりません。これは、結局BNのノード以上のカラムが来ないという仮定のもとに、一度ノードのリストであるVariableReferencesというリストを仮定しています。
そうすれば、事前に集計して、カラムを構成したりする必要がありません。(よって、最初のネットワーク構築の時などはこの技は使えない)
public bool Read(IEvidence evidence, IReadOptions readOptions)
{
if (this.disposed == DisposedTrue)
{
throw new ObjectDisposedException(this.GetType().Name);
}
var result = this.reader.Read();
if (result)
{
foreach (var vr in this.variables)
{
if ( vr.Variable.StateValueType == BayesServer.StateValueType.Boolean)
{
// reset all evidence to false
evidence.SetState(vr.Variable.FindStateByValue(false));
}
}
for (int i = 0; i < this.reader.FieldCount;i++)
{
var name = this.reader.GetName(i);
var value = this.reader.GetValue(i);
var fieldtype = this.reader.GetFieldType(i);
if(this.sparseVariables.Contains(name) )
{
// reparse sparse column
var varNames = ((string)value).Split(';');
foreach(var v in varNames)
{
var vr = this.variables.Single(s => s.Variable.Name == v);
// only supports boolean variable
setEvidence(evidence, vr, v, true, typeof(bool));
}
}
else
{
// search variable
var vr = this.variables.Single(s => s.Variable.Name == name);
setEvidence(evidence, vr, name, value, fieldtype);
}
}
}
return result;
}
setEvidenceはどうするの
setEvidenceですが、おそらくBayesServer内部にはこのようなユーティリティ関数があるだろうということで、想像で書いてみたいのですが、やはり条件分岐などを完全再現とはいきません。ノードタイプ、あるいは入力のカラムの型などにより、場合分けをするのですが、GUIの構成などを見ながら、この場合変数型の場合はこの値しか無い、という関係を読み取って、書いてみました(この辺はいろいろ勉強になる)。
開発元にも、こういう実装でいいのか、もしよかったらsetEvidence関数を教えてくれ、と言ってみたのですが、回答として、「同じことは、EvidenceReaderいじらなくてもDataReaderの改造で十分出来るよ。」と帰ってきました。そのサンプルもくれヨとも思いましたが、あくまで標準の.NETのしきたりで書けばよいので、それを聞くのはお門違いかと思い、自作しました。
void setEvidence(IEvidence evidence, VariableReference vr, string name,object value,Type type)
{
switch (vr.ColumnValueType)
{
case ColumnValueType.Name:
evidence.SetState(vr.Variable.States[value.ToString()]);
break;
case ColumnValueType.Value:
switch (vr.Variable.ValueType)
{
case BayesServer.VariableValueType.Discrete:
switch (vr.Variable.StateValueType)
{
case BayesServer.StateValueType.None:
var str = value.ToString();
evidence.SetState(vr.Variable.States[str]);
break;
case BayesServer.StateValueType.Integer:
var ivalue = Convert.ToInt32(value);
evidence.SetState(vr.Variable.FindStateByValue(ivalue));
break;
case BayesServer.StateValueType.Boolean:
var b = Convert.ToBoolean(value);
evidence.SetState(vr.Variable.FindStateByValue(b));
break;
case BayesServer.StateValueType.DoubleInterval:
// not supported
break;
}
break;
case BayesServer.VariableValueType.Continuous:
var d = System.Convert.ToDouble(value);
evidence.Set(vr.Variable, d);
break;
}
break;
}
}
カスタムIDataReaderを実装する
考え方としては、標準の(普通に列が来ると思っている)IEvidenceReaderに対して行のデータにアクセスさせてあげれば良いだけです。
DataTableを入力として、DataTableDataReaderの派生のような形で実装しました。
Readに関しては、すでにデータテーブルにパース済みの行データをイテレートするだけです。currentRowに現在のRead対象の行が格納されます。
public bool Read()
{
try
{
if (current >= dt.Rows.Count)
{
return false;
}
currentRow = dt.Rows[current];
current++;
}
catch(Exception e)
{
return false;
}
return true;
}
元がDataTableなので、スパース行はすでにcurrentRowとして読み込まれており、そのうちsparseVariablesにある列を再展開するのはさっきと同じ。スパース列の文字列リストにその指定した列名が存在すればその列はtrueということにしています、
スパース列内の要素が少ない場合は、これでもそこまで性能劣化は無いと思われます。
public object this[string name]
{
get
{
if (dt.Columns.Contains(name))
{
return currentRow[name];
}
else
{
foreach (var v in sparseVariables)
{
var elems = currentRow[v].ToString().Split(';');
if (elems.Contains(name))
{
return true;
}
}
return false;
}
return null;
}
}
IDataReaderの派生で実装した方が、カスタムの!EvidenceReaderの実装をしなくてよい、またその結果IEvidenceReaderと組み合わせて使うオプションが使いまわせるなどがありますので、この程度の特殊な事例はIDataReader側で吸収するのが良いようですね。