386. Symbolic Differentiation

בהמשך לפעם הקודמת, הפעם נראה דרך לגזור פונקציה בצורה סימבולית.

תזכורת: ההבדל ממה שראינו בפעם הקודמת, הוא שאנחנו נקבל בתור Input איזשהו ביטוי שמתאר את הפונקציה שלנו, ונחזיר ביטוי שמתאר את הנגזרת שלה.

איך נעשה את זה? הכרנו בעבר Expression Trees (טיפ מספר 173-175).

נשתמש בהם בשביל לייצג את הפונקציות והנגזרות שלהן.

המימוש הוא יחסית פשוט, זה נראה משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public static Expression Differentiate<T>(Expression<T> expression)
{
return InnerDifferentiate(expression.Body);
}
private static Expression InnerDifferentiate(Expression expression)
{
BinaryExpression binaryExpression =
expression as BinaryExpression;
switch (expression.NodeType)
{
case ExpressionType.Constant:
return Expression.Constant(0.0);
case ExpressionType.Parameter:
return Expression.Constant(1.0);
case ExpressionType.Call:
return DifferentiateUnary(expression);
case ExpressionType.Add:
return Expression.Add(InnerDifferentiate(binaryExpression.Left),
InnerDifferentiate(binaryExpression.Right));
case ExpressionType.Subtract:
return Expression.Subtract(InnerDifferentiate(binaryExpression.Left),
InnerDifferentiate(binaryExpression.Right));
case ExpressionType.Multiply:
return Expression.Add(
Expression.Multiply(InnerDifferentiate(binaryExpression.Left),
binaryExpression.Right),
Expression.Multiply(InnerDifferentiate(binaryExpression.Right),
binaryExpression.Left));
case ExpressionType.Divide:
return Expression.Divide(
Expression.Subtract(
Expression.Multiply(InnerDifferentiate(binaryExpression.Left),
binaryExpression.Right),
Expression.Multiply(InnerDifferentiate(binaryExpression.Right),
binaryExpression.Left)),
Expression.Multiply(binaryExpression.Right,
binaryExpression.Right));
}
return null;
}
private static Expression DifferentiateUnary(Expression expression)
{
MethodCallExpression methodCall = expression as MethodCallExpression;
Expression argument = methodCall.Arguments[0];
return Expression.Multiply(GetDerivative(methodCall.Method, argument),
InnerDifferentiate(argument));
}
private static Expression GetDerivative(MethodInfo function, Expression argument)
{
if (function.DeclaringType == typeof(Math))
{
switch (function.Name)
{
case "Sin":
return Expression.Call(GetMethod(() => Math.Cos(default(double))),
argument);
case "Cos":
return Expression.Negate
(Expression.Call(GetMethod(() => Math.Sin(default(double))),
argument));
case "Exp":
return (Expression.Call(GetMethod(() => Math.Exp(default(double))),
argument));
case "Log":
return Expression.Divide(Expression.Constant(1),
argument);
}
}
return null;
}
private static MethodInfo GetMethod<T>(Expression<Func<T>> expression)
{
return (expression.Body as MethodCallExpression).Method;
}

קצת הסבר: הפונקציה InnerDifferentiate יודעת לגזור פונקציות לפי חוקי הגזירה:

  • $ (u + v)’ = u’ + v’ $
  • $ (u - v)’ = u’ - v’ $
  • $ (u \cdot v)’ = u’v + v’u $
  • $ \displaystyle{(\frac{u}{v})’ = \frac{u’v - v’u}{v^2}} $

בנוסף: היא מחזירה 0 במידה והיא מקבלת קבוע, ומחזירה 1 במידה והיא מקבלת פרמטר. (הפונקציה כרגע מניחה שקיים פרמטר יחיד, לפיה היא גוזרת)

הפונקציה DifferentiateUnary יודעת לגזור קריאה לפונקציה פשוטה (למשל $ \sin $, $ \cos $, $ \exp $) עפ"י כלל השרשרת

$ (f(g(x)))’ = f’(g(x)) \cdot g’(x) $

שימו לב שהפונקציה GetDerivative משיגה MethodInfo על ידי טריק שראינו בעבר (ראו גם טיפ מספר 176)

כעקרון המימוש יחסית פשוט, ולא עושה הרבה דברים שיכל לעשות, למשל לפשט את הנגזרות, להתמודד עם מינוס לביטוי, להתמודד עם בסיסים אחרים של הלוגריתם וכו’.

איך משתמשים בזה? ככה:

1
2
3
4
Expression<Func<double, double>> expression =
x => Math.Sin(x) / (x * x + 1);
Expression derivative = Differentiate(expression);

זה מחזיר משהו כזה:

((((Cos(x) 1) ((x x) + 1)) - ((((1 x) + (1 x)) + 0) Sin(x))) / (((x x) + 1) ((x * x) + 1)))

שאחרי כמה פישוטים אפשר לראות שזה זהה למה שקיבלנו פעם שעברה.

אז למה זה יותר טוב?

קודם כל, אנחנו מקבלים הפעם ביטוי מתמטי ולא סתם פונקציה אמורפית, כך שאנחנו יכולים לעשות על ביטוי זה אנליזה מתמטית טהורה (לצורך העניין, אם למשל קיבלנו בשיעורי בית איזשהו ביטוי מתמטי ואנחנו צריכים לגזור אותו, אפשר בתיאוריה להשתמש בקוד כזה כדי לעשות את זה)

בנוסף, יותר קל לגזור את הביטוי מספר פעמים, במידה וזו מטרתנו (בפעם הקודמת התקשינו לגזור את הפונקציה יותר מפעם אחת)

המשך יום סימבולי טוב

שתף