Neural networks¶
Network construction, forward and backward propagation, and the training loop that drives them.
dlhub.nn ¶
Neural Networks¶
Network construction, forward and backward propagation, and the training loop that drives them.
License
MIT
DeepNeuralNetwork ¶
A comprehensive Deep Neural Network implementation with modern techniques.
This implementation includes: - Multiple initialization methods (He, Xavier, Random) - Regularization techniques (L1, L2, Dropout) - Batch Normalization - Gradient Clipping - Learning Rate Scheduling - Early Stopping - Comprehensive metrics tracking
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_dims
|
List
|
Layer dimensions [n_x, n_h1, n_h2, ..., n_y] |
required |
initialization
|
str
|
Initialization method ('he', 'xavier', 'random') |
'he'
|
regularization
|
str(optional)
|
Regularization type (None, 'l1', 'l2', 'dropout') |
None
|
lambda_reg
|
float
|
Regularization strength parameter |
0.01
|
keep_prob
|
float
|
Dropout keep probability (0 < keep_prob <= 1) |
0.8
|
use_batch_norm
|
bool
|
Whether to use batch normalization |
True
|
gradient_clipping
|
bool
|
Whether to apply gradient clipping |
True
|
clip_value
|
float
|
Maximum gradient norm for clipping |
True
|
Attributes:
| Name | Type | Description |
|---|---|---|
layer_dims |
List[int]
|
Dimensions of each layer |
L |
int
|
Number of layers (excluding input) |
parameters |
Dict
|
Network weights and biases |
bn_params |
Dict
|
Batch normalization parameters |
costs_history |
List
|
Training cost history |
val_costs_history |
List
|
Validation cost history |
accuracies_history |
List
|
Training accuracy history |
val_accuracies_history |
List
|
Validation accuracy history |
Notes
Raises ValueError: If invalid parameters are provided
Source code in src/dlhub/nn/fully_connected.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 | |
forward_propagation ¶
forward_propagation(X: ndarray, training: bool = True) -> tuple[np.ndarray, list]
Perform forward propagation through the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Input data of shape (n_features, m_samples) |
required |
training
|
bool
|
Whether in training mode (affects dropout and batch norm) |
True
|
Returns:
| Type | Description |
|---|---|
Tuple[ndarray, List]
|
Contains final output and caches for backprob |
Source code in src/dlhub/nn/fully_connected.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | |
compute_cost ¶
compute_cost(AL: ndarray, Y: ndarray) -> float
Compute the cost function with regularization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
AL
|
ndarray
|
Network output of shape (1, m_samples) |
required |
Y
|
ndarray
|
True labels of shape (1, m_samples) |
required |
Returns:
| Type | Description |
|---|---|
float
|
Total cost including regularization |
Source code in src/dlhub/nn/fully_connected.py
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
backward_propagation ¶
backward_propagation(AL: ndarray, Y: ndarray, caches: list) -> dict[str, np.ndarray]
Perform backward propagation to compute gradients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
AL
|
ndarray
|
Network output |
required |
Y
|
ndarray
|
True labels |
required |
caches
|
List
|
Forward propagation caches |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, ndarray]
|
Contains gradients for all parameters |
Source code in src/dlhub/nn/fully_connected.py
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 | |
update_parameters ¶
update_parameters(grads: dict[str, ndarray], learning_rate: float) -> None
Update network parameters using gradients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grads
|
Dict[str, ndarray]
|
Dictionary of gradients |
required |
learning_rate
|
float
|
Learning rate for parameter updates |
required |
Source code in src/dlhub/nn/fully_connected.py
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 | |
compute_accuracy ¶
compute_accuracy(AL: ndarray, Y: ndarray) -> float
Compute classification accuracy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
AL
|
ndarray
|
Network predictions |
required |
Y
|
ndarray
|
True labels |
required |
Returns:
| Type | Description |
|---|---|
float
|
Accuracy percentage |
Source code in src/dlhub/nn/fully_connected.py
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 | |
train ¶
train(X: ndarray, Y: ndarray, X_val: ndarray, Y_val: ndarray, learning_rate: float = 0.01, num_epochs: int = 1000, print_cost: bool = True, learning_rate_decay: float = 0.95, decay_step: int = 100, early_stopping: bool = True, patience: int = 50) -> dict[str, list]
Train the neural network with advanced techniques.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Training data of shape (n_features, m_samples) |
required |
Y
|
ndarray
|
Training labels of shape (1, m_samples) |
required |
X_val
|
ndarray
|
Validation data |
required |
Y_val
|
ndarray
|
Validation labels |
required |
learning_rate
|
float
|
Initial learning rate |
0.01
|
num_epochs
|
int
|
Number of training epochs |
1000
|
print_cost
|
bool
|
Whether to print cost during training |
True
|
learning_rate_decay
|
float
|
Learning rate decay factor |
0.95
|
decay_step
|
int
|
Steps between learning rate decay |
100
|
early_stopping
|
bool
|
Whether to use early stopping |
True
|
patience
|
int
|
Early stopping patience |
50
|
Returns:
| Type | Description |
|---|---|
Dict[str, List]
|
Contains training history |
Source code in src/dlhub/nn/fully_connected.py
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 | |
predict ¶
predict(X: ndarray) -> np.ndarray
Make predictions on new data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Input data of shape (n_features, m_samples) |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Binary predictions of shape (1, m_samples) |
Source code in src/dlhub/nn/fully_connected.py
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 | |
predict_proba ¶
predict_proba(X: ndarray) -> np.ndarray
Get prediction probabilities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Input data of shape (n_features, m_samples) |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Probabilities of shape (1, m_samples) |
Source code in src/dlhub/nn/fully_connected.py
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 | |
plot_training_history ¶
plot_training_history(figsize: tuple[int, int] = (15, 5)) -> None
Plot training history including cost and accuracy curves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
figsize
|
Tuple[int, int]
|
Figure size tuple |
(15, 5)
|
Source code in src/dlhub/nn/fully_connected.py
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 | |
get_model_info ¶
get_model_info() -> dict[str, Any]
Get comprehensive model information.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Contains model configuration and statistics |
Source code in src/dlhub/nn/fully_connected.py
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 | |
create_sample_data ¶
create_sample_data(n_features: int = 128, n_train: int = 1000, n_val: int = 200) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
Create sample dataset for testing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_features
|
int
|
Number of input features |
128
|
n_train
|
int
|
Number of training samples |
1000
|
n_val
|
int
|
Number of validation samples |
200
|
Returns:
| Type | Description |
|---|---|
Tuple[ndarray, ndarray, ndarray, ndarray]
|
(X_train, Y_train, X_val, Y_val) |
Source code in src/dlhub/nn/fully_connected.py
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 | |
fully_connected ¶
Deep Neural Network Implementation¶
Defines a configurable fully-connected deep neural network from scratch using NumPy, including forward/backward propagation, parameter initialization, optimization, and prediction routines.
References
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.
- Ng, A. (2017). Deep Learning Specialization: Course on Neural Networks. (gradient checking, initialization, forward/backward propagation)
License
MIT License
Notes
- Supports L-layer architectures with ReLU and Sigmoid activations
- Includes optional gradient checking and loss tracking
DeepNeuralNetwork ¶
A comprehensive Deep Neural Network implementation with modern techniques.
This implementation includes: - Multiple initialization methods (He, Xavier, Random) - Regularization techniques (L1, L2, Dropout) - Batch Normalization - Gradient Clipping - Learning Rate Scheduling - Early Stopping - Comprehensive metrics tracking
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_dims
|
List
|
Layer dimensions [n_x, n_h1, n_h2, ..., n_y] |
required |
initialization
|
str
|
Initialization method ('he', 'xavier', 'random') |
'he'
|
regularization
|
str(optional)
|
Regularization type (None, 'l1', 'l2', 'dropout') |
None
|
lambda_reg
|
float
|
Regularization strength parameter |
0.01
|
keep_prob
|
float
|
Dropout keep probability (0 < keep_prob <= 1) |
0.8
|
use_batch_norm
|
bool
|
Whether to use batch normalization |
True
|
gradient_clipping
|
bool
|
Whether to apply gradient clipping |
True
|
clip_value
|
float
|
Maximum gradient norm for clipping |
True
|
Attributes:
| Name | Type | Description |
|---|---|---|
layer_dims |
List[int]
|
Dimensions of each layer |
L |
int
|
Number of layers (excluding input) |
parameters |
Dict
|
Network weights and biases |
bn_params |
Dict
|
Batch normalization parameters |
costs_history |
List
|
Training cost history |
val_costs_history |
List
|
Validation cost history |
accuracies_history |
List
|
Training accuracy history |
val_accuracies_history |
List
|
Validation accuracy history |
Notes
Raises ValueError: If invalid parameters are provided
Source code in src/dlhub/nn/fully_connected.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 | |
forward_propagation ¶
forward_propagation(X: ndarray, training: bool = True) -> tuple[np.ndarray, list]
Perform forward propagation through the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Input data of shape (n_features, m_samples) |
required |
training
|
bool
|
Whether in training mode (affects dropout and batch norm) |
True
|
Returns:
| Type | Description |
|---|---|
Tuple[ndarray, List]
|
Contains final output and caches for backprob |
Source code in src/dlhub/nn/fully_connected.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | |
compute_cost ¶
compute_cost(AL: ndarray, Y: ndarray) -> float
Compute the cost function with regularization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
AL
|
ndarray
|
Network output of shape (1, m_samples) |
required |
Y
|
ndarray
|
True labels of shape (1, m_samples) |
required |
Returns:
| Type | Description |
|---|---|
float
|
Total cost including regularization |
Source code in src/dlhub/nn/fully_connected.py
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
backward_propagation ¶
backward_propagation(AL: ndarray, Y: ndarray, caches: list) -> dict[str, np.ndarray]
Perform backward propagation to compute gradients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
AL
|
ndarray
|
Network output |
required |
Y
|
ndarray
|
True labels |
required |
caches
|
List
|
Forward propagation caches |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, ndarray]
|
Contains gradients for all parameters |
Source code in src/dlhub/nn/fully_connected.py
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 | |
update_parameters ¶
update_parameters(grads: dict[str, ndarray], learning_rate: float) -> None
Update network parameters using gradients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grads
|
Dict[str, ndarray]
|
Dictionary of gradients |
required |
learning_rate
|
float
|
Learning rate for parameter updates |
required |
Source code in src/dlhub/nn/fully_connected.py
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 | |
compute_accuracy ¶
compute_accuracy(AL: ndarray, Y: ndarray) -> float
Compute classification accuracy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
AL
|
ndarray
|
Network predictions |
required |
Y
|
ndarray
|
True labels |
required |
Returns:
| Type | Description |
|---|---|
float
|
Accuracy percentage |
Source code in src/dlhub/nn/fully_connected.py
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 | |
train ¶
train(X: ndarray, Y: ndarray, X_val: ndarray, Y_val: ndarray, learning_rate: float = 0.01, num_epochs: int = 1000, print_cost: bool = True, learning_rate_decay: float = 0.95, decay_step: int = 100, early_stopping: bool = True, patience: int = 50) -> dict[str, list]
Train the neural network with advanced techniques.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Training data of shape (n_features, m_samples) |
required |
Y
|
ndarray
|
Training labels of shape (1, m_samples) |
required |
X_val
|
ndarray
|
Validation data |
required |
Y_val
|
ndarray
|
Validation labels |
required |
learning_rate
|
float
|
Initial learning rate |
0.01
|
num_epochs
|
int
|
Number of training epochs |
1000
|
print_cost
|
bool
|
Whether to print cost during training |
True
|
learning_rate_decay
|
float
|
Learning rate decay factor |
0.95
|
decay_step
|
int
|
Steps between learning rate decay |
100
|
early_stopping
|
bool
|
Whether to use early stopping |
True
|
patience
|
int
|
Early stopping patience |
50
|
Returns:
| Type | Description |
|---|---|
Dict[str, List]
|
Contains training history |
Source code in src/dlhub/nn/fully_connected.py
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 | |
predict ¶
predict(X: ndarray) -> np.ndarray
Make predictions on new data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Input data of shape (n_features, m_samples) |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Binary predictions of shape (1, m_samples) |
Source code in src/dlhub/nn/fully_connected.py
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 | |
predict_proba ¶
predict_proba(X: ndarray) -> np.ndarray
Get prediction probabilities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ndarray
|
Input data of shape (n_features, m_samples) |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Probabilities of shape (1, m_samples) |
Source code in src/dlhub/nn/fully_connected.py
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 | |
plot_training_history ¶
plot_training_history(figsize: tuple[int, int] = (15, 5)) -> None
Plot training history including cost and accuracy curves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
figsize
|
Tuple[int, int]
|
Figure size tuple |
(15, 5)
|
Source code in src/dlhub/nn/fully_connected.py
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 | |
get_model_info ¶
get_model_info() -> dict[str, Any]
Get comprehensive model information.
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Contains model configuration and statistics |
Source code in src/dlhub/nn/fully_connected.py
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 | |
create_sample_data ¶
create_sample_data(n_features: int = 128, n_train: int = 1000, n_val: int = 200) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
Create sample dataset for testing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_features
|
int
|
Number of input features |
128
|
n_train
|
int
|
Number of training samples |
1000
|
n_val
|
int
|
Number of validation samples |
200
|
Returns:
| Type | Description |
|---|---|
Tuple[ndarray, ndarray, ndarray, ndarray]
|
(X_train, Y_train, X_val, Y_val) |
Source code in src/dlhub/nn/fully_connected.py
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 | |
main ¶
main()
Example usage of the Deep Neural Network.
Source code in src/dlhub/nn/fully_connected.py
810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 | |
sequence ¶
Sequence Models¶
Recurrent architectures for sequential data, written to be read alongside the documents that derive them. The vanilla RNN here is the from-scratch reference that the framework parity port is checked against; its notation follows Andrew Ng's Course 5 so the gated cells (LSTM, GRU) extend the same symbols.
Each module carries its own make_fixture. The name re-exported here is the
vanilla RNN's, because a package-level name can only mean one thing; import the
gated ones from their modules, from dlhub.nn.sequence.lstm import
make_fixture.
License
MIT
compute_loss ¶
compute_loss(y_pred: ndarray, y: ndarray) -> float
Total softmax cross-entropy loss, summed over timesteps.
Implements L = sum_t L^{<t>} with each per-step loss the batch-mean
cross-entropy L^{<t>} = -(1/m) sum_{i,c} y^{<t>}_{c,i} log y_hat^{<t>}_{c,i}.
No epsilon guards the log: the stable softmax output is strictly
positive (see module Notes).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_pred
|
ndarray
|
Predicted distributions, shape |
required |
y
|
ndarray
|
One-hot targets, same shape |
required |
Returns:
| Type | Description |
|---|---|
float
|
The scalar loss summed over all timesteps. |
Source code in src/dlhub/nn/sequence/_common.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
sigmoid ¶
sigmoid(z: ndarray) -> np.ndarray
Element-wise logistic sigmoid, in the overflow-free form.
Computes sigma(z) = 1 / (1 + exp(-z)) without ever exponentiating a
positive number. See the module Notes for why the two branches are
written this way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
ndarray
|
Pre-activations of any shape. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Values in |
Source code in src/dlhub/nn/sequence/_common.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
softmax ¶
softmax(z: ndarray) -> np.ndarray
Column-wise softmax with the max-subtraction stability shift.
Normalizes over axis=0 (the class/feature axis), so each column is a
probability distribution that sums to one. See the module Notes for why
the maximum is subtracted first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
ndarray
|
Logits of shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Probabilities of shape |
Source code in src/dlhub/nn/sequence/_common.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
split_gate_matrix ¶
split_gate_matrix(matrix: ndarray, n_a: int) -> tuple[np.ndarray, np.ndarray]
Split a concatenated gate matrix into its recurrent and input halves.
The gated cells use one weight per gate acting on the stacked vector
[a^{<t-1>}; x^{<t>}], so W_g = [W_ga | W_gx]. The two halves are
never needed separately by the forward or backward pass -- the point of the
concatenated form is that they are not -- but they are worth naming when
reporting a gradient, because dW_ga is the recurrent path and dW_gx
the input path, which is the split the vanilla RNN keeps explicit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
ndarray
|
A gate weight or gate weight gradient, shape |
required |
n_a
|
int
|
The hidden width, which is the column the split falls on. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
recurrent |
ndarray
|
|
input_ |
ndarray
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/_common.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
gru_backward ¶
gru_backward(y: ndarray, caches: list[dict[str, ndarray]], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backpropagation through time (BPTT) for the GRU.
Walks the unrolled graph from t = T_x back to t = 1. Every gate
weight is shared across time, so each receives an accumulated gradient
summed over all timesteps, and the hidden-state gradient is threaded
backward so each step adds its recurrence contribution (da_prev) to the
next step's incoming gradient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ndarray
|
One-hot targets, shape |
required |
caches
|
list[dict[str, ndarray]]
|
Per-timestep caches from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Accumulated gradients |
Source code in src/dlhub/nn/sequence/gru.py
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | |
gru_cell_backward ¶
gru_cell_backward(y_t: ndarray, da_next: ndarray, cache: dict[str, ndarray], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backward pass for a single GRU timestep.
Walks the forward pass backwards, one equation at a time. Three separate
paths deposit gradient into a^{<t-1>}, and the whole point of the cell is
that the first of them is a plain elementwise factor:
- the blend, through
(1 - Gamma_u)-- no weight matrix, no saturating nonlinearity, so gradient crosses the step essentially undamped when the update gate is near zero; - the candidate, through
W_cand then the reset gate; - the gates themselves, through
W_randW_u.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_t
|
ndarray
|
One-hot target at this timestep, shape |
required |
da_next
|
ndarray
|
Gradient of the loss w.r.t. this step's hidden state arriving from the
recurrence at |
required |
cache
|
dict[str, ndarray]
|
This step's cache from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
This step's gradient contributions: |
Source code in src/dlhub/nn/sequence/gru.py
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
gru_cell_forward ¶
gru_cell_forward(x_t: ndarray, a_prev: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray, dict[str, np.ndarray]]
One timestep of the GRU forward recurrence.
Computes the two gates, the candidate activation, the blended state, and the softmax read-out for a single step, in that order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
ndarray
|
Input at this timestep, shape |
required |
a_prev
|
ndarray
|
Hidden state carried in from the previous step, |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
a_next |
ndarray
|
Updated hidden state |
y_pred_t |
ndarray
|
Output distribution |
cache |
dict[str, ndarray]
|
Everything this step's backward pass needs, each array named. |
Source code in src/dlhub/nn/sequence/gru.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
gru_forward ¶
gru_forward(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> tuple[np.ndarray, np.ndarray, list[dict[str, np.ndarray]]]
Forward propagation through time across a whole sequence.
Unrolls :func:gru_cell_forward over T_x timesteps, threading the hidden
state from one step to the next.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence of shape |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
a0
|
ndarray or None
|
Initial hidden state |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a |
ndarray
|
All hidden states stacked over time, shape |
y_pred |
ndarray
|
All output distributions, shape |
caches |
list[dict[str, ndarray]]
|
Per-timestep caches, in order, for :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/gru.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
lstm_backward ¶
lstm_backward(y: ndarray, caches: list[dict[str, ndarray]], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backpropagation through time (BPTT) for the LSTM.
Walks the unrolled graph from t = T_x back to t = 1, threading
two recurrent gradients backward -- one along the hidden state and one
along the cell state. Every gate weight is shared across time, so each
receives an accumulated gradient summed over all timesteps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ndarray
|
One-hot targets, shape |
required |
caches
|
list[dict[str, ndarray]]
|
Per-timestep caches from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Accumulated gradients |
Source code in src/dlhub/nn/sequence/lstm.py
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | |
lstm_cell_backward ¶
lstm_cell_backward(y_t: ndarray, da_next: ndarray, dc_next: ndarray, cache: dict[str, ndarray], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backward pass for a single LSTM timestep.
Walks the forward pass backwards, one equation at a time. Two gradients
arrive from the future, not one: da_next along the hidden state and
dc_next along the cell state. They meet at c^{<t>}, because the
hidden state is a function of the cell state at the same step.
The line worth reading twice is dc_prev = dc_t * Gamma_f. That is the
whole thesis of the architecture as one statement: the gradient crossing
from t to t-1 along the cell path is scaled elementwise by the forget
gate, with no weight matrix and no saturating nonlinearity in the way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_t
|
ndarray
|
One-hot target at this timestep, shape |
required |
da_next
|
ndarray
|
Gradient w.r.t. this step's hidden state arriving from the recurrence at
|
required |
dc_next
|
ndarray
|
Gradient w.r.t. this step's cell state arriving from the recurrence at
|
required |
cache
|
dict[str, ndarray]
|
This step's cache from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
This step's gradient contributions: |
Source code in src/dlhub/nn/sequence/lstm.py
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | |
lstm_cell_forward ¶
lstm_cell_forward(x_t: ndarray, a_prev: ndarray, c_prev: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray, np.ndarray, dict[str, np.ndarray]]
One timestep of the LSTM forward recurrence.
Computes the four gate quantities in the topic's order (forget, update, candidate, output), then the cell state, the hidden state, and the softmax read-out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
ndarray
|
Input at this timestep, shape |
required |
a_prev
|
ndarray
|
Hidden state carried in from the previous step, |
required |
c_prev
|
ndarray
|
Cell state carried in from the previous step, |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
a_next |
ndarray
|
Updated hidden state |
c_next |
ndarray
|
Updated cell state |
y_pred_t |
ndarray
|
Output distribution |
cache |
dict[str, ndarray]
|
Everything this step's backward pass needs, each array named. |
Source code in src/dlhub/nn/sequence/lstm.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
lstm_forward ¶
lstm_forward(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, c0: ndarray | None = None) -> tuple[np.ndarray, np.ndarray, np.ndarray, list[dict[str, np.ndarray]]]
Forward propagation through time across a whole sequence.
Unrolls :func:lstm_cell_forward over T_x timesteps, threading two
states from one step to the next: the hidden state a and the cell state
c.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence of shape |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
a0
|
ndarray or None
|
Initial hidden state |
None
|
c0
|
ndarray or None
|
Initial cell state |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a |
ndarray
|
All hidden states stacked over time, shape |
c |
ndarray
|
All cell states stacked over time, shape |
y_pred |
ndarray
|
All output distributions, shape |
caches |
list[dict[str, ndarray]]
|
Per-timestep caches, in order, for :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/lstm.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | |
g ¶
g(k: ndarray) -> np.ndarray
The strictly positive candidate activation of Sec. B.2.1.
::
g(k) = k + 0.5 for k >= 0
= sigma(k) for k < 0
Continuous at zero (both branches give 0.5), positive everywhere, and
still slope-1 linear on the positive half -- which is what lets the
log-space scan take log(beta) at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
ndarray
|
The candidate pre-activation |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Strictly positive values, same shape as |
Source code in src/dlhub/nn/sequence/min_gated.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
identity_candidate ¶
identity_candidate(k: ndarray) -> np.ndarray
The minimal cells' candidate activation: the identity (Sec. 3.1.2).
The classical GRU and LSTM squash their candidate with tanh. The
minimal cells do not: the candidate is the plain linear map
a_tilde^{<t>} = W_c x^{<t>}. This function exists so the deletion has a
name and a docstring, rather than being an absence a later reader restores
by accident.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
ndarray
|
The candidate pre-activation |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
log_g ¶
log_g(k: ndarray) -> np.ndarray
log(g(k)), computed without forming g(k) first (Sec. B.2.1).
::
log g(k) = log(k + 0.5) for k >= 0
= -softplus(-k) for k < 0
The negative branch reuses the identity log sigma(k) = -softplus(-k),
so no logarithm of an underflowed sigmoid is ever taken.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
ndarray
|
The candidate pre-activation |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
log_space_scan ¶
log_space_scan(log_alpha: ndarray, log_beta: ndarray, a0: ndarray | None = None) -> np.ndarray
Resolve the recurrence in log space -- the stable scan of Sec. B.1.
Writing A_t = sum_{i <= t} log alpha_i for the cumulative log-decay, the
closed form of the recurrence is::
a^{<t>} = exp( A_t + log( sum_{j <= t} exp(log beta_j - A_j) ) )
with the initial state entering as the j = 0 term (coefficient one,
hence log alpha_0 = 0). Every product has become a sum and every sum a
logaddexp, so nothing underflows on the way. This is Heinsen's (2023)
formulation, which the paper adopts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log_alpha
|
ndarray
|
Logs of the per-step coefficients, each |
required |
log_beta
|
ndarray
|
Logs of the per-step coefficients, each |
required |
a0
|
ndarray or None
|
Initial state |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The hidden states, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the two coefficient arrays disagree in shape, if |
Source code in src/dlhub/nn/sequence/min_gated.py
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 | |
logcumsumexp ¶
logcumsumexp(z: ndarray) -> np.ndarray
Cumulative log(sum(exp(.))) along the last (time) axis.
Built with the same stride-doubling as :func:parallel_scan, with
np.logaddexp -- itself associative -- in place of affine composition.
NumPy has no built-in for this, and the naive log(cumsum(exp(z)))
overflows exactly where a log-space scan is worth having.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
ndarray
|
Log-domain values, shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 | |
min_gru_coefficients ¶
min_gru_coefficients(x: ndarray, parameters: dict[str, ndarray], candidate: Callable[[ndarray], ndarray] = identity_candidate) -> tuple[np.ndarray, np.ndarray]
The minGRU recurrence coefficients (alpha, beta) (Sec. 3.1.3).
::
Gamma_u = sigma(W_u x)
a_tilde = candidate(W_c x)
alpha = 1 - Gamma_u
beta = Gamma_u * a_tilde
so that a^{<t>} = alpha^{<t>} * a^{<t-1>} + beta^{<t>} reproduces the
cell's blend. There is no reset gate: the candidate never reads
a^{<t-1>}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation applied to |
identity_candidate
|
Returns:
| Name | Type | Description |
|---|---|---|
alpha |
ndarray
|
Per-step decay |
beta |
ndarray
|
Per-step injection |
Source code in src/dlhub/nn/sequence/min_gated.py
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | |
min_gru_forward_log ¶
min_gru_forward_log(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> np.ndarray
Forward pass for minGRU through the log-space scan (Sec. B.1).
This is the positive-candidate cell: the candidate is :func:g, not the
identity, because a log-space scan needs beta > 0. It therefore matches
:func:min_gru_forward_sequential called with candidate=g, and not the
default linear one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Non-negative initial state, shape |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 | |
min_gru_forward_parallel ¶
min_gru_forward_parallel(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minGRU, resolved by the plain-space associative scan.
Same function as :func:min_gru_forward_sequential, evaluated in a
different association order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 | |
min_gru_forward_sequential ¶
min_gru_forward_sequential(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minGRU, resolved by the serial recurrence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 | |
min_gru_log_coefficients ¶
min_gru_log_coefficients(x: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray]
The minGRU coefficients in log space (Sec. B.1), using :func:g.
Takes the gate logarithms straight from the pre-activation
k_u = W_u x, never forming the gate itself::
log alpha = log(1 - Gamma_u) = -softplus(k_u)
log beta = log Gamma_u + log g(k_c) = -softplus(-k_u) + log_g(k_c)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
log_alpha |
ndarray
|
|
log_beta |
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | |
min_lstm_coefficients ¶
min_lstm_coefficients(x: ndarray, parameters: dict[str, ndarray], candidate: Callable[[ndarray], ndarray] = identity_candidate) -> tuple[np.ndarray, np.ndarray]
The minLSTM recurrence coefficients (alpha, beta) (Sec. 3.2.4).
::
Gamma_f = sigma(W_f x)
Gamma_u = sigma(W_u x)
a_tilde = candidate(W_c x)
Gamma_f' = Gamma_f / (Gamma_f + Gamma_u)
Gamma_u' = Gamma_u / (Gamma_f + Gamma_u)
alpha = Gamma_f'
beta = Gamma_u' * a_tilde
The normalisation is what replaces the missing output gate: because
Gamma_f' + Gamma_u' = 1 exactly, the state is a convex blend at every
step and so keeps a time-independent scale.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation applied to |
identity_candidate
|
Returns:
| Name | Type | Description |
|---|---|---|
alpha |
ndarray
|
The normalised forget gate |
beta |
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | |
min_lstm_forward_log ¶
min_lstm_forward_log(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> np.ndarray
Forward pass for minLSTM through the log-space scan (Sec. B.1).
As with :func:min_gru_forward_log, the candidate is :func:g, so this
matches :func:min_lstm_forward_sequential called with candidate=g.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Non-negative initial state, shape |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 | |
min_lstm_forward_parallel ¶
min_lstm_forward_parallel(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minLSTM, resolved by the plain-space associative scan.
Same function as :func:min_lstm_forward_sequential, evaluated in a
different association order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 | |
min_lstm_forward_sequential ¶
min_lstm_forward_sequential(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minLSTM, resolved by the serial recurrence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 | |
min_lstm_log_coefficients ¶
min_lstm_log_coefficients(x: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray]
The minLSTM coefficients in log space (Sec. B.1), using :func:g.
The gate normalisation becomes a subtraction::
log Gamma_f = -softplus(-k_f)
log Gamma_u = -softplus(-k_u)
log(Gamma_f + Gamma_u) = logaddexp(log Gamma_f, log Gamma_u)
log alpha = log Gamma_f - log(Gamma_f + Gamma_u)
log beta = log Gamma_u - log(Gamma_f + Gamma_u) + log_g(k_c)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
log_alpha |
ndarray
|
|
log_beta |
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 | |
parallel_scan ¶
parallel_scan(alpha: ndarray, beta: ndarray, a0: ndarray | None = None) -> np.ndarray
Resolve the same recurrence by an associative scan, in plain space.
Each timestep is an affine map a -> alpha * a + beta, and composing two
of them -- first (alpha_1, beta_1), then (alpha_2, beta_2) -- gives
another affine map::
(alpha_1, beta_1) . (alpha_2, beta_2)
= (alpha_2 * alpha_1, alpha_2 * beta_1 + beta_2)
Composition is associative, so the prefix compositions can be built by
doubling the stride (the Hillis-Steele inclusive scan) in
ceil(log2 T_x) rounds instead of T_x serial steps. After the
doubling, entry t holds the single affine map from a^{<0>} to
a^{<t>}, which is then applied to the initial state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
ndarray
|
Per-step coefficients, each |
required |
beta
|
ndarray
|
Per-step coefficients, each |
required |
a0
|
ndarray or None
|
Initial state |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The hidden states, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/min_gated.py
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 | |
project_over_time ¶
project_over_time(W: ndarray, x: ndarray) -> np.ndarray
Apply one weight matrix to every timestep in a single contraction.
Computes W x^{<t>} for all t, which is exactly the freedom the
reduction buys: with no dependence on a^{<t-1>}, nothing in a gate has
to wait for the step before it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
Weight matrix of shape |
required |
x
|
ndarray
|
Input sequence of shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Pre-activations of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/min_gated.py
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
sequential_scan ¶
sequential_scan(alpha: ndarray, beta: ndarray, a0: ndarray | None = None) -> np.ndarray
Resolve a^{<t>} = alpha^{<t>} a^{<t-1>} + beta^{<t>} one step at a time.
This is the definition, written as the loop the recurrence describes. It is the reference the two scans below are checked against.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
ndarray
|
Per-step coefficients, each |
required |
beta
|
ndarray
|
Per-step coefficients, each |
required |
a0
|
ndarray or None
|
Initial state |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The hidden states, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/min_gated.py
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 | |
softplus ¶
softplus(z: ndarray) -> np.ndarray
Stable log(1 + exp(z)).
Rewritten as log1p(exp(-|z|)) + max(z, 0), which is algebraically the
same function but only ever exponentiates a non-positive number. See the
module Notes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
ndarray
|
Pre-activation of any shape. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
bidirectional_rnn_forward ¶
bidirectional_rnn_forward(x: ndarray, parameters_forward: dict[str, ndarray], parameters_backward: dict[str, ndarray], a0_forward: ndarray | None = None, a0_backward: ndarray | None = None) -> tuple[np.ndarray, dict[str, np.ndarray]]
Bidirectional RNN forward pass, as a composition of the vanilla cell.
Runs two independent recurrences over the sequence -- one left-to-right
(a->) and one right-to-left (a<-) -- and concatenates their hidden
states at each timestep as [a->^{<t>}; a<-^{<t>}]. There is no new cell
mathematics: this is :func:rnn_forward applied twice, once to the
time-reversed input. A bidirectional model attaches its own output layer,
mapping the 2 n_a-dimensional concatenation to n_y; the per-direction
read-outs computed by the reused forward pass are not used here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters_forward
|
dict[str, ndarray]
|
Independent weight sets for the two directions. |
required |
parameters_backward
|
dict[str, ndarray]
|
Independent weight sets for the two directions. |
required |
a0_forward
|
ndarray or None
|
Initial hidden states for each direction, each shape |
None
|
a0_backward
|
ndarray or None
|
Initial hidden states for each direction, each shape |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a_bidirectional |
ndarray
|
Concatenated hidden states, shape |
parts |
dict[str, ndarray]
|
The two direction sequences, |
Source code in src/dlhub/nn/sequence/rnn.py
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
clip_gradients ¶
clip_gradients(gradients: dict[str, ndarray], max_norm: float) -> dict[str, np.ndarray]
Global-norm gradient clipping, the exploding-gradient remedy.
Computes the single L2 norm of all gradients viewed as one long vector and,
if it exceeds max_norm, rescales every gradient by the same factor. This
preserves the gradient direction while capping its magnitude, exactly as
prescribed by Pascanu, Mikolov & Bengio (2013).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gradients
|
dict[str, ndarray]
|
Gradients to clip, typically the parameter gradients |
required |
max_norm
|
float
|
The maximum allowed global L2 norm. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
A new dictionary of clipped gradients. The input is not modified. |
Source code in src/dlhub/nn/sequence/rnn.py
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | |
deep_rnn_forward ¶
deep_rnn_forward(x: ndarray, parameters_per_layer: list[dict[str, ndarray]], a0_per_layer: list[ndarray] | None = None) -> tuple[list[np.ndarray], np.ndarray]
Deep (stacked) RNN forward pass, as a composition of the vanilla cell.
Each layer is itself a full RNN over the sequence produced by the layer
below: layer l consumes the activation sequence a^{[l-1]} in place of
the input x. There is no new cell mathematics -- this is
:func:rnn_forward applied once per layer. Only the top layer's read-out is
a model output; the intermediate read-outs computed by the reused forward
pass are not used.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence to the first layer, shape |
required |
parameters_per_layer
|
list[dict[str, ndarray]]
|
One weight set per layer, bottom to top. Layer |
required |
a0_per_layer
|
list[ndarray] or None
|
Per-layer initial hidden states. Defaults to zero for every layer. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
activations |
list[ndarray]
|
Each layer's hidden-state sequence, bottom to top, each
|
y_pred_top |
ndarray
|
The top layer's output distributions, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/rnn.py
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 | |
make_fixture ¶
make_fixture() -> dict
Build the deterministic fixture every implementation and test shares.
Draws are recorded in exactly the order the topic dossier pins, under
np.random.seed(1) and float64 (the default of randn), so the
seven parameter arrays reproduce bit-for-bit across implementations. The
one-hot targets are drawn after those seven so that adding them cannot
disturb the arrays a forward-parity check compares.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/nn/sequence/rnn.py
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | |
rnn_backward ¶
rnn_backward(y: ndarray, caches: list[tuple], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backpropagation through time (BPTT).
Walks the unrolled graph from t = T_x back to t = 1. The shared
weights receive accumulated gradients over every timestep, and the
hidden-state gradient is threaded backward so that each step adds its
recurrence contribution (da_prev) to the next step's incoming gradient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ndarray
|
One-hot targets, shape |
required |
caches
|
list[tuple]
|
Per-timestep caches from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Accumulated gradients |
Source code in src/dlhub/nn/sequence/rnn.py
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | |
rnn_cell_backward ¶
rnn_cell_backward(y_t: ndarray, da_next: ndarray, cache: tuple, parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backward pass for a single timestep.
Combines the two gradient sources that reach a^{<t>} -- the output at
this step and the recurrence carried back from t+1 (da_next) -- and
propagates them through the softmax read-out and the tanh recurrence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_t
|
ndarray
|
One-hot target at this timestep, shape |
required |
da_next
|
ndarray
|
Gradient of the loss w.r.t. this step's hidden state arriving from the
recurrence at |
required |
cache
|
tuple
|
|
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
This step's gradient contributions: |
Source code in src/dlhub/nn/sequence/rnn.py
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | |
rnn_cell_forward ¶
rnn_cell_forward(x_t: ndarray, a_prev: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray, tuple]
One timestep of the forward recurrence.
Computes the hidden-state update and the softmax read-out for a single step::
a^{<t>} = tanh(W_aa a^{<t-1>} + W_ax x^{<t>} + b_a)
y_hat^{<t>} = softmax(W_ya a^{<t>} + b_y)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
ndarray
|
Input at this timestep, shape |
required |
a_prev
|
ndarray
|
Hidden state carried in from the previous step, |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
a_next |
ndarray
|
Updated hidden state |
y_pred_t |
ndarray
|
Output distribution |
cache |
tuple
|
|
Source code in src/dlhub/nn/sequence/rnn.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
rnn_forward ¶
rnn_forward(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> tuple[np.ndarray, np.ndarray, list[tuple]]
Forward propagation through time across a whole sequence.
Unrolls :func:rnn_cell_forward over T_x timesteps, threading the
hidden state from one step to the next.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence of shape |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
a0
|
ndarray or None
|
Initial hidden state |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a |
ndarray
|
All hidden states stacked over time, shape |
y_pred |
ndarray
|
All output distributions, shape |
caches |
list[tuple]
|
Per-timestep caches, in order, for :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/rnn.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
update_parameters ¶
update_parameters(parameters: dict[str, ndarray], gradients: dict[str, ndarray], learning_rate: float) -> dict[str, np.ndarray]
One step of gradient descent on the shared weights.
Applies theta <- theta - learning_rate * dtheta to each of Wax,
Waa, Wya, ba, by.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameters
|
dict[str, ndarray]
|
The current weights and biases. |
required |
gradients
|
dict[str, ndarray]
|
Their gradients, keyed |
required |
learning_rate
|
float
|
The step size. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
A new dictionary of updated parameters. The input is not modified. |
Source code in src/dlhub/nn/sequence/rnn.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 | |
gru ¶
Gated Recurrent Unit (GRU)¶
A from-scratch, teaching reference for the GRU: the two gates, the candidate activation, the blend that carries state forward, and the hand-derived backpropagation through time. This module is the explanation the accompanying documents point at, so the forward pass appears in the shapes the topic declares and every gradient is computed term by term rather than fused into one expression.
Notation extends the vanilla RNN reference in :mod:dlhub.nn.sequence.rnn --
same a^{<t>}, same shape conventions, same read-out and loss -- with the
gate vectors written as Andrew Ng's Course 5 writes them. For the GRU the hidden
state and the cell state are the same object, a^{<t>} = c^{<t>}, so there is
no separate c to thread. For t = 1 ... T_x::
Gamma_r^{<t>} = sigma(W_r [a^{<t-1>}; x^{<t>}] + b_r)
Gamma_u^{<t>} = sigma(W_u [a^{<t-1>}; x^{<t>}] + b_u)
a_tilde^{<t>} = tanh(W_c [Gamma_r^{<t>} * a^{<t-1>}; x^{<t>}] + b_c)
a^{<t>} = Gamma_u^{<t>} * a_tilde^{<t>} + (1 - Gamma_u^{<t>}) * a^{<t-1>}
y_hat^{<t>} = softmax(W_y a^{<t>} + b_y)
(* is elementwise.) The update gate Gamma_u interpolates between keeping
the previous state and taking the candidate; the reset gate Gamma_r decides
how much of the previous state the candidate is allowed to see. The
(1 - Gamma_u) term is the GRU's answer to the vanishing gradient: when the
gate is near zero the state is copied forward, and its gradient with it, instead
of being pushed through another tanh and another weight matrix. The vanilla
RNN in this package has no such path, which is the comparison the topic exists
to make. The LSTM's version of the same idea, with a separate cell state, is in
:mod:dlhub.nn.sequence.lstm.
References
- Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. EMNLP. https://arxiv.org/abs/1406.1078
- Chung, J., Gulcehre, C., Cho, K., & Bengio, Y. (2014). Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling. https://arxiv.org/abs/1412.3555
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, Ch. 10. MIT Press. https://www.deeplearningbook.org/
- Ng, A. (2018). Deep Learning Specialization, Course 5 (Sequence Models), Week 1. https://www.coursera.org/specializations/deep-learning
License
MIT
Notes
- The reset gate is applied before the hidden transform. The candidate reads
[Gamma_r * a^{<t-1>}; x^{<t>}], so the gate acts on the state inside the concatenationW_cmultiplies. This is the Cho/Ng form and it is canonical for this hub. Several frameworks implement the reset after the recurrent transform instead --tanh(W_x x + r * (W_h a + b_h)), which is whattorch.nn.GRUandtf.keras.layers.GRU(reset_after=True)compute. The two are not algebraically equal; the same layer name denotes two different functions across the ecosystem. The hub's TensorFlow port reproduces this module withreset_after=False. - Stable sigmoid. Gates use :func:
dlhub.nn.sequence._common.sigmoid, which never exponentiates a positive number; see that module'sNotes. The softmax read-out and the loss are imported from :mod:dlhub.nn.sequence.rnn, the canonical home of the max-subtraction stability shift, rather than restated. - Derivatives are read off the cached activations.
sigma'(z) = Gamma (1 - Gamma)andtanh'(z) = 1 - a_tilde^2both reuse the value the forward pass already computed, so the backward pass evaluates no new sigmoid and no new tanh -- the same trick the vanilla RNN uses for its single tanh. - Concatenated weight form. One matrix per gate acts on
[a^{<t-1>}; x^{<t>}], soW_gis(n_a, n_a + n_x). The vanilla RNN deliberately keepsW_aaandW_axseparate to expose the two BPTT paths; that lesson is taught there, and repeating it across three gates is clutter. The two halves are still nameable: :func:split_gate_matrixcuts anyW_gordW_gat columnn_ainto its recurrent and input parts, which is what :func:mainreports. - The cache is a dict, not a tuple. The vanilla RNN's four-element positional cache is readable; a gated cell's is not, so every cached array is named.
gru_cell_forward ¶
gru_cell_forward(x_t: ndarray, a_prev: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray, dict[str, np.ndarray]]
One timestep of the GRU forward recurrence.
Computes the two gates, the candidate activation, the blended state, and the softmax read-out for a single step, in that order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
ndarray
|
Input at this timestep, shape |
required |
a_prev
|
ndarray
|
Hidden state carried in from the previous step, |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
a_next |
ndarray
|
Updated hidden state |
y_pred_t |
ndarray
|
Output distribution |
cache |
dict[str, ndarray]
|
Everything this step's backward pass needs, each array named. |
Source code in src/dlhub/nn/sequence/gru.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
gru_forward ¶
gru_forward(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> tuple[np.ndarray, np.ndarray, list[dict[str, np.ndarray]]]
Forward propagation through time across a whole sequence.
Unrolls :func:gru_cell_forward over T_x timesteps, threading the hidden
state from one step to the next.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence of shape |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
a0
|
ndarray or None
|
Initial hidden state |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a |
ndarray
|
All hidden states stacked over time, shape |
y_pred |
ndarray
|
All output distributions, shape |
caches |
list[dict[str, ndarray]]
|
Per-timestep caches, in order, for :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/gru.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
gru_cell_backward ¶
gru_cell_backward(y_t: ndarray, da_next: ndarray, cache: dict[str, ndarray], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backward pass for a single GRU timestep.
Walks the forward pass backwards, one equation at a time. Three separate
paths deposit gradient into a^{<t-1>}, and the whole point of the cell is
that the first of them is a plain elementwise factor:
- the blend, through
(1 - Gamma_u)-- no weight matrix, no saturating nonlinearity, so gradient crosses the step essentially undamped when the update gate is near zero; - the candidate, through
W_cand then the reset gate; - the gates themselves, through
W_randW_u.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_t
|
ndarray
|
One-hot target at this timestep, shape |
required |
da_next
|
ndarray
|
Gradient of the loss w.r.t. this step's hidden state arriving from the
recurrence at |
required |
cache
|
dict[str, ndarray]
|
This step's cache from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
This step's gradient contributions: |
Source code in src/dlhub/nn/sequence/gru.py
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
gru_backward ¶
gru_backward(y: ndarray, caches: list[dict[str, ndarray]], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backpropagation through time (BPTT) for the GRU.
Walks the unrolled graph from t = T_x back to t = 1. Every gate
weight is shared across time, so each receives an accumulated gradient
summed over all timesteps, and the hidden-state gradient is threaded
backward so each step adds its recurrence contribution (da_prev) to the
next step's incoming gradient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ndarray
|
One-hot targets, shape |
required |
caches
|
list[dict[str, ndarray]]
|
Per-timestep caches from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Accumulated gradients |
Source code in src/dlhub/nn/sequence/gru.py
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | |
make_fixture ¶
make_fixture() -> dict
Build the deterministic fixture every GRU implementation and test shares.
Draws are recorded in exactly the order the topic dossier pins, under
np.random.seed(1) and float64 (the default of randn), so the
arrays reproduce bit-for-bit across implementations. The one-hot targets are
drawn last so that adding them cannot disturb the arrays a forward-parity
check compares. Dimensions match the vanilla RNN fixture, so the notebook can
contrast the two cells on identical shapes.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/nn/sequence/gru.py
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 | |
main ¶
main() -> None
Run the shared fixture end to end and print shapes, loss, and grad norms.
Source code in src/dlhub/nn/sequence/gru.py
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 | |
lstm ¶
Long Short-Term Memory (LSTM)¶
A from-scratch, teaching reference for the LSTM: the three gates, the cell state, the additive path that carries gradient across time, and the hand-derived backpropagation through time. This module is the explanation the accompanying documents point at, so the forward pass appears in the shapes the topic declares and every gradient is computed term by term rather than fused into one expression.
Notation extends the vanilla RNN reference in :mod:dlhub.nn.sequence.rnn --
same a^{<t>}, same shape conventions, same read-out and loss -- by adding a
separate cell state c^{<t>} and the gate vectors, written as Andrew Ng's
Course 5 writes them. For t = 1 ... T_x::
Gamma_f^{<t>} = sigma(W_f [a^{<t-1>}; x^{<t>}] + b_f)
Gamma_u^{<t>} = sigma(W_u [a^{<t-1>}; x^{<t>}] + b_u)
c_tilde^{<t>} = tanh (W_c [a^{<t-1>}; x^{<t>}] + b_c)
Gamma_o^{<t>} = sigma(W_o [a^{<t-1>}; x^{<t>}] + b_o)
c^{<t>} = Gamma_u^{<t>} * c_tilde^{<t>} + Gamma_f^{<t>} * c^{<t-1>}
a^{<t>} = Gamma_o^{<t>} * tanh(c^{<t>})
y_hat^{<t>} = softmax(W_y a^{<t>} + b_y)
(* is elementwise.) Gates are computed and reported in the order forget,
update, candidate, output throughout this module. The update gate is
Hochreiter & Schmidhuber's input gate i; Ng writes it Gamma_u, which
this hub follows so that its correspondence with the GRU's update gate is
visible at a glance.
The cell-state line is the reason the architecture exists. c^{<t>} is built
by addition, and holding the gates fixed its derivative with respect to
c^{<t-1>} is the elementwise factor Gamma_f^{<t>} -- not a repeated
multiplication by W_aa^T through a tanh derivative, as in the vanilla
RNN, whose product decays geometrically. A forget gate near 1 therefore
transports gradient across many steps essentially undamped. The GRU in
:mod:dlhub.nn.sequence.gru achieves the same thing with one gate fewer and no
separate cell state.
References
- Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735-1780.
- Gers, F. A., Schmidhuber, J., & Cummins, F. (2000). Learning to Forget: Continual Prediction with LSTM. Neural Computation, 12(10), 2451-2471. (The forget gate, which the 1997 cell lacked and every modern LSTM has.)
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, Ch. 10. MIT Press. https://www.deeplearningbook.org/
- Ng, A. (2018). Deep Learning Specialization, Course 5 (Sequence Models), Week 1. https://www.coursera.org/specializations/deep-learning
License
MIT
Notes
- Stable sigmoid. Gates use :func:
dlhub.nn.sequence._common.sigmoid, which never exponentiates a positive number; see that module'sNotes. The softmax read-out and the loss are imported from :mod:dlhub.nn.sequence.rnn, the canonical home of the max-subtraction stability shift, rather than restated. - Derivatives are read off the cached activations.
sigma'(z) = Gamma (1 - Gamma)andtanh'(z) = 1 - tanh(z)^2both reuse values the forward pass already computed, so the backward pass evaluates no new sigmoid and no new tanh.tanh(c^{<t>})is cached for the same reason: it appears in the output-gate gradient and again in the cell-state gradient. - Concatenated weight form. One matrix per gate acts on
[a^{<t-1>}; x^{<t>}], soW_gis(n_a, n_a + n_x). The vanilla RNN deliberately keepsW_aaandW_axseparate to expose the two BPTT paths; that lesson is taught there, and repeating it across four gates is clutter. The two halves are still nameable: :func:split_gate_matrixcuts anyW_gordW_gat columnn_ainto its recurrent and input parts, which is what :func:mainreports. - The cache is a dict, not a tuple. The vanilla RNN's four-element positional cache is readable; a gated cell's eleven-element one is not, so every cached array is named.
- No epsilon anywhere. Nothing in the cell divides by a learned quantity, and
the stable softmax output is strictly positive, so the cross-entropy
logneeds no guard. An epsilon added "for safety" would shift the loss and break the finite-difference gradient check at the topic's1e-7tolerance.
lstm_cell_forward ¶
lstm_cell_forward(x_t: ndarray, a_prev: ndarray, c_prev: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray, np.ndarray, dict[str, np.ndarray]]
One timestep of the LSTM forward recurrence.
Computes the four gate quantities in the topic's order (forget, update, candidate, output), then the cell state, the hidden state, and the softmax read-out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
ndarray
|
Input at this timestep, shape |
required |
a_prev
|
ndarray
|
Hidden state carried in from the previous step, |
required |
c_prev
|
ndarray
|
Cell state carried in from the previous step, |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
a_next |
ndarray
|
Updated hidden state |
c_next |
ndarray
|
Updated cell state |
y_pred_t |
ndarray
|
Output distribution |
cache |
dict[str, ndarray]
|
Everything this step's backward pass needs, each array named. |
Source code in src/dlhub/nn/sequence/lstm.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
lstm_forward ¶
lstm_forward(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, c0: ndarray | None = None) -> tuple[np.ndarray, np.ndarray, np.ndarray, list[dict[str, np.ndarray]]]
Forward propagation through time across a whole sequence.
Unrolls :func:lstm_cell_forward over T_x timesteps, threading two
states from one step to the next: the hidden state a and the cell state
c.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence of shape |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
a0
|
ndarray or None
|
Initial hidden state |
None
|
c0
|
ndarray or None
|
Initial cell state |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a |
ndarray
|
All hidden states stacked over time, shape |
c |
ndarray
|
All cell states stacked over time, shape |
y_pred |
ndarray
|
All output distributions, shape |
caches |
list[dict[str, ndarray]]
|
Per-timestep caches, in order, for :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/lstm.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | |
lstm_cell_backward ¶
lstm_cell_backward(y_t: ndarray, da_next: ndarray, dc_next: ndarray, cache: dict[str, ndarray], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backward pass for a single LSTM timestep.
Walks the forward pass backwards, one equation at a time. Two gradients
arrive from the future, not one: da_next along the hidden state and
dc_next along the cell state. They meet at c^{<t>}, because the
hidden state is a function of the cell state at the same step.
The line worth reading twice is dc_prev = dc_t * Gamma_f. That is the
whole thesis of the architecture as one statement: the gradient crossing
from t to t-1 along the cell path is scaled elementwise by the forget
gate, with no weight matrix and no saturating nonlinearity in the way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_t
|
ndarray
|
One-hot target at this timestep, shape |
required |
da_next
|
ndarray
|
Gradient w.r.t. this step's hidden state arriving from the recurrence at
|
required |
dc_next
|
ndarray
|
Gradient w.r.t. this step's cell state arriving from the recurrence at
|
required |
cache
|
dict[str, ndarray]
|
This step's cache from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
This step's gradient contributions: |
Source code in src/dlhub/nn/sequence/lstm.py
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | |
lstm_backward ¶
lstm_backward(y: ndarray, caches: list[dict[str, ndarray]], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backpropagation through time (BPTT) for the LSTM.
Walks the unrolled graph from t = T_x back to t = 1, threading
two recurrent gradients backward -- one along the hidden state and one
along the cell state. Every gate weight is shared across time, so each
receives an accumulated gradient summed over all timesteps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ndarray
|
One-hot targets, shape |
required |
caches
|
list[dict[str, ndarray]]
|
Per-timestep caches from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Accumulated gradients |
Source code in src/dlhub/nn/sequence/lstm.py
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | |
make_fixture ¶
make_fixture() -> dict
Build the deterministic fixture every LSTM implementation and test shares.
Draws are recorded in exactly the order the topic dossier pins, under
np.random.seed(1) and float64 (the default of randn), so the
arrays reproduce bit-for-bit across implementations. The one-hot targets are
drawn last so that adding them cannot disturb the arrays a forward-parity
check compares. Dimensions match the vanilla RNN fixture, so the notebook can
contrast the two cells on identical shapes.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/nn/sequence/lstm.py
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 | |
main ¶
main() -> None
Run the shared fixture end to end and print shapes, loss, and grad norms.
Source code in src/dlhub/nn/sequence/lstm.py
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 | |
min_gated ¶
Minimal Gated Recurrent Cells (minGRU and minLSTM)¶
A from-scratch, teaching reference for the two minimal gated cells of "Were RNNs All We Needed?" (Feng et al., 2024). Section numbers quoted below are that paper's, so any line here can be checked against it without reading it whole.
The reduction that makes these cells parallelisable is a single deletion: the
gates stop depending on a^{<t-1>} and read x^{<t>} alone (Sec. 3.1.1,
Sec. 3.2.1). For t = 1 ... T_x::
minGRU (Sec. 3.1.3)
Gamma_u^{<t>} = sigma(W_u x^{<t>})
a_tilde^{<t>} = W_c x^{<t>}
a^{<t>} = (1 - Gamma_u^{<t>}) * a^{<t-1>} + Gamma_u^{<t>} * a_tilde^{<t>}
minLSTM (Sec. 3.2.4)
Gamma_f^{<t>} = sigma(W_f x^{<t>})
Gamma_u^{<t>} = sigma(W_u x^{<t>})
a_tilde^{<t>} = W_c x^{<t>}
Gamma_f'^{<t>} = Gamma_f^{<t>} / (Gamma_f^{<t>} + Gamma_u^{<t>})
Gamma_u'^{<t>} = Gamma_u^{<t>} / (Gamma_f^{<t>} + Gamma_u^{<t>})
a^{<t>} = Gamma_f'^{<t>} * a^{<t-1>} + Gamma_u'^{<t>} * a_tilde^{<t>}
Neither cell has bias terms, and neither has a recurrent weight half: every
weight here is (n_a, n_x). Both cells are then the same first-order
linear recurrence (Sec. 2.3)::
a^{<t>} = alpha^{<t>} * a^{<t-1>} + beta^{<t>}
with alpha = 1 - Gamma_u and beta = Gamma_u * a_tilde for minGRU, and
alpha = Gamma_f' and beta = Gamma_u' * a_tilde for minLSTM. Because
alpha and beta depend on x alone, they are computable for every
timestep at once, so the sequence resolves by an associative (parallel)
scan rather than a serial loop. This module ships all three forms:
:func:sequential_scan is the definition, :func:parallel_scan is the same
function evaluated in a different association order, and :func:log_space_scan
is the numerically stable version of the parallel one (Sec. B.1).
What the reduction throws away -- the three details a reader arriving from the classical cells is most likely to "fix" back:
- minGRU has no reset gate (Sec. 3.1.1). The candidate no longer reads the previous state, so there is nothing left to reset.
- minLSTM has no output gate (Sec. 3.2.3), and because
a^{<t>} = c^{<t>}the separate cell state collapses with it. The two remaining gates are normalised to sum to one, which gives the state a time-independent scale. - The candidate is a plain linear map: there is no tanh (Sec. 3.1.2). See
:func:
identity_candidate.
Parameter counts follow from the deletions: minGRU is O(2 d_h d_x) against
the GRU's O(3 d_h (d_x + d_h)), and minLSTM O(3 d_h d_x) against the
LSTM's O(4 d_h (d_x + d_h)).
Shapes, in the sequence topic's convention (time last)::
x(n_x, m, T_x)
W_f, W_u, W_c(n_a, n_x)
a0(n_a, m)
alpha, beta, a(n_a, m, T_x)
Scope: this module derives no BPTT. The classical GRU and LSTM references in this package carry the hand-derived, gradient-checked backward passes; what is new here is the reduction and the scan, and a fourth backward derivation would only repeat a lesson those two already teach.
References
- Feng, L., Tung, F., Ahmed, M. O., Bengio, Y., & Hajimirsadeghi, H. (2024). Were RNNs All We Needed? arXiv:2410.01201. https://arxiv.org/abs/2410.01201
- Heinsen, F. A. (2023). Efficient Parallelization of a Ubiquitous Sequential Computation. arXiv:2311.06281. https://arxiv.org/abs/2311.06281
- Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. EMNLP. https://arxiv.org/abs/1406.1078
- Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735-1780.
License
MIT
Notes
- Softplus carries the gates in log space.
sigmoidis the shared, overflow-free helper the classical cells in this package use;softplusis added here, computed aslog1p(exp(-|z|)) + max(z, 0)so it never exponentiates a positive number. The two are the same function seen twice, sincesigma(z) = exp(-softplus(-z)), which is what makes the log-space gates a rearrangement rather than a second derivation:log Gamma_u = -softplus(-k)andlog(1 - Gamma_u) = -softplus(k), with no gate ever formed and then logged. - Why a log-space scan at all. Resolving the recurrence needs the product
of
alphaacross a prefix, and everyalphalies strictly inside(0, 1), so that product decays exponentially with the prefix: over 512 steps of a nearly-closed gate it underflows to exactly zero even in float64. :func:parallel_scannever forms it as a bare number -- it composes the affine steps, so an underflowed decay merely zeroes a contribution that has genuinely vanished -- but any formulation that recoversbetaby dividing through the running product divides by that zero, and in float32 the decay bottoms out far sooner. Sec. B.1 sidesteps the question by keeping every product as a sum of logs (Heinsen, 2023), where a 512-step decay is an ordinary negative number, and that is the form the paper ships. The price is the round trip throughlogandexp: measured here, the two scans agree to about4e-15on the fixture and2e-12over 512 steps, which is why the equivalence tolerance is1e-10rather than machine epsilon. - The log-space form needs a positive state. Logs of
betaand of the initial state only exist when both are non-negative, which is why Sec. B.2.1 replaces the candidate's identity with :func:g, a map that is strictly positive everywhere and still linear (slope 1) onx >= 0. The two candidates give different cells, so an equivalence check compares the log-space scan against the same candidate, not against the linear one. - No epsilon in minLSTM's normaliser.
Gamma_f + Gamma_uis a sum of two strictly positive sigmoids, so the division in :func:min_lstm_coefficientsis safe without a guard. It underflows only when both pre-activations fall below about-745, where both gates round to zero -- precisely the regime :func:min_lstm_log_coefficientshandles exactly, by taking the normaliser withlogaddexpinstead of a division. - No power-of-two assumption. Both :func:
parallel_scanand :func:logcumsumexpdouble their stride withwhile stride < T_x, so a sequence of any length gets theceil(log2 T_x)rounds it needs. The fixture'sT_x = 7is odd and not a power of two on purpose: a scan that quietly assumes padding to 8 passes on a length of 4 and fails here.
softplus ¶
softplus(z: ndarray) -> np.ndarray
Stable log(1 + exp(z)).
Rewritten as log1p(exp(-|z|)) + max(z, 0), which is algebraically the
same function but only ever exponentiates a non-positive number. See the
module Notes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
ndarray
|
Pre-activation of any shape. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
identity_candidate ¶
identity_candidate(k: ndarray) -> np.ndarray
The minimal cells' candidate activation: the identity (Sec. 3.1.2).
The classical GRU and LSTM squash their candidate with tanh. The
minimal cells do not: the candidate is the plain linear map
a_tilde^{<t>} = W_c x^{<t>}. This function exists so the deletion has a
name and a docstring, rather than being an absence a later reader restores
by accident.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
ndarray
|
The candidate pre-activation |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
g ¶
g(k: ndarray) -> np.ndarray
The strictly positive candidate activation of Sec. B.2.1.
::
g(k) = k + 0.5 for k >= 0
= sigma(k) for k < 0
Continuous at zero (both branches give 0.5), positive everywhere, and
still slope-1 linear on the positive half -- which is what lets the
log-space scan take log(beta) at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
ndarray
|
The candidate pre-activation |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Strictly positive values, same shape as |
Source code in src/dlhub/nn/sequence/min_gated.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
log_g ¶
log_g(k: ndarray) -> np.ndarray
log(g(k)), computed without forming g(k) first (Sec. B.2.1).
::
log g(k) = log(k + 0.5) for k >= 0
= -softplus(-k) for k < 0
The negative branch reuses the identity log sigma(k) = -softplus(-k),
so no logarithm of an underflowed sigmoid is ever taken.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
ndarray
|
The candidate pre-activation |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
project_over_time ¶
project_over_time(W: ndarray, x: ndarray) -> np.ndarray
Apply one weight matrix to every timestep in a single contraction.
Computes W x^{<t>} for all t, which is exactly the freedom the
reduction buys: with no dependence on a^{<t-1>}, nothing in a gate has
to wait for the step before it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
W
|
ndarray
|
Weight matrix of shape |
required |
x
|
ndarray
|
Input sequence of shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Pre-activations of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/min_gated.py
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
min_gru_coefficients ¶
min_gru_coefficients(x: ndarray, parameters: dict[str, ndarray], candidate: Callable[[ndarray], ndarray] = identity_candidate) -> tuple[np.ndarray, np.ndarray]
The minGRU recurrence coefficients (alpha, beta) (Sec. 3.1.3).
::
Gamma_u = sigma(W_u x)
a_tilde = candidate(W_c x)
alpha = 1 - Gamma_u
beta = Gamma_u * a_tilde
so that a^{<t>} = alpha^{<t>} * a^{<t-1>} + beta^{<t>} reproduces the
cell's blend. There is no reset gate: the candidate never reads
a^{<t-1>}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation applied to |
identity_candidate
|
Returns:
| Name | Type | Description |
|---|---|---|
alpha |
ndarray
|
Per-step decay |
beta |
ndarray
|
Per-step injection |
Source code in src/dlhub/nn/sequence/min_gated.py
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | |
min_lstm_coefficients ¶
min_lstm_coefficients(x: ndarray, parameters: dict[str, ndarray], candidate: Callable[[ndarray], ndarray] = identity_candidate) -> tuple[np.ndarray, np.ndarray]
The minLSTM recurrence coefficients (alpha, beta) (Sec. 3.2.4).
::
Gamma_f = sigma(W_f x)
Gamma_u = sigma(W_u x)
a_tilde = candidate(W_c x)
Gamma_f' = Gamma_f / (Gamma_f + Gamma_u)
Gamma_u' = Gamma_u / (Gamma_f + Gamma_u)
alpha = Gamma_f'
beta = Gamma_u' * a_tilde
The normalisation is what replaces the missing output gate: because
Gamma_f' + Gamma_u' = 1 exactly, the state is a convex blend at every
step and so keeps a time-independent scale.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation applied to |
identity_candidate
|
Returns:
| Name | Type | Description |
|---|---|---|
alpha |
ndarray
|
The normalised forget gate |
beta |
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | |
min_gru_log_coefficients ¶
min_gru_log_coefficients(x: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray]
The minGRU coefficients in log space (Sec. B.1), using :func:g.
Takes the gate logarithms straight from the pre-activation
k_u = W_u x, never forming the gate itself::
log alpha = log(1 - Gamma_u) = -softplus(k_u)
log beta = log Gamma_u + log g(k_c) = -softplus(-k_u) + log_g(k_c)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
log_alpha |
ndarray
|
|
log_beta |
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | |
min_lstm_log_coefficients ¶
min_lstm_log_coefficients(x: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray]
The minLSTM coefficients in log space (Sec. B.1), using :func:g.
The gate normalisation becomes a subtraction::
log Gamma_f = -softplus(-k_f)
log Gamma_u = -softplus(-k_u)
log(Gamma_f + Gamma_u) = logaddexp(log Gamma_f, log Gamma_u)
log alpha = log Gamma_f - log(Gamma_f + Gamma_u)
log beta = log Gamma_u - log(Gamma_f + Gamma_u) + log_g(k_c)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
log_alpha |
ndarray
|
|
log_beta |
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 | |
sequential_scan ¶
sequential_scan(alpha: ndarray, beta: ndarray, a0: ndarray | None = None) -> np.ndarray
Resolve a^{<t>} = alpha^{<t>} a^{<t-1>} + beta^{<t>} one step at a time.
This is the definition, written as the loop the recurrence describes. It is the reference the two scans below are checked against.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
ndarray
|
Per-step coefficients, each |
required |
beta
|
ndarray
|
Per-step coefficients, each |
required |
a0
|
ndarray or None
|
Initial state |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The hidden states, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/min_gated.py
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 | |
parallel_scan ¶
parallel_scan(alpha: ndarray, beta: ndarray, a0: ndarray | None = None) -> np.ndarray
Resolve the same recurrence by an associative scan, in plain space.
Each timestep is an affine map a -> alpha * a + beta, and composing two
of them -- first (alpha_1, beta_1), then (alpha_2, beta_2) -- gives
another affine map::
(alpha_1, beta_1) . (alpha_2, beta_2)
= (alpha_2 * alpha_1, alpha_2 * beta_1 + beta_2)
Composition is associative, so the prefix compositions can be built by
doubling the stride (the Hillis-Steele inclusive scan) in
ceil(log2 T_x) rounds instead of T_x serial steps. After the
doubling, entry t holds the single affine map from a^{<0>} to
a^{<t>}, which is then applied to the initial state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
ndarray
|
Per-step coefficients, each |
required |
beta
|
ndarray
|
Per-step coefficients, each |
required |
a0
|
ndarray or None
|
Initial state |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The hidden states, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/min_gated.py
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 | |
logcumsumexp ¶
logcumsumexp(z: ndarray) -> np.ndarray
Cumulative log(sum(exp(.))) along the last (time) axis.
Built with the same stride-doubling as :func:parallel_scan, with
np.logaddexp -- itself associative -- in place of affine composition.
NumPy has no built-in for this, and the naive log(cumsum(exp(z)))
overflows exactly where a log-space scan is worth having.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
ndarray
|
Log-domain values, shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 | |
log_space_scan ¶
log_space_scan(log_alpha: ndarray, log_beta: ndarray, a0: ndarray | None = None) -> np.ndarray
Resolve the recurrence in log space -- the stable scan of Sec. B.1.
Writing A_t = sum_{i <= t} log alpha_i for the cumulative log-decay, the
closed form of the recurrence is::
a^{<t>} = exp( A_t + log( sum_{j <= t} exp(log beta_j - A_j) ) )
with the initial state entering as the j = 0 term (coefficient one,
hence log alpha_0 = 0). Every product has become a sum and every sum a
logaddexp, so nothing underflows on the way. This is Heinsen's (2023)
formulation, which the paper adopts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log_alpha
|
ndarray
|
Logs of the per-step coefficients, each |
required |
log_beta
|
ndarray
|
Logs of the per-step coefficients, each |
required |
a0
|
ndarray or None
|
Initial state |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The hidden states, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the two coefficient arrays disagree in shape, if |
Source code in src/dlhub/nn/sequence/min_gated.py
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 | |
min_gru_forward_sequential ¶
min_gru_forward_sequential(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minGRU, resolved by the serial recurrence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 | |
min_gru_forward_parallel ¶
min_gru_forward_parallel(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minGRU, resolved by the plain-space associative scan.
Same function as :func:min_gru_forward_sequential, evaluated in a
different association order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 | |
min_gru_forward_log ¶
min_gru_forward_log(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> np.ndarray
Forward pass for minGRU through the log-space scan (Sec. B.1).
This is the positive-candidate cell: the candidate is :func:g, not the
identity, because a log-space scan needs beta > 0. It therefore matches
:func:min_gru_forward_sequential called with candidate=g, and not the
default linear one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Non-negative initial state, shape |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 | |
min_lstm_forward_sequential ¶
min_lstm_forward_sequential(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minLSTM, resolved by the serial recurrence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 | |
min_lstm_forward_parallel ¶
min_lstm_forward_parallel(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None, candidate: Callable[[ndarray], ndarray] = identity_candidate) -> np.ndarray
Forward pass for minLSTM, resolved by the plain-space associative scan.
Same function as :func:min_lstm_forward_sequential, evaluated in a
different association order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Initial state, shape |
None
|
candidate
|
Callable[[ndarray], ndarray]
|
Candidate activation, default :func: |
identity_candidate
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 | |
min_lstm_forward_log ¶
min_lstm_forward_log(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> np.ndarray
Forward pass for minLSTM through the log-space scan (Sec. B.1).
As with :func:min_gru_forward_log, the candidate is :func:g, so this
matches :func:min_lstm_forward_sequential called with candidate=g.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters
|
dict[str, ndarray]
|
|
required |
a0
|
ndarray or None
|
Non-negative initial state, shape |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Hidden states |
Source code in src/dlhub/nn/sequence/min_gated.py
846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 | |
make_fixture ¶
make_fixture() -> dict
Build the deterministic fixture this module's tests and documents share.
Draws follow the order the topic dossier pins, under np.random.seed(1)
and float64: x, a0, then minGRU's Wu, Wc and minLSTM's
Wf, Wu, Wc. The dimensions match the sibling cell modules except
for T_x = 7, which is deliberately odd and not a power of two -- a scan
that assumes a padded length passes at T_x = 4 and fails here.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/nn/sequence/min_gated.py
876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 | |
main ¶
main() -> None
Run the fixture through all three forms and print the disagreements.
Source code in src/dlhub/nn/sequence/min_gated.py
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 | |
rnn ¶
Recurrent Neural Network (Vanilla RNN)¶
A from-scratch, teaching reference for the simple recurrent network: the recurrence relation, forward propagation through time, and backpropagation through time (BPTT). This module is the explanation the accompanying documents point at, so the forward pass appears in the shapes the topic declares and the gradients are computed term by term rather than collapsed into a single fused expression.
Notation follows Andrew Ng's Deep Learning Specialization, Course 5, Week 1,
verbatim, so the sibling sequence topics (LSTM/GRU, language modeling) extend
the same symbols. For t = 1 ... T_x::
a^{<t>} = tanh(W_aa a^{<t-1>} + W_ax x^{<t>} + b_a)
y_hat^{<t>} = softmax(W_ya a^{<t>} + b_y)
with a^{<0>} the initial hidden state (the zero vector by default). The
total loss is summed over timesteps, L = sum_t L^{<t>}, with each L^{<t>}
the softmax cross-entropy of y_hat^{<t>} against the target y^{<t>}.
This implements the many-to-many, equal-length case (one output per input
step). The other input/output cardinalities -- one-to-one, one-to-many,
many-to-one, many-to-many with unequal lengths -- reuse this exact cell and
differ only in which steps receive an input and which produce a read-out; they
need no new cell mathematics. Bidirectional and deep (stacked) RNNs are
provided below as compositions of this cell, shown as forward-pass shape
walkthroughs rather than re-derived with their own BPTT. Gated cells (GRU,
LSTM), the answer to the vanishing-gradient problem stated here, live in the
lstm-and-gru topic.
References
- Elman, J. L. (1990). Finding Structure in Time. Cognitive Science, 14(2), 179-211.
- Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). Learning representations by back-propagating errors. Nature, 323, 533-536.
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, Ch. 10. MIT Press. https://www.deeplearningbook.org/
- Pascanu, R., Mikolov, T., & Bengio, Y. (2013). On the difficulty of training Recurrent Neural Networks. ICML. https://arxiv.org/abs/1211.5063
- Schuster, M., & Paliwal, K. K. (1997). Bidirectional Recurrent Neural Networks. IEEE Transactions on Signal Processing, 45(11), 2673-2681.
- Ng, A. (2018). Deep Learning Specialization, Course 5 (Sequence Models), Week 1. https://www.coursera.org/specializations/deep-learning
License
MIT
Notes
- Stable softmax. The textbook softmax
exp(z) / sum(exp(z))overflows for large logits. The read-out subtracts the per-column maximum before exponentiating, which is algebraically identical (the shift cancels in the ratio) but keeps every exponent<= 0. Because the shifted denominator is>= 1, the output is strictly positive, so the cross-entropylogis taken without any epsilon guard -- and the analytic gradient(y_hat - y) / mstays exact for the finite-difference check. Thesoftmaxandcompute_lossimplementations are the shared sequence-cell helpers in :mod:dlhub.nn.sequence._common, re-exported here -- the module whose lesson this is -- and from the package root, so every existing import path still resolves. - The tanh backward uses
1 - tanh(z)^2 = 1 - (a^{<t>})^2: sincea^{<t>}is already the tanh output, its derivative is read straight off the cached activation with no second tanh evaluation. - Stacked weight equivalence. Ng also writes the recurrence with a single
weight
W_a = [W_aa | W_ax]acting on the concatenation[a^{<t-1>}; x^{<t>}]. This module keepsW_aaandW_axseparate so that the two weight-gradient accumulations -- the recurrent pathdW_aaand the input pathdW_ax-- appear as distinct terms, which is the BPTT lesson. The two forms are numerically identical, and PyTorch'snn.RNNlikewise keepsweight_hhandweight_ihseparate.
rnn_cell_forward ¶
rnn_cell_forward(x_t: ndarray, a_prev: ndarray, parameters: dict[str, ndarray]) -> tuple[np.ndarray, np.ndarray, tuple]
One timestep of the forward recurrence.
Computes the hidden-state update and the softmax read-out for a single step::
a^{<t>} = tanh(W_aa a^{<t-1>} + W_ax x^{<t>} + b_a)
y_hat^{<t>} = softmax(W_ya a^{<t>} + b_y)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_t
|
ndarray
|
Input at this timestep, shape |
required |
a_prev
|
ndarray
|
Hidden state carried in from the previous step, |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
a_next |
ndarray
|
Updated hidden state |
y_pred_t |
ndarray
|
Output distribution |
cache |
tuple
|
|
Source code in src/dlhub/nn/sequence/rnn.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
rnn_forward ¶
rnn_forward(x: ndarray, parameters: dict[str, ndarray], a0: ndarray | None = None) -> tuple[np.ndarray, np.ndarray, list[tuple]]
Forward propagation through time across a whole sequence.
Unrolls :func:rnn_cell_forward over T_x timesteps, threading the
hidden state from one step to the next.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence of shape |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
a0
|
ndarray or None
|
Initial hidden state |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a |
ndarray
|
All hidden states stacked over time, shape |
y_pred |
ndarray
|
All output distributions, shape |
caches |
list[tuple]
|
Per-timestep caches, in order, for :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/rnn.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
rnn_cell_backward ¶
rnn_cell_backward(y_t: ndarray, da_next: ndarray, cache: tuple, parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backward pass for a single timestep.
Combines the two gradient sources that reach a^{<t>} -- the output at
this step and the recurrence carried back from t+1 (da_next) -- and
propagates them through the softmax read-out and the tanh recurrence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_t
|
ndarray
|
One-hot target at this timestep, shape |
required |
da_next
|
ndarray
|
Gradient of the loss w.r.t. this step's hidden state arriving from the
recurrence at |
required |
cache
|
tuple
|
|
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
This step's gradient contributions: |
Source code in src/dlhub/nn/sequence/rnn.py
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | |
rnn_backward ¶
rnn_backward(y: ndarray, caches: list[tuple], parameters: dict[str, ndarray]) -> dict[str, np.ndarray]
Backpropagation through time (BPTT).
Walks the unrolled graph from t = T_x back to t = 1. The shared
weights receive accumulated gradients over every timestep, and the
hidden-state gradient is threaded backward so that each step adds its
recurrence contribution (da_prev) to the next step's incoming gradient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ndarray
|
One-hot targets, shape |
required |
caches
|
list[tuple]
|
Per-timestep caches from :func: |
required |
parameters
|
dict[str, ndarray]
|
The shared weights and biases (see :func: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Accumulated gradients |
Source code in src/dlhub/nn/sequence/rnn.py
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | |
clip_gradients ¶
clip_gradients(gradients: dict[str, ndarray], max_norm: float) -> dict[str, np.ndarray]
Global-norm gradient clipping, the exploding-gradient remedy.
Computes the single L2 norm of all gradients viewed as one long vector and,
if it exceeds max_norm, rescales every gradient by the same factor. This
preserves the gradient direction while capping its magnitude, exactly as
prescribed by Pascanu, Mikolov & Bengio (2013).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gradients
|
dict[str, ndarray]
|
Gradients to clip, typically the parameter gradients |
required |
max_norm
|
float
|
The maximum allowed global L2 norm. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
A new dictionary of clipped gradients. The input is not modified. |
Source code in src/dlhub/nn/sequence/rnn.py
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | |
update_parameters ¶
update_parameters(parameters: dict[str, ndarray], gradients: dict[str, ndarray], learning_rate: float) -> dict[str, np.ndarray]
One step of gradient descent on the shared weights.
Applies theta <- theta - learning_rate * dtheta to each of Wax,
Waa, Wya, ba, by.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameters
|
dict[str, ndarray]
|
The current weights and biases. |
required |
gradients
|
dict[str, ndarray]
|
Their gradients, keyed |
required |
learning_rate
|
float
|
The step size. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
A new dictionary of updated parameters. The input is not modified. |
Source code in src/dlhub/nn/sequence/rnn.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 | |
bidirectional_rnn_forward ¶
bidirectional_rnn_forward(x: ndarray, parameters_forward: dict[str, ndarray], parameters_backward: dict[str, ndarray], a0_forward: ndarray | None = None, a0_backward: ndarray | None = None) -> tuple[np.ndarray, dict[str, np.ndarray]]
Bidirectional RNN forward pass, as a composition of the vanilla cell.
Runs two independent recurrences over the sequence -- one left-to-right
(a->) and one right-to-left (a<-) -- and concatenates their hidden
states at each timestep as [a->^{<t>}; a<-^{<t>}]. There is no new cell
mathematics: this is :func:rnn_forward applied twice, once to the
time-reversed input. A bidirectional model attaches its own output layer,
mapping the 2 n_a-dimensional concatenation to n_y; the per-direction
read-outs computed by the reused forward pass are not used here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence, shape |
required |
parameters_forward
|
dict[str, ndarray]
|
Independent weight sets for the two directions. |
required |
parameters_backward
|
dict[str, ndarray]
|
Independent weight sets for the two directions. |
required |
a0_forward
|
ndarray or None
|
Initial hidden states for each direction, each shape |
None
|
a0_backward
|
ndarray or None
|
Initial hidden states for each direction, each shape |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
a_bidirectional |
ndarray
|
Concatenated hidden states, shape |
parts |
dict[str, ndarray]
|
The two direction sequences, |
Source code in src/dlhub/nn/sequence/rnn.py
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
deep_rnn_forward ¶
deep_rnn_forward(x: ndarray, parameters_per_layer: list[dict[str, ndarray]], a0_per_layer: list[ndarray] | None = None) -> tuple[list[np.ndarray], np.ndarray]
Deep (stacked) RNN forward pass, as a composition of the vanilla cell.
Each layer is itself a full RNN over the sequence produced by the layer
below: layer l consumes the activation sequence a^{[l-1]} in place of
the input x. There is no new cell mathematics -- this is
:func:rnn_forward applied once per layer. Only the top layer's read-out is
a model output; the intermediate read-outs computed by the reused forward
pass are not used.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input sequence to the first layer, shape |
required |
parameters_per_layer
|
list[dict[str, ndarray]]
|
One weight set per layer, bottom to top. Layer |
required |
a0_per_layer
|
list[ndarray] or None
|
Per-layer initial hidden states. Defaults to zero for every layer. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
activations |
list[ndarray]
|
Each layer's hidden-state sequence, bottom to top, each
|
y_pred_top |
ndarray
|
The top layer's output distributions, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/dlhub/nn/sequence/rnn.py
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 | |
make_fixture ¶
make_fixture() -> dict
Build the deterministic fixture every implementation and test shares.
Draws are recorded in exactly the order the topic dossier pins, under
np.random.seed(1) and float64 (the default of randn), so the
seven parameter arrays reproduce bit-for-bit across implementations. The
one-hot targets are drawn after those seven so that adding them cannot
disturb the arrays a forward-parity check compares.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/dlhub/nn/sequence/rnn.py
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | |
main ¶
main() -> None
Run the shared fixture end to end and print shapes, loss, and grad norms.
Source code in src/dlhub/nn/sequence/rnn.py
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | |