Geschrieben von: Christoph Rüegg
Mit der System.Math Klasse deckt das .NET Framework mit gut zwei dutzend Funktionen die wichtigsten numerischen Basisoperationen ab. Bei mathematiklastigen Anwendungen stösst man damit aber schnell an Grenzen, insbesondere wenn der Benutzer die mathematischen Terme zur Laufzeit an neue Anforderungen anpassen können muss, oder wenn sich das ganze auf einer symbolischen Ebene abspielt, also Variablen vorkommen. Diese Lücke kann mit der Math.NET Library weitgehend überbrückt werden - diese ist dank der modifizierten BSD Lizenz auch für kommerzielle Anwendungen gratis und kann mit vollständigem C# Quelltext von der Math.NET Projekt Website heruntergeladen werden.
In vielen Anwendungen werden Werte mit Formeln berechnet, die nicht unbedingt für immer in der gleichen Form bleiben müssen. So entscheidet sich vielleicht ein Web Designer, der bisher die Preise für seine Arbeit im Quadrat zum Zeitaufwand berechnete, nach einiger Zeit zu einem Preis, der nur noch linear oder sogar logarithmisch zum Zeitaufwand berechnet wird, um auch sehr grosse Aufträge noch einigermassen bezahlbar zu machen. Es wäre für ihn also interessant, wenn er die Formel zur Preisberechnung zu Laufzeit ändern könnte, idealerweise direkt in einem Textfeld im Einstellungsdialog. Ein anderes ähnliches Beispiel ist die Aktivierungsfunktion bei klassischen neuronalen Netzwerken, die zwecks Optimierung des Lernverhaltens durchaus öfter verändert wird.
Bei beiden angesprochenen Beispielen wird einerseits ein Parser benötigt, um aus dem String einen auswertbaren Ausdrucksbaum zu evaluieren, und andererseits ein breites Fundament an Operatoren, um auch komplexere Probleme handhaben zu können. Beim zweiten Beispiel verschärft sich das Problem noch weiter, da für die Backpropagation (ein klassischer Algorithmus, um ein neuronales Netzwerk zu lehren) neben der Aktivierungsfunktion auch deren erste Ableitung benötigt wird.
Beide Beispiele können mit Math.NET mit relativ wenig Code gelöst werden:
using cdrnet.Lib.MathLib.Core;
using cdrnet.Lib.MathLib.Scalar;
using cdrnet.Lib.MathLib.Parsing;
...
string ausdruck = "ln(x^2)/x";
Parser p = new Parser();
p.Provider = (ITreeTokenProvider) new InfixTokenizer();
IScalarExpression exp = p.Parse(ausdruck)
as IScalarExpression;
ScalarExpressionVariable x =
Variable.Variables.GetCreateVariable("x")
as ScalarExpressionVariable;
if(exp == null || x == null)
Console.WriteLine("Kein skalarer Ausdruck!");
else
{
x.Value = new ScalarExpressionValue(10);
double ergebnis = exp.Calculate()
Console.WriteLine("Ergebnis an der Stelle 10: "
+ergebnis.ToString());
IScalarExpression ableitung = exp.Differentiate(x);
Console.WriteLine("Ableitungsfunktion: "
+ableitung.StringExpression);
Console.WriteLine("Ergebnis an der Stelle 10: "
+ableitung.Calculate().ToString());
}
Das Parser Subsystem von Math.NET kann mit verschiedenen Formatierungen umgehen. Die wohl wichtigste Darstellung ist die Infix Notation, die dank impliziten Klammern durch Prioritäten relativ kompakt und gut lesbar ist (Infix: 'a*b+c*d' = '(a*b)+(c*d)' und entspricht Postfix: 'a b * c d * +'). Wir müssen dem Parser mitteilen welche Notation geparst werden soll, indem wir ihm eine Instanz eines Infix Tokenizers übergeben, der die Infix Notation in eine allgemeinere Darstellung vorkonvertiert.
p.Provider = (ITreeTokenProvider) new InfixTokenizer();
Das eigentliche Parsen erfolgt mit dem Aufrufen der Parse() Methode, der ein Ausdruck als String übergeben wird. Parse liefert eine IExpression Instanz zurück. Da wir die Calculate() Methode verwenden möchten, die nur für skalare Ausdrücke Sinn macht und entsprechend auch nur dort definiert ist, müssen wir diese Instanz erst in den Typ IScalarExpression konvertieren:
IScalarExpression exp = p.Parse(ausdruck)
as IScalarExpression;
Der Parameter unserer benutzerdefinierten Funktion heisst 'x'. Um x einen Wert zuweisen zu können, müssen wir an deren Instanz herankommen. Die VariableManager Klasse stellt dazu die Methode GetCreateVariable() zur Verfügung. Der Wert einer Variablen kann mit der Value Eigenschaft beliebig geändert werden.
Hinweis: Math.NET arbeitet nicht mit diskreten Typen wie Integer oder Double, sondern (hier) mit abstrakteren 'Skalaren Ausdrücken'. Soll x der Wert 10 zugewiesen werden, so muss dieser mit der ScalarExpressionValue Klasse gekapselt werden. Anstelle des Wertes 10 könnte man x somit auch einen anderen, beliebig komplizierten skalaren Ausdruck zuweisen, der wiederum von anderen Variablen abhängig sein kann.
ScalarExpressionVariable x =
Variable.Variables.GetCreateVariable("x")
as ScalarExpressionVariable;
x.Value = new ScalarExpressionValue(10);
Die Ableitung eines skalaren Ausdrucks kann am einfachsten mit Hilfe der Methode Differentiate() berechnet werden, der als Parameter die Variable übergeben wird, nach der partiell abgeleitet werden soll:
IScalarExpression ableitung = exp.Differentiate(x);
Der Parser bietet zwar einen einfachen und bequemen Weg um Math.NET Ausdrücke automatisch zusammenzusetzen, doch man kann sie auch von Hand bauen. Ausgangspunkt sind dabei jeweils existierende Ausdrücke, die zu neuen Ausdrücken kombiniert werden können. Wird als weitere Funktion beispielsweise der Quotient der Originalfunktion im Quadrat und der Ableitung benötigt, so erhält man den wie folgt:
IScalarExpression term2;
term2 = new ScalarRaiseToPower(exp,
new ScalarExpressionValue(2));
term2 = new ScalarDivision(term2,ableitung);
Die Ausgangsterme werden dem Konstruktor der neuen Operatoren übergeben. Während die zweite Zeile den Ausdruck des Quadrates der Originalfunktion baut, so wird dieser bei der dritten Zeile durch die Ableitung dividiert. Interessant wird das ganze bei komplexeren Operatoren wie d, map, reduce, jet oder sim. Eine Liste der verfügbaren Operatoren kann mit dem beiliegenden SymbolTableViewer generiert werden.
Math.NET vereinfacht Terme von sich aus nur in einigen Ausnahmefällen, bietet aber entsprechende Funktionalität in zwei Verfahren an. Das erste ist effizient, vereinfacht jedoch nur sehr grundlegend. Das zweite basiert auf dem musterorientierten Term Conversion Subsystem und kann auch kompliziertere Terme vereinfachen. Das beste Resultat erhält man durch die Kombination beider Verfahren:
term2 = term2.BasicSimplify(); ScalarConversionMap.Convert(ref term2, "simple");
Wir haben gesehen, wie man Ausdrücke mit oder ohne Parser generieren, zu neuen Ausdrücken kombinieren und vereinfachen kann. Diese ganze formale Übung bringt in den meisten Fällen aber wenig (ausser vielleicht in Anwendungen im Ingenieur- oder Naturwissenschaftsbereich), wenn man nicht wieder auf die Ebene der diskreten Werte wie Integers oder Strings hinabsteigen kann. Jeder Ausdruck kann mit der StringExpression Eigenschaft einen entsprechenden String in Infix Notation generieren, womit der Kreis zum Parser geschlossen wäre. Des Weiteren definieren die meisten Typen spezifische Methoden für eine 'Berechnung' in einen diskreten Wert, die zumeist Calculate() genannt wird. Bei skalaren Ausdrücken liefert diese Methode einen double, bei komplexen Zahlen, Vektoren, Matrizen, Bits etc. entsprechende Strukturen aus diskreten Werten:
string infix = term2.StringExpression; double val = term2.Calculate(); IBitExpression ebit = DigitalBit.One; ebit = new BitXnor(ebit,new BitNot(ebit)); bool bval = ebit.Calculate();
Neben skalaren Termen kann Math.NET auch mit weiteren Typen umgehen, so beispielsweise mit IComplexExpression für komplexe Zahlen, IVectorExpression für Vektoren, IComplexMatrixExpression für Matrizen aus komplexen Zahlen, etc. Der oben benutzte Typ IBitExpression steht zum Beispiel für ein Bit im Kontext der Digitaltechnik. Der SymbolTableViewer listet neben den Operatoren auch alle verfügbaren Typen auf.
Mit Math.NET kann eine Anwendung mit relativ wenig Aufwand mit mathematischen Fähigkeiten aufgewertet werden, wie mit benutzerdefinierten Funktionen, einem Graph Plotter, formalen Simulationen oder auch einfach einem leistungsstarken symbolischen Taschenrechner. Bei rein numerischen Problemen lohnt sich auch der Blick auf weitere freie Projekte, wie Mapack, dotNum oder Exocortex.DSP.
Math.NET Projekt Website (http://www.cdrnet.ch/projects/nmath/)
Mapack (http://www.aisto.com/roeder/dotnet/)
dotNum (http://www.jens-thiel.de/dotnet/dotnum)
Exocortex.DSP (http://www.exocortex.org/dsp/)