{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "Wesam Khallaf",
  "title_listed": "nn.Module Explained: The Same Model Built with Raw Tensors and with nn.Module",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "技术教程",
  "is_paid_flag": "否",
  "url": "https://dev.to/pytorchfromgroundup/nnmodule-explained-the-same-model-built-with-raw-tensors-and-with-nnmodule-1ccl",
  "publish_date": "2026-09-26",
  "corpus_stage": "",
  "notes": "",
  "id": "d771c6f99b0f",
  "collected_at": "2026-10-02T13:53:47",
  "title": "nn.Module Explained: The Same Model Built with Raw Tensors and with nn.Module - DEV Community",
  "body_markdown": "Every PyTorch model is a subclass of\nnn.Module\n. This article explains what that class does by building the same model two ways: first with raw tensors only, then with\nnn.Module\n. Comparing the two shows exactly which parts of the work the module takes over.\nThe problem\nThe model learns the line\ny = 3x + 1\nfrom 20 noisy points.\nimport\ntorch\ntorch\n.\nmanual_seed\n(\n0\n)\nx\n=\ntorch\n.\nlinspace\n(\n0\n,\n2\n,\n20\n).\nunsqueeze\n(\n1\n)\n# shape (20, 1)\ny\n=\n3\n*\nx\n+\n1\n+\n0.3\n*\ntorch\n.\nrandn_like\n(\nx\n)\n# shape (20, 1)\nEnter fullscreen mode\nExit fullscreen mode\nVersion 1: raw tensors\nA linear model has two parameters, a weight\nw\nand a bias\nb\n. Here they are created as plain tensors with\nrequires_grad=True\n, so autograd tracks them.\nw\n=\ntorch\n.\nrandn\n(\n1\n,\nrequires_grad\n=\nTrue\n)\nb\n=\ntorch\n.\nzeros\n(\n1\n,\nrequires_grad\n=\nTrue\n)\nlr\n=\n0.1\nfor\nepoch\nin\nrange\n(\n200\n):\ny_hat\n=\nw\n*\nx\n+\nb\n# 1. forward pass\nloss\n=\n((\ny_hat\n-\ny\n)\n**\n2\n).\nmean\n()\n# 2. loss (MSE)\nloss\n.\nbackward\n()\n# 3. gradients into w.grad, b.grad\nwith\ntorch\n.\nno_grad\n():\n# 4. update\nw\n-=\nlr\n*\nw\n.\ngrad\nb\n-=\nlr\n*\nb\n.\ngrad\nw\n.\ngrad\n.\nzero_\n()\n# 5. reset gradients\nb\n.\ngrad\n.\nzero_\n()\nprint\n(\nf\n\"\ny =\n{\nw\n.\nitem\n()\n:\n.\n4\nf\n}\nx +\n{\nb\n.\nitem\n()\n:\n.\n4\nf\n}\n\"\n)\nEnter fullscreen mode\nExit fullscreen mode\nOutput:\ny = 3.2885x + 0.8159\nEnter fullscreen mode\nExit fullscreen mode\nThe result is not exactly\n3x + 1\nbecause of the noise in the data. The best straight line through these 20 points, computed directly with least squares (\ntorch.linalg.lstsq\n), is\ny = 3.2899x + 0.8142\n. Gradient descent reached that line to within 0.002.\nThe loop has five steps: forward pass, loss, backward pass, update, reset. Steps 4 and 5 each need a short explanation.\nWhy the update is inside\ntorch.no_grad()\nw\nis a leaf tensor with\nrequires_grad=True\n. PyTorch does not allow a tracked leaf to be modified in place, because the computation graph depends on its value. Without\nno_grad()\n, the first update raises an error:\nw\n-=\nlr\n*\nw\n.\ngrad\nEnter fullscreen mode\nExit fullscreen mode\nRuntimeError: a leaf Variable that requires grad is being used in an in-place operation.\nEnter fullscreen mode\nExit fullscreen mode\nWriting the update as\nw = w - lr * w.grad\navoids that error but creates a different problem. The result of\nw - lr * w.grad\nis a new, non-leaf tensor, and autograd only stores\n.grad\non leaf tensors. On the next step\nw.grad\nis\nNone\n:\nTypeError: unsupported operand type(s) for *: 'float' and 'NoneType'\nEnter fullscreen mode\nExit fullscreen mode\nInside\ntorch.no_grad()\n, the in-place update is allowed and is not recorded in the graph.\nw\nstays the same leaf tensor.\nWhy the gradients are reset\nloss.backward()\nadds to\n.grad\n. It does not overwrite it. If the gradients are not reset, each step uses the sum of all previous gradients. With the same data and seed, the loss over time looks like this:\nepoch                 0       2       5      10      50     199\n----------------------------------------------------------------\nwith zero_()      31.47    3.70    0.66    0.42    0.09   0.067\nwithout zero_()   31.47    1.13   24.47   16.28   29.91   33.73\nEnter fullscreen mode\nExit fullscreen mode\nWithout the reset, the updates grow and change direction, and the loss does not converge.\nVersion 2: nn.Module\nThe same model, written as an\nnn.Module\nsubclass:\nimport\ntorch.nn\nas\nnn\nclass\nTinyModel\n(\nnn\n.\nModule\n):\ndef\n__init__\n(\nself\n):\nsuper\n().\n__init__\n()\nself\n.\nlinear\n=\nnn\n.\nLinear\n(\n1\n,\n1\n)\ndef\nforward\n(\nself\n,\nx\n):\nreturn\nself\n.\nlinear\n(\nx\n)\nmodel\n=\nTinyModel\n()\noptimizer\n=\ntorch\n.\noptim\n.\nSGD\n(\nmodel\n.\nparameters\n(),\nlr\n=\n0.1\n)\nloss_fn\n=\nnn\n.\nMSELoss\n()\nfor\nepoch\nin\nrange\n(\n200\n):\nloss\n=\nloss_fn\n(\nmodel\n(\nx\n),\ny\n)\noptimizer\n.\nzero_grad\n()\nloss\n.\nbackward\n()\noptimizer\n.\nstep\n()\nw\n=\nmodel\n.\nlinear\n.\nweight\n.\nitem\n()\nb\n=\nmodel\n.\nlinear\n.\nbias\n.\nitem\n()\nprint\n(\nf\n\"\ny =\n{\nw\n:\n.\n4\nf\n}\nx +\n{\nb\n:\n.\n4\nf\n}\n\"\n)\nEnter fullscreen mode\nExit fullscreen mode\nOutput:\ny = 3.2882x + 0.8163\nEnter fullscreen mode\nExit fullscreen mode\nThe result matches Version 1. The small difference in the last digits comes from a different random starting value.\nEach part of Version 2 replaces a part of Version 1:\nVersion 1 (raw tensors)\nVersion 2 (nn.Module)\nw\n,\nb\ncreated with\nrequires_grad=True\nnn.Linear(1, 1)\ny_hat = w * x + b\nmodel(x)\n((y_hat - y) ** 2).mean()\nnn.MSELoss()\nupdate inside\ntorch.no_grad()\noptimizer.step()\nw.grad.zero_()\n,\nb.grad.zero_()\noptimizer.zero_grad()\nnn.Linear\ndoes no extra computation. Its forward pass is\nx @ W.T + b\n:\nlin\n=\nnn\n.\nLinear\n(\n1\n,\n1\n)\nxx\n=\ntorch\n.\nrandn\n(\n5\n,\n1\n)\nprint\n(\ntorch\n.\nallclose\n(\nlin\n(\nxx\n),\nxx\n@\nlin\n.\nweight\n.\nT\n+\nlin\n.\nbias\n))\n# True\nEnter fullscreen mode\nExit fullscreen mode\nIts weight and bias are ordinary leaf tensors with\nrequires_grad=True\n, of type\nnn.Parameter\n:\nfor\nname\n,\np\nin\nmodel\n.\nnamed_parameters\n():\nprint\n(\nname\n,\ntuple\n(\np\n.\nshape\n),\np\n.\nis_leaf\n,\ntype\n(\np\n).\n__name__\n)\nEnter fullscreen mode\nExit fullscreen mode\nlinear.weight (1, 1) True Parameter\nlinear.bias (1,) True Parameter\nEnter fullscreen mode\nExit fullscreen mode\nWhat nn.Module keeps track of\nnn.Module\nkeeps a record of the parameters and sub-modules that belong to the model. This record is filled automatically when attributes are assigned in\n__init__\n.\nnn.Module\noverrides attribute assignment (\n__setattr__\n). When a line such as\nself.linear = nn.Linear(1, 1)\nruns, the module checks the type of the value:\nif it is an\nnn.Module\n, it is stored in the module's\n_modules\ndictionary\nif it is an\nnn.Parameter\n, it is stored in the\n_parameters\ndictionary\nanything else is stored as an ordinary Python attribute\nprint\n(\nnn\n.\nLinear\n(\n1\n,\n1\n).\n_parameters\n.\nkeys\n())\n# odict_keys(['weight', 'bias'])\nEnter fullscreen mode\nExit fullscreen mode\nThis is why\nsuper().__init__()\nmust be the first line of\n__init__\n. It creates these dictionaries. If it is missing, the first assignment fails:\nAttributeError: cannot assign module before Module.__init__() call\nEnter fullscreen mode\nExit fullscreen mode\nThe main methods of\nnn.Module\nall work by going through these dictionaries, recursively into every sub-module:\nMethod\nWhat it does with the registered parameters\nmodel.parameters()\nreturns them, usually to pass to the optimizer\nmodel.to(device)\nmoves each one to the device\nmodel.state_dict()\ncollects them for saving\nprint(model)\nlists the registered sub-modules\nNone of these methods read\nforward()\n. A layer is included only if it was registered in\n__init__\n.\nStoring layers in a list: nn.ModuleList\nA Python list is not an\nnn.Module\n, so a list assigned to\nself\nis stored as an ordinary attribute, and the layers inside it are not registered.\nclass\nNet\n(\nnn\n.\nModule\n):\ndef\n__init__\n(\nself\n):\nsuper\n().\n__init__\n()\nself\n.\nblocks\n=\n[\nnn\n.\nLinear\n(\n10\n,\n64\n),\nnn\n.\nLinear\n(\n64\n,\n64\n),\nnn\n.\nLinear\n(\n64\n,\n64\n)]\n# plain list\nself\n.\nhead\n=\nnn\n.\nLinear\n(\n64\n,\n3\n)\ndef\nforward\n(\nself\n,\nx\n):\nfor\nlayer\nin\nself\n.\nblocks\n:\nx\n=\ntorch\n.\nrelu\n(\nlayer\n(\nx\n))\nreturn\nself\n.\nhead\n(\nx\n)\nEnter fullscreen mode\nExit fullscreen mode\nforward()\nstill uses all four layers, so the model runs without error. But only\nhead\nis registered:\nprint\n(\nsum\n(\np\n.\nnumel\n()\nfor\np\nin\nmodel\n.\nparameters\n()))\n# 195\nprint\n(\nmodel\n)\nEnter fullscreen mode\nExit fullscreen mode\nNet\n(\n(\nhead\n):\nLinear\n(\nin_features\n=\n64\n,\nout_features\n=\n3\n,\nbias\n=\nTrue\n)\n)\nEnter fullscreen mode\nExit fullscreen mode\nThe full network has\n(10*64 + 64) + 2*(64*64 + 64) + (64*3 + 3) = 9219\nparameters. The optimizer receives 195 of them, so the three hidden layers are never updated. Their gradients are still computed during\nbackward()\n, because they are part of the computation, but the optimizer does not apply them.\nThe fix is\nnn.ModuleList\n, which is itself an\nnn.Module\nand registers every layer inside it:\nself\n.\nblocks\n=\nnn\n.\nModuleList\n([\nnn\n.\nLinear\n(\n10\n,\n64\n),\nnn\n.\nLinear\n(\n64\n,\n64\n),\nnn\n.\nLinear\n(\n64\n,\n64\n)])\nEnter fullscreen mode\nExit fullscreen mode\nTrained on the same 3-class dataset (2000 training rows, Adam,\nlr=1e-3\n), the two versions give:\nparameters trained   test acc, epoch 10     50       300\n--------------------------------------------------------------------------------\nnn.ModuleList([...])        9219 / 9219              55.5%        84.9%    84.4%\nplain list [...]             195 / 9219              36.2%        53.6%    61.3%\nEnter fullscreen mode\nExit fullscreen mode\nThe unregistered layers are also missing from\nstate_dict()\n, so they are not saved in a checkpoint:\nnn.ModuleList: blocks.0.weight, blocks.0.bias, blocks.1.weight, blocks.1.bias,\n               blocks.2.weight, blocks.2.bias, head.weight, head.bias\nplain list:    head.weight, head.bias\nEnter fullscreen mode\nExit fullscreen mode\nAnd\nmodel.to(...)\ndoes not move them. After\nmodel.to(torch.float64)\n, the head is float64 and the hidden layers remain float32, and the next forward pass fails:\nRuntimeError: mat1 and mat2 must have the same dtype, but got Double and Float\nEnter fullscreen mode\nExit fullscreen mode\nThe same rule applies to other containers and to raw tensors:\nWhat you want to store\nUse\na list of layers\nnn.ModuleList\na dictionary of layers\nnn.ModuleDict\nlayers applied one after another\nnn.Sequential\na single trainable tensor\nnn.Parameter(tensor)\nA quick check for any model is to compare\nsum(p.numel() for p in model.parameters())\nwith the count calculated from the layer sizes.\nLayers belong in\n__init__\n, not in\nforward\nA layer created inside\nforward()\nis a new layer, with new random weights, on every call:\ndef\nforward\n(\nself\n,\nx\n):\nlayer\n=\nnn\n.\nLinear\n(\n10\n,\n64\n)\n# new layer each call\nreturn\nself\n.\nhead\n(\ntorch\n.\nrelu\n(\nlayer\n(\nx\n)))\nEnter fullscreen mode\nExit fullscreen mode\nPassing the same input twice gives two different outputs:\n[[ 0.326 -0.136 -0.508]]\n[[-0.650 -0.677 -0.995]]\nEnter fullscreen mode\nExit fullscreen mode\nLayers are created once in\n__init__\nand used in\nforward()\n.\nmodel(x)\nvs\nmodel.forward(x)\nCalling\nmodel(x)\nruns\nnn.Module.__call__\n, which calls\nforward()\nand also runs any hooks registered on the module. Calling\nmodel.forward(x)\nruns only\nforward()\nand skips the hooks.\ncalls\n=\n[]\nmodel\n.\nregister_forward_hook\n(\nlambda\nmod\n,\ninp\n,\nout\n:\ncalls\n.\nappend\n(\n\"\nhook\n\"\n))\nmodel\n(\nx\n);\nprint\n(\ncalls\n)\n# ['hook']\nmodel\n.\nforward\n(\nx\n);\nprint\n(\ncalls\n)\n# ['hook']   (the hook did not run again)\nEnter fullscreen mode\nExit fullscreen mode\nHooks are used by tools that attach to a model, such as pruning (\ntorch.nn.utils.prune\n), quantization observers, feature extractors and FLOP counters. Always call\nmodel(x)\n.\nSummary\nA model is a set of parameters and a forward function. It can be written with raw tensors.\nWith raw tensors, the update must be inside\ntorch.no_grad()\nand the gradients must be reset each step.\nnn.Module\nregisters every\nnn.Module\nand\nnn.Parameter\nassigned as an attribute in\n__init__\n.\nsuper().__init__()\nmust come first.\n.parameters()\n,\n.to()\n,\nstate_dict()\nand\nprint()\nonly see registered parameters and sub-modules.\nLayers in a plain Python list are not registered. Use\nnn.ModuleList\n,\nnn.ModuleDict\nor\nnn.Sequential\n.\nCreate layers in\n__init__\n, use them in\nforward()\n, and call the model as\nmodel(x)\n.\nThis is one chapter's worth of an idea from my book,\nPyTorch From Ground Up\n,\nwhich builds everything from tensors upward so nothing stays vague. If it helped:\n8 chapters are free, no email required\n, there's a\nfree one-page tensor cheat-sheet here\n, every example runs in the\ncompanion notebooks on GitHub\n, and the full book is on\nLeanpub\nor in\npaperback and Kindle on Amazon\n.\nMore in this series\nHow Training Actually Works\n:\nPyTorch Autograd Explained: What\n.backward()\nActually Does\nBackpropagation by Hand: Two Layers, a Pen, and Then Autograd Agrees\nWhat\noptimizer.step()\nActually Does: SGD, Momentum and Adam by Hand\nWhat Your Loss Function Actually Tells the Model\nShape mechanics\n:\nReshape vs View in PyTorch\nPyTorch Broadcasting Explained\nWhat Does\nunsqueeze\nDo in PyTorch?\nWhat Does\nkeepdim\nDo in PyTorch?\nPyTorch\npermute\nvs\ntranspose\nNext in this series:\nmodel.train()\n,\nmodel.eval()\nand\ntorch.no_grad()\n.",
  "metadata": {
    "title": "nn.Module Explained: The Same Model Built with Raw Tensors and with nn.Module - DEV Community",
    "description": "Every PyTorch model is a subclass of nn.Module. This article explains what that class does by... Tagged with pytorch, deeplearning, beginners, machinelearning.",
    "keywords": "pytorch, deeplearning, beginners, machinelearning, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/pytorchfromgroundup/nnmodule-explained-the-same-model-built-with-raw-tensors-and-with-nnmodule-1ccl",
    "og:title": "nn.Module Explained: The Same Model Built with Raw Tensors and with nn.Module",
    "og:description": "Every PyTorch model is a subclass of nn.Module. This article explains what that class does by...",
    "og:site_name": "DEV Community",
    "og:image": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7bt70q9bimb8csptz8dy.png",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@wesamkhallaf",
    "twitter:title": "nn.Module Explained: The Same Model Built with Raw Tensors and with nn.Module",
    "twitter:description": "Every PyTorch model is a subclass of nn.Module. This article explains what that class does by...",
    "twitter:card": "summary_large_image",
    "twitter:widgets:new-embed-design": "on",
    "twitter:image:src": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7bt70q9bimb8csptz8dy.png"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/d771c6f99b0f/code/body-000.txt",
      "chars": 161,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-001.txt",
      "chars": 547,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-002.txt",
      "chars": 21,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-003.txt",
      "chars": 17,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-004.txt",
      "chars": 89,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-005.txt",
      "chars": 69,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-006.txt",
      "chars": 257,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-007.txt",
      "chars": 516,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-008.txt",
      "chars": 21,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-009.txt",
      "chars": 116,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-010.txt",
      "chars": 102,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-011.txt",
      "chars": 68,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-012.txt",
      "chars": 78,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-013.txt",
      "chars": 67,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-014.txt",
      "chars": 331,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-015.txt",
      "chars": 72,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-016.txt",
      "chars": 67,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-017.txt",
      "chars": 87,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-018.txt",
      "chars": 324,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-019.txt",
      "chars": 187,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-020.txt",
      "chars": 79,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-021.txt",
      "chars": 126,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-022.txt",
      "chars": 50,
      "language": "text"
    },
    {
      "file": "attachments/d771c6f99b0f/code/body-023.txt",
      "chars": 207,
      "language": "text"
    }
  ],
  "images": [],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://github.com/pytorch-from-ground-up/book_code/blob/main/PyTorch-From-Ground-Up-8-Free-Chapters.pdf",
      "label": "8 chapters are free, no email required",
      "type": "a"
    },
    {
      "href": "https://payhip.com/b/7ukxh",
      "label": "free one-page tensor cheat-sheet here",
      "type": "a"
    },
    {
      "href": "https://github.com/pytorch-from-ground-up/book_code",
      "label": "companion notebooks on GitHub",
      "type": "a"
    },
    {
      "href": "https://leanpub.com/pytorchfromgroundup",
      "label": "Leanpub",
      "type": "a"
    },
    {
      "href": "https://www.amazon.com/dp/B0H8WMCV33",
      "label": "paperback and Kindle on Amazon",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/pytorch-autograd-explained-what-backward-actually-does-4acg",
      "label": "PyTorch Autograd Explained: What .backward() Actually Does",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/backpropagation-by-hand-two-layers-a-pen-and-then-autograd-agrees-13i6",
      "label": "Backpropagation by Hand: Two Layers, a Pen, and Then Autograd Agrees",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/what-optimizerstep-actually-does-one-step-of-sgd-momentum-and-adam-by-hand-44fe",
      "label": "What optimizer.step() Actually Does: SGD, Momentum and Adam by Hand",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/what-your-loss-function-actually-tells-the-model-mse-cross-entropy-and-the-softmax-bug-that-fan",
      "label": "What Your Loss Function Actually Tells the Model",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o",
      "label": "Reshape vs View in PyTorch",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606",
      "label": "PyTorch Broadcasting Explained",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a",
      "label": "What Does unsqueeze Do in PyTorch?",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/what-does-keepdim-do-in-pytorch-and-the-silent-bug-when-you-forget-it-17p1",
      "label": "What Does keepdim Do in PyTorch?",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/pytorch-permute-vs-transpose-whats-the-difference-and-the-reshape-bug-that-scrambles-your-7ke",
      "label": "PyTorch permute vs transpose",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:48:51Z",
  "source_url": "https://dev.to/pytorchfromgroundup/nnmodule-explained-the-same-model-built-with-raw-tensors-and-with-nnmodule-1ccl",
  "papers": [
    {
      "status": "ok",
      "url": "https://raw.githubusercontent.com/pytorch-from-ground-up/book_code/main/PyTorch-From-Ground-Up-8-Free-Chapters.pdf",
      "bytes": 2552607,
      "reused": true,
      "local_path": "papers/repair/2927ef550363909d894f.pdf"
    }
  ],
  "actual_video_files": [],
  "body_chars": 12006,
  "expected_body_images": 0,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/d771c6f99b0f/pages/source.html",
  "local_html": "attachments/d771c6f99b0f/pages/article.html",
  "body_base_path": "attachments/d771c6f99b0f/pages/",
  "body_markdown_path": "attachments/d771c6f99b0f/pages/article.md"
}