├── .gitignore ├── README.md ├── package.json ├── src ├── app.js ├── css │ └── style.less ├── js │ ├── arch.coffee │ ├── color.coffee │ ├── hashy.coffee │ ├── main.coffee │ ├── neural_network.coffee │ ├── neuro_vis.coffee │ ├── simulation.coffee │ └── tour.coffee └── priority.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.s3-sync-cache 2 | /.neurovis 3 | /dist 4 | /node_modules 5 | .DS_Store 6 | sync.js 7 | Makefile -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neurovis 2 | 3 | This is a browser-based interactive neural network visualizer and tutorial. It’s a fun way to develop some intuition about neural nets. It was created using vis.js, and a lot of love. 4 | 5 | - See it live at [neurovis.dataphoric.com](http://neurovis.dataphoric.com). 6 | - Join the [discussion](https://news.ycombinator.com/item?id=10074948) on Hacker News. 7 | 8 | ## Usage 9 | 10 | This app is built using node.js and webpack. You can start a development server using: 11 | 12 | ``` 13 | npm install 14 | npm run server 15 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "content.js", 6 | "dependencies": { 7 | "hopscotch": "^0.2.5", 8 | "jquery": "^2.1.4", 9 | "less": "^2.5.1", 10 | "lodash": "^3.10.1" 11 | }, 12 | "devDependencies": { 13 | "coffee-loader": "^0.7.2", 14 | "coffee-script": "^1.9.3", 15 | "css-loader": "^0.15.6", 16 | "file-loader": "^0.8.4", 17 | "less-loader": "^2.2.0", 18 | "s3-sync": "^0.6.0", 19 | "style-loader": "^0.12.3", 20 | "url-loader": "^0.5.6" 21 | }, 22 | "scripts": { 23 | "test": "echo \"Error: no test specified\" && exit 1", 24 | "start": "webpack", 25 | "server": "webpack-dev-server --content-base dist/ --progress --colors", 26 | "build": "webpack -p", 27 | "sync": "node sync.js" 28 | }, 29 | "author": "", 30 | "license": "ISC" 31 | } 32 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | require('./js/main'); -------------------------------------------------------------------------------- /src/css/style.less: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0; 3 | margin: 0; 4 | height: 100%; 5 | width: 100%; 6 | background: #2c3e50; 7 | font-family: 'Open Sans', sans-serif; 8 | } 9 | 10 | body > main { 11 | display: block; 12 | } 13 | 14 | #network { 15 | width: 100vw; 16 | height: 100vh; 17 | padding: 0; 18 | margin: 0; 19 | } 20 | 21 | #by { 22 | position: fixed; 23 | top: 56px; 24 | left: -40px; 25 | font-family: 'Francois One', sans-serif; 26 | color: rgb(236,240,241); 27 | font-size: 16px; 28 | z-index: 1000; 29 | transform: rotatez(-90deg); 30 | 31 | a { 32 | font-weight: bold; 33 | color: rgb(236,240,241); 34 | text-decoration: none; 35 | font-size: 18px; 36 | } 37 | } 38 | 39 | .vis-network { 40 | position: relative; 41 | overflow: hidden; 42 | touch-action: none; 43 | -webkit-user-select: none; 44 | -webkit-user-drag: none; 45 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 46 | width: 100%; 47 | height: 100%; 48 | } 49 | 50 | #network canvas { 51 | position: relative; 52 | touch-action: none; 53 | -webkit-user-select: none; 54 | -webkit-user-drag: none; 55 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 56 | width: 100%; 57 | height: 100%; 58 | } 59 | 60 | #simulation { 61 | position: absolute; 62 | right: 0; 63 | bottom: 0; 64 | margin: 12px 16px; 65 | } 66 | 67 | #simulation span { 68 | display: block; 69 | width: 100%; 70 | text-align: center; 71 | color: white; 72 | font-size: 80px; 73 | position: absolute; 74 | top: 85px; 75 | line-height: 0; 76 | opacity: 1; 77 | transition: opacity 0.3s; 78 | cursor: pointer; 79 | color: #2c3e50; 80 | } 81 | 82 | #simulation.filled span { 83 | opacity: 0; 84 | } 85 | 86 | #simulation:hover span, #simulation.filled:hover span { 87 | opacity: 1; 88 | } 89 | 90 | #simulation canvas { 91 | background-color: #34495e; 92 | width: 240px; 93 | height: 240px; 94 | cursor: pointer; 95 | border-radius: 4px; 96 | border: 2px solid #34495e; 97 | } 98 | 99 | #tour-anchor { 100 | position: absolute; 101 | top: 0; 102 | left: 0; 103 | } 104 | 105 | body { 106 | overflow: hidden; 107 | } 108 | 109 | #architectures { 110 | position: absolute; 111 | bottom: 0; 112 | left: 0; 113 | z-index: 1; 114 | bottom: -280px; 115 | left: -50px; 116 | list-style: none; 117 | margin: 0; 118 | font-weight: normal; 119 | line-height: 1.8; 120 | letter-spacing: 0.5px; 121 | transition: bottom 0.3s, left 0.3s, border-top-right-radius 0.3s, background-color 0.3s; 122 | background-color: #16a085; 123 | padding: 20px 20px; 124 | border-top-right-radius: 65px; 125 | } 126 | 127 | #architectures:hover { 128 | bottom: 0; 129 | left: 0; 130 | border-top-right-radius: 4px; 131 | background-color: #ecf0f1; 132 | } 133 | 134 | #architectures a { 135 | text-decoration: none; 136 | font-family: 'Open Sans', sans-serif; 137 | color: #2c3e50; 138 | font-size: 18px; 139 | } 140 | 141 | #architectures li { 142 | text-align: right; 143 | } 144 | 145 | #architectures li.toggle { 146 | font-size: 30px; 147 | position: relative; 148 | color:#ecf0f1; 149 | position: relative; 150 | top: -5px; 151 | left: -8px; 152 | max-height: 50px; 153 | overflow: hidden; 154 | transition: max-height 0.2s; 155 | } 156 | 157 | #architectures:hover li.toggle { 158 | max-height: 0px; 159 | } 160 | 161 | #architectures a:hover { 162 | text-decoration: underline; 163 | } 164 | 165 | a.twitter-share-button { 166 | display: none 167 | } 168 | 169 | iframe.twitter-share-button { 170 | position: absolute !important; 171 | top: 0; 172 | right: 0; 173 | margin: 10px; 174 | } 175 | 176 | 177 | /* Vis Overrides */ 178 | 179 | div.vis-network-tooltip { 180 | font-family: 'Open Sans', sans-serif; 181 | font-size: 18px; 182 | color: #ecf0f1; 183 | background-color: #16a085; 184 | border: none; 185 | padding: 8px 16px; 186 | } 187 | 188 | div.vis-network-tooltip small { 189 | font-style: italic; 190 | font-weight: 300; 191 | } 192 | 193 | /* Hopscotch Overrides */ 194 | 195 | div.hopscotch-bubble { 196 | font-family: 'Open Sans', sans-serif; 197 | } 198 | 199 | div.hopscotch-bubble .hopscotch-bubble-container { 200 | background-color: #ecf0f1; 201 | } 202 | 203 | div.hopscotch-bubble h3 { 204 | font-family: 'Open Sans', sans-serif; 205 | font-size: 20px; 206 | color: #2c3e50; 207 | -webkit-font-smoothing: inherit; 208 | line-height: 1.5; 209 | } 210 | 211 | div.hopscotch-bubble a { 212 | color: rgb(22, 160, 133); 213 | } 214 | 215 | div.hopscotch-bubble .hopscotch-content { 216 | font-family: 'Open Sans', sans-serif; 217 | font-size: 16px; 218 | line-height: 1.5; 219 | -webkit-font-smoothing: inherit; 220 | color: #2c3e50; 221 | } 222 | 223 | div.hopscotch-bubble .hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow { 224 | border-bottom-color: #ecf0f1; 225 | } 226 | 227 | div.hopscotch-bubble .hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow { 228 | border-left-color: #ecf0f1; 229 | } 230 | 231 | div.hopscotch-bubble .hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow { 232 | border-right-color: #ecf0f1; 233 | } 234 | 235 | div.hopscotch-bubble .hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow { 236 | border-top-color: #ecf0f1; 237 | } 238 | 239 | div.hopscotch-bubble .hopscotch-bubble-number { 240 | display: none; 241 | } 242 | 243 | div.hopscotch-bubble .hopscotch-bubble-content { 244 | margin: 0; 245 | } 246 | 247 | div.hopscotch-bubble .hopscotch-nav-button.next, 248 | div.hopscotch-bubble .hopscotch-nav-button.prev { 249 | text-shadow: none; 250 | border: none; 251 | background-color: #16a085; 252 | color: #ecf0f1; 253 | background-image: none; 254 | font-size: 16px; 255 | transition: background-color 0.5s; 256 | height: initial; 257 | padding: 6px 16px; 258 | box-shadow: none; 259 | } 260 | 261 | div.hopscotch-bubble .hopscotch-nav-button.next:hover, 262 | div.hopscotch-bubble .hopscotch-nav-button.prev:hover { 263 | background-image: none; 264 | background-color: #2ecc71; 265 | } 266 | 267 | div.hopscotch-bubble .hopscotch-nav-button.prev { 268 | background-color: #7f8c8d; 269 | } 270 | 271 | div.hopscotch-bubble .hopscotch-nav-button.prev:hover { 272 | background-color: #95a5a6; 273 | } 274 | 275 | @media (max-width: 1110px) { 276 | div.hopscotch-bubble .hopscotch-content { 277 | font-size: 14px; 278 | } 279 | div.hopscotch-bubble h3 { 280 | font-size: 18px; 281 | line-height: 1.3; 282 | } 283 | div.hopscotch-bubble .hopscotch-nav-button.next, 284 | div.hopscotch-bubble .hopscotch-nav-button.prev { 285 | padding: 3px 12px; 286 | font-size: 14px; 287 | } 288 | } 289 | 290 | button.hopscotch-bubble-close.hopscotch-close { 291 | display: none; 292 | } -------------------------------------------------------------------------------- /src/js/arch.coffee: -------------------------------------------------------------------------------- 1 | # Arch 2 | # 3 | # Utilities and constants for building network architectures. 4 | 5 | Arch = {} 6 | 7 | Arch.DEFAULT = [[{"bias":-10,"weights":[15,8]},{"bias":5,"weights":[-10,10]},{"bias":-9,"weights":[0,14]}],[{"bias":0,"weights":[12,0,-9]},{"bias":10,"weights":[-15,10,0]},{"bias":-10,"weights":[0,12,-10]},{"bias":5,"weights":[10,0,-20]}],[{"bias":-8,"weights":[-11,18,-20,8]},{"bias":13,"weights":[-15,20,-20,5]}],[{"bias":-15,"weights":[-10,22]}]] 8 | 9 | Arch.OR = [ 10 | [ 11 | { bias: -10, weights: [20, 20] } 12 | ] 13 | ] 14 | 15 | Arch.XOR = [ 16 | [ 17 | { bias: -10, weights: [20, 20] }, 18 | { bias: -30, weights: [20, 20] } 19 | ], 20 | [ 21 | { bias: -10, weights: [20, -20] } 22 | ] 23 | ] 24 | 25 | Arch.A = [ 26 | [ 27 | { bias: -10, weights: [20, 20] }, 28 | { bias: 10, weights: [-10, -10] }, 29 | { bias: 20, weights: [0, 10] } 30 | ], 31 | [ 32 | { bias: 10, weights: [-10, 15, -20] } 33 | ], 34 | ] 35 | 36 | Arch.B = [ 37 | [ 38 | { bias: -10, weights: [20, 20] }, 39 | { bias: 10, weights: [-10, -10] }, 40 | { bias: 0, weights: [0, 10] } 41 | ], 42 | [ 43 | { bias: 0, weights: [10, 0, -10] }, 44 | { bias: 10, weights: [-5, 10, 0] }, 45 | { bias: -10, weights: [0, 5, -10] }, 46 | { bias: 5, weights: [10, 0, -20] } 47 | ], 48 | [ 49 | { bias: -12, weights: [-15, 20, -20, 5] } 50 | ] 51 | ] 52 | 53 | Arch.C = [ 54 | [ 55 | { bias: -10, weights: [20, 15] }, 56 | { bias: 5, weights: [-10, 10] }, 57 | { bias: 0, weights: [0, 10] } 58 | ], 59 | [ 60 | { bias: 0, weights: [10, 0, -10] }, 61 | { bias: 10, weights: [-15, 10, 0] }, 62 | { bias: -10, weights: [0, 5, -10] }, 63 | { bias: 5, weights: [10, 0, -20] } 64 | ], 65 | [ 66 | { bias: -10, weights: [-15, 20, -20, 5] }, 67 | { bias: 10, weights: [-15, 20, -20, 5] }, 68 | ], 69 | [ 70 | { bias: -15, weights: [-10, 20] } 71 | ] 72 | ] 73 | 74 | Arch.D = [ 75 | [ 76 | { bias: -10, weights: [20, 15] }, 77 | { bias: 5, weights: [-10, 10] }, 78 | { bias: 0, weights: [5, 5] }, 79 | { bias: -5, weights: [0, 10] } 80 | ], 81 | [ 82 | { bias: 0, weights: [10, 0, -10, 5] }, 83 | { bias: 10, weights: [-15, 10, 0, -10] }, 84 | { bias: 5, weights: [0, 5, -10, 10] }, 85 | { bias: 5, weights: [10, 0, -20, 0] } 86 | ], 87 | [ 88 | { bias: 0, weights: [10, 0, -10, 5] }, 89 | { bias: 5, weights: [-15, 10, 0, 5] }, 90 | { bias: -10, weights: [0, 5, -10, 5] }, 91 | { bias: 5, weights: [10, 0, -20, 0] } 92 | ], 93 | [ 94 | { bias: -10, weights: [-15, 20, -20, 5] }, 95 | { bias: 10, weights: [-15, 20, -20, 5] }, 96 | ], 97 | [ 98 | { bias: -15, weights: [-10, 20] } 99 | ] 100 | ] 101 | 102 | randomWeight = (sparsity) -> 103 | if sparsity and Math.random() > sparsity 104 | 0 105 | else 106 | # Random between -25 to 25 with 5 step intervals. 107 | # Never have a 0, so we recurse if we do. 108 | (5*Math.round(Math.random()*10) - 25) or randomWeight() 109 | 110 | 111 | Arch.random = -> 112 | nLayers = Math.floor(Math.random()*5 + 1) 113 | layerSizes = (Math.floor(Math.random()*5 + 1 + Math.floor(nLayers/2)) for _ in [0..nLayers]) 114 | Arch.build(layerSizes) 115 | 116 | Arch.build = (layerSizes) -> 117 | arch = [] 118 | for layerSize, l in layerSizes 119 | layer = [] 120 | prevLayerSize = if l == 0 then 2 else layerSizes[l-1] 121 | # Set connection sparsity so that an average of 3 connections per 122 | # node. Except on the output layer, where it doesn't make sense to have sparsity. 123 | sparsity = if l == layerSizes.length-1 then 0 else 3/Math.max(prevLayerSize, layerSize) 124 | for _ in [0...layerSize] 125 | weights = (randomWeight(sparsity) for _ in [0...prevLayerSize]) 126 | layer.push({bias: randomWeight(0.8), weights: weights}) 127 | 128 | arch.push(layer) 129 | 130 | return arch 131 | 132 | Arch.compress = (arch) -> 133 | layerSizes = [] 134 | weights = [] 135 | biases = [] 136 | 137 | for layer, l in arch 138 | if l < arch.length - 1 139 | layerSizes.push(layer.length) 140 | 141 | for node in layer 142 | biases.push(node.bias) 143 | for w in node.weights 144 | weights.push(w) 145 | 146 | 147 | layerSizes + '|' + biases + '|' + weights 148 | 149 | parseList = (list) -> 150 | list.split(',').map((i) -> parseInt(i)) 151 | 152 | Arch.expand = (string) -> 153 | [layerSizes, biases, weights] = string.split('|') 154 | 155 | if layerSizes.length == 0 156 | # Handle 0 hidden layers properly. 157 | layerSizes = [1] 158 | else 159 | layerSizes = parseList(layerSizes) 160 | layerSizes.push(1) 161 | 162 | if !(biases and weights) 163 | console.log('No biases or weights provided. Generating randomly') 164 | return Arch.build(layerSizes) 165 | 166 | biases = parseList(biases) 167 | weights = parseList(weights) 168 | 169 | layers = [] 170 | 171 | w = 0 172 | b = 0 173 | 174 | for layerSize, i in layerSizes 175 | layer = [] 176 | 177 | prevLayerSize = if i == 0 then 2 else layerSizes[i-1] 178 | 179 | for _ in [0...layerSize] 180 | layer.push({bias: biases[b], weights: (weights[j + w] for j in [0...prevLayerSize])}) 181 | b += 1 182 | w += prevLayerSize 183 | 184 | layers.push(layer) 185 | 186 | layers 187 | 188 | module.exports = Arch -------------------------------------------------------------------------------- /src/js/color.coffee: -------------------------------------------------------------------------------- 1 | # Color utilities 2 | 3 | rgba = (values, a) -> 4 | rnd = Math.round 5 | [r, g, b] = values 6 | "rgba(#{rnd(r)}, #{rnd(g)}, #{rnd(b)}, #{a})" 7 | 8 | rgb = (values) -> rgba(values, 1) 9 | 10 | lerp = (a, b, u) -> (1 - u) * a + u * b 11 | 12 | lerpRGB = (a, b, u) -> 13 | lerp(a[i], b[i], u) for i in [0..2] 14 | 15 | lerpColor = (a, b, u) -> 16 | rgba(lerpRGB(a, b, u), 1) 17 | 18 | Color = { 19 | rgba, 20 | rgb, 21 | lerp, 22 | lerpRGB, 23 | lerpColor, 24 | } 25 | 26 | Color.RGB_POSITIVE = [192, 57, 43] 27 | Color.RGB_NEGATIVE = [52, 152, 219] 28 | Color.RGB_BG = [44, 62, 80] 29 | Color.RGB_ACTIVATED = [236, 240, 241] 30 | Color.RGB_HIGHLIGHT = [22, 160, 133] 31 | 32 | Color.POSITIVE = rgb(Color.RGB_POSITIVE) 33 | Color.NEGATIVE = rgb(Color.RGB_NEGATIVE) 34 | Color.ACTIVATED = rgb(Color.RGB_ACTIVATED) 35 | Color.BG = rgb(Color.RGB_BG) 36 | Color.HIGHLIGHT = rgb(Color.RGB_HIGHLIGHT) 37 | 38 | module.exports = Color -------------------------------------------------------------------------------- /src/js/hashy.coffee: -------------------------------------------------------------------------------- 1 | # Hashy 2 | # 3 | # Utilities for working with the URL hash. 4 | 5 | read = -> window.location.hash 6 | 7 | write = (val) -> window.location.hash = val 8 | 9 | set = (key, value) -> 10 | write("#{key}=#{value}") 11 | 12 | has = (key) -> 13 | h = read() 14 | h and h.indexOf(key + '=') >= 0 15 | 16 | get = (key) -> 17 | if has(key) 18 | read().split(key + '=')[1] 19 | 20 | wrap = (key) -> 21 | { 22 | set: (v) -> set(key, v), 23 | get: get.bind(null, key), 24 | } 25 | 26 | module.exports = {set, has, get, wrap} -------------------------------------------------------------------------------- /src/js/main.coffee: -------------------------------------------------------------------------------- 1 | $ = require('jquery') 2 | 3 | Simulation = require('./simulation') 4 | hashy = require('./hashy') 5 | Arch = require('./arch') 6 | NeuroVis = require('./neuro_vis') 7 | Tour = require('./tour') 8 | 9 | KEY_SHIFT = 16 10 | MAX_WEIGHT = 20 11 | INIT_INPUTS = [0, 0] 12 | NETWORK_ID = 'network' 13 | ARCH_KEY = 'arch' 14 | 15 | shiftPressed = false 16 | 17 | clamp = (val, min, max) -> Math.min(max, Math.max(min, val)) 18 | 19 | incrementInput = (index, amount, min, max) -> 20 | inputs = nv.inputs 21 | inputs[index] = clamp(inputs[index] + amount, min, max) 22 | nv.setInputs(inputs) 23 | 24 | handleNodeClick = (id, leftClick) -> 25 | dirn = if leftClick then 1 else -1 26 | step = if shiftPressed then 0.5 else 0.1 27 | d = step * dirn 28 | 29 | index = nv.inputIds().indexOf(id) 30 | 31 | if index >= 0 32 | incrementInput(index, d, 0, 1) 33 | 34 | updateArch = (arch) -> 35 | hash.set(Arch.compress(arch)) 36 | 37 | handleEdgeClick = (edge, leftClick) -> 38 | [from, to] = edge 39 | toNode = nv.findNode(to) 40 | 41 | fromNode = nv.findNode(from) 42 | fromType = nv.nodeType(fromNode) 43 | 44 | [toLayer, toIndex] = nv.nodeCoords(toNode) 45 | 46 | direction = if leftClick then 1 else -1 47 | step = if shiftPressed then 5 else 1 48 | 49 | d = step * direction 50 | 51 | archNode = arch[toLayer][toIndex] 52 | 53 | if fromType == 'normal' or fromType == 'input' 54 | [fromLayer, fromIndex] = nv.nodeCoords(fromNode) 55 | w = archNode.weights[fromIndex] 56 | archNode.weights[fromIndex] = clamp(w + d, -MAX_WEIGHT, MAX_WEIGHT) 57 | 58 | else 59 | # Bias 60 | b = archNode.bias 61 | archNode.bias = clamp(b + d, -MAX_WEIGHT, MAX_WEIGHT) 62 | 63 | updateArch(arch) 64 | 65 | loadArch = -> 66 | # Load the arch from url hash, or default. 67 | if str = hash.get() 68 | Arch.expand(str) 69 | else 70 | Arch.DEFAULT 71 | 72 | onKeydown = (e) -> 73 | if e.which == KEY_SHIFT 74 | shiftPressed = true 75 | 76 | onKeyup = (e) -> 77 | if e.which == KEY_SHIFT 78 | shiftPressed = false 79 | 80 | isTourEvent = (e) -> 81 | e.target.className.indexOf('hopscotch') >= 0 82 | 83 | onMouseDown = (e) -> 84 | if isTourEvent(e) then return 85 | 86 | [x, y] = [e.clientX, e.clientY] 87 | 88 | node = nv.nodeAt(x, y) 89 | edge = nv.edgeAt(x, y) 90 | 91 | isLeft = e.which == 1 92 | 93 | if node 94 | handleNodeClick(node, isLeft) 95 | else if edge 96 | handleEdgeClick(edge, isLeft) 97 | 98 | onRightClick = (e) -> 99 | if isTourEvent(e) then return 100 | 101 | [x, y] = [e.clientX, e.clientY] 102 | 103 | # Don't show context menu when right-clicking a node/edge. 104 | if nv.nodeAt(x, y) or nv.edgeAt(x, y) 105 | e.preventDefault() 106 | 107 | hash = hashy.wrap(ARCH_KEY) 108 | 109 | arch = loadArch() 110 | 111 | $(document).keydown(onKeydown). 112 | keyup(onKeyup). 113 | mousedown(onMouseDown). 114 | on('contextmenu', onRightClick) 115 | 116 | $(window).on 'hashchange', -> 117 | if str = hash.get() 118 | arch = Arch.expand(str) 119 | nv.setArch(arch) 120 | 121 | nv = new NeuroVis(NETWORK_ID, arch, INIT_INPUTS) 122 | 123 | setTimeout(Tour.start.bind(null, nv), 300) 124 | 125 | $('#start-tutorial').click (e) -> 126 | setTimeout(Tour.forceStart.bind(null, nv), 300) 127 | 128 | $('#toggle-architecture').click (e) -> 129 | e.preventDefault() 130 | $(this).hide() 131 | $('#architecture').show() 132 | 133 | $('#random-arch').click (e) -> 134 | e.preventDefault() 135 | updateArch(Arch.random()) 136 | 137 | $('#simulation').click (e) -> 138 | $(this).addClass('filled') 139 | canvas = $(this).children('canvas').get(0) 140 | 141 | Simulation.run(nv, canvas, shiftPressed) -------------------------------------------------------------------------------- /src/js/neural_network.coffee: -------------------------------------------------------------------------------- 1 | # NeuralNetwork 2 | # 3 | # Represents the state of a Neural Network, including layers, 4 | # inputs, and activations. 5 | 6 | _ = require('lodash') 7 | 8 | sigmoid = (x) -> 1 / (1 + Math.pow(Math.E, -x)) 9 | 10 | sum = (a) -> _.reduce(a, (t, s) -> t + s) 11 | 12 | class NeuralNetwork 13 | 14 | constructor: (@layers) -> 15 | @_assignIds() 16 | 17 | getOutputs: -> 18 | node.activation for node in _.last(@layers) 19 | 20 | computeActivations: (inputs) -> 21 | for layer, i in @layers 22 | prevActivations = if i == 0 23 | inputs 24 | else 25 | n.activation for n in @layers[i-1] 26 | 27 | for node in layer 28 | a = node.bias + sum(a * node.weights[k] for a, k in prevActivations) 29 | node.activation = sigmoid(a) 30 | 31 | _assignIds: -> 32 | id = 1 33 | for layer in @layers 34 | for node in layer 35 | node.id = id 36 | id += 1 37 | 38 | module.exports = NeuralNetwork -------------------------------------------------------------------------------- /src/js/neuro_vis.coffee: -------------------------------------------------------------------------------- 1 | # NeuroVis 2 | # 3 | # A wrapper around vis.js the works with neural networks. 4 | 5 | _ = require('lodash') 6 | 7 | Color = require('./color') 8 | NeuralNetwork = require('./neural_network') 9 | 10 | BIAS_LABEL = '+' 11 | 12 | VIS_OPTIONS = { 13 | autoResize: false, 14 | layout: { 15 | hierarchical: { 16 | direction: 'LR', 17 | sortMethod: 'directed' 18 | } 19 | }, 20 | edges: { 21 | font: { 22 | color: Color.ACTIVATED, 23 | strokeWidth: 0, 24 | align: 'bottom' 25 | } 26 | }, 27 | nodes: { 28 | borderWidth: 2, 29 | fixed: true 30 | font: { color: Color.ACTIVATED } 31 | } 32 | interaction:{ 33 | dragNodes: false, 34 | dragView: false, 35 | multiselect: false, 36 | navigationButtons: false, 37 | selectable: false, 38 | zoomView: false 39 | } 40 | } 41 | 42 | MIN_EDGE_ALPHA = 0.1 43 | 44 | INPUT_LABELS = ['A', 'B'] 45 | 46 | fontColor = (alpha) -> 47 | if alpha < 0.5 then Color.ACTIVATED else Color.BG 48 | 49 | nodeColor = (activation) -> 50 | bgColor = Color.lerpColor(Color.RGB_BG, Color.RGB_ACTIVATED, activation) 51 | 52 | { 53 | background: bgColor, 54 | border: Color.ACTIVATED, 55 | highlight: { 56 | background: bgColor, 57 | border: Color.HIGHLIGHT 58 | } 59 | } 60 | 61 | nodeTitle = (activation) -> 62 | "activation #{activation.toFixed(1)}" 63 | 64 | edgeColor = (weight, input) -> 65 | alpha = if weight == 0 66 | 0 67 | else 68 | (input * (1 - MIN_EDGE_ALPHA)) + MIN_EDGE_ALPHA 69 | 70 | rgbArray = if weight < 0 then Color.RGB_NEGATIVE else Color.RGB_POSITIVE 71 | Color.rgba(rgbArray, alpha) 72 | 73 | buildEdge = (fromId, toId, weight, input) -> 74 | value = Math.abs(weight) 75 | { 76 | from: fromId, 77 | to: toId, 78 | value: Math.abs(weight), 79 | color: { 80 | color: edgeColor(weight, input), 81 | highlight: Color.HIGHLIGHT 82 | } 83 | } 84 | 85 | buildGraph = (network, inputs) -> 86 | nodes = [] 87 | edges = [] 88 | layers = network.layers 89 | 90 | # Nodes 91 | for layer, l in layers 92 | for neuron in layer 93 | a = neuron.activation 94 | if l == network.layers.length - 1 95 | # output node 96 | nodes.push({ 97 | id: neuron.id, 98 | color: nodeColor(a), 99 | title: '', 100 | label: a.toFixed(1), 101 | font: { 102 | size: 20, 103 | face: 'Open Sans', 104 | color: fontColor(a) 105 | } 106 | }) 107 | else 108 | # hidden node 109 | nodes.push({ 110 | id: neuron.id, 111 | color: nodeColor(a) 112 | }) 113 | 114 | # Edges 115 | for layer, i in layers[1..] 116 | prevLayer = layers[i] 117 | for neuron in layer 118 | for axon, k in prevLayer 119 | edges.push( buildEdge(axon.id, neuron.id, neuron.weights[k], axon.activation) ) 120 | 121 | # Input Nodes 122 | 123 | firstHiddenLayer = layers[0] 124 | 125 | for input, i in inputs 126 | id = -(i+1) 127 | nodes.push({ 128 | id: id, 129 | label: input.toFixed(1), 130 | color: nodeColor(input), 131 | title: '', 132 | font: {color: fontColor(input), size: 20, face: 'Open Sans'} 133 | }) 134 | 135 | for neuron in firstHiddenLayer 136 | edges.push( buildEdge(id, neuron.id, neuron.weights[i], input) ) 137 | 138 | # Bias Nodes 139 | 140 | for layer, i in layers 141 | id = -(i+inputs.length+1) 142 | nodes.push({ 143 | id: id, 144 | color: nodeColor(1), 145 | label: BIAS_LABEL, 146 | font: {color: fontColor(1)} 147 | }) 148 | 149 | for neuron in layer 150 | edges.push( buildEdge(id, neuron.id, neuron.bias, 1) ) 151 | 152 | {nodes: nodes, edges: edges} 153 | 154 | class NeuroVis 155 | 156 | constructor: (id, arch, @inputs) -> 157 | target = document.getElementById(id) 158 | @_vis = new vis.Network(target, {}, VIS_OPTIONS) 159 | @setArch(arch) 160 | 161 | findNode: (id) -> 162 | @_vis.findNode(id)[0] 163 | 164 | nodeAt: (x, y) -> 165 | id = @_vis.getNodeAt({x, y}) 166 | id and String(id) 167 | 168 | edgeAt: (x, y) -> 169 | edge = @_vis.getEdgeAt({x, y}) 170 | edge and @_vis.getConnectedNodes(edge) 171 | 172 | nodeType: (node) -> 173 | if node.id > 0 174 | 'normal' 175 | else if node.id >= -@inputs.length 176 | 'input' 177 | else 178 | 'bias' 179 | 180 | outputNode: -> 181 | id = _.last(@_network.layers)[0].id 182 | @findNode(id) 183 | 184 | inputIds: -> 185 | String(-i) for i in [1..@inputs.length] 186 | 187 | nodeCoords: (testNode) -> 188 | if @nodeType(testNode) == 'input' 189 | return [-1, -parseInt(testNode.id) - 1] 190 | 191 | for layer, l in @_network.layers 192 | for node, i in layer 193 | if node.id == testNode.id 194 | return [l, i] 195 | 196 | getOutput: -> 197 | @_network.getOutputs()[0] 198 | 199 | setInputs: (inputs) -> 200 | @inputs = inputs 201 | @_loadNetworkData() 202 | 203 | setArch: (arch) -> 204 | @_network = new NeuralNetwork(arch) 205 | @_loadNetworkData() 206 | 207 | _loadNetworkData: -> 208 | @_network.computeActivations(@inputs) 209 | graph = buildGraph(@_network, @inputs) 210 | @_vis.setData({ 211 | nodes: new vis.DataSet(graph.nodes), 212 | edges: new vis.DataSet(graph.edges) 213 | }) 214 | 215 | module.exports = NeuroVis -------------------------------------------------------------------------------- /src/js/simulation.coffee: -------------------------------------------------------------------------------- 1 | # Simulation 2 | # 3 | # Simulation.run() will sequetially simulate all input values 4 | # and plot the output on the given canvas. 5 | # 6 | # By default, there is a delay so that the simulation doesn't 7 | # run too fast. 8 | 9 | Color = require('./color') 10 | 11 | N = 30 # Number of steps 12 | SLOW_DELAY = 20 13 | FAST_DELAY = 0 14 | 15 | fillColor = (value) -> 16 | Color.lerpColor(Color.RGB_BG, Color.RGB_ACTIVATED, value) 17 | 18 | run = (nv, canvas, isFast) -> 19 | 20 | delay = if isFast then FAST_DELAY else SLOW_DELAY 21 | 22 | ctx = canvas.getContext('2d') 23 | w = canvas.width 24 | h = canvas.height 25 | 26 | dx = w/N 27 | dy = h/N 28 | 29 | ctx.clearRect(0, 0, w, h) 30 | 31 | i = 0 32 | 33 | step = -> 34 | a = i % N 35 | b = Math.floor(i/N) 36 | 37 | # Get the output for these inputs. 38 | nv.setInputs([a/N, b/N]) 39 | output = nv.getOutput() 40 | 41 | ctx.fillStyle = fillColor(output) 42 | ctx.fillRect(a * dx, b * dy, dx, dy) 43 | 44 | # Continue until grid is filled. 45 | i += 1 46 | if i < N * N 47 | setTimeout(step, delay) 48 | 49 | step() 50 | 51 | module.exports = {run} -------------------------------------------------------------------------------- /src/js/tour.coffee: -------------------------------------------------------------------------------- 1 | # Tour 2 | # 3 | # Utilities for running the tour. 4 | # Uses the hopscotch library. 5 | 6 | $ = require('jquery') 7 | hopscotch = require('hopscotch') 8 | 9 | setCookie = (key, value) -> 10 | expires = new Date() 11 | expires.setTime(expires.getTime() + (1 * 24 * 60 * 60 * 1000)) 12 | cookie = key + '=' + value + ';path=/' + ';expires=' + expires.toUTCString() 13 | document.cookie = cookie 14 | 15 | getCookie = (key) -> 16 | keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)') 17 | if keyValue then keyValue[2] else null 18 | 19 | buildTour = (nv) -> 20 | TARGET = 'tour-anchor' 21 | 22 | inputNode = nv.findNode('-1') 23 | inputPos = nv._vis.canvasToDOM(inputNode) 24 | 25 | outputNode = nv.outputNode() 26 | outputPos = nv._vis.canvasToDOM(outputNode) 27 | 28 | centerPos = {x: $(document).outerWidth()/2 - 250, y: 0} 29 | 30 | neuronNode = nv.findNode('1') 31 | neuronPos = nv._vis.canvasToDOM(neuronNode) 32 | 33 | synapsePos = {x: inputPos.x + (neuronPos.x - inputPos.x)/2, y: inputPos.y + (neuronPos.y - inputPos.y)/2} 34 | 35 | biasNode = nv.findNode('-3') 36 | biasPos = nv._vis.canvasToDOM(biasNode) 37 | 38 | bottomLeftPos = {x: 0, y: $(document).outerHeight()} 39 | 40 | markTourViewed = -> 41 | setCookie('toured', 'toured') 42 | 43 | tour = { 44 | id: "hello-hopscotch", 45 | steps: [ 46 | { 47 | title: "This is a neural network.", 48 | content: "
Before you start playing with it, there are a few things you should know…
", 49 | width: 350, 50 | target: TARGET, 51 | placement: "bottom", 52 | xOffset: centerPos.x, 53 | yOffset: centerPos.y, 54 | arrowOffset: 'center' 55 | }, { 56 | title: "This is an input node.", 57 | content: "Inputs nodes reflect the data being fed in to a neural network. This data could represent images, sounds, text, etc. This neural network has two inputs, but it could have many more.
Do: Increase the value of this input by clicking on it a few times. Right click to reduce its value again. Hold shift to go faster.
", 58 | width: 350, 59 | target: TARGET, 60 | placement: "right", 61 | xOffset: inputPos.x + 50, 62 | yOffset: inputPos.y - 30, 63 | showPrevButton: true 64 | }, { 65 | title: "This is an output node.", 66 | content: "A neural network's purpose is to compute a function of its inputs. The result of its computation is reflected in its output nodes. This network has only one output.
Do: Change the input values and watch this output change automatically.
", 67 | target: TARGET, 68 | placement: "left", 69 | width: 350, 70 | xOffset: outputPos.x - 50, 71 | yOffset: outputPos.y - 30, 72 | showPrevButton: true 73 | }, { 74 | title: "This is a neuron.", 75 | content: "Neurons are arranged into layers, from left to right. The arrangement of neurons in a neural network is called its architecture. In this network, there are 3 inner layers, 1 input layer, and 1 output layer.
Each neuron computes its own activation based on a weighted sum of its inputs—the activations of the neurons in the layer to its left.
Do: Change the inputs until this neuron is painted solid white. That means it is fully activated.
", 76 | target: TARGET, 77 | width: 350, 78 | placement: "right", 79 | xOffset: neuronPos.x + 30, 80 | yOffset: neuronPos.y - 30, 81 | showPrevButton: true 82 | }, { 83 | title: "This is a synapse.", 84 | content: "A synapse connects two neurons from adjacent layers. Each has a weight, which dictates how strongly the neuron on the left can stimulate the one on the right. The weight can be negative, which means that the synapse is inhibitory.
In this visualization, thicker synapses have a larger weight. Red represents a positive weight, and blue, negative.
Do: Change the weight of this synapse by left/right clicking. Hold shift to go faster.
", 85 | target: TARGET, 86 | placement: "bottom", 87 | xOffset: synapsePos.x - 175, 88 | yOffset: synapsePos.y + 10, 89 | arrowOffset: 'center', 90 | width: 350, 91 | showPrevButton: true 92 | }, { 93 | title: "This is a bias node.", 94 | content: "Each neuron can have a positive or negative bias. This is a constant term in the weighted sum it computes. We represent the biases visually as a synapse connected to a bias node, a special neuron that is always fully activated. The weight of this synapse is the neuron's bias. Bias nodes are labeled with a sign.
Do: Change a neuron's bias by changing the weight of the synapse that connects it to this bias node.
", 95 | target: TARGET, 96 | placement: "right", 97 | xOffset: biasPos.x + 20, 98 | yOffset: biasPos.y - 40, 99 | width: 400, 100 | showPrevButton: true 101 | }, { 102 | title: "This is a map of the computed function.", 103 | content: "A neural network computes a function of its inputs. That function depends on the architecture, and synapse weights. What type of function does this neural network compute?
Do: Click to map the outputs for a range of input values to visualize the function this network computes. Change some weights and see how that changes the map. (This works best in Chrome and Safari.)
", 104 | target: 'simulation', 105 | placement: "left", 106 | yOffset: -110, 107 | width: 390, 108 | arrowOffset: 'center', 109 | showPrevButton: true 110 | }, { 111 | title: "This is a list of architectures.", 112 | content: "A neural network's architecture describes the number of layers, the number of neurons in each layer, and the connections between neurons. So far we've only seen one architecture.
Do: Hover the and select a different architecture.
", 113 | width: 380, 114 | target: TARGET, 115 | xOffset: bottomLeftPos.x + 105, 116 | yOffset: bottomLeftPos.y - 560, 117 | arrowOffset: '195', 118 | placement: "right", 119 | showPrevButton: true 120 | }, { 121 | title: "This is the end of the tutorial.", 122 | content: "Now it's time to play around. Develop some intuition about neural networks. How does the activation change for a neuron as you change the weights of its synapses? How does the computed function change as you modify synapse weights? What types of functions can neural networks compute?
Do: Create your own awesome neural network. Choose an architecture, modify the weights, and then share it with your friends by copying the URL in the address bar.
P.S. The source code for this simulation is on github.", 123 | width: 400, 124 | target: TARGET, 125 | placement: "bottom", 126 | xOffset: centerPos.x, 127 | yOffset: centerPos.y, 128 | arrowOffset: 'center', 129 | showPrevButton: true 130 | } 131 | ], 132 | onEnd: markTourViewed, 133 | onClose: markTourViewed 134 | } 135 | 136 | start = (nv) -> 137 | tour = buildTour(nv) 138 | if !getCookie('toured') 139 | hopscotch.startTour(tour) 140 | 141 | forceStart = (nv) -> 142 | tour = buildTour(nv) 143 | hopscotch.startTour(tour, 0) 144 | 145 | module.exports = {start, forceStart} -------------------------------------------------------------------------------- /src/priority.js: -------------------------------------------------------------------------------- 1 | // This bundle is used to load generated CSS before we load 2 | // the (heavy) simulator. Otherwise, there is a flash of unstyled 3 | // screen. 4 | require('hopscotch/dist/css/hopscotch.min.css'); 5 | require('./css/style'); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | priority: './src/priority.js', 4 | app: './src/app.js' 5 | }, 6 | output: { 7 | path: './dist', 8 | filename: '[name].bundle.js' 9 | }, 10 | module: { 11 | loaders: [ 12 | { test: /\.coffee$/, loader: 'coffee-loader' }, 13 | { test: /\.css$/, loader: 'style!css' }, 14 | { test: /\.less$/, loader: 'style!css!less' }, 15 | { test: /\.png$/, loader: "url-loader?limit=100000" }, 16 | ] 17 | }, resolve: { 18 | extensions: ['', '.less', '.coffee'] 19 | } 20 | }; --------------------------------------------------------------------------------