|
28 | 28 |
|
29 | 29 | sys.path.insert(0, str(Path(__file__).parent.parent.parent)) |
30 | 30 |
|
31 | | -from tinytorch.core.layers import Layer |
| 31 | +from tinytorch.core.layers import Layer, Sequential, Linear, ReLU, Dropout |
32 | 32 | from tinytorch.core.tensor import Tensor |
33 | 33 |
|
34 | 34 |
|
@@ -488,5 +488,74 @@ def output_shape(self, input_shape): |
488 | 488 | ) |
489 | 489 |
|
490 | 490 |
|
| 491 | +class TestSequentialRealImplementation: |
| 492 | + """ |
| 493 | + Test the real tinytorch.core.layers.Sequential container. |
| 494 | +
|
| 495 | + CONCEPT: Sequential accepts layers either as separate positional |
| 496 | + arguments (Sequential(l1, l2)) or as a single list (Sequential([l1, l2])). |
| 497 | + Both forms must produce a working, correctly shaped forward pass. |
| 498 | + """ |
| 499 | + |
| 500 | + def test_sequential_positional_args_construction_and_shape(self): |
| 501 | + """ |
| 502 | + WHAT: Sequential(Linear(2, 2), ReLU()) built from positional args. |
| 503 | +
|
| 504 | + WHY: Students commonly write Sequential(layer1, layer2, ...) the |
| 505 | + same way they would in PyTorch, without wrapping layers in a list. |
| 506 | +
|
| 507 | + STUDENT LEARNING: The real Sequential supports both call styles. |
| 508 | + """ |
| 509 | + model = Sequential(Linear(2, 2), ReLU()) |
| 510 | + assert len(model.layers) == 2 |
| 511 | + |
| 512 | + x = Tensor(np.array([[1.0, -1.0]])) |
| 513 | + output = model(x) |
| 514 | + assert output.shape == (1, 2), ( |
| 515 | + f"Sequential(Linear(2,2), ReLU()) forward shape wrong.\n" |
| 516 | + f" Expected: (1, 2)\n" |
| 517 | + f" Got: {output.shape}" |
| 518 | + ) |
| 519 | + |
| 520 | + |
| 521 | +class TestDropoutLayer: |
| 522 | + """ |
| 523 | + Test the Dropout layer's validation and training/inference behavior. |
| 524 | +
|
| 525 | + CONCEPT: Dropout only zeros elements during training and only when |
| 526 | + p > 0. It must also reject invalid probabilities at construction. |
| 527 | + """ |
| 528 | + |
| 529 | + def test_dropout_valid_construction(self): |
| 530 | + """Dropout(0.5) constructs successfully and stores p.""" |
| 531 | + dropout = Dropout(0.5) |
| 532 | + assert dropout.p == 0.5 |
| 533 | + |
| 534 | + def test_dropout_negative_p_raises(self): |
| 535 | + """Dropout(-0.1) raises ValueError.""" |
| 536 | + with pytest.raises(ValueError): |
| 537 | + Dropout(-0.1) |
| 538 | + |
| 539 | + def test_dropout_p_above_one_raises(self): |
| 540 | + """Dropout(1.1) raises ValueError.""" |
| 541 | + with pytest.raises(ValueError): |
| 542 | + Dropout(1.1) |
| 543 | + |
| 544 | + def test_should_apply_dropout_training_and_p_positive(self): |
| 545 | + """_should_apply_dropout is True when training=True and p > 0.""" |
| 546 | + dropout = Dropout(0.5) |
| 547 | + assert dropout._should_apply_dropout(training=True) is True |
| 548 | + |
| 549 | + def test_should_apply_dropout_false_when_not_training(self): |
| 550 | + """_should_apply_dropout is False when training=False.""" |
| 551 | + dropout = Dropout(0.5) |
| 552 | + assert dropout._should_apply_dropout(training=False) is False |
| 553 | + |
| 554 | + def test_should_apply_dropout_false_when_p_zero(self): |
| 555 | + """_should_apply_dropout is False when p=0, even during training.""" |
| 556 | + dropout = Dropout(0.0) |
| 557 | + assert dropout._should_apply_dropout(training=True) is False |
| 558 | + |
| 559 | + |
491 | 560 | if __name__ == "__main__": |
492 | 561 | pytest.main([__file__, "-v"]) |
0 commit comments