<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Andrew's Notes]]></title><description><![CDATA[Engineering, machine learning, and maybe a bit of biology.]]></description><link>https://andrew.gibiansky.com/</link><image><url>https://andrew.gibiansky.com/favicon.png</url><title>Andrew&apos;s Notes</title><link>https://andrew.gibiansky.com/</link></image><generator>Ghost 3.12</generator><lastBuildDate>Fri, 04 Jul 2025 17:07:53 GMT</lastBuildDate><atom:link href="https://andrew.gibiansky.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Efficient WaveRNN: Optimizing Nonlinearities]]></title><description><![CDATA[Implementing efficient nonlinearities for WaveRNN CPU inference is tricky but critical.]]></description><link>https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/</link><guid isPermaLink="false">6121716d78c79a0007f277bd</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Sun, 09 Jun 2024 20:44:18 GMT</pubDate><content:encoded><![CDATA[<p>In this series of posts, we're going to go through the WaveRNN neural vocoder for audio waveform synthesis, along with a variety of implementation details and commonly used extensions. For a real implementation, check out the <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> repository.</p><p><strong>Posts in the Series:</strong></p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-part-1/">Intro</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul><h1 id="accelerating-nonlinearities">Accelerating Nonlinearities</h1><p>In previous posts, we talked about a wide variety of optimizations, including rewriting the compute kernel in C++, batching GRU input matrix multiplication, using block-sparse matrix-vector multiplication, SIMD intrinsics, and quantization. Implementing all of these can create a faster-than-realtime synthesis kernel for WaveRNN, but there's still room to squeeze more out of our processors. Benchmarking our kernel, we observed that 10-20% of the time is spent in nonlinearities, including the tanh and sigmoid in the GRU and the softmax in the output layer.</p><h3 id="approximating-tanh-and-sigmoid">Approximating Tanh and Sigmoid</h3><p>The core autoregressive component of WaveRNN is a Gated Recurrent Unit (GRU) RNN. A <a href="https://pytorch.org/docs/stable/generated/torch.nn.GRU.html">GRU</a> uses a state update function which requires two sigmoids and one tanh evaluation per state dimension. To compute these, you can use C++ standard library functions (very slow) or the Intel MKL library on x86 (faster), in either of these cases, these nonlinearities will end up a significant percentage of your inference time.</p><p>To speed these up, we can implement our own tanh and sigmoid which are slightly less accurate than standard library or MKL variants, but are significantly faster. First of all, we can write sigmoid as a rescaled tanh:</p><p>$$\sigma(x) = \frac{1}{2} \tanh\left(\frac{x}{2}\right) + \frac{1}{2}.$$</p><p>We will then use a Padé approximation of tanh. A Padé approximation of order [p/q] for a function $f$ is a rational function $F(x)$</p><p>$$F(x) = \frac{a_0  + a_1 x + a_2 x^2 + \cdots + a_p x^p}{1  + b_1 x + b_2 x^2 + \cdots + b_q x^q}.$$</p><p>Higher values of $p$ and $q$ allow for more precise approximations at the expense of additional computation.</p><p>The coefficients of a Padé approximation are defined to be the coefficients which have the first $p + q$ derivatives of tanh to match at zero.</p><p>$$\begin{align*}F(0) &amp;= \tanh(0) \\ F'(0) &amp;= \tanh'(0) \\ F''(0) &amp;= \tanh''(0) \\ \vdots \\ F^{(p+q)}(0) &amp;= \tanh^{(p+q)}(0)  \end{align*}$$</p><p>This system of equations can be solved to yield unique coefficients for this approximation. That said, I'm lazy and will instead take advantage of some resources to make this easy:</p><ul><li><a href="https://en.wikipedia.org/wiki/Pad%C3%A9_approximant">Wikipedia page on Padé approximants</a></li><li><a href="https://mathr.co.uk/blog/2017-09-06_approximating_hyperbolic_tangent.html">Some Haskell code for solving this system of equations</a></li><li><a href="https://pdfs.semanticscholar.org/bb2a/f84f8a179ac5486cf197c409c01289ff9064.pdf">A paper comparing quality of tanh approximations</a></li><li><a href="https://github.com/hfp/libxsmm/blob/55c6a9f92a6ff0b7124ff351aa3f7c20ec789170/include/libxsmm_intrinsics_x86.h#L653">Some LibXSMM code with the [7/8] coefficients</a></li></ul><p>Given these resources, we can implement an efficient tanh approximation with AVX, AVX-512, or NEON. This approximation can be fused with the sparse GRU GEMV if necessary and results in a 3-4X speedup for these nonlinearities over Intel MKL (which itself is a huge speedup over C++ standard library functions).</p><h3 id="faster-sampling-with-gumbel-softmax">Faster Sampling with Gumbel-Softmax</h3><p>In addition to the tanh and sigmoid in the GRU layer, our WaveRNN performs a softmax after its output layer and prior to sampling. Traditionally, this process has the following steps:</p><ol><li>Given the final layer outputs $x_i$, compute the maximum value $x_{\text{max}}$.</li><li>Subtract the maximum value from each $x_i$.</li><li>Compute $e^{x_i - x_{\text{max}}}$ for every $x_i$ in the logits.</li><li>Compute the normalization term, $\left(\Sigma e^{x_i - x_{\text{max}}}\right)^{-1}$.</li><li>Multiply the exponentiated values by the normalization to get a probability distribution $p_i$.</li><li>Sample a random value $v \sim U(0, 1)$ from the uniform distribution [0, 1]. (In order to accelerate sampling in a tight inner loop, pre-compute several thousand random numbers and cycle through them.)</li><li>Find the index $i$ where the cumulative sum of $p_i$ is greater than $v$.</li></ol><p>The index $i$ is a sample from your discrete probability distribution. </p><p>This sampling procedure has a few downsides. It requires requires scanning through your logits at least three times: once to find the maximum, once to exponentiate and compute normalization terms, and once to sample the final value. It also requires computing $e^x$, which is an expensive nonlinearity to compute accurately. </p><p>We can address both of these downsides using Gumbel-Softmax. <a href="https://arxiv.org/abs/1611.01144">Gumbel-Softmax was originally introduced</a> in order to approximate sampling during training of a neural network, so a discrete sampling step could be introduced in an intermediate layer of a network. The key point for our purposes, however, is the following:</p><ul><li>Sample $v_i \sim U(0, 1)$ from the uniform distribution [0, 1].</li><li>Compute $g_i = -\ln(-\ln v_i)$. $g_i$ is a sample from the Gumbel distribution.</li><li>Compute modified logits ${\hat x}_i = x_i + g_i$. (You must sample a distinct $g_i$ for each element of the logits.)</li><li>The index $i = \text{argmax}\; \hat x$ is a sample from the discrete distribution $\text{softmax}(x)$.</li></ul><p>You can find the derivation for this neat property in the <a href="https://arxiv.org/pdf/1611.01144.pdf">Gumbel-Softmax paper</a> or the <a href="https://arxiv.org/abs/1611.00712">concrete distribution paper</a> (which refers to the same distribution). </p><p>Using this property, we can do our sampling in a single pass. Given a vector of logits $x_i$ and a vector of pre-computed samples $g_i$ from the Gumbel distribution, we can compute $x_i + g_i$. As we compute this sum, keep track of the maximum value so far and the index of the maximum value. When you reach the end of $x$, the resulting index is a sample from your distribution.</p><p>Although sampling was a small fraction (5%) of our inference costs, applying this small optimization sped it up by a factor of five, making the cost of sampling negligible.</p><h2 id="summary">Summary</h2><p>Once the matrix-vector multiplies in WaveRNN are sufficiently optimized, the nonlinearities (tanh, sigmoid, and softmax) start being a significant fraction of the WaveRNN inference time. <a href="https://en.wikipedia.org/wiki/Pad%C3%A9_approximant">Padé approximants</a>, we can approximate tanh and sigmoid by an easy-to-compute rational function which can be computed in only a few arithmetic instructions. We can speed up sampling from a softmax by using the Gumbel-Softmax trick, drawing samples from a Gumbel distribution and taking an argmax of the sampled value plus the logit in order to sample from the original softmax distribution. These two optimizations, while small, can speed up inference by 10-15%.</p><p>Check out the implementation at <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> or any of the other blog posts in this series:</p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-part-1/">Intro</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Efficient WaveRNN: Block Sparsity]]></title><description><![CDATA[WaveRNN inference can be accelerated by using block-sparse weight matrices combined with specialized block-sparse matrix-vector multiplication kernels.]]></description><link>https://andrew.gibiansky.com/wavernn-demystified-sparsity/</link><guid isPermaLink="false">6112d11278c79a0007f2721f</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Sun, 09 Jun 2024 20:43:44 GMT</pubDate><media:content url="https://andrew.gibiansky.com/content/images/2021/08/Matrix-Packing-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://andrew.gibiansky.com/content/images/2021/08/Matrix-Packing-1.png" alt="Efficient WaveRNN: Block Sparsity"><p>In this series of posts, we're going to go through the WaveRNN neural vocoder for audio waveform synthesis, along with a variety of implementation details and commonly used extensions. For a real implementation, check out the <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> repository.</p><p><strong>Posts in the Series:</strong></p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-part-1/">Intro</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul><h1 id="accelerating-inference-with-block-sparsity-matrices">Accelerating Inference with Block Sparsity Matrices</h1><p>As we reviewed in the previous blog post on WaveRNN inference, a single step of WaveRNN consists of sample embeddings, a GRU layer, two linear layers, and a sampling step.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/WaveRNN-Single-Inference-Step.png" class="kg-image" alt="Efficient WaveRNN: Block Sparsity"><figcaption>Diagram of a single step of inference for WaveRNN. A sample is embedded with a sample embedding matrix and added to the conditioner output to create the GRU input. The result is run through a GRU, a linear layer, a ReLU, another linear layer, and a softmax activation. The distribution is sampled to produce the next sample in the waveform.</figcaption></figure><p>The bulk of the required compute (arithmetic) in this process can be grouped into four parts:</p><ol><li>The GRU state-to-activation matrix multiply. For example, for a GRU with a state dimension of 512, this is a 512 x 1536 matrix-vector multiplication.</li><li>The hidden layer matrix multiply. For a hidden layer with 512 units and GRU with 512 units, this is a 512 x 512 matrix-vector multiplication.</li><li>The output layer matrix multiply. For a hidden layer with 512 units and 256 output buckets, this is a 512 x 256 matrix-vector multiplication.</li><li>The nonlinearities and element-wise operations, including tanh and sigmoid (for the GRU), ReLU (for the hidden layer), and softmax (for the output layer). </li></ol><p>(As we saw in the previous post on WaveRNN inference, the GRU input-to-activation matrix can be accelerated by precomputing it and thus takes negligible compute.) Of these four steps, the first three (the matrix-vector multiplications) can be sped by replacing the matrices involved with sparse matrices.</p><h3 id="sparse-matrix-multiplication">Sparse Matrix Multiplication</h3><p>A sparse matrix is one where a significant proportion of its entries are zero. For example, a matrix with 90% sparsity has 90% of its entries filled with zeros. Since multiplying by zero produces zero, these entries contribute nothing to the overall final result. This means that if we know which entries are zero and don't bother computing with them, we can reduce the amount of compute by 10X and in theory realize a 10X speedup.</p><p>Of course, it's not that easy. In practice, efficient sparse matrix multiplication algorithms are very challenging to write and require high degrees of sparsity (&gt;80%) to obtain <em>any</em> speedup, and the speedup they do obtain can be meager (for example, a 2X speedup for a matrix with 95% sparsity). </p><p>Sparse matrix multiplication algorithms are often slow due to the overhead of tracking which matrix entries are zeros and due to poor memory access patterns. For example, a simple sparse multiply can be implemented by storing a list of coordinates of non-zero entries and iterating over them to perform the multiplications and accumulations on those elements. However, if you load the coordinates from memory, then load the value at that coordinate from memory, then do the multiplication and accumulation, you end up doing two memory reads (one for the coordinates and one for the value) per multiplication – twice as many memory reads as a dense matrix multiply. Additionally, since you are accessing non-contiguous parts of your matrix, you will have a very unpredictable and cache-unfriendly memory access pattern. When benchmarked against dense matrix multiplies with optimized tiling for cache-friendly memory access implemented with vector instructions (AVX, SSE, NEON), a naive sparse matrix multiply will end up much slower up until ridiculous levels of sparsity and very large matrices.</p><h3 id="block-sparsity">Block Sparsity</h3><p>Luckily, in deep learning (unlike in some other fields), we rarely care too much about the specific locations of the non-zero entries. We can train our neural networks to use any sparsity pattern we desire. Thus, we can use <em>block sparsity</em> to make our sparse matrix multiplication kernels much easier to write and much more efficient.</p><p>A block sparse matrix is a sparse matrix where entire blocks (a rectangular grid of values in the matrix) of the matrix are either zero or non-zero. With block sparsity, we only need to store the indices of non-zero blocks, which can be a significant reduction in the amount of indexing we need to perform (relative to unstructured sparsity).</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/Matrix-Sparsity.png" class="kg-image" alt="Efficient WaveRNN: Block Sparsity"><figcaption>Diagram of four 8x8 matrices: a dense matrix, a matrix with unstructured sparsity, a block sparse matrix with 1x4 blocks, and a block sparse matrix with 2x2 blocks.</figcaption></figure><p>Additionally, we can choose the block size so that we can use our processor's vector registers to do our arithmetic. Pretty much all modern processors support some sort of vector arithmetic. Vector instructions allow us to execute a single instruction to load, store, or perform arithmetic on multiple values at the same time, while still taking only one clock cycle (approximately). For instance, if our matrix block size is equal to our vector register length (e.g. 8 floats with AVX), we can implement simple kernels which load and multiply blocks as just a few instructions per block.</p><h3 id="matrix-packing">Matrix Packing</h3><p>While block sparsity easily addresses the amount of indexing loading and arithmetic we need to do in our compute kernel, our memory access patterns can still be quite unfriendly to the cache. Since the non-zero blocks may be far away from each other in memory when the matrix is laid out densely, the memory accesses will not be on the same cache line. Additionally, the CPU prefetcher will not be able to predict access patterns and fetch the needed cache line in advance.</p><p>To improve our memory access patterns, we can repack our matrices in memory for easier access. For the purposes of WaveRNN inference, our matrix is fixed and we reuse it thousands of times, so the cost of repacking the matrix is negligible.</p><p>We can repack our matrix into a representation consisting of three arrays:</p><ol><li>A float array consisting of the matrix data. Only non-zero blocks are kept; all zeros are removed.</li><li>An integer array indicating the input indices corresponding to each matrix block.</li><li>An integer array indicating how many blocks correspond to each output block.</li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/Matrix-Packing.png" class="kg-image" alt="Efficient WaveRNN: Block Sparsity"><figcaption>Packing a block-sparse matrix turns it into a linearly accessed data array, input index array, and blocks per row array.</figcaption></figure><p>Our block sparse matrix-vector multiplication kernels can then read through these arrays start to finish in a linear pattern. Since we store only non-zero entries, our packed matrix might fit entirely in cache, and the prefetcher together with our linear access patterns can ensure that all our data has been loaded into the cache by the time we need it.</p><p>A simple algorithm for multiplying with these packed matrices (assuming 1x4 blocks) is to loop over the rows of the output vector. For each row, look up the number of blocks you need to multiply. For each block you need to multiply, read it from the data buffer, find the index its being multiplied by, read from that index, and perform your multiplication and accumulation. With this algorithm, the data buffer, the input indices, and the blocks per row are accessed in a completely predictable linear fashion, leading to good performance without much modification.</p><h3 id="inducing-sparsity-during-training">Inducing Sparsity During Training</h3><p>So far, we have discussed how to accelerate WaveRNN inference through switching out our dense matrix-vector multiplications for sparse (or block sparse) matrix-vector multiplications. To do so, we need to ensure that the weight matrices in the WaveRNN are primarily composed of zeros in a block sparse manner.</p><p>We can force our model to learn sparse weight matrices using magnitude-based weight pruning. During training, we identify the least important weights (as evidenced by their absolute value or magnitude) and then forcibly set them to zero. As training progresses, we snap progressively more and more weights to zero. Since the model starts out completely random, we allocate a bit of time (a warmup period) for the model to learn prior to beginning sparsification. The specific sparsification schedule used with WaveRNN is usually a cubic function which starts out rapidly pruning weights but, as the number of non-zero weights falls, slows down towards the end until it reaches its full expected level of sparsity.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/sparsity.png" class="kg-image" alt="Efficient WaveRNN: Block Sparsity"><figcaption>Plot of enforced sparsity levels throughout training, starting with a warmup period with no pruned weights and ramping up to a very sparse model after a million iterations.</figcaption></figure><p>To get a block-sparse matrix, instead of pruning individual weights based on their magnitude, we prune blocks, where the magnitude of a block is defined as the maximum magnitude of the weights in the block. We can implement sparsity by computing a block mask after every training iteration based on the block magnitudes and setting the weights to zero for the lowest magnitude weights.</p><p>Deep neural networks tend to be vastly overparameterized, and so the models learned this way with a very high degree of sparsity (90-95% sparse) are only slightly worse than dense models. However, training sparse models requires a long time – they take a very long time to converge.</p><h2 id="summary">Summary</h2><p>A large fraction of WaveRNN inference time consists of matrix-vector multiplications. We can train deep neural networks which use sparse matrices – matrices which have a large fraction with zero entries. Since zero entries don't contribute to the final output, we can write highly efficient inference kernels for sparse matrix-vector multiplications speed up WaveRNN inference significantly. Sparse matrix-vector multiplications with unstructured sparsity (where non-zero entries are located anywhere) require very high levels of sparsity, but we can require block sparsity (where non-zero entries are contiguous in blocks) which allow for much more efficient memory access patterns and higher speedups. Block sparsity integrates well with vector instructions on modern processors (such as AVX and NEON instructions) which allow processing multiple values with a single instruction.</p><p>Check out the implementation at <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> or proceed to the subsequent blog posts:</p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Efficient WaveRNN: Optimizing Arithmetic]]></title><description><![CDATA[WaveRNN inference can be made dramatically faster with SIMD inference kernels and int16 quantization.]]></description><link>https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/</link><guid isPermaLink="false">6121202f78c79a0007f275a5</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Sun, 09 Jun 2024 20:43:09 GMT</pubDate><content:encoded><![CDATA[<p>In this series of posts, we're going to go through the WaveRNN neural vocoder for audio waveform synthesis, along with a variety of implementation details and commonly used extensions. For a real implementation, check out the <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> repository.</p><p><strong>Posts in the Series:</strong></p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-part-1/">Intro</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul><h1 id="advanced-kernel-optimizations">Advanced Kernel Optimizations</h1><p>Using WaveRNN in production relies on a host of optimizations which can accelerate the model by several orders of magnitude. As we discussed in previous posts, we can start our optimizations by rewriting the inference kernel in C++, batching the GRU input matrix multiply, and by block-sparsifying and repacking the weight matrices for use with a custom matrix-vector multiply (GEMV) kernel. Although these drastically increase the performance of our model, there's still a lot we can do to squeeze speed out of our processors.</p><h3 id="simd-intrinsics">SIMD Intrinsics </h3><p>Generally speaking, a computer processor reads and executes a stream of instructions, where each instruction operates on one or two values in memory or in processor registers. However, in order to accelerate repeating the same operation across thousands or millions of values, most modern processors support some form of Single-Instruction Multiple-Data (SIMD) instructions. These instructions operate on vectors of a few contiguous values  (and hence are often called vector instructions). Different processors use different SIMD instructions: for our purposes, we care about x86 AVX2 instructions (pre-2017 Intel), AVX-512 instructions (post-2017 Intel), and NEON instructions (ARM). NEON instructions operate on 128 bits of data, AVX2 on 256 bits of data, and AVX-512 on (you guessed it!) 512 bits of data.</p><p>In an ideal world, we would never have to think about what instructions our C++ compiler is generating to perform our arithmetic, and indeed, GCC and Clang try hard to auto-vectorize code and use SIMD instructions as much as they can. But look around you – the world is not ideal, not by a long stretch. For performance-sensitive parts of code, you can get significant speedups by directly writing SIMD instructions instead of relying on a compiler to guess what you mean. In fact, if you check out the kernel code in <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a>, you'll find direct SIMD implementations of almost every performance-sensitive part.</p><p>SIMD instructions are used in C / C++ through SIMD intrinsics, special functions which the compiler recognizes and converts to SIMD instructions. To give you a taste, let's go through how we would hand-vectorize a simple function which adds two float32 vectors:</p><!--kg-card-begin: html--><pre><code class="language-cpp">// Computes out[i] = a[i] + b[i]
void elementwise_add(int size, float* out, float* a, float* b) {
    for(int i = 0; i < size; i++) {
        out[i] = a[i] + b[i];
    }
}
</code></pre><!--kg-card-end: html--><p>For a function this simple, you should not expect a huge performance increase for rewriting it with SIMD intrinsics; the compiler should do a good job auto-vectorizing this with -Ofast (though you may need to tell it these are not aliasing pointers with __restrict__). So treat this as an opportunity to look at some SIMD code, not as a real-world example.</p><p>When using AVX, we use __m256 and __m512 data types to represent vectors of 256 or 512 bits storing float data. Instructions for working with these are prefixed _mm256_ or _mm512_, respectively, and suffixed for the type of data they are working with (_ps for "packed single", _pd for "packed double", _ss for "scalar single", etc). For example, the AVX2 unaligned load intrinsic is _mm256_loadu_ps. (An unaligned load is a load from memory that may not be on a 32-byte boundary. Older processors execute aligned loads much faster than unaligned loads, though this penalty is lower on recent CPUs.)</p><p>Putting this all together, here is an equivalent function using AVX2 intrinsics:</p><!--kg-card-begin: html--><pre><code class="language-cpp">#include &lt;immintrin.h&gt;

// Computes out[i] = a[i] + b[i]
void elementwise_add(int size, float* out, float* a, float* b) {
    int i = 0;
    for(; i + 7 < size; i += 8) {
        // Load 8 floats from a.
        __m256 x = _mm256_loadu_ps(a + i);
        
        // Load 8 floats from b.
        __m256 y = _mm256_loadu_ps(b + i);
        
        // Sum up the floats.
        __m256 sum = _mm256_add_ps(x, y);
        
        // Write out the 8 floats to out.
        _mm256_storeu_ps(out + i, sum);
    }    
    
    // In case size is not divisible by 8.
    for(; i < size; i++) {
        out[i] = in1[i] + in2[i];
    }
}
</code></pre><!--kg-card-end: html--><p>AVX-512 will look very similar, using __m512 and _mm512 instead of __m256 and _mm256, respectively. (AVX-512 adds lots of other functionality besides longer vector registers, but it's not very relevant for this simple function.)</p><!--kg-card-begin: html--><pre><code class="language-cpp">#include &lt;immintrin.h&gt;	

// Computes out[i] = a[i] + b[i]
void elementwise_add(int size, float* out, float* a, float* b) {
    int i = 0;
    for(; i + 15 < size; i += 16) {
        // Load 16 floats from a.
        __m512 x = _mm512_loadu_ps(a + i);
        
        // Load 16 floats from b.
        __m512 y = _mm512_loadu_ps(b + i);
        
        // Sum up the floats.
        __m512 sum = _mm512_add_ps(x, y);
        
        // Write out the 16 floats to out.
        _mm512_storeu_ps(out + i, sum);
    }    
    
    // In case size is not divisible by 16.
    for(; i < size; i++) {
        out[i] = in1[i] + in2[i];
    }
}
</code></pre><!--kg-card-end: html--><p>NEON intrinsics for ARM use 128-bit vector registers. The types are of the form {data}x{count}_t; for example, float32x4_t is a 128-bit register with 4 float32 values in it. Intrinsics start with "v" (for "vector") and end with a suffix indicating the data type, such as "_f32" for 32-bit floats. Instructions which operate on 128-bit registers have named that end in "q". For example, loading from memory is done with the vld1q_f32 intrinsic, storing to memory uses the vst1q_f32 intrinsic, and vaddq_f32 adds float32x4_t values. Putting it together, you get the following elementwise sum function:</p><!--kg-card-begin: html--><pre><code class="language-cpp">#include &lt;arm_neon.h&gt;

// Computes out[i] = a[i] + b[i]
void elementwise_add(int size, float* out, float* a, float* b) {
    int i = 0;
    for(; i + 3 < size; i += 4) {
        // Load 4 floats from a.
        float32x4_t x = vld1q_f32(a + i);
        
        // Load 4 floats from b.
        float32x4_t y = vld1q_f32(b + i);
        
        // Sum up the floats.
        float32x4_t sum = vaddq_f32(x, y);
        
        // Write out the 4 floats to out.
        vst1q_f32(out + i, sum);
    }    
    
    // In case size is not divisible by 4.
    for(; i < size; i++) {
        out[i] = in1[i] + in2[i];
    }
}
</code></pre><!--kg-card-end: html--><h3 id="quantized-inference">Quantized Inference</h3><p>SIMD registers generally fit a fixed number of bits (128, 256, or 512), but depending on our data type, these can hold different amounts of values. For example, a 256-bit register can hold 8 32-bit floats, 16 16-bit floats or ints, ant 32 8-bit ints. Instructions for multiplication and addition generally take a single cycle (that is, you can complete one such instruction per cycle) no matter what data they are operating on, which means that reducing the bit precision of our operands is a great way to accelerate our inference kernels.</p><p>Unfortunately, unlike GPUs, CPUs thus far tend to have poor support for float16. This means that in order to squeeze more speed out of our kernels, we're going to have to shift to quantized arithmetic and do our matrix-vector multiplies in int8 or int16.</p><p>Quantizing WaveRNN to 8 bits results in significant quality degradation unless it is trained in a quantization-aware way, but if we stick to 16-bit inference, we can accelerate inference while keeping audio quality high.</p><p>In order to do an int16 matrix-vector multiply, we can:</p><ol><li>Compute the maximum magnitude of each row, $\beta_r$.</li><li>Rescale each row to the range [-8192, 8192] by multiplying by $\frac{8192}{\beta_r}$.</li><li>Round each row to the nearest integer in int16.</li><li>Compute the maximum magnitude $\alpha$ of the input vector.</li><li>Rescale the input vector to the range [-8192, 8192] by multiplying by $\frac{8192}{\alpha}.$</li><li>Round the input vector elements to the nearest integer in int16.</li><li>For every row, compute the dot product with the input, doing multiplication in int16 and accumulation in int32.</li><li>Scale result of the dot product to undo the scaling done on the inputs, multiplying the results by $\frac{\alpha \beta_r}{8192^2}.$</li></ol><p>Storing a per-row maximum weight magnitude is convenient if the matrix-vector multiply is done row-wise; another alternative with slightly reduced precision is to store a single scaling factor for the entire matrix. </p><p>Since the weights are fixed, we can perform steps (1), (2), and (3) in advance. This allows us to reduce the amount of data we load from RAM by 2x. In theory, we could get up to a 2X speedup, but in practice, getting a 1.5X speedup from quantization is more doable.</p><h2 id="summary">Summary</h2><p>WaveRNN inference can be fast, but making it fast requires a variety of low-level optimizations to the inference kernels. One crucial optimization is using SIMD instructions such as SSE, AVX, AVX-512, NEON, and SVE for arithmetic when the processor the kernel is running on supports it. Although compilers have auto-vectorizers to take advantage of these instructions, manually writing your arithmetic routines using compiler intrinsics or assembly can still provide a speed boost. A second optimization is int16 quantization – since twice as many int16 values fit in vector registers as float32 values, rewriting matrix-vector multiplies to operate primarily on int16 data can yield a speed boost. Together, these optimizations can speed up a WaveRNN kernel significantly, allowing you to synthesize audio faster than realtime.</p><p>Check out the implementation at <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> or proceed to the subsequent blog posts:</p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Efficient WaveRNN: Autoregressive Inference]]></title><description><![CDATA[If implemented in Python and Pytorch, WaveRNN inference is too slow, but we can make it faster with several simple optimizations.]]></description><link>https://andrew.gibiansky.com/wavernn-demystified-inference/</link><guid isPermaLink="false">6111cb0678c79a0007f270c5</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Sun, 09 Jun 2024 20:42:28 GMT</pubDate><content:encoded><![CDATA[<p>In this series of posts, we're going to go through the WaveRNN neural vocoder for audio waveform synthesis, along with a variety of implementation details and commonly used extensions. For a real implementation, check out the <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> repository.</p><p><strong>Posts in the Series:</strong></p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-part-1/">Intro</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul><h2 id="wavernn-autoregressive-inference">WaveRNN Autoregressive Inference</h2><p>As we discussed in the previous blog post, WaveRNN is an autoregressive neural vocoder which synthesizes audio sample-by-sample, as shown in the following diagram. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/Baseline-WaveRNN--inference---1-.png" class="kg-image"><figcaption>Neural network diagram for WaveRNN inference.</figcaption></figure><p>This means that after we run the conditioning network on the input spectrograms, we need to, for each sample:</p><ul><li>Compute a sample embedding vector for the previously synthesized sample.</li><li>Add the sample embedding to the conditioning vector from the conditioning network to get the GRU input vector.</li><li>Run the GRU RNN on the GRU input. This consists of multiplying the input vector and the GRU state vector by the GRU weight matrices and then applying the GRU nonlinearities to compute a new GRU state (also its output).</li><li>Run the linear layer and the ReLU nonlinearity on the GRU output to get the hidden layer activations.</li><li>Run the final linear layer on the hidden layer activations and the softmax nonlinearity to get a discrete probability distribution over the next samples.</li><li>Randomly sample from the probability distribution to get the next sample.</li></ul><p>Once the waveform is generated, dequantize the discretized samples and apply µ-law expanding to get the final waveform.</p><h3 id="streaming-inference">Streaming Inference</h3><p>WaveRNN takes a lot of compute to run – for every synthesized sample, you need to run one timestep of the neural network. As a result, it can be quite slow to synthesize with. For interactive applications of TTS (such as voice assistants), you may want to start playing audio to the user before the entire synthesis is finished, which means you want to be able to stream through WaveRNN synthesis to minimize latency between receiving a TTS request and responding with initial synthesized audio.</p><p>To stream through the conditioning network, you can chunk up the input spectrograms into overlapping chunks and run those chunks separately through the network. (The chunks must be overlapping to avoid discontinuities at the boundaries; don't use zero padding when streaming through convolutions!) Alternatively, you can use a <a href="https://andrew.gibiansky.com/streaming-audio-synthesis/">more clever approach</a> to streaming through audio synthesis to avoid repeating computation in the conditioning network.</p><p>Streaming through the autoregressive network requires keeping track of two pieces of state: the previously synthesized sample (initialized to 128 representing zero) and the current GRU state (initialized to a vector of zeros). At each timestep, you run the autoregressive network to update the GRU state and generate a new sample.</p><p>Starting compute kernels generally has some overhead, so it is best to stream in chunks. A single invocation of the WaveRNN inference kernel should synthesize at least a few hundred samples (a few milliseconds of audio), and an outer loop should repeatedly call the inference kernel to synthesize the whole audio clip while sending intermediate results to the user.</p><h3 id="optimizations">Optimizations</h3><p>Productionizing an implementation of WaveRNN requires a heavy focus on optimizing inference speed to achieve faster-than-realtime synthesis. </p><p><strong>C++ Implementation: </strong>The first and largest optimization you can make is simply removing Python from the equation and implementing your inner inference loop in C++. For example in recent testing, using Python-based inference logic took about 100 seconds to synthesize 10 seconds of audio, but the same logic implemented in C++ (using the same matrix multiplication kernels, etc) took only 30 seconds (a roughly 3X speed-up). Implementing the kernel in C++ also opens the door to further optimizations, such as minimizing memory allocations, fine-grained multithreading, and more. </p><p><strong>GRU Input Matrix Multiply Batching: </strong>Another optimization opportunity arises in the way GRUs are implemented. If you look at the <a href="https://pytorch.org/docs/stable/generated/torch.nn.GRU.html">PyTorch GRU implementation</a>, we have two matrix multiplies: one that applies to the state and one that applies to the inputs. </p><p>$$\begin{align*}r_t &amp;= \sigma(W_{ir} x_t + b_{ir} + W_{hr} h_{(t-1)} + b_{hr}) \\            z_t &amp;= \sigma(W_{iz} x_t + b_{iz} + W_{hz} h_{(t-1)} + b_{hz}) \\            n_t &amp;= \tanh(W_{in} x_t + b_{in} + r_t * (W_{hn} h_{(t-1)}+ b_{hn})) \\            h_t &amp;= (1 - z_t) * n_t + z_t * h_{(t-1)}\end{align*}$$</p><p>For WaveRNN, the input is the sum of the conditioner network output $c_t$ (which changes once per frame) and the sample embedding $s_t$:</p><p>$$x_t = c_t + s_t.$$</p><p>We can compute a new sample embedding matrix which incorporates $W_{ir}$, $W_{iz}$, $W_{in}$, and their respective biases:</p><p>$$S = [W_{ir}; W_{iz}; W_{in}] s + [b_{ir}; b_{iz}; b_{in}].$$</p><p>We can also compute the product of the conditioner network $c_t$ with all these matrices. Since the conditioner network output is available in advance and only changes once per frame, we can batch these computations and do them once per frame outside the critical loop:</p><p>$$C = [W_{ir}; W_{iz}; W_{in}] c.$$</p><p>Then the GRU equations end up with one fewer matrix multiply:</p><p>$$\begin{align*}r_t &amp;= \sigma(C_{rt} + S_{rt} + W_{hr} h_{(t-1)} + b_{hr}) \\            z_t &amp;= \sigma(C_{zt} + S_{zt} + W_{hz} h_{(t-1)} + b_{hz}) \\            n_t &amp;= \tanh(C_{nt} + S_{nt} + r_t * (W_{hn} h_{(t-1)}+ b_{hn})) \\            h_t &amp;= (1 - z_t) * n_t + z_t * h_{(t-1)}\end{align*}$$</p><p>Implementing these optimizations leads to a 15-25% speedup, since you remove a significant fraction of the GRU layer compute from the inner loop.</p><p><strong>Further Optimization: </strong>These optimizations are just the start and are insufficient for high quality synthesis at faster-than-realtime speeds. Further optimizations include block sparsity, int8 quantization, approximate nonlinearities, vector intrinsics (AVX-512, NEON, CUDA WMMA), multithreading, and so on.</p><h2 id="summary">Summary</h2><p>The WaveRNN inference process consists of two distinct pieces: the conditioner network and the autoregressive inference. The conditioner network is simple and fast and can be implemented using standard PyTorch and Python tools. The autoregressive network runs once per audio sample and requires very heavy optimizations, starting with an implementation in C++. One straight forward optimization is batching the GRU input matrix multiply to reduce the amount of compute required for the GRU layer at each timestep. Many more inference optimizations are required to get a high quality faster-than-realtime neural vocoder.</p><p>Check out the implementation at <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> or proceed to the subsequent blog posts:</p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul><p></p>]]></content:encoded></item><item><title><![CDATA[Efficient WaveRNN: Reducing Quantization Noise]]></title><description><![CDATA[Discretizing audio samples to 8 bits each during WaveRNN inferences introduces significant quantization noise, which we can reduce using µ-law companding and pre-emphasis.]]></description><link>https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/</link><guid isPermaLink="false">611af1fa78c79a0007f273c1</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Sun, 09 Jun 2024 20:41:04 GMT</pubDate><content:encoded><![CDATA[<p>In this series of posts, we're going to go through the WaveRNN neural vocoder for audio waveform synthesis, along with a variety of implementation details and commonly used extensions. For a real implementation, check out the <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> repository.</p><p><strong>Posts in the Series:</strong></p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-part-1/">Intro</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul><h2 id="quantization-noise">Quantization Noise</h2><p>As we discussed in our introductory post, our WaveRNN represents audio using an 8-bit quantization, with each audio sample being stored as an integer between 0 and 255. Since audio is a continuous-valued signal (usually stored with 16 or 24 bits per sample), quantizing it into 256 different values causes audible degradation. The quantization noise is effectively uniform noise added to the signal, distributed evenly across the different frequencies as you can see below. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/quantization.gif" class="kg-image"><figcaption>Log Mel spectrogram of an audio clip as well as the spectrograms of the same clip quantized to different bit depths, from 5-bit quantization (32 distinct values) to 10 bit quantization (1024 distinct values).</figcaption></figure><p>In addition to being easily visible in a spectrogram, you can hear the quantization noise in the audio samples below. (All audio samples in this blog post are based on the first sample in the <a href="https://keithito.com/LJ-Speech-Dataset/">LJ Speech dataset</a> by Keith Ito.) With 5-bit quantization, you'll be able to easily hear the noise with laptop speakers, but as you get closer to 10-bit quantization, you'll need high volume or headphones to hear it clearly.</p><!--kg-card-begin: html--><strong>Original</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/original.wav" type="audio/wav">
</audio>
<br><br>


<strong>5-bit Quantization</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-5-bit.wav" type="audio/wav">
</audio>
<br><br>


<strong>6-bit Quantization</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-6-bit.wav" type="audio/wav">
</audio>
<br><br>

<strong>7-bit Quantization</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-7-bit.wav" type="audio/wav">
</audio>
<br><br>


<strong>8-bit Quantization</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-8-bit.wav" type="audio/wav">
</audio>
<br><br>

<strong>9-bit Quantization</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-9-bit.wav" type="audio/wav">
</audio>
<br><br>


<strong>10-bit Quantization</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-10-bit.wav" type="audio/wav">
</audio>
<br><br>
<!--kg-card-end: html--><h2 id="-law-companding">µ-law Companding</h2><p>The simple quantization we demonstrated in the spectrograms and audio clips above was done by splitting the audio range (-1, 1) uniformly into segments, with each segment covering an equal part of the audio range. </p><p>Part of the reason that the quantization noise is so audible, though, is that human hearing is sensitive to audio on a wide range of volume scales. (This is why the unit used to measure audio volume, decibels, is a logarithmic scale!) Thus, one approach to reducing quantization noise is to instead apply a non-uniform quantization, where the audio range near zero is quantized with much more detail than the range far from zero.</p><p>This non-uniform quantization is done by applying a transformation called µ-law companding prior to quantization. For a signal $x$, the companded signal $\hat x$ is</p><p>$$\hat x = \text{sgn}(x) \frac{\ln(1+ \mu |x|)}{\ln(1+\mu)}~~~~-1 \leq x \leq 1$$</p><p>When listening to the audio, the inverse operation (µ-law expanding) is done after dequantizing. The resulting audio, as you can see below for a variety of different quantization levels, sounds better – although you can still hear the effects of quantization noise as a subtle background buzz, even with 8-bit quantization.</p><!--kg-card-begin: html--><strong>5-bit Quantization, No Companding</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-5-bit.wav" type="audio/wav">
</audio>
<br><br>


<strong>5-bit Quantization, Companded with µ=255</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-5-bit-mu-law-255.wav" type="audio/wav">
</audio>
<br><br>


<strong>6-bit Quantization, Companded with µ=255</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-6-bit-mu-law-255.wav" type="audio/wav">
</audio>
<br><br>

<strong>7-bit Quantization, Companded with µ=255</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-7-bit-mu-law-255.wav" type="audio/wav">
</audio>
<br><br>


<strong>8-bit Quantization, Companded with µ=255</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-8-bit-mu-law-255.wav" type="audio/wav">
</audio>
<br><br><!--kg-card-end: html--><h2 id="pre-emphasis">Pre-Emphasis</h2><p>To reduce the quantization noise even further, we'll apply another operation, pre-emphasis, prior to µ-law companding. (Correspondingly, the inverse operation, de-emphasis, is applied after µ-law expanding in order to listen to the audio.)</p><p>Quantization noise is effectively uniform noise, and thus has an equal power across the entire frequency spectrum. However, human perception of pitch is not linear, but rather logarithmic. For example, a pitch difference of 100 Hz is much more perceptible you are comparing 100 Hz to 200 Hz than if you are comparing 10 kHz to 10.1 kHz. The <a href="https://en.wikipedia.org/wiki/Mel_scale">Mel scale</a> is a logarithmic scale meant to emulate human hearing; pitches equally distant on the Mel scale are perceived by people to be equally distant. Because human perception of pitch is logarithmic, and quantization noise is equal across frequencies, human perception of quantization noise is <em>primarily</em> driven by high frequency quantization noise.</p><p>Thus, to reduce perceptible quantization noise, we can <em>boost</em> high frequencies using a pre-emphasis filter prior to quantization and <em>attenuate</em> those same frequencies using a de-emphasis filter after dequantization. This will leave the speech content (primarily in the lower frequencies) unaffected and will reduce the power of the added quantization noise in the higher frequencies (since the quantization noise is added right before high frequencies are attenuated).</p><p>To apply pre-emphasis, replace a signal $x[t]$ with $y[t]$ as defined by</p><p>$$y[t] = x[t] - \alpha x[t - 1].$$</p><p>$\alpha$ is a value between 0 and 1, frequently around 0.9 or 0.95 for audio applications.</p><p>For the sake of intuition, you can consider what this filter would do to a fixed or low frequency signal and what it would do to a quickly varying signal. If the signal is constant or slowly varying ($x[t] \approx x[t-1]$), then this reduces to approximately $(1 - \alpha)x[t]$, effectively attenuating it by $1 - \alpha$. If the signal is quickly varying, then $y[t]$ will have a high magnitude (little attenuation). (If you are of a more analytical bent, you can compute the frequency response of this filter by taking the Fourier transform of $y[t]$ and evaluating the resulting magnitude as a function of frequency – you will find that attenuation is minimal at the Nyquist frequency of half your sampling rate.)</p><p>You can observe the effect of pre-emphasis visually in the log mel spectrograms below. As you increase $\alpha$ and the strength of the pre-emphasis, the lower frequencies are attenuated and the higher ones become dominant.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/preemphasis-2.gif" class="kg-image"><figcaption>Log mel spectrogram of an audio clip as well as the same audio clip with pre-emphasis with varying levels (alpha of 0.1, 0.5, 0.9, and 0.99).</figcaption></figure><p>You can also hear the effects of pre-emphasis in the audio clips below.</p><!--kg-card-begin: html--><strong>Original</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/original.wav" type="audio/wav">
</audio>
<br><br>


<strong>No Quantization, Pre-Emphasis with α=0.5</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/preemphasis-0.5.wav" type="audio/wav">
</audio>
<br><br>

<strong>No Quantization, Pre-Emphasis with α=0.9</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/preemphasis-0.9.wav" type="audio/wav">
</audio>
<br><br>

<strong>No Quantization, Pre-Emphasis with α=0.97</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/preemphasis-0.97.wav" type="audio/wav">
</audio>
<br><br><!--kg-card-end: html--><p>To undo pre-emphasis by applying de-emphasis, compute $x[t]$ from $y[t]$ via the autoregressive equation</p><p>$$x[i] = y[i] + \alpha x[i-1].$$</p><p>The effects of pre-emphasis can be heard below. In these examples, the audio is pre-emphasized, companded (with µ = 255), quantized (8 bits), dequantized, expanded, and then de-emphasized.</p><!--kg-card-begin: html--><strong>Original</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/original.wav" type="audio/wav">
</audio>
<br><br>


<strong>8-bit Quantization, Companding, No Pre-Emphasis (α=0.0)</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-companded-preemphasis-0.0.wav" type="audio/wav">
</audio>
<br><br>


<strong>8-bit Quantization, Companding, Pre-Emphasis with α=0.5</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-companded-preemphasis-0.5.wav" type="audio/wav">
</audio>
<br><br>

<strong>8-bit Quantization, Companding, Pre-Emphasis with α=0.9</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-companded-preemphasis-0.9.wav" type="audio/wav">
</audio>
<br><br>

<strong>8-bit Quantization, Companding, Pre-Emphasis with α=0.97</strong><br>
<audio controls>
  <source src="https://andrew.gibiansky.com/images/audio/preemphasis-samples/quantized-companded-preemphasis-0.97.wav" type="audio/wav">
</audio>
<br><br><!--kg-card-end: html--><h3 id="training-with-pre-emphasis">Training with Pre-Emphasis</h3><p>As you can hear above, µ-law companding and pre-emphasis can be used to reduce the effect of quantization noise in audio. In order to model audio with WaveRNN, we have to quantize it to 8 bits (though 9 or 10 is also feasible), so that our final layer softmax outputs can have a reasonable dimension (256 for 8 bits, 512 or 1024 for 9 or 10 bits). In order to synthesize clean audio with WaveRNN, we can train our WaveRNN to produce audio which has been pre-emphasized and µ-law companded. To generate the final real-valued audio clip from the WaveRNN-generated integers, we dequantize, µ-law expand, and then de-emphasize the audio.</p><p>To recap, when training WaveRNN we:</p><ol><li>Load and resample the audio clip to the needed sample rate.</li><li>Apply pre-emphasis with $\alpha \approx 0.9$ or 0.97.</li><li>Apply µ-law companding with $\mu = 255$.</li><li>Quantize to 8-bits symmetrically around zero.</li></ol><p>When synthesizing with WaveRNN, we:</p><ol><li>Autoregressively synthesize an 8-bit integer signal with WaveRNN.</li><li>Apply de-quantization, ensuring that 127 maps exactly to zero.</li><li>Apply µ-law expanding.</li><li>Apply the autoregressive de-emphasis filter as shown above.</li></ol><p>This allows us to model high-fidelity audio with only an 8-bit output.</p><h2 id="summary">Summary</h2><p>Our discrete-valued WaveRNN requires our audio to be quantized to an 8-bit representation. Naive quantization introduces a significant amount of noise, known as quantization noise. µ-law companding and pre-emphasis are two transformations we can apply to our signal prior to quantization in order to reduce the impact of quantization noise. If you apply pre-emphasis and companding to the input audio, you need to apply expanding and de-emphasis to the synthesized audio prior to listening to it. Together, these two transformations allow us to model high-fidelity audio with only 8 bits.</p><p>Check out the implementation at <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> or proceed to the subsequent blog posts:</p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Efficient WaveRNN: Intro]]></title><description><![CDATA[WaveRNN is an autoregressive neural vocoder, a neural network based process for converting low-dimensional acoustic features into an audio waveform by predicting the next sample in a stream of samples. Specialized compute kernels are necessary to make WaveRNN inference fast.]]></description><link>https://andrew.gibiansky.com/wavernn-demystified-part-1/</link><guid isPermaLink="false">6111a1b078c79a0007f26e34</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Sun, 09 Jun 2024 20:38:15 GMT</pubDate><content:encoded><![CDATA[<p>In this series of posts, we're going to go through the WaveRNN neural vocoder for audio waveform synthesis, along with a variety of implementation details and commonly used extensions. For a real implementation, check out the <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> repository.</p><p><strong>Posts in the Series:</strong></p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-part-1/">Intro</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul><h2 id="autoregressive-neural-vocoders">Autoregressive Neural Vocoders</h2><p>WaveRNN is an autoregressive neural vocoder. Before diving into WaveRNN itself, let's break that statement down a bit.</p><p>A <a href="https://en.wikipedia.org/wiki/Speech_synthesis">text-to-speech (TTS) system</a> which converts text into spoken audio is comprised of many components. For instance, the frontend of a TTS engine converts input text to <a href="https://en.wikipedia.org/wiki/Phoneme">phonemes</a>. Prosody and acoustic models assign durations to those phonemes and convert them into <a href="https://en.wikipedia.org/wiki/Spectrogram">spectrograms</a> (or equivalent acoustic features). Finally, a unit selection algorithm or a vocoder convert those acoustic features into an audio waveform. A neural vocoder is the <em>last</em> step in a speech synthesis pipeline.</p><p>To summarize, a neural vocoder is a neural network based algorithm for converting audio from an acoustic feature representation (such as log mel spectrograms) to a waveform. </p><p>(To synthesize audio, the spectrogram representation must already exist and have been created by another step in the TTS pipeline. <a href="https://andrew.gibiansky.com/wavenet-and-tacotron-arent-tts-systems">WaveRNN alone cannot generate speech from text</a>.)</p><p>There are many possible ways to build a neural vocoder – autoregressive models, GANs, invertible normalizing flows, diffusion models. Audio synthesis is one of the most well-studied areas of <a href="https://openai.com/blog/generative-models/">neural generative modeling</a>, lagging only behind image synthesis.</p><p>An audio waveform consists of thousands of samples. Each sample is a single number corresponding to an instantaneous reading from a microphone. An autoregressive model (such as WaveRNN) generates a stream of audio by predicting the next sample given all previous samples and the spectrograms of nearby audio frames.</p><p>To generate audio, we start with an empty audio stream and predict  the first sample. Using the first sample, we predict the second sample. Using the first and second samples, we predict the third sample. This continues until the entire waveform is generated. The spectrograms are an extra input to each of these predictions and guide all the predictions made by WaveRNN, which is trained to generate audio which corresponds to the spectrograms it is given.</p><h2 id="baseline-wavernn">Baseline WaveRNN</h2><p>In this section (and elsewhere), we assume that you are familiar with the basics of neural networks and deep learning. If not, grabbing a book (like <a href="http://neuralnetworksanddeeplearning.com/">this one</a> or <a href="https://www.deeplearningbook.org/">this one</a>) may help.</p><p>WaveRNN consists of a few conceptual pieces:</p><ul><li>The <strong>sample embeddings</strong> take the raw audio samples and process them to be used as inputs to the autoregressive network.</li><li>A <strong>conditioning network</strong> processes the log mel spectrogram features. This network is small and fast and consists of a few layers of convolutions. The outputs of this network are used as inputs to the autoregressive network.</li><li>The <strong>autoregressive network</strong> takes the conditioning information and sample embeddings up to a time <em>t</em> and predicts an audio sample for time <em>t + 1</em>. </li></ul><p>The conditioning network can be run once on the entire input spectrogram, and the autoregressive network and sample embedding layer are alternated: a sample is generated, then prepared for the next timestep, then the next timestep is generated, and so on. At training time, though, the entire (real, non-synthesized) audio clip is available, and can be fed to the autogressive network, as shown below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/Baseline-WaveRNN--training--1.png" class="kg-image"><figcaption>Diagram of the WaveRNN neural vocoder architecture.</figcaption></figure><h3 id="sample-embeddings-discretized-law-audio">Sample Embeddings: Discretized µ-law Audio</h3><p>Audio waveforms are usually represented as a sequence of floating point numbers between -1 and 1. For WaveRNN, however, we apply two transformations to the audio waveform prior to using it:</p><ul><li><strong><a href="https://en.wikipedia.org/wiki/%CE%9C-law_algorithm">µ-law Companding</a>: </strong>We remap the -1 to 1 scale from a linear scale to a log scale to make small differences near zero much more perceptible. </li><li><strong>Discretization: </strong>We divide the range -1 to 1 into 256 different equally spaced regions and represent each sample by the index of the region it falls into, with -1 mapping to 0 and 1 mapping to 255. </li></ul><p>(You could equivalently say that we discretize the range -1 to 1 into 256 different regions with regions far away from zero having an exponentially larger width than regions close to zero.)</p><p>For a given value $x$, µ-law companding maps it to $F(x)$ via</p><p>$$F(x) = \text{sgn}(x) \frac{\ln(1+ \mu |x|)}{\ln(1+\mu)}~~~~-1 \leq x \leq 1$$</p><p>As shown in this animation, companding stretches the example audio waveform scale, so that small variations near zero become big variations. The human ear is sensitive to the <em>log</em> of amplitude / intensity, so without this, the generated audio would be perceived as noisy.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/mu_law_companding.gif" class="kg-image"><figcaption>Animation alternating between a waveform and the companded version of the waveform.</figcaption></figure><p>The companded waveform is then converted to an integer (from 0 to 255) by subdividing the range -1 to 1 into equally spaced chunks. Mathematically, the quantization is done via $D(y)$ where</p><p>$$D(y) = \lfloor \frac{255}{2}(y + 1)+ \frac{1}{2}\rfloor.$$</p><p>We add $\frac{1}{2}$ so that zero (a common value in audio!) can be exactly mapped to the integer 128. You can verify that values near -1 will map to 0 and values near (but less than) 1 will map to 255. </p><p>Discretizing the audio waveform is crucial, because the way we predict the next audio sample is by treating the prediction as a multi-class classification problem. The network is trained to predict which class (from 0 to 255) the next audio sample falls in using a softmax cross entropy loss. To generate a sample, we sample from the multinomial distribution defined by the softmax probabilities.</p><p>To feed the audio samples into the autoregressive network, we convert them into sample <em>embeddings</em> by learning an <a href="https://pytorch.org/docs/stable/generated/torch.nn.Embedding.html">embedding matrix</a> with one row for each of the possible values. We can then obtain the input to the autoregressive network corresponding to an audio sample by looking at the corresponding row of the embedding matrix (that is, if the sample value is 193, we look at the 193rd value of the matrix and use that row as the input).</p><h3 id="conditioning-network">Conditioning Network</h3><p>The input to a neural vocoder is a spectrogram or similar low-dimensional acoustic representation. A common choice is a log mel magnitude spectrogram. Let's briefly break that down!</p><p>Recall that a waveform <a href="https://en.wikipedia.org/wiki/Fourier_transform">can be represented</a> as the sum of a large number of oscillating sine waves of varying frequencies and magnitudes. A magnitude spectrogram (computed via a <a href="https://en.wikipedia.org/wiki/Short-time_Fourier_transform">short-time Fourier transform</a>) indicates the power of the signal in each frequency band at a given time. In other words, a value in a spectrogram tells you the amplitude of the oscillating waves of a given frequency at a given time. A spectrogram looks like this:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/spectrogram.png" class="kg-image"><figcaption>Magnitude spectrogram of an audio clip.</figcaption></figure><p>As mentioned earlier, human hearing is sensitive to the <em>log</em> of the power, which means that small differences near zero are as audible as large differences far away from zero. In this plot, it's hard to see those small differences, so instead we use a log spectrogram:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/log_spectrogram.png" class="kg-image"><figcaption>Log magnitude spectrogram of an audio clip.</figcaption></figure><p>In addition to being sensitive to the log of power, human hearing is also sensitive to the log of <em>frequency </em>(pitch). High volumes in a narrow low frequency band near zero are as audible as high volumes spread throughout a wide high frequency band. To visualize this, we usually plot log <em>mel</em> magnitude spectrograms, as shown below. <a href="https://en.wikipedia.org/wiki/Mel_scale">Mel spectrograms</a> compress the frequency bands in a way that maps to human hearing.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://andrew.gibiansky.com/content/images/2021/08/log_mel_spectrogram.png" class="kg-image"><figcaption>Log magnitude mel spectrogram of an audio clip.</figcaption></figure><p>The conditioning subnetwork of WaveRNN takes log mel spectrograms (or other low-dimensional acoustic features) as input, normalizes them (to be roughly -1 to 1 in magnitude), and then processes them with several layers of alternating convolutions and nonlinearities. It is crucial that the convolutions are non-causal and have both forwards and backwards context, as predicting the next sample at any given point depends not only on the audio sample history but also on the future spectrogram. Including the future context is what makes the spectrogram conditioning information helpful for next sample prediction and thus what causes WaveRNN to closely follow the spectrograms in its generated audio.</p><p>A single frame of the spectrogram corresponds to many samples. Depending on the hop length of the short-time Fourier transform used to compute the spectrogram, each frame of the spectrogram will correspond to dozens or hundreds of samples. Thus, the output of the final convolution and nonlinearity  needs to be upsampled by a corresponding factor. For example, if a spectrogram frame is computed from a hop length of 256 samples, then each output timestep must be upsampled to 256 timesteps prior to being provided as input to WaveRNN.</p><p>The output of the conditioning network (post-upsampling) is a sequence of hidden layer output vectors, each vector corresponding to exactly one sample in the audio being synthesized.</p><h3 id="autoregressive-network">Autoregressive Network</h3><p>The autoregressive network takes as input a sequence of sample embeddings and a sequence of conditioning vectors and uses them to predict the next sample in the sequence. This is done by:</p><ul><li>Sum the sample embeddings and conditioning vectors to form a single input vector per timestep.</li><li>Run the input vectors through a <a href="https://andrew.gibiansky.com/p/32d39ecc-8733-4101-a72d-2448a00090da/html">Gated Recurrent Unit (GRU) RNN</a>.</li><li>Take the last output of the GRU and run it through a <a href="https://pytorch.org/docs/stable/generated/torch.nn.Linear.html">linear layer</a> followed by a <a href="https://pytorch.org/docs/stable/generated/torch.nn.ReLU.html">ReLU activation</a>.</li><li>Run the output of that through a linear layer with 256 outputs (for a 256-wide discretization) followed by a <a href="https://pytorch.org/docs/stable/generated/torch.nn.Softmax.html">softmax activation</a>.</li></ul><p>The output of the softmax is a probability distribution. You can <a href="https://pytorch.org/docs/stable/generated/torch.multinomial.html">sample</a> from that distribution to choose the next sample in the audio waveform.</p><p>To generate the full waveform, start by feeding the autoregressive network a sample corresponding to zero (assuming that most audio clips start with a bit of silence) and predicting the first sample. Then, feed the generated sample back to the autoregressive network to generate the second sample. Continue this process until the entire waveform is generated. </p><p>Since our audio is discretized, after the waveform is generated, the audio needs to be un-discretized and expanded (the opposite of companding is expanding) prior to being played back to the user.</p><p>Audio waveforms consist of many thousands of samples; a single second will usually contain between 16,000 samples (for 16 kHz audio) and 48,000 samples (for 48 kHz audio). This means that the GRU and linear layers must be run tens of thousands of times. This process is very computationally intense, and any non-computational overhead will result in it taking many seconds or even minutes to synthesize a short audio clip. In order to make WaveRNN usable for real-world applications, highly specialized and optimized implementations (compute kernels) are needed to make the synthesis process fast.</p><h2 id="summary">Summary</h2><p>WaveRNN is an autoregressive neural vocoder. A neural vocoder is a neural network based process for converting low-dimensional acoustic features (such as log mel spectrograms) into an audio waveform. Autoregressive vocoders work by predicting the next sample in a stream of samples when given the acoustic features and all the previous samples. WaveRNN uses a discretized µ-law representation of audio to represent the audio as 8-bit integers and then predicts those values with a GRU, a fully connected layer, and a softmax layer, trained as a multi-class classification problem with a cross-entropy loss. Specialized compute kernels are necessary to make WaveRNN inference fast because an audio clip consists of tens or hundreds of thousands of samples, each of which require running a neural network to generate.</p><p>Check out the implementation at <a href="https://github.com/gibiansky/wavernn">gibiansky/wavernn</a> or proceed to the subsequent blog posts:</p><ul><li><a href="https://andrew.gibiansky.com/wavernn-demystified-quantization-noise-and-pre-emphasis/">Reducing Quantization Noise</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-inference/">Autoregressive Inference</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-advanced-optimizations/">Optimizing Inference Arithmetic</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-sparsity/">Efficient Block-Sparse Weight Matrices</a></li><li><a href="https://andrew.gibiansky.com/wavernn-demystified-optimizing-nonlinearities/">Optimizing Nonlinearities</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Streaming Audio Synthesis]]></title><description><![CDATA[The naive approach to streaming audio synthesis using deep neural networks is to break up the input into chunks and then run synthesis on each chunk. Unfortunately, this introduces wasted computation and discontinuities. In this blog post, I present a simple and robust alternative.]]></description><link>https://andrew.gibiansky.com/streaming-audio-synthesis/</link><guid isPermaLink="false">605d4fbd08b9c30006591acc</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Fri, 26 Mar 2021 18:23:20 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1453503795393-c496eee08c98?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDZ8fHN0cmVhbXxlbnwwfHx8fDE2MTY3MzM2MTY&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1453503795393-c496eee08c98?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDZ8fHN0cmVhbXxlbnwwfHx8fDE2MTY3MzM2MTY&ixlib=rb-1.2.1&q=80&w=2000" alt="Streaming Audio Synthesis"><p>Nowadays, there are a boatload of different deep neural networks for audio synthesis. There's <a href="https://arxiv.org/abs/1703.10135">Tacotron</a>, <a href="https://ai.googleblog.com/2017/12/tacotron-2-generating-human-like-speech.html">Tacotron2</a>, <a href="https://arxiv.org/abs/1802.08435">WaveRNN</a>, <a href="https://arxiv.org/abs/1811.00002">WaveGlow</a>, <a href="https://arxiv.org/abs/1912.01219">WaveFlow</a>, <a href="https://arxiv.org/abs/1810.11846">LPCNet</a>, <a href="https://arxiv.org/abs/1910.06711">MelGAN</a>, <a href="https://arxiv.org/abs/2005.05106">MB-MelGAN</a>, and fifty more other networks handling every part of the text-to-speech audio synthesis pipeline.</p><p>When synthesizing audio in a text-to-speech scenario, you might want to synthesize minutes of audio at a time, for example, if you are reading a Wikipedia page or a news article. At the same time, you want the user to experience low latency (&lt;200ms), so that they can start listening to it immediately. None of the aforementioned networks can synthesize several minutes of audio within 200 milliseconds – which means that to satisfy both of these constraints,  you have to start streaming your output before you're done synthesizing.</p><p>In this blog post, I'd like to share an easy way to implement audio streaming for any network.</p><h2 id="naive-approach-run-inference-on-chunks">Naive Approach: Run Inference on Chunks</h2><p>The naive approach to audio streaming is to break up the input into chunks, and then run synthesis on each chunk separately, glueing the results together to form the final audio stream. For RNNs (Tacotron, WaveRNN, etc), pass the hidden state between the chunks.</p><p>For RNNs, this approach works fine – it's identical to just running inference on the entire utterance at once. With CNNs, however, you run into trouble.</p><p>For a non-causal convolution with width <em>k</em>, you need to have <em>k-1</em> timesteps of padding (half that on either side) in order to output something of the same size as the input. This means that if you've broken your input into chunks, you now need to add padding on either side of your input.</p><p>If you add zero-padding, then the outputs near the edges of your chunk may create a discontinuity, resulting in a periodic artifact or slight quality degradation. If you add padding from the original input to the CNN, you have to pad by the total receptive field of the network, which ends up duplicating computation and slowing down your overall synthesis speed. (For example, for a 5 layer CNN with width 7 kernels, your receptive field is 1+5*(7-1)=31, so you need 30 timesteps of padding.)</p><p>One approach is to synthesize overlapping chunks and then average them to reduce the effect of the discontinuities; however, this just patches over the problem, rather than solving it.</p><p>When trying to stream through a model with transposed convolutions (such as MelGAN), you have the same issue but for transposed convolutions. If you mix convolutions and transposed convolutions, even <em>computing</em> the receptive field becomes a bit confusing!</p><p>In summary – although breaking your input into chunks and running inference on each chunk separately <em>does</em> work, it can introduce wasted computation and discontinuities at near the boundaries.</p><h2 id="alternative-approach-perfect-streaming">Alternative Approach: "Perfect Streaming"</h2><p>The approach of chunking up your input, synthesizing the chunks, and then glueing them together leads to wasted computation and discontinuities at chunk boundaries. What can we do instead?</p><p>Instead, with a little bit of work, we can implement "perfect streaming". Perfect streaming results in an output that is <em>identical</em> (barring floating point error) to the same utterance synthesized without streaming – no wasted computation, no discontinuities at chunk boundaries.</p><p>To do this, we take the following approach:</p><ul><li>For every layer in your network, define an initial state, an <em>update</em> function, and a <em>finish</em> function. The <em>update</em> function incorporates new input and returns any available output; and the <em>finish</em> function completes the synthesis for that layer, returning any leftover outputs.</li><li>Compose those layers together: to initialize the full network, collect the initial states of each of the layers; to run an input through the network, run the <em>update</em> functions of the network layers sequentially; to finish synthesis, call <em>finish</em> and <em>update</em> on each of the layers of the network.</li></ul><p>The result will be a network that can be initialized, run streaming synthesis on chunks of input with <em>update</em>, and can be finalized to get the last bit of output with <em>finish</em>.</p><p>Next, we'll go through how to implement these for each of the common layers. Reading through these examples will hopefully make it clear how this ends up working.</p><h3 id="lstms-and-grus">LSTMs and GRUs</h3><p>Streaming through RNNs such as LSTMs and GRUs is easy! The initial state is simply the initial state (of zeros) for the LSTM or the GRU. </p><p>The <em>update</em> function will take the input and run it through the network, outputting exactly as many timesteps as it had in the input while also updating the state with the final state of the RNN after it has run on all the inputs.</p><p>The <em>finish</em> function in this case does nothing – there are no leftover outputs to be emitted.</p><h3 id="conv1d-non-causal-">Conv1D (Non-Causal)</h3><p>Streaming through  Conv1D is slightly more complex than an RNN, because you need to manage the extra state, composed of past inputs to the model.</p><p>The initial state for a Conv1D with (odd) width <em>k</em> is a tensor of zeros with <em>(k-1)/2</em> timesteps. (That is, a tensor of shape <em>batch_size </em>x <em>(k-1)/2 </em>x <em>num_input_channels.</em>)</p><p>The <em>update</em> function will take the input, concatenate it with the state, and then run the Conv1D (in "valid" mode, with no extra padding), returning any resulting outputs. The new state is the <em>last</em> <em>k-1</em> timesteps of the concatenated input tensor.</p><p>Finally, the <em>finish</em> function will take the state and pad it on the right with <em>(k-1)/2</em> timesteps of zeros, run the Conv1D (with no extra padding), and return the resulting outputs.</p><p>To make this concrete, let's work through an example. Let's say we have a Conv1D layer with width 7, 256 input channels, operating with batch size 16. We initialize the state to a tensor of zeros of shape [16, 3, 256]. We have three input chunks of 4 timesteps each (total input size 12). We start by running <em>update</em> with the first chunk, creating a tensor of 3 + 4 = 7 inputs; after running the Conv1D, the layer produces 1 output. The last 6 of these are kept as the state. When we run <em>update</em> with the second chunk, we create a tensor of shape 6 + 4 = 10, which after we run Conv1D, produces 4 outputs. When we run <em>update</em> with the third chunk, we again produce 4 outputs. Finally, we run <em>finish</em>, which takes the state of 6 timesteps, pads them with 3 timesteps of zeros on the right, resulting in input size 9; after we run Conv1D, we produce 3 outputs. In total, we produce 1 + 4 + 4 + 3 outputs, for a total of 12 outputs – exactly as many outputs as we had inputs. </p><h3 id="conv1d-causal-">Conv1D (Causal)</h3><p>A causal Conv1D is very similar to a non-causal Conv1D. However, instead of the initial state having <em>(k-1)/2</em> timesteps, and then <em>finish</em> padding the sequence with <em>(k-1)/2 </em>timesteps on the right, we start with an initial state of <em>k-1</em> timesteps of zeros. Besides that, everything stays the same.</p><h3 id="transposed-conv1d">Transposed Conv1D</h3><p>A transposed convolution with stride upsamples the input; for this example, though, we'll assume a stride of one for simplicity. The implementation for a transposed conv looks very similar to a standard convolution – but instead of the state representing future inputs, the state represents a component of future <em>outputs</em>.</p><p>The key observation to make here is that when we break up a transposed convolution into two chunks, the outputs near the edge have contributions from both chunks. Each input timestep contributes to <em>k</em> different outputs, which means the last input timestep in a chunk affects the first <em>(k-1)/2</em> outputs of the <em>next</em> chunk. We need to make sure to accumulate those outputs and return them only when <em>all</em> of their inputs' contributions have been accounted for.</p><p>The initial state for a transposed Conv1D with (odd) width <em>k</em> is a tensor of zeros with <em>(k-1)/2</em> timesteps. (That is, a tensor of shape <em>batch_size </em>x <em>(k-1)/2 </em>x <em>num_<strong>output</strong>_channels.</em> Output channels, not input channels!)</p><p>The <em>update</em> function for an input with <em>n</em> timesteps will take the input and run it through the transposed convolution, generating <em>n + k - 1</em> output timesteps. Take the current state and add it to the left edge of the output; that is, if you have <em>(k-1)/2</em> timesteps of state, set the left <em>(k-1)/2</em> timesteps to the elementwise sum of the state and the first timesteps of the transposed convolution outputs. If this is the first time <em>update</em> is being called, throw away the first <em>(k-1)/2</em> timesteps. Return all but the last <em>k-1</em> timesteps. Keep the last <em>k-1</em> timesteps of the <strong>output </strong>as your state.</p><p>The <em>finish</em> function returns the first <em>(k-1)/2</em> timesteps of the state as output.</p><p>To help understand this one, let's go through an example again; we'll use the same setup as we did with non-causal Conv1D. Let's say we have a transposed Conv1D layer with width 7, 256 output channels, operating with batch size 16. We initialize the state to a tensor of zeros of shape [16, 3, 256]. We have three input chunks of 4 timesteps each (total input size 12). We start by running <em>update</em> with the first chunk.  With an input of width 4, the transposed Conv1D layer will create a tensor of 4 + 6 = 10 outputs. On the first time we call <em>update</em>, we drop the left 3 timesteps and return the next one timestep of outputs. The last 6 timesteps of output are kept as the state. When we run <em>update</em> with the second chunk, we get an tensor of shape 6 + 4 = 10 again, and we add the state (6 timesteps) to the left edge of that tensor. We return the first four timesteps and keep the last 6 timesteps as state again. When we run <em>update</em> with the third chunk, we again produce 4 outputs. Finally, we run <em>finish</em>, we take the first 3 timesteps of the state and return that. In total, we produce 1 + 4 + 4 + 3 outputs, for a total of 12 outputs – exactly as many outputs as we had inputs.</p><h3 id="concat-and-sum">Concat and Sum</h3><p>For some models (MelGAN, Resnets), you will end up with the output of two different subnetworks being concatenated or summed. These different subnetworks may have different inputs and thus might, during streaming, output different numbers of timesteps. To address this, you need to define <em>update</em> and <em>finish</em> for concat and sum operators, just like you do for compute layers such as convolutions and RNNs.</p><p>In MelGAN, you'll see this used for the generator resnets. The resnets use convolutions, which means that they output less timesteps than their input (initially), so naively summing the output of the resnet with its input doesn't work (since they'll have different numbers of timesteps).</p><p>For both concat and sum, the <em>update</em> function should take the minimum number of timesteps in the inputs and return the concatenation or sum of that many timesteps, storing all leftovers in the state. The <em>finish</em> function should – if everything works out – be left with nothing in the state, since by the time <em>finish</em> is called, all the inputs should be available with the same number of timesteps.</p><h3 id="implementation">Implementation</h3><p>You can implement these in practice as methods on the relevant Pytorch or TensorFlow layers. Then, when you use torch.nn.Sequential or keras.Sequential, you can compose the different layers into a single network with the same methods. </p><p>In the end, you end up with a flexible structure where you can freely experiment with RNNs, convolutions (with stride, dilation, etc), transposed convolutions, concat, resnets, and anything else, mixing the layers in any order and size, knowing that at inference your streaming implementation will be exactly identical to a non-streaming implementation. To test your implementation, you can write a unit test which tests every layer or network by running inference with an utterance and then running the same input but with its input broken into chunks, and then using np.allclose to verify that the outputs are identical.</p><p>If you have any questions about this technique, feel free to reach out and email me or find me <a href="https://twitter.com/agibiansky">on Twitter</a>!</p>]]></content:encoded></item><item><title><![CDATA[Brainstorming: Neural Transducers for Speech Synthesis]]></title><description><![CDATA[<p><a href="https://arxiv.org/pdf/1211.3711.pdf">Neural transducers</a> are commonly used for automatic speech recognition (ASR), often achieving state-of-the-art results for quality and inference speech; for instance, they <a href="https://ai.googleblog.com/2019/03/an-all-neural-on-device-speech.html">power Google's offline ASR engine</a>. In this post, I'd like to propose a neural transducer model for speech synthesis. I'm writing this idea down before trying this model,</p>]]></description><link>https://andrew.gibiansky.com/neural-transducers-for-speech-synthesis/</link><guid isPermaLink="false">5f8c6584535e140006f0f306</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Mon, 16 Nov 2020 00:49:25 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1562369083-e501b585fd2c?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1562369083-e501b585fd2c?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Brainstorming: Neural Transducers for Speech Synthesis"><p><a href="https://arxiv.org/pdf/1211.3711.pdf">Neural transducers</a> are commonly used for automatic speech recognition (ASR), often achieving state-of-the-art results for quality and inference speech; for instance, they <a href="https://ai.googleblog.com/2019/03/an-all-neural-on-device-speech.html">power Google's offline ASR engine</a>. In this post, I'd like to propose a neural transducer model for speech synthesis. I'm writing this idea down before trying this model, so the reality is that this is just a fun avenue for me to brainstorm – and maybe someday I'll try this, or it'll inspire someone else to go down a similar path!</p><h2 id="neural-transducers-for-asr">Neural Transducers for ASR</h2><p>A neural transducer models the alignment between an input and output sequence,  but does so in a way that allows aggregating label probabilities over all possible (monotonic) alignments. Given that ability, we can maximize the probability of a sample label and marginalize over the latent alignment. </p><p>A transducer model has the following inputs:</p><ol><li>An input sequence $x_i$ of length $N$. For ASR, this is usually spectrogram frames or an analogous audio representation.</li><li>A label sequence $y_t$ of length $T$. For ASR, this is a sequence of letters, phonemes, or word pieces.</li><li>A character set of size $V$. The output sequence consists of these characters.</li></ol><p>The model itself has three components:</p><ol><li>An encoder network, $E(x)$. Outputs an encoding vector $h_t$ for each input timestep.</li><li>A decoder network, $D(y_{1:t})$. Outputs an encoding vector $g_u$ for each output prefix. (Similar to an autoregressive language model.)</li><li>A joint prediction network, $P(h_t, g_u)$. Outputs a probability distribution over the model character set plus the blank token $\varnothing$. Let index zero in this distribution represent $\varnothing$.</li></ol><p>The encoder network is usually a bidirectional network of some kind, the decoder network is a unidirectional RNN or causal convolution model, and the prediction network is a multilayer feedforward model. The encoder runs over all $N$ input timesteps, the decoder runs over all $T$ timesteps, and the prediction network runs over all $N\cdot T$ pairs of encoding vectors from those two models.</p><p>The output of the prediction network $P(h_t, g_u)_k$ is the probability that, if input timestep $i$ was aligned to output timestep $t$, the next token is the $k$th character in the character set (if $k &gt; 1$) or that the alignment should advance to the next encoder vector (if $k = 0$ for $\varnothing$).</p><p>Given this matrix of probability distributions, we can maximize over the probability of a label while marginalizing (summing) over alignments using a <a href="https://en.wikipedia.org/wiki/Forward%E2%80%93backward_algorithm">forward-backward algorithm</a>. Given a specific alignment, the loss reduces to a cross-entropy next-character language-model-like prediction loss; introducing the blank character and joint network allows you to write out every possible alignment and its loss. Since there's an exponentially large number of possible alignments, we use a forward-backward algorithm, which allows summing over the alignments with a dynamic programming algorithm. </p><h2 id="proposed-neural-transducers-for-tts">Proposed Neural Transducers for TTS</h2><p>The RNN-transducer (RNN-T) model and loss is a great fit for end-to-end ASR model. However, it's initially a poor fit for text-to-speech (TTS) models: the model relies on a single discrete output per timestep, but TTS models such as <a href="https://arxiv.org/abs/1703.10135">Tacotron</a> output continuous-valued spectrograms.</p><p>To fix this, I'd like to propose a modified RNN-T model called the "<em>generative transducer</em>". This model has the following components:</p><ol><li>An encoder network, $E(x)$. Outputs an encoding vector $h_t$ for each input timestep.</li><li>A decoder network, $D(y_{1:t})$. Outputs an encoding vector $g_u$ for each output prefix.</li><li>A controller network, $C(y)$. Outputs an encoding vector $c_u$ for each output timestep.</li><li>A joint prediction network, $P(h_t, g_u)$. Outputs whatever the output of the model should be; in the case of TTS, this could for example be an 80-dimensional log-mel spectrogram.</li><li>A joint controller network, $\varnothing(h_t, c_u)$. Outputs the probability of advancing to the next encoder timestep instead of making this prediction.</li></ol><p>In this model, we've decoupled the model output network $P(h_t, g_u)$ from the alignment generator network $\varnothing$; this model is strictly a generalization of the standard transducer model.</p><p>In the same way as before, we can use a forward-backward algorithm to sum over alignments. Instead of maximizing the probability of a particular label, we can maximize the expected value of the loss, as if we are sampling from the alignment distribution.</p><p>This setup provides us with an extra interesting modeling choice. If $C(y)$ is a bidirectional network, it can use future output context to improve its transition prediction. However, this means that we cannot use $C(y)$ at inference time. For TTS, this means that we must extract the most likely alignments once this model is trained, convert them to phoneme durations, and then at inference have a separate model to predict phoneme durations. If $C(y) = C(y_{1:t})$ is a unidirectional model, however, we don't need an external phoneme duration model; we can alternate running $D(y_{1:t})$ to predict a frame and sampling from $C(y_{1:t})$ to decide when to transition to the next phoneme.</p><h3 id="implementing-the-generative-transducer-loss">Implementing the Generative Transducer Loss</h3><p>Next, I'd like to go through the practical considerations of computing this loss function, similar to how the <a href="https://arxiv.org/pdf/1211.3711.pdf">RNN-T paper</a> derives forward and backward equations for the RNN-T loss.</p><p><strong>Forward Pass</strong></p><p>Similar to RNN-T, we'll compute transition probabilities $\varnothing(t, u)$ and outputs $P(h_t, g_u)$ for every point $(t, u)$. To compute the expected loss, we'll sum the loss for every $(t, u)$ weighted by the probability of encoder timestep $t$ being aligned to decoder timestep $u$.</p><p>So, let $\alpha(t, u)$ be the probability that encoder timestep $t$ is aligned with decoder timestep $u$. We can compute $\alpha(t, u)$ recursively via</p><p>$$\begin{align*}\alpha(t, u) &amp;= \alpha(t - 1, u) \varnothing(t - 1, u) + \alpha(t, u - 1) (1 - \varnothing(t, u  - 1)) \\ \alpha(1, 0) &amp;= 1\end{align*}$$</p><p>The probability of emitting an output when encoder timestep $t$ is aligned with decoder timestep $u$ is then $\alpha(t, u) (1 - \varnothing(t, u))$.</p><p>The prediction network $P(h_t, g_u)$ yields an output vector for every point $(t, u)$. Let the loss for that output vector be $L(t, u).$ To create a Tacotron-like model, we would use an autoregressive L2 loss such as</p><p>$$L(t, u) = \left(P(h_t, g_u) - y_{u+1}\right)^2.$$</p><p>We could similarly use a discrete cross-entropy loss if our outputs are discretized.</p><p>Finally, the overall loss $L$ will be the expected value of $L(t, u)$ when summed over all the timesteps:</p><p>$$L = \sum_{t=1}^T \sum_{u=1}^U \alpha(t, u) (1 - \varnothing(t, u)) L(u, v).$$</p><p><strong>Backward Pass</strong></p><p>Backpropagating through the tensor operations that yield $L$ (from $\alpha(t, u)$) is easy. However, we will need a custom op to compute the full partials with respect to $\alpha(t, u)$ as well as $\varnothing(t, u)$, since $\alpha(t, u)$ is used in computation of future $\alpha(t + 1, u)$ and $\alpha(t, u + 1).$ For notational simplicity, let $\delta(t, u) = \frac{\partial L}{\partial \alpha(t, u)}$. We can compute the $\delta(t, u)$ via the recurrence relation (with base cases)</p><p>$$\begin{align*}\delta(t, u) &amp;= \delta(t, u)_\text{base} + \varnothing(t + 1, u)\delta(t + 1, u) + (1 - \varnothing(t, u + 1)) \delta(t, u + 1) \\ \delta(T, u) &amp;= \delta(T, u)_\text{base}  + (1 - \varnothing(t, u + 1)) \delta(t, u + 1) \\ \delta(t, U) &amp;= \delta(t, U)_\text{base}  + \varnothing(t + 1, u)\delta(t + 1, u) \\ \delta(T, U) &amp;= \delta(T, U)_\text{base}\end{align*}$$</p><p>$\delta(t, u)_\text{base}$ is the contribution to the partial that was backpropagated from $L$ via tensor operations.</p><h3 id="summary">Summary</h3><p>This proposed model is an extension of the RNN-T model to situations with a continuous-valued output. We create an external alignment model which just models the transitions in the encoder, and then use its outputs to compute an expected value over the loss in all possible alignments.</p><p>As described, there's nothing penalizing the model for not using the encoder inputs; we in no way require the model to use all of its encoder inputs. Perhaps extra loss terms that encourage $\alpha(T, U)$ to be high and $\alpha(t, U)$ to be low for $t &lt; T$ would be valuable.</p>]]></content:encoded></item><item><title><![CDATA[PQMF: Sub-band Coding for Neural Vocoders (Part 2)]]></title><description><![CDATA[<p>This is a continuation of <a href="https://andrew.gibiansky.com/pqmf-subband/">Part 1</a> of this two-part series. In this post, I'll try to go over the implementation of PQMF filters in sufficient detail such that you'll be able to use this technique in your own code.</p><h2 id="overview">Overview</h2><p>In the previous post, I summarized by presenting a</p>]]></description><link>https://andrew.gibiansky.com/pqmf-sub-band-coding-for-neural-vocoders-part-2/</link><guid isPermaLink="false">5fa8d174535e140006f0f993</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Mon, 16 Nov 2020 00:48:04 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1605423357213-765e720a4bf7?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1605423357213-765e720a4bf7?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="PQMF: Sub-band Coding for Neural Vocoders (Part 2)"><p>This is a continuation of <a href="https://andrew.gibiansky.com/pqmf-subband/">Part 1</a> of this two-part series. In this post, I'll try to go over the implementation of PQMF filters in sufficient detail such that you'll be able to use this technique in your own code.</p><h2 id="overview">Overview</h2><p>In the previous post, I summarized by presenting a recipe for converting a neural vocoder into a sub-band vocoder:</p><ol><li><strong>Design a Prototype Filter: </strong>Choose a prototype filter $h[n]$ to use for your PQMF filter bank. The <a href="https://ir.nctu.edu.tw/bitstream/11536/32569/1/000073958700002.pdf">Kaiser window approach</a> seems easiest here.</li><li><strong>Compute PQMF Filter Bank: </strong>Choose the number of bands $K$ you plan to use. Then, compute your analysis and synthesis filters $h_k[n]$ and $g_k[n]$ using the equations above.</li><li><strong>Training Data Analysis: </strong>Calculate sub-band signals for all your training data using the analysis filters.</li><li><strong>Vocoder Training: </strong>Train your neural vocoders to predict the sub-band signals. Although you can train separate models per sub-band, to get inference speed improvements you must modify your model to output all sub-band signals in each timestep. This will reduce the number of output timesteps by a factor of $K$, which should reduce inference time by approximately that same factor. For example, for WaveRNN, you can have each timestep output $K$ values and input the previous samples for each sub-band. For MelGAN or WaveGlow, you can have the output consist of $K$ channels which get combined to create the final audio using your synthesis filters.</li><li><strong>Inference-time Synthesis: </strong>After running your vocoder during inference, run synthesis on the outputs to get your new audio stream.</li></ol><p>I left some of these pieces rather vague, and now it's time to fill in the details. Specifically, I'd like to address:</p><ul><li><strong>Designing a Prototype Filter: </strong>How do we compute a prototype filter?</li><li><strong>Computing Analysis-Synthesis Filters: </strong>How do we create the necessary filters?</li><li><strong>Implementation with Vocoders: </strong>How do we use these analysis and synthesis filters in standard deep learning frameworks?</li></ul><p>The bulk of this post will be addressing prototype filters, and we'll be using PyTorch for all code samples.</p><h2 id="designing-a-prototype-filter">Designing a Prototype Filter</h2><p>In this section, we'll dive into a prototype filter design method based on <a href="https://ir.nctu.edu.tw/bitstream/11536/32569/1/000073958700002.pdf">Kaiser windows</a>.</p><h3 id="vocabulary">Vocabulary</h3><p>Prior to jumping in, let's go over some vocabulary from filter design. For me, all of these rang bells from my signal processing classes years ago, but it was valuable to go through each of them.</p><ul><li><strong>Window Function: </strong>A <a href="https://en.wikipedia.org/wiki/Window_function">window function</a> is a function which is zero outside of a given interval and, usually, symmetric around zero. Window functions are multiplied with a signal before doing Fourier transforms to avoid discontinuities at the edge (since Fourier transforms assume that the signal is repeating). </li><li><strong>Bessel Function: </strong><a href="https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions:_I%CE%B1,_K%CE%B1">Bessel functions</a> are a family of functions which are the solution to Bessel's differential equation, and are used in a variety of areas in physics, signal processing, etc. For our purposes, they're functions which can be used to create a bell-shaped window function.</li><li><strong>Cutoff Frequency: </strong>The <a href="https://en.wikipedia.org/wiki/Cutoff_frequency">cutoff frequency</a> is the frequency at which the response of a filter begins to attenuate significantly, for example, at which the filter reduces the power by a factor of two.</li><li><strong>Stopband Attenuation: </strong>The <a href="https://en.wikipedia.org/wiki/Stopband">stopband</a> attenuation is the attenuation that must be attained in the stopband (the frequency range in which the filter is not supposed to let signal through).</li><li><strong>Transition Bandwidth: </strong>The <a href="https://en.wikipedia.org/wiki/Transition_band">transition bandwidth</a> (as the name implies) is the width of the frequency range between the passband and the stopband; good filters will minimize this width.</li></ul><h3 id="kaiser-window-filter">Kaiser Window Filter</h3><p><strong>tl; dr: </strong>You can do this with one call to <code><code><a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.firwin.htmlhttps://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.firwin.html">scipy.signal.firwin</a></code></code>.</p><p>Given a window length $N$, Kaiser window filter $w(n)$ is the following:</p><figure class="kg-card kg-image-card"><img src="https://andrew.gibiansky.com/content/images/2020/11/image.png" class="kg-image" alt="PQMF: Sub-band Coding for Neural Vocoders (Part 2)"></figure><p>This defines a window of length $N+1$ using the modified Bessel function $I_0$ of the first kind. In practice, this can be computed using <code><a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.windows.kaiser.html">scipy.signal.windows.kaiser</a></code> (which assumes $n$ is centered around zero by default). </p><p>The Kaiser window looks like you'd expect a window to look like:</p><figure class="kg-card kg-image-card"><img src="https://andrew.gibiansky.com/content/images/2020/11/image-1.png" class="kg-image" alt="PQMF: Sub-band Coding for Neural Vocoders (Part 2)"></figure><p>Given a stopband attenuation $As$ and transition bandwidth $\Delta w$, the filter length should be approximately:</p><p>$$N \approx \frac{As -7.95}{14.36\Delta w} 2\pi.$$</p><p>The derivation for this specific fact is supposedly available <a href="https://ieeexplore.ieee.org/document/376856">here</a>, but I am for the time being willing to take this on faith. Similarly, the needed value for $\beta$ is also a function of your stopband attenuation $As$.</p><p>Given your chosen window function $w(n)$, your final prototype filter can be computed from your desired cutoff frequency $\omega_c$:</p><p>$$h(n) = \frac{\sin(\omega_c n) w(n)}{\pi n}.$$</p><p>We have yet to compute the cutoff frequency $\omega_c$, which, according to <a href="https://ir.nctu.edu.tw/bitstream/11536/32569/1/000073958700002.pdf">our reference paper</a>, is best computed by minimizing the objective function</p><p>$$\phi_{\text{new}}(\omega_c) = \max |h(n) * h(N - n)|.$$</p><p>(That is, given an $\omega_c$, compute the prototype filter, convolve it with its reverse, and take the maximum absolute value. More details <a href="https://github.com/kan-bayashi/ParallelWaveGAN/issues/195#issuecomment-671408750">discussed here</a>, but this seems to be right.)</p><p>For computing this in practice, you can use <code><a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.firwin.html">scipy.signal.firwin</a></code>. <code>firwin</code> allows you to specify a window function; additionally, if you specify the <code>width</code> parameter, you can directly set the transition bandwidth and it will calculate the appropriate value of <code>beta</code> for your Kaiser window.</p><h2 id="implementing-analysis-synthesis-filters">Implementing Analysis-Synthesis Filters</h2><p>Now that we have a prototype filter computed using <code>scipy.signal.firwin</code>, we need to create a set of $K$ analysis filters $h_k(n)$ and synthesis filters $g_k(n)$. Recall <a href="https://andrew.gibiansky.com/pqmf-subband/">from the previous post</a> that these are defined as follows in PQMF filter banks:</p><p>$$\begin{align*}h_k[n] &amp;= h[n] \cos\left(\frac{\pi}{4K}\left(2k + 1\right)\left(2n - N + 1\right) + \Phi_k\right)\\g_k[n] &amp;= h_k[N - 1 - n]\\\Phi_k &amp;= (-1)^{k} \frac{\pi}{4}.\end{align*}$$</p><p>Computing this is not too hard, just a bit of arithmetic. The process of using these filters is then equivalent to standard convolution.</p><h3 id="pseudocode">Pseudocode</h3><p>With the math out of the way, let's convert this to code. Warning – <em>don't</em> copy this code and run it. I haven't tested it and I haven't run it, so it won't be useful to you that way. Instead, view this as a formalization of what I wrote above and trace through the logic to understand it.</p><pre><code class="language-python">def create_prototype_filter(bands: int, cutoff: float) -&gt; np.ndarray:
    """Create a prototype filter."""
    transition_bandwidth = 0.9 * np.pi / 2 / bands
    attenuation = 100 # dB
    taps = int(2 * np.pi * (attenuation -7.95) / (14.36 * transition_bandwidth)) 
    return scipy.signal.firwin(
        taps, cutoff, width=transition_bandwidth, fs=2 * np.pi
    )
    
def optimize_cutoff(bands: int) -&gt; float:
    """Choose the best cutoff frequency."""
    options = np.linspace(0.0, np.pi, 100000)
    best_cutoff, best_objective = None, 1e9
    for option in options:
        h = create_prototype_filter(bands, option)
        objective = np.abs(np.convolve(h, h[::-1])).max()
        if objective &lt; best_objective:
            best_cutoff, best_objective = option, objective
    return best_cutoff
    
def create_filter_bank(bands: int):
    """Create the entire filter bank."""
    cutoff = optimize_cutoff(bands)
    proto = create_prototype_filter(bands, cutoff)
    taps = proto.size
    h = np.zeros((bands, taps))
    g = np.zeros((bands, taps))
    factor = (np.pi / (2 * bands)) * (
        np.arange(taps + 1) - ((taps - 1) / 2)
    )
    for k in range(bands):
        scale = (2 * k + 1) * factor
        phase = (-1) ** k * np.pi / 4
        h[k] = 2 * proto * np.cos(scale + phase)
        g[k] = 2 * proto * np.cos(scale - phase)
        
    return h, g</code></pre><h2 id="conclusion">Conclusion</h2><p>All in all, as is often the case with mathematical ideas, the amount of <em>code</em> it takes to implement all of this is pretty small. Thank you additionally to @kan-bayashi who's <a href="https://github.com/kan-bayashi/ParallelWaveGAN">Github repo</a> was hugely helpful in tracing through some of the logic required.</p><p>This approach definitely raises additional questions, which I look forward to answering or seeing answered:</p><ul><li>How does the speed and quality depend on the number of bands, the number of taps, and the prototype filter used?</li><li>Instead of using this method to design the prototype filter, can we learn the prototype filter as part of vocoder training?</li><li>Do we need to use PQMF filters? Could we instead learn both analysis and synthesis filters entirely from scratch?</li></ul><p>Definitely looking forward to seeing where all of this goes! I'm consistently impressed with the rapid progress in neural speech synthesis, so I'm sure the answers will come quickly.</p>]]></content:encoded></item><item><title><![CDATA[PQMF: Sub-band Coding for Neural Vocoders (Part 1)]]></title><description><![CDATA[<p>In the past year or so, there's been several papers that investigate using sub-band coding with neural vocoders to model audio and accelerate inference:</p><ul><li><a href="https://ieeexplore.ieee.org/document/8639687?denied=">FFTNet with sub-band coding</a></li><li><a href="https://ieeexplore.ieee.org/document/8462237">WaveNet with sub-band coding</a></li><li><a href="https://arxiv.org/pdf/1909.01700.pdf">DurIan TTS System from Tencent</a></li><li><a href="https://arxiv.org/abs/2005.05106">MelGAN with sub-band coding</a></li><li><a href="https://www.isca-speech.org/archive/VCC_BC_2020/pdfs/VCC2020_paper_27.pdf">Sogou System for Blizzard 2020</a></li></ul><p>In this blog post,</p>]]></description><link>https://andrew.gibiansky.com/pqmf-subband/</link><guid isPermaLink="false">5f9d9077535e140006f0f624</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Sun, 01 Nov 2020 00:27:41 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1595598237436-bf64a3bf18cd?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1595598237436-bf64a3bf18cd?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="PQMF: Sub-band Coding for Neural Vocoders (Part 1)"><p>In the past year or so, there's been several papers that investigate using sub-band coding with neural vocoders to model audio and accelerate inference:</p><ul><li><a href="https://ieeexplore.ieee.org/document/8639687?denied=">FFTNet with sub-band coding</a></li><li><a href="https://ieeexplore.ieee.org/document/8462237">WaveNet with sub-band coding</a></li><li><a href="https://arxiv.org/pdf/1909.01700.pdf">DurIan TTS System from Tencent</a></li><li><a href="https://arxiv.org/abs/2005.05106">MelGAN with sub-band coding</a></li><li><a href="https://www.isca-speech.org/archive/VCC_BC_2020/pdfs/VCC2020_paper_27.pdf">Sogou System for Blizzard 2020</a></li></ul><p>In this blog post, I'd like to go over the ideas behind sub-band coding and specifically the math behind PQMF coding. You can find a more textbook-style approach to this in <a href="https://link.springer.com/chapter/10.1007/978-1-4615-0327-9_4">chapter 4 of this book</a> (and, if you don't have a copy of this book or some of the above-linked papers, a wonderful person named Alexandra Elbakyan can likely help you). </p><h2 id="sub-band-coding">Sub-band Coding</h2><p>When you work with audio, you can represent it in the time domain (as a waveform) or in the frequency domain (as a spectrogram). A spectrogram is obtained through a <a href="https://en.wikipedia.org/wiki/Discrete_Fourier_transform">Discrete Fourier Transform</a>, and tells you the power and phase of the audio at every frequency present in the audio.</p><p>The key idea behind sub-band coding is that instead of representing the audio as a single high-frequency (24kHz) signal that covers the entire range of frequencies, we can instead represent it as <em>multiple</em> lower-frequency (e.g. 6kHz) signals that cover ranges of frequencies (sub-bands). </p><p>We can use this alternate representation for different applications. In file compression (MP3), this is used to apply different levels of quantization and compression to the different bands (because human hearing is sensitive to them in different ways). In neural vocoding for TTS, we can use this to accelerate inference by having our neural vocoders output multiple sub-band values per output timestep, thus reducing the total amount of compute needed while keeping quality high.</p><p>Sub-band coding consists of two phases. In the first phase, <em>analysis</em>, the signal is processed with a set of $k$ <em>analysis filters</em>, creating $k$ new signals. These signals are downsampled by a factor of $k$ by taking every $k$th value (thus maintaining the same total amount of data). In audio compression applications, the downsampled signals are processed (quantization, compression, etc) and transmitted. The second phase, <em>synthesis</em>, reconstructs the original signal from the downsampled and processed signals. To reconstruct the original signal, the signals are then upsampled by a factor of $k$ to the original data rate by inserting $k - 1$ zeros after each value. These upsampled signals are processed with a set of $k$ <em>synthesis filters</em> (one filter per signal) and added together. If the analysis and synthesis filters are chosen appropriately, the output signal can either approximate or perfectly reconstruct the input signal.</p><p>For the application of neural vocoding, we apply the analysis filters to the training data and train our networks to produce all $k$ sub-band signals as outputs. We then apply synthesis to our network outputs to produce the final audio stream. This can accelerate inference over standard modeling techniques because the signal we are producing has been downsampled by a factor of $k$, and different bands for a single timestep are modeled as conditionally independently.</p><p>Next up, let's talk about the math behind sub-band coding.</p><h2 id="the-z-transforms">The Z Transforms</h2><p>We'll soon be talking a lot about discrete signal filters. A filter is a function that, given an input signal $x[t]$, produces a modified signal $y[t]$. Just like we use spectrograms to analyze audio signals in the frequency domain, analyzing the behavior of filters is commonly done in the frequency domain as well, and we use the <a href="https://en.wikipedia.org/wiki/Z-transform">Z transform</a> (a discrete equivalent of a Laplace transform) to convert filters into the frequency domain.</p><p>Since the Z transform is a little less common than the discrete Fourier transform, it's worth going over. If you're familiar with it, skip to the next section.</p><p>The discrete Fourier transform of $x[n]$ is defined as:</p><p>$$X[f] = \sum_{n=0}^N x[n] e^{\frac{-i f n 2 \pi}{N}}.$$</p><p>The Z transform generalizes the $e^{\frac{i 2 \pi}{N}f}$ term to be <em>any</em> complex value $z$ instead of being restricted to the unit circle, and is defined as:</p><p>$$X[z] = \sum_{n=0}^\infty x[n] z^{-n}.$$</p><p>The Z transform has <a href="https://en.wikipedia.org/wiki/Z-transform#Properties">similar properties to the Fourier transforms</a>. The relevant ones to our discussions are:</p><ul><li><strong>Linearity</strong>: The transform of $a x[n] + b y[n]$ is $a X[n] + B y[n].$</li><li><strong>Convolution: </strong>The transform of the convolution $x[n] * y[n]$ is $X[n] Y[n].$</li><li><strong>Time Delay: </strong>The transform of $x[n + k]$ is $X[n] z^k.$</li></ul><p>We will also need the <a href="https://web.eecs.umich.edu/~fessler/course/451/l/pdf/updown.pdf">Z transform for downsampling and upsampling</a> (by dropping samples or inserting zeros):</p><ul><li><strong>Downsampling: </strong>The transform of $y[n] = x[nK]$ is $$Y[z] = \frac{1}{K} \sum_{k=0}^{K-1} X[e^{-i\frac{2\pi}{K}kn}z^{-n/K}.$$</li><li><strong>Upsampling</strong>: The transform of $y[n] = x[n / K] \text{ if $n$ divides $K$ else } 0$ is $Y[z] = X[z^K].$</li></ul><p>If you need to, rederive the above properties to make sure they make sense to you!</p><h2 id="frequency-domain-sub-band-coding">Frequency Domain Sub-Band Coding</h2><p>Now that we have reviewed the Z transform, let's use it to analyze the synthesis and analysis processes and derive a set of filters for a pair of bands ($k=2$).</p><p>Consider the following diagram of analysis and synthesis (from Ch 4. of the aforementioned book):</p><figure class="kg-card kg-image-card"><img src="https://andrew.gibiansky.com/content/images/2020/10/image-1.png" class="kg-image" alt="PQMF: Sub-band Coding for Neural Vocoders (Part 1)"></figure><p>In our setup, we have analysis filters $H_0$ and $H_1$ and synthesis filters $G_0$ and $G_1$. The Z transform of $x'[n]$ (expressed using the transforms of the bands and the synthesis filters) is then:</p><p>$$X'[z] = Y_0(z^2) G_0(z) + Y_1(z^2) G_1(z).$$</p><p>This relies on two previously-discussed properties. First, passing a signal through a series of filters multiplies the filters', because applying a filter is convolving a signal with the filter's impulse response. Second, applying a time delay of 2 to a signal $Y_0(z)$ yields $Y_0(z^2)$, as mentioned above.</p><p>By the same general approach, we can write $Y_0(z)$ and $Y_1(z)$ using the analysis filters and the input signal:</p><p>$$Y_i(z) = \frac{1}{2}H_i(z^{1/2}) X[z^{1/2}] + \frac{1}{2}H_i(z^{-1/2}) X[z^{-1/2}].$$</p><p>Combining this, we can write $X'[z]$:</p><p>$$\begin{align*}X'[z] =&amp; \frac{1}{2}X[z]\left(H_0(z)G_0(z) + H_1(z)G_1(z)\right) + \\  &amp;\frac{1}{2}X[-z](H_0(-z)G_0(z) + H_1(-z)G_1(z)) \\ =&amp; X[z].\end{align*}$$</p><p>To achieve perfect reconstruction, we need to choose filters such that $X'[z] = X[z].$ There are many possible filters that satisfy this.</p><h2 id="quadrature-mirror-filters-qmf-">Quadrature Mirror Filters (QMF)</h2><p>One common choice of filters is a set of <a href="https://en.wikipedia.org/wiki/Quadrature_mirror_filter">Quadrature Mirror Filters</a>.</p><p>To motivate this choice, we first of all want to get rid of the $X[-z]$ term in the equation above (this term causes aliasing). We can do so by setting:</p><p>$$G_0(z) = -H_1(-z)\\G_1(z) = H_0(-z).$$</p><p>Plugging this in to the equation for $X'[z],$ we get:</p><p>$$X'[z] =\frac{1}{2}X[z](-H_0(z) H_1(-z) + H_1(z)H_0(-z)).$$</p><p>The QMF solution continues to simplify by setting $H_1(z) = -H_0(-z),$ so that</p><p>$$X'[z] =\frac{1}{2}X[z](H_0(z)^2 - H_0(-z)^2).$$</p><p>We can then choose any $H_0(z),$ as long as it satisfies:</p><p>$$H_0(z)^2 - H_0(-z)^2 = 2z^{-D}.$$</p><p>The factor of $z^{-D}$ allows us to create a delayed output signal.</p><p>One way to achieve this is using a <strong>2-tap Haar filter</strong>. This filter has the impulse response</p><p>$$h_0[n] = \left\{\frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0, 0, \ldots\right\}.$$</p><p>To clarify, this means that $y[n] = \frac{1}{\sqrt{2}} (x[n] + x[n-1]).$ Using linearity and time delay properties, the Z transform of this Haar filter's impulse response is</p><p>$$H_0(z) = \frac{1}{\sqrt{2}}(1 + z^{-1}),$$</p><p>which we can verify meets the aforementioned constraint on $H_0(z)$.</p><p>From the equations we had earlier, we can drive all our analysis and synthesis filters:</p><p>$$\begin{align*}h_0[n] &amp;= \left\{\frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0, 0, \ldots\right\}\\h_1[n] &amp;= \left\{-\frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0, 0, \ldots\right\}\\g_0[n] &amp;= \left\{\frac{1}{\sqrt{2}}, \frac{1}{\sqrt{2}}, 0, 0, \ldots\right\}\\g_1[n] &amp;= \left\{\frac{1}{\sqrt{2}}, -\frac{1}{\sqrt{2}}, 0, 0, \ldots\right\}\end{align*}$$</p><p>With a bunch of arithmetic, you can verify that the output signal is indeed identical to the input signal.</p><p>While these filters do work <em>mathematically</em>, they end up being less useful in practice. For filters to be useful, you want them to separate the frequency domain neatly, such that each filter significantly attenuates frequencies outside its band and does <em>not</em> attenuate frequencies in its band. These Haar filters have incredibly wide bands, as shown below, and thus are not very useful:</p><figure class="kg-card kg-image-card"><img src="https://andrew.gibiansky.com/content/images/2020/10/image-2.png" class="kg-image" alt="PQMF: Sub-band Coding for Neural Vocoders (Part 1)"></figure><p>There exist better but approximate QMF solutions that have tighter frequency bands; these are longer (they have more than two taps).</p><h2 id="pseudo-quadrature-mirror-filters-pqmf-">Pseudo-Quadrature Mirror Filters (PQMF)</h2><p>The QMF filter bank works for two channels. However, in practice, we may want more than two channels. A generalization of the QMF filter bank to many channels exists, and is called the Pseudo-Quadrature Mirror Filter Bank (PQMF). These filters are called "pseudo-QMF" because these are approximate, not exact.</p><p>The filters used for <a href="https://arxiv.org/pdf/1909.01700.pdf">DurIAN</a> (see Appendix A) and <a href="https://arxiv.org/pdf/2005.05106.pdf">MB-MelGAN</a> are PQMF filters. These are created by following the methodology in <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.18.2036&amp;rep=rep1&amp;type=pdf">"Near-Perfect-Reconstruction Pseudo-QMF Banks"</a>, and use four bands with a filter order of 63 (that is, have 63 "taps"). PQMF filter banks are also used in MP3 and other audio codecs.</p><p>PQMF filter banks consist of $K$ channels based on a low-pass filter $h[n]$. The $K$ analysis and synthesis filters are then computed from this chosen low-pass filter as follows:</p><p>$$\begin{align*}h_k[n] &amp;= h[n] \cos\left(\frac{\pi}{4K}\left(2k + 1\right)\left(2n - N + 1\right) + \Phi_k\right)\\g_k[n] &amp;= h_k[N - 1 - n]\end{align*}$$</p><p>The analysis filters $h_k[n]$ are cosine-modulated versions of the original filter (making PQMF filters part of a class of filters known as Cosine-Modulated Filter Banks or CMFBs). The synthesis filter is a time-reversed version of the analysis filter. Additionally, the cosine phases of adjacent bands are constrained and must satisfy (for integral $r$)</p><p>$$\Phi_k - \Phi_{k-1} = \frac{\pi}{2}(2r + 1).$$</p><p>One option, recommended in <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.18.2036&amp;rep=rep1&amp;type=pdf">Eq. 11 here</a>, is the following choice:</p><p>$$\Phi_k = (-1)^{k} \frac{\pi}{4}.$$</p><p>We can verify that this indeed meets the criterion above. </p><p>The choice of the prototype low-pass filter $h[n]$ is important for reducing reconstruction error. In fact, this filter can be found using computational methods by optimizing an objective function which minimizes the magnitude of the reconstruction error and maximizes stopband attenuation. </p><p>One approach to computationally designing the prototype filter is <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.18.2036&amp;rep=rep1&amp;type=pdf">presented here</a>; another approach based on limiting the search to <a href="https://en.wikipedia.org/wiki/Kaiser_window">Kaiser windows</a> is <a href="https://ir.nctu.edu.tw/bitstream/11536/32569/1/000073958700002.pdf">available here</a>. The latter approach seems significantly simpler to understand and easier to implement.</p><h2 id="training-neural-vocoders-with-pqmf-sub-band-coding">Training Neural Vocoders with PQMF Sub-band Coding</h2><p>Finally, let's summarize the approach into a series of steps we can take to modify any neural vocoder with sub-band coding.</p><ol><li><strong>Design a Prototype Filter: </strong>Choose a prototype filter $h[n]$ to use for your PQMF filter bank. The <a href="https://ir.nctu.edu.tw/bitstream/11536/32569/1/000073958700002.pdf">Kaiser window approach</a> seems easiest here.</li><li><strong>Compute PQMF Filter Bank: </strong>Choose the number of bands $K$ you plan to use. Then, compute your analysis and synthesis filters $h_k[n]$ and $g_k[n]$ using the equations above.</li><li><strong>Training Data Analysis: </strong>Calculate sub-band signals for all your training data using the analysis filters.</li><li><strong>Vocoder Training: </strong>Train your neural vocoders to predict the sub-band signals. Although you can train separate models per sub-band, to get inference speed improvements you must modify your model to output all sub-band signals in each timestep. This will reduce the number of output timesteps by a factor of $K$, which should reduce inference time by approximately that same factor. For example, for WaveRNN, you can have each timestep output $K$ values and input the previous samples for each sub-band. For MelGAN or WaveGlow, you can have the output consist of $K$ channels which get combined to create the final audio using your synthesis filters.</li><li><strong>Inference-time Synthesis: </strong>After running your vocoder during inference, run synthesis on the outputs to get your new audio stream.</li></ol><p>As some meta-commentary, I'm fascinated that this clever idea took such a long time to reach neural vocoders, and I suspect that now that it's been shown to be effective in several works, it will spread quickly. This seems like a classic case of slow progress due to siloed knowledge: the people with the deep understanding of MPEG filter banks for the most part were unlikely to be training neural vocoders, and the people training neural vocoders were unlikely to have deep knowledge of MPEG filter banks. It took <em>several</em> skilled and cross-functional researchers to make this happen – at least one person to have the initial idea, and then several more to reproduce it in several more works to get wider acclaim for this idea. This also really highlights how valuable domain experience can be – you can't dive into a new field, sprinkle some machine learning fairy dust, and get great results!</p>]]></content:encoded></item><item><title><![CDATA[Facebook's Knowledge-Assisted NLP]]></title><description><![CDATA[A deep dive into several Facebook publications about knowledge-augmented language tasks, such as question answering and entity linking.]]></description><link>https://andrew.gibiansky.com/facebooks-knowledge-assisted-nlp/</link><guid isPermaLink="false">5f73cb5c6cc48f0006cca024</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Tue, 06 Oct 2020 20:08:30 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1565462905102-140e712045aa?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1565462905102-140e712045aa?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Facebook's Knowledge-Assisted NLP"><p>Facebook recently published <a href="https://ai.facebook.com/blog/retrieval-augmented-generation-streamlining-the-creation-of-intelligent-natural-language-processing-models/">a blog post</a> about <a href="https://arxiv.org/abs/2005.11401">their Retrieval-Augmented Generation (RAG) paper</a> (published in May 2020). The blog post is light on detail, but, as usual, the news coverage is <a href="https://www.marktechpost.com/2020/09/29/facebook-ai-open-sources-rag-an-innovation-in-intelligent-nlp-models/">much worse</a> (filled with ads and poorly written). I decided to dive in and figure out what the work was about.</p><p>In 2020, Facebook has had several publications about knowledge-aided NLP (<a href="https://arxiv.org/abs/2009.02252">KILT</a>, <a href="https://arxiv.org/abs/2005.11401">RAG</a>, <a href="https://arxiv.org/abs/1910.13461">BART</a>, <a href="https://arxiv.org/abs/2004.04906">DPR</a>, <a href="https://arxiv.org/pdf/1911.03814.pdf">BLINK</a>), so in this blog post I'd like to go through what all these acronyms are and how they fit together.</p><p>To summarize:</p><ul><li>Dense Passage Retrieval (DPR): A model which, given a question, retrieves relevant passages from a database of passages built from Wikipedia.</li><li>BART: A sequence-to-sequence (seq2seq) version of BERT.</li><li>BLINK: A DPR-based entity linker.</li><li>Retrieval-Augmented Generation (RAG): A DPR- and BART-based question answering model.</li><li>Knowledge Intensive Language Tasks (KILT): A benchmark for evaluating quality of knowledge-based tasks, tested on baselines and Facebook's models.</li></ul><p>I'll go through these models in more detail below.</p><h2 id="dense-passage-retrieval-dpr-">Dense Passage Retrieval (DPR)</h2><p>One of the core components of knowledge-based question answering is <em>passage retrieval</em>. Given a question, passage retrieval selects from a large database candidate passages that may be relevant to the question.</p><p>Traditional approaches to this problem include <a href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf">TF-IDF</a> and <a href="https://en.wikipedia.org/wiki/Okapi_BM25">BM25</a>, which, at their core, are fairly simple models for assessing document similarity based on word frequencies in those documents. There are many variations and changes to the core models that make these work well, such as stemming, smoothing, stopword removal, etc.</p><p><a href="https://arxiv.org/pdf/2004.04906.pdf">Dense Passage Retrieval (DPR)</a> (April 2020), a neural-network based passage retriever, is Facebook's approach to this challenge. Although it's published as an independent paper, it seems like it's really part of the same effort as RAG – published less than two months apart with significant author overlap.</p><p>The DPR model consists of two parts: a passage encoder (fine-tuned based on BERT) and a question encoder (also fine-tuned based on BERT). Each encoder returns a dense vector representation of its input by selecting the output embedding of the [CLS] token. Given a question vector and a passage vector, the similarity between the two is the dot product of the two vectors.</p><p>A single training sample for this model consists of a question, a positive passage (the passage which has the answer to the question), and a set of negative passages (which are irrelevant to the question). The model is trained to maximize similarity of the question vector to the positive passage vector, and minimize similarity to negative passage vectors. (The loss is equivalent to using the passage similarities as logits and then using a softmax cross-entropy loss.)</p><p>Models trained like this are dependent on the quality of the negative passages chosen. If all the negative passages are just completely random unrelated passages, the task is too easy, and the model will learn shallow features based on word frequency, etc, and won't generalize well. In order to learn high-quality representations, the negative passages for a question must be a mix of arbitrary unrelated passages and passages that are <em>close</em> to the positive but are still wrong; the latter of these two are called "hard negatives". DPR is trained with a single hard negative per question, which is sourced by running a BM25 passage retriever and choosing one of its retrieved candidates.</p><p>During inference, passage embeddings are generated by the passage encoder and cached. To retrieve passages related to a question, the question is encoded with the question encoder, and then a fast similarity search algorithm (<a href="https://engineering.fb.com/data-infrastructure/faiss-a-library-for-efficient-similarity-search/">FAISS</a>) is used to find the top $k$ cached passage encodings with maximal similarity to the question encoding.</p><p>In the end, this neural passage retriever works better on most datasets than a Lucene-based TF-IDF or BM25 retriever. Given the recent experience in NLP, this isn't too surprising.</p><p>In the DPR paper, not only do they implement their passage retriever, but they also implement an extractive question answerer. This is an alternative to RAG, which generates the answer with a seq2seq model, instead of selecting a span in the supporting documents. As we'll see below, RAG, in some sense, is just DPR with a slightly more advanced answer-generating model.</p><h2 id="bart-bert-for-seq2seq-models">BART: BERT for Seq2Seq Models</h2><p><a href="https://arxiv.org/pdf/1910.13461.pdf">BART</a> (Oct 2019) is a model from Facebook that attempts to answer the question: How do you do BERT, but for seq2seq models?</p><p>BERT is (effectively) a denoising autoencoder for text, replacing noised [MASK] tokens with the original tokens. BART is a denoising autoencoder as well, but one where the noise function can alter the sequence length, and thus it uses a seq2seq transformer instead of BERT's vanilla feedforward transformer. In some sense, BART is an extension of BERT, since it allows for a strictly more powerful noise function than BERT.</p><p>The biggest question in all of this is, what noise function do you use in this setup that yields a useful pretrained model?</p><p>In this paper, the following noise functions are evaluated:</p><ul><li><strong>Token Masking</strong>: Replace tokens with [MASK] (as in BERT).</li><li><strong>Token Deletion</strong>: Delete tokens. Do not replace them with [MASK].</li><li><strong>Token Infilling</strong>: Replace a span of text (length 0 upward) with [MASK]. (Thus, [MASK] isn't guaranteed to be a single token.)</li><li><strong>Sentence Permutation</strong>: Shuffle sentences, delineated by periods. (Not ultimately helpful.)</li><li><strong>Document Rotation</strong>: "A token is chosen uniformly at random, and the document is rotated so that it begins with that token. This task trains the model to identify the start of the document." (Not ultimately helpful.)</li></ul><p>The encoder encodes a noised input, and the decoder (autoregressively) predicts the original input.</p><p>On first glance, this model felt weird to me. The additional noise function flexibility is obviously a positive, but using a seq2seq model to predict the output feels like overkill. However, the decoder effectively learns a smarter copy function, one which alternates between text copying and text generation appropriately.</p><p>The pretrained model can be fine-tuned for a variety of tasks, including sequence classification (using final timestep decoder state as output layer), token classification (using last layer decoder state for each token as outputs), and sequence generation (using the full model). Machine translation into English is also tried by re-initializing encoder token embeddings (and keeping the rest of the model).</p><p>Interpreting the results here is hard. Document rotation and sentence shuffling do not improve performance, which is unsurprising, given that they resemble next sentence prediction (NSP) in BERT, a loss which has been shown to <a href="https://arxiv.org/pdf/1906.08237.pdf">be unnecessary or even harmful</a>. Text infilling seems to be superior to other noise functions, which isn't too surprising -- it's strictly more general than masking or deletion. BART mostly does well on all the tested tasks, except for one, which seems to be an outlier, as it is best handled by a straight language model. BART isn't any better than SotA (state-of-the-art) on SQuAD and GLUE, but isn't any worse either. BART works better for summarization than other approaches, likely since summarization is a seq2seq task with a lot of copying in it.</p><p>All in all, it's a valuable data point, but I don't see this approach becoming popular outside of Facebook, possibly with the exception of summarization. The performance isn't generally superior, and there are too many details to interpret in this paper; it's hard to tell a single cohesive story about this paper. Regardless, it's one of the building blocks of RAG, the paper that initiated this blog post.</p><h2 id="blink-retrieval-augmented-entity-linking">BLINK: Retrieval-Augmented Entity Linking</h2><p><a href="https://arxiv.org/pdf/1911.03814.pdf">BLINK</a> (September 2020) is a recent Facebook model for entity linking. Entity linking is the task or process of connecting a short span of text (a "mention") to an entity, an object in some sort of database with an associated description. Entity linking is inherently knowledge-based, since there can be millions of candidate entities. In some sense, question answering with a database (RAG) and entity linking are very similar tasks, with the caveat that entity linking is guaranteed to only link to a single entity, whereas knowledge-assisted question answering may require multiple supporting sources.</p><p>More specifically, BLINK is a zero-shot entity linker, making it even more similar to knowledge-assisted QA. Zero-shot, in this case, means that the entities are not part of the model, so the set of entities can be different during inference than during training. You won't find any learned entity embeddings in this paper, but you will find an entity encoder, so adding an entity just corresponds to running its description through the entity encoder. (In fact, to evaluate this fairly, the entity set used in training is disjoint from the test entity set.)</p><p>BLINK operates in two phases for performance reasons. The first phase chooses a set of candidate entities for each mention. The second phase links precisely one of those candidates to the mention. Since the first phase needs to consider millions of entities, it must be incredibly fast, while the second phase can involve more computation for each entity-mention candidate.</p><p>Model-wise, BLINK is more or less what you would expect. Phase one of BLINK is more-or-less identical to DPR (see above), with the difference that the hard negatives are sourced by running BLINK itself (rather than BM25, as in DPR).</p><p>Phase two of BLINK is yet another transformer (initialized with BERT), this time taking both entities (titles and descriptions) and mentions (along with context) at the same time and outputting a single vector by using the [CLS] output embedding. The output vector for each pair is reduced to a single logit with a fully-connected layer, and these logits are used with a softmax loss (with the target being the correct candidate entity). The candidates are generated for each mention by phase one, which means that any time phase one is retrained, phase two must also be retrained; the training distribution for phase two depends on the phase one performance.</p><p>As with DPR, selecting the top $k$ candidates in phase one is done by fast approximate nearest neighbor search (<a href="https://engineering.fb.com/data-infrastructure/faiss-a-library-for-efficient-similarity-search/">FAISS</a>). A hyperparameter sweep suggests $k=10$ is optimal, and searching through 5.9M entities takes just 2ms at inference time.</p><p>To summarize the results: apply this at scale (5.9M entities from wikipedia), and it works great. As usual, train on a large dataset, fine-tune on your smaller dataset. As often lately in NLP, simple model designs and scale dominate the benchmarks.</p><h2 id="retrieval-augmented-generation-rag-">Retrieval Augmented Generation (RAG)</h2><p>Now, finally, onto the paper that spawned this blog post.</p><p><a href="https://arxiv.org/pdf/2005.11401.pdf">Retrieval-Augmented Generation (RAG)</a> is a question answering model. It's roughly what you would get if you took DPR and then used your retrieved passages (along with your question) as input to a seq2seq model (pretrained via BART), which was trained to generate your answer.</p><p>If you have a passage retriever, you could take its outputs and then feed them as inputs to your seq2seq model, trained to generate the answer to your questions. However, this means that your two models need to be trained in sequence, and that your second model depends on your trained first models. Pipelines like this are harder operationally and generally more brittle – so instead, RAG opts to train this system end-to-end.</p><p>This is the key question for RAG, as I see it: <em>How do you jointly train a passage retriever and a seq2seq answer generator?</em></p><p>To train this model end-to-end, you cannot simply choose and use the top passage from DPR. RAG, instead, <em>marginalizes</em> over the top-$k$ passages, and does so in two different ways. This bit is crucial, so I'm going to just screenshot the relevant passage in <a href="https://arxiv.org/pdf/2005.11401.pdf">the paper</a>:</p><figure class="kg-card kg-image-card"><img src="https://andrew.gibiansky.com/content/images/2020/10/image.png" class="kg-image" alt="Facebook's Knowledge-Assisted NLP"></figure><p>In both of these models, we sum over the probabilities given the different top-$k$ passages, weighted by the probability assigned to each passage by the passage retriever. In sequence-level marginalization, we compute the probability of the target sequence conditional on the chosen passage (for the entire sequence), and then take the weighted average of those probabilities. In token-level marginalization, we compute the probability of the sequence as the product of the probabilities of the tokens, where each token probability is the weighted average of the token probabilities of the model conditioned upon different retrieved passages.</p><p>Decoding from these models must be done in different ways. When using token-level marginalization, decoding is easy since we can compute token probabilities (marginalized over passages); we can use a simple beam search. When using sequence-level marginalization, we cannot use a single beam search. Instead, we do $k$ separate beam searches and take the set of all their final candidates. We then evaluate all candidates likelihoods under all possible conditioning passages and score each candidate based on the weighted sum of those likelihoods. (Unfortunately, this is much slower, since each candidate must be evaluated with each possible conditioning passages; for $n$ candidates per search, you might have to do $k^2n$ evaluations, since each of $k$ passages might generate $n$ candidates which each must be evaluated under all $k$ conditionings.)</p><p>Much of the paper focuses on evaluating the created model and decoding schemes, and in general, the results are good. They show that substituting the knowledge base (by using a Wikipedia snapshot from a different year) significantly changes the answers, which is important as it demonstrates that the passages are being used effectively. There's no clear conclusion as to whether a token-level or sequence-level marginalization is preferable in general; it depends somewhat on the task. Questions that require using multiple sources (such as Jeopardy) are easier for token-level marginalization, but if the answer is generally contained in one passage, the results are less clear.</p><p>It's worth noting that even though this QA system seems state-of-the-art on many metrics, it still only achieves 50% accuracy (give or take) in human evaluations of its answers to Jeopardy questions. So we're pretty far from a simple end-to-end system which can reliably synthesize cohesive responses to any Jeopardy question when using Wikipedia as a database.</p><h2 id="knowledge-intensive-language-tasks-kilt-">Knowledge Intensive Language Tasks (KILT)</h2><p>Machine learning research is driven not only by model development, but also by a variety of other factors, such as hardware developments, datasets, and metrics. In NLP, there exist several commonly-used benchmarks for assessing model quality, such as SQuAD (for question answering) and GLUE (for general language understanding). These benchmarks are crucial for measuring the quality of models and the progress of the field as a whole. Additionally, benchmarks are key to every researchers dream – claiming state-of-the-art performance on their task of choice.</p><p><a href="https://arxiv.org/pdf/2009.02252.pdf">Knowledge Intensive Language Tasks (KILT)</a> (September 2020) is a new benchmark from Facebook to assess progress in NLP for areas that require access to a large database of factual information. KILT is based on a snapshot of Wikipedia with five key tasks: fact checking, entity linking, slot filling, question answering, and dialogue.</p><p>Facebook's desire for a new benchmark is easily understandable, given all the work described above. On one hand, a benchmark is necessary for them to evaluate their own models and measure modeling progress, and given that a benchmark is necessary, it might as well be public and have an associated publication. On the other hand, this benchmark is practically explicitly created for Facebook's models to do well on – so it should come as no surprise that the best-performing models in the KILT paper are Facebook's BLINK, DPR, BART, and RAG. The paper ends up being half benchmark and half showing off the quality of the aforementioned models.</p><p>Even though the benchmark seems tailored to Facebook's prior work, it nonetheless seems like a very useful addition. From the best performance, it's clear that there is probably still room to improve, with maximum accuracy across all the tasks peaking at about 80%. We can't know for sure, as the benchmark doesn't include a human evaluation – it's possible that human performance would be no higher than the current models (although I doubt that's the case). The release also includes <a href="https://github.com/facebookresearch/KILT">a library</a>, so that future papers can evaluate their models on KILT using the same data and evaluation criteria.</p><p>It's too early to tell if this ends up being useful for the field. In the month since publication, there have been no citations, and the Github project is moderately quiet with 12 commits and 3 (closed) issues. However, it's only been a month, and the benchmark is also available through HuggingFace's library, which receives quite a bit of use and may drive more adoption. We'll see in 3-6 months whether this benchmark gets any uptake.</p><h2 id="summary">Summary</h2><p>In the past year, folks at Facebook have done a ton of good work on knowledge-aided NLP. Almost all of the work is based on taking snapshots of Wikipedia, chunking it up into small BERT-sized passages, and then using BERT-based encoders and dot-product similarity to look up passages relevant to various target tasks. There's clearly room for improvement on all fronts, but explicitly incorporating knowledge databases into neural NLP seems like a great direction, and the results generally support that. </p><h3 id="references">References</h3><ul><li><a href="https://arxiv.org/pdf/2005.11401.pdf">Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks</a></li><li><a href="https://arxiv.org/pdf/2009.02252.pdf">KILT: a Benchmark for Knowledge Intensive Language Tasks</a></li><li><a href="https://arxiv.org/pdf/2004.04906.pdf">Dense Passage Retrieval for Open-Domain Question Answering</a></li><li><a href="https://arxiv.org/pdf/1911.03814.pdf">Scalable Zero-Shot Entity Linking with Dense Entity Retrieval</a></li><li><a href="https://arxiv.org/pdf/1910.13461.pdf">BART: Denoising Sequence-to-Sequence Pretraining for Natural Language Generation, Translation, and Comprehension</a></li><li><a href="https://arxiv.org/pdf/1906.08237.pdf">XLNet: Generalized Autoregressive Pretraining for Language Understanding</a></li></ul>]]></content:encoded></item><item><title><![CDATA[DiffWave and WaveGrad: Theory (Part 2)]]></title><description><![CDATA[In this post, I'll derive the equations for DiffWave and WaveGrad using diffusion probabilistic processes. ]]></description><link>https://andrew.gibiansky.com/diffwave-and-wavegrad-theory/</link><guid isPermaLink="false">5f6e82ab6cc48f0006cc9dd9</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Mon, 28 Sep 2020 03:14:36 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1565556601977-f2241a07ab32?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1565556601977-f2241a07ab32?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="DiffWave and WaveGrad: Theory (Part 2)"><p><em>This is Part 2 of a blog post about DiffWave and WaveGrad. If you haven't, read <a href="https://andrew.gibiansky.com/diffwave-and-wavegrad-overview/">Part 1</a>! </em></p><p>In this post, I'll derive the equations for DiffWave and WaveGrad using diffusion probabilistic processes. As far as I can tell, there's no cohesive and simple explanation for this in any of the referenced papers, so I hope this is accurate and useful.</p><p>A diffusion probabilistic process (in this context) consists of the following:</p><ul><li>$x_0$: A random variable representing your data distribution.</li><li>$x_1$, $x_2$, ..., $x_T$: A sequence of random variables which gradually add noise, starting from $x_T$.</li><li>$q(x_t | x_{t-1})$: A Gaussian distribution (also called the forward diffusion process) describing the process.</li><li>$p_\theta(x_{t - 1} | x_t)$: A parameterized Gaussian (also called the reverse process) which attempts to "undo" the forward process.</li></ul><h2 id="forward-process">Forward Process</h2><p>Let's assume that there are $T$ steps of corruption with noise variances $\beta_1$ through $\beta_2$. Then, each step corresponds to random variable $y_i$:</p><p>$$\begin{align*}x_1 &amp;= \sqrt{1 - \beta_1} x_0 + \sqrt{\beta_1} \epsilon_1 \\ x_2 &amp;= \sqrt{1 - \beta_2} x_1 + \sqrt{\beta_2} \epsilon_2 \\ \vdots \\ x_T &amp;= \sqrt{1 - \beta_T} y_{T-1} + \sqrt{\beta_T}\epsilon_T \end{align*}$$</p><p>Since these are all linear combinations of $x_0$ and $\epsilon$ noise, we can write any step in a closed form:</p><p>$$x_t = \left(\prod_{i=1}^t\sqrt{1 - \beta_i}\right) x_0  + \sqrt{\left(1 - \prod_{i=1}^t 1 - \beta_i\right)}\epsilon$$</p><p>The factor on $x_0$ is intuitive (every step adds a multiplicative factor), but the factor on $\epsilon$ is not, and requires proof by induction to verify. To match the standard notation, define $\alpha_n$ and $bar \alpha_n$ as</p><p>$$\alpha_n = 1 - \beta_n \\ \bar \alpha_n = \prod_{i=1}^n \alpha_n,$$</p><p>at which point the above equation defines the forward process distribution</p><p>$$q(x_t|x_0) = N(\sqrt{\bar \alpha_t}x_0, (1 - \bar \alpha_t)I).$$</p><p>We'll need this closed form solution later! </p><h2 id="reverse-process">Reverse Process </h2><p>The reverse process $p_\theta(x_{t - 1} | x_t)$ is a Gaussian parameterized by a learned $\theta$:</p><p>$$p_\theta(x_{t-1} | x_t) = N(\mu_\theta(x_t, t), {\sigma_\theta(x_t, t)}^2 I).$$</p><p>We are free to choose the representation $\mu_\theta$ and $\sigma_\theta$. The training process will find parameters $\theta$ which best reverse the effects of the diffusion process, but the quality of this reversal (and thus synthesis quality) will depend on our choices for $\mu_\theta$ and $\sigma_\theta$.</p><h3 id="inference">Inference</h3><p>To run inference, we will additionally need some latent prior, $p(x_T)$, which we can easily sample from. Once $p_\theta$ is trained, we can run inference on our model by sampling from $p(x_T)$ and then iteratively sampling $x_{T-1}$, $x_{T-2}$, and so on, until we get our results $x_0$. Sampling $x_{t-1}$ given $x_t$ and $p_\theta$ is straight-forward – run $\mu_\theta$ and $\sigma_\theta$ to get a mean and variance for your Gaussian for $x_{t-1}$ and then sample from it.</p><p>In order for this to work, the latent prior $p(x_T)$ must be sufficiently close to $q(x_T | x_0)$. (The KL-divergence $KL(q(x_T | x_0) || p(x_T))$ must be low.) We're going to use a zero-mean unit-variance prior $p(x_T) = N(0, I)$, which means that we need enough diffusion steps $T$ and large enough diffusion variances $\beta_t$ that the final distribution $q(x_T | x_0)$ resembles white noise. WaveGrad gets $T$ down to as few as 6 iterations by carefully tuning $\beta_t$, but most of the papers that use these methods have dozens or hundreds of iterations for this reason.</p><h3 id="training">Training</h3><p>We want to train $p_\theta(x_{t-1} | x_t)$ to most accurately sample $x_{t-1}$ given $x_t$. To do this, we would (ideally) like to minimize the KL-divergence to the forward process posterior $q(x_{t-1} | x_t)$:</p><p>$$\min_\theta J(\theta) = KL\left(q(x_{t-1} | x_t) \; ||  \; p_\theta(x_{t-1} | x_t)\right)$$</p><p>The forward process posterior is related to the forward process distribution via Bayes rule:</p><p>$$q(x_{t-1} | x_t) = \frac{q(x_t | x_{t-1}) q(x_{t-1})}{q(x_t)}$$</p><p>This distribution cannot be computed, as do not have access to $q(x_t)$ or $q(x_{t-1}).$ However, we <em>do</em>, <strong>if we condition upon $x_0$:</strong></p><p>$$q(x_{t-1} | x_t, x_0) = \frac{q(x_t | x_{t-1}, x_0) q(x_{t-1} | x_0)}{q(x_t | x_0)}$$</p><p>Recall (from above) the closed form equation</p><p>$$q(x_t|x_0) = N(\sqrt{\bar \alpha_t}x_0, (1 - \bar \alpha_t)I).$$</p><p>We now have closed form expressions for Gaussians $p_\theta(x_{t-1} | x_t)$ and $q(x_{t-1} | x_t),$ and the KL-divergence between two Gaussians can be computed analytically, which means that we can minimize this loss.</p><p>From <a href="https://en.wikipedia.org/wiki/Normal_distribution">Wikipedia</a>,</p><p>$$KL(N_0 || N_1) = \frac{1}{2} \left( \frac{{\sigma_0}^2 + (\mu_1 - \mu_0)^2}{{\sigma_1}^2} - 1 + 2 \log \frac{\sigma_1}{\sigma_0} \right)$$</p><p>The two normal distributions in our case are</p><p>$$\begin{align*}N_0 &amp;= q(x_{t-1} | x_t, x_0) = \frac{q(x_t | x_{t-1}, x_0) q(x_{t-1} | x_0)}{q(x_t | x_0)}. \\ N_1 &amp;= p_\theta(x_{t-1} | x_t) = N(\mu_\theta(x_t, t), {\sigma_\theta(x_t, t)}^2 I). \end{align*}$$</p><p>The algebra for simplifying $N_0$ gets gnarly. You start by explicitly writing the PDFs for the distributions involved:</p><p>$$q(x_t | x_{t-1}, x_0)=N(\sqrt{1 - \beta_t}x_{t-1}, \beta_t) = \frac{1}{\sqrt{2\pi\beta_t}}\exp\left(-\frac{(x_t - \sqrt{1 - \beta_t}x_{t-1})^2}{2\beta_t}\right) \\ q(x_{t-1} | x_0) = N(\sqrt{\bar \alpha_{t-1}}x_0, (1 - \bar \alpha_{t-1})I) = \frac{1}{\sqrt{2\pi(1-\bar\alpha_{t-1})}}\exp\left(-\frac{(x_{t-1} - \sqrt{\bar\alpha_{t-1}} x_0)^2}{2(1-\bar\alpha_{t-1})}\right)\\ q(x_t | x_0) = N(\sqrt{\bar \alpha_t}x_0, (1 - \bar \alpha_t)I) = \frac{1}{\sqrt{2\pi(1-\bar\alpha_t)}}\exp\left(-\frac{(x_t - \sqrt{\bar\alpha_t} x_0)^2}{2(1-\bar\alpha_t)}\right)$$</p><p>Next, substitute these into the forward process posterior conditioned upon $x_0$. After a lot of painful simplification, you get a PDF that corresponds to the following normal distribution:</p><p>$$ \frac{q(x_t | x_{t-1}, x_0) q(x_{t-1} | x_0)}{q(x_t | x_0)} = N(\tilde \mu(x_t, x_0), \tilde\beta_t) \\ \tilde\beta_t = \frac{1-\bar\alpha_{t-1}}{1 - \bar\alpha_t}\beta_t\; ; \; \tilde\mu(x_t, x_0) = \frac{\sqrt{\bar\alpha_{t-1}}\beta_t}{1 - \bar\alpha_t}x_0 + \frac{\sqrt{\alpha_t(1-\bar\alpha_{t-1})}}{1 - \bar\alpha_t}x_t$$</p><p>Now, finally, we can substitute this and our reverse process Gaussians into the closed form for KL-divergence between two Gaussians and get our loss function. Once again, writing out the algebra is somewhat gnarly, but if you do so, and you fix the variance to $\beta_t$ (instead of learning it), you arrive in the end at Eq. 10 in <a href="https://arxiv.org/pdf/2006.11239.pdf">this paper</a>. From here on out, the reasoning is straightforward. Your network must learn $\mu_\theta$ to be</p><p>$$\mu_\theta(x_t) = \frac{1}{\sqrt{\alpha_t}} \left(x_t - \frac{\beta_t}{\sqrt{1 - \bar\alpha_t}}\epsilon\right).$$</p><p>Since we have access to $x_t$ itself, we can have the network directly predict $\epsilon$. Theoretically, it doesn't really matter if the network is trained to predict $\mu$ or $\epsilon$ or even $x_0$, but some configurations may be worse in practice.</p><h3 id="conclusion">Conclusion </h3><p>In the first post, I described the overall structure of the DiffWave and WaveGrad models, but didn't explain how to choose the parameters $c_1$, $c_2$, and $\sigma$ in the model. In this post, we derived values for those parameters – the coefficients in the equation above for $\mu_\theta$. The derivation requires quite a bit of nasty algebra, which explains why it is not presented in full (unfortunately) in any of the linked papers. According to the authors, using these precise values may be important for best performance, which if true would be a rare case in deep learning of theory driving practice.</p><h3 id="references">References</h3><ul><li><a href="https://arxiv.org/pdf/2009.00713.pdf">WaveGrad: Estimating Gradients for Waveform Generation</a></li><li><a href="https://arxiv.org/pdf/2009.09761.pdf">DiffWave: A Versatile Diffusion Model for Audio Synthesis</a></li><li><a href="https://arxiv.org/pdf/2006.09011.pdf">Improved Techniques for Training Score-Based Generative Models</a></li><li><a href="https://arxiv.org/pdf/2006.11239.pdf">Denoising Diffusion Probabilistic Models</a></li><li><a href="https://arxiv.org/pdf/1503.03585.pdf">Deep Unsupervised Learning using Nonequilibrium Thermodynamics</a></li></ul>]]></content:encoded></item><item><title><![CDATA[DiffWave and WaveGrad: Overview (Part 1)]]></title><description><![CDATA[DiffWave and WaveGrad propose a new neural vocoder model based on diffusion probabilistic processes, with several nice properties and a solid theoretical justification.]]></description><link>https://andrew.gibiansky.com/diffwave-and-wavegrad-overview/</link><guid isPermaLink="false">5f6cd6046cc48f0006cc9ba7</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Mon, 28 Sep 2020 03:14:12 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1565562591245-00f5d7c4bf57?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1565562591245-00f5d7c4bf57?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="DiffWave and WaveGrad: Overview (Part 1)"><p><em>This is the first part of a two part blog post. If you've read this, move on to <a href="https://andrew.gibiansky.com/diffwave-and-wavegrad-theory/">Part 2</a>!</em></p><p>Two recent papers, <a href="https://arxiv.org/pdf/2009.09761.pdf">DiffWave</a> (NVidia) and <a href="https://arxiv.org/pdf/2009.00713.pdf">WaveGrad</a> (Google), propose a new neural vocoder model based on diffusion probabilistic processes. These vocoders have several nice properties – they achieve high quality synthesis, are non-autoregressive, are easy to train (no adversarial losses), do extremely well on unconditional audio synthesis, and are likely fast enough for eventual production deployment on GPUs.</p><p>These models are conceptually simple, but come with a fairly hard-to-parse theoretical justification. In this blog post, I'd like to go over how these models work at a high level, and then dive in to the theoretical justification for them.</p><h3 id="training">Training</h3><p>The model itself is a neural denoising autoencoder.</p><p>To train it, start with a clean audio signal \(x\) and a sample of white noise \(\epsilon \sim N(0, I)\). Then, create a corrupted audio signal $\tilde x$ by scaling the noise to have variance \(\sigma^2\) and adding it to the clean audio:</p><p>$$\tilde x = \sqrt{1 - \sigma^2} x + \sigma \epsilon$$</p><p>If we scale $x$ by $\sqrt{1 - \sigma^2}$, the variance of $\tilde x$ is unchanged from the original audio, if your input is unit variance.</p><p>Then, train a neural network $f_\theta$ to predict the noise that was added with an L2 loss:</p><p>$$J(\theta) = \left(f_\theta(\tilde x, \sigma) - \epsilon\right)^2$$</p><p>The network $f_\theta$ is conditioned on the noise magnitude $\sigma$. (WaveGrad uses an L1 loss here, finding that it "offers better training stability".) This conditioning is important, as we will use different values of $\sigma$ throughout training. In a conditional synthesis setting, $f_\theta$ is also conditioned upon any input features such as linguistic information or mel spectrograms (but I won't write that explicitly here).</p><h3 id="inference">Inference</h3><p>To use this model for inference, we will perform $T$ steps of denoising with sampling. First, we sample a starting point $y_T$ of white noise. Then, each step of denoising is computed with:</p><p>$$y_{t-1} = c_1 y_t - c_2 f_\theta(y_t) + \sigma_t z,$$</p><p>where $z \sim N(0, I)$. In essence, each step reduces the magnitude of the signal (scaling by $c_1$), removes some noise (subtracting $f_\theta(y_t)$), and samples some noise to add to the signal of variance $\sigma^2$ (with gradually decreasing $\sigma^2$ and $\sigma_1 = 0$). </p><p>The result, $y_0$, is the final audio.</p><p>Choosing values for $c_1$, $c_2$, and $\sigma_t$ depends on the theoretical justification for this model. In the WaveGrad paper, Section 2 describes an interpretation based on sampling via Langevin dynamics, in which case $c_1 = 1$, $c_2 = \frac{{\sigma_t}^2}{2}$, and $\sigma_t$ is any sequence of magnitudes that is sufficiently long and gradually decreasing.</p><p>However, both WaveGrad and DiffWave choose these values based instead on the interpretation of diffusion probabilistic processes, and according to one of the authors of DiffWave, the <a href="https://twitter.com/ZhifengKong/status/1309355587776307203">exact values are important for high-quality synthesis</a>.</p><p>In <a href="https://andrew.gibiansky.com/diffwave-and-wavegrad-theory/">Part 2</a>, I'll dig into the somewhat gnarly math required to justify all of this, ignoring the Langevin dynamics interpretation entirely.</p><h3 id="references">References</h3><ul><li><a href="https://arxiv.org/pdf/2009.00713.pdf">WaveGrad: Estimating Gradients for Waveform Generation</a></li><li><a href="https://arxiv.org/pdf/2009.09761.pdf">DiffWave: A Versatile Diffusion Model for Audio Synthesis</a></li><li><a href="https://arxiv.org/pdf/2006.09011.pdf">Improved Techniques for Training Score-Based Generative Models</a></li><li><a href="https://arxiv.org/pdf/2006.11239.pdf">Denoising Diffusion Probabilistic Models</a></li><li><a href="https://arxiv.org/pdf/1503.03585.pdf">Deep Unsupervised Learning using Nonequilibrium Thermodynamics</a></li></ul>]]></content:encoded></item><item><title><![CDATA[WaveNet and Tacotron aren't TTS systems]]></title><description><![CDATA[Deep learning models for speech synthesis, such as Google's WaveNet and Tacotron, are not complete text-to-speech systems.]]></description><link>https://andrew.gibiansky.com/wavenet-and-tacotron-arent-tts-systems/</link><guid isPermaLink="false">5e8a82bc6cc48f0006cc9a7c</guid><dc:creator><![CDATA[Andrew Gibiansky]]></dc:creator><pubDate>Mon, 06 Apr 2020 02:09:45 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1552785903-9301946db8c1?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1552785903-9301946db8c1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="WaveNet and Tacotron aren't TTS systems"><p><em><strong>Summary: </strong>Deep learning models for speech synthesis, such as Google's WaveNet and Tacotron, are not complete text-to-speech systems. They are each just one part of a large pipeline of models and heuristics that together form a text-to-speech engine. WaveNet is not a text-to-speech engine. Tacotron isn't either. </em></p><p>In the past few years, researchers have designed many neural network architectures for synthesizing audio. The most commonly referenced ones are Google's WaveNet and Tacotron, but there are many, many others, such as Google's WaveRNN, Baidu's Deep Voice papers, NVidia's WaveGlow, SampleRNN, Microsoft's FastSpeech, and many others. </p><p>These papers are announced to great fanfare on company websites and tech news sites, with great audio samples and interesting demos. This gives the impression that the paper describes a <em>complete</em> text-to-speech system. Google even brands its neural vocoder voices in Google Cloud as WaveNet voices, which can obscure the fact that a significant part of the TTS engine pipeline is shared between the two engines. As a result, a lot of people online end up (understandably!) confused and refer to a "WaveNet TTS system" or "Tacotron TTS system", or assume that a Github repo with a re-implementation of one of these can be used to build a complete speech synthesis engine.</p><p>This blog post is an attempt to rectify this (slight) misconception.</p><h2 id="what-is-a-text-to-speech-engine">What is a text-to-speech engine?</h2><p>A text-to-speech engine is a piece of software which converts text into speech (audio). This process is typically separated into a pipeline, where each step in the pipeline is its own model or set of models. An example pipeline might include:</p><ul><li><strong>Normalization: </strong>Converting non-spoken tokens (numbers, dates, etc) to spoken words, such as "1901" to "nineteen oh one" or "5/12" to "may twelfth". </li><li><strong>Part-of-Speech Tagging: </strong>Labeling words by their part of speech.</li><li><strong>Phoneme Conversion: </strong>Converting words to a phonetic representation, such as IPA.</li><li><strong>High-Level Audio Synthesis: </strong>Converting the phonemes into a high-level representation of audio, such as mel spectrograms, F0, spectral envelope, LSP or LPC coefficients, etc.</li><li><strong>Waveform Synthesis: </strong>Converting the high-level representation into a final audio waveform.</li></ul><p>This list does not include miscellaneous things such as networking, request parsing, audio encoding, etc. </p><p>A complete TTS engine has to do (more or less) <em>all</em> of these things and connect them all together.</p><h2 id="what-are-wavenet-and-tacotron">What are WaveNet and Tacotron?</h2><p>WaveNet and Tacotron are neural network models that address <em>one step</em> of the above pipeline. Specifically, WaveNet is a <em>neural vocoder</em>, and is responsible for the "waveform synthesis" step of the pipeline. Tacotron is a sequence-to-sequence model for spectrogram synthesis, and addresses the "high level audio synthesis" step.</p><p>Now that we have these distinctions, we can ask more specifically: What models have been developed for each stage of this pipeline? </p><ul><li><strong>Normalization: </strong>Normalization is tricky to do with machine learning approaches, but people are working on it. For example, <a href="https://arxiv.org/pdf/1712.06994.pdf">this</a> and <a href="https://arxiv.org/pdf/1611.00068.pdf">this</a>.</li><li><strong>Phoneme Conversion: </strong>Relative to the other problems on this list, this one isn't as hard, and so there's not as much research dedicated to it. You can find a few solid papers, such as <a href="https://arxiv.org/abs/1708.01464">this one</a> and <a href="https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43264.pdf">this one</a>.</li><li><strong>Audio Synthesis</strong>: In addition to <a href="https://arxiv.org/abs/1703.10135">Tacotron</a> (and <a href="https://ai.googleblog.com/2017/12/tacotron-2-generating-human-like-speech.html">Tacotron 2</a>), there's <a href="https://arxiv.org/abs/1710.07654">Deep Voice</a>, <a href="https://arxiv.org/abs/1905.09263">FastSpeech</a>, <a href="http://research.baidu.com/Blog/index-view?id=116">ParaNet</a>, and <a href="https://arxiv.org/abs/1809.08895">more</a>. </li><li><strong>Waveform Synthesis</strong>: In addition to <a href="https://arxiv.org/abs/1609.03499">WaveNet</a>, there's <a href="https://arxiv.org/abs/1811.00002">WaveGlow</a>, <a href="https://arxiv.org/abs/1802.08435">WaveRNN</a>, <a href="https://arxiv.org/abs/1807.07281">ClariNet</a>, <a href="https://arxiv.org/abs/1912.01219">WaveFlow</a>, <a href="https://arxiv.org/abs/1612.07837">SampleRNN</a>, and more.</li></ul><h3 id="disclaimer">Disclaimer</h3><p>I personally know the authors of many of the papers I listed above and worked on several of them myself. I'm not listing them in any order or trying to promote my own papers. These are just the systems that came to mind. Don't judge me for my choices here.</p><p>Additionally, I'm one of the founders of <a href="https://www.voicery.com/">Voicery</a>, where we build custom text-to-speech engines based on models similar to the ones above. If you have any questions or are looking to deploy your own models like these, check out our <a href="https://www.voicery.com/demos">demos</a> and <a href="https://www.voicery.com/#contact-us">get in touch</a>.</p>]]></content:encoded></item></channel></rss>