← Blog
A cream ceramic collar around a row of green ridges on an ivory rail, with a coral accent against a lavender background.
Published on
2026-11-10

PyTorch Conv1d: channels, sequence length, and output shape

PyTorch Conv1d applies a learned filter along a sequence. For a batch, arrange the input as (N, C, L): examples, channels, then sequence length. If your data arrives as time steps followed by features, swap those last two axes before passing it to the layer.

This guide follows five small examples with fixed weights so you can check the arithmetic. We ran 26 CPU checks with Python 3.12.14 and PyTorch 2.15.0+cpu, covering shapes, values, invalid settings, and a backward pass. Each Python block runs independently with PyTorch installed; no dataset or downloaded model is required.

Start with one channel and one filter

The Conv1d API reference documents the input convention and settings. Here, one example has one channel and five values. We choose the filter [1, 0, -1], disable its bias, and leave stride at 1 with no padding.

import torch

x = torch.tensor([[[1., 2., 3., 4., 5.]]])
layer = torch.nn.Conv1d(1, 1, kernel_size=3, bias=False)
with torch.no_grad():
    layer.weight.copy_(torch.tensor([[[1., 0., -1.]]]))
y = layer(x)
print('Input:', tuple(x.shape))
print('Output:', tuple(y.shape))
print('Values:', y.detach().tolist())

Observed output:

Input: (1, 1, 5)
Output: (1, 1, 3)
Values: [[[-2.0, -2.0, -2.0]]]

The first calculation is 1 × 1 + 2 × 0 + 3 × (-1) = -2. Moving one position gives 2 × 1 + 3 × 0 + 4 × (-1) = -2, followed by another -2. Three complete windows fit into the five-value signal.

The weights are used in their displayed order. This is cross-correlation, which PyTorch calls convolution in its layer API. Our fixed weights make the result predictable; a training loop would normally learn those weights. The result above demonstrates the operation, not a useful trained detector.

Put channels before sequence length

Suppose each time step contains three sensor measurements. Two five-step examples might arrive with shape (2, 5, 3). The three measurements belong on the channel axis for this example. They are not three separate time steps.

Use transpose to exchange the sequence and feature axes. The following input has deliberately distinct values so we can see whether each feature keeps its identity:

import torch

# Two sequences, five time steps, three features per step.
raw = torch.arange(30, dtype=torch.float32).reshape(2, 5, 3)
x = raw.transpose(1, 2)
layer = torch.nn.Conv1d(3, 4, kernel_size=3, bias=False)
with torch.no_grad():
    layer.weight.fill_(1.)
y = layer(x)
print('Input:', tuple(x.shape))
print('First feature:', x[0, 0].tolist())
print('Output:', tuple(y.shape))
print('First output channel:', y[0, 0].detach().tolist())

Observed output:

Input: (2, 3, 5)
First feature: [0.0, 3.0, 6.0, 9.0, 12.0]
Output: (2, 4, 3)
First output channel: [36.0, 63.0, 90.0]

The first feature is still [0, 3, 6, 9, 12]. The first output value adds the first three positions of all three channels: (0 + 3 + 6) + (1 + 4 + 7) + (2 + 5 + 8) = 36. All four output channels happen to match because we deliberately gave their filters identical weights.

Our checks confirmed that raw.reshape(2, 3, 5) produces different values from this transpose. A reshape that yields the desired dimensions can still mix up their meaning. Passing the original (2, 5, 3) tensor directly also raised a channel mismatch: this layer expects three input channels, but that arrangement puts five on the channel axis.

Inspect one batch before building the rest of the model. Our PyTorch DataLoader guide covers batch construction. For a batch of single-channel signals shaped (N, L), insert a channel dimension with unsqueeze(1); do not assume every two-dimensional tensor represents a batch. Our unbatched check also confirmed that Conv1d accepts (C, L) for one example.

Check output length before adding another layer

For integer padding applied equally to both ends, calculate:

L_out = floor((L_in + 2p - d(k - 1) - 1) / s) + 1

Here, k is kernel size, s is stride, p is padding per side, and d is dilation. We checked this calculation against six parameter combinations. For length 8, kernel size 3, stride 2, padding 0, and dilation 1, it gives floor(5 / 2) + 1 = 3.

This next example uses a filter of three ones. Its outputs show which values each window collects:

import torch

x = torch.arange(1, 9, dtype=torch.float32).reshape(1, 1, 8)
cases = [
    ('valid', dict(kernel_size=3)),
    ('stride 2', dict(kernel_size=3, stride=2)),
    ('pad 1', dict(kernel_size=3, padding=1)),
    ('dilation 2', dict(kernel_size=3, dilation=2)),
    ('same', dict(kernel_size=3, padding='same')),
]
for label, options in cases:
    layer = torch.nn.Conv1d(1, 1, bias=False, **options)
    with torch.no_grad():
        layer.weight.fill_(1.)
    y = layer(x)
    print(label, tuple(y.shape), y.detach().flatten().tolist())

Observed output:

valid (1, 1, 6) [6.0, 9.0, 12.0, 15.0, 18.0, 21.0]
stride 2 (1, 1, 3) [6.0, 12.0, 18.0]
pad 1 (1, 1, 8) [3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 15.0]
dilation 2 (1, 1, 4) [9.0, 12.0, 15.0, 18.0]
same (1, 1, 8) [3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 15.0]

With stride 2, the windows start at input positions 0, 2, and 4. With dilation 2, the first window reads values 1, 3, and 5, producing 9. The three weights now span five positions, but the filter still contains three learned coefficients per connected input channel.

One zero on each end keeps the length at eight in this example. The first sum is 3 because its window is [0, 1, 2]; the final sum is 15 from [7, 8, 0]. The same case matches these values. Our separate test confirmed that constructing a layer with padding='same' and stride 2 raises an error in the tested version.

Also check the effective kernel width, d(k - 1) + 1. Our test with an unpadded length-eight input and a size-nine kernel failed. Increasing the number of output channels does not resolve a window that cannot fit.

See what groups changes

In the next example, channel two is ten times channel one. With groups=1, each of our two output filters adds windows from both input channels. With groups=2, each output reads just one channel.

import torch

x = torch.tensor([[[1., 2., 3., 4.], [10., 20., 30., 40.]]])
for groups in (1, 2):
    layer = torch.nn.Conv1d(2, 2, kernel_size=2, groups=groups, bias=False)
    with torch.no_grad():
        layer.weight.fill_(1.)
    print('Groups:', groups, 'Weight:', tuple(layer.weight.shape))
    print('Values:', layer(x).detach().tolist())

Observed output:

Groups: 1 Weight: (2, 2, 2)
Values: [[[33.0, 55.0, 77.0], [33.0, 55.0, 77.0]]]
Groups: 2 Weight: (2, 1, 2)
Values: [[[3.0, 5.0, 7.0], [30.0, 50.0, 70.0]]]

The first combined value is 1 + 2 + 10 + 20 = 33. Separating the groups gives 3 and 30 instead. Notice the weight shapes: the grouped layer has four coefficients rather than eight, while both outputs still have shape (1, 2, 3).

The API requires both channel counts to be divisible by the group count. We tested invalid input and output counts separately, and both were rejected. We also checked a depthwise multiplier of two: Conv1d(2, 4, 2, groups=2) produced two independent filters per input channel. It did not mix the two channels. Fewer coefficients do not establish a speed advantage on your hardware; this guide contains no timing benchmark.

Use left padding when future values must stay hidden

Keeping output length unchanged does not establish causality. In a forecasting model, a feature for position t may need to exclude all later input positions. The symmetric three-value window in our same example includes the next sample.

For the stride-one layer below, we add dilation × (kernel_size - 1) zeros on the left and none on the right. The layer itself uses no padding. F.pad accepts the left/right pair for the last axis.

import torch

import torch.nn.functional as F

x = torch.tensor([[[1., 2., 3., 4., 5.]]])
layer = torch.nn.Conv1d(1, 1, kernel_size=3, dilation=2, bias=False)
with torch.no_grad():
    layer.weight.fill_(1.)
left = layer.dilation[0] * (layer.kernel_size[0] - 1)
y = layer(F.pad(x, (left, 0)))
changed = x.clone()
changed[..., 3:] = 100.
y_changed = layer(F.pad(changed, (left, 0)))
print('Values:', y.detach().flatten().tolist())
print('Earlier outputs unchanged:', torch.equal(y[..., :3], y_changed[..., :3]))

Observed output:

Values: [1.0, 2.0, 4.0, 6.0, 9.0]
Earlier outputs unchanged: True

With dilation 2, the value at position 4 is 1 + 3 + 5 = 9. Changing positions 3 and 4 left positions 0 through 2 of the output unchanged. A separate test with symmetric same padding changed the earlier output at position 2 when we changed position 3.

This check establishes the behavior of this one layer. A full forecasting system can still leak information through normalization, feature construction, target alignment, or its train/test split. Inspect those choices separately. If you predict the next value, define the target offset explicitly rather than assuming the convolution has shifted the labels.

Debug shapes before tuning the model

When a layer fails, inspect the tensor's meaning alongside its shape:

  • Channel mismatch: identify which axis holds features and transpose it deliberately. Do not change in_channels just to accept an accidentally swapped input.
  • A signal that is too short: calculate the effective kernel width and available padded length.
  • An unexpected output length: recompute it after every stride, dilation, and padding change.
  • Unexpected channel mixing: check the group count and the actual filter values.

We also checked one backward pass with a length-five signal and a three-value filter of ones. Summing the three outputs gave input gradients [1, 2, 3, 2, 1]: the middle input participates in three windows. That verifies gradient flow for this example; it says nothing about convergence on a real task.

Once those mechanics are clear, choose the surrounding model for the problem. Our neural network architecture guide discusses that broader decision. These examples stay with one-dimensional sequences and CPU arithmetic. They do not test CUDA, model accuracy, or throughput. The local runtime emitted an initialization warning because NumPy was absent; all tensor-only checks completed without it.

Your next workload belongs on Hivenet.

Pick one AI, compute, or storage workload and see the difference for yourself. Spin it up in minutes, or let our team map your fastest path to production.