Lab notebook · deepsc-s reproduction

What five notebooks taught me about semantic communication

Notes from reproducing DeepSC-S (Weng, Qin & Li, 2021) — a neural network that replaces a speech codec and a channel coder with one model, trained end-to-end to survive a noisy radio channel. Five stages, each one mechanism proven in isolation before trusting the next.

Paper: arXiv:2012.05369 Repo: prashantrajbista/semantic-communication Dataset: Edinburgh DataShare 28-speaker (Valentini 2016)
Signal path: waveform through semantic encoder, channel encoder, a noisy or fading channel, channel decoder, semantic decoder, back to waveform. waveform x semantic encoder channel encoder channel awgn / fading y = h·x + w semantic decoder¹

¹ channel decoder omitted for width — full path is encoder → channel-encoder → channel → channel-decoder → decoder.

Why joint source-channel coding

The pitch this whole project tests, before any code runs.

A conventional transmitter splits the job in two: source coding compresses speech to as few bits as possible (a codec — Opus, AMR), then channel coding adds redundancy so those exact bits survive a noisy link (Turbo codes, LDPC). The stages are independent and each optimizes a proxy — compression ratio, bit error rate — rather than what a listener actually hears.

DeepSC-S collapses both stages into a single neural network, trained end-to-end against reconstructed-speech quality directly. It never has to be bit-exact — only close enough that the decoded waveform sounds right. That's the "semantic" claim: the network learns to protect the parts of the signal that matter for meaning, not the parts that happen to be bits.

The five notebooks below prove that claim incrementally: framing and reconstruction first, with no noise at all; then a noise channel; then a fading channel; then real speech. Every stage runs and verifies on cheap synthetic data even before the real one does — so a broken later stage is never explained away by "maybe it's the data."

01 01_audio_framing.ipynb

Turning a waveform into an image, losslessly

No FFT, no windowing — just a reshape. The trick is making that reshape mean something to a CNN.

Waveform vs. spectrogram

A waveform (amplitude vs. time) shows loudness, timing, silence — but hides frequency content by design. A spectrogram (time × frequency × energy) shows pitch, formants, harmonics. Most audio models train on a spectrogram for exactly that reason. This project doesn't use either directly — it does something narrower.

Framing here is a reshape, not an STFT

Classic "framing" chops audio into short overlapping windows for an FFT. This codebase's frame() does something much simpler: take a fixed W = 16384-sample clip and reshape it into a 128×128 grid, row-major, no windowing, no overlap, no frequency axis.

stepshapenote
raw clip(16384,)1-D amplitude samples
frame(x)(128, 128)row 0 = samples[0:128], row 1 = samples[128:256], …
deframe(frame(x))SDR = ∞bit-exact, verified at atol=0

Row-major keeps each row temporally contiguous, so a 2-D CNN sliding a 5×5 filter over the grid is scanning real local structure — the same trick 2-D image models use, borrowed for audio. Column-major reshape would scatter same-row samples 128 apart in time, destroying that locality — it would look like structured noise to a convolution. W = 16384 was chosen specifically because it's a perfect square (128²) — a convenience, not a requirement.

Naming trap "Channel" means three different things in this codebase: the noisy transmission medium itself (AWGN/fading), the ChannelEncoder/ChannelDecoder layers that compress the tensor's feature axis, and the tensor channel axis itself (32 stacked feature maps, same sense as RGB channels). Worth pinning down early — every later stage uses at least two of the three.
02 02_autoencoder_no_channel.ipynb

Proving the encoder/decoder alone, channel off

If reconstruction fails with the easiest possible channel — none — nothing downstream can be trusted.

DeepSC_S is 115,177 parameters, spatial size 128×128 held constant throughout by "same" padding — only the channel-depth axis moves.

stageshaperole
input(B, 1, 128, 128)framed waveform
semantic encoder stem(B, 32, 128, 128)1→16→32, D=32 feature depth
4× SE-ResNet block(B, 32, 128, 128)refines features, shape unchanged
channel encoder(B, 8, 128, 128)32→16→8, compression bottleneck
[channel](B, 8, 128, 128)identity here — noise arrives in stage 03
channel decoder → semantic decoder(B, 1, 128, 128)mirrors the encoder path, separate weights

The SE-ResNet block

Each block computes x + s · t: a grouped convolution (cardinality 4, ResNeXt-style) produces t; a squeeze-excite branch — global-average-pool → 32→8→32 bottleneck → sigmoid — produces a per-channel gate s ∈ [0,1]³², recomputed fresh on every forward pass from the actual input (that's the "attention": data-dependent, not a static learned weight). The gated result is added back to the input as a residual.

Two checks that had to pass first Overfitting 4 clips for 1,200 epochs drove loss from 0.160 → 0.00017 — confirms the architecture can memorize before trusting it to generalize. Then, on a proper split, reconstruction cleared the SDR > 20 dB "near-lossless" bar with a ~24.5 dB mean.

Do the SE gates track anything real?

Zeroed the top-6 highest-gated channels vs. the bottom-6 lowest-gated channels and compared reconstruction damage: zeroing the important-looking channels hurt more (11.3 dB) than zeroing the ones the gate had already suppressed (13.0 dB). The gate isn't decorative — it's pointing at channels the network actually relies on.

03 03_awgn_power_norm.ipynb

Adding noise — and why power has to be pinned first

SNR is only a meaningful number if signal power is fixed. Otherwise "10 dB" means something different every run.

Power normalization

Before the channel, every batch of transmitted symbols is rescaled so mean|xk|² = 1.0 — dividing by sqrt(measured_power), not the power itself, since power scales as amplitude². Two reasons this matters: real transmitters have a fixed energy budget, and SNR (signal power / noise power) is only comparable run-to-run if the numerator isn't drifting with random init and training step.

SNR sets the noise, not the signal

Signal power stays fixed at 1.0. What changes with SNR is noise std: σ = √(10-SNR/10 / 2).

SNRσ (noise std)
0 dB0.7071
6 dB0.3544
12 dB0.1776
18 dB0.0890

Training across a random SNR, every step

for epoch in range(1500):
    channel.snr_db = random.uniform(0, 20)   # fresh SNR every step
    x_hat = model(x, channel=channel)
    loss = MSELoss(x_hat, x)
    loss.backward()   # gradients flow through the noise op itself
    opt.step()

Sampling a new SNR every step — instead of fixing one — is data augmentation for noise level: it forces one encoding that's robust across the whole range, not tuned to a single point. torch.randn is differentiable, so gradients flow encoder ← channel ← decoder; the network learns to be robust to noise, not just to compress.

Listening at a fixed 0 dB High-frequency detail — consonants, sibilants, anything low-energy and transient — washes out first. Low-frequency voiced/vowel content survives longest, simply because it dominates total signal power and MSE loss protects power, not fine detail.
04 04_fading_cross_channel.ipynb

Fading: when the channel multiplies, not just adds

And the headline question — does a model trained for one channel survive a different one it never saw?

AWGN only adds noise: y = x + w. Real wireless channels also fade — multipath propagation makes the channel gain itself a random complex number: y = h·x + w.

AWGN — no fading
Rician — LoS + scatter
Rayleigh — scatter only
  • Rayleigh — no line-of-sight path, h ~ CN(0,1), pure scattering. Models dense-urban or indoor non-line-of-sight: deep fades are common.
  • Rician — a line-of-sight path plus scatter, weighted by K-factor (LoS power / scattered power). K→∞ recovers AWGN; K=0 recovers Rayleigh.
  • Perfect-CSI equalization — the receiver is assumed to know h exactly and divides it out (x̂ = y/h) before decoding, isolating fading's noise-amplification effect from the harder, out-of-scope problem of estimating h itself.

Train once, test on every channel

Two models, same architecture, each trained on one channel only (1,500 epochs, toy data, random SNR 0–20 dB) — then every model evaluated against all three channel types, no retraining.

trained ontested onSDR @ 0 dBSDR @ 20 dB
awgnawgn13.0 dB19.6 dB
awgnrayleigh4.5 dB18.0 dB
awgnrician5.8 dB18.4 dB
rayleighawgn14.9 dB19.6 dB
rayleighrayleigh8.0 dB17.5 dB
rayleighrician9.4 dB17.6 dB

Two things worth remembering here. First, the gap between matched and mismatched channels is largest at low SNR and nearly closes by 20 dB — noise dominates less, so the fading mismatch matters less. Second, and less obvious: the model trained on Rayleigh generalizes to AWGN better than the reverse. Training on the harder channel produces a more robust encoding than training on the easy one and hoping it transfers.

05 05_results_sdr_pesq.ipynb

The headline result, on real speech

Same robustness question as stage 04 — now on the Edinburgh DataShare set, with a perceptual metric added.

Trained on a 2,000-clip subset of the 28-speaker clean set (of ~10k available — a deliberate, documented scope choice for a tractable single session, not a shortcut hiding a limitation), 40 epochs, AWGN only, then evaluated across all three channel types.

SDR in decibels versus SNR in decibels, three lines for AWGN, Rician and Rayleigh channels, all rising from left to right and converging at high SNR. 0 3 6 9 12 SDR (dB) 0 4 8 12 16 20 SNR (dB)
Stage 05, real Edinburgh DataShare data. One AWGN-trained model, evaluated on all three channels.
awgn — 7.5 → 10.5 dB rician — 3.2 → 10.0 dB rayleigh — 2.7 → 9.9 dB

Same shape as the toy-data result in stage 04, but now with real absolute numbers: a ~4–5 dB gap between matched and mismatched channels at 0 dB SNR, narrowing to under 1 dB by 20 dB SNR.

PESQ — a metric SDR can't give you

SDR is a pure signal-energy ratio; it can't tell you what actually sounds bad to a human ear. PESQ (Perceptual Evaluation of Speech Quality, scale 1.0 worst – 4.5 best) is the industry-standard perceptual metric for speech codecs, wired in as an optional dependency that degrades gracefully to an SDR-only plot if the pesq package isn't installed. Here it tracked the same channel ordering as SDR (awgn best, rayleigh/rician trailing) but stayed low in absolute terms, ~1.0–1.35 — the expected signature of a model trained on a 2k-clip subset for 40 epochs rather than the paper's full-scale run, not a bug.

Two small engineering notes

  • Checkpoints are saved every N_EPOCHS/4 epochs during training to build a "quality over time" gallery — the same held-out clip decoded with progressively later weights.
  • A later revision made training skip itself entirely if checkpoints/deepsc_s_awgn_final.pt already exists — no reason to burn a real-data training run again just because the notebook restarted.

What actually generalizes from this

Not the numbers — the shape of the argument.

Prove mechanisms, not pipelines

Each stage isolates one new variable — reshape, then noise, then fading, then real data — so a failure is never ambiguous about its cause.

Architecture follows data structure

The row-major reshape exists specifically so a 2-D CNN's local receptive field means something; the SE gate is validated, not assumed, by an ablation.

Physical-layer intuition is half the system

Power normalization, the SNR/noise-std relationship, and the additive-vs-multiplicative distinction between AWGN and fading matter as much as the network design.

Robustness is measurable, and asymmetric

Training on the harder channel (Rayleigh) generalizes to the easier one (AWGN) better than the reverse — a concrete, testable claim, confirmed on both toy and real data.

Toy data first is a discipline, not a shortcut

Every stage runs and verifies on cheap synthetic data before real data is required — a broken stage 5 is never explained away by "maybe it's the download."

Two metrics tell different stories

SDR and PESQ agreed on channel ordering but not on how good "good" actually sounds — a reminder that a signal-energy metric and a perceptual one aren't interchangeable.