Verso - это open-source платформа интерактивных ноутбуков и embeddable .NET execution engine для C#, F#, PowerShell, Python, SQL, JavaScript, TypeScript, HTTP, HTML, Markdown и Mermaid. Контекст важен: dotnet/interactive теперь архивирован, в README указано, что Polyglot Notebooks deprecated 27 марта 2026, .NET Interactive deprecated 24 апреля 2026, а сам репозиторий был архивирован 27 апреля 2026. Если коротко, .NET Interactive был привычным multi-language notebook engine, а Verso становится новой активно развиваемой заменой с более чистой extension model, VS Code и browser front ends, notebook/dashboard layouts и полноценными language kernels. Я тоже отправил туда пару PR-ов. Цель простая: сделать PowerShell first-class citizen в современных notebook workflows.
В этой статье я разбираю samples/Notebooks/powershell/micrograd/micrograd-ps.verso, ноутбук Verso, который переносит основные идеи из micrograd Андрея Карпати в PowerShell. Оригинальный проект нарочно маленький: scalar-valued reverse-mode automatic differentiation, а сверху небольшая neural-network library. Видео Карпати The spelled-out intro to neural networks and backpropagation: building micrograd до сих пор одно из лучших объяснений backpropagation, потому что оно не прячет граф вычислений. PowerShell-версия сохраняет эту идею, но использует eosfor/PSGraph и eosfor/PSGraphView, чтобы рисовать вычислительный граф прямо из объектов, созданных в ноутбуке.
Observable здесь тоже работает не просто как статический блог. SVG ниже не являются скриншотами, заранее положенными в репозиторий. Их генерируют Observable Framework file loaders, например scalar-graph.svg.ps1: во время сборки Observable запускает PowerShell, импортирует графовые модули, dot-source-ит те же скрипты, что использует Verso notebook, экспортирует Graphviz SVG через PSGraphView и встраивает результат в страницу.
Подготовка ноутбука
Ноутбук начинается с обычного импорта модулей. Никакой локальной сборки Verso для этого примера не нужно:
Import-Module PSQuickGraph
Import-Module PSGraphView
Реализация разбита на четыре небольших скрипта:
value.ps1определяет scalar classValueи operator overloads.graphHelper.ps1превращаетValueobjects в вершины графа и рисует их.neuronHelper.ps1определяетNeuron,LayerиMLP.helpers.ps1содержитZipиSum-Value, маленькие утилиты для построения loss.
В ноутбуке они подключаются напрямую:
. ./value.ps1
. ./graphHelper.ps1
. ./neuronHelper.ps1
. ./helpers.ps1
Главный класс - Value. Каждый объект хранит data, grad, label, операцию, которая его создала, дочерние значения и backward closure. В этом весь фокус: обычная арифметика возвращает не только результат, но и маленький кусок локальной логики для производной.
Для сложения производная равна единице по обоим входам:
static [Value] op_Addition([Value]$left, [Value]$right) {
$out = [Value]::new($left.data + $right.data, @($left, $right), "+", "+_res")
$out.backward = {
$left.grad += 1 * $out.grad
$right.grad += 1 * $out.grad
}.GetNewClosure()
return $out
}
Для умножения каждый вход получает значение другого входа, умноженное на gradient результата:
static [Value] op_Multiply([Value]$left, [Value]$right) {
$out = [Value]::new($left.data * $right.data, @($left, $right), "*", "*_res")
$out.backward = {
$left.grad += $right.data * $out.grad
$right.grad += $left.data * $out.grad
}.GetNewClosure()
return $out
}
Tanh() устроен так же, только derivative равен 1 - tanh(x)^2:
[Value] Tanh(){
$v = $this
$t = [Math]::Tanh($this.data)
$out = [Value]::new($t, @($this), "tanh")
$out.backward = {
$v.grad += (1 - [Math]::Pow($t, 2)) * $out.grad
}.GetNewClosure()
return $out
}
Scalar Computation Graph
Первый пример в ноутбуке - скалярное выражение в стиле micrograd:
$a = [Value]::new( 2.0, 'a')
$b = [Value]::new(-3.0, 'b')
$c = [Value]::new(10.0, 'c')
$e = $a * $b; $e.label = 'e'
$d = $e + $c; $d.label = 'd'
$f = [Value]::new(-2.0, 'f')
$L = $d * $f; $L.label = 'L'
На этом этапе $L.data равно -8, а все gradients еще нулевые. Граф строится от выходного значения:
$scalarGraph = New-ExpressionGraph -val $L
Show-ExpressionGraph -Graph $scalarGraph
New-ExpressionGraph идет от output node назад по children. Для значений создаются record-shaped nodes, для операций - ellipse-shaped nodes. Так как Value objects являются настоящими object references, вспомогательные hashtables не дают создать дубликаты вершин, если один и тот же объект встречается в графе повторно.
Порядок backpropagation
Backpropagation выполняется не по display graph. Ноутбук строит второй граф, уже прямо из оригинальных объектов Value:
$bpGraph = New-BackpropagationGraph -val $L
$L.grad = 1.0
Get-GraphTopologicalSort -Graph $bpGraph -Reverse |
ForEach-Object { $_.OriginalObject } |
ForEach-Object { & $_.backward }
Gradient у output начинается с 1.0, потому что dL/dL = 1. Затем Get-GraphTopologicalSort -Reverse посещает output первым и идет назад к leaf nodes. Каждый узел выполняет closure, который был захвачен в момент создания значения. После прохода display graph пересобирается, чтобы в ноды попал свежий snapshot grad.
Это ключевая деталь реализации: граф здесь не только картинка. Это dependency structure, по которой выполняется reverse-mode autodiff.
Один нейрон
Следующая ячейка руками собирает маленький нейрон: два входа, два веса, bias и tanh activation.
$x1 = [Value]::new(2.0, 'x1')
$x2 = [Value]::new(0.0, 'x2')
$w1 = [Value]::new(-3.0, 'w1')
$w2 = [Value]::new(1.0, 'w2')
$b = [Value]::new(6.8813735870195432, 'b')
$x1w1 = $x1 * $w1; $x1w1.label = 'x1*w1'
$x2w2 = $x2 * $w2; $x2w2.label = 'x2*w2'
$x1w1x2w2 = $x1w1 + $x2w2; $x1w1x2w2.label = 'x1*w1 + x2*w2'
$n = $x1w1x2w2 + $b; $n.label = 'n'
$o = $n.Tanh(); $o.label = 'o'
Тот же topological backward pass от $o заполняет gradients для входов, весов, bias и промежуточных значений:
$bpNeuronGraph = New-BackpropagationGraph -val $o
$o.grad = 1.0
Get-GraphTopologicalSort -Graph $bpNeuronGraph -Reverse |
ForEach-Object { $_.OriginalObject } |
ForEach-Object { & $_.backward }
В этот момент ноутбук становится особенно полезным как учебный инструмент. Можно посмотреть каждый scalar contribution к нейрону, а не воспринимать нейрон как черный ящик.
Layer и MLP
После ручного нейрона neuronHelper.ps1 заворачивает ту же логику в классы. Neuron хранит массив weights и bias:
class Neuron {
[Value[]]$w
[Value]$b
Neuron([int]$nin) {
$this.w = for ($i = 0; $i -lt $nin; $i++) {
[Value]::new(([Random]::Shared.NextDouble() * 2 - 1), "w$i")
}
$this.b = [Value]::new(([Random]::Shared.NextDouble() * 2 - 1), "b")
}
[Value] Invoke([Value[]]$x) {
$sum = $this.b
for ($i = 0; $i -lt $this.w.Count; $i++) {
$sum = $sum + ($this.w[$i] * $x[$i])
}
return $sum.Tanh()
}
}
Layer применяет несколько нейронов к одному input vector. MLP связывает layers так, что каждый следующий layer получает output vector предыдущего:
$x = @(
[Value]::new(2.0, 'x1')
[Value]::new(3.0, 'x2')
[Value]::new(-1.0, 'x3')
)
$layer = [Layer]::new(3, 4)
$layer.Invoke($x)
$net = [MLP]::new(3, @(4, 4, 1))
$res = $net.Invoke($x)
$res
В ноутбуке можно отрисовать и полный expression graph для MLP:
$netGraph = New-ExpressionGraph -val $res[0]
Show-ExpressionGraph -Graph $netGraph -rankdir 'TD'
Я не встраиваю его в статью намеренно: он уже достаточно широкий, чтобы быть неудобным для чтения в блоге. Маленькие графы scalar expression и neuron лучше показывают механику.
Training Data и Loss
Training set взят из micrograd walkthrough:
$xs = @(
@([Value]::new(2.0, 'x11'), [Value]::new( 3.0, 'x12'), [Value]::new(-1.0, 'x13')),
@([Value]::new(3.0, 'x21'), [Value]::new(-1.0, 'x22'), [Value]::new( 0.5, 'x23')),
@([Value]::new(0.5, 'x31'), [Value]::new( 1.0, 'x32'), [Value]::new( 1.0, 'x33')),
@([Value]::new(1.0, 'x41'), [Value]::new( 1.0, 'x42'), [Value]::new(-1.0, 'x43'))
)
$ys = @(
[Value]::new( 1.0, 'y1'),
[Value]::new(-1.0, 'y2'),
[Value]::new(-1.0, 'y3'),
[Value]::new( 1.0, 'y4')
)
Loss - это sum of squared errors:
$net = [MLP]::new(3, @(4, 4, 1))
$ypred = $xs | ForEach-Object { $net.Invoke($_)[0] }
$loss = Zip -Left $ys -Right $ypred | Sum-Value {
$diff = $_.Right - $_.Left
$diff * $diff
}
Zip соединяет expected и predicted values. Sum-Value стартует с Value по имени loss и добавляет выбранные terms. Так как каждое вычитание, умножение и сложение возвращает новый Value, loss тоже становится scalar root полного computation graph.
Один training step
Один optimization step выглядит почти как PyTorch, только без скрытой магии:
foreach ($p in $net.parameters()) {
$p.grad = 0.0
}
foreach ($row in $xs) {
foreach ($v in $row) { $v.grad = 0.0 }
}
foreach ($y in $ys) {
$y.grad = 0.0
}
$ypred = $xs | ForEach-Object { $net.Invoke($_)[0] }
$loss = Zip -Left $ys -Right $ypred | Sum-Value {
$diff = $_.Right - $_.Left
$diff * $diff
}
$loss.grad = 1.0
$bpLossGraph = New-BackpropagationGraph -val $loss
Get-GraphTopologicalSort -Graph $bpLossGraph -Reverse |
ForEach-Object { $_.OriginalObject } |
ForEach-Object { & $_.backward }
foreach ($p in $net.parameters()) {
$p.data += -0.1 * $p.grad
}
Здесь пять фаз: очистить gradients, выполнить forward pass, построить loss, выполнить backward pass, обновить parameters. Learning rate жестко задан как 0.1, потому что это demo notebook, а не training framework.
Training loop
В ноутбуке этот шаг повторяется 200 раз. Для блога я использую еще один Observable PowerShell loader, loss-history.csv.ps1: он запускает короткий training loop и возвращает CSV. Observable читает CSV и строит график loss.
Последняя ячейка ноутбука рисует полный loss graph после обучения:
$lossGraph = New-ExpressionGraph -val $loss
Show-ExpressionGraph -Graph $lossGraph -rankdir 'TD'
Это полезный stress test для PSGraphView, но для страницы он слишком большой: внутри находится полный scalar computation, который привел к loss. И в этом как раз смысл micrograd: neural network можно понимать как большое скалярное выражение, а backpropagation - как аккуратный reverse walk по этому выражению.
Зачем это нужно
Главная мысль не в том, что PowerShell является лучшим языком для neural networks. Не является. Смысл в другом: Verso возвращает PowerShell notebooks в нормальное состояние после окончания .NET Interactive, а PowerShell kernel теперь умеет то, чего пользователи ждут от notebook experience: long-running host output, cancellation, persistent state, rich display и обычные workflows вокруг модулей.
Для infrastructure engineers это важно. Те же механики, которые здесь используются для micrograd graphs, применимы к dependency graphs, Azure topology, policy validation, incident analysis и любому другому сценарию, где PowerShell производит structured objects, а notebook должен сделать эти объекты видимыми.