{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "Wesam Khallaf",
  "title_listed": "What Does `unsqueeze` Do in PyTorch? (And Why Your Model Keeps Asking For It)",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "技术教程",
  "is_paid_flag": "否",
  "url": "https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a",
  "publish_date": "2026-07-19",
  "corpus_stage": "",
  "notes": "",
  "id": "a0524fb6d23f",
  "collected_at": "2026-10-02T13:54:02",
  "title": "What Does `unsqueeze` Do in PyTorch? (And Why Your Model Keeps Asking For It) - DEV Community",
  "body_markdown": "You passed a single image to your model and got\nValueError: expected 4D input (got 3D input)\n. Someone on Stack Overflow said \"just add\n.unsqueeze(0)\n\" — and it worked. But nobody explained\nwhat\nit did, or how you'd know to reach for it next time.\nThe short answer\nunsqueeze(dim)\ninserts a new axis of size 1 at position\ndim\n.\nThe data doesn't change at all — only the shape gains an extra\n1\n.\nA tensor of shape\n(4,)\nbecomes\n(1, 4)\nwith\nunsqueeze(0)\nor\n(4, 1)\nwith\nunsqueeze(1)\n. Its inverse,\nsqueeze(dim)\n, removes a size-1 axis. Both are free: no memory is allocated and no data is copied.\nimport\ntorch\nv\n=\ntorch\n.\ntensor\n([\n1\n,\n2\n,\n3\n,\n4\n])\n# shape (4,)\nrow\n=\nv\n.\nunsqueeze\n(\n0\n)\n# (1, 4) — one row\ncol\n=\nv\n.\nunsqueeze\n(\n1\n)\n# (4, 1) — one column\nprint\n(\nrow\n.\nshape\n,\ncol\n.\nshape\n)\n# torch.Size([1, 4]) torch.Size([4, 1])\nEnter fullscreen mode\nExit fullscreen mode\nSame four numbers, three different shapes. That's the whole idea.\nWhy does PyTorch need size-1 dimensions at all?\nBecause a shape isn't just a size — it's a\nmeaning\n. In PyTorch, shape\n(4,)\nmeans \"four numbers.\" Shape\n(1, 4)\nmeans \"one row of four.\" Shape\n(4, 1)\nmeans \"four rows of one.\" Those are three different claims about your data, and PyTorch takes them literally.\nThat's why so many frustrating shape errors happen not because your data is the wrong size, but because it's missing a dimension — or has one too many. A model expects a batch of inputs and you hand it a single sample. A loss function wants a column and you give it a flat row. The fix, nearly every time, is one call.\nThe number you pass is\nwhere\nthe new axis goes. Dimension 0 is the front, dimension 1 is next, and negative indexing works too —\nunsqueeze(-1)\nalways appends a new last axis no matter how many dimensions the tensor already has.\nt\n=\ntorch\n.\nrandn\n(\n3\n,\n4\n)\nprint\n(\nt\n.\nunsqueeze\n(\n0\n).\nshape\n)\n# torch.Size([1, 3, 4])\nprint\n(\nt\n.\nunsqueeze\n(\n1\n).\nshape\n)\n# torch.Size([3, 1, 4])\nprint\n(\nt\n.\nunsqueeze\n(\n-\n1\n).\nshape\n)\n# torch.Size([3, 4, 1])\nEnter fullscreen mode\nExit fullscreen mode\nThe\nNone\nshortcut (same thing, fewer characters)\nThere's a terser spelling you'll see constantly in real codebases: put\nNone\ninside an index expression. Wherever you place\nNone\n, a new size-1 axis appears.\nv\n=\ntorch\n.\ntensor\n([\n1\n,\n2\n,\n3\n,\n4\n])\nrow\n=\nv\n[\nNone\n,\n:]\n# same as unsqueeze(0)\ncol\n=\nv\n[:,\nNone\n]\n# same as unsqueeze(1)\nprint\n(\nrow\n.\nshape\n,\ncol\n.\nshape\n)\n# torch.Size([1, 4]) torch.Size([4, 1])\nEnter fullscreen mode\nExit fullscreen mode\nBoth spellings do exactly the same thing.\nNone\nindexing is shorter;\nunsqueeze\nis more explicit about where the axis lands. Use whichever reads more clearly — you'll meet both in the wild.\nWhat does\nsqueeze\ndo in PyTorch?\nsqueeze\nis the inverse: it removes dimensions whose size is 1. With no argument it removes\nall\nof them; with a\ndim\nargument it removes only that one — and only if it really is size 1.\nt\n=\ntorch\n.\nrandn\n(\n1\n,\n3\n,\n1\n,\n4\n)\nprint\n(\nt\n.\nshape\n)\n# torch.Size([1, 3, 1, 4])\nprint\n(\nt\n.\nsqueeze\n().\nshape\n)\n# torch.Size([3, 4])     — all 1s gone\nprint\n(\nt\n.\nsqueeze\n(\n0\n).\nshape\n)\n# torch.Size([3, 1, 4])  — only dim 0\nprint\n(\nt\n.\nsqueeze\n(\n1\n).\nshape\n)\n# torch.Size([1, 3, 1, 4]) — dim 1 is size 3, unchanged\nEnter fullscreen mode\nExit fullscreen mode\nLook at that last line. Squeezing dimension 1 did nothing, because dimension 1 has size 3, not 1.\nsqueeze\nonly removes axes that are actually size 1, so it's safe to call even when you're not sure.\nSafe, but not always\nwise\n— see the gotchas below.\nA worked example: the batch dimension\nThis is the case that sends most beginners to Google, so let's walk it end to end.\nMost PyTorch models expect a\nbatch\n— shape\n(N, ...)\n, where\nN\nis how many samples you're passing. A single CIFAR-style image is shape\n(3, 32, 32)\n: three channels, 32 by 32 pixels. That's three dimensions, and a lot of layers want four.\nHere's the part that trips people up, because it's changed over the years: a bare\nnn.Conv2d\nwill actually accept your unbatched image just fine. Modern PyTorch added no-batch-dim support to many\nnn\nmodules, so this runs:\nimport\ntorch\nimport\ntorch.nn\nas\nnn\nconv\n=\nnn\n.\nConv2d\n(\n3\n,\n16\n,\nkernel_size\n=\n3\n)\nsample\n=\ntorch\n.\nrandn\n(\n3\n,\n32\n,\n32\n)\n# one image — 3 dims\nprint\n(\nconv\n(\nsample\n).\nshape\n)\n# torch.Size([16, 30, 30]) — no error\nEnter fullscreen mode\nExit fullscreen mode\nSo why does everyone tell you to\nunsqueeze\n? Because the moment there's a normalization layer in the stack, the tolerance ends.\nBatchNorm2d\ncomputes statistics\nacross the batch\n, so it genuinely cannot work without a batch axis:\nmodel\n=\nnn\n.\nSequential\n(\nnn\n.\nConv2d\n(\n3\n,\n16\n,\nkernel_size\n=\n3\n),\nnn\n.\nBatchNorm2d\n(\n16\n),\n)\nmodel\n(\nsample\n)\nEnter fullscreen mode\nExit fullscreen mode\nValueError: expected 4D input (got 3D input)\nEnter fullscreen mode\nExit fullscreen mode\nRead it literally: it got 3 dimensions and wanted 4. Nothing is wrong with your image — it's just not in a batch.\nunsqueeze(0)\nwraps it in a batch of exactly one:\nbatch\n=\nsample\n.\nunsqueeze\n(\n0\n)\n# (1, 3, 32, 32)\noutput\n=\nmodel\n(\nbatch\n)\nprint\n(\nbatch\n.\nshape\n)\n# torch.Size([1, 3, 32, 32])\nprint\n(\noutput\n.\nshape\n)\n# torch.Size([1, 16, 30, 30])\nEnter fullscreen mode\nExit fullscreen mode\nThe lesson generalizes: whether a\nsingle\nlayer tolerates a missing batch dimension is a detail that varies by layer and by version. Whether your\nmodel\ndoes is not worth gambling on. Add the batch axis and the question disappears.\nAnd on the way out, you often want that batch axis gone again, or a trailing singleton removed — a regression head that outputs\n(32, 1)\nwon't line up with targets shaped\n(32,)\n:\npreds\n=\ntorch\n.\nrandn\n(\n32\n,\n1\n)\n# model output\nprint\n(\npreds\n.\nsqueeze\n(\n-\n1\n).\nshape\n)\n# torch.Size([32])\nEnter fullscreen mode\nExit fullscreen mode\nunsqueeze\ngoing in,\nsqueeze\ncoming out. That pairing shows up in almost every inference script you'll ever write.\nThe other big use: lining up shapes for broadcasting\nThe second place\nunsqueeze\nearns its keep is broadcasting. To subtract a per-channel mean from an image, the mean has to be shaped so it aligns with the channel axis — not the width axis.\nmean\n=\ntorch\n.\ntensor\n([\n0.485\n,\n0.456\n,\n0.406\n])\n# (3,)\nimg\n=\ntorch\n.\nrandn\n(\n3\n,\n224\n,\n224\n)\n# (C, H, W)\nimg\n-\nmean\nEnter fullscreen mode\nExit fullscreen mode\nRuntimeError: The size of tensor a (224) must match the size of tensor b (3)\nat non-singleton dimension 2\nEnter fullscreen mode\nExit fullscreen mode\nBroadcasting aligns shapes from the\nright\n, so\n(3,)\ntries to line up with the 224-wide last axis and fails. Give it two trailing size-1 axes and it lines up correctly:\nmean\n=\nmean\n[:,\nNone\n,\nNone\n]\n# (3, 1, 1)\nimg\n=\nimg\n-\nmean\n# broadcasts to (3, 224, 224)\nprint\n(\nmean\n.\nshape\n,\nimg\n.\nshape\n)\n# torch.Size([3, 1, 1]) torch.Size([3, 224, 224])\nEnter fullscreen mode\nExit fullscreen mode\nThe\n1\ns are placeholders that say \"stretch me along this axis.\" That's what makes the subtraction apply per channel instead of per pixel column. If the right-to-left alignment rule is new to you, I wrote up\nhow PyTorch broadcasting works in full here\n.\nAre\nsqueeze\nand\nunsqueeze\nexpensive?\nNo — they're free. Both return a\nview\nof the same data with adjusted strides. No memory is allocated, nothing is copied, so you can call them freely, even inside a hot loop.\nv\n=\ntorch\n.\narange\n(\n4\n)\nu\n=\nv\n.\nunsqueeze\n(\n0\n)\nprint\n(\nu\n.\ndata_ptr\n()\n==\nv\n.\ndata_ptr\n())\n# True\nprint\n(\nu\n.\nstride\n())\n# (4, 1)\nEnter fullscreen mode\nExit fullscreen mode\nIdentical data pointer means no copy happened.\nunsqueeze\njust added a stride entry. This is\nthe same principle behind\nview\nand\nreshape\n: any shape change expressible as a stride change is a view; anything else needs a copy.\nCommon mistakes and gotchas\nBare\nsqueeze()\neating your batch dimension.\nIf your batch size happens to be 1,\nsqueeze()\nwith no argument silently deletes it along with every other singleton — and your shapes break several lines later, far from the cause. Prefer\nsqueeze(dim)\n, always.\nConfusing\nunsqueeze(1)\nwith\nunsqueeze(-1)\n.\nThey're the same only for 1-D tensors. On a\n(3, 4)\ntensor,\nunsqueeze(1)\ngives\n(3, 1, 4)\nwhile\nunsqueeze(-1)\ngives\n(3, 4, 1)\n.\nExpecting\nsqueeze(dim)\nto raise on a non-1 axis.\nIt doesn't — it quietly returns the tensor unchanged. Convenient, but it means a typo'd\ndim\nfails silently.\nReaching for\nreshape\nwhere\nunsqueeze\nis clearer.\nx.reshape(1, *x.shape)\nworks, but\nx.unsqueeze(0)\nsays what you mean.\nForgetting the batch axis at inference time.\nTraining works because your\nDataLoader\nadds the batch dimension for you. Then you feed a single sample by hand and it breaks. That's not a new bug — it's the\nDataLoader\nno longer doing you a favor.\nTrusting that \"it ran\" means the shape was right.\nSince some\nnn\nmodules accept unbatched input, a missing batch dimension can survive several layers before something downstream complains — or worse, quietly produce output of the wrong rank. Print\n.shape\n.\nRecap\nunsqueeze(dim)\ninserts a size-1 axis at\ndim\n;\nsqueeze(dim)\nremoves one.\nv[None, :]\nis the same thing written inline. Both are free views — no copy. Use\nunsqueeze(0)\nto add a batch dimension for a single sample,\nunsqueeze(-1)\nor\n[:, None, None]\nto align shapes for broadcasting, and\nsqueeze(-1)\nto drop a trailing singleton from model outputs.\nThis is one idea from my book\nPyTorch From Ground Up\n, which builds everything from tensors upward so nothing stays vague. If it helped, you can grab the\nfree PyTorch tensor cheat-sheet here\n, run every example from the book in the\ncompanion notebooks on GitHub\n, or find the\nfull paperback on Amazon here\n.",
  "metadata": {
    "title": "What Does `unsqueeze` Do in PyTorch? (And Why Your Model Keeps Asking For It) - DEV Community",
    "description": "You passed a single image to your model and got ValueError: expected 4D input (got 3D input). Someone... Tagged with pytorch, python, beginners, deeplearning.",
    "keywords": "pytorch, python, beginners, deeplearning, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a",
    "og:title": "What Does `unsqueeze` Do in PyTorch? (And Why Your Model Keeps Asking For It)",
    "og:description": "You passed a single image to your model and got ValueError: expected 4D input (got 3D input). Someone...",
    "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%2Fqyn0f5md45s6uinzwehn.png",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@wesamkhallaf",
    "twitter:title": "What Does `unsqueeze` Do in PyTorch? (And Why Your Model Keeps Asking For It)",
    "twitter:description": "You passed a single image to your model and got ValueError: expected 4D input (got 3D input). Someone...",
    "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%2Fqyn0f5md45s6uinzwehn.png"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/a0524fb6d23f/code/body-000.txt",
      "chars": 237,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-001.txt",
      "chars": 188,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-002.txt",
      "chars": 189,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-003.txt",
      "chars": 309,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-004.txt",
      "chars": 209,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-005.txt",
      "chars": 101,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-006.txt",
      "chars": 45,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-007.txt",
      "chars": 210,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-008.txt",
      "chars": 108,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-009.txt",
      "chars": 121,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-010.txt",
      "chars": 106,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-011.txt",
      "chars": 196,
      "language": "text"
    },
    {
      "file": "attachments/a0524fb6d23f/code/body-012.txt",
      "chars": 132,
      "language": "text"
    }
  ],
  "images": [],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606",
      "label": "how PyTorch broadcasting works in full here",
      "type": "a"
    },
    {
      "href": "https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o",
      "label": "the same principle behind view and reshape",
      "type": "a"
    },
    {
      "href": "https://payhip.com/b/7ukxh",
      "label": "free PyTorch 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://www.amazon.com/dp/B0H8WMCV33",
      "label": "full paperback on Amazon here",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:48:53Z",
  "source_url": "https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 9563,
  "expected_body_images": 0,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/a0524fb6d23f/pages/source.html",
  "local_html": "attachments/a0524fb6d23f/pages/article.html",
  "body_base_path": "attachments/a0524fb6d23f/pages/",
  "body_markdown_path": "attachments/a0524fb6d23f/pages/article.md"
}