From 0a377751bc5c2f00e8e60bb52c31e83b8047a803 Mon Sep 17 00:00:00 2001 From: mhjensen Date: Fri, 19 Oct 2018 05:04:37 +0200 Subject: [PATCH] deleted files --- doc/src/NeuralNet/NeuralNet-bs.html | 385 -- doc/src/NeuralNet/NeuralNet-minted.pdf | Bin 591416 -> 0 bytes doc/src/NeuralNet/NeuralNet-plain-minted.tex | 3190 ---------- doc/src/NeuralNet/NeuralNet-reveal.html | 3916 ------------ doc/src/NeuralNet/NeuralNet-solarized.html | 3642 ------------ doc/src/NeuralNet/NeuralNet.html | 3647 ------------ doc/src/NeuralNet/NeuralNet.ipynb | 4020 ------------- doc/src/NeuralNet/NeuralNet.p.tex | 3220 ---------- doc/src/NeuralNet/NeuralNet.tex | 3190 ---------- ...1524234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 44 - ...E854234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 3 - ...C6A4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 32 - ...CBB4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 17 - ...81F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 102 - ...0084234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 13 - ...8534234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 18 - ...96C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 70 - ...B5C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 29 - ...EFC4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 133 - ...7D94234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 9 - ...B324234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 17 - ...3DF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 35 - ...5814234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 3 - ...AE64234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 18 - ...B4C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 14 - ...1874234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 22 - ...6244234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 73 - ...27D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 33 - ...5A24234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 36 - ...AE44234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 3 - ...3CF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 37 - ...C1F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 26 - ...5114234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 17 - ...0D44234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 17 - ...9AE4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 3 - ...3AF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 36 - ...D2D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 42 - ...EE04234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 18 - ...9174234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 6 - ...0C34234D82CF76A2559C5DD98A6DB88BEC4.pygtex | 33 - .../_minted-NeuralNet/default.pygstyle | 101 - doc/src/NeuralNet/ipynb-NeuralNet-src.tar.gz | Bin 88051 -> 0 bytes doc/src/NeuralNet/reveal.js/.gitignore | 13 - doc/src/NeuralNet/reveal.js/.travis.yml | 7 - doc/src/NeuralNet/reveal.js/CONTRIBUTING.md | 23 - doc/src/NeuralNet/reveal.js/Gruntfile.js | 140 - doc/src/NeuralNet/reveal.js/LICENSE | 19 - doc/src/NeuralNet/reveal.js/README.md | 1246 ---- doc/src/NeuralNet/reveal.js/bower.json | 27 - .../reveal.js/css/images/cbc_footer.png | Bin 10008 -> 0 bytes .../reveal.js/css/images/cbc_symbol.png | Bin 2946 -> 0 bytes .../reveal.js/css/images/simula_footer.png | Bin 2513 -> 0 bytes .../reveal.js/css/images/simula_logo.png | Bin 2138 -> 0 bytes .../reveal.js/css/images/simula_symbol.png | Bin 2138 -> 0 bytes .../reveal.js/css/images/uio_footer.png | Bin 18189 -> 0 bytes .../reveal.js/css/images/uio_symbol.png | Bin 11352 -> 0 bytes .../NeuralNet/reveal.js/css/print/paper.css | 203 - doc/src/NeuralNet/reveal.js/css/print/pdf.css | 178 - doc/src/NeuralNet/reveal.js/css/reveal.css | 1886 ------ doc/src/NeuralNet/reveal.js/css/reveal.scss | 1717 ------ .../NeuralNet/reveal.js/css/theme/README.md | 21 - .../NeuralNet/reveal.js/css/theme/beige.css | 154 - .../reveal.js/css/theme/beigesmall.css | 155 - .../NeuralNet/reveal.js/css/theme/black.css | 273 - .../NeuralNet/reveal.js/css/theme/blood.css | 180 - doc/src/NeuralNet/reveal.js/css/theme/cbc.css | 144 - .../reveal.js/css/theme/darkgray.css | 153 - .../NeuralNet/reveal.js/css/theme/default.css | 153 - .../NeuralNet/reveal.js/css/theme/league.css | 279 - .../NeuralNet/reveal.js/css/theme/moon.css | 153 - .../NeuralNet/reveal.js/css/theme/night.css | 141 - .../NeuralNet/reveal.js/css/theme/serif.css | 143 - .../NeuralNet/reveal.js/css/theme/simple.css | 144 - .../NeuralNet/reveal.js/css/theme/simula.css | 144 - doc/src/NeuralNet/reveal.js/css/theme/sky.css | 150 - .../reveal.js/css/theme/solarized.css | 153 - .../reveal.js/css/theme/source/beige.scss | 50 - .../css/theme/source/beigesmall.scss | 51 - .../reveal.js/css/theme/source/black.scss | 49 - .../reveal.js/css/theme/source/blood.scss | 91 - .../reveal.js/css/theme/source/cbc.scss | 39 - .../reveal.js/css/theme/source/darkgray.scss | 42 - .../reveal.js/css/theme/source/default.scss | 42 - .../reveal.js/css/theme/source/league.scss | 34 - .../reveal.js/css/theme/source/moon.scss | 68 - .../reveal.js/css/theme/source/night.scss | 35 - .../reveal.js/css/theme/source/serif.scss | 35 - .../reveal.js/css/theme/source/simple.scss | 38 - .../reveal.js/css/theme/source/simula.scss | 39 - .../reveal.js/css/theme/source/sky.scss | 46 - .../reveal.js/css/theme/source/solarized.scss | 74 - .../reveal.js/css/theme/source/white.scss | 49 - .../NeuralNet/reveal.js/css/theme/white.css | 273 - doc/src/NeuralNet/reveal.js/demo.html | 410 -- doc/src/NeuralNet/reveal.js/index.html | 49 - doc/src/NeuralNet/reveal.js/js/reveal.js | 5241 ----------------- .../NeuralNet/reveal.js/lib/css/zenburn.css | 80 - .../reveal.js/lib/font/league-gothic/LICENSE | 2 - .../lib/font/league-gothic/league-gothic.css | 10 - .../lib/font/league-gothic/league-gothic.eot | Bin 25696 -> 0 bytes .../lib/font/league-gothic/league-gothic.ttf | Bin 64256 -> 0 bytes .../lib/font/league-gothic/league-gothic.woff | Bin 30764 -> 0 bytes .../lib/font/source-sans-pro/LICENSE | 45 - .../source-sans-pro-italic.eot | Bin 75720 -> 0 bytes .../source-sans-pro-italic.ttf | Bin 238084 -> 0 bytes .../source-sans-pro-italic.woff | Bin 98556 -> 0 bytes .../source-sans-pro-regular.eot | Bin 88070 -> 0 bytes .../source-sans-pro-regular.ttf | Bin 288008 -> 0 bytes .../source-sans-pro-regular.woff | Bin 114324 -> 0 bytes .../source-sans-pro-semibold.eot | Bin 89897 -> 0 bytes .../source-sans-pro-semibold.ttf | Bin 284640 -> 0 bytes .../source-sans-pro-semibold.woff | Bin 115648 -> 0 bytes .../source-sans-pro-semibolditalic.eot | Bin 75706 -> 0 bytes .../source-sans-pro-semibolditalic.ttf | Bin 240944 -> 0 bytes .../source-sans-pro-semibolditalic.woff | Bin 98816 -> 0 bytes .../font/source-sans-pro/source-sans-pro.css | 39 - .../NeuralNet/reveal.js/lib/js/classList.js | 2 - .../NeuralNet/reveal.js/lib/js/head.min.js | 9 - .../NeuralNet/reveal.js/lib/js/html5shiv.js | 7 - doc/src/NeuralNet/reveal.js/package.json | 43 - .../reveal.js/plugin/highlight/highlight.js | 77 - .../reveal.js/plugin/markdown/example.html | 136 - .../reveal.js/plugin/markdown/example.md | 36 - .../reveal.js/plugin/markdown/markdown.js | 412 -- .../reveal.js/plugin/markdown/marked.js | 6 - .../NeuralNet/reveal.js/plugin/math/math.js | 67 - .../reveal.js/plugin/multiplex/client.js | 13 - .../reveal.js/plugin/multiplex/index.js | 64 - .../reveal.js/plugin/multiplex/master.js | 34 - .../reveal.js/plugin/multiplex/package.json | 19 - .../reveal.js/plugin/notes-server/client.js | 65 - .../reveal.js/plugin/notes-server/index.js | 69 - .../reveal.js/plugin/notes-server/notes.html | 585 -- .../reveal.js/plugin/notes/notes.html | 759 --- .../NeuralNet/reveal.js/plugin/notes/notes.js | 155 - .../reveal.js/plugin/print-pdf/print-pdf.js | 69 - .../reveal.js/plugin/search/search.js | 206 - .../reveal.js/plugin/zoom-js/zoom.js | 272 - .../reveal.js/test/examples/assets/image1.png | Bin 21991 -> 0 bytes .../reveal.js/test/examples/assets/image2.png | Bin 10237 -> 0 bytes .../reveal.js/test/examples/barebones.html | 41 - .../test/examples/embedded-media.html | 49 - .../reveal.js/test/examples/math.html | 185 - .../test/examples/slide-backgrounds.html | 144 - .../test/examples/slide-transitions.html | 101 - .../NeuralNet/reveal.js/test/qunit-1.12.0.css | 244 - .../NeuralNet/reveal.js/test/qunit-1.12.0.js | 2212 ------- doc/src/NeuralNet/reveal.js/test/simple.md | 12 - .../test-markdown-element-attributes.html | 134 - .../test/test-markdown-element-attributes.js | 46 - .../test/test-markdown-external.html | 36 - .../reveal.js/test/test-markdown-external.js | 24 - .../reveal.js/test/test-markdown-options.html | 41 - .../reveal.js/test/test-markdown-options.js | 26 - .../test/test-markdown-slide-attributes.html | 128 - .../test/test-markdown-slide-attributes.js | 47 - .../reveal.js/test/test-markdown.html | 52 - .../NeuralNet/reveal.js/test/test-markdown.js | 15 - .../NeuralNet/reveal.js/test/test-pdf.html | 83 - doc/src/NeuralNet/reveal.js/test/test-pdf.js | 15 - doc/src/NeuralNet/reveal.js/test/test.html | 86 - doc/src/NeuralNet/reveal.js/test/test.js | 597 -- 162 files changed, 48623 deletions(-) delete mode 100644 doc/src/NeuralNet/NeuralNet-bs.html delete mode 100644 doc/src/NeuralNet/NeuralNet-minted.pdf delete mode 100644 doc/src/NeuralNet/NeuralNet-plain-minted.tex delete mode 100644 doc/src/NeuralNet/NeuralNet-reveal.html delete mode 100644 doc/src/NeuralNet/NeuralNet-solarized.html delete mode 100644 doc/src/NeuralNet/NeuralNet.html delete mode 100644 doc/src/NeuralNet/NeuralNet.ipynb delete mode 100644 doc/src/NeuralNet/NeuralNet.p.tex delete mode 100644 doc/src/NeuralNet/NeuralNet.tex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/03D77A9F0C0688F425C5F453449B11524234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/09F4D9A376DDB6BE1E73593E04CB8E854234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/0CC1A7A85AEE6BE30862E176C9E57C6A4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/15964F0B401001BEF599F2E4776F3CBB4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/2E8469C060C87841FDF49D194310781F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/32D39C646616C734C492A964B85F90084234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/43CB1A5F1E3939FC231D7BC1782EC8534234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/445B1F5065FDF5AC11F178B59D23C96C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/4761C81BB8B86E4C60BBB9DB7F1D6B5C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/4A1EEDBB213837E19DAA4E5C494C8EFC4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/5674DC583991D33CB701DC26C374A7D94234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/611AA00BADB5FE57A27680962EA01B324234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/72543A9CB7A748EAAE812564510313DF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/7F4C3D222E8A3446EC779D8AF22905814234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/852D2EC506B5A30B409F5280E1666AE64234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/8871809795A90C7990F0293FE5FDBB4C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/8C55CD49CDB4373D11001CA844ABD1874234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/A17309BF5FA276F8C2FDE6BADB2726244234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/A31C7A0824D277E4D150BB687CB3D27D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/A9997A77EBA561AC5DC567F426BBE5A24234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/A9F4724DDF4E401785DB96FBEB7EEAE44234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/C630EE0722FF84E219F35E63A84603CF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/C79E225D867290EAA7053C20C84DEC1F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/CC3B313264565E4CB32DE0B3DBE155114234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/D2D0331B4BFCC5E4D11BBB2D4F8220D44234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/D52EBE0E5A069D53B1692A08908FA9AE4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/DDF61950C6DED1D1F6693063F7FFF3AF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/DE149C594620EAD33F825D2C744DFD2D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/E2A52A7C269FA0C93FD4D1225C8F6EE04234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/E916A642A79FF5FC8777234EAA3679174234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/FD25B0786C79644DB8919AE1039CB0C34234D82CF76A2559C5DD98A6DB88BEC4.pygtex delete mode 100644 doc/src/NeuralNet/_minted-NeuralNet/default.pygstyle delete mode 100644 doc/src/NeuralNet/ipynb-NeuralNet-src.tar.gz delete mode 100644 doc/src/NeuralNet/reveal.js/.gitignore delete mode 100644 doc/src/NeuralNet/reveal.js/.travis.yml delete mode 100644 doc/src/NeuralNet/reveal.js/CONTRIBUTING.md delete mode 100644 doc/src/NeuralNet/reveal.js/Gruntfile.js delete mode 100644 doc/src/NeuralNet/reveal.js/LICENSE delete mode 100644 doc/src/NeuralNet/reveal.js/README.md delete mode 100644 doc/src/NeuralNet/reveal.js/bower.json delete mode 100644 doc/src/NeuralNet/reveal.js/css/images/cbc_footer.png delete mode 100644 doc/src/NeuralNet/reveal.js/css/images/cbc_symbol.png delete mode 100644 doc/src/NeuralNet/reveal.js/css/images/simula_footer.png delete mode 100644 doc/src/NeuralNet/reveal.js/css/images/simula_logo.png delete mode 100644 doc/src/NeuralNet/reveal.js/css/images/simula_symbol.png delete mode 100644 doc/src/NeuralNet/reveal.js/css/images/uio_footer.png delete mode 100644 doc/src/NeuralNet/reveal.js/css/images/uio_symbol.png delete mode 100644 doc/src/NeuralNet/reveal.js/css/print/paper.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/print/pdf.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/reveal.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/reveal.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/README.md delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/beige.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/beigesmall.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/black.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/blood.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/cbc.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/darkgray.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/default.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/league.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/moon.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/night.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/serif.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/simple.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/simula.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/sky.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/solarized.css delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/beige.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/beigesmall.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/black.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/blood.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/cbc.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/darkgray.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/default.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/league.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/moon.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/night.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/serif.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/simple.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/simula.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/sky.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/solarized.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/source/white.scss delete mode 100644 doc/src/NeuralNet/reveal.js/css/theme/white.css delete mode 100644 doc/src/NeuralNet/reveal.js/demo.html delete mode 100644 doc/src/NeuralNet/reveal.js/index.html delete mode 100644 doc/src/NeuralNet/reveal.js/js/reveal.js delete mode 100644 doc/src/NeuralNet/reveal.js/lib/css/zenburn.css delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/league-gothic/LICENSE delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/league-gothic/league-gothic.css delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/league-gothic/league-gothic.eot delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/league-gothic/league-gothic.ttf delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/league-gothic/league-gothic.woff delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/LICENSE delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-italic.eot delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-italic.ttf delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-italic.woff delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-regular.eot delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-regular.ttf delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-regular.woff delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-semibold.eot delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-semibold.ttf delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-semibold.woff delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-semibolditalic.eot delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-semibolditalic.ttf delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro-semibolditalic.woff delete mode 100644 doc/src/NeuralNet/reveal.js/lib/font/source-sans-pro/source-sans-pro.css delete mode 100644 doc/src/NeuralNet/reveal.js/lib/js/classList.js delete mode 100644 doc/src/NeuralNet/reveal.js/lib/js/head.min.js delete mode 100644 doc/src/NeuralNet/reveal.js/lib/js/html5shiv.js delete mode 100644 doc/src/NeuralNet/reveal.js/package.json delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/highlight/highlight.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/markdown/example.html delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/markdown/example.md delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/markdown/markdown.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/markdown/marked.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/math/math.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/multiplex/client.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/multiplex/index.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/multiplex/master.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/multiplex/package.json delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/notes-server/client.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/notes-server/index.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/notes-server/notes.html delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/notes/notes.html delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/notes/notes.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/print-pdf/print-pdf.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/search/search.js delete mode 100644 doc/src/NeuralNet/reveal.js/plugin/zoom-js/zoom.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/examples/assets/image1.png delete mode 100644 doc/src/NeuralNet/reveal.js/test/examples/assets/image2.png delete mode 100644 doc/src/NeuralNet/reveal.js/test/examples/barebones.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/examples/embedded-media.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/examples/math.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/examples/slide-backgrounds.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/examples/slide-transitions.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/qunit-1.12.0.css delete mode 100644 doc/src/NeuralNet/reveal.js/test/qunit-1.12.0.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/simple.md delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-element-attributes.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-element-attributes.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-external.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-external.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-options.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-options.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-slide-attributes.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown-slide-attributes.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-markdown.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-pdf.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/test-pdf.js delete mode 100644 doc/src/NeuralNet/reveal.js/test/test.html delete mode 100644 doc/src/NeuralNet/reveal.js/test/test.js diff --git a/doc/src/NeuralNet/NeuralNet-bs.html b/doc/src/NeuralNet/NeuralNet-bs.html deleted file mode 100644 index 77def0bb5..000000000 --- a/doc/src/NeuralNet/NeuralNet-bs.html +++ /dev/null @@ -1,385 +0,0 @@ - - - - - - - -Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

 

 

 

- - - - - - -
-

Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks

- -

- - -

-Morten Hjorth-Jensen [1, 2] -
- -

- - -

[1] Department of Physics, University of Oslo
-
[2] Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University
-
-

-

Oct 19, 2018

-
-

- - -

Read »

- - -
- -

- -

- - -
- - - - - - - -
- © 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license -
- - - - - - diff --git a/doc/src/NeuralNet/NeuralNet-minted.pdf b/doc/src/NeuralNet/NeuralNet-minted.pdf deleted file mode 100644 index 96f40fd3edd4952c69fc4d67066d16cbd07e7cdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 591416 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

P^Rt?40hN#bUAEa^JSk6KAJ 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�a)Is?nT_Uq#edP0y_0!l_-*c5HVk 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

t=Kmj4juQ;1?BqPZ zd92DI5g`V~8;Ty7bJs}k@sXq{yjNr-&r&q6{3w2MzvAeQg-Gn&Zz1v42neUg5rE(e z>Ma9vBL?l&oA)C;tQQO2gyGthy^&XJUX6=RN*$DQ`DZ!pB7?+~W$D-O%f6uW&{B6_ z)^A+{Hd*4&fL1V#nOm1m%LSi=SRQ=-Z^tvhe?9#FV?5KKb_qBTV-eF1^^C8p7m7!I zqP_7pmZPM8wTu;S%xE|QJbfIE@THDf*HXVj>ZLIqDdYDPDUt#MaC!XhWy4wTOWz+~ z7yZa3viUNy!eYHXV?{XWqd;J-zOSdIr`+SaU1@h6GhF@+y0Qm#EyAp< zOVBx00-z&cXJ-uj{Y%`l-0|K*hJr)7=M)?poz(d)G+%um%UigzRc+?9cA2l`Iz{YM ze%PM~?F>nJHxIYWxm?tEG#9VPH#NdiTfyU3$t;^k-U=>n6D-b-#>!+9N1pw)c^+TC%>EGeJNc8-ay9Fb+ z+rLWlG|tAQQh&__#&ko3$s)btTpw;@ece~@K$i6#!M`5o>}aZ;K7d~*>24$fTVPVyLi}g*)$OKaBA;zs2UPjta9Vr9!a2(m5Y66RQ`t7Nwrac4DrQ2PQCHm`BxlogWGV4FLl$Vwk z?zy|Zj4^c0?UwRjxUVcuBVN)jc=o9pe9fK9jVeV7jQ!-E)1K;62qoQ$52-PttPS6& z&3qeho8`u+Q|JC%);3Fr2wdOUm84GH*Cl%8^i8@qOR+uK@J-DA5V*`tA5J@WTTsn| z=GZt0?+RqZ-Hol+0Igrxi?fTQ7W13^eH$9&jd>#8^N>DGVkj}WVEi>(r4rMD%04y;>FQ<0@T$I}c*K*&#(mfVkA*M&%{7)du^o zA7iC+Fy+xHONVp8{ST5ofBw&ru$mW-?c8OfI8A1#cm_61Uj(h^)R*wAF04tiQ|_a1 zngM;BDl3853$5O#T0`w1GK#JJ60tQL<4j<^>*_MlUDdhyF@&<{^K3(SkvX@X@7Z+`p|KZI#f^SE^zj*9qy&)mmVfb_sGzC8sFs6NoJ&ppj>BDGGu&yE}P+a z`vXb2=hhKeh^)WN=uNgnaw~MyR=Fg_-FlLJE_hHBO`r4&cQ;YZ=mc}*Po~A8XG?MR zKr0?wXS?~=ctyp8^O@9@*Q?Mg>X{d5)aDAzd_J{SKEK?1dK>;@?n3(UN-(;UlPta4 zjeZ@iVw0;(L_4gPMu$976L|VT$+o@Oj}D8Ss~9SzrO` z*vYfZB{gDFOu3)Mi;yDUzp7)teZx_h@ay15MjzQC?JMrlAlzXebjZMykbZ4Sezmn@ z3L!%s^_J*)xoUTUiZy%tS@ht>9!(!tYPf_H!wIIFdUXQsh1-VMjny-$7)wwuY1RL3 zd1=UX3>t_8m_-q?3;*HZ3(-1C|2*4O=AJdCRX<%k#F*fgYt*cs$MV+T;>paKJ|^kP z?vcz{`AaIxlI?v(jG{2JiUWhKZIky`XqrpiLRgorpW$AL6?@402ltI1e;6a64OK>X zH;1lPLIlZ^hD7mrg^Ef$*3eftGrzsYXZNUAAZPyJ%xJHAzQEu?hr0p77IAf0<^^z7 zrG0PdpN1NZ5nm0*hfk@`KGHBsy*(dXVTkvhXEc`q%~2GI33D%$&xUgy-zBRP^HTyC zPrqMp#R3Ncx(#fd>zfxUDMA>956j=)k2vO6@Oge@i+x87MA;X)3NlKsBgNp)i2_}@ zU|C}e^ZFV$N$#$q*{i`B+9GCmQLmf@rRkR1g9N}KS&h2W1>%;!HYqB&T*aD&;;kHSicW>KJO9Vmm!}F;QTN0wPic%{x5iMYf07z6k}3JRcPg(^v2xfZj__Jb>FnYvaP`Ar@rZ<5a(ITdH04? z>p7cVPfwsXPLNANB02o(GG}qHK;Lfo7;FwkvBI^Ga7eK~6{|KIB#^ud3wWg`K#&l0 zTU0dpDqTf$N*6$ZVGJwNadgjjYcSnS^>U_NeVNd-vC|L0)CN{t8<$lh4jtn2>}F5C#42$WbFe7Xm@mO-xeB%kl2FtBfuAJsXrZfC~AP4UDW>1NnrY?zLyS0 z4R5!8D!fZgopSb;I)`|8iXh1zpOffGh%4aM1H>xY^{X_g{ASMrI1-s$9N+2}ptEf6 zpGv|_*?^@M&J0$vwkn!2@p5$U0cW}CLjUMgMl=ENAAmK8)x`y#K9zG`EB7uBcA4P1 zwuZDX9!Z2>M_i-`5}~~n4^NJgyh4@p_YMUiK|Y0|+}HN>^VP(s_kI+Y!8UQIlvQ#l zF{LjgCCMP-UY2o7jGD#$&c1Pt+;DWmG<~>^<`aiV#8vVH6^CGidG-qXV~+m3*oIrJ ziD8`|LbJGce3%{;<=rW#V|TEpTqZ++w;h=$YNvQNJo_2O4*Q)p?@vET?EuJxb)KXd zF#dql0ya7UV#?!|ucV{g&ty+L-)XJ=CiqhPM_ZTLJ;TILc}1y_Q%wYr{hzsv60{57 z@1!Z!hIEDXwZ^xGA}J3$-|A14kx||1(n&Y_Cl41DW}bZY{_7IJ<-=JvdM&Jv9rm&S zfV1T>v}Ku*4GFBHEDJZ5=Ct|cjyJm&2PyV{944$t8ZVq;W~wA)bleMIojKYx1EAdBiht|Q|M0b!W8g>3R`f) zq6f6zm-|1wB>%Mmg-8qS&~Cw`9@1mE7N4bcy^DFGcQNvSjc))>r^ zOw$JkGE(xwMExh$`_D^3&2Kq|7@;N>1*UUYiZO8&Rk^P2ykUAjpz7&du)g2dw}Ca1 z`A+9j9)%@?W0@a;d#?Z%P32*me`lGaJ!0NX#xj`*=e`;DX;SLAM&lI~o32Z;<* zqpx)Tvtdar%rl`q@N6l2;q|ew6Njj)QHblh2#v^X@o}LwE(M_x#I;Y`J`i; z7JusOzwFJn1kPHvllbk=+v>X3`b?{94s(&RhTeHn*E4f;eKOEk7%^eJb)PtSq(xfW z4h?W2?9M4s_wNxa6BS+|W2~QzzD(^w5bVBS7$wsfch!Cuo<{DZ(7=_5S>09aH<64m z?+3&m@4t<-XM4PreGA^-779FkKBzVF3Ro~3nY`Q!$@^?{ZQxpUd9jse2>eJmDYw1A zS$!*W4iMF31jaL7(}#IX^GSl`$kV`62REKvrI?JM^FR|*@S-#WsqQ<&#SWrt!P2;{c{z{JPW|Bb`43H3ug6m9 zIe)gjc`#8~XLGY|a%+1_Uh?#<&0WBma_=EcJ}0U*WljL^a57lu9TW2G{dwnetNZS| z6kSMya8nVYA1vI|YOLGWyxK?@F%ZpGk!>{SIRE%t7;EY_z|y-%;{U8k(lqc2?u^oFfa=#H08}iD)T0e>}23$-lzTFf@=Qo-}qJhdwB7C%`0~iAuuL6^7UM62OvqYVCz6#o7=q5_g1h%XoiY0%fT%S z`T=0274pk5*sX90Iy^$D^xe&>A$X|F@R0WC-q ztSU**qdosEf~otg5F<17N8?!O1(_IO%w5a#ZBZ&pOY?+GRTm z619z)!M_bI#@`>5OXM>JXry^GbZx=!xEmit7`?|QWY^g*GzYl-pjF@Iftv+`@AOl56$Owf9@yH%FSHTF=m$o04TI! zW8vmSHlEg6V?CKOvTr$`KT>-lmTe$eQ*{Zf=jY@z%++!;jM+Wt{8^>ylUjX$FD1ED zW7Zf$uEdx}w=YI8NtB>}Xh|FEtZdTb5NAyC1gFukYLZgcO;i&Db&|*tf z$qfrcJ)^JO&N%psn5`HT+7h$ehUhY%9F+X#Yq@g4!+7q7>legKN5dkjD+Wpn2R8t> zUf1ck#>oSbd;8Jp*YTboeG2rybGbyIO900rtod-#roWNA%LA0^c5gVzSPcwL~y}n@< zAt=rSj``-z17{|wc5Oc=^g1vq9BFT_VqtpQ!#1|BXNCrsPuLs;lhN8YB3)Q(ri*xY1$_gk@%ua!`RFM2zk=r8gxL~;2aP1$S?Pn-3`yg>xr)lc2siO|= zS=BLD@A+3G^rckeJI+sQY3dD7?R?mA6qVKkK(gNqLHD5fHv|wuGb&FEZ_?LjSa)Ce zhsHvZgY_A0vKc}C^-D8IJhVyYk*Xj5-R8>>ECUK1PJq zs?n<_daaEF#r{PQ?yj%<80JbFetzL{a*wedMSPCGt zM@ix?+<>L`q$}}nHvpPf17A%acG6j{ORm;VW|f@}X2T?_39V#m zMw8EowPs@h15_C@&!SBaKR3aAqI*^A(G-oK5|a14nEG2}`1beZBf%%EaaRNvMOtlad*Y^#h9{FY@u)l` z|IQX%1E1@S$>W<+(2ub;bNAw3Ze@5%dnxQrp4zDF)Uw5A#IOVZ(O0svIVv)9(&g=e zmNjVAf+QZ%9zv2&orLR_Tc4#?!arnl*?_lt=tLDqM(Jj9G*wGBa;lqLygp$xPYP14 z&o_*|7Ziip)gNUZ^j65;`YuOrWj8xnww4fS>OWF>8)@XqbB9j~H~|`$?hm(1iGXSKU%$&FJdt8W3FGPv$hbMK>DYoovmKMBKbX zdQkLzZZT0c2Z7@LUi-JEqD(xJO=%Ha6b?6n2)oZ=;~hzyNx}{nJX<>ORyboBwi@fT z^FUZ2$n`U2Bz2zygLx&Z788qee=eZp=3Q!YuNVrOkn*#Z^N?WK0`Dwv{93!MhAwFr zPZ4zsxnqn|q$5AxN=po@{`uF%T`LEAGyOn1;c(jBA|q!!*cNx>k)xr=zV5(}P}ul_ z@hwAR?D!v=3`^MmO&980)_I(mT)tD%)`;r>mORjyuf@Nsv(Kt3Birij%;MJ+XZ%*Y zbo2c3#Y1xTfYWUSAu*)Y*mb$>oM+;#snAZ^)kK0@gZULk9y~DO1R9*VB8*aR_mg%9ujcvW;0AQ`yhz z_ZHkhU7$D!bYai`c;w!h-eX+^ugYx{DID0*5fa<0SgaYM+y&4>DrAK)$Xy^q6I)r| z_TobrSVV24x664i){G~$OY(dS^I=yRmpkt)${;eXw@Yiol<#sX~~Z!^%}*Uuwr-R8unhXbMvHke0j*R6D9tR_Hebh z+}dp}toQKn`;)-Hb(fsrkJt46J$dUmC~b;MlK`82#ey4{HxsZ3uz9WA@DL{5N%sKP z^l*S5|5^7ty93Iu?+~FF%usl7Vj8`V?4ip|SLR)=}ln#hl9?f%+gian6oH<-CbjD{9#cfcs>A}D=%DnH#!$6Q*(HP1{3j_}hooeOcx(2b8P)wqoFZ=CM|ReI+I-pX zw)gLx@Tm3cjXE{0B5}%RpE}l2WZ=r7>jxSJ(6XeZSW@GCQk}zv$*vIll+W&gYuo(~ zTCDhw&Wpx7?K8u#_7DG{*~t&*tan3aQ8-DB1Y{4AqpKPg+SM-5FC???CMAswxJt#!>-5W6J9F6F!JB2eUx>R1soGdjSU{4{5nn=jpP_~1EFhQITo{QVF1XZZHn z-DPflYT-(2acx9QVnM6zXYq$scZT$!T$}QoF)Ie&z^{s!wdrcRr6kX`0?{q)bZ~`Q zvMkZHtDW&{l4Q!=$x97yOKwQb``?TeTe`Co_G@MZ(jyQ4UFUHAK0M187DDJdd4PD~ z%Mia~HrVw;=`K{QQ_ElkG50g$^H7C}U3}?z)*|2z>YwvJ0==MIWbIB4m<=%t0U>y7 zsJnNY@<0Oaw>A(4X+AMe3&(`;Db!h(lh1~a1m_IJOfR@(C)0=*%>cG+QMp5R$Z}Cu zJnsJcSHOnK7RIx{ymLWNGo{XOwKl0J>%1fDOd_dMa$NE(uUb=IVw8Xs*vON3v2!X1 zU70UGU>yZ%*Y7xbKxeP^sZvtZ>m)}5w_YKn$w0dIs>#kcbTmcy!urYO;-b&j zrDxk6PRX<0KihyH<(|SUvKe_GX+KDZXJ zjPmmoKKm7~W+@L?-+%u@(WtR^O33s#Duh# z7+4x2Hg$v6`^X}Ort}{gK8x+hGUPBeiprxaecEW<^@I{Z+WD+c;@&%x#HFzQv1c3G zz8S{fC$C{#Zm3uoiH>w2U#uZM$S#NIBU6_D^JDnKWRQ39$5JOWi~X zBV;yj>1tG!&gigln(j;bgpwTW#;rpQj}`OmM(N2Gr>qoyvaUbhYWniL*L}0N9b;V^ z=g@0;y?3+kO4gV&F176>Todv36Y=)>lpw=&Z8hZ|noBm&lZ}oa<YWDE;WRSW z3?XF}q+bAV^)|)hO#g32_XS#l@_&ZhbN?&!?l}S;e*t9TO@SxF(K}-7)^>hZ0D44z zrv`51p2HXMH!Ex684Y)+g2DL|y4ylh=PrkF6<0 zH+y}C5M)?v8D(<~!VYIWD+UCL13Qa;@YVEX#n&(O$eL;e=jM9G#UtF6s)Vlj1aV`p zCmigu0C{y8q-z@!b3hWS1t~AzCKT&A_kGn+B-v7X{1U~Y>4#^5X_(b-=#7NHsFwlF z-o0o#PgMyll!S`it>!eSsyC4G9<9U+Vjc&sDv(?P43FS+U+AlsRNoI^o_|+sH1OW)LF4l znqoJKZGo>oy34e}juS80d=?@@N7Le*P3kKygFBM8JAkh>yKQ}a`O|=H6Y$*|$P3!X zx0jPwqSuAnV`nI)%jj7Q?)OIvr7KxQR{ASev%Nr#CMJzLq?X6e@>nWOwjr$!`muk+ zq&k-a`*>PTQ>z#nhV*AZmCp6qTdh7#?TKuX4wqe3W`om zBi}wP&{;raOuYIa&uZK-2~@|uc;WR{pS_idTherlM*T^D4jg#svY`5qUq`loHWD|# zlaNuRb0>L`fsAu~i+^c zXHOW9(K-BaFDL+y#CI?`s7AK>(<lgVwGQ%yO2BJhPp%R+ zf~&g|PCQ^CuU3BZm9H38mQ;>@L^_LU!>^w;Al(#WEs~v~JoO!K>e|-MJ?u^D8-3gi zw1XVW&+r}BBVg27uo#IEgXIEBRHBH+x0O!Vl}YG4Izz(H*>#P*>!|l zEI?QQQ6rR~k6GmHL-4oRZ1X;8Gj^6R*?!#1hG4lI$s*~deW4Qq8WMLCq^^j-*Y0ofqm5x;fc7JiBRSW>rm0XZ!H*j$QN>Ev^U9(`bf za{v2#?j{^XQx=b;FEceMuVWb~kV%RbaTt@w*;?N?C0AFaFk^B9DhOM+$5Y~iTpaPW zMsFSSr-z)NUaJg`GH=kYbS6J*NS}STpW^&2)Z)vy1N7R%v|9bQrB}CtRb$#!U>!Jx z8aj$Dv8S>5nbnh-n=1QU=WaAjhO22Eoywq#;pa&<-ALZb_DNjZ%b7==3q)D3Jjik3 z3#BuI{$hVB>%*msuPx89e7ku3*Vq}hfApAW@dm9|UW|c69v*lr$=|<&yjXGM6#09! zd0X=k%_<}yocZ!ZJc$vu)cRe-4nHJk=x zXuU(QBhM4XmkjIU zyO4Ze*DUJ+@u)z(C?|S2$T7Dng2Rlr^C5%K2tboAL5!j}1H5@XkC6hJz5*08Ye2i{ zsUGYCFpAuJ(ToMxd^EPD^6VB_;h*)T#8;wYDOe^eV^el zp}pwpv6e{43+L^n@q$CirNGuUDo-fOYGxqhOGy=->%GPF~>CWh1l&ley ztcU+`F9&2-?P=#P(|P~K=U=+adLbB2lcZ{H>(PB)Iam*=<}f=mHm?^`pRxS%t){&( zVdn?_R)X7=hl3K_5k=zp2{do+{oK;jFSgzYv0G3*37cTP)btu7i7u0*Bs6PZ1ZWr@ ze?zT^jX;WE(P^zmoI8fl17QPVX&dUaPW$e&KV0HIQu2ci1QDIY;B%-7^s#!Nd+sajHRc@vYHoFKpW{$Sauv@j6Px+q8d4foUq|8jf8$?p{Z*0Y6^ zi>3hDUzPF{84sz8`QXJ$)&nuZUS3hX75gOc)DBa2QS>Pvy=nj*s?`%iWuvf8fh9Kj z!t`V+Z;Dv4jLOPbWW4T5QpQTsADxgK3Xt5$nP30V0N3+(3n5WM7Ox~e0jX`%*J*x8 z>mxTu$4A^BlUXu~nQs^VM$d2JzIJS(LIGaS@3Ok|Zq>l%>7s`huiW5$7bG-#Ep1>W z&t&rBYWo`5im<7efMjn~B(STEZ`F_7!ahvAG~l3gy%$>W$mne_J(uVA!#3g3o)8sk zdRb?qb=X;JX5)3pVw&`$>e62k`UD3JYA2Rkp`C_um&Cc%Mer;&l-s#HMd3yI`7}pI zMHji59V{>fby5{IT~~xg#f(Oby=pAo7jM_@&RzY~T3GjQE)a+r1)MbkMiz3%^W7)_ zp7@MAjKtexe3p&Ny>fZ~O3v>A2i2!5m7%ZyMD+5K%jninQF9S+o3jptCeS6;(|jw^ z%Nr-6)_-m1gXURjK~Ls)KMjNS)-zLk4d9aF1koT^)~RAm5_TcW4TY5p3=1x8@93mQ zC^p)bT{~Rjhg;?c+tgQk)UrUHdw%0`;PS ziDsFAB6pl>+qzrh$s)Mz_ay0*mtUhLF}kHEt))9VVm>B}PqsKxx{%>Okb$$hgrpdU z=8T62BePnjcbI*-UzT%(aLj*U7WDyJ({z#@i6HZtRwEP_LiRyUN5cEquM>LgCGVSt z!hsw@<3moh=YL~6{ww*(H(&>|KJv_d_L%>tA4YxykufuF^@Ale^=kP`UlcLkAq1F^ z!a&4AD!1&eWyfwIm2Y<3>XeCl>(hVxvi_Iw(5oL^DH0Y-HAt*J(2qo(nAiGn@ zchPN5KzKBTY~gxOyqora|I_6MfG}upkIuwkEpA7}us8Q_fluf_~J z-qtu(BKV;H6cPoh)mV4DE$5eTwu8nuXfp{BDnBLZITGX?~g9$PPu=C zvgQ08?aJAWqv+!0?`+`^ENkK64@9H<+jqB z!XF}Mu)hu!IbkUe&OU6txM_T4oMKMc*1@#pw%gy2|0OEKG{~gfeW(Q+blh-YN4jq7 zTazvzG|6bcoPTH%<~cn}c!Wod#qLC(R${1s8By8o9{pr_;;1fwf9w6cp*k2)`M_$o zzU_{7Q5y3LWl)CyhF>Vk1k;PHAH_JZ4E-c-a^hvTwr%WqBz*6I_XD+OyRWj`%t2AMk2Y{v;FFq`fm?*>XV`BK4F9uf6g9@ z_lBpI9^mVNk+=>_n}4sIp?OC$wANbYaB-l&_JYbhS^4N0zKJ)4jV7;|f_mU#B^^K)ZJw-_8zqBpr7f;r&hJkv(7ZUb6-Ai?gZ_#x*p6hl(?hJ&Vt2*5Lm>8L}y}1TVpVS|! z%^WS#iS0aCKr1rjtsM)JE7uQz~f;?TyU@;8847v zX5|!cZJltflc*!s3lE`+l7QdOd)hBtng|y7^O)Sf++LwNSHDZ&Gy824RA_4>%3H5r zf-sF0G*3nJe)>|Sr&8!H{J88-OCbR7+lCOaN9kj{j{(ru_U@_~*1z~cx9q0Lugr5n zw0Wv*_ggz7`nr3Xp^BMwm^Y4 z(gRH^VFLgn(!HMdT5^Nlyn4UmGM~CvHzBu!wT(PL^TV#ygvjPa`ErG>BVsMI2igYdtzZ2VxvuS)T3j z`P~!8E4(b%A^Lm*vH#FKRb<_;rr2L1m*cTK6sMWOzCpNQ*ri4j|L8ZqUQJX6zi%{N z<;fg%K$~~KpXw55FKCXpAJVv55VXu0#pM8dygaGaFhdCnrA@FiQAj!e^*X(b(0K%j zo^fv~!@CG92)N^py8t(vdmb;b?QPA39IuCrMxLIuyksLugU1j|dHU2sr^G$b`U=Y1 zvD8|}(fMV>Z7(k;OMJZkJ@bb&bYte9kFGirbb?&Z8&wzX0WgE+@5WeqfV|M_P2B{p z#w%?Kt&?4ly&E?FaXvd?u7u7>;tzKtE4Sd%xVx(Dnk42uxr=YkSB()}y&w2Tzy_p< zC9P~ol10HE$9B%_0g4Mrp_nw)H%d5M@F4CEofA=C;s9&hiaoKuY1GYIx*i#m(b(yVnIQNm1GN+qBu{-90G}Mj}`_ z2Td5|_hvo@0RiI;UySh}qC@RDTI5UDBrAq|W64@4BOZ2;XT8@g(( zOtv@STPW=XRg3?gmIhhVjH;oUzf+{Yh&3j(3KhTz#FyPCqIiw6$ZhH7yw8Av zmB#%S*2W_+)y`_+UXbUton)A(`t3uu+Y+VdTt%eIwkPgvAt$RFB4iJHsi%RxH}!GB zatN?)CTo0cS}QB61Io?s5m#5`A^6>hodfxPr_JU^*;_^6qI15Rd$r<-^?c64t7wH8 zi2G=aHUDezYN~uqYmE8#Cr8jVTeX85oY~M*oPIgqT0g$O7oA8L2FOmPICcgHzz)k5 zF6FW{-X8(E?As3%hV0`z_}A^(C!V#F7)srXf6?fq?P(W$JIxojJY`FF;B8ENS(g$Y zQX1FcIs&q2n-G=`vmV(J{Ns0%Vb<4F@P@SA#*=gO89U$4pgfe1=lC8I&iG3o1o)&&NqUV@q8nC#X#!T_nu|iH+K%3N ztl+Ez4BG4VqBR&tSaFyi#@Jq5Hfe7nNcpC5>hW#W<3M##47-`V72KG3`#O12+injd zyyC%+(I|8Q1xKX6o~pK}UIk3o|(mjL&w~4X!%ryZ}Mt+sn83j4{w?^8JG394?~y3Ue4o?+p=z& z+yqOSn91Hp;Ww*X7|%VvD&p+iCEx9;wl_NH;O%~rR9)qKemm*AYC{l{R1AIz2)|qs zVynD;F&W<;O4PVpPA{ z!THD|_|cd#`-aq~UIh@(GO(Z7h86*l5SSeEaSF^ZtHT#nuY+D<-(FP#&KZshn2b=#!qH$h^pT z;ZHZEtY2>#dh;&A{KuD!4`gFI56|*lR2^!2a~Tx=6%t#Q=^Y2+aYfe7lJ&K{)sdP9 zRQU|DgdkiR*f;-;ulIrCAQrjk2_`BlDo7LEfg%$(h~6e_C^@c8lTU`83j3iz!}*rY5gblLzNL> zt$n(h;iUqqT!}pnYVw)c({}OZHdD#y zhev;oqNMT|gfIGqsVCQOPcJ((qTdt{Ew+M4PE>b#C&Jw} z4uRav^|vPh^Ak7j?7?v8S!B_iyRXe`+I_)KcU|dIT6+;BQ8pi62Sc)QH;DJK?i=H= zt%DRAcQ!g9W&r`eHF9?QiyENxWcgLE#GM_Aa7_zhCXoQe(>2tY?u+>ka&3D5l!vPQP#a4S+u{Vn{Le8}bd2TqupL5cqM} z;p+MqQBoQWc`h&ABYv?KDZJU-@#gn)jWj&&Q(Pi%IISGxcyyQy4iuBeM-+90LvUvh z!xvsk^PQibfVLq^-aR>gKinaX_KmV{9If3W=_TRsME&*IXrMm6(Y0abT3RvlqC*s+ z2zx+_M@Q~s^G|2MVom$8Q+n-={nOE&P_G}N)+*XMtB9C5=f#sOxZW#g% zrN6j?^0AckKFqGyfJkS*<~fbm_KqY7Mjf?5s-+7FVSb%+>)5tK_fbzUrBo(lAq8^aYDy z2f(!Hh83h3k~1J(qqBQ`?B0E;=TuNU3s3)?2)lanEW`vze~h60d#B~M#kCA)Bh3i~ zF8@INW{MuUV;LP~?SX~$_sj*wF8e(248G2DKe^r3Sz&Ef2aQ4l-ii+PT?OgL?mSUa zX|lC)it63*_%~&ntLk4i&#*>^z?K|9fE%M@5v;9H9*TY%G7${^u+}_HF?n!=JCkhm z8!`)2@|J8^iri#ZHu^d)=k#^5a(hb-FXG%-AMc#Rm~T$Q+1(mUSjB#;4AN)kBPs7U zdy}uCW9kIF$wq*+U!PdSnlz&x6UTp(o;UM~w!TT{Och-E>@$^*;+d)8%|h<(+kE8P zmmSbNx1NV{dMgXovXESA?*ghau%|2(VKRw+ViU|&KcVvTGGXT6JiqGr;Oubzxkr#c zpS~4~2HZXS?b~`&yswRqbX0+NNWgvp#mU`7C#WZl_Mjq;Ygs)(BKo{4N0~j8nneY} zi~dtJ7hMG3I)kj&PkK}0ZdMpWAG83dSd-Rx*T&`sAJVW0TZJqK)5SxybrS4}MLb5` zuVicZovHcm?)E{8%&x-f{ZLD;LAkG@*RBdl&FEx5?t$>$9F1x=?~kk3ZF`!O$?Eb# ztEbn%U}hsEuakEUb*c$ho|;%a#UqnDf(h~gnmx&;b?!fl*lRwcnvGuP;j|fZXZKQK zYPtY>zl*L0lr^@ERyuiu8wpFL7Hx zHEki&+OeOzJNHbO==~P9J4H5U+AqK*05QA^%b}1_Fi#~BXDlYdU7L}+8X;^S6?O8| z(*iYlm%{{b*jV{jTIYJ@Zl3yfE6Qz!wAB25e|)7bAjum_JVMD+%fq}&t?nvc?wzU- z_0XD)1Coyd?m`je&o9y_8E!j3fXS*px1^^0{wKIHakYT3`0SZbe$uDfH)RVy^b7_L z%(ZfAXO>-iQWDvu$%O>|iMnhxurTot(|hVxnjeQ2#{w6cIE9<^XDOy%yf~dy-$dW+YZav z3l7LzmhP3mNr&3Gx z(e_prQEKzIlnljxXC-X{y$T~l-FxKTBE81Tb&DaRYmtf8= zcVM7cv`!u}1NG*fbee#pui zx3g7UpscH@FDr3tf(aaDal_uQ7+iP?CZ9lJL$<$XiSu>@p)fg#YdbF;n;Eo}Ke7e> zU);TCR8wucE*bNd7kIKuf~7v8R4kuRQXz3LFz`Z z@q{tg<(pM}7;v8+E%XvjW;u>%9^x*&aWP3AV?ALt=v#j?i|}^k40MSf^l%4|yyR}m z?j((y8(dt{;URhn&*+fhMAo7Wan4A&6wB!5v;^pHhIe2xRbNZoSCYF9_m6J>991#E`{a2} zDD$FAJGJUf+s$v~*nlQZEa*`AVK;U4s7x^maFt!}#OfJXL#%Raa4LBIU>GzZB72 zJtB{1cotR+WocR#ExXr`S zYLYm^e!=;^&sc7fg6LO=H^w>RAL0`|{WnG)rCZ5Z6fQa!#O*3{wQBw+Z_mcsf=8M# z^Pb3skK)%F*KHn^x^SPYNRI3|WMb%D_Xk*5@xfaDlP>&w#Hx!dr`%3`g!RBAVJY0QYZN zin$iPagj|6sLP=HRsqy8Y8vVbZ z%=U9zfE|K#|LtY#x$ojwyQdF3-V{2$(Y#LiDj~`3BSGa%-38842Q07YZC%)wnDJ5h z&AWa&kDndmGc`IEE@(bsxW1C)mYOe;XnwQHv~7NS;~37DhZZjLv9+8ZBq8gF$(98z z{ir{nJ|qwbFHFF6w*Opy24F;v7`=K@#rHGaoj;%poi*&_0|V$|!VG{9ZKEY$5=BXg zqz}N#TW4sVgv8pI&IWv;-T66B{sVf9eDrmuM83;AM5m}_K7e?ZA@U=^N$@bfX5T5e zG$==Fyv|)oyJgb7@CU4h*4O!tb(mlXVmtR=QAK$oo~9t(NFK)q-gM6W%B zm8FT19^x(QD>06K(LF+M%IIZ=eSyeYYK=!{VbvKrv6h|3Fn-)5aK_w!sx1D9*w7Zw zEq{Myxo|rXp+Sm77y$%epFSsp1N?2GH$m`mgpOg zx)6=3G@ujqC&;wk6J;N?J`pM3GJD8nzst@oKN2$YsAt^Ur)^j~Z^E5Fd(j31Kn;&f z$3$Cs7Kt+-w*2WfDE5t1L( zPaY1H?!AyO{2qm7PEQHF5}F(mP*HB_oNc{U7%G|Oee@G7UKi4M+o!Rf`_fh11#AQU~?`%AjG38tl`}=?Q@*!W8QTA z>3EO1UdvT~6Lv?wl(`1Cb@=T4UQW-5c85wX>uCw~k2z1X(nfD{bj805q5pLV^5?%? z#(=fP(1q%_yLCg(`;(ly9oVACmesUr*U17g8mrM~MU&cWkFt}Mriu=A=EzxJj&2pl z8WdleT(V~U0}?ks%wxqYagqwfp#C@~OZ;+_pUd8EZOkt@MS<^256bZ`M{>H_ad{o# zqq_9oKcP1WX)btQ)LJsSLSFS-)>xWAS=#qo4?h`C1YGb^Tc_lsW*y`FoZEO~cLy=B zGmNpwJqfn?-7o}iuKqb@W4;1TdP6wqw^17B9Uq@i!xf%LxqVqA);yAG2@VB4X$Fh8 z;3)c)y}FNUr-+9Bp>o~Jk#oz5eq36dLQ_3}XHt@hO`E~npY2uh9vXLtpW@Sl?Ik>8 zt(}+hux`a`J=+sWl}B!C4Gp2eU=4knP@FI3c{CpJu=(qRUE@TZimEN&lX1@p$KIk` zT=!T0Yhb&JRE###aF0uS#gnzb(Aido=7#l=LxZKUvVl47Ee{xKmBgs3*7(oQdDpHs zIh4hJ(9X)XWooKh9B=S6CJ73~e0*PSt64HyeWb#gfwH!PUm-X>nv!_IKVBv24v?Za zawI7n>8g{c`Q(-Ltt-uEb2J`#8(2!208zX8|CVnHeTPr8^_=xQ{zib~tE@U#+L?it zewPsX-OO(1*ENoXQ@ZMg@!4n?T*E#^Fwr3~W9+4)qXHEV5P{Z!Ms|xcVAx*DAT9<) zEsu^L$v^fm>*2Z^L~iN}k|s*cm`8L0*En_e{p+A^s=^PThdn~!~$q4p*{Ri~x8PJ3SR6YQB@~HVHQD}4x(qj`TZm2$SGz90< zudbiG{s)wA`s7o^$*mhL^F2VcRM#@|1u25Y$jJR~Ve+k26Fb#!+TZqu)Uh_Mf|bFu z)MUmraUe;XEQ~`IL7Wdh@XWLHr|f7I0dtP!aU3p)S`QD%@f_X3i?598_Z^?@8k<5A z-XRMBbK87|{?RxaCSGMrTmacm|6q{-!(c^Jj&DL4DiVyb;NuB`cc^^Bl*1HM#y?lu zqWIOhlI5Z0@Lg|$Y0EUM8by-qktJ(Xe4O3#f{%)RV=^YbWxUG{<0MqCjIz6P%Ed|N z-P72lHFa4d-_%8&n3~&YqV;AqQJ%34wQZ~opEH`YK7Ko%hsC{?Br92E5$~uhM*(es z*FtD`oU&}W&u1OEIo{J3hV1<_(iDd}tOp%G+MP!)WV-2H33BVdI@Hw*r=DHoBcHnh z^KgNn3IIoEqw{!p=djU@KcG*|Ki19+7{bcgDUL!wSlBJtXf{Ld^LRro{vXPwZc()} zFI3G_vUc}6xjH)fuP62RB4oc1CeYw0Nuh`~8J7W>j0I0O7j%r>fw<(AGWNIjWgEH_ z>uXky$$AB_IWV1!@NnCQz*0iM26rWWh256XDKifa!X(DLbog6pOyn8iCb?QnnNZI6is4wvkk&CPWvyCY*`C?&3n|36o8PHt6@t z1)zUUQOio#L6P=6MDmh( zAGKhhZEE!#1w8y0An`=L!l1KmPDC{H{>M&WYD}kd4|MctF3gnOVkjx)djqid&ci~X z%neg;2ZlLO15$q7_P|~XYbAend$^2fVJ2hhuy~=IPB+8m0q;`?^gZm&OfLdw48E{{ z?7-5XGZWfuW81E|Wj57Sb|rj{(lQWcUetceex~8u=jJNIEnLQ@_{K0^vuKCtZOi>< ztiSG19$3sj3V(&r;Qg$kpb`a5>zso}Lm^I1{+OH^ffm~5wYa+MleAIcl>skmnp|4Z zHxE0`TEO;<=2~a+#6KY9c6Iloyey>?o?hHpXW(+JM&(C~#d#eRBC_7f!sSg~>T)@~ z+?5aqf8rOP?k(FZfUhhdX;)Tq^DN!sghxxy2i4D;3sSOj7smPw6DUV5PP)4lqk9o? z+38o8cq=TW)!-X>ShdiKbTiH7T=x z5bmkZ@eQCbH85`5*zjRa;I@2UvbuaF&(wfur=hxruH^HB*`Z7&J^9ypjA$ku+2rN> zE5Jq-A&{N-OG1;+rh!n8azF{lnYv#u%BSYYsB*7-1w&eVHOYBO_g}~zTdT5CwG8ww zHmrYz#Dpo%XagZJ^M8lLoJ}YGbLkHlpi5$qQ9M@(S@y@aOdFX^1>to9DFH+50E=6_ zTXID`s0C58mkeW;vLfEZ7kzOV)CQ(KIV3`sa^dxhr%oF{Td?g-!AsQ;fX3x|fJ(e4 z4n~(mdJ^f%wv0$voUGF9&2;|r)#F{X<~XIljY)rFB;v8W{fs$NZ3GTlWd`24Q{9> zJNjjv??40xr=>!&JnidWwE3&)0Tv*9avWzs9ZK}3>NVn%uWlqz8h}ImPB@o&i?K_r z1POW?;X|RE(Jj`-MEW331ixQ&CMyJM#z+_NnuOD~d+0Q3u{QbKK479)C}{+!ZwbJ$ z0GK0!ZukSxB92U|b`!gE-|f@p@LR$3SD%~kwZEMB>_KXv6sq-=6GeW8$o>~v~L0`T4eR!*`KSfGj(+mH>j%l%lGXJLcH?;ehuo0=mb{> zCLcDt47%=X^JLgJ?7e7Fa-dn}y}A;i$Rv6gcE^Hm3f$#! z9HKjD2QOvhl4Yudu58`AZSC)%Upyo@ss)7h4l8#3Wl!5CPik*}4z$`BT}%*MZP&^^ z6KYn>$g|hp@IvM?^?wa{-bik{$0nlqSuA)WwB{nquDYAdgFl$72toCK}}_O?o4 zvbu&T`?8fuV4K#eNZMUrb(Kim0EcEyK1*hm-JS;m%w+X4-FxRUOrJ2ZzaM6 z$Hd>zy_sJSVN=_w5cont?nLV2M|<-$t)`bd0rq2|^?ggv%x%MMPrKHU-N=`2;Z&ANml6Pmn5M?%2 z^&FJa`bH4ZA0vt+JcPOj$nmE=EBJivWi@?Hw9X>6I%R5ba+K|*+P!F#aa{*@J)5Qz zq>OfwLrOBgM2G~Oc_IJyq0>J>!>`_P9+wUG8axR?I@CN)?AeF<>cP}re~Yv1OSGMTVh9)uSTyWOJE>rFt{Z*tr(GXa&cU;q8d&CuWFQFcI++nxl|YHF7Ky5~>X z=|S);dBpAM$2By0b@yi&=K2dj3EgqdM6&ND<_oD7#$@F}uzrM|HrqY+cfR|EZaF+{ z|F{AV8wFn>vjoK(HxcfmTSfZSrUd1foo%ki&av~WU>r8khTSN%d)XeLeq`Kh4I^-UK=!` zTjq-t8^{Mnu1=pFHpuL4uP*!omVyV#a|ldG{-sRH&#La|Z9 z-TmtuS}Xs6sO|7s`aMd{lQNqnBF?`@=i-(I*N@@&u4NHv(GsPPv6vHLOqr<{fX)IjJZcghbk2z zwdGUC8R_msB?)AzuFi?gu}!!|F5YU||hG?8os;vynca=4}3d* zW)w#M2b3PP^au1)5Mbjy;;*dO3kS?o!%zc68H{I)qS_DTh)C(TW_r*;NI=a)JL`(C z64=IX)UT$S5tLAxHUNWso=I@3w6zpJtT&ZSwVu72%6~`c%jX4_Tn`Q^`snRPt%_gY zq7|Ob$fz2J&Hrx04i~H&)G2(eP0b`NOP+Aq*60!6K+eH`N=U9+gL`dH1bHO?fIh1) zlVd|x;7$zDa7Hq_3)6<`t?9!s+j}*UyyJ$tbZcFdviH+C9iAp(6HGr6W&p3=e7YIG z9H&GH&mEz<*kbiN<3KhcGQ4YJ z07iH+4Jq1hQLm5F&!R8xBZ^BmQ{au&H;-TYKQ3UDKKssdU@e!}@Sgq1tIig|xc+dW z&`L7HfCu84wcVPQ%}v?Fw`QYY33&(bqp`7e$FumoS=bB;9^=pD?0=I-D;3BkToltE zT5pNzxOK0aF5;GAZh(yC13=?=QY#9lb+u8!t>qQ;Ua42C&vD~#N_kE9p6**ImnT>F zx9!y6DmKoAU2!F!zJ+?NS-0=M$FQqwUK5dK_tGs~1S%^-m)%1w@e3WK*B`R2MbQpy z`}YH-+(v3zL}QBVif-$Z#(l##t@gSR+{v?fFqt3;Y*+t>=xON(Z-8>bJfO%8QPD}r zMA`tsR+O&o6zllJ#9nMMIkwcMvM=YxqhX&b9_(*fjOh{)ekr+!NKsN5jK>AR3~|wm zwm6?2S*NsdaoRosSFb;?Cj27sx^}{;WS#FwqJ_~dd`ZzlSk7kCGI`Mj@}Q?sk=@gI zruC9+=I@s5hP;Wh9o31>ETApf4%DJo7GOBHkkt>9dqw0sLVnZ$b73+CQC3F2=-kd@ zN6Xdf=-Olao(-PAk(xgDO<}pwARQEeuir1C>+cIyVC7j09C~I#rmR5Q-0Mq;8`TsEj$!>FKeuv9$nVe9ZR#8 zaaJ^5cwam)#9n3EAijgh$S3_?7(=b<7`pCdNMl9%}05Uv`b2XR~3_9nktVj?V}6k3!pbT`CJ_2AjEo|%@xw|i3LSJ^|7 zTItoUAE)bomM;10mwt7zSeZQCoAb`DV&C#w^!t(}HBDm-C ziDFxteR*#}aA5tw9n1tRU9fFD+yNQcbX3=~j7t>E@nU_eU82X&j`>1a^9RI@bSedg z!T*)#^ReNc)9Qs1+`RRqvQ}C52WQ1rGJ<=udVXYjJ6UNVcvQ8BESt^MkmiBj zWETrs=dmA-2nqjjZDz3!py?<;+v(vi^yZXMtHFO@l zi_AUc=g#xn5O?--J|4jQQaG@gGG_HPsTzz%v`MduLz5%?mYJXsV|SsI9lIS`uK7eQ zejq=&BjyAaA8s>+YwzOg0MkK*QHU%>)eN_i)Vxb^0{7rdMKgM~YI+wxlzaREN$x8* zn)-}YGDzWOdckq^b1epzP?5989kP7n$wc4Iy*L2sKzS3L0izM%@?^vS#Z|K>FrojG zK)hh*UG{wM!%VbGx4QlM9-uPdglxqB0j=6W09g(jxG(t>Xyu%kB@;zAI7MG?s=@%7 zO+UVn9MeuV;rkDD&42v{;2C-O|J{u)kv>7)@yxyXtm6fEy>K{+Y2sPalE9g(Rqz-0 zrxpFWG^XFLSPuE$QK$8kt_mpEppgRKrM4g`k=`qxFD5>is2!FvJs2+XAr-!0_*`uR z5~5Cid)WEAO1q0piTfRa+CQ|uF{6iZNtOPdphA;dP1FgI{+wFLD!Sn_()4kzzGR)D z4-(DU9(~wOVMWN%YZ|W|*6f{gY!UU!RnC7V^gUUrKIVIL%D_rsKUN$T3ZRu5M48Y} zu;D<9>0I_@f~85#%(L0tS6px7y-8E=-U6cACRHB7$PilYB~bxgDf7zAEyTaA@B+P8 zYF`!gq(|?wn1a&L3{C??ZQ|??;ISA43?Tj|nGfbm48`@F0+In1`zI5AD!LerX@3ai zyuehC)5RCW-v?T|mbbg!)B2yL90rC%&leJv#+Vt2wy@fMa6WXWAe)=9~9lzkgWhkyP1KX;{) z{1)!=zxj^tGcC9fz~cbnn?z^-K0N*7p!W4_`0|Zp`!(>K_6Y<`pnm($$)_xy*42km z?dTlB@}6>Be_kx|C|1~_6VRkZU;pPN`|Gn28{o@=@%{s{nm;i*`iCe+fAP{z&=HHi ztyPfIAh_-rQ3>TF#IHc+AYId`w$?%S)SjWa*!6}3iy!vLH4LpiyyN@oM#BSSoAP-~ zB{SOdl{B0+$b+n+cYmrfb!&V}s=JW>b-+`f%lT?lJnF&zq{#XTdtb*;Pu9h}@9wnI z2iN^F*slx@23?O(qpotpV7c`1B}8VYUW>@x*pIeB^T+;E9y*J=In;ZSP&v`3Qhiz{ zg_}cOcPZi;M#!>K^m|zQ10~#%aU;j6y}H-(MY3EF7p5VKZ-z*N)|R}oxkLR{{8!KP z4m`p+iANSdygFnSX5IvzXEA}bqUof%=2Vs{KW)i2>1MfbWTKBx9i!Nv_&*Q0ev_x@ z$Aw6kPta(uK6xGtWCZi1$<4!9puA}#Zl$BUGU88jBccn-??^E4F)C0qCCn41&ddqO zdXe|R^mhgpK*?3<3BD_u-B_hA zRrdN@KP@8mhlTDeKK5>NVF-A^@Dwlu0;$jna!;n_)@zD~l#6C~1WN0xhf%C?wE@se zO58FDfY*QMgjffcM8JR9H2_8$e?W)KHd+6Oiv#472T7m*W_bR$cgko&Jh`o(G()!g zdb*h8||7+KySQ9GJe`feNz(w_5kXTLwmYUZ*3gY+k z^lFRH@dr{47sfa$0}ZfplL66q8z;PWL-CMSKcjPj_f1j&le{vPKlh5c#ce;8T2JmD z8{~w&C7h~C}dY-zq{_nq+I_3 zYx^afmV)X^FqP@3^mM{e;8g%4q_AN;dk&kp3aO#)%)4)WuR&F0QtBBBdy8(*KL~R< z!2jku`lSv@6A+@d9h-$aIJO)A93?Xltp6HJP={XCX7`hicXaTF?9YIBf(I6UJ}u7~D-;=Djm=#v ztEg!qC6O;JK^Y0R9UUvvB;){YihM34sBBpg}l5ZEr8Cj%;g9*3PyKm%5m~53V2gP0}=$e@ag=_~ve}j+p!9 ziC?d)e$Q^TAltufeU~?tAKcJmjQ=piE?vB*{%zNhL z3TAX)t-&F>NP0ayLQ!aSgic-iIhr6g3dhvd0t)TC4#8#A_BYl|#QApTd4{;trAkzu z_Wd>-Jkc*>IrxLCkT2D@6AYAY`#@429K`s>*H9B2-6LP_(=J<~ft=BpMm8<_OZR(a z`e#m*nX=v+%>QEb!zZq_G1p#5fNk7lHEh(jpaqFa?gtahmYLhG6YifepFzlsN--;E zPIJWNIu8k{8oQmok#`=o&_@K|PQHAAF6jow9{?tRJHxw7rBlKhurS-%09KCK&wC?# zdy}R00*J75(&#}i-yC|d3@T_ zwZlAn2K5b7{&&zL1FPp;j)en7S|466O8Wtmsj$5awZ)GnibLF&FD_d`RZJYR(xu|m2j`=fOCG?vZ99vw?1ftrU)&tgEiIv}|cdPp- zWElJUB#`w1`dK1NdyDZ0%`+cbzWRLRyOcFb!J%7qLqH``kXSg444h&!JAKHL{?kl+;JHTf3FCW^zdZlTOP$EO`1SrY+!;pm zl`>=L##0yAO$}}LQ0&`FUYB^ZqMZ~3dGjzNXH49j{2a4Zna5;+o9P#q3go*1)5w|- z89rU3G@pc$t0k9&H7*~lraNGSSX&Hz_Enz8t&ru2W;FyM+>9~a9q^eT4^;DhVLH)3 zn|hUpfA*eC_41-Xe1P|}By0ko^s(|w1ng1( zNrSptpar~5hEvN>WgPlZ1VjI+vcA3d(xo#Ya zc-4FAF>2E7c#2g0SWOq;kZD>=$n|LYxZ9LptsVpj%;tOz33Zsm@lUX8ZrSkHyEigJ zA*?phNi;D(ygDDN!R%jxe1d=ZBZDh9rlW4~jOR2Rx*aHg8WQbYWz7V0(VN%iA=wJ| z1{d?QOP)9V<(Bx5Ho%j=?C%o$Nxq(|hLitTff;N12~}ezfw|oq6)F3`Tt8(}@fL4J zlp0S|o;nCRidp`t*`^={`-hf&l7#5GEt>4bRG?i3y?} z`7-9!W5Tj~>k<4=k5PEv9(%U6r!hnNLv4 z`D#2{H{?!^O6 z#Kv#?W64OP{n5%WYxT!oes+6qi8H0A?^LhiGUHdm=;t+X{ACyRh&qV)9e?& z=wc)NL%c4=Q1t;e{cD3zi%%5??BxpPjaygk=qz9{)yaQ_$U-;d3+SFF+F)>N}y#Ry5e&Qj1d$H;>^v55-n1<_6MtFn7ot$*_)mHZuG|>Ed(plKIb}pj{bh3E|Gz2e{(G(PZ~ms>?k9#0kvGZ4fH@3M zEDG}CwMoNHe?Sk%+mb;ev-ep(d<9r%~pyny6*GcLtp^%o+uhk%DiC)ac-aE;G-}+v$#5^=rG`= zx1|#b>0pR%O723wB6HzBR?1ZN6@KjI_tkdT8N2Ob-8Vj1gzGM4{SFj5l9fayLJi)q zmosA;p^89PeA|6dSeG(qHf3S(;6ut?NDJ7nA*9RzQQuu#8Hb{ zI}6-}hms2JOpq6KPI-XN0Ts6TOFYUc&I4t1=C^bsPvROyvui(3uh0(QIG>zxaUp z?G-_7MUhw0r=eg6U0^DU=|)|DkNz+u2*7M1Zb&(sxrI|QZzhBAsPE&hDiJAvUPagJ zwe_`J7I%KkU?KP3_uW)&Yc{sCJBjVJd)RX&ua3`rUu`zII$?!Gz;iV#w9mr*o4(q$ zc8K#ZY`{fk@OwL03>J)`FT=B;AaS*e;3v~|W=ATLma*A$2Jh)Q$`#gKbKR-9f4;cQ zn#izIji)vsG_L5yz=ZG_$ua(wvO7a%A50?H-Dh}=WMisVR^D9_uThHT7?9D(3o`r= z?YG=0JwHCZ?y+sTa~7x|Y`0(jE^cHsF~Xqxt54{{mK(!Pu5GhJRc*%3ZtAio6hI5p zUlN4?G>LnZ9N&U0uUH?QV^|SNAiCkfU5G0{59gK67*y{=r$CjAkv)yZd?SDk#`06K zZgc~bMhxn0otd&iaetN0`M%oBYVpJA_c2Ae*AF--C@)*`d&wgW&vQ)pKa;Q9lSODC zJSPdt+PDX|#>EpJcOE<`brlhx@91sT#s>8f_S$%(hQj7$skG zg0I9Izc$;Mq=3#x?&BrDcR1c|PNn9)TUHWTX2AY-SpLB0Yom%sT1~**u%O>_j$<&T zwn)XRJOv;WqjdGMR_UD0sQP!L0DrDkQ*%Y#bFwZW93u~Lg~WiVpaKqU&y&4h#g-yPgdADgN zWz`T(NaC>K{yfQRfRIW&aWd8#G)UUZ7`Ue$s-ABT^pmJyV@qN<-Nr&fv^By0YHX+r zzno8FbAl#^jjgYGr0C@B4Gp&ui1XgXE!7L8Xk~`?u1$GSHG%dOy^O6BLQ<9k;8U#x zz6^uA_bMi;SvPsE6S(oe(fJAaSMmKlVEQ?Ng+WD_Z1f8t_M%1oI`*l(I!DLIICUxC zt4Z9P zNo$Y0zE#v)6nJF!iTds_cBt#5yv`yiWLIpatJIMhPV$UX| z=e3obr@2Cd4A?PhFU!k8ls_p<%2=M=J!Gjna$j5^(i`}A=>0WK-njR7Rs5mlzCh7d zq1d+$2E(}QIG5Q41`&Xw>16Z=gy4XSKu{c0ko7*>!B&&FLvp*}7kP3e&w2*DOR5|` zn$VsB#wBba4lt6dK(r&+wrrMqs&HW+Nzm}uJHcn|1I}A{hGeKCv0}=(aQjH<`e|)w zKEP_fI8ycpq~vbBBe*l(V|N zTbjx<<@#n*g}N=lB7GZs9T>d%^BeLl+?tnDTC*3e&B?R_xROY}cB&v>y&rzs(x1ma z@m{fn2as+a>;AyQJT&`p1j~v`OmeU65nbIWIe?hMt*&!+>=wzKD*E^ zU$-2=Dc6r62#@xN)AUsO%0*$NEw#@URcWFlJtHh<%hh@BUDDY_zSO)OM`CzY`pg?V zce_v3OiVe)Re9LL?lazX*6MtIVs$;y$ChHIrVT<@!}fdYbBY6Ur0%Z zXq3>+KTw2r$;uPmMpSv7nU`P_VEQ^~LIXHzrLHc|wnH3d@9rxsxkshy6Dtdtjm-1d z>L;l_C6%dO3er4wB#hyN`j+XWdvO`7>#=s@dzCv5-mE=hV-c4#uRGGldH;UpJKj8+ z+`ga6FuOe9?ON!_x+r3$04P_!-UMn3I$(IiprlbN%{3kT|!$*R;PR-yM2zbH;-25jp``<$}w z5+LPJW&DIHqBLlFdT{G^H)oq&W5TN@p&(oO);mQhSgTwZEl@+ekesjRpYu$j%0`B_ zam}l{J$qkU^+wBwK)cJ5u$pNQ;vn|)*Et68*$eVe8zVsuWBYnZ6ekl+D-U|@eAPw?H@wtjJH%~s9A~$QmvIKJzg5BGEw=WKJauK}5rBpuZi(Vf>9yRGjyI~fd#&to zD@1+t@=sesg1axORb+ZkYclm`Wqu=lPT=v+b(;N05V5A=+ukK?8Xns<_1{ZCvbB3y}Oi_M-SS6D6~zNbm^)uCISt7huQ3^(K4>+_5zvxTxuLw!+|0()pJ?A0^VhSv)k z3l;s1eMc%W-(+xdL~)WmhKM%6MFBa+KK5iuzn2F@Ntz7dWg(Yo3 zG;IKBMM6NH!6Wl;6V9XZZ2Ow7vC{FO+l$XnckR77G6ftS#yn-THz@zg&EljY zI$YWHm9~(3-*;!tYDCrq)-38V{kdllDO6+2#IypJvPOGcR%MREqwBR-p+)`H%RjMT! z))13Z;VbWK}49J3*G}q$Pa*I z8G-s8egpuMcD7u4DL$eN@?SqdWdMNY?>iHOi!H{2uxMUzGajw7%A^aZ(`v zc>up;+SACLHm@jzmHS*En)C%=?vAe28%e+kvNZPH*eKP#@Q=r6#j zbuwgfXi;;>1i9**O7JG$%^t}=y?l2@yu5Op%Q#j|oHtA9-pfnkxwM>tQAGX1pdpP{ zrZ_UvzRE?8t0jjdyDD)#&5hOl3$KWm8Z~vqMPvkgB}Z%f{D;P)GIOwwzysjQ1on=GE4Fqrpv3j-RK(Xk96slOZykby zb;H(W4rUuJ-Ioa3GTH(vk#fTt6OX(HRdda*Pkexk4vU(HXfFNMHX$~5azRJ4&a@W1Sx`PAEL%HPA^sla8Cdq7qm6VnN(}~KYK6svT9pYz9Bb+nz4f(5^K{2 zBOzeofSx5Oqdce`vpBq*&?dOu;Hwifml4_fUAEdo^qO}hm+A&J>CL@_^GJ*6bG{>W zq+)d-Rtlqg?ZDrO$37J5#4Yts(FjQ1zn6UjwVB4@6dD3Zo#q4bTM;k2NU_Y3tsnM+4c}`l=|B zE^tpW!_9Na$fEVv;Z*DFsJfPM>Brr}KcI0#pzdf_RP1V@ImY89kU;Zb_I`hM+pF@W zjqJMGfR8jJMHF^DUe2p*{>GJ|4E}38uj78toph5g1MM3@PdqcZ5%A^Oe8GCSaQouq z=;tfhP5xyc5w@`F{$LuIG{LYNegVeWkk}Gj=n}G2j9OGh<=9T&h+5lRTBLahouOUL zQBeN1)@o{)DX!Vw;A82^INz;OQ5%T=CaY0QFF?znmN!d{N4|kLL4InYG-KgT%QRc6 zw|;Q-kZ<7W6Ws#?D{QU5JH=>Sev5e>bxtPna>0t9e(vF?QG!r@ajDvw643}(ym;6! zozFfE7`69)R1juJr61dn^L!D~h%EiS|G55>cTh`HOKz$0BiAR7#C3IqXtbIID_sQq zoj&$or7zZ75Ed73T(3Jis!@GSN?ff>bRs}-=ntskfcA@% z(_-H)@B`A&%i$mJ4{_h~rE&W~DTpCXCD+e1UCWF6fqSF%q6h7$tlmV^l|3R;)t>2( zDw`qmmMIVEAeg1W?9sXV04K2kZ;r>*{X78HxxfZ*E#;j)U z`7d8I*h5B)P!~nz0xJT(q}h0yK#qa)8w0fu#Fx6A9+yg^3$QZYL}BD}AK z{@hWU)i@u;yl;g~zZ-{Yc+pcS0BGw& z&=e4Ep9Mheabb0lDTd)?&#IYD z7q!L*p7NIDVlwOpSPwL7BkbgZf?R%9o=pJnN>i;QL}a+i`B zBX`}RJT1fQDgMKQ_7gBaY{5nr&-S`a05*EHiKo5Xd3vrS9$nuat=E!D)f_YILF-d~ zi%EhbYWmx+)0ruYzJ`*ZK;4`tQ=VAAp^F-u9!Np6f`pKo+aQ==n-VZAhyMW$7?IvV zb^@XS5Z%)B{+TsV2xl5c<|D}U+Om;vY@TX%i}P@tBiuXIcxk&x?Eg;xeake{( zO|p}GFMNA{Z+o7H@2y-4LZg&Kt*;>6xX!wGRHYqbye=3l>-{xLq1#dlxg6=*@zB|; z+;VC2#qDB8DTP8uOTVLTGgx#P_7QMuJecoksAfFya;SWzF|f{DvqR#%-_CxF2j=r< z!WLauwm4jg7Khix>96Qm$n;;z7ZRYBx5Xuls`?ogw9tu$WG=*pAE^^#*TqiH53UO! zeol%QbJU({Xj}D4mO4UX6+E;#H$2n`G%orEQL`nB`53OGQ&xt&PchS=Jz!^;4hfxB zEgbR=w3gs0IJNKIqk6Gp3TLymoAKy{5ydo@cpA76R2^IpUJpU0AO?CI=|PKPSJg>v2xzYd|-Co ztw1(2BXzbpnUd~MmV2Mas%sbmIQMsq-8awlV6D^zFiBI$e&>VJ75fStVL)42Ha$us5mz5}Y9Lkzu@+o(9m+XJh z7jK?W_{OiePnzi6Y3~$NrNW^NJc&>Wwb)H_pMPfa=0NlC@q(M1NOCl`88fh+^J~%s zV-@JU!W_^}hMClCkYJ)KDK@cuD!r&I@r+IX{Jjte=)LwJW*3{qjRW?=wPV4^=dSpe z2IG$d<(l8-pLn}3nZ8U~qvkz#6?8x6YTfE!nXXDwFMr_e_)XKWC|4BWtS$}bmZne0N zX{G!ey5G(GV~g}lwR~6G+v}Y6S4R;rVi$nim7#7g>**Se&ztk6>))r7YM$=GK{&whH39x_ zFLDU5LbU+@wwL1)4+58sAqm5a1v2vOmzZAHHGRBGG@SbQiLCIJOigbvDUDqr zMi5wSBA0I{IL5%<0SLV}%XjMJe4htuKc00>yB6oqQ|9HP(twqGWbJJvegaAXYe6kB z-K&syZ6pBBKITjmkp6RrQ@1qzO1e4IjVKpr2LTHP4Yu6`a7a1v#(*SrPJ-y{ z&$7ZgaW;x^&auz5xdd+yvfT`G2(C}~s_=a5P7zWG=y*t6&l(b0_%4=(S0z=#@J%BP z_h2zE_gjM$9P^1wyjj(6C-af!N{zi@o4T_T`5B;Mj!GW?&(g&Ik35DcUG>Elimd$s zT}RiqUkY@!P3N`lSBm0B!XL0?E+I0CTg%@n8_zAT!t0g;$-g~CDDjc|ehFUzsn-{?2m|BhkhO$wR`yn{ao1VWr$-}BS2oiJ zL&f~)nd@;{UeA*~6i;Q77Nk5Ov?Va$8?Lt<-Vhzcvuw{4UyCg5_ZhD!3@m)IC7FIwVj-gI5;C3Z1+ zK%xQG6DA^T*ZNwif#1jANc+{gT2jyDlRcO8ZYc509 z$5ahfkBU?Y5;t<{VF$%|SQx+w06=Y_Vn5GD^q-Hw`y<6Nn%N2Kvq;AnRfnjD;{-nn zN%pOY%%2uT_w%o{2+)~GV-~>h^01oF#s&RY+u@J-gNJMLza2^)h6_@%YYG+$dDIu? zuVB$n7UM*Mh}c|YFb{ZmGS6uTRv zDfXtUhh8=}+*CEftpxWLjag*obIO@Ph2-S)o9k;uW?z%qtp0G-QLKo7GXh#Wo zZ|iq&`YF6AQP&GATD6t(l+;*E6MYMxOxZn~iquJwH`X$8;Dk@skM{Sp*J)stz5}zt z#qZnRT0!bopRW#L%$u-|_fCUR+xwxo5LAnRgmoi(z0;2NTsKbhXf^FRM%opZtYR6b z^j31=Wbbo8#}zHvOcumfwZgtnHd=gKz=9Q9KE1$mJSgnZnrwE~3dd5!w6A=I9fL6a ze?Zo)%edeawAb>k4N$0`+^%?WB8rA?X3-$IqAy) zZ!%PKZ5hLD+`92Az3vTLKWg|_8>0cD{%Cv-A~6&bM_9=2MK z3Ha8Hf@nwlXq#}B6`kB|Xb8p**Eo+2FVcRZ-3bO&j(_7jc=WiaUwid6n+UB&lX&l$;%hV)+Mi2(mIhj zmA1t_j6OE+IXr%_;f+Y{A~}-{_A9Hz@UkUVVmUBzU$DAJf8j;HVwD!8Wzh%ZglM%; zSh$p6I7dK7%H`cTgnKM{1ZoaI>q2u_07g@VcuOi2QQsojQ)K0PEQQDUWzgq>g@?-s z&0)<72^L-Q4!%g;+rLGO`&RRE_lAZ?r;Yuixnxe=m^rRfAqCdrkc6X#yl$ zg&kR$0!&aC!W_qt(d7w{EG>T7V`3b4DVCORcCeY8TQ+Zr;~wlzJ`uU2?Ds}|q5%AJ zkZ)b6MY)h%lYAnfq1e(fzpjzv8{gGH#uj zpbi7DnrcS}3BU$*+sI(QYP|u}(b3+4BTr?3=m@N?WYyu=xgJu~Zk zCZbdKTVb<`=CnlM z2SKLXPICw}eE%ABlJoD+83=z+2jm!NrLfn&ViFU)riOTrTedztpVRFdM;l-E>4{Z*8WSs6Oq=N5DEh_El`7;xu)4jvAsPxHD%`gS>dj(=Py6_$goAg zWmbp|oSd#W{Yb%~Fh;CgrG55&a(=3D);z2bZqFZWLX5DNisNI zpk1=tQs$?aLl7M%!N3{dd>7AfvFYZ#4RDeu15t^yKw-YavPUZGT!Y}-p>&*2{bBbxk$_` zxrgmTJW*&d0v)WMDn?&IYh&7Lw^xye5uy&0rJ8VgQ!%3#BYiI_lQf*JW{oG!sf`lg zo6U=y=KzkRAm}A-ewkJDD6YMBp$QPWBG^?!o~yMsv`<5yU)rGqgD+aX=DsuGO^g{V ztdiFuB49~q6H6f3QDDY_+TM#si6^gV;@wx9B?qP6(Nj9tGmQ97yFI;@Q#TeVFR|_a zIuYCnBimkLZ)`qUo#L3bQ%xTVJuXauj+c5Tvu|z)GK@N!wutzx91jz)M|c%W^?8=g zs0*BYOnNcUtz4EJrF;ozP^YDJP)8fVTaJn^oM0W8BOD286-)zF!Yl%$sj4wb$N<&k zu9*it9~v}e_xfMyvEOI$p-e}Q&HJuy#fpLfNaL*_X$EykRU-GN12<;vk_4>Y^t|IC0ov}v^>-=y2$^&oS<_Hd=Pod8Y9zVu=Jak;STv;6O z7<7NfP{8~fl|S2}$mC=~WyI>552e78W!Q1x+rTqyoI0K9hhAyR7t0U3?rJ=u5tve( z6IK0a*`rHkPt9L(9U%H^-39$=tFWg#r(Zt4w&0B04FA0W|FO8$AnXM}%Oo0{6x6iIoG$z(5T91(r?`4` z)_=)Lac2Hvtyhd%AuQm#ak) zDNo&{L)Ea>y{aK+Jj12Lu99iGMb^{Ihh)V40<|lHLK$zd23x4Ep zN*`fFQ|=y!9l6l|&4IB}8me3Hh8WT6Yg)(&ankk0Y3k3`O=?2kG#0yVI5y55cnaq6 zySS&_KU98w6&FVz(K3^x!RaY7@j!?KyhpaTQRJXT`eULbA+l*N20C=BfuXG>S(m8t z_S#pve!j^IPtUL4faU&cCRJ9z_uw1m%w;n(97C{IWVf8N1&P{JuD~*N%R!$AjRxSz z%fSwo*z%KT=8;@q7p2oLsj)`{78#p5&YycaqpI$NE`DT{kgj3b+U+gk7+}ukuXw&8 z04BoQZQORs_cURB^=IIeW%uJGIr*dS`MlkXV5UmzwGKyb57uXi)O#ks2wPr$dzrbb zLKMB;aX0=sJ*l=N@)K_qKJ5j)iQ-j@!o z&{{)@5T}$*W3c?4+|;)kZ7SI2nuxb-W}n@UDpR+qk~4Sm$LZ#~i$3uybB%s%puF!y zpTOkuo7>v|qw*6PGkH_vnFhnorLiLTPS(5o!56{pCC6HM0S)iQyr}*h*N^O($KSIz z)C?SorMmm_OW3pS#PX(X3681RS>qs>^wg73r@=d3wQs8DAlKfspG?g6xMEc$XnH?8 z+0%cq*gEkIXvGvZUee=Qrr?nl2^;dGYs6;$MO?!4WTkG7{`yNzPBm67QwrrX>V*Tx zgbj4YHuKys@NaobNIFj*HrJ{|xmiasg_)PG6<&n3hd?5|=ItNc5UkMj z(O!lQu3Dc&D&3`^SVpEDsc{JPRfo8{9dd{c`3fVB22U~?tnTnN5!H2`}W<6XK|w&<8*67Z9m4Mbh01QR@1B6xb$AYWyCC#G8?# zdbeOD*ns@4V>ChNc{I9_XJ4n0%ulW!_T08*fZpr=3#Wk}+4epx$&a)is|a{Tx8GK3 zO*AdLKHZKCeJ!b8YUyo_tNS_UtP|jY&-m4a4X( z&zZZW!BT6IHHj5r=)}v#4jnU>yISHA^I5UT9{yFXmmKzRfAj{ZBqIDLn?-Bu zO;!{Pf@p`i2h7!wa<-yES{)vZ34x<8=R16VNDz)_Jg~8y`#fPA)NQ@Q^^TQ@8YkAc4S$l+VNx#$b0+pR-Era&iTcaHl6fEwJAYg6mG2~cNuS8>hJR} z%`h^$Er0kppD8zRwv@6=>i#Iy^gCD528VtEL&E3F?JE2ZM*40_wkR}LokxZ#ED zSIt_YSDC3HyBLg+diCSlQx5jI6c5X#tIJxy&}-}>ljhgZBcD^hB$DBQ4~LRlZx&KT z_$Zz&)@3sc5)rvsP~O=8=vlW$DX+Gpt}N=ATJP-dvJ8(Ks^@`T)uyVBG2ybN+jDPU z6u2qT%9Z?Fjj7@E;Hi0cw+AG1xTP{T{Epmlh?yY~cPrLj)&I`TZ$we=Xq0T__XhcA zksptLY{+b9W8K%r-v%2|-b88ENO^gva=oxTlD)aGg+t-p0!2QZ78yCr(6XY>ReInn z89~w^bKbVrU~{KUF~8XS(it8vprS!tdKX@s{K8LyDOIC`o=d-Nh_CP_-%fGm$9IBt z`tpT;B-e+tN5UDOyZBV;tsEm=@cqr-{HU)F3wHU`MKBt<c*<#iFK)ZqoQ+j3|M6ejUjO-F?(xo>+z-il&$Z-fN^cP1)g``lbNORW z+u3)xtv31YspGV4`Bzc_wne9#*Uf^9D1aRnTh$TBHmdI*nQ!YWYW-^4|1d~fx-L0>^PHa@%H-4odyH(de>LH-o3LvQ!4Q<0C<+(Y2u$BK7es~9?0Pk2^?NLfPs6X5WVrNFu;(rrhwUGVcIK>jwhzgl#syV&YuNeB zW&=F160#SQ{Rv0f^JMbI*FgxaFRL{L{1}Yed=mQG(Ia}G;k_s9-*Z|J1 zn79@R!t}eDNUUwz`=@^TOib#+OK6QQL>Y?vu zWevRhGg}`AYg;`P#ecfW&z>6m2S@qOuRklGJL<}6${;*E5C{+W0{vM9_(}a7UV}iI znxF@Ojh@Z~J<9JpVlA2K7Jpfn)z}z&ro*=g&Oo zDG2|@jlbW(MgV*h5)%><5D=0Q5#1yvCnYB*BPAodb(`wWt=p8h$;j@|-JztWp{1oI zr=Vw`qhX+;p{4ozCV2S3YX}HQ2nk7OZjs%h`MSm@g9dL%;p5#0-Jrz7r^Nfy z0|EnMCB*y3_K(5;9s3Pnj5mphNl3|n1K@W+H}LTBZxG=B9X0T5An-hhfRd2v-lHcs zsdcT0?z__nze~&|W>>84q}3Zoa)`Y02q7V*qi0}b;^gAy;e8+~CN3cqPshPEn?Q1)G2S-mYZy#U3xBl-#!@?sXqoR|NKc=Lnefpf9mtRm=RQ$E% zTTN{pyuRUkV^ddmPj6rUkAcC7$*Jj?*uU&W` z{C_bExcwK){ujF_0lRJx5a1IK{k044hVNg)DG3PgJ-SKtM3>0Qo%+7;J7OBe#N6sm z5_SeM5|!rYBDni*N}vD;Q@Sl$zLX4XU&#oWv( z?xYRRaA2E?^E1#L;K=Do@;}zJ<(+@#tJ5uAnr_CYBmHqtICX;zT3&%Gn^VEFBCPr&+^lB07{(QHWIr|R!6cgc%3c&@EDQ0Ux zh#p@FdNYj9Ofh)Ji(HTCFYg2gD9p(Z-jY}+iutRd%fB{$kiz~X_~wb0u=8B9S*#VC z{hL7Bzf>~}|L5}j;^%I^OmY$7#DJcbev+C(6vx>{I-m;_dDLyyTHG$zE;L@}EYX$zkSQpK zz=+AYAV5!SRab>02}Y&1Hz0O$-lGud3te-J_%|Y90W$dXKs>8=9lKE{2oNw z@2WqA6Np*G0tqw1l#jrt8r4@nq(ccz#oYwa&Ib*(ADdxq0nXG6*K6|Gdls<$=070L z%03911gJ3l95Gag>}D(!{Cs9hqiew({vQA?|1EHg!Oix<`g(-WRQc_Ss_TNCHSep( zF_I^;Er;{j64s1`UUk$y3++lY`oJ`~_mY?(So_J|V=^VXUw-d3rd^Nr%iw$(L-RKc zn(G^~EHz$RrBLxmDXQFMd0ofLmEJ@PFrg?t3j0^iD1hAQAKX#@^}QYyh$9nkDfq@e z&eGXYHU$(e7d2+wj7$i1*{DI<)_(G?ywl<4N`W#rmvEoU$z@xH0_pFBGm3cs*5yzr zyDw+jd;ZMsa=&o=_kOTw#9VXy@HH{|>iTtDzC#<$_{+&t%>AlKXLdwlC26!2M=~Z! z+fuEpSjIjcE6$7vIGI1W#jHFD<<09X-a#r^6(LmM?3T1N9%|euj_j6Xh{At)25Lvb z?i{uWLB?|J^#`0*w!yZC`HTp}``guhu*ThhYaz1zvM-a)_u=rUmkm)q1@=OqGM_U! z?X~ST@E8tcYWk!Iq2%O5)#*xgp!)MAp8mL!T3hFTm?j zWYUD+zsKMU1FBcq|Dt;3`&ac!+scyO^Ge%dw<&B3dvA$gO4dBzeDI}(CExdhwXE5r zw^n_oPu6{d914EtDLV6y`R29I>stIMxFsOm9)FLoPX((r~wc-q1D2 z>r8-MFD6}X%>Tb}v_0ArXT&mo7%)w{(vJLz69ejGPfB1bOB6V+1dKh5=?`f9?o>O{ zl?dP(RH@B5uG&oST>4sh14(gUuq1#fw_TBY+C{zXpYuk{`24u$+JV98yDfw&o z&GusG!|QUWW&i+1_sWv#Wm0NDzb^D&lO*d7OQL3ne)Ah8VQo${xwMP+%87=-RbrC6 z>BlXVWoZR6o5E^uMys`~p0LL=+*)yA0T^kKoMtWtyOzg#C5PzWanD-N@S|m|h%CJq zXDqgSoIIrt@U^4mv2oT|pduCt)tSAFcqh-e%O-$q{a8u1r-0Bw1b)fw&hX2m`*_<; zStQnbtq!Vt^?bWYyHf)eC2bhnRFTEmV>Dqp#GNQgKJmsImQwY@vp=Yxfa&6D#V2J| z1{@Bs&Y-}3f^--n^KqN(TTdTr`txx#c>5?cg{?n)LI6&adZnbXHUtJ>hs?jz!HksL>dM4k#WnIjS`;H@yqj$!OPW^Ym6-E*raYaE0xK?E9oZX8mSctG6tP3%c7U{iAEeEg9DKb^BdEqLYZ@Uf_4N%NH#3zbQ?lH@X2nNb&u!w`o zT^|ILY?bk$(&9!Q&(cfGd}Ab|g!$BYZVmb+Wvq{^X(C?VWK9Y*Yd<=qVuZg z0;jILcr^|jNAhfv6YnU3z5P}=Ivb0|b}Nt9EtA?;Ib1?1O&sbI4^(Hvlvt9~uxi|u z9J6D~cX0O>p)cEW6n_>c&368jA6irvfx~aIS_3sI&#Cjvm(*+E9+}Em`jc%lnqxDJ zG|^nZ*FK;6FWqJCIfwoLBk7E5Ux{v`I50hu?c7w@x^ICV%ztrodLea z^oi`Pn-Um^$nGFdUXwQY|3qr?CJlagcC5O`VoxU0*r6kx6hO+EayM!taYphdot;R=nw!p_VfLD2Bx%S(rexVtldyu-4H)Bx=7&u zlp)H*E2Py%R3zq}8Pq)WB_wMbtHl@q+L(LlDZOL7hREajTi?E5Kwo`vE zyT-E*Xv;-Sw`?%-x*_5rG*Uv1y*kM$N^6Jf!_cvv5 zXRBxjgxjQ%c}9)&h5J2$WgR~70RtRnr+_6G+N;8;1@>>tx38A7_Zo2xnNO6#WA1mh zjlo=!`xAd2xG6i{;_J)?|EySRn)Koq(Qb@h-cKBHvSZQu;IjDprVg>@!U2n2rg5JS z44^Y|EZuXL4Wjt0gU(QP`kYwvwXfD1%*P-Ho)Cbbl|${$B{lJ6VZ*#}mgHHnxc}Z-4((HxtMs%Uw%b2t&C#w0BNUo5LBsPaxSw_LFy2;UKN)TOL`Y6KFgDMK zejz{c&Cy#5y~dPv^_R-I1_a-{OLKM-*o_PuFJ#@<#k%Eq4toKG1 zV@94f6{Wc~ue52BB$1>|b3pkh+%z@TwGk@lB+T1_7mjUrkqb>hpIyS$jufiWRjqWV zvnKDUG?louVqBjpKe%sC zN`5GFzG6+la(efDtg*_KNYDcA9>A-nDFIOItkk!+%;>q^58z1!SBn}Qx%non-Hv$| zA4=hW=0@6PYh+M0#kF9~+wL=HSqnf9x@N@xM^zCR1fP`;$UGR!v9=83ukrd$bvR%I54-y6_d^8(y;ZEuS03Y)z zbgUFu>2_rQ@v0(?2T!O6*vs6%JRIG^d%UZ*&y#8p!kwgIvi{gKYxKsuspMbABbGv_ zu=4}jZ`!%ro+H*s#DqRuZb^^2TA6gVRU|qT+_KzjOf1!Al7xtEreN^vxvy%qNPC(MgqJ_9;gp_hIo1c(Uj39aHE4~sep1?n;t1Y&89+r zjNxkA!=(kD=TGL#jg>{zpm*&taFpZb%%wJopSn^q1nBqalJR3G16^T=`ktBK#!5fOh^x*_oWTNA4LHtB2nO=<;l76~2z z0S4c*B%@~%fu;)GV;7Rk;jnXR0Hr5h3FrGyR==ny93QTUEllto5O=@m%l$ichzwRA zQyhq5Hfl#gLe41muh}Mf*C=T6K_3KB<{gXVp*A93X31-io)PV}@W!_*kQ+;4MHwzj zMy+cm6qFQe^$jk^Y+(_LmFewvkU5~SEeB2?+jegrtK8byGJ&O75@;=)i4xYo(yL_HBgo86LTA53z;_bb7lNJ63rb!C{ zt*Qi%qXVKVYfJz-4P&~d5CI{PeFV@YfGN$ybG+AD9s4w8o=oHg-Renx@gVtr;Hl1+ zLmg#RQolfj-c)dHT3_W&%+=GzD$Hb|sh_}PCnBrj{a5ovC!d=)A*aoL5}QiNL>>aS z)i{GIiqQrbl9lCoiLOc%Wc_xbLIPBcE7y|(d!Ncu<960+GD~cXGuqZx{`E9@pLPqQ z7Rg1%cAO`g_&OW=q_8c=6PeRp)iu;GTpf%UmQU(2?8G|0&8JyQ8LV{{Nvj2Vmojjp z9*7_3OEMYHn!}!I7w`J>pBmpu#&SO65OE(T^JasuLF#4?C<=q-aDW)@QShnEl>+hN zGGcL8feNePdi~j|sFyrXbH-VGCdr4`;H3cV*lSLQ?ytKFwF?KOW(4+3Ar_+MyEO%5-rONY6Gm&qFNOC@}-9^rnplLyIQC6LR#ut-EQ)(&MC z`EcTR?MT1Hw|a8JjXd>(IXl4@V1fMm^O zn2k>V!`m%(L>mW6rB{Il%L9u9-|`YB zjik(Yk7y$GT+~xE9)|5wX)99Hi4qy{V)9kMC#dOaM&IN}_%wGw#VX~K*!stkMC6Sl zDx%I#w@1%e0|DTG#C294Jnr;}dBHCPZOe5_Mt$71u<;Y>I85lP{+Kc!gazsbPea$C z`eWtq4|)lPO;jW}6l_~Epk5ccItNxV2TU~yepP(IFkSyvJubQHBy3<3!@8DbjqXE5 zgvhgDl~8hHVX{)bt9x-qtEaD-pi2_zd~?oI7oP}}trLdrJII4PK+Nia?fJM)SnX0o zF@Ib`m+NgcVuo99U0e!x63&uRVf|v3`(RjySknI6+4r!OA_X$%(;+_BNVN29SD@3Y zzM1rj&tkn9RqNMmn*?4-3M%)W;Ga~l!7#izT`DzA?^m&AN4*x#l-0%dOqxCJy!bLj z+4ke?u`uemUo0!LDM2#|_$~BTbbglLsCE)D5-4-|K#94z#tN)K7U3GDXPU?vY_2*2 z?|S4s5d5t;SoQuNP_IJd*!nfIK>&d?towk8#D$dSIKF6{*F}Z-6`{ZH-t6JDw0lYI zY_)K5)bVmRBsLin&~0`U=6#F8qIH~o`mjY~ja8=8%zAOoer-*2hoI)(4L-lFEqvL- zy-rq%{qr?eupw}|r}`d+pA3-Ygbi|YJrH+GJ5Ri#fJpjTueTm%UFl$7 z16iJ!7JchN40E>;)AJd2J*NN@XG=d{)#p9DG8whi{cnuN&rk~CK@=E`wKOaZcwKBZ z#!ExtPG^CGzs9`DuI@whl1=Il*}kZEMlWgJeP?lzyQuuMI>;b>iaKIy{Wab?p`~_Z zrjBYYTU>A*muMSvv-gC9As5ROG{6YdG$ zv(jT+w4L}qHyy|;!$m*|IfV+)Os#Oc8kJjo;nZN6^gS4wEL&dI{ z`-7OTvth5}nAbRN5k-0(MIkJ{9koUuq|e!8{1~p5;j<#PZ0w4iErfC*6(ZVXaML-g z-tAy}v?X^=H@#2#?Zl$JJN(0R;%(-&($7{ZY^<7nM221-J4s`u!gIsDoo4k4RS=d2 zQ|4LU`ZC|zPD^kcaa|ZisyF*U>skpIbr|f5PP?%wrT$bZK85XUrVh zgiI}ZWvi7@snJG{)rfx$CJ8OpT1s6B;3k@0w_nKtSxZdD`T^-=Z~B6Mvv|o{CoBGO z^Ac;l#9P70Z}Nr*dzRKn{ngR;$VzmHtmzdM!trt|RJ7IFd}d$p_xEW*=T!5r(LflC5;*n9hJ=wOT4*%UWtcQ?qu^!@OxV@ z>())5HWx0t23Sdt5e#M8vZ@&|5`Azw2`~<)poewJ!|bkAOyLbvN2@5M@a!}Row6kU z?=9I1JdX(Wd6{@ELwUy3uQ?}9Ggv9KL%KC<8z%Q(T6{ntda9L}E+2daQ55>Uc=~Zw zIFt6l9nG0IGvvo6jky6Oho<8*oAL4o(=_#3ls+deklzM zG=#}np5N&_h~y_!#Uam6>(_<<%=cLbnzZbC^qg$ThDbcc?#pW7Rwk2TTz54aaVZM5oi9Sk}a$3qlfO^BjsSkG#`?Lwu36l@Y)#j{x|0OW3Db~)(kzBf; z5w}%xblM?T9E+pIh!1sM9`vv=PTtGrBiV?5R>XLHre<W2lcf8ZZVVr~(cCV`?#i*$zA_v{;>Oy9?H3c{>KxTGSs3#P0*N_-MDez8 z|8VV7hrT#1cowzb<6k(;sOC`X{5CTQ0!IV*BUwka;z*rkFEo^s%HPI z+@dzd&Xw>FXbk=bgd=|q)S*fT+Gr5HuB)$Y9eM)GG<>Il+`w}Z92;z)4=eHq^gRcc2oEfQ&|+l)Mawvh-5-#taog^G3H+S}s#BvfX8f%})&hA(eV zbR;%2GOSmqv;djvy0Q|Y9!O{nko9$V8#KJ{Mng=`kv@^Gmmae_kX&ajd(}}9(>>+;Z> z=E+ads1J)E!l@4+P%xjaY-w+8wcPuIACehI}4GPDPOT-DA~L4w8$2TZ!Zdao6i`H6H{jIc@)?L zKeiopsxNf(-B5!AlO41bCxtZ^>EOcPU#S4lV7|)uY(A$O;wkT69NYD4pAFsKU&#*2 zPML+?!w9Zv2Uj~vq!UHR3VEBOn)PPS0@XJ+FOOI@)iSpjK_7$~8T*{fSB?APoVu8N zdlE>YFB(Jr8dlRkpULUX@K-zfdp@%bITq_+&Y}r+};4oy-zHlz_(|WKByclHEeY;C2?B*&JYY4(Qj1RG-NVfS)9bk z;KAM)qGFqR-#_+7zy`E<4DjlWHS>bTEJaXu@pZj5sI+$i#p7a&TsILlyN_!68DdXD zQ?g}+elTY+k_}H7@JS4p^-Jib8%?z$D_8rYa3H_eff8M9w8+fRT$aUywI=V5obj=gk(Sc5OlDo)}y7{V@aocXn6P~fhBHn;}_2h3{ zON>tX%w;xTY0!uQE7Xd(4h8F7rou$1%W4#HN3xABt$LnwQa&TAtXFaIt5h0qV|4-u zw_oE3;Yf+@d5W9Q%W0aNhbKiP2phSc-s4h{d9u*cFetV1Dz)&+Og{HYY+GIeiB3Q# zF#E+H3*O!dVoQ^C@=zUTV9rIv$1kjVd}SnjS8?76MruI__KY6wL6}`C+xRi)1A?@M zrm$0#safMjuLHsJ=)}q=iOI3SpAq6;w|_Qp&cAj;r^pLLyxRu;puGyjysiMnARLBg zKS4Eak5Gx?`abz;K!fj{s{dXlf2HVxU{~e5+eqVrmK?v(WDaDqU3&^ z^_?YL9Q6SI2dma?(B0@Wa4e(-Kxzp_oKZw6)c)>8M~fObKQ%$p_U_M0zpiu)uIm!} z(5xh>(`%}B3bIRI1{A^U<+OXUi7^hv*as%BQN_LVs&(A0j2gtyYR=xN()Wosz@J{8 zB)^ZPRkPZnm+-b4nU2vlTgXM(ttywI__~@P5m>`j!5j0Y4qt9@X#~veZ%(3}JL?lk zKcV1~EN+cNH@+__prO~fbpc|v<86|DMavrVq2cQgiRV#sdd8tuAU8Px@AO6X5r7?? z!N@+O+v-P~CG>*nwD2K*5j|!p1E~(u^*<(*R3|wObV%?)wbo*rK!N?{QcyRYJ5~*2 z3j6i~Do~RxhEfPVI^1XupP62`*IeyDCM`@y_#yebhZa3P*g?6V_4Hr~STD#nGL4(Y zQsqUl{L?r#OnfJRpa*0ap?cHY@X0!hr;usRsiO4VU5Efrw>&Ma+y0Hpmm(tpumkEV zcE9&%D-0VnFdzE_B@tUt6~>RXE_9rHIOAKN{EFAME&~6@BYHw(jyEisytlx82YP+5 zd3`i&3SqxBAt2T&Gx+I_weLrD{vT&Ul+jlz=pRkBb=S#lLUJfw5@b;!)8WphSZx_3 z{`k{*nIrBnUuWBaB8azksC^}4nNGAk0If6O6>zP%G--^yo1;3DpZzj#?!K@6Z_et- zSWaTkunQBr-(o2GPH>b@JKf*ZSCea;Gl>PaDT;7?=R~zxcF!in{vH&3^w0^DUW~Lu zKS32Ad`4a_1LTJO++m^z>>Bky^8-~5WU#vKPNWap-i9mPpm?N6)GaXxYkrr4#P7xL zh18#S&4Hgn$Sr?ab{8&fN9G#!tu5Mws|K@zY$RFVF>bc&ATI>VHXmt2`E4=m$ux_( zIF6t>KuuF(1%|y3VQagKRmUN&AXv`SdhzAC9NRfGq*kTYH>2ffahBPb?QGY;c+=~V zch%2QQoWiIHdA|ef~c*t&kXOP=?-GcE3c>w;KpLpTwMPw0?;4#JzKm&Zg2P@%-WR^BdFSjEbaz0M$BeIgV-!&!@NjDBL!U^ z0!o)0KIieBYx>{UL5Z0g)g0HlN&$dCw{YHk+_UK~hv1KFL!@< zRA+gk5A2OBnY4FRVq!kp|M~85A(aR zcZim_57akXtdV0t3(cM0?b~W%_JJM6>3VoZS4HnKQ=D+`rxj7m;VQI?;X`t`)-Sd zq99d}CJ3mYR7L4X2}Qb8rI&z!^xkWbDqTQ8k)m`#2wi&UReJ9bdQYeUl6cm)_uc#M z-*@)d`!~+LXPomFWUP>^mAtHXKJ%H+oK_{vL507MgSrycuDQ|vD=5?_f4Fa}MxXN#d)ps|^lZoZpqV@gLB~r{Dj8 zOpl@4k6Yeiv!UzpXnS0}@*hyHX?ZC!tIPzWw}VquTN}QlYLW+pdwbbfn#t85e8Ap^ z4|ojI2V_zHg$dww1t9(;v4XK{nfw7n(K-XI^lsqv{@vU9 z?+UN!`|3BmAeC?Ufy^c909%-0=kfNkWk{E^7eef@?f`c-qIEApYnMmGKPyNGX6-Dn zo6cE;8>P=)_l??eT;4cjs@=NVQ{X?XjG)#_!cG1G9XkgB_v$h#=r($9s-zEU2Sl8T zz#0C4ritOF0Ume>`Ow~{!??OaTnP81&KP{19)P~aum6B#Xzf~>9eiW}h2Xy^tYB$4 zpt0Or1V1|g<6`ZuLRuhbAVlY34)9XH|A4NZm!!gaSQW3BuIl_HzX`R=ScK@TWQ~ZCmP8* zCx=tfYX@Ip0kO#_so~gEUZB<;v_T56S?!}t5`Y(<_ZiiD#(d>drF;>MGU9>HVqGQzy7C^ICJGY`RQu9`zRyFJ$ zm>~1Br{^&AW{9Xb_FTmn#gKO1ma{Ca!Go#<~X(atSlq*D*GFIIAId#UXuN> zBBD`w?T9jHbeZOq)K`e_R?pfZ{}o{{K40nSIHQRg>pQum>xSErb+FgtKo?l`ydm0| z)PDJ15R=>@#wr&wv-^4hO>JIkw|oROX(OsCzHpHsd&rP9Z8vic|CJHM0(-4~u2e6H@EE~{)2LN~t5CV)V< zIOgDzHEJT=tQCGL+2k#JK48G=vN5Hg1_|lB)SsObzXzYSR=g9|$1!k^05E4xcS@IV zzqXl=0U|&wk*2vTb(Yk2EVt1*0v})@i8W4ziND(=f>kVtfQ=!~9jjXieiRp2E#{lR zjy8}A}Iga1%xw8FAl+<$*bds~A3aQD9W8Dy*A9&kEt@pBA)|lrLwEys`|cz;#|r4^7v5S+LJ+-Fo~ybf#Qt2L)b*Mg~3eVbeqwM(xWVCi;A% zeO;?$?3Sd^0R)A~V1NYl22o{|jEw4*_8%r(JRVY?VEEW33CDai+qo(lFn*o2oUD#I zRb_fBB5~oZG}2krY!E7ip>PR1hkX`uj>~3%zw=6He-_|@Rzui}n^DgMsHge}+4}@D zY0EiWCWl_EudlG0Phh}ki{bzmlzJpe4gu|S!*^o{zSt(c)^R}#3!8&Qp_THAA1#{c|>Cxq139XI< zn%JZou_kcd+zw+}N6GV2w3m^R>{r?vy!AL2skN8!B&Y0LV%%KQt~yT~LoyefJc0H; zU8Yq&t{Z-9wS0bEI5xRV>wUm5K*p;CC7!sY}Et?@)|J=}eyDYoz00xTW(Q&8h}0#;dFqla$*# zklfASrD?PUix4*Ds>T92&h|lk7S0GbH9CITc)~F!ptQ<(q2d_tRR6q8+)a%33KE%^ zHeFFsi?$m;DoQ<7O_eIbHUKVZ$DvJ}MH^Y1j6z8-V|(Xs3*gn4s}ucb_w_r(bL;MJk$@p z6E6FV(tX%GJXtv;1~>S@`O2-lY3&UhaaRoh(KGsQYY?jKM@3mPER zWXM&4u9+SyM&2bI+(i!QxH75zT7Y4n=G}Mvs7OX7A)}?Zc6Cvbjcf=2p82w|ll9`5 z5A`#!kEr9ZRL{1xE@N{|rdt~EC%+qY3 zxEy_oBEn)~{z_2~A4fp09xdJf1A-?)<5(uoF>-UvP>j$;2jq&_4ua;c16VlD|M?W% zSpV^A014}VJVSpyz)HggSXY0$l3X0)^(qWY2nf$2dWxn$n4OIJIlStAfL{^&x+8LR%5^`sIfKdqrwuq$%!MRsSZhzTcOH5^YvgiCv z$o`?NV>QUu4PAQx-TeaHP-rp7mOMg#rHsdL;~oY+5L$Hr6tV}a1RktRmw@@%S1&)u z1IV|OTa~};D?H3TEQB3*cHKnQ4=pabW=$$64rGI#!fn0;{TAwVgvhSYqZG9FoWPOR zN2LC}lvB;(=cU6Kf$3O&5ZF5A{=Gk-0iV68z5ffKZOQ8I7>z9BDJj=V^n8u23*H}c0 zI<}w?{nZ(j_R`T{-vVWQ{zi5L{cIs&(RbXDRb&|Mg;|Srynu}|h8Y;ebT{69`854G zRkqoZCbjJ0UTl)vlMzW_*Aywp~$&Odi?c;p3SkZr^PvYxVBq1>s6h_-z7Jy=?%jw`?p9w)b1)vwGu#N^#-9e=V20BFaR3HW@%19YaB4GO7okB zVMB@%);0F3=)|*^8mrcyf(BXf1()YG^w!e48u`#lf-%=oJ)sz8=*iBNXNb5$)ga|! zsx3){cpF1o+ofzE5sLI&5d8$j(NX!=MwE%)N-HIQ9%oJ)w^(xSQxK)|DFm;F+l8MP zQRfW!^d%es$(SPBEfn?h7KkH!dgOV1p44RelzyJ40yj8(1fPk4U9^u|El^rXQ~5q# z-{Tx@w`8{kTlSv`!vI$upfv@zzJ|n@;-Qm^)ez#t5niEh%BS*~tfvIOuvrIJuy0!N zY&<{X7Q0`u{|EFAJj(`LSIk zR6QSXj2Jw0L<|3WA?K)UO3%m0-@VaZyT4xbr|RJ;id#|h+ z*ip7eg)_qJMeyz;vlp8ZZ({|w=q|;dPsJJr$Nx;bYayK<%r z$}B779+(Lc*MTvir(Xi{WoDyJ9NWahv1&=~#ly10Em8eZMXB{H5HP?Ax^-Xg(c+<_ zl|3uoB5oYVg;LyKUahZI<#Y&o>XT-yLC|B%joTg2w|FCv77@#xF%^1Ah`!YF5U0sdA4 zd9l`#pFd0<#(IYXHI!R;@H;hK?KDC-+2*=SvvY;w*`+9GdUrFXRNp5-sdCbzvDcGa z_?)J$wz1X)HOK_gnT#a7EpxL0C6JNfd(FnE6%FTeUq70AeJ(ayO%81Dh{!e*;ptZJ z9SJbOvLam!@vq9Mw=yk8s$V_ug_pjUw-VHqX}E5-@S~SYF)qkRpb{a$2sOQ+{LsQv z(zt6A&@=keJXZ2F+6RjW=q-%>=ptv9P!6_%#lGNZ%?=IXM8!9{)7+jl=&c=5((_y# zcyPNldO<+k%z`L3xmD=$8u?2)`&_dLn-XA-IDHWmWn~?>iVgBla?MvQI!wR$G0BU& z_jmll4bs>({Ahs;+d;+hVuhz#_2*k18Z(V6!|g|uiUs;;B84dqu+UHm%%GOhL-hS-izl`P_gLSD^4tdH6sxl6lhCc}hPHk= z_Ifkxd~+utiOuv{SGMz%fw`9`mEYC2$K5Mg*SZ%{?H#u-2nJFsfiIl>6F8ze=73GR zslhxvx_xy?_%oKU7|M7}e}0CJF<)VwB|@mJALq7I@FB?PD<#2=^6UyHQUU8!9gK2EW9kQ!zlkp~Nwp#+#0s1mdi zvRjxTKA&)laLQ9KgUT<{$AAlNX*JW{gh`3l)Wxe3QI_~{re(1sNFk)0F4w9!2&fb` z%X>aH@Jr9@|2nepjyHww<-nO6ykJ9ojCFZDe% zp)@FY^>;><#4A3O=1=(XYcfL>svsL|7VP09cs1Y;$ae8B7t!_x-0(^E{<7UQ^y`L` zxX8hp*Ygb53`Db6H0Ilp6c@H^8PNo#@JK~{K}PQk{OmN7x+n5>-Pl*aarkTZV&j#2 zX3H7m9NL7NoXKMw}?Oce0-jEmk z*d0s_-RpS;z-k2m6bih^CJ^7{%xM}A{s#d?tJEXFBEB8?_j_9j@L@*N@aaJ)Qw_RF z95pxR6=77riCpkHpQ_z9RE~)VN34j4*>V%wA)B_i)W=g-pcS(H;xjIX@)NAlL!M${*g3i3y0(_u_d?3N_ zb=6Vm-V}I4UvX8^FuT(<;tXiQoBzes%81h3d9Q`0Z*9o#X?8OaS5GyfygA!f^?HhJ zwT#l=htf?^cN_2OrmdJb8)J=yio_TtKAQeD`l3v=&~|LVd}Urt*v_vrK&b7=8ShYO zcM$}ZUVi7}+~H>(CMYh+s#q1@0ryUugu|&C^DE zST}fb0l0%xD047Ve!u@;A7h`-$Zo&5#ZNTkO?@rGyGI`8m>14uaPQo{*E4BjmY=vdcwOoQTEu3U-nV? zJJBlw!!aEDztFU+@amqhAXYS!K>`9O7^t0aq&~B2*Zk;6rLPz1A)E~H_L`J)Dbl9G zTN}n1cnNe>=wW+Q14`l%>drZQ>ZqD&`QuKxkCp@{;PQIIbkFRj@X%OTZ0He06$iAD zZURw2(TplVBS2e*_rbJ;GoqfoZWPg?C1^;IQCT_P;ZPX$$Xq=lvz@xqjkIDJus1S4=Hy`CDxp?>6a@qZv8P{Fod=Hv+(HU;>#+!r1U)yw zkfF+|U#4l%9L1G7|n0abhIl3?fU;NM-*egUUbJfA5YeS#2rtm1@jI-%*B8 z`>3dHJrQj1C~g?!Mf$bL&kld$S7x7BwQmtX__H?xwE%ePX@*WysyRlH;C(Qt zNQ%+K2YjB7vPywoSG;a@ctgl<8N9fNh3nzqYehM}`)T zdjhU$)8FJCCn8jJW&;b1DJ+u26qsxrsK0brOyG-2Hi=!<)1RS(ZsLZFo0__H4%4iE z*e35xdzCNjT->f{s;%s1l*7IJZhifScte&Oj}68LwHyy@p4r4`skt-3qZp}&C@r+F{}13_ z;%SvU+oQpiR_2GAokc6XD(~Mzd;=)3*-cc}5@@OU4jlRDw+bDZ^HQ0QLW`Ksm-$)7 zRx???D;^-w4NpW#yYeHZvuC*~Du?G%572iC(M;!Is7zRNfEKFYVl5%YgG*}Ex?neT zdDd&rA$V;l;Z=LMi}WQ@`hkWfd7v5serp>ihTOn~!gfaN<|Hxic)p6KEF?X3AFjK9 zTk(Fd0&y|8=3>7i4`Yn<+=`I^6Z%nhK`Gkj;@=~TXs8x_N4cy*bQw+_x{%NYyl?vW zBJ@P61SS3k`?KjeQlOPlXupLO;Ih6F0uvxjm2WGJ^bI+#ET3mBIlRw(JxoAo9;eGn zr7pOh;pp6{W2-!9U*ER7Ob2vBgZ;vQTub|FLwi4|#J4Ksc$jA`ye48qEXLk9h~ovX zzl%_d5;gk`@6WF$M8fa@524$b!tvL5hT~<1pfhO&5jZLJlT(7RqZ4 z8%>`zpdgiB**h~3474{KhgRQr{Zto1pQ;-k>FM~5H9zqEwI}-Na1W%Cqf1q_THC>c zC(iU1@nvrMD0x4BhKoBLzc{t{G+^nU9qr_Vab4kyWnrvNzYGw#nYe~$O~vBa6@NUg zK-r{idCdj$!`1@Ey3Y7}%awzNi7U=DJ6R8>TOQ1j^Qcx=Hs~grea3U;?dKnT_|lTR ziR?LOEyFs>>j#%)oQ=Y3tuUSWu|=nV3nUN1x?5v*ZIv(^8DfZb)<;@L?dMGI=TDaz zE}-2uH)(uZbchBc-K{K4-Ot49Au|`}uJu6GEsJ)B7}PM!@=@+m*{9bw$-?HG##R~PS%^CWsYz7B{p+QnFvXKa#dw`&7|OUu;E`pbvAjRk17pGl=tH) z?Z$S`!Oz#z_#~_6uzWL(^47b!2V+g(bMS$h`moxyRlA{4VCCw=lP!mXC=sgqAqpBd z7oU=iV6%Fr5A0;eikA;#3MiCE%VWpe-pQ;HQ*;#fQD?Uxjgv)~nDi=;}wQzxZeH zU;J|^z(3O~V)%&wUYTeYf)@IVSKgZ|`bJ3$kjv74@#xZ6fE~a2ixp$SitJ?t_-|H# z|DFQ)?--a}POyX??k)j99TS}KVHg*nC=cl0&!V|F8ia!U?Np{NfIZe4jE9j#k=UoJ zCqI~Ei|wK9+W4Jz^SCg?aBud3BKtaQ;Fk^!tDsmfgDpa@MbbWjqUa4m)!@LcleSUpTWsd*cv% z*F-=Sv|RurMbcxU{P?hbJ(!Y3316U-F7g4s&+HCN&89c;0p9+Pw&7=ctj9mukXfY~ z2tGN#XwHIX-!e#UwH9GWS4=e4mg=8o7OFmp40Twa{jo=doF-~+BC2OA@c>~71T!)d z;|weES!{{^TguqE>c63RM_1wGJ6X3!_D=!ZL2+PqJaoe#^(46^t~XsqJ_ySxGLlhZW7 z5|KBA!HigL11P!mU)AQo%^XlPaPwgU0BSFnvE|#}LAnU3xk}!z}m4 zd>o!a7qoAY@M`7G2wsONyz0WDe*Aw0CI4kq(Bh0O6%>`wKqN$5N%nQse!I%d%LBPo z8iq-*P!gWO@_2fKBm?+R8VN#RqTLpn`mqP_k;1x91(jnz)!F4Cm+$g2%&v6WiUvh$ z`jVOq6;q&Xk~l8uAO?;$IMu6kKu#MOep+(y^xfRaTyA#NqAVpJAoq#um_gokKhw?s zVQ4cKIEckg2n!2WRF5wj_x^P|w91_MNGna4m1LUAw_IiCqQ}|2gX37V~ z->oF#m4RnAMu_@SpZ%bhYj0q6q0{v~Qk*2+CpH=&#r07+G`* z;zN@j^F??Q)0*h`0Lx=!d|}bf^v3g8f#Qz~ApU-J(A^%|(bu!0@-n-z--q%RQe&@G zyG{8=06n6G7K%PfNY#-^Zv4BzBE+Pn7a@b`+XnTq2jj7mV0tG4@DR=kU?QpK7=y9u z#u!QWt`@dhltx5#Nf>s$*3VbVzWMpZMx13Uv$E{1o6RghjPMSCV5auZz|1~l1Bfbv z-bb~SsFvm;h(Y^`85Jl_^K82{bjS2Oo1#VRWporrN3}eIj&2KxQs#K<+m6CvE=jag zKSu$E7`?WdJx9OVlD0-kW|CKXq4nO=a)|7l9u#!EWVKsX$d=gKc9)Rx7b})oeL4vL_KuYL~?(A}nPAIic%U)Od&BkvSVU*hH z;AZJcAVhQAs!?pnN;8}RNUwKNS641-61c_-P#SN7SOecbIzb+tjbNf%w(?(#v(}ur z@zq)^S~5B&G(_t4FHr1la`!ojsg{UeUd;&{wMe2E9CH8(Qlpz^kkLRwCl`%?e3%=G zWXu-gJ#P#OLW8?ih}bB^C&fxPNv721lnO12u5BPEh>Qog#kI?=eON7|!PAj|1MU z@6VcLyXQ8By@R{^W?a52GV|m#=JUL8w&1q2!}LsmYpf4$$zPpoMmDb5@xTNfnO>AVm-NHu~)iftp zv=NKe>X&nUu%Mu7<-%`2hP#Jr2S&&65H%>P!@^Y$Fi!zWoLgO)E|-18z|8T9V*1`P8%mmb0=oNKsx zzNbuY5Y=hcA7$uLX>C?#MU*z$)QI_);t#L>img7N+LA9H31GE)YyELGpw8uM!b;5w zFv+86fjUC$*<_P9aTTD0#@_My7q?lm&EEzKR?@O^C*7O@;wlA(HK6FNYhH{k>LTXU z_9?w=7PE#`-0wN|6XGX{w_Hx5q#c_z1~O6z?($@p2ayEOATL_sH#@#w5TSQ(Bt)nQ=s!LA!y6zz-FninBZCnu}Vs z@$Qg8Xm|i-n%v!f6$n6suc}9 zd|TpTiuy^NQ-~)M0vuWl#E+{)U!NxeS~b4z6UF-zf?%?pbgQrp7l1jf*7EkO_Ij*C zu-#twoOai2W8c=6*7lKeW%4E@!~}boT0i}9<(Y&G(U;A^zHTQW%k{%WJ>s!cpc$s_ zW@{isDqfNeM4%zoby=w&BN=T%b!1lOGo3Z-?6aSkhbzBKaO$XQwDe@Sw{lu0*S^ex z5m-r>9cX9atU@+5STa_{JYj8bczt=S>~h@j^Ld5NkV+M`bhqD~U|;88!zMVbAUKAV z_ONt`pv(w(%b6a|O>uvFW8v}bF>&if`=RR6C1&)kYY{Z!Sj-Z66me!9sFe!@nu@*i zG?=02)q|B#8fx6pF?G^~Kdm)X&|!r4)A{YVw0Rdom~EKU4%%v@dr38OJ|8 z@i!UdR?hcy6!G2$S_Wz;jVRCMuynqd7M3M6_gRyD!O~o>C$aOvW}HFdr#=>8C9;@f zs&NhpJ(&U@Sa;$1(v%qswekw%J>4+j%J*jF*ZO}zYM?^i8KOQ0@WC5sHLUCCCc48E zEt6xzc$f)W1{U%jV$QMMn3swtWuTIprsAU+3*ZFioItf>%80EO5zCczH{t*YA4NnWCMx<95LnK=#pX}f)SOOVXaqjl$p3X@@6~3+6YWELb6d}j zwQ2m~;}q9G)}Vcfzt_mB6iI+OFjNa%8&nY)*|E2L&rYs8J$$2$9ywzq!}Lt&rh!0x zpTZ?*TL2=Dp+h%Yp@3wr-Kndkl`gb**<^Ve~!+xx1^Dt?Ytr-ex zn*J?UJE0`gCBl8z$V`}k;65w11L}} zpz9j&lIdecPGj8wkgd=hDE<%AgwKP9;m3-9)gH!q01FM%*F|gSJ;m{y{sFNSptdj{ zmL391CeLA9v>g(T2klJ*KGEr)(EjG}p6p{bpBP-*H28|Eq#bhu0O6)FfDX9g4~V-R zd~0_C_wZDthyy>wg&X|e(#;cq@`C^Wi1GsX47p=N{@wZtK$cBu=YW7H*&mgFbpb9& zvB$K=_*WMtUKdKF)vzbxaic%+8N(#`Z<~7s%=*lEvB$a1Am7>);8Sk|Um>!uh)Z-5 z==Y})XbYur#L~B-Z`=v{A4CaoH`IZ$%msK=+Ce!$-E!!IIM)&>w zC@}gb>eGXZ-uw2TiRg@*F^`-KbK*9Kr$H1!@5*9$Kz#W(qZF|Xz(P4!Y z&PGn=FHFIb0*H!DYGW*NY*HOdd=fMpGoSDRPc4f}`*%E_Q1n0jy(g3qY&}T2l9@R^ z*FRVG%ctjnv9!~iIa!Rj_|{BwFHtf8)iz6GUsr`3T`|4>wuT~VU4CDiO(M7hgB6$< zb@u}~Nh z)B&c?ANQz@(Hh8y=U2*u$dt`DibzA0IpW=H=e#2YGg;-;SUOazD#xhEY|R~=5?hPh zQ>cPz2$B=yH>+)OQzfHQ{Hhv1eQj)<+`WcNnif7WM;5ldFjl5b3q2)OT5C|9P!jCD z{iNO6vh~L5Gza;F<;o=1x-EzWMVD_;WH(3g<;U{OGk?{R0nyFW#rK2^i|;wz@Os%2 zjo!Su7JX#TW*ytvA2g<8xl#7Zdpa=S>dTd4WOXSXj3TYTV5i`Gwd^o1zd?TM_p+bj z+|4b~yB0|{V*!%+HspSD@4i?U)a)O4dN@8AW0M%X$P4`DSwJ^Z7V}* zV(i`&ZOfgB0eCbP+`HHGx3Zk4Rgr}hXkkr)Q~a&t&v+D(iXU{GzP)?l*$PtPhk7iX zA+iYp=_2br!2=QH8ry34E&YyreznE9_Cu}sfVmy?NJN@)L|8M%uIKxQ5=L$9z&O}{ zdt>?ksgH%^ev@M0t^};x-2d#R`}dph|IWvFl@I%4N=S7dZBeBn|c?HJj0Y(u(+>wJiQ$e;(_98h0fq`XBNE{w?n6xhVfX8+X;LKj5}j zOTBo_Q%$Nv+twMAu>ZJa?US?b{+sUluU~j=5|i}h61kHq*8&0?M#;7Ftd=bk9oWC2 z>`vEC@jAMItgD~~V(UL(DPv<(`+6mL#veAQas#F05BkOOmKNWuO(fQ1_}Zp5y9bt8 zaN<83n{;iVWtOG9`vf_$H>jhD(Z6g=ZG?hTK%c#vx&@$M+T!kmkw!r1mn z=wL;$sqS8ZY5%vUQ-O&cb$J!sM#bzt2F0SBhSZX2I^m_y^hK^aD)^KZj_NAPD$8WO1`OuX?`UXxjCF8Rl9Z1Lqq`5L@r8YWtq;f8VkcsvA z_>9G-li=A~dFBSmNQ#*Vq~!-95sngW_!Etvu5f{he47@Zc#pPyf0EU| z*~=s87VP9N{i6TLj7;=|VYqK4ewb2+PsLb;(oc5HK>dQpHdPJtf(Kcx+_h4|rdx)b zV;;nmA9h7m=%$puTdK^NEG~+LJB(bL)T`PVxj5E`6BiY2spo74H!n@m{aj;rCU>6O z#4b{99NiGfP8G)2ySHPQ8;*VShWm}>Oy^*y`OOs-rlmIt!LYl?%$iKw2$T95gS({6 zm9~ehjX8maS3XhZ*FOt&r*E$)jvILiM(2}U1uCHK5=H(DpE-)W^@**#0WZ!dX+sTb zLTO;*z_1XzcjXrNv-12}um14hw&C|uG4dLA8Q5DkmJ_@?%y!0#YeeyHy+o8ouAjBt zke*5lX5=jt@6a_E_-Z||d~Z)ko&9?U-^VGn;gANu2UK=lH#OePeWeK&w(_?wXZB%M zN1UX5r_rOJCv~WcO~(7=x%=KTTP|7~&& z$;avpZN^;UaH$0uFQ$Fz3Z7lR*CHH!3#n_Ep{!#X)QJaqCtLD*8rJV$W@>6tI4PvG zQB!6qs9DTepp?y98sOK<8$yOBcAcB+Zx-)V7D$xy)BO4<&XQ*G554X+X$xv=(5?_}xvXPbk zNZ3#Q>27RT057S&q1_dfu&h!{&zz3ui!}9k15;V=qAQ$e4B4??@yy@+!vupUz2N&N z%YJ6R_Y9JTqDvz{-HN z8;|KD>8z7=xG}gOq|?$VGUP?44YSMlw`*SOq?NqXrN*UT?bQkICBZU2%z}bb&|r7P`!-X3^QU66ZsNNfb2xkhoS{u}QtrOrshz31mY3-5cE znrpaL?%kQQ2vM6FqV`?ZtYdiEIEcq{1-6{IG?G?*^03Pb>(icRVV#CPJs_Jhg~iAn z;on~^50TYoy|2qzo+SMz5?9E#{C=F>0vMqC)bufD(wXVnCevuPRmX!4OKaeX7s07i>@FB;(=@3gnaW7MJRLfJ6%0#!ndvH(Ors&lysRJ zQRloN6fJo}#LOkyL!QB07%%rGnnHCn*(=6SqfA2YXitV4zM;l_d2QF<%YyHA-U7e9 zCywQiJNf1pB zHpesd(f9Op$3i|{wx97=9M&2df*6QiQXi2Qryi~nxb+OKW-qg4@m$X%LKv5kT#2Ru z9l2iIr_1yUyJbXE4W5=PevWuyMEX|hCir4e0b9hvNvB!}d?CtkgX;NkXWa34*Q`;v zLRc1jalznD#A(!;eGzYslq)fajlIn=vF5k`H$@L3MiFDa@0WN_(fAm9YTp>XL-A<; zt#zC|Dc^Q6Klqa7WQe(?PE0R5B^BmPB-_YCGdh|N$UM98MlYtB&f6^E!ea;$p|30?su zyZPJsma~~eWzIYp?;@kmMMJBIEaSboo1aAy;Pa1WtGQE|!T30{Hyfwae$!LRHX#}g zqhvsQn7a2P7X}jDtb>o>SN2z zI7-(7<-C#a7pz|%_HVpCvUtMHudyWACi1ehF!9cZA0Pt-hp{eh^_L-e#EBC#eR&Ch z=McUaY#;QW#2yL#$JnEPgEhipV*f0xDK$_{o)@NWxz^jReKScW+k|%Ej`Yl^Ynsgr z70~hja1l%;Mm<3Dx(7_Ya*ZT|`2x8v5n4iaM7yJ1fHX zdU$r6OHOohVtbDrJmHad{=_TdHRtS#^tAAiUU^XN8Ko#3Fb1`h>CmN!2+FjhR5c&Z z-1?P^3pUDfAzda=(9$IEWNq})Xg2S;Hd$u9Ao`^D*(EehlQ(!E%JYw=_WNo<)DVqe z?~cnozg@~FweQkTc{kWp1$%$*DK!^vOfESNnErGoZyLpxy|uGw1P4ohY+Yf@6C4qZ z=1S;~6F;}Vn^LH_*486g21U*f3_-|P#a(F>v%YZE1?3G5)gs-7WC$P3+!- zLFz|ud`fj3_>?Mm@LgdPb{F>d*8ZU@ zTp`OXS;hO25pwPtmwGVBsq>PAFLAqwwYZ_6M!e@{+E`xnoP{DYY%NHnh|qCCNB;eV z^LEww*X(JT>Sr&D1AY39v!h$>9wdK~A9-lvc4KMk&N0>9 zq&#PjVDrJ>c!)Ql4q5&Y$fR<7$8LYV{LQ5pgE5tuPF}HeOldl^_aWYv-nf{LbZKgs zrqXnR@QQ2eEZ?ttnxQ$VLH1;LeA)KA^`$qc0${vNdeF(7SQ-+K0p}f}Uu|aqSo8^0 zS11M774O-8@O*0XJ9)f8Mp;_MlE#MYtnz>~({Yj0*V1Y8ZSUemy=;=Nb_6o>஁*e( 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 diff --git a/doc/src/NeuralNet/NeuralNet-plain-minted.tex b/doc/src/NeuralNet/NeuralNet-plain-minted.tex deleted file mode 100644 index 6b0c4177a..000000000 --- a/doc/src/NeuralNet/NeuralNet-plain-minted.tex +++ /dev/null @@ -1,3190 +0,0 @@ -%% -%% Automatically generated file from DocOnce source -%% (https://github.com/hplgit/doconce/) -%% -%% - - -%-------------------- begin preamble ---------------------- - -\documentclass[% -oneside, % oneside: electronic viewing, twoside: printing -final, % draft: marks overfull hboxes, figures with paths -10pt]{article} - -\listfiles % print all files needed to compile this document - -\usepackage{relsize,makeidx,color,setspace,amsmath,amsfonts,amssymb} -\usepackage[table]{xcolor} -\usepackage{bm,ltablex,microtype} - -\usepackage[pdftex]{graphicx} - -\usepackage{fancyvrb} % packages needed for verbatim environments -\usepackage{minted} -\usemintedstyle{default} - -\usepackage[T1]{fontenc} -%\usepackage[latin1]{inputenc} -\usepackage{ucs} -\usepackage[utf8x]{inputenc} - -\usepackage{lmodern} % Latin Modern fonts derived from Computer Modern - -% Hyperlinks in PDF: -\definecolor{linkcolor}{rgb}{0,0,0.4} -\usepackage{hyperref} -\hypersetup{ - breaklinks=true, - colorlinks=true, - linkcolor=linkcolor, - urlcolor=linkcolor, - citecolor=black, - filecolor=black, - %filecolor=blue, - pdfmenubar=true, - pdftoolbar=true, - bookmarksdepth=3 % Uncomment (and tweak) for PDF bookmarks with more levels than the TOC - } -%\hyperbaseurl{} % hyperlinks are relative to this root - -\setcounter{tocdepth}{2} % levels in table of contents - -% Tricks for having figures close to where they are defined: -% 1. define less restrictive rules for where to put figures -\setcounter{topnumber}{2} -\setcounter{bottomnumber}{2} -\setcounter{totalnumber}{4} -\renewcommand{\topfraction}{0.95} -\renewcommand{\bottomfraction}{0.95} -\renewcommand{\textfraction}{0} -\renewcommand{\floatpagefraction}{0.75} -% floatpagefraction must always be less than topfraction! -% 2. ensure all figures are flushed before next section -\usepackage[section]{placeins} -% 3. enable begin{figure}[H] (often leads to ugly pagebreaks) -%\usepackage{float}\restylefloat{figure} - -% --- fancyhdr package for fancy headers --- -\usepackage{fancyhdr} -\fancyhf{} % sets both header and footer to nothing -\renewcommand{\headrulewidth}{0pt} -\fancyfoot[LE,RO]{\thepage} -% Ensure copyright on titlepage (article style) and chapter pages (book style) -\fancypagestyle{plain}{ - \fancyhf{} - \fancyfoot[C]{{\footnotesize \copyright\ 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license}} -% \renewcommand{\footrulewidth}{0mm} - \renewcommand{\headrulewidth}{0mm} -} -% Ensure copyright on titlepages with \thispagestyle{empty} -\fancypagestyle{empty}{ - \fancyhf{} - \fancyfoot[C]{{\footnotesize \copyright\ 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license}} - \renewcommand{\footrulewidth}{0mm} - \renewcommand{\headrulewidth}{0mm} -} - -\pagestyle{fancy} - - -\usepackage[framemethod=TikZ]{mdframed} - -% --- begin definitions of admonition environments --- - -% --- end of definitions of admonition environments --- - -% prevent orhpans and widows -\clubpenalty = 10000 -\widowpenalty = 10000 - -% --- end of standard preamble for documents --- - - -% insert custom LaTeX commands... - -\raggedbottom -\makeindex -\usepackage[totoc]{idxlayout} % for index in the toc -\usepackage[nottoc]{tocbibind} % for references/bibliography in the toc - -%-------------------- end preamble ---------------------- - -\begin{document} - -% matching end for #ifdef PREAMBLE - -\newcommand{\exercisesection}[1]{\subsection*{#1}} - - -% ------------------- main content ---------------------- - - - -% ----------------- title ------------------------- - -\thispagestyle{empty} - -\begin{center} -{\LARGE\bf -\begin{spacing}{1.25} -Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks -\end{spacing} -} -\end{center} - -% ----------------- author(s) ------------------------- - -\begin{center} -{\bf Morten Hjorth-Jensen${}^{1, 2}$} \\ [0mm] -\end{center} - -\begin{center} -% List of all institutions: -\centerline{{\small ${}^1$Department of Physics, University of Oslo}} -\centerline{{\small ${}^2$Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University}} -\end{center} - -% ----------------- end author(s) ------------------------- - -% --- begin date --- -\begin{center} -Oct 19, 2018 -\end{center} -% --- end date --- - -\vspace{1cm} - - -% !split -\subsection*{Neural networks} - -Artificial neural networks are computational systems that can learn to -perform tasks by considering examples, generally without being -programmed with any task-specific rules. It is supposed to mimic a -biological system, wherein neurons interact by sending signals in the -form of mathematical functions between layers. All layers can contain -an arbitrary number of neurons, and each connection is represented by -a weight variable. - - -% !split -\subsection*{Artificial neurons} - -The field of artificial neural networks has a long history of -development, and is closely connected with the advancement of computer -science and computers in general. A model of artificial neurons was -first developed by McCulloch and Pitts in 1943 to study signal -processing in the brain and has later been refined by others. The -general idea is to mimic neural networks in the human brain, which is -composed of billions of neurons that communicate with each other by -sending electrical signals. Each neuron accumulates its incoming -signals, which must exceed an activation threshold to yield an -output. If the threshold is not overcome, the neuron remains inactive, -i.e.~has zero output. - -This behaviour has inspired a simple mathematical model for an artificial neuron. - -\begin{equation} - y = f\left(\sum_{i=1}^n w_ix_i\right) = f(u) - \label{artificialNeuron} -\end{equation} -Here, the output $y$ of the neuron is the value of its activation function, which have as input -a weighted sum of signals $x_i, \dots ,x_n$ received by $n$ other neurons. - -Conceptually, it is helpful to divide neural networks into four -categories: -\begin{enumerate} -\item general purpose neural networks for supervised learning, - -\item neural networks designed specifically for image processing, the most prominent example of this class being Convolutional Neural Networks (CNNs), - -\item neural networks for sequential data such as Recurrent Neural Networks (RNNs), and - -\item neural networks for unsupervised learning such as Deep Boltzmann Machines. -\end{enumerate} - -\noindent -In natural science, DNNs and CNNs have already found numerous -applications. In statistical physics, they have been applied to detect -phase transitions in 2D Ising and Potts models, lattice gauge -theories, and different phases of polymers, or solving the -Navier-Stokes equation in weather forecasting. Deep learning has also -found interesting applications in quantum physics. Various quantum -phase transitions can be detected and studied using DNNs and CNNs, -topological phases, and even non-equilibrium many-body -localization. Representing quantum states as DNNs quantum state -tomography are among some of the impressive achievements to reveal the -potential of DNNs to facilitate the study of quantum systems. - -In quantum information theory, it has been shown that one can perform -gate decompositions with the help of neural. - -The applications are not limited to the natural sciences. There is a -plethora of applications in essentially all disciplines, from the -humanities to life science and medicine. - -% !split -\subsection*{Neural network types} - -An artificial neural network (ANN), is a computational model that -consists of layers of connected neurons, or nodes or units. We will -refer to these interchangeably as units or nodes, and sometimes as -neurons. - -It is supposed to mimic a biological nervous system by letting each -neuron interact with other neurons by sending signals in the form of -mathematical functions between layers. A wide variety of different -ANNs have been developed, but most of them consist of an input layer, -an output layer and eventual layers in-between, called \emph{hidden -layers}. All layers can contain an arbitrary number of nodes, and each -connection between two nodes is associated with a weight variable. - -Neural networks (also called neural nets) are neural-inspired -nonlinear models for supervised learning. As we will see, neural nets -can be viewed as natural, more powerful extensions of supervised -learning methods such as linear and logistic regression and soft-max -methods we discussed earlier. - - -% !split -\subsection*{Feed-forward neural networks} - -The feed-forward neural network (FFNN) was the first and simplest type -of ANNs that were devised. In this network, the information moves in -only one direction: forward through the layers. - -Nodes are represented by circles, while the arrows display the -connections between the nodes, including the direction of information -flow. Additionally, each arrow corresponds to a weight variable -(figure to come). We observe that each node in a layer is connected -to \emph{all} nodes in the subsequent layer, making this a so-called -\emph{fully-connected} FFNN. - - - -% !split -\subsection*{Convolutional Neural Network} - -A different variant of FFNNs are \emph{convolutional neural networks} -(CNNs), which have a connectivity pattern inspired by the animal -visual cortex. Individual neurons in the visual cortex only respond to -stimuli from small sub-regions of the visual field, called a receptive -field. This makes the neurons well-suited to exploit the strong -spatially local correlation present in natural images. The response of -each neuron can be approximated mathematically as a convolution -operation. (figure to come) - -Convolutional neural networks emulate the behaviour of neurons in the -visual cortex by enforcing a \emph{local} connectivity pattern between -nodes of adjacent layers: Each node in a convolutional layer is -connected only to a subset of the nodes in the previous layer, in -contrast to the fully-connected FFNN. Often, CNNs consist of several -convolutional layers that learn local features of the input, with a -fully-connected layer at the end, which gathers all the local data and -produces the outputs. They have wide applications in image and video -recognition. - -% !split -\subsection*{Recurrent neural networks} - -So far we have only mentioned ANNs where information flows in one -direction: forward. \emph{Recurrent neural networks} on the other hand, -have connections between nodes that form directed \emph{cycles}. This -creates a form of internal memory which are able to capture -information on what has been calculated before; the output is -dependent on the previous computations. Recurrent NNs make use of -sequential information by performing the same task for every element -in a sequence, where each element depends on previous elements. An -example of such information is sentences, making recurrent NNs -especially well-suited for handwriting and speech recognition. - -% !split -\subsection*{Other types of networks} - -There are many other kinds of ANNs that have been developed. One type -that is specifically designed for interpolation in multidimensional -space is the radial basis function (RBF) network. RBFs are typically -made up of three layers: an input layer, a hidden layer with -non-linear radial symmetric activation functions and a linear output -layer (''linear'' here means that each node in the output layer has a -linear activation function). The layers are normally fully-connected -and there are no cycles, thus RBFs can be viewed as a type of -fully-connected FFNN. They are however usually treated as a separate -type of NN due the unusual activation functions. - -% !split -\subsection*{Multilayer perceptrons} - -One uses often so-called fully-connected feed-forward neural networks -with three or more layers (an input layer, one or more hidden layers -and an output layer) consisting of neurons that have non-linear -activation functions. - -Such networks are often called \emph{multilayer perceptrons} (MLPs). - -% !split -\subsection*{Why multilayer perceptrons?} - -According to the \emph{Universal approximation theorem}, a feed-forward -neural network with just a single hidden layer containing a finite -number of neurons can approximate a continuous multidimensional -function to arbitrary accuracy, assuming the activation function for -the hidden layer is a \textbf{non-constant, bounded and -monotonically-increasing continuous function}. - -Note that the requirements on the activation function only applies to -the hidden layer, the output nodes are always assumed to be linear, so -as to not restrict the range of output values. - - -% !split -\subsection*{Mathematical model} - -The output $y$ is produced via the activation function $f$ -\[ - y = f\left(\sum_{i=1}^n w_ix_i + b_i\right) = f(z), -\] -This function receives $x_i$ as inputs. -Here the activation $z=(\sum_{i=1}^n w_ix_i+b_i)$. -In an FFNN of such neurons, the \emph{inputs} $x_i$ are the \emph{outputs} of -the neurons in the preceding layer. Furthermore, an MLP is -fully-connected, which means that each neuron receives a weighted sum -of the outputs of \emph{all} neurons in the previous layer. - -% !split -\subsection*{Mathematical model} - -First, for each node $i$ in the first hidden layer, we calculate a weighted sum $z_i^1$ of the input coordinates $x_j$, - -\begin{equation} z_i^1 = \sum_{j=1}^{M} w_{ij}^1 x_j + b_i^1 -\end{equation} - -Here $b_i$ is the so-called bias which is normally needed in -case of zero activation weights or inputs. How to fix the biases and -the weights will be discussed below. The value of $z_i^1$ is the -argument to the activation function $f_i$ of each node $i$, The -variable $M$ stands for all possible inputs to a given node $i$ in the -first layer. We define the output $y_i^1$ of all neurons in layer 1 as - -\begin{equation} - y_i^1 = f(z_i^1) = f\left(\sum_{j=1}^M w_{ij}^1 x_j + b_i^1\right) - \label{outputLayer1} -\end{equation} - -where we assume that all nodes in the same layer have identical -activation functions, hence the notation $f$. In general, we could assume in the more general case that different layers have different activation functions. -In this case we would identify these functions with a superscript $l$ for the $l$-th layer, - -\begin{equation} - y_i^l = f^l(u_i^l) = f^l\left(\sum_{j=1}^{N_{l-1}} w_{ij}^l y_j^{l-1} + b_i^l\right) - \label{generalLayer} -\end{equation} - -where $N_l$ is the number of nodes in layer $l$. When the output of -all the nodes in the first hidden layer are computed, the values of -the subsequent layer can be calculated and so forth until the output -is obtained. - - - -% !split -\subsection*{Mathematical model} - -The output of neuron $i$ in layer 2 is thus, - -\begin{align} - y_i^2 &= f^2\left(\sum_{j=1}^N w_{ij}^2 y_j^1 + b_i^2\right) \\ - &= f^2\left[\sum_{j=1}^N w_{ij}^2f^1\left(\sum_{k=1}^M w_{jk}^1 x_k + b_j^1\right) + b_i^2\right] - \label{outputLayer2} -\end{align} -where we have substituted $y_k^1$ with the inputs $x_k$. Finally, the ANN output reads - -\begin{align} - y_i^3 &= f^3\left(\sum_{j=1}^N w_{ij}^3 y_j^2 + b_i^3\right) \\ - &= f_3\left[\sum_{j} w_{ij}^3 f^2\left(\sum_{k} w_{jk}^2 f^1\left(\sum_{m} w_{km}^1 x_m + b_k^1\right) + b_j^2\right) - + b_1^3\right] -\end{align} - -% !split -\subsection*{Mathematical model} - -We can generalize this expression to an MLP with $l$ hidden -layers. The complete functional form is, - -\begin{align} -&y^{l+1}_i = f^{l+1}\left[\!\sum_{j=1}^{N_l} w_{ij}^3 f^l\left(\sum_{k=1}^{N_{l-1}}w_{jk}^{l-1}\left(\dots f^1\left(\sum_{n=1}^{N_0} w_{mn}^1 x_n+ b_m^1\right)\dots\right)+b_k^2\right)+b_1^3\right] && - \label{completeNN} -\end{align} - -which illustrates a basic property of MLPs: The only independent -variables are the input values $x_n$. - -% !split -\subsection*{Mathematical model} - -This confirms that an MLP, despite its quite convoluted mathematical -form, is nothing more than an analytic function, specifically a -mapping of real-valued vectors $\hat{x} \in \mathbb{R}^n \rightarrow -\hat{y} \in \mathbb{R}^m$. - -Furthermore, the flexibility and universality of an MLP can be -illustrated by realizing that the expression is essentially a nested -sum of scaled activation functions of the form - -\begin{equation} - f(x) = c_1 f(c_2 x + c_3) + c_4 -\end{equation} - -where the parameters $c_i$ are weights and biases. By adjusting these -parameters, the activation functions can be shifted up and down or -left and right, change slope or be rescaled which is the key to the -flexibility of a neural network. - -% !split -\paragraph{Matrix-vector notation.} -We can introduce a more convenient notation for the activations in an A NN. - -Additionally, we can represent the biases and activations -as layer-wise column vectors $\hat{b}_l$ and $\hat{y}_l$, so that the $i$-th element of each vector -is the bias $b_i^l$ and activation $y_i^l$ of node $i$ in layer $l$ respectively. - -We have that $\mathrm{W}_l$ is an $N_{l-1} \times N_l$ matrix, while $\hat{b}_l$ and $\hat{y}_l$ are $N_l \times 1$ column vectors. -With this notation, the sum becomes a matrix-vector multiplication, and we can write -the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as -\begin{equation} - \hat{y}_2 = f_2(\mathrm{W}_2 \hat{y}_{1} + \hat{b}_{2}) = - f_2\left(\left[\begin{array}{ccc} - w^2_{11} &w^2_{12} &w^2_{13} \\ - w^2_{21} &w^2_{22} &w^2_{23} \\ - w^2_{31} &w^2_{32} &w^2_{33} \\ - \end{array} \right] \cdot - \left[\begin{array}{c} - y^1_1 \\ - y^1_2 \\ - y^1_3 \\ - \end{array}\right] + - \left[\begin{array}{c} - b^2_1 \\ - b^2_2 \\ - b^2_3 \\ - \end{array}\right]\right). -\end{equation} - -% !split -\paragraph{Matrix-vector notation and activation.} -The activation of node $i$ in layer 2 is - -\begin{equation} - y^2_i = f_2\Bigr(w^2_{i1}y^1_1 + w^2_{i2}y^1_2 + w^2_{i3}y^1_3 + b^2_i\Bigr) = - f_2\left(\sum_{j=1}^3 w^2_{ij} y_j^1 + b^2_i\right). -\end{equation} - -This is not just a convenient and compact notation, but also a useful -and intuitive way to think about MLPs: The output is calculated by a -series of matrix-vector multiplications and vector additions that are -used as input to the activation functions. For each operation -$\mathrm{W}_l \hat{y}_{l-1}$ we move forward one layer. - - -% !split -\paragraph{Activation functions.} -A property that characterizes a neural network, other than its -connectivity, is the choice of activation function(s). As described -in, the following restrictions are imposed on an activation function -for a FFNN to fulfill the universal approximation theorem - -\begin{itemize} - \item Non-constant - - \item Bounded - - \item Monotonically-increasing - - \item Continuous -\end{itemize} - -\noindent -% !split -\paragraph{Activation functions, Logistic and Hyperbolic ones.} -The second requirement excludes all linear functions. Furthermore, in -a MLP with only linear activation functions, each layer simply -performs a linear transformation of its inputs. - -Regardless of the number of layers, the output of the NN will be -nothing but a linear function of the inputs. Thus we need to introduce -some kind of non-linearity to the NN to be able to fit non-linear -functions Typical examples are the logistic \emph{Sigmoid} - -\[ - f(x) = \frac{1}{1 + e^{-x}}, -\] -and the \emph{hyperbolic tangent} function -\[ - f(x) = \tanh(x) -\] - -% !split -\paragraph{Relevance.} -The \emph{sigmoid} function are more biologically plausible because the -output of inactive neurons are zero. Such activation function are -called \emph{one-sided}. However, it has been shown that the hyperbolic -tangent performs better than the sigmoid for training MLPs. has -become the most popular for \emph{deep neural networks} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -"""The sigmoid function (or the logistic curve) is a -function that takes any real number, z, and outputs a number (0,1). -It is useful in neural networks for assigning weights on a relative scale. -The value z is the weighted sum of parameters involved in the learning algorithm.""" - -import numpy -import matplotlib.pyplot as plt -import math as mt - -z = numpy.arange(-5, 5, .1) -sigma_fn = numpy.vectorize(lambda z: 1/(1+numpy.exp(-z))) -sigma = sigma_fn(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, sigma) -ax.set_ylim([-0.1, 1.1]) -ax.set_xlim([-5,5]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('sigmoid function') - -plt.show() - -"""Step Function""" -z = numpy.arange(-5, 5, .02) -step_fn = numpy.vectorize(lambda z: 1.0 if z >= 0.0 else 0.0) -step = step_fn(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, step) -ax.set_ylim([-0.5, 1.5]) -ax.set_xlim([-5,5]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('step function') - -plt.show() - -"""Sine Function""" -z = numpy.arange(-2*mt.pi, 2*mt.pi, 0.1) -t = numpy.sin(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, t) -ax.set_ylim([-1.0, 1.0]) -ax.set_xlim([-2*mt.pi,2*mt.pi]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('sine function') - -plt.show() - -"""Plots a graph of the squashing function used by a rectified linear -unit""" -z = numpy.arange(-2, 2, .1) -zero = numpy.zeros(len(z)) -y = numpy.max([zero, z], axis=0) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, y) -ax.set_ylim([-2.0, 2.0]) -ax.set_xlim([-2.0, 2.0]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('Rectified linear unit') - -plt.show() -\end{minted} - - -% !split -\subsection*{The multilayer perceptron (MLP)} - -The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of -\begin{enumerate} -\item A neural network with one or more layers of nodes between the input and the output nodes. - -\item The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer. - -\item The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer. -\end{enumerate} - -\noindent -As a convention it is normal to call a network with one layer of input units, one layer of hidden -units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc. - -For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units. -Hereafter we will call the various entities of a layer for nodes. -There are also no connections within a single layer. - -The number of input nodes does not need to equal the number of output -nodes. This applies also to the hidden layers. Each layer may have its -own number of nodes and activation functions. - -The hidden layers have their name from the fact that they are not -linked to observables and as we will see below when we define the -so-called activation $\hat{z}$, we can think of this as a basis -expansion of the original inputs $\hat{x}$. The difference however -between neural networks and say linear regression is that now these -basis functions (which will correspond to the weights in the network) -are learned from data. This results in an important difference between -neural networks and deep learning approaches on one side and methods -like logistic regression or linear regression and their modifications on the other side. - - -% !split -\subsection*{From one to many layers, the universal approximation theorem} - - -A neural network with only one layer, what we called the simple -perceptron, is best suited if we have a standard binary model with -clear (linear) boundaries between the outcomes. As such it could -equally well be replaced by standard linear regression or logistic -regression. Networks with one or more hidden layers approximate -systems with more complex boundaries. - -As stated earlier, -an important theorem in studies of neural networks, restated without -proof here, is the \href{{http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.441.7873&rep=rep1&type=pdf}}{universal approximation -theorem}. - -It states that a feed-forward network with a single hidden layer -containing a finite number of neurons can approximate continuous -functions on compact subsets of real functions. The theorem thus -states that simple neural networks can represent a wide variety of -interesting functions when given appropriate parameters. It is the -multilayer feedforward architecture itself which gives neural networks -the potential of being universal approximators. - - -% !split -\subsection*{Deriving the back propagation code for a multilayer perceptron model} - - -\textbf{Note: figures will be inserted later!} - -As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications. -The unknowwn quantities are our weights $w_{ij}$ and we need to find an algorithm for changing them so that our errors are as small as possible. -This leads us to the famous \href{{https://www.nature.com/articles/323533a0}}{back propagation algorithm}. - -The questions we want to ask are how do changes in the biases and the -weights in our network change the cost function and how can we use the -final output to modify the weights? - -To derive these equations let us start with a plain regression problem -and define our cost function as - -\[ -{\cal C}(\hat{W}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2, -\] - -where the $t_i$s are our $n$ targets (the values we want to -reproduce), while the outputs of the network after having propagated -all inputs $\hat{x}$ are given by $y_i$. Below we will demonstrate -how the basic equations arising from the back propagation algorithm -can be modified in order to study classification problems with $K$ -classes. - -% !split -\subsection*{Definitions} - -With our definition of the targets $\hat{t}$, the outputs of the -network $\hat{y}$ and the inputs $\hat{x}$ we -define now the activation $z_j^l$ of node/neuron/unit $j$ of the -$l$-th layer as a function of the bias, the weights which add up from -the previous layer $l-1$ and the forward passes/outputs -$\hat{a}^{l-1}$ from the previous layer as - - -\[ -z_j^l = \sum_{i=1}^{M_{l-1}}w_{ij}^la_i^{l-1}+b_j^l, -\] - -where $b_k^l$ are the biases from layer $l$. Here $M_{l-1}$ -represents the total number of nodes/neurons/units of layer $l-1$. The -figure here illustrates this equation. We can rewrite this in a more -compact form as the matrix-vector products we discussed earlier, - -\[ -\hat{z}^l = \left(\hat{W}^l\right)^T\hat{a}^{l-1}+\hat{b}^l. -\] - -With the activation values $\hat{z}^l$ we can in turn define the -output of layer $l$ as $\hat{a}^l = f(\hat{z}^l)$ where $f$ is our -activation function. In the examples here we will use the sigmoid -function discussed in our logistic regression lectures. We will also use the same activation function $f$ for all layers -and their nodes. It means we have - -\[ -a_j^l = f(z_j^l) = \frac{1}{1+\exp{-(z_j^l)}}. -\] - - -% !split -\subsection*{Derivatives and the chain rule} - -From the definition of the activation $z_j^l$ we have -\[ -\frac{\partial z_j^l}{\partial w_{ij}^l} = a_i^{l-1}, -\] -and -\[ -\frac{\partial z_j^l}{\partial a_i^{l-1}} = w_{ji}^l. -\] - -With our definition of the activation function we have that (note that this function depends only on $z_j^l$) -\[ -\frac{\partial a_j^l}{\partial z_j^{l}} = a_j^l(1-a_j^l)=f(z_j^l)(1-f(z_j^l)). -\] - - -% !split -\subsection*{Derivative of the cost function} - -With these definitions we can now compute the derivative of the cost function in terms of the weights. - -Let us specialize to the output layer $l=L$. Our cost function is -\[ -{\cal C}(\hat{W^L}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2=\frac{1}{2}\sum_{i=1}^n\left(a_i^L - t_i\right)^2, -\] -The derivative of this function with respect to the weights is - -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)\frac{\partial a_j^L}{\partial w_{jk}^{L}}, -\] -The last partial derivative can easily be computed and reads (by applying the chain rule) -\[ -\frac{\partial a_j^L}{\partial w_{jk}^{L}} = \frac{\partial a_j^L}{\partial z_{j}^{L}}\frac{\partial z_j^L}{\partial w_{jk}^{L}}=a_j^L(1-a_j^L)a_k^{L-1}, -\] - - - -% !split -\subsection*{Bringing it together, first back propagation equation} - -We have thus -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)a_j^L(1-a_j^L)a_k^{L-1}, -\] - -Defining -\[ -\delta_j^L = a_j^L(1-a_j^L)\left(a_j^L - t_j\right) = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\] -and using the Hadamard product of two vectors we can write this as -\[ -\hat{\delta}^L = f'(\hat{z}^L)\circ\frac{\partial {\cal C}}{\partial (\hat{a}L)}. -\] - -This is an important expression. The second term on the right handside -measures how fast the cost function is changing as a function of the $j$th -output activation. If, for example, the cost function doesn't depend -much on a particular output node $j$, then $\delta_j^L$ will be small, -which is what we would expect. The first term on the right, measures -how fast the activation function $f$ is changing at a given activation -value $z_j^L$. - -Notice that everything in the above equations is easily computed. In -particular, we compute $z_j^L$ while computing the behaviour of the -network, and it is only a small additional overhead to compute -$f'(z^L_j)$. The exact form of the derivative with respect to the -output depends on the form of the cost function. -However, provided the cost function is known there should be little -trouble in calculating - -\[ -\frac{\partial {\cal C}}{\partial (a_j^L)} -\] - -With the definition of $\delta_j^L$ we have a more compact definition of the derivative of the cost function in terms of the weights, namely -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}. -\] - -% !split -\subsection*{Derivatives in terms of $z_j^L$} - -It is also easy to see that our previous equation can be written as - -\[ -\delta_j^L =\frac{\partial {\cal C}}{\partial z_j^L}= \frac{\partial {\cal C}}{\partial a_j^L}\frac{\partial a_j^L}{\partial z_j^L}, -\] -which can also be interpreted as the partial derivative of the cost function with respect to the biases $b_j^L$, namely -\[ -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}\frac{\partial b_j^L}{\partial z_j^L}=\frac{\partial {\cal C}}{\partial b_j^L}, -\] -That is, the error $\delta_j^L$ is exactly equal to the rate of change of the cost function as a function of the bias. -% !split -\subsection*{Bringing it together} - -We have now three equations that are essential for the computations of the derivatives of the cost function at the output layer. These equations are needed to start the algorithm and they are - - -% --- begin paragraph admon --- -\paragraph{The starting equations.} - -\begin{equation} -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}, -\end{equation} -and -\begin{equation} -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\end{equation} -and - -\begin{equation} -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}, -\end{equation} -% --- end paragraph admon --- - - - - -An interesting consequence of the above equations is that when the -activation $a_k^{L-1}$ is small, the gradient term, that is the -derivative of the cost function with respect to the weights, will also -tend to be small. We say then that the weight learns slowly, meaning -that it changes slowly when we minimize the weights via say gradient -descent. In this case we say the system learns slowly. - -Another interesting feature is that is when the activation function, -represented by the sigmoid function here, is rather flat when we move towards -its end values $0$ and $1$ (see the above Python codes). In these -cases, the derivatives of the activation function will also be close -to zero, meaning again that the gradients will be small and the -network learns slowly again. - - - -We need a fourth equation and we are set. We are going to propagate -backwards in order to the determine the weights and biases. In order -to do so we need to represent the error in the layer before the final -one $L-1$ in terms of the errors in the final output layer. - -% !split -\subsection*{Final back propagating equation} - -We have that (replacing $L$ with a general layer $l$) -\[ -\delta_j^l =\frac{\partial {\cal C}}{\partial z_j^l}. -\] -We want to express this in terms of the equations for layer $l+1$. Using the chain rule and summing over all $k$ entries we have - -\[ -\delta_j^l =\sum_k \frac{\partial {\cal C}}{\partial z_k^{l+1}}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}=\sum_k \delta_k^{l+1}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}, -\] -and recalling that -\[ -z_j^{l+1} = \sum_{i=1}^{M_{l}}w_{ij}^{l+1}a_j^{l}+b_j^{l+1}, -\] -with $M_l$ being the number of nodes in layer $l$, we obtain -\[ -\delta_j^l =\sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l), -\] -This is our final equation. - -We are now ready to set up the algorithm for back propagation and learning the weights and biases. - -% !split -\subsection*{Setting up the Back propagation algorithm} - - - -The four equations provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm. - - -% --- begin paragraph admon --- -\paragraph{} -First, we set up the input data $\hat{x}$ and the activations -$\hat{z}_1$ of the input layer and compute the activation function and -the pertinent outputs $\hat{a}^1$. -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Secondly, we perform then the feed forward till we reach the output -layer and compute all $\hat{z}_l$ of the input layer and compute the -activation function and the pertinent outputs $\hat{a}^l$ for -$l=2,3,\dots,L$. -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Thereafter we compute the ouput error $\hat{\delta}^L$ by computing all -\[ -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}. -\] -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Then we compute the back propagate error for each $l=L-1,L-2,\dots,2$ as -\[ -\delta_j^l = \sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l). -\] -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Finally, we update the weights and the biases using gradient descent for each $l=L-1,L-2,\dots,2$ and update the weights and biases according to the rules -\[ -w_{jk}^l\leftarrow = w_{jk}^l- \eta \delta_j^la_k^{l-1}, -\] - -\[ -b_j^l \leftarrow b_j^l-\eta \frac{\partial {\cal C}}{\partial b_j^l}=b_j^l-\eta \delta_j^l, -\] -% --- end paragraph admon --- - - - -The parameter $\eta$ is the learning parameter discussed in connection with the gradient descent methods. -Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training. - - -% !split -\subsection*{Setting up a Multi-layer perceptron model for classification} - -We are now gong to develop an example based on the MNIST data -base. This is a classification problem and we need to use our -cross-entropy function we discussed in connection with logistic -regression. The cross-entropy defines our cost function for the -classificaton problems with neural networks. - -In binary classification with two classes $(0, 1)$ we define the -logistic/sigmoid function as the probability that a particular input -is in class $0$ or $1$. This is possible because the logistic -function takes any input from the real numbers and inputs a number -between 0 and 1, and can therefore be interpreted as a probability. It -also has other nice properties, such as a derivative that is simple to -calculate. - -For an input $\boldsymbol{a}$ from the hidden layer, the probability that the input $\boldsymbol{x}$ -is in class 0 or 1 is just. We let $\theta$ represent the unknown weights and biases to be adjusted by our equations). The variable $x$ -represents our activation values $z$. We have -\[ -P(y = 0 \mid \hat{x}, \hat{\theta}) = \frac{1}{1 + \exp{(- \hat{x}})} , -\] -and -\[ -P(y = 1 \mid \hat{x}, \hat{\theta}) = 1 - P(y = 0 \mid \hat{x}, \hat{\theta}) , -\] - -where $y \in \{0, 1\}$ and $\hat{\theta}$ represents the weights and biases -of our network. - - -% !split -\subsection*{Defining the cost function} - -Our cost function is given as (see the Logistic regression lectures) -\[ -\mathcal{C}(\hat{\theta}) = - \ln P(\mathcal{D} \mid \hat{\theta}) = - \sum_{i=1}^n -y_i \ln[P(y_i = 0)] + (1 - y_i) \ln [1 - P(y_i = 0)] = \sum_{i=1}^n \mathcal{L}_i(\hat{\theta}) . -\] - -This last equality means that we can interpret our \emph{cost} function as a sum over the \emph{loss} function -for each point in the dataset $\mathcal{L}_i(\hat{\theta})$. -The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather -than maximizing a negative number. - -In \emph{multiclass} classification it is common to treat each integer label as a so called \emph{one-hot} vector: - -$y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$ and - - -$y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$ - - -i.e.~a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset (numbers from $0$ to $9$).. - -If $\hat{x}_i$ is the $i$-th input (image), $y_{ic}$ refers to the $c$-th component of the $i$-th -output vector $\hat{y}_i$. -The probability of $\hat{x}_i$ being in class $c$ will be given by the softmax function: - -\[ -P(y_{ic} = 1 \mid \hat{x}_i, \hat{\theta}) = \frac{\exp{((\hat{a}_i^{hidden})^T \hat{w}_c)}} -{\sum_{c'=0}^{C-1} \exp{((\hat{a}_i^{hidden})^T \hat{w}_{c'})}} , -\] - -which reduces to the logistic function in the binary case. -The likelihood of this $C$-class classifier -is now given as: - -\[ -P(\mathcal{D} \mid \hat{\theta}) = \prod_{i=1}^n \prod_{c=0}^{C-1} [P(y_{ic} = 1)]^{y_{ic}} . -\] -Again we take the negative log-likelihood to define our cost function: - -\[ -\mathcal{C}(\hat{\theta}) = - \log{P(\mathcal{D} \mid \hat{\theta})}. -\] -See the logistic regression lectures for a full definition of the cost function. - -The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before! - -% !split -\subsection*{Example: binary classification problem} - -As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters $\beta$ as -\[ -\mathcal{C}(\hat{\beta}) = - \sum_{i=1}^n \left(y_i\log{p(y_i \vert x_i,\hat{\beta})}+(i-y_i)\log{1-p(y_i \vert x_i,\hat{\beta})}\right), -\] -where we had defined the logistic (sigmoid) function -\[ -p(y_i =1\vert x_i,\hat{\beta})=\frac{\exp{(\beta_0+\beta_1 x_i)}}{1+\exp{(\beta_0+\beta_1 x_i)}}, -\] -and -\[ -p(y_i =0\vert x_i,\hat{\beta})=1-p(y_i =1\vert x_i,\hat{\beta}). -\] -The parameters $\hat{\beta}$ were defined using a minimization method like gradient descent or Newton-Raphson's method. - -Now we replace $x_i$ with the activation $z_i^l$ for a given layer $l$ and the outputs as $y_i=a_i^l=f(z_i^l)$, with $z_i^l$ now being a function of the weights $w_{ij}^l$ and biases $b_i^l$. -We have then -\[ -a_i^l = y_i = \frac{\exp{(z_i^l)}}{1+\exp{(z_i^l)}}, -\] -with -\[ -z_i^l = \sum_{j}w_{ij}^l a_j^{l-1}+b_i^l, -\] -where the superscript $l-1$ indicates that these are the outputs from layer $l-1$. -Our cost function at the final layer $l=L$ is now -\[ -\mathcal{C}(\hat{W}) = - \sum_{i=1}^n \left(t_i\log{a_i^L}+(i-t_i)\log{(1-a_i^L)}\right), -\] -where we have defined the targets $t_i$. The derivatives of the cost function with respect to the output $a_i^L$ are then easily calculated and we get -\[ -\frac{\partial \mathcal{C}(\hat{W})}{\partial a_i^L} = \frac{a_i^L-t_i}{a_i^L(1-a_i^L)}. -\] -In case we use another activation function than the logistic one, we need to evaluate other derivatives. - - -% !split -\subsection*{The Softmax function} -In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation $z_i^l$, that is we need -\[ -\frac{\partial f(z_i^l)}{\partial w_{jk}^l} = -\frac{\partial f(z_i^l)}{\partial z_j^l} \frac{\partial z_j^l}{\partial w_{jk}^l}= \frac{\partial f(z_i^l)}{\partial z_j^l}a_k^{l-1}. -\] -For the Softmax function we have -\[ -f(z_i^l) = \frac{\exp{(z_i^l)}}{\sum_{m=1}^K\exp{(z_m^l)}}. -\] -Its derivative with respect to $z_j^l$ gives -\[ -\frac{\partial f(z_i^l)}{\partial z_j^l}= f(z_i^l)\left(\delta_{ij}-f(z_j^l)\right), -\] -which in case of the simply binary model reduces to having $i=j$. - -% !split -\subsection*{Developing a code for doing neural networks with back propagation} - - -One can identify a set of key steps when using neural networks to solve supervised learning problems: - -\begin{enumerate} -\item Collect and pre-process data - -\item Define model and architecture - -\item Choose cost function and optimizer - -\item Train the model - -\item Evaluate model performance on test data - -\item Adjust hyperparameters (if necessary, network architecture) -\end{enumerate} - -\noindent -% !split -\subsection*{Collect and pre-process data} - -Here we will be using the MNIST dataset, which is readily available through the \textbf{scikit-learn} -package. You may also find it for example \href{{http://yann.lecun.com/exdb/mnist/}}{here}. -The \emph{MNIST} (Modified National Institute of Standards and Technology) database is a large database -of handwritten digits that is commonly used for training various image processing systems. -The MNIST dataset consists of 70 000 images of size $28\times 28$ pixels, each labeled from 0 to 9. -The scikit-learn dataset we will use consists of a selection of 1797 images of size $8\times 8$ collected and processed from this database. - -To feed data into a feed-forward neural network we need to represent -the inputs as a feature matrix $X = (n_{inputs}, n_{features})$. Each -row represents an \emph{input}, in this case a handwritten digit, and -each column represents a \emph{feature}, in this case a pixel. The -correct answers, also known as \emph{labels} or \emph{targets} are -represented as a 1D array of integers -$Y = (n_{inputs}) = (5, 3, 1, 8,...)$. - -As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from -measurements of height (in m) -and weight (in kg). If we have measurements of 5 people the feature matrix could be for example: - -$$ X = \begin{bmatrix} -1.85 {\&} 81\\ -1.71 {\&} 65\\ -1.95 {\&} 103\\ -1.55 {\&} 42\\ -1.63 {\&} 56 -\end{bmatrix} ,$$ - -and the targets would be: - -$$ Y = (23.7, 22.2, 27.1, 17.5, 21.1) $$ - -Since each input image is a 2D matrix, we need to flatten the image -(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a -feature matrix. This means we lose all spatial information in the -image, such as locality and translational invariance. More complicated -architectures such as Convolutional Neural Networks can take advantage -of such information, and are most commonly applied when analyzing -images. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# flatten the image -# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64 -n_inputs = len(inputs) -inputs = inputs.reshape(n_inputs, -1) -print("X = (n_inputs, n_features) = " + str(inputs.shape)) - - -# choose some random images to display -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\end{minted} - -% !split -\subsection*{Train and test datasets} - -Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions. - -We will reserve $80 \%$ of our dataset for training and $20 \%$ for testing. - -It is important that the train and test datasets are drawn randomly from our dataset, to ensure -no bias in the sampling. -Say you are taking measurements of weather data to predict the weather in the coming 5 days. -You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data -collected from 12.00 to 24.00. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from sklearn.model_selection import train_test_split - -# one-liner from scikit-learn library -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) - -# equivalently in numpy -def train_test_split_numpy(inputs, labels, train_size, test_size): - n_inputs = len(inputs) - inputs_shuffled = inputs.copy() - labels_shuffled = labels.copy() - - np.random.shuffle(inputs_shuffled) - np.random.shuffle(labels_shuffled) - - train_end = int(n_inputs*train_size) - X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:] - Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:] - - return X_train, X_test, Y_train, Y_test - -#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size) - -print("Number of training images: " + str(len(X_train))) -print("Number of test images: " + str(len(X_test))) -\end{minted} - -% !split -\subsection*{Define model and architecture} - -Our simple feed-forward neural network will consist of an \emph{input} layer, a single \emph{hidden} layer and an \emph{output} layer. The activation $y$ of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have - -$$ z = \sum_{i=1}^n w_i a_i ,$$ - -$$ y = f(z) ,$$ - -where $f$ is the activation function, $a_i$ represents input from neuron $i$ in the preceding layer -and $w_i$ is the weight to input $i$. -The activation of the neurons in the input layer is just the features (e.g.~a pixel value). - -The simplest activation function for a neuron is the \emph{Heaviside} function: - -$$ f(z) = -\begin{cases} -1, & z > 0\\ -0, & \text{otherwise} -\end{cases} -$$ - -A feed-forward neural network with this activation is known as a \emph{perceptron}. -For a binary classifier (i.e.~two classes, 0 or 1, dog or not-dog) we can also use this in our output layer. -This activation can be generalized to $k$ classes (using e.g.~the \emph{one-against-all} strategy), -and we call these architectures \emph{multiclass perceptrons}. - -However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and -Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function. - -Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU). -We will be using the sigmoid function $\sigma(x)$: - -$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$ - -which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions. - -% !split -\subsection*{Layers} - -\begin{itemize} -\item Input -\end{itemize} - -\noindent -Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons. - -\begin{itemize} -\item Hidden layer -\end{itemize} - -\noindent -We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer. -Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer. - -\begin{itemize} -\item Output -\end{itemize} - -\noindent -If we were building a binary classifier, it would be sufficient with a single neuron in the output layer, -which could output 0 or 1 according to the Heaviside function. This would be an example of a \emph{hard} classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a \emph{soft} classifier, which outputs the probability of being in class 0 or 1. - -For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class. - -Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons $j = 0,1,...,9$. The activation of each output neuron $j$ will be according to the \emph{softmax} function: - -$$ P(\text{class $j$} \mid \text{input $\hat{a}$}) = \frac{\exp{(\hat{a}^T \hat{w}_j)}} -{\sum_{c=0}^{9} \exp{(\hat{a}^T \hat{w}_c)}} ,$$ - -i.e.~each neuron $j$ outputs the probability of being in class $j$ given an input from the hidden layer $\hat{a}$, with $\hat{w}_j$ the weights of neuron $j$ to the inputs. -The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1. -The exponent is just the weighted sum of inputs as before: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$ - -Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500 -weights to the output layer. - -% !split -\subsection*{Weights and biases} - -Typically weights are initialized with small values distributed around zero, drawn from a uniform -or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless. - -Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range -of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron $j$, $b_j$: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$ - -The bias weights $\hat{b}$ are often initialized to zero, but a small value like $0.01$ ensures all neurons have some output which can be backpropagated in the first training cycle. -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# building our neural network - -n_inputs, n_features = X_train.shape -n_hidden_neurons = 50 -n_categories = 10 - -# we make the weights normally distributed using numpy.random.randn - -# weights and bias in the hidden layer -hidden_weights = np.random.randn(n_features, n_hidden_neurons) -hidden_bias = np.zeros(n_hidden_neurons) + 0.01 - -# weights and bias in the output layer -output_weights = np.random.randn(n_hidden_neurons, n_categories) -output_bias = np.zeros(n_categories) + 0.01 -\end{minted} - -% !split -\subsection*{Feed-forward pass} - -Denote $F$ the number of features, $H$ the number of hidden neurons and $C$ the number of categories. -For each input image we calculate a weighted sum of input features (pixel values) to each neuron $j$ in the hidden layer $l$: - -$$ z_{j}^{l} = \sum_{i=1}^{F} w_{ij}^{l} x_i + b_{j}^{l},$$ - -this is then passed through our activation function - -$$ a_{j}^{l} = f(z_{j}^{l}) .$$ - -We calculate a weighted sum of inputs (activations in the hidden layer) to each neuron $j$ in the output layer: - -$$ z_{j}^{L} = \sum_{i=1}^{H} w_{ij}^{L} a_{i}^{l} + b_{j}^{L}.$$ - -Finally we calculate the output of neuron $j$ in the output layer using the softmax function: - -$$ a_{j}^{L} = \frac{\exp{(z_j^{L})}} -{\sum_{c=0}^{C-1} \exp{(z_c^{L})}} .$$ - -% !split -\subsection*{Matrix multiplications} - -Since our data has the dimensions $X = (n_{inputs}, n_{features})$ and our weights to the hidden -layer have the dimensions -$W_{hidden} = (n_{features}, n_{hidden})$, -we can easily feed the network all our training data in one go by taking the matrix product - -$$ X W^{h} = (n_{inputs}, n_{hidden}),$$ - -and obtain a matrix that holds the weighted sum of inputs to the hidden layer -for each input image and each hidden neuron. -We also add the bias to obtain a matrix of weighted sums to the hidden layer $Z^{h}$: - -$$ \hat{z}^{l} = \hat{X} \hat{W}^{l} + \hat{b}^{l} ,$$ - -meaning the same bias (1D array with size equal number of hidden neurons) is added to each input image. -This is then passed through the activation: - -$$ \hat{a}^{l} = f(\hat{z}^l) .$$ - -This is fed to the output layer: - -$$ \hat{z}^{L} = \hat{a}^{L} \hat{W}^{L} + \hat{b}^{L} .$$ - -Finally we receive our output values for each image and each category by passing it through the softmax function: - -$$ output = softmax (\hat{z}^{L}) = (n_{inputs}, n_{categories}) .$$ - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# setup the feed-forward pass, subscript h = hidden layer - -def sigmoid(x): - return 1/(1 + np.exp(-x)) - -def feed_forward(X): - # weighted sum of inputs to the hidden layer - z_h = np.matmul(X, hidden_weights) + hidden_bias - # activation in the hidden layer - a_h = sigmoid(z_h) - - # weighted sum of inputs to the output layer - z_o = np.matmul(a_h, output_weights) + output_bias - # softmax output - # axis 0 holds each input and axis 1 the probabilities of each category - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - return probabilities - -probabilities = feed_forward(X_train) -print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape)) -print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0])) -print("probabilities sum up to: " + str(probabilities[0].sum())) -print() - -# we obtain a prediction by taking the class with the highest likelihood -def predict(X): - probabilities = feed_forward(X) - return np.argmax(probabilities, axis=1) - -predictions = predict(X_train) -print("predictions = (n_inputs) = " + str(predictions.shape)) -print("prediction for image 0: " + str(predictions[0])) -print("correct label for image 0: " + str(Y_train[0])) -\end{minted} - -% !split -\subsection*{Choose cost function and optimizer} - -To measure how well our neural network is doing we need to introduce a cost function. -We will call the function that gives the error of a single sample output the \emph{loss} function, and the function -that gives the total error of our network across all samples the \emph{cost} function. -A typical choice for multiclass classification is the \emph{cross-entropy} loss, also known as the negative log likelihood. - -In \emph{multiclass} classification it is common to treat each integer label as a so called \emph{one-hot} vector: - -$$ y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$$ - - -$$ y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$$ - - -i.e.~a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset. - -Let $y_{ic}$ denote the $c$-th component of the $i$-th one-hot vector. -We define the cost function $\mathcal{C}$ as a sum over the cross-entropy loss for each point $\hat{x}_i$ in the dataset. - -In the one-hot representation only one of the terms in the loss function is non-zero, namely the -probability of the correct category $c'$ -(i.e.~the category $c'$ such that $y_{ic'} = 1$). This means that the cross entropy loss only punishes you for how wrong -you got the correct label. The probability of category $c$ is given by the softmax function. The vector $\hat{\theta}$ represents the parameters of our network, i.e.~all the weights and biases. - - -% !split -\subsection*{Optimizing the cost function} - -The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is \emph{gradient descent} and its generalizations. The idea behind gradient descent -is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a \emph{local} minimum of the cost function. -Each parameter $\theta$ is iteratively adjusted according to the rule - -$$ \theta_{i+1} = \theta_i - \eta \nabla \mathcal{C}(\theta_i) ,$$ - -where $\eta$ is known as the \emph{learning rate}, which controls how big a step we take towards the minimum. -This update can be repeated for any number of iterations, or until we are satisfied with the result. - -A simple and effective improvement is a variant called \emph{Batch Gradient Descent}. -Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient -on a subset of the data called a \emph{minibatch}. -If there are $N$ data points and we have a minibatch size of $M$, the total number of batches -is $N/M$. -We denote each minibatch $B_k$, with $k = 1, 2,...,N/M$. The gradient then becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{M} \sum_{i \in B_k} \nabla \mathcal{L}_i(\theta) ,$$ - -i.e.~instead of averaging the loss over the entire dataset, we average over a minibatch. - -This has two important benefits: -\begin{enumerate} -\item Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima. - -\item It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient. -\end{enumerate} - -\noindent -The various optmization methods, with codes and algorithms, are discussed in our lectures on \href{{https://compphysics.github.io/MachineLearning/doc/pub/Splines/html/Splines-bs.html}}{Gradient descent approaches}. - -% !split -\subsection*{Regularization} - -It is common to add an extra term to the cost function, proportional -to the size of the weights. This is equivalent to constraining the -size of the weights, so that they do not grow out of control. -Constraining the size of the weights means that the weights cannot -grow arbitrarily large to fit the training data, and in this way -reduces \emph{overfitting}. - -We will measure the size of the weights using the so called \emph{L2-norm}, meaning our cost function becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) + \lambda \lvert \lvert \hat{w} \rvert \rvert_2^2 -= \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}(\theta) + \lambda \sum_{ij} w_{ij}^2,$$ - -i.e.~we sum up all the weights squared. The factor $\lambda$ is known as a regularization parameter. - - -In order to train the model, we need to calculate the derivative of -the cost function with respect to every bias and weight in the -network. In total our network has $(64 + 1)\times 50=3250$ weights in -the hidden layer and $(50 + 1)\times 10=510$ weights to the output -layer ($+1$ for the bias), and the gradient must be calculated for -every parameter. We use the \emph{backpropagation} algorithm discussed -above. This is a clever use of the chain rule that allows us to -calculate the gradient efficently. - - -% !split -\subsection*{Matrix multiplication} - -To more efficently train our network these equations are implemented using matrix operations. -The error in the output layer is calculated simply as, with $\hat{t}$ being our targets, - -$$ \delta_L = \hat{t} - \hat{y} = (n_{inputs}, n_{categories}) .$$ - -The gradient for the output weights is calculated as - -$$ \nabla W_{L} = \hat{a}^T \delta_L = (n_{hidden}, n_{categories}) ,$$ - -where $\hat{a} = (n_{inputs}, n_{hidden})$. This simply means that we are summing up the gradients for each input. -Since we are going backwards we have to transpose the activation matrix. - -The gradient with respect to the output bias is then - -$$ \nabla \hat{b}_{L} = \sum_{i=1}^{n_{inputs}} \delta_L = (n_{categories}) .$$ - -The error in the hidden layer is - -$$ \Delta_h = \delta_L W_{L}^T \circ f'(z_{h}) = \delta_L W_{L}^T \circ a_{h} \circ (1 - a_{h}) = (n_{inputs}, n_{hidden}) ,$$ - -where $f'(a_{h})$ is the derivative of the activation in the hidden layer. The matrix products mean -that we are summing up the products for each neuron in the output layer. The symbol $\circ$ denotes -the \emph{Hadamard product}, meaning element-wise multiplication. - -This again gives us the gradients in the hidden layer: - -$$ \nabla W_{h} = X^T \delta_h = (n_{features}, n_{hidden}) ,$$ - -$$ \nabla b_{h} = \sum_{i=1}^{n_{inputs}} \delta_h = (n_{hidden}) .$$ - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# to categorical turns our integer vector into a onehot representation -from sklearn.metrics import accuracy_score - -# one-hot in numpy -def to_categorical_numpy(integer_vector): - n_inputs = len(integer_vector) - n_categories = np.max(integer_vector) + 1 - onehot_vector = np.zeros((n_inputs, n_categories)) - onehot_vector[range(n_inputs), integer_vector] = 1 - - return onehot_vector - -#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test) -Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test) - -def feed_forward_train(X): - # weighted sum of inputs to the hidden layer - z_h = np.matmul(X, hidden_weights) + hidden_bias - # activation in the hidden layer - a_h = sigmoid(z_h) - - # weighted sum of inputs to the output layer - z_o = np.matmul(a_h, output_weights) + output_bias - # softmax output - # axis 0 holds each input and axis 1 the probabilities of each category - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - # for backpropagation need activations in hidden and output layers - return a_h, probabilities - -def backpropagation(X, Y): - a_h, probabilities = feed_forward_train(X) - - # error in the output layer - error_output = probabilities - Y - # error in the hidden layer - error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h) - - # gradients for the output layer - output_weights_gradient = np.matmul(a_h.T, error_output) - output_bias_gradient = np.sum(error_output, axis=0) - - # gradient for the hidden layer - hidden_weights_gradient = np.matmul(X.T, error_hidden) - hidden_bias_gradient = np.sum(error_hidden, axis=0) - - return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient - -print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train))) - -eta = 0.01 -lmbd = 0.01 -for i in range(1000): - # calculate gradients - dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot) - - # regularization term gradients - dWo += lmbd * output_weights - dWh += lmbd * hidden_weights - - # update weights and biases - output_weights -= eta * dWo - output_bias -= eta * dBo - hidden_weights -= eta * dWh - hidden_bias -= eta * dBh - -print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train))) -\end{minted} - -% !split -\subsection*{Improving performance} - -As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image. -In order to obtain a network that does something useful, we will have to do a bit more work. - -The choice of \emph{hyperparameters} such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a \emph{grid-search} is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates $\eta = 10^{-6}, 10^{-5},...,10^{-1}$ with different regularization parameters $\lambda = 10^{-6},...,10^{-0}$. - -Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an \emph{iteration}, and a full training period -going through the entire dataset ($n/M$ batches) an \emph{epoch}. - -If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers. -Andrew Ng goes through some of these considerations in this \href{{https://youtu.be/F1ka6a13S9I}}{video}. You can find a summary of the video \href{{https://kevinzakka.github.io/2016/09/26/applying-deep-learning/}}{here}. - -% !split -\subsection*{Full object-oriented implementation} - -It is very natural to think of the network as an object, with specific instances of the network -being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -class NeuralNetwork: - def __init__( - self, - X_data, - Y_data, - n_hidden_neurons=50, - n_categories=10, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - - ): - self.X_data_full = X_data - self.Y_data_full = Y_data - - self.n_inputs = X_data.shape[0] - self.n_features = X_data.shape[1] - self.n_hidden_neurons = n_hidden_neurons - self.n_categories = n_categories - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - self.create_biases_and_weights() - - def create_biases_and_weights(self): - self.hidden_weights = np.random.randn(self.n_features, self.n_hidden_neurons) - self.hidden_bias = np.zeros(self.n_hidden_neurons) + 0.01 - - self.output_weights = np.random.randn(self.n_hidden_neurons, self.n_categories) - self.output_bias = np.zeros(self.n_categories) + 0.01 - - def feed_forward(self): - # feed-forward for training - self.z_h = np.matmul(self.X_data, self.hidden_weights) + self.hidden_bias - self.a_h = sigmoid(self.z_h) - - self.z_o = np.matmul(self.a_h, self.output_weights) + self.output_bias - - exp_term = np.exp(self.z_o) - self.probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - def feed_forward_out(self, X): - # feed-forward for output - z_h = np.matmul(X, self.hidden_weights) + self.hidden_bias - a_h = sigmoid(z_h) - - z_o = np.matmul(a_h, self.output_weights) + self.output_bias - - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - return probabilities - - def backpropagation(self): - error_output = self.probabilities - self.Y_data - error_hidden = np.matmul(error_output, self.output_weights.T) * self.a_h * (1 - self.a_h) - - self.output_weights_gradient = np.matmul(self.a_h.T, error_output) - self.output_bias_gradient = np.sum(error_output, axis=0) - - self.hidden_weights_gradient = np.matmul(self.X_data.T, error_hidden) - self.hidden_bias_gradient = np.sum(error_hidden, axis=0) - - if self.lmbd > 0.0: - self.output_weights_gradient += self.lmbd * self.output_weights - self.hidden_weights_gradient += self.lmbd * self.hidden_weights - - self.output_weights -= self.eta * self.output_weights_gradient - self.output_bias -= self.eta * self.output_bias_gradient - self.hidden_weights -= self.eta * self.hidden_weights_gradient - self.hidden_bias -= self.eta * self.hidden_bias_gradient - - def predict(self, X): - probabilities = self.feed_forward_out(X) - return np.argmax(probabilities, axis=1) - - def predict_probabilities(self, X): - probabilities = self.feed_forward_out(X) - return probabilities - - def train(self): - data_indices = np.arange(self.n_inputs) - - for i in range(self.epochs): - for j in range(self.iterations): - # pick datapoints with replacement - chosen_datapoints = np.random.choice( - data_indices, size=self.batch_size, replace=False - ) - - # minibatch training data - self.X_data = self.X_data_full[chosen_datapoints] - self.Y_data = self.Y_data_full[chosen_datapoints] - - self.feed_forward() - self.backpropagation() -\end{minted} - -% !split -\subsection*{Evaluate model performance on test data} - -To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e.~the test data. -We measure the performance of the network using the \emph{accuracy} score. -The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of $1$. - -$$ \text{Accuracy} = \frac{\sum_{i=1}^n I(\hat{y}_i = y_i)}{n} ,$$ - -where $I$ is the indicator function, $1$ if $\hat{y}_i = y_i$ and $0$ otherwise. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -epochs = 100 -batch_size = 100 - -dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size, - n_hidden_neurons=n_hidden_neurons, n_categories=n_categories) -dnn.train() -test_predict = dnn.predict(X_test) - -# accuracy score from scikit library -print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict)) - -# equivalent in numpy -def accuracy_score_numpy(Y_test, Y_pred): - return np.sum(Y_test == Y_pred) / len(Y_test) - -#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict)) -\end{minted} - -% !split -\subsection*{Adjust hyperparameters} - -We now perform a grid search to find the optimal hyperparameters for the network. -Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around $98\%$ ($2\%$ error rate). - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -# store the models for later use -DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -# grid search -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size, - n_hidden_neurons=n_hidden_neurons, n_categories=n_categories) - dnn.train() - - DNN_numpy[i][j] = dnn - - test_predict = dnn.predict(X_test) - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict)) - print() -\end{minted} - -% !split -\subsection*{Visualization} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# visual representation of grid search -# uses seaborn heatmap, you can also do this with matplotlib imshow -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - dnn = DNN_numpy[i][j] - - train_pred = dnn.predict(X_train) - test_pred = dnn.predict(X_test) - - train_accuracy[i][j] = accuracy_score(Y_train, train_pred) - test_accuracy[i][j] = accuracy_score(Y_test, test_pred) - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - -% !split -\subsection*{scikit-learn implementation} - -\textbf{scikit-learn} focuses more -on traditional machine learning methods, such as regression, -clustering, decision trees, etc. As such, it has only two types of -neural networks: Multi Layer Perceptron outputting continuous values, -\emph{MPLRegressor}, and Multi Layer Perceptron outputting labels, -\emph{MLPClassifier}. We will see how simple it is to use these classes. - -\textbf{scikit-learn} implements a few improvements from our neural network, -such as early stopping, a varying learning rate, different -optimization methods, etc. We would therefore expect a better -performance overall. - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from sklearn.neural_network import MLPClassifier -# store models for later use -DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic', - alpha=lmbd, learning_rate_init=eta, max_iter=epochs) - dnn.fit(X_train, Y_train) - - DNN_scikit[i][j] = dnn - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Accuracy score on test set: ", dnn.score(X_test, Y_test)) - print() -\end{minted} - - -% !split -\subsection*{Visualization} -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - dnn = DNN_scikit[i][j] - - train_pred = dnn.predict(X_train) - test_pred = dnn.predict(X_test) - - train_accuracy[i][j] = accuracy_score(Y_train, train_pred) - test_accuracy[i][j] = accuracy_score(Y_test, test_pred) - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - - -% !split -\subsection*{Building neural networks in Tensorflow and Keras} - -Now we want to build on the experience gained from our neural network implementation in NumPy and scikit-learn -and use it to construct a neural network in Tensorflow. Once we have constructed a neural network in NumPy -and Tensorflow, building one in Keras is really quite trivial, though the performance may suffer. - -In our previous example we used only one hidden layer, and in this we will use two. From this it should be quite -clear how to build one using an arbitrary number of hidden layers, using data structures such as Python lists or -NumPy arrays. - -% !split -\subsection*{Tensorflow} - -Tensorflow is an open source library machine learning library -developed by the Google Brain team for internal use. It was released -under the Apache 2.0 open source license in November 9, 2015. - -Tensorflow is a computational framework that allows you to construct -machine learning models at different levels of abstraction, from -high-level, object-oriented APIs like Keras, down to the C++ kernels -that Tensorflow is built upon. The higher levels of abstraction are -simpler to use, but less flexible, and our choice of implementation -should reflect the problems we are trying to solve. - -\href{{https://www.tensorflow.org/guide/graphs}}{Tensorflow uses} so-called graphs to represent your computation -in terms of the dependencies between individual operations, such that you first build a Tensorflow \emph{graph} -to represent your model, and then create a Tensorflow \emph{session} to run the graph. - -In this guide we will analyze the same data as we did in our NumPy and -scikit-learn tutorial, gathered from the MNIST database of images. We -will give an introduction to the lower level Python Application -Program Interfaces (APIs), and see how we use them to build our graph. -Then we will build (effectively) the same graph in Keras, to see just -how simple solving a machine learning problem can be. - -To install tensorflow on Unix/Linux systems, use pip as -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -pip3 install tensorflow -\end{minted} -and/or if you use \textbf{anaconda}, just write (or install from the graphical user interface) -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -conda install tensorflow -\end{minted} - -% !split -\subsection*{Collect and pre-process data} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# flatten the image -# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64 -n_inputs = len(inputs) -inputs = inputs.reshape(n_inputs, -1) -print("X = (n_inputs, n_features) = " + str(inputs.shape)) - - -# choose some random images to display -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.utils import to_categorical -from sklearn.model_selection import train_test_split - -# one-hot representation of labels -labels = to_categorical(labels) - -# split into train and test data -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) -\end{minted} - -% !split -\subsection*{Using TensorFlow backend} - -\begin{enumerate} -\item Define model and architecture - -\item Choose cost function and optimizer -\end{enumerate} - -\noindent -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -import tensorflow as tf - -class NeuralNetworkTensorflow: - def __init__( - self, - X_train, - Y_train, - X_test, - Y_test, - n_neurons_layer1=100, - n_neurons_layer2=50, - n_categories=2, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - ): - - # keep track of number of steps - self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step') - - self.X_train = X_train - self.Y_train = Y_train - self.X_test = X_test - self.Y_test = Y_test - - self.n_inputs = X_train.shape[0] - self.n_features = X_train.shape[1] - self.n_neurons_layer1 = n_neurons_layer1 - self.n_neurons_layer2 = n_neurons_layer2 - self.n_categories = n_categories - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - # build network piece by piece - # name scopes (with) are used to enforce creation of new variables - # https://www.tensorflow.org/guide/variables - self.create_placeholders() - self.create_DNN() - self.create_loss() - self.create_optimiser() - self.create_accuracy() - - def create_placeholders(self): - # placeholders are fine here, but "Datasets" are the preferred method - # of streaming data into a model - with tf.name_scope('data'): - self.X = tf.placeholder(tf.float32, shape=(None, self.n_features), name='X_data') - self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data') - - def create_DNN(self): - with tf.name_scope('DNN'): - # the weights are stored to calculate regularization loss later - - # Fully connected layer 1 - self.W_fc1 = self.weight_variable([self.n_features, self.n_neurons_layer1], name='fc1', dtype=tf.float32) - b_fc1 = self.bias_variable([self.n_neurons_layer1], name='fc1', dtype=tf.float32) - a_fc1 = tf.nn.sigmoid(tf.matmul(self.X, self.W_fc1) + b_fc1) - - # Fully connected layer 2 - self.W_fc2 = self.weight_variable([self.n_neurons_layer1, self.n_neurons_layer2], name='fc2', dtype=tf.float32) - b_fc2 = self.bias_variable([self.n_neurons_layer2], name='fc2', dtype=tf.float32) - a_fc2 = tf.nn.sigmoid(tf.matmul(a_fc1, self.W_fc2) + b_fc2) - - # Output layer - self.W_out = self.weight_variable([self.n_neurons_layer2, self.n_categories], name='out', dtype=tf.float32) - b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32) - self.z_out = tf.matmul(a_fc2, self.W_out) + b_out - - def create_loss(self): - with tf.name_scope('loss'): - softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out)) - - regularizer_loss_fc1 = tf.nn.l2_loss(self.W_fc1) - regularizer_loss_fc2 = tf.nn.l2_loss(self.W_fc2) - regularizer_loss_out = tf.nn.l2_loss(self.W_out) - regularizer_loss = self.lmbd*(regularizer_loss_fc1 + regularizer_loss_fc2 + regularizer_loss_out) - - self.loss = softmax_loss + regularizer_loss - - def create_accuracy(self): - with tf.name_scope('accuracy'): - probabilities = tf.nn.softmax(self.z_out) - predictions = tf.argmax(probabilities, axis=1) - labels = tf.argmax(self.Y, axis=1) - - correct_predictions = tf.equal(predictions, labels) - correct_predictions = tf.cast(correct_predictions, tf.float32) - self.accuracy = tf.reduce_mean(correct_predictions) - - def create_optimiser(self): - with tf.name_scope('optimizer'): - self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step) - - def weight_variable(self, shape, name='', dtype=tf.float32): - initial = tf.truncated_normal(shape, stddev=0.1) - return tf.Variable(initial, name=name, dtype=dtype) - - def bias_variable(self, shape, name='', dtype=tf.float32): - initial = tf.constant(0.1, shape=shape) - return tf.Variable(initial, name=name, dtype=dtype) - - def fit(self): - data_indices = np.arange(self.n_inputs) - - with tf.Session() as sess: - sess.run(tf.global_variables_initializer()) - for i in range(self.epochs): - for j in range(self.iterations): - chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False) - batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints] - - sess.run([DNN.loss, DNN.optimizer], - feed_dict={DNN.X: batch_X, - DNN.Y: batch_Y}) - accuracy = sess.run(DNN.accuracy, - feed_dict={DNN.X: batch_X, - DNN.Y: batch_Y}) - step = sess.run(DNN.global_step) - - self.train_loss, self.train_accuracy = sess.run([DNN.loss, DNN.accuracy], - feed_dict={DNN.X: self.X_train, - DNN.Y: self.Y_train}) - - self.test_loss, self.test_accuracy = sess.run([DNN.loss, DNN.accuracy], - feed_dict={DNN.X: self.X_test, - DNN.Y: self.Y_test}) -\end{minted} - - -% !split -\subsection*{Optimizing and using gradient descent} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -epochs = 100 -batch_size = 100 -n_neurons_layer1 = 100 -n_neurons_layer2 = 50 -n_categories = 10 -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -\end{minted} - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -DNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - DNN = NeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test, - n_neurons_layer1, n_neurons_layer2, n_categories, - epochs=epochs, batch_size=batch_size, eta=eta, lmbd=lmbd) - DNN.fit() - - DNN_tf[i][j] = DNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % DNN.test_accuracy) - print() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - DNN = DNN_tf[i][j] - - train_accuracy[i][j] = DNN.train_accuracy - test_accuracy[i][j] = DNN.test_accuracy - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# we can use log files to visualize our graph in Tensorboard -writer = tf.summary.FileWriter('logs/') -writer.add_graph(tf.get_default_graph()) -\end{minted} - - -% !split -\subsection*{Using Keras} - -Keras is a high level \href{{https://en.wikipedia.org/wiki/Application_programming_interface}}{neural network} -that supports Tensorflow, CTNK and Theano as backends. -If you have Tensorflow installed Keras is available through the \emph{tf.keras} module. -If you have Anaconda installed you may run the following command -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -conda install keras -\end{minted} - -Alternatively, if you have Tensorflow or one of the other supported backends install you may use the pip package manager: - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -pip3 install keras -\end{minted} -or look up the \href{{https://keras.io/}}{instructions here}. - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.models import Sequential -from keras.layers import Dense -from keras.regularizers import l2 -from keras.optimizers import SGD - -def create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, eta, lmbd): - model = Sequential() - model.add(Dense(n_neurons_layer1, activation='sigmoid', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_neurons_layer2, activation='sigmoid', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_categories, activation='softmax')) - - sgd = SGD(lr=eta) - model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) - - return model -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -DNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - DNN = create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, - eta=eta, lmbd=lmbd) - DNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0) - scores = DNN.evaluate(X_test, Y_test) - - DNN_keras[i][j] = DNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % scores[1]) - print() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - DNN = DNN_keras[i][j] - - train_accuracy[i][j] = DNN.evaluate(X_train, Y_train)[1] - test_accuracy[i][j] = DNN.evaluate(X_test, Y_test)[1] - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - - - - -% !split -\subsection*{Which activation function should I use?} - -The Back propagation algorithm we derived above works by going from -the output layer to the input layer, propagating the error gradient on -the way. Once the algorithm has computed the gradient of the cost -function with regards to each parameter in the network, it uses these -gradients to update each parameter with a Gradient Descent (GD) step. - - -Unfortunately for us, the gradients often get smaller and smaller as the -algorithm progresses down to the first hidden layers. As a result, the -GD update leaves the lower layer connection weights -virtually unchanged, and training never converges to a good -solution. This is known in the literature as -\textbf{the vanishing gradients problem}. - -In other cases, the opposite can happen, namely the the gradients can grow bigger and -bigger. The result is that many of the layers get large updates of the -weights the -algorithm diverges. This is the \textbf{exploding gradients problem}, which is -mostly encountered in recurrent neural networks. More generally, deep -neural networks suffer from unstable gradients, different layers may -learn at widely different speeds - -% !split -\subsection*{Is the Logistic activation function (Sigmoid) our choice?} - -Although this unfortunate behavior has been empirically observed for -quite a while (it was one of the reasons why deep neural networks were -mostly abandoned for a long time), it is only around 2010 that -significant progress was made in understanding it. - -A paper titled \href{{http://proceedings.mlr.press/v9/glorot10a.html}}{Understanding the Difficulty of Training Deep -Feedforward Neural Networks by Xavier Glorot and Yoshua Bengio} found that -the problems with the popular logistic -sigmoid activation function and the weight initialization technique -that was most popular at the time, namely random initialization using -a normal distribution with a mean of 0 and a standard deviation of -1. - -They showed that with this activation function and this -initialization scheme, the variance of the outputs of each layer is -much greater than the variance of its inputs. Going forward in the -network, the variance keeps increasing after each layer until the -activation function saturates at the top layers. This is actually made -worse by the fact that the logistic function has a mean of 0.5, not 0 -(the hyperbolic tangent function has a mean of 0 and behaves slightly -better than the logistic function in deep networks). - - -% !split -\subsection*{The derivative of the Logistic funtion} - -Looking at the logistic activation function, when inputs become large -(negative or positive), the function saturates at 0 or 1, with a -derivative extremely close to 0. Thus when backpropagation kicks in, -it has virtually no gradient to propagate back through the network, -and what little gradient exists keeps getting diluted as -backpropagation progresses down through the top layers, so there is -really nothing left for the lower layers. - -In their paper, Glorot and Bengio propose a way to significantly -alleviate this problem. We need the signal to flow properly in both -directions: in the forward direction when making predictions, and in -the reverse direction when backpropagating gradients. We don’t want -the signal to die out, nor do we want it to explode and saturate. For -the signal to flow properly, the authors argue that we need the -variance of the outputs of each layer to be equal to the variance of -its inputs, and we also need the gradients to have equal variance -before and after flowing through a layer in the reverse direction. - - - -One of the insights in the 2010 paper by Glorot and Bengio was that -the vanishing/exploding gradients problems were in part due to a poor -choice of activation function. Until then most people had assumed that -if Nature had chosen to use roughly sigmoid activation functions in -biological neurons, they must be an excellent choice. But it turns out -that other activation functions behave much better in deep neural -networks, in particular the ReLU activation function, mostly because -it does not saturate for positive values (and also because it is quite -fast to compute). - - -% !split -\subsection*{The RELU function family} - -The ReLU activation function suffers from a problem known as the dying -ReLUs: during training, some neurons effectively die, meaning they -stop outputting anything other than 0. - -In some cases, you may find that half of your network’s neurons are -dead, especially if you used a large learning rate. During training, -if a neuron’s weights get updated such that the weighted sum of the -neuron’s inputs is negative, it will start outputting 0. When this -happen, the neuron is unlikely to come back to life since the gradient -of the ReLU function is 0 when its input is negative. - -To solve this problem, nowadays practitioners use a variant of the ReLU -function, such as the leaky ReLU discussed above or the so-called -exponential linear unit (ELU) function - - -\[ -ELU(z) = \left\{\begin{array}{cc} \alpha\left( \exp{(z)}-1\right) & z < 0,\\ z & z \ge 0.\end{array}\right. -\] - -% !split -\subsection*{Which activation function should we use?} - -In general it seems that the ELU activation function is better than -the leaky ReLU function (and its variants), which is better than -ReLU. ReLU performs better than $\tanh$ which in turn performs better -than the logistic function. - -If runtime -performance is an issue, then you may opt for the leaky ReLU function over the -ELU function If you don’t -want to tweak yet another hyperparameter, you may just use the default -$\alpha$ of $0.01$ for the leaky ReLU, and $1$ for ELU. If you have -spare time and computing power, you can use cross-validation or -bootstrap to evaluate other activation functions. - - -% !split -\subsection*{A top-down perspective on Neural networks} - - -The first thing we would like to do is divide the data into two or three -parts. A training set, a validation or dev (development) set, and a -test set. The test set is the data on which we want to make -predictions. The dev set is a subset of the training data we use to -check how well we are doing out-of-sample, after training the model on -the training dataset. We use the validation error as a proxy for the -test error in order to make tweaks to our model. It is crucial that we -do not use any of the test data to train the algorithm. This is a -cardinal sin in ML. Then: - - -\begin{itemize} -\item Estimate optimal error rate - -\item Minimize underfitting (bias) on training data set. - -\item Make sure you are not overfitting. -\end{itemize} - -\noindent -If the validation and test sets are drawn from the same distributions, -then a good performance on the validation set should lead to similarly -good performance on the test set. - -However, sometimes -the training data and test data differ in subtle ways because, for -example, they are collected using slightly different methods, or -because it is cheaper to collect data in one way versus another. In -this case, there can be a mismatch between the training and test -data. This can lead to the neural network overfitting these small -differences between the test and training sets, and a poor performance -on the test set despite having a good performance on the validation -set. To rectify this, Andrew Ng suggests making two validation or dev -sets, one constructed from the training data and one constructed from -the test data. The difference between the performance of the algorithm -on these two validation sets quantifies the train-test mismatch. This -can serve as another important diagnostic when using DNNs for -supervised learning. - -% !split -\subsection*{Limitations of supervised learning with deep networks} - -Like all statistical methods, supervised learning using neural -networks has important limitations. This is especially important when -one seeks to apply these methods, especially to physics problems. Like -all tools, DNNs are not a universal solution. Often, the same or -better performance on a task can be achieved by using a few -hand-engineered features (or even a collection of random -features). - -Here we list some of the important limitations of supervised neural network based models. - - - -\begin{itemize} -\item \textbf{Need labeled data}. All supervised learning methods, DNNs for supervised learning require labeled data. Often, labeled data is harder to acquire than unlabeled data (e.g.~one must pay for human experts to label images). - -\item \textbf{Supervised neural networks are extremely data intensive.} DNNs are data hungry. They perform best when data is plentiful. This is doubly so for supervised methods where the data must also be labeled. The utility of DNNs is extremely limited if data is hard to acquire or the datasets are small (hundreds to a few thousand samples). In this case, the performance of other methods that utilize hand-engineered features can exceed that of DNNs. - -\item \textbf{Homogeneous data.} Almost all DNNs deal with homogeneous data of one type. It is very hard to design architectures that mix and match data types (i.e.~some continuous variables, some discrete variables, some time series). In applications beyond images, video, and language, this is often what is required. In contrast, ensemble models like random forests or gradient-boosted trees have no difficulty handling mixed data types. - -\item \textbf{Many problems are not about prediction.} In natural science we are often interested in learning something about the underlying distribution that generates the data. In this case, it is often difficult to cast these ideas in a supervised learning setting. While the problems are related, it is possible to make good predictions with a \emph{wrong} model. The model might or might not be useful for understanding the underlying science. -\end{itemize} - -\noindent -Some of these remarks are particular to DNNs, others are shared by all supervised learning methods. This motivates the use of unsupervised methods which in part circumvent these problems. - - -% !split -\subsection*{Convolutional Neural Networks (recognizing images)} - -Convolutional Neural Networks (CNN) are very similar to ordinary Neural Networks. - -They are made up of neurons that have learnable weights and -biases. Each neuron receives some inputs, performs a dot product and -optionally follows it with a non-linearity. The whole network still -expresses a single differentiable score function: from the raw image -pixels on one end to class scores at the other. And they still have a -loss function (for example Softmax) on the last (fully-connected) layer -and all the tips/tricks we developed for learning regular Neural -Networks still apply (back propagation, gradient descent etc etc). - -What is the difference? \textbf{CNN architectures make the explicit assumption that -the inputs are images, which allows us to encode certain properties -into the architecture. These then make the forward function more -efficient to implement and vastly reduce the amount of parameters in -the network.} - -Here we provide only a superficial overview, for the more interested, we recommend highly the course -\href{{https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html}}{IN5400 – Machine Learning for Image Analysis} -and the slides of \href{{http://cs231n.github.io/convolutional-networks/}}{CS231}. - -% !split -\subsection*{Regular NNs don’t scale well to full images} - -As an example, consider -an image of size $32\times 32\times 3$ (32 wide, 32 high, 3 color channels), so a -single fully-connected neuron in a first hidden layer of a regular -Neural Network would have $32\times 32\times 3 = 3072$ weights. This amount still -seems manageable, but clearly this fully-connected structure does not -scale to larger images. For example, an image of more respectable -size, say $200\times 200\times 3$, would lead to neurons that have -$200\times 200\times 3 = 120,000$ weights. - -We could have -several such neurons, and the parameters would add up quickly! Clearly, -this full connectivity is wasteful and the huge number of parameters -would quickly lead to possible overfitting. - - -\begin{figure}[!ht] % - \centerline{\includegraphics[width=0.6\linewidth]{figslides/nn.jpeg}} - \caption{ - A regular 3-layer Neural Network. - } -\end{figure} -%\clearpage % flush figures - - -% !split -\subsection*{3D volumes of neurons} - -Convolutional Neural Networks take advantage of the fact that the -input consists of images and they constrain the architecture in a more -sensible way. - -In particular, unlike a regular Neural Network, the -layers of a CNN have neurons arranged in 3 dimensions: width, -height, depth. (Note that the word depth here refers to the third -dimension of an activation volume, not to the depth of a full Neural -Network, which can refer to the total number of layers in a network.) - -To understand it better, the above example of an image -with an input volume of -activations has dimensions $32\times 32\times 3$ (width, height, -depth respectively). - -The neurons in a layer will -only be connected to a small region of the layer before it, instead of -all of the neurons in a fully-connected manner. Moreover, the final -output layer could for this specific image have dimensions $1\times 1 times 10$, -because by the -end of the CNN architecture we will reduce the full image into a -single vector of class scores, arranged along the depth -dimension. - - -\begin{figure}[!ht] % - \centerline{\includegraphics[width=0.6\linewidth]{figslides/cnn.jpeg}} - \caption{ - A CNN arranges its neurons in three dimensions (width, height, depth), as visualized in one of the layers. Every layer of a CNN transforms the 3D input volume to a 3D output volume of neuron activations. In this example, the red input layer holds the image, so its width and height would be the dimensions of the image, and the depth would be 3 (Red, Green, Blue channels). - } -\end{figure} -%\clearpage % flush figures - - - - -% !split -\subsection*{Layers used to build CNNs} - - -A simple CNN is a sequence of layers, and every layer of a CNN -transforms one volume of activations to another through a -differentiable function. We use three main types of layers to build -CNN architectures: Convolutional Layer, Pooling Layer, and -Fully-Connected Layer (exactly as seen in regular Neural Networks). We -will stack these layers to form a full CNN architecture. - -A simple CNN for image classification could have the architecture: - -\begin{itemize} -\item \textbf{INPUT} ($32\times 32 \times 3$) will hold the raw pixel values of the image, in this case an image of width 32, height 32, and with three color channels R,G,B. - -\item \textbf{CONV} (convolutional )layer will compute the output of neurons that are connected to local regions in the input, each computing a dot product between their weights and a small region they are connected to in the input volume. This may result in volume such as $[32\times 32\times 12]$ if we decided to use 12 filters. - -\item \textbf{RELU} layer will apply an elementwise activation function, such as the $max(0,x)$ thresholding at zero. This leaves the size of the volume unchanged ($[32\times 32\times 12]$). - -\item \textbf{POOL} (pooling) layer will perform a downsampling operation along the spatial dimensions (width, height), resulting in volume such as $[16\times 16\times 12]$. - -\item \textbf{FC} (i.e.~fully-connected) layer will compute the class scores, resulting in volume of size $[1\times 1\times 10]$, where each of the 10 numbers correspond to a class score, such as among the 10 categories of the MNIST images we considered above . As with ordinary Neural Networks and as the name implies, each neuron in this layer will be connected to all the numbers in the previous volume. -\end{itemize} - -\noindent -% !split -\subsection*{Transforming images} - -CNNs transform the original image layer by layer from the original -pixel values to the final class scores. - -Observe that some layers contain -parameters and other don’t. In particular, the CNN layers perform -transformations that are a function of not only the activations in the -input volume, but also of the parameters (the weights and biases of -the neurons). On the other hand, the RELU/POOL layers will implement a -fixed function. The parameters in the CONV/FC layers will be trained -with gradient descent so that the class scores that the CNN computes -are consistent with the labels in the training set for each image. - - -% !split -\subsection*{CNNs in brief} - -In summary: - -\begin{itemize} -\item A CNN architecture is in the simplest case a list of Layers that transform the image volume into an output volume (e.g.~holding the class scores) - -\item There are a few distinct types of Layers (e.g.~CONV/FC/RELU/POOL are by far the most popular) - -\item Each Layer accepts an input 3D volume and transforms it to an output 3D volume through a differentiable function - -\item Each Layer may or may not have parameters (e.g.~CONV/FC do, RELU/POOL don’t) - -\item Each Layer may or may not have additional hyperparameters (e.g.~CONV/FC/POOL do, RELU doesn’t) -\end{itemize} - -\noindent -For more material on convolutional networks, we strongly recommend -the course -\href{{https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html}}{IN5400 – Machine Learning for Image Analysis} -and the slides of \href{{http://cs231n.github.io/convolutional-networks/}}{CS231} which is taught at Stanford University (consistently ranked as one of the top computer science programs in the world). \href{{http://neuralnetworksanddeeplearning.com/chap6.html}}{Michael Nielsen's book is a must read, in particular chapter 6 which deals with CNNs}. - - -% !split -\subsection*{CNNs in more detail, building convolutional neural networks in Tensorflow and Keras} - - -As discussed above, CNNs are neural networks built from the assumption that the inputs -to the network are 2D images. This is important because the number of features or pixels in images -grows very fast with the image size, and an enormous number of weights and biases are needed in order to build an accurate network. - -As before, we still have our input, a hidden layer and an output. What's novel about convolutional networks -are the \textbf{convolutional} and \textbf{pooling} layers stacked in pairs between the input and the hidden layer. -In addition, the data is no longer represented as a 2D feature matrix, instead each input is a number of 2D -matrices, typically 1 for each color dimension (Red, Green, Blue). - - -% !split -\subsection*{Setting it up} - -It means that to represent the entire -dataset of images, we require a 4D matrix or \textbf{tensor}. This tensor has the dimensions: -\[ -(n_{inputs},\, n_{pixels, width},\, n_{pixels, height},\, depth) . -\] - -% !split -\subsection*{The MNIST dataset again} - -The MNIST dataset consists of grayscale images with a pixel size of -$28\times 28$, meaning we require $28 \times 28 = 724$ weights to each -neuron in the first hidden layer. - -If we were to analyze images of size $128\times 128$ we would require -$128 \times 128 = 16384$ weights to each neuron. Even worse if we were -dealing with color images, as most images are, we have an image matrix -of size $128\times 128$ for each color dimension (Red, Green, Blue), -meaning 3 times the number of weights $= 49152$ are required for every -single neuron in the first hidden layer. - - -% !split -\subsection*{Strong correlations} -Images typically have strong local correlations, meaning that a small -part of the image varies little from its neighboring regions. If for -example we have an image of a blue car, we can roughly assume that a -small blue part of the image is surrounded by other blue regions. - -Therefore, instead of connecting every single pixel to a neuron in the -first hidden layer, as we have previously done with deep neural -networks, we can instead connect each neuron to a small part of the -image (in all 3 RGB depth dimensions). The size of each small area is -fixed, and known as a \href{{https://en.wikipedia.org/wiki/Receptive_field}}{receptive}. - - -% !split -\subsection*{Layers of a CNN} -The layers of a convolutional neural network arrange neurons in 3D: width, height and depth. -The input image is typically a square matrix of depth 3. - -A \textbf{convolution} is performed on the image which outputs -a 3D volume of neurons. The weights to the input are arranged in a number of 2D matrices, known as \textbf{filters}. - - -Each filter slides along the input image, taking the dot product -between each small part of the image and the filter, in all depth -dimensions. This is then passed through a non-linear function, -typically the \textbf{Rectified Linear (ReLu)} function, which serves as the -activation of the neurons in the first convolutional layer. This is -further passed through a \textbf{pooling layer}, which reduces the size of the -convolutional layer, e.g.~by taking the maximum or average across some -small regions, and this serves as input to the next convolutional -layer. - - -% !split -\subsection*{Systematic reduction} - -By systematically reducing the size of the input volume, through -convolution and pooling, the network should create representations of -small parts of the input, and then from them assemble representations -of larger areas. The final pooling layer is flattened to serve as -input to a hidden layer, such that each neuron in the final pooling -layer is connected to every single neuron in the hidden layer. This -then serves as input to the output layer, e.g.~a softmax output for -classification. - - -% !split -\subsection*{Prerequisites: Collect and pre-process data} -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -# RGB images have a depth of 3 -# our images are grayscale so they should have a depth of 1 -inputs = inputs[:,:,:,np.newaxis] - -print("inputs = (n_inputs, pixel_width, pixel_height, depth) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# choose some random images to display -n_inputs = len(inputs) -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\end{minted} - - -% !split -\subsection*{Importing Keras and Tensorflow} -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.utils import to_categorical -from sklearn.model_selection import train_test_split - -# representation of labels -labels = to_categorical(labels) - -# split into train and test data -# one-liner from scikit-learn library -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) -\end{minted} - -% !split -\subsection*{Using TensorFlow backend} - -We need to define model and architecture and choose cost function and optmizer. -\begin{Verbatim}[numbers=none,fontsize=\fontsize{9pt}{9pt},baselinestretch=0.95] - -import tensorflow as tf - -class ConvolutionalNeuralNetworkTensorflow: - def __init__( - self, - X_train, - Y_train, - X_test, - Y_test, - n_filters=10, - n_neurons_connected=50, - n_categories=10, - receptive_field=3, - stride=1, - padding=1, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - ): - - self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step') - - self.X_train = X_train - self.Y_train = Y_train - self.X_test = X_test - self.Y_test = Y_test - - self.n_inputs, self.input_width, self.input_height, self.depth = X_train.shape - - self.n_filters = n_filters - self.n_downsampled = int(self.input_width*self.input_height*n_filters / 4) - self.n_neurons_connected = n_neurons_connected - self.n_categories = n_categories - - self.receptive_field = receptive_field - self.stride = stride - self.strides = [stride, stride, stride, stride] - self.padding = padding - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - self.create_placeholders() - self.create_CNN() - self.create_loss() - self.create_optimiser() - self.create_accuracy() - - def create_placeholders(self): - with tf.name_scope('data'): - self.X = tf.placeholder(tf.float32, shape=(None, self.input_width, self.input_height, self.depth), name='X_data') - self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data') - - def create_CNN(self): - with tf.name_scope('CNN'): - - # Convolutional layer - self.W_conv = self.weight_variable([self.receptive_field, self.receptive_field, self.depth, self.n_filters], name='conv', dtype=tf.float32) - b_conv = self.weight_variable([self.n_filters], name='conv', dtype=tf.float32) - z_conv = tf.nn.conv2d(self.X, self.W_conv, self.strides, padding='SAME', name='conv') + b_conv - a_conv = tf.nn.relu(z_conv) - - # 2x2 max pooling - a_pool = tf.nn.max_pool(a_conv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME', name='pool') - - # Fully connected layer - a_pool_flat = tf.reshape(a_pool, [-1, self.n_downsampled]) - self.W_fc = self.weight_variable([self.n_downsampled, self.n_neurons_connected], name='fc', dtype=tf.float32) - b_fc = self.bias_variable([self.n_neurons_connected], name='fc', dtype=tf.float32) - a_fc = tf.nn.relu(tf.matmul(a_pool_flat, self.W_fc) + b_fc) - - # Output layer - self.W_out = self.weight_variable([self.n_neurons_connected, self.n_categories], name='out', dtype=tf.float32) - b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32) - self.z_out = tf.matmul(a_fc, self.W_out) + b_out - - def create_loss(self): - with tf.name_scope('loss'): - softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out)) - - regularizer_loss_conv = tf.nn.l2_loss(self.W_conv) - regularizer_loss_fc = tf.nn.l2_loss(self.W_fc) - regularizer_loss_out = tf.nn.l2_loss(self.W_out) - regularizer_loss = self.lmbd*(regularizer_loss_conv + regularizer_loss_fc + regularizer_loss_out) - - self.loss = softmax_loss + regularizer_loss - - def create_accuracy(self): - with tf.name_scope('accuracy'): - probabilities = tf.nn.softmax(self.z_out) - predictions = tf.argmax(probabilities, 1) - labels = tf.argmax(self.Y, 1) - - correct_predictions = tf.equal(predictions, labels) - correct_predictions = tf.cast(correct_predictions, tf.float32) - self.accuracy = tf.reduce_mean(correct_predictions) - - def create_optimiser(self): - with tf.name_scope('optimizer'): - self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step) - - def weight_variable(self, shape, name='', dtype=tf.float32): - initial = tf.truncated_normal(shape, stddev=0.1) - return tf.Variable(initial, name=name, dtype=dtype) - - def bias_variable(self, shape, name='', dtype=tf.float32): - initial = tf.constant(0.1, shape=shape) - return tf.Variable(initial, name=name, dtype=dtype) - - def fit(self): - data_indices = np.arange(self.n_inputs) - - with tf.Session() as sess: - sess.run(tf.global_variables_initializer()) - for i in range(self.epochs): - for j in range(self.iterations): - chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False) - batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints] - - sess.run([CNN.loss, CNN.optimizer], - feed_dict={CNN.X: batch_X, - CNN.Y: batch_Y}) - accuracy = sess.run(CNN.accuracy, - feed_dict={CNN.X: batch_X, - CNN.Y: batch_Y}) - step = sess.run(CNN.global_step) - - self.train_loss, self.train_accuracy = sess.run([CNN.loss, CNN.accuracy], - feed_dict={CNN.X: self.X_train, - CNN.Y: self.Y_train}) - - self.test_loss, self.test_accuracy = sess.run([CNN.loss, CNN.accuracy], - feed_dict={CNN.X: self.X_test, - CNN.Y: self.Y_test}) -\end{Verbatim} - -% !split -\subsection*{Train the model} - -We need now to train the model, evaluate it and test its performance on test data, and eventually include hyperparameters. -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -epochs = 100 -batch_size = 100 -n_filters = 10 -n_neurons_connected = 50 -n_categories = 10 - -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -CNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - CNN = ConvolutionalNeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test, - n_filters=n_filters, n_neurons_connected=n_neurons_connected, - n_categories=n_categories, epochs=epochs, batch_size=batch_size, - eta=eta, lmbd=lmbd) - CNN.fit() - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % CNN.test_accuracy) - print() - - CNN_tf[i][j] = CNN -\end{minted} - -% !split -\subsection*{Visualizing the results} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - CNN = CNN_tf[i][j] - - train_accuracy[i][j] = CNN.train_accuracy - test_accuracy[i][j] = CNN.test_accuracy - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - -% !split -\subsection*{Running with Keras} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.models import Sequential -from keras.layers.convolutional import Conv2D -from keras.layers.convolutional import MaxPooling2D -from keras.layers import Flatten -from keras.layers import Dense -from keras.regularizers import l2 -from keras.optimizers import SGD - -def create_convolutional_neural_network_keras(input_shape, receptive_field, - n_filters, n_neurons_connected, n_categories, - eta, lmbd): - model = Sequential() - model.add(Conv2D(n_filters, (receptive_field, receptive_field), input_shape=input_shape, padding='same', - activation='relu', kernel_regularizer=l2(lmbd))) - model.add(MaxPooling2D(pool_size=(2, 2))) - model.add(Flatten()) - model.add(Dense(n_neurons_connected, activation='relu', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_categories, activation='softmax', kernel_regularizer=l2(lmbd))) - - sgd = SGD(lr=eta) - model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) - - return model - -epochs = 100 -batch_size = 100 -input_shape = X_train.shape[1:4] -receptive_field = 3 -n_filters = 10 -n_neurons_connected = 50 -n_categories = 10 - -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -\end{minted} - -% !split -\subsection*{Final part} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -CNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - CNN = create_convolutional_neural_network_keras(input_shape, receptive_field, - n_filters, n_neurons_connected, n_categories, - eta, lmbd) - CNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0) - scores = CNN.evaluate(X_test, Y_test) - - CNN_keras[i][j] = CNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % scores[1]) - print() -\end{minted} - -% !split -\subsection*{Final visualization} - -\begin{Verbatim}[numbers=none,fontsize=\fontsize{9pt}{9pt},baselinestretch=0.95] -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - CNN = CNN_keras[i][j] - - train_accuracy[i][j] = CNN.evaluate(X_train, Y_train)[1] - test_accuracy[i][j] = CNN.evaluate(X_test, Y_test)[1] - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{Verbatim} - -% !split -\subsection*{Fun links} - -\begin{enumerate} -\item \href{{https://arxiv.org/abs/1604.07316}}{Self-Driving cars using a convolutional neural network} - -\item \href{{https://deepdreamgenerator.com/}}{Abstract art using convolutional neural networks} -\end{enumerate} - -\noindent - -% ------------------- end of main content --------------- - -\end{document} - diff --git a/doc/src/NeuralNet/NeuralNet-reveal.html b/doc/src/NeuralNet/NeuralNet-reveal.html deleted file mode 100644 index e9961348d..000000000 --- a/doc/src/NeuralNet/NeuralNet-reveal.html +++ /dev/null @@ -1,3916 +0,0 @@ -\ - - - - - - -Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - -
- - - - - - - - - - - - - - -
- - - - -

Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks

- -

- - -

-Morten Hjorth-Jensen [1, 2] -
- -

 
- - -

[1] Department of Physics, University of Oslo
-
[2] Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University
-
-

 
-

Oct 19, 2018

-
-

- -

- © 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license -
-
- - -
-

Neural networks

- -

-Artificial neural networks are computational systems that can learn to -perform tasks by considering examples, generally without being -programmed with any task-specific rules. It is supposed to mimic a -biological system, wherein neurons interact by sending signals in the -form of mathematical functions between layers. All layers can contain -an arbitrary number of neurons, and each connection is represented by -a weight variable. -

- - -
-

Artificial neurons

- -

-The field of artificial neural networks has a long history of -development, and is closely connected with the advancement of computer -science and computers in general. A model of artificial neurons was -first developed by McCulloch and Pitts in 1943 to study signal -processing in the brain and has later been refined by others. The -general idea is to mimic neural networks in the human brain, which is -composed of billions of neurons that communicate with each other by -sending electrical signals. Each neuron accumulates its incoming -signals, which must exceed an activation threshold to yield an -output. If the threshold is not overcome, the neuron remains inactive, -i.e. has zero output. - -

-This behaviour has inspired a simple mathematical model for an artificial neuron. - -

 
-$$ -\begin{equation} - y = f\left(\sum_{i=1}^n w_ix_i\right) = f(u) -\tag{1} -\end{equation} -$$ -

 
- -Here, the output \( y \) of the neuron is the value of its activation function, which have as input -a weighted sum of signals \( x_i, \dots ,x_n \) received by \( n \) other neurons. - -

-Conceptually, it is helpful to divide neural networks into four -categories: - -

    -

  1. general purpose neural networks for supervised learning,
  2. -

  3. neural networks designed specifically for image processing, the most prominent example of this class being Convolutional Neural Networks (CNNs),
  4. -

  5. neural networks for sequential data such as Recurrent Neural Networks (RNNs), and
  6. -

  7. neural networks for unsupervised learning such as Deep Boltzmann Machines.
  8. -
-

- -In natural science, DNNs and CNNs have already found numerous -applications. In statistical physics, they have been applied to detect -phase transitions in 2D Ising and Potts models, lattice gauge -theories, and different phases of polymers, or solving the -Navier-Stokes equation in weather forecasting. Deep learning has also -found interesting applications in quantum physics. Various quantum -phase transitions can be detected and studied using DNNs and CNNs, -topological phases, and even non-equilibrium many-body -localization. Representing quantum states as DNNs quantum state -tomography are among some of the impressive achievements to reveal the -potential of DNNs to facilitate the study of quantum systems. - -

-In quantum information theory, it has been shown that one can perform -gate decompositions with the help of neural. - -

-The applications are not limited to the natural sciences. There is a -plethora of applications in essentially all disciplines, from the -humanities to life science and medicine. -

- - -
-

Neural network types

- -

-An artificial neural network (ANN), is a computational model that -consists of layers of connected neurons, or nodes or units. We will -refer to these interchangeably as units or nodes, and sometimes as -neurons. - -

-It is supposed to mimic a biological nervous system by letting each -neuron interact with other neurons by sending signals in the form of -mathematical functions between layers. A wide variety of different -ANNs have been developed, but most of them consist of an input layer, -an output layer and eventual layers in-between, called hidden -layers. All layers can contain an arbitrary number of nodes, and each -connection between two nodes is associated with a weight variable. - -

-Neural networks (also called neural nets) are neural-inspired -nonlinear models for supervised learning. As we will see, neural nets -can be viewed as natural, more powerful extensions of supervised -learning methods such as linear and logistic regression and soft-max -methods we discussed earlier. -

- - -
-

Feed-forward neural networks

- -

-The feed-forward neural network (FFNN) was the first and simplest type -of ANNs that were devised. In this network, the information moves in -only one direction: forward through the layers. - -

-Nodes are represented by circles, while the arrows display the -connections between the nodes, including the direction of information -flow. Additionally, each arrow corresponds to a weight variable -(figure to come). We observe that each node in a layer is connected -to all nodes in the subsequent layer, making this a so-called -fully-connected FFNN. -

- - -
-

Convolutional Neural Network

- -

-A different variant of FFNNs are convolutional neural networks -(CNNs), which have a connectivity pattern inspired by the animal -visual cortex. Individual neurons in the visual cortex only respond to -stimuli from small sub-regions of the visual field, called a receptive -field. This makes the neurons well-suited to exploit the strong -spatially local correlation present in natural images. The response of -each neuron can be approximated mathematically as a convolution -operation. (figure to come) - -

-Convolutional neural networks emulate the behaviour of neurons in the -visual cortex by enforcing a local connectivity pattern between -nodes of adjacent layers: Each node in a convolutional layer is -connected only to a subset of the nodes in the previous layer, in -contrast to the fully-connected FFNN. Often, CNNs consist of several -convolutional layers that learn local features of the input, with a -fully-connected layer at the end, which gathers all the local data and -produces the outputs. They have wide applications in image and video -recognition. -

- - -
-

Recurrent neural networks

- -

-So far we have only mentioned ANNs where information flows in one -direction: forward. Recurrent neural networks on the other hand, -have connections between nodes that form directed cycles. This -creates a form of internal memory which are able to capture -information on what has been calculated before; the output is -dependent on the previous computations. Recurrent NNs make use of -sequential information by performing the same task for every element -in a sequence, where each element depends on previous elements. An -example of such information is sentences, making recurrent NNs -especially well-suited for handwriting and speech recognition. -

- - -
-

Other types of networks

- -

-There are many other kinds of ANNs that have been developed. One type -that is specifically designed for interpolation in multidimensional -space is the radial basis function (RBF) network. RBFs are typically -made up of three layers: an input layer, a hidden layer with -non-linear radial symmetric activation functions and a linear output -layer (''linear'' here means that each node in the output layer has a -linear activation function). The layers are normally fully-connected -and there are no cycles, thus RBFs can be viewed as a type of -fully-connected FFNN. They are however usually treated as a separate -type of NN due the unusual activation functions. -

- - -
-

Multilayer perceptrons

- -

-One uses often so-called fully-connected feed-forward neural networks -with three or more layers (an input layer, one or more hidden layers -and an output layer) consisting of neurons that have non-linear -activation functions. - -

-Such networks are often called multilayer perceptrons (MLPs). -

- - -
-

Why multilayer perceptrons?

- -

-According to the Universal approximation theorem, a feed-forward -neural network with just a single hidden layer containing a finite -number of neurons can approximate a continuous multidimensional -function to arbitrary accuracy, assuming the activation function for -the hidden layer is a non-constant, bounded and -monotonically-increasing continuous function. - -

-Note that the requirements on the activation function only applies to -the hidden layer, the output nodes are always assumed to be linear, so -as to not restrict the range of output values. -

- - -
-

Mathematical model

- -

-The output \( y \) is produced via the activation function \( f \) -

 
-$$ - y = f\left(\sum_{i=1}^n w_ix_i + b_i\right) = f(z), -$$ -

 
- -This function receives \( x_i \) as inputs. -Here the activation \( z=(\sum_{i=1}^n w_ix_i+b_i) \). -In an FFNN of such neurons, the inputs \( x_i \) are the outputs of -the neurons in the preceding layer. Furthermore, an MLP is -fully-connected, which means that each neuron receives a weighted sum -of the outputs of all neurons in the previous layer. -

- - -
-

Mathematical model

- -

-First, for each node \( i \) in the first hidden layer, we calculate a weighted sum \( z_i^1 \) of the input coordinates \( x_j \), - -

 
-$$ -\begin{equation} z_i^1 = \sum_{j=1}^{M} w_{ij}^1 x_j + b_i^1 -\tag{2} -\end{equation} -$$ -

 
- -

-Here \( b_i \) is the so-called bias which is normally needed in -case of zero activation weights or inputs. How to fix the biases and -the weights will be discussed below. The value of \( z_i^1 \) is the -argument to the activation function \( f_i \) of each node \( i \), The -variable \( M \) stands for all possible inputs to a given node \( i \) in the -first layer. We define the output \( y_i^1 \) of all neurons in layer 1 as - -

 
-$$ -\begin{equation} - y_i^1 = f(z_i^1) = f\left(\sum_{j=1}^M w_{ij}^1 x_j + b_i^1\right) -\tag{3} -\end{equation} -$$ -

 
- -

-where we assume that all nodes in the same layer have identical -activation functions, hence the notation \( f \). In general, we could assume in the more general case that different layers have different activation functions. -In this case we would identify these functions with a superscript \( l \) for the \( l \)-th layer, - -

 
-$$ -\begin{equation} - y_i^l = f^l(u_i^l) = f^l\left(\sum_{j=1}^{N_{l-1}} w_{ij}^l y_j^{l-1} + b_i^l\right) -\tag{4} -\end{equation} -$$ -

 
- -

-where \( N_l \) is the number of nodes in layer \( l \). When the output of -all the nodes in the first hidden layer are computed, the values of -the subsequent layer can be calculated and so forth until the output -is obtained. -

- - -
-

Mathematical model

- -

-The output of neuron \( i \) in layer 2 is thus, - -

 
-$$ -\begin{align} - y_i^2 &= f^2\left(\sum_{j=1}^N w_{ij}^2 y_j^1 + b_i^2\right) -\tag{5}\\ - &= f^2\left[\sum_{j=1}^N w_{ij}^2f^1\left(\sum_{k=1}^M w_{jk}^1 x_k + b_j^1\right) + b_i^2\right] -\tag{6} -\end{align} -$$ -

 
- -where we have substituted \( y_k^1 \) with the inputs \( x_k \). Finally, the ANN output reads - -

 
-$$ -\begin{align} - y_i^3 &= f^3\left(\sum_{j=1}^N w_{ij}^3 y_j^2 + b_i^3\right) -\tag{7}\\ - &= f_3\left[\sum_{j} w_{ij}^3 f^2\left(\sum_{k} w_{jk}^2 f^1\left(\sum_{m} w_{km}^1 x_m + b_k^1\right) + b_j^2\right) - + b_1^3\right] -\tag{8} -\end{align} -$$ -

 
-

- - -
-

Mathematical model

- -

-We can generalize this expression to an MLP with \( l \) hidden -layers. The complete functional form is, - -

 
-$$ -\begin{align} -&y^{l+1}_i = f^{l+1}\left[\!\sum_{j=1}^{N_l} w_{ij}^3 f^l\left(\sum_{k=1}^{N_{l-1}}w_{jk}^{l-1}\left(\dots f^1\left(\sum_{n=1}^{N_0} w_{mn}^1 x_n+ b_m^1\right)\dots\right)+b_k^2\right)+b_1^3\right] && -\tag{9} -\end{align} -$$ -

 
- -

-which illustrates a basic property of MLPs: The only independent -variables are the input values \( x_n \). -

- - -
-

Mathematical model

- -

-This confirms that an MLP, despite its quite convoluted mathematical -form, is nothing more than an analytic function, specifically a -mapping of real-valued vectors \( \hat{x} \in \mathbb{R}^n \rightarrow -\hat{y} \in \mathbb{R}^m \). - -

-Furthermore, the flexibility and universality of an MLP can be -illustrated by realizing that the expression is essentially a nested -sum of scaled activation functions of the form - -

 
-$$ -\begin{equation} - f(x) = c_1 f(c_2 x + c_3) + c_4 -\tag{10} -\end{equation} -$$ -

 
- -

-where the parameters \( c_i \) are weights and biases. By adjusting these -parameters, the activation functions can be shifted up and down or -left and right, change slope or be rescaled which is the key to the -flexibility of a neural network. -

- - -
- -

Matrix-vector notation

- -

-We can introduce a more convenient notation for the activations in an A NN. - -

-Additionally, we can represent the biases and activations -as layer-wise column vectors \( \hat{b}_l \) and \( \hat{y}_l \), so that the \( i \)-th element of each vector -is the bias \( b_i^l \) and activation \( y_i^l \) of node \( i \) in layer \( l \) respectively. - -

-We have that \( \mathrm{W}_l \) is an \( N_{l-1} \times N_l \) matrix, while \( \hat{b}_l \) and \( \hat{y}_l \) are \( N_l \times 1 \) column vectors. -With this notation, the sum becomes a matrix-vector multiplication, and we can write -the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as -

 
-$$ -\begin{equation} - \hat{y}_2 = f_2(\mathrm{W}_2 \hat{y}_{1} + \hat{b}_{2}) = - f_2\left(\left[\begin{array}{ccc} - w^2_{11} &w^2_{12} &w^2_{13} \\ - w^2_{21} &w^2_{22} &w^2_{23} \\ - w^2_{31} &w^2_{32} &w^2_{33} \\ - \end{array} \right] \cdot - \left[\begin{array}{c} - y^1_1 \\ - y^1_2 \\ - y^1_3 \\ - \end{array}\right] + - \left[\begin{array}{c} - b^2_1 \\ - b^2_2 \\ - b^2_3 \\ - \end{array}\right]\right). -\tag{11} -\end{equation} -$$ -

 
-

- - -
- -

Matrix-vector notation and activation

- -

-The activation of node \( i \) in layer 2 is - -

 
-$$ -\begin{equation} - y^2_i = f_2\Bigr(w^2_{i1}y^1_1 + w^2_{i2}y^1_2 + w^2_{i3}y^1_3 + b^2_i\Bigr) = - f_2\left(\sum_{j=1}^3 w^2_{ij} y_j^1 + b^2_i\right). -\tag{12} -\end{equation} -$$ -

 
- -

-This is not just a convenient and compact notation, but also a useful -and intuitive way to think about MLPs: The output is calculated by a -series of matrix-vector multiplications and vector additions that are -used as input to the activation functions. For each operation -\( \mathrm{W}_l \hat{y}_{l-1} \) we move forward one layer. -

- - -
- -

Activation functions

- -

-A property that characterizes a neural network, other than its -connectivity, is the choice of activation function(s). As described -in, the following restrictions are imposed on an activation function -for a FFNN to fulfill the universal approximation theorem - -

    - -

  • Non-constant
  • - -

  • Bounded
  • - -

  • Monotonically-increasing
  • - -

  • Continuous
  • -
-
- - -
- -

Activation functions, Logistic and Hyperbolic ones

- -

-The second requirement excludes all linear functions. Furthermore, in -a MLP with only linear activation functions, each layer simply -performs a linear transformation of its inputs. - -

-Regardless of the number of layers, the output of the NN will be -nothing but a linear function of the inputs. Thus we need to introduce -some kind of non-linearity to the NN to be able to fit non-linear -functions Typical examples are the logistic Sigmoid - -

 
-$$ - f(x) = \frac{1}{1 + e^{-x}}, -$$ -

 
- -and the hyperbolic tangent function -

 
-$$ - f(x) = \tanh(x) -$$ -

 
-

- - -
- -

Relevance

- -

-The sigmoid function are more biologically plausible because the -output of inactive neurons are zero. Such activation function are -called one-sided. However, it has been shown that the hyperbolic -tangent performs better than the sigmoid for training MLPs. has -become the most popular for deep neural networks - -

- - -

"""The sigmoid function (or the logistic curve) is a 
-function that takes any real number, z, and outputs a number (0,1).
-It is useful in neural networks for assigning weights on a relative scale.
-The value z is the weighted sum of parameters involved in the learning algorithm."""
-
-import numpy
-import matplotlib.pyplot as plt
-import math as mt
-
-z = numpy.arange(-5, 5, .1)
-sigma_fn = numpy.vectorize(lambda z: 1/(1+numpy.exp(-z)))
-sigma = sigma_fn(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, sigma)
-ax.set_ylim([-0.1, 1.1])
-ax.set_xlim([-5,5])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('sigmoid function')
-
-plt.show()
-
-"""Step Function"""
-z = numpy.arange(-5, 5, .02)
-step_fn = numpy.vectorize(lambda z: 1.0 if z >= 0.0 else 0.0)
-step = step_fn(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, step)
-ax.set_ylim([-0.5, 1.5])
-ax.set_xlim([-5,5])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('step function')
-
-plt.show()
-
-"""Sine Function"""
-z = numpy.arange(-2*mt.pi, 2*mt.pi, 0.1)
-t = numpy.sin(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, t)
-ax.set_ylim([-1.0, 1.0])
-ax.set_xlim([-2*mt.pi,2*mt.pi])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('sine function')
-
-plt.show()
-
-"""Plots a graph of the squashing function used by a rectified linear
-unit"""
-z = numpy.arange(-2, 2, .1)
-zero = numpy.zeros(len(z))
-y = numpy.max([zero, z], axis=0)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, y)
-ax.set_ylim([-2.0, 2.0])
-ax.set_xlim([-2.0, 2.0])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('Rectified linear unit')
-
-plt.show()
-
-
- - -
-

The multilayer perceptron (MLP)

- -

-The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of - -

    -

  1. A neural network with one or more layers of nodes between the input and the output nodes.
  2. -

  3. The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer.
  4. -

  5. The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer.
  6. -
-

- -As a convention it is normal to call a network with one layer of input units, one layer of hidden -units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc. - -

-For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units. -Hereafter we will call the various entities of a layer for nodes. -There are also no connections within a single layer. - -

-The number of input nodes does not need to equal the number of output -nodes. This applies also to the hidden layers. Each layer may have its -own number of nodes and activation functions. - -

-The hidden layers have their name from the fact that they are not -linked to observables and as we will see below when we define the -so-called activation \( \hat{z} \), we can think of this as a basis -expansion of the original inputs \( \hat{x} \). The difference however -between neural networks and say linear regression is that now these -basis functions (which will correspond to the weights in the network) -are learned from data. This results in an important difference between -neural networks and deep learning approaches on one side and methods -like logistic regression or linear regression and their modifications on the other side. -

- - -
-

From one to many layers, the universal approximation theorem

- -

-A neural network with only one layer, what we called the simple -perceptron, is best suited if we have a standard binary model with -clear (linear) boundaries between the outcomes. As such it could -equally well be replaced by standard linear regression or logistic -regression. Networks with one or more hidden layers approximate -systems with more complex boundaries. - -

-As stated earlier, -an important theorem in studies of neural networks, restated without -proof here, is the universal approximation -theorem. - -

-It states that a feed-forward network with a single hidden layer -containing a finite number of neurons can approximate continuous -functions on compact subsets of real functions. The theorem thus -states that simple neural networks can represent a wide variety of -interesting functions when given appropriate parameters. It is the -multilayer feedforward architecture itself which gives neural networks -the potential of being universal approximators. -

- - -
-

Deriving the back propagation code for a multilayer perceptron model

- -

-Note: figures will be inserted later! - -

-As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications. -The unknowwn quantities are our weights \( w_{ij} \) and we need to find an algorithm for changing them so that our errors are as small as possible. -This leads us to the famous back propagation algorithm. - -

-The questions we want to ask are how do changes in the biases and the -weights in our network change the cost function and how can we use the -final output to modify the weights? - -

-To derive these equations let us start with a plain regression problem -and define our cost function as - -

 
-$$ -{\cal C}(\hat{W}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2, -$$ -

 
- -

-where the $t_i$s are our \( n \) targets (the values we want to -reproduce), while the outputs of the network after having propagated -all inputs \( \hat{x} \) are given by \( y_i \). Below we will demonstrate -how the basic equations arising from the back propagation algorithm -can be modified in order to study classification problems with \( K \) -classes. -

- - -
-

Definitions

- -

-With our definition of the targets \( \hat{t} \), the outputs of the -network \( \hat{y} \) and the inputs \( \hat{x} \) we -define now the activation \( z_j^l \) of node/neuron/unit \( j \) of the -\( l \)-th layer as a function of the bias, the weights which add up from -the previous layer \( l-1 \) and the forward passes/outputs -\( \hat{a}^{l-1} \) from the previous layer as - -

 
-$$ -z_j^l = \sum_{i=1}^{M_{l-1}}w_{ij}^la_i^{l-1}+b_j^l, -$$ -

 
- -

-where \( b_k^l \) are the biases from layer \( l \). Here \( M_{l-1} \) -represents the total number of nodes/neurons/units of layer \( l-1 \). The -figure here illustrates this equation. We can rewrite this in a more -compact form as the matrix-vector products we discussed earlier, - -

 
-$$ -\hat{z}^l = \left(\hat{W}^l\right)^T\hat{a}^{l-1}+\hat{b}^l. -$$ -

 
- -

-With the activation values \( \hat{z}^l \) we can in turn define the -output of layer \( l \) as \( \hat{a}^l = f(\hat{z}^l) \) where \( f \) is our -activation function. In the examples here we will use the sigmoid -function discussed in our logistic regression lectures. We will also use the same activation function \( f \) for all layers -and their nodes. It means we have - -

 
-$$ -a_j^l = f(z_j^l) = \frac{1}{1+\exp{-(z_j^l)}}. -$$ -

 
-

- - -
-

Derivatives and the chain rule

- -

-From the definition of the activation \( z_j^l \) we have -

 
-$$ -\frac{\partial z_j^l}{\partial w_{ij}^l} = a_i^{l-1}, -$$ -

 
- -and -

 
-$$ -\frac{\partial z_j^l}{\partial a_i^{l-1}} = w_{ji}^l. -$$ -

 
- -

-With our definition of the activation function we have that (note that this function depends only on \( z_j^l \)) -

 
-$$ -\frac{\partial a_j^l}{\partial z_j^{l}} = a_j^l(1-a_j^l)=f(z_j^l)(1-f(z_j^l)). -$$ -

 
-

- - -
-

Derivative of the cost function

- -

-With these definitions we can now compute the derivative of the cost function in terms of the weights. - -

-Let us specialize to the output layer \( l=L \). Our cost function is -

 
-$$ -{\cal C}(\hat{W^L}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2=\frac{1}{2}\sum_{i=1}^n\left(a_i^L - t_i\right)^2, -$$ -

 
- -The derivative of this function with respect to the weights is - -

 
-$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)\frac{\partial a_j^L}{\partial w_{jk}^{L}}, -$$ -

 
- -The last partial derivative can easily be computed and reads (by applying the chain rule) -

 
-$$ -\frac{\partial a_j^L}{\partial w_{jk}^{L}} = \frac{\partial a_j^L}{\partial z_{j}^{L}}\frac{\partial z_j^L}{\partial w_{jk}^{L}}=a_j^L(1-a_j^L)a_k^{L-1}, -$$ -

 
-

- - -
-

Bringing it together, first back propagation equation

- -

-We have thus -

 
-$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)a_j^L(1-a_j^L)a_k^{L-1}, -$$ -

 
- -

-Defining -

 
-$$ -\delta_j^L = a_j^L(1-a_j^L)\left(a_j^L - t_j\right) = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -$$ -

 
- -and using the Hadamard product of two vectors we can write this as -

 
-$$ -\hat{\delta}^L = f'(\hat{z}^L)\circ\frac{\partial {\cal C}}{\partial (\hat{a}L)}. -$$ -

 
- -

-This is an important expression. The second term on the right handside -measures how fast the cost function is changing as a function of the $j$th -output activation. If, for example, the cost function doesn't depend -much on a particular output node \( j \), then \( \delta_j^L \) will be small, -which is what we would expect. The first term on the right, measures -how fast the activation function \( f \) is changing at a given activation -value \( z_j^L \). - -

-Notice that everything in the above equations is easily computed. In -particular, we compute \( z_j^L \) while computing the behaviour of the -network, and it is only a small additional overhead to compute -\( f'(z^L_j) \). The exact form of the derivative with respect to the -output depends on the form of the cost function. -However, provided the cost function is known there should be little -trouble in calculating - -

 
-$$ -\frac{\partial {\cal C}}{\partial (a_j^L)} -$$ -

 
- -

-With the definition of \( \delta_j^L \) we have a more compact definition of the derivative of the cost function in terms of the weights, namely -

 
-$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}. -$$ -

 
-

- - -
-

Derivatives in terms of \( z_j^L \)

- -

-It is also easy to see that our previous equation can be written as - -

 
-$$ -\delta_j^L =\frac{\partial {\cal C}}{\partial z_j^L}= \frac{\partial {\cal C}}{\partial a_j^L}\frac{\partial a_j^L}{\partial z_j^L}, -$$ -

 
- -which can also be interpreted as the partial derivative of the cost function with respect to the biases \( b_j^L \), namely -

 
-$$ -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}\frac{\partial b_j^L}{\partial z_j^L}=\frac{\partial {\cal C}}{\partial b_j^L}, -$$ -

 
- -That is, the error \( \delta_j^L \) is exactly equal to the rate of change of the cost function as a function of the bias. -

- - -
-

Bringing it together

- -

-We have now three equations that are essential for the computations of the derivatives of the cost function at the output layer. These equations are needed to start the algorithm and they are - -

-

-The starting equations. -

 
-$$ -\begin{equation} -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}, -\tag{13} -\end{equation} -$$ -

 
- -and -

 
-$$ -\begin{equation} -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\tag{14} -\end{equation} -$$ -

 
- -and - -

 
-$$ -\begin{equation} -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}, -\tag{15} -\end{equation} -$$ -

 
-

- -

-An interesting consequence of the above equations is that when the -activation \( a_k^{L-1} \) is small, the gradient term, that is the -derivative of the cost function with respect to the weights, will also -tend to be small. We say then that the weight learns slowly, meaning -that it changes slowly when we minimize the weights via say gradient -descent. In this case we say the system learns slowly. - -

-Another interesting feature is that is when the activation function, -represented by the sigmoid function here, is rather flat when we move towards -its end values \( 0 \) and \( 1 \) (see the above Python codes). In these -cases, the derivatives of the activation function will also be close -to zero, meaning again that the gradients will be small and the -network learns slowly again. - -

-We need a fourth equation and we are set. We are going to propagate -backwards in order to the determine the weights and biases. In order -to do so we need to represent the error in the layer before the final -one \( L-1 \) in terms of the errors in the final output layer. -

- - -
-

Final back propagating equation

- -

-We have that (replacing \( L \) with a general layer \( l \)) -

 
-$$ -\delta_j^l =\frac{\partial {\cal C}}{\partial z_j^l}. -$$ -

 
- -We want to express this in terms of the equations for layer \( l+1 \). Using the chain rule and summing over all \( k \) entries we have - -

 
-$$ -\delta_j^l =\sum_k \frac{\partial {\cal C}}{\partial z_k^{l+1}}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}=\sum_k \delta_k^{l+1}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}, -$$ -

 
- -and recalling that -

 
-$$ -z_j^{l+1} = \sum_{i=1}^{M_{l}}w_{ij}^{l+1}a_j^{l}+b_j^{l+1}, -$$ -

 
- -with \( M_l \) being the number of nodes in layer \( l \), we obtain -

 
-$$ -\delta_j^l =\sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l), -$$ -

 
- -This is our final equation. - -

-We are now ready to set up the algorithm for back propagation and learning the weights and biases. -

- - -
-

Setting up the Back propagation algorithm

- -

-The four equations provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm. - -

-

- -

-First, we set up the input data \( \hat{x} \) and the activations -\( \hat{z}_1 \) of the input layer and compute the activation function and -the pertinent outputs \( \hat{a}^1 \). -

- -

-

- -

-Secondly, we perform then the feed forward till we reach the output -layer and compute all \( \hat{z}_l \) of the input layer and compute the -activation function and the pertinent outputs \( \hat{a}^l \) for -\( l=2,3,\dots,L \). -

- -

-

- -

-Thereafter we compute the ouput error \( \hat{\delta}^L \) by computing all -

 
-$$ -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}. -$$ -

 
-

- -

-

- -

-Then we compute the back propagate error for each \( l=L-1,L-2,\dots,2 \) as -

 
-$$ -\delta_j^l = \sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l). -$$ -

 
-

- -

-

- -

-Finally, we update the weights and the biases using gradient descent for each \( l=L-1,L-2,\dots,2 \) and update the weights and biases according to the rules -

 
-$$ -w_{jk}^l\leftarrow = w_{jk}^l- \eta \delta_j^la_k^{l-1}, -$$ -

 
- -

 
-$$ -b_j^l \leftarrow b_j^l-\eta \frac{\partial {\cal C}}{\partial b_j^l}=b_j^l-\eta \delta_j^l, -$$ -

 
-

- -

-The parameter \( \eta \) is the learning parameter discussed in connection with the gradient descent methods. -Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training. -

- - -
-

Setting up a Multi-layer perceptron model for classification

- -

-We are now gong to develop an example based on the MNIST data -base. This is a classification problem and we need to use our -cross-entropy function we discussed in connection with logistic -regression. The cross-entropy defines our cost function for the -classificaton problems with neural networks. - -

-In binary classification with two classes \( (0, 1) \) we define the -logistic/sigmoid function as the probability that a particular input -is in class \( 0 \) or \( 1 \). This is possible because the logistic -function takes any input from the real numbers and inputs a number -between 0 and 1, and can therefore be interpreted as a probability. It -also has other nice properties, such as a derivative that is simple to -calculate. - -

-For an input \( \boldsymbol{a} \) from the hidden layer, the probability that the input \( \boldsymbol{x} \) -is in class 0 or 1 is just. We let \( \theta \) represent the unknown weights and biases to be adjusted by our equations). The variable \( x \) -represents our activation values \( z \). We have -

 
-$$ -P(y = 0 \mid \hat{x}, \hat{\theta}) = \frac{1}{1 + \exp{(- \hat{x}})} , -$$ -

 
- -and -

 
-$$ -P(y = 1 \mid \hat{x}, \hat{\theta}) = 1 - P(y = 0 \mid \hat{x}, \hat{\theta}) , -$$ -

 
- -

-where \( y \in \{0, 1\} \) and \( \hat{\theta} \) represents the weights and biases -of our network. -

- - -
-

Defining the cost function

- -

-Our cost function is given as (see the Logistic regression lectures) -

 
-$$ -\mathcal{C}(\hat{\theta}) = - \ln P(\mathcal{D} \mid \hat{\theta}) = - \sum_{i=1}^n -y_i \ln[P(y_i = 0)] + (1 - y_i) \ln [1 - P(y_i = 0)] = \sum_{i=1}^n \mathcal{L}_i(\hat{\theta}) . -$$ -

 
- -

-This last equality means that we can interpret our cost function as a sum over the loss function -for each point in the dataset \( \mathcal{L}_i(\hat{\theta}) \). -The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather -than maximizing a negative number. - -

-In multiclass classification it is common to treat each integer label as a so called one-hot vector: - -

-\( y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) , \) and - -

-\( y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) , \) - -

-i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset (numbers from \( 0 \) to \( 9 \)).. - -

-If \( \hat{x}_i \) is the \( i \)-th input (image), \( y_{ic} \) refers to the \( c \)-th component of the \( i \)-th -output vector \( \hat{y}_i \). -The probability of \( \hat{x}_i \) being in class \( c \) will be given by the softmax function: - -

 
-$$ -P(y_{ic} = 1 \mid \hat{x}_i, \hat{\theta}) = \frac{\exp{((\hat{a}_i^{hidden})^T \hat{w}_c)}} -{\sum_{c'=0}^{C-1} \exp{((\hat{a}_i^{hidden})^T \hat{w}_{c'})}} , -$$ -

 
- -

-which reduces to the logistic function in the binary case. -The likelihood of this \( C \)-class classifier -is now given as: - -

 
-$$ -P(\mathcal{D} \mid \hat{\theta}) = \prod_{i=1}^n \prod_{c=0}^{C-1} [P(y_{ic} = 1)]^{y_{ic}} . -$$ -

 
- -Again we take the negative log-likelihood to define our cost function: - -

 
-$$ -\mathcal{C}(\hat{\theta}) = - \log{P(\mathcal{D} \mid \hat{\theta})}. -$$ -

 
- -See the logistic regression lectures for a full definition of the cost function. - -

-The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before! -

- - -
-

Example: binary classification problem

- -

-As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters \( \beta \) as -

 
-$$ -\mathcal{C}(\hat{\beta}) = - \sum_{i=1}^n \left(y_i\log{p(y_i \vert x_i,\hat{\beta})}+(i-y_i)\log{1-p(y_i \vert x_i,\hat{\beta})}\right), -$$ -

 
- -where we had defined the logistic (sigmoid) function -

 
-$$ -p(y_i =1\vert x_i,\hat{\beta})=\frac{\exp{(\beta_0+\beta_1 x_i)}}{1+\exp{(\beta_0+\beta_1 x_i)}}, -$$ -

 
- -and -

 
-$$ -p(y_i =0\vert x_i,\hat{\beta})=1-p(y_i =1\vert x_i,\hat{\beta}). -$$ -

 
- -The parameters \( \hat{\beta} \) were defined using a minimization method like gradient descent or Newton-Raphson's method. - -

-Now we replace \( x_i \) with the activation \( z_i^l \) for a given layer \( l \) and the outputs as \( y_i=a_i^l=f(z_i^l) \), with \( z_i^l \) now being a function of the weights \( w_{ij}^l \) and biases \( b_i^l \). -We have then -

 
-$$ -a_i^l = y_i = \frac{\exp{(z_i^l)}}{1+\exp{(z_i^l)}}, -$$ -

 
- -with -

 
-$$ -z_i^l = \sum_{j}w_{ij}^l a_j^{l-1}+b_i^l, -$$ -

 
- -where the superscript \( l-1 \) indicates that these are the outputs from layer \( l-1 \). -Our cost function at the final layer \( l=L \) is now -

 
-$$ -\mathcal{C}(\hat{W}) = - \sum_{i=1}^n \left(t_i\log{a_i^L}+(i-t_i)\log{(1-a_i^L)}\right), -$$ -

 
- -where we have defined the targets \( t_i \). The derivatives of the cost function with respect to the output \( a_i^L \) are then easily calculated and we get -

 
-$$ -\frac{\partial \mathcal{C}(\hat{W})}{\partial a_i^L} = \frac{a_i^L-t_i}{a_i^L(1-a_i^L)}. -$$ -

 
- -In case we use another activation function than the logistic one, we need to evaluate other derivatives. -

- - -
-

The Softmax function

-In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation \( z_i^l \), that is we need -

 
-$$ -\frac{\partial f(z_i^l)}{\partial w_{jk}^l} = -\frac{\partial f(z_i^l)}{\partial z_j^l} \frac{\partial z_j^l}{\partial w_{jk}^l}= \frac{\partial f(z_i^l)}{\partial z_j^l}a_k^{l-1}. -$$ -

 
- -For the Softmax function we have -

 
-$$ -f(z_i^l) = \frac{\exp{(z_i^l)}}{\sum_{m=1}^K\exp{(z_m^l)}}. -$$ -

 
- -Its derivative with respect to \( z_j^l \) gives -

 
-$$ -\frac{\partial f(z_i^l)}{\partial z_j^l}= f(z_i^l)\left(\delta_{ij}-f(z_j^l)\right), -$$ -

 
- -which in case of the simply binary model reduces to having \( i=j \). -

- - -
-

Developing a code for doing neural networks with back propagation

- -

-One can identify a set of key steps when using neural networks to solve supervised learning problems: - -

    -

  1. Collect and pre-process data
  2. - -

  3. Define model and architecture
  4. - -

  5. Choose cost function and optimizer
  6. - -

  7. Train the model
  8. - -

  9. Evaluate model performance on test data
  10. - -

  11. Adjust hyperparameters (if necessary, network architecture)
  12. -
-
- - -
-

Collect and pre-process data

- -

-Here we will be using the MNIST dataset, which is readily available through the scikit-learn -package. You may also find it for example here. -The MNIST (Modified National Institute of Standards and Technology) database is a large database -of handwritten digits that is commonly used for training various image processing systems. -The MNIST dataset consists of 70 000 images of size \( 28\times 28 \) pixels, each labeled from 0 to 9. -The scikit-learn dataset we will use consists of a selection of 1797 images of size \( 8\times 8 \) collected and processed from this database. - -

-To feed data into a feed-forward neural network we need to represent -the inputs as a feature matrix \( X = (n_{inputs}, n_{features}) \). Each -row represents an input, in this case a handwritten digit, and -each column represents a feature, in this case a pixel. The -correct answers, also known as labels or targets are -represented as a 1D array of integers -\( Y = (n_{inputs}) = (5, 3, 1, 8,...) \). - -

-As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from -measurements of height (in m) -and weight (in kg). If we have measurements of 5 people the feature matrix could be for example: - -

 
-$$ X = \begin{bmatrix} -1.85 & 81\\ -1.71 & 65\\ -1.95 & 103\\ -1.55 & 42\\ -1.63 & 56 -\end{bmatrix} ,$$ -

 
- -

-and the targets would be: - -

 
-$$ Y = (23.7, 22.2, 27.1, 17.5, 21.1) $$ -

 
- -

-Since each input image is a 2D matrix, we need to flatten the image -(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a -feature matrix. This means we lose all spatial information in the -image, such as locality and translational invariance. More complicated -architectures such as Convolutional Neural Networks can take advantage -of such information, and are most commonly applied when analyzing -images. - -

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# flatten the image
-# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
-n_inputs = len(inputs)
-inputs = inputs.reshape(n_inputs, -1)
-print("X = (n_inputs, n_features) = " + str(inputs.shape))
-
-
-# choose some random images to display
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-
- - -
-

Train and test datasets

- -

-Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions. - -

-We will reserve \( 80 \% \) of our dataset for training and \( 20 \% \) for testing. - -

-It is important that the train and test datasets are drawn randomly from our dataset, to ensure -no bias in the sampling. -Say you are taking measurements of weather data to predict the weather in the coming 5 days. -You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data -collected from 12.00 to 24.00. - -

- - -

from sklearn.model_selection import train_test_split
-
-# one-liner from scikit-learn library
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-# equivalently in numpy
-def train_test_split_numpy(inputs, labels, train_size, test_size):
-    n_inputs = len(inputs)
-    inputs_shuffled = inputs.copy()
-    labels_shuffled = labels.copy()
-    
-    np.random.shuffle(inputs_shuffled)
-    np.random.shuffle(labels_shuffled)
-    
-    train_end = int(n_inputs*train_size)
-    X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:]
-    Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:]
-    
-    return X_train, X_test, Y_train, Y_test
-
-#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size)
-
-print("Number of training images: " + str(len(X_train)))
-print("Number of test images: " + str(len(X_test)))
-
-
- - -
-

Define model and architecture

- -

-Our simple feed-forward neural network will consist of an input layer, a single hidden layer and an output layer. The activation \( y \) of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have - -

 
-$$ z = \sum_{i=1}^n w_i a_i ,$$ -

 
- -

 
-$$ y = f(z) ,$$ -

 
- -

-where \( f \) is the activation function, \( a_i \) represents input from neuron \( i \) in the preceding layer -and \( w_i \) is the weight to input \( i \). -The activation of the neurons in the input layer is just the features (e.g. a pixel value). - -

-The simplest activation function for a neuron is the Heaviside function: - -

 
-$$ f(z) = -\begin{cases} -1, & z > 0\\ -0, & \text{otherwise} -\end{cases} -$$ -

 
- -

-A feed-forward neural network with this activation is known as a perceptron. -For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer. -This activation can be generalized to \( k \) classes (using e.g. the one-against-all strategy), -and we call these architectures multiclass perceptrons. - -

-However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and -Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function. - -

-Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU). -We will be using the sigmoid function \( \sigma(x) \): - -

 
-$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$ -

 
- -

-which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions. -

- - -
-

Layers

- -
    -

  • Input
  • -
-

- -Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons. - -

    -

  • Hidden layer
  • -
-

- -We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer. -Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer. - -

    -

  • Output
  • -
-

- -If we were building a binary classifier, it would be sufficient with a single neuron in the output layer, -which could output 0 or 1 according to the Heaviside function. This would be an example of a hard classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a soft classifier, which outputs the probability of being in class 0 or 1. - -

-For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class. - -

-Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons \( j = 0,1,...,9 \). The activation of each output neuron \( j \) will be according to the softmax function: - -

 
-$$ P(\text{class \( j \)} \mid \text{input \( \hat{a} \)}) = \frac{\exp{(\hat{a}^T \hat{w}_j)}} -{\sum_{c=0}^{9} \exp{(\hat{a}^T \hat{w}_c)}} ,$$ -

 
- -

-i.e. each neuron \( j \) outputs the probability of being in class \( j \) given an input from the hidden layer \( \hat{a} \), with \( \hat{w}_j \) the weights of neuron \( j \) to the inputs. -The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1. -The exponent is just the weighted sum of inputs as before: - -

 
-$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$ -

 
- -

-Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500 -weights to the output layer. -

- - -
-

Weights and biases

- -

-Typically weights are initialized with small values distributed around zero, drawn from a uniform -or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless. - -

-Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range -of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron \( j \), \( b_j \): - -

 
-$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$ -

 
- -

-The bias weights \( \hat{b} \) are often initialized to zero, but a small value like \( 0.01 \) ensures all neurons have some output which can be backpropagated in the first training cycle. -

- - -

# building our neural network
-
-n_inputs, n_features = X_train.shape
-n_hidden_neurons = 50
-n_categories = 10
-
-# we make the weights normally distributed using numpy.random.randn
-
-# weights and bias in the hidden layer
-hidden_weights = np.random.randn(n_features, n_hidden_neurons)
-hidden_bias = np.zeros(n_hidden_neurons) + 0.01
-
-# weights and bias in the output layer
-output_weights = np.random.randn(n_hidden_neurons, n_categories)
-output_bias = np.zeros(n_categories) + 0.01
-
-
- - -
-

Feed-forward pass

- -

-Denote \( F \) the number of features, \( H \) the number of hidden neurons and \( C \) the number of categories. -For each input image we calculate a weighted sum of input features (pixel values) to each neuron \( j \) in the hidden layer \( l \): - -

 
-$$ z_{j}^{l} = \sum_{i=1}^{F} w_{ij}^{l} x_i + b_{j}^{l},$$ -

 
- -

-this is then passed through our activation function - -

 
-$$ a_{j}^{l} = f(z_{j}^{l}) .$$ -

 
- -

-We calculate a weighted sum of inputs (activations in the hidden layer) to each neuron \( j \) in the output layer: - -

 
-$$ z_{j}^{L} = \sum_{i=1}^{H} w_{ij}^{L} a_{i}^{l} + b_{j}^{L}.$$ -

 
- -

-Finally we calculate the output of neuron \( j \) in the output layer using the softmax function: - -

 
-$$ a_{j}^{L} = \frac{\exp{(z_j^{L})}} -{\sum_{c=0}^{C-1} \exp{(z_c^{L})}} .$$ -

 
-

- - -
-

Matrix multiplications

- -

-Since our data has the dimensions \( X = (n_{inputs}, n_{features}) \) and our weights to the hidden -layer have the dimensions -\( W_{hidden} = (n_{features}, n_{hidden}) \), -we can easily feed the network all our training data in one go by taking the matrix product - -

 
-$$ X W^{h} = (n_{inputs}, n_{hidden}),$$ -

 
- -

-and obtain a matrix that holds the weighted sum of inputs to the hidden layer -for each input image and each hidden neuron. -We also add the bias to obtain a matrix of weighted sums to the hidden layer \( Z^{h} \): - -

 
-$$ \hat{z}^{l} = \hat{X} \hat{W}^{l} + \hat{b}^{l} ,$$ -

 
- -

-meaning the same bias (1D array with size equal number of hidden neurons) is added to each input image. -This is then passed through the activation: - -

 
-$$ \hat{a}^{l} = f(\hat{z}^l) .$$ -

 
- -

-This is fed to the output layer: - -

 
-$$ \hat{z}^{L} = \hat{a}^{L} \hat{W}^{L} + \hat{b}^{L} .$$ -

 
- -

-Finally we receive our output values for each image and each category by passing it through the softmax function: - -

 
-$$ output = softmax (\hat{z}^{L}) = (n_{inputs}, n_{categories}) .$$ -

 
- -

- - -

# setup the feed-forward pass, subscript h = hidden layer
-
-def sigmoid(x):
-    return 1/(1 + np.exp(-x))
-
-def feed_forward(X):
-    # weighted sum of inputs to the hidden layer
-    z_h = np.matmul(X, hidden_weights) + hidden_bias
-    # activation in the hidden layer
-    a_h = sigmoid(z_h)
-    
-    # weighted sum of inputs to the output layer
-    z_o = np.matmul(a_h, output_weights) + output_bias
-    # softmax output
-    # axis 0 holds each input and axis 1 the probabilities of each category
-    exp_term = np.exp(z_o)
-    probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-    
-    return probabilities
-
-probabilities = feed_forward(X_train)
-print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape))
-print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0]))
-print("probabilities sum up to: " + str(probabilities[0].sum()))
-print()
-
-# we obtain a prediction by taking the class with the highest likelihood
-def predict(X):
-    probabilities = feed_forward(X)
-    return np.argmax(probabilities, axis=1)
-
-predictions = predict(X_train)
-print("predictions = (n_inputs) = " + str(predictions.shape))
-print("prediction for image 0: " + str(predictions[0]))
-print("correct label for image 0: " + str(Y_train[0]))
-
-
- - -
-

Choose cost function and optimizer

- -

-To measure how well our neural network is doing we need to introduce a cost function. -We will call the function that gives the error of a single sample output the loss function, and the function -that gives the total error of our network across all samples the cost function. -A typical choice for multiclass classification is the cross-entropy loss, also known as the negative log likelihood. - -

-In multiclass classification it is common to treat each integer label as a so called one-hot vector: - -

 
-$$ y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$$ -

 
- -

 
-$$ y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$$ -

 
- -

-i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset. - -

-Let \( y_{ic} \) denote the \( c \)-th component of the \( i \)-th one-hot vector. -We define the cost function \( \mathcal{C} \) as a sum over the cross-entropy loss for each point \( \hat{x}_i \) in the dataset. - -

-In the one-hot representation only one of the terms in the loss function is non-zero, namely the -probability of the correct category \( c' \) -(i.e. the category \( c' \) such that \( y_{ic'} = 1 \)). This means that the cross entropy loss only punishes you for how wrong -you got the correct label. The probability of category \( c \) is given by the softmax function. The vector \( \hat{\theta} \) represents the parameters of our network, i.e. all the weights and biases. -

- - -
-

Optimizing the cost function

- -

-The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is gradient descent and its generalizations. The idea behind gradient descent -is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a local minimum of the cost function. -Each parameter \( \theta \) is iteratively adjusted according to the rule - -

 
-$$ \theta_{i+1} = \theta_i - \eta \nabla \mathcal{C}(\theta_i) ,$$ -

 
- -

-where \( \eta \) is known as the learning rate, which controls how big a step we take towards the minimum. -This update can be repeated for any number of iterations, or until we are satisfied with the result. - -

-A simple and effective improvement is a variant called Batch Gradient Descent. -Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient -on a subset of the data called a minibatch. -If there are \( N \) data points and we have a minibatch size of \( M \), the total number of batches -is \( N/M \). -We denote each minibatch \( B_k \), with \( k = 1, 2,...,N/M \). The gradient then becomes: - -

 
-$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{M} \sum_{i \in B_k} \nabla \mathcal{L}_i(\theta) ,$$ -

 
- -

-i.e. instead of averaging the loss over the entire dataset, we average over a minibatch. - -

-This has two important benefits: - -

    -

  1. Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima.
  2. - -

  3. It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient.
  4. -
-

- -The various optmization methods, with codes and algorithms, are discussed in our lectures on Gradient descent approaches. -

- - -
-

Regularization

- -

-It is common to add an extra term to the cost function, proportional -to the size of the weights. This is equivalent to constraining the -size of the weights, so that they do not grow out of control. -Constraining the size of the weights means that the weights cannot -grow arbitrarily large to fit the training data, and in this way -reduces overfitting. - -

-We will measure the size of the weights using the so called L2-norm, meaning our cost function becomes: - -

 
-$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) + \lambda \lvert \lvert \hat{w} \rvert \rvert_2^2 -= \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}(\theta) + \lambda \sum_{ij} w_{ij}^2,$$ -

 
- -

-i.e. we sum up all the weights squared. The factor \( \lambda \) is known as a regularization parameter. - -

-In order to train the model, we need to calculate the derivative of -the cost function with respect to every bias and weight in the -network. In total our network has \( (64 + 1)\times 50=3250 \) weights in -the hidden layer and \( (50 + 1)\times 10=510 \) weights to the output -layer (\( +1 \) for the bias), and the gradient must be calculated for -every parameter. We use the backpropagation algorithm discussed -above. This is a clever use of the chain rule that allows us to -calculate the gradient efficently. -

- - -
-

Matrix multiplication

- -

-To more efficently train our network these equations are implemented using matrix operations. -The error in the output layer is calculated simply as, with \( \hat{t} \) being our targets, - -

 
-$$ \delta_L = \hat{t} - \hat{y} = (n_{inputs}, n_{categories}) .$$ -

 
- -

-The gradient for the output weights is calculated as - -

 
-$$ \nabla W_{L} = \hat{a}^T \delta_L = (n_{hidden}, n_{categories}) ,$$ -

 
- -

-where \( \hat{a} = (n_{inputs}, n_{hidden}) \). This simply means that we are summing up the gradients for each input. -Since we are going backwards we have to transpose the activation matrix. - -

-The gradient with respect to the output bias is then - -

 
-$$ \nabla \hat{b}_{L} = \sum_{i=1}^{n_{inputs}} \delta_L = (n_{categories}) .$$ -

 
- -

-The error in the hidden layer is - -

 
-$$ \Delta_h = \delta_L W_{L}^T \circ f'(z_{h}) = \delta_L W_{L}^T \circ a_{h} \circ (1 - a_{h}) = (n_{inputs}, n_{hidden}) ,$$ -

 
- -

-where \( f'(a_{h}) \) is the derivative of the activation in the hidden layer. The matrix products mean -that we are summing up the products for each neuron in the output layer. The symbol \( \circ \) denotes -the Hadamard product, meaning element-wise multiplication. - -

-This again gives us the gradients in the hidden layer: - -

 
-$$ \nabla W_{h} = X^T \delta_h = (n_{features}, n_{hidden}) ,$$ -

 
- -

 
-$$ \nabla b_{h} = \sum_{i=1}^{n_{inputs}} \delta_h = (n_{hidden}) .$$ -

 
- -

- - -

# to categorical turns our integer vector into a onehot representation
-from sklearn.metrics import accuracy_score
-
-# one-hot in numpy
-def to_categorical_numpy(integer_vector):
-    n_inputs = len(integer_vector)
-    n_categories = np.max(integer_vector) + 1
-    onehot_vector = np.zeros((n_inputs, n_categories))
-    onehot_vector[range(n_inputs), integer_vector] = 1
-    
-    return onehot_vector
-
-#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test)
-Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test)
-
-def feed_forward_train(X):
-    # weighted sum of inputs to the hidden layer
-    z_h = np.matmul(X, hidden_weights) + hidden_bias
-    # activation in the hidden layer
-    a_h = sigmoid(z_h)
-    
-    # weighted sum of inputs to the output layer
-    z_o = np.matmul(a_h, output_weights) + output_bias
-    # softmax output
-    # axis 0 holds each input and axis 1 the probabilities of each category
-    exp_term = np.exp(z_o)
-    probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-    
-    # for backpropagation need activations in hidden and output layers
-    return a_h, probabilities
-
-def backpropagation(X, Y):
-    a_h, probabilities = feed_forward_train(X)
-    
-    # error in the output layer
-    error_output = probabilities - Y
-    # error in the hidden layer
-    error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h)
-    
-    # gradients for the output layer
-    output_weights_gradient = np.matmul(a_h.T, error_output)
-    output_bias_gradient = np.sum(error_output, axis=0)
-    
-    # gradient for the hidden layer
-    hidden_weights_gradient = np.matmul(X.T, error_hidden)
-    hidden_bias_gradient = np.sum(error_hidden, axis=0)
-
-    return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient
-
-print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
-
-eta = 0.01
-lmbd = 0.01
-for i in range(1000):
-    # calculate gradients
-    dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot)
-    
-    # regularization term gradients
-    dWo += lmbd * output_weights
-    dWh += lmbd * hidden_weights
-    
-    # update weights and biases
-    output_weights -= eta * dWo
-    output_bias -= eta * dBo
-    hidden_weights -= eta * dWh
-    hidden_bias -= eta * dBh
-
-print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
-
-
- - -
-

Improving performance

- -

-As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image. -In order to obtain a network that does something useful, we will have to do a bit more work. - -

-The choice of hyperparameters such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a grid-search is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates \( \eta = 10^{-6}, 10^{-5},...,10^{-1} \) with different regularization parameters \( \lambda = 10^{-6},...,10^{-0} \). - -

-Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an iteration, and a full training period -going through the entire dataset (\( n/M \) batches) an epoch. - -

-If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers. -Andrew Ng goes through some of these considerations in this video. You can find a summary of the video here. -

- - -
-

Full object-oriented implementation

- -

-It is very natural to think of the network as an object, with specific instances of the network -being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below. - -

- - -

class NeuralNetwork:
-    def __init__(
-        self,
-        X_data,
-        Y_data,
-        n_hidden_neurons=50,
-        n_categories=10,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-
-    ):
-        self.X_data_full = X_data
-        self.Y_data_full = Y_data
-
-        self.n_inputs = X_data.shape[0]
-        self.n_features = X_data.shape[1]
-        self.n_hidden_neurons = n_hidden_neurons
-        self.n_categories = n_categories
-
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-
-        self.create_biases_and_weights()
-
-    def create_biases_and_weights(self):
-        self.hidden_weights = np.random.randn(self.n_features, self.n_hidden_neurons)
-        self.hidden_bias = np.zeros(self.n_hidden_neurons) + 0.01
-
-        self.output_weights = np.random.randn(self.n_hidden_neurons, self.n_categories)
-        self.output_bias = np.zeros(self.n_categories) + 0.01
-
-    def feed_forward(self):
-        # feed-forward for training
-        self.z_h = np.matmul(self.X_data, self.hidden_weights) + self.hidden_bias
-        self.a_h = sigmoid(self.z_h)
-
-        self.z_o = np.matmul(self.a_h, self.output_weights) + self.output_bias
-
-        exp_term = np.exp(self.z_o)
-        self.probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-
-    def feed_forward_out(self, X):
-        # feed-forward for output
-        z_h = np.matmul(X, self.hidden_weights) + self.hidden_bias
-        a_h = sigmoid(z_h)
-
-        z_o = np.matmul(a_h, self.output_weights) + self.output_bias
-        
-        exp_term = np.exp(z_o)
-        probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-        return probabilities
-
-    def backpropagation(self):
-        error_output = self.probabilities - self.Y_data
-        error_hidden = np.matmul(error_output, self.output_weights.T) * self.a_h * (1 - self.a_h)
-
-        self.output_weights_gradient = np.matmul(self.a_h.T, error_output)
-        self.output_bias_gradient = np.sum(error_output, axis=0)
-
-        self.hidden_weights_gradient = np.matmul(self.X_data.T, error_hidden)
-        self.hidden_bias_gradient = np.sum(error_hidden, axis=0)
-
-        if self.lmbd > 0.0:
-            self.output_weights_gradient += self.lmbd * self.output_weights
-            self.hidden_weights_gradient += self.lmbd * self.hidden_weights
-
-        self.output_weights -= self.eta * self.output_weights_gradient
-        self.output_bias -= self.eta * self.output_bias_gradient
-        self.hidden_weights -= self.eta * self.hidden_weights_gradient
-        self.hidden_bias -= self.eta * self.hidden_bias_gradient
-
-    def predict(self, X):
-        probabilities = self.feed_forward_out(X)
-        return np.argmax(probabilities, axis=1)
-
-    def predict_probabilities(self, X):
-        probabilities = self.feed_forward_out(X)
-        return probabilities
-
-    def train(self):
-        data_indices = np.arange(self.n_inputs)
-
-        for i in range(self.epochs):
-            for j in range(self.iterations):
-                # pick datapoints with replacement
-                chosen_datapoints = np.random.choice(
-                    data_indices, size=self.batch_size, replace=False
-                )
-
-                # minibatch training data
-                self.X_data = self.X_data_full[chosen_datapoints]
-                self.Y_data = self.Y_data_full[chosen_datapoints]
-
-                self.feed_forward()
-                self.backpropagation()
-
-
- - -
-

Evaluate model performance on test data

- -

-To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data. -We measure the performance of the network using the accuracy score. -The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of \( 1 \). - -

 
-$$ \text{Accuracy} = \frac{\sum_{i=1}^n I(\hat{y}_i = y_i)}{n} ,$$ -

 
- -

-where \( I \) is the indicator function, \( 1 \) if \( \hat{y}_i = y_i \) and \( 0 \) otherwise. - -

- - -

epochs = 100
-batch_size = 100
-
-dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
-                    n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
-dnn.train()
-test_predict = dnn.predict(X_test)
-
-# accuracy score from scikit library
-print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
-
-# equivalent in numpy
-def accuracy_score_numpy(Y_test, Y_pred):
-    return np.sum(Y_test == Y_pred) / len(Y_test)
-
-#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict))
-
-
- - -
-

Adjust hyperparameters

- -

-We now perform a grid search to find the optimal hyperparameters for the network. -Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around \( 98\% \) (\( 2\% \) error rate). - -

- - -

eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-# store the models for later use
-DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-
-# grid search
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
-                            n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
-        dnn.train()
-        
-        DNN_numpy[i][j] = dnn
-        
-        test_predict = dnn.predict(X_test)
-        
-        print("Learning rate  = ", eta)
-        print("Lambda = ", lmbd)
-        print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
-        print()
-
-
- - -
-

Visualization

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, you can also do this with matplotlib imshow
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        dnn = DNN_numpy[i][j]
-        
-        train_pred = dnn.predict(X_train) 
-        test_pred = dnn.predict(X_test)
-
-        train_accuracy[i][j] = accuracy_score(Y_train, train_pred)
-        test_accuracy[i][j] = accuracy_score(Y_test, test_pred)
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-
- - -
-

scikit-learn implementation

- -

-scikit-learn focuses more -on traditional machine learning methods, such as regression, -clustering, decision trees, etc. As such, it has only two types of -neural networks: Multi Layer Perceptron outputting continuous values, -MPLRegressor, and Multi Layer Perceptron outputting labels, -MLPClassifier. We will see how simple it is to use these classes. - -

-scikit-learn implements a few improvements from our neural network, -such as early stopping, a varying learning rate, different -optimization methods, etc. We would therefore expect a better -performance overall. - -

- - -

from sklearn.neural_network import MLPClassifier
-# store models for later use
-DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',
-                            alpha=lmbd, learning_rate_init=eta, max_iter=epochs)
-        dnn.fit(X_train, Y_train)
-        
-        DNN_scikit[i][j] = dnn
-        
-        print("Learning rate  = ", eta)
-        print("Lambda = ", lmbd)
-        print("Accuracy score on test set: ", dnn.score(X_test, Y_test))
-        print()
-
-
- - -
-

Visualization

-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        dnn = DNN_scikit[i][j]
-        
-        train_pred = dnn.predict(X_train) 
-        test_pred = dnn.predict(X_test)
-
-        train_accuracy[i][j] = accuracy_score(Y_train, train_pred)
-        test_accuracy[i][j] = accuracy_score(Y_test, test_pred)
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-
- - -
-

Building neural networks in Tensorflow and Keras

- -

-Now we want to build on the experience gained from our neural network implementation in NumPy and scikit-learn -and use it to construct a neural network in Tensorflow. Once we have constructed a neural network in NumPy -and Tensorflow, building one in Keras is really quite trivial, though the performance may suffer. - -

-In our previous example we used only one hidden layer, and in this we will use two. From this it should be quite -clear how to build one using an arbitrary number of hidden layers, using data structures such as Python lists or -NumPy arrays. -

- - -
-

Tensorflow

- -

-Tensorflow is an open source library machine learning library -developed by the Google Brain team for internal use. It was released -under the Apache 2.0 open source license in November 9, 2015. - -

-Tensorflow is a computational framework that allows you to construct -machine learning models at different levels of abstraction, from -high-level, object-oriented APIs like Keras, down to the C++ kernels -that Tensorflow is built upon. The higher levels of abstraction are -simpler to use, but less flexible, and our choice of implementation -should reflect the problems we are trying to solve. - -

-Tensorflow uses so-called graphs to represent your computation -in terms of the dependencies between individual operations, such that you first build a Tensorflow graph -to represent your model, and then create a Tensorflow session to run the graph. - -

-In this guide we will analyze the same data as we did in our NumPy and -scikit-learn tutorial, gathered from the MNIST database of images. We -will give an introduction to the lower level Python Application -Program Interfaces (APIs), and see how we use them to build our graph. -Then we will build (effectively) the same graph in Keras, to see just -how simple solving a machine learning problem can be. - -

-To install tensorflow on Unix/Linux systems, use pip as -

- - -

pip3 install tensorflow
-
-

-and/or if you use anaconda, just write (or install from the graphical user interface) -

- - -

conda install tensorflow
-
-
- - -
-

Collect and pre-process data

- -

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# flatten the image
-# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
-n_inputs = len(inputs)
-inputs = inputs.reshape(n_inputs, -1)
-print("X = (n_inputs, n_features) = " + str(inputs.shape))
-
-
-# choose some random images to display
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-

- - -

from keras.utils import to_categorical
-from sklearn.model_selection import train_test_split
-
-# one-hot representation of labels
-labels = to_categorical(labels)
-
-# split into train and test data
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-
- - -
-

Using TensorFlow backend

- -
    -

  1. Define model and architecture
  2. -

  3. Choose cost function and optimizer
  4. -
-

- - -

import tensorflow as tf
-
-class NeuralNetworkTensorflow:
-    def __init__(
-        self,
-        X_train,
-        Y_train,
-        X_test,
-        Y_test,
-        n_neurons_layer1=100,
-        n_neurons_layer2=50,
-        n_categories=2,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-    ):
-        
-        # keep track of number of steps
-        self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')
-        
-        self.X_train = X_train
-        self.Y_train = Y_train
-        self.X_test = X_test
-        self.Y_test = Y_test
-        
-        self.n_inputs = X_train.shape[0]
-        self.n_features = X_train.shape[1]
-        self.n_neurons_layer1 = n_neurons_layer1
-        self.n_neurons_layer2 = n_neurons_layer2
-        self.n_categories = n_categories
-        
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-        
-        # build network piece by piece
-        # name scopes (with) are used to enforce creation of new variables
-        # https://www.tensorflow.org/guide/variables
-        self.create_placeholders()
-        self.create_DNN()
-        self.create_loss()
-        self.create_optimiser()
-        self.create_accuracy()
-    
-    def create_placeholders(self):
-        # placeholders are fine here, but "Datasets" are the preferred method
-        # of streaming data into a model
-        with tf.name_scope('data'):
-            self.X = tf.placeholder(tf.float32, shape=(None, self.n_features), name='X_data')
-            self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')
-    
-    def create_DNN(self):
-        with tf.name_scope('DNN'):
-            # the weights are stored to calculate regularization loss later
-            
-            # Fully connected layer 1
-            self.W_fc1 = self.weight_variable([self.n_features, self.n_neurons_layer1], name='fc1', dtype=tf.float32)
-            b_fc1 = self.bias_variable([self.n_neurons_layer1], name='fc1', dtype=tf.float32)
-            a_fc1 = tf.nn.sigmoid(tf.matmul(self.X, self.W_fc1) + b_fc1)
-            
-            # Fully connected layer 2
-            self.W_fc2 = self.weight_variable([self.n_neurons_layer1, self.n_neurons_layer2], name='fc2', dtype=tf.float32)
-            b_fc2 = self.bias_variable([self.n_neurons_layer2], name='fc2', dtype=tf.float32)
-            a_fc2 = tf.nn.sigmoid(tf.matmul(a_fc1, self.W_fc2) + b_fc2)
-            
-            # Output layer
-            self.W_out = self.weight_variable([self.n_neurons_layer2, self.n_categories], name='out', dtype=tf.float32)
-            b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)
-            self.z_out = tf.matmul(a_fc2, self.W_out) + b_out
-    
-    def create_loss(self):
-        with tf.name_scope('loss'):
-            softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))
-            
-            regularizer_loss_fc1 = tf.nn.l2_loss(self.W_fc1)
-            regularizer_loss_fc2 = tf.nn.l2_loss(self.W_fc2)
-            regularizer_loss_out = tf.nn.l2_loss(self.W_out)
-            regularizer_loss = self.lmbd*(regularizer_loss_fc1 + regularizer_loss_fc2 + regularizer_loss_out)
-            
-            self.loss = softmax_loss + regularizer_loss
-
-    def create_accuracy(self):
-        with tf.name_scope('accuracy'):
-            probabilities = tf.nn.softmax(self.z_out)
-            predictions = tf.argmax(probabilities, axis=1)
-            labels = tf.argmax(self.Y, axis=1)
-            
-            correct_predictions = tf.equal(predictions, labels)
-            correct_predictions = tf.cast(correct_predictions, tf.float32)
-            self.accuracy = tf.reduce_mean(correct_predictions)
-    
-    def create_optimiser(self):
-        with tf.name_scope('optimizer'):
-            self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)
-            
-    def weight_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.truncated_normal(shape, stddev=0.1)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def bias_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.constant(0.1, shape=shape)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def fit(self):
-        data_indices = np.arange(self.n_inputs)
-
-        with tf.Session() as sess:
-            sess.run(tf.global_variables_initializer())
-            for i in range(self.epochs):
-                for j in range(self.iterations):
-                    chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)
-                    batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]
-            
-                    sess.run([DNN.loss, DNN.optimizer],
-                        feed_dict={DNN.X: batch_X,
-                                   DNN.Y: batch_Y})
-                    accuracy = sess.run(DNN.accuracy,
-                        feed_dict={DNN.X: batch_X,
-                                   DNN.Y: batch_Y})
-                    step = sess.run(DNN.global_step)
-    
-            self.train_loss, self.train_accuracy = sess.run([DNN.loss, DNN.accuracy],
-                feed_dict={DNN.X: self.X_train,
-                           DNN.Y: self.Y_train})
-        
-            self.test_loss, self.test_accuracy = sess.run([DNN.loss, DNN.accuracy],
-                feed_dict={DNN.X: self.X_test,
-                           DNN.Y: self.Y_test})
-
-
- - -
-

Optimizing and using gradient descent

- -

- - -

epochs = 100
-batch_size = 100
-n_neurons_layer1 = 100
-n_neurons_layer2 = 50
-n_categories = 10
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-
-

- - -

DNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        DNN = NeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,
-                                      n_neurons_layer1, n_neurons_layer2, n_categories,
-                                      epochs=epochs, batch_size=batch_size, eta=eta, lmbd=lmbd)
-        DNN.fit()
-        
-        DNN_tf[i][j] = DNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % DNN.test_accuracy)
-        print()
-
-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        DNN = DNN_tf[i][j]
-
-        train_accuracy[i][j] = DNN.train_accuracy
-        test_accuracy[i][j] = DNN.test_accuracy
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

- - -

# optional
-# we can use log files to visualize our graph in Tensorboard
-writer = tf.summary.FileWriter('logs/')
-writer.add_graph(tf.get_default_graph())
-
-
- - -
-

Using Keras

- -

-Keras is a high level neural network -that supports Tensorflow, CTNK and Theano as backends. -If you have Tensorflow installed Keras is available through the tf.keras module. -If you have Anaconda installed you may run the following command -

- - -

conda install keras
-
-

-Alternatively, if you have Tensorflow or one of the other supported backends install you may use the pip package manager: - -

- - -

pip3 install keras
-
-

-or look up the instructions here. - -

- - -

from keras.models import Sequential
-from keras.layers import Dense
-from keras.regularizers import l2
-from keras.optimizers import SGD
-
-def create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, eta, lmbd):
-    model = Sequential()
-    model.add(Dense(n_neurons_layer1, activation='sigmoid', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_neurons_layer2, activation='sigmoid', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_categories, activation='softmax'))
-    
-    sgd = SGD(lr=eta)
-    model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
-    
-    return model
-
-

- - -

DNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        DNN = create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories,
-                                         eta=eta, lmbd=lmbd)
-        DNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)
-        scores = DNN.evaluate(X_test, Y_test)
-        
-        DNN_keras[i][j] = DNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % scores[1])
-        print()
-
-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        DNN = DNN_keras[i][j]
-
-        train_accuracy[i][j] = DNN.evaluate(X_train, Y_train)[1]
-        test_accuracy[i][j] = DNN.evaluate(X_test, Y_test)[1]
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-
- - -
-

Which activation function should I use?

- -

-The Back propagation algorithm we derived above works by going from -the output layer to the input layer, propagating the error gradient on -the way. Once the algorithm has computed the gradient of the cost -function with regards to each parameter in the network, it uses these -gradients to update each parameter with a Gradient Descent (GD) step. - -

-Unfortunately for us, the gradients often get smaller and smaller as the -algorithm progresses down to the first hidden layers. As a result, the -GD update leaves the lower layer connection weights -virtually unchanged, and training never converges to a good -solution. This is known in the literature as -the vanishing gradients problem. - -

-In other cases, the opposite can happen, namely the the gradients can grow bigger and -bigger. The result is that many of the layers get large updates of the -weights the -algorithm diverges. This is the exploding gradients problem, which is -mostly encountered in recurrent neural networks. More generally, deep -neural networks suffer from unstable gradients, different layers may -learn at widely different speeds -

- - -
-

Is the Logistic activation function (Sigmoid) our choice?

- -

-Although this unfortunate behavior has been empirically observed for -quite a while (it was one of the reasons why deep neural networks were -mostly abandoned for a long time), it is only around 2010 that -significant progress was made in understanding it. - -

-A paper titled Understanding the Difficulty of Training Deep -Feedforward Neural Networks by Xavier Glorot and Yoshua Bengio found that -the problems with the popular logistic -sigmoid activation function and the weight initialization technique -that was most popular at the time, namely random initialization using -a normal distribution with a mean of 0 and a standard deviation of -1. - -

-They showed that with this activation function and this -initialization scheme, the variance of the outputs of each layer is -much greater than the variance of its inputs. Going forward in the -network, the variance keeps increasing after each layer until the -activation function saturates at the top layers. This is actually made -worse by the fact that the logistic function has a mean of 0.5, not 0 -(the hyperbolic tangent function has a mean of 0 and behaves slightly -better than the logistic function in deep networks). -

- - -
-

The derivative of the Logistic funtion

- -

-Looking at the logistic activation function, when inputs become large -(negative or positive), the function saturates at 0 or 1, with a -derivative extremely close to 0. Thus when backpropagation kicks in, -it has virtually no gradient to propagate back through the network, -and what little gradient exists keeps getting diluted as -backpropagation progresses down through the top layers, so there is -really nothing left for the lower layers. - -

-In their paper, Glorot and Bengio propose a way to significantly -alleviate this problem. We need the signal to flow properly in both -directions: in the forward direction when making predictions, and in -the reverse direction when backpropagating gradients. We don’t want -the signal to die out, nor do we want it to explode and saturate. For -the signal to flow properly, the authors argue that we need the -variance of the outputs of each layer to be equal to the variance of -its inputs, and we also need the gradients to have equal variance -before and after flowing through a layer in the reverse direction. - -

-One of the insights in the 2010 paper by Glorot and Bengio was that -the vanishing/exploding gradients problems were in part due to a poor -choice of activation function. Until then most people had assumed that -if Nature had chosen to use roughly sigmoid activation functions in -biological neurons, they must be an excellent choice. But it turns out -that other activation functions behave much better in deep neural -networks, in particular the ReLU activation function, mostly because -it does not saturate for positive values (and also because it is quite -fast to compute). -

- - -
-

The RELU function family

- -

-The ReLU activation function suffers from a problem known as the dying -ReLUs: during training, some neurons effectively die, meaning they -stop outputting anything other than 0. - -

-In some cases, you may find that half of your network’s neurons are -dead, especially if you used a large learning rate. During training, -if a neuron’s weights get updated such that the weighted sum of the -neuron’s inputs is negative, it will start outputting 0. When this -happen, the neuron is unlikely to come back to life since the gradient -of the ReLU function is 0 when its input is negative. - -

-To solve this problem, nowadays practitioners use a variant of the ReLU -function, such as the leaky ReLU discussed above or the so-called -exponential linear unit (ELU) function - -

 
-$$ -ELU(z) = \left\{\begin{array}{cc} \alpha\left( \exp{(z)}-1\right) & z < 0,\\ z & z \ge 0.\end{array}\right. -$$ -

 
-

- - -
-

Which activation function should we use?

- -

-In general it seems that the ELU activation function is better than -the leaky ReLU function (and its variants), which is better than -ReLU. ReLU performs better than \( \tanh \) which in turn performs better -than the logistic function. - -

-If runtime -performance is an issue, then you may opt for the leaky ReLU function over the -ELU function If you don’t -want to tweak yet another hyperparameter, you may just use the default -\( \alpha \) of \( 0.01 \) for the leaky ReLU, and \( 1 \) for ELU. If you have -spare time and computing power, you can use cross-validation or -bootstrap to evaluate other activation functions. -

- - -
-

A top-down perspective on Neural networks

- -

-The first thing we would like to do is divide the data into two or three -parts. A training set, a validation or dev (development) set, and a -test set. The test set is the data on which we want to make -predictions. The dev set is a subset of the training data we use to -check how well we are doing out-of-sample, after training the model on -the training dataset. We use the validation error as a proxy for the -test error in order to make tweaks to our model. It is crucial that we -do not use any of the test data to train the algorithm. This is a -cardinal sin in ML. Then: - -

    -

  • Estimate optimal error rate
  • -

  • Minimize underfitting (bias) on training data set.
  • -

  • Make sure you are not overfitting.
  • -
-

- -If the validation and test sets are drawn from the same distributions, -then a good performance on the validation set should lead to similarly -good performance on the test set. - -

-However, sometimes -the training data and test data differ in subtle ways because, for -example, they are collected using slightly different methods, or -because it is cheaper to collect data in one way versus another. In -this case, there can be a mismatch between the training and test -data. This can lead to the neural network overfitting these small -differences between the test and training sets, and a poor performance -on the test set despite having a good performance on the validation -set. To rectify this, Andrew Ng suggests making two validation or dev -sets, one constructed from the training data and one constructed from -the test data. The difference between the performance of the algorithm -on these two validation sets quantifies the train-test mismatch. This -can serve as another important diagnostic when using DNNs for -supervised learning. -

- - -
-

Limitations of supervised learning with deep networks

- -

-Like all statistical methods, supervised learning using neural -networks has important limitations. This is especially important when -one seeks to apply these methods, especially to physics problems. Like -all tools, DNNs are not a universal solution. Often, the same or -better performance on a task can be achieved by using a few -hand-engineered features (or even a collection of random -features). - -

-Here we list some of the important limitations of supervised neural network based models. - -

    -

  • Need labeled data. All supervised learning methods, DNNs for supervised learning require labeled data. Often, labeled data is harder to acquire than unlabeled data (e.g. one must pay for human experts to label images).
  • -

  • Supervised neural networks are extremely data intensive. DNNs are data hungry. They perform best when data is plentiful. This is doubly so for supervised methods where the data must also be labeled. The utility of DNNs is extremely limited if data is hard to acquire or the datasets are small (hundreds to a few thousand samples). In this case, the performance of other methods that utilize hand-engineered features can exceed that of DNNs.
  • -

  • Homogeneous data. Almost all DNNs deal with homogeneous data of one type. It is very hard to design architectures that mix and match data types (i.e. some continuous variables, some discrete variables, some time series). In applications beyond images, video, and language, this is often what is required. In contrast, ensemble models like random forests or gradient-boosted trees have no difficulty handling mixed data types.
  • -

  • Many problems are not about prediction. In natural science we are often interested in learning something about the underlying distribution that generates the data. In this case, it is often difficult to cast these ideas in a supervised learning setting. While the problems are related, it is possible to make good predictions with a wrong model. The model might or might not be useful for understanding the underlying science.
  • -
-

- -Some of these remarks are particular to DNNs, others are shared by all supervised learning methods. This motivates the use of unsupervised methods which in part circumvent these problems. -

- - -
-

Convolutional Neural Networks (recognizing images)

- -

-Convolutional Neural Networks (CNN) are very similar to ordinary Neural Networks. - -

-They are made up of neurons that have learnable weights and -biases. Each neuron receives some inputs, performs a dot product and -optionally follows it with a non-linearity. The whole network still -expresses a single differentiable score function: from the raw image -pixels on one end to class scores at the other. And they still have a -loss function (for example Softmax) on the last (fully-connected) layer -and all the tips/tricks we developed for learning regular Neural -Networks still apply (back propagation, gradient descent etc etc). - -

-What is the difference? CNN architectures make the explicit assumption that -the inputs are images, which allows us to encode certain properties -into the architecture. These then make the forward function more -efficient to implement and vastly reduce the amount of parameters in -the network. - -

-Here we provide only a superficial overview, for the more interested, we recommend highly the course -IN5400 – Machine Learning for Image Analysis -and the slides of CS231. -

- - -
-

Regular NNs don’t scale well to full images

- -

-As an example, consider -an image of size \( 32\times 32\times 3 \) (32 wide, 32 high, 3 color channels), so a -single fully-connected neuron in a first hidden layer of a regular -Neural Network would have \( 32\times 32\times 3 = 3072 \) weights. This amount still -seems manageable, but clearly this fully-connected structure does not -scale to larger images. For example, an image of more respectable -size, say \( 200\times 200\times 3 \), would lead to neurons that have -\( 200\times 200\times 3 = 120,000 \) weights. - -

-We could have -several such neurons, and the parameters would add up quickly! Clearly, -this full connectivity is wasteful and the huge number of parameters -would quickly lead to possible overfitting. - -

-

-
-

Figure 1: A regular 3-layer Neural Network.

-

-
-
- - -
-

3D volumes of neurons

- -

-Convolutional Neural Networks take advantage of the fact that the -input consists of images and they constrain the architecture in a more -sensible way. - -

-In particular, unlike a regular Neural Network, the -layers of a CNN have neurons arranged in 3 dimensions: width, -height, depth. (Note that the word depth here refers to the third -dimension of an activation volume, not to the depth of a full Neural -Network, which can refer to the total number of layers in a network.) - -

-To understand it better, the above example of an image -with an input volume of -activations has dimensions \( 32\times 32\times 3 \) (width, height, -depth respectively). - -

-The neurons in a layer will -only be connected to a small region of the layer before it, instead of -all of the neurons in a fully-connected manner. Moreover, the final -output layer could for this specific image have dimensions \( 1\times 1 times 10 \), -because by the -end of the CNN architecture we will reduce the full image into a -single vector of class scores, arranged along the depth -dimension. - -

-

-
-

Figure 2: A CNN arranges its neurons in three dimensions (width, height, depth), as visualized in one of the layers. Every layer of a CNN transforms the 3D input volume to a 3D output volume of neuron activations. In this example, the red input layer holds the image, so its width and height would be the dimensions of the image, and the depth would be 3 (Red, Green, Blue channels).

-

-
-
- - -
-

Layers used to build CNNs

- -

-A simple CNN is a sequence of layers, and every layer of a CNN -transforms one volume of activations to another through a -differentiable function. We use three main types of layers to build -CNN architectures: Convolutional Layer, Pooling Layer, and -Fully-Connected Layer (exactly as seen in regular Neural Networks). We -will stack these layers to form a full CNN architecture. - -

-A simple CNN for image classification could have the architecture: - -

    -

  • INPUT (\( 32\times 32 \times 3 \)) will hold the raw pixel values of the image, in this case an image of width 32, height 32, and with three color channels R,G,B.
  • -

  • CONV (convolutional )layer will compute the output of neurons that are connected to local regions in the input, each computing a dot product between their weights and a small region they are connected to in the input volume. This may result in volume such as \( [32\times 32\times 12] \) if we decided to use 12 filters.
  • -

  • RELU layer will apply an elementwise activation function, such as the \( max(0,x) \) thresholding at zero. This leaves the size of the volume unchanged (\( [32\times 32\times 12] \)).
  • -

  • POOL (pooling) layer will perform a downsampling operation along the spatial dimensions (width, height), resulting in volume such as \( [16\times 16\times 12] \).
  • -

  • FC (i.e. fully-connected) layer will compute the class scores, resulting in volume of size \( [1\times 1\times 10] \), where each of the 10 numbers correspond to a class score, such as among the 10 categories of the MNIST images we considered above . As with ordinary Neural Networks and as the name implies, each neuron in this layer will be connected to all the numbers in the previous volume.
  • -
-
- - -
-

Transforming images

- -

-CNNs transform the original image layer by layer from the original -pixel values to the final class scores. - -

-Observe that some layers contain -parameters and other don’t. In particular, the CNN layers perform -transformations that are a function of not only the activations in the -input volume, but also of the parameters (the weights and biases of -the neurons). On the other hand, the RELU/POOL layers will implement a -fixed function. The parameters in the CONV/FC layers will be trained -with gradient descent so that the class scores that the CNN computes -are consistent with the labels in the training set for each image. -

- - -
-

CNNs in brief

- -

-In summary: - -

    -

  • A CNN architecture is in the simplest case a list of Layers that transform the image volume into an output volume (e.g. holding the class scores)
  • -

  • There are a few distinct types of Layers (e.g. CONV/FC/RELU/POOL are by far the most popular)
  • -

  • Each Layer accepts an input 3D volume and transforms it to an output 3D volume through a differentiable function
  • -

  • Each Layer may or may not have parameters (e.g. CONV/FC do, RELU/POOL don’t)
  • -

  • Each Layer may or may not have additional hyperparameters (e.g. CONV/FC/POOL do, RELU doesn’t)
  • -
-

- -For more material on convolutional networks, we strongly recommend -the course -IN5400 – Machine Learning for Image Analysis -and the slides of CS231 which is taught at Stanford University (consistently ranked as one of the top computer science programs in the world). Michael Nielsen's book is a must read, in particular chapter 6 which deals with CNNs. -

- - -
-

CNNs in more detail, building convolutional neural networks in Tensorflow and Keras

- -

-As discussed above, CNNs are neural networks built from the assumption that the inputs -to the network are 2D images. This is important because the number of features or pixels in images -grows very fast with the image size, and an enormous number of weights and biases are needed in order to build an accurate network. - -

-As before, we still have our input, a hidden layer and an output. What's novel about convolutional networks -are the convolutional and pooling layers stacked in pairs between the input and the hidden layer. -In addition, the data is no longer represented as a 2D feature matrix, instead each input is a number of 2D -matrices, typically 1 for each color dimension (Red, Green, Blue). -

- - -
-

Setting it up

- -

-It means that to represent the entire -dataset of images, we require a 4D matrix or tensor. This tensor has the dimensions: - -

 
-$$ -(n_{inputs},\, n_{pixels, width},\, n_{pixels, height},\, depth) . -$$ -

 
-

- - -
-

The MNIST dataset again

- -

-The MNIST dataset consists of grayscale images with a pixel size of -\( 28\times 28 \), meaning we require \( 28 \times 28 = 724 \) weights to each -neuron in the first hidden layer. - -

-If we were to analyze images of size \( 128\times 128 \) we would require -\( 128 \times 128 = 16384 \) weights to each neuron. Even worse if we were -dealing with color images, as most images are, we have an image matrix -of size \( 128\times 128 \) for each color dimension (Red, Green, Blue), -meaning 3 times the number of weights \( = 49152 \) are required for every -single neuron in the first hidden layer. -

- - -
-

Strong correlations

-Images typically have strong local correlations, meaning that a small -part of the image varies little from its neighboring regions. If for -example we have an image of a blue car, we can roughly assume that a -small blue part of the image is surrounded by other blue regions. - -

-Therefore, instead of connecting every single pixel to a neuron in the -first hidden layer, as we have previously done with deep neural -networks, we can instead connect each neuron to a small part of the -image (in all 3 RGB depth dimensions). The size of each small area is -fixed, and known as a receptive. -

- - -
-

Layers of a CNN

-The layers of a convolutional neural network arrange neurons in 3D: width, height and depth. -The input image is typically a square matrix of depth 3. - -

-A convolution is performed on the image which outputs -a 3D volume of neurons. The weights to the input are arranged in a number of 2D matrices, known as filters. - -

-Each filter slides along the input image, taking the dot product -between each small part of the image and the filter, in all depth -dimensions. This is then passed through a non-linear function, -typically the Rectified Linear (ReLu) function, which serves as the -activation of the neurons in the first convolutional layer. This is -further passed through a pooling layer, which reduces the size of the -convolutional layer, e.g. by taking the maximum or average across some -small regions, and this serves as input to the next convolutional -layer. -

- - -
-

Systematic reduction

- -

-By systematically reducing the size of the input volume, through -convolution and pooling, the network should create representations of -small parts of the input, and then from them assemble representations -of larger areas. The final pooling layer is flattened to serve as -input to a hidden layer, such that each neuron in the final pooling -layer is connected to every single neuron in the hidden layer. This -then serves as input to the output layer, e.g. a softmax output for -classification. -

- - -
-

Prerequisites: Collect and pre-process data

-

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-# RGB images have a depth of 3
-# our images are grayscale so they should have a depth of 1
-inputs = inputs[:,:,:,np.newaxis]
-
-print("inputs = (n_inputs, pixel_width, pixel_height, depth) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# choose some random images to display
-n_inputs = len(inputs)
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-
- - -
-

Importing Keras and Tensorflow

-

- - -

from keras.utils import to_categorical
-from sklearn.model_selection import train_test_split
-
-# representation of labels
-labels = to_categorical(labels)
-
-# split into train and test data
-# one-liner from scikit-learn library
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-
- - -
-

Using TensorFlow backend

- -

-We need to define model and architecture and choose cost function and optmizer. -

- - -

import tensorflow as tf
-
-class ConvolutionalNeuralNetworkTensorflow:
-    def __init__(
-        self,
-        X_train,
-        Y_train,
-        X_test,
-        Y_test,
-        n_filters=10,
-        n_neurons_connected=50,
-        n_categories=10,
-        receptive_field=3,
-        stride=1,
-        padding=1,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-    ):
-        
-        self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')
-        
-        self.X_train = X_train
-        self.Y_train = Y_train
-        self.X_test = X_test
-        self.Y_test = Y_test
-        
-        self.n_inputs, self.input_width, self.input_height, self.depth = X_train.shape
-        
-        self.n_filters = n_filters
-        self.n_downsampled = int(self.input_width*self.input_height*n_filters / 4)
-        self.n_neurons_connected = n_neurons_connected
-        self.n_categories = n_categories
-        
-        self.receptive_field = receptive_field
-        self.stride = stride
-        self.strides = [stride, stride, stride, stride]
-        self.padding = padding
-        
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-        
-        self.create_placeholders()
-        self.create_CNN()
-        self.create_loss()
-        self.create_optimiser()
-        self.create_accuracy()
-    
-    def create_placeholders(self):
-        with tf.name_scope('data'):
-            self.X = tf.placeholder(tf.float32, shape=(None, self.input_width, self.input_height, self.depth), name='X_data')
-            self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')
-    
-    def create_CNN(self):
-        with tf.name_scope('CNN'):
-            
-            # Convolutional layer
-            self.W_conv = self.weight_variable([self.receptive_field, self.receptive_field, self.depth, self.n_filters], name='conv', dtype=tf.float32)
-            b_conv = self.weight_variable([self.n_filters], name='conv', dtype=tf.float32)
-            z_conv = tf.nn.conv2d(self.X, self.W_conv, self.strides, padding='SAME', name='conv') + b_conv
-            a_conv = tf.nn.relu(z_conv)
-            
-            # 2x2 max pooling
-            a_pool = tf.nn.max_pool(a_conv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME', name='pool')
-            
-            # Fully connected layer
-            a_pool_flat = tf.reshape(a_pool, [-1, self.n_downsampled])
-            self.W_fc = self.weight_variable([self.n_downsampled, self.n_neurons_connected], name='fc', dtype=tf.float32)
-            b_fc = self.bias_variable([self.n_neurons_connected], name='fc', dtype=tf.float32)
-            a_fc = tf.nn.relu(tf.matmul(a_pool_flat, self.W_fc) + b_fc)
-            
-            # Output layer
-            self.W_out = self.weight_variable([self.n_neurons_connected, self.n_categories], name='out', dtype=tf.float32)
-            b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)
-            self.z_out = tf.matmul(a_fc, self.W_out) + b_out
-    
-    def create_loss(self):
-        with tf.name_scope('loss'):
-            softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))
-            
-            regularizer_loss_conv = tf.nn.l2_loss(self.W_conv)
-            regularizer_loss_fc = tf.nn.l2_loss(self.W_fc)
-            regularizer_loss_out = tf.nn.l2_loss(self.W_out)
-            regularizer_loss = self.lmbd*(regularizer_loss_conv + regularizer_loss_fc + regularizer_loss_out)
-            
-            self.loss = softmax_loss + regularizer_loss
-
-    def create_accuracy(self):
-        with tf.name_scope('accuracy'):
-            probabilities = tf.nn.softmax(self.z_out)
-            predictions = tf.argmax(probabilities, 1)
-            labels = tf.argmax(self.Y, 1)
-            
-            correct_predictions = tf.equal(predictions, labels)
-            correct_predictions = tf.cast(correct_predictions, tf.float32)
-            self.accuracy = tf.reduce_mean(correct_predictions)
-    
-    def create_optimiser(self):
-        with tf.name_scope('optimizer'):
-            self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)
-            
-    def weight_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.truncated_normal(shape, stddev=0.1)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def bias_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.constant(0.1, shape=shape)
-        return tf.Variable(initial, name=name, dtype=dtype)
-
-    def fit(self):
-        data_indices = np.arange(self.n_inputs)
-
-        with tf.Session() as sess:
-            sess.run(tf.global_variables_initializer())
-            for i in range(self.epochs):
-                for j in range(self.iterations):
-                    chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)
-                    batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]
-            
-                    sess.run([CNN.loss, CNN.optimizer],
-                        feed_dict={CNN.X: batch_X,
-                                   CNN.Y: batch_Y})
-                    accuracy = sess.run(CNN.accuracy,
-                        feed_dict={CNN.X: batch_X,
-                                   CNN.Y: batch_Y})
-                    step = sess.run(CNN.global_step)
-    
-            self.train_loss, self.train_accuracy = sess.run([CNN.loss, CNN.accuracy],
-                feed_dict={CNN.X: self.X_train,
-                           CNN.Y: self.Y_train})
-        
-            self.test_loss, self.test_accuracy = sess.run([CNN.loss, CNN.accuracy],
-                feed_dict={CNN.X: self.X_test,
-                           CNN.Y: self.Y_test})
-
-
- - -
-

Train the model

- -

-We need now to train the model, evaluate it and test its performance on test data, and eventually include hyperparameters. -

- - -

epochs = 100
-batch_size = 100
-n_filters = 10
-n_neurons_connected = 50
-n_categories = 10
-
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-CNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        CNN = ConvolutionalNeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,
-                                      n_filters=n_filters, n_neurons_connected=n_neurons_connected,
-                                      n_categories=n_categories, epochs=epochs, batch_size=batch_size,
-                                      eta=eta, lmbd=lmbd)
-        CNN.fit()
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % CNN.test_accuracy)
-        print()
-            
-        CNN_tf[i][j] = CNN
-
-
- - -
-

Visualizing the results

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        CNN = CNN_tf[i][j]
-
-        train_accuracy[i][j] = CNN.train_accuracy
-        test_accuracy[i][j] = CNN.test_accuracy
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-
- - -
-

Running with Keras

- -

- - -

from keras.models import Sequential
-from keras.layers.convolutional import Conv2D
-from keras.layers.convolutional import MaxPooling2D
-from keras.layers import Flatten
-from keras.layers import Dense
-from keras.regularizers import l2
-from keras.optimizers import SGD
-
-def create_convolutional_neural_network_keras(input_shape, receptive_field,
-                                              n_filters, n_neurons_connected, n_categories,
-                                              eta, lmbd):
-    model = Sequential()
-    model.add(Conv2D(n_filters, (receptive_field, receptive_field), input_shape=input_shape, padding='same',
-              activation='relu', kernel_regularizer=l2(lmbd)))
-    model.add(MaxPooling2D(pool_size=(2, 2)))
-    model.add(Flatten())
-    model.add(Dense(n_neurons_connected, activation='relu', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_categories, activation='softmax', kernel_regularizer=l2(lmbd)))
-    
-    sgd = SGD(lr=eta)
-    model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
-    
-    return model
-
-epochs = 100
-batch_size = 100
-input_shape = X_train.shape[1:4]
-receptive_field = 3
-n_filters = 10
-n_neurons_connected = 50
-n_categories = 10
-
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-
-
- - -
-

Final part

- -

- - -

CNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        CNN = create_convolutional_neural_network_keras(input_shape, receptive_field,
-                                              n_filters, n_neurons_connected, n_categories,
-                                              eta, lmbd)
-        CNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)
-        scores = CNN.evaluate(X_test, Y_test)
-        
-        CNN_keras[i][j] = CNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % scores[1])
-        print()
-
-
- - -
-

Final visualization

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        CNN = CNN_keras[i][j]
-
-        train_accuracy[i][j] = CNN.evaluate(X_train, Y_train)[1]
-        test_accuracy[i][j] = CNN.evaluate(X_test, Y_test)[1]
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-
- - -
-

Fun links

- -
    -

  1. Self-Driving cars using a convolutional neural network
  2. -

  3. Abstract art using convolutional neural networks
  4. -
-
- - - -
-
- - - - - - - - - - - - diff --git a/doc/src/NeuralNet/NeuralNet-solarized.html b/doc/src/NeuralNet/NeuralNet-solarized.html deleted file mode 100644 index 7dd813788..000000000 --- a/doc/src/NeuralNet/NeuralNet-solarized.html +++ /dev/null @@ -1,3642 +0,0 @@ - - - - - - - -Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks

- -

- - -

-Morten Hjorth-Jensen [1, 2] -
- -

- - -

[1] Department of Physics, University of Oslo
-
[2] Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University
-
-

-

Oct 19, 2018

-
-

-









- -

Neural networks

- -

-Artificial neural networks are computational systems that can learn to -perform tasks by considering examples, generally without being -programmed with any task-specific rules. It is supposed to mimic a -biological system, wherein neurons interact by sending signals in the -form of mathematical functions between layers. All layers can contain -an arbitrary number of neurons, and each connection is represented by -a weight variable. - -

-









- -

Artificial neurons

- -

-The field of artificial neural networks has a long history of -development, and is closely connected with the advancement of computer -science and computers in general. A model of artificial neurons was -first developed by McCulloch and Pitts in 1943 to study signal -processing in the brain and has later been refined by others. The -general idea is to mimic neural networks in the human brain, which is -composed of billions of neurons that communicate with each other by -sending electrical signals. Each neuron accumulates its incoming -signals, which must exceed an activation threshold to yield an -output. If the threshold is not overcome, the neuron remains inactive, -i.e. has zero output. - -

-This behaviour has inspired a simple mathematical model for an artificial neuron. - -$$ -\begin{equation} - y = f\left(\sum_{i=1}^n w_ix_i\right) = f(u) -\label{artificialNeuron} -\end{equation} -$$ - -Here, the output \( y \) of the neuron is the value of its activation function, which have as input -a weighted sum of signals \( x_i, \dots ,x_n \) received by \( n \) other neurons. - -

-Conceptually, it is helpful to divide neural networks into four -categories: - -

    -
  1. general purpose neural networks for supervised learning,
  2. -
  3. neural networks designed specifically for image processing, the most prominent example of this class being Convolutional Neural Networks (CNNs),
  4. -
  5. neural networks for sequential data such as Recurrent Neural Networks (RNNs), and
  6. -
  7. neural networks for unsupervised learning such as Deep Boltzmann Machines.
  8. -
- -In natural science, DNNs and CNNs have already found numerous -applications. In statistical physics, they have been applied to detect -phase transitions in 2D Ising and Potts models, lattice gauge -theories, and different phases of polymers, or solving the -Navier-Stokes equation in weather forecasting. Deep learning has also -found interesting applications in quantum physics. Various quantum -phase transitions can be detected and studied using DNNs and CNNs, -topological phases, and even non-equilibrium many-body -localization. Representing quantum states as DNNs quantum state -tomography are among some of the impressive achievements to reveal the -potential of DNNs to facilitate the study of quantum systems. - -

-In quantum information theory, it has been shown that one can perform -gate decompositions with the help of neural. - -

-The applications are not limited to the natural sciences. There is a -plethora of applications in essentially all disciplines, from the -humanities to life science and medicine. - -

-









- -

Neural network types

- -

-An artificial neural network (ANN), is a computational model that -consists of layers of connected neurons, or nodes or units. We will -refer to these interchangeably as units or nodes, and sometimes as -neurons. - -

-It is supposed to mimic a biological nervous system by letting each -neuron interact with other neurons by sending signals in the form of -mathematical functions between layers. A wide variety of different -ANNs have been developed, but most of them consist of an input layer, -an output layer and eventual layers in-between, called hidden -layers. All layers can contain an arbitrary number of nodes, and each -connection between two nodes is associated with a weight variable. - -

-Neural networks (also called neural nets) are neural-inspired -nonlinear models for supervised learning. As we will see, neural nets -can be viewed as natural, more powerful extensions of supervised -learning methods such as linear and logistic regression and soft-max -methods we discussed earlier. - -

-









- -

Feed-forward neural networks

- -

-The feed-forward neural network (FFNN) was the first and simplest type -of ANNs that were devised. In this network, the information moves in -only one direction: forward through the layers. - -

-Nodes are represented by circles, while the arrows display the -connections between the nodes, including the direction of information -flow. Additionally, each arrow corresponds to a weight variable -(figure to come). We observe that each node in a layer is connected -to all nodes in the subsequent layer, making this a so-called -fully-connected FFNN. - -

-









- -

Convolutional Neural Network

- -

-A different variant of FFNNs are convolutional neural networks -(CNNs), which have a connectivity pattern inspired by the animal -visual cortex. Individual neurons in the visual cortex only respond to -stimuli from small sub-regions of the visual field, called a receptive -field. This makes the neurons well-suited to exploit the strong -spatially local correlation present in natural images. The response of -each neuron can be approximated mathematically as a convolution -operation. (figure to come) - -

-Convolutional neural networks emulate the behaviour of neurons in the -visual cortex by enforcing a local connectivity pattern between -nodes of adjacent layers: Each node in a convolutional layer is -connected only to a subset of the nodes in the previous layer, in -contrast to the fully-connected FFNN. Often, CNNs consist of several -convolutional layers that learn local features of the input, with a -fully-connected layer at the end, which gathers all the local data and -produces the outputs. They have wide applications in image and video -recognition. - -

-









- -

Recurrent neural networks

- -

-So far we have only mentioned ANNs where information flows in one -direction: forward. Recurrent neural networks on the other hand, -have connections between nodes that form directed cycles. This -creates a form of internal memory which are able to capture -information on what has been calculated before; the output is -dependent on the previous computations. Recurrent NNs make use of -sequential information by performing the same task for every element -in a sequence, where each element depends on previous elements. An -example of such information is sentences, making recurrent NNs -especially well-suited for handwriting and speech recognition. - -

-









- -

Other types of networks

- -

-There are many other kinds of ANNs that have been developed. One type -that is specifically designed for interpolation in multidimensional -space is the radial basis function (RBF) network. RBFs are typically -made up of three layers: an input layer, a hidden layer with -non-linear radial symmetric activation functions and a linear output -layer (''linear'' here means that each node in the output layer has a -linear activation function). The layers are normally fully-connected -and there are no cycles, thus RBFs can be viewed as a type of -fully-connected FFNN. They are however usually treated as a separate -type of NN due the unusual activation functions. - -

-









- -

Multilayer perceptrons

- -

-One uses often so-called fully-connected feed-forward neural networks -with three or more layers (an input layer, one or more hidden layers -and an output layer) consisting of neurons that have non-linear -activation functions. - -

-Such networks are often called multilayer perceptrons (MLPs). - -

-









- -

Why multilayer perceptrons?

- -

-According to the Universal approximation theorem, a feed-forward -neural network with just a single hidden layer containing a finite -number of neurons can approximate a continuous multidimensional -function to arbitrary accuracy, assuming the activation function for -the hidden layer is a non-constant, bounded and -monotonically-increasing continuous function. - -

-Note that the requirements on the activation function only applies to -the hidden layer, the output nodes are always assumed to be linear, so -as to not restrict the range of output values. - -

-









- -

Mathematical model

- -

-The output \( y \) is produced via the activation function \( f \) -$$ - y = f\left(\sum_{i=1}^n w_ix_i + b_i\right) = f(z), -$$ - -This function receives \( x_i \) as inputs. -Here the activation \( z=(\sum_{i=1}^n w_ix_i+b_i) \). -In an FFNN of such neurons, the inputs \( x_i \) are the outputs of -the neurons in the preceding layer. Furthermore, an MLP is -fully-connected, which means that each neuron receives a weighted sum -of the outputs of all neurons in the previous layer. - -

-









- -

Mathematical model

- -

-First, for each node \( i \) in the first hidden layer, we calculate a weighted sum \( z_i^1 \) of the input coordinates \( x_j \), - -$$ -\begin{equation} z_i^1 = \sum_{j=1}^{M} w_{ij}^1 x_j + b_i^1 -\label{_auto1} -\end{equation} -$$ - -

-Here \( b_i \) is the so-called bias which is normally needed in -case of zero activation weights or inputs. How to fix the biases and -the weights will be discussed below. The value of \( z_i^1 \) is the -argument to the activation function \( f_i \) of each node \( i \), The -variable \( M \) stands for all possible inputs to a given node \( i \) in the -first layer. We define the output \( y_i^1 \) of all neurons in layer 1 as - -$$ -\begin{equation} - y_i^1 = f(z_i^1) = f\left(\sum_{j=1}^M w_{ij}^1 x_j + b_i^1\right) -\label{outputLayer1} -\end{equation} -$$ - -

-where we assume that all nodes in the same layer have identical -activation functions, hence the notation \( f \). In general, we could assume in the more general case that different layers have different activation functions. -In this case we would identify these functions with a superscript \( l \) for the \( l \)-th layer, - -$$ -\begin{equation} - y_i^l = f^l(u_i^l) = f^l\left(\sum_{j=1}^{N_{l-1}} w_{ij}^l y_j^{l-1} + b_i^l\right) -\label{generalLayer} -\end{equation} -$$ - -

-where \( N_l \) is the number of nodes in layer \( l \). When the output of -all the nodes in the first hidden layer are computed, the values of -the subsequent layer can be calculated and so forth until the output -is obtained. - -

-









- -

Mathematical model

- -

-The output of neuron \( i \) in layer 2 is thus, - -$$ -\begin{align} - y_i^2 &= f^2\left(\sum_{j=1}^N w_{ij}^2 y_j^1 + b_i^2\right) -\label{_auto2}\\ - &= f^2\left[\sum_{j=1}^N w_{ij}^2f^1\left(\sum_{k=1}^M w_{jk}^1 x_k + b_j^1\right) + b_i^2\right] -\label{outputLayer2} -\end{align} -$$ - -where we have substituted \( y_k^1 \) with the inputs \( x_k \). Finally, the ANN output reads - -$$ -\begin{align} - y_i^3 &= f^3\left(\sum_{j=1}^N w_{ij}^3 y_j^2 + b_i^3\right) -\label{_auto3}\\ - &= f_3\left[\sum_{j} w_{ij}^3 f^2\left(\sum_{k} w_{jk}^2 f^1\left(\sum_{m} w_{km}^1 x_m + b_k^1\right) + b_j^2\right) - + b_1^3\right] -\label{_auto4} -\end{align} -$$ - -

-









- -

Mathematical model

- -

-We can generalize this expression to an MLP with \( l \) hidden -layers. The complete functional form is, - -$$ -\begin{align} -&y^{l+1}_i = f^{l+1}\left[\!\sum_{j=1}^{N_l} w_{ij}^3 f^l\left(\sum_{k=1}^{N_{l-1}}w_{jk}^{l-1}\left(\dots f^1\left(\sum_{n=1}^{N_0} w_{mn}^1 x_n+ b_m^1\right)\dots\right)+b_k^2\right)+b_1^3\right] && -\label{completeNN} -\end{align} -$$ - -

-which illustrates a basic property of MLPs: The only independent -variables are the input values \( x_n \). - -

-









- -

Mathematical model

- -

-This confirms that an MLP, despite its quite convoluted mathematical -form, is nothing more than an analytic function, specifically a -mapping of real-valued vectors \( \hat{x} \in \mathbb{R}^n \rightarrow -\hat{y} \in \mathbb{R}^m \). - -

-Furthermore, the flexibility and universality of an MLP can be -illustrated by realizing that the expression is essentially a nested -sum of scaled activation functions of the form - -$$ -\begin{equation} - f(x) = c_1 f(c_2 x + c_3) + c_4 -\label{_auto5} -\end{equation} -$$ - -

-where the parameters \( c_i \) are weights and biases. By adjusting these -parameters, the activation functions can be shifted up and down or -left and right, change slope or be rescaled which is the key to the -flexibility of a neural network. - -

-









- -

Matrix-vector notation

- -

-We can introduce a more convenient notation for the activations in an A NN. - -

-Additionally, we can represent the biases and activations -as layer-wise column vectors \( \hat{b}_l \) and \( \hat{y}_l \), so that the \( i \)-th element of each vector -is the bias \( b_i^l \) and activation \( y_i^l \) of node \( i \) in layer \( l \) respectively. - -

-We have that \( \mathrm{W}_l \) is an \( N_{l-1} \times N_l \) matrix, while \( \hat{b}_l \) and \( \hat{y}_l \) are \( N_l \times 1 \) column vectors. -With this notation, the sum becomes a matrix-vector multiplication, and we can write -the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as -$$ -\begin{equation} - \hat{y}_2 = f_2(\mathrm{W}_2 \hat{y}_{1} + \hat{b}_{2}) = - f_2\left(\left[\begin{array}{ccc} - w^2_{11} &w^2_{12} &w^2_{13} \\ - w^2_{21} &w^2_{22} &w^2_{23} \\ - w^2_{31} &w^2_{32} &w^2_{33} \\ - \end{array} \right] \cdot - \left[\begin{array}{c} - y^1_1 \\ - y^1_2 \\ - y^1_3 \\ - \end{array}\right] + - \left[\begin{array}{c} - b^2_1 \\ - b^2_2 \\ - b^2_3 \\ - \end{array}\right]\right). -\label{_auto6} -\end{equation} -$$ - -

-









- -

Matrix-vector notation and activation

- -

-The activation of node \( i \) in layer 2 is - -$$ -\begin{equation} - y^2_i = f_2\Bigr(w^2_{i1}y^1_1 + w^2_{i2}y^1_2 + w^2_{i3}y^1_3 + b^2_i\Bigr) = - f_2\left(\sum_{j=1}^3 w^2_{ij} y_j^1 + b^2_i\right). -\label{_auto7} -\end{equation} -$$ - -

-This is not just a convenient and compact notation, but also a useful -and intuitive way to think about MLPs: The output is calculated by a -series of matrix-vector multiplications and vector additions that are -used as input to the activation functions. For each operation -\( \mathrm{W}_l \hat{y}_{l-1} \) we move forward one layer. - -

-









- -

Activation functions

- -

-A property that characterizes a neural network, other than its -connectivity, is the choice of activation function(s). As described -in, the following restrictions are imposed on an activation function -for a FFNN to fulfill the universal approximation theorem - -

    -
  • Non-constant
  • -
  • Bounded
  • -
  • Monotonically-increasing
  • -
  • Continuous
  • -
- -









- -

Activation functions, Logistic and Hyperbolic ones

- -

-The second requirement excludes all linear functions. Furthermore, in -a MLP with only linear activation functions, each layer simply -performs a linear transformation of its inputs. - -

-Regardless of the number of layers, the output of the NN will be -nothing but a linear function of the inputs. Thus we need to introduce -some kind of non-linearity to the NN to be able to fit non-linear -functions Typical examples are the logistic Sigmoid - -$$ - f(x) = \frac{1}{1 + e^{-x}}, -$$ - -and the hyperbolic tangent function -$$ - f(x) = \tanh(x) -$$ - -

-









- -

Relevance

- -

-The sigmoid function are more biologically plausible because the -output of inactive neurons are zero. Such activation function are -called one-sided. However, it has been shown that the hyperbolic -tangent performs better than the sigmoid for training MLPs. has -become the most popular for deep neural networks - -

- - -

"""The sigmoid function (or the logistic curve) is a 
-function that takes any real number, z, and outputs a number (0,1).
-It is useful in neural networks for assigning weights on a relative scale.
-The value z is the weighted sum of parameters involved in the learning algorithm."""
-
-import numpy
-import matplotlib.pyplot as plt
-import math as mt
-
-z = numpy.arange(-5, 5, .1)
-sigma_fn = numpy.vectorize(lambda z: 1/(1+numpy.exp(-z)))
-sigma = sigma_fn(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, sigma)
-ax.set_ylim([-0.1, 1.1])
-ax.set_xlim([-5,5])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('sigmoid function')
-
-plt.show()
-
-"""Step Function"""
-z = numpy.arange(-5, 5, .02)
-step_fn = numpy.vectorize(lambda z: 1.0 if z >= 0.0 else 0.0)
-step = step_fn(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, step)
-ax.set_ylim([-0.5, 1.5])
-ax.set_xlim([-5,5])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('step function')
-
-plt.show()
-
-"""Sine Function"""
-z = numpy.arange(-2*mt.pi, 2*mt.pi, 0.1)
-t = numpy.sin(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, t)
-ax.set_ylim([-1.0, 1.0])
-ax.set_xlim([-2*mt.pi,2*mt.pi])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('sine function')
-
-plt.show()
-
-"""Plots a graph of the squashing function used by a rectified linear
-unit"""
-z = numpy.arange(-2, 2, .1)
-zero = numpy.zeros(len(z))
-y = numpy.max([zero, z], axis=0)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, y)
-ax.set_ylim([-2.0, 2.0])
-ax.set_xlim([-2.0, 2.0])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('Rectified linear unit')
-
-plt.show()
-
-

-









- -

The multilayer perceptron (MLP)

- -

-The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of - -

    -
  1. A neural network with one or more layers of nodes between the input and the output nodes.
  2. -
  3. The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer.
  4. -
  5. The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer.
  6. -
- -As a convention it is normal to call a network with one layer of input units, one layer of hidden -units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc. - -

-For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units. -Hereafter we will call the various entities of a layer for nodes. -There are also no connections within a single layer. - -

-The number of input nodes does not need to equal the number of output -nodes. This applies also to the hidden layers. Each layer may have its -own number of nodes and activation functions. - -

-The hidden layers have their name from the fact that they are not -linked to observables and as we will see below when we define the -so-called activation \( \hat{z} \), we can think of this as a basis -expansion of the original inputs \( \hat{x} \). The difference however -between neural networks and say linear regression is that now these -basis functions (which will correspond to the weights in the network) -are learned from data. This results in an important difference between -neural networks and deep learning approaches on one side and methods -like logistic regression or linear regression and their modifications on the other side. - -

-









- -

From one to many layers, the universal approximation theorem

- -

-A neural network with only one layer, what we called the simple -perceptron, is best suited if we have a standard binary model with -clear (linear) boundaries between the outcomes. As such it could -equally well be replaced by standard linear regression or logistic -regression. Networks with one or more hidden layers approximate -systems with more complex boundaries. - -

-As stated earlier, -an important theorem in studies of neural networks, restated without -proof here, is the universal approximation -theorem. - -

-It states that a feed-forward network with a single hidden layer -containing a finite number of neurons can approximate continuous -functions on compact subsets of real functions. The theorem thus -states that simple neural networks can represent a wide variety of -interesting functions when given appropriate parameters. It is the -multilayer feedforward architecture itself which gives neural networks -the potential of being universal approximators. - -

-









- -

Deriving the back propagation code for a multilayer perceptron model

- -

-Note: figures will be inserted later! - -

-As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications. -The unknowwn quantities are our weights \( w_{ij} \) and we need to find an algorithm for changing them so that our errors are as small as possible. -This leads us to the famous back propagation algorithm. - -

-The questions we want to ask are how do changes in the biases and the -weights in our network change the cost function and how can we use the -final output to modify the weights? - -

-To derive these equations let us start with a plain regression problem -and define our cost function as - -$$ -{\cal C}(\hat{W}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2, -$$ - -

-where the $t_i$s are our \( n \) targets (the values we want to -reproduce), while the outputs of the network after having propagated -all inputs \( \hat{x} \) are given by \( y_i \). Below we will demonstrate -how the basic equations arising from the back propagation algorithm -can be modified in order to study classification problems with \( K \) -classes. - -

-









- -

Definitions

- -

-With our definition of the targets \( \hat{t} \), the outputs of the -network \( \hat{y} \) and the inputs \( \hat{x} \) we -define now the activation \( z_j^l \) of node/neuron/unit \( j \) of the -\( l \)-th layer as a function of the bias, the weights which add up from -the previous layer \( l-1 \) and the forward passes/outputs -\( \hat{a}^{l-1} \) from the previous layer as - -$$ -z_j^l = \sum_{i=1}^{M_{l-1}}w_{ij}^la_i^{l-1}+b_j^l, -$$ - -

-where \( b_k^l \) are the biases from layer \( l \). Here \( M_{l-1} \) -represents the total number of nodes/neurons/units of layer \( l-1 \). The -figure here illustrates this equation. We can rewrite this in a more -compact form as the matrix-vector products we discussed earlier, - -$$ -\hat{z}^l = \left(\hat{W}^l\right)^T\hat{a}^{l-1}+\hat{b}^l. -$$ - -

-With the activation values \( \hat{z}^l \) we can in turn define the -output of layer \( l \) as \( \hat{a}^l = f(\hat{z}^l) \) where \( f \) is our -activation function. In the examples here we will use the sigmoid -function discussed in our logistic regression lectures. We will also use the same activation function \( f \) for all layers -and their nodes. It means we have - -$$ -a_j^l = f(z_j^l) = \frac{1}{1+\exp{-(z_j^l)}}. -$$ - -

-









- -

Derivatives and the chain rule

- -

-From the definition of the activation \( z_j^l \) we have -$$ -\frac{\partial z_j^l}{\partial w_{ij}^l} = a_i^{l-1}, -$$ - -and -$$ -\frac{\partial z_j^l}{\partial a_i^{l-1}} = w_{ji}^l. -$$ - -

-With our definition of the activation function we have that (note that this function depends only on \( z_j^l \)) -$$ -\frac{\partial a_j^l}{\partial z_j^{l}} = a_j^l(1-a_j^l)=f(z_j^l)(1-f(z_j^l)). -$$ - -

-









- -

Derivative of the cost function

- -

-With these definitions we can now compute the derivative of the cost function in terms of the weights. - -

-Let us specialize to the output layer \( l=L \). Our cost function is -$$ -{\cal C}(\hat{W^L}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2=\frac{1}{2}\sum_{i=1}^n\left(a_i^L - t_i\right)^2, -$$ - -The derivative of this function with respect to the weights is - -$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)\frac{\partial a_j^L}{\partial w_{jk}^{L}}, -$$ - -The last partial derivative can easily be computed and reads (by applying the chain rule) -$$ -\frac{\partial a_j^L}{\partial w_{jk}^{L}} = \frac{\partial a_j^L}{\partial z_{j}^{L}}\frac{\partial z_j^L}{\partial w_{jk}^{L}}=a_j^L(1-a_j^L)a_k^{L-1}, -$$ - -

-









- -

Bringing it together, first back propagation equation

- -

-We have thus -$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)a_j^L(1-a_j^L)a_k^{L-1}, -$$ - -

-Defining -$$ -\delta_j^L = a_j^L(1-a_j^L)\left(a_j^L - t_j\right) = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -$$ - -and using the Hadamard product of two vectors we can write this as -$$ -\hat{\delta}^L = f'(\hat{z}^L)\circ\frac{\partial {\cal C}}{\partial (\hat{a}L)}. -$$ - -

-This is an important expression. The second term on the right handside -measures how fast the cost function is changing as a function of the $j$th -output activation. If, for example, the cost function doesn't depend -much on a particular output node \( j \), then \( \delta_j^L \) will be small, -which is what we would expect. The first term on the right, measures -how fast the activation function \( f \) is changing at a given activation -value \( z_j^L \). - -

-Notice that everything in the above equations is easily computed. In -particular, we compute \( z_j^L \) while computing the behaviour of the -network, and it is only a small additional overhead to compute -\( f'(z^L_j) \). The exact form of the derivative with respect to the -output depends on the form of the cost function. -However, provided the cost function is known there should be little -trouble in calculating - -$$ -\frac{\partial {\cal C}}{\partial (a_j^L)} -$$ - -

-With the definition of \( \delta_j^L \) we have a more compact definition of the derivative of the cost function in terms of the weights, namely -$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}. -$$ - -

-









- -

Derivatives in terms of \( z_j^L \)

- -

-It is also easy to see that our previous equation can be written as - -$$ -\delta_j^L =\frac{\partial {\cal C}}{\partial z_j^L}= \frac{\partial {\cal C}}{\partial a_j^L}\frac{\partial a_j^L}{\partial z_j^L}, -$$ - -which can also be interpreted as the partial derivative of the cost function with respect to the biases \( b_j^L \), namely -$$ -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}\frac{\partial b_j^L}{\partial z_j^L}=\frac{\partial {\cal C}}{\partial b_j^L}, -$$ - -That is, the error \( \delta_j^L \) is exactly equal to the rate of change of the cost function as a function of the bias. -









- -

Bringing it together

- -

-We have now three equations that are essential for the computations of the derivatives of the cost function at the output layer. These equations are needed to start the algorithm and they are - -

-

-The starting equations. -

- -$$ -\begin{equation} -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}, -\label{_auto8} -\end{equation} -$$ - -and -$$ -\begin{equation} -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\label{_auto9} -\end{equation} -$$ - -and - -$$ -\begin{equation} -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}, -\label{_auto10} -\end{equation} -$$ -

- - -

-An interesting consequence of the above equations is that when the -activation \( a_k^{L-1} \) is small, the gradient term, that is the -derivative of the cost function with respect to the weights, will also -tend to be small. We say then that the weight learns slowly, meaning -that it changes slowly when we minimize the weights via say gradient -descent. In this case we say the system learns slowly. - -

-Another interesting feature is that is when the activation function, -represented by the sigmoid function here, is rather flat when we move towards -its end values \( 0 \) and \( 1 \) (see the above Python codes). In these -cases, the derivatives of the activation function will also be close -to zero, meaning again that the gradients will be small and the -network learns slowly again. - -

-We need a fourth equation and we are set. We are going to propagate -backwards in order to the determine the weights and biases. In order -to do so we need to represent the error in the layer before the final -one \( L-1 \) in terms of the errors in the final output layer. - -

-









- -

Final back propagating equation

- -

-We have that (replacing \( L \) with a general layer \( l \)) -$$ -\delta_j^l =\frac{\partial {\cal C}}{\partial z_j^l}. -$$ - -We want to express this in terms of the equations for layer \( l+1 \). Using the chain rule and summing over all \( k \) entries we have - -$$ -\delta_j^l =\sum_k \frac{\partial {\cal C}}{\partial z_k^{l+1}}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}=\sum_k \delta_k^{l+1}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}, -$$ - -and recalling that -$$ -z_j^{l+1} = \sum_{i=1}^{M_{l}}w_{ij}^{l+1}a_j^{l}+b_j^{l+1}, -$$ - -with \( M_l \) being the number of nodes in layer \( l \), we obtain -$$ -\delta_j^l =\sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l), -$$ - -This is our final equation. - -

-We are now ready to set up the algorithm for back propagation and learning the weights and biases. - -

-









- -

Setting up the Back propagation algorithm

- -

-The four equations provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm. - -

-

- -

-First, we set up the input data \( \hat{x} \) and the activations -\( \hat{z}_1 \) of the input layer and compute the activation function and -the pertinent outputs \( \hat{a}^1 \). -

- - -

-

- -

-Secondly, we perform then the feed forward till we reach the output -layer and compute all \( \hat{z}_l \) of the input layer and compute the -activation function and the pertinent outputs \( \hat{a}^l \) for -\( l=2,3,\dots,L \). -

- - -

-

- -

-Thereafter we compute the ouput error \( \hat{\delta}^L \) by computing all -$$ -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}. -$$ -

- - -

-

- -

-Then we compute the back propagate error for each \( l=L-1,L-2,\dots,2 \) as -$$ -\delta_j^l = \sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l). -$$ -

- - -

-

- -

-Finally, we update the weights and the biases using gradient descent for each \( l=L-1,L-2,\dots,2 \) and update the weights and biases according to the rules -$$ -w_{jk}^l\leftarrow = w_{jk}^l- \eta \delta_j^la_k^{l-1}, -$$ - - -$$ -b_j^l \leftarrow b_j^l-\eta \frac{\partial {\cal C}}{\partial b_j^l}=b_j^l-\eta \delta_j^l, -$$ -

- - -

-The parameter \( \eta \) is the learning parameter discussed in connection with the gradient descent methods. -Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training. - -

- - -

Setting up a Multi-layer perceptron model for classification

- -

-We are now gong to develop an example based on the MNIST data -base. This is a classification problem and we need to use our -cross-entropy function we discussed in connection with logistic -regression. The cross-entropy defines our cost function for the -classificaton problems with neural networks. - -

-In binary classification with two classes \( (0, 1) \) we define the -logistic/sigmoid function as the probability that a particular input -is in class \( 0 \) or \( 1 \). This is possible because the logistic -function takes any input from the real numbers and inputs a number -between 0 and 1, and can therefore be interpreted as a probability. It -also has other nice properties, such as a derivative that is simple to -calculate. - -

-For an input \( \boldsymbol{a} \) from the hidden layer, the probability that the input \( \boldsymbol{x} \) -is in class 0 or 1 is just. We let \( \theta \) represent the unknown weights and biases to be adjusted by our equations). The variable \( x \) -represents our activation values \( z \). We have -$$ -P(y = 0 \mid \hat{x}, \hat{\theta}) = \frac{1}{1 + \exp{(- \hat{x}})} , -$$ - -and -$$ -P(y = 1 \mid \hat{x}, \hat{\theta}) = 1 - P(y = 0 \mid \hat{x}, \hat{\theta}) , -$$ - -

-where \( y \in \{0, 1\} \) and \( \hat{\theta} \) represents the weights and biases -of our network. - -

-









- -

Defining the cost function

- -

-Our cost function is given as (see the Logistic regression lectures) -$$ -\mathcal{C}(\hat{\theta}) = - \ln P(\mathcal{D} \mid \hat{\theta}) = - \sum_{i=1}^n -y_i \ln[P(y_i = 0)] + (1 - y_i) \ln [1 - P(y_i = 0)] = \sum_{i=1}^n \mathcal{L}_i(\hat{\theta}) . -$$ - -

-This last equality means that we can interpret our cost function as a sum over the loss function -for each point in the dataset \( \mathcal{L}_i(\hat{\theta}) \). -The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather -than maximizing a negative number. - -

-In multiclass classification it is common to treat each integer label as a so called one-hot vector: - -

-\( y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) , \) and - -

-\( y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) , \) - -

-i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset (numbers from \( 0 \) to \( 9 \)).. - -

-If \( \hat{x}_i \) is the \( i \)-th input (image), \( y_{ic} \) refers to the \( c \)-th component of the \( i \)-th -output vector \( \hat{y}_i \). -The probability of \( \hat{x}_i \) being in class \( c \) will be given by the softmax function: - -$$ -P(y_{ic} = 1 \mid \hat{x}_i, \hat{\theta}) = \frac{\exp{((\hat{a}_i^{hidden})^T \hat{w}_c)}} -{\sum_{c'=0}^{C-1} \exp{((\hat{a}_i^{hidden})^T \hat{w}_{c'})}} , -$$ - -

-which reduces to the logistic function in the binary case. -The likelihood of this \( C \)-class classifier -is now given as: - -$$ -P(\mathcal{D} \mid \hat{\theta}) = \prod_{i=1}^n \prod_{c=0}^{C-1} [P(y_{ic} = 1)]^{y_{ic}} . -$$ - -Again we take the negative log-likelihood to define our cost function: - -$$ -\mathcal{C}(\hat{\theta}) = - \log{P(\mathcal{D} \mid \hat{\theta})}. -$$ - -See the logistic regression lectures for a full definition of the cost function. - -

-The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before! - -

-









- -

Example: binary classification problem

- -

-As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters \( \beta \) as -$$ -\mathcal{C}(\hat{\beta}) = - \sum_{i=1}^n \left(y_i\log{p(y_i \vert x_i,\hat{\beta})}+(i-y_i)\log{1-p(y_i \vert x_i,\hat{\beta})}\right), -$$ - -where we had defined the logistic (sigmoid) function -$$ -p(y_i =1\vert x_i,\hat{\beta})=\frac{\exp{(\beta_0+\beta_1 x_i)}}{1+\exp{(\beta_0+\beta_1 x_i)}}, -$$ - -and -$$ -p(y_i =0\vert x_i,\hat{\beta})=1-p(y_i =1\vert x_i,\hat{\beta}). -$$ - -The parameters \( \hat{\beta} \) were defined using a minimization method like gradient descent or Newton-Raphson's method. - -

-Now we replace \( x_i \) with the activation \( z_i^l \) for a given layer \( l \) and the outputs as \( y_i=a_i^l=f(z_i^l) \), with \( z_i^l \) now being a function of the weights \( w_{ij}^l \) and biases \( b_i^l \). -We have then -$$ -a_i^l = y_i = \frac{\exp{(z_i^l)}}{1+\exp{(z_i^l)}}, -$$ - -with -$$ -z_i^l = \sum_{j}w_{ij}^l a_j^{l-1}+b_i^l, -$$ - -where the superscript \( l-1 \) indicates that these are the outputs from layer \( l-1 \). -Our cost function at the final layer \( l=L \) is now -$$ -\mathcal{C}(\hat{W}) = - \sum_{i=1}^n \left(t_i\log{a_i^L}+(i-t_i)\log{(1-a_i^L)}\right), -$$ - -where we have defined the targets \( t_i \). The derivatives of the cost function with respect to the output \( a_i^L \) are then easily calculated and we get -$$ -\frac{\partial \mathcal{C}(\hat{W})}{\partial a_i^L} = \frac{a_i^L-t_i}{a_i^L(1-a_i^L)}. -$$ - -In case we use another activation function than the logistic one, we need to evaluate other derivatives. - -

-









- -

The Softmax function

-In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation \( z_i^l \), that is we need -$$ -\frac{\partial f(z_i^l)}{\partial w_{jk}^l} = -\frac{\partial f(z_i^l)}{\partial z_j^l} \frac{\partial z_j^l}{\partial w_{jk}^l}= \frac{\partial f(z_i^l)}{\partial z_j^l}a_k^{l-1}. -$$ - -For the Softmax function we have -$$ -f(z_i^l) = \frac{\exp{(z_i^l)}}{\sum_{m=1}^K\exp{(z_m^l)}}. -$$ - -Its derivative with respect to \( z_j^l \) gives -$$ -\frac{\partial f(z_i^l)}{\partial z_j^l}= f(z_i^l)\left(\delta_{ij}-f(z_j^l)\right), -$$ - -which in case of the simply binary model reduces to having \( i=j \). - -

- - -

Developing a code for doing neural networks with back propagation

- -

-One can identify a set of key steps when using neural networks to solve supervised learning problems: - -

    -
  1. Collect and pre-process data
  2. -
  3. Define model and architecture
  4. -
  5. Choose cost function and optimizer
  6. -
  7. Train the model
  8. -
  9. Evaluate model performance on test data
  10. -
  11. Adjust hyperparameters (if necessary, network architecture)
  12. -
- -









- -

Collect and pre-process data

- -

-Here we will be using the MNIST dataset, which is readily available through the scikit-learn -package. You may also find it for example here. -The MNIST (Modified National Institute of Standards and Technology) database is a large database -of handwritten digits that is commonly used for training various image processing systems. -The MNIST dataset consists of 70 000 images of size \( 28\times 28 \) pixels, each labeled from 0 to 9. -The scikit-learn dataset we will use consists of a selection of 1797 images of size \( 8\times 8 \) collected and processed from this database. - -

-To feed data into a feed-forward neural network we need to represent -the inputs as a feature matrix \( X = (n_{inputs}, n_{features}) \). Each -row represents an input, in this case a handwritten digit, and -each column represents a feature, in this case a pixel. The -correct answers, also known as labels or targets are -represented as a 1D array of integers -\( Y = (n_{inputs}) = (5, 3, 1, 8,...) \). - -

-As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from -measurements of height (in m) -and weight (in kg). If we have measurements of 5 people the feature matrix could be for example: - -$$ X = \begin{bmatrix} -1.85 & 81\\ -1.71 & 65\\ -1.95 & 103\\ -1.55 & 42\\ -1.63 & 56 -\end{bmatrix} ,$$ - -

-and the targets would be: - -$$ Y = (23.7, 22.2, 27.1, 17.5, 21.1) $$ - -

-Since each input image is a 2D matrix, we need to flatten the image -(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a -feature matrix. This means we lose all spatial information in the -image, such as locality and translational invariance. More complicated -architectures such as Convolutional Neural Networks can take advantage -of such information, and are most commonly applied when analyzing -images. - -

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# flatten the image
-# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
-n_inputs = len(inputs)
-inputs = inputs.reshape(n_inputs, -1)
-print("X = (n_inputs, n_features) = " + str(inputs.shape))
-
-
-# choose some random images to display
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-

-









- -

Train and test datasets

- -

-Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions. - -

-We will reserve \( 80 \% \) of our dataset for training and \( 20 \% \) for testing. - -

-It is important that the train and test datasets are drawn randomly from our dataset, to ensure -no bias in the sampling. -Say you are taking measurements of weather data to predict the weather in the coming 5 days. -You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data -collected from 12.00 to 24.00. - -

- - -

from sklearn.model_selection import train_test_split
-
-# one-liner from scikit-learn library
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-# equivalently in numpy
-def train_test_split_numpy(inputs, labels, train_size, test_size):
-    n_inputs = len(inputs)
-    inputs_shuffled = inputs.copy()
-    labels_shuffled = labels.copy()
-    
-    np.random.shuffle(inputs_shuffled)
-    np.random.shuffle(labels_shuffled)
-    
-    train_end = int(n_inputs*train_size)
-    X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:]
-    Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:]
-    
-    return X_train, X_test, Y_train, Y_test
-
-#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size)
-
-print("Number of training images: " + str(len(X_train)))
-print("Number of test images: " + str(len(X_test)))
-
-

-









- -

Define model and architecture

- -

-Our simple feed-forward neural network will consist of an input layer, a single hidden layer and an output layer. The activation \( y \) of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have - -$$ z = \sum_{i=1}^n w_i a_i ,$$ - -$$ y = f(z) ,$$ - -

-where \( f \) is the activation function, \( a_i \) represents input from neuron \( i \) in the preceding layer -and \( w_i \) is the weight to input \( i \). -The activation of the neurons in the input layer is just the features (e.g. a pixel value). - -

-The simplest activation function for a neuron is the Heaviside function: - -$$ f(z) = -\begin{cases} -1, & z > 0\\ -0, & \text{otherwise} -\end{cases} -$$ - -

-A feed-forward neural network with this activation is known as a perceptron. -For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer. -This activation can be generalized to \( k \) classes (using e.g. the one-against-all strategy), -and we call these architectures multiclass perceptrons. - -

-However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and -Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function. - -

-Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU). -We will be using the sigmoid function \( \sigma(x) \): - -$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$ - -

-which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions. - -

- - -

Layers

- -
    -
  • Input
  • -
- -Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons. - -
    -
  • Hidden layer
  • -
- -We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer. -Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer. - -
    -
  • Output
  • -
- -If we were building a binary classifier, it would be sufficient with a single neuron in the output layer, -which could output 0 or 1 according to the Heaviside function. This would be an example of a hard classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a soft classifier, which outputs the probability of being in class 0 or 1. - -

-For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class. - -

-Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons \( j = 0,1,...,9 \). The activation of each output neuron \( j \) will be according to the softmax function: - -$$ P(\text{class \( j \)} \mid \text{input \( \hat{a} \)}) = \frac{\exp{(\hat{a}^T \hat{w}_j)}} -{\sum_{c=0}^{9} \exp{(\hat{a}^T \hat{w}_c)}} ,$$ - -

-i.e. each neuron \( j \) outputs the probability of being in class \( j \) given an input from the hidden layer \( \hat{a} \), with \( \hat{w}_j \) the weights of neuron \( j \) to the inputs. -The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1. -The exponent is just the weighted sum of inputs as before: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$ - -

-Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500 -weights to the output layer. - -

- - -

Weights and biases

- -

-Typically weights are initialized with small values distributed around zero, drawn from a uniform -or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless. - -

-Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range -of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron \( j \), \( b_j \): - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$ - -

-The bias weights \( \hat{b} \) are often initialized to zero, but a small value like \( 0.01 \) ensures all neurons have some output which can be backpropagated in the first training cycle. -

- - -

# building our neural network
-
-n_inputs, n_features = X_train.shape
-n_hidden_neurons = 50
-n_categories = 10
-
-# we make the weights normally distributed using numpy.random.randn
-
-# weights and bias in the hidden layer
-hidden_weights = np.random.randn(n_features, n_hidden_neurons)
-hidden_bias = np.zeros(n_hidden_neurons) + 0.01
-
-# weights and bias in the output layer
-output_weights = np.random.randn(n_hidden_neurons, n_categories)
-output_bias = np.zeros(n_categories) + 0.01
-
-

-









- -

Feed-forward pass

- -

-Denote \( F \) the number of features, \( H \) the number of hidden neurons and \( C \) the number of categories. -For each input image we calculate a weighted sum of input features (pixel values) to each neuron \( j \) in the hidden layer \( l \): - -$$ z_{j}^{l} = \sum_{i=1}^{F} w_{ij}^{l} x_i + b_{j}^{l},$$ - -

-this is then passed through our activation function - -$$ a_{j}^{l} = f(z_{j}^{l}) .$$ - -

-We calculate a weighted sum of inputs (activations in the hidden layer) to each neuron \( j \) in the output layer: - -$$ z_{j}^{L} = \sum_{i=1}^{H} w_{ij}^{L} a_{i}^{l} + b_{j}^{L}.$$ - -

-Finally we calculate the output of neuron \( j \) in the output layer using the softmax function: - -$$ a_{j}^{L} = \frac{\exp{(z_j^{L})}} -{\sum_{c=0}^{C-1} \exp{(z_c^{L})}} .$$ - -

- - -

Matrix multiplications

- -

-Since our data has the dimensions \( X = (n_{inputs}, n_{features}) \) and our weights to the hidden -layer have the dimensions -\( W_{hidden} = (n_{features}, n_{hidden}) \), -we can easily feed the network all our training data in one go by taking the matrix product - -$$ X W^{h} = (n_{inputs}, n_{hidden}),$$ - -

-and obtain a matrix that holds the weighted sum of inputs to the hidden layer -for each input image and each hidden neuron. -We also add the bias to obtain a matrix of weighted sums to the hidden layer \( Z^{h} \): - -$$ \hat{z}^{l} = \hat{X} \hat{W}^{l} + \hat{b}^{l} ,$$ - -

-meaning the same bias (1D array with size equal number of hidden neurons) is added to each input image. -This is then passed through the activation: - -$$ \hat{a}^{l} = f(\hat{z}^l) .$$ - -

-This is fed to the output layer: - -$$ \hat{z}^{L} = \hat{a}^{L} \hat{W}^{L} + \hat{b}^{L} .$$ - -

-Finally we receive our output values for each image and each category by passing it through the softmax function: - -$$ output = softmax (\hat{z}^{L}) = (n_{inputs}, n_{categories}) .$$ - -

- - -

# setup the feed-forward pass, subscript h = hidden layer
-
-def sigmoid(x):
-    return 1/(1 + np.exp(-x))
-
-def feed_forward(X):
-    # weighted sum of inputs to the hidden layer
-    z_h = np.matmul(X, hidden_weights) + hidden_bias
-    # activation in the hidden layer
-    a_h = sigmoid(z_h)
-    
-    # weighted sum of inputs to the output layer
-    z_o = np.matmul(a_h, output_weights) + output_bias
-    # softmax output
-    # axis 0 holds each input and axis 1 the probabilities of each category
-    exp_term = np.exp(z_o)
-    probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-    
-    return probabilities
-
-probabilities = feed_forward(X_train)
-print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape))
-print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0]))
-print("probabilities sum up to: " + str(probabilities[0].sum()))
-print()
-
-# we obtain a prediction by taking the class with the highest likelihood
-def predict(X):
-    probabilities = feed_forward(X)
-    return np.argmax(probabilities, axis=1)
-
-predictions = predict(X_train)
-print("predictions = (n_inputs) = " + str(predictions.shape))
-print("prediction for image 0: " + str(predictions[0]))
-print("correct label for image 0: " + str(Y_train[0]))
-
-

-









- -

Choose cost function and optimizer

- -

-To measure how well our neural network is doing we need to introduce a cost function. -We will call the function that gives the error of a single sample output the loss function, and the function -that gives the total error of our network across all samples the cost function. -A typical choice for multiclass classification is the cross-entropy loss, also known as the negative log likelihood. - -

-In multiclass classification it is common to treat each integer label as a so called one-hot vector: - -$$ y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$$ - -$$ y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$$ - -

-i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset. - -

-Let \( y_{ic} \) denote the \( c \)-th component of the \( i \)-th one-hot vector. -We define the cost function \( \mathcal{C} \) as a sum over the cross-entropy loss for each point \( \hat{x}_i \) in the dataset. - -

-In the one-hot representation only one of the terms in the loss function is non-zero, namely the -probability of the correct category \( c' \) -(i.e. the category \( c' \) such that \( y_{ic'} = 1 \)). This means that the cross entropy loss only punishes you for how wrong -you got the correct label. The probability of category \( c \) is given by the softmax function. The vector \( \hat{\theta} \) represents the parameters of our network, i.e. all the weights and biases. - -

-









- -

Optimizing the cost function

- -

-The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is gradient descent and its generalizations. The idea behind gradient descent -is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a local minimum of the cost function. -Each parameter \( \theta \) is iteratively adjusted according to the rule - -$$ \theta_{i+1} = \theta_i - \eta \nabla \mathcal{C}(\theta_i) ,$$ - -

-where \( \eta \) is known as the learning rate, which controls how big a step we take towards the minimum. -This update can be repeated for any number of iterations, or until we are satisfied with the result. - -

-A simple and effective improvement is a variant called Batch Gradient Descent. -Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient -on a subset of the data called a minibatch. -If there are \( N \) data points and we have a minibatch size of \( M \), the total number of batches -is \( N/M \). -We denote each minibatch \( B_k \), with \( k = 1, 2,...,N/M \). The gradient then becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{M} \sum_{i \in B_k} \nabla \mathcal{L}_i(\theta) ,$$ - -

-i.e. instead of averaging the loss over the entire dataset, we average over a minibatch. - -

-This has two important benefits: - -

    -
  1. Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima.
  2. -
  3. It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient.
  4. -
- -The various optmization methods, with codes and algorithms, are discussed in our lectures on Gradient descent approaches. - -

- - -

Regularization

- -

-It is common to add an extra term to the cost function, proportional -to the size of the weights. This is equivalent to constraining the -size of the weights, so that they do not grow out of control. -Constraining the size of the weights means that the weights cannot -grow arbitrarily large to fit the training data, and in this way -reduces overfitting. - -

-We will measure the size of the weights using the so called L2-norm, meaning our cost function becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) + \lambda \lvert \lvert \hat{w} \rvert \rvert_2^2 -= \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}(\theta) + \lambda \sum_{ij} w_{ij}^2,$$ - -

-i.e. we sum up all the weights squared. The factor \( \lambda \) is known as a regularization parameter. - -

-In order to train the model, we need to calculate the derivative of -the cost function with respect to every bias and weight in the -network. In total our network has \( (64 + 1)\times 50=3250 \) weights in -the hidden layer and \( (50 + 1)\times 10=510 \) weights to the output -layer (\( +1 \) for the bias), and the gradient must be calculated for -every parameter. We use the backpropagation algorithm discussed -above. This is a clever use of the chain rule that allows us to -calculate the gradient efficently. - -

-









- -

Matrix multiplication

- -

-To more efficently train our network these equations are implemented using matrix operations. -The error in the output layer is calculated simply as, with \( \hat{t} \) being our targets, - -$$ \delta_L = \hat{t} - \hat{y} = (n_{inputs}, n_{categories}) .$$ - -

-The gradient for the output weights is calculated as - -$$ \nabla W_{L} = \hat{a}^T \delta_L = (n_{hidden}, n_{categories}) ,$$ - -

-where \( \hat{a} = (n_{inputs}, n_{hidden}) \). This simply means that we are summing up the gradients for each input. -Since we are going backwards we have to transpose the activation matrix. - -

-The gradient with respect to the output bias is then - -$$ \nabla \hat{b}_{L} = \sum_{i=1}^{n_{inputs}} \delta_L = (n_{categories}) .$$ - -

-The error in the hidden layer is - -$$ \Delta_h = \delta_L W_{L}^T \circ f'(z_{h}) = \delta_L W_{L}^T \circ a_{h} \circ (1 - a_{h}) = (n_{inputs}, n_{hidden}) ,$$ - -

-where \( f'(a_{h}) \) is the derivative of the activation in the hidden layer. The matrix products mean -that we are summing up the products for each neuron in the output layer. The symbol \( \circ \) denotes -the Hadamard product, meaning element-wise multiplication. - -

-This again gives us the gradients in the hidden layer: - -$$ \nabla W_{h} = X^T \delta_h = (n_{features}, n_{hidden}) ,$$ - -$$ \nabla b_{h} = \sum_{i=1}^{n_{inputs}} \delta_h = (n_{hidden}) .$$ - -

- - -

# to categorical turns our integer vector into a onehot representation
-from sklearn.metrics import accuracy_score
-
-# one-hot in numpy
-def to_categorical_numpy(integer_vector):
-    n_inputs = len(integer_vector)
-    n_categories = np.max(integer_vector) + 1
-    onehot_vector = np.zeros((n_inputs, n_categories))
-    onehot_vector[range(n_inputs), integer_vector] = 1
-    
-    return onehot_vector
-
-#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test)
-Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test)
-
-def feed_forward_train(X):
-    # weighted sum of inputs to the hidden layer
-    z_h = np.matmul(X, hidden_weights) + hidden_bias
-    # activation in the hidden layer
-    a_h = sigmoid(z_h)
-    
-    # weighted sum of inputs to the output layer
-    z_o = np.matmul(a_h, output_weights) + output_bias
-    # softmax output
-    # axis 0 holds each input and axis 1 the probabilities of each category
-    exp_term = np.exp(z_o)
-    probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-    
-    # for backpropagation need activations in hidden and output layers
-    return a_h, probabilities
-
-def backpropagation(X, Y):
-    a_h, probabilities = feed_forward_train(X)
-    
-    # error in the output layer
-    error_output = probabilities - Y
-    # error in the hidden layer
-    error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h)
-    
-    # gradients for the output layer
-    output_weights_gradient = np.matmul(a_h.T, error_output)
-    output_bias_gradient = np.sum(error_output, axis=0)
-    
-    # gradient for the hidden layer
-    hidden_weights_gradient = np.matmul(X.T, error_hidden)
-    hidden_bias_gradient = np.sum(error_hidden, axis=0)
-
-    return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient
-
-print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
-
-eta = 0.01
-lmbd = 0.01
-for i in range(1000):
-    # calculate gradients
-    dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot)
-    
-    # regularization term gradients
-    dWo += lmbd * output_weights
-    dWh += lmbd * hidden_weights
-    
-    # update weights and biases
-    output_weights -= eta * dWo
-    output_bias -= eta * dBo
-    hidden_weights -= eta * dWh
-    hidden_bias -= eta * dBh
-
-print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
-
-

-









- -

Improving performance

- -

-As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image. -In order to obtain a network that does something useful, we will have to do a bit more work. - -

-The choice of hyperparameters such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a grid-search is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates \( \eta = 10^{-6}, 10^{-5},...,10^{-1} \) with different regularization parameters \( \lambda = 10^{-6},...,10^{-0} \). - -

-Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an iteration, and a full training period -going through the entire dataset (\( n/M \) batches) an epoch. - -

-If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers. -Andrew Ng goes through some of these considerations in this video. You can find a summary of the video here. - -

-









- -

Full object-oriented implementation

- -

-It is very natural to think of the network as an object, with specific instances of the network -being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below. - -

- - -

class NeuralNetwork:
-    def __init__(
-        self,
-        X_data,
-        Y_data,
-        n_hidden_neurons=50,
-        n_categories=10,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-
-    ):
-        self.X_data_full = X_data
-        self.Y_data_full = Y_data
-
-        self.n_inputs = X_data.shape[0]
-        self.n_features = X_data.shape[1]
-        self.n_hidden_neurons = n_hidden_neurons
-        self.n_categories = n_categories
-
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-
-        self.create_biases_and_weights()
-
-    def create_biases_and_weights(self):
-        self.hidden_weights = np.random.randn(self.n_features, self.n_hidden_neurons)
-        self.hidden_bias = np.zeros(self.n_hidden_neurons) + 0.01
-
-        self.output_weights = np.random.randn(self.n_hidden_neurons, self.n_categories)
-        self.output_bias = np.zeros(self.n_categories) + 0.01
-
-    def feed_forward(self):
-        # feed-forward for training
-        self.z_h = np.matmul(self.X_data, self.hidden_weights) + self.hidden_bias
-        self.a_h = sigmoid(self.z_h)
-
-        self.z_o = np.matmul(self.a_h, self.output_weights) + self.output_bias
-
-        exp_term = np.exp(self.z_o)
-        self.probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-
-    def feed_forward_out(self, X):
-        # feed-forward for output
-        z_h = np.matmul(X, self.hidden_weights) + self.hidden_bias
-        a_h = sigmoid(z_h)
-
-        z_o = np.matmul(a_h, self.output_weights) + self.output_bias
-        
-        exp_term = np.exp(z_o)
-        probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-        return probabilities
-
-    def backpropagation(self):
-        error_output = self.probabilities - self.Y_data
-        error_hidden = np.matmul(error_output, self.output_weights.T) * self.a_h * (1 - self.a_h)
-
-        self.output_weights_gradient = np.matmul(self.a_h.T, error_output)
-        self.output_bias_gradient = np.sum(error_output, axis=0)
-
-        self.hidden_weights_gradient = np.matmul(self.X_data.T, error_hidden)
-        self.hidden_bias_gradient = np.sum(error_hidden, axis=0)
-
-        if self.lmbd > 0.0:
-            self.output_weights_gradient += self.lmbd * self.output_weights
-            self.hidden_weights_gradient += self.lmbd * self.hidden_weights
-
-        self.output_weights -= self.eta * self.output_weights_gradient
-        self.output_bias -= self.eta * self.output_bias_gradient
-        self.hidden_weights -= self.eta * self.hidden_weights_gradient
-        self.hidden_bias -= self.eta * self.hidden_bias_gradient
-
-    def predict(self, X):
-        probabilities = self.feed_forward_out(X)
-        return np.argmax(probabilities, axis=1)
-
-    def predict_probabilities(self, X):
-        probabilities = self.feed_forward_out(X)
-        return probabilities
-
-    def train(self):
-        data_indices = np.arange(self.n_inputs)
-
-        for i in range(self.epochs):
-            for j in range(self.iterations):
-                # pick datapoints with replacement
-                chosen_datapoints = np.random.choice(
-                    data_indices, size=self.batch_size, replace=False
-                )
-
-                # minibatch training data
-                self.X_data = self.X_data_full[chosen_datapoints]
-                self.Y_data = self.Y_data_full[chosen_datapoints]
-
-                self.feed_forward()
-                self.backpropagation()
-
-

-









- -

Evaluate model performance on test data

- -

-To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data. -We measure the performance of the network using the accuracy score. -The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of \( 1 \). - -$$ \text{Accuracy} = \frac{\sum_{i=1}^n I(\hat{y}_i = y_i)}{n} ,$$ - -

-where \( I \) is the indicator function, \( 1 \) if \( \hat{y}_i = y_i \) and \( 0 \) otherwise. - -

- - -

epochs = 100
-batch_size = 100
-
-dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
-                    n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
-dnn.train()
-test_predict = dnn.predict(X_test)
-
-# accuracy score from scikit library
-print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
-
-# equivalent in numpy
-def accuracy_score_numpy(Y_test, Y_pred):
-    return np.sum(Y_test == Y_pred) / len(Y_test)
-
-#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict))
-
-

-









- -

Adjust hyperparameters

- -

-We now perform a grid search to find the optimal hyperparameters for the network. -Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around \( 98\% \) (\( 2\% \) error rate). - -

- - -

eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-# store the models for later use
-DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-
-# grid search
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
-                            n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
-        dnn.train()
-        
-        DNN_numpy[i][j] = dnn
-        
-        test_predict = dnn.predict(X_test)
-        
-        print("Learning rate  = ", eta)
-        print("Lambda = ", lmbd)
-        print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
-        print()
-
-

-









- -

Visualization

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, you can also do this with matplotlib imshow
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        dnn = DNN_numpy[i][j]
-        
-        train_pred = dnn.predict(X_train) 
-        test_pred = dnn.predict(X_test)
-
-        train_accuracy[i][j] = accuracy_score(Y_train, train_pred)
-        test_accuracy[i][j] = accuracy_score(Y_test, test_pred)
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

-









- -

scikit-learn implementation

- -

-scikit-learn focuses more -on traditional machine learning methods, such as regression, -clustering, decision trees, etc. As such, it has only two types of -neural networks: Multi Layer Perceptron outputting continuous values, -MPLRegressor, and Multi Layer Perceptron outputting labels, -MLPClassifier. We will see how simple it is to use these classes. - -

-scikit-learn implements a few improvements from our neural network, -such as early stopping, a varying learning rate, different -optimization methods, etc. We would therefore expect a better -performance overall. - -

- - -

from sklearn.neural_network import MLPClassifier
-# store models for later use
-DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',
-                            alpha=lmbd, learning_rate_init=eta, max_iter=epochs)
-        dnn.fit(X_train, Y_train)
-        
-        DNN_scikit[i][j] = dnn
-        
-        print("Learning rate  = ", eta)
-        print("Lambda = ", lmbd)
-        print("Accuracy score on test set: ", dnn.score(X_test, Y_test))
-        print()
-
-

-









- -

Visualization

-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        dnn = DNN_scikit[i][j]
-        
-        train_pred = dnn.predict(X_train) 
-        test_pred = dnn.predict(X_test)
-
-        train_accuracy[i][j] = accuracy_score(Y_train, train_pred)
-        test_accuracy[i][j] = accuracy_score(Y_test, test_pred)
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

-









- -

Building neural networks in Tensorflow and Keras

- -

-Now we want to build on the experience gained from our neural network implementation in NumPy and scikit-learn -and use it to construct a neural network in Tensorflow. Once we have constructed a neural network in NumPy -and Tensorflow, building one in Keras is really quite trivial, though the performance may suffer. - -

-In our previous example we used only one hidden layer, and in this we will use two. From this it should be quite -clear how to build one using an arbitrary number of hidden layers, using data structures such as Python lists or -NumPy arrays. - -

-









- -

Tensorflow

- -

-Tensorflow is an open source library machine learning library -developed by the Google Brain team for internal use. It was released -under the Apache 2.0 open source license in November 9, 2015. - -

-Tensorflow is a computational framework that allows you to construct -machine learning models at different levels of abstraction, from -high-level, object-oriented APIs like Keras, down to the C++ kernels -that Tensorflow is built upon. The higher levels of abstraction are -simpler to use, but less flexible, and our choice of implementation -should reflect the problems we are trying to solve. - -

-Tensorflow uses so-called graphs to represent your computation -in terms of the dependencies between individual operations, such that you first build a Tensorflow graph -to represent your model, and then create a Tensorflow session to run the graph. - -

-In this guide we will analyze the same data as we did in our NumPy and -scikit-learn tutorial, gathered from the MNIST database of images. We -will give an introduction to the lower level Python Application -Program Interfaces (APIs), and see how we use them to build our graph. -Then we will build (effectively) the same graph in Keras, to see just -how simple solving a machine learning problem can be. - -

-To install tensorflow on Unix/Linux systems, use pip as -

- - -

pip3 install tensorflow
-
-

-and/or if you use anaconda, just write (or install from the graphical user interface) -

- - -

conda install tensorflow
-
-

-









- -

Collect and pre-process data

- -

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# flatten the image
-# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
-n_inputs = len(inputs)
-inputs = inputs.reshape(n_inputs, -1)
-print("X = (n_inputs, n_features) = " + str(inputs.shape))
-
-
-# choose some random images to display
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-

- - -

from keras.utils import to_categorical
-from sklearn.model_selection import train_test_split
-
-# one-hot representation of labels
-labels = to_categorical(labels)
-
-# split into train and test data
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-

-









- -

Using TensorFlow backend

- -
    -
  1. Define model and architecture
  2. -
  3. Choose cost function and optimizer
  4. -
- -

- - -

import tensorflow as tf
-
-class NeuralNetworkTensorflow:
-    def __init__(
-        self,
-        X_train,
-        Y_train,
-        X_test,
-        Y_test,
-        n_neurons_layer1=100,
-        n_neurons_layer2=50,
-        n_categories=2,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-    ):
-        
-        # keep track of number of steps
-        self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')
-        
-        self.X_train = X_train
-        self.Y_train = Y_train
-        self.X_test = X_test
-        self.Y_test = Y_test
-        
-        self.n_inputs = X_train.shape[0]
-        self.n_features = X_train.shape[1]
-        self.n_neurons_layer1 = n_neurons_layer1
-        self.n_neurons_layer2 = n_neurons_layer2
-        self.n_categories = n_categories
-        
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-        
-        # build network piece by piece
-        # name scopes (with) are used to enforce creation of new variables
-        # https://www.tensorflow.org/guide/variables
-        self.create_placeholders()
-        self.create_DNN()
-        self.create_loss()
-        self.create_optimiser()
-        self.create_accuracy()
-    
-    def create_placeholders(self):
-        # placeholders are fine here, but "Datasets" are the preferred method
-        # of streaming data into a model
-        with tf.name_scope('data'):
-            self.X = tf.placeholder(tf.float32, shape=(None, self.n_features), name='X_data')
-            self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')
-    
-    def create_DNN(self):
-        with tf.name_scope('DNN'):
-            # the weights are stored to calculate regularization loss later
-            
-            # Fully connected layer 1
-            self.W_fc1 = self.weight_variable([self.n_features, self.n_neurons_layer1], name='fc1', dtype=tf.float32)
-            b_fc1 = self.bias_variable([self.n_neurons_layer1], name='fc1', dtype=tf.float32)
-            a_fc1 = tf.nn.sigmoid(tf.matmul(self.X, self.W_fc1) + b_fc1)
-            
-            # Fully connected layer 2
-            self.W_fc2 = self.weight_variable([self.n_neurons_layer1, self.n_neurons_layer2], name='fc2', dtype=tf.float32)
-            b_fc2 = self.bias_variable([self.n_neurons_layer2], name='fc2', dtype=tf.float32)
-            a_fc2 = tf.nn.sigmoid(tf.matmul(a_fc1, self.W_fc2) + b_fc2)
-            
-            # Output layer
-            self.W_out = self.weight_variable([self.n_neurons_layer2, self.n_categories], name='out', dtype=tf.float32)
-            b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)
-            self.z_out = tf.matmul(a_fc2, self.W_out) + b_out
-    
-    def create_loss(self):
-        with tf.name_scope('loss'):
-            softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))
-            
-            regularizer_loss_fc1 = tf.nn.l2_loss(self.W_fc1)
-            regularizer_loss_fc2 = tf.nn.l2_loss(self.W_fc2)
-            regularizer_loss_out = tf.nn.l2_loss(self.W_out)
-            regularizer_loss = self.lmbd*(regularizer_loss_fc1 + regularizer_loss_fc2 + regularizer_loss_out)
-            
-            self.loss = softmax_loss + regularizer_loss
-
-    def create_accuracy(self):
-        with tf.name_scope('accuracy'):
-            probabilities = tf.nn.softmax(self.z_out)
-            predictions = tf.argmax(probabilities, axis=1)
-            labels = tf.argmax(self.Y, axis=1)
-            
-            correct_predictions = tf.equal(predictions, labels)
-            correct_predictions = tf.cast(correct_predictions, tf.float32)
-            self.accuracy = tf.reduce_mean(correct_predictions)
-    
-    def create_optimiser(self):
-        with tf.name_scope('optimizer'):
-            self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)
-            
-    def weight_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.truncated_normal(shape, stddev=0.1)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def bias_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.constant(0.1, shape=shape)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def fit(self):
-        data_indices = np.arange(self.n_inputs)
-
-        with tf.Session() as sess:
-            sess.run(tf.global_variables_initializer())
-            for i in range(self.epochs):
-                for j in range(self.iterations):
-                    chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)
-                    batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]
-            
-                    sess.run([DNN.loss, DNN.optimizer],
-                        feed_dict={DNN.X: batch_X,
-                                   DNN.Y: batch_Y})
-                    accuracy = sess.run(DNN.accuracy,
-                        feed_dict={DNN.X: batch_X,
-                                   DNN.Y: batch_Y})
-                    step = sess.run(DNN.global_step)
-    
-            self.train_loss, self.train_accuracy = sess.run([DNN.loss, DNN.accuracy],
-                feed_dict={DNN.X: self.X_train,
-                           DNN.Y: self.Y_train})
-        
-            self.test_loss, self.test_accuracy = sess.run([DNN.loss, DNN.accuracy],
-                feed_dict={DNN.X: self.X_test,
-                           DNN.Y: self.Y_test})
-
-

-









- -

Optimizing and using gradient descent

- -

- - -

epochs = 100
-batch_size = 100
-n_neurons_layer1 = 100
-n_neurons_layer2 = 50
-n_categories = 10
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-
-

- - -

DNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        DNN = NeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,
-                                      n_neurons_layer1, n_neurons_layer2, n_categories,
-                                      epochs=epochs, batch_size=batch_size, eta=eta, lmbd=lmbd)
-        DNN.fit()
-        
-        DNN_tf[i][j] = DNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % DNN.test_accuracy)
-        print()
-
-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        DNN = DNN_tf[i][j]
-
-        train_accuracy[i][j] = DNN.train_accuracy
-        test_accuracy[i][j] = DNN.test_accuracy
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

- - -

# optional
-# we can use log files to visualize our graph in Tensorboard
-writer = tf.summary.FileWriter('logs/')
-writer.add_graph(tf.get_default_graph())
-
-

-









- -

Using Keras

- -

-Keras is a high level neural network -that supports Tensorflow, CTNK and Theano as backends. -If you have Tensorflow installed Keras is available through the tf.keras module. -If you have Anaconda installed you may run the following command -

- - -

conda install keras
-
-

-Alternatively, if you have Tensorflow or one of the other supported backends install you may use the pip package manager: - -

- - -

pip3 install keras
-
-

-or look up the instructions here. - -

- - -

from keras.models import Sequential
-from keras.layers import Dense
-from keras.regularizers import l2
-from keras.optimizers import SGD
-
-def create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, eta, lmbd):
-    model = Sequential()
-    model.add(Dense(n_neurons_layer1, activation='sigmoid', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_neurons_layer2, activation='sigmoid', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_categories, activation='softmax'))
-    
-    sgd = SGD(lr=eta)
-    model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
-    
-    return model
-
-

- - -

DNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        DNN = create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories,
-                                         eta=eta, lmbd=lmbd)
-        DNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)
-        scores = DNN.evaluate(X_test, Y_test)
-        
-        DNN_keras[i][j] = DNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % scores[1])
-        print()
-
-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        DNN = DNN_keras[i][j]
-
-        train_accuracy[i][j] = DNN.evaluate(X_train, Y_train)[1]
-        test_accuracy[i][j] = DNN.evaluate(X_test, Y_test)[1]
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

- - -

Which activation function should I use?

- -

-The Back propagation algorithm we derived above works by going from -the output layer to the input layer, propagating the error gradient on -the way. Once the algorithm has computed the gradient of the cost -function with regards to each parameter in the network, it uses these -gradients to update each parameter with a Gradient Descent (GD) step. - -

-Unfortunately for us, the gradients often get smaller and smaller as the -algorithm progresses down to the first hidden layers. As a result, the -GD update leaves the lower layer connection weights -virtually unchanged, and training never converges to a good -solution. This is known in the literature as -the vanishing gradients problem. - -

-In other cases, the opposite can happen, namely the the gradients can grow bigger and -bigger. The result is that many of the layers get large updates of the -weights the -algorithm diverges. This is the exploding gradients problem, which is -mostly encountered in recurrent neural networks. More generally, deep -neural networks suffer from unstable gradients, different layers may -learn at widely different speeds - -

- - -

Is the Logistic activation function (Sigmoid) our choice?

- -

-Although this unfortunate behavior has been empirically observed for -quite a while (it was one of the reasons why deep neural networks were -mostly abandoned for a long time), it is only around 2010 that -significant progress was made in understanding it. - -

-A paper titled Understanding the Difficulty of Training Deep -Feedforward Neural Networks by Xavier Glorot and Yoshua Bengio found that -the problems with the popular logistic -sigmoid activation function and the weight initialization technique -that was most popular at the time, namely random initialization using -a normal distribution with a mean of 0 and a standard deviation of -1. - -

-They showed that with this activation function and this -initialization scheme, the variance of the outputs of each layer is -much greater than the variance of its inputs. Going forward in the -network, the variance keeps increasing after each layer until the -activation function saturates at the top layers. This is actually made -worse by the fact that the logistic function has a mean of 0.5, not 0 -(the hyperbolic tangent function has a mean of 0 and behaves slightly -better than the logistic function in deep networks). - -

-









- -

The derivative of the Logistic funtion

- -

-Looking at the logistic activation function, when inputs become large -(negative or positive), the function saturates at 0 or 1, with a -derivative extremely close to 0. Thus when backpropagation kicks in, -it has virtually no gradient to propagate back through the network, -and what little gradient exists keeps getting diluted as -backpropagation progresses down through the top layers, so there is -really nothing left for the lower layers. - -

-In their paper, Glorot and Bengio propose a way to significantly -alleviate this problem. We need the signal to flow properly in both -directions: in the forward direction when making predictions, and in -the reverse direction when backpropagating gradients. We don’t want -the signal to die out, nor do we want it to explode and saturate. For -the signal to flow properly, the authors argue that we need the -variance of the outputs of each layer to be equal to the variance of -its inputs, and we also need the gradients to have equal variance -before and after flowing through a layer in the reverse direction. - -

-One of the insights in the 2010 paper by Glorot and Bengio was that -the vanishing/exploding gradients problems were in part due to a poor -choice of activation function. Until then most people had assumed that -if Nature had chosen to use roughly sigmoid activation functions in -biological neurons, they must be an excellent choice. But it turns out -that other activation functions behave much better in deep neural -networks, in particular the ReLU activation function, mostly because -it does not saturate for positive values (and also because it is quite -fast to compute). - -

-









- -

The RELU function family

- -

-The ReLU activation function suffers from a problem known as the dying -ReLUs: during training, some neurons effectively die, meaning they -stop outputting anything other than 0. - -

-In some cases, you may find that half of your network’s neurons are -dead, especially if you used a large learning rate. During training, -if a neuron’s weights get updated such that the weighted sum of the -neuron’s inputs is negative, it will start outputting 0. When this -happen, the neuron is unlikely to come back to life since the gradient -of the ReLU function is 0 when its input is negative. - -

-To solve this problem, nowadays practitioners use a variant of the ReLU -function, such as the leaky ReLU discussed above or the so-called -exponential linear unit (ELU) function - -$$ -ELU(z) = \left\{\begin{array}{cc} \alpha\left( \exp{(z)}-1\right) & z < 0,\\ z & z \ge 0.\end{array}\right. -$$ - -

-









- -

Which activation function should we use?

- -

-In general it seems that the ELU activation function is better than -the leaky ReLU function (and its variants), which is better than -ReLU. ReLU performs better than \( \tanh \) which in turn performs better -than the logistic function. - -

-If runtime -performance is an issue, then you may opt for the leaky ReLU function over the -ELU function If you don’t -want to tweak yet another hyperparameter, you may just use the default -\( \alpha \) of \( 0.01 \) for the leaky ReLU, and \( 1 \) for ELU. If you have -spare time and computing power, you can use cross-validation or -bootstrap to evaluate other activation functions. - -

- - -

A top-down perspective on Neural networks

- -

-The first thing we would like to do is divide the data into two or three -parts. A training set, a validation or dev (development) set, and a -test set. The test set is the data on which we want to make -predictions. The dev set is a subset of the training data we use to -check how well we are doing out-of-sample, after training the model on -the training dataset. We use the validation error as a proxy for the -test error in order to make tweaks to our model. It is crucial that we -do not use any of the test data to train the algorithm. This is a -cardinal sin in ML. Then: - -

    -
  • Estimate optimal error rate
  • -
  • Minimize underfitting (bias) on training data set.
  • -
  • Make sure you are not overfitting.
  • -
- -If the validation and test sets are drawn from the same distributions, -then a good performance on the validation set should lead to similarly -good performance on the test set. - -

-However, sometimes -the training data and test data differ in subtle ways because, for -example, they are collected using slightly different methods, or -because it is cheaper to collect data in one way versus another. In -this case, there can be a mismatch between the training and test -data. This can lead to the neural network overfitting these small -differences between the test and training sets, and a poor performance -on the test set despite having a good performance on the validation -set. To rectify this, Andrew Ng suggests making two validation or dev -sets, one constructed from the training data and one constructed from -the test data. The difference between the performance of the algorithm -on these two validation sets quantifies the train-test mismatch. This -can serve as another important diagnostic when using DNNs for -supervised learning. - -

-









- -

Limitations of supervised learning with deep networks

- -

-Like all statistical methods, supervised learning using neural -networks has important limitations. This is especially important when -one seeks to apply these methods, especially to physics problems. Like -all tools, DNNs are not a universal solution. Often, the same or -better performance on a task can be achieved by using a few -hand-engineered features (or even a collection of random -features). - -

-Here we list some of the important limitations of supervised neural network based models. - -

    -
  • Need labeled data. All supervised learning methods, DNNs for supervised learning require labeled data. Often, labeled data is harder to acquire than unlabeled data (e.g. one must pay for human experts to label images).
  • -
  • Supervised neural networks are extremely data intensive. DNNs are data hungry. They perform best when data is plentiful. This is doubly so for supervised methods where the data must also be labeled. The utility of DNNs is extremely limited if data is hard to acquire or the datasets are small (hundreds to a few thousand samples). In this case, the performance of other methods that utilize hand-engineered features can exceed that of DNNs.
  • -
  • Homogeneous data. Almost all DNNs deal with homogeneous data of one type. It is very hard to design architectures that mix and match data types (i.e. some continuous variables, some discrete variables, some time series). In applications beyond images, video, and language, this is often what is required. In contrast, ensemble models like random forests or gradient-boosted trees have no difficulty handling mixed data types.
  • -
  • Many problems are not about prediction. In natural science we are often interested in learning something about the underlying distribution that generates the data. In this case, it is often difficult to cast these ideas in a supervised learning setting. While the problems are related, it is possible to make good predictions with a wrong model. The model might or might not be useful for understanding the underlying science.
  • -
- -Some of these remarks are particular to DNNs, others are shared by all supervised learning methods. This motivates the use of unsupervised methods which in part circumvent these problems. - -

-









- -

Convolutional Neural Networks (recognizing images)

- -

-Convolutional Neural Networks (CNN) are very similar to ordinary Neural Networks. - -

-They are made up of neurons that have learnable weights and -biases. Each neuron receives some inputs, performs a dot product and -optionally follows it with a non-linearity. The whole network still -expresses a single differentiable score function: from the raw image -pixels on one end to class scores at the other. And they still have a -loss function (for example Softmax) on the last (fully-connected) layer -and all the tips/tricks we developed for learning regular Neural -Networks still apply (back propagation, gradient descent etc etc). - -

-What is the difference? CNN architectures make the explicit assumption that -the inputs are images, which allows us to encode certain properties -into the architecture. These then make the forward function more -efficient to implement and vastly reduce the amount of parameters in -the network. - -

-Here we provide only a superficial overview, for the more interested, we recommend highly the course -IN5400 – Machine Learning for Image Analysis -and the slides of CS231. - -

-









- -

Regular NNs don’t scale well to full images

- -

-As an example, consider -an image of size \( 32\times 32\times 3 \) (32 wide, 32 high, 3 color channels), so a -single fully-connected neuron in a first hidden layer of a regular -Neural Network would have \( 32\times 32\times 3 = 3072 \) weights. This amount still -seems manageable, but clearly this fully-connected structure does not -scale to larger images. For example, an image of more respectable -size, say \( 200\times 200\times 3 \), would lead to neurons that have -\( 200\times 200\times 3 = 120,000 \) weights. - -

-We could have -several such neurons, and the parameters would add up quickly! Clearly, -this full connectivity is wasteful and the huge number of parameters -would quickly lead to possible overfitting. - -

-

-
-

Figure 1: A regular 3-layer Neural Network.

-

-
- -

-









- -

3D volumes of neurons

- -

-Convolutional Neural Networks take advantage of the fact that the -input consists of images and they constrain the architecture in a more -sensible way. - -

-In particular, unlike a regular Neural Network, the -layers of a CNN have neurons arranged in 3 dimensions: width, -height, depth. (Note that the word depth here refers to the third -dimension of an activation volume, not to the depth of a full Neural -Network, which can refer to the total number of layers in a network.) - -

-To understand it better, the above example of an image -with an input volume of -activations has dimensions \( 32\times 32\times 3 \) (width, height, -depth respectively). - -

-The neurons in a layer will -only be connected to a small region of the layer before it, instead of -all of the neurons in a fully-connected manner. Moreover, the final -output layer could for this specific image have dimensions \( 1\times 1 times 10 \), -because by the -end of the CNN architecture we will reduce the full image into a -single vector of class scores, arranged along the depth -dimension. - -

-

-
-

Figure 2: A CNN arranges its neurons in three dimensions (width, height, depth), as visualized in one of the layers. Every layer of a CNN transforms the 3D input volume to a 3D output volume of neuron activations. In this example, the red input layer holds the image, so its width and height would be the dimensions of the image, and the depth would be 3 (Red, Green, Blue channels).

-

-
- -

- - -

Layers used to build CNNs

- -

-A simple CNN is a sequence of layers, and every layer of a CNN -transforms one volume of activations to another through a -differentiable function. We use three main types of layers to build -CNN architectures: Convolutional Layer, Pooling Layer, and -Fully-Connected Layer (exactly as seen in regular Neural Networks). We -will stack these layers to form a full CNN architecture. - -

-A simple CNN for image classification could have the architecture: - -

    -
  • INPUT (\( 32\times 32 \times 3 \)) will hold the raw pixel values of the image, in this case an image of width 32, height 32, and with three color channels R,G,B.
  • -
  • CONV (convolutional )layer will compute the output of neurons that are connected to local regions in the input, each computing a dot product between their weights and a small region they are connected to in the input volume. This may result in volume such as \( [32\times 32\times 12] \) if we decided to use 12 filters.
  • -
  • RELU layer will apply an elementwise activation function, such as the \( max(0,x) \) thresholding at zero. This leaves the size of the volume unchanged (\( [32\times 32\times 12] \)).
  • -
  • POOL (pooling) layer will perform a downsampling operation along the spatial dimensions (width, height), resulting in volume such as \( [16\times 16\times 12] \).
  • -
  • FC (i.e. fully-connected) layer will compute the class scores, resulting in volume of size \( [1\times 1\times 10] \), where each of the 10 numbers correspond to a class score, such as among the 10 categories of the MNIST images we considered above . As with ordinary Neural Networks and as the name implies, each neuron in this layer will be connected to all the numbers in the previous volume.
  • -
- -









- -

Transforming images

- -

-CNNs transform the original image layer by layer from the original -pixel values to the final class scores. - -

-Observe that some layers contain -parameters and other don’t. In particular, the CNN layers perform -transformations that are a function of not only the activations in the -input volume, but also of the parameters (the weights and biases of -the neurons). On the other hand, the RELU/POOL layers will implement a -fixed function. The parameters in the CONV/FC layers will be trained -with gradient descent so that the class scores that the CNN computes -are consistent with the labels in the training set for each image. - -

-









- -

CNNs in brief

- -

-In summary: - -

    -
  • A CNN architecture is in the simplest case a list of Layers that transform the image volume into an output volume (e.g. holding the class scores)
  • -
  • There are a few distinct types of Layers (e.g. CONV/FC/RELU/POOL are by far the most popular)
  • -
  • Each Layer accepts an input 3D volume and transforms it to an output 3D volume through a differentiable function
  • -
  • Each Layer may or may not have parameters (e.g. CONV/FC do, RELU/POOL don’t)
  • -
  • Each Layer may or may not have additional hyperparameters (e.g. CONV/FC/POOL do, RELU doesn’t)
  • -
- -For more material on convolutional networks, we strongly recommend -the course -IN5400 – Machine Learning for Image Analysis -and the slides of CS231 which is taught at Stanford University (consistently ranked as one of the top computer science programs in the world). Michael Nielsen's book is a must read, in particular chapter 6 which deals with CNNs. - -

-









- -

CNNs in more detail, building convolutional neural networks in Tensorflow and Keras

- -

-As discussed above, CNNs are neural networks built from the assumption that the inputs -to the network are 2D images. This is important because the number of features or pixels in images -grows very fast with the image size, and an enormous number of weights and biases are needed in order to build an accurate network. - -

-As before, we still have our input, a hidden layer and an output. What's novel about convolutional networks -are the convolutional and pooling layers stacked in pairs between the input and the hidden layer. -In addition, the data is no longer represented as a 2D feature matrix, instead each input is a number of 2D -matrices, typically 1 for each color dimension (Red, Green, Blue). - -

-









- -

Setting it up

- -

-It means that to represent the entire -dataset of images, we require a 4D matrix or tensor. This tensor has the dimensions: -$$ -(n_{inputs},\, n_{pixels, width},\, n_{pixels, height},\, depth) . -$$ - -

-









- -

The MNIST dataset again

- -

-The MNIST dataset consists of grayscale images with a pixel size of -\( 28\times 28 \), meaning we require \( 28 \times 28 = 724 \) weights to each -neuron in the first hidden layer. - -

-If we were to analyze images of size \( 128\times 128 \) we would require -\( 128 \times 128 = 16384 \) weights to each neuron. Even worse if we were -dealing with color images, as most images are, we have an image matrix -of size \( 128\times 128 \) for each color dimension (Red, Green, Blue), -meaning 3 times the number of weights \( = 49152 \) are required for every -single neuron in the first hidden layer. - -

-









- -

Strong correlations

-Images typically have strong local correlations, meaning that a small -part of the image varies little from its neighboring regions. If for -example we have an image of a blue car, we can roughly assume that a -small blue part of the image is surrounded by other blue regions. - -

-Therefore, instead of connecting every single pixel to a neuron in the -first hidden layer, as we have previously done with deep neural -networks, we can instead connect each neuron to a small part of the -image (in all 3 RGB depth dimensions). The size of each small area is -fixed, and known as a receptive. - -

- - -

Layers of a CNN

-The layers of a convolutional neural network arrange neurons in 3D: width, height and depth. -The input image is typically a square matrix of depth 3. - -

-A convolution is performed on the image which outputs -a 3D volume of neurons. The weights to the input are arranged in a number of 2D matrices, known as filters. - -

-Each filter slides along the input image, taking the dot product -between each small part of the image and the filter, in all depth -dimensions. This is then passed through a non-linear function, -typically the Rectified Linear (ReLu) function, which serves as the -activation of the neurons in the first convolutional layer. This is -further passed through a pooling layer, which reduces the size of the -convolutional layer, e.g. by taking the maximum or average across some -small regions, and this serves as input to the next convolutional -layer. - -

-









- -

Systematic reduction

- -

-By systematically reducing the size of the input volume, through -convolution and pooling, the network should create representations of -small parts of the input, and then from them assemble representations -of larger areas. The final pooling layer is flattened to serve as -input to a hidden layer, such that each neuron in the final pooling -layer is connected to every single neuron in the hidden layer. This -then serves as input to the output layer, e.g. a softmax output for -classification. - -

-









- -

Prerequisites: Collect and pre-process data

-

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-# RGB images have a depth of 3
-# our images are grayscale so they should have a depth of 1
-inputs = inputs[:,:,:,np.newaxis]
-
-print("inputs = (n_inputs, pixel_width, pixel_height, depth) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# choose some random images to display
-n_inputs = len(inputs)
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-

-









- -

Importing Keras and Tensorflow

-

- - -

from keras.utils import to_categorical
-from sklearn.model_selection import train_test_split
-
-# representation of labels
-labels = to_categorical(labels)
-
-# split into train and test data
-# one-liner from scikit-learn library
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-

-









- -

Using TensorFlow backend

- -

-We need to define model and architecture and choose cost function and optmizer. -

- - -

import tensorflow as tf
-
-class ConvolutionalNeuralNetworkTensorflow:
-    def __init__(
-        self,
-        X_train,
-        Y_train,
-        X_test,
-        Y_test,
-        n_filters=10,
-        n_neurons_connected=50,
-        n_categories=10,
-        receptive_field=3,
-        stride=1,
-        padding=1,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-    ):
-        
-        self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')
-        
-        self.X_train = X_train
-        self.Y_train = Y_train
-        self.X_test = X_test
-        self.Y_test = Y_test
-        
-        self.n_inputs, self.input_width, self.input_height, self.depth = X_train.shape
-        
-        self.n_filters = n_filters
-        self.n_downsampled = int(self.input_width*self.input_height*n_filters / 4)
-        self.n_neurons_connected = n_neurons_connected
-        self.n_categories = n_categories
-        
-        self.receptive_field = receptive_field
-        self.stride = stride
-        self.strides = [stride, stride, stride, stride]
-        self.padding = padding
-        
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-        
-        self.create_placeholders()
-        self.create_CNN()
-        self.create_loss()
-        self.create_optimiser()
-        self.create_accuracy()
-    
-    def create_placeholders(self):
-        with tf.name_scope('data'):
-            self.X = tf.placeholder(tf.float32, shape=(None, self.input_width, self.input_height, self.depth), name='X_data')
-            self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')
-    
-    def create_CNN(self):
-        with tf.name_scope('CNN'):
-            
-            # Convolutional layer
-            self.W_conv = self.weight_variable([self.receptive_field, self.receptive_field, self.depth, self.n_filters], name='conv', dtype=tf.float32)
-            b_conv = self.weight_variable([self.n_filters], name='conv', dtype=tf.float32)
-            z_conv = tf.nn.conv2d(self.X, self.W_conv, self.strides, padding='SAME', name='conv') + b_conv
-            a_conv = tf.nn.relu(z_conv)
-            
-            # 2x2 max pooling
-            a_pool = tf.nn.max_pool(a_conv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME', name='pool')
-            
-            # Fully connected layer
-            a_pool_flat = tf.reshape(a_pool, [-1, self.n_downsampled])
-            self.W_fc = self.weight_variable([self.n_downsampled, self.n_neurons_connected], name='fc', dtype=tf.float32)
-            b_fc = self.bias_variable([self.n_neurons_connected], name='fc', dtype=tf.float32)
-            a_fc = tf.nn.relu(tf.matmul(a_pool_flat, self.W_fc) + b_fc)
-            
-            # Output layer
-            self.W_out = self.weight_variable([self.n_neurons_connected, self.n_categories], name='out', dtype=tf.float32)
-            b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)
-            self.z_out = tf.matmul(a_fc, self.W_out) + b_out
-    
-    def create_loss(self):
-        with tf.name_scope('loss'):
-            softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))
-            
-            regularizer_loss_conv = tf.nn.l2_loss(self.W_conv)
-            regularizer_loss_fc = tf.nn.l2_loss(self.W_fc)
-            regularizer_loss_out = tf.nn.l2_loss(self.W_out)
-            regularizer_loss = self.lmbd*(regularizer_loss_conv + regularizer_loss_fc + regularizer_loss_out)
-            
-            self.loss = softmax_loss + regularizer_loss
-
-    def create_accuracy(self):
-        with tf.name_scope('accuracy'):
-            probabilities = tf.nn.softmax(self.z_out)
-            predictions = tf.argmax(probabilities, 1)
-            labels = tf.argmax(self.Y, 1)
-            
-            correct_predictions = tf.equal(predictions, labels)
-            correct_predictions = tf.cast(correct_predictions, tf.float32)
-            self.accuracy = tf.reduce_mean(correct_predictions)
-    
-    def create_optimiser(self):
-        with tf.name_scope('optimizer'):
-            self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)
-            
-    def weight_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.truncated_normal(shape, stddev=0.1)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def bias_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.constant(0.1, shape=shape)
-        return tf.Variable(initial, name=name, dtype=dtype)
-
-    def fit(self):
-        data_indices = np.arange(self.n_inputs)
-
-        with tf.Session() as sess:
-            sess.run(tf.global_variables_initializer())
-            for i in range(self.epochs):
-                for j in range(self.iterations):
-                    chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)
-                    batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]
-            
-                    sess.run([CNN.loss, CNN.optimizer],
-                        feed_dict={CNN.X: batch_X,
-                                   CNN.Y: batch_Y})
-                    accuracy = sess.run(CNN.accuracy,
-                        feed_dict={CNN.X: batch_X,
-                                   CNN.Y: batch_Y})
-                    step = sess.run(CNN.global_step)
-    
-            self.train_loss, self.train_accuracy = sess.run([CNN.loss, CNN.accuracy],
-                feed_dict={CNN.X: self.X_train,
-                           CNN.Y: self.Y_train})
-        
-            self.test_loss, self.test_accuracy = sess.run([CNN.loss, CNN.accuracy],
-                feed_dict={CNN.X: self.X_test,
-                           CNN.Y: self.Y_test})
-
-

-









- -

Train the model

- -

-We need now to train the model, evaluate it and test its performance on test data, and eventually include hyperparameters. -

- - -

epochs = 100
-batch_size = 100
-n_filters = 10
-n_neurons_connected = 50
-n_categories = 10
-
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-CNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        CNN = ConvolutionalNeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,
-                                      n_filters=n_filters, n_neurons_connected=n_neurons_connected,
-                                      n_categories=n_categories, epochs=epochs, batch_size=batch_size,
-                                      eta=eta, lmbd=lmbd)
-        CNN.fit()
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % CNN.test_accuracy)
-        print()
-            
-        CNN_tf[i][j] = CNN
-
-

-









- -

Visualizing the results

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        CNN = CNN_tf[i][j]
-
-        train_accuracy[i][j] = CNN.train_accuracy
-        test_accuracy[i][j] = CNN.test_accuracy
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

- - -

Running with Keras

- -

- - -

from keras.models import Sequential
-from keras.layers.convolutional import Conv2D
-from keras.layers.convolutional import MaxPooling2D
-from keras.layers import Flatten
-from keras.layers import Dense
-from keras.regularizers import l2
-from keras.optimizers import SGD
-
-def create_convolutional_neural_network_keras(input_shape, receptive_field,
-                                              n_filters, n_neurons_connected, n_categories,
-                                              eta, lmbd):
-    model = Sequential()
-    model.add(Conv2D(n_filters, (receptive_field, receptive_field), input_shape=input_shape, padding='same',
-              activation='relu', kernel_regularizer=l2(lmbd)))
-    model.add(MaxPooling2D(pool_size=(2, 2)))
-    model.add(Flatten())
-    model.add(Dense(n_neurons_connected, activation='relu', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_categories, activation='softmax', kernel_regularizer=l2(lmbd)))
-    
-    sgd = SGD(lr=eta)
-    model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
-    
-    return model
-
-epochs = 100
-batch_size = 100
-input_shape = X_train.shape[1:4]
-receptive_field = 3
-n_filters = 10
-n_neurons_connected = 50
-n_categories = 10
-
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-
-

-









- -

Final part

- -

- - -

CNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        CNN = create_convolutional_neural_network_keras(input_shape, receptive_field,
-                                              n_filters, n_neurons_connected, n_categories,
-                                              eta, lmbd)
-        CNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)
-        scores = CNN.evaluate(X_test, Y_test)
-        
-        CNN_keras[i][j] = CNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % scores[1])
-        print()
-
-

-









- -

Final visualization

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        CNN = CNN_keras[i][j]
-
-        train_accuracy[i][j] = CNN.evaluate(X_train, Y_train)[1]
-        test_accuracy[i][j] = CNN.evaluate(X_test, Y_test)[1]
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

-









- -

Fun links

- -
    -
  1. Self-Driving cars using a convolutional neural network
  2. -
  3. Abstract art using convolutional neural networks
  4. -
- - - - - -
- © 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license -
- - - - - - diff --git a/doc/src/NeuralNet/NeuralNet.html b/doc/src/NeuralNet/NeuralNet.html deleted file mode 100644 index afffc73d6..000000000 --- a/doc/src/NeuralNet/NeuralNet.html +++ /dev/null @@ -1,3647 +0,0 @@ - - - - - - - -Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks - - - - - - - - - - - - - - - - - - - - - - - -

Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks

- -

- - -

-Morten Hjorth-Jensen [1, 2] -
- -

- - -

[1] Department of Physics, University of Oslo
-
[2] Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University
-
-

-

Oct 19, 2018

-
-

-









- -

Neural networks

- -

-Artificial neural networks are computational systems that can learn to -perform tasks by considering examples, generally without being -programmed with any task-specific rules. It is supposed to mimic a -biological system, wherein neurons interact by sending signals in the -form of mathematical functions between layers. All layers can contain -an arbitrary number of neurons, and each connection is represented by -a weight variable. - -

-









- -

Artificial neurons

- -

-The field of artificial neural networks has a long history of -development, and is closely connected with the advancement of computer -science and computers in general. A model of artificial neurons was -first developed by McCulloch and Pitts in 1943 to study signal -processing in the brain and has later been refined by others. The -general idea is to mimic neural networks in the human brain, which is -composed of billions of neurons that communicate with each other by -sending electrical signals. Each neuron accumulates its incoming -signals, which must exceed an activation threshold to yield an -output. If the threshold is not overcome, the neuron remains inactive, -i.e. has zero output. - -

-This behaviour has inspired a simple mathematical model for an artificial neuron. - -$$ -\begin{equation} - y = f\left(\sum_{i=1}^n w_ix_i\right) = f(u) -\label{artificialNeuron} -\end{equation} -$$ - -Here, the output \( y \) of the neuron is the value of its activation function, which have as input -a weighted sum of signals \( x_i, \dots ,x_n \) received by \( n \) other neurons. - -

-Conceptually, it is helpful to divide neural networks into four -categories: - -

    -
  1. general purpose neural networks for supervised learning,
  2. -
  3. neural networks designed specifically for image processing, the most prominent example of this class being Convolutional Neural Networks (CNNs),
  4. -
  5. neural networks for sequential data such as Recurrent Neural Networks (RNNs), and
  6. -
  7. neural networks for unsupervised learning such as Deep Boltzmann Machines.
  8. -
- -In natural science, DNNs and CNNs have already found numerous -applications. In statistical physics, they have been applied to detect -phase transitions in 2D Ising and Potts models, lattice gauge -theories, and different phases of polymers, or solving the -Navier-Stokes equation in weather forecasting. Deep learning has also -found interesting applications in quantum physics. Various quantum -phase transitions can be detected and studied using DNNs and CNNs, -topological phases, and even non-equilibrium many-body -localization. Representing quantum states as DNNs quantum state -tomography are among some of the impressive achievements to reveal the -potential of DNNs to facilitate the study of quantum systems. - -

-In quantum information theory, it has been shown that one can perform -gate decompositions with the help of neural. - -

-The applications are not limited to the natural sciences. There is a -plethora of applications in essentially all disciplines, from the -humanities to life science and medicine. - -

-









- -

Neural network types

- -

-An artificial neural network (ANN), is a computational model that -consists of layers of connected neurons, or nodes or units. We will -refer to these interchangeably as units or nodes, and sometimes as -neurons. - -

-It is supposed to mimic a biological nervous system by letting each -neuron interact with other neurons by sending signals in the form of -mathematical functions between layers. A wide variety of different -ANNs have been developed, but most of them consist of an input layer, -an output layer and eventual layers in-between, called hidden -layers. All layers can contain an arbitrary number of nodes, and each -connection between two nodes is associated with a weight variable. - -

-Neural networks (also called neural nets) are neural-inspired -nonlinear models for supervised learning. As we will see, neural nets -can be viewed as natural, more powerful extensions of supervised -learning methods such as linear and logistic regression and soft-max -methods we discussed earlier. - -

-









- -

Feed-forward neural networks

- -

-The feed-forward neural network (FFNN) was the first and simplest type -of ANNs that were devised. In this network, the information moves in -only one direction: forward through the layers. - -

-Nodes are represented by circles, while the arrows display the -connections between the nodes, including the direction of information -flow. Additionally, each arrow corresponds to a weight variable -(figure to come). We observe that each node in a layer is connected -to all nodes in the subsequent layer, making this a so-called -fully-connected FFNN. - -

-









- -

Convolutional Neural Network

- -

-A different variant of FFNNs are convolutional neural networks -(CNNs), which have a connectivity pattern inspired by the animal -visual cortex. Individual neurons in the visual cortex only respond to -stimuli from small sub-regions of the visual field, called a receptive -field. This makes the neurons well-suited to exploit the strong -spatially local correlation present in natural images. The response of -each neuron can be approximated mathematically as a convolution -operation. (figure to come) - -

-Convolutional neural networks emulate the behaviour of neurons in the -visual cortex by enforcing a local connectivity pattern between -nodes of adjacent layers: Each node in a convolutional layer is -connected only to a subset of the nodes in the previous layer, in -contrast to the fully-connected FFNN. Often, CNNs consist of several -convolutional layers that learn local features of the input, with a -fully-connected layer at the end, which gathers all the local data and -produces the outputs. They have wide applications in image and video -recognition. - -

-









- -

Recurrent neural networks

- -

-So far we have only mentioned ANNs where information flows in one -direction: forward. Recurrent neural networks on the other hand, -have connections between nodes that form directed cycles. This -creates a form of internal memory which are able to capture -information on what has been calculated before; the output is -dependent on the previous computations. Recurrent NNs make use of -sequential information by performing the same task for every element -in a sequence, where each element depends on previous elements. An -example of such information is sentences, making recurrent NNs -especially well-suited for handwriting and speech recognition. - -

-









- -

Other types of networks

- -

-There are many other kinds of ANNs that have been developed. One type -that is specifically designed for interpolation in multidimensional -space is the radial basis function (RBF) network. RBFs are typically -made up of three layers: an input layer, a hidden layer with -non-linear radial symmetric activation functions and a linear output -layer (''linear'' here means that each node in the output layer has a -linear activation function). The layers are normally fully-connected -and there are no cycles, thus RBFs can be viewed as a type of -fully-connected FFNN. They are however usually treated as a separate -type of NN due the unusual activation functions. - -

-









- -

Multilayer perceptrons

- -

-One uses often so-called fully-connected feed-forward neural networks -with three or more layers (an input layer, one or more hidden layers -and an output layer) consisting of neurons that have non-linear -activation functions. - -

-Such networks are often called multilayer perceptrons (MLPs). - -

-









- -

Why multilayer perceptrons?

- -

-According to the Universal approximation theorem, a feed-forward -neural network with just a single hidden layer containing a finite -number of neurons can approximate a continuous multidimensional -function to arbitrary accuracy, assuming the activation function for -the hidden layer is a non-constant, bounded and -monotonically-increasing continuous function. - -

-Note that the requirements on the activation function only applies to -the hidden layer, the output nodes are always assumed to be linear, so -as to not restrict the range of output values. - -

-









- -

Mathematical model

- -

-The output \( y \) is produced via the activation function \( f \) -$$ - y = f\left(\sum_{i=1}^n w_ix_i + b_i\right) = f(z), -$$ - -This function receives \( x_i \) as inputs. -Here the activation \( z=(\sum_{i=1}^n w_ix_i+b_i) \). -In an FFNN of such neurons, the inputs \( x_i \) are the outputs of -the neurons in the preceding layer. Furthermore, an MLP is -fully-connected, which means that each neuron receives a weighted sum -of the outputs of all neurons in the previous layer. - -

-









- -

Mathematical model

- -

-First, for each node \( i \) in the first hidden layer, we calculate a weighted sum \( z_i^1 \) of the input coordinates \( x_j \), - -$$ -\begin{equation} z_i^1 = \sum_{j=1}^{M} w_{ij}^1 x_j + b_i^1 -\label{_auto1} -\end{equation} -$$ - -

-Here \( b_i \) is the so-called bias which is normally needed in -case of zero activation weights or inputs. How to fix the biases and -the weights will be discussed below. The value of \( z_i^1 \) is the -argument to the activation function \( f_i \) of each node \( i \), The -variable \( M \) stands for all possible inputs to a given node \( i \) in the -first layer. We define the output \( y_i^1 \) of all neurons in layer 1 as - -$$ -\begin{equation} - y_i^1 = f(z_i^1) = f\left(\sum_{j=1}^M w_{ij}^1 x_j + b_i^1\right) -\label{outputLayer1} -\end{equation} -$$ - -

-where we assume that all nodes in the same layer have identical -activation functions, hence the notation \( f \). In general, we could assume in the more general case that different layers have different activation functions. -In this case we would identify these functions with a superscript \( l \) for the \( l \)-th layer, - -$$ -\begin{equation} - y_i^l = f^l(u_i^l) = f^l\left(\sum_{j=1}^{N_{l-1}} w_{ij}^l y_j^{l-1} + b_i^l\right) -\label{generalLayer} -\end{equation} -$$ - -

-where \( N_l \) is the number of nodes in layer \( l \). When the output of -all the nodes in the first hidden layer are computed, the values of -the subsequent layer can be calculated and so forth until the output -is obtained. - -

-









- -

Mathematical model

- -

-The output of neuron \( i \) in layer 2 is thus, - -$$ -\begin{align} - y_i^2 &= f^2\left(\sum_{j=1}^N w_{ij}^2 y_j^1 + b_i^2\right) -\label{_auto2}\\ - &= f^2\left[\sum_{j=1}^N w_{ij}^2f^1\left(\sum_{k=1}^M w_{jk}^1 x_k + b_j^1\right) + b_i^2\right] -\label{outputLayer2} -\end{align} -$$ - -where we have substituted \( y_k^1 \) with the inputs \( x_k \). Finally, the ANN output reads - -$$ -\begin{align} - y_i^3 &= f^3\left(\sum_{j=1}^N w_{ij}^3 y_j^2 + b_i^3\right) -\label{_auto3}\\ - &= f_3\left[\sum_{j} w_{ij}^3 f^2\left(\sum_{k} w_{jk}^2 f^1\left(\sum_{m} w_{km}^1 x_m + b_k^1\right) + b_j^2\right) - + b_1^3\right] -\label{_auto4} -\end{align} -$$ - -

-









- -

Mathematical model

- -

-We can generalize this expression to an MLP with \( l \) hidden -layers. The complete functional form is, - -$$ -\begin{align} -&y^{l+1}_i = f^{l+1}\left[\!\sum_{j=1}^{N_l} w_{ij}^3 f^l\left(\sum_{k=1}^{N_{l-1}}w_{jk}^{l-1}\left(\dots f^1\left(\sum_{n=1}^{N_0} w_{mn}^1 x_n+ b_m^1\right)\dots\right)+b_k^2\right)+b_1^3\right] && -\label{completeNN} -\end{align} -$$ - -

-which illustrates a basic property of MLPs: The only independent -variables are the input values \( x_n \). - -

-









- -

Mathematical model

- -

-This confirms that an MLP, despite its quite convoluted mathematical -form, is nothing more than an analytic function, specifically a -mapping of real-valued vectors \( \hat{x} \in \mathbb{R}^n \rightarrow -\hat{y} \in \mathbb{R}^m \). - -

-Furthermore, the flexibility and universality of an MLP can be -illustrated by realizing that the expression is essentially a nested -sum of scaled activation functions of the form - -$$ -\begin{equation} - f(x) = c_1 f(c_2 x + c_3) + c_4 -\label{_auto5} -\end{equation} -$$ - -

-where the parameters \( c_i \) are weights and biases. By adjusting these -parameters, the activation functions can be shifted up and down or -left and right, change slope or be rescaled which is the key to the -flexibility of a neural network. - -

-









- -

Matrix-vector notation

- -

-We can introduce a more convenient notation for the activations in an A NN. - -

-Additionally, we can represent the biases and activations -as layer-wise column vectors \( \hat{b}_l \) and \( \hat{y}_l \), so that the \( i \)-th element of each vector -is the bias \( b_i^l \) and activation \( y_i^l \) of node \( i \) in layer \( l \) respectively. - -

-We have that \( \mathrm{W}_l \) is an \( N_{l-1} \times N_l \) matrix, while \( \hat{b}_l \) and \( \hat{y}_l \) are \( N_l \times 1 \) column vectors. -With this notation, the sum becomes a matrix-vector multiplication, and we can write -the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as -$$ -\begin{equation} - \hat{y}_2 = f_2(\mathrm{W}_2 \hat{y}_{1} + \hat{b}_{2}) = - f_2\left(\left[\begin{array}{ccc} - w^2_{11} &w^2_{12} &w^2_{13} \\ - w^2_{21} &w^2_{22} &w^2_{23} \\ - w^2_{31} &w^2_{32} &w^2_{33} \\ - \end{array} \right] \cdot - \left[\begin{array}{c} - y^1_1 \\ - y^1_2 \\ - y^1_3 \\ - \end{array}\right] + - \left[\begin{array}{c} - b^2_1 \\ - b^2_2 \\ - b^2_3 \\ - \end{array}\right]\right). -\label{_auto6} -\end{equation} -$$ - -

-









- -

Matrix-vector notation and activation

- -

-The activation of node \( i \) in layer 2 is - -$$ -\begin{equation} - y^2_i = f_2\Bigr(w^2_{i1}y^1_1 + w^2_{i2}y^1_2 + w^2_{i3}y^1_3 + b^2_i\Bigr) = - f_2\left(\sum_{j=1}^3 w^2_{ij} y_j^1 + b^2_i\right). -\label{_auto7} -\end{equation} -$$ - -

-This is not just a convenient and compact notation, but also a useful -and intuitive way to think about MLPs: The output is calculated by a -series of matrix-vector multiplications and vector additions that are -used as input to the activation functions. For each operation -\( \mathrm{W}_l \hat{y}_{l-1} \) we move forward one layer. - -

-









- -

Activation functions

- -

-A property that characterizes a neural network, other than its -connectivity, is the choice of activation function(s). As described -in, the following restrictions are imposed on an activation function -for a FFNN to fulfill the universal approximation theorem - -

    -
  • Non-constant
  • -
  • Bounded
  • -
  • Monotonically-increasing
  • -
  • Continuous
  • -
- -









- -

Activation functions, Logistic and Hyperbolic ones

- -

-The second requirement excludes all linear functions. Furthermore, in -a MLP with only linear activation functions, each layer simply -performs a linear transformation of its inputs. - -

-Regardless of the number of layers, the output of the NN will be -nothing but a linear function of the inputs. Thus we need to introduce -some kind of non-linearity to the NN to be able to fit non-linear -functions Typical examples are the logistic Sigmoid - -$$ - f(x) = \frac{1}{1 + e^{-x}}, -$$ - -and the hyperbolic tangent function -$$ - f(x) = \tanh(x) -$$ - -

-









- -

Relevance

- -

-The sigmoid function are more biologically plausible because the -output of inactive neurons are zero. Such activation function are -called one-sided. However, it has been shown that the hyperbolic -tangent performs better than the sigmoid for training MLPs. has -become the most popular for deep neural networks - -

- - -

"""The sigmoid function (or the logistic curve) is a 
-function that takes any real number, z, and outputs a number (0,1).
-It is useful in neural networks for assigning weights on a relative scale.
-The value z is the weighted sum of parameters involved in the learning algorithm."""
-
-import numpy
-import matplotlib.pyplot as plt
-import math as mt
-
-z = numpy.arange(-5, 5, .1)
-sigma_fn = numpy.vectorize(lambda z: 1/(1+numpy.exp(-z)))
-sigma = sigma_fn(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, sigma)
-ax.set_ylim([-0.1, 1.1])
-ax.set_xlim([-5,5])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('sigmoid function')
-
-plt.show()
-
-"""Step Function"""
-z = numpy.arange(-5, 5, .02)
-step_fn = numpy.vectorize(lambda z: 1.0 if z >= 0.0 else 0.0)
-step = step_fn(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, step)
-ax.set_ylim([-0.5, 1.5])
-ax.set_xlim([-5,5])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('step function')
-
-plt.show()
-
-"""Sine Function"""
-z = numpy.arange(-2*mt.pi, 2*mt.pi, 0.1)
-t = numpy.sin(z)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, t)
-ax.set_ylim([-1.0, 1.0])
-ax.set_xlim([-2*mt.pi,2*mt.pi])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('sine function')
-
-plt.show()
-
-"""Plots a graph of the squashing function used by a rectified linear
-unit"""
-z = numpy.arange(-2, 2, .1)
-zero = numpy.zeros(len(z))
-y = numpy.max([zero, z], axis=0)
-
-fig = plt.figure()
-ax = fig.add_subplot(111)
-ax.plot(z, y)
-ax.set_ylim([-2.0, 2.0])
-ax.set_xlim([-2.0, 2.0])
-ax.grid(True)
-ax.set_xlabel('z')
-ax.set_title('Rectified linear unit')
-
-plt.show()
-
-

-









- -

The multilayer perceptron (MLP)

- -

-The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of - -

    -
  1. A neural network with one or more layers of nodes between the input and the output nodes.
  2. -
  3. The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer.
  4. -
  5. The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer.
  6. -
- -As a convention it is normal to call a network with one layer of input units, one layer of hidden -units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc. - -

-For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units. -Hereafter we will call the various entities of a layer for nodes. -There are also no connections within a single layer. - -

-The number of input nodes does not need to equal the number of output -nodes. This applies also to the hidden layers. Each layer may have its -own number of nodes and activation functions. - -

-The hidden layers have their name from the fact that they are not -linked to observables and as we will see below when we define the -so-called activation \( \hat{z} \), we can think of this as a basis -expansion of the original inputs \( \hat{x} \). The difference however -between neural networks and say linear regression is that now these -basis functions (which will correspond to the weights in the network) -are learned from data. This results in an important difference between -neural networks and deep learning approaches on one side and methods -like logistic regression or linear regression and their modifications on the other side. - -

-









- -

From one to many layers, the universal approximation theorem

- -

-A neural network with only one layer, what we called the simple -perceptron, is best suited if we have a standard binary model with -clear (linear) boundaries between the outcomes. As such it could -equally well be replaced by standard linear regression or logistic -regression. Networks with one or more hidden layers approximate -systems with more complex boundaries. - -

-As stated earlier, -an important theorem in studies of neural networks, restated without -proof here, is the universal approximation -theorem. - -

-It states that a feed-forward network with a single hidden layer -containing a finite number of neurons can approximate continuous -functions on compact subsets of real functions. The theorem thus -states that simple neural networks can represent a wide variety of -interesting functions when given appropriate parameters. It is the -multilayer feedforward architecture itself which gives neural networks -the potential of being universal approximators. - -

-









- -

Deriving the back propagation code for a multilayer perceptron model

- -

-Note: figures will be inserted later! - -

-As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications. -The unknowwn quantities are our weights \( w_{ij} \) and we need to find an algorithm for changing them so that our errors are as small as possible. -This leads us to the famous back propagation algorithm. - -

-The questions we want to ask are how do changes in the biases and the -weights in our network change the cost function and how can we use the -final output to modify the weights? - -

-To derive these equations let us start with a plain regression problem -and define our cost function as - -$$ -{\cal C}(\hat{W}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2, -$$ - -

-where the $t_i$s are our \( n \) targets (the values we want to -reproduce), while the outputs of the network after having propagated -all inputs \( \hat{x} \) are given by \( y_i \). Below we will demonstrate -how the basic equations arising from the back propagation algorithm -can be modified in order to study classification problems with \( K \) -classes. - -

-









- -

Definitions

- -

-With our definition of the targets \( \hat{t} \), the outputs of the -network \( \hat{y} \) and the inputs \( \hat{x} \) we -define now the activation \( z_j^l \) of node/neuron/unit \( j \) of the -\( l \)-th layer as a function of the bias, the weights which add up from -the previous layer \( l-1 \) and the forward passes/outputs -\( \hat{a}^{l-1} \) from the previous layer as - -$$ -z_j^l = \sum_{i=1}^{M_{l-1}}w_{ij}^la_i^{l-1}+b_j^l, -$$ - -

-where \( b_k^l \) are the biases from layer \( l \). Here \( M_{l-1} \) -represents the total number of nodes/neurons/units of layer \( l-1 \). The -figure here illustrates this equation. We can rewrite this in a more -compact form as the matrix-vector products we discussed earlier, - -$$ -\hat{z}^l = \left(\hat{W}^l\right)^T\hat{a}^{l-1}+\hat{b}^l. -$$ - -

-With the activation values \( \hat{z}^l \) we can in turn define the -output of layer \( l \) as \( \hat{a}^l = f(\hat{z}^l) \) where \( f \) is our -activation function. In the examples here we will use the sigmoid -function discussed in our logistic regression lectures. We will also use the same activation function \( f \) for all layers -and their nodes. It means we have - -$$ -a_j^l = f(z_j^l) = \frac{1}{1+\exp{-(z_j^l)}}. -$$ - -

-









- -

Derivatives and the chain rule

- -

-From the definition of the activation \( z_j^l \) we have -$$ -\frac{\partial z_j^l}{\partial w_{ij}^l} = a_i^{l-1}, -$$ - -and -$$ -\frac{\partial z_j^l}{\partial a_i^{l-1}} = w_{ji}^l. -$$ - -

-With our definition of the activation function we have that (note that this function depends only on \( z_j^l \)) -$$ -\frac{\partial a_j^l}{\partial z_j^{l}} = a_j^l(1-a_j^l)=f(z_j^l)(1-f(z_j^l)). -$$ - -

-









- -

Derivative of the cost function

- -

-With these definitions we can now compute the derivative of the cost function in terms of the weights. - -

-Let us specialize to the output layer \( l=L \). Our cost function is -$$ -{\cal C}(\hat{W^L}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2=\frac{1}{2}\sum_{i=1}^n\left(a_i^L - t_i\right)^2, -$$ - -The derivative of this function with respect to the weights is - -$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)\frac{\partial a_j^L}{\partial w_{jk}^{L}}, -$$ - -The last partial derivative can easily be computed and reads (by applying the chain rule) -$$ -\frac{\partial a_j^L}{\partial w_{jk}^{L}} = \frac{\partial a_j^L}{\partial z_{j}^{L}}\frac{\partial z_j^L}{\partial w_{jk}^{L}}=a_j^L(1-a_j^L)a_k^{L-1}, -$$ - -

-









- -

Bringing it together, first back propagation equation

- -

-We have thus -$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)a_j^L(1-a_j^L)a_k^{L-1}, -$$ - -

-Defining -$$ -\delta_j^L = a_j^L(1-a_j^L)\left(a_j^L - t_j\right) = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -$$ - -and using the Hadamard product of two vectors we can write this as -$$ -\hat{\delta}^L = f'(\hat{z}^L)\circ\frac{\partial {\cal C}}{\partial (\hat{a}L)}. -$$ - -

-This is an important expression. The second term on the right handside -measures how fast the cost function is changing as a function of the $j$th -output activation. If, for example, the cost function doesn't depend -much on a particular output node \( j \), then \( \delta_j^L \) will be small, -which is what we would expect. The first term on the right, measures -how fast the activation function \( f \) is changing at a given activation -value \( z_j^L \). - -

-Notice that everything in the above equations is easily computed. In -particular, we compute \( z_j^L \) while computing the behaviour of the -network, and it is only a small additional overhead to compute -\( f'(z^L_j) \). The exact form of the derivative with respect to the -output depends on the form of the cost function. -However, provided the cost function is known there should be little -trouble in calculating - -$$ -\frac{\partial {\cal C}}{\partial (a_j^L)} -$$ - -

-With the definition of \( \delta_j^L \) we have a more compact definition of the derivative of the cost function in terms of the weights, namely -$$ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}. -$$ - -

-









- -

Derivatives in terms of \( z_j^L \)

- -

-It is also easy to see that our previous equation can be written as - -$$ -\delta_j^L =\frac{\partial {\cal C}}{\partial z_j^L}= \frac{\partial {\cal C}}{\partial a_j^L}\frac{\partial a_j^L}{\partial z_j^L}, -$$ - -which can also be interpreted as the partial derivative of the cost function with respect to the biases \( b_j^L \), namely -$$ -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}\frac{\partial b_j^L}{\partial z_j^L}=\frac{\partial {\cal C}}{\partial b_j^L}, -$$ - -That is, the error \( \delta_j^L \) is exactly equal to the rate of change of the cost function as a function of the bias. -









- -

Bringing it together

- -

-We have now three equations that are essential for the computations of the derivatives of the cost function at the output layer. These equations are needed to start the algorithm and they are - -

-

-The starting equations. -

- -$$ -\begin{equation} -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}, -\label{_auto8} -\end{equation} -$$ - -and -$$ -\begin{equation} -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\label{_auto9} -\end{equation} -$$ - -and - -$$ -\begin{equation} -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}, -\label{_auto10} -\end{equation} -$$ -

- - -

-An interesting consequence of the above equations is that when the -activation \( a_k^{L-1} \) is small, the gradient term, that is the -derivative of the cost function with respect to the weights, will also -tend to be small. We say then that the weight learns slowly, meaning -that it changes slowly when we minimize the weights via say gradient -descent. In this case we say the system learns slowly. - -

-Another interesting feature is that is when the activation function, -represented by the sigmoid function here, is rather flat when we move towards -its end values \( 0 \) and \( 1 \) (see the above Python codes). In these -cases, the derivatives of the activation function will also be close -to zero, meaning again that the gradients will be small and the -network learns slowly again. - -

-We need a fourth equation and we are set. We are going to propagate -backwards in order to the determine the weights and biases. In order -to do so we need to represent the error in the layer before the final -one \( L-1 \) in terms of the errors in the final output layer. - -

-









- -

Final back propagating equation

- -

-We have that (replacing \( L \) with a general layer \( l \)) -$$ -\delta_j^l =\frac{\partial {\cal C}}{\partial z_j^l}. -$$ - -We want to express this in terms of the equations for layer \( l+1 \). Using the chain rule and summing over all \( k \) entries we have - -$$ -\delta_j^l =\sum_k \frac{\partial {\cal C}}{\partial z_k^{l+1}}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}=\sum_k \delta_k^{l+1}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}, -$$ - -and recalling that -$$ -z_j^{l+1} = \sum_{i=1}^{M_{l}}w_{ij}^{l+1}a_j^{l}+b_j^{l+1}, -$$ - -with \( M_l \) being the number of nodes in layer \( l \), we obtain -$$ -\delta_j^l =\sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l), -$$ - -This is our final equation. - -

-We are now ready to set up the algorithm for back propagation and learning the weights and biases. - -

-









- -

Setting up the Back propagation algorithm

- -

-The four equations provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm. - -

-

- -

-First, we set up the input data \( \hat{x} \) and the activations -\( \hat{z}_1 \) of the input layer and compute the activation function and -the pertinent outputs \( \hat{a}^1 \). -

- - -

-

- -

-Secondly, we perform then the feed forward till we reach the output -layer and compute all \( \hat{z}_l \) of the input layer and compute the -activation function and the pertinent outputs \( \hat{a}^l \) for -\( l=2,3,\dots,L \). -

- - -

-

- -

-Thereafter we compute the ouput error \( \hat{\delta}^L \) by computing all -$$ -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}. -$$ -

- - -

-

- -

-Then we compute the back propagate error for each \( l=L-1,L-2,\dots,2 \) as -$$ -\delta_j^l = \sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l). -$$ -

- - -

-

- -

-Finally, we update the weights and the biases using gradient descent for each \( l=L-1,L-2,\dots,2 \) and update the weights and biases according to the rules -$$ -w_{jk}^l\leftarrow = w_{jk}^l- \eta \delta_j^la_k^{l-1}, -$$ - - -$$ -b_j^l \leftarrow b_j^l-\eta \frac{\partial {\cal C}}{\partial b_j^l}=b_j^l-\eta \delta_j^l, -$$ -

- - -

-The parameter \( \eta \) is the learning parameter discussed in connection with the gradient descent methods. -Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training. - -

- - -

Setting up a Multi-layer perceptron model for classification

- -

-We are now gong to develop an example based on the MNIST data -base. This is a classification problem and we need to use our -cross-entropy function we discussed in connection with logistic -regression. The cross-entropy defines our cost function for the -classificaton problems with neural networks. - -

-In binary classification with two classes \( (0, 1) \) we define the -logistic/sigmoid function as the probability that a particular input -is in class \( 0 \) or \( 1 \). This is possible because the logistic -function takes any input from the real numbers and inputs a number -between 0 and 1, and can therefore be interpreted as a probability. It -also has other nice properties, such as a derivative that is simple to -calculate. - -

-For an input \( \boldsymbol{a} \) from the hidden layer, the probability that the input \( \boldsymbol{x} \) -is in class 0 or 1 is just. We let \( \theta \) represent the unknown weights and biases to be adjusted by our equations). The variable \( x \) -represents our activation values \( z \). We have -$$ -P(y = 0 \mid \hat{x}, \hat{\theta}) = \frac{1}{1 + \exp{(- \hat{x}})} , -$$ - -and -$$ -P(y = 1 \mid \hat{x}, \hat{\theta}) = 1 - P(y = 0 \mid \hat{x}, \hat{\theta}) , -$$ - -

-where \( y \in \{0, 1\} \) and \( \hat{\theta} \) represents the weights and biases -of our network. - -

-









- -

Defining the cost function

- -

-Our cost function is given as (see the Logistic regression lectures) -$$ -\mathcal{C}(\hat{\theta}) = - \ln P(\mathcal{D} \mid \hat{\theta}) = - \sum_{i=1}^n -y_i \ln[P(y_i = 0)] + (1 - y_i) \ln [1 - P(y_i = 0)] = \sum_{i=1}^n \mathcal{L}_i(\hat{\theta}) . -$$ - -

-This last equality means that we can interpret our cost function as a sum over the loss function -for each point in the dataset \( \mathcal{L}_i(\hat{\theta}) \). -The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather -than maximizing a negative number. - -

-In multiclass classification it is common to treat each integer label as a so called one-hot vector: - -

-\( y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) , \) and - -

-\( y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) , \) - -

-i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset (numbers from \( 0 \) to \( 9 \)).. - -

-If \( \hat{x}_i \) is the \( i \)-th input (image), \( y_{ic} \) refers to the \( c \)-th component of the \( i \)-th -output vector \( \hat{y}_i \). -The probability of \( \hat{x}_i \) being in class \( c \) will be given by the softmax function: - -$$ -P(y_{ic} = 1 \mid \hat{x}_i, \hat{\theta}) = \frac{\exp{((\hat{a}_i^{hidden})^T \hat{w}_c)}} -{\sum_{c'=0}^{C-1} \exp{((\hat{a}_i^{hidden})^T \hat{w}_{c'})}} , -$$ - -

-which reduces to the logistic function in the binary case. -The likelihood of this \( C \)-class classifier -is now given as: - -$$ -P(\mathcal{D} \mid \hat{\theta}) = \prod_{i=1}^n \prod_{c=0}^{C-1} [P(y_{ic} = 1)]^{y_{ic}} . -$$ - -Again we take the negative log-likelihood to define our cost function: - -$$ -\mathcal{C}(\hat{\theta}) = - \log{P(\mathcal{D} \mid \hat{\theta})}. -$$ - -See the logistic regression lectures for a full definition of the cost function. - -

-The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before! - -

-









- -

Example: binary classification problem

- -

-As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters \( \beta \) as -$$ -\mathcal{C}(\hat{\beta}) = - \sum_{i=1}^n \left(y_i\log{p(y_i \vert x_i,\hat{\beta})}+(i-y_i)\log{1-p(y_i \vert x_i,\hat{\beta})}\right), -$$ - -where we had defined the logistic (sigmoid) function -$$ -p(y_i =1\vert x_i,\hat{\beta})=\frac{\exp{(\beta_0+\beta_1 x_i)}}{1+\exp{(\beta_0+\beta_1 x_i)}}, -$$ - -and -$$ -p(y_i =0\vert x_i,\hat{\beta})=1-p(y_i =1\vert x_i,\hat{\beta}). -$$ - -The parameters \( \hat{\beta} \) were defined using a minimization method like gradient descent or Newton-Raphson's method. - -

-Now we replace \( x_i \) with the activation \( z_i^l \) for a given layer \( l \) and the outputs as \( y_i=a_i^l=f(z_i^l) \), with \( z_i^l \) now being a function of the weights \( w_{ij}^l \) and biases \( b_i^l \). -We have then -$$ -a_i^l = y_i = \frac{\exp{(z_i^l)}}{1+\exp{(z_i^l)}}, -$$ - -with -$$ -z_i^l = \sum_{j}w_{ij}^l a_j^{l-1}+b_i^l, -$$ - -where the superscript \( l-1 \) indicates that these are the outputs from layer \( l-1 \). -Our cost function at the final layer \( l=L \) is now -$$ -\mathcal{C}(\hat{W}) = - \sum_{i=1}^n \left(t_i\log{a_i^L}+(i-t_i)\log{(1-a_i^L)}\right), -$$ - -where we have defined the targets \( t_i \). The derivatives of the cost function with respect to the output \( a_i^L \) are then easily calculated and we get -$$ -\frac{\partial \mathcal{C}(\hat{W})}{\partial a_i^L} = \frac{a_i^L-t_i}{a_i^L(1-a_i^L)}. -$$ - -In case we use another activation function than the logistic one, we need to evaluate other derivatives. - -

-









- -

The Softmax function

-In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation \( z_i^l \), that is we need -$$ -\frac{\partial f(z_i^l)}{\partial w_{jk}^l} = -\frac{\partial f(z_i^l)}{\partial z_j^l} \frac{\partial z_j^l}{\partial w_{jk}^l}= \frac{\partial f(z_i^l)}{\partial z_j^l}a_k^{l-1}. -$$ - -For the Softmax function we have -$$ -f(z_i^l) = \frac{\exp{(z_i^l)}}{\sum_{m=1}^K\exp{(z_m^l)}}. -$$ - -Its derivative with respect to \( z_j^l \) gives -$$ -\frac{\partial f(z_i^l)}{\partial z_j^l}= f(z_i^l)\left(\delta_{ij}-f(z_j^l)\right), -$$ - -which in case of the simply binary model reduces to having \( i=j \). - -

- - -

Developing a code for doing neural networks with back propagation

- -

-One can identify a set of key steps when using neural networks to solve supervised learning problems: - -

    -
  1. Collect and pre-process data
  2. -
  3. Define model and architecture
  4. -
  5. Choose cost function and optimizer
  6. -
  7. Train the model
  8. -
  9. Evaluate model performance on test data
  10. -
  11. Adjust hyperparameters (if necessary, network architecture)
  12. -
- -









- -

Collect and pre-process data

- -

-Here we will be using the MNIST dataset, which is readily available through the scikit-learn -package. You may also find it for example here. -The MNIST (Modified National Institute of Standards and Technology) database is a large database -of handwritten digits that is commonly used for training various image processing systems. -The MNIST dataset consists of 70 000 images of size \( 28\times 28 \) pixels, each labeled from 0 to 9. -The scikit-learn dataset we will use consists of a selection of 1797 images of size \( 8\times 8 \) collected and processed from this database. - -

-To feed data into a feed-forward neural network we need to represent -the inputs as a feature matrix \( X = (n_{inputs}, n_{features}) \). Each -row represents an input, in this case a handwritten digit, and -each column represents a feature, in this case a pixel. The -correct answers, also known as labels or targets are -represented as a 1D array of integers -\( Y = (n_{inputs}) = (5, 3, 1, 8,...) \). - -

-As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from -measurements of height (in m) -and weight (in kg). If we have measurements of 5 people the feature matrix could be for example: - -$$ X = \begin{bmatrix} -1.85 & 81\\ -1.71 & 65\\ -1.95 & 103\\ -1.55 & 42\\ -1.63 & 56 -\end{bmatrix} ,$$ - -

-and the targets would be: - -$$ Y = (23.7, 22.2, 27.1, 17.5, 21.1) $$ - -

-Since each input image is a 2D matrix, we need to flatten the image -(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a -feature matrix. This means we lose all spatial information in the -image, such as locality and translational invariance. More complicated -architectures such as Convolutional Neural Networks can take advantage -of such information, and are most commonly applied when analyzing -images. - -

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# flatten the image
-# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
-n_inputs = len(inputs)
-inputs = inputs.reshape(n_inputs, -1)
-print("X = (n_inputs, n_features) = " + str(inputs.shape))
-
-
-# choose some random images to display
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-

-









- -

Train and test datasets

- -

-Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions. - -

-We will reserve \( 80 \% \) of our dataset for training and \( 20 \% \) for testing. - -

-It is important that the train and test datasets are drawn randomly from our dataset, to ensure -no bias in the sampling. -Say you are taking measurements of weather data to predict the weather in the coming 5 days. -You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data -collected from 12.00 to 24.00. - -

- - -

from sklearn.model_selection import train_test_split
-
-# one-liner from scikit-learn library
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-# equivalently in numpy
-def train_test_split_numpy(inputs, labels, train_size, test_size):
-    n_inputs = len(inputs)
-    inputs_shuffled = inputs.copy()
-    labels_shuffled = labels.copy()
-    
-    np.random.shuffle(inputs_shuffled)
-    np.random.shuffle(labels_shuffled)
-    
-    train_end = int(n_inputs*train_size)
-    X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:]
-    Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:]
-    
-    return X_train, X_test, Y_train, Y_test
-
-#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size)
-
-print("Number of training images: " + str(len(X_train)))
-print("Number of test images: " + str(len(X_test)))
-
-

-









- -

Define model and architecture

- -

-Our simple feed-forward neural network will consist of an input layer, a single hidden layer and an output layer. The activation \( y \) of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have - -$$ z = \sum_{i=1}^n w_i a_i ,$$ - -$$ y = f(z) ,$$ - -

-where \( f \) is the activation function, \( a_i \) represents input from neuron \( i \) in the preceding layer -and \( w_i \) is the weight to input \( i \). -The activation of the neurons in the input layer is just the features (e.g. a pixel value). - -

-The simplest activation function for a neuron is the Heaviside function: - -$$ f(z) = -\begin{cases} -1, & z > 0\\ -0, & \text{otherwise} -\end{cases} -$$ - -

-A feed-forward neural network with this activation is known as a perceptron. -For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer. -This activation can be generalized to \( k \) classes (using e.g. the one-against-all strategy), -and we call these architectures multiclass perceptrons. - -

-However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and -Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function. - -

-Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU). -We will be using the sigmoid function \( \sigma(x) \): - -$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$ - -

-which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions. - -

- - -

Layers

- -
    -
  • Input
  • -
- -Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons. - -
    -
  • Hidden layer
  • -
- -We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer. -Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer. - -
    -
  • Output
  • -
- -If we were building a binary classifier, it would be sufficient with a single neuron in the output layer, -which could output 0 or 1 according to the Heaviside function. This would be an example of a hard classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a soft classifier, which outputs the probability of being in class 0 or 1. - -

-For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class. - -

-Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons \( j = 0,1,...,9 \). The activation of each output neuron \( j \) will be according to the softmax function: - -$$ P(\text{class \( j \)} \mid \text{input \( \hat{a} \)}) = \frac{\exp{(\hat{a}^T \hat{w}_j)}} -{\sum_{c=0}^{9} \exp{(\hat{a}^T \hat{w}_c)}} ,$$ - -

-i.e. each neuron \( j \) outputs the probability of being in class \( j \) given an input from the hidden layer \( \hat{a} \), with \( \hat{w}_j \) the weights of neuron \( j \) to the inputs. -The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1. -The exponent is just the weighted sum of inputs as before: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$ - -

-Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500 -weights to the output layer. - -

- - -

Weights and biases

- -

-Typically weights are initialized with small values distributed around zero, drawn from a uniform -or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless. - -

-Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range -of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron \( j \), \( b_j \): - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$ - -

-The bias weights \( \hat{b} \) are often initialized to zero, but a small value like \( 0.01 \) ensures all neurons have some output which can be backpropagated in the first training cycle. -

- - -

# building our neural network
-
-n_inputs, n_features = X_train.shape
-n_hidden_neurons = 50
-n_categories = 10
-
-# we make the weights normally distributed using numpy.random.randn
-
-# weights and bias in the hidden layer
-hidden_weights = np.random.randn(n_features, n_hidden_neurons)
-hidden_bias = np.zeros(n_hidden_neurons) + 0.01
-
-# weights and bias in the output layer
-output_weights = np.random.randn(n_hidden_neurons, n_categories)
-output_bias = np.zeros(n_categories) + 0.01
-
-

-









- -

Feed-forward pass

- -

-Denote \( F \) the number of features, \( H \) the number of hidden neurons and \( C \) the number of categories. -For each input image we calculate a weighted sum of input features (pixel values) to each neuron \( j \) in the hidden layer \( l \): - -$$ z_{j}^{l} = \sum_{i=1}^{F} w_{ij}^{l} x_i + b_{j}^{l},$$ - -

-this is then passed through our activation function - -$$ a_{j}^{l} = f(z_{j}^{l}) .$$ - -

-We calculate a weighted sum of inputs (activations in the hidden layer) to each neuron \( j \) in the output layer: - -$$ z_{j}^{L} = \sum_{i=1}^{H} w_{ij}^{L} a_{i}^{l} + b_{j}^{L}.$$ - -

-Finally we calculate the output of neuron \( j \) in the output layer using the softmax function: - -$$ a_{j}^{L} = \frac{\exp{(z_j^{L})}} -{\sum_{c=0}^{C-1} \exp{(z_c^{L})}} .$$ - -

- - -

Matrix multiplications

- -

-Since our data has the dimensions \( X = (n_{inputs}, n_{features}) \) and our weights to the hidden -layer have the dimensions -\( W_{hidden} = (n_{features}, n_{hidden}) \), -we can easily feed the network all our training data in one go by taking the matrix product - -$$ X W^{h} = (n_{inputs}, n_{hidden}),$$ - -

-and obtain a matrix that holds the weighted sum of inputs to the hidden layer -for each input image and each hidden neuron. -We also add the bias to obtain a matrix of weighted sums to the hidden layer \( Z^{h} \): - -$$ \hat{z}^{l} = \hat{X} \hat{W}^{l} + \hat{b}^{l} ,$$ - -

-meaning the same bias (1D array with size equal number of hidden neurons) is added to each input image. -This is then passed through the activation: - -$$ \hat{a}^{l} = f(\hat{z}^l) .$$ - -

-This is fed to the output layer: - -$$ \hat{z}^{L} = \hat{a}^{L} \hat{W}^{L} + \hat{b}^{L} .$$ - -

-Finally we receive our output values for each image and each category by passing it through the softmax function: - -$$ output = softmax (\hat{z}^{L}) = (n_{inputs}, n_{categories}) .$$ - -

- - -

# setup the feed-forward pass, subscript h = hidden layer
-
-def sigmoid(x):
-    return 1/(1 + np.exp(-x))
-
-def feed_forward(X):
-    # weighted sum of inputs to the hidden layer
-    z_h = np.matmul(X, hidden_weights) + hidden_bias
-    # activation in the hidden layer
-    a_h = sigmoid(z_h)
-    
-    # weighted sum of inputs to the output layer
-    z_o = np.matmul(a_h, output_weights) + output_bias
-    # softmax output
-    # axis 0 holds each input and axis 1 the probabilities of each category
-    exp_term = np.exp(z_o)
-    probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-    
-    return probabilities
-
-probabilities = feed_forward(X_train)
-print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape))
-print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0]))
-print("probabilities sum up to: " + str(probabilities[0].sum()))
-print()
-
-# we obtain a prediction by taking the class with the highest likelihood
-def predict(X):
-    probabilities = feed_forward(X)
-    return np.argmax(probabilities, axis=1)
-
-predictions = predict(X_train)
-print("predictions = (n_inputs) = " + str(predictions.shape))
-print("prediction for image 0: " + str(predictions[0]))
-print("correct label for image 0: " + str(Y_train[0]))
-
-

-









- -

Choose cost function and optimizer

- -

-To measure how well our neural network is doing we need to introduce a cost function. -We will call the function that gives the error of a single sample output the loss function, and the function -that gives the total error of our network across all samples the cost function. -A typical choice for multiclass classification is the cross-entropy loss, also known as the negative log likelihood. - -

-In multiclass classification it is common to treat each integer label as a so called one-hot vector: - -$$ y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$$ - -$$ y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$$ - -

-i.e. a binary bit string of length \( C \), where \( C = 10 \) is the number of classes in the MNIST dataset. - -

-Let \( y_{ic} \) denote the \( c \)-th component of the \( i \)-th one-hot vector. -We define the cost function \( \mathcal{C} \) as a sum over the cross-entropy loss for each point \( \hat{x}_i \) in the dataset. - -

-In the one-hot representation only one of the terms in the loss function is non-zero, namely the -probability of the correct category \( c' \) -(i.e. the category \( c' \) such that \( y_{ic'} = 1 \)). This means that the cross entropy loss only punishes you for how wrong -you got the correct label. The probability of category \( c \) is given by the softmax function. The vector \( \hat{\theta} \) represents the parameters of our network, i.e. all the weights and biases. - -

-









- -

Optimizing the cost function

- -

-The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is gradient descent and its generalizations. The idea behind gradient descent -is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a local minimum of the cost function. -Each parameter \( \theta \) is iteratively adjusted according to the rule - -$$ \theta_{i+1} = \theta_i - \eta \nabla \mathcal{C}(\theta_i) ,$$ - -

-where \( \eta \) is known as the learning rate, which controls how big a step we take towards the minimum. -This update can be repeated for any number of iterations, or until we are satisfied with the result. - -

-A simple and effective improvement is a variant called Batch Gradient Descent. -Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient -on a subset of the data called a minibatch. -If there are \( N \) data points and we have a minibatch size of \( M \), the total number of batches -is \( N/M \). -We denote each minibatch \( B_k \), with \( k = 1, 2,...,N/M \). The gradient then becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{M} \sum_{i \in B_k} \nabla \mathcal{L}_i(\theta) ,$$ - -

-i.e. instead of averaging the loss over the entire dataset, we average over a minibatch. - -

-This has two important benefits: - -

    -
  1. Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima.
  2. -
  3. It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient.
  4. -
- -The various optmization methods, with codes and algorithms, are discussed in our lectures on Gradient descent approaches. - -

- - -

Regularization

- -

-It is common to add an extra term to the cost function, proportional -to the size of the weights. This is equivalent to constraining the -size of the weights, so that they do not grow out of control. -Constraining the size of the weights means that the weights cannot -grow arbitrarily large to fit the training data, and in this way -reduces overfitting. - -

-We will measure the size of the weights using the so called L2-norm, meaning our cost function becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) + \lambda \lvert \lvert \hat{w} \rvert \rvert_2^2 -= \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}(\theta) + \lambda \sum_{ij} w_{ij}^2,$$ - -

-i.e. we sum up all the weights squared. The factor \( \lambda \) is known as a regularization parameter. - -

-In order to train the model, we need to calculate the derivative of -the cost function with respect to every bias and weight in the -network. In total our network has \( (64 + 1)\times 50=3250 \) weights in -the hidden layer and \( (50 + 1)\times 10=510 \) weights to the output -layer (\( +1 \) for the bias), and the gradient must be calculated for -every parameter. We use the backpropagation algorithm discussed -above. This is a clever use of the chain rule that allows us to -calculate the gradient efficently. - -

-









- -

Matrix multiplication

- -

-To more efficently train our network these equations are implemented using matrix operations. -The error in the output layer is calculated simply as, with \( \hat{t} \) being our targets, - -$$ \delta_L = \hat{t} - \hat{y} = (n_{inputs}, n_{categories}) .$$ - -

-The gradient for the output weights is calculated as - -$$ \nabla W_{L} = \hat{a}^T \delta_L = (n_{hidden}, n_{categories}) ,$$ - -

-where \( \hat{a} = (n_{inputs}, n_{hidden}) \). This simply means that we are summing up the gradients for each input. -Since we are going backwards we have to transpose the activation matrix. - -

-The gradient with respect to the output bias is then - -$$ \nabla \hat{b}_{L} = \sum_{i=1}^{n_{inputs}} \delta_L = (n_{categories}) .$$ - -

-The error in the hidden layer is - -$$ \Delta_h = \delta_L W_{L}^T \circ f'(z_{h}) = \delta_L W_{L}^T \circ a_{h} \circ (1 - a_{h}) = (n_{inputs}, n_{hidden}) ,$$ - -

-where \( f'(a_{h}) \) is the derivative of the activation in the hidden layer. The matrix products mean -that we are summing up the products for each neuron in the output layer. The symbol \( \circ \) denotes -the Hadamard product, meaning element-wise multiplication. - -

-This again gives us the gradients in the hidden layer: - -$$ \nabla W_{h} = X^T \delta_h = (n_{features}, n_{hidden}) ,$$ - -$$ \nabla b_{h} = \sum_{i=1}^{n_{inputs}} \delta_h = (n_{hidden}) .$$ - -

- - -

# to categorical turns our integer vector into a onehot representation
-from sklearn.metrics import accuracy_score
-
-# one-hot in numpy
-def to_categorical_numpy(integer_vector):
-    n_inputs = len(integer_vector)
-    n_categories = np.max(integer_vector) + 1
-    onehot_vector = np.zeros((n_inputs, n_categories))
-    onehot_vector[range(n_inputs), integer_vector] = 1
-    
-    return onehot_vector
-
-#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test)
-Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test)
-
-def feed_forward_train(X):
-    # weighted sum of inputs to the hidden layer
-    z_h = np.matmul(X, hidden_weights) + hidden_bias
-    # activation in the hidden layer
-    a_h = sigmoid(z_h)
-    
-    # weighted sum of inputs to the output layer
-    z_o = np.matmul(a_h, output_weights) + output_bias
-    # softmax output
-    # axis 0 holds each input and axis 1 the probabilities of each category
-    exp_term = np.exp(z_o)
-    probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-    
-    # for backpropagation need activations in hidden and output layers
-    return a_h, probabilities
-
-def backpropagation(X, Y):
-    a_h, probabilities = feed_forward_train(X)
-    
-    # error in the output layer
-    error_output = probabilities - Y
-    # error in the hidden layer
-    error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h)
-    
-    # gradients for the output layer
-    output_weights_gradient = np.matmul(a_h.T, error_output)
-    output_bias_gradient = np.sum(error_output, axis=0)
-    
-    # gradient for the hidden layer
-    hidden_weights_gradient = np.matmul(X.T, error_hidden)
-    hidden_bias_gradient = np.sum(error_hidden, axis=0)
-
-    return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient
-
-print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
-
-eta = 0.01
-lmbd = 0.01
-for i in range(1000):
-    # calculate gradients
-    dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot)
-    
-    # regularization term gradients
-    dWo += lmbd * output_weights
-    dWh += lmbd * hidden_weights
-    
-    # update weights and biases
-    output_weights -= eta * dWo
-    output_bias -= eta * dBo
-    hidden_weights -= eta * dWh
-    hidden_bias -= eta * dBh
-
-print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train)))
-
-

-









- -

Improving performance

- -

-As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image. -In order to obtain a network that does something useful, we will have to do a bit more work. - -

-The choice of hyperparameters such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a grid-search is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates \( \eta = 10^{-6}, 10^{-5},...,10^{-1} \) with different regularization parameters \( \lambda = 10^{-6},...,10^{-0} \). - -

-Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an iteration, and a full training period -going through the entire dataset (\( n/M \) batches) an epoch. - -

-If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers. -Andrew Ng goes through some of these considerations in this video. You can find a summary of the video here. - -

-









- -

Full object-oriented implementation

- -

-It is very natural to think of the network as an object, with specific instances of the network -being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below. - -

- - -

class NeuralNetwork:
-    def __init__(
-        self,
-        X_data,
-        Y_data,
-        n_hidden_neurons=50,
-        n_categories=10,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-
-    ):
-        self.X_data_full = X_data
-        self.Y_data_full = Y_data
-
-        self.n_inputs = X_data.shape[0]
-        self.n_features = X_data.shape[1]
-        self.n_hidden_neurons = n_hidden_neurons
-        self.n_categories = n_categories
-
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-
-        self.create_biases_and_weights()
-
-    def create_biases_and_weights(self):
-        self.hidden_weights = np.random.randn(self.n_features, self.n_hidden_neurons)
-        self.hidden_bias = np.zeros(self.n_hidden_neurons) + 0.01
-
-        self.output_weights = np.random.randn(self.n_hidden_neurons, self.n_categories)
-        self.output_bias = np.zeros(self.n_categories) + 0.01
-
-    def feed_forward(self):
-        # feed-forward for training
-        self.z_h = np.matmul(self.X_data, self.hidden_weights) + self.hidden_bias
-        self.a_h = sigmoid(self.z_h)
-
-        self.z_o = np.matmul(self.a_h, self.output_weights) + self.output_bias
-
-        exp_term = np.exp(self.z_o)
-        self.probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-
-    def feed_forward_out(self, X):
-        # feed-forward for output
-        z_h = np.matmul(X, self.hidden_weights) + self.hidden_bias
-        a_h = sigmoid(z_h)
-
-        z_o = np.matmul(a_h, self.output_weights) + self.output_bias
-        
-        exp_term = np.exp(z_o)
-        probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)
-        return probabilities
-
-    def backpropagation(self):
-        error_output = self.probabilities - self.Y_data
-        error_hidden = np.matmul(error_output, self.output_weights.T) * self.a_h * (1 - self.a_h)
-
-        self.output_weights_gradient = np.matmul(self.a_h.T, error_output)
-        self.output_bias_gradient = np.sum(error_output, axis=0)
-
-        self.hidden_weights_gradient = np.matmul(self.X_data.T, error_hidden)
-        self.hidden_bias_gradient = np.sum(error_hidden, axis=0)
-
-        if self.lmbd > 0.0:
-            self.output_weights_gradient += self.lmbd * self.output_weights
-            self.hidden_weights_gradient += self.lmbd * self.hidden_weights
-
-        self.output_weights -= self.eta * self.output_weights_gradient
-        self.output_bias -= self.eta * self.output_bias_gradient
-        self.hidden_weights -= self.eta * self.hidden_weights_gradient
-        self.hidden_bias -= self.eta * self.hidden_bias_gradient
-
-    def predict(self, X):
-        probabilities = self.feed_forward_out(X)
-        return np.argmax(probabilities, axis=1)
-
-    def predict_probabilities(self, X):
-        probabilities = self.feed_forward_out(X)
-        return probabilities
-
-    def train(self):
-        data_indices = np.arange(self.n_inputs)
-
-        for i in range(self.epochs):
-            for j in range(self.iterations):
-                # pick datapoints with replacement
-                chosen_datapoints = np.random.choice(
-                    data_indices, size=self.batch_size, replace=False
-                )
-
-                # minibatch training data
-                self.X_data = self.X_data_full[chosen_datapoints]
-                self.Y_data = self.Y_data_full[chosen_datapoints]
-
-                self.feed_forward()
-                self.backpropagation()
-
-

-









- -

Evaluate model performance on test data

- -

-To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data. -We measure the performance of the network using the accuracy score. -The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of \( 1 \). - -$$ \text{Accuracy} = \frac{\sum_{i=1}^n I(\hat{y}_i = y_i)}{n} ,$$ - -

-where \( I \) is the indicator function, \( 1 \) if \( \hat{y}_i = y_i \) and \( 0 \) otherwise. - -

- - -

epochs = 100
-batch_size = 100
-
-dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
-                    n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
-dnn.train()
-test_predict = dnn.predict(X_test)
-
-# accuracy score from scikit library
-print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
-
-# equivalent in numpy
-def accuracy_score_numpy(Y_test, Y_pred):
-    return np.sum(Y_test == Y_pred) / len(Y_test)
-
-#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict))
-
-

-









- -

Adjust hyperparameters

- -

-We now perform a grid search to find the optimal hyperparameters for the network. -Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around \( 98\% \) (\( 2\% \) error rate). - -

- - -

eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-# store the models for later use
-DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-
-# grid search
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,
-                            n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)
-        dnn.train()
-        
-        DNN_numpy[i][j] = dnn
-        
-        test_predict = dnn.predict(X_test)
-        
-        print("Learning rate  = ", eta)
-        print("Lambda = ", lmbd)
-        print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict))
-        print()
-
-

-









- -

Visualization

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, you can also do this with matplotlib imshow
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        dnn = DNN_numpy[i][j]
-        
-        train_pred = dnn.predict(X_train) 
-        test_pred = dnn.predict(X_test)
-
-        train_accuracy[i][j] = accuracy_score(Y_train, train_pred)
-        test_accuracy[i][j] = accuracy_score(Y_test, test_pred)
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

-









- -

scikit-learn implementation

- -

-scikit-learn focuses more -on traditional machine learning methods, such as regression, -clustering, decision trees, etc. As such, it has only two types of -neural networks: Multi Layer Perceptron outputting continuous values, -MPLRegressor, and Multi Layer Perceptron outputting labels, -MLPClassifier. We will see how simple it is to use these classes. - -

-scikit-learn implements a few improvements from our neural network, -such as early stopping, a varying learning rate, different -optimization methods, etc. We would therefore expect a better -performance overall. - -

- - -

from sklearn.neural_network import MLPClassifier
-# store models for later use
-DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',
-                            alpha=lmbd, learning_rate_init=eta, max_iter=epochs)
-        dnn.fit(X_train, Y_train)
-        
-        DNN_scikit[i][j] = dnn
-        
-        print("Learning rate  = ", eta)
-        print("Lambda = ", lmbd)
-        print("Accuracy score on test set: ", dnn.score(X_test, Y_test))
-        print()
-
-

-









- -

Visualization

-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        dnn = DNN_scikit[i][j]
-        
-        train_pred = dnn.predict(X_train) 
-        test_pred = dnn.predict(X_test)
-
-        train_accuracy[i][j] = accuracy_score(Y_train, train_pred)
-        test_accuracy[i][j] = accuracy_score(Y_test, test_pred)
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

-









- -

Building neural networks in Tensorflow and Keras

- -

-Now we want to build on the experience gained from our neural network implementation in NumPy and scikit-learn -and use it to construct a neural network in Tensorflow. Once we have constructed a neural network in NumPy -and Tensorflow, building one in Keras is really quite trivial, though the performance may suffer. - -

-In our previous example we used only one hidden layer, and in this we will use two. From this it should be quite -clear how to build one using an arbitrary number of hidden layers, using data structures such as Python lists or -NumPy arrays. - -

-









- -

Tensorflow

- -

-Tensorflow is an open source library machine learning library -developed by the Google Brain team for internal use. It was released -under the Apache 2.0 open source license in November 9, 2015. - -

-Tensorflow is a computational framework that allows you to construct -machine learning models at different levels of abstraction, from -high-level, object-oriented APIs like Keras, down to the C++ kernels -that Tensorflow is built upon. The higher levels of abstraction are -simpler to use, but less flexible, and our choice of implementation -should reflect the problems we are trying to solve. - -

-Tensorflow uses so-called graphs to represent your computation -in terms of the dependencies between individual operations, such that you first build a Tensorflow graph -to represent your model, and then create a Tensorflow session to run the graph. - -

-In this guide we will analyze the same data as we did in our NumPy and -scikit-learn tutorial, gathered from the MNIST database of images. We -will give an introduction to the lower level Python Application -Program Interfaces (APIs), and see how we use them to build our graph. -Then we will build (effectively) the same graph in Keras, to see just -how simple solving a machine learning problem can be. - -

-To install tensorflow on Unix/Linux systems, use pip as -

- - -

pip3 install tensorflow
-
-

-and/or if you use anaconda, just write (or install from the graphical user interface) -

- - -

conda install tensorflow
-
-

-









- -

Collect and pre-process data

- -

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# flatten the image
-# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64
-n_inputs = len(inputs)
-inputs = inputs.reshape(n_inputs, -1)
-print("X = (n_inputs, n_features) = " + str(inputs.shape))
-
-
-# choose some random images to display
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-

- - -

from keras.utils import to_categorical
-from sklearn.model_selection import train_test_split
-
-# one-hot representation of labels
-labels = to_categorical(labels)
-
-# split into train and test data
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-

-









- -

Using TensorFlow backend

- -
    -
  1. Define model and architecture
  2. -
  3. Choose cost function and optimizer
  4. -
- -

- - -

import tensorflow as tf
-
-class NeuralNetworkTensorflow:
-    def __init__(
-        self,
-        X_train,
-        Y_train,
-        X_test,
-        Y_test,
-        n_neurons_layer1=100,
-        n_neurons_layer2=50,
-        n_categories=2,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-    ):
-        
-        # keep track of number of steps
-        self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')
-        
-        self.X_train = X_train
-        self.Y_train = Y_train
-        self.X_test = X_test
-        self.Y_test = Y_test
-        
-        self.n_inputs = X_train.shape[0]
-        self.n_features = X_train.shape[1]
-        self.n_neurons_layer1 = n_neurons_layer1
-        self.n_neurons_layer2 = n_neurons_layer2
-        self.n_categories = n_categories
-        
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-        
-        # build network piece by piece
-        # name scopes (with) are used to enforce creation of new variables
-        # https://www.tensorflow.org/guide/variables
-        self.create_placeholders()
-        self.create_DNN()
-        self.create_loss()
-        self.create_optimiser()
-        self.create_accuracy()
-    
-    def create_placeholders(self):
-        # placeholders are fine here, but "Datasets" are the preferred method
-        # of streaming data into a model
-        with tf.name_scope('data'):
-            self.X = tf.placeholder(tf.float32, shape=(None, self.n_features), name='X_data')
-            self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')
-    
-    def create_DNN(self):
-        with tf.name_scope('DNN'):
-            # the weights are stored to calculate regularization loss later
-            
-            # Fully connected layer 1
-            self.W_fc1 = self.weight_variable([self.n_features, self.n_neurons_layer1], name='fc1', dtype=tf.float32)
-            b_fc1 = self.bias_variable([self.n_neurons_layer1], name='fc1', dtype=tf.float32)
-            a_fc1 = tf.nn.sigmoid(tf.matmul(self.X, self.W_fc1) + b_fc1)
-            
-            # Fully connected layer 2
-            self.W_fc2 = self.weight_variable([self.n_neurons_layer1, self.n_neurons_layer2], name='fc2', dtype=tf.float32)
-            b_fc2 = self.bias_variable([self.n_neurons_layer2], name='fc2', dtype=tf.float32)
-            a_fc2 = tf.nn.sigmoid(tf.matmul(a_fc1, self.W_fc2) + b_fc2)
-            
-            # Output layer
-            self.W_out = self.weight_variable([self.n_neurons_layer2, self.n_categories], name='out', dtype=tf.float32)
-            b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)
-            self.z_out = tf.matmul(a_fc2, self.W_out) + b_out
-    
-    def create_loss(self):
-        with tf.name_scope('loss'):
-            softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))
-            
-            regularizer_loss_fc1 = tf.nn.l2_loss(self.W_fc1)
-            regularizer_loss_fc2 = tf.nn.l2_loss(self.W_fc2)
-            regularizer_loss_out = tf.nn.l2_loss(self.W_out)
-            regularizer_loss = self.lmbd*(regularizer_loss_fc1 + regularizer_loss_fc2 + regularizer_loss_out)
-            
-            self.loss = softmax_loss + regularizer_loss
-
-    def create_accuracy(self):
-        with tf.name_scope('accuracy'):
-            probabilities = tf.nn.softmax(self.z_out)
-            predictions = tf.argmax(probabilities, axis=1)
-            labels = tf.argmax(self.Y, axis=1)
-            
-            correct_predictions = tf.equal(predictions, labels)
-            correct_predictions = tf.cast(correct_predictions, tf.float32)
-            self.accuracy = tf.reduce_mean(correct_predictions)
-    
-    def create_optimiser(self):
-        with tf.name_scope('optimizer'):
-            self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)
-            
-    def weight_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.truncated_normal(shape, stddev=0.1)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def bias_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.constant(0.1, shape=shape)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def fit(self):
-        data_indices = np.arange(self.n_inputs)
-
-        with tf.Session() as sess:
-            sess.run(tf.global_variables_initializer())
-            for i in range(self.epochs):
-                for j in range(self.iterations):
-                    chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)
-                    batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]
-            
-                    sess.run([DNN.loss, DNN.optimizer],
-                        feed_dict={DNN.X: batch_X,
-                                   DNN.Y: batch_Y})
-                    accuracy = sess.run(DNN.accuracy,
-                        feed_dict={DNN.X: batch_X,
-                                   DNN.Y: batch_Y})
-                    step = sess.run(DNN.global_step)
-    
-            self.train_loss, self.train_accuracy = sess.run([DNN.loss, DNN.accuracy],
-                feed_dict={DNN.X: self.X_train,
-                           DNN.Y: self.Y_train})
-        
-            self.test_loss, self.test_accuracy = sess.run([DNN.loss, DNN.accuracy],
-                feed_dict={DNN.X: self.X_test,
-                           DNN.Y: self.Y_test})
-
-

-









- -

Optimizing and using gradient descent

- -

- - -

epochs = 100
-batch_size = 100
-n_neurons_layer1 = 100
-n_neurons_layer2 = 50
-n_categories = 10
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-
-

- - -

DNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        DNN = NeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,
-                                      n_neurons_layer1, n_neurons_layer2, n_categories,
-                                      epochs=epochs, batch_size=batch_size, eta=eta, lmbd=lmbd)
-        DNN.fit()
-        
-        DNN_tf[i][j] = DNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % DNN.test_accuracy)
-        print()
-
-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        DNN = DNN_tf[i][j]
-
-        train_accuracy[i][j] = DNN.train_accuracy
-        test_accuracy[i][j] = DNN.test_accuracy
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

- - -

# optional
-# we can use log files to visualize our graph in Tensorboard
-writer = tf.summary.FileWriter('logs/')
-writer.add_graph(tf.get_default_graph())
-
-

-









- -

Using Keras

- -

-Keras is a high level neural network -that supports Tensorflow, CTNK and Theano as backends. -If you have Tensorflow installed Keras is available through the tf.keras module. -If you have Anaconda installed you may run the following command -

- - -

conda install keras
-
-

-Alternatively, if you have Tensorflow or one of the other supported backends install you may use the pip package manager: - -

- - -

pip3 install keras
-
-

-or look up the instructions here. - -

- - -

from keras.models import Sequential
-from keras.layers import Dense
-from keras.regularizers import l2
-from keras.optimizers import SGD
-
-def create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, eta, lmbd):
-    model = Sequential()
-    model.add(Dense(n_neurons_layer1, activation='sigmoid', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_neurons_layer2, activation='sigmoid', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_categories, activation='softmax'))
-    
-    sgd = SGD(lr=eta)
-    model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
-    
-    return model
-
-

- - -

DNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        DNN = create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories,
-                                         eta=eta, lmbd=lmbd)
-        DNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)
-        scores = DNN.evaluate(X_test, Y_test)
-        
-        DNN_keras[i][j] = DNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % scores[1])
-        print()
-
-

- - -

# optional
-# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        DNN = DNN_keras[i][j]
-
-        train_accuracy[i][j] = DNN.evaluate(X_train, Y_train)[1]
-        test_accuracy[i][j] = DNN.evaluate(X_test, Y_test)[1]
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

- - -

Which activation function should I use?

- -

-The Back propagation algorithm we derived above works by going from -the output layer to the input layer, propagating the error gradient on -the way. Once the algorithm has computed the gradient of the cost -function with regards to each parameter in the network, it uses these -gradients to update each parameter with a Gradient Descent (GD) step. - -

-Unfortunately for us, the gradients often get smaller and smaller as the -algorithm progresses down to the first hidden layers. As a result, the -GD update leaves the lower layer connection weights -virtually unchanged, and training never converges to a good -solution. This is known in the literature as -the vanishing gradients problem. - -

-In other cases, the opposite can happen, namely the the gradients can grow bigger and -bigger. The result is that many of the layers get large updates of the -weights the -algorithm diverges. This is the exploding gradients problem, which is -mostly encountered in recurrent neural networks. More generally, deep -neural networks suffer from unstable gradients, different layers may -learn at widely different speeds - -

- - -

Is the Logistic activation function (Sigmoid) our choice?

- -

-Although this unfortunate behavior has been empirically observed for -quite a while (it was one of the reasons why deep neural networks were -mostly abandoned for a long time), it is only around 2010 that -significant progress was made in understanding it. - -

-A paper titled Understanding the Difficulty of Training Deep -Feedforward Neural Networks by Xavier Glorot and Yoshua Bengio found that -the problems with the popular logistic -sigmoid activation function and the weight initialization technique -that was most popular at the time, namely random initialization using -a normal distribution with a mean of 0 and a standard deviation of -1. - -

-They showed that with this activation function and this -initialization scheme, the variance of the outputs of each layer is -much greater than the variance of its inputs. Going forward in the -network, the variance keeps increasing after each layer until the -activation function saturates at the top layers. This is actually made -worse by the fact that the logistic function has a mean of 0.5, not 0 -(the hyperbolic tangent function has a mean of 0 and behaves slightly -better than the logistic function in deep networks). - -

-









- -

The derivative of the Logistic funtion

- -

-Looking at the logistic activation function, when inputs become large -(negative or positive), the function saturates at 0 or 1, with a -derivative extremely close to 0. Thus when backpropagation kicks in, -it has virtually no gradient to propagate back through the network, -and what little gradient exists keeps getting diluted as -backpropagation progresses down through the top layers, so there is -really nothing left for the lower layers. - -

-In their paper, Glorot and Bengio propose a way to significantly -alleviate this problem. We need the signal to flow properly in both -directions: in the forward direction when making predictions, and in -the reverse direction when backpropagating gradients. We don’t want -the signal to die out, nor do we want it to explode and saturate. For -the signal to flow properly, the authors argue that we need the -variance of the outputs of each layer to be equal to the variance of -its inputs, and we also need the gradients to have equal variance -before and after flowing through a layer in the reverse direction. - -

-One of the insights in the 2010 paper by Glorot and Bengio was that -the vanishing/exploding gradients problems were in part due to a poor -choice of activation function. Until then most people had assumed that -if Nature had chosen to use roughly sigmoid activation functions in -biological neurons, they must be an excellent choice. But it turns out -that other activation functions behave much better in deep neural -networks, in particular the ReLU activation function, mostly because -it does not saturate for positive values (and also because it is quite -fast to compute). - -

-









- -

The RELU function family

- -

-The ReLU activation function suffers from a problem known as the dying -ReLUs: during training, some neurons effectively die, meaning they -stop outputting anything other than 0. - -

-In some cases, you may find that half of your network’s neurons are -dead, especially if you used a large learning rate. During training, -if a neuron’s weights get updated such that the weighted sum of the -neuron’s inputs is negative, it will start outputting 0. When this -happen, the neuron is unlikely to come back to life since the gradient -of the ReLU function is 0 when its input is negative. - -

-To solve this problem, nowadays practitioners use a variant of the ReLU -function, such as the leaky ReLU discussed above or the so-called -exponential linear unit (ELU) function - -$$ -ELU(z) = \left\{\begin{array}{cc} \alpha\left( \exp{(z)}-1\right) & z < 0,\\ z & z \ge 0.\end{array}\right. -$$ - -

-









- -

Which activation function should we use?

- -

-In general it seems that the ELU activation function is better than -the leaky ReLU function (and its variants), which is better than -ReLU. ReLU performs better than \( \tanh \) which in turn performs better -than the logistic function. - -

-If runtime -performance is an issue, then you may opt for the leaky ReLU function over the -ELU function If you don’t -want to tweak yet another hyperparameter, you may just use the default -\( \alpha \) of \( 0.01 \) for the leaky ReLU, and \( 1 \) for ELU. If you have -spare time and computing power, you can use cross-validation or -bootstrap to evaluate other activation functions. - -

- - -

A top-down perspective on Neural networks

- -

-The first thing we would like to do is divide the data into two or three -parts. A training set, a validation or dev (development) set, and a -test set. The test set is the data on which we want to make -predictions. The dev set is a subset of the training data we use to -check how well we are doing out-of-sample, after training the model on -the training dataset. We use the validation error as a proxy for the -test error in order to make tweaks to our model. It is crucial that we -do not use any of the test data to train the algorithm. This is a -cardinal sin in ML. Then: - -

    -
  • Estimate optimal error rate
  • -
  • Minimize underfitting (bias) on training data set.
  • -
  • Make sure you are not overfitting.
  • -
- -If the validation and test sets are drawn from the same distributions, -then a good performance on the validation set should lead to similarly -good performance on the test set. - -

-However, sometimes -the training data and test data differ in subtle ways because, for -example, they are collected using slightly different methods, or -because it is cheaper to collect data in one way versus another. In -this case, there can be a mismatch between the training and test -data. This can lead to the neural network overfitting these small -differences between the test and training sets, and a poor performance -on the test set despite having a good performance on the validation -set. To rectify this, Andrew Ng suggests making two validation or dev -sets, one constructed from the training data and one constructed from -the test data. The difference between the performance of the algorithm -on these two validation sets quantifies the train-test mismatch. This -can serve as another important diagnostic when using DNNs for -supervised learning. - -

-









- -

Limitations of supervised learning with deep networks

- -

-Like all statistical methods, supervised learning using neural -networks has important limitations. This is especially important when -one seeks to apply these methods, especially to physics problems. Like -all tools, DNNs are not a universal solution. Often, the same or -better performance on a task can be achieved by using a few -hand-engineered features (or even a collection of random -features). - -

-Here we list some of the important limitations of supervised neural network based models. - -

    -
  • Need labeled data. All supervised learning methods, DNNs for supervised learning require labeled data. Often, labeled data is harder to acquire than unlabeled data (e.g. one must pay for human experts to label images).
  • -
  • Supervised neural networks are extremely data intensive. DNNs are data hungry. They perform best when data is plentiful. This is doubly so for supervised methods where the data must also be labeled. The utility of DNNs is extremely limited if data is hard to acquire or the datasets are small (hundreds to a few thousand samples). In this case, the performance of other methods that utilize hand-engineered features can exceed that of DNNs.
  • -
  • Homogeneous data. Almost all DNNs deal with homogeneous data of one type. It is very hard to design architectures that mix and match data types (i.e. some continuous variables, some discrete variables, some time series). In applications beyond images, video, and language, this is often what is required. In contrast, ensemble models like random forests or gradient-boosted trees have no difficulty handling mixed data types.
  • -
  • Many problems are not about prediction. In natural science we are often interested in learning something about the underlying distribution that generates the data. In this case, it is often difficult to cast these ideas in a supervised learning setting. While the problems are related, it is possible to make good predictions with a wrong model. The model might or might not be useful for understanding the underlying science.
  • -
- -Some of these remarks are particular to DNNs, others are shared by all supervised learning methods. This motivates the use of unsupervised methods which in part circumvent these problems. - -

-









- -

Convolutional Neural Networks (recognizing images)

- -

-Convolutional Neural Networks (CNN) are very similar to ordinary Neural Networks. - -

-They are made up of neurons that have learnable weights and -biases. Each neuron receives some inputs, performs a dot product and -optionally follows it with a non-linearity. The whole network still -expresses a single differentiable score function: from the raw image -pixels on one end to class scores at the other. And they still have a -loss function (for example Softmax) on the last (fully-connected) layer -and all the tips/tricks we developed for learning regular Neural -Networks still apply (back propagation, gradient descent etc etc). - -

-What is the difference? CNN architectures make the explicit assumption that -the inputs are images, which allows us to encode certain properties -into the architecture. These then make the forward function more -efficient to implement and vastly reduce the amount of parameters in -the network. - -

-Here we provide only a superficial overview, for the more interested, we recommend highly the course -IN5400 – Machine Learning for Image Analysis -and the slides of CS231. - -

-









- -

Regular NNs don’t scale well to full images

- -

-As an example, consider -an image of size \( 32\times 32\times 3 \) (32 wide, 32 high, 3 color channels), so a -single fully-connected neuron in a first hidden layer of a regular -Neural Network would have \( 32\times 32\times 3 = 3072 \) weights. This amount still -seems manageable, but clearly this fully-connected structure does not -scale to larger images. For example, an image of more respectable -size, say \( 200\times 200\times 3 \), would lead to neurons that have -\( 200\times 200\times 3 = 120,000 \) weights. - -

-We could have -several such neurons, and the parameters would add up quickly! Clearly, -this full connectivity is wasteful and the huge number of parameters -would quickly lead to possible overfitting. - -

-

-
-

Figure 1: A regular 3-layer Neural Network.

-

-
- -

-









- -

3D volumes of neurons

- -

-Convolutional Neural Networks take advantage of the fact that the -input consists of images and they constrain the architecture in a more -sensible way. - -

-In particular, unlike a regular Neural Network, the -layers of a CNN have neurons arranged in 3 dimensions: width, -height, depth. (Note that the word depth here refers to the third -dimension of an activation volume, not to the depth of a full Neural -Network, which can refer to the total number of layers in a network.) - -

-To understand it better, the above example of an image -with an input volume of -activations has dimensions \( 32\times 32\times 3 \) (width, height, -depth respectively). - -

-The neurons in a layer will -only be connected to a small region of the layer before it, instead of -all of the neurons in a fully-connected manner. Moreover, the final -output layer could for this specific image have dimensions \( 1\times 1 times 10 \), -because by the -end of the CNN architecture we will reduce the full image into a -single vector of class scores, arranged along the depth -dimension. - -

-

-
-

Figure 2: A CNN arranges its neurons in three dimensions (width, height, depth), as visualized in one of the layers. Every layer of a CNN transforms the 3D input volume to a 3D output volume of neuron activations. In this example, the red input layer holds the image, so its width and height would be the dimensions of the image, and the depth would be 3 (Red, Green, Blue channels).

-

-
- -

- - -

Layers used to build CNNs

- -

-A simple CNN is a sequence of layers, and every layer of a CNN -transforms one volume of activations to another through a -differentiable function. We use three main types of layers to build -CNN architectures: Convolutional Layer, Pooling Layer, and -Fully-Connected Layer (exactly as seen in regular Neural Networks). We -will stack these layers to form a full CNN architecture. - -

-A simple CNN for image classification could have the architecture: - -

    -
  • INPUT (\( 32\times 32 \times 3 \)) will hold the raw pixel values of the image, in this case an image of width 32, height 32, and with three color channels R,G,B.
  • -
  • CONV (convolutional )layer will compute the output of neurons that are connected to local regions in the input, each computing a dot product between their weights and a small region they are connected to in the input volume. This may result in volume such as \( [32\times 32\times 12] \) if we decided to use 12 filters.
  • -
  • RELU layer will apply an elementwise activation function, such as the \( max(0,x) \) thresholding at zero. This leaves the size of the volume unchanged (\( [32\times 32\times 12] \)).
  • -
  • POOL (pooling) layer will perform a downsampling operation along the spatial dimensions (width, height), resulting in volume such as \( [16\times 16\times 12] \).
  • -
  • FC (i.e. fully-connected) layer will compute the class scores, resulting in volume of size \( [1\times 1\times 10] \), where each of the 10 numbers correspond to a class score, such as among the 10 categories of the MNIST images we considered above . As with ordinary Neural Networks and as the name implies, each neuron in this layer will be connected to all the numbers in the previous volume.
  • -
- -









- -

Transforming images

- -

-CNNs transform the original image layer by layer from the original -pixel values to the final class scores. - -

-Observe that some layers contain -parameters and other don’t. In particular, the CNN layers perform -transformations that are a function of not only the activations in the -input volume, but also of the parameters (the weights and biases of -the neurons). On the other hand, the RELU/POOL layers will implement a -fixed function. The parameters in the CONV/FC layers will be trained -with gradient descent so that the class scores that the CNN computes -are consistent with the labels in the training set for each image. - -

-









- -

CNNs in brief

- -

-In summary: - -

    -
  • A CNN architecture is in the simplest case a list of Layers that transform the image volume into an output volume (e.g. holding the class scores)
  • -
  • There are a few distinct types of Layers (e.g. CONV/FC/RELU/POOL are by far the most popular)
  • -
  • Each Layer accepts an input 3D volume and transforms it to an output 3D volume through a differentiable function
  • -
  • Each Layer may or may not have parameters (e.g. CONV/FC do, RELU/POOL don’t)
  • -
  • Each Layer may or may not have additional hyperparameters (e.g. CONV/FC/POOL do, RELU doesn’t)
  • -
- -For more material on convolutional networks, we strongly recommend -the course -IN5400 – Machine Learning for Image Analysis -and the slides of CS231 which is taught at Stanford University (consistently ranked as one of the top computer science programs in the world). Michael Nielsen's book is a must read, in particular chapter 6 which deals with CNNs. - -

-









- -

CNNs in more detail, building convolutional neural networks in Tensorflow and Keras

- -

-As discussed above, CNNs are neural networks built from the assumption that the inputs -to the network are 2D images. This is important because the number of features or pixels in images -grows very fast with the image size, and an enormous number of weights and biases are needed in order to build an accurate network. - -

-As before, we still have our input, a hidden layer and an output. What's novel about convolutional networks -are the convolutional and pooling layers stacked in pairs between the input and the hidden layer. -In addition, the data is no longer represented as a 2D feature matrix, instead each input is a number of 2D -matrices, typically 1 for each color dimension (Red, Green, Blue). - -

-









- -

Setting it up

- -

-It means that to represent the entire -dataset of images, we require a 4D matrix or tensor. This tensor has the dimensions: -$$ -(n_{inputs},\, n_{pixels, width},\, n_{pixels, height},\, depth) . -$$ - -

-









- -

The MNIST dataset again

- -

-The MNIST dataset consists of grayscale images with a pixel size of -\( 28\times 28 \), meaning we require \( 28 \times 28 = 724 \) weights to each -neuron in the first hidden layer. - -

-If we were to analyze images of size \( 128\times 128 \) we would require -\( 128 \times 128 = 16384 \) weights to each neuron. Even worse if we were -dealing with color images, as most images are, we have an image matrix -of size \( 128\times 128 \) for each color dimension (Red, Green, Blue), -meaning 3 times the number of weights \( = 49152 \) are required for every -single neuron in the first hidden layer. - -

-









- -

Strong correlations

-Images typically have strong local correlations, meaning that a small -part of the image varies little from its neighboring regions. If for -example we have an image of a blue car, we can roughly assume that a -small blue part of the image is surrounded by other blue regions. - -

-Therefore, instead of connecting every single pixel to a neuron in the -first hidden layer, as we have previously done with deep neural -networks, we can instead connect each neuron to a small part of the -image (in all 3 RGB depth dimensions). The size of each small area is -fixed, and known as a receptive. - -

- - -

Layers of a CNN

-The layers of a convolutional neural network arrange neurons in 3D: width, height and depth. -The input image is typically a square matrix of depth 3. - -

-A convolution is performed on the image which outputs -a 3D volume of neurons. The weights to the input are arranged in a number of 2D matrices, known as filters. - -

-Each filter slides along the input image, taking the dot product -between each small part of the image and the filter, in all depth -dimensions. This is then passed through a non-linear function, -typically the Rectified Linear (ReLu) function, which serves as the -activation of the neurons in the first convolutional layer. This is -further passed through a pooling layer, which reduces the size of the -convolutional layer, e.g. by taking the maximum or average across some -small regions, and this serves as input to the next convolutional -layer. - -

-









- -

Systematic reduction

- -

-By systematically reducing the size of the input volume, through -convolution and pooling, the network should create representations of -small parts of the input, and then from them assemble representations -of larger areas. The final pooling layer is flattened to serve as -input to a hidden layer, such that each neuron in the final pooling -layer is connected to every single neuron in the hidden layer. This -then serves as input to the output layer, e.g. a softmax output for -classification. - -

-









- -

Prerequisites: Collect and pre-process data

-

- - -

# import necessary packages
-import numpy as np
-import matplotlib.pyplot as plt
-from sklearn import datasets
-
-
-# ensure the same random numbers appear every time
-np.random.seed(0)
-
-# display images in notebook
-%matplotlib inline
-plt.rcParams['figure.figsize'] = (12,12)
-
-
-# download MNIST dataset
-digits = datasets.load_digits()
-
-# define inputs and labels
-inputs = digits.images
-labels = digits.target
-
-# RGB images have a depth of 3
-# our images are grayscale so they should have a depth of 1
-inputs = inputs[:,:,:,np.newaxis]
-
-print("inputs = (n_inputs, pixel_width, pixel_height, depth) = " + str(inputs.shape))
-print("labels = (n_inputs) = " + str(labels.shape))
-
-
-# choose some random images to display
-n_inputs = len(inputs)
-indices = np.arange(n_inputs)
-random_indices = np.random.choice(indices, size=5)
-
-for i, image in enumerate(digits.images[random_indices]):
-    plt.subplot(1, 5, i+1)
-    plt.axis('off')
-    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
-    plt.title("Label: %d" % digits.target[random_indices[i]])
-plt.show()
-
-

-









- -

Importing Keras and Tensorflow

-

- - -

from keras.utils import to_categorical
-from sklearn.model_selection import train_test_split
-
-# representation of labels
-labels = to_categorical(labels)
-
-# split into train and test data
-# one-liner from scikit-learn library
-train_size = 0.8
-test_size = 1 - train_size
-X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,
-                                                    test_size=test_size)
-
-

-









- -

Using TensorFlow backend

- -

-We need to define model and architecture and choose cost function and optmizer. -

- - -

import tensorflow as tf
-
-class ConvolutionalNeuralNetworkTensorflow:
-    def __init__(
-        self,
-        X_train,
-        Y_train,
-        X_test,
-        Y_test,
-        n_filters=10,
-        n_neurons_connected=50,
-        n_categories=10,
-        receptive_field=3,
-        stride=1,
-        padding=1,
-        epochs=10,
-        batch_size=100,
-        eta=0.1,
-        lmbd=0.0,
-    ):
-        
-        self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')
-        
-        self.X_train = X_train
-        self.Y_train = Y_train
-        self.X_test = X_test
-        self.Y_test = Y_test
-        
-        self.n_inputs, self.input_width, self.input_height, self.depth = X_train.shape
-        
-        self.n_filters = n_filters
-        self.n_downsampled = int(self.input_width*self.input_height*n_filters / 4)
-        self.n_neurons_connected = n_neurons_connected
-        self.n_categories = n_categories
-        
-        self.receptive_field = receptive_field
-        self.stride = stride
-        self.strides = [stride, stride, stride, stride]
-        self.padding = padding
-        
-        self.epochs = epochs
-        self.batch_size = batch_size
-        self.iterations = self.n_inputs // self.batch_size
-        self.eta = eta
-        self.lmbd = lmbd
-        
-        self.create_placeholders()
-        self.create_CNN()
-        self.create_loss()
-        self.create_optimiser()
-        self.create_accuracy()
-    
-    def create_placeholders(self):
-        with tf.name_scope('data'):
-            self.X = tf.placeholder(tf.float32, shape=(None, self.input_width, self.input_height, self.depth), name='X_data')
-            self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')
-    
-    def create_CNN(self):
-        with tf.name_scope('CNN'):
-            
-            # Convolutional layer
-            self.W_conv = self.weight_variable([self.receptive_field, self.receptive_field, self.depth, self.n_filters], name='conv', dtype=tf.float32)
-            b_conv = self.weight_variable([self.n_filters], name='conv', dtype=tf.float32)
-            z_conv = tf.nn.conv2d(self.X, self.W_conv, self.strides, padding='SAME', name='conv') + b_conv
-            a_conv = tf.nn.relu(z_conv)
-            
-            # 2x2 max pooling
-            a_pool = tf.nn.max_pool(a_conv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME', name='pool')
-            
-            # Fully connected layer
-            a_pool_flat = tf.reshape(a_pool, [-1, self.n_downsampled])
-            self.W_fc = self.weight_variable([self.n_downsampled, self.n_neurons_connected], name='fc', dtype=tf.float32)
-            b_fc = self.bias_variable([self.n_neurons_connected], name='fc', dtype=tf.float32)
-            a_fc = tf.nn.relu(tf.matmul(a_pool_flat, self.W_fc) + b_fc)
-            
-            # Output layer
-            self.W_out = self.weight_variable([self.n_neurons_connected, self.n_categories], name='out', dtype=tf.float32)
-            b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)
-            self.z_out = tf.matmul(a_fc, self.W_out) + b_out
-    
-    def create_loss(self):
-        with tf.name_scope('loss'):
-            softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))
-            
-            regularizer_loss_conv = tf.nn.l2_loss(self.W_conv)
-            regularizer_loss_fc = tf.nn.l2_loss(self.W_fc)
-            regularizer_loss_out = tf.nn.l2_loss(self.W_out)
-            regularizer_loss = self.lmbd*(regularizer_loss_conv + regularizer_loss_fc + regularizer_loss_out)
-            
-            self.loss = softmax_loss + regularizer_loss
-
-    def create_accuracy(self):
-        with tf.name_scope('accuracy'):
-            probabilities = tf.nn.softmax(self.z_out)
-            predictions = tf.argmax(probabilities, 1)
-            labels = tf.argmax(self.Y, 1)
-            
-            correct_predictions = tf.equal(predictions, labels)
-            correct_predictions = tf.cast(correct_predictions, tf.float32)
-            self.accuracy = tf.reduce_mean(correct_predictions)
-    
-    def create_optimiser(self):
-        with tf.name_scope('optimizer'):
-            self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)
-            
-    def weight_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.truncated_normal(shape, stddev=0.1)
-        return tf.Variable(initial, name=name, dtype=dtype)
-    
-    def bias_variable(self, shape, name='', dtype=tf.float32):
-        initial = tf.constant(0.1, shape=shape)
-        return tf.Variable(initial, name=name, dtype=dtype)
-
-    def fit(self):
-        data_indices = np.arange(self.n_inputs)
-
-        with tf.Session() as sess:
-            sess.run(tf.global_variables_initializer())
-            for i in range(self.epochs):
-                for j in range(self.iterations):
-                    chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)
-                    batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]
-            
-                    sess.run([CNN.loss, CNN.optimizer],
-                        feed_dict={CNN.X: batch_X,
-                                   CNN.Y: batch_Y})
-                    accuracy = sess.run(CNN.accuracy,
-                        feed_dict={CNN.X: batch_X,
-                                   CNN.Y: batch_Y})
-                    step = sess.run(CNN.global_step)
-    
-            self.train_loss, self.train_accuracy = sess.run([CNN.loss, CNN.accuracy],
-                feed_dict={CNN.X: self.X_train,
-                           CNN.Y: self.Y_train})
-        
-            self.test_loss, self.test_accuracy = sess.run([CNN.loss, CNN.accuracy],
-                feed_dict={CNN.X: self.X_test,
-                           CNN.Y: self.Y_test})
-
-

-









- -

Train the model

- -

-We need now to train the model, evaluate it and test its performance on test data, and eventually include hyperparameters. -

- - -

epochs = 100
-batch_size = 100
-n_filters = 10
-n_neurons_connected = 50
-n_categories = 10
-
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-CNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        CNN = ConvolutionalNeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,
-                                      n_filters=n_filters, n_neurons_connected=n_neurons_connected,
-                                      n_categories=n_categories, epochs=epochs, batch_size=batch_size,
-                                      eta=eta, lmbd=lmbd)
-        CNN.fit()
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % CNN.test_accuracy)
-        print()
-            
-        CNN_tf[i][j] = CNN
-
-

-









- -

Visualizing the results

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        CNN = CNN_tf[i][j]
-
-        train_accuracy[i][j] = CNN.train_accuracy
-        test_accuracy[i][j] = CNN.test_accuracy
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

- - -

Running with Keras

- -

- - -

from keras.models import Sequential
-from keras.layers.convolutional import Conv2D
-from keras.layers.convolutional import MaxPooling2D
-from keras.layers import Flatten
-from keras.layers import Dense
-from keras.regularizers import l2
-from keras.optimizers import SGD
-
-def create_convolutional_neural_network_keras(input_shape, receptive_field,
-                                              n_filters, n_neurons_connected, n_categories,
-                                              eta, lmbd):
-    model = Sequential()
-    model.add(Conv2D(n_filters, (receptive_field, receptive_field), input_shape=input_shape, padding='same',
-              activation='relu', kernel_regularizer=l2(lmbd)))
-    model.add(MaxPooling2D(pool_size=(2, 2)))
-    model.add(Flatten())
-    model.add(Dense(n_neurons_connected, activation='relu', kernel_regularizer=l2(lmbd)))
-    model.add(Dense(n_categories, activation='softmax', kernel_regularizer=l2(lmbd)))
-    
-    sgd = SGD(lr=eta)
-    model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])
-    
-    return model
-
-epochs = 100
-batch_size = 100
-input_shape = X_train.shape[1:4]
-receptive_field = 3
-n_filters = 10
-n_neurons_connected = 50
-n_categories = 10
-
-eta_vals = np.logspace(-5, 1, 7)
-lmbd_vals = np.logspace(-5, 1, 7)
-
-

-









- -

Final part

- -

- - -

CNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)
-        
-for i, eta in enumerate(eta_vals):
-    for j, lmbd in enumerate(lmbd_vals):
-        CNN = create_convolutional_neural_network_keras(input_shape, receptive_field,
-                                              n_filters, n_neurons_connected, n_categories,
-                                              eta, lmbd)
-        CNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)
-        scores = CNN.evaluate(X_test, Y_test)
-        
-        CNN_keras[i][j] = CNN
-        
-        print("Learning rate = ", eta)
-        print("Lambda = ", lmbd)
-        print("Test accuracy: %.3f" % scores[1])
-        print()
-
-

-









- -

Final visualization

- -

- - -

# visual representation of grid search
-# uses seaborn heatmap, could probably do this in matplotlib
-import seaborn as sns
-
-sns.set()
-
-train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))
-
-for i in range(len(eta_vals)):
-    for j in range(len(lmbd_vals)):
-        CNN = CNN_keras[i][j]
-
-        train_accuracy[i][j] = CNN.evaluate(X_train, Y_train)[1]
-        test_accuracy[i][j] = CNN.evaluate(X_test, Y_test)[1]
-
-        
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Training Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-fig, ax = plt.subplots(figsize = (10, 10))
-sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis")
-ax.set_title("Test Accuracy")
-ax.set_ylabel("$\eta$")
-ax.set_xlabel("$\lambda$")
-plt.show()
-
-

-









- -

Fun links

- -
    -
  1. Self-Driving cars using a convolutional neural network
  2. -
  3. Abstract art using convolutional neural networks
  4. -
- - - - - -
- © 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license -
- - - - - - diff --git a/doc/src/NeuralNet/NeuralNet.ipynb b/doc/src/NeuralNet/NeuralNet.ipynb deleted file mode 100644 index 011e92f5f..000000000 --- a/doc/src/NeuralNet/NeuralNet.ipynb +++ /dev/null @@ -1,4020 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks\n", - "\n", - " \n", - "**Morten Hjorth-Jensen**, Department of Physics, University of Oslo and Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University\n", - "\n", - "Date: **Oct 19, 2018**\n", - "\n", - "Copyright 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license\n", - "\n", - "\n", - "\n", - "\n", - "## Neural networks\n", - "\n", - "Artificial neural networks are computational systems that can learn to\n", - "perform tasks by considering examples, generally without being\n", - "programmed with any task-specific rules. It is supposed to mimic a\n", - "biological system, wherein neurons interact by sending signals in the\n", - "form of mathematical functions between layers. All layers can contain\n", - "an arbitrary number of neurons, and each connection is represented by\n", - "a weight variable.\n", - "\n", - "\n", - "## Artificial neurons\n", - "\n", - "The field of artificial neural networks has a long history of\n", - "development, and is closely connected with the advancement of computer\n", - "science and computers in general. A model of artificial neurons was\n", - "first developed by McCulloch and Pitts in 1943 to study signal\n", - "processing in the brain and has later been refined by others. The\n", - "general idea is to mimic neural networks in the human brain, which is\n", - "composed of billions of neurons that communicate with each other by\n", - "sending electrical signals. Each neuron accumulates its incoming\n", - "signals, which must exceed an activation threshold to yield an\n", - "output. If the threshold is not overcome, the neuron remains inactive,\n", - "i.e. has zero output.\n", - "\n", - "This behaviour has inspired a simple mathematical model for an artificial neuron." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " y = f\\left(\\sum_{i=1}^n w_ix_i\\right) = f(u)\n", - "\\label{artificialNeuron} \\tag{1}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, the output $y$ of the neuron is the value of its activation function, which have as input\n", - "a weighted sum of signals $x_i, \\dots ,x_n$ received by $n$ other neurons.\n", - "\n", - "Conceptually, it is helpful to divide neural networks into four\n", - "categories:\n", - "1. general purpose neural networks for supervised learning,\n", - "\n", - "2. neural networks designed specifically for image processing, the most prominent example of this class being Convolutional Neural Networks (CNNs),\n", - "\n", - "3. neural networks for sequential data such as Recurrent Neural Networks (RNNs), and\n", - "\n", - "4. neural networks for unsupervised learning such as Deep Boltzmann Machines.\n", - "\n", - "In natural science, DNNs and CNNs have already found numerous\n", - "applications. In statistical physics, they have been applied to detect\n", - "phase transitions in 2D Ising and Potts models, lattice gauge\n", - "theories, and different phases of polymers, or solving the\n", - "Navier-Stokes equation in weather forecasting. Deep learning has also\n", - "found interesting applications in quantum physics. Various quantum\n", - "phase transitions can be detected and studied using DNNs and CNNs,\n", - "topological phases, and even non-equilibrium many-body\n", - "localization. Representing quantum states as DNNs quantum state\n", - "tomography are among some of the impressive achievements to reveal the\n", - "potential of DNNs to facilitate the study of quantum systems.\n", - "\n", - "In quantum information theory, it has been shown that one can perform\n", - "gate decompositions with the help of neural. \n", - "\n", - "The applications are not limited to the natural sciences. There is a\n", - "plethora of applications in essentially all disciplines, from the\n", - "humanities to life science and medicine.\n", - "\n", - "## Neural network types\n", - "\n", - "An artificial neural network (ANN), is a computational model that\n", - "consists of layers of connected neurons, or nodes or units. We will\n", - "refer to these interchangeably as units or nodes, and sometimes as\n", - "neurons.\n", - "\n", - "It is supposed to mimic a biological nervous system by letting each\n", - "neuron interact with other neurons by sending signals in the form of\n", - "mathematical functions between layers. A wide variety of different\n", - "ANNs have been developed, but most of them consist of an input layer,\n", - "an output layer and eventual layers in-between, called *hidden\n", - "layers*. All layers can contain an arbitrary number of nodes, and each\n", - "connection between two nodes is associated with a weight variable.\n", - "\n", - "Neural networks (also called neural nets) are neural-inspired\n", - "nonlinear models for supervised learning. As we will see, neural nets\n", - "can be viewed as natural, more powerful extensions of supervised\n", - "learning methods such as linear and logistic regression and soft-max\n", - "methods we discussed earlier.\n", - "\n", - "\n", - "## Feed-forward neural networks\n", - "\n", - "The feed-forward neural network (FFNN) was the first and simplest type\n", - "of ANNs that were devised. In this network, the information moves in\n", - "only one direction: forward through the layers.\n", - "\n", - "Nodes are represented by circles, while the arrows display the\n", - "connections between the nodes, including the direction of information\n", - "flow. Additionally, each arrow corresponds to a weight variable\n", - "(figure to come). We observe that each node in a layer is connected\n", - "to *all* nodes in the subsequent layer, making this a so-called\n", - "*fully-connected* FFNN.\n", - "\n", - "\n", - "\n", - "## Convolutional Neural Network\n", - "\n", - "A different variant of FFNNs are *convolutional neural networks*\n", - "(CNNs), which have a connectivity pattern inspired by the animal\n", - "visual cortex. Individual neurons in the visual cortex only respond to\n", - "stimuli from small sub-regions of the visual field, called a receptive\n", - "field. This makes the neurons well-suited to exploit the strong\n", - "spatially local correlation present in natural images. The response of\n", - "each neuron can be approximated mathematically as a convolution\n", - "operation. (figure to come)\n", - "\n", - "Convolutional neural networks emulate the behaviour of neurons in the\n", - "visual cortex by enforcing a *local* connectivity pattern between\n", - "nodes of adjacent layers: Each node in a convolutional layer is\n", - "connected only to a subset of the nodes in the previous layer, in\n", - "contrast to the fully-connected FFNN. Often, CNNs consist of several\n", - "convolutional layers that learn local features of the input, with a\n", - "fully-connected layer at the end, which gathers all the local data and\n", - "produces the outputs. They have wide applications in image and video\n", - "recognition.\n", - "\n", - "## Recurrent neural networks\n", - "\n", - "So far we have only mentioned ANNs where information flows in one\n", - "direction: forward. *Recurrent neural networks* on the other hand,\n", - "have connections between nodes that form directed *cycles*. This\n", - "creates a form of internal memory which are able to capture\n", - "information on what has been calculated before; the output is\n", - "dependent on the previous computations. Recurrent NNs make use of\n", - "sequential information by performing the same task for every element\n", - "in a sequence, where each element depends on previous elements. An\n", - "example of such information is sentences, making recurrent NNs\n", - "especially well-suited for handwriting and speech recognition.\n", - "\n", - "## Other types of networks\n", - "\n", - "There are many other kinds of ANNs that have been developed. One type\n", - "that is specifically designed for interpolation in multidimensional\n", - "space is the radial basis function (RBF) network. RBFs are typically\n", - "made up of three layers: an input layer, a hidden layer with\n", - "non-linear radial symmetric activation functions and a linear output\n", - "layer (''linear'' here means that each node in the output layer has a\n", - "linear activation function). The layers are normally fully-connected\n", - "and there are no cycles, thus RBFs can be viewed as a type of\n", - "fully-connected FFNN. They are however usually treated as a separate\n", - "type of NN due the unusual activation functions.\n", - "\n", - "## Multilayer perceptrons\n", - "\n", - "One uses often so-called fully-connected feed-forward neural networks\n", - "with three or more layers (an input layer, one or more hidden layers\n", - "and an output layer) consisting of neurons that have non-linear\n", - "activation functions.\n", - "\n", - "Such networks are often called *multilayer perceptrons* (MLPs).\n", - "\n", - "## Why multilayer perceptrons?\n", - "\n", - "According to the *Universal approximation theorem*, a feed-forward\n", - "neural network with just a single hidden layer containing a finite\n", - "number of neurons can approximate a continuous multidimensional\n", - "function to arbitrary accuracy, assuming the activation function for\n", - "the hidden layer is a **non-constant, bounded and\n", - "monotonically-increasing continuous function**.\n", - "\n", - "Note that the requirements on the activation function only applies to\n", - "the hidden layer, the output nodes are always assumed to be linear, so\n", - "as to not restrict the range of output values.\n", - "\n", - "\n", - "## Mathematical model\n", - "\n", - "The output $y$ is produced via the activation function $f$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "y = f\\left(\\sum_{i=1}^n w_ix_i + b_i\\right) = f(z),\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This function receives $x_i$ as inputs.\n", - "Here the activation $z=(\\sum_{i=1}^n w_ix_i+b_i)$. \n", - "In an FFNN of such neurons, the *inputs* $x_i$ are the *outputs* of\n", - "the neurons in the preceding layer. Furthermore, an MLP is\n", - "fully-connected, which means that each neuron receives a weighted sum\n", - "of the outputs of *all* neurons in the previous layer.\n", - "\n", - "## Mathematical model\n", - "\n", - "First, for each node $i$ in the first hidden layer, we calculate a weighted sum $z_i^1$ of the input coordinates $x_j$," - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation} z_i^1 = \\sum_{j=1}^{M} w_{ij}^1 x_j + b_i^1\n", - "\\label{_auto1} \\tag{2}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here $b_i$ is the so-called bias which is normally needed in\n", - "case of zero activation weights or inputs. How to fix the biases and\n", - "the weights will be discussed below. The value of $z_i^1$ is the\n", - "argument to the activation function $f_i$ of each node $i$, The\n", - "variable $M$ stands for all possible inputs to a given node $i$ in the\n", - "first layer. We define the output $y_i^1$ of all neurons in layer 1 as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " y_i^1 = f(z_i^1) = f\\left(\\sum_{j=1}^M w_{ij}^1 x_j + b_i^1\\right)\n", - "\\label{outputLayer1} \\tag{3}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where we assume that all nodes in the same layer have identical\n", - "activation functions, hence the notation $f$. In general, we could assume in the more general case that different layers have different activation functions.\n", - "In this case we would identify these functions with a superscript $l$ for the $l$-th layer," - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " y_i^l = f^l(u_i^l) = f^l\\left(\\sum_{j=1}^{N_{l-1}} w_{ij}^l y_j^{l-1} + b_i^l\\right)\n", - "\\label{generalLayer} \\tag{4}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where $N_l$ is the number of nodes in layer $l$. When the output of\n", - "all the nodes in the first hidden layer are computed, the values of\n", - "the subsequent layer can be calculated and so forth until the output\n", - "is obtained.\n", - "\n", - "\n", - "\n", - "## Mathematical model\n", - "\n", - "The output of neuron $i$ in layer 2 is thus," - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " y_i^2 = f^2\\left(\\sum_{j=1}^N w_{ij}^2 y_j^1 + b_i^2\\right) \n", - "\\label{_auto2} \\tag{5}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation} \n", - " = f^2\\left[\\sum_{j=1}^N w_{ij}^2f^1\\left(\\sum_{k=1}^M w_{jk}^1 x_k + b_j^1\\right) + b_i^2\\right]\n", - "\\label{outputLayer2} \\tag{6}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where we have substituted $y_k^1$ with the inputs $x_k$. Finally, the ANN output reads" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " y_i^3 = f^3\\left(\\sum_{j=1}^N w_{ij}^3 y_j^2 + b_i^3\\right) \n", - "\\label{_auto3} \\tag{7}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation} \n", - " = f_3\\left[\\sum_{j} w_{ij}^3 f^2\\left(\\sum_{k} w_{jk}^2 f^1\\left(\\sum_{m} w_{km}^1 x_m + b_k^1\\right) + b_j^2\\right)\n", - " + b_1^3\\right]\n", - "\\label{_auto4} \\tag{8}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Mathematical model\n", - "\n", - "We can generalize this expression to an MLP with $l$ hidden\n", - "layers. The complete functional form is," - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - "y^{l+1}_i = f^{l+1}\\left[\\!\\sum_{j=1}^{N_l} w_{ij}^3 f^l\\left(\\sum_{k=1}^{N_{l-1}}w_{jk}^{l-1}\\left(\\dots f^1\\left(\\sum_{n=1}^{N_0} w_{mn}^1 x_n+ b_m^1\\right)\\dots\\right)+b_k^2\\right)+b_1^3\\right] \n", - "\\label{completeNN} \\tag{9}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "which illustrates a basic property of MLPs: The only independent\n", - "variables are the input values $x_n$.\n", - "\n", - "## Mathematical model\n", - "\n", - "This confirms that an MLP, despite its quite convoluted mathematical\n", - "form, is nothing more than an analytic function, specifically a\n", - "mapping of real-valued vectors $\\hat{x} \\in \\mathbb{R}^n \\rightarrow\n", - "\\hat{y} \\in \\mathbb{R}^m$.\n", - "\n", - "Furthermore, the flexibility and universality of an MLP can be\n", - "illustrated by realizing that the expression is essentially a nested\n", - "sum of scaled activation functions of the form" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " f(x) = c_1 f(c_2 x + c_3) + c_4\n", - "\\label{_auto5} \\tag{10}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where the parameters $c_i$ are weights and biases. By adjusting these\n", - "parameters, the activation functions can be shifted up and down or\n", - "left and right, change slope or be rescaled which is the key to the\n", - "flexibility of a neural network.\n", - "\n", - "### Matrix-vector notation\n", - "\n", - "We can introduce a more convenient notation for the activations in an A NN. \n", - "\n", - "Additionally, we can represent the biases and activations\n", - "as layer-wise column vectors $\\hat{b}_l$ and $\\hat{y}_l$, so that the $i$-th element of each vector \n", - "is the bias $b_i^l$ and activation $y_i^l$ of node $i$ in layer $l$ respectively. \n", - "\n", - "We have that $\\mathrm{W}_l$ is an $N_{l-1} \\times N_l$ matrix, while $\\hat{b}_l$ and $\\hat{y}_l$ are $N_l \\times 1$ column vectors. \n", - "With this notation, the sum becomes a matrix-vector multiplication, and we can write\n", - "the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " \\hat{y}_2 = f_2(\\mathrm{W}_2 \\hat{y}_{1} + \\hat{b}_{2}) = \n", - " f_2\\left(\\left[\\begin{array}{ccc}\n", - " w^2_{11} &w^2_{12} &w^2_{13} \\\\\n", - " w^2_{21} &w^2_{22} &w^2_{23} \\\\\n", - " w^2_{31} &w^2_{32} &w^2_{33} \\\\\n", - " \\end{array} \\right] \\cdot\n", - " \\left[\\begin{array}{c}\n", - " y^1_1 \\\\\n", - " y^1_2 \\\\\n", - " y^1_3 \\\\\n", - " \\end{array}\\right] + \n", - " \\left[\\begin{array}{c}\n", - " b^2_1 \\\\\n", - " b^2_2 \\\\\n", - " b^2_3 \\\\\n", - " \\end{array}\\right]\\right).\n", - "\\label{_auto6} \\tag{11}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Matrix-vector notation and activation\n", - "\n", - "The activation of node $i$ in layer 2 is" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - " y^2_i = f_2\\Bigr(w^2_{i1}y^1_1 + w^2_{i2}y^1_2 + w^2_{i3}y^1_3 + b^2_i\\Bigr) = \n", - " f_2\\left(\\sum_{j=1}^3 w^2_{ij} y_j^1 + b^2_i\\right).\n", - "\\label{_auto7} \\tag{12}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is not just a convenient and compact notation, but also a useful\n", - "and intuitive way to think about MLPs: The output is calculated by a\n", - "series of matrix-vector multiplications and vector additions that are\n", - "used as input to the activation functions. For each operation\n", - "$\\mathrm{W}_l \\hat{y}_{l-1}$ we move forward one layer.\n", - "\n", - "\n", - "### Activation functions\n", - "\n", - "A property that characterizes a neural network, other than its\n", - "connectivity, is the choice of activation function(s). As described\n", - "in, the following restrictions are imposed on an activation function\n", - "for a FFNN to fulfill the universal approximation theorem\n", - "\n", - " * Non-constant\n", - "\n", - " * Bounded\n", - "\n", - " * Monotonically-increasing\n", - "\n", - " * Continuous\n", - "\n", - "### Activation functions, Logistic and Hyperbolic ones\n", - "\n", - "The second requirement excludes all linear functions. Furthermore, in\n", - "a MLP with only linear activation functions, each layer simply\n", - "performs a linear transformation of its inputs.\n", - "\n", - "Regardless of the number of layers, the output of the NN will be\n", - "nothing but a linear function of the inputs. Thus we need to introduce\n", - "some kind of non-linearity to the NN to be able to fit non-linear\n", - "functions Typical examples are the logistic *Sigmoid*" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "f(x) = \\frac{1}{1 + e^{-x}},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and the *hyperbolic tangent* function" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "f(x) = \\tanh(x)\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Relevance\n", - "\n", - "The *sigmoid* function are more biologically plausible because the\n", - "output of inactive neurons are zero. Such activation function are\n", - "called *one-sided*. However, it has been shown that the hyperbolic\n", - "tangent performs better than the sigmoid for training MLPs. has\n", - "become the most popular for *deep neural networks*" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "\n", - "\"\"\"The sigmoid function (or the logistic curve) is a \n", - "function that takes any real number, z, and outputs a number (0,1).\n", - "It is useful in neural networks for assigning weights on a relative scale.\n", - "The value z is the weighted sum of parameters involved in the learning algorithm.\"\"\"\n", - "\n", - "import numpy\n", - "import matplotlib.pyplot as plt\n", - "import math as mt\n", - "\n", - "z = numpy.arange(-5, 5, .1)\n", - "sigma_fn = numpy.vectorize(lambda z: 1/(1+numpy.exp(-z)))\n", - "sigma = sigma_fn(z)\n", - "\n", - "fig = plt.figure()\n", - "ax = fig.add_subplot(111)\n", - "ax.plot(z, sigma)\n", - "ax.set_ylim([-0.1, 1.1])\n", - "ax.set_xlim([-5,5])\n", - "ax.grid(True)\n", - "ax.set_xlabel('z')\n", - "ax.set_title('sigmoid function')\n", - "\n", - "plt.show()\n", - "\n", - "\"\"\"Step Function\"\"\"\n", - "z = numpy.arange(-5, 5, .02)\n", - "step_fn = numpy.vectorize(lambda z: 1.0 if z >= 0.0 else 0.0)\n", - "step = step_fn(z)\n", - "\n", - "fig = plt.figure()\n", - "ax = fig.add_subplot(111)\n", - "ax.plot(z, step)\n", - "ax.set_ylim([-0.5, 1.5])\n", - "ax.set_xlim([-5,5])\n", - "ax.grid(True)\n", - "ax.set_xlabel('z')\n", - "ax.set_title('step function')\n", - "\n", - "plt.show()\n", - "\n", - "\"\"\"Sine Function\"\"\"\n", - "z = numpy.arange(-2*mt.pi, 2*mt.pi, 0.1)\n", - "t = numpy.sin(z)\n", - "\n", - "fig = plt.figure()\n", - "ax = fig.add_subplot(111)\n", - "ax.plot(z, t)\n", - "ax.set_ylim([-1.0, 1.0])\n", - "ax.set_xlim([-2*mt.pi,2*mt.pi])\n", - "ax.grid(True)\n", - "ax.set_xlabel('z')\n", - "ax.set_title('sine function')\n", - "\n", - "plt.show()\n", - "\n", - "\"\"\"Plots a graph of the squashing function used by a rectified linear\n", - "unit\"\"\"\n", - "z = numpy.arange(-2, 2, .1)\n", - "zero = numpy.zeros(len(z))\n", - "y = numpy.max([zero, z], axis=0)\n", - "\n", - "fig = plt.figure()\n", - "ax = fig.add_subplot(111)\n", - "ax.plot(z, y)\n", - "ax.set_ylim([-2.0, 2.0])\n", - "ax.set_xlim([-2.0, 2.0])\n", - "ax.grid(True)\n", - "ax.set_xlabel('z')\n", - "ax.set_title('Rectified linear unit')\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## The multilayer perceptron (MLP)\n", - "\n", - "The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of\n", - "1. A neural network with one or more layers of nodes between the input and the output nodes.\n", - "\n", - "2. The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer.\n", - "\n", - "3. The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer.\n", - "\n", - "As a convention it is normal to call a network with one layer of input units, one layer of hidden\n", - "units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc.\n", - "\n", - "For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units.\n", - "Hereafter we will call the various entities of a layer for nodes.\n", - "There are also no connections within a single layer.\n", - "\n", - "The number of input nodes does not need to equal the number of output\n", - "nodes. This applies also to the hidden layers. Each layer may have its\n", - "own number of nodes and activation functions.\n", - "\n", - "The hidden layers have their name from the fact that they are not\n", - "linked to observables and as we will see below when we define the\n", - "so-called activation $\\hat{z}$, we can think of this as a basis\n", - "expansion of the original inputs $\\hat{x}$. The difference however\n", - "between neural networks and say linear regression is that now these\n", - "basis functions (which will correspond to the weights in the network)\n", - "are learned from data. This results in an important difference between\n", - "neural networks and deep learning approaches on one side and methods\n", - "like logistic regression or linear regression and their modifications on the other side.\n", - "\n", - "\n", - "## From one to many layers, the universal approximation theorem\n", - "\n", - "\n", - "A neural network with only one layer, what we called the simple\n", - "perceptron, is best suited if we have a standard binary model with\n", - "clear (linear) boundaries between the outcomes. As such it could\n", - "equally well be replaced by standard linear regression or logistic\n", - "regression. Networks with one or more hidden layers approximate\n", - "systems with more complex boundaries.\n", - "\n", - "As stated earlier, \n", - "an important theorem in studies of neural networks, restated without\n", - "proof here, is the [universal approximation\n", - "theorem](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.441.7873&rep=rep1&type=pdf).\n", - "\n", - "It states that a feed-forward network with a single hidden layer\n", - "containing a finite number of neurons can approximate continuous\n", - "functions on compact subsets of real functions. The theorem thus\n", - "states that simple neural networks can represent a wide variety of\n", - "interesting functions when given appropriate parameters. It is the\n", - "multilayer feedforward architecture itself which gives neural networks\n", - "the potential of being universal approximators.\n", - "\n", - "\n", - "## Deriving the back propagation code for a multilayer perceptron model\n", - "\n", - "\n", - "**Note: figures will be inserted later!**\n", - "\n", - "As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications.\n", - "The unknowwn quantities are our weights $w_{ij}$ and we need to find an algorithm for changing them so that our errors are as small as possible.\n", - "This leads us to the famous [back propagation algorithm](https://www.nature.com/articles/323533a0).\n", - "\n", - "The questions we want to ask are how do changes in the biases and the\n", - "weights in our network change the cost function and how can we use the\n", - "final output to modify the weights?\n", - "\n", - "To derive these equations let us start with a plain regression problem\n", - "and define our cost function as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "{\\cal C}(\\hat{W}) = \\frac{1}{2}\\sum_{i=1}^n\\left(y_i - t_i\\right)^2,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where the $t_i$s are our $n$ targets (the values we want to\n", - "reproduce), while the outputs of the network after having propagated\n", - "all inputs $\\hat{x}$ are given by $y_i$. Below we will demonstrate\n", - "how the basic equations arising from the back propagation algorithm\n", - "can be modified in order to study classification problems with $K$\n", - "classes.\n", - "\n", - "## Definitions\n", - "\n", - "With our definition of the targets $\\hat{t}$, the outputs of the\n", - "network $\\hat{y}$ and the inputs $\\hat{x}$ we\n", - "define now the activation $z_j^l$ of node/neuron/unit $j$ of the\n", - "$l$-th layer as a function of the bias, the weights which add up from\n", - "the previous layer $l-1$ and the forward passes/outputs\n", - "$\\hat{a}^{l-1}$ from the previous layer as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "z_j^l = \\sum_{i=1}^{M_{l-1}}w_{ij}^la_i^{l-1}+b_j^l,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where $b_k^l$ are the biases from layer $l$. Here $M_{l-1}$\n", - "represents the total number of nodes/neurons/units of layer $l-1$. The\n", - "figure here illustrates this equation. We can rewrite this in a more\n", - "compact form as the matrix-vector products we discussed earlier," - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\hat{z}^l = \\left(\\hat{W}^l\\right)^T\\hat{a}^{l-1}+\\hat{b}^l.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With the activation values $\\hat{z}^l$ we can in turn define the\n", - "output of layer $l$ as $\\hat{a}^l = f(\\hat{z}^l)$ where $f$ is our\n", - "activation function. In the examples here we will use the sigmoid\n", - "function discussed in our logistic regression lectures. We will also use the same activation function $f$ for all layers\n", - "and their nodes. It means we have" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "a_j^l = f(z_j^l) = \\frac{1}{1+\\exp{-(z_j^l)}}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Derivatives and the chain rule\n", - "\n", - "From the definition of the activation $z_j^l$ we have" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial z_j^l}{\\partial w_{ij}^l} = a_i^{l-1},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial z_j^l}{\\partial a_i^{l-1}} = w_{ji}^l.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With our definition of the activation function we have that (note that this function depends only on $z_j^l$)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial a_j^l}{\\partial z_j^{l}} = a_j^l(1-a_j^l)=f(z_j^l)(1-f(z_j^l)).\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Derivative of the cost function\n", - "\n", - "With these definitions we can now compute the derivative of the cost function in terms of the weights.\n", - "\n", - "Let us specialize to the output layer $l=L$. Our cost function is" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "{\\cal C}(\\hat{W^L}) = \\frac{1}{2}\\sum_{i=1}^n\\left(y_i - t_i\\right)^2=\\frac{1}{2}\\sum_{i=1}^n\\left(a_i^L - t_i\\right)^2,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The derivative of this function with respect to the weights is" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial{\\cal C}(\\hat{W^L})}{\\partial w_{jk}^L} = \\left(a_j^L - t_j\\right)\\frac{\\partial a_j^L}{\\partial w_{jk}^{L}},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The last partial derivative can easily be computed and reads (by applying the chain rule)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial a_j^L}{\\partial w_{jk}^{L}} = \\frac{\\partial a_j^L}{\\partial z_{j}^{L}}\\frac{\\partial z_j^L}{\\partial w_{jk}^{L}}=a_j^L(1-a_j^L)a_k^{L-1},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Bringing it together, first back propagation equation\n", - "\n", - "We have thus" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial{\\cal C}(\\hat{W^L})}{\\partial w_{jk}^L} = \\left(a_j^L - t_j\\right)a_j^L(1-a_j^L)a_k^{L-1},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Defining" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^L = a_j^L(1-a_j^L)\\left(a_j^L - t_j\\right) = f'(z_j^L)\\frac{\\partial {\\cal C}}{\\partial (a_j^L)},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and using the Hadamard product of two vectors we can write this as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\hat{\\delta}^L = f'(\\hat{z}^L)\\circ\\frac{\\partial {\\cal C}}{\\partial (\\hat{a}L)}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is an important expression. The second term on the right handside\n", - "measures how fast the cost function is changing as a function of the $j$th\n", - "output activation. If, for example, the cost function doesn't depend\n", - "much on a particular output node $j$, then $\\delta_j^L$ will be small,\n", - "which is what we would expect. The first term on the right, measures\n", - "how fast the activation function $f$ is changing at a given activation\n", - "value $z_j^L$.\n", - "\n", - "Notice that everything in the above equations is easily computed. In\n", - "particular, we compute $z_j^L$ while computing the behaviour of the\n", - "network, and it is only a small additional overhead to compute\n", - "$f'(z^L_j)$. The exact form of the derivative with respect to the\n", - "output depends on the form of the cost function.\n", - "However, provided the cost function is known there should be little\n", - "trouble in calculating" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial {\\cal C}}{\\partial (a_j^L)}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With the definition of $\\delta_j^L$ we have a more compact definition of the derivative of the cost function in terms of the weights, namely" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial{\\cal C}(\\hat{W^L})}{\\partial w_{jk}^L} = \\delta_j^La_k^{L-1}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Derivatives in terms of $z_j^L$\n", - "\n", - "It is also easy to see that our previous equation can be written as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^L =\\frac{\\partial {\\cal C}}{\\partial z_j^L}= \\frac{\\partial {\\cal C}}{\\partial a_j^L}\\frac{\\partial a_j^L}{\\partial z_j^L},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "which can also be interpreted as the partial derivative of the cost function with respect to the biases $b_j^L$, namely" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^L = \\frac{\\partial {\\cal C}}{\\partial b_j^L}\\frac{\\partial b_j^L}{\\partial z_j^L}=\\frac{\\partial {\\cal C}}{\\partial b_j^L},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That is, the error $\\delta_j^L$ is exactly equal to the rate of change of the cost function as a function of the bias. \n", - "## Bringing it together\n", - "\n", - "We have now three equations that are essential for the computations of the derivatives of the cost function at the output layer. These equations are needed to start the algorithm and they are\n", - "\n", - "**The starting equations.**" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - "\\frac{\\partial{\\cal C}(\\hat{W^L})}{\\partial w_{jk}^L} = \\delta_j^La_k^{L-1},\n", - "\\label{_auto8} \\tag{13}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - "\\delta_j^L = f'(z_j^L)\\frac{\\partial {\\cal C}}{\\partial (a_j^L)},\n", - "\\label{_auto9} \\tag{14}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "$$\n", - "\\begin{equation}\n", - "\\delta_j^L = \\frac{\\partial {\\cal C}}{\\partial b_j^L},\n", - "\\label{_auto10} \\tag{15}\n", - "\\end{equation}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An interesting consequence of the above equations is that when the\n", - "activation $a_k^{L-1}$ is small, the gradient term, that is the\n", - "derivative of the cost function with respect to the weights, will also\n", - "tend to be small. We say then that the weight learns slowly, meaning\n", - "that it changes slowly when we minimize the weights via say gradient\n", - "descent. In this case we say the system learns slowly.\n", - "\n", - "Another interesting feature is that is when the activation function,\n", - "represented by the sigmoid function here, is rather flat when we move towards\n", - "its end values $0$ and $1$ (see the above Python codes). In these\n", - "cases, the derivatives of the activation function will also be close\n", - "to zero, meaning again that the gradients will be small and the\n", - "network learns slowly again.\n", - "\n", - "\n", - "\n", - "We need a fourth equation and we are set. We are going to propagate\n", - "backwards in order to the determine the weights and biases. In order\n", - "to do so we need to represent the error in the layer before the final\n", - "one $L-1$ in terms of the errors in the final output layer.\n", - "\n", - "## Final back propagating equation\n", - "\n", - "We have that (replacing $L$ with a general layer $l$)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^l =\\frac{\\partial {\\cal C}}{\\partial z_j^l}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We want to express this in terms of the equations for layer $l+1$. Using the chain rule and summing over all $k$ entries we have" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^l =\\sum_k \\frac{\\partial {\\cal C}}{\\partial z_k^{l+1}}\\frac{\\partial z_k^{l+1}}{\\partial z_j^{l}}=\\sum_k \\delta_k^{l+1}\\frac{\\partial z_k^{l+1}}{\\partial z_j^{l}},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and recalling that" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "z_j^{l+1} = \\sum_{i=1}^{M_{l}}w_{ij}^{l+1}a_j^{l}+b_j^{l+1},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "with $M_l$ being the number of nodes in layer $l$, we obtain" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^l =\\sum_k \\delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l),\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is our final equation.\n", - "\n", - "We are now ready to set up the algorithm for back propagation and learning the weights and biases.\n", - "\n", - "## Setting up the Back propagation algorithm\n", - "\n", - "\n", - "\n", - "The four equations provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm.\n", - "\n", - "First, we set up the input data $\\hat{x}$ and the activations\n", - "$\\hat{z}_1$ of the input layer and compute the activation function and\n", - "the pertinent outputs $\\hat{a}^1$.\n", - "\n", - "\n", - "\n", - "Secondly, we perform then the feed forward till we reach the output\n", - "layer and compute all $\\hat{z}_l$ of the input layer and compute the\n", - "activation function and the pertinent outputs $\\hat{a}^l$ for\n", - "$l=2,3,\\dots,L$.\n", - "\n", - "\n", - "\n", - "Thereafter we compute the ouput error $\\hat{\\delta}^L$ by computing all" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^L = f'(z_j^L)\\frac{\\partial {\\cal C}}{\\partial (a_j^L)}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then we compute the back propagate error for each $l=L-1,L-2,\\dots,2$ as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\delta_j^l = \\sum_k \\delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l).\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we update the weights and the biases using gradient descent for each $l=L-1,L-2,\\dots,2$ and update the weights and biases according to the rules" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "w_{jk}^l\\leftarrow = w_{jk}^l- \\eta \\delta_j^la_k^{l-1},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "b_j^l \\leftarrow b_j^l-\\eta \\frac{\\partial {\\cal C}}{\\partial b_j^l}=b_j^l-\\eta \\delta_j^l,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The parameter $\\eta$ is the learning parameter discussed in connection with the gradient descent methods.\n", - "Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training.\n", - "\n", - "\n", - "\n", - "## Setting up a Multi-layer perceptron model for classification\n", - "\n", - "We are now gong to develop an example based on the MNIST data\n", - "base. This is a classification problem and we need to use our\n", - "cross-entropy function we discussed in connection with logistic\n", - "regression. The cross-entropy defines our cost function for the\n", - "classificaton problems with neural networks.\n", - "\n", - "In binary classification with two classes $(0, 1)$ we define the\n", - "logistic/sigmoid function as the probability that a particular input\n", - "is in class $0$ or $1$. This is possible because the logistic\n", - "function takes any input from the real numbers and inputs a number\n", - "between 0 and 1, and can therefore be interpreted as a probability. It\n", - "also has other nice properties, such as a derivative that is simple to\n", - "calculate.\n", - "\n", - "For an input $\\boldsymbol{a}$ from the hidden layer, the probability that the input $\\boldsymbol{x}$\n", - "is in class 0 or 1 is just. We let $\\theta$ represent the unknown weights and biases to be adjusted by our equations). The variable $x$\n", - "represents our activation values $z$. We have" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "P(y = 0 \\mid \\hat{x}, \\hat{\\theta}) = \\frac{1}{1 + \\exp{(- \\hat{x}})} ,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "P(y = 1 \\mid \\hat{x}, \\hat{\\theta}) = 1 - P(y = 0 \\mid \\hat{x}, \\hat{\\theta}) ,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where $y \\in \\{0, 1\\}$ and $\\hat{\\theta}$ represents the weights and biases\n", - "of our network.\n", - "\n", - "\n", - "## Defining the cost function\n", - "\n", - "Our cost function is given as (see the Logistic regression lectures)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\mathcal{C}(\\hat{\\theta}) = - \\ln P(\\mathcal{D} \\mid \\hat{\\theta}) = - \\sum_{i=1}^n\n", - "y_i \\ln[P(y_i = 0)] + (1 - y_i) \\ln [1 - P(y_i = 0)] = \\sum_{i=1}^n \\mathcal{L}_i(\\hat{\\theta}) .\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This last equality means that we can interpret our *cost* function as a sum over the *loss* function\n", - "for each point in the dataset $\\mathcal{L}_i(\\hat{\\theta})$. \n", - "The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather\n", - "than maximizing a negative number. \n", - "\n", - "In *multiclass* classification it is common to treat each integer label as a so called *one-hot* vector: \n", - "\n", - "$y = 5 \\quad \\rightarrow \\quad \\hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$ and\n", - "\n", - "\n", - "$y = 1 \\quad \\rightarrow \\quad \\hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$ \n", - "\n", - "\n", - "i.e. a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset (numbers from $0$ to $9$).. \n", - "\n", - "If $\\hat{x}_i$ is the $i$-th input (image), $y_{ic}$ refers to the $c$-th component of the $i$-th\n", - "output vector $\\hat{y}_i$. \n", - "The probability of $\\hat{x}_i$ being in class $c$ will be given by the softmax function:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "P(y_{ic} = 1 \\mid \\hat{x}_i, \\hat{\\theta}) = \\frac{\\exp{((\\hat{a}_i^{hidden})^T \\hat{w}_c)}}\n", - "{\\sum_{c'=0}^{C-1} \\exp{((\\hat{a}_i^{hidden})^T \\hat{w}_{c'})}} ,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "which reduces to the logistic function in the binary case. \n", - "The likelihood of this $C$-class classifier\n", - "is now given as:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "P(\\mathcal{D} \\mid \\hat{\\theta}) = \\prod_{i=1}^n \\prod_{c=0}^{C-1} [P(y_{ic} = 1)]^{y_{ic}} .\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Again we take the negative log-likelihood to define our cost function:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\mathcal{C}(\\hat{\\theta}) = - \\log{P(\\mathcal{D} \\mid \\hat{\\theta})}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "See the logistic regression lectures for a full definition of the cost function.\n", - "\n", - "The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before!\n", - "\n", - "## Example: binary classification problem\n", - "\n", - "As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters $\\beta$ as" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\mathcal{C}(\\hat{\\beta}) = - \\sum_{i=1}^n \\left(y_i\\log{p(y_i \\vert x_i,\\hat{\\beta})}+(i-y_i)\\log{1-p(y_i \\vert x_i,\\hat{\\beta})}\\right),\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where we had defined the logistic (sigmoid) function" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "p(y_i =1\\vert x_i,\\hat{\\beta})=\\frac{\\exp{(\\beta_0+\\beta_1 x_i)}}{1+\\exp{(\\beta_0+\\beta_1 x_i)}},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "p(y_i =0\\vert x_i,\\hat{\\beta})=1-p(y_i =1\\vert x_i,\\hat{\\beta}).\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The parameters $\\hat{\\beta}$ were defined using a minimization method like gradient descent or Newton-Raphson's method. \n", - "\n", - "Now we replace $x_i$ with the activation $z_i^l$ for a given layer $l$ and the outputs as $y_i=a_i^l=f(z_i^l)$, with $z_i^l$ now being a function of the weights $w_{ij}^l$ and biases $b_i^l$. \n", - "We have then" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "a_i^l = y_i = \\frac{\\exp{(z_i^l)}}{1+\\exp{(z_i^l)}},\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "with" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "z_i^l = \\sum_{j}w_{ij}^l a_j^{l-1}+b_i^l,\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where the superscript $l-1$ indicates that these are the outputs from layer $l-1$.\n", - "Our cost function at the final layer $l=L$ is now" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\mathcal{C}(\\hat{W}) = - \\sum_{i=1}^n \\left(t_i\\log{a_i^L}+(i-t_i)\\log{(1-a_i^L)}\\right),\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "where we have defined the targets $t_i$. The derivatives of the cost function with respect to the output $a_i^L$ are then easily calculated and we get" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial \\mathcal{C}(\\hat{W})}{\\partial a_i^L} = \\frac{a_i^L-t_i}{a_i^L(1-a_i^L)}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In case we use another activation function than the logistic one, we need to evaluate other derivatives. \n", - "\n", - "\n", - "## The Softmax function\n", - "In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation $z_i^l$, that is we need" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial f(z_i^l)}{\\partial w_{jk}^l} =\n", - "\\frac{\\partial f(z_i^l)}{\\partial z_j^l} \\frac{\\partial z_j^l}{\\partial w_{jk}^l}= \\frac{\\partial f(z_i^l)}{\\partial z_j^l}a_k^{l-1}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For the Softmax function we have" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "f(z_i^l) = \\frac{\\exp{(z_i^l)}}{\\sum_{m=1}^K\\exp{(z_m^l)}}.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Its derivative with respect to $z_j^l$ gives" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\frac{\\partial f(z_i^l)}{\\partial z_j^l}= f(z_i^l)\\left(\\delta_{ij}-f(z_j^l)\\right),\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "which in case of the simply binary model reduces to having $i=j$. \n", - "\n", - "\n", - "## Developing a code for doing neural networks with back propagation\n", - "\n", - "\n", - "One can identify a set of key steps when using neural networks to solve supervised learning problems: \n", - "\n", - "1. Collect and pre-process data \n", - "\n", - "2. Define model and architecture \n", - "\n", - "3. Choose cost function and optimizer \n", - "\n", - "4. Train the model \n", - "\n", - "5. Evaluate model performance on test data \n", - "\n", - "6. Adjust hyperparameters (if necessary, network architecture)\n", - "\n", - "## Collect and pre-process data\n", - "\n", - "Here we will be using the MNIST dataset, which is readily available through the **scikit-learn**\n", - "package. You may also find it for example [here](http://yann.lecun.com/exdb/mnist/). \n", - "The *MNIST* (Modified National Institute of Standards and Technology) database is a large database\n", - "of handwritten digits that is commonly used for training various image processing systems. \n", - "The MNIST dataset consists of 70 000 images of size $28\\times 28$ pixels, each labeled from 0 to 9. \n", - "The scikit-learn dataset we will use consists of a selection of 1797 images of size $8\\times 8$ collected and processed from this database. \n", - "\n", - "To feed data into a feed-forward neural network we need to represent\n", - "the inputs as a feature matrix $X = (n_{inputs}, n_{features})$. Each\n", - "row represents an *input*, in this case a handwritten digit, and\n", - "each column represents a *feature*, in this case a pixel. The\n", - "correct answers, also known as *labels* or *targets* are\n", - "represented as a 1D array of integers \n", - "$Y = (n_{inputs}) = (5, 3, 1, 8,...)$.\n", - "\n", - "As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from\n", - "measurements of height (in m) \n", - "and weight (in kg). If we have measurements of 5 people the feature matrix could be for example: \n", - "\n", - "$$ X = \\begin{bmatrix}\n", - "1.85 & 81\\\\\n", - "1.71 & 65\\\\\n", - "1.95 & 103\\\\\n", - "1.55 & 42\\\\\n", - "1.63 & 56\n", - "\\end{bmatrix} ,$$ \n", - "\n", - "and the targets would be: \n", - "\n", - "$$ Y = (23.7, 22.2, 27.1, 17.5, 21.1) $$ \n", - "\n", - "Since each input image is a 2D matrix, we need to flatten the image\n", - "(i.e. \"unravel\" the 2D matrix into a 1D array) to turn the data into a\n", - "feature matrix. This means we lose all spatial information in the\n", - "image, such as locality and translational invariance. More complicated\n", - "architectures such as Convolutional Neural Networks can take advantage\n", - "of such information, and are most commonly applied when analyzing\n", - "images." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# import necessary packages\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from sklearn import datasets\n", - "\n", - "\n", - "# ensure the same random numbers appear every time\n", - "np.random.seed(0)\n", - "\n", - "# display images in notebook\n", - "%matplotlib inline\n", - "plt.rcParams['figure.figsize'] = (12,12)\n", - "\n", - "\n", - "# download MNIST dataset\n", - "digits = datasets.load_digits()\n", - "\n", - "# define inputs and labels\n", - "inputs = digits.images\n", - "labels = digits.target\n", - "\n", - "print(\"inputs = (n_inputs, pixel_width, pixel_height) = \" + str(inputs.shape))\n", - "print(\"labels = (n_inputs) = \" + str(labels.shape))\n", - "\n", - "\n", - "# flatten the image\n", - "# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64\n", - "n_inputs = len(inputs)\n", - "inputs = inputs.reshape(n_inputs, -1)\n", - "print(\"X = (n_inputs, n_features) = \" + str(inputs.shape))\n", - "\n", - "\n", - "# choose some random images to display\n", - "indices = np.arange(n_inputs)\n", - "random_indices = np.random.choice(indices, size=5)\n", - "\n", - "for i, image in enumerate(digits.images[random_indices]):\n", - " plt.subplot(1, 5, i+1)\n", - " plt.axis('off')\n", - " plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')\n", - " plt.title(\"Label: %d\" % digits.target[random_indices[i]])\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Train and test datasets\n", - "\n", - "Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions. \n", - "\n", - "We will reserve $80 \\%$ of our dataset for training and $20 \\%$ for testing. \n", - "\n", - "It is important that the train and test datasets are drawn randomly from our dataset, to ensure\n", - "no bias in the sampling. \n", - "Say you are taking measurements of weather data to predict the weather in the coming 5 days.\n", - "You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data\n", - "collected from 12.00 to 24.00." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from sklearn.model_selection import train_test_split\n", - "\n", - "# one-liner from scikit-learn library\n", - "train_size = 0.8\n", - "test_size = 1 - train_size\n", - "X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,\n", - " test_size=test_size)\n", - "\n", - "# equivalently in numpy\n", - "def train_test_split_numpy(inputs, labels, train_size, test_size):\n", - " n_inputs = len(inputs)\n", - " inputs_shuffled = inputs.copy()\n", - " labels_shuffled = labels.copy()\n", - " \n", - " np.random.shuffle(inputs_shuffled)\n", - " np.random.shuffle(labels_shuffled)\n", - " \n", - " train_end = int(n_inputs*train_size)\n", - " X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:]\n", - " Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:]\n", - " \n", - " return X_train, X_test, Y_train, Y_test\n", - "\n", - "#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size)\n", - "\n", - "print(\"Number of training images: \" + str(len(X_train)))\n", - "print(\"Number of test images: \" + str(len(X_test)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define model and architecture\n", - "\n", - "Our simple feed-forward neural network will consist of an *input* layer, a single *hidden* layer and an *output* layer. The activation $y$ of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have \n", - "\n", - "$$ z = \\sum_{i=1}^n w_i a_i ,$$\n", - "\n", - "$$ y = f(z) ,$$\n", - "\n", - "where $f$ is the activation function, $a_i$ represents input from neuron $i$ in the preceding layer\n", - "and $w_i$ is the weight to input $i$. \n", - "The activation of the neurons in the input layer is just the features (e.g. a pixel value). \n", - "\n", - "The simplest activation function for a neuron is the *Heaviside* function:\n", - "\n", - "$$ f(z) = \n", - "\\begin{cases}\n", - "1, & z > 0\\\\\n", - "0, & \\text{otherwise}\n", - "\\end{cases}\n", - "$$\n", - "\n", - "A feed-forward neural network with this activation is known as a *perceptron*. \n", - "For a binary classifier (i.e. two classes, 0 or 1, dog or not-dog) we can also use this in our output layer. \n", - "This activation can be generalized to $k$ classes (using e.g. the *one-against-all* strategy), \n", - "and we call these architectures *multiclass perceptrons*. \n", - "\n", - "However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and \n", - "Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function. \n", - "\n", - "Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU). \n", - "We will be using the sigmoid function $\\sigma(x)$: \n", - "\n", - "$$ f(x) = \\sigma(x) = \\frac{1}{1 + e^{-x}} ,$$\n", - "\n", - "which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions.\n", - "\n", - "\n", - "## Layers\n", - "\n", - "* Input \n", - "\n", - "Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons. \n", - "\n", - "* Hidden layer\n", - "\n", - "We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer. \n", - "Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer. \n", - "\n", - "* Output\n", - "\n", - "If we were building a binary classifier, it would be sufficient with a single neuron in the output layer,\n", - "which could output 0 or 1 according to the Heaviside function. This would be an example of a *hard* classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a *soft* classifier, which outputs the probability of being in class 0 or 1. \n", - "\n", - "For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class. \n", - "\n", - "Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons $j = 0,1,...,9$. The activation of each output neuron $j$ will be according to the *softmax* function: \n", - "\n", - "$$ P(\\text{class $j$} \\mid \\text{input $\\hat{a}$}) = \\frac{\\exp{(\\hat{a}^T \\hat{w}_j)}}\n", - "{\\sum_{c=0}^{9} \\exp{(\\hat{a}^T \\hat{w}_c)}} ,$$ \n", - "\n", - "i.e. each neuron $j$ outputs the probability of being in class $j$ given an input from the hidden layer $\\hat{a}$, with $\\hat{w}_j$ the weights of neuron $j$ to the inputs. \n", - "The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1. \n", - "The exponent is just the weighted sum of inputs as before: \n", - "\n", - "$$ z_j = \\sum_{i=1}^n w_ {ij} a_i+b_j.$$ \n", - "\n", - "Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500\n", - "weights to the output layer.\n", - "\n", - "\n", - "## Weights and biases\n", - "\n", - "Typically weights are initialized with small values distributed around zero, drawn from a uniform\n", - "or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless. \n", - "\n", - "Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range\n", - "of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron $j$, $b_j$: \n", - "\n", - "$$ z_j = \\sum_{i=1}^n w_ {ij} a_i + b_j.$$ \n", - "\n", - "The bias weights $\\hat{b}$ are often initialized to zero, but a small value like $0.01$ ensures all neurons have some output which can be backpropagated in the first training cycle." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# building our neural network\n", - "\n", - "n_inputs, n_features = X_train.shape\n", - "n_hidden_neurons = 50\n", - "n_categories = 10\n", - "\n", - "# we make the weights normally distributed using numpy.random.randn\n", - "\n", - "# weights and bias in the hidden layer\n", - "hidden_weights = np.random.randn(n_features, n_hidden_neurons)\n", - "hidden_bias = np.zeros(n_hidden_neurons) + 0.01\n", - "\n", - "# weights and bias in the output layer\n", - "output_weights = np.random.randn(n_hidden_neurons, n_categories)\n", - "output_bias = np.zeros(n_categories) + 0.01" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Feed-forward pass\n", - "\n", - "Denote $F$ the number of features, $H$ the number of hidden neurons and $C$ the number of categories. \n", - "For each input image we calculate a weighted sum of input features (pixel values) to each neuron $j$ in the hidden layer $l$: \n", - "\n", - "$$ z_{j}^{l} = \\sum_{i=1}^{F} w_{ij}^{l} x_i + b_{j}^{l},$$\n", - "\n", - "this is then passed through our activation function \n", - "\n", - "$$ a_{j}^{l} = f(z_{j}^{l}) .$$ \n", - "\n", - "We calculate a weighted sum of inputs (activations in the hidden layer) to each neuron $j$ in the output layer: \n", - "\n", - "$$ z_{j}^{L} = \\sum_{i=1}^{H} w_{ij}^{L} a_{i}^{l} + b_{j}^{L}.$$ \n", - "\n", - "Finally we calculate the output of neuron $j$ in the output layer using the softmax function: \n", - "\n", - "$$ a_{j}^{L} = \\frac{\\exp{(z_j^{L})}}\n", - "{\\sum_{c=0}^{C-1} \\exp{(z_c^{L})}} .$$ \n", - "\n", - "\n", - "## Matrix multiplications\n", - "\n", - "Since our data has the dimensions $X = (n_{inputs}, n_{features})$ and our weights to the hidden\n", - "layer have the dimensions \n", - "$W_{hidden} = (n_{features}, n_{hidden})$,\n", - "we can easily feed the network all our training data in one go by taking the matrix product \n", - "\n", - "$$ X W^{h} = (n_{inputs}, n_{hidden}),$$ \n", - "\n", - "and obtain a matrix that holds the weighted sum of inputs to the hidden layer\n", - "for each input image and each hidden neuron. \n", - "We also add the bias to obtain a matrix of weighted sums to the hidden layer $Z^{h}$: \n", - "\n", - "$$ \\hat{z}^{l} = \\hat{X} \\hat{W}^{l} + \\hat{b}^{l} ,$$\n", - "\n", - "meaning the same bias (1D array with size equal number of hidden neurons) is added to each input image. \n", - "This is then passed through the activation: \n", - "\n", - "$$ \\hat{a}^{l} = f(\\hat{z}^l) .$$ \n", - "\n", - "This is fed to the output layer: \n", - "\n", - "$$ \\hat{z}^{L} = \\hat{a}^{L} \\hat{W}^{L} + \\hat{b}^{L} .$$\n", - "\n", - "Finally we receive our output values for each image and each category by passing it through the softmax function: \n", - "\n", - "$$ output = softmax (\\hat{z}^{L}) = (n_{inputs}, n_{categories}) .$$" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# setup the feed-forward pass, subscript h = hidden layer\n", - "\n", - "def sigmoid(x):\n", - " return 1/(1 + np.exp(-x))\n", - "\n", - "def feed_forward(X):\n", - " # weighted sum of inputs to the hidden layer\n", - " z_h = np.matmul(X, hidden_weights) + hidden_bias\n", - " # activation in the hidden layer\n", - " a_h = sigmoid(z_h)\n", - " \n", - " # weighted sum of inputs to the output layer\n", - " z_o = np.matmul(a_h, output_weights) + output_bias\n", - " # softmax output\n", - " # axis 0 holds each input and axis 1 the probabilities of each category\n", - " exp_term = np.exp(z_o)\n", - " probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)\n", - " \n", - " return probabilities\n", - "\n", - "probabilities = feed_forward(X_train)\n", - "print(\"probabilities = (n_inputs, n_categories) = \" + str(probabilities.shape))\n", - "print(\"probability that image 0 is in category 0,1,2,...,9 = \\n\" + str(probabilities[0]))\n", - "print(\"probabilities sum up to: \" + str(probabilities[0].sum()))\n", - "print()\n", - "\n", - "# we obtain a prediction by taking the class with the highest likelihood\n", - "def predict(X):\n", - " probabilities = feed_forward(X)\n", - " return np.argmax(probabilities, axis=1)\n", - "\n", - "predictions = predict(X_train)\n", - "print(\"predictions = (n_inputs) = \" + str(predictions.shape))\n", - "print(\"prediction for image 0: \" + str(predictions[0]))\n", - "print(\"correct label for image 0: \" + str(Y_train[0]))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Choose cost function and optimizer\n", - "\n", - "To measure how well our neural network is doing we need to introduce a cost function. \n", - "We will call the function that gives the error of a single sample output the *loss* function, and the function\n", - "that gives the total error of our network across all samples the *cost* function.\n", - "A typical choice for multiclass classification is the *cross-entropy* loss, also known as the negative log likelihood. \n", - "\n", - "In *multiclass* classification it is common to treat each integer label as a so called *one-hot* vector: \n", - "\n", - "$$ y = 5 \\quad \\rightarrow \\quad \\hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$$ \n", - "\n", - "\n", - "$$ y = 1 \\quad \\rightarrow \\quad \\hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$$ \n", - "\n", - "\n", - "i.e. a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset. \n", - "\n", - "Let $y_{ic}$ denote the $c$-th component of the $i$-th one-hot vector. \n", - "We define the cost function $\\mathcal{C}$ as a sum over the cross-entropy loss for each point $\\hat{x}_i$ in the dataset.\n", - "\n", - "In the one-hot representation only one of the terms in the loss function is non-zero, namely the\n", - "probability of the correct category $c'$ \n", - "(i.e. the category $c'$ such that $y_{ic'} = 1$). This means that the cross entropy loss only punishes you for how wrong\n", - "you got the correct label. The probability of category $c$ is given by the softmax function. The vector $\\hat{\\theta}$ represents the parameters of our network, i.e. all the weights and biases. \n", - "\n", - "\n", - "## Optimizing the cost function\n", - "\n", - "The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is *gradient descent* and its generalizations. The idea behind gradient descent\n", - "is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a *local* minimum of the cost function. \n", - "Each parameter $\\theta$ is iteratively adjusted according to the rule \n", - "\n", - "$$ \\theta_{i+1} = \\theta_i - \\eta \\nabla \\mathcal{C}(\\theta_i) ,$$\n", - "\n", - "where $\\eta$ is known as the *learning rate*, which controls how big a step we take towards the minimum. \n", - "This update can be repeated for any number of iterations, or until we are satisfied with the result. \n", - "\n", - "A simple and effective improvement is a variant called *Batch Gradient Descent*. \n", - "Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient\n", - "on a subset of the data called a *minibatch*. \n", - "If there are $N$ data points and we have a minibatch size of $M$, the total number of batches\n", - "is $N/M$. \n", - "We denote each minibatch $B_k$, with $k = 1, 2,...,N/M$. The gradient then becomes: \n", - "\n", - "$$ \\nabla \\mathcal{C}(\\theta) = \\frac{1}{N} \\sum_{i=1}^N \\nabla \\mathcal{L}_i(\\theta) \\quad \\rightarrow \\quad\n", - "\\frac{1}{M} \\sum_{i \\in B_k} \\nabla \\mathcal{L}_i(\\theta) ,$$\n", - "\n", - "i.e. instead of averaging the loss over the entire dataset, we average over a minibatch. \n", - "\n", - "This has two important benefits: \n", - "1. Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima. \n", - "\n", - "2. It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient. \n", - "\n", - "The various optmization methods, with codes and algorithms, are discussed in our lectures on [Gradient descent approaches](https://compphysics.github.io/MachineLearning/doc/pub/Splines/html/Splines-bs.html).\n", - "\n", - "\n", - "## Regularization\n", - "\n", - "It is common to add an extra term to the cost function, proportional\n", - "to the size of the weights. This is equivalent to constraining the\n", - "size of the weights, so that they do not grow out of control.\n", - "Constraining the size of the weights means that the weights cannot\n", - "grow arbitrarily large to fit the training data, and in this way\n", - "reduces *overfitting*.\n", - "\n", - "We will measure the size of the weights using the so called *L2-norm*, meaning our cost function becomes: \n", - "\n", - "$$ \\nabla \\mathcal{C}(\\theta) = \\frac{1}{N} \\sum_{i=1}^N \\nabla \\mathcal{L}_i(\\theta) \\quad \\rightarrow \\quad\n", - "\\frac{1}{N} \\sum_{i=1}^N \\nabla \\mathcal{L}_i(\\theta) + \\lambda \\lvert \\lvert \\hat{w} \\rvert \\rvert_2^2 \n", - "= \\frac{1}{N} \\sum_{i=1}^N \\nabla \\mathcal{L}(\\theta) + \\lambda \\sum_{ij} w_{ij}^2,$$ \n", - "\n", - "i.e. we sum up all the weights squared. The factor $\\lambda$ is known as a regularization parameter.\n", - "\n", - "\n", - "In order to train the model, we need to calculate the derivative of\n", - "the cost function with respect to every bias and weight in the\n", - "network. In total our network has $(64 + 1)\\times 50=3250$ weights in\n", - "the hidden layer and $(50 + 1)\\times 10=510$ weights to the output\n", - "layer ($+1$ for the bias), and the gradient must be calculated for\n", - "every parameter. We use the *backpropagation* algorithm discussed\n", - "above. This is a clever use of the chain rule that allows us to\n", - "calculate the gradient efficently. \n", - "\n", - "\n", - "## Matrix multiplication\n", - "\n", - "To more efficently train our network these equations are implemented using matrix operations. \n", - "The error in the output layer is calculated simply as, with $\\hat{t}$ being our targets, \n", - "\n", - "$$ \\delta_L = \\hat{t} - \\hat{y} = (n_{inputs}, n_{categories}) .$$ \n", - "\n", - "The gradient for the output weights is calculated as \n", - "\n", - "$$ \\nabla W_{L} = \\hat{a}^T \\delta_L = (n_{hidden}, n_{categories}) ,$$\n", - "\n", - "where $\\hat{a} = (n_{inputs}, n_{hidden})$. This simply means that we are summing up the gradients for each input. \n", - "Since we are going backwards we have to transpose the activation matrix. \n", - "\n", - "The gradient with respect to the output bias is then \n", - "\n", - "$$ \\nabla \\hat{b}_{L} = \\sum_{i=1}^{n_{inputs}} \\delta_L = (n_{categories}) .$$ \n", - "\n", - "The error in the hidden layer is \n", - "\n", - "$$ \\Delta_h = \\delta_L W_{L}^T \\circ f'(z_{h}) = \\delta_L W_{L}^T \\circ a_{h} \\circ (1 - a_{h}) = (n_{inputs}, n_{hidden}) ,$$ \n", - "\n", - "where $f'(a_{h})$ is the derivative of the activation in the hidden layer. The matrix products mean\n", - "that we are summing up the products for each neuron in the output layer. The symbol $\\circ$ denotes\n", - "the *Hadamard product*, meaning element-wise multiplication. \n", - "\n", - "This again gives us the gradients in the hidden layer: \n", - "\n", - "$$ \\nabla W_{h} = X^T \\delta_h = (n_{features}, n_{hidden}) ,$$ \n", - "\n", - "$$ \\nabla b_{h} = \\sum_{i=1}^{n_{inputs}} \\delta_h = (n_{hidden}) .$$" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# to categorical turns our integer vector into a onehot representation\n", - "from sklearn.metrics import accuracy_score\n", - "\n", - "# one-hot in numpy\n", - "def to_categorical_numpy(integer_vector):\n", - " n_inputs = len(integer_vector)\n", - " n_categories = np.max(integer_vector) + 1\n", - " onehot_vector = np.zeros((n_inputs, n_categories))\n", - " onehot_vector[range(n_inputs), integer_vector] = 1\n", - " \n", - " return onehot_vector\n", - "\n", - "#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test)\n", - "Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test)\n", - "\n", - "def feed_forward_train(X):\n", - " # weighted sum of inputs to the hidden layer\n", - " z_h = np.matmul(X, hidden_weights) + hidden_bias\n", - " # activation in the hidden layer\n", - " a_h = sigmoid(z_h)\n", - " \n", - " # weighted sum of inputs to the output layer\n", - " z_o = np.matmul(a_h, output_weights) + output_bias\n", - " # softmax output\n", - " # axis 0 holds each input and axis 1 the probabilities of each category\n", - " exp_term = np.exp(z_o)\n", - " probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)\n", - " \n", - " # for backpropagation need activations in hidden and output layers\n", - " return a_h, probabilities\n", - "\n", - "def backpropagation(X, Y):\n", - " a_h, probabilities = feed_forward_train(X)\n", - " \n", - " # error in the output layer\n", - " error_output = probabilities - Y\n", - " # error in the hidden layer\n", - " error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h)\n", - " \n", - " # gradients for the output layer\n", - " output_weights_gradient = np.matmul(a_h.T, error_output)\n", - " output_bias_gradient = np.sum(error_output, axis=0)\n", - " \n", - " # gradient for the hidden layer\n", - " hidden_weights_gradient = np.matmul(X.T, error_hidden)\n", - " hidden_bias_gradient = np.sum(error_hidden, axis=0)\n", - "\n", - " return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient\n", - "\n", - "print(\"Old accuracy on training data: \" + str(accuracy_score(predict(X_train), Y_train)))\n", - "\n", - "eta = 0.01\n", - "lmbd = 0.01\n", - "for i in range(1000):\n", - " # calculate gradients\n", - " dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot)\n", - " \n", - " # regularization term gradients\n", - " dWo += lmbd * output_weights\n", - " dWh += lmbd * hidden_weights\n", - " \n", - " # update weights and biases\n", - " output_weights -= eta * dWo\n", - " output_bias -= eta * dBo\n", - " hidden_weights -= eta * dWh\n", - " hidden_bias -= eta * dBh\n", - "\n", - "print(\"New accuracy on training data: \" + str(accuracy_score(predict(X_train), Y_train)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Improving performance\n", - "\n", - "As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image. \n", - "In order to obtain a network that does something useful, we will have to do a bit more work. \n", - "\n", - "The choice of *hyperparameters* such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a *grid-search* is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates $\\eta = 10^{-6}, 10^{-5},...,10^{-1}$ with different regularization parameters $\\lambda = 10^{-6},...,10^{-0}$. \n", - "\n", - "Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an *iteration*, and a full training period\n", - "going through the entire dataset ($n/M$ batches) an *epoch*.\n", - "\n", - "If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers. \n", - "Andrew Ng goes through some of these considerations in this [video](https://youtu.be/F1ka6a13S9I). You can find a summary of the video [here](https://kevinzakka.github.io/2016/09/26/applying-deep-learning/). \n", - "\n", - "## Full object-oriented implementation\n", - "\n", - "It is very natural to think of the network as an object, with specific instances of the network\n", - "being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "class NeuralNetwork:\n", - " def __init__(\n", - " self,\n", - " X_data,\n", - " Y_data,\n", - " n_hidden_neurons=50,\n", - " n_categories=10,\n", - " epochs=10,\n", - " batch_size=100,\n", - " eta=0.1,\n", - " lmbd=0.0,\n", - "\n", - " ):\n", - " self.X_data_full = X_data\n", - " self.Y_data_full = Y_data\n", - "\n", - " self.n_inputs = X_data.shape[0]\n", - " self.n_features = X_data.shape[1]\n", - " self.n_hidden_neurons = n_hidden_neurons\n", - " self.n_categories = n_categories\n", - "\n", - " self.epochs = epochs\n", - " self.batch_size = batch_size\n", - " self.iterations = self.n_inputs // self.batch_size\n", - " self.eta = eta\n", - " self.lmbd = lmbd\n", - "\n", - " self.create_biases_and_weights()\n", - "\n", - " def create_biases_and_weights(self):\n", - " self.hidden_weights = np.random.randn(self.n_features, self.n_hidden_neurons)\n", - " self.hidden_bias = np.zeros(self.n_hidden_neurons) + 0.01\n", - "\n", - " self.output_weights = np.random.randn(self.n_hidden_neurons, self.n_categories)\n", - " self.output_bias = np.zeros(self.n_categories) + 0.01\n", - "\n", - " def feed_forward(self):\n", - " # feed-forward for training\n", - " self.z_h = np.matmul(self.X_data, self.hidden_weights) + self.hidden_bias\n", - " self.a_h = sigmoid(self.z_h)\n", - "\n", - " self.z_o = np.matmul(self.a_h, self.output_weights) + self.output_bias\n", - "\n", - " exp_term = np.exp(self.z_o)\n", - " self.probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)\n", - "\n", - " def feed_forward_out(self, X):\n", - " # feed-forward for output\n", - " z_h = np.matmul(X, self.hidden_weights) + self.hidden_bias\n", - " a_h = sigmoid(z_h)\n", - "\n", - " z_o = np.matmul(a_h, self.output_weights) + self.output_bias\n", - " \n", - " exp_term = np.exp(z_o)\n", - " probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True)\n", - " return probabilities\n", - "\n", - " def backpropagation(self):\n", - " error_output = self.probabilities - self.Y_data\n", - " error_hidden = np.matmul(error_output, self.output_weights.T) * self.a_h * (1 - self.a_h)\n", - "\n", - " self.output_weights_gradient = np.matmul(self.a_h.T, error_output)\n", - " self.output_bias_gradient = np.sum(error_output, axis=0)\n", - "\n", - " self.hidden_weights_gradient = np.matmul(self.X_data.T, error_hidden)\n", - " self.hidden_bias_gradient = np.sum(error_hidden, axis=0)\n", - "\n", - " if self.lmbd > 0.0:\n", - " self.output_weights_gradient += self.lmbd * self.output_weights\n", - " self.hidden_weights_gradient += self.lmbd * self.hidden_weights\n", - "\n", - " self.output_weights -= self.eta * self.output_weights_gradient\n", - " self.output_bias -= self.eta * self.output_bias_gradient\n", - " self.hidden_weights -= self.eta * self.hidden_weights_gradient\n", - " self.hidden_bias -= self.eta * self.hidden_bias_gradient\n", - "\n", - " def predict(self, X):\n", - " probabilities = self.feed_forward_out(X)\n", - " return np.argmax(probabilities, axis=1)\n", - "\n", - " def predict_probabilities(self, X):\n", - " probabilities = self.feed_forward_out(X)\n", - " return probabilities\n", - "\n", - " def train(self):\n", - " data_indices = np.arange(self.n_inputs)\n", - "\n", - " for i in range(self.epochs):\n", - " for j in range(self.iterations):\n", - " # pick datapoints with replacement\n", - " chosen_datapoints = np.random.choice(\n", - " data_indices, size=self.batch_size, replace=False\n", - " )\n", - "\n", - " # minibatch training data\n", - " self.X_data = self.X_data_full[chosen_datapoints]\n", - " self.Y_data = self.Y_data_full[chosen_datapoints]\n", - "\n", - " self.feed_forward()\n", - " self.backpropagation()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Evaluate model performance on test data\n", - "\n", - "To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e. the test data. \n", - "We measure the performance of the network using the *accuracy* score. \n", - "The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of $1$. \n", - "\n", - "$$ \\text{Accuracy} = \\frac{\\sum_{i=1}^n I(\\hat{y}_i = y_i)}{n} ,$$ \n", - "\n", - "where $I$ is the indicator function, $1$ if $\\hat{y}_i = y_i$ and $0$ otherwise." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "epochs = 100\n", - "batch_size = 100\n", - "\n", - "dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,\n", - " n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)\n", - "dnn.train()\n", - "test_predict = dnn.predict(X_test)\n", - "\n", - "# accuracy score from scikit library\n", - "print(\"Accuracy score on test set: \", accuracy_score(Y_test, test_predict))\n", - "\n", - "# equivalent in numpy\n", - "def accuracy_score_numpy(Y_test, Y_pred):\n", - " return np.sum(Y_test == Y_pred) / len(Y_test)\n", - "\n", - "#print(\"Accuracy score on test set: \", accuracy_score_numpy(Y_test, test_predict))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adjust hyperparameters\n", - "\n", - "We now perform a grid search to find the optimal hyperparameters for the network. \n", - "Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around $98\\%$ ($2\\%$ error rate)." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "eta_vals = np.logspace(-5, 1, 7)\n", - "lmbd_vals = np.logspace(-5, 1, 7)\n", - "# store the models for later use\n", - "DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)\n", - "\n", - "# grid search\n", - "for i, eta in enumerate(eta_vals):\n", - " for j, lmbd in enumerate(lmbd_vals):\n", - " dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size,\n", - " n_hidden_neurons=n_hidden_neurons, n_categories=n_categories)\n", - " dnn.train()\n", - " \n", - " DNN_numpy[i][j] = dnn\n", - " \n", - " test_predict = dnn.predict(X_test)\n", - " \n", - " print(\"Learning rate = \", eta)\n", - " print(\"Lambda = \", lmbd)\n", - " print(\"Accuracy score on test set: \", accuracy_score(Y_test, test_predict))\n", - " print()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# visual representation of grid search\n", - "# uses seaborn heatmap, you can also do this with matplotlib imshow\n", - "import seaborn as sns\n", - "\n", - "sns.set()\n", - "\n", - "train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "\n", - "for i in range(len(eta_vals)):\n", - " for j in range(len(lmbd_vals)):\n", - " dnn = DNN_numpy[i][j]\n", - " \n", - " train_pred = dnn.predict(X_train) \n", - " test_pred = dnn.predict(X_test)\n", - "\n", - " train_accuracy[i][j] = accuracy_score(Y_train, train_pred)\n", - " test_accuracy[i][j] = accuracy_score(Y_test, test_pred)\n", - "\n", - " \n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(train_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Training Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()\n", - "\n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(test_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Test Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## scikit-learn implementation\n", - "\n", - "**scikit-learn** focuses more\n", - "on traditional machine learning methods, such as regression,\n", - "clustering, decision trees, etc. As such, it has only two types of\n", - "neural networks: Multi Layer Perceptron outputting continuous values,\n", - "*MPLRegressor*, and Multi Layer Perceptron outputting labels,\n", - "*MLPClassifier*. We will see how simple it is to use these classes.\n", - "\n", - "**scikit-learn** implements a few improvements from our neural network,\n", - "such as early stopping, a varying learning rate, different\n", - "optimization methods, etc. We would therefore expect a better\n", - "performance overall." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from sklearn.neural_network import MLPClassifier\n", - "# store models for later use\n", - "DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)\n", - "\n", - "for i, eta in enumerate(eta_vals):\n", - " for j, lmbd in enumerate(lmbd_vals):\n", - " dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic',\n", - " alpha=lmbd, learning_rate_init=eta, max_iter=epochs)\n", - " dnn.fit(X_train, Y_train)\n", - " \n", - " DNN_scikit[i][j] = dnn\n", - " \n", - " print(\"Learning rate = \", eta)\n", - " print(\"Lambda = \", lmbd)\n", - " print(\"Accuracy score on test set: \", dnn.score(X_test, Y_test))\n", - " print()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# optional\n", - "# visual representation of grid search\n", - "# uses seaborn heatmap, could probably do this in matplotlib\n", - "import seaborn as sns\n", - "\n", - "sns.set()\n", - "\n", - "train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "\n", - "for i in range(len(eta_vals)):\n", - " for j in range(len(lmbd_vals)):\n", - " dnn = DNN_scikit[i][j]\n", - " \n", - " train_pred = dnn.predict(X_train) \n", - " test_pred = dnn.predict(X_test)\n", - "\n", - " train_accuracy[i][j] = accuracy_score(Y_train, train_pred)\n", - " test_accuracy[i][j] = accuracy_score(Y_test, test_pred)\n", - "\n", - " \n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(train_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Training Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()\n", - "\n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(test_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Test Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Building neural networks in Tensorflow and Keras\n", - "\n", - "Now we want to build on the experience gained from our neural network implementation in NumPy and scikit-learn\n", - "and use it to construct a neural network in Tensorflow. Once we have constructed a neural network in NumPy\n", - "and Tensorflow, building one in Keras is really quite trivial, though the performance may suffer. \n", - "\n", - "In our previous example we used only one hidden layer, and in this we will use two. From this it should be quite\n", - "clear how to build one using an arbitrary number of hidden layers, using data structures such as Python lists or\n", - "NumPy arrays.\n", - "\n", - "## Tensorflow\n", - "\n", - "Tensorflow is an open source library machine learning library\n", - "developed by the Google Brain team for internal use. It was released\n", - "under the Apache 2.0 open source license in November 9, 2015.\n", - "\n", - "Tensorflow is a computational framework that allows you to construct\n", - "machine learning models at different levels of abstraction, from\n", - "high-level, object-oriented APIs like Keras, down to the C++ kernels\n", - "that Tensorflow is built upon. The higher levels of abstraction are\n", - "simpler to use, but less flexible, and our choice of implementation\n", - "should reflect the problems we are trying to solve.\n", - "\n", - "[Tensorflow uses](https://www.tensorflow.org/guide/graphs) so-called graphs to represent your computation\n", - "in terms of the dependencies between individual operations, such that you first build a Tensorflow *graph*\n", - "to represent your model, and then create a Tensorflow *session* to run the graph.\n", - "\n", - "In this guide we will analyze the same data as we did in our NumPy and\n", - "scikit-learn tutorial, gathered from the MNIST database of images. We\n", - "will give an introduction to the lower level Python Application\n", - "Program Interfaces (APIs), and see how we use them to build our graph.\n", - "Then we will build (effectively) the same graph in Keras, to see just\n", - "how simple solving a machine learning problem can be.\n", - "\n", - "To install tensorflow on Unix/Linux systems, use pip as" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "pip3 install tensorflow" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and/or if you use **anaconda**, just write (or install from the graphical user interface)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "conda install tensorflow" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Collect and pre-process data" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# import necessary packages\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from sklearn import datasets\n", - "\n", - "\n", - "# ensure the same random numbers appear every time\n", - "np.random.seed(0)\n", - "\n", - "# display images in notebook\n", - "%matplotlib inline\n", - "plt.rcParams['figure.figsize'] = (12,12)\n", - "\n", - "\n", - "# download MNIST dataset\n", - "digits = datasets.load_digits()\n", - "\n", - "# define inputs and labels\n", - "inputs = digits.images\n", - "labels = digits.target\n", - "\n", - "print(\"inputs = (n_inputs, pixel_width, pixel_height) = \" + str(inputs.shape))\n", - "print(\"labels = (n_inputs) = \" + str(labels.shape))\n", - "\n", - "\n", - "# flatten the image\n", - "# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64\n", - "n_inputs = len(inputs)\n", - "inputs = inputs.reshape(n_inputs, -1)\n", - "print(\"X = (n_inputs, n_features) = \" + str(inputs.shape))\n", - "\n", - "\n", - "# choose some random images to display\n", - "indices = np.arange(n_inputs)\n", - "random_indices = np.random.choice(indices, size=5)\n", - "\n", - "for i, image in enumerate(digits.images[random_indices]):\n", - " plt.subplot(1, 5, i+1)\n", - " plt.axis('off')\n", - " plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')\n", - " plt.title(\"Label: %d\" % digits.target[random_indices[i]])\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from keras.utils import to_categorical\n", - "from sklearn.model_selection import train_test_split\n", - "\n", - "# one-hot representation of labels\n", - "labels = to_categorical(labels)\n", - "\n", - "# split into train and test data\n", - "train_size = 0.8\n", - "test_size = 1 - train_size\n", - "X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,\n", - " test_size=test_size)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using TensorFlow backend\n", - "\n", - "1. Define model and architecture\n", - "\n", - "2. Choose cost function and optimizer" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import tensorflow as tf\n", - "\n", - "class NeuralNetworkTensorflow:\n", - " def __init__(\n", - " self,\n", - " X_train,\n", - " Y_train,\n", - " X_test,\n", - " Y_test,\n", - " n_neurons_layer1=100,\n", - " n_neurons_layer2=50,\n", - " n_categories=2,\n", - " epochs=10,\n", - " batch_size=100,\n", - " eta=0.1,\n", - " lmbd=0.0,\n", - " ):\n", - " \n", - " # keep track of number of steps\n", - " self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')\n", - " \n", - " self.X_train = X_train\n", - " self.Y_train = Y_train\n", - " self.X_test = X_test\n", - " self.Y_test = Y_test\n", - " \n", - " self.n_inputs = X_train.shape[0]\n", - " self.n_features = X_train.shape[1]\n", - " self.n_neurons_layer1 = n_neurons_layer1\n", - " self.n_neurons_layer2 = n_neurons_layer2\n", - " self.n_categories = n_categories\n", - " \n", - " self.epochs = epochs\n", - " self.batch_size = batch_size\n", - " self.iterations = self.n_inputs // self.batch_size\n", - " self.eta = eta\n", - " self.lmbd = lmbd\n", - " \n", - " # build network piece by piece\n", - " # name scopes (with) are used to enforce creation of new variables\n", - " # https://www.tensorflow.org/guide/variables\n", - " self.create_placeholders()\n", - " self.create_DNN()\n", - " self.create_loss()\n", - " self.create_optimiser()\n", - " self.create_accuracy()\n", - " \n", - " def create_placeholders(self):\n", - " # placeholders are fine here, but \"Datasets\" are the preferred method\n", - " # of streaming data into a model\n", - " with tf.name_scope('data'):\n", - " self.X = tf.placeholder(tf.float32, shape=(None, self.n_features), name='X_data')\n", - " self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')\n", - " \n", - " def create_DNN(self):\n", - " with tf.name_scope('DNN'):\n", - " # the weights are stored to calculate regularization loss later\n", - " \n", - " # Fully connected layer 1\n", - " self.W_fc1 = self.weight_variable([self.n_features, self.n_neurons_layer1], name='fc1', dtype=tf.float32)\n", - " b_fc1 = self.bias_variable([self.n_neurons_layer1], name='fc1', dtype=tf.float32)\n", - " a_fc1 = tf.nn.sigmoid(tf.matmul(self.X, self.W_fc1) + b_fc1)\n", - " \n", - " # Fully connected layer 2\n", - " self.W_fc2 = self.weight_variable([self.n_neurons_layer1, self.n_neurons_layer2], name='fc2', dtype=tf.float32)\n", - " b_fc2 = self.bias_variable([self.n_neurons_layer2], name='fc2', dtype=tf.float32)\n", - " a_fc2 = tf.nn.sigmoid(tf.matmul(a_fc1, self.W_fc2) + b_fc2)\n", - " \n", - " # Output layer\n", - " self.W_out = self.weight_variable([self.n_neurons_layer2, self.n_categories], name='out', dtype=tf.float32)\n", - " b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)\n", - " self.z_out = tf.matmul(a_fc2, self.W_out) + b_out\n", - " \n", - " def create_loss(self):\n", - " with tf.name_scope('loss'):\n", - " softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))\n", - " \n", - " regularizer_loss_fc1 = tf.nn.l2_loss(self.W_fc1)\n", - " regularizer_loss_fc2 = tf.nn.l2_loss(self.W_fc2)\n", - " regularizer_loss_out = tf.nn.l2_loss(self.W_out)\n", - " regularizer_loss = self.lmbd*(regularizer_loss_fc1 + regularizer_loss_fc2 + regularizer_loss_out)\n", - " \n", - " self.loss = softmax_loss + regularizer_loss\n", - "\n", - " def create_accuracy(self):\n", - " with tf.name_scope('accuracy'):\n", - " probabilities = tf.nn.softmax(self.z_out)\n", - " predictions = tf.argmax(probabilities, axis=1)\n", - " labels = tf.argmax(self.Y, axis=1)\n", - " \n", - " correct_predictions = tf.equal(predictions, labels)\n", - " correct_predictions = tf.cast(correct_predictions, tf.float32)\n", - " self.accuracy = tf.reduce_mean(correct_predictions)\n", - " \n", - " def create_optimiser(self):\n", - " with tf.name_scope('optimizer'):\n", - " self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)\n", - " \n", - " def weight_variable(self, shape, name='', dtype=tf.float32):\n", - " initial = tf.truncated_normal(shape, stddev=0.1)\n", - " return tf.Variable(initial, name=name, dtype=dtype)\n", - " \n", - " def bias_variable(self, shape, name='', dtype=tf.float32):\n", - " initial = tf.constant(0.1, shape=shape)\n", - " return tf.Variable(initial, name=name, dtype=dtype)\n", - " \n", - " def fit(self):\n", - " data_indices = np.arange(self.n_inputs)\n", - "\n", - " with tf.Session() as sess:\n", - " sess.run(tf.global_variables_initializer())\n", - " for i in range(self.epochs):\n", - " for j in range(self.iterations):\n", - " chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)\n", - " batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]\n", - " \n", - " sess.run([DNN.loss, DNN.optimizer],\n", - " feed_dict={DNN.X: batch_X,\n", - " DNN.Y: batch_Y})\n", - " accuracy = sess.run(DNN.accuracy,\n", - " feed_dict={DNN.X: batch_X,\n", - " DNN.Y: batch_Y})\n", - " step = sess.run(DNN.global_step)\n", - " \n", - " self.train_loss, self.train_accuracy = sess.run([DNN.loss, DNN.accuracy],\n", - " feed_dict={DNN.X: self.X_train,\n", - " DNN.Y: self.Y_train})\n", - " \n", - " self.test_loss, self.test_accuracy = sess.run([DNN.loss, DNN.accuracy],\n", - " feed_dict={DNN.X: self.X_test,\n", - " DNN.Y: self.Y_test})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Optimizing and using gradient descent" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "epochs = 100\n", - "batch_size = 100\n", - "n_neurons_layer1 = 100\n", - "n_neurons_layer2 = 50\n", - "n_categories = 10\n", - "eta_vals = np.logspace(-5, 1, 7)\n", - "lmbd_vals = np.logspace(-5, 1, 7)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "DNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)\n", - " \n", - "for i, eta in enumerate(eta_vals):\n", - " for j, lmbd in enumerate(lmbd_vals):\n", - " DNN = NeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,\n", - " n_neurons_layer1, n_neurons_layer2, n_categories,\n", - " epochs=epochs, batch_size=batch_size, eta=eta, lmbd=lmbd)\n", - " DNN.fit()\n", - " \n", - " DNN_tf[i][j] = DNN\n", - " \n", - " print(\"Learning rate = \", eta)\n", - " print(\"Lambda = \", lmbd)\n", - " print(\"Test accuracy: %.3f\" % DNN.test_accuracy)\n", - " print()" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# optional\n", - "# visual representation of grid search\n", - "# uses seaborn heatmap, could probably do this in matplotlib\n", - "import seaborn as sns\n", - "\n", - "sns.set()\n", - "\n", - "train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "\n", - "for i in range(len(eta_vals)):\n", - " for j in range(len(lmbd_vals)):\n", - " DNN = DNN_tf[i][j]\n", - "\n", - " train_accuracy[i][j] = DNN.train_accuracy\n", - " test_accuracy[i][j] = DNN.test_accuracy\n", - "\n", - " \n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(train_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Training Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()\n", - "\n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(test_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Test Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# optional\n", - "# we can use log files to visualize our graph in Tensorboard\n", - "writer = tf.summary.FileWriter('logs/')\n", - "writer.add_graph(tf.get_default_graph())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using Keras\n", - "\n", - "Keras is a high level [neural network](https://en.wikipedia.org/wiki/Application_programming_interface)\n", - "that supports Tensorflow, CTNK and Theano as backends. \n", - "If you have Tensorflow installed Keras is available through the *tf.keras* module. \n", - "If you have Anaconda installed you may run the following command" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "conda install keras" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Alternatively, if you have Tensorflow or one of the other supported backends install you may use the pip package manager:" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "pip3 install keras" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "or look up the [instructions here](https://keras.io/)." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from keras.models import Sequential\n", - "from keras.layers import Dense\n", - "from keras.regularizers import l2\n", - "from keras.optimizers import SGD\n", - "\n", - "def create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, eta, lmbd):\n", - " model = Sequential()\n", - " model.add(Dense(n_neurons_layer1, activation='sigmoid', kernel_regularizer=l2(lmbd)))\n", - " model.add(Dense(n_neurons_layer2, activation='sigmoid', kernel_regularizer=l2(lmbd)))\n", - " model.add(Dense(n_categories, activation='softmax'))\n", - " \n", - " sgd = SGD(lr=eta)\n", - " model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])\n", - " \n", - " return model" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "DNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)\n", - " \n", - "for i, eta in enumerate(eta_vals):\n", - " for j, lmbd in enumerate(lmbd_vals):\n", - " DNN = create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories,\n", - " eta=eta, lmbd=lmbd)\n", - " DNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)\n", - " scores = DNN.evaluate(X_test, Y_test)\n", - " \n", - " DNN_keras[i][j] = DNN\n", - " \n", - " print(\"Learning rate = \", eta)\n", - " print(\"Lambda = \", lmbd)\n", - " print(\"Test accuracy: %.3f\" % scores[1])\n", - " print()" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# optional\n", - "# visual representation of grid search\n", - "# uses seaborn heatmap, could probably do this in matplotlib\n", - "import seaborn as sns\n", - "\n", - "sns.set()\n", - "\n", - "train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "\n", - "for i in range(len(eta_vals)):\n", - " for j in range(len(lmbd_vals)):\n", - " DNN = DNN_keras[i][j]\n", - "\n", - " train_accuracy[i][j] = DNN.evaluate(X_train, Y_train)[1]\n", - " test_accuracy[i][j] = DNN.evaluate(X_test, Y_test)[1]\n", - "\n", - " \n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(train_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Training Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()\n", - "\n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(test_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Test Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Which activation function should I use?\n", - "\n", - "The Back propagation algorithm we derived above works by going from\n", - "the output layer to the input layer, propagating the error gradient on\n", - "the way. Once the algorithm has computed the gradient of the cost\n", - "function with regards to each parameter in the network, it uses these\n", - "gradients to update each parameter with a Gradient Descent (GD) step.\n", - "\n", - "\n", - "Unfortunately for us, the gradients often get smaller and smaller as the\n", - "algorithm progresses down to the first hidden layers. As a result, the\n", - "GD update leaves the lower layer connection weights\n", - "virtually unchanged, and training never converges to a good\n", - "solution. This is known in the literature as \n", - "**the vanishing gradients problem**. \n", - "\n", - "In other cases, the opposite can happen, namely the the gradients can grow bigger and\n", - "bigger. The result is that many of the layers get large updates of the \n", - "weights the\n", - "algorithm diverges. This is the **exploding gradients problem**, which is\n", - "mostly encountered in recurrent neural networks. More generally, deep\n", - "neural networks suffer from unstable gradients, different layers may\n", - "learn at widely different speeds\n", - "\n", - "\n", - "## Is the Logistic activation function (Sigmoid) our choice?\n", - "\n", - "Although this unfortunate behavior has been empirically observed for\n", - "quite a while (it was one of the reasons why deep neural networks were\n", - "mostly abandoned for a long time), it is only around 2010 that\n", - "significant progress was made in understanding it.\n", - "\n", - "A paper titled [Understanding the Difficulty of Training Deep\n", - "Feedforward Neural Networks by Xavier Glorot and Yoshua Bengio](http://proceedings.mlr.press/v9/glorot10a.html) found that\n", - "the problems with the popular logistic\n", - "sigmoid activation function and the weight initialization technique\n", - "that was most popular at the time, namely random initialization using\n", - "a normal distribution with a mean of 0 and a standard deviation of\n", - "1. \n", - "\n", - "They showed that with this activation function and this\n", - "initialization scheme, the variance of the outputs of each layer is\n", - "much greater than the variance of its inputs. Going forward in the\n", - "network, the variance keeps increasing after each layer until the\n", - "activation function saturates at the top layers. This is actually made\n", - "worse by the fact that the logistic function has a mean of 0.5, not 0\n", - "(the hyperbolic tangent function has a mean of 0 and behaves slightly\n", - "better than the logistic function in deep networks).\n", - "\n", - "\n", - "## The derivative of the Logistic funtion\n", - "\n", - "Looking at the logistic activation function, when inputs become large\n", - "(negative or positive), the function saturates at 0 or 1, with a\n", - "derivative extremely close to 0. Thus when backpropagation kicks in,\n", - "it has virtually no gradient to propagate back through the network,\n", - "and what little gradient exists keeps getting diluted as\n", - "backpropagation progresses down through the top layers, so there is\n", - "really nothing left for the lower layers.\n", - "\n", - "In their paper, Glorot and Bengio propose a way to significantly\n", - "alleviate this problem. We need the signal to flow properly in both\n", - "directions: in the forward direction when making predictions, and in\n", - "the reverse direction when backpropagating gradients. We don’t want\n", - "the signal to die out, nor do we want it to explode and saturate. For\n", - "the signal to flow properly, the authors argue that we need the\n", - "variance of the outputs of each layer to be equal to the variance of\n", - "its inputs, and we also need the gradients to have equal variance\n", - "before and after flowing through a layer in the reverse direction.\n", - "\n", - "\n", - "\n", - "One of the insights in the 2010 paper by Glorot and Bengio was that\n", - "the vanishing/exploding gradients problems were in part due to a poor\n", - "choice of activation function. Until then most people had assumed that\n", - "if Nature had chosen to use roughly sigmoid activation functions in\n", - "biological neurons, they must be an excellent choice. But it turns out\n", - "that other activation functions behave much better in deep neural\n", - "networks, in particular the ReLU activation function, mostly because\n", - "it does not saturate for positive values (and also because it is quite\n", - "fast to compute).\n", - "\n", - "\n", - "## The RELU function family\n", - "\n", - "The ReLU activation function suffers from a problem known as the dying\n", - "ReLUs: during training, some neurons effectively die, meaning they\n", - "stop outputting anything other than 0.\n", - "\n", - "In some cases, you may find that half of your network’s neurons are\n", - "dead, especially if you used a large learning rate. During training,\n", - "if a neuron’s weights get updated such that the weighted sum of the\n", - "neuron’s inputs is negative, it will start outputting 0. When this\n", - "happen, the neuron is unlikely to come back to life since the gradient\n", - "of the ReLU function is 0 when its input is negative.\n", - "\n", - "To solve this problem, nowadays practitioners use a variant of the ReLU\n", - "function, such as the leaky ReLU discussed above or the so-called\n", - "exponential linear unit (ELU) function" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "ELU(z) = \\left\\{\\begin{array}{cc} \\alpha\\left( \\exp{(z)}-1\\right) & z < 0,\\\\ z & z \\ge 0.\\end{array}\\right.\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Which activation function should we use?\n", - "\n", - "In general it seems that the ELU activation function is better than\n", - "the leaky ReLU function (and its variants), which is better than\n", - "ReLU. ReLU performs better than $\\tanh$ which in turn performs better\n", - "than the logistic function. \n", - "\n", - "If runtime\n", - "performance is an issue, then you may opt for the leaky ReLU function over the \n", - "ELU function If you don’t\n", - "want to tweak yet another hyperparameter, you may just use the default\n", - "$\\alpha$ of $0.01$ for the leaky ReLU, and $1$ for ELU. If you have\n", - "spare time and computing power, you can use cross-validation or\n", - "bootstrap to evaluate other activation functions.\n", - "\n", - "\n", - "\n", - "## A top-down perspective on Neural networks\n", - "\n", - "\n", - "The first thing we would like to do is divide the data into two or three\n", - "parts. A training set, a validation or dev (development) set, and a\n", - "test set. The test set is the data on which we want to make\n", - "predictions. The dev set is a subset of the training data we use to\n", - "check how well we are doing out-of-sample, after training the model on\n", - "the training dataset. We use the validation error as a proxy for the\n", - "test error in order to make tweaks to our model. It is crucial that we\n", - "do not use any of the test data to train the algorithm. This is a\n", - "cardinal sin in ML. Then:\n", - "\n", - "\n", - "* Estimate optimal error rate\n", - "\n", - "* Minimize underfitting (bias) on training data set.\n", - "\n", - "* Make sure you are not overfitting.\n", - "\n", - "If the validation and test sets are drawn from the same distributions,\n", - "then a good performance on the validation set should lead to similarly\n", - "good performance on the test set. \n", - "\n", - "However, sometimes\n", - "the training data and test data differ in subtle ways because, for\n", - "example, they are collected using slightly different methods, or\n", - "because it is cheaper to collect data in one way versus another. In\n", - "this case, there can be a mismatch between the training and test\n", - "data. This can lead to the neural network overfitting these small\n", - "differences between the test and training sets, and a poor performance\n", - "on the test set despite having a good performance on the validation\n", - "set. To rectify this, Andrew Ng suggests making two validation or dev\n", - "sets, one constructed from the training data and one constructed from\n", - "the test data. The difference between the performance of the algorithm\n", - "on these two validation sets quantifies the train-test mismatch. This\n", - "can serve as another important diagnostic when using DNNs for\n", - "supervised learning.\n", - "\n", - "## Limitations of supervised learning with deep networks\n", - "\n", - "Like all statistical methods, supervised learning using neural\n", - "networks has important limitations. This is especially important when\n", - "one seeks to apply these methods, especially to physics problems. Like\n", - "all tools, DNNs are not a universal solution. Often, the same or\n", - "better performance on a task can be achieved by using a few\n", - "hand-engineered features (or even a collection of random\n", - "features). \n", - "\n", - "Here we list some of the important limitations of supervised neural network based models. \n", - "\n", - "\n", - "\n", - "* **Need labeled data**. All supervised learning methods, DNNs for supervised learning require labeled data. Often, labeled data is harder to acquire than unlabeled data (e.g. one must pay for human experts to label images).\n", - "\n", - "* **Supervised neural networks are extremely data intensive.** DNNs are data hungry. They perform best when data is plentiful. This is doubly so for supervised methods where the data must also be labeled. The utility of DNNs is extremely limited if data is hard to acquire or the datasets are small (hundreds to a few thousand samples). In this case, the performance of other methods that utilize hand-engineered features can exceed that of DNNs.\n", - "\n", - "* **Homogeneous data.** Almost all DNNs deal with homogeneous data of one type. It is very hard to design architectures that mix and match data types (i.e. some continuous variables, some discrete variables, some time series). In applications beyond images, video, and language, this is often what is required. In contrast, ensemble models like random forests or gradient-boosted trees have no difficulty handling mixed data types.\n", - "\n", - "* **Many problems are not about prediction.** In natural science we are often interested in learning something about the underlying distribution that generates the data. In this case, it is often difficult to cast these ideas in a supervised learning setting. While the problems are related, it is possible to make good predictions with a *wrong* model. The model might or might not be useful for understanding the underlying science.\n", - "\n", - "Some of these remarks are particular to DNNs, others are shared by all supervised learning methods. This motivates the use of unsupervised methods which in part circumvent these problems.\n", - "\n", - "\n", - "## Convolutional Neural Networks (recognizing images)\n", - "\n", - "Convolutional Neural Networks (CNN) are very similar to ordinary Neural Networks.\n", - "\n", - "They are made up of neurons that have learnable weights and\n", - "biases. Each neuron receives some inputs, performs a dot product and\n", - "optionally follows it with a non-linearity. The whole network still\n", - "expresses a single differentiable score function: from the raw image\n", - "pixels on one end to class scores at the other. And they still have a\n", - "loss function (for example Softmax) on the last (fully-connected) layer\n", - "and all the tips/tricks we developed for learning regular Neural\n", - "Networks still apply (back propagation, gradient descent etc etc).\n", - "\n", - "What is the difference? **CNN architectures make the explicit assumption that\n", - "the inputs are images, which allows us to encode certain properties\n", - "into the architecture. These then make the forward function more\n", - "efficient to implement and vastly reduce the amount of parameters in\n", - "the network.**\n", - "\n", - "Here we provide only a superficial overview, for the more interested, we recommend highly the course\n", - "[IN5400 – Machine Learning for Image Analysis](https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html)\n", - "and the slides of [CS231](http://cs231n.github.io/convolutional-networks/).\n", - "\n", - "## Regular NNs don’t scale well to full images\n", - "\n", - "As an example, consider\n", - "an image of size $32\\times 32\\times 3$ (32 wide, 32 high, 3 color channels), so a\n", - "single fully-connected neuron in a first hidden layer of a regular\n", - "Neural Network would have $32\\times 32\\times 3 = 3072$ weights. This amount still\n", - "seems manageable, but clearly this fully-connected structure does not\n", - "scale to larger images. For example, an image of more respectable\n", - "size, say $200\\times 200\\times 3$, would lead to neurons that have \n", - "$200\\times 200\\times 3 = 120,000$ weights. \n", - "\n", - "We could have\n", - "several such neurons, and the parameters would add up quickly! Clearly,\n", - "this full connectivity is wasteful and the huge number of parameters\n", - "would quickly lead to possible overfitting.\n", - "\n", - "\n", - "\n", - "\n", - "

A regular 3-layer Neural Network.

\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "## 3D volumes of neurons\n", - "\n", - "Convolutional Neural Networks take advantage of the fact that the\n", - "input consists of images and they constrain the architecture in a more\n", - "sensible way. \n", - "\n", - "In particular, unlike a regular Neural Network, the\n", - "layers of a CNN have neurons arranged in 3 dimensions: width,\n", - "height, depth. (Note that the word depth here refers to the third\n", - "dimension of an activation volume, not to the depth of a full Neural\n", - "Network, which can refer to the total number of layers in a network.)\n", - "\n", - "To understand it better, the above example of an image \n", - "with an input volume of\n", - "activations has dimensions $32\\times 32\\times 3$ (width, height,\n", - "depth respectively). \n", - "\n", - "The neurons in a layer will\n", - "only be connected to a small region of the layer before it, instead of\n", - "all of the neurons in a fully-connected manner. Moreover, the final\n", - "output layer could for this specific image have dimensions $1\\times 1 times 10$, \n", - "because by the\n", - "end of the CNN architecture we will reduce the full image into a\n", - "single vector of class scores, arranged along the depth\n", - "dimension. \n", - "\n", - "\n", - "\n", - "\n", - "

A CNN arranges its neurons in three dimensions (width, height, depth), as visualized in one of the layers. Every layer of a CNN transforms the 3D input volume to a 3D output volume of neuron activations. In this example, the red input layer holds the image, so its width and height would be the dimensions of the image, and the depth would be 3 (Red, Green, Blue channels).

\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "## Layers used to build CNNs\n", - "\n", - "\n", - "A simple CNN is a sequence of layers, and every layer of a CNN\n", - "transforms one volume of activations to another through a\n", - "differentiable function. We use three main types of layers to build\n", - "CNN architectures: Convolutional Layer, Pooling Layer, and\n", - "Fully-Connected Layer (exactly as seen in regular Neural Networks). We\n", - "will stack these layers to form a full CNN architecture.\n", - "\n", - "A simple CNN for image classification could have the architecture:\n", - "\n", - "* **INPUT** ($32\\times 32 \\times 3$) will hold the raw pixel values of the image, in this case an image of width 32, height 32, and with three color channels R,G,B.\n", - "\n", - "* **CONV** (convolutional )layer will compute the output of neurons that are connected to local regions in the input, each computing a dot product between their weights and a small region they are connected to in the input volume. This may result in volume such as $[32\\times 32\\times 12]$ if we decided to use 12 filters.\n", - "\n", - "* **RELU** layer will apply an elementwise activation function, such as the $max(0,x)$ thresholding at zero. This leaves the size of the volume unchanged ($[32\\times 32\\times 12]$).\n", - "\n", - "* **POOL** (pooling) layer will perform a downsampling operation along the spatial dimensions (width, height), resulting in volume such as $[16\\times 16\\times 12]$.\n", - "\n", - "* **FC** (i.e. fully-connected) layer will compute the class scores, resulting in volume of size $[1\\times 1\\times 10]$, where each of the 10 numbers correspond to a class score, such as among the 10 categories of the MNIST images we considered above . As with ordinary Neural Networks and as the name implies, each neuron in this layer will be connected to all the numbers in the previous volume.\n", - "\n", - "## Transforming images\n", - "\n", - "CNNs transform the original image layer by layer from the original\n", - "pixel values to the final class scores. \n", - "\n", - "Observe that some layers contain\n", - "parameters and other don’t. In particular, the CNN layers perform\n", - "transformations that are a function of not only the activations in the\n", - "input volume, but also of the parameters (the weights and biases of\n", - "the neurons). On the other hand, the RELU/POOL layers will implement a\n", - "fixed function. The parameters in the CONV/FC layers will be trained\n", - "with gradient descent so that the class scores that the CNN computes\n", - "are consistent with the labels in the training set for each image.\n", - "\n", - "\n", - "## CNNs in brief\n", - "\n", - "In summary:\n", - "\n", - "* A CNN architecture is in the simplest case a list of Layers that transform the image volume into an output volume (e.g. holding the class scores)\n", - "\n", - "* There are a few distinct types of Layers (e.g. CONV/FC/RELU/POOL are by far the most popular)\n", - "\n", - "* Each Layer accepts an input 3D volume and transforms it to an output 3D volume through a differentiable function\n", - "\n", - "* Each Layer may or may not have parameters (e.g. CONV/FC do, RELU/POOL don’t)\n", - "\n", - "* Each Layer may or may not have additional hyperparameters (e.g. CONV/FC/POOL do, RELU doesn’t)\n", - "\n", - "For more material on convolutional networks, we strongly recommend\n", - "the course\n", - "[IN5400 – Machine Learning for Image Analysis](https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html)\n", - "and the slides of [CS231](http://cs231n.github.io/convolutional-networks/) which is taught at Stanford University (consistently ranked as one of the top computer science programs in the world). [Michael Nielsen's book is a must read, in particular chapter 6 which deals with CNNs](http://neuralnetworksanddeeplearning.com/chap6.html).\n", - "\n", - "\n", - "## CNNs in more detail, building convolutional neural networks in Tensorflow and Keras\n", - "\n", - "\n", - "As discussed above, CNNs are neural networks built from the assumption that the inputs\n", - "to the network are 2D images. This is important because the number of features or pixels in images\n", - "grows very fast with the image size, and an enormous number of weights and biases are needed in order to build an accurate network. \n", - "\n", - "As before, we still have our input, a hidden layer and an output. What's novel about convolutional networks\n", - "are the **convolutional** and **pooling** layers stacked in pairs between the input and the hidden layer.\n", - "In addition, the data is no longer represented as a 2D feature matrix, instead each input is a number of 2D\n", - "matrices, typically 1 for each color dimension (Red, Green, Blue). \n", - "\n", - "\n", - "## Setting it up\n", - "\n", - "It means that to represent the entire\n", - "dataset of images, we require a 4D matrix or **tensor**. This tensor has the dimensions:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "(n_{inputs},\\, n_{pixels, width},\\, n_{pixels, height},\\, depth) .\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## The MNIST dataset again\n", - "\n", - "The MNIST dataset consists of grayscale images with a pixel size of\n", - "$28\\times 28$, meaning we require $28 \\times 28 = 724$ weights to each\n", - "neuron in the first hidden layer.\n", - "\n", - "If we were to analyze images of size $128\\times 128$ we would require\n", - "$128 \\times 128 = 16384$ weights to each neuron. Even worse if we were\n", - "dealing with color images, as most images are, we have an image matrix\n", - "of size $128\\times 128$ for each color dimension (Red, Green, Blue),\n", - "meaning 3 times the number of weights $= 49152$ are required for every\n", - "single neuron in the first hidden layer.\n", - "\n", - "\n", - "## Strong correlations\n", - "Images typically have strong local correlations, meaning that a small\n", - "part of the image varies little from its neighboring regions. If for\n", - "example we have an image of a blue car, we can roughly assume that a\n", - "small blue part of the image is surrounded by other blue regions.\n", - "\n", - "Therefore, instead of connecting every single pixel to a neuron in the\n", - "first hidden layer, as we have previously done with deep neural\n", - "networks, we can instead connect each neuron to a small part of the\n", - "image (in all 3 RGB depth dimensions). The size of each small area is\n", - "fixed, and known as a [receptive](https://en.wikipedia.org/wiki/Receptive_field).\n", - "\n", - "\n", - "\n", - "## Layers of a CNN\n", - "The layers of a convolutional neural network arrange neurons in 3D: width, height and depth. \n", - "The input image is typically a square matrix of depth 3. \n", - "\n", - "A **convolution** is performed on the image which outputs\n", - "a 3D volume of neurons. The weights to the input are arranged in a number of 2D matrices, known as **filters**.\n", - "\n", - "\n", - "Each filter slides along the input image, taking the dot product\n", - "between each small part of the image and the filter, in all depth\n", - "dimensions. This is then passed through a non-linear function,\n", - "typically the **Rectified Linear (ReLu)** function, which serves as the\n", - "activation of the neurons in the first convolutional layer. This is\n", - "further passed through a **pooling layer**, which reduces the size of the\n", - "convolutional layer, e.g. by taking the maximum or average across some\n", - "small regions, and this serves as input to the next convolutional\n", - "layer.\n", - "\n", - "\n", - "## Systematic reduction\n", - "\n", - "By systematically reducing the size of the input volume, through\n", - "convolution and pooling, the network should create representations of\n", - "small parts of the input, and then from them assemble representations\n", - "of larger areas. The final pooling layer is flattened to serve as\n", - "input to a hidden layer, such that each neuron in the final pooling\n", - "layer is connected to every single neuron in the hidden layer. This\n", - "then serves as input to the output layer, e.g. a softmax output for\n", - "classification.\n", - "\n", - "\n", - "## Prerequisites: Collect and pre-process data" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# import necessary packages\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from sklearn import datasets\n", - "\n", - "\n", - "# ensure the same random numbers appear every time\n", - "np.random.seed(0)\n", - "\n", - "# display images in notebook\n", - "%matplotlib inline\n", - "plt.rcParams['figure.figsize'] = (12,12)\n", - "\n", - "\n", - "# download MNIST dataset\n", - "digits = datasets.load_digits()\n", - "\n", - "# define inputs and labels\n", - "inputs = digits.images\n", - "labels = digits.target\n", - "\n", - "# RGB images have a depth of 3\n", - "# our images are grayscale so they should have a depth of 1\n", - "inputs = inputs[:,:,:,np.newaxis]\n", - "\n", - "print(\"inputs = (n_inputs, pixel_width, pixel_height, depth) = \" + str(inputs.shape))\n", - "print(\"labels = (n_inputs) = \" + str(labels.shape))\n", - "\n", - "\n", - "# choose some random images to display\n", - "n_inputs = len(inputs)\n", - "indices = np.arange(n_inputs)\n", - "random_indices = np.random.choice(indices, size=5)\n", - "\n", - "for i, image in enumerate(digits.images[random_indices]):\n", - " plt.subplot(1, 5, i+1)\n", - " plt.axis('off')\n", - " plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')\n", - " plt.title(\"Label: %d\" % digits.target[random_indices[i]])\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Keras and Tensorflow" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from keras.utils import to_categorical\n", - "from sklearn.model_selection import train_test_split\n", - "\n", - "# representation of labels\n", - "labels = to_categorical(labels)\n", - "\n", - "# split into train and test data\n", - "# one-liner from scikit-learn library\n", - "train_size = 0.8\n", - "test_size = 1 - train_size\n", - "X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size,\n", - " test_size=test_size)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using TensorFlow backend\n", - "\n", - "We need to define model and architecture and choose cost function and optmizer." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "\n", - "import tensorflow as tf\n", - "\n", - "class ConvolutionalNeuralNetworkTensorflow:\n", - " def __init__(\n", - " self,\n", - " X_train,\n", - " Y_train,\n", - " X_test,\n", - " Y_test,\n", - " n_filters=10,\n", - " n_neurons_connected=50,\n", - " n_categories=10,\n", - " receptive_field=3,\n", - " stride=1,\n", - " padding=1,\n", - " epochs=10,\n", - " batch_size=100,\n", - " eta=0.1,\n", - " lmbd=0.0,\n", - " ):\n", - " \n", - " self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')\n", - " \n", - " self.X_train = X_train\n", - " self.Y_train = Y_train\n", - " self.X_test = X_test\n", - " self.Y_test = Y_test\n", - " \n", - " self.n_inputs, self.input_width, self.input_height, self.depth = X_train.shape\n", - " \n", - " self.n_filters = n_filters\n", - " self.n_downsampled = int(self.input_width*self.input_height*n_filters / 4)\n", - " self.n_neurons_connected = n_neurons_connected\n", - " self.n_categories = n_categories\n", - " \n", - " self.receptive_field = receptive_field\n", - " self.stride = stride\n", - " self.strides = [stride, stride, stride, stride]\n", - " self.padding = padding\n", - " \n", - " self.epochs = epochs\n", - " self.batch_size = batch_size\n", - " self.iterations = self.n_inputs // self.batch_size\n", - " self.eta = eta\n", - " self.lmbd = lmbd\n", - " \n", - " self.create_placeholders()\n", - " self.create_CNN()\n", - " self.create_loss()\n", - " self.create_optimiser()\n", - " self.create_accuracy()\n", - " \n", - " def create_placeholders(self):\n", - " with tf.name_scope('data'):\n", - " self.X = tf.placeholder(tf.float32, shape=(None, self.input_width, self.input_height, self.depth), name='X_data')\n", - " self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data')\n", - " \n", - " def create_CNN(self):\n", - " with tf.name_scope('CNN'):\n", - " \n", - " # Convolutional layer\n", - " self.W_conv = self.weight_variable([self.receptive_field, self.receptive_field, self.depth, self.n_filters], name='conv', dtype=tf.float32)\n", - " b_conv = self.weight_variable([self.n_filters], name='conv', dtype=tf.float32)\n", - " z_conv = tf.nn.conv2d(self.X, self.W_conv, self.strides, padding='SAME', name='conv') + b_conv\n", - " a_conv = tf.nn.relu(z_conv)\n", - " \n", - " # 2x2 max pooling\n", - " a_pool = tf.nn.max_pool(a_conv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME', name='pool')\n", - " \n", - " # Fully connected layer\n", - " a_pool_flat = tf.reshape(a_pool, [-1, self.n_downsampled])\n", - " self.W_fc = self.weight_variable([self.n_downsampled, self.n_neurons_connected], name='fc', dtype=tf.float32)\n", - " b_fc = self.bias_variable([self.n_neurons_connected], name='fc', dtype=tf.float32)\n", - " a_fc = tf.nn.relu(tf.matmul(a_pool_flat, self.W_fc) + b_fc)\n", - " \n", - " # Output layer\n", - " self.W_out = self.weight_variable([self.n_neurons_connected, self.n_categories], name='out', dtype=tf.float32)\n", - " b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32)\n", - " self.z_out = tf.matmul(a_fc, self.W_out) + b_out\n", - " \n", - " def create_loss(self):\n", - " with tf.name_scope('loss'):\n", - " softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out))\n", - " \n", - " regularizer_loss_conv = tf.nn.l2_loss(self.W_conv)\n", - " regularizer_loss_fc = tf.nn.l2_loss(self.W_fc)\n", - " regularizer_loss_out = tf.nn.l2_loss(self.W_out)\n", - " regularizer_loss = self.lmbd*(regularizer_loss_conv + regularizer_loss_fc + regularizer_loss_out)\n", - " \n", - " self.loss = softmax_loss + regularizer_loss\n", - "\n", - " def create_accuracy(self):\n", - " with tf.name_scope('accuracy'):\n", - " probabilities = tf.nn.softmax(self.z_out)\n", - " predictions = tf.argmax(probabilities, 1)\n", - " labels = tf.argmax(self.Y, 1)\n", - " \n", - " correct_predictions = tf.equal(predictions, labels)\n", - " correct_predictions = tf.cast(correct_predictions, tf.float32)\n", - " self.accuracy = tf.reduce_mean(correct_predictions)\n", - " \n", - " def create_optimiser(self):\n", - " with tf.name_scope('optimizer'):\n", - " self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step)\n", - " \n", - " def weight_variable(self, shape, name='', dtype=tf.float32):\n", - " initial = tf.truncated_normal(shape, stddev=0.1)\n", - " return tf.Variable(initial, name=name, dtype=dtype)\n", - " \n", - " def bias_variable(self, shape, name='', dtype=tf.float32):\n", - " initial = tf.constant(0.1, shape=shape)\n", - " return tf.Variable(initial, name=name, dtype=dtype)\n", - "\n", - " def fit(self):\n", - " data_indices = np.arange(self.n_inputs)\n", - "\n", - " with tf.Session() as sess:\n", - " sess.run(tf.global_variables_initializer())\n", - " for i in range(self.epochs):\n", - " for j in range(self.iterations):\n", - " chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False)\n", - " batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints]\n", - " \n", - " sess.run([CNN.loss, CNN.optimizer],\n", - " feed_dict={CNN.X: batch_X,\n", - " CNN.Y: batch_Y})\n", - " accuracy = sess.run(CNN.accuracy,\n", - " feed_dict={CNN.X: batch_X,\n", - " CNN.Y: batch_Y})\n", - " step = sess.run(CNN.global_step)\n", - " \n", - " self.train_loss, self.train_accuracy = sess.run([CNN.loss, CNN.accuracy],\n", - " feed_dict={CNN.X: self.X_train,\n", - " CNN.Y: self.Y_train})\n", - " \n", - " self.test_loss, self.test_accuracy = sess.run([CNN.loss, CNN.accuracy],\n", - " feed_dict={CNN.X: self.X_test,\n", - " CNN.Y: self.Y_test})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Train the model\n", - "\n", - "We need now to train the model, evaluate it and test its performance on test data, and eventually include hyperparameters." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "epochs = 100\n", - "batch_size = 100\n", - "n_filters = 10\n", - "n_neurons_connected = 50\n", - "n_categories = 10\n", - "\n", - "eta_vals = np.logspace(-5, 1, 7)\n", - "lmbd_vals = np.logspace(-5, 1, 7)\n", - "CNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)\n", - " \n", - "for i, eta in enumerate(eta_vals):\n", - " for j, lmbd in enumerate(lmbd_vals):\n", - " CNN = ConvolutionalNeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test,\n", - " n_filters=n_filters, n_neurons_connected=n_neurons_connected,\n", - " n_categories=n_categories, epochs=epochs, batch_size=batch_size,\n", - " eta=eta, lmbd=lmbd)\n", - " CNN.fit()\n", - " \n", - " print(\"Learning rate = \", eta)\n", - " print(\"Lambda = \", lmbd)\n", - " print(\"Test accuracy: %.3f\" % CNN.test_accuracy)\n", - " print()\n", - " \n", - " CNN_tf[i][j] = CNN" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualizing the results" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# visual representation of grid search\n", - "# uses seaborn heatmap, could probably do this in matplotlib\n", - "import seaborn as sns\n", - "\n", - "sns.set()\n", - "\n", - "train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - "\n", - "for i in range(len(eta_vals)):\n", - " for j in range(len(lmbd_vals)):\n", - " CNN = CNN_tf[i][j]\n", - "\n", - " train_accuracy[i][j] = CNN.train_accuracy\n", - " test_accuracy[i][j] = CNN.test_accuracy\n", - "\n", - " \n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(train_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Training Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()\n", - "\n", - "fig, ax = plt.subplots(figsize = (10, 10))\n", - "sns.heatmap(test_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - "ax.set_title(\"Test Accuracy\")\n", - "ax.set_ylabel(\"$\\eta$\")\n", - "ax.set_xlabel(\"$\\lambda$\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Running with Keras" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "from keras.models import Sequential\n", - "from keras.layers.convolutional import Conv2D\n", - "from keras.layers.convolutional import MaxPooling2D\n", - "from keras.layers import Flatten\n", - "from keras.layers import Dense\n", - "from keras.regularizers import l2\n", - "from keras.optimizers import SGD\n", - "\n", - "def create_convolutional_neural_network_keras(input_shape, receptive_field,\n", - " n_filters, n_neurons_connected, n_categories,\n", - " eta, lmbd):\n", - " model = Sequential()\n", - " model.add(Conv2D(n_filters, (receptive_field, receptive_field), input_shape=input_shape, padding='same',\n", - " activation='relu', kernel_regularizer=l2(lmbd)))\n", - " model.add(MaxPooling2D(pool_size=(2, 2)))\n", - " model.add(Flatten())\n", - " model.add(Dense(n_neurons_connected, activation='relu', kernel_regularizer=l2(lmbd)))\n", - " model.add(Dense(n_categories, activation='softmax', kernel_regularizer=l2(lmbd)))\n", - " \n", - " sgd = SGD(lr=eta)\n", - " model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])\n", - " \n", - " return model\n", - "\n", - "epochs = 100\n", - "batch_size = 100\n", - "input_shape = X_train.shape[1:4]\n", - "receptive_field = 3\n", - "n_filters = 10\n", - "n_neurons_connected = 50\n", - "n_categories = 10\n", - "\n", - "eta_vals = np.logspace(-5, 1, 7)\n", - "lmbd_vals = np.logspace(-5, 1, 7)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Final part" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "CNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object)\n", - " \n", - "for i, eta in enumerate(eta_vals):\n", - " for j, lmbd in enumerate(lmbd_vals):\n", - " CNN = create_convolutional_neural_network_keras(input_shape, receptive_field,\n", - " n_filters, n_neurons_connected, n_categories,\n", - " eta, lmbd)\n", - " CNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0)\n", - " scores = CNN.evaluate(X_test, Y_test)\n", - " \n", - " CNN_keras[i][j] = CNN\n", - " \n", - " print(\"Learning rate = \", eta)\n", - " print(\"Lambda = \", lmbd)\n", - " print(\"Test accuracy: %.3f\" % scores[1])\n", - " print()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Final visualization" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " # visual representation of grid search\n", - " # uses seaborn heatmap, could probably do this in matplotlib\n", - " import seaborn as sns\n", - " \n", - " sns.set()\n", - " \n", - " train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - " test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals)))\n", - " \n", - " for i in range(len(eta_vals)):\n", - " for j in range(len(lmbd_vals)):\n", - " CNN = CNN_keras[i][j]\n", - " \n", - " train_accuracy[i][j] = CNN.evaluate(X_train, Y_train)[1]\n", - " test_accuracy[i][j] = CNN.evaluate(X_test, Y_test)[1]\n", - " \n", - " \n", - " fig, ax = plt.subplots(figsize = (10, 10))\n", - " sns.heatmap(train_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - " ax.set_title(\"Training Accuracy\")\n", - " ax.set_ylabel(\"$\\eta$\")\n", - " ax.set_xlabel(\"$\\lambda$\")\n", - " plt.show()\n", - " \n", - " fig, ax = plt.subplots(figsize = (10, 10))\n", - " sns.heatmap(test_accuracy, annot=True, ax=ax, cmap=\"viridis\")\n", - " ax.set_title(\"Test Accuracy\")\n", - " ax.set_ylabel(\"$\\eta$\")\n", - " ax.set_xlabel(\"$\\lambda$\")\n", - " plt.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fun links\n", - "\n", - "1. [Self-Driving cars using a convolutional neural network](https://arxiv.org/abs/1604.07316)\n", - "\n", - "2. [Abstract art using convolutional neural networks](https://deepdreamgenerator.com/)" - ] - } - ], - "metadata": {}, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/doc/src/NeuralNet/NeuralNet.p.tex b/doc/src/NeuralNet/NeuralNet.p.tex deleted file mode 100644 index 13e53ced0..000000000 --- a/doc/src/NeuralNet/NeuralNet.p.tex +++ /dev/null @@ -1,3220 +0,0 @@ -%% -%% Automatically generated file from DocOnce source -%% (https://github.com/hplgit/doconce/) -%% -%% -% #ifdef PTEX2TEX_EXPLANATION -%% -%% The file follows the ptex2tex extended LaTeX format, see -%% ptex2tex: http://code.google.com/p/ptex2tex/ -%% -%% Run -%% ptex2tex myfile -%% or -%% doconce ptex2tex myfile -%% -%% to turn myfile.p.tex into an ordinary LaTeX file myfile.tex. -%% (The ptex2tex program: http://code.google.com/p/ptex2tex) -%% Many preprocess options can be added to ptex2tex or doconce ptex2tex -%% -%% ptex2tex -DMINTED myfile -%% doconce ptex2tex myfile envir=minted -%% -%% ptex2tex will typeset code environments according to a global or local -%% .ptex2tex.cfg configure file. doconce ptex2tex will typeset code -%% according to options on the command line (just type doconce ptex2tex to -%% see examples). If doconce ptex2tex has envir=minted, it enables the -%% minted style without needing -DMINTED. -% #endif - -% #define PREAMBLE - -% #ifdef PREAMBLE -%-------------------- begin preamble ---------------------- - -\documentclass[% -oneside, % oneside: electronic viewing, twoside: printing -final, % draft: marks overfull hboxes, figures with paths -10pt]{article} - -\listfiles % print all files needed to compile this document - -\usepackage{relsize,makeidx,color,setspace,amsmath,amsfonts,amssymb} -\usepackage[table]{xcolor} -\usepackage{bm,ltablex,microtype} - -\usepackage[pdftex]{graphicx} - -\usepackage{ptex2tex} -% #ifdef MINTED -\usepackage{minted} -\usemintedstyle{default} -% #endif - -\usepackage[T1]{fontenc} -%\usepackage[latin1]{inputenc} -\usepackage{ucs} -\usepackage[utf8x]{inputenc} - -\usepackage{lmodern} % Latin Modern fonts derived from Computer Modern - -% Hyperlinks in PDF: -\definecolor{linkcolor}{rgb}{0,0,0.4} -\usepackage{hyperref} -\hypersetup{ - breaklinks=true, - colorlinks=true, - linkcolor=linkcolor, - urlcolor=linkcolor, - citecolor=black, - filecolor=black, - %filecolor=blue, - pdfmenubar=true, - pdftoolbar=true, - bookmarksdepth=3 % Uncomment (and tweak) for PDF bookmarks with more levels than the TOC - } -%\hyperbaseurl{} % hyperlinks are relative to this root - -\setcounter{tocdepth}{2} % levels in table of contents - -% Tricks for having figures close to where they are defined: -% 1. define less restrictive rules for where to put figures -\setcounter{topnumber}{2} -\setcounter{bottomnumber}{2} -\setcounter{totalnumber}{4} -\renewcommand{\topfraction}{0.95} -\renewcommand{\bottomfraction}{0.95} -\renewcommand{\textfraction}{0} -\renewcommand{\floatpagefraction}{0.75} -% floatpagefraction must always be less than topfraction! -% 2. ensure all figures are flushed before next section -\usepackage[section]{placeins} -% 3. enable begin{figure}[H] (often leads to ugly pagebreaks) -%\usepackage{float}\restylefloat{figure} - -% --- fancyhdr package for fancy headers --- -\usepackage{fancyhdr} -\fancyhf{} % sets both header and footer to nothing -\renewcommand{\headrulewidth}{0pt} -\fancyfoot[LE,RO]{\thepage} -% Ensure copyright on titlepage (article style) and chapter pages (book style) -\fancypagestyle{plain}{ - \fancyhf{} - \fancyfoot[C]{{\footnotesize \copyright\ 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license}} -% \renewcommand{\footrulewidth}{0mm} - \renewcommand{\headrulewidth}{0mm} -} -% Ensure copyright on titlepages with \thispagestyle{empty} -\fancypagestyle{empty}{ - \fancyhf{} - \fancyfoot[C]{{\footnotesize \copyright\ 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license}} - \renewcommand{\footrulewidth}{0mm} - \renewcommand{\headrulewidth}{0mm} -} - -\pagestyle{fancy} - - -\usepackage[framemethod=TikZ]{mdframed} - -% --- begin definitions of admonition environments --- - -% --- end of definitions of admonition environments --- - -% prevent orhpans and widows -\clubpenalty = 10000 -\widowpenalty = 10000 - -% --- end of standard preamble for documents --- - - -% insert custom LaTeX commands... - -\raggedbottom -\makeindex -\usepackage[totoc]{idxlayout} % for index in the toc -\usepackage[nottoc]{tocbibind} % for references/bibliography in the toc - -%-------------------- end preamble ---------------------- - -\begin{document} - -% matching end for #ifdef PREAMBLE -% #endif - -\newcommand{\exercisesection}[1]{\subsection*{#1}} - - -% ------------------- main content ---------------------- - - - -% ----------------- title ------------------------- - -\thispagestyle{empty} - -\begin{center} -{\LARGE\bf -\begin{spacing}{1.25} -Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks -\end{spacing} -} -\end{center} - -% ----------------- author(s) ------------------------- - -\begin{center} -{\bf Morten Hjorth-Jensen${}^{1, 2}$} \\ [0mm] -\end{center} - -\begin{center} -% List of all institutions: -\centerline{{\small ${}^1$Department of Physics, University of Oslo}} -\centerline{{\small ${}^2$Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University}} -\end{center} - -% ----------------- end author(s) ------------------------- - -% --- begin date --- -\begin{center} -Oct 19, 2018 -\end{center} -% --- end date --- - -\vspace{1cm} - - -% !split -\subsection{Neural networks} - -Artificial neural networks are computational systems that can learn to -perform tasks by considering examples, generally without being -programmed with any task-specific rules. It is supposed to mimic a -biological system, wherein neurons interact by sending signals in the -form of mathematical functions between layers. All layers can contain -an arbitrary number of neurons, and each connection is represented by -a weight variable. - - -% !split -\subsection{Artificial neurons} - -The field of artificial neural networks has a long history of -development, and is closely connected with the advancement of computer -science and computers in general. A model of artificial neurons was -first developed by McCulloch and Pitts in 1943 to study signal -processing in the brain and has later been refined by others. The -general idea is to mimic neural networks in the human brain, which is -composed of billions of neurons that communicate with each other by -sending electrical signals. Each neuron accumulates its incoming -signals, which must exceed an activation threshold to yield an -output. If the threshold is not overcome, the neuron remains inactive, -i.e.~has zero output. - -This behaviour has inspired a simple mathematical model for an artificial neuron. - -\begin{equation} - y = f\left(\sum_{i=1}^n w_ix_i\right) = f(u) - \label{artificialNeuron} -\end{equation} -Here, the output $y$ of the neuron is the value of its activation function, which have as input -a weighted sum of signals $x_i, \dots ,x_n$ received by $n$ other neurons. - -Conceptually, it is helpful to divide neural networks into four -categories: -\begin{enumerate} -\item general purpose neural networks for supervised learning, - -\item neural networks designed specifically for image processing, the most prominent example of this class being Convolutional Neural Networks (CNNs), - -\item neural networks for sequential data such as Recurrent Neural Networks (RNNs), and - -\item neural networks for unsupervised learning such as Deep Boltzmann Machines. -\end{enumerate} - -\noindent -In natural science, DNNs and CNNs have already found numerous -applications. In statistical physics, they have been applied to detect -phase transitions in 2D Ising and Potts models, lattice gauge -theories, and different phases of polymers, or solving the -Navier-Stokes equation in weather forecasting. Deep learning has also -found interesting applications in quantum physics. Various quantum -phase transitions can be detected and studied using DNNs and CNNs, -topological phases, and even non-equilibrium many-body -localization. Representing quantum states as DNNs quantum state -tomography are among some of the impressive achievements to reveal the -potential of DNNs to facilitate the study of quantum systems. - -In quantum information theory, it has been shown that one can perform -gate decompositions with the help of neural. - -The applications are not limited to the natural sciences. There is a -plethora of applications in essentially all disciplines, from the -humanities to life science and medicine. - -% !split -\subsection{Neural network types} - -An artificial neural network (ANN), is a computational model that -consists of layers of connected neurons, or nodes or units. We will -refer to these interchangeably as units or nodes, and sometimes as -neurons. - -It is supposed to mimic a biological nervous system by letting each -neuron interact with other neurons by sending signals in the form of -mathematical functions between layers. A wide variety of different -ANNs have been developed, but most of them consist of an input layer, -an output layer and eventual layers in-between, called \emph{hidden -layers}. All layers can contain an arbitrary number of nodes, and each -connection between two nodes is associated with a weight variable. - -Neural networks (also called neural nets) are neural-inspired -nonlinear models for supervised learning. As we will see, neural nets -can be viewed as natural, more powerful extensions of supervised -learning methods such as linear and logistic regression and soft-max -methods we discussed earlier. - - -% !split -\subsection{Feed-forward neural networks} - -The feed-forward neural network (FFNN) was the first and simplest type -of ANNs that were devised. In this network, the information moves in -only one direction: forward through the layers. - -Nodes are represented by circles, while the arrows display the -connections between the nodes, including the direction of information -flow. Additionally, each arrow corresponds to a weight variable -(figure to come). We observe that each node in a layer is connected -to \emph{all} nodes in the subsequent layer, making this a so-called -\emph{fully-connected} FFNN. - - - -% !split -\subsection{Convolutional Neural Network} - -A different variant of FFNNs are \emph{convolutional neural networks} -(CNNs), which have a connectivity pattern inspired by the animal -visual cortex. Individual neurons in the visual cortex only respond to -stimuli from small sub-regions of the visual field, called a receptive -field. This makes the neurons well-suited to exploit the strong -spatially local correlation present in natural images. The response of -each neuron can be approximated mathematically as a convolution -operation. (figure to come) - -Convolutional neural networks emulate the behaviour of neurons in the -visual cortex by enforcing a \emph{local} connectivity pattern between -nodes of adjacent layers: Each node in a convolutional layer is -connected only to a subset of the nodes in the previous layer, in -contrast to the fully-connected FFNN. Often, CNNs consist of several -convolutional layers that learn local features of the input, with a -fully-connected layer at the end, which gathers all the local data and -produces the outputs. They have wide applications in image and video -recognition. - -% !split -\subsection{Recurrent neural networks} - -So far we have only mentioned ANNs where information flows in one -direction: forward. \emph{Recurrent neural networks} on the other hand, -have connections between nodes that form directed \emph{cycles}. This -creates a form of internal memory which are able to capture -information on what has been calculated before; the output is -dependent on the previous computations. Recurrent NNs make use of -sequential information by performing the same task for every element -in a sequence, where each element depends on previous elements. An -example of such information is sentences, making recurrent NNs -especially well-suited for handwriting and speech recognition. - -% !split -\subsection{Other types of networks} - -There are many other kinds of ANNs that have been developed. One type -that is specifically designed for interpolation in multidimensional -space is the radial basis function (RBF) network. RBFs are typically -made up of three layers: an input layer, a hidden layer with -non-linear radial symmetric activation functions and a linear output -layer (''linear'' here means that each node in the output layer has a -linear activation function). The layers are normally fully-connected -and there are no cycles, thus RBFs can be viewed as a type of -fully-connected FFNN. They are however usually treated as a separate -type of NN due the unusual activation functions. - -% !split -\subsection{Multilayer perceptrons} - -One uses often so-called fully-connected feed-forward neural networks -with three or more layers (an input layer, one or more hidden layers -and an output layer) consisting of neurons that have non-linear -activation functions. - -Such networks are often called \emph{multilayer perceptrons} (MLPs). - -% !split -\subsection{Why multilayer perceptrons?} - -According to the \emph{Universal approximation theorem}, a feed-forward -neural network with just a single hidden layer containing a finite -number of neurons can approximate a continuous multidimensional -function to arbitrary accuracy, assuming the activation function for -the hidden layer is a \textbf{non-constant, bounded and -monotonically-increasing continuous function}. - -Note that the requirements on the activation function only applies to -the hidden layer, the output nodes are always assumed to be linear, so -as to not restrict the range of output values. - - -% !split -\subsection{Mathematical model} - -The output $y$ is produced via the activation function $f$ -\[ - y = f\left(\sum_{i=1}^n w_ix_i + b_i\right) = f(z), -\] -This function receives $x_i$ as inputs. -Here the activation $z=(\sum_{i=1}^n w_ix_i+b_i)$. -In an FFNN of such neurons, the \emph{inputs} $x_i$ are the \emph{outputs} of -the neurons in the preceding layer. Furthermore, an MLP is -fully-connected, which means that each neuron receives a weighted sum -of the outputs of \emph{all} neurons in the previous layer. - -% !split -\subsection{Mathematical model} - -First, for each node $i$ in the first hidden layer, we calculate a weighted sum $z_i^1$ of the input coordinates $x_j$, - -\begin{equation} z_i^1 = \sum_{j=1}^{M} w_{ij}^1 x_j + b_i^1 -\end{equation} - -Here $b_i$ is the so-called bias which is normally needed in -case of zero activation weights or inputs. How to fix the biases and -the weights will be discussed below. The value of $z_i^1$ is the -argument to the activation function $f_i$ of each node $i$, The -variable $M$ stands for all possible inputs to a given node $i$ in the -first layer. We define the output $y_i^1$ of all neurons in layer 1 as - -\begin{equation} - y_i^1 = f(z_i^1) = f\left(\sum_{j=1}^M w_{ij}^1 x_j + b_i^1\right) - \label{outputLayer1} -\end{equation} - -where we assume that all nodes in the same layer have identical -activation functions, hence the notation $f$. In general, we could assume in the more general case that different layers have different activation functions. -In this case we would identify these functions with a superscript $l$ for the $l$-th layer, - -\begin{equation} - y_i^l = f^l(u_i^l) = f^l\left(\sum_{j=1}^{N_{l-1}} w_{ij}^l y_j^{l-1} + b_i^l\right) - \label{generalLayer} -\end{equation} - -where $N_l$ is the number of nodes in layer $l$. When the output of -all the nodes in the first hidden layer are computed, the values of -the subsequent layer can be calculated and so forth until the output -is obtained. - - - -% !split -\subsection{Mathematical model} - -The output of neuron $i$ in layer 2 is thus, - -\begin{align} - y_i^2 &= f^2\left(\sum_{j=1}^N w_{ij}^2 y_j^1 + b_i^2\right) \\ - &= f^2\left[\sum_{j=1}^N w_{ij}^2f^1\left(\sum_{k=1}^M w_{jk}^1 x_k + b_j^1\right) + b_i^2\right] - \label{outputLayer2} -\end{align} -where we have substituted $y_k^1$ with the inputs $x_k$. Finally, the ANN output reads - -\begin{align} - y_i^3 &= f^3\left(\sum_{j=1}^N w_{ij}^3 y_j^2 + b_i^3\right) \\ - &= f_3\left[\sum_{j} w_{ij}^3 f^2\left(\sum_{k} w_{jk}^2 f^1\left(\sum_{m} w_{km}^1 x_m + b_k^1\right) + b_j^2\right) - + b_1^3\right] -\end{align} - -% !split -\subsection{Mathematical model} - -We can generalize this expression to an MLP with $l$ hidden -layers. The complete functional form is, - -\begin{align} -&y^{l+1}_i = f^{l+1}\left[\!\sum_{j=1}^{N_l} w_{ij}^3 f^l\left(\sum_{k=1}^{N_{l-1}}w_{jk}^{l-1}\left(\dots f^1\left(\sum_{n=1}^{N_0} w_{mn}^1 x_n+ b_m^1\right)\dots\right)+b_k^2\right)+b_1^3\right] && - \label{completeNN} -\end{align} - -which illustrates a basic property of MLPs: The only independent -variables are the input values $x_n$. - -% !split -\subsection{Mathematical model} - -This confirms that an MLP, despite its quite convoluted mathematical -form, is nothing more than an analytic function, specifically a -mapping of real-valued vectors $\hat{x} \in \mathbb{R}^n \rightarrow -\hat{y} \in \mathbb{R}^m$. - -Furthermore, the flexibility and universality of an MLP can be -illustrated by realizing that the expression is essentially a nested -sum of scaled activation functions of the form - -\begin{equation} - f(x) = c_1 f(c_2 x + c_3) + c_4 -\end{equation} - -where the parameters $c_i$ are weights and biases. By adjusting these -parameters, the activation functions can be shifted up and down or -left and right, change slope or be rescaled which is the key to the -flexibility of a neural network. - -% !split -\paragraph{Matrix-vector notation.} -We can introduce a more convenient notation for the activations in an A NN. - -Additionally, we can represent the biases and activations -as layer-wise column vectors $\hat{b}_l$ and $\hat{y}_l$, so that the $i$-th element of each vector -is the bias $b_i^l$ and activation $y_i^l$ of node $i$ in layer $l$ respectively. - -We have that $\mathrm{W}_l$ is an $N_{l-1} \times N_l$ matrix, while $\hat{b}_l$ and $\hat{y}_l$ are $N_l \times 1$ column vectors. -With this notation, the sum becomes a matrix-vector multiplication, and we can write -the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as -\begin{equation} - \hat{y}_2 = f_2(\mathrm{W}_2 \hat{y}_{1} + \hat{b}_{2}) = - f_2\left(\left[\begin{array}{ccc} - w^2_{11} &w^2_{12} &w^2_{13} \\ - w^2_{21} &w^2_{22} &w^2_{23} \\ - w^2_{31} &w^2_{32} &w^2_{33} \\ - \end{array} \right] \cdot - \left[\begin{array}{c} - y^1_1 \\ - y^1_2 \\ - y^1_3 \\ - \end{array}\right] + - \left[\begin{array}{c} - b^2_1 \\ - b^2_2 \\ - b^2_3 \\ - \end{array}\right]\right). -\end{equation} - -% !split -\paragraph{Matrix-vector notation and activation.} -The activation of node $i$ in layer 2 is - -\begin{equation} - y^2_i = f_2\Bigr(w^2_{i1}y^1_1 + w^2_{i2}y^1_2 + w^2_{i3}y^1_3 + b^2_i\Bigr) = - f_2\left(\sum_{j=1}^3 w^2_{ij} y_j^1 + b^2_i\right). -\end{equation} - -This is not just a convenient and compact notation, but also a useful -and intuitive way to think about MLPs: The output is calculated by a -series of matrix-vector multiplications and vector additions that are -used as input to the activation functions. For each operation -$\mathrm{W}_l \hat{y}_{l-1}$ we move forward one layer. - - -% !split -\paragraph{Activation functions.} -A property that characterizes a neural network, other than its -connectivity, is the choice of activation function(s). As described -in, the following restrictions are imposed on an activation function -for a FFNN to fulfill the universal approximation theorem - -\begin{itemize} - \item Non-constant - - \item Bounded - - \item Monotonically-increasing - - \item Continuous -\end{itemize} - -\noindent -% !split -\paragraph{Activation functions, Logistic and Hyperbolic ones.} -The second requirement excludes all linear functions. Furthermore, in -a MLP with only linear activation functions, each layer simply -performs a linear transformation of its inputs. - -Regardless of the number of layers, the output of the NN will be -nothing but a linear function of the inputs. Thus we need to introduce -some kind of non-linearity to the NN to be able to fit non-linear -functions Typical examples are the logistic \emph{Sigmoid} - -\[ - f(x) = \frac{1}{1 + e^{-x}}, -\] -and the \emph{hyperbolic tangent} function -\[ - f(x) = \tanh(x) -\] - -% !split -\paragraph{Relevance.} -The \emph{sigmoid} function are more biologically plausible because the -output of inactive neurons are zero. Such activation function are -called \emph{one-sided}. However, it has been shown that the hyperbolic -tangent performs better than the sigmoid for training MLPs. has -become the most popular for \emph{deep neural networks} - -\bpycod -"""The sigmoid function (or the logistic curve) is a -function that takes any real number, z, and outputs a number (0,1). -It is useful in neural networks for assigning weights on a relative scale. -The value z is the weighted sum of parameters involved in the learning algorithm.""" - -import numpy -import matplotlib.pyplot as plt -import math as mt - -z = numpy.arange(-5, 5, .1) -sigma_fn = numpy.vectorize(lambda z: 1/(1+numpy.exp(-z))) -sigma = sigma_fn(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, sigma) -ax.set_ylim([-0.1, 1.1]) -ax.set_xlim([-5,5]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('sigmoid function') - -plt.show() - -"""Step Function""" -z = numpy.arange(-5, 5, .02) -step_fn = numpy.vectorize(lambda z: 1.0 if z >= 0.0 else 0.0) -step = step_fn(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, step) -ax.set_ylim([-0.5, 1.5]) -ax.set_xlim([-5,5]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('step function') - -plt.show() - -"""Sine Function""" -z = numpy.arange(-2*mt.pi, 2*mt.pi, 0.1) -t = numpy.sin(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, t) -ax.set_ylim([-1.0, 1.0]) -ax.set_xlim([-2*mt.pi,2*mt.pi]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('sine function') - -plt.show() - -"""Plots a graph of the squashing function used by a rectified linear -unit""" -z = numpy.arange(-2, 2, .1) -zero = numpy.zeros(len(z)) -y = numpy.max([zero, z], axis=0) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, y) -ax.set_ylim([-2.0, 2.0]) -ax.set_xlim([-2.0, 2.0]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('Rectified linear unit') - -plt.show() -\epycod - - -% !split -\subsection{The multilayer perceptron (MLP)} - -The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of -\begin{enumerate} -\item A neural network with one or more layers of nodes between the input and the output nodes. - -\item The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer. - -\item The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer. -\end{enumerate} - -\noindent -As a convention it is normal to call a network with one layer of input units, one layer of hidden -units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc. - -For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units. -Hereafter we will call the various entities of a layer for nodes. -There are also no connections within a single layer. - -The number of input nodes does not need to equal the number of output -nodes. This applies also to the hidden layers. Each layer may have its -own number of nodes and activation functions. - -The hidden layers have their name from the fact that they are not -linked to observables and as we will see below when we define the -so-called activation $\hat{z}$, we can think of this as a basis -expansion of the original inputs $\hat{x}$. The difference however -between neural networks and say linear regression is that now these -basis functions (which will correspond to the weights in the network) -are learned from data. This results in an important difference between -neural networks and deep learning approaches on one side and methods -like logistic regression or linear regression and their modifications on the other side. - - -% !split -\subsection{From one to many layers, the universal approximation theorem} - - -A neural network with only one layer, what we called the simple -perceptron, is best suited if we have a standard binary model with -clear (linear) boundaries between the outcomes. As such it could -equally well be replaced by standard linear regression or logistic -regression. Networks with one or more hidden layers approximate -systems with more complex boundaries. - -As stated earlier, -an important theorem in studies of neural networks, restated without -proof here, is the \href{{http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.441.7873&rep=rep1&type=pdf}}{universal approximation -theorem}. - -It states that a feed-forward network with a single hidden layer -containing a finite number of neurons can approximate continuous -functions on compact subsets of real functions. The theorem thus -states that simple neural networks can represent a wide variety of -interesting functions when given appropriate parameters. It is the -multilayer feedforward architecture itself which gives neural networks -the potential of being universal approximators. - - -% !split -\subsection{Deriving the back propagation code for a multilayer perceptron model} - - -\textbf{Note: figures will be inserted later!} - -As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications. -The unknowwn quantities are our weights $w_{ij}$ and we need to find an algorithm for changing them so that our errors are as small as possible. -This leads us to the famous \href{{https://www.nature.com/articles/323533a0}}{back propagation algorithm}. - -The questions we want to ask are how do changes in the biases and the -weights in our network change the cost function and how can we use the -final output to modify the weights? - -To derive these equations let us start with a plain regression problem -and define our cost function as - -\[ -{\cal C}(\hat{W}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2, -\] - -where the $t_i$s are our $n$ targets (the values we want to -reproduce), while the outputs of the network after having propagated -all inputs $\hat{x}$ are given by $y_i$. Below we will demonstrate -how the basic equations arising from the back propagation algorithm -can be modified in order to study classification problems with $K$ -classes. - -% !split -\subsection{Definitions} - -With our definition of the targets $\hat{t}$, the outputs of the -network $\hat{y}$ and the inputs $\hat{x}$ we -define now the activation $z_j^l$ of node/neuron/unit $j$ of the -$l$-th layer as a function of the bias, the weights which add up from -the previous layer $l-1$ and the forward passes/outputs -$\hat{a}^{l-1}$ from the previous layer as - - -\[ -z_j^l = \sum_{i=1}^{M_{l-1}}w_{ij}^la_i^{l-1}+b_j^l, -\] - -where $b_k^l$ are the biases from layer $l$. Here $M_{l-1}$ -represents the total number of nodes/neurons/units of layer $l-1$. The -figure here illustrates this equation. We can rewrite this in a more -compact form as the matrix-vector products we discussed earlier, - -\[ -\hat{z}^l = \left(\hat{W}^l\right)^T\hat{a}^{l-1}+\hat{b}^l. -\] - -With the activation values $\hat{z}^l$ we can in turn define the -output of layer $l$ as $\hat{a}^l = f(\hat{z}^l)$ where $f$ is our -activation function. In the examples here we will use the sigmoid -function discussed in our logistic regression lectures. We will also use the same activation function $f$ for all layers -and their nodes. It means we have - -\[ -a_j^l = f(z_j^l) = \frac{1}{1+\exp{-(z_j^l)}}. -\] - - -% !split -\subsection{Derivatives and the chain rule} - -From the definition of the activation $z_j^l$ we have -\[ -\frac{\partial z_j^l}{\partial w_{ij}^l} = a_i^{l-1}, -\] -and -\[ -\frac{\partial z_j^l}{\partial a_i^{l-1}} = w_{ji}^l. -\] - -With our definition of the activation function we have that (note that this function depends only on $z_j^l$) -\[ -\frac{\partial a_j^l}{\partial z_j^{l}} = a_j^l(1-a_j^l)=f(z_j^l)(1-f(z_j^l)). -\] - - -% !split -\subsection{Derivative of the cost function} - -With these definitions we can now compute the derivative of the cost function in terms of the weights. - -Let us specialize to the output layer $l=L$. Our cost function is -\[ -{\cal C}(\hat{W^L}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2=\frac{1}{2}\sum_{i=1}^n\left(a_i^L - t_i\right)^2, -\] -The derivative of this function with respect to the weights is - -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)\frac{\partial a_j^L}{\partial w_{jk}^{L}}, -\] -The last partial derivative can easily be computed and reads (by applying the chain rule) -\[ -\frac{\partial a_j^L}{\partial w_{jk}^{L}} = \frac{\partial a_j^L}{\partial z_{j}^{L}}\frac{\partial z_j^L}{\partial w_{jk}^{L}}=a_j^L(1-a_j^L)a_k^{L-1}, -\] - - - -% !split -\subsection{Bringing it together, first back propagation equation} - -We have thus -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)a_j^L(1-a_j^L)a_k^{L-1}, -\] - -Defining -\[ -\delta_j^L = a_j^L(1-a_j^L)\left(a_j^L - t_j\right) = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\] -and using the Hadamard product of two vectors we can write this as -\[ -\hat{\delta}^L = f'(\hat{z}^L)\circ\frac{\partial {\cal C}}{\partial (\hat{a}L)}. -\] - -This is an important expression. The second term on the right handside -measures how fast the cost function is changing as a function of the $j$th -output activation. If, for example, the cost function doesn't depend -much on a particular output node $j$, then $\delta_j^L$ will be small, -which is what we would expect. The first term on the right, measures -how fast the activation function $f$ is changing at a given activation -value $z_j^L$. - -Notice that everything in the above equations is easily computed. In -particular, we compute $z_j^L$ while computing the behaviour of the -network, and it is only a small additional overhead to compute -$f'(z^L_j)$. The exact form of the derivative with respect to the -output depends on the form of the cost function. -However, provided the cost function is known there should be little -trouble in calculating - -\[ -\frac{\partial {\cal C}}{\partial (a_j^L)} -\] - -With the definition of $\delta_j^L$ we have a more compact definition of the derivative of the cost function in terms of the weights, namely -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}. -\] - -% !split -\subsection{Derivatives in terms of $z_j^L$} - -It is also easy to see that our previous equation can be written as - -\[ -\delta_j^L =\frac{\partial {\cal C}}{\partial z_j^L}= \frac{\partial {\cal C}}{\partial a_j^L}\frac{\partial a_j^L}{\partial z_j^L}, -\] -which can also be interpreted as the partial derivative of the cost function with respect to the biases $b_j^L$, namely -\[ -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}\frac{\partial b_j^L}{\partial z_j^L}=\frac{\partial {\cal C}}{\partial b_j^L}, -\] -That is, the error $\delta_j^L$ is exactly equal to the rate of change of the cost function as a function of the bias. -% !split -\subsection{Bringing it together} - -We have now three equations that are essential for the computations of the derivatives of the cost function at the output layer. These equations are needed to start the algorithm and they are - - -% --- begin paragraph admon --- -\paragraph{The starting equations.} - -\begin{equation} -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}, -\end{equation} -and -\begin{equation} -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\end{equation} -and - -\begin{equation} -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}, -\end{equation} -% --- end paragraph admon --- - - - - -An interesting consequence of the above equations is that when the -activation $a_k^{L-1}$ is small, the gradient term, that is the -derivative of the cost function with respect to the weights, will also -tend to be small. We say then that the weight learns slowly, meaning -that it changes slowly when we minimize the weights via say gradient -descent. In this case we say the system learns slowly. - -Another interesting feature is that is when the activation function, -represented by the sigmoid function here, is rather flat when we move towards -its end values $0$ and $1$ (see the above Python codes). In these -cases, the derivatives of the activation function will also be close -to zero, meaning again that the gradients will be small and the -network learns slowly again. - - - -We need a fourth equation and we are set. We are going to propagate -backwards in order to the determine the weights and biases. In order -to do so we need to represent the error in the layer before the final -one $L-1$ in terms of the errors in the final output layer. - -% !split -\subsection{Final back propagating equation} - -We have that (replacing $L$ with a general layer $l$) -\[ -\delta_j^l =\frac{\partial {\cal C}}{\partial z_j^l}. -\] -We want to express this in terms of the equations for layer $l+1$. Using the chain rule and summing over all $k$ entries we have - -\[ -\delta_j^l =\sum_k \frac{\partial {\cal C}}{\partial z_k^{l+1}}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}=\sum_k \delta_k^{l+1}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}, -\] -and recalling that -\[ -z_j^{l+1} = \sum_{i=1}^{M_{l}}w_{ij}^{l+1}a_j^{l}+b_j^{l+1}, -\] -with $M_l$ being the number of nodes in layer $l$, we obtain -\[ -\delta_j^l =\sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l), -\] -This is our final equation. - -We are now ready to set up the algorithm for back propagation and learning the weights and biases. - -% !split -\subsection{Setting up the Back propagation algorithm} - - - -The four equations provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm. - - -% --- begin paragraph admon --- -\paragraph{} -First, we set up the input data $\hat{x}$ and the activations -$\hat{z}_1$ of the input layer and compute the activation function and -the pertinent outputs $\hat{a}^1$. -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Secondly, we perform then the feed forward till we reach the output -layer and compute all $\hat{z}_l$ of the input layer and compute the -activation function and the pertinent outputs $\hat{a}^l$ for -$l=2,3,\dots,L$. -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Thereafter we compute the ouput error $\hat{\delta}^L$ by computing all -\[ -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}. -\] -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Then we compute the back propagate error for each $l=L-1,L-2,\dots,2$ as -\[ -\delta_j^l = \sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l). -\] -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Finally, we update the weights and the biases using gradient descent for each $l=L-1,L-2,\dots,2$ and update the weights and biases according to the rules -\[ -w_{jk}^l\leftarrow = w_{jk}^l- \eta \delta_j^la_k^{l-1}, -\] - -\[ -b_j^l \leftarrow b_j^l-\eta \frac{\partial {\cal C}}{\partial b_j^l}=b_j^l-\eta \delta_j^l, -\] -% --- end paragraph admon --- - - - -The parameter $\eta$ is the learning parameter discussed in connection with the gradient descent methods. -Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training. - - -% !split -\subsection{Setting up a Multi-layer perceptron model for classification} - -We are now gong to develop an example based on the MNIST data -base. This is a classification problem and we need to use our -cross-entropy function we discussed in connection with logistic -regression. The cross-entropy defines our cost function for the -classificaton problems with neural networks. - -In binary classification with two classes $(0, 1)$ we define the -logistic/sigmoid function as the probability that a particular input -is in class $0$ or $1$. This is possible because the logistic -function takes any input from the real numbers and inputs a number -between 0 and 1, and can therefore be interpreted as a probability. It -also has other nice properties, such as a derivative that is simple to -calculate. - -For an input $\boldsymbol{a}$ from the hidden layer, the probability that the input $\boldsymbol{x}$ -is in class 0 or 1 is just. We let $\theta$ represent the unknown weights and biases to be adjusted by our equations). The variable $x$ -represents our activation values $z$. We have -\[ -P(y = 0 \mid \hat{x}, \hat{\theta}) = \frac{1}{1 + \exp{(- \hat{x}})} , -\] -and -\[ -P(y = 1 \mid \hat{x}, \hat{\theta}) = 1 - P(y = 0 \mid \hat{x}, \hat{\theta}) , -\] - -where $y \in \{0, 1\}$ and $\hat{\theta}$ represents the weights and biases -of our network. - - -% !split -\subsection{Defining the cost function} - -Our cost function is given as (see the Logistic regression lectures) -\[ -\mathcal{C}(\hat{\theta}) = - \ln P(\mathcal{D} \mid \hat{\theta}) = - \sum_{i=1}^n -y_i \ln[P(y_i = 0)] + (1 - y_i) \ln [1 - P(y_i = 0)] = \sum_{i=1}^n \mathcal{L}_i(\hat{\theta}) . -\] - -This last equality means that we can interpret our \emph{cost} function as a sum over the \emph{loss} function -for each point in the dataset $\mathcal{L}_i(\hat{\theta})$. -The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather -than maximizing a negative number. - -In \emph{multiclass} classification it is common to treat each integer label as a so called \emph{one-hot} vector: - -$y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$ and - - -$y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$ - - -i.e.~a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset (numbers from $0$ to $9$).. - -If $\hat{x}_i$ is the $i$-th input (image), $y_{ic}$ refers to the $c$-th component of the $i$-th -output vector $\hat{y}_i$. -The probability of $\hat{x}_i$ being in class $c$ will be given by the softmax function: - -\[ -P(y_{ic} = 1 \mid \hat{x}_i, \hat{\theta}) = \frac{\exp{((\hat{a}_i^{hidden})^T \hat{w}_c)}} -{\sum_{c'=0}^{C-1} \exp{((\hat{a}_i^{hidden})^T \hat{w}_{c'})}} , -\] - -which reduces to the logistic function in the binary case. -The likelihood of this $C$-class classifier -is now given as: - -\[ -P(\mathcal{D} \mid \hat{\theta}) = \prod_{i=1}^n \prod_{c=0}^{C-1} [P(y_{ic} = 1)]^{y_{ic}} . -\] -Again we take the negative log-likelihood to define our cost function: - -\[ -\mathcal{C}(\hat{\theta}) = - \log{P(\mathcal{D} \mid \hat{\theta})}. -\] -See the logistic regression lectures for a full definition of the cost function. - -The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before! - -% !split -\subsection{Example: binary classification problem} - -As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters $\beta$ as -\[ -\mathcal{C}(\hat{\beta}) = - \sum_{i=1}^n \left(y_i\log{p(y_i \vert x_i,\hat{\beta})}+(i-y_i)\log{1-p(y_i \vert x_i,\hat{\beta})}\right), -\] -where we had defined the logistic (sigmoid) function -\[ -p(y_i =1\vert x_i,\hat{\beta})=\frac{\exp{(\beta_0+\beta_1 x_i)}}{1+\exp{(\beta_0+\beta_1 x_i)}}, -\] -and -\[ -p(y_i =0\vert x_i,\hat{\beta})=1-p(y_i =1\vert x_i,\hat{\beta}). -\] -The parameters $\hat{\beta}$ were defined using a minimization method like gradient descent or Newton-Raphson's method. - -Now we replace $x_i$ with the activation $z_i^l$ for a given layer $l$ and the outputs as $y_i=a_i^l=f(z_i^l)$, with $z_i^l$ now being a function of the weights $w_{ij}^l$ and biases $b_i^l$. -We have then -\[ -a_i^l = y_i = \frac{\exp{(z_i^l)}}{1+\exp{(z_i^l)}}, -\] -with -\[ -z_i^l = \sum_{j}w_{ij}^l a_j^{l-1}+b_i^l, -\] -where the superscript $l-1$ indicates that these are the outputs from layer $l-1$. -Our cost function at the final layer $l=L$ is now -\[ -\mathcal{C}(\hat{W}) = - \sum_{i=1}^n \left(t_i\log{a_i^L}+(i-t_i)\log{(1-a_i^L)}\right), -\] -where we have defined the targets $t_i$. The derivatives of the cost function with respect to the output $a_i^L$ are then easily calculated and we get -\[ -\frac{\partial \mathcal{C}(\hat{W})}{\partial a_i^L} = \frac{a_i^L-t_i}{a_i^L(1-a_i^L)}. -\] -In case we use another activation function than the logistic one, we need to evaluate other derivatives. - - -% !split -\subsection{The Softmax function} -In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation $z_i^l$, that is we need -\[ -\frac{\partial f(z_i^l)}{\partial w_{jk}^l} = -\frac{\partial f(z_i^l)}{\partial z_j^l} \frac{\partial z_j^l}{\partial w_{jk}^l}= \frac{\partial f(z_i^l)}{\partial z_j^l}a_k^{l-1}. -\] -For the Softmax function we have -\[ -f(z_i^l) = \frac{\exp{(z_i^l)}}{\sum_{m=1}^K\exp{(z_m^l)}}. -\] -Its derivative with respect to $z_j^l$ gives -\[ -\frac{\partial f(z_i^l)}{\partial z_j^l}= f(z_i^l)\left(\delta_{ij}-f(z_j^l)\right), -\] -which in case of the simply binary model reduces to having $i=j$. - -% !split -\subsection{Developing a code for doing neural networks with back propagation} - - -One can identify a set of key steps when using neural networks to solve supervised learning problems: - -\begin{enumerate} -\item Collect and pre-process data - -\item Define model and architecture - -\item Choose cost function and optimizer - -\item Train the model - -\item Evaluate model performance on test data - -\item Adjust hyperparameters (if necessary, network architecture) -\end{enumerate} - -\noindent -% !split -\subsection{Collect and pre-process data} - -Here we will be using the MNIST dataset, which is readily available through the \textbf{scikit-learn} -package. You may also find it for example \href{{http://yann.lecun.com/exdb/mnist/}}{here}. -The \emph{MNIST} (Modified National Institute of Standards and Technology) database is a large database -of handwritten digits that is commonly used for training various image processing systems. -The MNIST dataset consists of 70 000 images of size $28\times 28$ pixels, each labeled from 0 to 9. -The scikit-learn dataset we will use consists of a selection of 1797 images of size $8\times 8$ collected and processed from this database. - -To feed data into a feed-forward neural network we need to represent -the inputs as a feature matrix $X = (n_{inputs}, n_{features})$. Each -row represents an \emph{input}, in this case a handwritten digit, and -each column represents a \emph{feature}, in this case a pixel. The -correct answers, also known as \emph{labels} or \emph{targets} are -represented as a 1D array of integers -$Y = (n_{inputs}) = (5, 3, 1, 8,...)$. - -As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from -measurements of height (in m) -and weight (in kg). If we have measurements of 5 people the feature matrix could be for example: - -$$ X = \begin{bmatrix} -1.85 {\&} 81\\ -1.71 {\&} 65\\ -1.95 {\&} 103\\ -1.55 {\&} 42\\ -1.63 {\&} 56 -\end{bmatrix} ,$$ - -and the targets would be: - -$$ Y = (23.7, 22.2, 27.1, 17.5, 21.1) $$ - -Since each input image is a 2D matrix, we need to flatten the image -(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a -feature matrix. This means we lose all spatial information in the -image, such as locality and translational invariance. More complicated -architectures such as Convolutional Neural Networks can take advantage -of such information, and are most commonly applied when analyzing -images. - - -\bpycod -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# flatten the image -# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64 -n_inputs = len(inputs) -inputs = inputs.reshape(n_inputs, -1) -print("X = (n_inputs, n_features) = " + str(inputs.shape)) - - -# choose some random images to display -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\epycod - -% !split -\subsection{Train and test datasets} - -Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions. - -We will reserve $80 \%$ of our dataset for training and $20 \%$ for testing. - -It is important that the train and test datasets are drawn randomly from our dataset, to ensure -no bias in the sampling. -Say you are taking measurements of weather data to predict the weather in the coming 5 days. -You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data -collected from 12.00 to 24.00. - - -\bpycod -from sklearn.model_selection import train_test_split - -# one-liner from scikit-learn library -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) - -# equivalently in numpy -def train_test_split_numpy(inputs, labels, train_size, test_size): - n_inputs = len(inputs) - inputs_shuffled = inputs.copy() - labels_shuffled = labels.copy() - - np.random.shuffle(inputs_shuffled) - np.random.shuffle(labels_shuffled) - - train_end = int(n_inputs*train_size) - X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:] - Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:] - - return X_train, X_test, Y_train, Y_test - -#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size) - -print("Number of training images: " + str(len(X_train))) -print("Number of test images: " + str(len(X_test))) -\epycod - -% !split -\subsection{Define model and architecture} - -Our simple feed-forward neural network will consist of an \emph{input} layer, a single \emph{hidden} layer and an \emph{output} layer. The activation $y$ of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have - -$$ z = \sum_{i=1}^n w_i a_i ,$$ - -$$ y = f(z) ,$$ - -where $f$ is the activation function, $a_i$ represents input from neuron $i$ in the preceding layer -and $w_i$ is the weight to input $i$. -The activation of the neurons in the input layer is just the features (e.g.~a pixel value). - -The simplest activation function for a neuron is the \emph{Heaviside} function: - -$$ f(z) = -\begin{cases} -1, & z > 0\\ -0, & \text{otherwise} -\end{cases} -$$ - -A feed-forward neural network with this activation is known as a \emph{perceptron}. -For a binary classifier (i.e.~two classes, 0 or 1, dog or not-dog) we can also use this in our output layer. -This activation can be generalized to $k$ classes (using e.g.~the \emph{one-against-all} strategy), -and we call these architectures \emph{multiclass perceptrons}. - -However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and -Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function. - -Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU). -We will be using the sigmoid function $\sigma(x)$: - -$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$ - -which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions. - -% !split -\subsection{Layers} - -\begin{itemize} -\item Input -\end{itemize} - -\noindent -Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons. - -\begin{itemize} -\item Hidden layer -\end{itemize} - -\noindent -We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer. -Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer. - -\begin{itemize} -\item Output -\end{itemize} - -\noindent -If we were building a binary classifier, it would be sufficient with a single neuron in the output layer, -which could output 0 or 1 according to the Heaviside function. This would be an example of a \emph{hard} classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a \emph{soft} classifier, which outputs the probability of being in class 0 or 1. - -For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class. - -Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons $j = 0,1,...,9$. The activation of each output neuron $j$ will be according to the \emph{softmax} function: - -$$ P(\text{class $j$} \mid \text{input $\hat{a}$}) = \frac{\exp{(\hat{a}^T \hat{w}_j)}} -{\sum_{c=0}^{9} \exp{(\hat{a}^T \hat{w}_c)}} ,$$ - -i.e.~each neuron $j$ outputs the probability of being in class $j$ given an input from the hidden layer $\hat{a}$, with $\hat{w}_j$ the weights of neuron $j$ to the inputs. -The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1. -The exponent is just the weighted sum of inputs as before: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$ - -Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500 -weights to the output layer. - -% !split -\subsection{Weights and biases} - -Typically weights are initialized with small values distributed around zero, drawn from a uniform -or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless. - -Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range -of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron $j$, $b_j$: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$ - -The bias weights $\hat{b}$ are often initialized to zero, but a small value like $0.01$ ensures all neurons have some output which can be backpropagated in the first training cycle. -\bpycod -# building our neural network - -n_inputs, n_features = X_train.shape -n_hidden_neurons = 50 -n_categories = 10 - -# we make the weights normally distributed using numpy.random.randn - -# weights and bias in the hidden layer -hidden_weights = np.random.randn(n_features, n_hidden_neurons) -hidden_bias = np.zeros(n_hidden_neurons) + 0.01 - -# weights and bias in the output layer -output_weights = np.random.randn(n_hidden_neurons, n_categories) -output_bias = np.zeros(n_categories) + 0.01 -\epycod - -% !split -\subsection{Feed-forward pass} - -Denote $F$ the number of features, $H$ the number of hidden neurons and $C$ the number of categories. -For each input image we calculate a weighted sum of input features (pixel values) to each neuron $j$ in the hidden layer $l$: - -$$ z_{j}^{l} = \sum_{i=1}^{F} w_{ij}^{l} x_i + b_{j}^{l},$$ - -this is then passed through our activation function - -$$ a_{j}^{l} = f(z_{j}^{l}) .$$ - -We calculate a weighted sum of inputs (activations in the hidden layer) to each neuron $j$ in the output layer: - -$$ z_{j}^{L} = \sum_{i=1}^{H} w_{ij}^{L} a_{i}^{l} + b_{j}^{L}.$$ - -Finally we calculate the output of neuron $j$ in the output layer using the softmax function: - -$$ a_{j}^{L} = \frac{\exp{(z_j^{L})}} -{\sum_{c=0}^{C-1} \exp{(z_c^{L})}} .$$ - -% !split -\subsection{Matrix multiplications} - -Since our data has the dimensions $X = (n_{inputs}, n_{features})$ and our weights to the hidden -layer have the dimensions -$W_{hidden} = (n_{features}, n_{hidden})$, -we can easily feed the network all our training data in one go by taking the matrix product - -$$ X W^{h} = (n_{inputs}, n_{hidden}),$$ - -and obtain a matrix that holds the weighted sum of inputs to the hidden layer -for each input image and each hidden neuron. -We also add the bias to obtain a matrix of weighted sums to the hidden layer $Z^{h}$: - -$$ \hat{z}^{l} = \hat{X} \hat{W}^{l} + \hat{b}^{l} ,$$ - -meaning the same bias (1D array with size equal number of hidden neurons) is added to each input image. -This is then passed through the activation: - -$$ \hat{a}^{l} = f(\hat{z}^l) .$$ - -This is fed to the output layer: - -$$ \hat{z}^{L} = \hat{a}^{L} \hat{W}^{L} + \hat{b}^{L} .$$ - -Finally we receive our output values for each image and each category by passing it through the softmax function: - -$$ output = softmax (\hat{z}^{L}) = (n_{inputs}, n_{categories}) .$$ - - -\bpycod -# setup the feed-forward pass, subscript h = hidden layer - -def sigmoid(x): - return 1/(1 + np.exp(-x)) - -def feed_forward(X): - # weighted sum of inputs to the hidden layer - z_h = np.matmul(X, hidden_weights) + hidden_bias - # activation in the hidden layer - a_h = sigmoid(z_h) - - # weighted sum of inputs to the output layer - z_o = np.matmul(a_h, output_weights) + output_bias - # softmax output - # axis 0 holds each input and axis 1 the probabilities of each category - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - return probabilities - -probabilities = feed_forward(X_train) -print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape)) -print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0])) -print("probabilities sum up to: " + str(probabilities[0].sum())) -print() - -# we obtain a prediction by taking the class with the highest likelihood -def predict(X): - probabilities = feed_forward(X) - return np.argmax(probabilities, axis=1) - -predictions = predict(X_train) -print("predictions = (n_inputs) = " + str(predictions.shape)) -print("prediction for image 0: " + str(predictions[0])) -print("correct label for image 0: " + str(Y_train[0])) -\epycod - -% !split -\subsection{Choose cost function and optimizer} - -To measure how well our neural network is doing we need to introduce a cost function. -We will call the function that gives the error of a single sample output the \emph{loss} function, and the function -that gives the total error of our network across all samples the \emph{cost} function. -A typical choice for multiclass classification is the \emph{cross-entropy} loss, also known as the negative log likelihood. - -In \emph{multiclass} classification it is common to treat each integer label as a so called \emph{one-hot} vector: - -$$ y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$$ - - -$$ y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$$ - - -i.e.~a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset. - -Let $y_{ic}$ denote the $c$-th component of the $i$-th one-hot vector. -We define the cost function $\mathcal{C}$ as a sum over the cross-entropy loss for each point $\hat{x}_i$ in the dataset. - -In the one-hot representation only one of the terms in the loss function is non-zero, namely the -probability of the correct category $c'$ -(i.e.~the category $c'$ such that $y_{ic'} = 1$). This means that the cross entropy loss only punishes you for how wrong -you got the correct label. The probability of category $c$ is given by the softmax function. The vector $\hat{\theta}$ represents the parameters of our network, i.e.~all the weights and biases. - - -% !split -\subsection{Optimizing the cost function} - -The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is \emph{gradient descent} and its generalizations. The idea behind gradient descent -is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a \emph{local} minimum of the cost function. -Each parameter $\theta$ is iteratively adjusted according to the rule - -$$ \theta_{i+1} = \theta_i - \eta \nabla \mathcal{C}(\theta_i) ,$$ - -where $\eta$ is known as the \emph{learning rate}, which controls how big a step we take towards the minimum. -This update can be repeated for any number of iterations, or until we are satisfied with the result. - -A simple and effective improvement is a variant called \emph{Batch Gradient Descent}. -Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient -on a subset of the data called a \emph{minibatch}. -If there are $N$ data points and we have a minibatch size of $M$, the total number of batches -is $N/M$. -We denote each minibatch $B_k$, with $k = 1, 2,...,N/M$. The gradient then becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{M} \sum_{i \in B_k} \nabla \mathcal{L}_i(\theta) ,$$ - -i.e.~instead of averaging the loss over the entire dataset, we average over a minibatch. - -This has two important benefits: -\begin{enumerate} -\item Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima. - -\item It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient. -\end{enumerate} - -\noindent -The various optmization methods, with codes and algorithms, are discussed in our lectures on \href{{https://compphysics.github.io/MachineLearning/doc/pub/Splines/html/Splines-bs.html}}{Gradient descent approaches}. - -% !split -\subsection{Regularization} - -It is common to add an extra term to the cost function, proportional -to the size of the weights. This is equivalent to constraining the -size of the weights, so that they do not grow out of control. -Constraining the size of the weights means that the weights cannot -grow arbitrarily large to fit the training data, and in this way -reduces \emph{overfitting}. - -We will measure the size of the weights using the so called \emph{L2-norm}, meaning our cost function becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) + \lambda \lvert \lvert \hat{w} \rvert \rvert_2^2 -= \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}(\theta) + \lambda \sum_{ij} w_{ij}^2,$$ - -i.e.~we sum up all the weights squared. The factor $\lambda$ is known as a regularization parameter. - - -In order to train the model, we need to calculate the derivative of -the cost function with respect to every bias and weight in the -network. In total our network has $(64 + 1)\times 50=3250$ weights in -the hidden layer and $(50 + 1)\times 10=510$ weights to the output -layer ($+1$ for the bias), and the gradient must be calculated for -every parameter. We use the \emph{backpropagation} algorithm discussed -above. This is a clever use of the chain rule that allows us to -calculate the gradient efficently. - - -% !split -\subsection{Matrix multiplication} - -To more efficently train our network these equations are implemented using matrix operations. -The error in the output layer is calculated simply as, with $\hat{t}$ being our targets, - -$$ \delta_L = \hat{t} - \hat{y} = (n_{inputs}, n_{categories}) .$$ - -The gradient for the output weights is calculated as - -$$ \nabla W_{L} = \hat{a}^T \delta_L = (n_{hidden}, n_{categories}) ,$$ - -where $\hat{a} = (n_{inputs}, n_{hidden})$. This simply means that we are summing up the gradients for each input. -Since we are going backwards we have to transpose the activation matrix. - -The gradient with respect to the output bias is then - -$$ \nabla \hat{b}_{L} = \sum_{i=1}^{n_{inputs}} \delta_L = (n_{categories}) .$$ - -The error in the hidden layer is - -$$ \Delta_h = \delta_L W_{L}^T \circ f'(z_{h}) = \delta_L W_{L}^T \circ a_{h} \circ (1 - a_{h}) = (n_{inputs}, n_{hidden}) ,$$ - -where $f'(a_{h})$ is the derivative of the activation in the hidden layer. The matrix products mean -that we are summing up the products for each neuron in the output layer. The symbol $\circ$ denotes -the \emph{Hadamard product}, meaning element-wise multiplication. - -This again gives us the gradients in the hidden layer: - -$$ \nabla W_{h} = X^T \delta_h = (n_{features}, n_{hidden}) ,$$ - -$$ \nabla b_{h} = \sum_{i=1}^{n_{inputs}} \delta_h = (n_{hidden}) .$$ - - -\bpycod -# to categorical turns our integer vector into a onehot representation -from sklearn.metrics import accuracy_score - -# one-hot in numpy -def to_categorical_numpy(integer_vector): - n_inputs = len(integer_vector) - n_categories = np.max(integer_vector) + 1 - onehot_vector = np.zeros((n_inputs, n_categories)) - onehot_vector[range(n_inputs), integer_vector] = 1 - - return onehot_vector - -#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test) -Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test) - -def feed_forward_train(X): - # weighted sum of inputs to the hidden layer - z_h = np.matmul(X, hidden_weights) + hidden_bias - # activation in the hidden layer - a_h = sigmoid(z_h) - - # weighted sum of inputs to the output layer - z_o = np.matmul(a_h, output_weights) + output_bias - # softmax output - # axis 0 holds each input and axis 1 the probabilities of each category - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - # for backpropagation need activations in hidden and output layers - return a_h, probabilities - -def backpropagation(X, Y): - a_h, probabilities = feed_forward_train(X) - - # error in the output layer - error_output = probabilities - Y - # error in the hidden layer - error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h) - - # gradients for the output layer - output_weights_gradient = np.matmul(a_h.T, error_output) - output_bias_gradient = np.sum(error_output, axis=0) - - # gradient for the hidden layer - hidden_weights_gradient = np.matmul(X.T, error_hidden) - hidden_bias_gradient = np.sum(error_hidden, axis=0) - - return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient - -print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train))) - -eta = 0.01 -lmbd = 0.01 -for i in range(1000): - # calculate gradients - dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot) - - # regularization term gradients - dWo += lmbd * output_weights - dWh += lmbd * hidden_weights - - # update weights and biases - output_weights -= eta * dWo - output_bias -= eta * dBo - hidden_weights -= eta * dWh - hidden_bias -= eta * dBh - -print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train))) -\epycod - -% !split -\subsection{Improving performance} - -As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image. -In order to obtain a network that does something useful, we will have to do a bit more work. - -The choice of \emph{hyperparameters} such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a \emph{grid-search} is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates $\eta = 10^{-6}, 10^{-5},...,10^{-1}$ with different regularization parameters $\lambda = 10^{-6},...,10^{-0}$. - -Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an \emph{iteration}, and a full training period -going through the entire dataset ($n/M$ batches) an \emph{epoch}. - -If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers. -Andrew Ng goes through some of these considerations in this \href{{https://youtu.be/F1ka6a13S9I}}{video}. You can find a summary of the video \href{{https://kevinzakka.github.io/2016/09/26/applying-deep-learning/}}{here}. - -% !split -\subsection{Full object-oriented implementation} - -It is very natural to think of the network as an object, with specific instances of the network -being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below. - - -\bpycod -class NeuralNetwork: - def __init__( - self, - X_data, - Y_data, - n_hidden_neurons=50, - n_categories=10, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - - ): - self.X_data_full = X_data - self.Y_data_full = Y_data - - self.n_inputs = X_data.shape[0] - self.n_features = X_data.shape[1] - self.n_hidden_neurons = n_hidden_neurons - self.n_categories = n_categories - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - self.create_biases_and_weights() - - def create_biases_and_weights(self): - self.hidden_weights = np.random.randn(self.n_features, self.n_hidden_neurons) - self.hidden_bias = np.zeros(self.n_hidden_neurons) + 0.01 - - self.output_weights = np.random.randn(self.n_hidden_neurons, self.n_categories) - self.output_bias = np.zeros(self.n_categories) + 0.01 - - def feed_forward(self): - # feed-forward for training - self.z_h = np.matmul(self.X_data, self.hidden_weights) + self.hidden_bias - self.a_h = sigmoid(self.z_h) - - self.z_o = np.matmul(self.a_h, self.output_weights) + self.output_bias - - exp_term = np.exp(self.z_o) - self.probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - def feed_forward_out(self, X): - # feed-forward for output - z_h = np.matmul(X, self.hidden_weights) + self.hidden_bias - a_h = sigmoid(z_h) - - z_o = np.matmul(a_h, self.output_weights) + self.output_bias - - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - return probabilities - - def backpropagation(self): - error_output = self.probabilities - self.Y_data - error_hidden = np.matmul(error_output, self.output_weights.T) * self.a_h * (1 - self.a_h) - - self.output_weights_gradient = np.matmul(self.a_h.T, error_output) - self.output_bias_gradient = np.sum(error_output, axis=0) - - self.hidden_weights_gradient = np.matmul(self.X_data.T, error_hidden) - self.hidden_bias_gradient = np.sum(error_hidden, axis=0) - - if self.lmbd > 0.0: - self.output_weights_gradient += self.lmbd * self.output_weights - self.hidden_weights_gradient += self.lmbd * self.hidden_weights - - self.output_weights -= self.eta * self.output_weights_gradient - self.output_bias -= self.eta * self.output_bias_gradient - self.hidden_weights -= self.eta * self.hidden_weights_gradient - self.hidden_bias -= self.eta * self.hidden_bias_gradient - - def predict(self, X): - probabilities = self.feed_forward_out(X) - return np.argmax(probabilities, axis=1) - - def predict_probabilities(self, X): - probabilities = self.feed_forward_out(X) - return probabilities - - def train(self): - data_indices = np.arange(self.n_inputs) - - for i in range(self.epochs): - for j in range(self.iterations): - # pick datapoints with replacement - chosen_datapoints = np.random.choice( - data_indices, size=self.batch_size, replace=False - ) - - # minibatch training data - self.X_data = self.X_data_full[chosen_datapoints] - self.Y_data = self.Y_data_full[chosen_datapoints] - - self.feed_forward() - self.backpropagation() -\epycod - -% !split -\subsection{Evaluate model performance on test data} - -To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e.~the test data. -We measure the performance of the network using the \emph{accuracy} score. -The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of $1$. - -$$ \text{Accuracy} = \frac{\sum_{i=1}^n I(\hat{y}_i = y_i)}{n} ,$$ - -where $I$ is the indicator function, $1$ if $\hat{y}_i = y_i$ and $0$ otherwise. - - -\bpycod -epochs = 100 -batch_size = 100 - -dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size, - n_hidden_neurons=n_hidden_neurons, n_categories=n_categories) -dnn.train() -test_predict = dnn.predict(X_test) - -# accuracy score from scikit library -print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict)) - -# equivalent in numpy -def accuracy_score_numpy(Y_test, Y_pred): - return np.sum(Y_test == Y_pred) / len(Y_test) - -#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict)) -\epycod - -% !split -\subsection{Adjust hyperparameters} - -We now perform a grid search to find the optimal hyperparameters for the network. -Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around $98\%$ ($2\%$ error rate). - -\bpycod -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -# store the models for later use -DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -# grid search -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size, - n_hidden_neurons=n_hidden_neurons, n_categories=n_categories) - dnn.train() - - DNN_numpy[i][j] = dnn - - test_predict = dnn.predict(X_test) - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict)) - print() -\epycod - -% !split -\subsection{Visualization} - -\bpycod -# visual representation of grid search -# uses seaborn heatmap, you can also do this with matplotlib imshow -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - dnn = DNN_numpy[i][j] - - train_pred = dnn.predict(X_train) - test_pred = dnn.predict(X_test) - - train_accuracy[i][j] = accuracy_score(Y_train, train_pred) - test_accuracy[i][j] = accuracy_score(Y_test, test_pred) - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\epycod - -% !split -\subsection{scikit-learn implementation} - -\textbf{scikit-learn} focuses more -on traditional machine learning methods, such as regression, -clustering, decision trees, etc. As such, it has only two types of -neural networks: Multi Layer Perceptron outputting continuous values, -\emph{MPLRegressor}, and Multi Layer Perceptron outputting labels, -\emph{MLPClassifier}. We will see how simple it is to use these classes. - -\textbf{scikit-learn} implements a few improvements from our neural network, -such as early stopping, a varying learning rate, different -optimization methods, etc. We would therefore expect a better -performance overall. - -\bpycod -from sklearn.neural_network import MLPClassifier -# store models for later use -DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic', - alpha=lmbd, learning_rate_init=eta, max_iter=epochs) - dnn.fit(X_train, Y_train) - - DNN_scikit[i][j] = dnn - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Accuracy score on test set: ", dnn.score(X_test, Y_test)) - print() -\epycod - - -% !split -\subsection{Visualization} -\bpycod -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - dnn = DNN_scikit[i][j] - - train_pred = dnn.predict(X_train) - test_pred = dnn.predict(X_test) - - train_accuracy[i][j] = accuracy_score(Y_train, train_pred) - test_accuracy[i][j] = accuracy_score(Y_test, test_pred) - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\epycod - - -% !split -\subsection{Building neural networks in Tensorflow and Keras} - -Now we want to build on the experience gained from our neural network implementation in NumPy and scikit-learn -and use it to construct a neural network in Tensorflow. Once we have constructed a neural network in NumPy -and Tensorflow, building one in Keras is really quite trivial, though the performance may suffer. - -In our previous example we used only one hidden layer, and in this we will use two. From this it should be quite -clear how to build one using an arbitrary number of hidden layers, using data structures such as Python lists or -NumPy arrays. - -% !split -\subsection{Tensorflow} - -Tensorflow is an open source library machine learning library -developed by the Google Brain team for internal use. It was released -under the Apache 2.0 open source license in November 9, 2015. - -Tensorflow is a computational framework that allows you to construct -machine learning models at different levels of abstraction, from -high-level, object-oriented APIs like Keras, down to the C++ kernels -that Tensorflow is built upon. The higher levels of abstraction are -simpler to use, but less flexible, and our choice of implementation -should reflect the problems we are trying to solve. - -\href{{https://www.tensorflow.org/guide/graphs}}{Tensorflow uses} so-called graphs to represent your computation -in terms of the dependencies between individual operations, such that you first build a Tensorflow \emph{graph} -to represent your model, and then create a Tensorflow \emph{session} to run the graph. - -In this guide we will analyze the same data as we did in our NumPy and -scikit-learn tutorial, gathered from the MNIST database of images. We -will give an introduction to the lower level Python Application -Program Interfaces (APIs), and see how we use them to build our graph. -Then we will build (effectively) the same graph in Keras, to see just -how simple solving a machine learning problem can be. - -To install tensorflow on Unix/Linux systems, use pip as -\bpycod -pip3 install tensorflow -\epycod -and/or if you use \textbf{anaconda}, just write (or install from the graphical user interface) -\bpycod -conda install tensorflow -\epycod - -% !split -\subsection{Collect and pre-process data} - -\bpycod -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# flatten the image -# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64 -n_inputs = len(inputs) -inputs = inputs.reshape(n_inputs, -1) -print("X = (n_inputs, n_features) = " + str(inputs.shape)) - - -# choose some random images to display -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\epycod - -\bpycod -from keras.utils import to_categorical -from sklearn.model_selection import train_test_split - -# one-hot representation of labels -labels = to_categorical(labels) - -# split into train and test data -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) -\epycod - -% !split -\subsection{Using TensorFlow backend} - -\begin{enumerate} -\item Define model and architecture - -\item Choose cost function and optimizer -\end{enumerate} - -\noindent -\bpycod -import tensorflow as tf - -class NeuralNetworkTensorflow: - def __init__( - self, - X_train, - Y_train, - X_test, - Y_test, - n_neurons_layer1=100, - n_neurons_layer2=50, - n_categories=2, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - ): - - # keep track of number of steps - self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step') - - self.X_train = X_train - self.Y_train = Y_train - self.X_test = X_test - self.Y_test = Y_test - - self.n_inputs = X_train.shape[0] - self.n_features = X_train.shape[1] - self.n_neurons_layer1 = n_neurons_layer1 - self.n_neurons_layer2 = n_neurons_layer2 - self.n_categories = n_categories - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - # build network piece by piece - # name scopes (with) are used to enforce creation of new variables - # https://www.tensorflow.org/guide/variables - self.create_placeholders() - self.create_DNN() - self.create_loss() - self.create_optimiser() - self.create_accuracy() - - def create_placeholders(self): - # placeholders are fine here, but "Datasets" are the preferred method - # of streaming data into a model - with tf.name_scope('data'): - self.X = tf.placeholder(tf.float32, shape=(None, self.n_features), name='X_data') - self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data') - - def create_DNN(self): - with tf.name_scope('DNN'): - # the weights are stored to calculate regularization loss later - - # Fully connected layer 1 - self.W_fc1 = self.weight_variable([self.n_features, self.n_neurons_layer1], name='fc1', dtype=tf.float32) - b_fc1 = self.bias_variable([self.n_neurons_layer1], name='fc1', dtype=tf.float32) - a_fc1 = tf.nn.sigmoid(tf.matmul(self.X, self.W_fc1) + b_fc1) - - # Fully connected layer 2 - self.W_fc2 = self.weight_variable([self.n_neurons_layer1, self.n_neurons_layer2], name='fc2', dtype=tf.float32) - b_fc2 = self.bias_variable([self.n_neurons_layer2], name='fc2', dtype=tf.float32) - a_fc2 = tf.nn.sigmoid(tf.matmul(a_fc1, self.W_fc2) + b_fc2) - - # Output layer - self.W_out = self.weight_variable([self.n_neurons_layer2, self.n_categories], name='out', dtype=tf.float32) - b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32) - self.z_out = tf.matmul(a_fc2, self.W_out) + b_out - - def create_loss(self): - with tf.name_scope('loss'): - softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out)) - - regularizer_loss_fc1 = tf.nn.l2_loss(self.W_fc1) - regularizer_loss_fc2 = tf.nn.l2_loss(self.W_fc2) - regularizer_loss_out = tf.nn.l2_loss(self.W_out) - regularizer_loss = self.lmbd*(regularizer_loss_fc1 + regularizer_loss_fc2 + regularizer_loss_out) - - self.loss = softmax_loss + regularizer_loss - - def create_accuracy(self): - with tf.name_scope('accuracy'): - probabilities = tf.nn.softmax(self.z_out) - predictions = tf.argmax(probabilities, axis=1) - labels = tf.argmax(self.Y, axis=1) - - correct_predictions = tf.equal(predictions, labels) - correct_predictions = tf.cast(correct_predictions, tf.float32) - self.accuracy = tf.reduce_mean(correct_predictions) - - def create_optimiser(self): - with tf.name_scope('optimizer'): - self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step) - - def weight_variable(self, shape, name='', dtype=tf.float32): - initial = tf.truncated_normal(shape, stddev=0.1) - return tf.Variable(initial, name=name, dtype=dtype) - - def bias_variable(self, shape, name='', dtype=tf.float32): - initial = tf.constant(0.1, shape=shape) - return tf.Variable(initial, name=name, dtype=dtype) - - def fit(self): - data_indices = np.arange(self.n_inputs) - - with tf.Session() as sess: - sess.run(tf.global_variables_initializer()) - for i in range(self.epochs): - for j in range(self.iterations): - chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False) - batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints] - - sess.run([DNN.loss, DNN.optimizer], - feed_dict={DNN.X: batch_X, - DNN.Y: batch_Y}) - accuracy = sess.run(DNN.accuracy, - feed_dict={DNN.X: batch_X, - DNN.Y: batch_Y}) - step = sess.run(DNN.global_step) - - self.train_loss, self.train_accuracy = sess.run([DNN.loss, DNN.accuracy], - feed_dict={DNN.X: self.X_train, - DNN.Y: self.Y_train}) - - self.test_loss, self.test_accuracy = sess.run([DNN.loss, DNN.accuracy], - feed_dict={DNN.X: self.X_test, - DNN.Y: self.Y_test}) -\epycod - - -% !split -\subsection{Optimizing and using gradient descent} - -\bpycod -epochs = 100 -batch_size = 100 -n_neurons_layer1 = 100 -n_neurons_layer2 = 50 -n_categories = 10 -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -\epycod - - -\bpycod -DNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - DNN = NeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test, - n_neurons_layer1, n_neurons_layer2, n_categories, - epochs=epochs, batch_size=batch_size, eta=eta, lmbd=lmbd) - DNN.fit() - - DNN_tf[i][j] = DNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % DNN.test_accuracy) - print() -\epycod - -\bpycod -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - DNN = DNN_tf[i][j] - - train_accuracy[i][j] = DNN.train_accuracy - test_accuracy[i][j] = DNN.test_accuracy - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\epycod - -\bpycod -# optional -# we can use log files to visualize our graph in Tensorboard -writer = tf.summary.FileWriter('logs/') -writer.add_graph(tf.get_default_graph()) -\epycod - - -% !split -\subsection{Using Keras} - -Keras is a high level \href{{https://en.wikipedia.org/wiki/Application_programming_interface}}{neural network} -that supports Tensorflow, CTNK and Theano as backends. -If you have Tensorflow installed Keras is available through the \emph{tf.keras} module. -If you have Anaconda installed you may run the following command -\bpycod -conda install keras -\epycod - -Alternatively, if you have Tensorflow or one of the other supported backends install you may use the pip package manager: - -\bpycod -pip3 install keras -\epycod -or look up the \href{{https://keras.io/}}{instructions here}. - -\bpycod -from keras.models import Sequential -from keras.layers import Dense -from keras.regularizers import l2 -from keras.optimizers import SGD - -def create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, eta, lmbd): - model = Sequential() - model.add(Dense(n_neurons_layer1, activation='sigmoid', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_neurons_layer2, activation='sigmoid', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_categories, activation='softmax')) - - sgd = SGD(lr=eta) - model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) - - return model -\epycod - -\bpycod -DNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - DNN = create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, - eta=eta, lmbd=lmbd) - DNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0) - scores = DNN.evaluate(X_test, Y_test) - - DNN_keras[i][j] = DNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % scores[1]) - print() -\epycod - -\bpycod -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - DNN = DNN_keras[i][j] - - train_accuracy[i][j] = DNN.evaluate(X_train, Y_train)[1] - test_accuracy[i][j] = DNN.evaluate(X_test, Y_test)[1] - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\epycod - - - - -% !split -\subsection{Which activation function should I use?} - -The Back propagation algorithm we derived above works by going from -the output layer to the input layer, propagating the error gradient on -the way. Once the algorithm has computed the gradient of the cost -function with regards to each parameter in the network, it uses these -gradients to update each parameter with a Gradient Descent (GD) step. - - -Unfortunately for us, the gradients often get smaller and smaller as the -algorithm progresses down to the first hidden layers. As a result, the -GD update leaves the lower layer connection weights -virtually unchanged, and training never converges to a good -solution. This is known in the literature as -\textbf{the vanishing gradients problem}. - -In other cases, the opposite can happen, namely the the gradients can grow bigger and -bigger. The result is that many of the layers get large updates of the -weights the -algorithm diverges. This is the \textbf{exploding gradients problem}, which is -mostly encountered in recurrent neural networks. More generally, deep -neural networks suffer from unstable gradients, different layers may -learn at widely different speeds - -% !split -\subsection{Is the Logistic activation function (Sigmoid) our choice?} - -Although this unfortunate behavior has been empirically observed for -quite a while (it was one of the reasons why deep neural networks were -mostly abandoned for a long time), it is only around 2010 that -significant progress was made in understanding it. - -A paper titled \href{{http://proceedings.mlr.press/v9/glorot10a.html}}{Understanding the Difficulty of Training Deep -Feedforward Neural Networks by Xavier Glorot and Yoshua Bengio} found that -the problems with the popular logistic -sigmoid activation function and the weight initialization technique -that was most popular at the time, namely random initialization using -a normal distribution with a mean of 0 and a standard deviation of -1. - -They showed that with this activation function and this -initialization scheme, the variance of the outputs of each layer is -much greater than the variance of its inputs. Going forward in the -network, the variance keeps increasing after each layer until the -activation function saturates at the top layers. This is actually made -worse by the fact that the logistic function has a mean of 0.5, not 0 -(the hyperbolic tangent function has a mean of 0 and behaves slightly -better than the logistic function in deep networks). - - -% !split -\subsection{The derivative of the Logistic funtion} - -Looking at the logistic activation function, when inputs become large -(negative or positive), the function saturates at 0 or 1, with a -derivative extremely close to 0. Thus when backpropagation kicks in, -it has virtually no gradient to propagate back through the network, -and what little gradient exists keeps getting diluted as -backpropagation progresses down through the top layers, so there is -really nothing left for the lower layers. - -In their paper, Glorot and Bengio propose a way to significantly -alleviate this problem. We need the signal to flow properly in both -directions: in the forward direction when making predictions, and in -the reverse direction when backpropagating gradients. We don’t want -the signal to die out, nor do we want it to explode and saturate. For -the signal to flow properly, the authors argue that we need the -variance of the outputs of each layer to be equal to the variance of -its inputs, and we also need the gradients to have equal variance -before and after flowing through a layer in the reverse direction. - - - -One of the insights in the 2010 paper by Glorot and Bengio was that -the vanishing/exploding gradients problems were in part due to a poor -choice of activation function. Until then most people had assumed that -if Nature had chosen to use roughly sigmoid activation functions in -biological neurons, they must be an excellent choice. But it turns out -that other activation functions behave much better in deep neural -networks, in particular the ReLU activation function, mostly because -it does not saturate for positive values (and also because it is quite -fast to compute). - - -% !split -\subsection{The RELU function family} - -The ReLU activation function suffers from a problem known as the dying -ReLUs: during training, some neurons effectively die, meaning they -stop outputting anything other than 0. - -In some cases, you may find that half of your network’s neurons are -dead, especially if you used a large learning rate. During training, -if a neuron’s weights get updated such that the weighted sum of the -neuron’s inputs is negative, it will start outputting 0. When this -happen, the neuron is unlikely to come back to life since the gradient -of the ReLU function is 0 when its input is negative. - -To solve this problem, nowadays practitioners use a variant of the ReLU -function, such as the leaky ReLU discussed above or the so-called -exponential linear unit (ELU) function - - -\[ -ELU(z) = \left\{\begin{array}{cc} \alpha\left( \exp{(z)}-1\right) & z < 0,\\ z & z \ge 0.\end{array}\right. -\] - -% !split -\subsection{Which activation function should we use?} - -In general it seems that the ELU activation function is better than -the leaky ReLU function (and its variants), which is better than -ReLU. ReLU performs better than $\tanh$ which in turn performs better -than the logistic function. - -If runtime -performance is an issue, then you may opt for the leaky ReLU function over the -ELU function If you don’t -want to tweak yet another hyperparameter, you may just use the default -$\alpha$ of $0.01$ for the leaky ReLU, and $1$ for ELU. If you have -spare time and computing power, you can use cross-validation or -bootstrap to evaluate other activation functions. - - -% !split -\subsection{A top-down perspective on Neural networks} - - -The first thing we would like to do is divide the data into two or three -parts. A training set, a validation or dev (development) set, and a -test set. The test set is the data on which we want to make -predictions. The dev set is a subset of the training data we use to -check how well we are doing out-of-sample, after training the model on -the training dataset. We use the validation error as a proxy for the -test error in order to make tweaks to our model. It is crucial that we -do not use any of the test data to train the algorithm. This is a -cardinal sin in ML. Then: - - -\begin{itemize} -\item Estimate optimal error rate - -\item Minimize underfitting (bias) on training data set. - -\item Make sure you are not overfitting. -\end{itemize} - -\noindent -If the validation and test sets are drawn from the same distributions, -then a good performance on the validation set should lead to similarly -good performance on the test set. - -However, sometimes -the training data and test data differ in subtle ways because, for -example, they are collected using slightly different methods, or -because it is cheaper to collect data in one way versus another. In -this case, there can be a mismatch between the training and test -data. This can lead to the neural network overfitting these small -differences between the test and training sets, and a poor performance -on the test set despite having a good performance on the validation -set. To rectify this, Andrew Ng suggests making two validation or dev -sets, one constructed from the training data and one constructed from -the test data. The difference between the performance of the algorithm -on these two validation sets quantifies the train-test mismatch. This -can serve as another important diagnostic when using DNNs for -supervised learning. - -% !split -\subsection{Limitations of supervised learning with deep networks} - -Like all statistical methods, supervised learning using neural -networks has important limitations. This is especially important when -one seeks to apply these methods, especially to physics problems. Like -all tools, DNNs are not a universal solution. Often, the same or -better performance on a task can be achieved by using a few -hand-engineered features (or even a collection of random -features). - -Here we list some of the important limitations of supervised neural network based models. - - - -\begin{itemize} -\item \textbf{Need labeled data}. All supervised learning methods, DNNs for supervised learning require labeled data. Often, labeled data is harder to acquire than unlabeled data (e.g.~one must pay for human experts to label images). - -\item \textbf{Supervised neural networks are extremely data intensive.} DNNs are data hungry. They perform best when data is plentiful. This is doubly so for supervised methods where the data must also be labeled. The utility of DNNs is extremely limited if data is hard to acquire or the datasets are small (hundreds to a few thousand samples). In this case, the performance of other methods that utilize hand-engineered features can exceed that of DNNs. - -\item \textbf{Homogeneous data.} Almost all DNNs deal with homogeneous data of one type. It is very hard to design architectures that mix and match data types (i.e.~some continuous variables, some discrete variables, some time series). In applications beyond images, video, and language, this is often what is required. In contrast, ensemble models like random forests or gradient-boosted trees have no difficulty handling mixed data types. - -\item \textbf{Many problems are not about prediction.} In natural science we are often interested in learning something about the underlying distribution that generates the data. In this case, it is often difficult to cast these ideas in a supervised learning setting. While the problems are related, it is possible to make good predictions with a \emph{wrong} model. The model might or might not be useful for understanding the underlying science. -\end{itemize} - -\noindent -Some of these remarks are particular to DNNs, others are shared by all supervised learning methods. This motivates the use of unsupervised methods which in part circumvent these problems. - - -% !split -\subsection{Convolutional Neural Networks (recognizing images)} - -Convolutional Neural Networks (CNN) are very similar to ordinary Neural Networks. - -They are made up of neurons that have learnable weights and -biases. Each neuron receives some inputs, performs a dot product and -optionally follows it with a non-linearity. The whole network still -expresses a single differentiable score function: from the raw image -pixels on one end to class scores at the other. And they still have a -loss function (for example Softmax) on the last (fully-connected) layer -and all the tips/tricks we developed for learning regular Neural -Networks still apply (back propagation, gradient descent etc etc). - -What is the difference? \textbf{CNN architectures make the explicit assumption that -the inputs are images, which allows us to encode certain properties -into the architecture. These then make the forward function more -efficient to implement and vastly reduce the amount of parameters in -the network.} - -Here we provide only a superficial overview, for the more interested, we recommend highly the course -\href{{https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html}}{IN5400 – Machine Learning for Image Analysis} -and the slides of \href{{http://cs231n.github.io/convolutional-networks/}}{CS231}. - -% !split -\subsection{Regular NNs don’t scale well to full images} - -As an example, consider -an image of size $32\times 32\times 3$ (32 wide, 32 high, 3 color channels), so a -single fully-connected neuron in a first hidden layer of a regular -Neural Network would have $32\times 32\times 3 = 3072$ weights. This amount still -seems manageable, but clearly this fully-connected structure does not -scale to larger images. For example, an image of more respectable -size, say $200\times 200\times 3$, would lead to neurons that have -$200\times 200\times 3 = 120,000$ weights. - -We could have -several such neurons, and the parameters would add up quickly! Clearly, -this full connectivity is wasteful and the huge number of parameters -would quickly lead to possible overfitting. - - -\begin{figure}[!ht] % - \centerline{\includegraphics[width=0.6\linewidth]{figslides/nn.jpeg}} - \caption{ - A regular 3-layer Neural Network. - } -\end{figure} -%\clearpage % flush figures - - -% !split -\subsection{3D volumes of neurons} - -Convolutional Neural Networks take advantage of the fact that the -input consists of images and they constrain the architecture in a more -sensible way. - -In particular, unlike a regular Neural Network, the -layers of a CNN have neurons arranged in 3 dimensions: width, -height, depth. (Note that the word depth here refers to the third -dimension of an activation volume, not to the depth of a full Neural -Network, which can refer to the total number of layers in a network.) - -To understand it better, the above example of an image -with an input volume of -activations has dimensions $32\times 32\times 3$ (width, height, -depth respectively). - -The neurons in a layer will -only be connected to a small region of the layer before it, instead of -all of the neurons in a fully-connected manner. Moreover, the final -output layer could for this specific image have dimensions $1\times 1 times 10$, -because by the -end of the CNN architecture we will reduce the full image into a -single vector of class scores, arranged along the depth -dimension. - - -\begin{figure}[!ht] % - \centerline{\includegraphics[width=0.6\linewidth]{figslides/cnn.jpeg}} - \caption{ - A CNN arranges its neurons in three dimensions (width, height, depth), as visualized in one of the layers. Every layer of a CNN transforms the 3D input volume to a 3D output volume of neuron activations. In this example, the red input layer holds the image, so its width and height would be the dimensions of the image, and the depth would be 3 (Red, Green, Blue channels). - } -\end{figure} -%\clearpage % flush figures - - - - -% !split -\subsection{Layers used to build CNNs} - - -A simple CNN is a sequence of layers, and every layer of a CNN -transforms one volume of activations to another through a -differentiable function. We use three main types of layers to build -CNN architectures: Convolutional Layer, Pooling Layer, and -Fully-Connected Layer (exactly as seen in regular Neural Networks). We -will stack these layers to form a full CNN architecture. - -A simple CNN for image classification could have the architecture: - -\begin{itemize} -\item \textbf{INPUT} ($32\times 32 \times 3$) will hold the raw pixel values of the image, in this case an image of width 32, height 32, and with three color channels R,G,B. - -\item \textbf{CONV} (convolutional )layer will compute the output of neurons that are connected to local regions in the input, each computing a dot product between their weights and a small region they are connected to in the input volume. This may result in volume such as $[32\times 32\times 12]$ if we decided to use 12 filters. - -\item \textbf{RELU} layer will apply an elementwise activation function, such as the $max(0,x)$ thresholding at zero. This leaves the size of the volume unchanged ($[32\times 32\times 12]$). - -\item \textbf{POOL} (pooling) layer will perform a downsampling operation along the spatial dimensions (width, height), resulting in volume such as $[16\times 16\times 12]$. - -\item \textbf{FC} (i.e.~fully-connected) layer will compute the class scores, resulting in volume of size $[1\times 1\times 10]$, where each of the 10 numbers correspond to a class score, such as among the 10 categories of the MNIST images we considered above . As with ordinary Neural Networks and as the name implies, each neuron in this layer will be connected to all the numbers in the previous volume. -\end{itemize} - -\noindent -% !split -\subsection{Transforming images} - -CNNs transform the original image layer by layer from the original -pixel values to the final class scores. - -Observe that some layers contain -parameters and other don’t. In particular, the CNN layers perform -transformations that are a function of not only the activations in the -input volume, but also of the parameters (the weights and biases of -the neurons). On the other hand, the RELU/POOL layers will implement a -fixed function. The parameters in the CONV/FC layers will be trained -with gradient descent so that the class scores that the CNN computes -are consistent with the labels in the training set for each image. - - -% !split -\subsection{CNNs in brief} - -In summary: - -\begin{itemize} -\item A CNN architecture is in the simplest case a list of Layers that transform the image volume into an output volume (e.g.~holding the class scores) - -\item There are a few distinct types of Layers (e.g.~CONV/FC/RELU/POOL are by far the most popular) - -\item Each Layer accepts an input 3D volume and transforms it to an output 3D volume through a differentiable function - -\item Each Layer may or may not have parameters (e.g.~CONV/FC do, RELU/POOL don’t) - -\item Each Layer may or may not have additional hyperparameters (e.g.~CONV/FC/POOL do, RELU doesn’t) -\end{itemize} - -\noindent -For more material on convolutional networks, we strongly recommend -the course -\href{{https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html}}{IN5400 – Machine Learning for Image Analysis} -and the slides of \href{{http://cs231n.github.io/convolutional-networks/}}{CS231} which is taught at Stanford University (consistently ranked as one of the top computer science programs in the world). \href{{http://neuralnetworksanddeeplearning.com/chap6.html}}{Michael Nielsen's book is a must read, in particular chapter 6 which deals with CNNs}. - - -% !split -\subsection{CNNs in more detail, building convolutional neural networks in Tensorflow and Keras} - - -As discussed above, CNNs are neural networks built from the assumption that the inputs -to the network are 2D images. This is important because the number of features or pixels in images -grows very fast with the image size, and an enormous number of weights and biases are needed in order to build an accurate network. - -As before, we still have our input, a hidden layer and an output. What's novel about convolutional networks -are the \textbf{convolutional} and \textbf{pooling} layers stacked in pairs between the input and the hidden layer. -In addition, the data is no longer represented as a 2D feature matrix, instead each input is a number of 2D -matrices, typically 1 for each color dimension (Red, Green, Blue). - - -% !split -\subsection{Setting it up} - -It means that to represent the entire -dataset of images, we require a 4D matrix or \textbf{tensor}. This tensor has the dimensions: -\[ -(n_{inputs},\, n_{pixels, width},\, n_{pixels, height},\, depth) . -\] - -% !split -\subsection{The MNIST dataset again} - -The MNIST dataset consists of grayscale images with a pixel size of -$28\times 28$, meaning we require $28 \times 28 = 724$ weights to each -neuron in the first hidden layer. - -If we were to analyze images of size $128\times 128$ we would require -$128 \times 128 = 16384$ weights to each neuron. Even worse if we were -dealing with color images, as most images are, we have an image matrix -of size $128\times 128$ for each color dimension (Red, Green, Blue), -meaning 3 times the number of weights $= 49152$ are required for every -single neuron in the first hidden layer. - - -% !split -\subsection{Strong correlations} -Images typically have strong local correlations, meaning that a small -part of the image varies little from its neighboring regions. If for -example we have an image of a blue car, we can roughly assume that a -small blue part of the image is surrounded by other blue regions. - -Therefore, instead of connecting every single pixel to a neuron in the -first hidden layer, as we have previously done with deep neural -networks, we can instead connect each neuron to a small part of the -image (in all 3 RGB depth dimensions). The size of each small area is -fixed, and known as a \href{{https://en.wikipedia.org/wiki/Receptive_field}}{receptive}. - - -% !split -\subsection{Layers of a CNN} -The layers of a convolutional neural network arrange neurons in 3D: width, height and depth. -The input image is typically a square matrix of depth 3. - -A \textbf{convolution} is performed on the image which outputs -a 3D volume of neurons. The weights to the input are arranged in a number of 2D matrices, known as \textbf{filters}. - - -Each filter slides along the input image, taking the dot product -between each small part of the image and the filter, in all depth -dimensions. This is then passed through a non-linear function, -typically the \textbf{Rectified Linear (ReLu)} function, which serves as the -activation of the neurons in the first convolutional layer. This is -further passed through a \textbf{pooling layer}, which reduces the size of the -convolutional layer, e.g.~by taking the maximum or average across some -small regions, and this serves as input to the next convolutional -layer. - - -% !split -\subsection{Systematic reduction} - -By systematically reducing the size of the input volume, through -convolution and pooling, the network should create representations of -small parts of the input, and then from them assemble representations -of larger areas. The final pooling layer is flattened to serve as -input to a hidden layer, such that each neuron in the final pooling -layer is connected to every single neuron in the hidden layer. This -then serves as input to the output layer, e.g.~a softmax output for -classification. - - -% !split -\subsection{Prerequisites: Collect and pre-process data} -\bpycod -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -# RGB images have a depth of 3 -# our images are grayscale so they should have a depth of 1 -inputs = inputs[:,:,:,np.newaxis] - -print("inputs = (n_inputs, pixel_width, pixel_height, depth) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# choose some random images to display -n_inputs = len(inputs) -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\epycod - - -% !split -\subsection{Importing Keras and Tensorflow} -\bpycod -from keras.utils import to_categorical -from sklearn.model_selection import train_test_split - -# representation of labels -labels = to_categorical(labels) - -# split into train and test data -# one-liner from scikit-learn library -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) -\epycod - -% !split -\subsection{Using TensorFlow backend} - -We need to define model and architecture and choose cost function and optmizer. -\bpycid - -import tensorflow as tf - -class ConvolutionalNeuralNetworkTensorflow: - def __init__( - self, - X_train, - Y_train, - X_test, - Y_test, - n_filters=10, - n_neurons_connected=50, - n_categories=10, - receptive_field=3, - stride=1, - padding=1, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - ): - - self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step') - - self.X_train = X_train - self.Y_train = Y_train - self.X_test = X_test - self.Y_test = Y_test - - self.n_inputs, self.input_width, self.input_height, self.depth = X_train.shape - - self.n_filters = n_filters - self.n_downsampled = int(self.input_width*self.input_height*n_filters / 4) - self.n_neurons_connected = n_neurons_connected - self.n_categories = n_categories - - self.receptive_field = receptive_field - self.stride = stride - self.strides = [stride, stride, stride, stride] - self.padding = padding - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - self.create_placeholders() - self.create_CNN() - self.create_loss() - self.create_optimiser() - self.create_accuracy() - - def create_placeholders(self): - with tf.name_scope('data'): - self.X = tf.placeholder(tf.float32, shape=(None, self.input_width, self.input_height, self.depth), name='X_data') - self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data') - - def create_CNN(self): - with tf.name_scope('CNN'): - - # Convolutional layer - self.W_conv = self.weight_variable([self.receptive_field, self.receptive_field, self.depth, self.n_filters], name='conv', dtype=tf.float32) - b_conv = self.weight_variable([self.n_filters], name='conv', dtype=tf.float32) - z_conv = tf.nn.conv2d(self.X, self.W_conv, self.strides, padding='SAME', name='conv') + b_conv - a_conv = tf.nn.relu(z_conv) - - # 2x2 max pooling - a_pool = tf.nn.max_pool(a_conv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME', name='pool') - - # Fully connected layer - a_pool_flat = tf.reshape(a_pool, [-1, self.n_downsampled]) - self.W_fc = self.weight_variable([self.n_downsampled, self.n_neurons_connected], name='fc', dtype=tf.float32) - b_fc = self.bias_variable([self.n_neurons_connected], name='fc', dtype=tf.float32) - a_fc = tf.nn.relu(tf.matmul(a_pool_flat, self.W_fc) + b_fc) - - # Output layer - self.W_out = self.weight_variable([self.n_neurons_connected, self.n_categories], name='out', dtype=tf.float32) - b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32) - self.z_out = tf.matmul(a_fc, self.W_out) + b_out - - def create_loss(self): - with tf.name_scope('loss'): - softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out)) - - regularizer_loss_conv = tf.nn.l2_loss(self.W_conv) - regularizer_loss_fc = tf.nn.l2_loss(self.W_fc) - regularizer_loss_out = tf.nn.l2_loss(self.W_out) - regularizer_loss = self.lmbd*(regularizer_loss_conv + regularizer_loss_fc + regularizer_loss_out) - - self.loss = softmax_loss + regularizer_loss - - def create_accuracy(self): - with tf.name_scope('accuracy'): - probabilities = tf.nn.softmax(self.z_out) - predictions = tf.argmax(probabilities, 1) - labels = tf.argmax(self.Y, 1) - - correct_predictions = tf.equal(predictions, labels) - correct_predictions = tf.cast(correct_predictions, tf.float32) - self.accuracy = tf.reduce_mean(correct_predictions) - - def create_optimiser(self): - with tf.name_scope('optimizer'): - self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step) - - def weight_variable(self, shape, name='', dtype=tf.float32): - initial = tf.truncated_normal(shape, stddev=0.1) - return tf.Variable(initial, name=name, dtype=dtype) - - def bias_variable(self, shape, name='', dtype=tf.float32): - initial = tf.constant(0.1, shape=shape) - return tf.Variable(initial, name=name, dtype=dtype) - - def fit(self): - data_indices = np.arange(self.n_inputs) - - with tf.Session() as sess: - sess.run(tf.global_variables_initializer()) - for i in range(self.epochs): - for j in range(self.iterations): - chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False) - batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints] - - sess.run([CNN.loss, CNN.optimizer], - feed_dict={CNN.X: batch_X, - CNN.Y: batch_Y}) - accuracy = sess.run(CNN.accuracy, - feed_dict={CNN.X: batch_X, - CNN.Y: batch_Y}) - step = sess.run(CNN.global_step) - - self.train_loss, self.train_accuracy = sess.run([CNN.loss, CNN.accuracy], - feed_dict={CNN.X: self.X_train, - CNN.Y: self.Y_train}) - - self.test_loss, self.test_accuracy = sess.run([CNN.loss, CNN.accuracy], - feed_dict={CNN.X: self.X_test, - CNN.Y: self.Y_test}) -\epycid - -% !split -\subsection{Train the model} - -We need now to train the model, evaluate it and test its performance on test data, and eventually include hyperparameters. -\bpycod -epochs = 100 -batch_size = 100 -n_filters = 10 -n_neurons_connected = 50 -n_categories = 10 - -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -CNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - CNN = ConvolutionalNeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test, - n_filters=n_filters, n_neurons_connected=n_neurons_connected, - n_categories=n_categories, epochs=epochs, batch_size=batch_size, - eta=eta, lmbd=lmbd) - CNN.fit() - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % CNN.test_accuracy) - print() - - CNN_tf[i][j] = CNN -\epycod - -% !split -\subsection{Visualizing the results} - -\bpycod -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - CNN = CNN_tf[i][j] - - train_accuracy[i][j] = CNN.train_accuracy - test_accuracy[i][j] = CNN.test_accuracy - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\epycod - -% !split -\subsection{Running with Keras} - -\bpycod -from keras.models import Sequential -from keras.layers.convolutional import Conv2D -from keras.layers.convolutional import MaxPooling2D -from keras.layers import Flatten -from keras.layers import Dense -from keras.regularizers import l2 -from keras.optimizers import SGD - -def create_convolutional_neural_network_keras(input_shape, receptive_field, - n_filters, n_neurons_connected, n_categories, - eta, lmbd): - model = Sequential() - model.add(Conv2D(n_filters, (receptive_field, receptive_field), input_shape=input_shape, padding='same', - activation='relu', kernel_regularizer=l2(lmbd))) - model.add(MaxPooling2D(pool_size=(2, 2))) - model.add(Flatten()) - model.add(Dense(n_neurons_connected, activation='relu', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_categories, activation='softmax', kernel_regularizer=l2(lmbd))) - - sgd = SGD(lr=eta) - model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) - - return model - -epochs = 100 -batch_size = 100 -input_shape = X_train.shape[1:4] -receptive_field = 3 -n_filters = 10 -n_neurons_connected = 50 -n_categories = 10 - -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -\epycod - -% !split -\subsection{Final part} - -\bpycod -CNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - CNN = create_convolutional_neural_network_keras(input_shape, receptive_field, - n_filters, n_neurons_connected, n_categories, - eta, lmbd) - CNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0) - scores = CNN.evaluate(X_test, Y_test) - - CNN_keras[i][j] = CNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % scores[1]) - print() -\epycod - -% !split -\subsection{Final visualization} - -\bccq -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - CNN = CNN_keras[i][j] - - train_accuracy[i][j] = CNN.evaluate(X_train, Y_train)[1] - test_accuracy[i][j] = CNN.evaluate(X_test, Y_test)[1] - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\eccq - -% !split -\subsection{Fun links} - -\begin{enumerate} -\item \href{{https://arxiv.org/abs/1604.07316}}{Self-Driving cars using a convolutional neural network} - -\item \href{{https://deepdreamgenerator.com/}}{Abstract art using convolutional neural networks} -\end{enumerate} - -\noindent - -% ------------------- end of main content --------------- - -% #ifdef PREAMBLE -\end{document} -% #endif - diff --git a/doc/src/NeuralNet/NeuralNet.tex b/doc/src/NeuralNet/NeuralNet.tex deleted file mode 100644 index 6b0c4177a..000000000 --- a/doc/src/NeuralNet/NeuralNet.tex +++ /dev/null @@ -1,3190 +0,0 @@ -%% -%% Automatically generated file from DocOnce source -%% (https://github.com/hplgit/doconce/) -%% -%% - - -%-------------------- begin preamble ---------------------- - -\documentclass[% -oneside, % oneside: electronic viewing, twoside: printing -final, % draft: marks overfull hboxes, figures with paths -10pt]{article} - -\listfiles % print all files needed to compile this document - -\usepackage{relsize,makeidx,color,setspace,amsmath,amsfonts,amssymb} -\usepackage[table]{xcolor} -\usepackage{bm,ltablex,microtype} - -\usepackage[pdftex]{graphicx} - -\usepackage{fancyvrb} % packages needed for verbatim environments -\usepackage{minted} -\usemintedstyle{default} - -\usepackage[T1]{fontenc} -%\usepackage[latin1]{inputenc} -\usepackage{ucs} -\usepackage[utf8x]{inputenc} - -\usepackage{lmodern} % Latin Modern fonts derived from Computer Modern - -% Hyperlinks in PDF: -\definecolor{linkcolor}{rgb}{0,0,0.4} -\usepackage{hyperref} -\hypersetup{ - breaklinks=true, - colorlinks=true, - linkcolor=linkcolor, - urlcolor=linkcolor, - citecolor=black, - filecolor=black, - %filecolor=blue, - pdfmenubar=true, - pdftoolbar=true, - bookmarksdepth=3 % Uncomment (and tweak) for PDF bookmarks with more levels than the TOC - } -%\hyperbaseurl{} % hyperlinks are relative to this root - -\setcounter{tocdepth}{2} % levels in table of contents - -% Tricks for having figures close to where they are defined: -% 1. define less restrictive rules for where to put figures -\setcounter{topnumber}{2} -\setcounter{bottomnumber}{2} -\setcounter{totalnumber}{4} -\renewcommand{\topfraction}{0.95} -\renewcommand{\bottomfraction}{0.95} -\renewcommand{\textfraction}{0} -\renewcommand{\floatpagefraction}{0.75} -% floatpagefraction must always be less than topfraction! -% 2. ensure all figures are flushed before next section -\usepackage[section]{placeins} -% 3. enable begin{figure}[H] (often leads to ugly pagebreaks) -%\usepackage{float}\restylefloat{figure} - -% --- fancyhdr package for fancy headers --- -\usepackage{fancyhdr} -\fancyhf{} % sets both header and footer to nothing -\renewcommand{\headrulewidth}{0pt} -\fancyfoot[LE,RO]{\thepage} -% Ensure copyright on titlepage (article style) and chapter pages (book style) -\fancypagestyle{plain}{ - \fancyhf{} - \fancyfoot[C]{{\footnotesize \copyright\ 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license}} -% \renewcommand{\footrulewidth}{0mm} - \renewcommand{\headrulewidth}{0mm} -} -% Ensure copyright on titlepages with \thispagestyle{empty} -\fancypagestyle{empty}{ - \fancyhf{} - \fancyfoot[C]{{\footnotesize \copyright\ 1999-2018, Morten Hjorth-Jensen. Released under CC Attribution-NonCommercial 4.0 license}} - \renewcommand{\footrulewidth}{0mm} - \renewcommand{\headrulewidth}{0mm} -} - -\pagestyle{fancy} - - -\usepackage[framemethod=TikZ]{mdframed} - -% --- begin definitions of admonition environments --- - -% --- end of definitions of admonition environments --- - -% prevent orhpans and widows -\clubpenalty = 10000 -\widowpenalty = 10000 - -% --- end of standard preamble for documents --- - - -% insert custom LaTeX commands... - -\raggedbottom -\makeindex -\usepackage[totoc]{idxlayout} % for index in the toc -\usepackage[nottoc]{tocbibind} % for references/bibliography in the toc - -%-------------------- end preamble ---------------------- - -\begin{document} - -% matching end for #ifdef PREAMBLE - -\newcommand{\exercisesection}[1]{\subsection*{#1}} - - -% ------------------- main content ---------------------- - - - -% ----------------- title ------------------------- - -\thispagestyle{empty} - -\begin{center} -{\LARGE\bf -\begin{spacing}{1.25} -Data Analysis and Machine Learning: Neural networks, from the simple perceptron to deep learning and convolutional networks -\end{spacing} -} -\end{center} - -% ----------------- author(s) ------------------------- - -\begin{center} -{\bf Morten Hjorth-Jensen${}^{1, 2}$} \\ [0mm] -\end{center} - -\begin{center} -% List of all institutions: -\centerline{{\small ${}^1$Department of Physics, University of Oslo}} -\centerline{{\small ${}^2$Department of Physics and Astronomy and National Superconducting Cyclotron Laboratory, Michigan State University}} -\end{center} - -% ----------------- end author(s) ------------------------- - -% --- begin date --- -\begin{center} -Oct 19, 2018 -\end{center} -% --- end date --- - -\vspace{1cm} - - -% !split -\subsection*{Neural networks} - -Artificial neural networks are computational systems that can learn to -perform tasks by considering examples, generally without being -programmed with any task-specific rules. It is supposed to mimic a -biological system, wherein neurons interact by sending signals in the -form of mathematical functions between layers. All layers can contain -an arbitrary number of neurons, and each connection is represented by -a weight variable. - - -% !split -\subsection*{Artificial neurons} - -The field of artificial neural networks has a long history of -development, and is closely connected with the advancement of computer -science and computers in general. A model of artificial neurons was -first developed by McCulloch and Pitts in 1943 to study signal -processing in the brain and has later been refined by others. The -general idea is to mimic neural networks in the human brain, which is -composed of billions of neurons that communicate with each other by -sending electrical signals. Each neuron accumulates its incoming -signals, which must exceed an activation threshold to yield an -output. If the threshold is not overcome, the neuron remains inactive, -i.e.~has zero output. - -This behaviour has inspired a simple mathematical model for an artificial neuron. - -\begin{equation} - y = f\left(\sum_{i=1}^n w_ix_i\right) = f(u) - \label{artificialNeuron} -\end{equation} -Here, the output $y$ of the neuron is the value of its activation function, which have as input -a weighted sum of signals $x_i, \dots ,x_n$ received by $n$ other neurons. - -Conceptually, it is helpful to divide neural networks into four -categories: -\begin{enumerate} -\item general purpose neural networks for supervised learning, - -\item neural networks designed specifically for image processing, the most prominent example of this class being Convolutional Neural Networks (CNNs), - -\item neural networks for sequential data such as Recurrent Neural Networks (RNNs), and - -\item neural networks for unsupervised learning such as Deep Boltzmann Machines. -\end{enumerate} - -\noindent -In natural science, DNNs and CNNs have already found numerous -applications. In statistical physics, they have been applied to detect -phase transitions in 2D Ising and Potts models, lattice gauge -theories, and different phases of polymers, or solving the -Navier-Stokes equation in weather forecasting. Deep learning has also -found interesting applications in quantum physics. Various quantum -phase transitions can be detected and studied using DNNs and CNNs, -topological phases, and even non-equilibrium many-body -localization. Representing quantum states as DNNs quantum state -tomography are among some of the impressive achievements to reveal the -potential of DNNs to facilitate the study of quantum systems. - -In quantum information theory, it has been shown that one can perform -gate decompositions with the help of neural. - -The applications are not limited to the natural sciences. There is a -plethora of applications in essentially all disciplines, from the -humanities to life science and medicine. - -% !split -\subsection*{Neural network types} - -An artificial neural network (ANN), is a computational model that -consists of layers of connected neurons, or nodes or units. We will -refer to these interchangeably as units or nodes, and sometimes as -neurons. - -It is supposed to mimic a biological nervous system by letting each -neuron interact with other neurons by sending signals in the form of -mathematical functions between layers. A wide variety of different -ANNs have been developed, but most of them consist of an input layer, -an output layer and eventual layers in-between, called \emph{hidden -layers}. All layers can contain an arbitrary number of nodes, and each -connection between two nodes is associated with a weight variable. - -Neural networks (also called neural nets) are neural-inspired -nonlinear models for supervised learning. As we will see, neural nets -can be viewed as natural, more powerful extensions of supervised -learning methods such as linear and logistic regression and soft-max -methods we discussed earlier. - - -% !split -\subsection*{Feed-forward neural networks} - -The feed-forward neural network (FFNN) was the first and simplest type -of ANNs that were devised. In this network, the information moves in -only one direction: forward through the layers. - -Nodes are represented by circles, while the arrows display the -connections between the nodes, including the direction of information -flow. Additionally, each arrow corresponds to a weight variable -(figure to come). We observe that each node in a layer is connected -to \emph{all} nodes in the subsequent layer, making this a so-called -\emph{fully-connected} FFNN. - - - -% !split -\subsection*{Convolutional Neural Network} - -A different variant of FFNNs are \emph{convolutional neural networks} -(CNNs), which have a connectivity pattern inspired by the animal -visual cortex. Individual neurons in the visual cortex only respond to -stimuli from small sub-regions of the visual field, called a receptive -field. This makes the neurons well-suited to exploit the strong -spatially local correlation present in natural images. The response of -each neuron can be approximated mathematically as a convolution -operation. (figure to come) - -Convolutional neural networks emulate the behaviour of neurons in the -visual cortex by enforcing a \emph{local} connectivity pattern between -nodes of adjacent layers: Each node in a convolutional layer is -connected only to a subset of the nodes in the previous layer, in -contrast to the fully-connected FFNN. Often, CNNs consist of several -convolutional layers that learn local features of the input, with a -fully-connected layer at the end, which gathers all the local data and -produces the outputs. They have wide applications in image and video -recognition. - -% !split -\subsection*{Recurrent neural networks} - -So far we have only mentioned ANNs where information flows in one -direction: forward. \emph{Recurrent neural networks} on the other hand, -have connections between nodes that form directed \emph{cycles}. This -creates a form of internal memory which are able to capture -information on what has been calculated before; the output is -dependent on the previous computations. Recurrent NNs make use of -sequential information by performing the same task for every element -in a sequence, where each element depends on previous elements. An -example of such information is sentences, making recurrent NNs -especially well-suited for handwriting and speech recognition. - -% !split -\subsection*{Other types of networks} - -There are many other kinds of ANNs that have been developed. One type -that is specifically designed for interpolation in multidimensional -space is the radial basis function (RBF) network. RBFs are typically -made up of three layers: an input layer, a hidden layer with -non-linear radial symmetric activation functions and a linear output -layer (''linear'' here means that each node in the output layer has a -linear activation function). The layers are normally fully-connected -and there are no cycles, thus RBFs can be viewed as a type of -fully-connected FFNN. They are however usually treated as a separate -type of NN due the unusual activation functions. - -% !split -\subsection*{Multilayer perceptrons} - -One uses often so-called fully-connected feed-forward neural networks -with three or more layers (an input layer, one or more hidden layers -and an output layer) consisting of neurons that have non-linear -activation functions. - -Such networks are often called \emph{multilayer perceptrons} (MLPs). - -% !split -\subsection*{Why multilayer perceptrons?} - -According to the \emph{Universal approximation theorem}, a feed-forward -neural network with just a single hidden layer containing a finite -number of neurons can approximate a continuous multidimensional -function to arbitrary accuracy, assuming the activation function for -the hidden layer is a \textbf{non-constant, bounded and -monotonically-increasing continuous function}. - -Note that the requirements on the activation function only applies to -the hidden layer, the output nodes are always assumed to be linear, so -as to not restrict the range of output values. - - -% !split -\subsection*{Mathematical model} - -The output $y$ is produced via the activation function $f$ -\[ - y = f\left(\sum_{i=1}^n w_ix_i + b_i\right) = f(z), -\] -This function receives $x_i$ as inputs. -Here the activation $z=(\sum_{i=1}^n w_ix_i+b_i)$. -In an FFNN of such neurons, the \emph{inputs} $x_i$ are the \emph{outputs} of -the neurons in the preceding layer. Furthermore, an MLP is -fully-connected, which means that each neuron receives a weighted sum -of the outputs of \emph{all} neurons in the previous layer. - -% !split -\subsection*{Mathematical model} - -First, for each node $i$ in the first hidden layer, we calculate a weighted sum $z_i^1$ of the input coordinates $x_j$, - -\begin{equation} z_i^1 = \sum_{j=1}^{M} w_{ij}^1 x_j + b_i^1 -\end{equation} - -Here $b_i$ is the so-called bias which is normally needed in -case of zero activation weights or inputs. How to fix the biases and -the weights will be discussed below. The value of $z_i^1$ is the -argument to the activation function $f_i$ of each node $i$, The -variable $M$ stands for all possible inputs to a given node $i$ in the -first layer. We define the output $y_i^1$ of all neurons in layer 1 as - -\begin{equation} - y_i^1 = f(z_i^1) = f\left(\sum_{j=1}^M w_{ij}^1 x_j + b_i^1\right) - \label{outputLayer1} -\end{equation} - -where we assume that all nodes in the same layer have identical -activation functions, hence the notation $f$. In general, we could assume in the more general case that different layers have different activation functions. -In this case we would identify these functions with a superscript $l$ for the $l$-th layer, - -\begin{equation} - y_i^l = f^l(u_i^l) = f^l\left(\sum_{j=1}^{N_{l-1}} w_{ij}^l y_j^{l-1} + b_i^l\right) - \label{generalLayer} -\end{equation} - -where $N_l$ is the number of nodes in layer $l$. When the output of -all the nodes in the first hidden layer are computed, the values of -the subsequent layer can be calculated and so forth until the output -is obtained. - - - -% !split -\subsection*{Mathematical model} - -The output of neuron $i$ in layer 2 is thus, - -\begin{align} - y_i^2 &= f^2\left(\sum_{j=1}^N w_{ij}^2 y_j^1 + b_i^2\right) \\ - &= f^2\left[\sum_{j=1}^N w_{ij}^2f^1\left(\sum_{k=1}^M w_{jk}^1 x_k + b_j^1\right) + b_i^2\right] - \label{outputLayer2} -\end{align} -where we have substituted $y_k^1$ with the inputs $x_k$. Finally, the ANN output reads - -\begin{align} - y_i^3 &= f^3\left(\sum_{j=1}^N w_{ij}^3 y_j^2 + b_i^3\right) \\ - &= f_3\left[\sum_{j} w_{ij}^3 f^2\left(\sum_{k} w_{jk}^2 f^1\left(\sum_{m} w_{km}^1 x_m + b_k^1\right) + b_j^2\right) - + b_1^3\right] -\end{align} - -% !split -\subsection*{Mathematical model} - -We can generalize this expression to an MLP with $l$ hidden -layers. The complete functional form is, - -\begin{align} -&y^{l+1}_i = f^{l+1}\left[\!\sum_{j=1}^{N_l} w_{ij}^3 f^l\left(\sum_{k=1}^{N_{l-1}}w_{jk}^{l-1}\left(\dots f^1\left(\sum_{n=1}^{N_0} w_{mn}^1 x_n+ b_m^1\right)\dots\right)+b_k^2\right)+b_1^3\right] && - \label{completeNN} -\end{align} - -which illustrates a basic property of MLPs: The only independent -variables are the input values $x_n$. - -% !split -\subsection*{Mathematical model} - -This confirms that an MLP, despite its quite convoluted mathematical -form, is nothing more than an analytic function, specifically a -mapping of real-valued vectors $\hat{x} \in \mathbb{R}^n \rightarrow -\hat{y} \in \mathbb{R}^m$. - -Furthermore, the flexibility and universality of an MLP can be -illustrated by realizing that the expression is essentially a nested -sum of scaled activation functions of the form - -\begin{equation} - f(x) = c_1 f(c_2 x + c_3) + c_4 -\end{equation} - -where the parameters $c_i$ are weights and biases. By adjusting these -parameters, the activation functions can be shifted up and down or -left and right, change slope or be rescaled which is the key to the -flexibility of a neural network. - -% !split -\paragraph{Matrix-vector notation.} -We can introduce a more convenient notation for the activations in an A NN. - -Additionally, we can represent the biases and activations -as layer-wise column vectors $\hat{b}_l$ and $\hat{y}_l$, so that the $i$-th element of each vector -is the bias $b_i^l$ and activation $y_i^l$ of node $i$ in layer $l$ respectively. - -We have that $\mathrm{W}_l$ is an $N_{l-1} \times N_l$ matrix, while $\hat{b}_l$ and $\hat{y}_l$ are $N_l \times 1$ column vectors. -With this notation, the sum becomes a matrix-vector multiplication, and we can write -the equation for the activations of hidden layer 2 (assuming three nodes for simplicity) as -\begin{equation} - \hat{y}_2 = f_2(\mathrm{W}_2 \hat{y}_{1} + \hat{b}_{2}) = - f_2\left(\left[\begin{array}{ccc} - w^2_{11} &w^2_{12} &w^2_{13} \\ - w^2_{21} &w^2_{22} &w^2_{23} \\ - w^2_{31} &w^2_{32} &w^2_{33} \\ - \end{array} \right] \cdot - \left[\begin{array}{c} - y^1_1 \\ - y^1_2 \\ - y^1_3 \\ - \end{array}\right] + - \left[\begin{array}{c} - b^2_1 \\ - b^2_2 \\ - b^2_3 \\ - \end{array}\right]\right). -\end{equation} - -% !split -\paragraph{Matrix-vector notation and activation.} -The activation of node $i$ in layer 2 is - -\begin{equation} - y^2_i = f_2\Bigr(w^2_{i1}y^1_1 + w^2_{i2}y^1_2 + w^2_{i3}y^1_3 + b^2_i\Bigr) = - f_2\left(\sum_{j=1}^3 w^2_{ij} y_j^1 + b^2_i\right). -\end{equation} - -This is not just a convenient and compact notation, but also a useful -and intuitive way to think about MLPs: The output is calculated by a -series of matrix-vector multiplications and vector additions that are -used as input to the activation functions. For each operation -$\mathrm{W}_l \hat{y}_{l-1}$ we move forward one layer. - - -% !split -\paragraph{Activation functions.} -A property that characterizes a neural network, other than its -connectivity, is the choice of activation function(s). As described -in, the following restrictions are imposed on an activation function -for a FFNN to fulfill the universal approximation theorem - -\begin{itemize} - \item Non-constant - - \item Bounded - - \item Monotonically-increasing - - \item Continuous -\end{itemize} - -\noindent -% !split -\paragraph{Activation functions, Logistic and Hyperbolic ones.} -The second requirement excludes all linear functions. Furthermore, in -a MLP with only linear activation functions, each layer simply -performs a linear transformation of its inputs. - -Regardless of the number of layers, the output of the NN will be -nothing but a linear function of the inputs. Thus we need to introduce -some kind of non-linearity to the NN to be able to fit non-linear -functions Typical examples are the logistic \emph{Sigmoid} - -\[ - f(x) = \frac{1}{1 + e^{-x}}, -\] -and the \emph{hyperbolic tangent} function -\[ - f(x) = \tanh(x) -\] - -% !split -\paragraph{Relevance.} -The \emph{sigmoid} function are more biologically plausible because the -output of inactive neurons are zero. Such activation function are -called \emph{one-sided}. However, it has been shown that the hyperbolic -tangent performs better than the sigmoid for training MLPs. has -become the most popular for \emph{deep neural networks} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -"""The sigmoid function (or the logistic curve) is a -function that takes any real number, z, and outputs a number (0,1). -It is useful in neural networks for assigning weights on a relative scale. -The value z is the weighted sum of parameters involved in the learning algorithm.""" - -import numpy -import matplotlib.pyplot as plt -import math as mt - -z = numpy.arange(-5, 5, .1) -sigma_fn = numpy.vectorize(lambda z: 1/(1+numpy.exp(-z))) -sigma = sigma_fn(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, sigma) -ax.set_ylim([-0.1, 1.1]) -ax.set_xlim([-5,5]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('sigmoid function') - -plt.show() - -"""Step Function""" -z = numpy.arange(-5, 5, .02) -step_fn = numpy.vectorize(lambda z: 1.0 if z >= 0.0 else 0.0) -step = step_fn(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, step) -ax.set_ylim([-0.5, 1.5]) -ax.set_xlim([-5,5]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('step function') - -plt.show() - -"""Sine Function""" -z = numpy.arange(-2*mt.pi, 2*mt.pi, 0.1) -t = numpy.sin(z) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, t) -ax.set_ylim([-1.0, 1.0]) -ax.set_xlim([-2*mt.pi,2*mt.pi]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('sine function') - -plt.show() - -"""Plots a graph of the squashing function used by a rectified linear -unit""" -z = numpy.arange(-2, 2, .1) -zero = numpy.zeros(len(z)) -y = numpy.max([zero, z], axis=0) - -fig = plt.figure() -ax = fig.add_subplot(111) -ax.plot(z, y) -ax.set_ylim([-2.0, 2.0]) -ax.set_xlim([-2.0, 2.0]) -ax.grid(True) -ax.set_xlabel('z') -ax.set_title('Rectified linear unit') - -plt.show() -\end{minted} - - -% !split -\subsection*{The multilayer perceptron (MLP)} - -The multilayer perceptron is a very popular, and easy to implement approach, to deep learning. It consists of -\begin{enumerate} -\item A neural network with one or more layers of nodes between the input and the output nodes. - -\item The multilayer network structure, or architecture, or topology, consists of an input layer, one or more hidden layers, and one output layer. - -\item The input nodes pass values to the first hidden layer, its nodes pass the information on to the second and so on till we reach the output layer. -\end{enumerate} - -\noindent -As a convention it is normal to call a network with one layer of input units, one layer of hidden -units and one layer of output units as a two-layer network. A network with two layers of hidden units is called a three-layer network etc etc. - -For an MLP network there is no direct connection between the output nodes/neurons/units and the input nodes/neurons/units. -Hereafter we will call the various entities of a layer for nodes. -There are also no connections within a single layer. - -The number of input nodes does not need to equal the number of output -nodes. This applies also to the hidden layers. Each layer may have its -own number of nodes and activation functions. - -The hidden layers have their name from the fact that they are not -linked to observables and as we will see below when we define the -so-called activation $\hat{z}$, we can think of this as a basis -expansion of the original inputs $\hat{x}$. The difference however -between neural networks and say linear regression is that now these -basis functions (which will correspond to the weights in the network) -are learned from data. This results in an important difference between -neural networks and deep learning approaches on one side and methods -like logistic regression or linear regression and their modifications on the other side. - - -% !split -\subsection*{From one to many layers, the universal approximation theorem} - - -A neural network with only one layer, what we called the simple -perceptron, is best suited if we have a standard binary model with -clear (linear) boundaries between the outcomes. As such it could -equally well be replaced by standard linear regression or logistic -regression. Networks with one or more hidden layers approximate -systems with more complex boundaries. - -As stated earlier, -an important theorem in studies of neural networks, restated without -proof here, is the \href{{http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.441.7873&rep=rep1&type=pdf}}{universal approximation -theorem}. - -It states that a feed-forward network with a single hidden layer -containing a finite number of neurons can approximate continuous -functions on compact subsets of real functions. The theorem thus -states that simple neural networks can represent a wide variety of -interesting functions when given appropriate parameters. It is the -multilayer feedforward architecture itself which gives neural networks -the potential of being universal approximators. - - -% !split -\subsection*{Deriving the back propagation code for a multilayer perceptron model} - - -\textbf{Note: figures will be inserted later!} - -As we have seen now in a feed forward network, we can express the final output of our network in terms of basic matrix-vector multiplications. -The unknowwn quantities are our weights $w_{ij}$ and we need to find an algorithm for changing them so that our errors are as small as possible. -This leads us to the famous \href{{https://www.nature.com/articles/323533a0}}{back propagation algorithm}. - -The questions we want to ask are how do changes in the biases and the -weights in our network change the cost function and how can we use the -final output to modify the weights? - -To derive these equations let us start with a plain regression problem -and define our cost function as - -\[ -{\cal C}(\hat{W}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2, -\] - -where the $t_i$s are our $n$ targets (the values we want to -reproduce), while the outputs of the network after having propagated -all inputs $\hat{x}$ are given by $y_i$. Below we will demonstrate -how the basic equations arising from the back propagation algorithm -can be modified in order to study classification problems with $K$ -classes. - -% !split -\subsection*{Definitions} - -With our definition of the targets $\hat{t}$, the outputs of the -network $\hat{y}$ and the inputs $\hat{x}$ we -define now the activation $z_j^l$ of node/neuron/unit $j$ of the -$l$-th layer as a function of the bias, the weights which add up from -the previous layer $l-1$ and the forward passes/outputs -$\hat{a}^{l-1}$ from the previous layer as - - -\[ -z_j^l = \sum_{i=1}^{M_{l-1}}w_{ij}^la_i^{l-1}+b_j^l, -\] - -where $b_k^l$ are the biases from layer $l$. Here $M_{l-1}$ -represents the total number of nodes/neurons/units of layer $l-1$. The -figure here illustrates this equation. We can rewrite this in a more -compact form as the matrix-vector products we discussed earlier, - -\[ -\hat{z}^l = \left(\hat{W}^l\right)^T\hat{a}^{l-1}+\hat{b}^l. -\] - -With the activation values $\hat{z}^l$ we can in turn define the -output of layer $l$ as $\hat{a}^l = f(\hat{z}^l)$ where $f$ is our -activation function. In the examples here we will use the sigmoid -function discussed in our logistic regression lectures. We will also use the same activation function $f$ for all layers -and their nodes. It means we have - -\[ -a_j^l = f(z_j^l) = \frac{1}{1+\exp{-(z_j^l)}}. -\] - - -% !split -\subsection*{Derivatives and the chain rule} - -From the definition of the activation $z_j^l$ we have -\[ -\frac{\partial z_j^l}{\partial w_{ij}^l} = a_i^{l-1}, -\] -and -\[ -\frac{\partial z_j^l}{\partial a_i^{l-1}} = w_{ji}^l. -\] - -With our definition of the activation function we have that (note that this function depends only on $z_j^l$) -\[ -\frac{\partial a_j^l}{\partial z_j^{l}} = a_j^l(1-a_j^l)=f(z_j^l)(1-f(z_j^l)). -\] - - -% !split -\subsection*{Derivative of the cost function} - -With these definitions we can now compute the derivative of the cost function in terms of the weights. - -Let us specialize to the output layer $l=L$. Our cost function is -\[ -{\cal C}(\hat{W^L}) = \frac{1}{2}\sum_{i=1}^n\left(y_i - t_i\right)^2=\frac{1}{2}\sum_{i=1}^n\left(a_i^L - t_i\right)^2, -\] -The derivative of this function with respect to the weights is - -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)\frac{\partial a_j^L}{\partial w_{jk}^{L}}, -\] -The last partial derivative can easily be computed and reads (by applying the chain rule) -\[ -\frac{\partial a_j^L}{\partial w_{jk}^{L}} = \frac{\partial a_j^L}{\partial z_{j}^{L}}\frac{\partial z_j^L}{\partial w_{jk}^{L}}=a_j^L(1-a_j^L)a_k^{L-1}, -\] - - - -% !split -\subsection*{Bringing it together, first back propagation equation} - -We have thus -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \left(a_j^L - t_j\right)a_j^L(1-a_j^L)a_k^{L-1}, -\] - -Defining -\[ -\delta_j^L = a_j^L(1-a_j^L)\left(a_j^L - t_j\right) = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\] -and using the Hadamard product of two vectors we can write this as -\[ -\hat{\delta}^L = f'(\hat{z}^L)\circ\frac{\partial {\cal C}}{\partial (\hat{a}L)}. -\] - -This is an important expression. The second term on the right handside -measures how fast the cost function is changing as a function of the $j$th -output activation. If, for example, the cost function doesn't depend -much on a particular output node $j$, then $\delta_j^L$ will be small, -which is what we would expect. The first term on the right, measures -how fast the activation function $f$ is changing at a given activation -value $z_j^L$. - -Notice that everything in the above equations is easily computed. In -particular, we compute $z_j^L$ while computing the behaviour of the -network, and it is only a small additional overhead to compute -$f'(z^L_j)$. The exact form of the derivative with respect to the -output depends on the form of the cost function. -However, provided the cost function is known there should be little -trouble in calculating - -\[ -\frac{\partial {\cal C}}{\partial (a_j^L)} -\] - -With the definition of $\delta_j^L$ we have a more compact definition of the derivative of the cost function in terms of the weights, namely -\[ -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}. -\] - -% !split -\subsection*{Derivatives in terms of $z_j^L$} - -It is also easy to see that our previous equation can be written as - -\[ -\delta_j^L =\frac{\partial {\cal C}}{\partial z_j^L}= \frac{\partial {\cal C}}{\partial a_j^L}\frac{\partial a_j^L}{\partial z_j^L}, -\] -which can also be interpreted as the partial derivative of the cost function with respect to the biases $b_j^L$, namely -\[ -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}\frac{\partial b_j^L}{\partial z_j^L}=\frac{\partial {\cal C}}{\partial b_j^L}, -\] -That is, the error $\delta_j^L$ is exactly equal to the rate of change of the cost function as a function of the bias. -% !split -\subsection*{Bringing it together} - -We have now three equations that are essential for the computations of the derivatives of the cost function at the output layer. These equations are needed to start the algorithm and they are - - -% --- begin paragraph admon --- -\paragraph{The starting equations.} - -\begin{equation} -\frac{\partial{\cal C}(\hat{W^L})}{\partial w_{jk}^L} = \delta_j^La_k^{L-1}, -\end{equation} -and -\begin{equation} -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}, -\end{equation} -and - -\begin{equation} -\delta_j^L = \frac{\partial {\cal C}}{\partial b_j^L}, -\end{equation} -% --- end paragraph admon --- - - - - -An interesting consequence of the above equations is that when the -activation $a_k^{L-1}$ is small, the gradient term, that is the -derivative of the cost function with respect to the weights, will also -tend to be small. We say then that the weight learns slowly, meaning -that it changes slowly when we minimize the weights via say gradient -descent. In this case we say the system learns slowly. - -Another interesting feature is that is when the activation function, -represented by the sigmoid function here, is rather flat when we move towards -its end values $0$ and $1$ (see the above Python codes). In these -cases, the derivatives of the activation function will also be close -to zero, meaning again that the gradients will be small and the -network learns slowly again. - - - -We need a fourth equation and we are set. We are going to propagate -backwards in order to the determine the weights and biases. In order -to do so we need to represent the error in the layer before the final -one $L-1$ in terms of the errors in the final output layer. - -% !split -\subsection*{Final back propagating equation} - -We have that (replacing $L$ with a general layer $l$) -\[ -\delta_j^l =\frac{\partial {\cal C}}{\partial z_j^l}. -\] -We want to express this in terms of the equations for layer $l+1$. Using the chain rule and summing over all $k$ entries we have - -\[ -\delta_j^l =\sum_k \frac{\partial {\cal C}}{\partial z_k^{l+1}}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}=\sum_k \delta_k^{l+1}\frac{\partial z_k^{l+1}}{\partial z_j^{l}}, -\] -and recalling that -\[ -z_j^{l+1} = \sum_{i=1}^{M_{l}}w_{ij}^{l+1}a_j^{l}+b_j^{l+1}, -\] -with $M_l$ being the number of nodes in layer $l$, we obtain -\[ -\delta_j^l =\sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l), -\] -This is our final equation. - -We are now ready to set up the algorithm for back propagation and learning the weights and biases. - -% !split -\subsection*{Setting up the Back propagation algorithm} - - - -The four equations provide us with a way of computing the gradient of the cost function. Let us write this out in the form of an algorithm. - - -% --- begin paragraph admon --- -\paragraph{} -First, we set up the input data $\hat{x}$ and the activations -$\hat{z}_1$ of the input layer and compute the activation function and -the pertinent outputs $\hat{a}^1$. -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Secondly, we perform then the feed forward till we reach the output -layer and compute all $\hat{z}_l$ of the input layer and compute the -activation function and the pertinent outputs $\hat{a}^l$ for -$l=2,3,\dots,L$. -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Thereafter we compute the ouput error $\hat{\delta}^L$ by computing all -\[ -\delta_j^L = f'(z_j^L)\frac{\partial {\cal C}}{\partial (a_j^L)}. -\] -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Then we compute the back propagate error for each $l=L-1,L-2,\dots,2$ as -\[ -\delta_j^l = \sum_k \delta_k^{l+1}w_{kj}^{l+1}f'(z_j^l). -\] -% --- end paragraph admon --- - - - - -% --- begin paragraph admon --- -\paragraph{} -Finally, we update the weights and the biases using gradient descent for each $l=L-1,L-2,\dots,2$ and update the weights and biases according to the rules -\[ -w_{jk}^l\leftarrow = w_{jk}^l- \eta \delta_j^la_k^{l-1}, -\] - -\[ -b_j^l \leftarrow b_j^l-\eta \frac{\partial {\cal C}}{\partial b_j^l}=b_j^l-\eta \delta_j^l, -\] -% --- end paragraph admon --- - - - -The parameter $\eta$ is the learning parameter discussed in connection with the gradient descent methods. -Here it is convenient to use stochastic gradient descent (see the examples below) with mini-batches with an outer loop that steps through multiple epochs of training. - - -% !split -\subsection*{Setting up a Multi-layer perceptron model for classification} - -We are now gong to develop an example based on the MNIST data -base. This is a classification problem and we need to use our -cross-entropy function we discussed in connection with logistic -regression. The cross-entropy defines our cost function for the -classificaton problems with neural networks. - -In binary classification with two classes $(0, 1)$ we define the -logistic/sigmoid function as the probability that a particular input -is in class $0$ or $1$. This is possible because the logistic -function takes any input from the real numbers and inputs a number -between 0 and 1, and can therefore be interpreted as a probability. It -also has other nice properties, such as a derivative that is simple to -calculate. - -For an input $\boldsymbol{a}$ from the hidden layer, the probability that the input $\boldsymbol{x}$ -is in class 0 or 1 is just. We let $\theta$ represent the unknown weights and biases to be adjusted by our equations). The variable $x$ -represents our activation values $z$. We have -\[ -P(y = 0 \mid \hat{x}, \hat{\theta}) = \frac{1}{1 + \exp{(- \hat{x}})} , -\] -and -\[ -P(y = 1 \mid \hat{x}, \hat{\theta}) = 1 - P(y = 0 \mid \hat{x}, \hat{\theta}) , -\] - -where $y \in \{0, 1\}$ and $\hat{\theta}$ represents the weights and biases -of our network. - - -% !split -\subsection*{Defining the cost function} - -Our cost function is given as (see the Logistic regression lectures) -\[ -\mathcal{C}(\hat{\theta}) = - \ln P(\mathcal{D} \mid \hat{\theta}) = - \sum_{i=1}^n -y_i \ln[P(y_i = 0)] + (1 - y_i) \ln [1 - P(y_i = 0)] = \sum_{i=1}^n \mathcal{L}_i(\hat{\theta}) . -\] - -This last equality means that we can interpret our \emph{cost} function as a sum over the \emph{loss} function -for each point in the dataset $\mathcal{L}_i(\hat{\theta})$. -The negative sign is just so that we can think about our algorithm as minimizing a positive number, rather -than maximizing a negative number. - -In \emph{multiclass} classification it is common to treat each integer label as a so called \emph{one-hot} vector: - -$y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$ and - - -$y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$ - - -i.e.~a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset (numbers from $0$ to $9$).. - -If $\hat{x}_i$ is the $i$-th input (image), $y_{ic}$ refers to the $c$-th component of the $i$-th -output vector $\hat{y}_i$. -The probability of $\hat{x}_i$ being in class $c$ will be given by the softmax function: - -\[ -P(y_{ic} = 1 \mid \hat{x}_i, \hat{\theta}) = \frac{\exp{((\hat{a}_i^{hidden})^T \hat{w}_c)}} -{\sum_{c'=0}^{C-1} \exp{((\hat{a}_i^{hidden})^T \hat{w}_{c'})}} , -\] - -which reduces to the logistic function in the binary case. -The likelihood of this $C$-class classifier -is now given as: - -\[ -P(\mathcal{D} \mid \hat{\theta}) = \prod_{i=1}^n \prod_{c=0}^{C-1} [P(y_{ic} = 1)]^{y_{ic}} . -\] -Again we take the negative log-likelihood to define our cost function: - -\[ -\mathcal{C}(\hat{\theta}) = - \log{P(\mathcal{D} \mid \hat{\theta})}. -\] -See the logistic regression lectures for a full definition of the cost function. - -The back propagation equations need now only a small change, namely the definition of a new cost function. We are thus ready to use the same equations as before! - -% !split -\subsection*{Example: binary classification problem} - -As an example of the above, relevant for project 2 as well, let us consider a binary class. As discussed in our logistic regression lectures, we defined a cost function in terms of the parameters $\beta$ as -\[ -\mathcal{C}(\hat{\beta}) = - \sum_{i=1}^n \left(y_i\log{p(y_i \vert x_i,\hat{\beta})}+(i-y_i)\log{1-p(y_i \vert x_i,\hat{\beta})}\right), -\] -where we had defined the logistic (sigmoid) function -\[ -p(y_i =1\vert x_i,\hat{\beta})=\frac{\exp{(\beta_0+\beta_1 x_i)}}{1+\exp{(\beta_0+\beta_1 x_i)}}, -\] -and -\[ -p(y_i =0\vert x_i,\hat{\beta})=1-p(y_i =1\vert x_i,\hat{\beta}). -\] -The parameters $\hat{\beta}$ were defined using a minimization method like gradient descent or Newton-Raphson's method. - -Now we replace $x_i$ with the activation $z_i^l$ for a given layer $l$ and the outputs as $y_i=a_i^l=f(z_i^l)$, with $z_i^l$ now being a function of the weights $w_{ij}^l$ and biases $b_i^l$. -We have then -\[ -a_i^l = y_i = \frac{\exp{(z_i^l)}}{1+\exp{(z_i^l)}}, -\] -with -\[ -z_i^l = \sum_{j}w_{ij}^l a_j^{l-1}+b_i^l, -\] -where the superscript $l-1$ indicates that these are the outputs from layer $l-1$. -Our cost function at the final layer $l=L$ is now -\[ -\mathcal{C}(\hat{W}) = - \sum_{i=1}^n \left(t_i\log{a_i^L}+(i-t_i)\log{(1-a_i^L)}\right), -\] -where we have defined the targets $t_i$. The derivatives of the cost function with respect to the output $a_i^L$ are then easily calculated and we get -\[ -\frac{\partial \mathcal{C}(\hat{W})}{\partial a_i^L} = \frac{a_i^L-t_i}{a_i^L(1-a_i^L)}. -\] -In case we use another activation function than the logistic one, we need to evaluate other derivatives. - - -% !split -\subsection*{The Softmax function} -In case we employ the more general case given by the Softmax equation, we need to evaluate the derivative of the activation function with respect to the activation $z_i^l$, that is we need -\[ -\frac{\partial f(z_i^l)}{\partial w_{jk}^l} = -\frac{\partial f(z_i^l)}{\partial z_j^l} \frac{\partial z_j^l}{\partial w_{jk}^l}= \frac{\partial f(z_i^l)}{\partial z_j^l}a_k^{l-1}. -\] -For the Softmax function we have -\[ -f(z_i^l) = \frac{\exp{(z_i^l)}}{\sum_{m=1}^K\exp{(z_m^l)}}. -\] -Its derivative with respect to $z_j^l$ gives -\[ -\frac{\partial f(z_i^l)}{\partial z_j^l}= f(z_i^l)\left(\delta_{ij}-f(z_j^l)\right), -\] -which in case of the simply binary model reduces to having $i=j$. - -% !split -\subsection*{Developing a code for doing neural networks with back propagation} - - -One can identify a set of key steps when using neural networks to solve supervised learning problems: - -\begin{enumerate} -\item Collect and pre-process data - -\item Define model and architecture - -\item Choose cost function and optimizer - -\item Train the model - -\item Evaluate model performance on test data - -\item Adjust hyperparameters (if necessary, network architecture) -\end{enumerate} - -\noindent -% !split -\subsection*{Collect and pre-process data} - -Here we will be using the MNIST dataset, which is readily available through the \textbf{scikit-learn} -package. You may also find it for example \href{{http://yann.lecun.com/exdb/mnist/}}{here}. -The \emph{MNIST} (Modified National Institute of Standards and Technology) database is a large database -of handwritten digits that is commonly used for training various image processing systems. -The MNIST dataset consists of 70 000 images of size $28\times 28$ pixels, each labeled from 0 to 9. -The scikit-learn dataset we will use consists of a selection of 1797 images of size $8\times 8$ collected and processed from this database. - -To feed data into a feed-forward neural network we need to represent -the inputs as a feature matrix $X = (n_{inputs}, n_{features})$. Each -row represents an \emph{input}, in this case a handwritten digit, and -each column represents a \emph{feature}, in this case a pixel. The -correct answers, also known as \emph{labels} or \emph{targets} are -represented as a 1D array of integers -$Y = (n_{inputs}) = (5, 3, 1, 8,...)$. - -As an example, say we want to build a neural network using supervised learning to predict Body-Mass Index (BMI) from -measurements of height (in m) -and weight (in kg). If we have measurements of 5 people the feature matrix could be for example: - -$$ X = \begin{bmatrix} -1.85 {\&} 81\\ -1.71 {\&} 65\\ -1.95 {\&} 103\\ -1.55 {\&} 42\\ -1.63 {\&} 56 -\end{bmatrix} ,$$ - -and the targets would be: - -$$ Y = (23.7, 22.2, 27.1, 17.5, 21.1) $$ - -Since each input image is a 2D matrix, we need to flatten the image -(i.e. "unravel" the 2D matrix into a 1D array) to turn the data into a -feature matrix. This means we lose all spatial information in the -image, such as locality and translational invariance. More complicated -architectures such as Convolutional Neural Networks can take advantage -of such information, and are most commonly applied when analyzing -images. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# flatten the image -# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64 -n_inputs = len(inputs) -inputs = inputs.reshape(n_inputs, -1) -print("X = (n_inputs, n_features) = " + str(inputs.shape)) - - -# choose some random images to display -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\end{minted} - -% !split -\subsection*{Train and test datasets} - -Performing analysis before partitioning the dataset is a major error, that can lead to incorrect conclusions. - -We will reserve $80 \%$ of our dataset for training and $20 \%$ for testing. - -It is important that the train and test datasets are drawn randomly from our dataset, to ensure -no bias in the sampling. -Say you are taking measurements of weather data to predict the weather in the coming 5 days. -You don't want to train your model on measurements taken from the hours 00.00 to 12.00, and then test it on data -collected from 12.00 to 24.00. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from sklearn.model_selection import train_test_split - -# one-liner from scikit-learn library -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) - -# equivalently in numpy -def train_test_split_numpy(inputs, labels, train_size, test_size): - n_inputs = len(inputs) - inputs_shuffled = inputs.copy() - labels_shuffled = labels.copy() - - np.random.shuffle(inputs_shuffled) - np.random.shuffle(labels_shuffled) - - train_end = int(n_inputs*train_size) - X_train, X_test = inputs_shuffled[:train_end], inputs_shuffled[train_end:] - Y_train, Y_test = labels_shuffled[:train_end], labels_shuffled[train_end:] - - return X_train, X_test, Y_train, Y_test - -#X_train, X_test, Y_train, Y_test = train_test_split_numpy(inputs, labels, train_size, test_size) - -print("Number of training images: " + str(len(X_train))) -print("Number of test images: " + str(len(X_test))) -\end{minted} - -% !split -\subsection*{Define model and architecture} - -Our simple feed-forward neural network will consist of an \emph{input} layer, a single \emph{hidden} layer and an \emph{output} layer. The activation $y$ of each neuron is a weighted sum of inputs, passed through an activation function. In case of the simple perceptron model we have - -$$ z = \sum_{i=1}^n w_i a_i ,$$ - -$$ y = f(z) ,$$ - -where $f$ is the activation function, $a_i$ represents input from neuron $i$ in the preceding layer -and $w_i$ is the weight to input $i$. -The activation of the neurons in the input layer is just the features (e.g.~a pixel value). - -The simplest activation function for a neuron is the \emph{Heaviside} function: - -$$ f(z) = -\begin{cases} -1, & z > 0\\ -0, & \text{otherwise} -\end{cases} -$$ - -A feed-forward neural network with this activation is known as a \emph{perceptron}. -For a binary classifier (i.e.~two classes, 0 or 1, dog or not-dog) we can also use this in our output layer. -This activation can be generalized to $k$ classes (using e.g.~the \emph{one-against-all} strategy), -and we call these architectures \emph{multiclass perceptrons}. - -However, it is now common to use the terms Single Layer Perceptron (SLP) (1 hidden layer) and -Multilayer Perceptron (MLP) (2 or more hidden layers) to refer to feed-forward neural networks with any activation function. - -Typical choices for activation functions include the sigmoid function, hyperbolic tangent, and Rectified Linear Unit (ReLU). -We will be using the sigmoid function $\sigma(x)$: - -$$ f(x) = \sigma(x) = \frac{1}{1 + e^{-x}} ,$$ - -which is inspired by probability theory (see logistic regression) and was most commonly used until about 2011. See the discussion below concerning other activation functions. - -% !split -\subsection*{Layers} - -\begin{itemize} -\item Input -\end{itemize} - -\noindent -Since each input image has 8x8 = 64 pixels or features, we have an input layer of 64 neurons. - -\begin{itemize} -\item Hidden layer -\end{itemize} - -\noindent -We will use 50 neurons in the hidden layer receiving input from the neurons in the input layer. -Since each neuron in the hidden layer is connected to the 64 inputs we have 64x50 = 3200 weights to the hidden layer. - -\begin{itemize} -\item Output -\end{itemize} - -\noindent -If we were building a binary classifier, it would be sufficient with a single neuron in the output layer, -which could output 0 or 1 according to the Heaviside function. This would be an example of a \emph{hard} classifier, meaning it outputs the class of the input directly. However, if we are dealing with noisy data it is often beneficial to use a \emph{soft} classifier, which outputs the probability of being in class 0 or 1. - -For a soft binary classifier, we could use a single neuron and interpret the output as either being the probability of being in class 0 or the probability of being in class 1. Alternatively we could use 2 neurons, and interpret each neuron as the probability of being in each class. - -Since we are doing multiclass classification, with 10 categories, it is natural to use 10 neurons in the output layer. We number the neurons $j = 0,1,...,9$. The activation of each output neuron $j$ will be according to the \emph{softmax} function: - -$$ P(\text{class $j$} \mid \text{input $\hat{a}$}) = \frac{\exp{(\hat{a}^T \hat{w}_j)}} -{\sum_{c=0}^{9} \exp{(\hat{a}^T \hat{w}_c)}} ,$$ - -i.e.~each neuron $j$ outputs the probability of being in class $j$ given an input from the hidden layer $\hat{a}$, with $\hat{w}_j$ the weights of neuron $j$ to the inputs. -The denominator is a normalization factor to ensure the outputs (probabilities) sum up to 1. -The exponent is just the weighted sum of inputs as before: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i+b_j.$$ - -Since each neuron in the output layer is connected to the 50 inputs from the hidden layer we have 50x10 = 500 -weights to the output layer. - -% !split -\subsection*{Weights and biases} - -Typically weights are initialized with small values distributed around zero, drawn from a uniform -or normal distribution. Setting all weights to zero means all neurons give the same output, making the network useless. - -Adding a bias value to the weighted sum of inputs allows the neural network to represent a greater range -of values. Without it, any input with the value 0 will be mapped to zero (before being passed through the activation). The bias unit has an output of 1, and a weight to each neuron $j$, $b_j$: - -$$ z_j = \sum_{i=1}^n w_ {ij} a_i + b_j.$$ - -The bias weights $\hat{b}$ are often initialized to zero, but a small value like $0.01$ ensures all neurons have some output which can be backpropagated in the first training cycle. -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# building our neural network - -n_inputs, n_features = X_train.shape -n_hidden_neurons = 50 -n_categories = 10 - -# we make the weights normally distributed using numpy.random.randn - -# weights and bias in the hidden layer -hidden_weights = np.random.randn(n_features, n_hidden_neurons) -hidden_bias = np.zeros(n_hidden_neurons) + 0.01 - -# weights and bias in the output layer -output_weights = np.random.randn(n_hidden_neurons, n_categories) -output_bias = np.zeros(n_categories) + 0.01 -\end{minted} - -% !split -\subsection*{Feed-forward pass} - -Denote $F$ the number of features, $H$ the number of hidden neurons and $C$ the number of categories. -For each input image we calculate a weighted sum of input features (pixel values) to each neuron $j$ in the hidden layer $l$: - -$$ z_{j}^{l} = \sum_{i=1}^{F} w_{ij}^{l} x_i + b_{j}^{l},$$ - -this is then passed through our activation function - -$$ a_{j}^{l} = f(z_{j}^{l}) .$$ - -We calculate a weighted sum of inputs (activations in the hidden layer) to each neuron $j$ in the output layer: - -$$ z_{j}^{L} = \sum_{i=1}^{H} w_{ij}^{L} a_{i}^{l} + b_{j}^{L}.$$ - -Finally we calculate the output of neuron $j$ in the output layer using the softmax function: - -$$ a_{j}^{L} = \frac{\exp{(z_j^{L})}} -{\sum_{c=0}^{C-1} \exp{(z_c^{L})}} .$$ - -% !split -\subsection*{Matrix multiplications} - -Since our data has the dimensions $X = (n_{inputs}, n_{features})$ and our weights to the hidden -layer have the dimensions -$W_{hidden} = (n_{features}, n_{hidden})$, -we can easily feed the network all our training data in one go by taking the matrix product - -$$ X W^{h} = (n_{inputs}, n_{hidden}),$$ - -and obtain a matrix that holds the weighted sum of inputs to the hidden layer -for each input image and each hidden neuron. -We also add the bias to obtain a matrix of weighted sums to the hidden layer $Z^{h}$: - -$$ \hat{z}^{l} = \hat{X} \hat{W}^{l} + \hat{b}^{l} ,$$ - -meaning the same bias (1D array with size equal number of hidden neurons) is added to each input image. -This is then passed through the activation: - -$$ \hat{a}^{l} = f(\hat{z}^l) .$$ - -This is fed to the output layer: - -$$ \hat{z}^{L} = \hat{a}^{L} \hat{W}^{L} + \hat{b}^{L} .$$ - -Finally we receive our output values for each image and each category by passing it through the softmax function: - -$$ output = softmax (\hat{z}^{L}) = (n_{inputs}, n_{categories}) .$$ - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# setup the feed-forward pass, subscript h = hidden layer - -def sigmoid(x): - return 1/(1 + np.exp(-x)) - -def feed_forward(X): - # weighted sum of inputs to the hidden layer - z_h = np.matmul(X, hidden_weights) + hidden_bias - # activation in the hidden layer - a_h = sigmoid(z_h) - - # weighted sum of inputs to the output layer - z_o = np.matmul(a_h, output_weights) + output_bias - # softmax output - # axis 0 holds each input and axis 1 the probabilities of each category - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - return probabilities - -probabilities = feed_forward(X_train) -print("probabilities = (n_inputs, n_categories) = " + str(probabilities.shape)) -print("probability that image 0 is in category 0,1,2,...,9 = \n" + str(probabilities[0])) -print("probabilities sum up to: " + str(probabilities[0].sum())) -print() - -# we obtain a prediction by taking the class with the highest likelihood -def predict(X): - probabilities = feed_forward(X) - return np.argmax(probabilities, axis=1) - -predictions = predict(X_train) -print("predictions = (n_inputs) = " + str(predictions.shape)) -print("prediction for image 0: " + str(predictions[0])) -print("correct label for image 0: " + str(Y_train[0])) -\end{minted} - -% !split -\subsection*{Choose cost function and optimizer} - -To measure how well our neural network is doing we need to introduce a cost function. -We will call the function that gives the error of a single sample output the \emph{loss} function, and the function -that gives the total error of our network across all samples the \emph{cost} function. -A typical choice for multiclass classification is the \emph{cross-entropy} loss, also known as the negative log likelihood. - -In \emph{multiclass} classification it is common to treat each integer label as a so called \emph{one-hot} vector: - -$$ y = 5 \quad \rightarrow \quad \hat{y} = (0, 0, 0, 0, 0, 1, 0, 0, 0, 0) ,$$ - - -$$ y = 1 \quad \rightarrow \quad \hat{y} = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) ,$$ - - -i.e.~a binary bit string of length $C$, where $C = 10$ is the number of classes in the MNIST dataset. - -Let $y_{ic}$ denote the $c$-th component of the $i$-th one-hot vector. -We define the cost function $\mathcal{C}$ as a sum over the cross-entropy loss for each point $\hat{x}_i$ in the dataset. - -In the one-hot representation only one of the terms in the loss function is non-zero, namely the -probability of the correct category $c'$ -(i.e.~the category $c'$ such that $y_{ic'} = 1$). This means that the cross entropy loss only punishes you for how wrong -you got the correct label. The probability of category $c$ is given by the softmax function. The vector $\hat{\theta}$ represents the parameters of our network, i.e.~all the weights and biases. - - -% !split -\subsection*{Optimizing the cost function} - -The network is trained by finding the weights and biases that minimize the cost function. One of the most widely used classes of methods is \emph{gradient descent} and its generalizations. The idea behind gradient descent -is simply to adjust the weights in the direction where the gradient of the cost function is large and negative. This ensures we flow toward a \emph{local} minimum of the cost function. -Each parameter $\theta$ is iteratively adjusted according to the rule - -$$ \theta_{i+1} = \theta_i - \eta \nabla \mathcal{C}(\theta_i) ,$$ - -where $\eta$ is known as the \emph{learning rate}, which controls how big a step we take towards the minimum. -This update can be repeated for any number of iterations, or until we are satisfied with the result. - -A simple and effective improvement is a variant called \emph{Batch Gradient Descent}. -Instead of calculating the gradient on the whole dataset, we calculate an approximation of the gradient -on a subset of the data called a \emph{minibatch}. -If there are $N$ data points and we have a minibatch size of $M$, the total number of batches -is $N/M$. -We denote each minibatch $B_k$, with $k = 1, 2,...,N/M$. The gradient then becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{M} \sum_{i \in B_k} \nabla \mathcal{L}_i(\theta) ,$$ - -i.e.~instead of averaging the loss over the entire dataset, we average over a minibatch. - -This has two important benefits: -\begin{enumerate} -\item Introducing stochasticity decreases the chance that the algorithm becomes stuck in a local minima. - -\item It significantly speeds up the calculation, since we do not have to use the entire dataset to calculate the gradient. -\end{enumerate} - -\noindent -The various optmization methods, with codes and algorithms, are discussed in our lectures on \href{{https://compphysics.github.io/MachineLearning/doc/pub/Splines/html/Splines-bs.html}}{Gradient descent approaches}. - -% !split -\subsection*{Regularization} - -It is common to add an extra term to the cost function, proportional -to the size of the weights. This is equivalent to constraining the -size of the weights, so that they do not grow out of control. -Constraining the size of the weights means that the weights cannot -grow arbitrarily large to fit the training data, and in this way -reduces \emph{overfitting}. - -We will measure the size of the weights using the so called \emph{L2-norm}, meaning our cost function becomes: - -$$ \nabla \mathcal{C}(\theta) = \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) \quad \rightarrow \quad -\frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}_i(\theta) + \lambda \lvert \lvert \hat{w} \rvert \rvert_2^2 -= \frac{1}{N} \sum_{i=1}^N \nabla \mathcal{L}(\theta) + \lambda \sum_{ij} w_{ij}^2,$$ - -i.e.~we sum up all the weights squared. The factor $\lambda$ is known as a regularization parameter. - - -In order to train the model, we need to calculate the derivative of -the cost function with respect to every bias and weight in the -network. In total our network has $(64 + 1)\times 50=3250$ weights in -the hidden layer and $(50 + 1)\times 10=510$ weights to the output -layer ($+1$ for the bias), and the gradient must be calculated for -every parameter. We use the \emph{backpropagation} algorithm discussed -above. This is a clever use of the chain rule that allows us to -calculate the gradient efficently. - - -% !split -\subsection*{Matrix multiplication} - -To more efficently train our network these equations are implemented using matrix operations. -The error in the output layer is calculated simply as, with $\hat{t}$ being our targets, - -$$ \delta_L = \hat{t} - \hat{y} = (n_{inputs}, n_{categories}) .$$ - -The gradient for the output weights is calculated as - -$$ \nabla W_{L} = \hat{a}^T \delta_L = (n_{hidden}, n_{categories}) ,$$ - -where $\hat{a} = (n_{inputs}, n_{hidden})$. This simply means that we are summing up the gradients for each input. -Since we are going backwards we have to transpose the activation matrix. - -The gradient with respect to the output bias is then - -$$ \nabla \hat{b}_{L} = \sum_{i=1}^{n_{inputs}} \delta_L = (n_{categories}) .$$ - -The error in the hidden layer is - -$$ \Delta_h = \delta_L W_{L}^T \circ f'(z_{h}) = \delta_L W_{L}^T \circ a_{h} \circ (1 - a_{h}) = (n_{inputs}, n_{hidden}) ,$$ - -where $f'(a_{h})$ is the derivative of the activation in the hidden layer. The matrix products mean -that we are summing up the products for each neuron in the output layer. The symbol $\circ$ denotes -the \emph{Hadamard product}, meaning element-wise multiplication. - -This again gives us the gradients in the hidden layer: - -$$ \nabla W_{h} = X^T \delta_h = (n_{features}, n_{hidden}) ,$$ - -$$ \nabla b_{h} = \sum_{i=1}^{n_{inputs}} \delta_h = (n_{hidden}) .$$ - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# to categorical turns our integer vector into a onehot representation -from sklearn.metrics import accuracy_score - -# one-hot in numpy -def to_categorical_numpy(integer_vector): - n_inputs = len(integer_vector) - n_categories = np.max(integer_vector) + 1 - onehot_vector = np.zeros((n_inputs, n_categories)) - onehot_vector[range(n_inputs), integer_vector] = 1 - - return onehot_vector - -#Y_train_onehot, Y_test_onehot = to_categorical(Y_train), to_categorical(Y_test) -Y_train_onehot, Y_test_onehot = to_categorical_numpy(Y_train), to_categorical_numpy(Y_test) - -def feed_forward_train(X): - # weighted sum of inputs to the hidden layer - z_h = np.matmul(X, hidden_weights) + hidden_bias - # activation in the hidden layer - a_h = sigmoid(z_h) - - # weighted sum of inputs to the output layer - z_o = np.matmul(a_h, output_weights) + output_bias - # softmax output - # axis 0 holds each input and axis 1 the probabilities of each category - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - # for backpropagation need activations in hidden and output layers - return a_h, probabilities - -def backpropagation(X, Y): - a_h, probabilities = feed_forward_train(X) - - # error in the output layer - error_output = probabilities - Y - # error in the hidden layer - error_hidden = np.matmul(error_output, output_weights.T) * a_h * (1 - a_h) - - # gradients for the output layer - output_weights_gradient = np.matmul(a_h.T, error_output) - output_bias_gradient = np.sum(error_output, axis=0) - - # gradient for the hidden layer - hidden_weights_gradient = np.matmul(X.T, error_hidden) - hidden_bias_gradient = np.sum(error_hidden, axis=0) - - return output_weights_gradient, output_bias_gradient, hidden_weights_gradient, hidden_bias_gradient - -print("Old accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train))) - -eta = 0.01 -lmbd = 0.01 -for i in range(1000): - # calculate gradients - dWo, dBo, dWh, dBh = backpropagation(X_train, Y_train_onehot) - - # regularization term gradients - dWo += lmbd * output_weights - dWh += lmbd * hidden_weights - - # update weights and biases - output_weights -= eta * dWo - output_bias -= eta * dBo - hidden_weights -= eta * dWh - hidden_bias -= eta * dBh - -print("New accuracy on training data: " + str(accuracy_score(predict(X_train), Y_train))) -\end{minted} - -% !split -\subsection*{Improving performance} - -As we can see the network does not seem to be learning at all. It seems to be just guessing the label for each image. -In order to obtain a network that does something useful, we will have to do a bit more work. - -The choice of \emph{hyperparameters} such as learning rate and regularization parameter is hugely influential for the performance of the network. Typically a \emph{grid-search} is performed, wherein we test different hyperparameters separated by orders of magnitude. For example we could test the learning rates $\eta = 10^{-6}, 10^{-5},...,10^{-1}$ with different regularization parameters $\lambda = 10^{-6},...,10^{-0}$. - -Next, we haven't implemented minibatching yet, which introduces stochasticity and is though to act as an important regularizer on the weights. We call a feed-forward + backward pass with a minibatch an \emph{iteration}, and a full training period -going through the entire dataset ($n/M$ batches) an \emph{epoch}. - -If this does not improve network performance, you may want to consider altering the network architecture, adding more neurons or hidden layers. -Andrew Ng goes through some of these considerations in this \href{{https://youtu.be/F1ka6a13S9I}}{video}. You can find a summary of the video \href{{https://kevinzakka.github.io/2016/09/26/applying-deep-learning/}}{here}. - -% !split -\subsection*{Full object-oriented implementation} - -It is very natural to think of the network as an object, with specific instances of the network -being realizations of this object with different hyperparameters. An implementation using Python classes provides a clean structure and interface, and the full implementation of our neural network is given below. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -class NeuralNetwork: - def __init__( - self, - X_data, - Y_data, - n_hidden_neurons=50, - n_categories=10, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - - ): - self.X_data_full = X_data - self.Y_data_full = Y_data - - self.n_inputs = X_data.shape[0] - self.n_features = X_data.shape[1] - self.n_hidden_neurons = n_hidden_neurons - self.n_categories = n_categories - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - self.create_biases_and_weights() - - def create_biases_and_weights(self): - self.hidden_weights = np.random.randn(self.n_features, self.n_hidden_neurons) - self.hidden_bias = np.zeros(self.n_hidden_neurons) + 0.01 - - self.output_weights = np.random.randn(self.n_hidden_neurons, self.n_categories) - self.output_bias = np.zeros(self.n_categories) + 0.01 - - def feed_forward(self): - # feed-forward for training - self.z_h = np.matmul(self.X_data, self.hidden_weights) + self.hidden_bias - self.a_h = sigmoid(self.z_h) - - self.z_o = np.matmul(self.a_h, self.output_weights) + self.output_bias - - exp_term = np.exp(self.z_o) - self.probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - - def feed_forward_out(self, X): - # feed-forward for output - z_h = np.matmul(X, self.hidden_weights) + self.hidden_bias - a_h = sigmoid(z_h) - - z_o = np.matmul(a_h, self.output_weights) + self.output_bias - - exp_term = np.exp(z_o) - probabilities = exp_term / np.sum(exp_term, axis=1, keepdims=True) - return probabilities - - def backpropagation(self): - error_output = self.probabilities - self.Y_data - error_hidden = np.matmul(error_output, self.output_weights.T) * self.a_h * (1 - self.a_h) - - self.output_weights_gradient = np.matmul(self.a_h.T, error_output) - self.output_bias_gradient = np.sum(error_output, axis=0) - - self.hidden_weights_gradient = np.matmul(self.X_data.T, error_hidden) - self.hidden_bias_gradient = np.sum(error_hidden, axis=0) - - if self.lmbd > 0.0: - self.output_weights_gradient += self.lmbd * self.output_weights - self.hidden_weights_gradient += self.lmbd * self.hidden_weights - - self.output_weights -= self.eta * self.output_weights_gradient - self.output_bias -= self.eta * self.output_bias_gradient - self.hidden_weights -= self.eta * self.hidden_weights_gradient - self.hidden_bias -= self.eta * self.hidden_bias_gradient - - def predict(self, X): - probabilities = self.feed_forward_out(X) - return np.argmax(probabilities, axis=1) - - def predict_probabilities(self, X): - probabilities = self.feed_forward_out(X) - return probabilities - - def train(self): - data_indices = np.arange(self.n_inputs) - - for i in range(self.epochs): - for j in range(self.iterations): - # pick datapoints with replacement - chosen_datapoints = np.random.choice( - data_indices, size=self.batch_size, replace=False - ) - - # minibatch training data - self.X_data = self.X_data_full[chosen_datapoints] - self.Y_data = self.Y_data_full[chosen_datapoints] - - self.feed_forward() - self.backpropagation() -\end{minted} - -% !split -\subsection*{Evaluate model performance on test data} - -To measure the performance of our network we evaluate how well it does it data it has never seen before, i.e.~the test data. -We measure the performance of the network using the \emph{accuracy} score. -The accuracy is as you would expect just the number of images correctly labeled divided by the total number of images. A perfect classifier will have an accuracy score of $1$. - -$$ \text{Accuracy} = \frac{\sum_{i=1}^n I(\hat{y}_i = y_i)}{n} ,$$ - -where $I$ is the indicator function, $1$ if $\hat{y}_i = y_i$ and $0$ otherwise. - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -epochs = 100 -batch_size = 100 - -dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size, - n_hidden_neurons=n_hidden_neurons, n_categories=n_categories) -dnn.train() -test_predict = dnn.predict(X_test) - -# accuracy score from scikit library -print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict)) - -# equivalent in numpy -def accuracy_score_numpy(Y_test, Y_pred): - return np.sum(Y_test == Y_pred) / len(Y_test) - -#print("Accuracy score on test set: ", accuracy_score_numpy(Y_test, test_predict)) -\end{minted} - -% !split -\subsection*{Adjust hyperparameters} - -We now perform a grid search to find the optimal hyperparameters for the network. -Note that we are only using 1 layer with 50 neurons, and human performance is estimated to be around $98\%$ ($2\%$ error rate). - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -# store the models for later use -DNN_numpy = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -# grid search -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - dnn = NeuralNetwork(X_train, Y_train_onehot, eta=eta, lmbd=lmbd, epochs=epochs, batch_size=batch_size, - n_hidden_neurons=n_hidden_neurons, n_categories=n_categories) - dnn.train() - - DNN_numpy[i][j] = dnn - - test_predict = dnn.predict(X_test) - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Accuracy score on test set: ", accuracy_score(Y_test, test_predict)) - print() -\end{minted} - -% !split -\subsection*{Visualization} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# visual representation of grid search -# uses seaborn heatmap, you can also do this with matplotlib imshow -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - dnn = DNN_numpy[i][j] - - train_pred = dnn.predict(X_train) - test_pred = dnn.predict(X_test) - - train_accuracy[i][j] = accuracy_score(Y_train, train_pred) - test_accuracy[i][j] = accuracy_score(Y_test, test_pred) - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - -% !split -\subsection*{scikit-learn implementation} - -\textbf{scikit-learn} focuses more -on traditional machine learning methods, such as regression, -clustering, decision trees, etc. As such, it has only two types of -neural networks: Multi Layer Perceptron outputting continuous values, -\emph{MPLRegressor}, and Multi Layer Perceptron outputting labels, -\emph{MLPClassifier}. We will see how simple it is to use these classes. - -\textbf{scikit-learn} implements a few improvements from our neural network, -such as early stopping, a varying learning rate, different -optimization methods, etc. We would therefore expect a better -performance overall. - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from sklearn.neural_network import MLPClassifier -# store models for later use -DNN_scikit = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - dnn = MLPClassifier(hidden_layer_sizes=(n_hidden_neurons), activation='logistic', - alpha=lmbd, learning_rate_init=eta, max_iter=epochs) - dnn.fit(X_train, Y_train) - - DNN_scikit[i][j] = dnn - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Accuracy score on test set: ", dnn.score(X_test, Y_test)) - print() -\end{minted} - - -% !split -\subsection*{Visualization} -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - dnn = DNN_scikit[i][j] - - train_pred = dnn.predict(X_train) - test_pred = dnn.predict(X_test) - - train_accuracy[i][j] = accuracy_score(Y_train, train_pred) - test_accuracy[i][j] = accuracy_score(Y_test, test_pred) - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - - -% !split -\subsection*{Building neural networks in Tensorflow and Keras} - -Now we want to build on the experience gained from our neural network implementation in NumPy and scikit-learn -and use it to construct a neural network in Tensorflow. Once we have constructed a neural network in NumPy -and Tensorflow, building one in Keras is really quite trivial, though the performance may suffer. - -In our previous example we used only one hidden layer, and in this we will use two. From this it should be quite -clear how to build one using an arbitrary number of hidden layers, using data structures such as Python lists or -NumPy arrays. - -% !split -\subsection*{Tensorflow} - -Tensorflow is an open source library machine learning library -developed by the Google Brain team for internal use. It was released -under the Apache 2.0 open source license in November 9, 2015. - -Tensorflow is a computational framework that allows you to construct -machine learning models at different levels of abstraction, from -high-level, object-oriented APIs like Keras, down to the C++ kernels -that Tensorflow is built upon. The higher levels of abstraction are -simpler to use, but less flexible, and our choice of implementation -should reflect the problems we are trying to solve. - -\href{{https://www.tensorflow.org/guide/graphs}}{Tensorflow uses} so-called graphs to represent your computation -in terms of the dependencies between individual operations, such that you first build a Tensorflow \emph{graph} -to represent your model, and then create a Tensorflow \emph{session} to run the graph. - -In this guide we will analyze the same data as we did in our NumPy and -scikit-learn tutorial, gathered from the MNIST database of images. We -will give an introduction to the lower level Python Application -Program Interfaces (APIs), and see how we use them to build our graph. -Then we will build (effectively) the same graph in Keras, to see just -how simple solving a machine learning problem can be. - -To install tensorflow on Unix/Linux systems, use pip as -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -pip3 install tensorflow -\end{minted} -and/or if you use \textbf{anaconda}, just write (or install from the graphical user interface) -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -conda install tensorflow -\end{minted} - -% !split -\subsection*{Collect and pre-process data} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -print("inputs = (n_inputs, pixel_width, pixel_height) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# flatten the image -# the value -1 means dimension is inferred from the remaining dimensions: 8x8 = 64 -n_inputs = len(inputs) -inputs = inputs.reshape(n_inputs, -1) -print("X = (n_inputs, n_features) = " + str(inputs.shape)) - - -# choose some random images to display -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.utils import to_categorical -from sklearn.model_selection import train_test_split - -# one-hot representation of labels -labels = to_categorical(labels) - -# split into train and test data -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) -\end{minted} - -% !split -\subsection*{Using TensorFlow backend} - -\begin{enumerate} -\item Define model and architecture - -\item Choose cost function and optimizer -\end{enumerate} - -\noindent -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -import tensorflow as tf - -class NeuralNetworkTensorflow: - def __init__( - self, - X_train, - Y_train, - X_test, - Y_test, - n_neurons_layer1=100, - n_neurons_layer2=50, - n_categories=2, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - ): - - # keep track of number of steps - self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step') - - self.X_train = X_train - self.Y_train = Y_train - self.X_test = X_test - self.Y_test = Y_test - - self.n_inputs = X_train.shape[0] - self.n_features = X_train.shape[1] - self.n_neurons_layer1 = n_neurons_layer1 - self.n_neurons_layer2 = n_neurons_layer2 - self.n_categories = n_categories - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - # build network piece by piece - # name scopes (with) are used to enforce creation of new variables - # https://www.tensorflow.org/guide/variables - self.create_placeholders() - self.create_DNN() - self.create_loss() - self.create_optimiser() - self.create_accuracy() - - def create_placeholders(self): - # placeholders are fine here, but "Datasets" are the preferred method - # of streaming data into a model - with tf.name_scope('data'): - self.X = tf.placeholder(tf.float32, shape=(None, self.n_features), name='X_data') - self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data') - - def create_DNN(self): - with tf.name_scope('DNN'): - # the weights are stored to calculate regularization loss later - - # Fully connected layer 1 - self.W_fc1 = self.weight_variable([self.n_features, self.n_neurons_layer1], name='fc1', dtype=tf.float32) - b_fc1 = self.bias_variable([self.n_neurons_layer1], name='fc1', dtype=tf.float32) - a_fc1 = tf.nn.sigmoid(tf.matmul(self.X, self.W_fc1) + b_fc1) - - # Fully connected layer 2 - self.W_fc2 = self.weight_variable([self.n_neurons_layer1, self.n_neurons_layer2], name='fc2', dtype=tf.float32) - b_fc2 = self.bias_variable([self.n_neurons_layer2], name='fc2', dtype=tf.float32) - a_fc2 = tf.nn.sigmoid(tf.matmul(a_fc1, self.W_fc2) + b_fc2) - - # Output layer - self.W_out = self.weight_variable([self.n_neurons_layer2, self.n_categories], name='out', dtype=tf.float32) - b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32) - self.z_out = tf.matmul(a_fc2, self.W_out) + b_out - - def create_loss(self): - with tf.name_scope('loss'): - softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out)) - - regularizer_loss_fc1 = tf.nn.l2_loss(self.W_fc1) - regularizer_loss_fc2 = tf.nn.l2_loss(self.W_fc2) - regularizer_loss_out = tf.nn.l2_loss(self.W_out) - regularizer_loss = self.lmbd*(regularizer_loss_fc1 + regularizer_loss_fc2 + regularizer_loss_out) - - self.loss = softmax_loss + regularizer_loss - - def create_accuracy(self): - with tf.name_scope('accuracy'): - probabilities = tf.nn.softmax(self.z_out) - predictions = tf.argmax(probabilities, axis=1) - labels = tf.argmax(self.Y, axis=1) - - correct_predictions = tf.equal(predictions, labels) - correct_predictions = tf.cast(correct_predictions, tf.float32) - self.accuracy = tf.reduce_mean(correct_predictions) - - def create_optimiser(self): - with tf.name_scope('optimizer'): - self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step) - - def weight_variable(self, shape, name='', dtype=tf.float32): - initial = tf.truncated_normal(shape, stddev=0.1) - return tf.Variable(initial, name=name, dtype=dtype) - - def bias_variable(self, shape, name='', dtype=tf.float32): - initial = tf.constant(0.1, shape=shape) - return tf.Variable(initial, name=name, dtype=dtype) - - def fit(self): - data_indices = np.arange(self.n_inputs) - - with tf.Session() as sess: - sess.run(tf.global_variables_initializer()) - for i in range(self.epochs): - for j in range(self.iterations): - chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False) - batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints] - - sess.run([DNN.loss, DNN.optimizer], - feed_dict={DNN.X: batch_X, - DNN.Y: batch_Y}) - accuracy = sess.run(DNN.accuracy, - feed_dict={DNN.X: batch_X, - DNN.Y: batch_Y}) - step = sess.run(DNN.global_step) - - self.train_loss, self.train_accuracy = sess.run([DNN.loss, DNN.accuracy], - feed_dict={DNN.X: self.X_train, - DNN.Y: self.Y_train}) - - self.test_loss, self.test_accuracy = sess.run([DNN.loss, DNN.accuracy], - feed_dict={DNN.X: self.X_test, - DNN.Y: self.Y_test}) -\end{minted} - - -% !split -\subsection*{Optimizing and using gradient descent} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -epochs = 100 -batch_size = 100 -n_neurons_layer1 = 100 -n_neurons_layer2 = 50 -n_categories = 10 -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -\end{minted} - - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -DNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - DNN = NeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test, - n_neurons_layer1, n_neurons_layer2, n_categories, - epochs=epochs, batch_size=batch_size, eta=eta, lmbd=lmbd) - DNN.fit() - - DNN_tf[i][j] = DNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % DNN.test_accuracy) - print() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - DNN = DNN_tf[i][j] - - train_accuracy[i][j] = DNN.train_accuracy - test_accuracy[i][j] = DNN.test_accuracy - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# we can use log files to visualize our graph in Tensorboard -writer = tf.summary.FileWriter('logs/') -writer.add_graph(tf.get_default_graph()) -\end{minted} - - -% !split -\subsection*{Using Keras} - -Keras is a high level \href{{https://en.wikipedia.org/wiki/Application_programming_interface}}{neural network} -that supports Tensorflow, CTNK and Theano as backends. -If you have Tensorflow installed Keras is available through the \emph{tf.keras} module. -If you have Anaconda installed you may run the following command -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -conda install keras -\end{minted} - -Alternatively, if you have Tensorflow or one of the other supported backends install you may use the pip package manager: - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -pip3 install keras -\end{minted} -or look up the \href{{https://keras.io/}}{instructions here}. - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.models import Sequential -from keras.layers import Dense -from keras.regularizers import l2 -from keras.optimizers import SGD - -def create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, eta, lmbd): - model = Sequential() - model.add(Dense(n_neurons_layer1, activation='sigmoid', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_neurons_layer2, activation='sigmoid', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_categories, activation='softmax')) - - sgd = SGD(lr=eta) - model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) - - return model -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -DNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - DNN = create_neural_network_keras(n_neurons_layer1, n_neurons_layer2, n_categories, - eta=eta, lmbd=lmbd) - DNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0) - scores = DNN.evaluate(X_test, Y_test) - - DNN_keras[i][j] = DNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % scores[1]) - print() -\end{minted} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# optional -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - DNN = DNN_keras[i][j] - - train_accuracy[i][j] = DNN.evaluate(X_train, Y_train)[1] - test_accuracy[i][j] = DNN.evaluate(X_test, Y_test)[1] - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - - - - -% !split -\subsection*{Which activation function should I use?} - -The Back propagation algorithm we derived above works by going from -the output layer to the input layer, propagating the error gradient on -the way. Once the algorithm has computed the gradient of the cost -function with regards to each parameter in the network, it uses these -gradients to update each parameter with a Gradient Descent (GD) step. - - -Unfortunately for us, the gradients often get smaller and smaller as the -algorithm progresses down to the first hidden layers. As a result, the -GD update leaves the lower layer connection weights -virtually unchanged, and training never converges to a good -solution. This is known in the literature as -\textbf{the vanishing gradients problem}. - -In other cases, the opposite can happen, namely the the gradients can grow bigger and -bigger. The result is that many of the layers get large updates of the -weights the -algorithm diverges. This is the \textbf{exploding gradients problem}, which is -mostly encountered in recurrent neural networks. More generally, deep -neural networks suffer from unstable gradients, different layers may -learn at widely different speeds - -% !split -\subsection*{Is the Logistic activation function (Sigmoid) our choice?} - -Although this unfortunate behavior has been empirically observed for -quite a while (it was one of the reasons why deep neural networks were -mostly abandoned for a long time), it is only around 2010 that -significant progress was made in understanding it. - -A paper titled \href{{http://proceedings.mlr.press/v9/glorot10a.html}}{Understanding the Difficulty of Training Deep -Feedforward Neural Networks by Xavier Glorot and Yoshua Bengio} found that -the problems with the popular logistic -sigmoid activation function and the weight initialization technique -that was most popular at the time, namely random initialization using -a normal distribution with a mean of 0 and a standard deviation of -1. - -They showed that with this activation function and this -initialization scheme, the variance of the outputs of each layer is -much greater than the variance of its inputs. Going forward in the -network, the variance keeps increasing after each layer until the -activation function saturates at the top layers. This is actually made -worse by the fact that the logistic function has a mean of 0.5, not 0 -(the hyperbolic tangent function has a mean of 0 and behaves slightly -better than the logistic function in deep networks). - - -% !split -\subsection*{The derivative of the Logistic funtion} - -Looking at the logistic activation function, when inputs become large -(negative or positive), the function saturates at 0 or 1, with a -derivative extremely close to 0. Thus when backpropagation kicks in, -it has virtually no gradient to propagate back through the network, -and what little gradient exists keeps getting diluted as -backpropagation progresses down through the top layers, so there is -really nothing left for the lower layers. - -In their paper, Glorot and Bengio propose a way to significantly -alleviate this problem. We need the signal to flow properly in both -directions: in the forward direction when making predictions, and in -the reverse direction when backpropagating gradients. We don’t want -the signal to die out, nor do we want it to explode and saturate. For -the signal to flow properly, the authors argue that we need the -variance of the outputs of each layer to be equal to the variance of -its inputs, and we also need the gradients to have equal variance -before and after flowing through a layer in the reverse direction. - - - -One of the insights in the 2010 paper by Glorot and Bengio was that -the vanishing/exploding gradients problems were in part due to a poor -choice of activation function. Until then most people had assumed that -if Nature had chosen to use roughly sigmoid activation functions in -biological neurons, they must be an excellent choice. But it turns out -that other activation functions behave much better in deep neural -networks, in particular the ReLU activation function, mostly because -it does not saturate for positive values (and also because it is quite -fast to compute). - - -% !split -\subsection*{The RELU function family} - -The ReLU activation function suffers from a problem known as the dying -ReLUs: during training, some neurons effectively die, meaning they -stop outputting anything other than 0. - -In some cases, you may find that half of your network’s neurons are -dead, especially if you used a large learning rate. During training, -if a neuron’s weights get updated such that the weighted sum of the -neuron’s inputs is negative, it will start outputting 0. When this -happen, the neuron is unlikely to come back to life since the gradient -of the ReLU function is 0 when its input is negative. - -To solve this problem, nowadays practitioners use a variant of the ReLU -function, such as the leaky ReLU discussed above or the so-called -exponential linear unit (ELU) function - - -\[ -ELU(z) = \left\{\begin{array}{cc} \alpha\left( \exp{(z)}-1\right) & z < 0,\\ z & z \ge 0.\end{array}\right. -\] - -% !split -\subsection*{Which activation function should we use?} - -In general it seems that the ELU activation function is better than -the leaky ReLU function (and its variants), which is better than -ReLU. ReLU performs better than $\tanh$ which in turn performs better -than the logistic function. - -If runtime -performance is an issue, then you may opt for the leaky ReLU function over the -ELU function If you don’t -want to tweak yet another hyperparameter, you may just use the default -$\alpha$ of $0.01$ for the leaky ReLU, and $1$ for ELU. If you have -spare time and computing power, you can use cross-validation or -bootstrap to evaluate other activation functions. - - -% !split -\subsection*{A top-down perspective on Neural networks} - - -The first thing we would like to do is divide the data into two or three -parts. A training set, a validation or dev (development) set, and a -test set. The test set is the data on which we want to make -predictions. The dev set is a subset of the training data we use to -check how well we are doing out-of-sample, after training the model on -the training dataset. We use the validation error as a proxy for the -test error in order to make tweaks to our model. It is crucial that we -do not use any of the test data to train the algorithm. This is a -cardinal sin in ML. Then: - - -\begin{itemize} -\item Estimate optimal error rate - -\item Minimize underfitting (bias) on training data set. - -\item Make sure you are not overfitting. -\end{itemize} - -\noindent -If the validation and test sets are drawn from the same distributions, -then a good performance on the validation set should lead to similarly -good performance on the test set. - -However, sometimes -the training data and test data differ in subtle ways because, for -example, they are collected using slightly different methods, or -because it is cheaper to collect data in one way versus another. In -this case, there can be a mismatch between the training and test -data. This can lead to the neural network overfitting these small -differences between the test and training sets, and a poor performance -on the test set despite having a good performance on the validation -set. To rectify this, Andrew Ng suggests making two validation or dev -sets, one constructed from the training data and one constructed from -the test data. The difference between the performance of the algorithm -on these two validation sets quantifies the train-test mismatch. This -can serve as another important diagnostic when using DNNs for -supervised learning. - -% !split -\subsection*{Limitations of supervised learning with deep networks} - -Like all statistical methods, supervised learning using neural -networks has important limitations. This is especially important when -one seeks to apply these methods, especially to physics problems. Like -all tools, DNNs are not a universal solution. Often, the same or -better performance on a task can be achieved by using a few -hand-engineered features (or even a collection of random -features). - -Here we list some of the important limitations of supervised neural network based models. - - - -\begin{itemize} -\item \textbf{Need labeled data}. All supervised learning methods, DNNs for supervised learning require labeled data. Often, labeled data is harder to acquire than unlabeled data (e.g.~one must pay for human experts to label images). - -\item \textbf{Supervised neural networks are extremely data intensive.} DNNs are data hungry. They perform best when data is plentiful. This is doubly so for supervised methods where the data must also be labeled. The utility of DNNs is extremely limited if data is hard to acquire or the datasets are small (hundreds to a few thousand samples). In this case, the performance of other methods that utilize hand-engineered features can exceed that of DNNs. - -\item \textbf{Homogeneous data.} Almost all DNNs deal with homogeneous data of one type. It is very hard to design architectures that mix and match data types (i.e.~some continuous variables, some discrete variables, some time series). In applications beyond images, video, and language, this is often what is required. In contrast, ensemble models like random forests or gradient-boosted trees have no difficulty handling mixed data types. - -\item \textbf{Many problems are not about prediction.} In natural science we are often interested in learning something about the underlying distribution that generates the data. In this case, it is often difficult to cast these ideas in a supervised learning setting. While the problems are related, it is possible to make good predictions with a \emph{wrong} model. The model might or might not be useful for understanding the underlying science. -\end{itemize} - -\noindent -Some of these remarks are particular to DNNs, others are shared by all supervised learning methods. This motivates the use of unsupervised methods which in part circumvent these problems. - - -% !split -\subsection*{Convolutional Neural Networks (recognizing images)} - -Convolutional Neural Networks (CNN) are very similar to ordinary Neural Networks. - -They are made up of neurons that have learnable weights and -biases. Each neuron receives some inputs, performs a dot product and -optionally follows it with a non-linearity. The whole network still -expresses a single differentiable score function: from the raw image -pixels on one end to class scores at the other. And they still have a -loss function (for example Softmax) on the last (fully-connected) layer -and all the tips/tricks we developed for learning regular Neural -Networks still apply (back propagation, gradient descent etc etc). - -What is the difference? \textbf{CNN architectures make the explicit assumption that -the inputs are images, which allows us to encode certain properties -into the architecture. These then make the forward function more -efficient to implement and vastly reduce the amount of parameters in -the network.} - -Here we provide only a superficial overview, for the more interested, we recommend highly the course -\href{{https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html}}{IN5400 – Machine Learning for Image Analysis} -and the slides of \href{{http://cs231n.github.io/convolutional-networks/}}{CS231}. - -% !split -\subsection*{Regular NNs don’t scale well to full images} - -As an example, consider -an image of size $32\times 32\times 3$ (32 wide, 32 high, 3 color channels), so a -single fully-connected neuron in a first hidden layer of a regular -Neural Network would have $32\times 32\times 3 = 3072$ weights. This amount still -seems manageable, but clearly this fully-connected structure does not -scale to larger images. For example, an image of more respectable -size, say $200\times 200\times 3$, would lead to neurons that have -$200\times 200\times 3 = 120,000$ weights. - -We could have -several such neurons, and the parameters would add up quickly! Clearly, -this full connectivity is wasteful and the huge number of parameters -would quickly lead to possible overfitting. - - -\begin{figure}[!ht] % - \centerline{\includegraphics[width=0.6\linewidth]{figslides/nn.jpeg}} - \caption{ - A regular 3-layer Neural Network. - } -\end{figure} -%\clearpage % flush figures - - -% !split -\subsection*{3D volumes of neurons} - -Convolutional Neural Networks take advantage of the fact that the -input consists of images and they constrain the architecture in a more -sensible way. - -In particular, unlike a regular Neural Network, the -layers of a CNN have neurons arranged in 3 dimensions: width, -height, depth. (Note that the word depth here refers to the third -dimension of an activation volume, not to the depth of a full Neural -Network, which can refer to the total number of layers in a network.) - -To understand it better, the above example of an image -with an input volume of -activations has dimensions $32\times 32\times 3$ (width, height, -depth respectively). - -The neurons in a layer will -only be connected to a small region of the layer before it, instead of -all of the neurons in a fully-connected manner. Moreover, the final -output layer could for this specific image have dimensions $1\times 1 times 10$, -because by the -end of the CNN architecture we will reduce the full image into a -single vector of class scores, arranged along the depth -dimension. - - -\begin{figure}[!ht] % - \centerline{\includegraphics[width=0.6\linewidth]{figslides/cnn.jpeg}} - \caption{ - A CNN arranges its neurons in three dimensions (width, height, depth), as visualized in one of the layers. Every layer of a CNN transforms the 3D input volume to a 3D output volume of neuron activations. In this example, the red input layer holds the image, so its width and height would be the dimensions of the image, and the depth would be 3 (Red, Green, Blue channels). - } -\end{figure} -%\clearpage % flush figures - - - - -% !split -\subsection*{Layers used to build CNNs} - - -A simple CNN is a sequence of layers, and every layer of a CNN -transforms one volume of activations to another through a -differentiable function. We use three main types of layers to build -CNN architectures: Convolutional Layer, Pooling Layer, and -Fully-Connected Layer (exactly as seen in regular Neural Networks). We -will stack these layers to form a full CNN architecture. - -A simple CNN for image classification could have the architecture: - -\begin{itemize} -\item \textbf{INPUT} ($32\times 32 \times 3$) will hold the raw pixel values of the image, in this case an image of width 32, height 32, and with three color channels R,G,B. - -\item \textbf{CONV} (convolutional )layer will compute the output of neurons that are connected to local regions in the input, each computing a dot product between their weights and a small region they are connected to in the input volume. This may result in volume such as $[32\times 32\times 12]$ if we decided to use 12 filters. - -\item \textbf{RELU} layer will apply an elementwise activation function, such as the $max(0,x)$ thresholding at zero. This leaves the size of the volume unchanged ($[32\times 32\times 12]$). - -\item \textbf{POOL} (pooling) layer will perform a downsampling operation along the spatial dimensions (width, height), resulting in volume such as $[16\times 16\times 12]$. - -\item \textbf{FC} (i.e.~fully-connected) layer will compute the class scores, resulting in volume of size $[1\times 1\times 10]$, where each of the 10 numbers correspond to a class score, such as among the 10 categories of the MNIST images we considered above . As with ordinary Neural Networks and as the name implies, each neuron in this layer will be connected to all the numbers in the previous volume. -\end{itemize} - -\noindent -% !split -\subsection*{Transforming images} - -CNNs transform the original image layer by layer from the original -pixel values to the final class scores. - -Observe that some layers contain -parameters and other don’t. In particular, the CNN layers perform -transformations that are a function of not only the activations in the -input volume, but also of the parameters (the weights and biases of -the neurons). On the other hand, the RELU/POOL layers will implement a -fixed function. The parameters in the CONV/FC layers will be trained -with gradient descent so that the class scores that the CNN computes -are consistent with the labels in the training set for each image. - - -% !split -\subsection*{CNNs in brief} - -In summary: - -\begin{itemize} -\item A CNN architecture is in the simplest case a list of Layers that transform the image volume into an output volume (e.g.~holding the class scores) - -\item There are a few distinct types of Layers (e.g.~CONV/FC/RELU/POOL are by far the most popular) - -\item Each Layer accepts an input 3D volume and transforms it to an output 3D volume through a differentiable function - -\item Each Layer may or may not have parameters (e.g.~CONV/FC do, RELU/POOL don’t) - -\item Each Layer may or may not have additional hyperparameters (e.g.~CONV/FC/POOL do, RELU doesn’t) -\end{itemize} - -\noindent -For more material on convolutional networks, we strongly recommend -the course -\href{{https://www.uio.no/studier/emner/matnat/ifi/IN5400/index-eng.html}}{IN5400 – Machine Learning for Image Analysis} -and the slides of \href{{http://cs231n.github.io/convolutional-networks/}}{CS231} which is taught at Stanford University (consistently ranked as one of the top computer science programs in the world). \href{{http://neuralnetworksanddeeplearning.com/chap6.html}}{Michael Nielsen's book is a must read, in particular chapter 6 which deals with CNNs}. - - -% !split -\subsection*{CNNs in more detail, building convolutional neural networks in Tensorflow and Keras} - - -As discussed above, CNNs are neural networks built from the assumption that the inputs -to the network are 2D images. This is important because the number of features or pixels in images -grows very fast with the image size, and an enormous number of weights and biases are needed in order to build an accurate network. - -As before, we still have our input, a hidden layer and an output. What's novel about convolutional networks -are the \textbf{convolutional} and \textbf{pooling} layers stacked in pairs between the input and the hidden layer. -In addition, the data is no longer represented as a 2D feature matrix, instead each input is a number of 2D -matrices, typically 1 for each color dimension (Red, Green, Blue). - - -% !split -\subsection*{Setting it up} - -It means that to represent the entire -dataset of images, we require a 4D matrix or \textbf{tensor}. This tensor has the dimensions: -\[ -(n_{inputs},\, n_{pixels, width},\, n_{pixels, height},\, depth) . -\] - -% !split -\subsection*{The MNIST dataset again} - -The MNIST dataset consists of grayscale images with a pixel size of -$28\times 28$, meaning we require $28 \times 28 = 724$ weights to each -neuron in the first hidden layer. - -If we were to analyze images of size $128\times 128$ we would require -$128 \times 128 = 16384$ weights to each neuron. Even worse if we were -dealing with color images, as most images are, we have an image matrix -of size $128\times 128$ for each color dimension (Red, Green, Blue), -meaning 3 times the number of weights $= 49152$ are required for every -single neuron in the first hidden layer. - - -% !split -\subsection*{Strong correlations} -Images typically have strong local correlations, meaning that a small -part of the image varies little from its neighboring regions. If for -example we have an image of a blue car, we can roughly assume that a -small blue part of the image is surrounded by other blue regions. - -Therefore, instead of connecting every single pixel to a neuron in the -first hidden layer, as we have previously done with deep neural -networks, we can instead connect each neuron to a small part of the -image (in all 3 RGB depth dimensions). The size of each small area is -fixed, and known as a \href{{https://en.wikipedia.org/wiki/Receptive_field}}{receptive}. - - -% !split -\subsection*{Layers of a CNN} -The layers of a convolutional neural network arrange neurons in 3D: width, height and depth. -The input image is typically a square matrix of depth 3. - -A \textbf{convolution} is performed on the image which outputs -a 3D volume of neurons. The weights to the input are arranged in a number of 2D matrices, known as \textbf{filters}. - - -Each filter slides along the input image, taking the dot product -between each small part of the image and the filter, in all depth -dimensions. This is then passed through a non-linear function, -typically the \textbf{Rectified Linear (ReLu)} function, which serves as the -activation of the neurons in the first convolutional layer. This is -further passed through a \textbf{pooling layer}, which reduces the size of the -convolutional layer, e.g.~by taking the maximum or average across some -small regions, and this serves as input to the next convolutional -layer. - - -% !split -\subsection*{Systematic reduction} - -By systematically reducing the size of the input volume, through -convolution and pooling, the network should create representations of -small parts of the input, and then from them assemble representations -of larger areas. The final pooling layer is flattened to serve as -input to a hidden layer, such that each neuron in the final pooling -layer is connected to every single neuron in the hidden layer. This -then serves as input to the output layer, e.g.~a softmax output for -classification. - - -% !split -\subsection*{Prerequisites: Collect and pre-process data} -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# import necessary packages -import numpy as np -import matplotlib.pyplot as plt -from sklearn import datasets - - -# ensure the same random numbers appear every time -np.random.seed(0) - -# display images in notebook -%matplotlib inline -plt.rcParams['figure.figsize'] = (12,12) - - -# download MNIST dataset -digits = datasets.load_digits() - -# define inputs and labels -inputs = digits.images -labels = digits.target - -# RGB images have a depth of 3 -# our images are grayscale so they should have a depth of 1 -inputs = inputs[:,:,:,np.newaxis] - -print("inputs = (n_inputs, pixel_width, pixel_height, depth) = " + str(inputs.shape)) -print("labels = (n_inputs) = " + str(labels.shape)) - - -# choose some random images to display -n_inputs = len(inputs) -indices = np.arange(n_inputs) -random_indices = np.random.choice(indices, size=5) - -for i, image in enumerate(digits.images[random_indices]): - plt.subplot(1, 5, i+1) - plt.axis('off') - plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest') - plt.title("Label: %d" % digits.target[random_indices[i]]) -plt.show() -\end{minted} - - -% !split -\subsection*{Importing Keras and Tensorflow} -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.utils import to_categorical -from sklearn.model_selection import train_test_split - -# representation of labels -labels = to_categorical(labels) - -# split into train and test data -# one-liner from scikit-learn library -train_size = 0.8 -test_size = 1 - train_size -X_train, X_test, Y_train, Y_test = train_test_split(inputs, labels, train_size=train_size, - test_size=test_size) -\end{minted} - -% !split -\subsection*{Using TensorFlow backend} - -We need to define model and architecture and choose cost function and optmizer. -\begin{Verbatim}[numbers=none,fontsize=\fontsize{9pt}{9pt},baselinestretch=0.95] - -import tensorflow as tf - -class ConvolutionalNeuralNetworkTensorflow: - def __init__( - self, - X_train, - Y_train, - X_test, - Y_test, - n_filters=10, - n_neurons_connected=50, - n_categories=10, - receptive_field=3, - stride=1, - padding=1, - epochs=10, - batch_size=100, - eta=0.1, - lmbd=0.0, - ): - - self.global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step') - - self.X_train = X_train - self.Y_train = Y_train - self.X_test = X_test - self.Y_test = Y_test - - self.n_inputs, self.input_width, self.input_height, self.depth = X_train.shape - - self.n_filters = n_filters - self.n_downsampled = int(self.input_width*self.input_height*n_filters / 4) - self.n_neurons_connected = n_neurons_connected - self.n_categories = n_categories - - self.receptive_field = receptive_field - self.stride = stride - self.strides = [stride, stride, stride, stride] - self.padding = padding - - self.epochs = epochs - self.batch_size = batch_size - self.iterations = self.n_inputs // self.batch_size - self.eta = eta - self.lmbd = lmbd - - self.create_placeholders() - self.create_CNN() - self.create_loss() - self.create_optimiser() - self.create_accuracy() - - def create_placeholders(self): - with tf.name_scope('data'): - self.X = tf.placeholder(tf.float32, shape=(None, self.input_width, self.input_height, self.depth), name='X_data') - self.Y = tf.placeholder(tf.float32, shape=(None, self.n_categories), name='Y_data') - - def create_CNN(self): - with tf.name_scope('CNN'): - - # Convolutional layer - self.W_conv = self.weight_variable([self.receptive_field, self.receptive_field, self.depth, self.n_filters], name='conv', dtype=tf.float32) - b_conv = self.weight_variable([self.n_filters], name='conv', dtype=tf.float32) - z_conv = tf.nn.conv2d(self.X, self.W_conv, self.strides, padding='SAME', name='conv') + b_conv - a_conv = tf.nn.relu(z_conv) - - # 2x2 max pooling - a_pool = tf.nn.max_pool(a_conv, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME', name='pool') - - # Fully connected layer - a_pool_flat = tf.reshape(a_pool, [-1, self.n_downsampled]) - self.W_fc = self.weight_variable([self.n_downsampled, self.n_neurons_connected], name='fc', dtype=tf.float32) - b_fc = self.bias_variable([self.n_neurons_connected], name='fc', dtype=tf.float32) - a_fc = tf.nn.relu(tf.matmul(a_pool_flat, self.W_fc) + b_fc) - - # Output layer - self.W_out = self.weight_variable([self.n_neurons_connected, self.n_categories], name='out', dtype=tf.float32) - b_out = self.bias_variable([self.n_categories], name='out', dtype=tf.float32) - self.z_out = tf.matmul(a_fc, self.W_out) + b_out - - def create_loss(self): - with tf.name_scope('loss'): - softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y, logits=self.z_out)) - - regularizer_loss_conv = tf.nn.l2_loss(self.W_conv) - regularizer_loss_fc = tf.nn.l2_loss(self.W_fc) - regularizer_loss_out = tf.nn.l2_loss(self.W_out) - regularizer_loss = self.lmbd*(regularizer_loss_conv + regularizer_loss_fc + regularizer_loss_out) - - self.loss = softmax_loss + regularizer_loss - - def create_accuracy(self): - with tf.name_scope('accuracy'): - probabilities = tf.nn.softmax(self.z_out) - predictions = tf.argmax(probabilities, 1) - labels = tf.argmax(self.Y, 1) - - correct_predictions = tf.equal(predictions, labels) - correct_predictions = tf.cast(correct_predictions, tf.float32) - self.accuracy = tf.reduce_mean(correct_predictions) - - def create_optimiser(self): - with tf.name_scope('optimizer'): - self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.eta).minimize(self.loss, global_step=self.global_step) - - def weight_variable(self, shape, name='', dtype=tf.float32): - initial = tf.truncated_normal(shape, stddev=0.1) - return tf.Variable(initial, name=name, dtype=dtype) - - def bias_variable(self, shape, name='', dtype=tf.float32): - initial = tf.constant(0.1, shape=shape) - return tf.Variable(initial, name=name, dtype=dtype) - - def fit(self): - data_indices = np.arange(self.n_inputs) - - with tf.Session() as sess: - sess.run(tf.global_variables_initializer()) - for i in range(self.epochs): - for j in range(self.iterations): - chosen_datapoints = np.random.choice(data_indices, size=self.batch_size, replace=False) - batch_X, batch_Y = self.X_train[chosen_datapoints], self.Y_train[chosen_datapoints] - - sess.run([CNN.loss, CNN.optimizer], - feed_dict={CNN.X: batch_X, - CNN.Y: batch_Y}) - accuracy = sess.run(CNN.accuracy, - feed_dict={CNN.X: batch_X, - CNN.Y: batch_Y}) - step = sess.run(CNN.global_step) - - self.train_loss, self.train_accuracy = sess.run([CNN.loss, CNN.accuracy], - feed_dict={CNN.X: self.X_train, - CNN.Y: self.Y_train}) - - self.test_loss, self.test_accuracy = sess.run([CNN.loss, CNN.accuracy], - feed_dict={CNN.X: self.X_test, - CNN.Y: self.Y_test}) -\end{Verbatim} - -% !split -\subsection*{Train the model} - -We need now to train the model, evaluate it and test its performance on test data, and eventually include hyperparameters. -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -epochs = 100 -batch_size = 100 -n_filters = 10 -n_neurons_connected = 50 -n_categories = 10 - -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -CNN_tf = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - CNN = ConvolutionalNeuralNetworkTensorflow(X_train, Y_train, X_test, Y_test, - n_filters=n_filters, n_neurons_connected=n_neurons_connected, - n_categories=n_categories, epochs=epochs, batch_size=batch_size, - eta=eta, lmbd=lmbd) - CNN.fit() - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % CNN.test_accuracy) - print() - - CNN_tf[i][j] = CNN -\end{minted} - -% !split -\subsection*{Visualizing the results} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - CNN = CNN_tf[i][j] - - train_accuracy[i][j] = CNN.train_accuracy - test_accuracy[i][j] = CNN.test_accuracy - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{minted} - -% !split -\subsection*{Running with Keras} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -from keras.models import Sequential -from keras.layers.convolutional import Conv2D -from keras.layers.convolutional import MaxPooling2D -from keras.layers import Flatten -from keras.layers import Dense -from keras.regularizers import l2 -from keras.optimizers import SGD - -def create_convolutional_neural_network_keras(input_shape, receptive_field, - n_filters, n_neurons_connected, n_categories, - eta, lmbd): - model = Sequential() - model.add(Conv2D(n_filters, (receptive_field, receptive_field), input_shape=input_shape, padding='same', - activation='relu', kernel_regularizer=l2(lmbd))) - model.add(MaxPooling2D(pool_size=(2, 2))) - model.add(Flatten()) - model.add(Dense(n_neurons_connected, activation='relu', kernel_regularizer=l2(lmbd))) - model.add(Dense(n_categories, activation='softmax', kernel_regularizer=l2(lmbd))) - - sgd = SGD(lr=eta) - model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) - - return model - -epochs = 100 -batch_size = 100 -input_shape = X_train.shape[1:4] -receptive_field = 3 -n_filters = 10 -n_neurons_connected = 50 -n_categories = 10 - -eta_vals = np.logspace(-5, 1, 7) -lmbd_vals = np.logspace(-5, 1, 7) -\end{minted} - -% !split -\subsection*{Final part} - -\begin{minted}[fontsize=\fontsize{9pt}{9pt},linenos=false,mathescape,baselinestretch=1.0,fontfamily=tt,xleftmargin=7mm]{python} -CNN_keras = np.zeros((len(eta_vals), len(lmbd_vals)), dtype=object) - -for i, eta in enumerate(eta_vals): - for j, lmbd in enumerate(lmbd_vals): - CNN = create_convolutional_neural_network_keras(input_shape, receptive_field, - n_filters, n_neurons_connected, n_categories, - eta, lmbd) - CNN.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, verbose=0) - scores = CNN.evaluate(X_test, Y_test) - - CNN_keras[i][j] = CNN - - print("Learning rate = ", eta) - print("Lambda = ", lmbd) - print("Test accuracy: %.3f" % scores[1]) - print() -\end{minted} - -% !split -\subsection*{Final visualization} - -\begin{Verbatim}[numbers=none,fontsize=\fontsize{9pt}{9pt},baselinestretch=0.95] -# visual representation of grid search -# uses seaborn heatmap, could probably do this in matplotlib -import seaborn as sns - -sns.set() - -train_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) -test_accuracy = np.zeros((len(eta_vals), len(lmbd_vals))) - -for i in range(len(eta_vals)): - for j in range(len(lmbd_vals)): - CNN = CNN_keras[i][j] - - train_accuracy[i][j] = CNN.evaluate(X_train, Y_train)[1] - test_accuracy[i][j] = CNN.evaluate(X_test, Y_test)[1] - - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(train_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Training Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() - -fig, ax = plt.subplots(figsize = (10, 10)) -sns.heatmap(test_accuracy, annot=True, ax=ax, cmap="viridis") -ax.set_title("Test Accuracy") -ax.set_ylabel("$\eta$") -ax.set_xlabel("$\lambda$") -plt.show() -\end{Verbatim} - -% !split -\subsection*{Fun links} - -\begin{enumerate} -\item \href{{https://arxiv.org/abs/1604.07316}}{Self-Driving cars using a convolutional neural network} - -\item \href{{https://deepdreamgenerator.com/}}{Abstract art using convolutional neural networks} -\end{enumerate} - -\noindent - -% ------------------- end of main content --------------- - -\end{document} - diff --git a/doc/src/NeuralNet/_minted-NeuralNet/03D77A9F0C0688F425C5F453449B11524234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/03D77A9F0C0688F425C5F453449B11524234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index fa1abf578..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/03D77A9F0C0688F425C5F453449B11524234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,44 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} import necessary packages} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{numpy} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{np} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{matplotlib.pyplot} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{plt} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{sklearn} \PYGdefault{k+kn}{import} \PYGdefault{n}{datasets} - - -\PYGdefault{c+c1}{\PYGdefaultZsh{} ensure the same random numbers appear every time} -\PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{seed}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} display images in notebook} -\PYGdefault{o}{\PYGdefaultZpc{}}\PYGdefault{n}{matplotlib} \PYGdefault{n}{inline} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{rcParams}\PYGdefault{p}{[}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}figure.figsize\PYGdefaultZsq{}}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{12}\PYGdefault{p}{,}\PYGdefault{l+m+mi}{12}\PYGdefault{p}{)} - - -\PYGdefault{c+c1}{\PYGdefaultZsh{} download MNIST dataset} -\PYGdefault{n}{digits} \PYGdefault{o}{=} \PYGdefault{n}{datasets}\PYGdefault{o}{.}\PYGdefault{n}{load\PYGdefaultZus{}digits}\PYGdefault{p}{()} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} define inputs and labels} -\PYGdefault{n}{inputs} \PYGdefault{o}{=} \PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{images} -\PYGdefault{n}{labels} \PYGdefault{o}{=} \PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{target} - -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}inputs = (n\PYGdefaultZus{}inputs, pixel\PYGdefaultZus{}width, pixel\PYGdefaultZus{}height) = \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{))} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}labels = (n\PYGdefaultZus{}inputs) = \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{labels}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{))} - - -\PYGdefault{c+c1}{\PYGdefaultZsh{} flatten the image} -\PYGdefault{c+c1}{\PYGdefaultZsh{} the value \PYGdefaultZhy{}1 means dimension is inferred from the remaining dimensions: 8x8 = 64} -\PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{=} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{p}{)} -\PYGdefault{n}{inputs} \PYGdefault{o}{=} \PYGdefault{n}{inputs}\PYGdefault{o}{.}\PYGdefault{n}{reshape}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{,} \PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}X = (n\PYGdefaultZus{}inputs, n\PYGdefaultZus{}features) = \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{))} - - -\PYGdefault{c+c1}{\PYGdefaultZsh{} choose some random images to display} -\PYGdefault{n}{indices} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{)} -\PYGdefault{n}{random\PYGdefaultZus{}indices} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{choice}\PYGdefault{p}{(}\PYGdefault{n}{indices}\PYGdefault{p}{,} \PYGdefault{n}{size}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{)} - -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{image} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{images}\PYGdefault{p}{[}\PYGdefault{n}{random\PYGdefaultZus{}indices}\PYGdefault{p}{]):} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplot}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{n}{i}\PYGdefault{o}{+}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{axis}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}off\PYGdefaultZsq{}}\PYGdefault{p}{)} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{imshow}\PYGdefault{p}{(}\PYGdefault{n}{image}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{cm}\PYGdefault{o}{.}\PYGdefault{n}{gray\PYGdefaultZus{}r}\PYGdefault{p}{,} \PYGdefault{n}{interpolation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}nearest\PYGdefaultZsq{}}\PYGdefault{p}{)} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Label: }\PYGdefault{l+s+si}{\PYGdefaultZpc{}d}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}} \PYGdefault{o}{\PYGdefaultZpc{}} \PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{target}\PYGdefault{p}{[}\PYGdefault{n}{random\PYGdefaultZus{}indices}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{]])} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/09F4D9A376DDB6BE1E73593E04CB8E854234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/09F4D9A376DDB6BE1E73593E04CB8E854234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index d511848b9..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/09F4D9A376DDB6BE1E73593E04CB8E854234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,3 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{pip3} \PYGdefault{n}{install} \PYGdefault{n}{keras} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/0CC1A7A85AEE6BE30862E176C9E57C6A4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/0CC1A7A85AEE6BE30862E176C9E57C6A4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index ab11a4be9..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/0CC1A7A85AEE6BE30862E176C9E57C6A4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,32 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} visual representation of grid search} -\PYGdefault{c+c1}{\PYGdefaultZsh{} uses seaborn heatmap, could probably do this in matplotlib} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{seaborn} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{sns} - -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{set}\PYGdefault{p}{()} - -\PYGdefault{n}{train\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} -\PYGdefault{n}{test\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} - -\PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{k}{for} \PYGdefault{n}{j} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{n}{CNN} \PYGdefault{o}{=} \PYGdefault{n}{CNN\PYGdefaultZus{}tf}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} - - \PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{CNN}\PYGdefault{o}{.}\PYGdefault{n}{train\PYGdefaultZus{}accuracy} - \PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{CNN}\PYGdefault{o}{.}\PYGdefault{n}{test\PYGdefaultZus{}accuracy} - - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Training Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/15964F0B401001BEF599F2E4776F3CBB4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/15964F0B401001BEF599F2E4776F3CBB4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 2d54384e9..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/15964F0B401001BEF599F2E4776F3CBB4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,17 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.models} \PYGdefault{k+kn}{import} \PYGdefault{n}{Sequential} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.layers} \PYGdefault{k+kn}{import} \PYGdefault{n}{Dense} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.regularizers} \PYGdefault{k+kn}{import} \PYGdefault{n}{l2} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.optimizers} \PYGdefault{k+kn}{import} \PYGdefault{n}{SGD} - -\PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}neural\PYGdefaultZus{}network\PYGdefaultZus{}keras}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{):} - \PYGdefault{n}{model} \PYGdefault{o}{=} \PYGdefault{n}{Sequential}\PYGdefault{p}{()} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{Dense}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{p}{,} \PYGdefault{n}{activation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}sigmoid\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{kernel\PYGdefaultZus{}regularizer}\PYGdefault{o}{=}\PYGdefault{n}{l2}\PYGdefault{p}{(}\PYGdefault{n}{lmbd}\PYGdefault{p}{)))} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{Dense}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{p}{,} \PYGdefault{n}{activation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}sigmoid\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{kernel\PYGdefaultZus{}regularizer}\PYGdefault{o}{=}\PYGdefault{n}{l2}\PYGdefault{p}{(}\PYGdefault{n}{lmbd}\PYGdefault{p}{)))} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{Dense}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} \PYGdefault{n}{activation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}softmax\PYGdefaultZsq{}}\PYGdefault{p}{))} - - \PYGdefault{n}{sgd} \PYGdefault{o}{=} \PYGdefault{n}{SGD}\PYGdefault{p}{(}\PYGdefault{n}{lr}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{compile}\PYGdefault{p}{(}\PYGdefault{n}{loss}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}categorical\PYGdefaultZus{}crossentropy\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{optimizer}\PYGdefault{o}{=}\PYGdefault{n}{sgd}\PYGdefault{p}{,} \PYGdefault{n}{metrics}\PYGdefault{o}{=}\PYGdefault{p}{[}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}accuracy\PYGdefaultZsq{}}\PYGdefault{p}{])} - - \PYGdefault{k}{return} \PYGdefault{n}{model} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/2E8469C060C87841FDF49D194310781F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/2E8469C060C87841FDF49D194310781F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 578997abe..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/2E8469C060C87841FDF49D194310781F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,102 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k}{class} \PYGdefault{n+nc}{NeuralNetwork}\PYGdefault{p}{:} - \PYGdefault{k}{def} \PYGdefault{n+nf+fm}{\PYGdefaultZus{}\PYGdefaultZus{}init\PYGdefaultZus{}\PYGdefaultZus{}}\PYGdefault{p}{(} - \PYGdefault{n+nb+bp}{self}\PYGdefault{p}{,} - \PYGdefault{n}{X\PYGdefaultZus{}data}\PYGdefault{p}{,} - \PYGdefault{n}{Y\PYGdefaultZus{}data}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{50}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} - \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} - \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{100}\PYGdefault{p}{,} - \PYGdefault{n}{eta}\PYGdefault{o}{=}\PYGdefault{l+m+mf}{0.1}\PYGdefault{p}{,} - \PYGdefault{n}{lmbd}\PYGdefault{o}{=}\PYGdefault{l+m+mf}{0.0}\PYGdefault{p}{,} - - \PYGdefault{p}{):} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}data\PYGdefaultZus{}full} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}data} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}data\PYGdefaultZus{}full} \PYGdefault{o}{=} \PYGdefault{n}{Y\PYGdefaultZus{}data} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}data}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{]} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}features} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}data}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{]} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons} \PYGdefault{o}{=} \PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}categories} \PYGdefault{o}{=} \PYGdefault{n}{n\PYGdefaultZus{}categories} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{epochs} \PYGdefault{o}{=} \PYGdefault{n}{epochs} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{batch\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{n}{batch\PYGdefaultZus{}size} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{iterations} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{//} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{batch\PYGdefaultZus{}size} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{eta} \PYGdefault{o}{=} \PYGdefault{n}{eta} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{lmbd} \PYGdefault{o}{=} \PYGdefault{n}{lmbd} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{create\PYGdefaultZus{}biases\PYGdefaultZus{}and\PYGdefaultZus{}weights}\PYGdefault{p}{()} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}biases\PYGdefaultZus{}and\PYGdefaultZus{}weights}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{randn}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}features}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}bias} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{l+m+mf}{0.01} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{randn}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}bias} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{l+m+mf}{0.01} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{feed\PYGdefaultZus{}forward}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} feed\PYGdefaultZhy{}forward for training} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{z\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}data}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}bias} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{a\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{sigmoid}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{z\PYGdefaultZus{}h}\PYGdefault{p}{)} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{z\PYGdefaultZus{}o} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}bias} - - \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{exp}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{z\PYGdefaultZus{}o}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{/} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{exp\PYGdefaultZus{}term}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{n}{keepdims}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{feed\PYGdefaultZus{}forward\PYGdefaultZus{}out}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{,} \PYGdefault{n}{X}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} feed\PYGdefaultZhy{}forward for output} - \PYGdefault{n}{z\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}bias} - \PYGdefault{n}{a\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{sigmoid}\PYGdefault{p}{(}\PYGdefault{n}{z\PYGdefaultZus{}h}\PYGdefault{p}{)} - - \PYGdefault{n}{z\PYGdefaultZus{}o} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}bias} - - \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{exp}\PYGdefault{p}{(}\PYGdefault{n}{z\PYGdefaultZus{}o}\PYGdefault{p}{)} - \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{/} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{exp\PYGdefaultZus{}term}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{n}{keepdims}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} - \PYGdefault{k}{return} \PYGdefault{n}{probabilities} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{backpropagation}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{n}{error\PYGdefaultZus{}output} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{probabilities} \PYGdefault{o}{\PYGdefaultZhy{}} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}data} - \PYGdefault{n}{error\PYGdefaultZus{}hidden} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{error\PYGdefaultZus{}output}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights}\PYGdefault{o}{.}\PYGdefault{n}{T}\PYGdefault{p}{)} \PYGdefault{o}{*} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{a\PYGdefaultZus{}h} \PYGdefault{o}{*} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{1} \PYGdefault{o}{\PYGdefaultZhy{}} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{)} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{o}{.}\PYGdefault{n}{T}\PYGdefault{p}{,} \PYGdefault{n}{error\PYGdefaultZus{}output}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}bias\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{error\PYGdefaultZus{}output}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}data}\PYGdefault{o}{.}\PYGdefault{n}{T}\PYGdefault{p}{,} \PYGdefault{n}{error\PYGdefaultZus{}hidden}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}bias\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{error\PYGdefaultZus{}hidden}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - - \PYGdefault{k}{if} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{lmbd} \PYGdefault{o}{\PYGdefaultZgt{}} \PYGdefault{l+m+mf}{0.0}\PYGdefault{p}{:} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} \PYGdefault{o}{+=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{lmbd} \PYGdefault{o}{*} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} \PYGdefault{o}{+=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{lmbd} \PYGdefault{o}{*} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}bias} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{output\PYGdefaultZus{}bias\PYGdefaultZus{}gradient} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}bias} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{hidden\PYGdefaultZus{}bias\PYGdefaultZus{}gradient} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{predict}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{,} \PYGdefault{n}{X}\PYGdefault{p}{):} - \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{feed\PYGdefaultZus{}forward\PYGdefaultZus{}out}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{)} - \PYGdefault{k}{return} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{argmax}\PYGdefault{p}{(}\PYGdefault{n}{probabilities}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{predict\PYGdefaultZus{}probabilities}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{,} \PYGdefault{n}{X}\PYGdefault{p}{):} - \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{feed\PYGdefaultZus{}forward\PYGdefaultZus{}out}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{)} - \PYGdefault{k}{return} \PYGdefault{n}{probabilities} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{train}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{n}{data\PYGdefaultZus{}indices} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{)} - - \PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{epochs}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{iterations}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} pick datapoints with replacement} - \PYGdefault{n}{chosen\PYGdefaultZus{}datapoints} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{choice}\PYGdefault{p}{(} - \PYGdefault{n}{data\PYGdefaultZus{}indices}\PYGdefault{p}{,} \PYGdefault{n}{size}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} \PYGdefault{n}{replace}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{False} - \PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} minibatch training data} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}data} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}data\PYGdefaultZus{}full}\PYGdefault{p}{[}\PYGdefault{n}{chosen\PYGdefaultZus{}datapoints}\PYGdefault{p}{]} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}data} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}data\PYGdefaultZus{}full}\PYGdefault{p}{[}\PYGdefault{n}{chosen\PYGdefaultZus{}datapoints}\PYGdefault{p}{]} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{feed\PYGdefaultZus{}forward}\PYGdefault{p}{()} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{backpropagation}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/32D39C646616C734C492A964B85F90084234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/32D39C646616C734C492A964B85F90084234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 2c64b3ece..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/32D39C646616C734C492A964B85F90084234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,13 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.utils} \PYGdefault{k+kn}{import} \PYGdefault{n}{to\PYGdefaultZus{}categorical} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{sklearn.model\PYGdefaultZus{}selection} \PYGdefault{k+kn}{import} \PYGdefault{n}{train\PYGdefaultZus{}test\PYGdefaultZus{}split} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} one\PYGdefaultZhy{}hot representation of labels} -\PYGdefault{n}{labels} \PYGdefault{o}{=} \PYGdefault{n}{to\PYGdefaultZus{}categorical}\PYGdefault{p}{(}\PYGdefault{n}{labels}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} split into train and test data} -\PYGdefault{n}{train\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mf}{0.8} -\PYGdefault{n}{test\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{1} \PYGdefault{o}{\PYGdefaultZhy{}} \PYGdefault{n}{train\PYGdefaultZus{}size} -\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test} \PYGdefault{o}{=} \PYGdefault{n}{train\PYGdefaultZus{}test\PYGdefaultZus{}split}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{p}{,} \PYGdefault{n}{labels}\PYGdefault{p}{,} \PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{p}{,} - \PYGdefault{n}{test\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{test\PYGdefaultZus{}size}\PYGdefault{p}{)} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/43CB1A5F1E3939FC231D7BC1782EC8534234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/43CB1A5F1E3939FC231D7BC1782EC8534234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index cac62f1f0..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/43CB1A5F1E3939FC231D7BC1782EC8534234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,18 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{CNN\PYGdefaultZus{}keras} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)),} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n+nb}{object}\PYGdefault{p}{)} - -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{eta} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j}\PYGdefault{p}{,} \PYGdefault{n}{lmbd} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{n}{CNN} \PYGdefault{o}{=} \PYGdefault{n}{create\PYGdefaultZus{}convolutional\PYGdefaultZus{}neural\PYGdefaultZus{}network\PYGdefaultZus{}keras}\PYGdefault{p}{(}\PYGdefault{n}{input\PYGdefaultZus{}shape}\PYGdefault{p}{,} \PYGdefault{n}{receptive\PYGdefaultZus{}field}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}filters}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}connected}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} - \PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{n}{CNN}\PYGdefault{o}{.}\PYGdefault{n}{fit}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{n}{epochs}\PYGdefault{p}{,} \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} \PYGdefault{n}{verbose}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - \PYGdefault{n}{scores} \PYGdefault{o}{=} \PYGdefault{n}{CNN}\PYGdefault{o}{.}\PYGdefault{n}{evaluate}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{)} - - \PYGdefault{n}{CNN\PYGdefaultZus{}keras}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{CNN} - - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Learning rate = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Lambda = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test accuracy: }\PYGdefault{l+s+si}{\PYGdefaultZpc{}.3f}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}} \PYGdefault{o}{\PYGdefaultZpc{}} \PYGdefault{n}{scores}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{])} - \PYGdefault{k}{print}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/445B1F5065FDF5AC11F178B59D23C96C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/445B1F5065FDF5AC11F178B59D23C96C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index d94c89036..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/445B1F5065FDF5AC11F178B59D23C96C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,70 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} to categorical turns our integer vector into a onehot representation} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{sklearn.metrics} \PYGdefault{k+kn}{import} \PYGdefault{n}{accuracy\PYGdefaultZus{}score} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} one\PYGdefaultZhy{}hot in numpy} -\PYGdefault{k}{def} \PYGdefault{n+nf}{to\PYGdefaultZus{}categorical\PYGdefaultZus{}numpy}\PYGdefault{p}{(}\PYGdefault{n}{integer\PYGdefaultZus{}vector}\PYGdefault{p}{):} - \PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{=} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{integer\PYGdefaultZus{}vector}\PYGdefault{p}{)} - \PYGdefault{n}{n\PYGdefaultZus{}categories} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{max}\PYGdefault{p}{(}\PYGdefault{n}{integer\PYGdefaultZus{}vector}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{l+m+mi}{1} - \PYGdefault{n}{onehot\PYGdefaultZus{}vector} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{))} - \PYGdefault{n}{onehot\PYGdefaultZus{}vector}\PYGdefault{p}{[}\PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{),} \PYGdefault{n}{integer\PYGdefaultZus{}vector}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{1} - - \PYGdefault{k}{return} \PYGdefault{n}{onehot\PYGdefaultZus{}vector} - -\PYGdefault{c+c1}{\PYGdefaultZsh{}Y\PYGdefaultZus{}train\PYGdefaultZus{}onehot, Y\PYGdefaultZus{}test\PYGdefaultZus{}onehot = to\PYGdefaultZus{}categorical(Y\PYGdefaultZus{}train), to\PYGdefaultZus{}categorical(Y\PYGdefaultZus{}test)} -\PYGdefault{n}{Y\PYGdefaultZus{}train\PYGdefaultZus{}onehot}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test\PYGdefaultZus{}onehot} \PYGdefault{o}{=} \PYGdefault{n}{to\PYGdefaultZus{}categorical\PYGdefaultZus{}numpy}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{),} \PYGdefault{n}{to\PYGdefaultZus{}categorical\PYGdefaultZus{}numpy}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{)} - -\PYGdefault{k}{def} \PYGdefault{n+nf}{feed\PYGdefaultZus{}forward\PYGdefaultZus{}train}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} weighted sum of inputs to the hidden layer} - \PYGdefault{n}{z\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{,} \PYGdefault{n}{hidden\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n}{hidden\PYGdefaultZus{}bias} - \PYGdefault{c+c1}{\PYGdefaultZsh{} activation in the hidden layer} - \PYGdefault{n}{a\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{sigmoid}\PYGdefault{p}{(}\PYGdefault{n}{z\PYGdefaultZus{}h}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} weighted sum of inputs to the output layer} - \PYGdefault{n}{z\PYGdefaultZus{}o} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{,} \PYGdefault{n}{output\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n}{output\PYGdefaultZus{}bias} - \PYGdefault{c+c1}{\PYGdefaultZsh{} softmax output} - \PYGdefault{c+c1}{\PYGdefaultZsh{} axis 0 holds each input and axis 1 the probabilities of each category} - \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{exp}\PYGdefault{p}{(}\PYGdefault{n}{z\PYGdefaultZus{}o}\PYGdefault{p}{)} - \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{/} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{exp\PYGdefaultZus{}term}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{n}{keepdims}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} for backpropagation need activations in hidden and output layers} - \PYGdefault{k}{return} \PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{,} \PYGdefault{n}{probabilities} - -\PYGdefault{k}{def} \PYGdefault{n+nf}{backpropagation}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{,} \PYGdefault{n}{Y}\PYGdefault{p}{):} - \PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{,} \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{feed\PYGdefaultZus{}forward\PYGdefaultZus{}train}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} error in the output layer} - \PYGdefault{n}{error\PYGdefaultZus{}output} \PYGdefault{o}{=} \PYGdefault{n}{probabilities} \PYGdefault{o}{\PYGdefaultZhy{}} \PYGdefault{n}{Y} - \PYGdefault{c+c1}{\PYGdefaultZsh{} error in the hidden layer} - \PYGdefault{n}{error\PYGdefaultZus{}hidden} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{error\PYGdefaultZus{}output}\PYGdefault{p}{,} \PYGdefault{n}{output\PYGdefaultZus{}weights}\PYGdefault{o}{.}\PYGdefault{n}{T}\PYGdefault{p}{)} \PYGdefault{o}{*} \PYGdefault{n}{a\PYGdefaultZus{}h} \PYGdefault{o}{*} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{1} \PYGdefault{o}{\PYGdefaultZhy{}} \PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} gradients for the output layer} - \PYGdefault{n}{output\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{o}{.}\PYGdefault{n}{T}\PYGdefault{p}{,} \PYGdefault{n}{error\PYGdefaultZus{}output}\PYGdefault{p}{)} - \PYGdefault{n}{output\PYGdefaultZus{}bias\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{error\PYGdefaultZus{}output}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} gradient for the hidden layer} - \PYGdefault{n}{hidden\PYGdefaultZus{}weights\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{o}{.}\PYGdefault{n}{T}\PYGdefault{p}{,} \PYGdefault{n}{error\PYGdefaultZus{}hidden}\PYGdefault{p}{)} - \PYGdefault{n}{hidden\PYGdefaultZus{}bias\PYGdefaultZus{}gradient} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{error\PYGdefaultZus{}hidden}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - - \PYGdefault{k}{return} \PYGdefault{n}{output\PYGdefaultZus{}weights\PYGdefaultZus{}gradient}\PYGdefault{p}{,} \PYGdefault{n}{output\PYGdefaultZus{}bias\PYGdefaultZus{}gradient}\PYGdefault{p}{,} \PYGdefault{n}{hidden\PYGdefaultZus{}weights\PYGdefaultZus{}gradient}\PYGdefault{p}{,} \PYGdefault{n}{hidden\PYGdefaultZus{}bias\PYGdefaultZus{}gradient} - -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Old accuracy on training data: \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{),} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{)))} - -\PYGdefault{n}{eta} \PYGdefault{o}{=} \PYGdefault{l+m+mf}{0.01} -\PYGdefault{n}{lmbd} \PYGdefault{o}{=} \PYGdefault{l+m+mf}{0.01} -\PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{1000}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} calculate gradients} - \PYGdefault{n}{dWo}\PYGdefault{p}{,} \PYGdefault{n}{dBo}\PYGdefault{p}{,} \PYGdefault{n}{dWh}\PYGdefault{p}{,} \PYGdefault{n}{dBh} \PYGdefault{o}{=} \PYGdefault{n}{backpropagation}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train\PYGdefaultZus{}onehot}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} regularization term gradients} - \PYGdefault{n}{dWo} \PYGdefault{o}{+=} \PYGdefault{n}{lmbd} \PYGdefault{o}{*} \PYGdefault{n}{output\PYGdefaultZus{}weights} - \PYGdefault{n}{dWh} \PYGdefault{o}{+=} \PYGdefault{n}{lmbd} \PYGdefault{o}{*} \PYGdefault{n}{hidden\PYGdefaultZus{}weights} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} update weights and biases} - \PYGdefault{n}{output\PYGdefaultZus{}weights} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n}{dWo} - \PYGdefault{n}{output\PYGdefaultZus{}bias} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n}{dBo} - \PYGdefault{n}{hidden\PYGdefaultZus{}weights} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n}{dWh} - \PYGdefault{n}{hidden\PYGdefaultZus{}bias} \PYGdefault{o}{\PYGdefaultZhy{}=} \PYGdefault{n}{eta} \PYGdefault{o}{*} \PYGdefault{n}{dBh} - -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}New accuracy on training data: \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{),} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{)))} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/4761C81BB8B86E4C60BBB9DB7F1D6B5C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/4761C81BB8B86E4C60BBB9DB7F1D6B5C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 4e33e8cd4..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/4761C81BB8B86E4C60BBB9DB7F1D6B5C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,29 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{sklearn.model\PYGdefaultZus{}selection} \PYGdefault{k+kn}{import} \PYGdefault{n}{train\PYGdefaultZus{}test\PYGdefaultZus{}split} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} one\PYGdefaultZhy{}liner from scikit\PYGdefaultZhy{}learn library} -\PYGdefault{n}{train\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mf}{0.8} -\PYGdefault{n}{test\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{1} \PYGdefault{o}{\PYGdefaultZhy{}} \PYGdefault{n}{train\PYGdefaultZus{}size} -\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test} \PYGdefault{o}{=} \PYGdefault{n}{train\PYGdefaultZus{}test\PYGdefaultZus{}split}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{p}{,} \PYGdefault{n}{labels}\PYGdefault{p}{,} \PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{p}{,} - \PYGdefault{n}{test\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{test\PYGdefaultZus{}size}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} equivalently in numpy} -\PYGdefault{k}{def} \PYGdefault{n+nf}{train\PYGdefaultZus{}test\PYGdefaultZus{}split\PYGdefaultZus{}numpy}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{p}{,} \PYGdefault{n}{labels}\PYGdefault{p}{,} \PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{p}{,} \PYGdefault{n}{test\PYGdefaultZus{}size}\PYGdefault{p}{):} - \PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{=} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{p}{)} - \PYGdefault{n}{inputs\PYGdefaultZus{}shuffled} \PYGdefault{o}{=} \PYGdefault{n}{inputs}\PYGdefault{o}{.}\PYGdefault{n}{copy}\PYGdefault{p}{()} - \PYGdefault{n}{labels\PYGdefaultZus{}shuffled} \PYGdefault{o}{=} \PYGdefault{n}{labels}\PYGdefault{o}{.}\PYGdefault{n}{copy}\PYGdefault{p}{()} - - \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{shuffle}\PYGdefault{p}{(}\PYGdefault{n}{inputs\PYGdefaultZus{}shuffled}\PYGdefault{p}{)} - \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{shuffle}\PYGdefault{p}{(}\PYGdefault{n}{labels\PYGdefaultZus{}shuffled}\PYGdefault{p}{)} - - \PYGdefault{n}{train\PYGdefaultZus{}end} \PYGdefault{o}{=} \PYGdefault{n+nb}{int}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{o}{*}\PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{p}{)} - \PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{X\PYGdefaultZus{}test} \PYGdefault{o}{=} \PYGdefault{n}{inputs\PYGdefaultZus{}shuffled}\PYGdefault{p}{[:}\PYGdefault{n}{train\PYGdefaultZus{}end}\PYGdefault{p}{],} \PYGdefault{n}{inputs\PYGdefaultZus{}shuffled}\PYGdefault{p}{[}\PYGdefault{n}{train\PYGdefaultZus{}end}\PYGdefault{p}{:]} - \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test} \PYGdefault{o}{=} \PYGdefault{n}{labels\PYGdefaultZus{}shuffled}\PYGdefault{p}{[:}\PYGdefault{n}{train\PYGdefaultZus{}end}\PYGdefault{p}{],} \PYGdefault{n}{labels\PYGdefaultZus{}shuffled}\PYGdefault{p}{[}\PYGdefault{n}{train\PYGdefaultZus{}end}\PYGdefault{p}{:]} - - \PYGdefault{k}{return} \PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test} - -\PYGdefault{c+c1}{\PYGdefaultZsh{}X\PYGdefaultZus{}train, X\PYGdefaultZus{}test, Y\PYGdefaultZus{}train, Y\PYGdefaultZus{}test = train\PYGdefaultZus{}test\PYGdefaultZus{}split\PYGdefaultZus{}numpy(inputs, labels, train\PYGdefaultZus{}size, test\PYGdefaultZus{}size)} - -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Number of training images: \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{)))} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Number of test images: \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{)))} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/4A1EEDBB213837E19DAA4E5C494C8EFC4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/4A1EEDBB213837E19DAA4E5C494C8EFC4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index ab7e1ca0a..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/4A1EEDBB213837E19DAA4E5C494C8EFC4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,133 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{tensorflow} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{tf} - -\PYGdefault{k}{class} \PYGdefault{n+nc}{NeuralNetworkTensorflow}\PYGdefault{p}{:} - \PYGdefault{k}{def} \PYGdefault{n+nf+fm}{\PYGdefaultZus{}\PYGdefaultZus{}init\PYGdefaultZus{}\PYGdefaultZus{}}\PYGdefault{p}{(} - \PYGdefault{n+nb+bp}{self}\PYGdefault{p}{,} - \PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} - \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} - \PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} - \PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{100}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{50}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{2}\PYGdefault{p}{,} - \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} - \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{100}\PYGdefault{p}{,} - \PYGdefault{n}{eta}\PYGdefault{o}{=}\PYGdefault{l+m+mf}{0.1}\PYGdefault{p}{,} - \PYGdefault{n}{lmbd}\PYGdefault{o}{=}\PYGdefault{l+m+mf}{0.0}\PYGdefault{p}{,} - \PYGdefault{p}{):} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} keep track of number of steps} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{global\PYGdefaultZus{}step} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{Variable}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{int32}\PYGdefault{p}{,} \PYGdefault{n}{trainable}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{False}\PYGdefault{p}{,} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}global\PYGdefaultZus{}step\PYGdefaultZsq{}}\PYGdefault{p}{)} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}train} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}train} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}train} \PYGdefault{o}{=} \PYGdefault{n}{Y\PYGdefaultZus{}train} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}test} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}test} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}test} \PYGdefault{o}{=} \PYGdefault{n}{Y\PYGdefaultZus{}test} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{]} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}features} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{]} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1} \PYGdefault{o}{=} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2} \PYGdefault{o}{=} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}categories} \PYGdefault{o}{=} \PYGdefault{n}{n\PYGdefaultZus{}categories} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{epochs} \PYGdefault{o}{=} \PYGdefault{n}{epochs} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{batch\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{n}{batch\PYGdefaultZus{}size} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{iterations} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{//} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{batch\PYGdefaultZus{}size} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{eta} \PYGdefault{o}{=} \PYGdefault{n}{eta} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{lmbd} \PYGdefault{o}{=} \PYGdefault{n}{lmbd} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} build network piece by piece} - \PYGdefault{c+c1}{\PYGdefaultZsh{} name scopes (with) are used to enforce creation of new variables} - \PYGdefault{c+c1}{\PYGdefaultZsh{} https://www.tensorflow.org/guide/variables} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{create\PYGdefaultZus{}placeholders}\PYGdefault{p}{()} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{create\PYGdefaultZus{}DNN}\PYGdefault{p}{()} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{create\PYGdefaultZus{}loss}\PYGdefault{p}{()} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{create\PYGdefaultZus{}optimiser}\PYGdefault{p}{()} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{create\PYGdefaultZus{}accuracy}\PYGdefault{p}{()} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}placeholders}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} placeholders are fine here, but \PYGdefaultZdq{}Datasets\PYGdefaultZdq{} are the preferred method} - \PYGdefault{c+c1}{\PYGdefaultZsh{} of streaming data into a model} - \PYGdefault{k}{with} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{name\PYGdefaultZus{}scope}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}data\PYGdefaultZsq{}}\PYGdefault{p}{):} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{placeholder}\PYGdefault{p}{(}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{,} \PYGdefault{n}{shape}\PYGdefault{o}{=}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{None}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}features}\PYGdefault{p}{),} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}X\PYGdefaultZus{}data\PYGdefaultZsq{}}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{placeholder}\PYGdefault{p}{(}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{,} \PYGdefault{n}{shape}\PYGdefault{o}{=}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{None}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{),} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}Y\PYGdefaultZus{}data\PYGdefaultZsq{}}\PYGdefault{p}{)} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}DNN}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{k}{with} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{name\PYGdefaultZus{}scope}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}DNN\PYGdefaultZsq{}}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} the weights are stored to calculate regularization loss later} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} Fully connected layer 1} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}fc1} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{weight\PYGdefaultZus{}variable}\PYGdefault{p}{([}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}features}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{p}{],} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}fc1\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{)} - \PYGdefault{n}{b\PYGdefaultZus{}fc1} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{bias\PYGdefaultZus{}variable}\PYGdefault{p}{([}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{p}{],} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}fc1\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{)} - \PYGdefault{n}{a\PYGdefaultZus{}fc1} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{nn}\PYGdefault{o}{.}\PYGdefault{n}{sigmoid}\PYGdefault{p}{(}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}fc1}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n}{b\PYGdefaultZus{}fc1}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} Fully connected layer 2} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}fc2} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{weight\PYGdefaultZus{}variable}\PYGdefault{p}{([}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{p}{],} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}fc2\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{)} - \PYGdefault{n}{b\PYGdefaultZus{}fc2} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{bias\PYGdefaultZus{}variable}\PYGdefault{p}{([}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{p}{],} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}fc2\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{)} - \PYGdefault{n}{a\PYGdefaultZus{}fc2} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{nn}\PYGdefault{o}{.}\PYGdefault{n}{sigmoid}\PYGdefault{p}{(}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{a\PYGdefaultZus{}fc1}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}fc2}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n}{b\PYGdefaultZus{}fc2}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} Output layer} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}out} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{weight\PYGdefaultZus{}variable}\PYGdefault{p}{([}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{],} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}out\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{)} - \PYGdefault{n}{b\PYGdefaultZus{}out} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{bias\PYGdefaultZus{}variable}\PYGdefault{p}{([}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{],} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}out\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{z\PYGdefaultZus{}out} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{a\PYGdefaultZus{}fc2}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}out}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n}{b\PYGdefaultZus{}out} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}loss}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{k}{with} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{name\PYGdefaultZus{}scope}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}loss\PYGdefaultZsq{}}\PYGdefault{p}{):} - \PYGdefault{n}{softmax\PYGdefaultZus{}loss} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{reduce\PYGdefaultZus{}mean}\PYGdefault{p}{(}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{nn}\PYGdefault{o}{.}\PYGdefault{n}{softmax\PYGdefaultZus{}cross\PYGdefaultZus{}entropy\PYGdefaultZus{}with\PYGdefaultZus{}logits\PYGdefaultZus{}v2}\PYGdefault{p}{(}\PYGdefault{n}{labels}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y}\PYGdefault{p}{,} \PYGdefault{n}{logits}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{z\PYGdefaultZus{}out}\PYGdefault{p}{))} - - \PYGdefault{n}{regularizer\PYGdefaultZus{}loss\PYGdefaultZus{}fc1} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{nn}\PYGdefault{o}{.}\PYGdefault{n}{l2\PYGdefaultZus{}loss}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}fc1}\PYGdefault{p}{)} - \PYGdefault{n}{regularizer\PYGdefaultZus{}loss\PYGdefaultZus{}fc2} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{nn}\PYGdefault{o}{.}\PYGdefault{n}{l2\PYGdefaultZus{}loss}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}fc2}\PYGdefault{p}{)} - \PYGdefault{n}{regularizer\PYGdefaultZus{}loss\PYGdefaultZus{}out} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{nn}\PYGdefault{o}{.}\PYGdefault{n}{l2\PYGdefaultZus{}loss}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{W\PYGdefaultZus{}out}\PYGdefault{p}{)} - \PYGdefault{n}{regularizer\PYGdefaultZus{}loss} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{lmbd}\PYGdefault{o}{*}\PYGdefault{p}{(}\PYGdefault{n}{regularizer\PYGdefaultZus{}loss\PYGdefaultZus{}fc1} \PYGdefault{o}{+} \PYGdefault{n}{regularizer\PYGdefaultZus{}loss\PYGdefaultZus{}fc2} \PYGdefault{o}{+} \PYGdefault{n}{regularizer\PYGdefaultZus{}loss\PYGdefaultZus{}out}\PYGdefault{p}{)} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{loss} \PYGdefault{o}{=} \PYGdefault{n}{softmax\PYGdefaultZus{}loss} \PYGdefault{o}{+} \PYGdefault{n}{regularizer\PYGdefaultZus{}loss} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}accuracy}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{k}{with} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{name\PYGdefaultZus{}scope}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}accuracy\PYGdefaultZsq{}}\PYGdefault{p}{):} - \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{nn}\PYGdefault{o}{.}\PYGdefault{n}{softmax}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{z\PYGdefaultZus{}out}\PYGdefault{p}{)} - \PYGdefault{n}{predictions} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{argmax}\PYGdefault{p}{(}\PYGdefault{n}{probabilities}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} - \PYGdefault{n}{labels} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{argmax}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} - - \PYGdefault{n}{correct\PYGdefaultZus{}predictions} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{equal}\PYGdefault{p}{(}\PYGdefault{n}{predictions}\PYGdefault{p}{,} \PYGdefault{n}{labels}\PYGdefault{p}{)} - \PYGdefault{n}{correct\PYGdefaultZus{}predictions} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{cast}\PYGdefault{p}{(}\PYGdefault{n}{correct\PYGdefaultZus{}predictions}\PYGdefault{p}{,} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{)} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{accuracy} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{reduce\PYGdefaultZus{}mean}\PYGdefault{p}{(}\PYGdefault{n}{correct\PYGdefaultZus{}predictions}\PYGdefault{p}{)} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}optimiser}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{k}{with} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{name\PYGdefaultZus{}scope}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}optimizer\PYGdefaultZsq{}}\PYGdefault{p}{):} - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{optimizer} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{train}\PYGdefault{o}{.}\PYGdefault{n}{GradientDescentOptimizer}\PYGdefault{p}{(}\PYGdefault{n}{learning\PYGdefaultZus{}rate}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{eta}\PYGdefault{p}{)}\PYGdefault{o}{.}\PYGdefault{n}{minimize}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{loss}\PYGdefault{p}{,} \PYGdefault{n}{global\PYGdefaultZus{}step}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{global\PYGdefaultZus{}step}\PYGdefault{p}{)} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{weight\PYGdefaultZus{}variable}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{,} \PYGdefault{n}{shape}\PYGdefault{p}{,} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{):} - \PYGdefault{n}{initial} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{truncated\PYGdefaultZus{}normal}\PYGdefault{p}{(}\PYGdefault{n}{shape}\PYGdefault{p}{,} \PYGdefault{n}{stddev}\PYGdefault{o}{=}\PYGdefault{l+m+mf}{0.1}\PYGdefault{p}{)} - \PYGdefault{k}{return} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{Variable}\PYGdefault{p}{(}\PYGdefault{n}{initial}\PYGdefault{p}{,} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{n}{name}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{dtype}\PYGdefault{p}{)} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{bias\PYGdefaultZus{}variable}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{,} \PYGdefault{n}{shape}\PYGdefault{p}{,} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{float32}\PYGdefault{p}{):} - \PYGdefault{n}{initial} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{constant}\PYGdefault{p}{(}\PYGdefault{l+m+mf}{0.1}\PYGdefault{p}{,} \PYGdefault{n}{shape}\PYGdefault{o}{=}\PYGdefault{n}{shape}\PYGdefault{p}{)} - \PYGdefault{k}{return} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{Variable}\PYGdefault{p}{(}\PYGdefault{n}{initial}\PYGdefault{p}{,} \PYGdefault{n}{name}\PYGdefault{o}{=}\PYGdefault{n}{name}\PYGdefault{p}{,} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n}{dtype}\PYGdefault{p}{)} - - \PYGdefault{k}{def} \PYGdefault{n+nf}{fit}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{p}{):} - \PYGdefault{n}{data\PYGdefaultZus{}indices} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{)} - - \PYGdefault{k}{with} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{Session}\PYGdefault{p}{()} \PYGdefault{k}{as} \PYGdefault{n}{sess}\PYGdefault{p}{:} - \PYGdefault{n}{sess}\PYGdefault{o}{.}\PYGdefault{n}{run}\PYGdefault{p}{(}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{global\PYGdefaultZus{}variables\PYGdefaultZus{}initializer}\PYGdefault{p}{())} - \PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{epochs}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{iterations}\PYGdefault{p}{):} - \PYGdefault{n}{chosen\PYGdefaultZus{}datapoints} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{choice}\PYGdefault{p}{(}\PYGdefault{n}{data\PYGdefaultZus{}indices}\PYGdefault{p}{,} \PYGdefault{n}{size}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} \PYGdefault{n}{replace}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{False}\PYGdefault{p}{)} - \PYGdefault{n}{batch\PYGdefaultZus{}X}\PYGdefault{p}{,} \PYGdefault{n}{batch\PYGdefaultZus{}Y} \PYGdefault{o}{=} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{[}\PYGdefault{n}{chosen\PYGdefaultZus{}datapoints}\PYGdefault{p}{],} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{[}\PYGdefault{n}{chosen\PYGdefaultZus{}datapoints}\PYGdefault{p}{]} - - \PYGdefault{n}{sess}\PYGdefault{o}{.}\PYGdefault{n}{run}\PYGdefault{p}{([}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{loss}\PYGdefault{p}{,} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{optimizer}\PYGdefault{p}{],} - \PYGdefault{n}{feed\PYGdefaultZus{}dict}\PYGdefault{o}{=}\PYGdefault{p}{\PYGdefaultZob{}}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{X}\PYGdefault{p}{:} \PYGdefault{n}{batch\PYGdefaultZus{}X}\PYGdefault{p}{,} - \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{Y}\PYGdefault{p}{:} \PYGdefault{n}{batch\PYGdefaultZus{}Y}\PYGdefault{p}{\PYGdefaultZcb{})} - \PYGdefault{n}{accuracy} \PYGdefault{o}{=} \PYGdefault{n}{sess}\PYGdefault{o}{.}\PYGdefault{n}{run}\PYGdefault{p}{(}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{accuracy}\PYGdefault{p}{,} - \PYGdefault{n}{feed\PYGdefaultZus{}dict}\PYGdefault{o}{=}\PYGdefault{p}{\PYGdefaultZob{}}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{X}\PYGdefault{p}{:} \PYGdefault{n}{batch\PYGdefaultZus{}X}\PYGdefault{p}{,} - \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{Y}\PYGdefault{p}{:} \PYGdefault{n}{batch\PYGdefaultZus{}Y}\PYGdefault{p}{\PYGdefaultZcb{})} - \PYGdefault{n}{step} \PYGdefault{o}{=} \PYGdefault{n}{sess}\PYGdefault{o}{.}\PYGdefault{n}{run}\PYGdefault{p}{(}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{global\PYGdefaultZus{}step}\PYGdefault{p}{)} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{train\PYGdefaultZus{}loss}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{train\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{sess}\PYGdefault{o}{.}\PYGdefault{n}{run}\PYGdefault{p}{([}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{loss}\PYGdefault{p}{,} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{accuracy}\PYGdefault{p}{],} - \PYGdefault{n}{feed\PYGdefaultZus{}dict}\PYGdefault{o}{=}\PYGdefault{p}{\PYGdefaultZob{}}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{X}\PYGdefault{p}{:} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} - \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{Y}\PYGdefault{p}{:} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{\PYGdefaultZcb{})} - - \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{test\PYGdefaultZus{}loss}\PYGdefault{p}{,} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{test\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{sess}\PYGdefault{o}{.}\PYGdefault{n}{run}\PYGdefault{p}{([}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{loss}\PYGdefault{p}{,} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{accuracy}\PYGdefault{p}{],} - \PYGdefault{n}{feed\PYGdefaultZus{}dict}\PYGdefault{o}{=}\PYGdefault{p}{\PYGdefaultZob{}}\PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{X}\PYGdefault{p}{:} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} - \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{Y}\PYGdefault{p}{:} \PYGdefault{n+nb+bp}{self}\PYGdefault{o}{.}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{\PYGdefaultZcb{})} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/5674DC583991D33CB701DC26C374A7D94234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/5674DC583991D33CB701DC26C374A7D94234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 627bf0fed..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/5674DC583991D33CB701DC26C374A7D94234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,9 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{epochs} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{batch\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{50} -\PYGdefault{n}{n\PYGdefaultZus{}categories} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{10} -\PYGdefault{n}{eta\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\PYGdefault{n}{lmbd\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/611AA00BADB5FE57A27680962EA01B324234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/611AA00BADB5FE57A27680962EA01B324234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 6599bd190..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/611AA00BADB5FE57A27680962EA01B324234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,17 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{DNN\PYGdefaultZus{}tf} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)),} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n+nb}{object}\PYGdefault{p}{)} - -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{eta} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j}\PYGdefault{p}{,} \PYGdefault{n}{lmbd} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{n}{DNN} \PYGdefault{o}{=} \PYGdefault{n}{NeuralNetworkTensorflow}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} - \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{n}{epochs}\PYGdefault{p}{,} \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{o}{=}\PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{fit}\PYGdefault{p}{()} - - \PYGdefault{n}{DNN\PYGdefaultZus{}tf}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{DNN} - - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Learning rate = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Lambda = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test accuracy: }\PYGdefault{l+s+si}{\PYGdefaultZpc{}.3f}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}} \PYGdefault{o}{\PYGdefaultZpc{}} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/72543A9CB7A748EAAE812564510313DF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/72543A9CB7A748EAAE812564510313DF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 550606a69..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/72543A9CB7A748EAAE812564510313DF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,35 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} visual representation of grid search} -\PYGdefault{c+c1}{\PYGdefaultZsh{} uses seaborn heatmap, you can also do this with matplotlib imshow} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{seaborn} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{sns} - -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{set}\PYGdefault{p}{()} - -\PYGdefault{n}{train\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} -\PYGdefault{n}{test\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} - -\PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{k}{for} \PYGdefault{n}{j} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{n}{dnn} \PYGdefault{o}{=} \PYGdefault{n}{DNN\PYGdefaultZus{}numpy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} - - \PYGdefault{n}{train\PYGdefaultZus{}pred} \PYGdefault{o}{=} \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{)} - \PYGdefault{n}{test\PYGdefaultZus{}pred} \PYGdefault{o}{=} \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{)} - - \PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{train\PYGdefaultZus{}pred}\PYGdefault{p}{)} - \PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{test\PYGdefaultZus{}pred}\PYGdefault{p}{)} - - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Training Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/7F4C3D222E8A3446EC779D8AF22905814234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/7F4C3D222E8A3446EC779D8AF22905814234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 52ca2488f..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/7F4C3D222E8A3446EC779D8AF22905814234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,3 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{pip3} \PYGdefault{n}{install} \PYGdefault{n}{tensorflow} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/852D2EC506B5A30B409F5280E1666AE64234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/852D2EC506B5A30B409F5280E1666AE64234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index ba2f76898..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/852D2EC506B5A30B409F5280E1666AE64234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,18 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{sklearn.neural\PYGdefaultZus{}network} \PYGdefault{k+kn}{import} \PYGdefault{n}{MLPClassifier} -\PYGdefault{c+c1}{\PYGdefaultZsh{} store models for later use} -\PYGdefault{n}{DNN\PYGdefaultZus{}scikit} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)),} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n+nb}{object}\PYGdefault{p}{)} - -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{eta} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j}\PYGdefault{p}{,} \PYGdefault{n}{lmbd} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{n}{dnn} \PYGdefault{o}{=} \PYGdefault{n}{MLPClassifier}\PYGdefault{p}{(}\PYGdefault{n}{hidden\PYGdefaultZus{}layer\PYGdefaultZus{}sizes}\PYGdefault{o}{=}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{),} \PYGdefault{n}{activation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}logistic\PYGdefaultZsq{}}\PYGdefault{p}{,} - \PYGdefault{n}{alpha}\PYGdefault{o}{=}\PYGdefault{n}{lmbd}\PYGdefault{p}{,} \PYGdefault{n}{learning\PYGdefaultZus{}rate\PYGdefaultZus{}init}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{max\PYGdefaultZus{}iter}\PYGdefault{o}{=}\PYGdefault{n}{epochs}\PYGdefault{p}{)} - \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{fit}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{)} - - \PYGdefault{n}{DNN\PYGdefaultZus{}scikit}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{dnn} - - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Learning rate = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Lambda = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Accuracy score on test set: \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{score}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{))} - \PYGdefault{k}{print}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/8871809795A90C7990F0293FE5FDBB4C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/8871809795A90C7990F0293FE5FDBB4C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 222e4ae4e..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/8871809795A90C7990F0293FE5FDBB4C4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,14 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.utils} \PYGdefault{k+kn}{import} \PYGdefault{n}{to\PYGdefaultZus{}categorical} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{sklearn.model\PYGdefaultZus{}selection} \PYGdefault{k+kn}{import} \PYGdefault{n}{train\PYGdefaultZus{}test\PYGdefaultZus{}split} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} representation of labels} -\PYGdefault{n}{labels} \PYGdefault{o}{=} \PYGdefault{n}{to\PYGdefaultZus{}categorical}\PYGdefault{p}{(}\PYGdefault{n}{labels}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} split into train and test data} -\PYGdefault{c+c1}{\PYGdefaultZsh{} one\PYGdefaultZhy{}liner from scikit\PYGdefaultZhy{}learn library} -\PYGdefault{n}{train\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mf}{0.8} -\PYGdefault{n}{test\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{1} \PYGdefault{o}{\PYGdefaultZhy{}} \PYGdefault{n}{train\PYGdefaultZus{}size} -\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test} \PYGdefault{o}{=} \PYGdefault{n}{train\PYGdefaultZus{}test\PYGdefaultZus{}split}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{p}{,} \PYGdefault{n}{labels}\PYGdefault{p}{,} \PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{train\PYGdefaultZus{}size}\PYGdefault{p}{,} - \PYGdefault{n}{test\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{test\PYGdefaultZus{}size}\PYGdefault{p}{)} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/8C55CD49CDB4373D11001CA844ABD1874234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/8C55CD49CDB4373D11001CA844ABD1874234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index cc15a9d83..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/8C55CD49CDB4373D11001CA844ABD1874234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,22 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{eta\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\PYGdefault{n}{lmbd\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\PYGdefault{c+c1}{\PYGdefaultZsh{} store the models for later use} -\PYGdefault{n}{DNN\PYGdefaultZus{}numpy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)),} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n+nb}{object}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} grid search} -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{eta} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j}\PYGdefault{p}{,} \PYGdefault{n}{lmbd} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{n}{dnn} \PYGdefault{o}{=} \PYGdefault{n}{NeuralNetwork}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train\PYGdefaultZus{}onehot}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{o}{=}\PYGdefault{n}{lmbd}\PYGdefault{p}{,} \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{n}{epochs}\PYGdefault{p}{,} \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{o}{=}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{o}{=}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{)} - \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{train}\PYGdefault{p}{()} - - \PYGdefault{n}{DNN\PYGdefaultZus{}numpy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{dnn} - - \PYGdefault{n}{test\PYGdefaultZus{}predict} \PYGdefault{o}{=} \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{)} - - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Learning rate = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Lambda = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Accuracy score on test set: \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{test\PYGdefaultZus{}predict}\PYGdefault{p}{))} - \PYGdefault{k}{print}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/A17309BF5FA276F8C2FDE6BADB2726244234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/A17309BF5FA276F8C2FDE6BADB2726244234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index b24c53635..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/A17309BF5FA276F8C2FDE6BADB2726244234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,73 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{l+s+sd}{\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}The sigmoid function (or the logistic curve) is a} -\PYGdefault{l+s+sd}{function that takes any real number, z, and outputs a number (0,1).} -\PYGdefault{l+s+sd}{It is useful in neural networks for assigning weights on a relative scale.} -\PYGdefault{l+s+sd}{The value z is the weighted sum of parameters involved in the learning algorithm.\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}} - -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{numpy} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{matplotlib.pyplot} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{plt} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{math} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{mt} - -\PYGdefault{n}{z} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{o}{.}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} -\PYGdefault{n}{sigma\PYGdefaultZus{}fn} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{vectorize}\PYGdefault{p}{(}\PYGdefault{k}{lambda} \PYGdefault{n}{z}\PYGdefault{p}{:} \PYGdefault{l+m+mi}{1}\PYGdefault{o}{/}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{1}\PYGdefault{o}{+}\PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{exp}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{n}{z}\PYGdefault{p}{)))} -\PYGdefault{n}{sigma} \PYGdefault{o}{=} \PYGdefault{n}{sigma\PYGdefaultZus{}fn}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{)} - -\PYGdefault{n}{fig} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{figure}\PYGdefault{p}{()} -\PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{fig}\PYGdefault{o}{.}\PYGdefault{n}{add\PYGdefaultZus{}subplot}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{111}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{plot}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{,} \PYGdefault{n}{sigma}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mf}{0.1}\PYGdefault{p}{,} \PYGdefault{l+m+mf}{1.1}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{grid}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}z\PYGdefaultZsq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}sigmoid function\PYGdefaultZsq{}}\PYGdefault{p}{)} - -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{l+s+sd}{\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}Step Function\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}} -\PYGdefault{n}{z} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{o}{.}\PYGdefault{l+m+mo}{02}\PYGdefault{p}{)} -\PYGdefault{n}{step\PYGdefaultZus{}fn} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{vectorize}\PYGdefault{p}{(}\PYGdefault{k}{lambda} \PYGdefault{n}{z}\PYGdefault{p}{:} \PYGdefault{l+m+mf}{1.0} \PYGdefault{k}{if} \PYGdefault{n}{z} \PYGdefault{o}{\PYGdefaultZgt{}=} \PYGdefault{l+m+mf}{0.0} \PYGdefault{k}{else} \PYGdefault{l+m+mf}{0.0}\PYGdefault{p}{)} -\PYGdefault{n}{step} \PYGdefault{o}{=} \PYGdefault{n}{step\PYGdefaultZus{}fn}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{)} - -\PYGdefault{n}{fig} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{figure}\PYGdefault{p}{()} -\PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{fig}\PYGdefault{o}{.}\PYGdefault{n}{add\PYGdefaultZus{}subplot}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{111}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{plot}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{,} \PYGdefault{n}{step}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mf}{0.5}\PYGdefault{p}{,} \PYGdefault{l+m+mf}{1.5}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{grid}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}z\PYGdefaultZsq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}step function\PYGdefaultZsq{}}\PYGdefault{p}{)} - -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{l+s+sd}{\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}Sine Function\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}} -\PYGdefault{n}{z} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{2}\PYGdefault{o}{*}\PYGdefault{n}{mt}\PYGdefault{o}{.}\PYGdefault{n}{pi}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{2}\PYGdefault{o}{*}\PYGdefault{n}{mt}\PYGdefault{o}{.}\PYGdefault{n}{pi}\PYGdefault{p}{,} \PYGdefault{l+m+mf}{0.1}\PYGdefault{p}{)} -\PYGdefault{n}{t} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{sin}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{)} - -\PYGdefault{n}{fig} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{figure}\PYGdefault{p}{()} -\PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{fig}\PYGdefault{o}{.}\PYGdefault{n}{add\PYGdefaultZus{}subplot}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{111}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{plot}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{,} \PYGdefault{n}{t}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mf}{1.0}\PYGdefault{p}{,} \PYGdefault{l+m+mf}{1.0}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{2}\PYGdefault{o}{*}\PYGdefault{n}{mt}\PYGdefault{o}{.}\PYGdefault{n}{pi}\PYGdefault{p}{,}\PYGdefault{l+m+mi}{2}\PYGdefault{o}{*}\PYGdefault{n}{mt}\PYGdefault{o}{.}\PYGdefault{n}{pi}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{grid}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}z\PYGdefaultZsq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}sine function\PYGdefaultZsq{}}\PYGdefault{p}{)} - -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{l+s+sd}{\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}Plots a graph of the squashing function used by a rectified linear} -\PYGdefault{l+s+sd}{unit\PYGdefaultZdq{}\PYGdefaultZdq{}\PYGdefaultZdq{}} -\PYGdefault{n}{z} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{2}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{2}\PYGdefault{p}{,} \PYGdefault{o}{.}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} -\PYGdefault{n}{zero} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{))} -\PYGdefault{n}{y} \PYGdefault{o}{=} \PYGdefault{n}{numpy}\PYGdefault{o}{.}\PYGdefault{n}{max}\PYGdefault{p}{([}\PYGdefault{n}{zero}\PYGdefault{p}{,} \PYGdefault{n}{z}\PYGdefault{p}{],} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - -\PYGdefault{n}{fig} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{figure}\PYGdefault{p}{()} -\PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{fig}\PYGdefault{o}{.}\PYGdefault{n}{add\PYGdefaultZus{}subplot}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{111}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{plot}\PYGdefault{p}{(}\PYGdefault{n}{z}\PYGdefault{p}{,} \PYGdefault{n}{y}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mf}{2.0}\PYGdefault{p}{,} \PYGdefault{l+m+mf}{2.0}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlim}\PYGdefault{p}{([}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mf}{2.0}\PYGdefault{p}{,} \PYGdefault{l+m+mf}{2.0}\PYGdefault{p}{])} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{grid}\PYGdefault{p}{(}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}z\PYGdefaultZsq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}Rectified linear unit\PYGdefaultZsq{}}\PYGdefault{p}{)} - -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/A31C7A0824D277E4D150BB687CB3D27D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/A31C7A0824D277E4D150BB687CB3D27D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 776545250..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/A31C7A0824D277E4D150BB687CB3D27D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,33 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} optional} -\PYGdefault{c+c1}{\PYGdefaultZsh{} visual representation of grid search} -\PYGdefault{c+c1}{\PYGdefaultZsh{} uses seaborn heatmap, could probably do this in matplotlib} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{seaborn} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{sns} - -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{set}\PYGdefault{p}{()} - -\PYGdefault{n}{train\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} -\PYGdefault{n}{test\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} - -\PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{k}{for} \PYGdefault{n}{j} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{n}{DNN} \PYGdefault{o}{=} \PYGdefault{n}{DNN\PYGdefaultZus{}keras}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} - - \PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{evaluate}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{)[}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{]} - \PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{evaluate}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{)[}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{]} - - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Training Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/A9997A77EBA561AC5DC567F426BBE5A24234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/A9997A77EBA561AC5DC567F426BBE5A24234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 65f43ea1a..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/A9997A77EBA561AC5DC567F426BBE5A24234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,36 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.models} \PYGdefault{k+kn}{import} \PYGdefault{n}{Sequential} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.layers.convolutional} \PYGdefault{k+kn}{import} \PYGdefault{n}{Conv2D} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.layers.convolutional} \PYGdefault{k+kn}{import} \PYGdefault{n}{MaxPooling2D} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.layers} \PYGdefault{k+kn}{import} \PYGdefault{n}{Flatten} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.layers} \PYGdefault{k+kn}{import} \PYGdefault{n}{Dense} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.regularizers} \PYGdefault{k+kn}{import} \PYGdefault{n}{l2} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{keras.optimizers} \PYGdefault{k+kn}{import} \PYGdefault{n}{SGD} - -\PYGdefault{k}{def} \PYGdefault{n+nf}{create\PYGdefaultZus{}convolutional\PYGdefaultZus{}neural\PYGdefaultZus{}network\PYGdefaultZus{}keras}\PYGdefault{p}{(}\PYGdefault{n}{input\PYGdefaultZus{}shape}\PYGdefault{p}{,} \PYGdefault{n}{receptive\PYGdefaultZus{}field}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}filters}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}connected}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} - \PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{):} - \PYGdefault{n}{model} \PYGdefault{o}{=} \PYGdefault{n}{Sequential}\PYGdefault{p}{()} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{Conv2D}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}filters}\PYGdefault{p}{,} \PYGdefault{p}{(}\PYGdefault{n}{receptive\PYGdefaultZus{}field}\PYGdefault{p}{,} \PYGdefault{n}{receptive\PYGdefaultZus{}field}\PYGdefault{p}{),} \PYGdefault{n}{input\PYGdefaultZus{}shape}\PYGdefault{o}{=}\PYGdefault{n}{input\PYGdefaultZus{}shape}\PYGdefault{p}{,} \PYGdefault{n}{padding}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}same\PYGdefaultZsq{}}\PYGdefault{p}{,} - \PYGdefault{n}{activation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}relu\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{kernel\PYGdefaultZus{}regularizer}\PYGdefault{o}{=}\PYGdefault{n}{l2}\PYGdefault{p}{(}\PYGdefault{n}{lmbd}\PYGdefault{p}{)))} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{MaxPooling2D}\PYGdefault{p}{(}\PYGdefault{n}{pool\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{2}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{2}\PYGdefault{p}{)))} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{Flatten}\PYGdefault{p}{())} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{Dense}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}connected}\PYGdefault{p}{,} \PYGdefault{n}{activation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}relu\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{kernel\PYGdefaultZus{}regularizer}\PYGdefault{o}{=}\PYGdefault{n}{l2}\PYGdefault{p}{(}\PYGdefault{n}{lmbd}\PYGdefault{p}{)))} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{add}\PYGdefault{p}{(}\PYGdefault{n}{Dense}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} \PYGdefault{n}{activation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}softmax\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{kernel\PYGdefaultZus{}regularizer}\PYGdefault{o}{=}\PYGdefault{n}{l2}\PYGdefault{p}{(}\PYGdefault{n}{lmbd}\PYGdefault{p}{)))} - - \PYGdefault{n}{sgd} \PYGdefault{o}{=} \PYGdefault{n}{SGD}\PYGdefault{p}{(}\PYGdefault{n}{lr}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{n}{model}\PYGdefault{o}{.}\PYGdefault{n}{compile}\PYGdefault{p}{(}\PYGdefault{n}{loss}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}categorical\PYGdefaultZus{}crossentropy\PYGdefaultZsq{}}\PYGdefault{p}{,} \PYGdefault{n}{optimizer}\PYGdefault{o}{=}\PYGdefault{n}{sgd}\PYGdefault{p}{,} \PYGdefault{n}{metrics}\PYGdefault{o}{=}\PYGdefault{p}{[}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}accuracy\PYGdefaultZsq{}}\PYGdefault{p}{])} - - \PYGdefault{k}{return} \PYGdefault{n}{model} - -\PYGdefault{n}{epochs} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{batch\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{input\PYGdefaultZus{}shape} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{:}\PYGdefault{l+m+mi}{4}\PYGdefault{p}{]} -\PYGdefault{n}{receptive\PYGdefaultZus{}field} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{3} -\PYGdefault{n}{n\PYGdefaultZus{}filters} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{10} -\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}connected} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{50} -\PYGdefault{n}{n\PYGdefaultZus{}categories} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{10} - -\PYGdefault{n}{eta\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\PYGdefault{n}{lmbd\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/A9F4724DDF4E401785DB96FBEB7EEAE44234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/A9F4724DDF4E401785DB96FBEB7EEAE44234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index b02c0a802..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/A9F4724DDF4E401785DB96FBEB7EEAE44234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,3 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{conda} \PYGdefault{n}{install} \PYGdefault{n}{keras} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/C630EE0722FF84E219F35E63A84603CF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/C630EE0722FF84E219F35E63A84603CF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 36e29983d..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/C630EE0722FF84E219F35E63A84603CF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,37 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} setup the feed\PYGdefaultZhy{}forward pass, subscript h = hidden layer} - -\PYGdefault{k}{def} \PYGdefault{n+nf}{sigmoid}\PYGdefault{p}{(}\PYGdefault{n}{x}\PYGdefault{p}{):} - \PYGdefault{k}{return} \PYGdefault{l+m+mi}{1}\PYGdefault{o}{/}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{1} \PYGdefault{o}{+} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{exp}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{n}{x}\PYGdefault{p}{))} - -\PYGdefault{k}{def} \PYGdefault{n+nf}{feed\PYGdefaultZus{}forward}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{):} - \PYGdefault{c+c1}{\PYGdefaultZsh{} weighted sum of inputs to the hidden layer} - \PYGdefault{n}{z\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{,} \PYGdefault{n}{hidden\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n}{hidden\PYGdefaultZus{}bias} - \PYGdefault{c+c1}{\PYGdefaultZsh{} activation in the hidden layer} - \PYGdefault{n}{a\PYGdefaultZus{}h} \PYGdefault{o}{=} \PYGdefault{n}{sigmoid}\PYGdefault{p}{(}\PYGdefault{n}{z\PYGdefaultZus{}h}\PYGdefault{p}{)} - - \PYGdefault{c+c1}{\PYGdefaultZsh{} weighted sum of inputs to the output layer} - \PYGdefault{n}{z\PYGdefaultZus{}o} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{matmul}\PYGdefault{p}{(}\PYGdefault{n}{a\PYGdefaultZus{}h}\PYGdefault{p}{,} \PYGdefault{n}{output\PYGdefaultZus{}weights}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{n}{output\PYGdefaultZus{}bias} - \PYGdefault{c+c1}{\PYGdefaultZsh{} softmax output} - \PYGdefault{c+c1}{\PYGdefaultZsh{} axis 0 holds each input and axis 1 the probabilities of each category} - \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{exp}\PYGdefault{p}{(}\PYGdefault{n}{z\PYGdefaultZus{}o}\PYGdefault{p}{)} - \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{exp\PYGdefaultZus{}term} \PYGdefault{o}{/} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{exp\PYGdefaultZus{}term}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{n}{keepdims}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{)} - - \PYGdefault{k}{return} \PYGdefault{n}{probabilities} - -\PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{feed\PYGdefaultZus{}forward}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{)} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}probabilities = (n\PYGdefaultZus{}inputs, n\PYGdefaultZus{}categories) = \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{probabilities}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{))} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}probability that image 0 is in category 0,1,2,...,9 = }\PYGdefault{l+s+se}{\PYGdefaultZbs{}n}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{probabilities}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{]))} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}probabilities sum up to: \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{probabilities}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{]}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{()))} -\PYGdefault{k}{print}\PYGdefault{p}{()} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} we obtain a prediction by taking the class with the highest likelihood} -\PYGdefault{k}{def} \PYGdefault{n+nf}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{):} - \PYGdefault{n}{probabilities} \PYGdefault{o}{=} \PYGdefault{n}{feed\PYGdefaultZus{}forward}\PYGdefault{p}{(}\PYGdefault{n}{X}\PYGdefault{p}{)} - \PYGdefault{k}{return} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{argmax}\PYGdefault{p}{(}\PYGdefault{n}{probabilities}\PYGdefault{p}{,} \PYGdefault{n}{axis}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} - -\PYGdefault{n}{predictions} \PYGdefault{o}{=} \PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{)} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}predictions = (n\PYGdefaultZus{}inputs) = \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{predictions}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{))} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}prediction for image 0: \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{predictions}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{]))} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}correct label for image 0: \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{]))} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/C79E225D867290EAA7053C20C84DEC1F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/C79E225D867290EAA7053C20C84DEC1F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 75f06b629..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/C79E225D867290EAA7053C20C84DEC1F4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,26 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{epochs} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{batch\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{n\PYGdefaultZus{}filters} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{10} -\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}connected} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{50} -\PYGdefault{n}{n\PYGdefaultZus{}categories} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{10} - -\PYGdefault{n}{eta\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\PYGdefault{n}{lmbd\PYGdefaultZus{}vals} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{logspace}\PYGdefault{p}{(}\PYGdefault{o}{\PYGdefaultZhy{}}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{7}\PYGdefault{p}{)} -\PYGdefault{n}{CNN\PYGdefaultZus{}tf} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)),} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n+nb}{object}\PYGdefault{p}{)} - -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{eta} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j}\PYGdefault{p}{,} \PYGdefault{n}{lmbd} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{n}{CNN} \PYGdefault{o}{=} \PYGdefault{n}{ConvolutionalNeuralNetworkTensorflow}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}filters}\PYGdefault{o}{=}\PYGdefault{n}{n\PYGdefaultZus{}filters}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}connected}\PYGdefault{o}{=}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}connected}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{o}{=}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{n}{epochs}\PYGdefault{p}{,} \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} - \PYGdefault{n}{eta}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{o}{=}\PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{n}{CNN}\PYGdefault{o}{.}\PYGdefault{n}{fit}\PYGdefault{p}{()} - - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Learning rate = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Lambda = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test accuracy: }\PYGdefault{l+s+si}{\PYGdefaultZpc{}.3f}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}} \PYGdefault{o}{\PYGdefaultZpc{}} \PYGdefault{n}{CNN}\PYGdefault{o}{.}\PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{()} - - \PYGdefault{n}{CNN\PYGdefaultZus{}tf}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{CNN} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/CC3B313264565E4CB32DE0B3DBE155114234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/CC3B313264565E4CB32DE0B3DBE155114234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 5b41f6a23..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/CC3B313264565E4CB32DE0B3DBE155114234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,17 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{DNN\PYGdefaultZus{}keras} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)),} \PYGdefault{n}{dtype}\PYGdefault{o}{=}\PYGdefault{n+nb}{object}\PYGdefault{p}{)} - -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{eta} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{k}{for} \PYGdefault{n}{j}\PYGdefault{p}{,} \PYGdefault{n}{lmbd} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{):} - \PYGdefault{n}{DNN} \PYGdefault{o}{=} \PYGdefault{n}{create\PYGdefaultZus{}neural\PYGdefaultZus{}network\PYGdefaultZus{}keras}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer1}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}neurons\PYGdefaultZus{}layer2}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{,} - \PYGdefault{n}{eta}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{o}{=}\PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{fit}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{n}{epochs}\PYGdefault{p}{,} \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} \PYGdefault{n}{verbose}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - \PYGdefault{n}{scores} \PYGdefault{o}{=} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{evaluate}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{)} - - \PYGdefault{n}{DNN\PYGdefaultZus{}keras}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{DNN} - - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Learning rate = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Lambda = \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{p}{)} - \PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test accuracy: }\PYGdefault{l+s+si}{\PYGdefaultZpc{}.3f}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}} \PYGdefault{o}{\PYGdefaultZpc{}} \PYGdefault{n}{scores}\PYGdefault{p}{[}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{])} - \PYGdefault{k}{print}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/D2D0331B4BFCC5E4D11BBB2D4F8220D44234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/D2D0331B4BFCC5E4D11BBB2D4F8220D44234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index c54d88870..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/D2D0331B4BFCC5E4D11BBB2D4F8220D44234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,17 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} building our neural network} - -\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}features} \PYGdefault{o}{=} \PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{o}{.}\PYGdefault{n}{shape} -\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{50} -\PYGdefault{n}{n\PYGdefaultZus{}categories} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{10} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} we make the weights normally distributed using numpy.random.randn} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} weights and bias in the hidden layer} -\PYGdefault{n}{hidden\PYGdefaultZus{}weights} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{randn}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}features}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{)} -\PYGdefault{n}{hidden\PYGdefaultZus{}bias} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{l+m+mf}{0.01} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} weights and bias in the output layer} -\PYGdefault{n}{output\PYGdefaultZus{}weights} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{randn}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{)} -\PYGdefault{n}{output\PYGdefaultZus{}bias} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{)} \PYGdefault{o}{+} \PYGdefault{l+m+mf}{0.01} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/D52EBE0E5A069D53B1692A08908FA9AE4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/D52EBE0E5A069D53B1692A08908FA9AE4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 4370c5ea3..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/D52EBE0E5A069D53B1692A08908FA9AE4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,3 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{conda} \PYGdefault{n}{install} \PYGdefault{n}{tensorflow} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/DDF61950C6DED1D1F6693063F7FFF3AF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/DDF61950C6DED1D1F6693063F7FFF3AF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 150ad51dc..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/DDF61950C6DED1D1F6693063F7FFF3AF4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,36 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} optional} -\PYGdefault{c+c1}{\PYGdefaultZsh{} visual representation of grid search} -\PYGdefault{c+c1}{\PYGdefaultZsh{} uses seaborn heatmap, could probably do this in matplotlib} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{seaborn} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{sns} - -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{set}\PYGdefault{p}{()} - -\PYGdefault{n}{train\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} -\PYGdefault{n}{test\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} - -\PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{k}{for} \PYGdefault{n}{j} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{n}{dnn} \PYGdefault{o}{=} \PYGdefault{n}{DNN\PYGdefaultZus{}scikit}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} - - \PYGdefault{n}{train\PYGdefaultZus{}pred} \PYGdefault{o}{=} \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{)} - \PYGdefault{n}{test\PYGdefaultZus{}pred} \PYGdefault{o}{=} \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{)} - - \PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{train\PYGdefaultZus{}pred}\PYGdefault{p}{)} - \PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{test\PYGdefaultZus{}pred}\PYGdefault{p}{)} - - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Training Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/DE149C594620EAD33F825D2C744DFD2D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/DE149C594620EAD33F825D2C744DFD2D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 4d684e423..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/DE149C594620EAD33F825D2C744DFD2D4234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,42 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} import necessary packages} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{numpy} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{np} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{matplotlib.pyplot} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{plt} -\PYGdefault{k+kn}{from} \PYGdefault{n+nn}{sklearn} \PYGdefault{k+kn}{import} \PYGdefault{n}{datasets} - - -\PYGdefault{c+c1}{\PYGdefaultZsh{} ensure the same random numbers appear every time} -\PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{seed}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{0}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} display images in notebook} -\PYGdefault{o}{\PYGdefaultZpc{}}\PYGdefault{n}{matplotlib} \PYGdefault{n}{inline} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{rcParams}\PYGdefault{p}{[}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}figure.figsize\PYGdefaultZsq{}}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{12}\PYGdefault{p}{,}\PYGdefault{l+m+mi}{12}\PYGdefault{p}{)} - - -\PYGdefault{c+c1}{\PYGdefaultZsh{} download MNIST dataset} -\PYGdefault{n}{digits} \PYGdefault{o}{=} \PYGdefault{n}{datasets}\PYGdefault{o}{.}\PYGdefault{n}{load\PYGdefaultZus{}digits}\PYGdefault{p}{()} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} define inputs and labels} -\PYGdefault{n}{inputs} \PYGdefault{o}{=} \PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{images} -\PYGdefault{n}{labels} \PYGdefault{o}{=} \PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{target} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} RGB images have a depth of 3} -\PYGdefault{c+c1}{\PYGdefaultZsh{} our images are grayscale so they should have a depth of 1} -\PYGdefault{n}{inputs} \PYGdefault{o}{=} \PYGdefault{n}{inputs}\PYGdefault{p}{[:,:,:,}\PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{newaxis}\PYGdefault{p}{]} - -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}inputs = (n\PYGdefaultZus{}inputs, pixel\PYGdefaultZus{}width, pixel\PYGdefaultZus{}height, depth) = \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{))} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}labels = (n\PYGdefaultZus{}inputs) = \PYGdefaultZdq{}} \PYGdefault{o}{+} \PYGdefault{n+nb}{str}\PYGdefault{p}{(}\PYGdefault{n}{labels}\PYGdefault{o}{.}\PYGdefault{n}{shape}\PYGdefault{p}{))} - - -\PYGdefault{c+c1}{\PYGdefaultZsh{} choose some random images to display} -\PYGdefault{n}{n\PYGdefaultZus{}inputs} \PYGdefault{o}{=} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{inputs}\PYGdefault{p}{)} -\PYGdefault{n}{indices} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{arange}\PYGdefault{p}{(}\PYGdefault{n}{n\PYGdefaultZus{}inputs}\PYGdefault{p}{)} -\PYGdefault{n}{random\PYGdefaultZus{}indices} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{random}\PYGdefault{o}{.}\PYGdefault{n}{choice}\PYGdefault{p}{(}\PYGdefault{n}{indices}\PYGdefault{p}{,} \PYGdefault{n}{size}\PYGdefault{o}{=}\PYGdefault{l+m+mi}{5}\PYGdefault{p}{)} - -\PYGdefault{k}{for} \PYGdefault{n}{i}\PYGdefault{p}{,} \PYGdefault{n}{image} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{enumerate}\PYGdefault{p}{(}\PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{images}\PYGdefault{p}{[}\PYGdefault{n}{random\PYGdefaultZus{}indices}\PYGdefault{p}{]):} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplot}\PYGdefault{p}{(}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{5}\PYGdefault{p}{,} \PYGdefault{n}{i}\PYGdefault{o}{+}\PYGdefault{l+m+mi}{1}\PYGdefault{p}{)} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{axis}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}off\PYGdefaultZsq{}}\PYGdefault{p}{)} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{imshow}\PYGdefault{p}{(}\PYGdefault{n}{image}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{cm}\PYGdefault{o}{.}\PYGdefault{n}{gray\PYGdefaultZus{}r}\PYGdefault{p}{,} \PYGdefault{n}{interpolation}\PYGdefault{o}{=}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}nearest\PYGdefaultZsq{}}\PYGdefault{p}{)} - \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Label: }\PYGdefault{l+s+si}{\PYGdefaultZpc{}d}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}} \PYGdefault{o}{\PYGdefaultZpc{}} \PYGdefault{n}{digits}\PYGdefault{o}{.}\PYGdefault{n}{target}\PYGdefault{p}{[}\PYGdefault{n}{random\PYGdefaultZus{}indices}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{]])} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/E2A52A7C269FA0C93FD4D1225C8F6EE04234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/E2A52A7C269FA0C93FD4D1225C8F6EE04234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 7a252bb78..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/E2A52A7C269FA0C93FD4D1225C8F6EE04234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,18 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{n}{epochs} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} -\PYGdefault{n}{batch\PYGdefaultZus{}size} \PYGdefault{o}{=} \PYGdefault{l+m+mi}{100} - -\PYGdefault{n}{dnn} \PYGdefault{o}{=} \PYGdefault{n}{NeuralNetwork}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}train}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}train\PYGdefaultZus{}onehot}\PYGdefault{p}{,} \PYGdefault{n}{eta}\PYGdefault{o}{=}\PYGdefault{n}{eta}\PYGdefault{p}{,} \PYGdefault{n}{lmbd}\PYGdefault{o}{=}\PYGdefault{n}{lmbd}\PYGdefault{p}{,} \PYGdefault{n}{epochs}\PYGdefault{o}{=}\PYGdefault{n}{epochs}\PYGdefault{p}{,} \PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{o}{=}\PYGdefault{n}{batch\PYGdefaultZus{}size}\PYGdefault{p}{,} - \PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{o}{=}\PYGdefault{n}{n\PYGdefaultZus{}hidden\PYGdefaultZus{}neurons}\PYGdefault{p}{,} \PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{o}{=}\PYGdefault{n}{n\PYGdefaultZus{}categories}\PYGdefault{p}{)} -\PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{train}\PYGdefault{p}{()} -\PYGdefault{n}{test\PYGdefaultZus{}predict} \PYGdefault{o}{=} \PYGdefault{n}{dnn}\PYGdefault{o}{.}\PYGdefault{n}{predict}\PYGdefault{p}{(}\PYGdefault{n}{X\PYGdefaultZus{}test}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} accuracy score from scikit library} -\PYGdefault{k}{print}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Accuracy score on test set: \PYGdefaultZdq{}}\PYGdefault{p}{,} \PYGdefault{n}{accuracy\PYGdefaultZus{}score}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{test\PYGdefaultZus{}predict}\PYGdefault{p}{))} - -\PYGdefault{c+c1}{\PYGdefaultZsh{} equivalent in numpy} -\PYGdefault{k}{def} \PYGdefault{n+nf}{accuracy\PYGdefaultZus{}score\PYGdefaultZus{}numpy}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{,} \PYGdefault{n}{Y\PYGdefaultZus{}pred}\PYGdefault{p}{):} - \PYGdefault{k}{return} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{sum}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test} \PYGdefault{o}{==} \PYGdefault{n}{Y\PYGdefaultZus{}pred}\PYGdefault{p}{)} \PYGdefault{o}{/} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{Y\PYGdefaultZus{}test}\PYGdefault{p}{)} - -\PYGdefault{c+c1}{\PYGdefaultZsh{}print(\PYGdefaultZdq{}Accuracy score on test set: \PYGdefaultZdq{}, accuracy\PYGdefaultZus{}score\PYGdefaultZus{}numpy(Y\PYGdefaultZus{}test, test\PYGdefaultZus{}predict))} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/E916A642A79FF5FC8777234EAA3679174234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/E916A642A79FF5FC8777234EAA3679174234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 1a99615e3..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/E916A642A79FF5FC8777234EAA3679174234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,6 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} optional} -\PYGdefault{c+c1}{\PYGdefaultZsh{} we can use log files to visualize our graph in Tensorboard} -\PYGdefault{n}{writer} \PYGdefault{o}{=} \PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{summary}\PYGdefault{o}{.}\PYGdefault{n}{FileWriter}\PYGdefault{p}{(}\PYGdefault{l+s+s1}{\PYGdefaultZsq{}logs/\PYGdefaultZsq{}}\PYGdefault{p}{)} -\PYGdefault{n}{writer}\PYGdefault{o}{.}\PYGdefault{n}{add\PYGdefaultZus{}graph}\PYGdefault{p}{(}\PYGdefault{n}{tf}\PYGdefault{o}{.}\PYGdefault{n}{get\PYGdefaultZus{}default\PYGdefaultZus{}graph}\PYGdefault{p}{())} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/FD25B0786C79644DB8919AE1039CB0C34234D82CF76A2559C5DD98A6DB88BEC4.pygtex b/doc/src/NeuralNet/_minted-NeuralNet/FD25B0786C79644DB8919AE1039CB0C34234D82CF76A2559C5DD98A6DB88BEC4.pygtex deleted file mode 100644 index 40781aec5..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/FD25B0786C79644DB8919AE1039CB0C34234D82CF76A2559C5DD98A6DB88BEC4.pygtex +++ /dev/null @@ -1,33 +0,0 @@ -\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8}] -\PYGdefault{c+c1}{\PYGdefaultZsh{} optional} -\PYGdefault{c+c1}{\PYGdefaultZsh{} visual representation of grid search} -\PYGdefault{c+c1}{\PYGdefaultZsh{} uses seaborn heatmap, could probably do this in matplotlib} -\PYGdefault{k+kn}{import} \PYGdefault{n+nn}{seaborn} \PYGdefault{k+kn}{as} \PYGdefault{n+nn}{sns} - -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{set}\PYGdefault{p}{()} - -\PYGdefault{n}{train\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} -\PYGdefault{n}{test\PYGdefaultZus{}accuracy} \PYGdefault{o}{=} \PYGdefault{n}{np}\PYGdefault{o}{.}\PYGdefault{n}{zeros}\PYGdefault{p}{((}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{),} \PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)))} - -\PYGdefault{k}{for} \PYGdefault{n}{i} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{eta\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{k}{for} \PYGdefault{n}{j} \PYGdefault{o+ow}{in} \PYGdefault{n+nb}{range}\PYGdefault{p}{(}\PYGdefault{n+nb}{len}\PYGdefault{p}{(}\PYGdefault{n}{lmbd\PYGdefaultZus{}vals}\PYGdefault{p}{)):} - \PYGdefault{n}{DNN} \PYGdefault{o}{=} \PYGdefault{n}{DNN\PYGdefaultZus{}tf}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} - - \PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{train\PYGdefaultZus{}accuracy} - \PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{[}\PYGdefault{n}{i}\PYGdefault{p}{][}\PYGdefault{n}{j}\PYGdefault{p}{]} \PYGdefault{o}{=} \PYGdefault{n}{DNN}\PYGdefault{o}{.}\PYGdefault{n}{test\PYGdefaultZus{}accuracy} - - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{train\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Training Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} - -\PYGdefault{n}{fig}\PYGdefault{p}{,} \PYGdefault{n}{ax} \PYGdefault{o}{=} \PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{subplots}\PYGdefault{p}{(}\PYGdefault{n}{figsize} \PYGdefault{o}{=} \PYGdefault{p}{(}\PYGdefault{l+m+mi}{10}\PYGdefault{p}{,} \PYGdefault{l+m+mi}{10}\PYGdefault{p}{))} -\PYGdefault{n}{sns}\PYGdefault{o}{.}\PYGdefault{n}{heatmap}\PYGdefault{p}{(}\PYGdefault{n}{test\PYGdefaultZus{}accuracy}\PYGdefault{p}{,} \PYGdefault{n}{annot}\PYGdefault{o}{=}\PYGdefault{n+nb+bp}{True}\PYGdefault{p}{,} \PYGdefault{n}{ax}\PYGdefault{o}{=}\PYGdefault{n}{ax}\PYGdefault{p}{,} \PYGdefault{n}{cmap}\PYGdefault{o}{=}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}viridis\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}title}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}Test Accuracy\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}ylabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}eta\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{ax}\PYGdefault{o}{.}\PYGdefault{n}{set\PYGdefaultZus{}xlabel}\PYGdefault{p}{(}\PYGdefault{l+s+s2}{\PYGdefaultZdq{}\PYGdefaultZdl{}\PYGdefaultZbs{}lambda\PYGdefaultZdl{}\PYGdefaultZdq{}}\PYGdefault{p}{)} -\PYGdefault{n}{plt}\PYGdefault{o}{.}\PYGdefault{n}{show}\PYGdefault{p}{()} -\end{Verbatim} diff --git a/doc/src/NeuralNet/_minted-NeuralNet/default.pygstyle b/doc/src/NeuralNet/_minted-NeuralNet/default.pygstyle deleted file mode 100644 index 08728b372..000000000 --- a/doc/src/NeuralNet/_minted-NeuralNet/default.pygstyle +++ /dev/null @@ -1,101 +0,0 @@ - -\makeatletter -\def\PYGdefault@reset{\let\PYGdefault@it=\relax \let\PYGdefault@bf=\relax% - \let\PYGdefault@ul=\relax \let\PYGdefault@tc=\relax% - \let\PYGdefault@bc=\relax \let\PYGdefault@ff=\relax} -\def\PYGdefault@tok#1{\csname PYGdefault@tok@#1\endcsname} -\def\PYGdefault@toks#1+{\ifx\relax#1\empty\else% - \PYGdefault@tok{#1}\expandafter\PYGdefault@toks\fi} -\def\PYGdefault@do#1{\PYGdefault@bc{\PYGdefault@tc{\PYGdefault@ul{% - \PYGdefault@it{\PYGdefault@bf{\PYGdefault@ff{#1}}}}}}} -\def\PYGdefault#1#2{\PYGdefault@reset\PYGdefault@toks#1+\relax+\PYGdefault@do{#2}} - -\expandafter\def\csname PYGdefault@tok@w\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.73,0.73}{##1}}} -\expandafter\def\csname PYGdefault@tok@c\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@cp\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.74,0.48,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@k\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@kp\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@kt\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.69,0.00,0.25}{##1}}} -\expandafter\def\csname PYGdefault@tok@o\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@ow\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.67,0.13,1.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@nb\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@nf\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@nc\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@nn\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@ne\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.82,0.25,0.23}{##1}}} -\expandafter\def\csname PYGdefault@tok@nv\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} -\expandafter\def\csname PYGdefault@tok@no\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.53,0.00,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@nl\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.63,0.63,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@ni\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.60,0.60,0.60}{##1}}} -\expandafter\def\csname PYGdefault@tok@na\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.49,0.56,0.16}{##1}}} -\expandafter\def\csname PYGdefault@tok@nt\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@nd\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.67,0.13,1.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@s\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@sd\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@si\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.40,0.53}{##1}}} -\expandafter\def\csname PYGdefault@tok@se\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.40,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@sr\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.40,0.53}{##1}}} -\expandafter\def\csname PYGdefault@tok@ss\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} -\expandafter\def\csname PYGdefault@tok@sx\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@m\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@gh\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@gu\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.50,0.00,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@gd\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.63,0.00,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@gi\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.63,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@gr\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{1.00,0.00,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@ge\endcsname{\let\PYGdefault@it=\textit} -\expandafter\def\csname PYGdefault@tok@gs\endcsname{\let\PYGdefault@bf=\textbf} -\expandafter\def\csname PYGdefault@tok@gp\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@go\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.53,0.53,0.53}{##1}}} -\expandafter\def\csname PYGdefault@tok@gt\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.27,0.87}{##1}}} -\expandafter\def\csname PYGdefault@tok@err\endcsname{\def\PYGdefault@bc##1{\setlength{\fboxsep}{0pt}\fcolorbox[rgb]{1.00,0.00,0.00}{1,1,1}{\strut ##1}}} -\expandafter\def\csname PYGdefault@tok@kc\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@kd\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@kn\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@kr\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@bp\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@fm\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}} -\expandafter\def\csname PYGdefault@tok@vc\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} -\expandafter\def\csname PYGdefault@tok@vg\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} -\expandafter\def\csname PYGdefault@tok@vi\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} -\expandafter\def\csname PYGdefault@tok@vm\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} -\expandafter\def\csname PYGdefault@tok@sa\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@sb\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@sc\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@dl\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@s2\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@sh\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@s1\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} -\expandafter\def\csname PYGdefault@tok@mb\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@mf\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@mh\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@mi\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@il\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@mo\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} -\expandafter\def\csname PYGdefault@tok@ch\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@cm\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@cpf\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@c1\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} -\expandafter\def\csname PYGdefault@tok@cs\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} - -\def\PYGdefaultZbs{\char`\\} -\def\PYGdefaultZus{\char`\_} -\def\PYGdefaultZob{\char`\{} -\def\PYGdefaultZcb{\char`\}} -\def\PYGdefaultZca{\char`\^} -\def\PYGdefaultZam{\char`\&} -\def\PYGdefaultZlt{\char`\<} -\def\PYGdefaultZgt{\char`\>} -\def\PYGdefaultZsh{\char`\#} -\def\PYGdefaultZpc{\char`\%} -\def\PYGdefaultZdl{\char`\$} -\def\PYGdefaultZhy{\char`\-} -\def\PYGdefaultZsq{\char`\'} -\def\PYGdefaultZdq{\char`\"} -\def\PYGdefaultZti{\char`\~} -% for compatibility with earlier versions -\def\PYGdefaultZat{@} -\def\PYGdefaultZlb{[} -\def\PYGdefaultZrb{]} -\makeatother - diff --git a/doc/src/NeuralNet/ipynb-NeuralNet-src.tar.gz b/doc/src/NeuralNet/ipynb-NeuralNet-src.tar.gz deleted file mode 100644 index b3dd04a5793087757b4c015525fa9f262861d696..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88051 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
9>V}MzIm>f6r@na7W_&wgdz1F#6 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 diff --git a/doc/src/NeuralNet/reveal.js/.gitignore b/doc/src/NeuralNet/reveal.js/.gitignore deleted file mode 100644 index e7b4f216a..000000000 --- a/doc/src/NeuralNet/reveal.js/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -.idea/ -*.iml -*.iws -*.eml -out/ -.DS_Store -.svn -log/*.log -tmp/** -node_modules/ -.sass-cache -css/reveal.min.css -js/reveal.min.js \ No newline at end of file diff --git a/doc/src/NeuralNet/reveal.js/.travis.yml b/doc/src/NeuralNet/reveal.js/.travis.yml deleted file mode 100644 index ec3b27d5d..000000000 --- a/doc/src/NeuralNet/reveal.js/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js -node_js: - - 4 -before_script: - - npm install -g grunt-cli -after_script: - - grunt retire diff --git a/doc/src/NeuralNet/reveal.js/CONTRIBUTING.md b/doc/src/NeuralNet/reveal.js/CONTRIBUTING.md deleted file mode 100644 index c2091e88f..000000000 --- a/doc/src/NeuralNet/reveal.js/CONTRIBUTING.md +++ /dev/null @@ -1,23 +0,0 @@ -## Contributing - -Please keep the [issue tracker](http://github.com/hakimel/reveal.js/issues) limited to **bug reports**, **feature requests** and **pull requests**. - - -### Personal Support -If you have personal support or setup questions the best place to ask those are [StackOverflow](http://stackoverflow.com/questions/tagged/reveal.js). - - -### Bug Reports -When reporting a bug make sure to include information about which browser and operating system you are on as well as the necessary steps to reproduce the issue. If possible please include a link to a sample presentation where the bug can be tested. - - -### Pull Requests -- Should follow the coding style of the file you work in, most importantly: - - Tabs to indent - - Single-quoted strings -- Should be made towards the **dev branch** -- Should be submitted from a feature/topic branch (not your master) - - -### Plugins -Please do not submit plugins as pull requests. They should be maintained in their own separate repository. More information here: https://github.com/hakimel/reveal.js/wiki/Plugin-Guidelines diff --git a/doc/src/NeuralNet/reveal.js/Gruntfile.js b/doc/src/NeuralNet/reveal.js/Gruntfile.js deleted file mode 100644 index b257e8f32..000000000 --- a/doc/src/NeuralNet/reveal.js/Gruntfile.js +++ /dev/null @@ -1,140 +0,0 @@ -/* global module:false */ -module.exports = function(grunt) { - var port = grunt.option('port') || 8000; - // Project configuration - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - meta: { - banner: - '/*!\n' + - ' * reveal.js <%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd, HH:MM") %>)\n' + - ' * http://lab.hakim.se/reveal-js\n' + - ' * MIT licensed\n' + - ' *\n' + - ' * Copyright (C) 2014 Hakim El Hattab, http://hakim.se\n' + - ' */' - }, - - qunit: { - files: [ 'test/*.html' ] - }, - - uglify: { - options: { - banner: '<%= meta.banner %>\n' - }, - build: { - src: 'js/reveal.js', - dest: 'js/reveal.min.js' - } - }, - - cssmin: { - compress: { - files: { - 'css/reveal.min.css': [ 'css/reveal.css' ] - } - } - }, - - sass: { - main: { - files: { - 'css/theme/darkgray.css': 'css/theme/source/darkgray.scss', - 'css/theme/beigesmall.css': 'css/theme/source/beigesmall.scss', - 'css/theme/cbc.css': 'css/theme/source/cbc.scss', - 'css/theme/default.css': 'css/theme/source/default.scss', - 'css/theme/beige.css': 'css/theme/source/beige.scss', - 'css/theme/night.css': 'css/theme/source/night.scss', - 'css/theme/serif.css': 'css/theme/source/serif.scss', - 'css/theme/simple.css': 'css/theme/source/simple.scss', - 'css/theme/sky.css': 'css/theme/source/sky.scss', - 'css/theme/moon.css': 'css/theme/source/moon.scss', - 'css/theme/solarized.css': 'css/theme/source/solarized.scss', - 'css/theme/blood.css': 'css/theme/source/blood.scss' - } - } - }, - - jshint: { - options: { - curly: false, - eqeqeq: true, - immed: true, - latedef: true, - newcap: true, - noarg: true, - sub: true, - undef: true, - eqnull: true, - browser: true, - expr: true, - globals: { - head: false, - module: false, - console: false, - unescape: false - } - }, - files: [ 'Gruntfile.js', 'js/reveal.js' ] - }, - - connect: { - server: { - options: { - port: port, - base: '.' - } - } - }, - - zip: { - 'reveal-js-presentation.zip': [ - 'index.html', - 'css/**', - 'js/**', - 'lib/**', - 'images/**', - 'plugin/**' - ] - }, - - watch: { - main: { - files: [ 'Gruntfile.js', 'js/reveal.js', 'css/reveal.css' ], - tasks: 'default' - }, - theme: { - files: [ 'css/theme/source/*.scss', 'css/theme/template/*.scss' ], - tasks: 'themes' - } - } - - }); - - // Dependencies - grunt.loadNpmTasks( 'grunt-contrib-qunit' ); - grunt.loadNpmTasks( 'grunt-contrib-jshint' ); - grunt.loadNpmTasks( 'grunt-contrib-cssmin' ); - grunt.loadNpmTasks( 'grunt-contrib-uglify' ); - grunt.loadNpmTasks( 'grunt-contrib-watch' ); - grunt.loadNpmTasks( 'grunt-contrib-sass' ); - grunt.loadNpmTasks( 'grunt-contrib-connect' ); - grunt.loadNpmTasks( 'grunt-zip' ); - - // Default task - grunt.registerTask( 'default', [ 'jshint', 'cssmin', 'uglify', 'qunit' ] ); - - // Theme task - grunt.registerTask( 'themes', [ 'sass' ] ); - - // Package presentation to archive - grunt.registerTask( 'package', [ 'default', 'zip' ] ); - - // Serve presentation locally - grunt.registerTask( 'serve', [ 'connect', 'watch' ] ); - - // Run tests - grunt.registerTask( 'test', [ 'jshint', 'qunit' ] ); - -}; diff --git a/doc/src/NeuralNet/reveal.js/LICENSE b/doc/src/NeuralNet/reveal.js/LICENSE deleted file mode 100644 index c3e6e5fd6..000000000 --- a/doc/src/NeuralNet/reveal.js/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2017 Hakim El Hattab, http://hakim.se, and reveal.js contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/doc/src/NeuralNet/reveal.js/README.md b/doc/src/NeuralNet/reveal.js/README.md deleted file mode 100644 index f2ab6ca88..000000000 --- a/doc/src/NeuralNet/reveal.js/README.md +++ /dev/null @@ -1,1246 +0,0 @@ -# reveal.js [![Build Status](https://travis-ci.org/hakimel/reveal.js.svg?branch=master)](https://travis-ci.org/hakimel/reveal.js) Slides - -A framework for easily creating beautiful presentations using HTML. [Check out the live demo](http://revealjs.com/). - -reveal.js comes with a broad range of features including [nested slides](https://github.com/hakimel/reveal.js#markup), [Markdown contents](https://github.com/hakimel/reveal.js#markdown), [PDF export](https://github.com/hakimel/reveal.js#pdf-export), [speaker notes](https://github.com/hakimel/reveal.js#speaker-notes) and a [JavaScript API](https://github.com/hakimel/reveal.js#api). There's also a fully featured visual editor and platform for sharing reveal.js presentations at [slides.com](https://slides.com?ref=github). - -## Table of contents -- [Online Editor](#online-editor) -- [Instructions](#instructions) - - [Markup](#markup) - - [Markdown](#markdown) - - [Element Attributes](#element-attributes) - - [Slide Attributes](#slide-attributes) -- [Configuration](#configuration) -- [Presentation Size](#presentation-size) -- [Dependencies](#dependencies) -- [Ready Event](#ready-event) -- [Auto-sliding](#auto-sliding) -- [Keyboard Bindings](#keyboard-bindings) -- [Touch Navigation](#touch-navigation) -- [Lazy Loading](#lazy-loading) -- [API](#api) - - [Slide Changed Event](#slide-changed-event) - - [Presentation State](#presentation-state) - - [Slide States](#slide-states) - - [Slide Backgrounds](#slide-backgrounds) - - [Parallax Background](#parallax-background) - - [Slide Transitions](#slide-transitions) - - [Internal links](#internal-links) - - [Fragments](#fragments) - - [Fragment events](#fragment-events) - - [Code syntax highlighting](#code-syntax-highlighting) - - [Slide number](#slide-number) - - [Overview mode](#overview-mode) - - [Fullscreen mode](#fullscreen-mode) - - [Embedded media](#embedded-media) - - [Stretching elements](#stretching-elements) - - [postMessage API](#postmessage-api) -- [PDF Export](#pdf-export) -- [Theming](#theming) -- [Speaker Notes](#speaker-notes) - - [Share and Print Speaker Notes](#share-and-print-speaker-notes) - - [Server Side Speaker Notes](#server-side-speaker-notes) -- [Multiplexing](#multiplexing) - - [Master presentation](#master-presentation) - - [Client presentation](#client-presentation) - - [Socket.io server](#socketio-server) -- [MathJax](#mathjax) -- [Installation](#installation) - - [Basic setup](#basic-setup) - - [Full setup](#full-setup) - - [Folder Structure](#folder-structure) -- [License](#license) - -#### More reading -- [Changelog](https://github.com/hakimel/reveal.js/releases): Up-to-date version history. -- [Examples](https://github.com/hakimel/reveal.js/wiki/Example-Presentations): Presentations created with reveal.js, add your own! -- [Browser Support](https://github.com/hakimel/reveal.js/wiki/Browser-Support): Explanation of browser support and fallbacks. -- [Plugins](https://github.com/hakimel/reveal.js/wiki/Plugins,-Tools-and-Hardware): A list of plugins that can be used to extend reveal.js. - -## Online Editor - -Presentations are written using HTML or Markdown but there's also an online editor for those of you who prefer a graphical interface. Give it a try at [https://slides.com](https://slides.com?ref=github). - - -## Instructions - -### Markup - -Here's a barebones example of a fully working reveal.js presentation: -```html - - - - - - -

-
-
Slide 1
-
Slide 2
-
-
- - - - -``` - -The presentation markup hierarchy needs to be `.reveal > .slides > section` where the `section` represents one slide and can be repeated indefinitely. If you place multiple `section` elements inside of another `section` they will be shown as vertical slides. The first of the vertical slides is the "root" of the others (at the top), and will be included in the horizontal sequence. For example: - -```html -
-
-
Single Horizontal Slide
-
-
Vertical Slide 1
-
Vertical Slide 2
-
-
-
-``` - -### Markdown - -It's possible to write your slides using Markdown. To enable Markdown, add the `data-markdown` attribute to your `
` elements and wrap the contents in a ` -
-``` - -#### External Markdown - -You can write your content as a separate file and have reveal.js load it at runtime. Note the separator arguments which determine how slides are delimited in the external file: the `data-separator` attribute defines a regular expression for horizontal slides (defaults to `^\r?\n---\r?\n$`, a newline-bounded horizontal rule) and `data-separator-vertical` defines vertical slides (disabled by default). The `data-separator-notes` attribute is a regular expression for specifying the beginning of the current slide's speaker notes (defaults to `note:`). The `data-charset` attribute is optional and specifies which charset to use when loading the external file. - -When used locally, this feature requires that reveal.js [runs from a local web server](#full-setup). The following example customises all available options: - -```html -
- -
-``` - -#### Element Attributes - -Special syntax (in html comment) is available for adding attributes to Markdown elements. This is useful for fragments, amongst other things. - -```html -
- -
-``` - -#### Slide Attributes - -Special syntax (in html comment) is available for adding attributes to the slide `
` elements generated by your Markdown. - -```html -
- -
-``` - -#### Configuring *marked* - -We use [marked](https://github.com/chjj/marked) to parse Markdown. To customise marked's rendering, you can pass in options when [configuring Reveal](#configuration): - -```javascript -Reveal.initialize({ - // Options which are passed into marked - // See https://github.com/chjj/marked#options-1 - markdown: { - smartypants: true - } -}); -``` - -### Configuration - -At the end of your page you need to initialize reveal by running the following code. Note that all config values are optional and will default as specified below. - -```javascript -Reveal.initialize({ - - // Display presentation control arrows - controls: true, - - // Help the user learn the controls by providing hints, for example by - // bouncing the down arrow when they first encounter a vertical slide - controlsTutorial: true, - - // Determines where controls appear, "edges" or "bottom-right" - controlsLayout: 'bottom-right', - - // Visibility rule for backwards navigation arrows; "faded", "hidden" - // or "visible" - controlsBackArrows: 'faded', - - // Display a presentation progress bar - progress: true, - - // Set default timing of 2 minutes per slide - defaultTiming: 120, - - // Display the page number of the current slide - slideNumber: false, - - // Push each slide change to the browser history - history: false, - - // Enable keyboard shortcuts for navigation - keyboard: true, - - // Enable the slide overview mode - overview: true, - - // Vertical centering of slides - center: true, - - // Enables touch navigation on devices with touch input - touch: true, - - // Loop the presentation - loop: false, - - // Change the presentation direction to be RTL - rtl: false, - - // Randomizes the order of slides each time the presentation loads - shuffle: false, - - // Turns fragments on and off globally - fragments: true, - - // Flags if the presentation is running in an embedded mode, - // i.e. contained within a limited portion of the screen - embedded: false, - - // Flags if we should show a help overlay when the questionmark - // key is pressed - help: true, - - // Flags if speaker notes should be visible to all viewers - showNotes: false, - - // Global override for autoplaying embedded media (video/audio/iframe) - // - null: Media will only autoplay if data-autoplay is present - // - true: All media will autoplay, regardless of individual setting - // - false: No media will autoplay, regardless of individual setting - autoPlayMedia: null, - - // Number of milliseconds between automatically proceeding to the - // next slide, disabled when set to 0, this value can be overwritten - // by using a data-autoslide attribute on your slides - autoSlide: 0, - - // Stop auto-sliding after user input - autoSlideStoppable: true, - - // Use this method for navigation when auto-sliding - autoSlideMethod: Reveal.navigateNext, - - // Enable slide navigation via mouse wheel - mouseWheel: false, - - // Hides the address bar on mobile devices - hideAddressBar: true, - - // Opens links in an iframe preview overlay - // Add `data-preview-link` and `data-preview-link="false"` to customise each link - // individually - previewLinks: false, - - // Transition style - transition: 'slide', // none/fade/slide/convex/concave/zoom - - // Transition speed - transitionSpeed: 'default', // default/fast/slow - - // Transition style for full page slide backgrounds - backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom - - // Number of slides away from the current that are visible - viewDistance: 3, - - // Parallax background image - parallaxBackgroundImage: '', // e.g. "'https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg'" - - // Parallax background size - parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px" - - // Number of pixels to move the parallax background per slide - // - Calculated automatically unless specified - // - Set to 0 to disable movement along an axis - parallaxBackgroundHorizontal: null, - parallaxBackgroundVertical: null, - - // The display mode that will be used to show slides - display: 'block' - -}); -``` - - -The configuration can be updated after initialization using the ```configure``` method: - -```javascript -// Turn autoSlide off -Reveal.configure({ autoSlide: 0 }); - -// Start auto-sliding every 5s -Reveal.configure({ autoSlide: 5000 }); -``` - - -### Presentation Size - -All presentations have a normal size, that is the resolution at which they are authored. The framework will automatically scale presentations uniformly based on this size to ensure that everything fits on any given display or viewport. - -See below for a list of configuration options related to sizing, including default values: - -```javascript -Reveal.initialize({ - - ... - - // The "normal" size of the presentation, aspect ratio will be preserved - // when the presentation is scaled to fit different resolutions. Can be - // specified using percentage units. - width: 960, - height: 700, - - // Factor of the display size that should remain empty around the content - margin: 0.1, - - // Bounds for smallest/largest possible scale to apply to content - minScale: 0.2, - maxScale: 1.5 - -}); -``` - -If you wish to disable this behavior and do your own scaling (e.g. using media queries), try these settings: - -```javascript -Reveal.initialize({ - - ... - - width: "100%", - height: "100%", - margin: 0, - minScale: 1, - maxScale: 1 -}); -``` - -### Dependencies - -Reveal.js doesn't _rely_ on any third party scripts to work but a few optional libraries are included by default. These libraries are loaded as dependencies in the order they appear, for example: - -```javascript -Reveal.initialize({ - dependencies: [ - // Cross-browser shim that fully implements classList - https://github.com/eligrey/classList.js/ - { src: 'lib/js/classList.js', condition: function() { return !document.body.classList; } }, - - // Interpret Markdown in
elements - { src: 'plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, - { src: 'plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, - - // Syntax highlight for elements - { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } }, - - // Zoom in and out with Alt+click - { src: 'plugin/zoom-js/zoom.js', async: true }, - - // Speaker notes - { src: 'plugin/notes/notes.js', async: true }, - - // MathJax - { src: 'plugin/math/math.js', async: true } - ] -}); -``` - -You can add your own extensions using the same syntax. The following properties are available for each dependency object: -- **src**: Path to the script to load -- **async**: [optional] Flags if the script should load after reveal.js has started, defaults to false -- **callback**: [optional] Function to execute when the script has loaded -- **condition**: [optional] Function which must return true for the script to be loaded - -To load these dependencies, reveal.js requires [head.js](http://headjs.com/) *(a script loading library)* to be loaded before reveal.js. - -### Ready Event - -A 'ready' event is fired when reveal.js has loaded all non-async dependencies and is ready to start navigating. To check if reveal.js is already 'ready' you can call `Reveal.isReady()`. - -```javascript -Reveal.addEventListener( 'ready', function( event ) { - // event.currentSlide, event.indexh, event.indexv -} ); -``` - -Note that we also add a `.ready` class to the `.reveal` element so that you can hook into this with CSS. - -### Auto-sliding - -Presentations can be configured to progress through slides automatically, without any user input. To enable this you will need to tell the framework how many milliseconds it should wait between slides: - -```javascript -// Slide every five seconds -Reveal.configure({ - autoSlide: 5000 -}); -``` -When this is turned on a control element will appear that enables users to pause and resume auto-sliding. Alternatively, sliding can be paused or resumed by pressing »a« on the keyboard. Sliding is paused automatically as soon as the user starts navigating. You can disable these controls by specifying ```autoSlideStoppable: false``` in your reveal.js config. - -You can also override the slide duration for individual slides and fragments by using the ```data-autoslide``` attribute: - -```html -
-

After 2 seconds the first fragment will be shown.

-

After 10 seconds the next fragment will be shown.

-

Now, the fragment is displayed for 2 seconds before the next slide is shown.

-
-``` - -To override the method used for navigation when auto-sliding, you can specify the ```autoSlideMethod``` setting. To only navigate along the top layer and ignore vertical slides, set this to ```Reveal.navigateRight```. - -Whenever the auto-slide mode is resumed or paused the ```autoslideresumed``` and ```autoslidepaused``` events are fired. - - -### Keyboard Bindings - -If you're unhappy with any of the default keyboard bindings you can override them using the ```keyboard``` config option: - -```javascript -Reveal.configure({ - keyboard: { - 13: 'next', // go to the next slide when the ENTER key is pressed - 27: function() {}, // do something custom when ESC is pressed - 32: null // don't do anything when SPACE is pressed (i.e. disable a reveal.js default binding) - } -}); -``` - -### Touch Navigation - -You can swipe to navigate through a presentation on any touch-enabled device. Horizontal swipes change between horizontal slides, vertical swipes change between vertical slides. If you wish to disable this you can set the `touch` config option to false when initializing reveal.js. - -If there's some part of your content that needs to remain accessible to touch events you'll need to highlight this by adding a `data-prevent-swipe` attribute to the element. One common example where this is useful is elements that need to be scrolled. - - -### Lazy Loading - -When working on presentation with a lot of media or iframe content it's important to load lazily. Lazy loading means that reveal.js will only load content for the few slides nearest to the current slide. The number of slides that are preloaded is determined by the `viewDistance` configuration option. - -To enable lazy loading all you need to do is change your "src" attributes to "data-src" as shown below. This is supported for image, video, audio and iframe elements. Lazy loaded iframes will also unload when the containing slide is no longer visible. - -```html -
- - - -
-``` - - -### API - -The ``Reveal`` object exposes a JavaScript API for controlling navigation and reading state: - -```javascript -// Navigation -Reveal.slide( indexh, indexv, indexf ); -Reveal.left(); -Reveal.right(); -Reveal.up(); -Reveal.down(); -Reveal.prev(); -Reveal.next(); -Reveal.prevFragment(); -Reveal.nextFragment(); - -// Randomize the order of slides -Reveal.shuffle(); - -// Toggle presentation states, optionally pass true/false to force on/off -Reveal.toggleOverview(); -Reveal.togglePause(); -Reveal.toggleAutoSlide(); - -// Shows a help overlay with keyboard shortcuts, optionally pass true/false -// to force on/off -Reveal.toggleHelp(); - -// Change a config value at runtime -Reveal.configure({ controls: true }); - -// Returns the present configuration options -Reveal.getConfig(); - -// Fetch the current scale of the presentation -Reveal.getScale(); - -// Retrieves the previous and current slide elements -Reveal.getPreviousSlide(); -Reveal.getCurrentSlide(); - -Reveal.getIndices(); // { h: 0, v: 0 } } -Reveal.getPastSlideCount(); -Reveal.getProgress(); // (0 == first slide, 1 == last slide) -Reveal.getSlides(); // Array of all slides -Reveal.getTotalSlides(); // total number of slides - -// Returns the speaker notes for the current slide -Reveal.getSlideNotes(); - -// State checks -Reveal.isFirstSlide(); -Reveal.isLastSlide(); -Reveal.isOverview(); -Reveal.isPaused(); -Reveal.isAutoSliding(); -``` - -### Slide Changed Event - -A 'slidechanged' event is fired each time the slide is changed (regardless of state). The event object holds the index values of the current slide as well as a reference to the previous and current slide HTML nodes. - -Some libraries, like MathJax (see [#226](https://github.com/hakimel/reveal.js/issues/226#issuecomment-10261609)), get confused by the transforms and display states of slides. Often times, this can be fixed by calling their update or render function from this callback. - -```javascript -Reveal.addEventListener( 'slidechanged', function( event ) { - // event.previousSlide, event.currentSlide, event.indexh, event.indexv -} ); -``` - -### Presentation State - -The presentation's current state can be fetched by using the `getState` method. A state object contains all of the information required to put the presentation back as it was when `getState` was first called. Sort of like a snapshot. It's a simple object that can easily be stringified and persisted or sent over the wire. - -```javascript -Reveal.slide( 1 ); -// we're on slide 1 - -var state = Reveal.getState(); - -Reveal.slide( 3 ); -// we're on slide 3 - -Reveal.setState( state ); -// we're back on slide 1 -``` - -### Slide States - -If you set ``data-state="somestate"`` on a slide ``
``, "somestate" will be applied as a class on the document element when that slide is opened. This allows you to apply broad style changes to the page based on the active slide. - -Furthermore you can also listen to these changes in state via JavaScript: - -```javascript -Reveal.addEventListener( 'somestate', function() { - // TODO: Sprinkle magic -}, false ); -``` - -### Slide Backgrounds - -Slides are contained within a limited portion of the screen by default to allow them to fit any display and scale uniformly. You can apply full page backgrounds outside of the slide area by adding a ```data-background``` attribute to your ```
``` elements. Four different types of backgrounds are supported: color, image, video and iframe. - -#### Color Backgrounds -All CSS color formats are supported, like rgba() or hsl(). -```html -
-

Color

-
-``` - -#### Image Backgrounds -By default, background images are resized to cover the full page. Available options: - -| Attribute | Default | Description | -| :--------------------------- | :--------- | :---------- | -| data-background-image | | URL of the image to show. GIFs restart when the slide opens. | -| data-background-size | cover | See [background-size](https://developer.mozilla.org/docs/Web/CSS/background-size) on MDN. | -| data-background-position | center | See [background-position](https://developer.mozilla.org/docs/Web/CSS/background-position) on MDN. | -| data-background-repeat | no-repeat | See [background-repeat](https://developer.mozilla.org/docs/Web/CSS/background-repeat) on MDN. | -```html -
-

Image

-
-
-

This background image will be sized to 100px and repeated

-
-``` - -#### Video Backgrounds -Automatically plays a full size video behind the slide. - -| Attribute | Default | Description | -| :--------------------------- | :------ | :---------- | -| data-background-video | | A single video source, or a comma separated list of video sources. | -| data-background-video-loop | false | Flags if the video should play repeatedly. | -| data-background-video-muted | false | Flags if the audio should be muted. | -| data-background-size | cover | Use `cover` for full screen and some cropping or `contain` for letterboxing. | - -```html -
-

Video

-
-``` - -#### Iframe Backgrounds -Embeds a web page as a slide background that covers 100% of the reveal.js width and height. The iframe is in the background layer, behind your slides, and as such it's not possible to interact with it by default. To make your background interactive, you can add the `data-background-interactive` attribute. -```html -
-

Iframe

-
-``` - -#### Background Transitions -Backgrounds transition using a fade animation by default. This can be changed to a linear sliding transition by passing ```backgroundTransition: 'slide'``` to the ```Reveal.initialize()``` call. Alternatively you can set ```data-background-transition``` on any section with a background to override that specific transition. - - -### Parallax Background - -If you want to use a parallax scrolling background, set the first two config properties below when initializing reveal.js (the other two are optional). - -```javascript -Reveal.initialize({ - - // Parallax background image - parallaxBackgroundImage: '', // e.g. "https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg" - - // Parallax background size - parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px" - currently only pixels are supported (don't use % or auto) - - // Number of pixels to move the parallax background per slide - // - Calculated automatically unless specified - // - Set to 0 to disable movement along an axis - parallaxBackgroundHorizontal: 200, - parallaxBackgroundVertical: 50 - -}); -``` - -Make sure that the background size is much bigger than screen size to allow for some scrolling. [View example](http://revealjs.com/?parallaxBackgroundImage=https%3A%2F%2Fs3.amazonaws.com%2Fhakim-static%2Freveal-js%2Freveal-parallax-1.jpg¶llaxBackgroundSize=2100px%20900px). - - - -### Slide Transitions -The global presentation transition is set using the ```transition``` config value. You can override the global transition for a specific slide by using the ```data-transition``` attribute: - -```html -
-

This slide will override the presentation transition and zoom!

-
- -
-

Choose from three transition speeds: default, fast or slow!

-
-``` - -You can also use different in and out transitions for the same slide: - -```html -
- The train goes on … -
-
- and on … -
-
- and stops. -
-
- (Passengers entering and leaving) -
-
- And it starts again. -
-``` - - -### Internal links - -It's easy to link between slides. The first example below targets the index of another slide whereas the second targets a slide with an ID attribute (```
```): - -```html -Link -Link -``` - -You can also add relative navigation links, similar to the built in reveal.js controls, by appending one of the following classes on any element. Note that each element is automatically given an ```enabled``` class when it's a valid navigation route based on the current slide. - -```html - - - - - - -``` - - -### Fragments -Fragments are used to highlight individual elements on a slide. Every element with the class ```fragment``` will be stepped through before moving on to the next slide. Here's an example: http://revealjs.com/#/fragments - -The default fragment style is to start out invisible and fade in. This style can be changed by appending a different class to the fragment: - -```html -
-

grow

-

shrink

-

fade-out

-

fade-up (also down, left and right!)

-

visible only once

-

blue only once

-

highlight-red

-

highlight-green

-

highlight-blue

-
-``` - -Multiple fragments can be applied to the same element sequentially by wrapping it, this will fade in the text on the first step and fade it back out on the second. - -```html -
- - I'll fade in, then out - -
-``` - -The display order of fragments can be controlled using the ```data-fragment-index``` attribute. - -```html -
-

Appears last

-

Appears first

-

Appears second

-
-``` - -### Fragment events - -When a slide fragment is either shown or hidden reveal.js will dispatch an event. - -Some libraries, like MathJax (see #505), get confused by the initially hidden fragment elements. Often times this can be fixed by calling their update or render function from this callback. - -```javascript -Reveal.addEventListener( 'fragmentshown', function( event ) { - // event.fragment = the fragment DOM element -} ); -Reveal.addEventListener( 'fragmenthidden', function( event ) { - // event.fragment = the fragment DOM element -} ); -``` - -### Code syntax highlighting - -By default, Reveal is configured with [highlight.js](https://highlightjs.org/) for code syntax highlighting. To enable syntax highlighting, you'll have to load the highlight plugin ([plugin/highlight/highlight.js](plugin/highlight/highlight.js)) and a highlight.js CSS theme (Reveal comes packaged with the zenburn theme: [lib/css/zenburn.css](lib/css/zenburn.css)). - -Below is an example with clojure code that will be syntax highlighted. When the `data-trim` attribute is present, surrounding whitespace is automatically removed. HTML will be escaped by default. To avoid this, for example if you are using `` to call out a line of code, add the `data-noescape` attribute to the `` element. - -```html -
-

-(def lazy-fib
-  (concat
-   [0 1]
-   ((fn rfib [a b]
-        (lazy-cons (+ a b) (rfib b (+ a b)))) 0 1)))
-	
-
-``` - -### Slide number -If you would like to display the page number of the current slide you can do so using the ```slideNumber``` and ```showSlideNumber``` configuration values. - -```javascript -// Shows the slide number using default formatting -Reveal.configure({ slideNumber: true }); - -// Slide number formatting can be configured using these variables: -// "h.v": horizontal . vertical slide number (default) -// "h/v": horizontal / vertical slide number -// "c": flattened slide number -// "c/t": flattened slide number / total slides -Reveal.configure({ slideNumber: 'c/t' }); - -// Control which views the slide number displays on using the "showSlideNumber" value: -// "all": show on all views (default) -// "speaker": only show slide numbers on speaker notes view -// "print": only show slide numbers when printing to PDF -Reveal.configure({ showSlideNumber: 'speaker' }); - -``` - - -### Overview mode - -Press "Esc" or "o" keys to toggle the overview mode on and off. While you're in this mode, you can still navigate between slides, -as if you were at 1,000 feet above your presentation. The overview mode comes with a few API hooks: - -```javascript -Reveal.addEventListener( 'overviewshown', function( event ) { /* ... */ } ); -Reveal.addEventListener( 'overviewhidden', function( event ) { /* ... */ } ); - -// Toggle the overview mode programmatically -Reveal.toggleOverview(); -``` - - -### Fullscreen mode -Just press »F« on your keyboard to show your presentation in fullscreen mode. Press the »ESC« key to exit fullscreen mode. - - -### Embedded media -Add `data-autoplay` to your media element if you want it to automatically start playing when the slide is shown: - -```html - -``` - -If you want to enable or disable autoplay globally, for all embedded media, you can use the `autoPlayMedia` configuration option. If you set this to `true` ALL media will autoplay regardless of individual `data-autoplay` attributes. If you initialize with `autoPlayMedia: false` NO media will autoplay. - -Note that embedded HTML5 `