I want to understand how activation functions, specifically tanh and sigmoid, are used in int8 quantized neural networks. Even more specific, I want to understand a Look-up-Table based approach.

My problem is that the relation of data width and size of the LUT, scaling factor and zero point will affect which index of the LUT I have to use for the calculation. E.g. TFLite-Micro has an implementation of such a LUT for tanh and sigmoid. They have an implementation, but I do not understand how they calculate the indices. Is there some guidance on how to design such a function available somewhere? I haven't managed to find anything more specific.

  • $\begingroup$ Can you please reformulate the title as your specific question? $\endgroup$
    – nbro
    Jun 30, 2023 at 1:00

2 Answers 2


Their lookup-table is declared as uint16_t sigmoid_table_uint16[256]. At the bottom is a plot.

Input Scaling

Their Tanh() function gets 16bit inputs. Those are presumably the accumulated (and biased) results of several 8bit multiplications. So at this point there is no zero-point to consider any more, only a scaling factor.

(My understanding is that zero-point this is only relevant during the 8bit matrix-multiplication, but I'm not 100% sure to be honest. The results of the 8bit multiplications are of course 16bit, and you don't go back to 8bit until you have to.)

According to the code, the scaling is $\frac{\text{input_multiplier}}{2^{\text{input_left_shift}}}$. But the point is really to scale the input such that $2^{8+8}$ corresponds to a floating-point input of 10.7 (the chosen end of the table). We are in 32bit integer-land by now, and the input can be larger than $2^{8+8}$.


The number $2^{8+8}$ contains two engineering choices. First, the size of the look-up table. They have chosen 256 entries, so the top 8 bits will be the index. Second, the precision for linear interpolation (fraction part of the index). They have chosen 8 bits. An alternative would be zero, simply returning the nearest value from the table.

Now they divide by $2^8$ (the expression abs_input_data >> 8) to get the index, and use the remainder of the division (abs_input_data & 0xFF) for linear interpolation between the actual and the next value from the look-up table.


result = (ua << 8) + ut * (ub - ua);

Here they interpolate between ua and ub, which are adjacent 16bit values from the table, using ut (the 8-bit remainder).

To get a float sigmoid result (between 0.5 and 1.0) you'd have to divide by $2^{16+8}$.

Scaling back

If you check the plot below, the table has the right half of a 16bit unsigned sigmoid. But they want tanh. So they add back the sign and subtract the zero-point. The zero-point is at $2^{16+8-1}$ and written as 1<<(14 + 9) in their the code.

We still have to divide by $2^8$ from the interpolation, and shift one bit right to make room for the sign that we added, but we also have to divide by two to go from sigmoid to tanh. Those cancel out, and we divide by $2^8$ with rounding.

Rounding is done by adding 128 (or 1 << (9 - 2) in the code). But you also have to be concerned about the asymmetric range of signed integers, and whether C is rounding towards zero or towards negative infinity. Which is probably why they saturate at 0xFFFF00 instead of 0xFFFFFF but I'm not certain. With that, we're knee-deep in the engineering swamp and I've probably lost all the machine learning people, so I'll leave it at that.

TL;DR: Fixed-point math is not too hard to grasp, just impossible to read and validate. That's the price you pay to avoid costly conversions to/from floats.

tanh look-up table

  • $\begingroup$ Thank you very much! Why is net necessary to scale the input so that 2^(8+8) corresponds to the end of the LUT? Because that expression always matches the maximum value. Also, do you understand what the shifting of the result does? I understand it's fixed point, but I don't see where the exact values are derived from. $\endgroup$
    – Necrotos
    Jul 1, 2023 at 16:04
  • $\begingroup$ I've made a major edit which should answer both questions, I think. $\endgroup$
    – maxy
    Jul 1, 2023 at 20:57
  • $\begingroup$ Thanks! There are a few minor things I wonder after reading the update. Do you see any indication where the scaling factor for the input would come from? Also, you mention that they subtract the zero-point, but what zero point does this refer to? Finally, why do they add 128 to round? That number seems very random. Is it just an engineering choice, or is there some deeper meaning? $\endgroup$
    – Necrotos
    Jul 4, 2023 at 16:31
  • $\begingroup$ The first value in the table (see plot) is used for input 0. Because tanh(0)=0 this is the zero-point. They multiply it by 256 with (ua << 8) and later divide by 256 with an arithmetic right shift (line 108, result >>= 8). This division truncates the result (rounds towards -oo). If you add 128 first, you get normal rounding. $\endgroup$
    – maxy
    Jul 4, 2023 at 18:17
  • $\begingroup$ For the scaling factor, you'd have to read the rest of their code. I've only read the part that interested me personally, which was the LUT. $\endgroup$
    – maxy
    Jul 4, 2023 at 18:22

Please refer to following document link, we convert any Q15 integer to Q4.12 fixed point format with help of input scaling with factor of 3/4. Q4.12 {3 bits - Integer, 12 bits of fraction}. Then ua uses upper 8 bits {Q4(INT).4(precision)} while ut uses lower fixed point precision.

Link - https://github.com/tensorflow/tflite-micro/pull/1253

Regards, Chaitanya

  • $\begingroup$ Thanks! But I don't understand what scaling with the factor 3/4 does, or where in the code it is applied. Could you expand on that? $\endgroup$
    – Necrotos
    Sep 27, 2023 at 19:55

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .