sourcelibs/Net/src/Net.xtlm
1⍝# Net: a network written as one line -- its layer sizes and activations become ordinary NN calls
2⍝#
3⍝# Import it with an alias of your choice: "net:" u_se< "Net".
4⍝# Put libs/Net/src on XETAL_PATH ("just path"); the reference is libs/Net/docs.
5⍝# Names with m: are macros; the others are private to this file.
6⍝#
7⍝# A macro is a function from the source text written left and right of
8⍝# its call to the source that replaces the call, before the program is
9⍝# compiled (X_eTaL MC10); xetal expand FILE shows the result. A spec
10⍝# is a list of words: sizes and activations, the input's size first:
11⍝# "784 128 relu 10 softmax". A size after the first is a dense layer
12⍝# (nn:d_ense, with its weights: one array per layer, the bias its last
13⍝# row); an activation is a function of NN (relu, sigmoid, tanh,
14⍝# softmax, logSoftmax). The expansion calls NN under the alias nn:
15⍝# (an expansion cannot ask which alias the caller chose), so import
16⍝# NN as "nn:" u_se< "NN".
17
18⍝## Reading a spec (private)
19
20⍝# The words of t: runs of characters other than white space, boxed
21⍝# (after X_eTaL's Combinators.xtlm).
22ʰw̲ords ← { t → (n̲ot t m̲ember? " \n\t") p̲artition t }
23⍝# The boxed texts b joined into one text ("" for none).
24ʰj̲oin ← { b →
25 0 = t̲ally b ? ""
26 d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ b
27}
28⍝# Whether the text w is a whole number (a layer's size).
29ʰs̲ize? ← { w → '∧ r̲/ w m̲ember? "0123456789" }
30⍝# Whether the text w names an activation.
31ʰa̲ct? ← { w → '∨ r̲/ '{ a → w m̲atch d̲isclose a } e̲ach ʰw̲ords "relu sigmoid tanh softmax logSoftmax" }
32⍝# An activation's function in NN: relu -> nn:r_elu.
33ʰa̲ct ← { w → "nn:" c̲at (1 t̲ake w) c̲at "_" c̲at 1 d̲rop w }
34⍝# The spec's sizes, as Ints.
35ʰs̲izes ← { ws → f̲loor n̲umbers ʰj̲oin '{ w → (ʰs̲ize? d̲isclose w) ? (d̲isclose w) c̲at " "◆ "" } m̲ap ws }
36⍝# What a spec looks like, for the messages.
37specShape ← "a spec is sizes and activations, the input's size first: \"784 128 relu 10 softmax\""
38⍝# What is wrong with the spec's words ws, or "".
39ʰw̲rong ← { ws →
40 2 > t̲ally ws ? specShape
41 n̲ot ʰs̲ize? d̲isclose f̲irst ws ? specShape
42 bad ← '{ w → n̲ot (ʰs̲ize? d̲isclose w) ∨ ʰa̲ct? d̲isclose w } e̲ach ws
43 0 < '+ r̲/ 0 + bad ? "not a size or an activation (relu, sigmoid, tanh, softmax, logSoftmax): " c̲at d̲isclose f̲irst (w̲here bad) s̲elect ws
44 ""
45}
46
47⍝## Writing the forward pass (private)
48
49⍝# The forward function for the spec's words ws and the weight names
50⍝# ns: a lambda of nn: calls, the input x through each layer in turn.
51ʰf̲unction ← { ws ns →
52 size ← '{ w → ʰs̲ize? d̲isclose w } e̲ach ws
53 k ← '+ s̲\ 0 + size
54 pre ← '{ i →
55 n̲ot i s̲elect size ? (ʰa̲ct d̲isclose i s̲elect ws) c̲at " "
56 2 = i s̲elect k ? ""
57 "("
58 } m̲ap 1 d̲rop r̲ange t̲ally ws
59 post ← '{ i →
60 n̲ot i s̲elect size ? ""
61 d ← " nn:d_ense " c̲at d̲isclose ((i s̲elect k) − 1) s̲elect ns
62 2 = i s̲elect k ? d
63 ")" c̲at d
64 } m̲ap 1 d̲rop r̲ange t̲ally ws
65 "{ x -> " c̲at (ʰj̲oin r̲ev pre) c̲at "x" c̲at (ʰj̲oin post) c̲at " }"
66}
67⍝# What is wrong with the weight names ns for the spec's words ws, or "".
68ʰc̲ount ← { ws ns →
69 dense ← -1 + '+ r̲/ '{ w → 0 + ʰs̲ize? d̲isclose w } e̲ach ws
70 (t̲ally ns) = dense ? ""
71 "name one weight array per dense layer: " c̲at (f̲ormat dense) c̲at " for this spec, " c̲at (f̲ormat t̲ally ns) c̲at " given"
72}
73
74⍝## Networks
75
76⍝# A forward function for the network the spec on the left describes,
77⍝# using the weight arrays named on the right, one per dense layer in
78⍝# order: an ordinary lambda of nn: calls, a batch (one example per
79⍝# row) in, the last layer's output out.
80ᵐn̲etwork< ← { spec weights →
81 ws ← ʰw̲ords spec
82 ns ← ʰw̲ords weights
83 0 < t̲ally ʰw̲rong ws ? "bad-macro-argument left" ⎕R̲EJECT ʰw̲rong ws
84 0 < t̲ally ws ʰc̲ount ns ? "bad-macro-argument right" ⎕R̲EJECT ws ʰc̲ount ns
85 ws ʰf̲unction ns
86}
87
88⍝# A whole model from one spec: on the left the function's name and a
89⍝# prefix ("u:d_eep c"), on the right the spec. It writes, as
90⍝# statements, each dense layer's weights read from data/<prefix>K.txt
91⍝# and reshaped as the spec says (inputs + 1 by outputs; the program
92⍝# stops with an error naming the layer when a file does not hold that
93⍝# many numbers, since r_eshape alone would silently repeat or cut the
94⍝# data), then the function, as n_etwork< writes it. The spec is the
95⍝# one place the network is described.
96ᵐm̲odel< ← { what spec →
97 ws ← ʰw̲ords spec
98 0 < t̲ally ʰw̲rong ws ? "bad-macro-argument right" ⎕R̲EJECT ʰw̲rong ws
99 nm ← ʰw̲ords what
100 2 ≠ t̲ally nm ? "bad-macro-argument left" ⎕R̲EJECT "write the function's name and the weight files' prefix: \"u:d_eep c\" (for data/c1.txt, data/c2.txt, ...)"
101 name ← d̲isclose f̲irst nm
102 prefix ← d̲isclose 2 s̲elect nm
103 s ← ʰs̲izes ws
104 ns ← '{ i → prefix c̲at f̲ormat i } m̲ap r̲ange -1 + t̲ally s
105 load ← '{ i →
106 w ← d̲isclose i s̲elect ns
107 need ← (1 + i s̲elect s) × (i + 1) s̲elect s
108 w c̲at " := { v -> " c̲at (f̲ormat need) c̲at " = t_ally v ? " c̲at (f̲ormat 1 + i s̲elect s) c̲at " " c̲at (f̲ormat (i + 1) s̲elect s) c̲at " r_eshape v; []P_ANIC \"data/" c̲at w c̲at ".txt holds \" c_at (f_ormat t_ally v) c_at \" numbers; layer " c̲at (f̲ormat i) c̲at " of \\\"" c̲at spec c̲at "\\\" (" c̲at (f̲ormat i s̲elect s) c̲at " inputs and a bias, " c̲at (f̲ormat (i + 1) s̲elect s) c̲at " outputs) needs " c̲at (f̲ormat need) c̲at "\" } n_umbers []N_GET \"data/" c̲at w c̲at ".txt\"\n"
109 } m̲ap r̲ange -1 + t̲ally s
110 (ʰj̲oin load) c̲at name c̲at " := " c̲at ws ʰf̲unction ns
111}
112
113⍝## Writing backpropagation (private)
114
115⍝# The function of NN for an activation word (relu -> nn:r_elu); "" for none.
116ʰa̲ctFn ← { w → 0 = t̲ally w ? ""◆ (ʰa̲ct w) c̲at " " }
117⍝# Its slope at its output A (the name of the array), times which the
118⍝# error goes back through it; "" when there is none.
119ʰs̲lope ← { w a →
120 w m̲atch "relu" ? " * f_loat " c̲at a c̲at " > 0.0"
121 w m̲atch "tanh" ? " * 1.0 - " c̲at a c̲at " * " c̲at a
122 w m̲atch "sigmoid" ? " * " c̲at a c̲at " * 1.0 - " c̲at a
123 ""
124}
125⍝# The activation after each dense layer of the spec's words ws (a box
126⍝# per layer, "" for none).
127ʰa̲cts ← { ws →
128 size ← '{ w → ʰs̲ize? d̲isclose w } e̲ach ws
129 at ← 1 d̲rop w̲here size
130 '{ p → (p < t̲ally ws) ∧ (n̲ot (p + 1) s̲elect size) ? d̲isclose (p + 1) s̲elect ws◆ "" } m̲ap at
131}
132⍝# N as text.
133ʰt̲xt ← { n → f̲ormat n }
134
135⍝# The names P1 .. PL as X_eTaL's tuple text, "(P1, P2)"; one alone is
136⍝# not a tuple, "P1" (X_eTaL has no one-element tuple).
137ʰt̲uple ← { p L →
138 L = 1 ? p c̲at "1"
139 "(" c̲at (ʰj̲oin '{ l → (l = 1) ? p c̲at ʰt̲xt l◆ ", " c̲at p c̲at ʰt̲xt l } m̲ap r̲ange L) c̲at ")"
140}
141
142⍝# The lines that run the spec's network forward from the inputs
143⍝# (the weights W1 .. WL bound by the caller) and back from the targets,
144⍝# and bind each layer's gradient G1 .. GL. ws: the spec's words; xy:
145⍝# the inputs' and targets' names, boxed.
146ʰb̲ackprop ← { ws xy →
147 x ← d̲isclose 1 s̲elect xy
148 y ← d̲isclose 2 s̲elect xy
149 acts ← ʰa̲cts ws
150 L ← t̲ally acts
151 r ← r̲ange L
152 fwd ← ʰj̲oin '{ l → " A" c̲at (ʰt̲xt l) c̲at " := " c̲at (ʰa̲ctFn d̲isclose l s̲elect acts) c̲at "A" c̲at (ʰt̲xt l − 1) c̲at " nn:d_ense W" c̲at (ʰt̲xt l) c̲at "\n" } m̲ap r
153 dL ← " D" c̲at (ʰt̲xt L) c̲at " := (A" c̲at (ʰt̲xt L) c̲at " - " c̲at y c̲at ") / f_loat t_ally " c̲at x c̲at "\n"
154 back ← ʰj̲oin '{ l → " D" c̲at (ʰt̲xt l) c̲at " := (D" c̲at (ʰt̲xt l + 1) c̲at " '+ '* i_nner o_\\ -1 d_rop W" c̲at (ʰt̲xt l + 1) c̲at ")" c̲at ((d̲isclose l s̲elect acts) ʰs̲lope "A" c̲at ʰt̲xt l) c̲at "\n" } m̲ap r̲ev -1 d̲rop r
155 grads ← ʰj̲oin '{ l → " G" c̲at (ʰt̲xt l) c̲at " := (o_\\ A" c̲at (ʰt̲xt l − 1) c̲at " c_at_2 ((t_ally A" c̲at (ʰt̲xt l − 1) c̲at ") c_at 1) r_eshape 1.0) '+ '* i_nner D" c̲at (ʰt̲xt l) c̲at "\n" } m̲ap r
156 " A0 := " c̲at x c̲at "\n" c̲at fwd c̲at dL c̲at back c̲at grads
157}
158⍝# What is wrong with a trainable spec's words ws, or "".
159ʰt̲rainable ← { ws →
160 0 < t̲ally ʰw̲rong ws ? ʰw̲rong ws
161 n̲ot (d̲isclose f̲irst r̲ev ws) m̲atch "softmax" ? "a network trained with cross-entropy ends in softmax"
162 ""
163}
164
165⍝## Training
166
167⍝# The gradient of the cross-entropy loss by each layer's weights, for
168⍝# the network the spec on the right describes: on the left the
169⍝# function's name, the inputs' and the targets' (one-hot rows):
170⍝# "u:g_rad X Y". The function takes the weights as a tuple
171⍝# (W1, .., WL) and gives the gradients as a tuple (G1, .., GL),
172⍝# each the shape of its layer's weights: backpropagation, written out.
173ᵐg̲radient< ← { what spec →
174 ws ← ʰw̲ords spec
175 0 < t̲ally ʰt̲rainable ws ? "bad-macro-argument right" ⎕R̲EJECT ʰt̲rainable ws
176 nm ← ʰw̲ords what
177 3 ≠ t̲ally nm ? "bad-macro-argument left" ⎕R̲EJECT "write the function's name, then the inputs' and the targets': \"u:g_rad X Y\""
178 L ← t̲ally ʰa̲cts ws
179 (d̲isclose 1 s̲elect nm) c̲at " := { " c̲at ("W" ʰt̲uple L) c̲at " ->\n" c̲at (ws ʰb̲ackprop 1 d̲rop nm) c̲at " " c̲at ("G" ʰt̲uple L) c̲at "\n}"
180}
181
182⍝# A training step for the network the spec on the right describes:
183⍝# on the left the function's name, the inputs', the targets' (one-hot
184⍝# rows) and the learning rate's ("u:s_tep X Y lr"). The step takes the
185⍝# state, a tuple of 3L + 1 arrays (each layer's weights, Adam's running
186⍝# averages of gradients and of their squares, the step count; @
187⍝# net:s_tate< writes the first), and gives the next: the forward pass,
188⍝# each layer's gradient by backpropagation (as g_radient< writes it),
189⍝# and Adam. The spec must end in softmax.
190ᵐt̲rain< ← { what spec →
191 ws ← ʰw̲ords spec
192 0 < t̲ally ʰt̲rainable ws ? "bad-macro-argument right" ⎕R̲EJECT ʰt̲rainable ws
193 nm ← ʰw̲ords what
194 4 ≠ t̲ally nm ? "bad-macro-argument left" ⎕R̲EJECT "write the step's name, then the inputs', the targets' and the learning rate's: \"u:s_tep X Y lr\""
195 lr ← d̲isclose 4 s̲elect nm
196 L ← t̲ally ʰa̲cts ws
197 r ← r̲ange L
198 adam ← ʰj̲oin '{ l →
199 i ← ʰt̲xt l
200 " M" c̲at i c̲at " := (0.9 * M" c̲at i c̲at ") + 0.1 * G" c̲at i c̲at "\n V" c̲at i c̲at " := (0.999 * V" c̲at i c̲at ") + 0.001 * G" c̲at i c̲at " * G" c̲at i c̲at "\n W" c̲at i c̲at " := W" c̲at i c̲at " - " c̲at lr c̲at " * (M" c̲at i c̲at " / 1.0 - 0.9 ^ k) / 0.00000001 + (V" c̲at i c̲at " / 1.0 - 0.999 ^ k) ^ 0.5\n"
201 } m̲ap r
202 names ← ʰj̲oin '{ l → "W" c̲at (ʰt̲xt l) c̲at ", " } m̲ap r
203 names ← names c̲at (ʰj̲oin '{ l → "M" c̲at (ʰt̲xt l) c̲at ", " } m̲ap r) c̲at ʰj̲oin '{ l → "V" c̲at (ʰt̲xt l) c̲at ", " } m̲ap r
204 state ← "(" c̲at names c̲at "k)"
205 (d̲isclose 1 s̲elect nm) c̲at " := { " c̲at state c̲at " ->\n" c̲at (ws ʰb̲ackprop 1 d̲rop 3 t̲ake nm) c̲at " k := 1.0 + k\n" c̲at adam c̲at " " c̲at state c̲at "\n}"
206}
207
208⍝# The starting state for a training step (t_rain<) from the weight
209⍝# arrays named on the right: the weights, zero averages, step 0.
210ᵐs̲tate< ← { @ weights →
211 ns ← ʰw̲ords weights
212 0 = t̲ally ns ? "bad-macro-argument right" ⎕R̲EJECT "name the starting weight arrays, one per dense layer"
213 ws ← ʰj̲oin '{ w → (d̲isclose w) c̲at ", " } m̲ap ns
214 zs ← ʰj̲oin '{ w → "0.0 * " c̲at (d̲isclose w) c̲at ", " } m̲ap ns
215 "(" c̲at ws c̲at zs c̲at zs c̲at "0.0)"
216}
217
218⍝## Counting and checking
219
220⍝# How many numbers the network of the spec on the left has to learn
221⍝# (weights and biases), worked out when the program is compiled: the
222⍝# call becomes that number. Nothing is written on the right: @.
223⍝# >> "net:" u_se< "Net"
224⍝# >> "2 16 relu 16 relu 3 softmax" net:p_arams< @
225⍝# 371
226ᵐp̲arams< ← { spec @ →
227 ws ← ʰw̲ords spec
228 0 < t̲ally ʰw̲rong ws ? "bad-macro-argument left" ⎕R̲EJECT ʰw̲rong ws
229 s ← ʰs̲izes ws
230 f̲ormat '+ r̲/ (1 + -1 d̲rop s) × 1 d̲rop s
231}
232
233⍝# Whether the weight arrays named on the right have the shapes the
234⍝# spec on the left says (a layer from m inputs to n outputs is
235⍝# m + 1 by n: the bias is the last row): 1 or 0.
236⍝# >> "net:" u_se< "Net"
237⍝# >> w1 := 3 4 r_eshape 0.0
238⍝# >> w2 := 5 3 r_eshape 0.0
239⍝# >> "2 4 tanh 3 softmax" net:s_hapes< "w1 w2"
240⍝# 1
241ᵐs̲hapes< ← { spec weights →
242 ws ← ʰw̲ords spec
243 ns ← ʰw̲ords weights
244 0 < t̲ally ʰw̲rong ws ? "bad-macro-argument left" ⎕R̲EJECT ʰw̲rong ws
245 s ← ʰs̲izes ws
246 0 < t̲ally ws ʰc̲ount ns ? "bad-macro-argument right" ⎕R̲EJECT ws ʰc̲ount ns
247 one ← '{ i → "((s_hape " c̲at (d̲isclose i s̲elect ns) c̲at ") m_atch " c̲at (f̲ormat 1 + i s̲elect s) c̲at " " c̲at (f̲ormat (i + 1) s̲elect s) c̲at ")" } m̲ap r̲ange t̲ally ns
248 ʰj̲oin '{ i → (i = 1) ? d̲isclose i s̲elect one◆ " & " c̲at d̲isclose i s̲elect one } m̲ap r̲ange t̲ally ns
249}