Skip to content

Commit 3dc49c2

Browse files
authored
Merge pull request #391 from wedesoft/mlp-draft
Draft post about machine learning
2 parents aa93e6f + 536e139 commit 3dc49c2

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

site/db.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
:name "Jan Wedekind"
116116
:url "https://www.wedesoft.de/"
117117
:image "https://avatars.githubusercontent.com/u/28663?v=4"
118+
:email "jan@wedesoft.de"
118119
:links [{:icon "github" :href "https://github.com/wedesoft"}]}
119120
{:id :kloimhardt
120121
:name "Markus Agwin Kloimwieder"

src/mlp/main.clj

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
^{:kindly/hide-code true
2+
:clay {:title "Machine learning using Clojure, libpython-clj2, and Pytorch"
3+
:external-requirements []
4+
:quarto {:author [:janwedekind]
5+
:draft true
6+
:description "Machine learning explained using the parabola example"
7+
:image "parabola.png"
8+
:type :post
9+
:date "2026-05-24"
10+
:category :ml
11+
:tags [:machine-learning]}}}
12+
13+
(ns mlp.main
14+
(:require [clojure.math :refer (PI cos sin exp to-radians)]
15+
[tablecloth.api :as tc]
16+
[scicloj.tableplot.v1.plotly :as plotly]
17+
[libpython-clj2.require :refer (require-python)]
18+
[libpython-clj2.python :refer (py.) :as py]))
19+
20+
21+
(require-python '[torch :as torch]
22+
'[torch.nn :as nn]
23+
'[torch.utils.data :as data]
24+
'[torch.nn.functional :as F]
25+
'[torch.optim :as optim])
26+
27+
(torch/manual_seed 42)
28+
29+
(def ParabolaNet
30+
(py/create-class
31+
"ParabolaNet" [nn/Module]
32+
{"__init__"
33+
(py/make-instance-fn
34+
(fn [self n-hidden]
35+
(py. nn/Module __init__ self)
36+
(py/set-attrs!
37+
self
38+
{"fc1" (nn/Linear 1 n-hidden)
39+
"fc2" (nn/Linear n-hidden n-hidden)
40+
"fc3" (nn/Linear n-hidden n-hidden)
41+
"fc4" (nn/Linear n-hidden 1)})
42+
nil))
43+
"forward"
44+
(py/make-instance-fn
45+
(fn [self x]
46+
(let [x (py. self fc1 x)
47+
x (F/sigmoid x)
48+
x (py. self fc2 x)
49+
x (F/sigmoid x)
50+
x (py. self fc3 x)
51+
x (F/sigmoid x)
52+
x (py. self fc4 x)]
53+
x)))}))
54+
55+
(defmacro without-gradient
56+
[& body]
57+
`(let [no-grad# (torch/no_grad)]
58+
(try
59+
(py. no-grad# ~'__enter__)
60+
~@body
61+
(finally
62+
(py. no-grad# ~'__exit__ nil nil nil)))))
63+
64+
(def extent 6.0)
65+
(def n 32)
66+
(def noise 1.0)
67+
(def features (torch/sub (torch/mul (torch/rand [n 1]) (* 2 extent)) extent))
68+
(def labels (torch/add (torch/mul features features) (torch/mul noise (torch/randn [n 1]))))
69+
70+
(def dataset (data/TensorDataset features labels))
71+
72+
(def train-size (int (* 0.8 n)))
73+
(def dev-size (int (* 0.1 n)))
74+
(def test-size (- n train-size dev-size))
75+
76+
(def splits (data/random_split dataset [train-size dev-size test-size]))
77+
(def train-ds (nth splits 0))
78+
(def dev-ds (nth splits 1))
79+
(def test-ds (nth splits 2))
80+
81+
(def train-data-loader (data/DataLoader train-ds :batch_size 4 :shuffle true))
82+
(def dev-data-loader (data/DataLoader dev-ds :batch_size 4 :shuffle true))
83+
84+
(defn average [numbers]
85+
(/ (reduce + numbers) (count numbers)))
86+
87+
(defn train-epoch
88+
[train-data-loader criterion model optimizer]
89+
(py. model train)
90+
(for [[features labels] train-data-loader]
91+
(do
92+
(py. optimizer zero_grad)
93+
(let [prediction (py. model __call__ features)
94+
loss (py. criterion __call__ prediction labels)]
95+
(py. loss backward)
96+
(py. optimizer step)
97+
(py. loss item)))))
98+
99+
(defn dev-epoch
100+
[dev-data-loader criterion model]
101+
(py. model eval)
102+
(without-gradient
103+
(for [[features labels] dev-data-loader]
104+
(let [prediction (py. model __call__ features)
105+
loss (py. criterion __call__ prediction labels)]
106+
(py. loss item)))))
107+
108+
(defn training-run
109+
[train-data-loader dev-data-loader epochs n-hidden lr]
110+
(let [model (ParabolaNet n-hidden)
111+
optimizer (optim/SGD (py. model "parameters") :lr lr :weight_decay 0.0)
112+
criterion (nn/MSELoss)]
113+
(loop [epoch 1 train-losses [] dev-losses []]
114+
(let [train-loss (average (train-epoch train-data-loader criterion model optimizer))
115+
dev-loss (average (dev-epoch dev-data-loader criterion model))]
116+
(if (< epoch epochs)
117+
(recur (inc epoch) (conj train-losses train-loss) (conj dev-losses dev-loss))
118+
{:model model :train-losses (conj train-losses train-loss) :dev-losses (conj dev-losses dev-loss)})))))
119+
120+
(def result (training-run train-data-loader dev-data-loader 5000 200 0.01))
121+
122+
(defn plot-model
123+
[features labels {:keys [model]}]
124+
(without-gradient
125+
(let [x (range (- extent) (+ extent 0.01) 0.01)
126+
y (map (fn [x] (py. (first (py. model __call__ (torch/tensor [x]))) item)) x)
127+
ds (tc/dataset {:x x :y y})
128+
pts (tc/dataset {:x (map first (py/->jvm (py. features tolist)))
129+
:y (map first (py/->jvm (py. labels tolist)))})]
130+
(-> ds
131+
(plotly/base {:=title "Model"})
132+
(plotly/layer-point {:=dataset pts :=x :x :=y :y :=name "data"})
133+
(plotly/layer-line {:=x :x :=y :y :=name "prediction"})))))
134+
135+
136+
137+
(defn smoothing
138+
[alpha]
139+
(fn [coll]
140+
(reductions (fn [prev-avg current] (+ (* alpha prev-avg) (* (- 1 alpha) current)))
141+
(first coll)
142+
(rest coll))))
143+
144+
145+
(plot-model features labels result)
146+
147+
(defn plot-losses
148+
[{:keys [train-losses dev-losses]} smoothing-fn]
149+
(-> (tc/dataset {:x (range 1 (count train-losses)) :y (smoothing-fn train-losses)})
150+
(plotly/base {:=title "Losses"})
151+
(plotly/layer-line {:=x :x :=y :y :=name "training loss"})
152+
(plotly/layer-line {:=dataset (tc/dataset {:x (range 1 (count dev-losses)) :y (smoothing-fn dev-losses)})
153+
:=x :x :=y :y :=name "dev loss"})))
154+
155+
(plot-losses result (smoothing 0.99))

src/mlp/parabola.png

21.9 KB
Loading

0 commit comments

Comments
 (0)