对数据进行建模、拟合,我的经验是 当拿到一份数据时,先把它画出来,获得一些直观感受, 然后尝试从某个角度或者多个角度来理解这份数据, 按照对数据的理解整理成为数学模型, 最后就可以使用工具来解出这个模型。

R 的数值计算能力很强, 在 R 中可以十分方便地对数据进行建模、拟合。

比如我在做 aac-lc 音频解码程序时需要输入音频采样率数据, 通常情况下音频采样率数据是一个从几千Hz到几十万Hz之间的一个数。 但是在 aac-lc 音频编码中,常见的采样率只有几种(C语言表示):

int r[] = {
    96000, 88200, 64000, 48000, 44100, 32000,
    24000, 22050, 16000, 12000, 11025, 8000
};

从而一个自然的想法就是把它定义为一个枚举,也就是把它们转换成另一串整数:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11

即用 0-11 中的每个数来表示原来的一个采样率,例如用 4 来表示 44100 (即 r[4] 的值为 44100)。 那么就多了一个任务,就是把原来采样率翻译成枚举值,也就是要从这组数中找 出与采样率相同的那个数,它的索引值就是这个枚举值,如果没有找到就报错。 对应的 C 代码简单如下:

#define NUM_OF_ELEMENT(array) (sizeof(array)/sizeof(array[0]))

int get_rate_index(int rate)
{
    static int r[] = {
        96000, 88200, 64000, 48000, 44100, 32000,
        24000, 22050, 16000, 12000, 11025, 8000
    };
    int i;
    for (i = 0; i < NUM_OF_ELEMENT(r); i++)
    {
        if (r[i] == rate)
        {
            return i;
        }
    }
    return -1; // 没有找到对应的值
}

其实这里可以把索引值看成是关于采样率的函数,如果能找到函数解析式, 很有可能通过这个解析式计算出来比通过如上的方法查找出来要快。 想要找这个函数的解析式, R 可以帮上忙。先画上图来观察一下。

> r <- c( 96000, 88200, 64000, 48000, 44100, 32000,
+         24000, 22050, 16000, 12000, 11025, 8000)
> idx <- 0:11
> plot(idx ~ r, type='b')

r-vector-srates* aac-lc采样率 *

在 R 中最简单的模型是线性模型,对于一个向量的线性模型就是一条直线。 可以使用 lm() 快速得到直线的拟合结果:

> f1 <- lm(idx ~ r)
> f1

Call:
lm(formula = idx ~ r)

Coefficients:
(Intercept)            r
  9.8995347   -0.0001134

注意到表达式 idx ~ r,表示的是 idx 按变量 r 变化的一个线性模型,相当于 $ idx = a r + b $。 上面的结果告诉我们,如果把这个函数理解成是直线,那么通过 lm() 求得的直线为

\( idx = 9.8995347 - 0.0001134 * r \)

在图上画出这条直线的效果如下图:

> plot(idx ~ r, type='b')
> abline(f1, lty=2, col=2)

r-vector-sratesaac-lc采样率的直线拟合

从这张图上来看,这条直线和原始的点拟合得不好。看来要换一个模型了。 通过观察,感觉可以把这个函数设计为如下形式:

\( f(x) = \frac{a}{x + b} + c \)

也就是函数中包含了一个分式、一个常数项。 在 R 中使用 nls() 来拟合一个非线性模型。

> f2 <- nls(idx ~ a/(r + b) + c, start=list(a=1,b=1,c=1))
Error in nlsModel(formula, mf, start, wts) :
  singular gradient matrix at initial parameter estimates

当给定初始参数都为 1 的情况下,没有使模型快速收敛,R报出了错误信息。 这时需要对数据或模型进行改造,使得可以快速收敛。 改造的思路无外乎使数据的计算尽可能在-100到100之间, 初始参数选在一个实际存在的值上。

可以按第一个思路即把数据集变换到一个比较小的区间段,例如把 r 除以 10000 后再来拟合:

> x <- r / 10000
> f2 <- nls(idx ~ a/(x + b) + c, start=list(a=1,b=1,c=1))
> f2
Nonlinear regression model
  model: idx ~ a/(x + b) + c
   data: parent.frame()
     a      b      c
54.214  2.874 -3.918
 residual sum-of-squares: 1.004

Number of iterations to convergence: 8
Achieved convergence tolerance: 3.314e-06

这时拟合成功,残差为1.004,残差就是拟合值和实际值的差的平方和,这个值越小说明对结果拟合得越好。 在图上画出来:

> plot(idx ~ x, type='b')
> lines(x, predict(f2), lty=2, col=2)

r-vector-sratesaac-lc采样率的非线性拟合

从图上可以看出拟合得很好。因为索引值需要的是整数,试着计算一下:

> round(predict(f2))
 [1]  0  1  2  3  4  5  6  7  8  9 10 11

发现在已知的值点上都对得上,所以就可以使用这个模型来直接计算了:

#define NUM_OF_ELEMENT(array) (sizeof(array)/sizeof(array[0]))

/*
 * idx = a / (x + b) + c
 *  x  = r / 10000
 *      a      b      c
 * 54.214  2.874 -3.918
 */
int get_rate_index(int rate)
{
    static int r[] = {
        96000, 88200, 64000, 48000, 44100, 32000,
        24000, 22050, 16000, 12000, 11025, 8000
    };
    double x = rate / 10000.0
    int idx = round(54.214 / (x + 2.874) - 3.918);
    if (r[idx] == rate)
        return idx;
    else
        return -1;  // 没有找到对应的值
}