教程名称:使用 C# 入门深度学习

作者:痴者工良

地址:

https://torch.whuanle.cn

微积分

由于学习微积分需要一定的基础知识,但是由于本教程不是数学书,所以不能一一详细介绍基础知识,读者需要自行了解学习初等函数、三角函数等基础知识。

极限

极限的符号是
\(\lim\)
,在高等数学中,主要是数列极限和函数极限,限于篇幅,本文只讨论函数存在极限时的一些情况。

数学上有正无穷大(
\(+\infty\)
)和负无穷大( $ -\infty $ )的概念,大家都知道无穷大的意思,但是比较容易理解错无穷小、负无穷大,
无穷小指的是无限接近 0,而不是负数的无穷大。


举个例子你就明白了,当 $ x \to + \infty$ 时,
\(\frac{1}{x}\)
的值,我们都知道 x 越大,
\(\frac{1}{x}\)
越小,但是不可能为 0,只能越来越接近于 0。


求解极限,一般会碰到这几种情况,当 x 无穷大时,y 是多少。

例如下图所示,当 x 无穷大时,y 逐渐贴近 x 轴,即 y 越来越接近,我们使用
\(y\to 0\)
表示趋近于 0 或者说接近 0。

image-20241110092447001

图片来自《高等数学上册》第一章第三节函数极限的定义与计算,同济大学数学系编著。


所以:

\[\lim_{x \to \infty} f(x) = \lim_{x \to \infty} \frac{1}{x} = 0
\]

使用
C#
表示时,我们使用一个极大的数表示无穷大。

var x = torch.tensor(double.MaxValue);
var y = 1 / x;

var lim = (int)y.item<double>();
Console.WriteLine(lim);


上面使用了
y.item<double>()
将张量转换为标量,我们也可以使用函数
y.ToScalar().ToInt32();
转换。


再比如下图所示,当 x 无穷大时,y 越来越接近
\(\frac{\pi}{2}\)
,所以 :

\[\lim_{x \to \infty} \arctan x = \frac{\pi}{2}
\]

image-20241110092718306

图片来自《高等数学上册》第一章第三节函数极限的定义与计算,同济大学数学系编著。


上面求极限时,是当 $\lim_{x \to \infty} $ 或 $\lim_{x \to 0} $ 时的情况,在实际中更多的是给出某点,求其极限,例如:

\[\lim_{x \to x_{0}} f(x) = \lim_{x \to x_{0}} \frac{1}{x}
\]



\(x=1\)
时,我们直接计算其实可以得到
y=1
,极限就是 1,或者换句话来说,我们求一个函数在
\(x_{0}\)
的极限时,如果你可以直接计算出
\(y_{0}\)
的值,那么这个值就是该点的极限。

这种函数计算极限很简单,因为可以直接通过
\(y=f(x)\)
计算出来。


下面这道题是也是同济大学《高等数学上册》中的两道题。

image-20241110095607086


当 x 解决 0 时,分子是 0,0 除以任何数都是 0,所以极限是 0?肯定不是呀。

当碰到这种
\(x\to0\)
分子或分母为 0 的情况,就不能直接计算了。这两道题的解答过程:

image-20241110095620201


由于本文不是数学教程,因此这里不再深入讨论细节。

在高等数学中,有两种非常重要的极限:

\[\lim_{x \to 0} \frac{\sin x}{x} = 1 ,x \in (0,\frac{\pi }{2})
\]

\[\lim_{x \to 0} (1 + x)^{\frac{1}{x}} = e
\]


导数

给定一个函数,如何计算函数在某个区间上的变化率?

如图所示,函数
\(y = x^{2}\)
在区间
\([1,3]\)
的起点 A 和 终点 B。

image-20241110102423654

那么平均变化率就是:

\[\frac{\bigtriangleup y}{\bigtriangleup x} = \frac{9-1}{3-1} = \frac{8}{2} = 4
\]


但是当这个
\(\bigtriangleup{x}\)

\(\bigtriangleup{y}\)
非常小时,事情就会变得非常复杂。如果我们要求
\(x=9\)
附近的平均变化率,则:

\[\frac{y + \bigtriangleup y}{x+ \bigtriangleup x} = \frac{9 + \bigtriangleup y}{3 + \bigtriangleup x}
\]



\(\frac{\bigtriangleup y}{\bigtriangleup x}\)
非常小时,实际上反映了函数在
\(x=9\)
时的瞬时变化率。那么这个瞬时变化率,我们可以过 A、B 点使用切线表示。

切线是轻轻接触函数一点的一条线,由图可知,当 x 越来越大时,
\(y_{2} = x+1\)

\(y_{1} = x\)
大很多,比如
\(5^2\)

\(4^{2}\)

\(3^{2}\)
之间的差,越来越大。

那么切线可以反映这种变化率。如图所示,B 点的切线角度比 A 的的切线大。

image-20241110103552859


因此,出现了一种新的函数,叫原函数的导函数,简称导数,导数也是一个函数,通过导数可以计算原函数任一点的瞬时变化率。

导数的表示符号有多种,例如:

\[\frac{\bigtriangleup y}{\bigtriangleup x} = f'(x) = y' = \frac{dy}{dx} = \frac{df(x)}{x}= \frac{df}{x}
\]

d 是微分符号,例如 dy 是对 y 的微分,dx 是对 x 的微分。


如果要求在某点
\(x_{0}\)
的瞬时变化率,则:

\[\frac{\bigtriangleup y}{\bigtriangleup x} \big|_{x_{0}} = f'(x) \big|_{x_{0}} = y' \big|_{x_{0}} = \frac{dy}{dx} \big|_{x_{0}} = \frac{df(x)}{x} \big|_{x_{0}} = \frac{df}{x} \big|_{x_{0}}
\]


读者应该都有一定的数学基础吧,前面两种应该很容易理解,而后面三种也很重要,在积分和微积分的学习中,我们将会大量使用这种方式。

我们可以这样理解:

\[dy = \bigtriangleup y
\]

\[dx = \bigtriangleup x
\]


在 Pytorch 中,我们可以通过微分系统进行计算,例如我们要计算
\(d(x^2) \big|_{x=3}\)

// 定义 y = x^2 函数
var func = (torch.Tensor x) => x.pow(2);

var x = torch.tensor(3.0, requires_grad: true);
var y = func(x);

// 计算导数
y.backward();

// 转换为标量值
var grad = x.grad.ToScalar().ToDouble();
Console.WriteLine(grad);


不要搞错,计算导数后,要使用 x 输出导数值,而不是使用 y,因为 y 是函数结果。为什么求导的时候不直接输出求导结果呢?因为 Pytorch 自动求导系统是非常复杂的,计算的是偏导数,对于一元函数来说,对 x 的偏导数就是 y 的导数,在后面的偏导数和梯度时,会更多介绍这方面的知识。

另外创建 x 的张量类型时,需要添加
requires_grad: true
参数。


求导公式

下面是同济大学《高等数学》中的一些基本求导公式。

image-20241110105635460


例如,我们求
\(y = x^2\)
的导数,使用上图的 (2)式,得到
\(y = 2x\)


对于复合函数和复杂函数的求导会很麻烦,这里不再赘述。对于复杂的函数,还存在高阶导数,即导数的导数,二阶导数公式如下:

\[f''(x) = y'' = \frac{d^2y}{dx^2} = \frac{d^2f(x)}{x^2}= \frac{d^2f}{x^2}
\]


乘除求导例题

主要例题有乘法求导、商求导、指数求导几种。

① 求下面函数的导函数。

\[f(x) = e^x \cos x
\]

解:

\[\begin{align}
f'(x) &= (e^x \cos{x})' \\
&= (e^x)'\cos{x} + e^x (\cos{x})' \\
&= e^x \cos{x} - e^x \sin{x}
\end{align}
\]


求下面函数的导数:

\[y = \frac{x+1}{\ln x}
\]

解:

\[\begin{align}
y' &= \frac{(x+1)'\ln{x} - (x+1)(\ln{x})}{(\ln{x})^2} \\
&= \frac{\ln{x} - (x+1)^{\frac{1}{x}}}{(\ln{x})^2} \\
&= \frac{x \ln{x} -(x+1)}{x(\ln{x})^2}
\end{align}
\]


复合函数求导的链式法则

如果
\(y=f(u)\)
在点
\(u\)
处可导,
\(u=g(x)\)
在点
\(x\)
出可导,则复合函数
\(y=f[g(x)]\)
在点
\(x\)
处可导,且有:

\[\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}
\]


如果函数比较复杂,还可以推广到有限个复合函数的情况,例如:

\[\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dv} \cdot \frac{dv}{dx}
\]


例题,求
\(y = e^{2x}\)
的导函数。


\(u=2x\)
,则:

\[y' = (e^u)' = e^u \cdot (u)' = e^{2x} \cdot (2x)' = 2e^{ex}
\]


Sigmoid 函数的导数

经过上面的学习,我们知道由复合函数求导公式可知:

\[\big (\frac{u}{v} \big )' = \frac{u' \dot v - u \dot v'}{v^2}
\]


所以对于
\(\big ( \frac{1}{f(x)} \big )'\)
此类函数的求导,可得出:

\[\big ( \frac{1}{f(x)} \big )' = - \frac{f'(x)}{f(x)^2}
\]


Sigmoid 函数
\(σ(x)\)
是神经网络中最有名的激活函数之一,其定义如下:

\[\sigma (x) = \frac{1}{1+e^{-x}}
\]

image-20241114203340853


在后面学习梯度下降时,需要对 Sigmoid 函数进行求导,使用下面的公式求导会比较方便:

\[\sigma '(x) = \sigma (x)(1 - \sigma (x) )
\]


当然,你也可以使用分数的求导方法慢慢推导。

\[\begin{align}
\sigma '(x) &= (\frac{1}{1+e^{-x}})' \\
&= \frac{1}{1+e^{-x}} - \frac{1}{(1+e^{-x})^2} \\
&= \frac{1}{1+e^{-x}} (1 - \frac{1}{1+e^{-x}}) \\
&= \sigma (x)(1 - \sigma (x) )
\end{align}
\]


求最小值问题

函数的斜率有一个性质,当函数的斜率为 0 时,该点 a 取得极值,即
\(f'(a) = 0\)
,该点的切线平行于 x 轴。

画出
\(y = 3x^4 - 4x^3 - 12x^2 + 32\)
的图形如下所示,由图可知分别在
\(x = -1\)

\(0\)

\(2\)
三个点存在极值,此时斜率都是 0 ,其中当
\(x = 2\)
时,函数取得最小值,该函数无最大值。

image-20241113213533007


那么如果给定一个函数,我们如果取得这个函数的所有极值和最小最大值?这里可以使用穿针引线法。

首先对函数进行求导并化简。

\[\begin{align}
y' &= (3x^4 - 4x^3 - 12x^2 + 32)' \\
&= 12x^3 - 12x^2 - 24x \\
&= 12x(x^2 - x - 2) \\
&= 12x(x + 1)(x - 2)
\end{align}
\]


由此可知,该函数在
\(x = -1,0,2\)
三点的斜率为 0,然后分别计算在这三点的值,做出如下图所示。

image-20241113215104872


然后计算导数在区间的正负,例如当
\(x<-1\)
时,由于导数的结果是负数,所以
\(f(x)\)
递减区间。根据导数的正负,确定
\(f(x)\)
的递增递减区间,然后作出新的表格。

image-20241113215631143


接下来就简单了,根据三个点的值,在坐标轴上描点,最后按照递减区间连线,因此最小值是 0。

手画图不需要准确,主要是知道递增递减区间和极值就行。

e951f1d6da6d09477880fc13f655b23


微分

下面是同济大学《高等数学》中的一张图。

image-20241110110700577


由图可知,在正方形 A 中,其面积是
\(A = (x_{0})^2\)
,而大正方形的面积是
\((x_{0}+ \bigtriangleup x)^2\)
,或者通过多个矩形面积相加得出大正方形面积为:

\[S = x_{0}^2 + 2x_{0} \bigtriangleup x + (\bigtriangleup x)^2
\]


那么,在边长增加了
\(\bigtriangleup x\)
的时候,面积增加了多少呢?

\[\bigtriangleup S = 2x_{0} \bigtriangleup x + (\bigtriangleup x)^2
\]


我们可以使用下面的公式来表示当 $y = f(x)$ 满足一定关系时,其增量的表达式:

\[\bigtriangleup y = A \bigtriangleup x + O(\bigtriangleup x)
\]


前面在讲解导数时,我们知道
\(\bigtriangleup y =f(x + \bigtriangleup x) - f(x)\)
,所以:

\[\bigtriangleup y =f(x + \bigtriangleup x) - f(x) = A \bigtriangleup x + O(\bigtriangleup x)
\]



\(\bigtriangleup x\)
非常小时,并且
\(A \not= 0\)
时,可以忽略
\(O(\bigtriangleup x)\)
,我们使用 $A \bigtriangleup x $ 近似计算 $\bigtriangleup y $ 的值,这就是微分的定义,其中
\(A = f'(x)\)

\[dy = f'(x)\bigtriangleup x
\]


举个例子,求
\(y = x^3\)

\(x=1\)
时,
\(\bigtriangleup x = 0.01\)

\(\bigtriangleup x = 0.001\)
的增量。

上题本质就是求
\(x = 1.01\)

\(x = 1\)

\(\bigtriangleup y\)
以及
\(x = 1.001\)

\(x = 1\)

\(\bigtriangleup y\)

先求:

\[(1.01)^3 = 1.030301
\]

\[(1.001)^3 = 1.003003001
\]


所以两个增量方便是 0.030301、0.003003001。

但是如果只需要求近似值,那么我们使用微分方式去求,首先求出导数:

\[y' = dy = (x^3)' = 3x^2
\]


所以:

\[dy = 3x^2 \bigtriangleup x
\]


所以
\(\bigtriangleup x = 0.01\)
时,有:

\[dy = 3*(1)^2 * 0.01 = 3 * 0.01 = 0.03
\]


所以
\(\bigtriangleup x = 0.001\)
时,有:

\[dy = 3*(1)^2 * 0.001 = 3 * 0.001 = 0.003
\]


所以可以这样通过微分 dy 的方式近似计算函数的增量。

因为:

\[\bigtriangleup y = \frac{dy}{dx}
\]


我们使用 dy 近似代替
\(\bigtriangleup y\)
,这就是微分的应用场景之一。


积分

前面介绍了导数,我们知道
\(y = x^3\)
的导数是
\(y = 3x^2\)

那么反过来,我们知道一个函数
\(F(x)\)
的导数是
\(y=x^3\)
,对于幂函数,我们很容易反推出
\(\frac{1}{4} x^4\)
的导数是
\(x^3\)
,但是
\(\frac{1}{4} x^4 + 1\)

\(\frac{1}{4} x^4 + 666\)
的导数都是
\(x^3\)
,所以
\(x^3\)
的原函数是不确定的,所以反推得出的积分公式,又叫不定积分,我们使用
\(C\)
来表示这个不确定的常数。


假设原函数是 F(x),导数是
\(f(x)\)
,由于常数在求导时会被消去,所以求积分时,需要出现加上这个不确定的常数,所以:

\[\int f(x)dx = F(x) + C
\]


下面是同济大学《高等数学》给出一些积分公式。

image-20241110135308642


前面介绍了微分的作用,这里也给出导数在平面中的简单应用场景。


如图所示,图中的是
\(y = x^2\)
函数的封闭区域,和
\(x=0\)

\(x=2\)
两个直线围成了一个封闭区域,求 ABC 所围成的封闭区域的面积。

image-20241110140743986


首先求出其原函数为
\(y = \frac{1}{3}x^3\)
。使用积分区间表示求解的面积:

\[\int_{1}^{2} x^2=\frac{1}{3}x^3 \big|_{1}^{2} =\frac{1}{3}2^3 - \frac{1}{3}1^3=\frac{7}{3}
\]


对于上面求解的问题,使用的是积分公式,如下公式所示,∫ 表示积分符号,
\(f(x)\)
表示被积函数,
\(dx\)
表示积分变量增量(微分),
\(a\)

\(b\)
表示积分的下限和上限,即积分区间。

\[\int_{a}^{b} f(x) dx
\]


下面再来一道简单的题目,求 $y = 2x+3$ 和 $y = x^2$ 所围成的面积。

image-20241110143455304


首先要求得积分区间,即两者的两个交点,由
\(x^2=2x+3\)
得:

\[x^2 - 2x -3 = 0
\]


根据十字相乘法,得:

\[(x + 1)(x - 3) = 0
\]


所以
\(x_{1} = -1\)

\(x_{2} = 3\)


我们先求
\(y = 2x + 3\)
在这两个点之间围成的面积。

\[\int_{-1}^{3} 2x+3 = x^2+3x \big|_{-1}^{3} = (9 + 9) - (1 - 3) = 20
\]



\(y = x^2\)
在这两个点所围成的面积。

\[\int_{-1}^{3} x^2 = \frac{1}{3}x^3 \big|_{-1}^{3} = 9 - (-\frac{1}{3}) = 9 + \frac{1}{3}
\]


所以围成的面积是:
\(20 - (9+\frac{1}{3}) = \frac{32}{3}\)


在数学上,我们可以更加方便表示这种两个函数加减的方法,即:

\[\int_{-1}^{3} (2x+3 - x^2) = \int_{-1}^{3} (2x+3) - \int_{-1}^{3} (x^2)
\]


偏导数

偏导数属于多元函数的微分学,最常见的是求解空间问题,在初高中基本只涉及一元函数,在这里我们引入二元函数,记作:

\[z = f(x,y)
\]


在一元函数中,导数是函数沿着 x 轴的变化率,而在多元函数中由于有多个变量,不能直接计算导数,要针对某个轴方向进行求导,所以叫偏导数,接下来,我们将逐渐学习偏导数的一些基础知识。


多元函数定义域

下面给出一个二元函数构成的图形。

\[z=\sqrt{1-x^2-y^2}
\]

image-20241110145138140


下面提个问题,怎么求这个
\(z=\sqrt{1-x^2-y^2}\)
的定义域?

我们知道 $1 \ge x^2 + y^2 $ ,在设
\(y = 0\)
时, $1 \ge x^2 $ ,则
\(-1 \le x \le 1\)
,由于
\(1-x^2 \ge y^2\)
,所以
\(-\sqrt{1-x^2} \le y \le \sqrt{1 - x^2}\)

所以定义域:

\[-1 \le x \le 1 \\
-\sqrt{1-x^2} \le y \le \sqrt{1 - x^2} \\
z \ge 0
\]


这个函数是二元函数,求定义域还是比较简单的,z 是
\(f(x,y)\)
的函数,我们先求出 x 的定义域,然后求出 y 定义域。推广到
\(u=f(x,y,z)\)
三元函数,一般 x 定义域是常数,y 的定义域由 x 的函数组成,而 z 的定义域由 x、y 的函数组成。

求解空间中两个立体图形组成的封闭的空间体积时,就是使用定积分去求,计算定积分需要知道定义域,就是这种求法,本文不再赘述。


多元函数的值

已知函数
\(f(x,y) = \frac{xy}{x^2+y^2}\)
,求
\(f(1,2)\)

其实也很简单,方便使用
\(x=1,y=2\)
替代进去即可:

\[f(1,2) = \frac{2}{1^2+2^2} = \frac{1}{5}
\]


多元函数的极限

前面提到极限的时候,涉及到的都是一元函数,对于多元函数的极限,计算则复杂一些,我们可以使用以下公式表示二元函数在某点的极限值。

\[\lim_{_{y \longrightarrow y_{0}}^{x \longrightarrow x_{0}}} f(x,y) = A
\]


求二元函数的极限,称为二重极限。

例如求下面函数的二重极限。

\[\lim_{_{y \longrightarrow 2}^{x \longrightarrow 1}} \ln{(x+y^2)} = \ln{(1+2^2)} = \ln{5}
\]


偏导数

对多元函数求导的时候,由于函数有多个未知变量,例如 $z = x^2 + y^2 $ ,由于里面有 x、y 两个变量,因此函数也就有两个变化方向,求导的时候要设定是往哪个方向,例如要知道往 x 轴方向的变化率,那就是要针对 x 进行求导,求在
\(z=f(x_{0},y_{0})\)
时 x 的导数,这个就叫对 x 的偏导数。

偏导数使用符号
\(\partial\)
表示,那么对 x 的偏导数可以记作:

\[\frac{\partial z}{\partial x} \big|_{y=y_{0}}^{x=x_{0}}
\]


当然还有很多变体,Markdown 敲数学公式超级累,这里贴个图省事儿。

image-20241110151831018


下面给个简单函数的偏导数,方法很简单,当对 x 求偏导数时,把 y 当常数处理即可。

\[z = x^2 + y^2
\]

\[\frac{\partial z}{\partial x} = 2x,\frac{\partial z}{\partial y} = 2y
\]


再如:

\[z = x^2 + yx + y^2
\]

\[\frac{\partial z}{\partial x} = 2x + y,\frac{\partial z}{\partial y} = 2y +x
\]


前面提到积分可以求解平面中两个函数所组成的封闭区域的面积,偏导数则可以计算空间中立体几何和平面组成的封闭区域面积,这里就不再深入。


全微分

设二元函数
\(z = f(x,y)\)
则其全增量公式为:

\[\bigtriangleup z =A\bigtriangleup x + B \bigtriangleup y + O(\beta)
\]


那么关于 z 的微分:

\[dz=f_{x}(x,y)dx + f_{y}(x,y)dy
\]


求全微分,其实就是先求出所有偏导数,然后再进行计算。

例如求
\(z = e^{2x+3y}\)
的全微分。

389b1a036ebf0d36c4c2233c49c3556


给个例题,求函数
\(z = f(x,y) = \frac{x^2}{y}\)
在 点
\((1,-2)\)
出,当
\(\bigtriangleup x=0.02\)

\(\bigtriangleup y = -0.01\)
时的全增量。


先求函数的两个偏导数得出。

\[dz = \frac{2x}{y} \bigtriangleup x - \frac{x^2}{y^2} \bigtriangleup y
\]



\(\bigtriangleup x=0.02\)

\(\bigtriangleup y = -0.01\)
代入,得
\(-0.0175\)


下面是这个函数的图像。

image-20241110154210542


由微分和全微分的基础知识可知,在数学中进行一些计算时,其精确度会有所丢失。


偏导数求最小值

在学习导数时,我们知道当
\(f'(a) = 0\)
时,该函数取得极值,推广到多元函数中,也可以通过偏导数来求取极值。例如,对于二元函数
\(z = f(x,y)\)
,当符合下面条件时,可以取得极值:

\[\frac{\partial z}{\partial x} = 0,\frac{\partial z}{\partial y} = 0
\]


这是因为此时 x、y 切线的斜率都是 0,这里就不给出推理过程了,直接记住该方法即可。

如下图是函数
\(z=x^2 + y^2\)
的图像,求当 x、y 为何值时,函数取得最小值。

image-20241114204528433

很明显,当 x、y 都是 0 的时候,函数取得最小值,但是我们要通过数学来推到,不能只凭图像得出结论。

先求偏导数:

\[\frac{\partial z}{\partial x} = 2x \\
\frac{\partial z}{\partial y} = 2y
\]


可知,当
\(x=0,y=0\)
时,两个偏导数结果都是 0,所以
\(z=f(x,y)\)
只有在
\((0,0)\)
处有唯一的极值。

因为
\(z=x^2 + y^2 \ge 0\)
,所以可知,
\(z=f(0,0)\)
时取得最小值。


当 x、y 的斜率越来越接近 0 时,可以看到曲面切线越来越光滑。

image-20241114220105658


拉格朗日乘数法

有一个二元函数
\(z=(x,y)\)
,以及附加条件
\(\varphi (x,y) = 0\)
,而拉格朗日乘数法就是用来求解这种有条件限制的多元函数极值问题。

公式如下:

\[F(x,y,\lambda) = f(x,y) + \lambda{\varphi{(x,y)}}
\]


其中 $\lambda $ 是一个参数,也是我们要求解的值,求出
\(\lambda\)
后可以求得 z 的最小值。

首先将上面的公式进行偏导数求导,并且求出为 0 的条件:

\[F'_{x}(x,y,\lambda )= f'_{x}(x,y) +\lambda{\varphi{'_{x}(x,y)}} = 0
\]

\[F'_{y}(x,y,\lambda ) = f'_{y}(x,y) + \lambda{\varphi{'_{y}(x,y)}} = 0
\]

\[F'_{\lambda}(x,y,\lambda ) = \varphi{(x,y)} = 0
\]


通过上述方程求出 x、y、
\(\lambda\)
之后,代入
\(f(x,y)\)
求得极值。


例题
\(a + b = 1\)
,求
\(\frac{1}{a} + \frac{4}{b}\)
的最小值。

首先,二元函数是
\(z=f(a,b) = \frac{1}{a} + \frac{4}{b}\)

约束条件
\(\varphi (a,b) = a + b - 1=0\)

所以:

\[F(a,b,\lambda) = f(a,b)+ \lambda{\varphi{(a,b)}} = \frac{1}{a} + \frac{4}{b} + \lambda{(a + b - 1)}
\]


现在开始求偏导数。

\[F'_{a}(a,b,\lambda )= f'_{a}(a,b) +\lambda{\varphi{'_{a}(a,b)}} = -\frac{1}{a^2} + \lambda = 0 \qquad (1)
\]

\[F'_{b}(a,b,\lambda ) = f'_{b}(a,b) + \lambda{\varphi{'_{b}(a,b)}} = -\frac{4}{b^2} + \lambda= 0 \qquad (2)
\]

\[F'_{\lambda}(a,b,\lambda ) = \varphi{(a,b)} = a + b - 1= 0 \qquad (3)
\]


由 (1)、(2)、(3) 解得:

image-20241114212937793


代入
\(z=f(a,b) = \frac{1}{a} + \frac{4}{b}\)
,求得
\(z_{min} = f(\frac{1}{3},\frac{2}{3}) = 9\)
,所以最小值是 9。


梯度

在本节中,我们将学习深度学习里面重点之一的梯度下降法,梯度下降法要学习的知识比较多,本文的内容基本都是为梯度下降法做铺垫。

百度百科:方向导数本质上研究的是函数在某点处沿某特定方向上的变化率问题,梯度反映的是空间变量变化趋势的最大值和方向。


方向导数

前面提到导数,在一元函数中,
\(y=f(x)\)
,导数是反映了其在某点的变化率,而在
\(z = f(x,y)\)
中,两个偏导数 $\frac{\partial z}{\partial x} $ 、
\(\frac{\partial z}{\partial y}\)
则是反映函数沿着平行于 x 轴 、y 轴方向上的变化率。偏导数反映的是往某个轴方向的变化率,而方向导数则是某个方向的变化率,而不是某个轴方向。

image-20241110161833098


如上图所示,设
\(l\)
是一条有
\(P(x,y)\)
引出来的一条射线,
\(Q(x + \bigtriangleup x,y + \bigtriangleup y)\)

\(l\)
上的一点,设
\(\rho\)

\(P\)

\(Q\)
两点之间的距离,则:

\[\frac{\bigtriangleup z}{\rho}
\]


该公式反映函数在了
\(P\)

\(Q\)
两点之间沿着
\(l\)
方向的平均变化率,如果当
\(Q\)
趋近于
\(P\)
时,极限存在,则该极限值称为点
\(P\)
沿方向
\(l\)
的方向导数。


由于:

\[\bigtriangleup x = \rho \cos \alpha , \bigtriangleup y = \rho \cos \beta
\]


所以方向导数可以表示为:

\[\begin{align}
\frac{\partial z}{\partial l} &= \\
&= \frac{\partial z}{\partial x} \bigtriangleup x + \frac{\partial z}{\partial y} \bigtriangleup y \\
&= \frac{\partial z}{\partial x} \cos \alpha + \frac{\partial z}{\partial y} \cos \beta
\end{align}
\]


如果使用
\(i\)

\(j\)
表示 x、y 上的分量,也可以表示为:

\[\frac{\partial z}{\partial l} = \frac{\partial z}{\partial x}i + \frac{\partial z}{\partial y}j
\]


如果我们使用向量表示,也可以表示为:

\[(\frac{\partial z}{\partial x}, \frac{\partial z}{\partial y})
\]


梯度

梯度是指函数的值在哪个方向增长最快,后面学习的梯度下降则是相反的,是函数值下降最快的方向。


在空间中的一点,当点
\(P\)
固定时,方向
\(l\)
变化时,函数的方向导数 $\frac{\partial u}{\partial l} $ 也随之变化,说明了对于固定的点,函数在不同方向上的变化率也有所不同。那么对于点
\(P\)
,在什么方向上可以使得函数的变化率达到最大?这里需要引入梯度的概念。


下图是一个半球。

image-20241117094156647

问,怎么给定任意一点,怎么最快地达到顶部?很明显,垂直往上走,可以最快到底顶部,但是对于实际中凹凸不平的图像来说,是不能直接得出结论的,不过我们这里可以先简单讨论。

就像上面的图形,给定可微的二元函数
\(z = f(x,y)\)
,有一点
\((x_{0},y_{0})\)
,这个点可以往各种方向走,每个方向的方向导数都不一样,现在假设有个方向可以让方向导数最大,这个就是梯度 $gradf(x_{0},y_{0}) $。


如图所示,
\(A(x_{0},y_{0})\)
往 B 方向可以让 A 最快到达顶点,也就是变化率最大。而 A 有各种方向,其中一个是往 C 走。

往 B 方向的方向导数最大,就是梯度 $gradf(x_{0},y_{0}) $ 。由图所示,从 A 开始的任意一个方向导数,跟 $ \overrightarrow{AB}$ 都有一个夹角,因为是在空间,所以这个夹角表示起来有点麻烦,就是各个方向的余弦值,我们也是有向量表示:
\(n_{e} = (\cos \alpha ,\cos \beta)\)
,那么方向导数、梯度的关系:

\[\frac{\partial z}{\partial l} =gradf(x_{0},y_{0}) \cdot n_{e}
\]

\[\frac{\partial z}{\partial l} = \frac{\partial z}{\partial x} \cos \alpha + \frac{\partial z}{\partial y} \cos \beta =
gradf(x_{0},y_{0})\cdot n_{e}
\]

image-20241117094843430


如下图所示,当
\(\alpha = 0\)
时,
\(gradf(x_{0},y_{0})\)

\(e_{1}\)
重合, 由于
\(\cos \alpha = 1\)
,所以方向导数也达到最大值
\(|gradf(x_{0},y_{0})|\)
。也就是,沿着梯度方向的方向导数可以达到最大值。

所以:

\[\begin{align}
gradf(x_{0},y_{0}) &= \frac{\partial z}{\partial x} \cos \alpha + \frac{\partial z}{\partial y} \cos \beta \\
&= \frac{\partial z}{\partial x}i + \frac{\partial z}{\partial y}j \\
&= (\frac{\partial z}{\partial x}, \frac{\partial z}{\partial y})
\end{align}
\]

image-20241110164906093


例题,求函数
\(z = \ln(x^2 + y^2)\)
的梯度。

image-20241110165653935

来源:《高等数学工本》陈兆斗。


再来一道实际意义的题目。

image-20241110170325278

来源:《高等数学工本》陈兆斗。


使用
C#
求解该题,得:

// 定义 u = x^2 + y^2 + z^2 函数在 (2,1,-1) 点的值
var x = torch.tensor(2.0, requires_grad: true);
var y = torch.tensor(1.0, requires_grad: true);
var z = torch.tensor(-1.0, requires_grad: true);
var u = x.pow(2) + y.pow(2) + z.pow(2);

// 求导
u.backward();

var ux = x.grad;
var uy = y.grad;
var uz = z.grad;

Console.WriteLine($"gradu(2,1,-1) = {"{"}{ux.ToScalar().ToDouble()},{uy.ToScalar().ToDouble()},{uz.ToScalar().ToDouble()} {"}"}");
gradu(2,1,-1) = {4,2,-2 }


梯度下降法的基本公式

建议读者阅读这篇文章,这样很容易理解什么是梯度下降:
https://www.zhihu.com/question/434600945


前面提到,梯度是向上最快,那么梯度下降就是向下最快,跟梯度相反就是最快咯。


梯度下降法是神经网络的武器,相信大家在了解深度学习时,也最常出现梯度下降的相关知识,所以本小节将讲解梯度下降法的一些基础知识。

在偏导数求最小值一节中,我们学习到最小值需要满足以下条件:

\[\frac{\partial z}{\partial x} = 0,\frac{\partial z}{\partial y} = 0
\]


如果可以直接通过偏导数计算出梯度,那么问题就简单了,直接计算出最小值,都是对于实际场景要计算出来是比较可能的,尤其在神经网络里面。所以大佬们使用另一种方法来求出最小值的近似值,叫梯度下降法。

画出一个三维图像如图所示:

image-20241114223740021


假如你正在最高位置,将你蒙上眼睛后,你要从最上面移动到最底的位置,每次只能移动一个格。

我们要最快下降到底部,肯定要选择最徒的路径,但是因为蒙着眼睛,无法跳过一个格知道后面的格的位置,所以只能先从附近的格对比后,找到最徒的格,然后再走下一步。但是不可能所有的格都走一次吧?可以先选几个格,然后判断哪个格最徒,接着走下一步,然后再选几个格,再走下一步。


在前面学习梯度时,我们知道:

\[\frac{\partial z}{\partial l} = \frac{\partial z}{\partial x} \bigtriangleup x + \frac{\partial z}{\partial y} \bigtriangleup y
\]


即:

\[\bigtriangleup z = \frac{\partial z}{\partial x} \bigtriangleup x + \frac{\partial z}{\partial y} \bigtriangleup y
\]


如果我们把这个公式当作两个向量的内积,可以得出:

\[\bigtriangleup z = (\frac{\partial z}{\partial x},\frac{\partial z}{\partial y}) \cdot (\bigtriangleup x,\bigtriangleup y)
\]


image-20241115204555682


当以下向量方向相反时,
\(\bigtriangleup z\)
取得最小值。

image-20241115205039736


让我们回顾向量知识,当两个向量的方向相反时,向量内积取得最小值。由于:

\[a \cdot b = |a||b| \cos \theta
\]


所以向量 b 满足:

\[b = -ka \qquad
\]

(k 为正的常数)



\(b= (\bigtriangleup x,\bigtriangleup y)\)

\(a= (\frac{\partial z}{\partial x},\frac{\partial z}{\partial y})\)

\(k=\eta\)
,所以:

\[(\bigtriangleup x,\bigtriangleup y) = -\eta (\frac{\partial z}{\partial x},\frac{\partial z}{\partial y} ) \qquad ( \eta 为正的微小常数)
\]


这个公式称为二变量函数的梯度下降法基本公式,如果推广到三个变量以上:

\[(\bigtriangleup x_{},\bigtriangleup x_{2},...,\bigtriangleup x_{n}) = -\eta (\frac{\partial x}{\partial x_{1}},\frac{\partial z}{\partial x_{2}},...,,\frac{\partial z}{\partial x_{n}} )

\]


前面学习方向导数和梯度的时候,我们知道沿着梯度的方向导数最大,此时梯度是
\((\frac{\partial z}{\partial x},\frac{\partial z}{\partial y})\)
,也就是向上是最徒的。

由于
\((\bigtriangleup x,\bigtriangleup y)\)
是跟梯度相反的向量,所以向下是下降最快的,所以这就是梯度下降法求使得下降最快的向量。


回顾使用偏导数求最小值
\(z=x^2 + y^2\)
,求:当 x 从 1 变成
\(1+\bigtriangleup x\)
、y 从 2 变到
\(2 + \bigtriangleup y\)
时,求出使得这个函数减小最快的向量
\((\bigtriangleup x,\bigtriangleup y)\)

首先求出偏导数:

\[\frac{\partial z}{\partial x} = 2x \\
\frac{\partial z}{\partial y} = 2y
\]


根据梯度下降法的基本公式得出:

\[(\bigtriangleup x,\bigtriangleup y) = -\eta (2x,2y ) \qquad (\eta 为正的微小常数)
\]

由题意当
\(x=1\)

\(y=2\)
时,得出:

\[(\bigtriangleup x,\bigtriangleup y) = -\eta (2,4 ) \qquad
\]

(
\(\eta\)
为正的微小常数)


在本小节中,还有一个
\(\eta\)
没有讲解,它是一个非常小的正数,就像下山问题中的一个格,即移动的步长。在使用计算机进行计算时,需要确定一个合适的
\(\eta\)
值,
\(\eta\)
值过小或过大都会导致一些问题,而在神经网络中,
\(\eta\)
称为学习率,没有明确的方法求出
\(\eta\)
值,只能通过反复实验来寻找合适的值。


哈密算子
\(\bigtriangledown\)

当梯度下降法推广到多个变量时,下面的公式会显示非常复杂:

\[(\bigtriangleup x_{},\bigtriangleup x_{2},...,\bigtriangleup x_{n}) = -\eta (\frac{\partial x}{\partial x_{1}},\frac{\partial z}{\partial x_{2}},...,\frac{\partial z}{\partial x_{n}} )
\]


所以数学上经常使用 $\bigtriangledown $ 符号简化公式。

\[\bigtriangledown f = (\frac{\partial x}{\partial x_{1}},\frac{\partial z}{\partial x_{2}},...,\frac{\partial z}{\partial x_{n}} )
\]


替换到梯度下降法公式就是:

\[(\bigtriangleup x_{},\bigtriangleup x_{2},...,\bigtriangleup x_{n}) = -\eta \bigtriangledown f
\]


梯度下降法求最小值的近似值

在学习梯度下降法的基本公式时,提到了
\(\eta\)
,那么继续回顾
\(z = x^2 + y^2\)
的问题,我们如果设置学习率
\(\eta = 0.1\)
,那么根据梯度下降法,我们怎么使用这个算法求最小值呢?假设初始点是
\((3,2)\)
,根据梯度:

\[(\bigtriangleup x,\bigtriangleup y) = -0.1 (2x,2y ) \\
\bigtriangleup x = -0.2x \\
\bigtriangleup y = -0.2y
\]


代入
\((3,2)\)
,得:

第几次运算 当前位置 当前位置 梯度 梯度 位移向量 位移向量 函数值
i x y ∂z/∂x ∂z/∂y ∆x ∆y z
0 3.00 2.00 6.00 4.00 -0.60 -0.40 13.00


所以,点
\((3.00,2.00)\)
已经移动到
\((2.40,1.60)\)
,所以:

第几次运算 当前位置 当前位置 梯度 梯度 位移向量 位移向量 函数值
i x y ∂z/∂x ∂z/∂y ∆x ∆y z
0 3.00 2.00 6.00 4.00 -0.60 -0.40 13.00
1 2.40 1.60


重新计算梯度等步骤,得出:

第几次运算 当前位置 当前位置 梯度 梯度 位移向量 位移向量 函数值
i x y ∂z/∂x ∂z/∂y ∆x ∆y z
0 3.00 2.00 6.00 4.00 -0.60 -0.40 13.00
1 2.40 1.60 4.80 3.20 -0.48 -0.32 8.32

反复执行运算,最终可以算出最小值,如果步骤越少,那么下降的速度最快。


在 Pytorch 中,梯度下降算法有很多种,这里不再赘述,读者感兴趣可以参考这篇文章:
https://zhuanlan.zhihu.com/p/619988672

作者:来自 vivo 互联网大前端团队- Ke Jie

介绍 App 包体积优化的必要性,游戏中心 App 在实际优化过程中的有效措施,包括一些优化建议以及优化思路。

一、包体积优化的必要性

安装包大小与下载转化率的关系大致是成反比的,即安装包越大,下载转换率就越差。Google 曾在 2019 的谷歌大会上给出过一个统计结论,包体积体大小每上升 6MB,应用下载转化率就会下降 1%,在不同地区的表现可能会有所差异。

APK 减少 10MB,在不同国家转化率增长

(注:数据来自于
googleplaydev:Shrinking APKs, growing installs

二、游戏中心 APK 组成

APK 包含以下目录:

  • META-INF/:包含 CERT.SF 、CERT.RSA 签名文件、MANIFEST.MF 清单文件。

  • assets/:包含应用的资源。

  • res/:包含未编译到 resources.arsc 中的资源。

  • lib/:支持对应 CPU 架构的 so 文件。

  • resources.arsc:资源索引文件。

  • classes.dex:可以理解的 dex 文件就是项目代码编译为 class 文件后的集合。

  • AndroidManifest.xml:包含核心 Android 清单文件。此文件列出了应用的名称、版本、访问权限和引用的库文件。

发现占包体积比较大的主要是 lib、res、assets、resources 这几个部分,优化主要也从这几个方面入手。

三、包体积检测工具

Matrix-ApkChecker 作为 Matrix 系统的一部分,是针对 Android 安装包的分析检测工具,根据一系列设定好的规则检测 APK 是否存在特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪。

配置游戏中心的 Json,主要检测 APK 是否经过了资源混淆、不含 Alpha 通道的 PNG 文件、未经压缩的文件类型、冗余的文件、无用资源等信息。

对于生成的检测文件进行分析,可以优化不少体积。

工具 Matrix Apkcheck 介绍:
https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker

四、包体积优化措施

4.1 不含 Alpha 通道的 PNG 大图

项目中存在较多这种类型的图,可以替换为 JPG 或者 WebP 图,能减少不少体积。

4.2 代码做减法

随着业务的迭代,很多业务场景是不会再使用了,涉及到相关的资源和类文件都可以删除掉,相应的 APK 中 res 和 dex 都会相应减少。游戏中心这次去掉了些经过迭代后没有使用的业务场景和资源。

4.3 资源文件最少化配置

针对内销的项目,本地的 string.xml 或者 SDK 中的 string.xml 文件中的多语言,是根本用不到的。这部分资源可以优化掉,能减少不少体积。

在 APP 的 build.gradle 中下添加 resConfigs "zh-rCN", "zh-rTW", "zh-rHK"。这样配置不影响英文、中文、中国台湾繁体、中国香港繁体语言的展示。

资源文件最少化配置前

资源文件最少化配置后

4.4 配置资源优化

很多项目为了适配各种尺寸的分辨率,同一份资源可能在不同的分辨率的目录下放置了各种文件,然后现在主流的机型都是 xxh 分辨率,游戏游戏中心针对了内置的 APK,配置了优先使用"xxhdpi", "night-xxhdpi"。

这么配置如果 xxhdpi、night-xxhdpi 存在资源文件,就会优先使用该分辨率目录下文件,如果不存在则会取原来分辨率目录下子资源,能避免出现资源找不到的情形。

defaultConfig {
        resConfigs isNotBaselineApk ? "" : ["xxhdpi", "night-xxhdpi"]
}

4.5 内置包去除 v1 签名

同样对于内置包来说,肯定都是 Android 7 及以上的机型了,可以考虑去掉 v1 签名。

signingConfigs {
    gameConfig {
        if (isNotBaselineApk) {
            print("v1SigningEnabled true")
            v1SigningEnabled true
        } else {
            print("v1SigningEnabled false")
            v1SigningEnabled false
        }
        v2SigningEnabled true
    }
}

去掉 v1 签名后,上图的三个文件在 APK 中会消失,也能较少 600k 左右的体积。

4.6 动效资源文件优化

发现项目中用了不少的 GIF、Lottie 文件、SVG 文件,占用了很大一部分体积。考虑将这部分替换成更小的动画文件,目前游戏中心接入了 PAG 方案。替换了部分 GIF 图和 Lottie 文件。

PAG 文件采用可扩展的二进制文件格式,可单文件集成图片音频等资源,导出相同的 AE 动效内容,在文件解码速度和压缩率上均大幅领先于同类型方案,大约为 Lottie 的 0.5 倍,SVG 的 0.2 倍。

实际上可能由于设计导出的 Lottie 或者 GIF 不规范,在导出 PAG 文件时会提醒优化点,实际部分资源的压缩比率达到了 80~90%,部分动效资源从几百 K 降到了几十 K。

具体可以参考 PAG 官网:
https://github.com/Tencent/libpag/blob/main/README.zh_CN.md

游戏中心这边将比较大的 GIF 图,较多的 Lottie 图做过 PAG 替换。

举例

(1)游戏中心的榜单排行页上的头图,UI 那边导出的符合效果的 GIF 大小为 701K,替换为 PAG 格式后同样效果的图大小为 67K,只有原来的 1/10 不到。

(2)游戏中心的入口空间 Lottie 动效优化。

一份 Lottie 动效大概是这样的,一堆资源问题加上 Json 文件。像上述动效的整体资源为 112K,同样的动效格式转换为 PAG 格式后,资源大小变成 6K,只有原大小的 5%左右。之后新的动效会优先考虑使用 PAG。

4.7 编译期间优化图片

以游戏中心 App 为例,图片资源约占用了 25%的包体积,因此对图片压缩是能立杆见效的方式。

WebP 格式相比传统的 PNG 、JPG 等图片压缩率更高,并且同时支持有损、无损、和透明度。

思路就是在是在 mergeRes 和 processRes 任务之间插入 WebP 压缩任务,利用 Cwebp 对图片在编译期间压缩。

(注:图片来源于
https://booster.johnsonlee.io/zh/guide/shrinking/png-compression.html#pngquant-provider

已有的解决方法

(1)可以采用滴滴的方案 booster,booster-task-compression-cwebp 。

参考链接:
https://github.com/didi/booster

(2)公司内部官网模块也有类似基于 booster 的插件,基于 booster 提供的 API 实现的图片压缩插件。压缩过后需要对所有页面进行一次点检,防止图片失真,针对失真的图片,可以采用白名单的机制。

4.8 动态化加载 so

同样以游戏中心为例,so 的占比达到了 45.1%,可以对使用场景较少和较大的 so 进行动态化加载的策略,在需要使用的场景下载到本地,动态去加载。

使用的场景去服务端下载到本地加载的流程可以由以下流程图表示。

流程可以归纳为下载、解压、加载,主要问题就是解决 so 加载问题。

载入 so 库的传统做法是使用:

System.loadLibrary(library);

经常会出现 UnsatisfiedLinkError,Relinker 库能大幅减小报错的概率:

ReLinker.loadLibrary(context, "mylibrary")

具体可以参考:
https://github.com/KeepSafe/ReLinker

按需加载的情形,风险与收益是并存的,有很多情况需要考虑到,比如下载触发场景、网络环境、加载失败是否有降级策略等等,也需要做好给用户的提示交互。

4.9 内置包只放 64 位 so

目前新上市的手机 CPU 架构都是 arm64-v8a, 对应着 ARMV8 架构,所以在打包的时候针对内置项目,只打包 64 位 so 进去。

ndk {
            if ("64" == localMultilib)
                abiFilters "arm64-v8a"
            else if ("32" == localMultilib)
                abiFilters "armeabi"
            else
                abiFilters "armeabi", "arm64-v8a"
        }
//其中localMultilib为配置项变量
 
String localMultilib = getLocalMultilib()
String getLocalMultilib() {
    def propertyKey = "LOCAL_MULTILIB"
    def propertyValue = rootProject.hasProperty(propertyKey) ? rootProject.getProperty(propertyKey) : "both"
    println " --> ${project.name}: $propertyKey[$propertyValue], $propertyKey[${propertyValue.class}]"
    return propertyValue
}

4.10 开启代码混淆、移除无用资源、ProGuard 混淆代码

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
        }
    }
}

shrinkResources 和 minifyEnabled 必须同时开启才有效。

特别注意:这里需要强调一点的是开启之后无用的资源或者图片并没有真正的移除掉,而是用了一个同名的占位符号。

可以通过 ProGuard 来实现的,ProGuard 会检测和移除代码中未使用的类、字段、方法和属性,除此外还可以优化字节码,移除未使用的代码指令,以及用短名称混淆类、字段和方法。

proguard-android.txt 是 Android 提供的默认混淆配置文件,在配置的 Android sdk /tools/proguard 目录下,proguard-rules.pro 是我们自定义的混淆配置文件,我们可以将我们自定义的混淆规则放在里面。

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
        }
    }
}

4.11 R 文件内联优化

如果我们的 App 架构如下:

编译打包时每个模块生成的 R 文件如下:

R_lib1 = R_lib1;
R_lib2 = R_lib2;
R_lib3 = R_lib3;
R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1本身的R)
R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2本身的R)
R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app本身R)

可以看出各个模块的 R 文件都会包含下层组件的 R 文件内容,下层的模块生成的 id 除了自己会生成一个 R 文件外,同时也会在全局的 R 文件生成一个,R 文件的数量同样会膨胀上升。多模块情况下,会导致 APK 中的 R 文件将急剧的膨胀,对包体积的影响很大。

由于 App 模块目前的 R 文件中的资源 ID 全部是 final 的, Java 编译器在编译时会将 final 常量进行 inline 内联操作,将变量替换为常量值,这样项目中就不存在对于 App 模块 R 文件的引用了,这样在代码缩减阶段,App 模块 R 文件就会被移除,从而达到包体积优化的目的。

基于以上原理,如果我们将 library 模块中的资源 ID 也转化为常量的话,那么 library 模块的 R 文件也可以移除了,这样就可以有效地减少我们的包体积。

现在有不少开源的 R 文件内联方法,比如滴滴开源的 booster 与字节开源的 bytex 都包含了 R 文件内联的插件。

booster 参考:

https://booster.johnsonlee.io/zh/guide/shrinking/res-index-inlining.html#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8

bytex 参考:

https://github.com/bytedance/ByteX/blob/master/access-inline-plugin/README-zh.md

五、优化效果

5.1 优化效果

上述优化措施均在游戏中心实际中采用,以游戏中心某个相同的版本为例子,前后体积对比如下图所示:

(1)包体积优化的比例达到了 31%,包体积下降了 20M 左右,从长久来说对应用的转换率可以提升 3%的点左右。

(2)启动速度相对于未优化版本提升 2.2%个点。

5.2 总结

(1)读者想进行体积优化之前,需先分析下 APK 的各个模块占比,主要针对占比高的部分进行优化,比如:游戏中心中 lib、res、assets、resources 占比较高,就针对性的进行了优化;

(2)动效方案的切换、so 动态加载、编译期间图片优化等措施是长久的,相比于未进行优化,时间越长可能减少的体积越明显;

(3)资源文件最小化配置、配置资源优化,简单且效果显著;

(4)后续会对 dex 进行进一步探索,目前项目中代码基本上都在做加法,越来越复杂,很少有做减法,导致 dex 逐渐增大,目前还在探索怎么进一步缩小 dex 体积。

在网上购物时候,不止可以通过名称搜索商品,也可以拍照上传图片搜索商品。比如某宝上拍个图片就能搜索到对应的商品。

腾讯、阿里都提供了类似的图像搜索服务,这类服务原理都差不多:

  • 在一个具体的图库上,新增或者删除图片。
  • 通过图片搜索相似的图片。

本文对接的是
腾讯云的图像搜索

添加配置

添加 maven 依赖:

<dependency>
    <groupId>com.tencentcloudapi</groupId>
    <artifactId>tencentcloud-sdk-java</artifactId>
    <version>3.1.1129</version>
</dependency>

引入配置:

tencentcloud:
  tiia:
    secretId: ${SECRET_ID}
    secretKey: ${SECRET_KEY}
    endpoint: tiia.tencentcloudapi.com
    region: ap-guangzhou
    groupId: test1

secretId 和 secretKey 都是在
API秘钥
地址:
https://console.cloud.tencent.com/cam/capi
,groupId 是图库 id。

配置 bean

@Data
@Configuration
@ConfigurationProperties("tencentcloud.tiia")
public class TencentCloudTiiaProperties {

    private String secretId;

    private String secretKey;

    private String endpoint = "tiia.tencentcloudapi.com";

    private String region = "ap-guangzhou";

    private String groupId;

}
@Configuration
@ConditionalOnClass(TencentCloudTiiaProperties.class)
public class TencentCloudTiiaConfig {

    @Bean
    public TiiaClient tiiaClient(TencentCloudTiiaProperties properties) {
        Credential cred = new Credential(properties.getSecretId(), properties.getSecretKey());
        HttpProfile httpProfile = new HttpProfile();
        httpProfile.setEndpoint(properties.getEndpoint());
        ClientProfile clientProfile = new ClientProfile();
        clientProfile.setHttpProfile(httpProfile);
        TiiaClient client = new TiiaClient(cred, properties.getRegion(), clientProfile);
        return client;
    }
}

tiiaClient 是搜图的核心,在后面新增、删除、搜索图片都会使用到。

图库更新

新建图库之后,需要将图片批量的导入到图库中。一般开始会批量将上架的图片批量导入到图片库,一般只需要操作一次。

商品有修改、新增、下架操作时,图片也需要有对应的更新操作。但是每次都更新都同步更新操作,可能会导致数据库频繁更新,服务器压力增加,需要改成,每次更新图片后,同步到缓存中,然后定时处理缓存的数据:

腾讯图像搜索没有图像更新接口,只有图像删除和新增的接口,那就
先调用删除,再调用新增的接口

删除图片

图片删除调用
tiiaClient.DeleteImages
方法,主要注意请求频率限制。

默认接口请求频率限制:10次/秒

这里就简单处理,使用线程延迟处理
Thread.sleep(100)
,删除图片只要指定 EntityId:

@Data
public class DeleteImageDTO {

    private String entityId;

    private List<String> picName;
}

如果指定 PicName 就删除 EntityId 下面的具体的图片,如果不指定 PicName 就删除整个 EntityId。

删除图片代码如下:

public void deleteImage(List<DeleteImageDTO> list) {
    if (CollectionUtils.isEmpty(list)) {
        return;
    }
    list.stream().forEach(deleteImageDTO -> {
        List<String> picNameList = deleteImageDTO.getPicName();
        if (CollectionUtils.isEmpty(picNameList)) {
            DeleteImagesRequest request = new DeleteImagesRequest();
            request.setGroupId(tiiaProperties.getGroupId());
            request.setEntityId(deleteImageDTO.getEntityId());
            try {
                // 腾讯限制qps
                Thread.sleep(100);
                tiiaClient.DeleteImages(request);
            } catch (TencentCloudSDKException | InterruptedException e) {
                log.error("删除图片失败, entityId {} 错误信息 {}", deleteImageDTO.getEntityId(), e.getMessage());
            }
        } else {
            picNameList.stream().forEach(picName -> {
                DeleteImagesRequest request = new DeleteImagesRequest();
                request.setGroupId(tiiaProperties.getGroupId());
                request.setEntityId(deleteImageDTO.getEntityId());
                request.setPicName(picName);
                try {
                    Thread.sleep(100);
                    tiiaClient.DeleteImages(request);
                } catch (TencentCloudSDKException | InterruptedException e) {
                    log.error("删除图片失败, entityId {}, 错误信息 {}", deleteImageDTO.getEntityId(), picName, e.getMessage());
                }
            });
        }
    });

}

新增图片

新增图片调用
tiiaClient.CreateImage
方法,这里也需要注意调用频率的限制。除此之外还有两个限制:

  • 限制图片大小不可超过 5M
  • 限制图片分辨率不能超过分辨率不超过 4096*4096

既然压缩图片需要耗时,那就每次上传图片先压缩一遍,这样就能解决调用频率限制的问题。压缩图片引入 thumbnailator:

<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.20</version>
</dependency>

压缩工具类:

/**
 * 图片压缩
 * @param url             图片url
 * @param scale           压缩比例
 * @param targetSizeByte  压缩后大小 KB
 * @return
 */
public static byte[] compress(String url, double scale, long targetSizeByte) {
    if (StringUtils.isBlank(url)) {
        return null;
    }
    long targetSizeKB = targetSizeByte * 1024;
    try {
        URL u = new URL(url);
        double quality = 0.8;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
        do {
            Thumbnails.of(u).scale(scale) // 压缩比例
                    .outputQuality(quality) // 图片质量
                    .toOutputStream(outputStream);
            long fileSize = outputStream.size();
            if (fileSize <= targetSizeKB) {
                return outputStream.toByteArray();
            }
            outputStream.reset();
            if (quality > 0.1) {
                quality -= 0.1;
            } else {
                scale -= 0.1;
            }
        } while (quality > 0 || scale > 0);
    } catch (IOException e) {
        log.error(e.getMessage());
    }
    return null;
}

通过缩小图片尺寸和降低图片质量将图片压缩到固定的大小,这里都会先压缩一遍。解决调用频率限制的问题。

限制图片的分辨率也是使用到 thumbnailator 里面的 size 方法。

thumbnailator 压缩图片和限制大小,不能一起使用,只能分来调用。

设置尺寸方法:

/**
 * 图片压缩
 * @param imageData
 * @param width           宽度
 * @param height          高度
 * @return
 */
public static byte[] compressSize(byte[] imageData,String outputFormat,int width,int height) {
    if (imageData == null) {
        return null;
    }
    ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData);
    try {
        BufferedImage bufferedImage = ImageIO.read(inputStream);
        int imageWidth = bufferedImage.getWidth();
        int imageHeight = bufferedImage.getHeight();
        if (imageWidth <= width && imageHeight <= height) {
            return imageData;
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
        Thumbnails.of(bufferedImage)
                .size(width,height)
                .outputFormat(outputFormat)
                .toOutputStream(outputStream);
        return outputStream.toByteArray();
    } catch (IOException e) {
        log.error(e.getMessage());
    }
    return null;
}

这里的 width 和 height 并不是直接设置图片的长度和长度,而是不会超过这个长度和宽度。如果有一个超过限制大小,压缩尺寸,长宽比保持不变。

新增图片需要指定 EntityId、url 以及 picName。

@Data
public class AddImageDTO {

    private String entityId;

    private String imgUrl;

    private String picName;

}

解决了图片压缩问题,上传图片就比较简单了:

public void uploadImage(List<AddImageDTO> list) {
    if (CollectionUtils.isEmpty(list)) {
        return;
    }
    list.stream().forEach(imageDTO -> {
        String imgUrlStr = imageDTO.getImgUrl();
        if (StringUtils.isBlank(imgUrlStr)) {
            // 跳过当前元素
            return;
        }
        CreateImageRequest request = new CreateImageRequest();
        request.setGroupId(tiiaProperties.getGroupId());
        request.setEntityId(imageDTO.getEntityId());
        String imageUrl = imageDTO.getImgUrl();
        // 限制大小
        byte[] bytes = ImageUtils.compress(imageUrl,0.6,1024 * 5);
        String imageFormat = imageUrl.substring(imageUrl.lastIndexOf(".") + 1);
        // 限制分辨率
        bytes = ImageUtils.compressSize(bytes,imageFormat,4096,4096);
        request.setImageBase64(new String(Base64.encodeBase64(bytes), StandardCharsets.UTF_8));
        //JSONObject tagJson = new JSONObject();
        //tagJson.put("code","搜索字段");
        //request.setTags(JSONObject.toJSONString(tagJson));
        request.setPicName(imageDTO.getPicName());
        try {
            tiiaClient.CreateImage(request);
        } catch (TencentCloudSDKException e) {
            log.error("图像上传失败 error:{}", e.getMessage());
        }
    });
}

Tags 图片自定义标签,设置图片的参数,搜索的时候就可以根据参数搜索到不同的图片。

更新图片

一般商品更新,将数据存入缓存中:

  String value = "demo key";
  SetOperations<String, Object> opsForSet = redisTemplate.opsForSet();
  opsForSet.add(RedisKeyConstant.PRODUCT_IMAGE_SYNC_CACHE_KEY, value);

再定时执行任务:

public void syncImage() {
    while (true) {
        SetOperations<String, Object> operations = redisTemplate.opsForSet();
        Object obj = operations.pop(RedisKeyConstant.PRODUCT_IMAGE_SYNC_CACHE_KEY);
        if (obj == null) {
            log.info("暂未发现任务数据");
            return;
        }
        String pop = obj.toString();
        if (StringUtils.isBlank(pop)) {
            continue;
        }
        DeleteImageDTO deleteImageDTO = new DeleteImageDTO();
        deleteImageDTO.setEntityId(pop);
        try {
            this.deleteImage(Collections.singletonList(deleteImageDTO));
        } catch (Exception e) {
            log.error("删除图片失败,entityId {}",pop);
        }
        // todo 获取数据具体的数据
        String imageUrl="";
        // todo picName 需要全局唯一
        String picName="";

        AddImageDTO addImageDTO = new AddImageDTO();
        addImageDTO.setEntityId(pop);
        addImageDTO.setImgUrl(imageUrl);
        addImageDTO.setPicName(picName);
        try {
            this.uploadImage(Collections.singletonList(addImageDTO));
        } catch (Exception e) {
            log.error("上传图片失败,entityId {}",pop);
        }


    }
}

operations.pop
从集合随机取出一个数据并移除数据,先删除图片,再从数据库中查询是否存在数据,如果存在就新增图片。

搜索图片

图像搜索调用
tiiaClient.SearchImage
方法,需要传图片字节流,压缩图片需要文件后缀。

@Data
public class SearchRequest {

  private byte[] bytes;

  private String suffix;

}


public ImageInfo [] analysis(SearchRequest searchRequest) throws IOException, TencentCloudSDKException {
    SearchImageRequest request = new SearchImageRequest();
    request.setGroupId(tiiaProperties.getGroupId());
    // 筛选,对应上传接口 Tags
    //request.setFilter("channelCode=\"" + searchRequest.getChannelCode() + "\"");、
    byte[] bytes = searchRequest.getBytes();
    bytes = ImageUtils.compressSize(bytes,searchRequest.getSuffix(),4096,4096);
    String base64 = Base64.encodeBase64String(bytes);
    request.setImageBase64(base64);
    SearchImageResponse searchImageResponse = tiiaClient.SearchImage(request);
    return searchImageResponse.getImageInfos();
}

根据返回的 ImageInfos 数组获取到 EntityId,就能获取对应的商品信息了。

总结

对接图像搜索,主要是做图像的更新和同步操作。相对于每次更新就同步接口,这种方式对于服务器的压力也比较大,
先将数据同步到缓存中,然后在定时的处理数据
,而搜索图片对于数据一致性相对比较宽松,分散库写入的压力。

新增图片使用 thumbnailator 压缩图片和缩小图片,对于调用请求频率限制,新增图片每次都会压缩一次图片,每次压缩时间大概都大于 100ms,解决了请求频率限制的问题。而删除图片,就简单使用线程休眠的方式休眠 100ms。

做好图片更新的操作之后,搜索图库使用
tiiaClient.SearchImage
方法就能获取到对应的结果信息了。

Github示例

https://github.com/jeremylai7/springboot-learning/blob/master/springboot-test/src/main/java/com/test/controller/ImageSearchController.java

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: CNN Mixture-of-Depths

创新点


  • 提出新的卷积轻量化结构
    MoD
    ,在卷积块(
    Conv-Blocks
    )内通过动态选择特征图中的关键通道进行集中处理,提高效率。
  • CNN MoD
    保留了静态计算图,这提高了训练和推理的时间效率,而且不需要定制的
    CUDA
    内核、额外的损失函数或微调。
  • 通过将
    MoD
    与标准卷积交替使用,能够实现相等性能下的推理加速或相等推理速度下的性能提高。

CNN Mixture-of-Depths


MoD
由三个主要组件组成:

  1. 通道选择器:根据输入特征图与当前预测的相关性选择前
    \(k\)
    个最重要的通道。
  2. 卷积快:从现有架构(如
    ResNets

    ConvNext
    )中进行改编,旨在增强选定通道的特征。
  3. 融合算子:将处理后的通道加到特征图的前
    \(k\)
    个通道上。

通道选择器

通道选择器主要分为两个阶段:

  1. 自适应通道重要性计算:通过自适应平均池化压缩输入特征图,随后通过一个具有瓶颈设计的两层全连接网络进行处理,设定
    \(r = 16\)
    ,最后通过
    sigmoid
    激活函数生成一个分数向量
    \(\mathbf{s} \in \mathbb{R}^C\)
    ,量化了相应通道的重要性。
  2. Top-k
    通道选择与路由:利用重要性分数
    \(\mathbf{s}\)
    选择前
    \(k\)
    个通道输入卷积块处理,原始特征图
    \(X\)
    则直接传递融合算子。

这个选择过程使得通道选择器能够高效地管理计算资源,同时保持固定的计算图,从而实现动态选择要处理的通道。

动态通道处理

每个卷积块中处理的通道数量
\(k\)
由公式
\(k = \lfloor \frac{C}{c} \rfloor\)
决定,其中
\(C\)
表示该块的总输入通道数,
\(c\)
是一个超参数,用于确定通道减少的程度。例如在一个标准的
ResNet
瓶颈块中,通常处理
1024
个通道,设置
\(c = 64\)
会将处理减少到仅
16
个通道(
\(k = 16\)
)。

通过实验发现,超参数
\(c\)
应设置为第一卷积块中输入通道的最大数量,并在整个
CNN
中的每个
MoD
块中保持相同。例如,
ResNet

\(c = 64\)
MobileNetV2

\(c = 16\)

卷积块的最后一步涉及将处理后的通道与从自适应通道重要性计算中获得的重要性评分相乘,确保在训练过程中梯度能够有效地传递回通道选择器,这是优化选择机制所必需的。

融合机制

将处理后的特征添加到
\(X\)
的前
\(k\)
个通道中,保留其余未处理的通道。融合后的特征图
\(\bar{X}\)
具有与原始输入
\(X\)
相同的通道数
\(C\)
,从而保留了后续层所需的维度。

论文在实验中测试了多种将处理后的通道重新集成到特征图
\(X\)
中的策略,包括将处理后的通道添加回其原始位置,但结果并未显示任何改进。实验表明,始终在特征图中使用相同位置来处理信息似乎是有益的,将处理后的通道添加到后
\(k\)
个通道中得到了与添加到前
\(k\)
个通道时相当的结果。

集成到
CNN
结构

MoD
可以集成到各种
CNN
架构中,例如
ResNets

ConvNext

VGG

MobileNetV2,
这些架构被组织成包含多个相同类型(即输出通道数相同)的卷积块(
Conv-Blocks
)的模块。

实验表明,交替使用
MoD
块和标准卷积块在每个模块中是一种最有效的集成方法。需要注意的是,
MoD
块替换每第二个卷积块,从而保持原始架构的深度(例如,
ResNet50
中的
50
层)。每个模块以一个标准块开始,例如
BasicBlock
,然后是一个
MoD
块。

这种交替模式表明,网络能够处理显著的容量减少,只要定期进行全容量卷积。此外,该方法确保
MoD
块不会干扰通常发生在每个模块的第一个块中的空间维度缩减卷积。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

给网站免费升级HTTPS协议,可以通过申请并部署免费的SSL证书来实现。以下是一个详细的步骤指南:

一、申请免费SSL证书
选择证书颁发机构:
可以选择像JoySSL这样的公益项目,它提供免费、自动化的SSL/TLS证书颁发服务,适用于各种规模的网站。

免费SSL证书申请入口
提交申请:
登录所选证书颁发机构的官方网站,并创建一个账号,注:在注册的过程中需要填写注册码
230922
来获取免费证书申请权限。
根据需求选择合适的SSL证书类型,如单域名证书、多域名证书或通配符证书。
提交申请,并验证域名的所有权。这通常涉及DNS记录验证、文件验证或邮箱验证等方式。
下载证书:
验证通过后,证书颁发机构会签发证书。
下载收到的SSL证书文件,并解压备用。

二、部署SSL证书
登录服务器:
登录到托管网站的服务器。
上传证书:
将下载的SSL证书文件上传到服务器。
配置Web服务器:
根据所使用的Web服务器(如Apache、Nginx或IIS),修改相应的配置文件。
设置SSL模块和证书路径。
启用HTTPS监听端口(默认为443)。
重定向HTTP请求:
在Web服务器配置中设置规则,将所有HTTP请求自动重定向到对应的HTTPS URL。

三、更新网站链接
检查并更新内部链接:
确保网站上的所有内部链接(包括页面间的链接、CSS、JavaScript、图片等)都使用HTTPS协议。
如果存在混合内容(即页面通过HTTPS加载,但包含HTTP资源引用),浏览器可能会显示警告,影响用户体验和安全性。
使用开发者工具检查:
可以使用浏览器的开发者工具来检查并修正这些问题。

四、其他注意事项
备份网站数据:
在进行任何更改之前,备份网站数据以防万一。
定期更新证书:
免费SSL证书通常有一定的有效期。在证书到期之前,需要重新申请并部署新的证书。
监控和维护:
定期监控SSL证书的有效期和安全性。
使用在线SSL测试工具检查配置是否正确。

通过以上步骤,即可将网站从HTTP免费升级为HTTPS,从而享受加密通信带来的数据安全和用户信任提升。