Skip to content

Commit 507a73f

Browse files
authored
Add article: Explore Micrograd with Verso and PowerShell (#73)
## Summary - add a tutorial that uses a PowerShell notebook in Verso to build a small reverse-mode automatic differentiation engine - walk through scalar operations, computation graphs, neurons, and training a tiny neural network - include five SVG illustrations showing graph structure and loss history ## Why The article presents automatic differentiation as an approachable, inspectable PowerShell exercise. It also demonstrates how Verso notebooks and PSGraphView can support interactive exploration of computation graphs. ## Author profile The dedicated author profile is being added separately in #72. This article can be reviewed independently, but should preferably merge after that PR so its byline links to the complete profile. ## Validation - rebased onto the current `upstream/main` - `npm run build` - verified the generated article page includes all five SVG assets
1 parent 852f36f commit 507a73f

6 files changed

Lines changed: 405 additions & 0 deletions
Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
---
2+
title: "Explore Micrograd with Verso and PowerShell"
3+
description: "Use a PowerShell notebook in Verso to build a tiny reverse-mode automatic differentiation engine, visualize its computation graph, and train a small neural network."
4+
author: Andrey Vernigora
5+
authors:
6+
- Andrey Vernigora
7+
date: 2026-07-24T00:00:00+00:00
8+
categories:
9+
- PowerShell for Developers
10+
tags:
11+
- powershell
12+
- verso
13+
- notebooks
14+
- automatic-differentiation
15+
- psgraphview
16+
---
17+
18+
[Verso](https://github.com/DataficationSDK/Verso) is an open-source interactive
19+
notebook platform and embeddable .NET execution engine. Its language kernels include
20+
PowerShell, C#, F#, Python, SQL, JavaScript, TypeScript, and HTTP, and it provides
21+
VS Code and browser front ends.
22+
23+
That timing is useful for PowerShell users. The
24+
[.NET Interactive repository](https://github.com/dotnet/interactive) was archived in
25+
April 2026, leaving a gap for maintained multi-language .NET notebooks. Verso is an
26+
actively developed option with persistent kernel state, rich output, cross-language
27+
variable sharing, and headless notebook execution.
28+
29+
This post walks through an
30+
[experimental PowerShell micrograd notebook](https://github.com/DataficationSDK/Verso/blob/3f8629154a28824ad5fbd0eaca49c3ef57168704/samples/Notebooks/powershell/micrograd/micrograd-ps.verso)
31+
built for Verso. The sample was proposed separately and is not currently part of
32+
Verso's `main` branch, so treat it as an exploration rather than a shipped Verso
33+
sample.
34+
35+
The notebook ports the core ideas from Andrej Karpathy's
36+
[micrograd](https://github.com/karpathy/micrograd) to PowerShell. The original
37+
project is intentionally tiny: scalar-valued reverse-mode automatic differentiation,
38+
then a small neural-network library on top. Karpathy's video,
39+
[The spelled-out intro to neural networks and backpropagation: building micrograd](https://youtu.be/VMj-3S1tku0),
40+
is effective because it does not hide the graph. The PowerShell version keeps that
41+
spirit, using [PSQuickGraph](https://github.com/eosfor/PSGraph) and
42+
[PSGraphView](https://github.com/eosfor/PSGraphView/tree/feature/direct-graphviz-integration)
43+
to render the computation graph directly from objects created in the notebook.
44+
45+
The diagrams in this article were generated by PowerShell from the same helper
46+
scripts used by the notebook and exported as Graphviz SVG through PSGraphView.
47+
48+
## Notebook Setup
49+
50+
Install the Verso CLI and the two graph modules:
51+
52+
```powershell
53+
dotnet tool install --global Verso.Cli
54+
55+
Install-Module -Name PSQuickGraph -RequiredVersion 2.5.0 -Scope CurrentUser
56+
Install-Module -Name PSGraphView -RequiredVersion 0.1.0 -Scope CurrentUser
57+
```
58+
59+
To open the exact experimental notebook used in this article, check out its commit
60+
and pass the notebook path to Verso:
61+
62+
```powershell
63+
git clone https://github.com/DataficationSDK/Verso.git
64+
Set-Location ./Verso
65+
git checkout 3f8629154a28824ad5fbd0eaca49c3ef57168704
66+
67+
verso serve ./samples/Notebooks/powershell/micrograd/micrograd-ps.verso
68+
```
69+
70+
The notebook starts with the normal module path:
71+
72+
```powershell
73+
Import-Module PSQuickGraph
74+
Import-Module PSGraphView
75+
```
76+
77+
The implementation is split into four scripts:
78+
79+
- `value.ps1` defines the scalar `Value` class and operator overloads.
80+
- `graphHelper.ps1` converts `Value` objects into graph vertices and renders them.
81+
- `neuronHelper.ps1` defines `Neuron`, `Layer`, and `MLP`.
82+
- `helpers.ps1` contains `Zip` and `Sum-Value`, small utilities used when building the loss.
83+
84+
The notebook loads them directly:
85+
86+
```powershell
87+
. ./value.ps1
88+
. ./graphHelper.ps1
89+
. ./neuronHelper.ps1
90+
. ./helpers.ps1
91+
```
92+
93+
The key class is `Value`. Each instance stores `data`, `grad`, a `label`, the operation that produced it, the child values that fed into that operation, and a `backward` closure. That is the entire trick: normal arithmetic produces both a result and a tiny piece of local derivative logic.
94+
95+
For addition, the derivative is one for both inputs:
96+
97+
```powershell
98+
static [Value] op_Addition([Value]$left, [Value]$right) {
99+
$out = [Value]::new($left.data + $right.data, @($left, $right), "+", "+_res")
100+
101+
$out.backward = {
102+
$left.grad += 1 * $out.grad
103+
$right.grad += 1 * $out.grad
104+
}.GetNewClosure()
105+
106+
return $out
107+
}
108+
```
109+
110+
For multiplication, each input receives the other input's data multiplied by the output gradient:
111+
112+
```powershell
113+
static [Value] op_Multiply([Value]$left, [Value]$right) {
114+
$out = [Value]::new($left.data * $right.data, @($left, $right), "*", "*_res")
115+
116+
$out.backward = {
117+
$left.grad += $right.data * $out.grad
118+
$right.grad += $left.data * $out.grad
119+
}.GetNewClosure()
120+
121+
return $out
122+
}
123+
```
124+
125+
`Tanh()` follows the same pattern, but the derivative is `1 - tanh(x)^2`:
126+
127+
```powershell
128+
[Value] Tanh(){
129+
$v = $this
130+
$t = [Math]::Tanh($this.data)
131+
$out = [Value]::new($t, @($this), "tanh")
132+
133+
$out.backward = {
134+
$v.grad += (1 - [Math]::Pow($t, 2)) * $out.grad
135+
}.GetNewClosure()
136+
137+
return $out
138+
}
139+
```
140+
141+
## Scalar Computation Graph
142+
143+
The first notebook example is the same kind of scalar expression Karpathy uses to make backpropagation visible:
144+
145+
```powershell
146+
$a = [Value]::new( 2.0, 'a')
147+
$b = [Value]::new(-3.0, 'b')
148+
$c = [Value]::new(10.0, 'c')
149+
$e = $a * $b; $e.label = 'e'
150+
$d = $e + $c; $d.label = 'd'
151+
$f = [Value]::new(-2.0, 'f')
152+
$L = $d * $f; $L.label = 'L'
153+
```
154+
155+
At this point `$L.data` is `-8`, and all gradients are still zero. The graph is created from the output value:
156+
157+
```powershell
158+
$scalarGraph = New-ExpressionGraph -val $L
159+
Show-ExpressionGraph -Graph $scalarGraph
160+
```
161+
162+
![Scalar expression before backpropagation; data values are populated and every gradient is zero](/images/articles/verso-micrograd-scalar-before.svg)
163+
164+
`New-ExpressionGraph` walks from the output node back through `children`. It creates record-shaped nodes for values and ellipse-shaped nodes for operations. Because `Value` objects are actual object references, helper hashtables prevent duplicate vertices when a value is reached more than once.
165+
166+
## Backpropagation Order
167+
168+
Backpropagation is not run over the display graph. The notebook builds a second graph directly on the original `Value` objects:
169+
170+
```powershell
171+
$bpGraph = New-BackpropagationGraph -val $L
172+
$L.grad = 1.0
173+
174+
Get-GraphTopologicalSort -Graph $bpGraph -Reverse |
175+
ForEach-Object { $_.OriginalObject } |
176+
ForEach-Object { & $_.backward }
177+
```
178+
179+
The output gradient starts at `1.0`, because `dL/dL = 1`. Then `Get-GraphTopologicalSort -Reverse` visits the output first and walks backward toward the leaves. Each node executes the closure captured when the value was created. After the pass, the visualization graph is rebuilt so the display nodes get a fresh snapshot of `grad`.
180+
181+
![The scalar expression after backpropagation; gradients show how each input changes L](/images/articles/verso-micrograd-scalar-after.svg)
182+
183+
This is the important implementation detail: the graph is not just a drawing. It is the execution dependency structure for reverse-mode autodiff.
184+
185+
## One Neuron
186+
187+
The next cell builds a tiny neuron by hand: two inputs, two weights, a bias, and a `tanh` activation.
188+
189+
```powershell
190+
$x1 = [Value]::new(2.0, 'x1')
191+
$x2 = [Value]::new(0.0, 'x2')
192+
193+
$w1 = [Value]::new(-3.0, 'w1')
194+
$w2 = [Value]::new(1.0, 'w2')
195+
$b = [Value]::new(6.8813735870195432, 'b')
196+
197+
$x1w1 = $x1 * $w1; $x1w1.label = 'x1*w1'
198+
$x2w2 = $x2 * $w2; $x2w2.label = 'x2*w2'
199+
$x1w1x2w2 = $x1w1 + $x2w2; $x1w1x2w2.label = 'x1*w1 + x2*w2'
200+
$n = $x1w1x2w2 + $b; $n.label = 'n'
201+
$o = $n.Tanh(); $o.label = 'o'
202+
```
203+
204+
![A single tanh neuron before the backward pass](/images/articles/verso-micrograd-neuron-before.svg)
205+
206+
Running the same topological backward pass from `$o` fills the gradients for the input, weights, bias, and intermediate values:
207+
208+
```powershell
209+
$bpNeuronGraph = New-BackpropagationGraph -val $o
210+
$o.grad = 1.0
211+
212+
Get-GraphTopologicalSort -Graph $bpNeuronGraph -Reverse |
213+
ForEach-Object { $_.OriginalObject } |
214+
ForEach-Object { & $_.backward }
215+
```
216+
217+
![The neuron after the tanh derivative has propagated through additions and multiplications](/images/articles/verso-micrograd-neuron-after.svg)
218+
219+
This is where the notebook starts to feel useful as a teaching tool. You can inspect every scalar contribution to the neuron instead of treating the neuron as a black box.
220+
221+
## Layer and MLP
222+
223+
After the manual neuron, `neuronHelper.ps1` turns the same logic into classes. A `Neuron` owns an array of weights and a bias:
224+
225+
```powershell
226+
class Neuron {
227+
[Value[]]$w
228+
[Value]$b
229+
230+
Neuron([int]$nin) {
231+
$this.w = for ($i = 0; $i -lt $nin; $i++) {
232+
[Value]::new(([Random]::Shared.NextDouble() * 2 - 1), "w$i")
233+
}
234+
235+
$this.b = [Value]::new(([Random]::Shared.NextDouble() * 2 - 1), "b")
236+
}
237+
238+
[Value] Invoke([Value[]]$x) {
239+
$sum = $this.b
240+
for ($i = 0; $i -lt $this.w.Count; $i++) {
241+
$sum = $sum + ($this.w[$i] * $x[$i])
242+
}
243+
244+
return $sum.Tanh()
245+
}
246+
}
247+
```
248+
249+
A `Layer` applies several neurons to the same input vector. An `MLP` chains layers so each layer receives the output vector from the previous layer:
250+
251+
```powershell
252+
$x = @(
253+
[Value]::new(2.0, 'x1')
254+
[Value]::new(3.0, 'x2')
255+
[Value]::new(-1.0, 'x3')
256+
)
257+
258+
$layer = [Layer]::new(3, 4)
259+
$layer.Invoke($x)
260+
261+
$net = [MLP]::new(3, @(4, 4, 1))
262+
$res = $net.Invoke($x)
263+
$res
264+
```
265+
266+
The notebook can render the full MLP expression graph too:
267+
268+
```powershell
269+
$netGraph = New-ExpressionGraph -val $res[0]
270+
Show-ExpressionGraph -Graph $netGraph -rankdir 'TD'
271+
```
272+
273+
That graph is intentionally not embedded here: it is already wide enough to be less readable in a blog post. The smaller scalar and neuron graphs make the mechanics clearer.
274+
275+
## Training Data and Loss
276+
277+
The training set is the small toy dataset from the micrograd walkthrough:
278+
279+
```powershell
280+
$xs = @(
281+
@([Value]::new(2.0, 'x11'), [Value]::new( 3.0, 'x12'), [Value]::new(-1.0, 'x13')),
282+
@([Value]::new(3.0, 'x21'), [Value]::new(-1.0, 'x22'), [Value]::new( 0.5, 'x23')),
283+
@([Value]::new(0.5, 'x31'), [Value]::new( 1.0, 'x32'), [Value]::new( 1.0, 'x33')),
284+
@([Value]::new(1.0, 'x41'), [Value]::new( 1.0, 'x42'), [Value]::new(-1.0, 'x43'))
285+
)
286+
287+
$ys = @(
288+
[Value]::new( 1.0, 'y1'),
289+
[Value]::new(-1.0, 'y2'),
290+
[Value]::new(-1.0, 'y3'),
291+
[Value]::new( 1.0, 'y4')
292+
)
293+
```
294+
295+
The loss is sum of squared errors:
296+
297+
```powershell
298+
$net = [MLP]::new(3, @(4, 4, 1))
299+
300+
$ypred = $xs | ForEach-Object { $net.Invoke($_)[0] }
301+
$loss = Zip -Left $ys -Right $ypred | Sum-Value {
302+
$diff = $_.Right - $_.Left
303+
$diff * $diff
304+
}
305+
```
306+
307+
`Zip` pairs expected and predicted values. `Sum-Value` starts from a `Value` named `loss` and keeps adding selected terms. Because every subtraction, multiplication, and addition returns another `Value`, the loss is also a scalar root of a full computation graph.
308+
309+
## One Training Step
310+
311+
One optimization step follows the same shape as PyTorch, but without hiding anything:
312+
313+
```powershell
314+
foreach ($p in $net.parameters()) {
315+
$p.grad = 0.0
316+
}
317+
foreach ($row in $xs) {
318+
foreach ($v in $row) { $v.grad = 0.0 }
319+
}
320+
foreach ($y in $ys) {
321+
$y.grad = 0.0
322+
}
323+
324+
$ypred = $xs | ForEach-Object { $net.Invoke($_)[0] }
325+
$loss = Zip -Left $ys -Right $ypred | Sum-Value {
326+
$diff = $_.Right - $_.Left
327+
$diff * $diff
328+
}
329+
330+
$loss.grad = 1.0
331+
$bpLossGraph = New-BackpropagationGraph -val $loss
332+
333+
Get-GraphTopologicalSort -Graph $bpLossGraph -Reverse |
334+
ForEach-Object { $_.OriginalObject } |
335+
ForEach-Object { & $_.backward }
336+
337+
foreach ($p in $net.parameters()) {
338+
$p.data += -0.1 * $p.grad
339+
}
340+
```
341+
342+
There are five phases: clear gradients, forward pass, loss construction, backward pass, parameter update. The learning rate is hard-coded as `0.1` because this is a notebook demo, not a training framework.
343+
344+
## Training Loop
345+
346+
The notebook repeats that step 200 times. A shorter 80-epoch run shows the same
347+
behavior: the sum of squared errors falls rapidly and then continues to converge.
348+
349+
![Training loss over 80 epochs](/images/articles/verso-micrograd-loss-history.svg)
350+
351+
The final notebook cell renders the full loss graph after training:
352+
353+
```powershell
354+
$lossGraph = New-ExpressionGraph -val $loss
355+
Show-ExpressionGraph -Graph $lossGraph -rankdir 'TD'
356+
```
357+
358+
It is a useful stress test for `PSGraphView`, but it is too large for this page because it contains the complete scalar computation that produced the loss. That is also the point of micrograd: a neural network can be understood as a large scalar expression, and backpropagation is just the disciplined reverse walk over that expression.
359+
360+
## Why This Matters
361+
362+
The important part is not that PowerShell is the best language for building neural networks. It is not. The point is that Verso makes PowerShell notebooks feel real again after the end of .NET Interactive, and the PowerShell kernel can now do the things notebook users expect: long-running host output, cancellation, persistent state, rich display, and ordinary module-based workflows.
363+
364+
For infrastructure engineers, that matters. The same mechanics used here for micrograd graphs apply to dependency graphs, Azure topology, policy validation, incident analysis, and any other workflow where PowerShell produces structured objects and the notebook should make those objects visible.

0 commit comments

Comments
 (0)