Skip to content
Snippets Groups Projects
03-Using-Pytorch.ipynb 94.6 KiB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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 776 777 778 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 808 809 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 864 865 866 867 868 869 870 871 872 873 874 875 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 912 913 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 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "51be1de8",
   "metadata": {},
   "source": [
    "<img width=\"800px\" src=\"../fidle/img/header.svg\"></img>\n",
    "\n",
    "# <!-- TITLE --> [PYTORCH1] - Practical Lab : PyTorch\n",
    "<!-- DESC --> PyTorch est l'un des principaux framework utilisé dans le Deep Learning\n",
    "<!-- AUTHOR : Kamel Guerda (CNRS/IDRIS) -->\n",
    "\n",
    "## Objectives :\n",
    " - Understand PyTorch"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1959d3d5-388e-4c43-8318-342f08e6b024",
   "metadata": {
    "tags": []
   },
   "source": [
    "## **Introduction**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6da1305-551a-4549-abed-641415823a33",
   "metadata": {},
   "source": [
    "**PyTorch** is an open-source machine learning library developed by Facebook's AI Research lab. It offers an imperative and dynamic computational model, making it particularly easy and intuitive for researchers. Its primary feature is the tensor, a multi-dimensional array similar to NumPy's ndarray, but with GPU acceleration."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "54c79dfb-a061-4b72-afe3-c97c28071e5c",
   "metadata": {
    "tags": []
   },
   "source": [
    "### **Installation and usage**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20852981-c289-4c4e-8099-2c5efef58e3b",
   "metadata": {},
   "source": [
    "Whether you're working on the supercomputer Jean Zay or your own machine, getting your environment ready is the first step. Here's how to proceed:"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a88f32bd-37f6-4e99-97e0-62283a146a1f",
   "metadata": {
    "tags": []
   },
   "source": [
    "#### **On Jean Zay**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8421a9f0-130d-40ef-8a7a-066bf9147066",
   "metadata": {},
   "source": [
    "For those accessing the Jean Zay supercomputer (you should already be at step 3):\n",
    "\n",
    "1. **Access JupyterHub**: Go to [https://jupyterhub.idris.fr](https://jupyterhub.idris.fr). The login credentials are the same as those used to access the Jean Zay machine. Ensure your IP address is whitelisted (add a new IP via the account management form if needed).\n",
    "2. **Create a JupyterLab Instance**: Choose to create the instance either on a frontend node (e.g., for internet access) or on a compute node by reserving resources via Slurm. Select the appropriate options such as workspace, allocated resources, billing, etc.\n",
    "3. **Choose the Kernel**: IDRIS provides kernels based on modules installed on Jean Zay. This includes various versions of Python, Tensorflow, and PyTorch. Create a new notebook with the desired kernel through the launcher or change the kernel on an existing notebook by clicking the kernel name at the top right of the screen.\n",
    "4. For advanced features like Tensorboard, MLFlow, custom kernel creation, etc., refer to the [JupyterHub technical documentation](https://jupyterhub.idris.fr/services/documentation/).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a168594c-cf18-4ed8-babf-242b56b3e0b7",
   "metadata": {
    "tags": []
   },
   "source": [
    "> **Task:** Verifying Your Kernel in the upper top corner\n",
    ">    - In JupyterLab, at the top right of your notebook, you should see the name of your current kernel.\n",
    ">    - Ensure it matches \"PyTorch 2.0\" or a similar name indicating the PyTorch version.\n",
    ">    - If it doesn't, click on the kernel name and select the appropriate kernel from the list.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0aaadeee-5115-48d0-aa57-20a0a63d5054",
   "metadata": {
    "tags": []
   },
   "source": [
    "#### **Elsewhere**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d34951e-1b7b-4776-9449-eff57a9385f4",
   "metadata": {},
   "source": [
    "\n",
    "For users on other platforms:\n",
    "\n",
    "1. Install PyTorch by following the official [installation guide](https://pytorch.org/get-started/locally/).\n",
    "2. If you have a GPU, ensure you've installed the necessary CUDA toolkit and cuDNN libraries.\n",
    "3. Launch your preferred Python environment, whether it's Jupyter notebook, an IDE like PyCharm, or just the terminal.\n",
    "\n",
    "Once your setup is complete, you're ready to dive in. Let's explore the fascinating world of deep learning!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7552d5ac-eb8c-48e0-9e61-3b056d560f7b",
   "metadata": {
    "tags": []
   },
   "source": [
    "### **Version**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "272e492f-35c5-4293-b504-8e8632da1b73",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Importing PyTorch\n",
    "import torch\n",
    "\n",
    "# TODO: Print the version of PyTorch being used\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9fdbe225-4e06-4ad0-abca-4325457dc0e1",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "To print the version of PyTorch you're using, you can access the <code>__version__</code> attribute of the <code>torch</code> module.    \n",
    "    \n",
    "```python\n",
    "print(torch.__version__)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "72752068-02fe-4e44-8c27-40e8f66680c9",
   "metadata": {
    "tags": []
   },
   "source": [
    "**Why PyTorch 2.0 is a Game-Changer**\n",
    "\n",
    "PyTorch 2.0 represents a major step in the evolution of this popular deep learning library. As part of the transition to the 2-series, let's highlight some reasons why this version is pivotal:\n",
    "\n",
    "1. **Performance**: With PyTorch 2.0, performance has been supercharged at the compiler level, offering faster execution and support for Dynamic Shapes and Distributed systems.\n",
    "  \n",
    "2. **torch.compile**: This introduces a more Pythonic approach, moving some parts of PyTorch from C++ back to Python. Notably, across a test set of 163 open-source models, the use of `torch.compile` resulted in a 43% speed increase during training on an NVIDIA A100 GPU.\n",
    "\n",
    "3. **Innovative Technologies**: Technologies like TorchDynamo and TorchInductor, both written in Python, make PyTorch more flexible and developer-friendly.\n",
    "  \n",
    "4. **Staying Pythonic**: PyTorch 2.0 emphasizes Python-centric development, reducing barriers for developers and vendors.\n",
    "\n",
    "As we progress in this lab, we'll dive deeper into some of these features, giving you hands-on experience with the power and flexibility of PyTorch 2.0.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc215c02-1f16-48be-88f9-5080fd2be9ed",
   "metadata": {
    "tags": []
   },
   "source": [
    "## **Pytorch Fundamentals**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bcd7f0fc-a714-495e-9307-e48964abd85b",
   "metadata": {
    "tags": []
   },
   "source": [
    "### **Tensors**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e185bf6-3d3c-4a43-b425-e6aa3da5d5dd",
   "metadata": {
    "tags": []
   },
   "source": [
    "A **tensor** is a generalization of vectors and matrices and is easily understood as a multi-dimensional array. In the context of PyTorch:\n",
    "- A 0-dimensional tensor is a scalar (a single number).\n",
    "- A 1-dimensional tensor is a vector.\n",
    "- A 2-dimensional tensor is a matrix.\n",
    "- ... and so on for higher dimensions.\n",
    "\n",
    "Tensors are fundamental to PyTorch not just as data containers but also for their compatibility with GPU acceleration, making operations on them extremely fast. This acceleration is vital for training large neural networks.\n",
    "\n",
    "Let's start our journey with tensors by examining how PyTorch handles scalars."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa90e399-3955-4417-a4a3-c0c812ebb1d9",
   "metadata": {
    "tags": []
   },
   "source": [
    "#### **Scalars in PyTorch**\n",
    "\n",
    "### Scalars in PyTorch\n",
    "\n",
    "A scalar, being a 0-dimensional tensor, is simply a single number. While it might seem trivial, understanding scalars in PyTorch lays the foundation for grasping more complex tensor structures. Familiarize yourself with the `torch.tensor()` function from the [official documentation](https://pytorch.org/docs/stable/generated/torch.tensor.html) before proceeding.\n",
    "\n",
    "> **Task**: Create a scalar tensor in PyTorch and examine its properties.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "b6db1841-0fab-4df0-b699-058d5a477ca6",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "ename": "SyntaxError",
     "evalue": "invalid syntax (2309926818.py, line 2)",
     "output_type": "error",
     "traceback": [
      "\u001b[0;36m  Cell \u001b[0;32mIn[2], line 2\u001b[0;36m\u001b[0m\n\u001b[0;31m    scalar_tensor = # Your code here\u001b[0m\n\u001b[0m                    ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n"
     ]
    }
   ],
   "source": [
    "# TODO: Create a scalar tensor with the value 7.5\n",
    "scalar_tensor = # Your code here\n",
    "\n",
    "# Print the scalar tensor\n",
    "print(\"Scalar Tensor:\", scalar_tensor)\n",
    "\n",
    "# TODO: Print its dimension, shape, and type\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9bc265c-9a7f-4588-8586-562b390d63d9",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "To create a scalar tensor, use the <code>torch.tensor()</code> function. To retrieve its dimension, shape, and type, you can use the <code>.dim()</code>, <code>.shape</code>, and <code>.dtype</code> attributes respectively. \n",
    "\n",
    "Here's how you can achieve that:\n",
    "\n",
    "```python\n",
    "scalar_tensor = torch.tensor(7.5)\n",
    "print(\"Scalar Tensor:\", scalar_tensor)\n",
    "print(\"Dimension:\", scalar_tensor.dim())\n",
    "print(\"Shape:\", scalar_tensor.shape)\n",
    "print(\"Type:\", scalar_tensor.dtype)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc240c26-5866-4080-bbb9-d5cde1500300",
   "metadata": {
    "tags": []
   },
   "source": [
    "#### **Vectors in PyTorch**\n",
    "\n",
    "A vector in PyTorch is a 1-dimensional tensor. It's essentially a list of numbers that can represent anything from a sequence of data points to the weights of a neural network layer.\n",
    "\n",
    "In this section, we'll see how to create and manipulate vectors using PyTorch. We'll also look at some basic operations you can perform on them.\n",
    "\n",
    "> **Task**: Create a 1-dimensional tensor (vector) with values `[1.5, 2.3, 3.1, 4.8, 5.2]` and print its dimension, shape, and type.\n",
    "\n",
    "Start by referring to the `torch.tensor()` function in the [official documentation](https://pytorch.org/docs/stable/generated/torch.tensor.html) to understand how to create tensors of varying dimensions.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "e9503b49-38d1-45d9-910f-761da82cfbd0",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "ename": "SyntaxError",
     "evalue": "invalid syntax (138343520.py, line 2)",
     "output_type": "error",
     "traceback": [
      "\u001b[0;36m  Cell \u001b[0;32mIn[3], line 2\u001b[0;36m\u001b[0m\n\u001b[0;31m    vector_tensor = # Your code here\u001b[0m\n\u001b[0m                    ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n"
     ]
    }
   ],
   "source": [
    "# TODO: Create a 1-dimensional tensor (vector) with values [1.5, 2.3, 3.1, 4.8, 5.2]\n",
    "vector_tensor = # Your code here\n",
    "\n",
    "# Print the vector tensor\n",
    "print(\"Vector Tensor:\", vector_tensor)\n",
    "\n",
    "# TODO: Print its dimension, shape, and type\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13252d1f-004f-42e0-aec9-56322b43ab72",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "Creating a 1-dimensional tensor is similar to creating a scalar. Instead of a single number, you pass a list of numbers to the <code>torch.tensor()</code> function. The <code>.dim()</code>, <code>.shape</code>, and <code>.dtype</code> attributes will help you retrieve its properties.\n",
    "\n",
    "```python\n",
    "vector_tensor = torch.tensor([1.5, 2.3, 3.1, 4.8, 5.2])\n",
    "print(\"Vector Tensor:\", vector_tensor)\n",
    "print(\"Dimension:\", vector_tensor.dim())\n",
    "print(\"Shape:\", vector_tensor.shape)\n",
    "print(\"Type:\", vector_tensor.dtype)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7bfc47a8-e99d-4683-ac36-287f35a76fd0",
   "metadata": {},
   "source": [
    "#### **Vector Operations**\n",
    "\n",
    "Vectors are not just static entities; we often perform various operations on them, especially in the context of neural networks. This includes addition, subtraction, scalar multiplication, dot products, etc.\n",
    "\n",
    "> **Task**: Using the previously defined `vector_tensor`, perform the following operations:\n",
    "1. Add 5 to all the elements of the vector.\n",
    "2. Multiply all the elements of the vector by 2.\n",
    "3. Compute the dot product of the vector with itself."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "86182e1c-5491-4743-a7c8-10b9effd8194",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "ename": "SyntaxError",
     "evalue": "invalid syntax (184231995.py, line 2)",
     "output_type": "error",
     "traceback": [
      "\u001b[0;36m  Cell \u001b[0;32mIn[4], line 2\u001b[0;36m\u001b[0m\n\u001b[0;31m    vector_added = # Your code here\u001b[0m\n\u001b[0m                   ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n"
     ]
    }
   ],
   "source": [
    "# TODO: Add 5 to all elements\n",
    "vector_added = # Your code here\n",
    "\n",
    "# TODO: Multiply all elements by 2\n",
    "vector_multiplied = # Your code here\n",
    "\n",
    "# TODO: Compute the dot product with itself\n",
    "dot_product = # Your code here\n",
    "\n",
    "# Print the results\n",
    "print(\"Vector after addition:\", vector_added)\n",
    "print(\"Vector after multiplication:\", vector_multiplied)\n",
    "print(\"Dot Product:\", dot_product)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75773a02-3ab4-4325-99fb-7a742e997f21",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "PyTorch tensors support regular arithmetic operations. For the dot product, you can use the <code>torch.dot()</code> function.\n",
    "\n",
    "```python\n",
    "\n",
    "vector_added = vector_tensor + 5\n",
    "vector_multiplied = vector_tensor * 2\n",
    "dot_product = torch.dot(vector_tensor, vector_tensor)\n",
    "\n",
    "print(\"Vector after addition:\", vector_added)\n",
    "print(\"Vector after multiplication:\", vector_multiplied)\n",
    "print(\"Dot Product:\", dot_product)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b4766ba-ef9a-4f24-ba43-7358097a7b61",
   "metadata": {
    "tags": []
   },
   "source": [
    "#### **Matrices in PyTorch**\n",
    "\n",
    "A matrix in PyTorch is represented as a 2D tensor. Just as vectors are generalizations of scalars, matrices are generalizations of vectors, providing an additional dimension. Matrices are crucial for a range of operations in deep learning, including representing datasets, transformations, and more.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ec7544d-ef87-4773-88d8-cee731d1c43c",
   "metadata": {
    "tags": []
   },
   "source": [
    "##### **Creating Matrices**\n",
    "\n",
    "Before diving into manual matrix creation, it's beneficial to know some utility functions PyTorch provides:\n",
    "\n",
    "- `torch.rand()`: Generates a matrix with random values between 0 and 1.\n",
    "- `torch.eye()`: Creates an identity matrix.\n",
    "- `torch.zeros()`: Generates a matrix filled with zeros.\n",
    "- `torch.ones()`: Generates a matrix filled with ones.\n",
    "\n",
    "You can explore more about these functions in the [official documentation](https://pytorch.org/docs/stable/tensors.html).\n",
    "\n",
    "> **Task**: Using the above functions, create the following matrices:\n",
    "> 1. A 3x3 matrix with random values.\n",
    "> 2. A 5x5 identity matrix.\n",
    "> 3. A 2x4 matrix filled with zeros.\n",
    "> 4. A 4x2 matrix filled with ones.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5014b564-6bf5-4f00-a513-578ca72d94a8",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Your code for creating the matrices goes here\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86b2708c-45c6-4b2c-b526-41491fcafa08",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "\n",
    "To create these matrices, make use of the following functions:\n",
    "\n",
    "1. `torch.rand(size)`: Use this function and specify the size as `(3, 3)` to create a 3x3 matrix with random values.\n",
    "2. `torch.eye(n, m)`: Use this to generate an identity matrix. For a square matrix like 5x5, n and m would both be 5.\n",
    "3. `torch.zeros(m, n)`: For a 2x4 matrix filled with zeros, specify m=2 and n=4.\n",
    "4. `torch.ones(m, n)`: Similar to the `zeros` function but fills the matrix with ones.\n",
    "\n",
    "```python\n",
    "# 1. 3x3 matrix with random values\n",
    "random_matrix = torch.rand(3, 3)\n",
    "print(random_matrix)\n",
    "\n",
    "# 2. 5x5 identity matrix\n",
    "identity_matrix = torch.eye(5, 5)\n",
    "print(identity_matrix)\n",
    "\n",
    "# 3. 2x4 matrix filled with zeros\n",
    "zero_matrix = torch.zeros(2, 4)\n",
    "print(zero_matrix)\n",
    "\n",
    "# 4. 4x2 matrix filled with ones\n",
    "one_matrix = torch.ones(4, 2)\n",
    "print(one_matrix)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "60ff5e51-699e-46a1-8cc7-1d5fc9a4d078",
   "metadata": {},
   "source": [
    "#### **Matrix Operations in PyTorch**\n",
    "\n",
    "Just like vectors, matrices can undergo a variety of operations. Some of the basic ones include matrix addition, subtraction, and multiplication. More advanced operations include matrix inversion, transposition, and determinant calculation.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6bdb9d9-b299-4d63-b92f-7c4b8c32a1b7",
   "metadata": {
    "tags": []
   },
   "source": [
    "##### **Basic Matrix Operations**\n",
    "\n",
    "> **Task**: Perform the following operations on matrices:\n",
    "> 1. Create two 3x3 matrices with random values.\n",
    "> 2. Add the two matrices.\n",
    "> 3. Subtract the second matrix from the first one.\n",
    "> 4. Multiply the two matrices element-wise.\n",
    "\n",
    "Remember, for matrix multiplication that results in the dot product, you'd use `torch.mm` or `@`, but for element-wise multiplication, you use `*`.\n",
    "\n",
    "Here's the [official documentation](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.matmul) on matrix operations for your reference.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6be8c647-c455-4d3b-8a21-c4b7102ffa75",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Your code for creating the matrices and performing the operations goes here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0020b26b-b2bb-4efa-9bf3-3f037acd050e",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "\n",
    "Here's how you can perform the given matrix operations:\n",
    "\n",
    "```python\n",
    "# 1. Create two 3x3 matrices with random values\n",
    "matrix1 = torch.rand(3, 3)\n",
    "matrix2 = torch.rand(3, 3)\n",
    "print(\"Matrix 1:\\n\", matrix1)\n",
    "print(\"\\nMatrix 2:\\n\", matrix2)\n",
    "\n",
    "# 2. Add the two matrices\n",
    "sum_matrix = matrix1 + matrix2\n",
    "print(\"\\nSum of matrices:\\n\", sum_matrix)\n",
    "\n",
    "# 3. Subtract the second matrix from the first one\n",
    "difference_matrix = matrix1 - matrix2\n",
    "print(\"\\nDifference of matrices:\\n\", difference_matrix)\n",
    "\n",
    "# 4. Multiply the two matrices element-wise\n",
    "product_matrix = matrix1 * matrix2\n",
    "print(\"\\nElement-wise product of matrices:\\n\", product_matrix)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "07f57464-76e2-4670-8332-3fcec2e162bd",
   "metadata": {},
   "source": [
    "#### **Higher-Dimensional Tensors in PyTorch**\n",
    "\n",
    "While scalars, vectors, and matrices cover 0D, 1D, and 2D tensors respectively, in deep learning, especially in tasks like image processing, you often encounter tensors with more than two dimensions.\n",
    "\n",
    "For instance, a colored image is often represented as a 3D tensor: height x width x channels (e.g., RGB channels). A batch of such images would then be a 4D tensor: batch_size x height x width x channels.\n",
    "\n",
    "Let's get our hands dirty with some higher-dimensional tensors!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3dd1fea7-d290-49fe-ac1f-5a8387e3d386",
   "metadata": {
    "tags": []
   },
   "source": [
    "##### **Creating a 3D Tensor**\n",
    "\n",
    "> **Task**: Create a 3D tensor representing 2 images of size 4x4 with 3 channels (like RGB) filled with random values.\n",
    "\n",
    "Use the `torch.rand` function, and remember to specify the dimensions correctly.\n",
    "\n",
    "Here's the [official documentation](https://pytorch.org/docs/stable/tensors.html#creation-ops) for tensor creation.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e7c8ac6e-f870-4b5d-ac2c-05be1d0cc9f1",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Your code for creating the 3D tensor goes here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "efe61750-a91f-428a-b4e2-7df0cc2a782b",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "\n",
    "Creating a 3D tensor with the given specifications can be achieved using the `torch.rand` function. Here's how:\n",
    "\n",
    "```python\n",
    "# Create a 3D tensor representing 2 images of size 4x4 with 3 channels\n",
    "image_tensor = torch.rand(2, 4, 4, 3)\n",
    "print(image_tensor)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8cfbcaa0-a0f6-4869-ba94-65d4439a60ca",
   "metadata": {},
   "source": [
    "#### **Reshaping Tensors**\n",
    "\n",
    "In deep learning, we often need to reshape our tensors. For instance, an image represented as a 3D tensor might need to be reshaped into a 1D tensor before passing it through a fully connected layer. PyTorch provides methods to make this easy.\n",
    "\n",
    "The most commonly used method for reshaping tensors in PyTorch is the `view()` method. Another method that offers more flexibility (especially when you're unsure about the size of one dimension) is `reshape()`.\n",
    "\n",
    ">[Task]: Using the official documentation, find out how to use the [`view()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.view) and [`reshape()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.reshape) methods. Create a 2x3 tensor using `torch.tensor()` and then reshape it into a 3x2 tensor.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e6758ba7-aa35-42f0-87c1-86b88de64238",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Create a 2x3 tensor\n",
    "\n",
    "# Reshape it into a 3x2 tensor\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fea31255-c2fe-47b2-b03b-c2b35953e05a",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "To reshape a tensor using <code>view()</code> method:\n",
    "\n",
    "```python\n",
    "tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])\n",
    "reshaped_tensor = tensor.view(3, 2)\n",
    "```\n",
    "<br>\n",
    "Alternatively, using the <code>reshape()</code> method:\n",
    "\n",
    "```python\n",
    "reshaped_tensor = tensor.reshape(3, 2)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c580dbca-b75a-4b97-a24a-6a19c7cdf8d1",
   "metadata": {},
   "source": [
    "#### **Broadcasting**\n",
    "\n",
    "Broadcasting is a powerful feature in PyTorch that allows you to perform operations between tensors of different shapes. When possible, PyTorch will automatically reshape the tensors in a way that makes the operation valid. This can significantly reduce manual reshaping and is efficient in memory usage.\n",
    "\n",
    "However, it's essential to understand the rules and nuances of broadcasting to use it effectively and avoid unexpected behaviors.\n",
    "\n",
    ">[Task]: Given a tensor `A` of shape (4, 1) and another tensor `B` of shape (1, 4), use PyTorch operations to produce a result tensor of shape (4, 4). Check the [official documentation on broadcasting](https://pytorch.org/docs/stable/notes/broadcasting.html) for guidance.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "44566fb7-87ed-41ef-a86e-db32a1cf2179",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Define tensor A of shape (4, 1) and tensor B of shape (1, 4)\n",
    "\n",
    "# Perform an operation to get a result tensor of shape (4, 4)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2602f2c4-f507-4a9a-8e8d-dee5e95efc61",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "You can simply use addition, subtraction, multiplication, or any other element-wise operation. When you do this operation, PyTorch will automatically broadcast the tensors to a compatible shape. For example:\n",
    "\n",
    "```python\n",
    "A = torch.tensor([[1], [2], [3], [4]])\n",
    "B = torch.tensor([[1, 2, 3, 4]])\n",
    "result = A * B\n",
    "print(result)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba2cc439-8ecc-4d92-b78f-39ef762678f8",
   "metadata": {
    "tags": []
   },
   "source": [
    "### **GPU Support with CUDA**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "575536c5-87a7-4781-8557-558627f14c0a",
   "metadata": {
    "tags": []
   },
   "source": [
    "PyTorch seamlessly supports operations on Graphics Processing Units (GPUs) through CUDA, an API developed by NVIDIA for their GPUs. If you have a compatible NVIDIA GPU on your machine, PyTorch can utilize it to speed up tensor operations which can be orders of magnitude faster than on a CPU.\n",
    "\n",
    "To verify if your PyTorch installation can use CUDA, you can check the attribute `torch.cuda.is_available()`. This returns `True` if CUDA is available and PyTorch can use GPUs, otherwise it returns `False`.\n",
    "\n",
    ">[Task]: Print whether CUDA support is available on your system. The [CUDA documentation](https://pytorch.org/docs/stable/cuda.html) might be useful for this task."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "38e84bb7-5026-4262-8b78-b368c55a1450",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Check and print if CUDA is available\n",
    "cuda_available = None  # Replace None with the appropriate code\n",
    "print(\"CUDA available:\", cuda_availablez"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "646b5660-5131-4ce0-9592-0fd14608c6df",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "\n",
    "To check if CUDA is available, you can utilize the torch.cuda.is_available() function.\n",
    "```python\n",
    "cuda_available = torch.cuda.is_available()\n",
    "print(\"CUDA available:\", cuda_available)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86c8d7ed-0931-4874-bb27-e796ae1a1d7a",
   "metadata": {},
   "source": [
    "When developing deep learning models in PyTorch, it's a good habit to write device-agnostic code. This means your code can automatically use a GPU if available, or fall back to using the CPU if not. The `torch.device` object allows you to specify the device (either CPU or GPU) where you'd like your tensors to be allocated.\n",
    "\n",
    "To dynamically determine the device, a common pattern is to check `torch.cuda.is_available()`, and set the device accordingly. This is particularly useful when you want your code to be flexible, regardless of the underlying hardware.\n",
    "\n",
    ">[Task]: Define a `device` variable that is set to 'cuda:0' if CUDA is available and 'cpu' otherwise. Create a tensor on this device. The [documentation about torch.device](https://pytorch.org/docs/stable/tensor_attributes.html#torch-device) might be handy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "91e05e75-03ad-44cb-9842-89e2017ee709",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Define the device\n",
    "device = None  # Replace None with the appropriate code\n",
    "\n",
    "# Create a tensor on the specified device\n",
    "tensor_on_device = torch.tensor([1, 2, 3, 4, 5], device=device)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b80406b-b1cc-4831-a6ba-8e6385703755",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "\n",
    "To define the device variable dynamically:\n",
    "\n",
    "```python\n",
    "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
    "```\n",
    "<br>\n",
    "After setting the device, you can create tensors on it directly using the device argument.\n",
    "\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "574a2192-cc09-4d2c-8f01-97b051b7ffc8",
   "metadata": {
    "tags": []
   },
   "source": [
    "### **Automatic Differentiation with Autograd**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f5406f6-e295-4f70-a815-9eef18352390",
   "metadata": {
    "tags": []
   },
   "source": [
    "PyTorch's `autograd` module provides the tools for automatically computing the gradients for tensors. This feature is a cornerstone for neural network training, as gradients are essential for optimization algorithms like gradient descent.\n",
    "\n",
    "When we create a tensor, `requires_grad` is set to `False` by default, meaning it won't track operations. However, if we set `requires_grad=True`, PyTorch will start to track all operations on the tensor.\n",
    "\n",
    "Let's start with a simple example:\n",
    "\n",
    ">**Task:** Create a tensor that holds a single value, let's say 2, and set `requires_grad=True`. Then, define a simple operation like squaring the tensor. Finally, inspect the resulting tensor. The [documentation for requires_grad](https://pytorch.org/docs/stable/autograd.html#torch.Tensor.requires_grad) might be handy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fe63ab93-55be-434d-822f-8fd9cd727941",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# TODO: Create a tensor, perform a simple operation, and print its data and grad_fn separately.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa7ee20c-c2d6-4dcf-bb37-9eda580b5dc5",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "\n",
    "To create a tensor with requires_grad=True and square it:\n",
    "\n",
    "```python\n",
    "# TODO: Create a tensor, perform a simple operation, and print its data and grad_fn separately.\n",
    "x = torch.tensor([2.0], requires_grad=True)\n",
    "y = x ** 2\n",
    "print(\"Data:\", y.data)\n",
    "print(\"grad_fn:\", y.grad_fn)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c14dde16-a6be-4151-94cb-96ae98f0648a",
   "metadata": {},
   "source": [
    "Once the operation is executed on a tensor, a new attribute grad_fn is created. This attribute references a function that has created the tensor. In our example, since we squared the tensor, grad_fn will be of type PowBackward0.\n",
    "\n",
    "This grad_fn attribute provides a link to the computational history of the tensor, allowing PyTorch to backpropagate errors and compute gradients when training neural networks."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0965e79e-558a-45a9-8ab2-614c503e59c0",
   "metadata": {
    "tags": []
   },
   "source": [
    "#### **Computing Gradients**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "36fb6c5b-9b39-4a2f-a767-61032b1b4ffc",
   "metadata": {},
   "source": [
    "Now, let's compute the gradients of `out` with respect to `x`. To do this, we'll call the `backward()` method on the tensor `out`.\n",
    "\n",
    ">[Task]: Compute the gradients of `out` by calling the `backward()` method on it. Afterwards, print the gradients of `x`. The [documentation for backward()](https://pytorch.org/docs/stable/autograd.html#torch.autograd.backward) may be useful.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "83685760-bde9-4327-88f7-cfe02bdb3309",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# TODO: Compute the gradient and print it."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b1d104b-efef-4fff-869d-8dde1131868e",
   "metadata": {
    "tags": []
   },
   "source": [
    "<details>\n",
    "<summary>Hint (click to reveal)</summary>\n",
    "\n",
    "To compute the gradient:\n",
    "\n",
    "```python\n",
    "y.backward()\n",
    "print(x.grad)\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d7f5aecb-8623-481f-a5cf-f8b6dd0c9a37",
   "metadata": {
    "tags": []
   },
   "source": [
    "#### **Gradient Accumulation**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a4df0a1-12a0-4129-a258-915fa8440193",
   "metadata": {},
   "source": [
    "In PyTorch, the gradients of tensors are accumulated into the `.grad` attribute each time you call `.backward()`. This means that if you call `.backward()` multiple times, the gradients will add up.\n",
    "\n",
    "However, by default, calling `.backward()` consumes the computational graph to save memory. If you intend to call `.backward()` multiple times on the same graph, you need to specify `retain_graph=True` during all but the last call.\n",
    "\n",
    ">[Task]: Create a tensor, perform an operation on it, and then call `backward()` twice. Use `retain_graph=True` in the first call to retain the computational graph. Observe the `.grad` attribute after each call.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50a04095-9d7e-48ba-90ed-06718cd379f0",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Create a tensor\n",
    "w = torch.tensor([1.0], requires_grad=True)\n",
    "\n",
    "# Operation\n",
    "result = w * 2\n",