Logo

site iconLeo Van | 范叶亮

京东、美团。现从事数据科学在安全风险领域的技术应用和产品设计。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Leo Van | 范叶亮 RSS 预览

凡人歌

2024-09-22 08:00:00

最近在没有快进的情况下看完了一部剧《凡人歌》,你说有多好看吗,也不是,只是共鸣多些,剧中的种种都好似在“点”我。这一度让我陷入了第二次职业焦虑,但比第一次职业焦虑(大概 5 年前)会显得没那么严重,没那么慌张。

悲观与乐观

我是一个做事比较喜欢先考虑最坏情况的人,或者说是一个对风险敏感的保守型人,我理解这算是一种悲观。但我也乐观,因为我认为只要努力总还是能够进步的,不是对自己的能力由多自信,只是感觉在合适的地方总会有一席用武之地。说好听了这可能是乐观,说不好听了这可能是自我感觉良好,甚至有种逆风而上的狂妄自大。不过实话实说狂妄应该不至于,年纪大了,各种情绪都会收敛很多,会觉得容忍比自由更重要。为什么聊到悲观和乐观,因为看下来这可能是引起我第二次职业焦虑的根因:对宏观(不够悲观)和微观(过于乐观)的认知偏差。

大学学了七年的管理,两门经济学基础课宏观经济学考的算是所有科目中最烂的,微观经济学到着实不赖。宏观经济学确实会比微观经济学难一些,毕竟有一只看不见的手老在调控这儿调控那儿的 。后疫情时代已经有三年左右,经济确实恢复的不理想,这点大家都有目共睹。但可能我仍是一叶障目,背后到底有多少问题可能也只是片面了解。看了付鹏在凤凰湾区财经论坛上的演讲,让我这个没啥宏观认知的人也能清晰地直面市场现状。

还有就是近期在招人,骚扰了很多有段时间(短则个把月,长则一两年)没太联系的前同事。聊下来,感觉能维持现状就算不错的了,还有些是在走下坡路。或许在当前的职位供需下,对大部分人来说可能最好的就是保持不变,对于发生变化的也只有降低个人预期才不至于出现空档。

剧中与剧外

写本文前也简单看了看豆瓣上的剧评,里面有不少提到这里面哪一个都不算是“凡人”。没有错,但剧的背景在北京,再加上编剧可能也会有一些夸张的描述,所以同更多人理解的“凡人”有很大差距,但对于我们这些在一线城市打拼的人来看,距离就没有那么远了。至少就我而言,剧中每个人的好我可能粘不上,但每个人的烦我却都似曾相识。

那伟从买了 50 万的宝马,到被裁员,到最后用宝马换了哈弗是第一个触动我的点。因为我最近也再考虑买车,想买一辆越野车,所以价格上也不算便宜。在我看来,虽然车是一个消费品,但他能够带来的快乐会高于我付出的金钱,我的小摩托就是这样的。有了车至少不会被谢美蓝嫌弃沈磊用小电驴送他上班,不过北京的车牌确实是一个难受的客观因素,摇又摇不中,租又不好租。但那伟后面遭遇的一连串不幸,虽然感觉有点过了,但也确实让我会重新审视多攒些钱早点把房贷还完是不是更好的选择。

那隽是我羡慕的对象,学历好、工作好、房子好、女朋友好。但这些在他女朋友看来可能都不算好,但我很能理解那隽,因为这些会让他感觉很安全。剧中给到介绍那伟和那隽小时候家庭条件一般,那隽通过自己的努力走到这样一个位置,已经算是相当的成功了。最后没能和李晓悦走到一起倒不是工作和生活的平衡的问题,反到他身体的问题是,我认为他俩的问题在于人生观的差异过大。在我换到第二份工作之后,体重涨的速度应该超过了我挣钱的速度,Work Life Balance 这个道理谁人不懂呢,但又有哪些人能做到呢。之前会开玩笑的说:“若不是生活所迫,谁愿意把自己搞得一身才华!”,但殊不知这一切还不都是为了碎银几两。

那隽在那伟失业的时候讽刺他没有核心竞争力,看似狂妄却也实在,我感觉也也是他在发现后浪越来越优秀的时候对自己的质问。上面提到我的这次的职业焦虑没那么严重和慌张,就是当我在问自己“你的核心竞争力是什么?”时,我能够更快的发现问题并尽可能快的去改变。上周在和 HR 同学聊时,一个观点也给我带来了一些启发,他说核心竞争力应该是一个组合,上面说的我去改变具体一点是想在技术方面多下功夫,但多下功夫了就真的有核心竞争力了吗?我们总在谈短板和长板,那么核心竞争力到底是短板不那么短,还是长板要足够长?或许都不是,核心竞争力应该是你真的去思考这个问题了,然后也付诸行动去改变了,不奏效大不了再换一个思路去改变。

沈磊被谢美蓝诟病最大的就是在他母亲生命最后的阶段他没有去麻烦别人,看起来是他的理性在驱使,但如果事情发生在他自己的父母身上呢,这里不聊谁对谁错,选择是困难的,尤其是在不得不选的时候。不喜欢麻烦别人这个事我也是这样,我甚至把能不麻烦就不麻烦别人作为我的人生信条。但我现在有了些许改变,有些人值得去麻烦,因为我知道他不图有一天可以麻烦回我。我称这样的人为知己,人生有知己,三两足矣。

牛马一生

终其一生,不为牛马,能做到的就真的不是凡人了,大多数人可能是牛马一生。当下流行的说法是“牛马三件套”:房贷、车贷、传宗接代,好多鸡汤都在说让大家活出自我,其实没必要把两者放在对立面。牛马一生可能才是常态,每个人都有欲望,或大或小,但终会有两难全的事情,所以自我和解会更可取些。

每个人的情况各有不同,每个时段的情况也各有不同,审时度势,不把自己玩死,才会有追求更好的机会。所以在一个时间点大胆地假设小心地求证,事后不要以“都是为了什么或谁”的理由去懊悔,别绑架他人也别绑架自己,向前看就好。凡人,但不要烦心。

Shell 调用方式 fork,exec 和 source (Run Shell with fork, exec and source)

2024-05-18 08:00:00

在 Linux 中调用一个脚本有多种方式,例如 fork,exec 和 source。其中 fork 为 Linux 系统调用,exec 和 source 均为 bash 内部命令。下面以 parent.shchild.sh 两个脚本演示不同调用方式的区别。

parent.sh 内容如下:

#!/bin/bash

echo "--------------------------------------------------"
echo "Before calling child.sh"
echo "--------------------------------------------------"
echo "PID for parent.sh: $$"

var="parent"
export var

echo "In parent.sh, set var=$var"
echo "In parent.sh, variable var=$var"

echo "--------------------------------------------------"
case $1 in
    exec)
        echo "Call child.sh using exec"
        exec ./child.sh ;;
    source)
        echo "Call child.sh using source"
        source ./child.sh ;;
    *)
        echo "Call child.sh using fork"
        ./child.sh ;;
esac

echo "After calling child.sh"
echo "--------------------------------------------------"
echo "PID for parent.sh: $$"
echo "In parent.sh, variable var=$var"
echo "--------------------------------------------------"

child.sh 内容如下:

#!/bin/bash

echo "--------------------------------------------------"
echo "PID for child.sh: $$"
echo "In child.sh, variable var=$var from parent.sh"

var="child"
export var

echo "In child.sh, set var=$var"
echo "In child.sh, variable var=$var"
echo "--------------------------------------------------"

为了确保脚本可执行,需为其添加执行权限:

chmod +x parent.sh child.sh

fork

fork 通过进程复制来创建一个新进程,新进程称为子进程,当前进程称为父进程。在 fork 之后,子进程拥有父进程的副本,但两者的 PID 不同,同时子进程也拥有父进程的所有属性,例如:环境变量、打开的文件描述符等。

通过 fork 调用是最普遍的方式。在当前终端中通过 ./run.sh 执行时,终端会新建一个子 shell 执行 run.sh,子 shell 执行时,父 shell 仍在运行,当子 shell 运行完毕后会返回父 shell。

运行如下命令进行 fork 方式调用测试:

./parent.sh fork

测试结果如下:

--------------------------------------------------
Before calling child.sh
--------------------------------------------------
PID for parent.sh: 7149
In parent.sh, set var=parent
In parent.sh, variable var=parent
--------------------------------------------------
Call child.sh using fork
--------------------------------------------------
PID for child.sh: 7150
In child.sh, variable var=parent from parent.sh
In child.sh, set var=child
In child.sh, variable var=child
--------------------------------------------------
After calling child.sh
--------------------------------------------------
PID for parent.sh: 7149
In parent.sh, variable var=parent
--------------------------------------------------

exec

exec 与 fork 不同,其不需要开启一个新的 shell 执行子脚本。使用 exec 执行一个新脚本后,父脚本中 exec 后的内容将不再执行。

运行如下命令进行 exec 方式调用测试:

./parent.sh exec

测试结果如下:

--------------------------------------------------
Before calling child.sh
--------------------------------------------------
PID for parent.sh: 9629
In parent.sh, set var=parent
In parent.sh, variable var=parent
--------------------------------------------------
Call child.sh using exec
--------------------------------------------------
PID for child.sh: 9629
In child.sh, variable var=parent from parent.sh
In child.sh, set var=child
In child.sh, variable var=child
--------------------------------------------------

source

source 同 exec 类似,也不需要开启一个新的 shell 执行子脚本。使用 source 执行一个新脚本后,父脚本中 source 后的内容可以继续执行。

运行如下命令进行 source 方式调用测试:

./parent.sh source

测试结果如下:

--------------------------------------------------
Before calling child.sh
--------------------------------------------------
PID for parent.sh: 10274
In parent.sh, set var=parent
In parent.sh, variable var=parent
--------------------------------------------------
Call child.sh using source
--------------------------------------------------
PID for child.sh: 10274
In child.sh, variable var=parent from parent.sh
In child.sh, set var=child
In child.sh, variable var=child
--------------------------------------------------
After calling child.sh
--------------------------------------------------
PID for parent.sh: 10274
In parent.sh, variable var=child
--------------------------------------------------

重定向和管道 (Redirect and Pipe)

2024-05-12 08:00:00

输入输出文件描述符

在 Linux 启动后,init 进程会创建 3 个特殊的文件描述符分配给输入输出。

文件描述符 英文描述 中文描述
0 stdin 标准输入
1 stdout 标准输出
2 stderr 标准错误

默认情况下,程序经由标准输入(stdin)从键盘读取数据,并将标准输出(stdout)和标准错误(stderr)显示在屏幕上。

在 Linux 中,init 是所有进程的父进程,所有子进程均会继承父进程的文件描述符。因此在 Linux 中执行的所有程序都可以从 stdin 获取输入,并将结果打印到 stdout 中,同时将错误信息打印到 stderr 中。

重定向

当我们不希望从键盘获取标准输入或将标准输出和标准错误显示在屏幕上时,则需要采用重定向。

输出重定向

输出重定向的使用方式如下:

cmd [1-n]> [文件/文件描述符/设备等]

假设当前目录下存在一个名为 yes.txt 的文件,且不存在名为 no.txt 的文件。执行如下命令:

ls yes.txt no.txt

由于 yes.txt 存在,这部分结果将输出到 stdout,同时由于 no.txt 不存在,这部分结果将输出到 stderr。命令的输出结果为:

ls: cannot access 'no.txt': No such file or directory
yes.txt

执行如下命令:

ls yes.txt no.txt 1> success.log 2> fail.log

此时屏幕上将不再显示任何信息,当前目录下会生成 success.logfail.log 两个文件。其中 1> success.log 表示将 stdout 重定向至 success.log2> fail.log 表示将 stderr 重定向至 fail.log。因此 success.log 中的内容为 yes.txtfail.log 中的内容为 ls: cannot access 'no.txt': No such file or directory

重定向过程中,stdout 的文件描述符 1 可以省略,但 stderr 的文件描述符 2 不可以省略。因此,当只重定向 stdout 时,可简写为:

ls yes.txt no.txt > success.log

此时屏幕上依旧会显示 stderr 的内容 ls: cannot access 'no.txt': No such file or directory,而 stdout 的内容则被重定向至 success.log 文件中。

在 Linux 中 &-/dev/null 是两个特殊的输出设备,均表示为空,输出到该设备相当于抛弃输出。因此如下两行命令分别会抛弃 stdout 和 stderr 的内容:

ls yes.txt no.txt 1>&-
ls yes.txt no.txt 2> /dev/null

& 可以表示当前进程中已经存在的描述符,&1 表示 stdout,&2 表示 stderr。因此我们可以将 stdout 和 stderr 重定向到相同文件:

ls yes.txt no.txt > out.log 2> out.log
ls yes.txt no.txt > out.log 2>&1

在上述两种方式中,第一种会导致 out.log 文件被打开两次,stdout 和 stderr 内容会相互覆盖。第二种由于 stderr 重定向给了 stdout,stdout 重定向给了 out.log,因此 out.log 仅被打开了一次。

使用 > 进行输出重定向时会先判断文件是否存在,如果存在会先删除再创建,不存在则直接创建,无论命令是否执行成功均会创建。使用 >> 进行重定向时,如果文件存在则会以添加方式打开,不存在则直接创建。

输入重定向

输入重定向的使用方式如下:

cmd [1-n]< [文件/文件描述符/设备等]

例如:

cat > out.txt < in.txt

此时命令将从 in.txt 文件中获取输入而非 stdin,并将结果重定向到 out.txt 文件中。

Here Document

Here Document 是一种特殊的重定向方式,可以用来将多行输入传递给命令,使用方式如下:

cmd << delimiter
    ...
delimiter

这会将中间的内容 ... 传递给命令。需要注意结尾处的 delimiter 的前后均不能包含任何字符,起始处的 delimiter 的前后空白字符将被忽略。最为常用的 delimiterEOF,但这不是必须的,例如:

wc -l << SOMETHING
第一行
第二行
第三行
SOMETHING

上述命令的输出结果为:

3

管道

管道 | 可以将一个命令的 stdout 作为下一个命令的 stdin 使用,但无法对 stderr 进行处理。因此管道也可以理解为重定向的一种特殊形式。

假设存在一个如下内容的 test.txt 文档:

Here is a test in line 1.
Here is another test in line 2.
Here is something else in line 3.

利用如下命令可以过滤出包含 test 字符的行并显示行号:

cat test.txt | grep -n "test"

上述命令的输出结果为:

1:Here is a test in line 1.
2:Here is another test in line 2.

如果希望同时将 stdout 和 stderr 重定向到下一个命令的 stdin,可以采用如下方式:

ls yes.txt no.txt 2>&1 | grep "No such file or directory"

上述命令的输出结果为:

ls: no.txt: No such file or directory

上述命令也可以简写为:

ls yes.txt no.txt |& grep "No such file or directory"

模型压缩和推理加速 (Model Compression &amp; Inference Acceleration)

2024-04-14 08:00:00

随着深度神经网络模型的复杂度越来越高,除了训练阶段需要大量算力外,模型推理阶段也较多的资源。在深度学习落地应用中,受部署环境的影响,尤其是在边缘计算场景中,有限的计算资源成为了复杂模型的应用壁垒。

复杂模型的部署问题突出表现在三个方面,如下图所示:

  1. 速度:实时响应效率的要求,过长的响应耗时会严重影响用户体验。
  2. 存储:有限的内存空间要求,无法加载超大模型的权重从而无法使用模型。
  3. 能耗:移动场景的续航要求,大量的浮点计算导致移动设备耗电过快。

针对上述三类问题,可以从模型压缩推理加速两个角度出发,在保持一定模型精度的情况下,让模型速度更快、体积更小、能耗更低。

模型压缩

常用的模型压缩方法有如下几种类型:

剪裁

剪裁Pruning)的核心思想是在尽量保持模型精度不受影响的前提下减少网络的参数量,例如减少网络中连接或神经元的数量,如下图所示:

剪裁最常用的步骤如下:

  1. 训练:在整个剪裁过程中,该步骤主要为预训练过程,同时为后续的剪裁工作做准备。
  2. 修剪:通过具体的方法对网络进行剪裁,并对网络重新进行评估以确定是否符合要求。
  3. 微调:通过微调恢复由于剪裁对模型带来的性能损耗。

对网络进行剪裁的具体方法可以分为非结构化剪裁结构化剪裁

非结构化剪裁是细粒度的剪裁方法,一般通过设定一个阈值,高于该阈值的权重得以保留,低于该阈值的权重则被去除。非结构化剪裁虽然方法简单、模型压缩比高,但也存在诸多问题。例如:全局阈值设定未考虑不同层级的差异性,剪裁信息过多有损模型精度且无法还原,剪裁后的稀疏权重矩阵需要硬件层支持方可实现压缩和加速的效果等。

结构化剪裁是粗粒度的剪裁方法,例如对网络层、通道、滤波器等进行剪裁。在滤波器剪裁中,通过评估每个滤波器的重要性(例如:Lp 范数)确定是否保留。结构化剪裁算法相对复杂、控制精度较低,但剪裁策略更为有效且不需要硬件层的支持,可以在现有深度学习框架上直接应用。

量化

神经网络中的计算通常采用浮点数(FP32)进行计算,量化Quantization)的基本思想是将浮点计算替换为更低比特(例如:FP16,INT8 等)的计算,从而降低模型体积加快模型推理速度。

数值的量化可以看做一个近似过程,主要可以分为两类:

  1. 定点近似:通过缩小浮点数表示中指数部分和小数部分的位宽实现。映射过程不需要额外的参数,实现相对简单,但针对较大数值的精度损失较大。
  2. 范围近似:通过统计分析,经过缩放和平移映射浮点数。映射过程需要存储额外的参数,计算时需要先反量化,计算相对复杂,但精度更高。

范围近似又可以分为线性映射非线性映射两种。

线性映射将浮点数映射到量化空间时采用如下计算公式:

$$ \begin{aligned} r &= S \left(q - Z\right) \\ q &= round \left(\dfrac{r}{S} + Z\right) \end{aligned} $$

其中,$r, q$ 分别表示量化前和量化后的值,$S, Z$ 为量化系数。一般化的非对称映射如下图所示:

其中,

$$ \begin{aligned} S &= \dfrac{r_{max} - r_{min}}{q_{max} - q_{min}} \\ Z &= q_{min} - \dfrac{r_{min}}{S} \end{aligned} $$

非线性映射考虑了数据本身的分布情况。以分位量化方法为例,其基本思想是通过分位点对数据进行划分,使得各个区间之间的数据量相等,然后将同一个区间的数据映射为相同值,从而实现量化。

量化粒度是指控制多少个待量化的参数共享一组量化系数,通常粒度越大,精度损失越大。以 Transformer 模型为例,不同粒度的量化方式如下图所示:

其中,$d$ 为模型大小与隐层维度之比,$h$ 为多头自注意中的头数。

模型量化分为两种:

  1. 权重量化:即对网络中的权重执行量化操作。数值范围与输入无关,量化相对容易。
  2. 激活量化:即对网络中不含权重的激活类操作进行量化。输出与输入有关,需要统计数据动态范围,量化相对困难。

根据是否进行训练可以将量化方法分为两大类,如下图所示:

  1. 训练后量化(Post Traning Quantization,PTQ):方法简单高效,无需重新训练模型。根据是否量化激活又分为:
    • 动态:仅量化权重,激活在推理阶段量化,无需校准数据。
    • 静态:量化权重和激活,需要校准数据。
  2. 量化感知训练(Quantization Aware Traning,QAT):方法相对复杂,需要在模型中添加伪量化节点模拟量化,需要重新训练模型。

三种不同量化方法之间的差异如下图所示:

神经结构搜索

神经结构搜索Network Architecture Search,NAS)旨在以一种自动化的方式,解决高难度的复杂神经网络设计问题。根据预先定义的搜索空间,神经结构搜索算法在一个庞大的神经网络集合中评估结构性能并寻找到表现最佳的网络结构。整个架构如下图所示:

近年来基于权重共享的结构搜索方法受到广泛关注,搜索策略和性能评估高度相关,因此两者往往合为一体表示。

搜索空间包含了所有可搜索的网络结构,越大的搜索空间可以评估更多结构的性能,但不利于搜索算法的收敛。搜索空间从搜索方式角度分为两种:

  1. 全局搜索空间:通过链式、跳跃链接、分支等方式搜索整个网络结构。
  2. 基于结构单元的搜索空间:仅搜索结构单元,减少搜索代价,提高结构的可迁移性。

搜索策略即如何在搜索空间根据性能评估选择最优的网络结构。具体包含随机搜索贝叶斯优化进化算法强化学习基于梯度的方法

性能评估最简单的方法就是对数据划分验证集,针对不同的网络结构重新训练并评估其在验证集上的表现。但这种方法所需的计算成本很高,无法在实践中落地应用。一些基于权重共享的结构搜索方法能够一定程度地加速搜索,因此考虑搜索策略和性能评估任务的相关性,在当前架构中往往将这两部分统一表述为搜索策略。

知识蒸馏

知识蒸馏Knowledge Distillation,KD)是一种教师-学生(Teacher-Student)训练结构,通常是已训练好的教师模型提供知识,学生模型通过蒸馏训练来获取教师的知识。它能够以轻微的性能损失为代价将复杂教师模型的知识迁移到简单的学生模型中。

知识蒸馏架构如下图所示:

上半部分为教师模型,下半部分为学生(蒸馏)模型。将教师模型的输出作为软标签与学生模型的软预测计算蒸馏损失,将真实的硬标签与学生模型的硬预测计算学生损失,最终将两种损失结合训练学生模型。

论文 1 给出了软标签的计算公式:

$$ q_i = \dfrac{\exp \left(z_i / T\right)}{\sum_j \exp \left(z_j / T\right)} $$

其中,$T$ 为温度系数,用来控制输出概率的软化程度。不难看出当 $T = 1$ 时,公式的输出即为网络输出 $Softmax$ 的类概率。$T$ 越大,$Softmax$ 的类概率分布越平滑,这可以让学生模型学习到教师模型对负标签的归纳信息。

参考

可供使用的模型压缩库有:

  1. PyTorch:剪裁量化神经网络搜索知识蒸馏
  2. TensorFlow:剪裁量化
  3. PaddleSlim:支持剪裁、量化、神经网络搜索、知识蒸馏,适配 PaddlePaddle 框架。
  4. PocketFlow:支持剪裁、量化,适配 TensorFlow 框架。
  5. NNI:支持剪裁、量化、神经网络搜索,适配 TensorFlow、PyTorch 等框架。
  6. TinyNeuralNetwork:支持剪裁、量化,适配 PyTorch 框架。
  7. DeepSpeed:支持剪裁、量化,适配 PyTorch 框架。
  8. Intel Neural Compressor:支持剪裁、量化、知识蒸馏,适配 TensorFlow、PyTorch、ONNX 等框架。
  9. Neural Network Compression Framework:支持剪裁、量化,适配 TensorFlow、PyTorch、ONNX、OpenVINO 等框架。

推理加速

硬件加速

硬件加速是指将计算交由专门的硬件以获得更快的速度。在深度学习领域最简单的体现就是利用 GPU 进行推理会比利用 CPU 更快。除此之外,在给定的硬件环境中,利用针对性优化的推理框架可以更充分的利用硬件特性提升预测效率。

并行计算

并行计算是指将计算的过程分解成小部分,以并发方式运行实现计算效率的提升。在模型训练和推理阶段,主流的并行方式有:

  1. 数据并行Data Parallel):将数据集切分为多份,每个设备负责其中一部分。
  2. 流水线并行Pipeline Parallel):将模型纵向拆分,每个设备只包含模型的部分层,数据在一个设备完成计算后传递给下一个设备。
  3. 张量并行Tensor Parallel):将模型横向拆分,将模型的每一层拆分至不同设备,每一层计算都需要多个设备合作完成。

参考

可供使用的推理加速库有:

平台 CPU GPU & NPU 框架 系统
TensorRT 服务端 不支持 CUDA TensorFlow
PyTorch
ONNX 等
Windows
Linux 等
Triton 服务端 x86
ARM
CUDA TensorFlow
PyTorch
ONNX 等
Windows
Linux 等
OpenVINO 服务端 Intel
ARM
OpenCL TensorFlow
PyTorch
PaddlePaddle
ONNX 等
Windows
Linux
macOS 等
Paddle Inference 服务端 x86
ARM
CUDA PaddlePaddle Windows
Linux
macOS 等
MNN 服务端
移动端
x86
ARM
CUDA
OpenCL
Vulkan
Metal
HiAI
CoreML
TensorFlow
ONNX 等
Windows
Linux
macOS
Android
iOS 等
TNN 服务端
移动端
x86
ARM
CUDA
OpenCL
Metal
HiAI
CoreML
TensorFlow
PyTorch
ONNX 等
Windows
Linux
macOS
Android
iOS 等
Tensorflow Lite 移动端 ARM OpenCL
Metal
NNAPI
Core ML
Tensorflow Android
iOS 等
PyTorch Mobile 移动端 ARM Vulkan
Metal
NNAPI
PyTorch Android
iOS 等
Paddle Lite 移动端 x86
ARM
OpenCL
Metal
NNAPI
PaddlePaddle Android
iOS 等
ncnn 移动端 x86
ARM
Vulkan TensorFlow
PyTorch
ONNX 等
Android
iOS 等

本文未对模型压缩和推理加速进行深入展开,仅作为工业实践的基础概念解释。本文参考了大量前人之作,在此一并引用 234567


  1. Hinton, G., Vinyals, O., & Dean, J. (2015). Distilling the knowledge in a neural network. arXiv preprint arXiv:1503.02531↩︎

  2. 模型压缩概述:https://paddlepedia.readthedocs.io/en/latest/tutorials/model_compress/model_compress.html ↩︎

  3. Large Transformer Model Inference Optimization:https://lilianweng.github.io/posts/2023-01-10-inference-optimization/ ↩︎

  4. 深度学习模型压缩方法:剪枝:https://zhuanlan.zhihu.com/p/609126518 ↩︎

  5. 模型量化原理与实践:https://robot9.me/ai-model-quantization-principles-practice/ ↩︎

  6. 李航宇, 王楠楠, 朱明瑞, 杨曦, & 高新波. (2021). 神经结构搜索的研究进展综述. 软件学报, 33(1), 129-149. ↩︎

  7. 黄震华, 杨顺志, 林威, 倪娟, 孙圣力, 陈运文, & 汤庸. (2022). 知识蒸馏研究综述. 计算机学报, 45(3). ↩︎

我们需要多少种编程语言 (How Many Programming Languages do We Need)

2024-02-09 08:00:00

编程语言「只是」达成目标的工具,这是我一直推崇的说法,因为我认为达成目标更重要的在于个人思考,编程语言不过是个「工具」,选哪个并没有那么重要。现在我越来越认为这个「工具」的选择还是很重要的,因为编程语言之于目标实现的可能性和效率都制约了目标的最终达成。

以个人数据科学的工作背景,结合我的编程语言学习路径,尝试回答一下我们需要多少种编程语言这个问题。我的编程语言学习路径大致如下:

语言分类

在我的编程语言学习路径中,各个编程语言之间还是存在很大差异的,以最早接触的 Logo 为例,其除了教学目的之外似乎就真的没有什么大用途了。这些编程语言可以大致划分为如下三种类型:

  • 通用编程语言(General-purpose Programming Language,GPL):Basic,C,C++,Python,JavaScript,TypeScript,Java,Rust
  • 领域特定语言(Domain-Specific Language,DSL):Logo,CSS,Matlab,R,SQL
  • 标记语言(Markup Language):HTML,Markdown

领域特定语言和标记语言就不用多说了,其应用范围有限。通用编程语言虽然定义为通用,但各自也有擅长和不擅长的领域,需要针对实际场景进行选择。

领域偏好

领域偏好是指你所从事的领域内大家对编程语言的使用偏好,这对于语言的选择至关重要,因为靠一己之力改变领域内大多数人的选择还是相当有难度的,从众可以极大地充分利用前人的成果降低工作的成本。

数据科学

数据科学可选的编程语言有 Python、R 和 Matlab 等,从技多不压身的角度出发肯定是掌握的越多越好,但精力终归是有限的。Matlab 在版权、仿真等方面具有一定的特殊性,Python 和 R 在数据科学中的竞争可谓是旷日持久,相关对比也数不胜数 123

如果非要二选一我现在会选择 Python,因为其作为通用编程语言在将数据科学和工程代码结合时会展现出更多的优势。但在一些特定领域,例如生物信息学,R 的采用率会更高。一些新起之秀例如 Julia 和 Mojo 仍需要进一步观测其发展,过早地大面积使用新语言可能会面临各种风险。

后端

企业级后端应用中 Java 应该是首选,对一些高性能场景 C/C++、Rust 可能更加适合。

在实际工作中,Python 依旧可以作为一个不错的后端语言选择。Python 在 HTTP 和 RPC 接口、高并发、开源组件 SDK 支持等方面都不错的表现。Python 作为一种解释型语言,时常被诟病运行慢,这确实是解释型语言的一个问题。不过现在很多流行扩展包都是基于 C/C++ 构建,并且从 3.13 版本开始已经可选去除全局解释锁,这些都会使 Python 的性能变得越来越好。个人认为 Python 运行慢的另一个原因是使用者对其理解仍不够深入,使用的技巧仍有待提升,《流畅的 Python》可以让你对 Python 有更深入的认识。

前端

前端是程序与用户进行交互的必经之路,HTML,CSS 和 JavaScript 可以算得上前端三剑客了,分别负责元素的定义、样式和交互。随着 Node.js 的发展,JavaScript 也可以作为后端语言使用。TypeScript 作为 JavaScript 的超集,扩展了 JavaScript 的功能和特性,同时随着 React 和 Vue 框架的出现,前端的发展可谓是盛况空前。

Python 此时就真的很难插入一脚了,不过话无绝对,基于 WebAssembly 技术 Python 也可以在前端运行。WebAssembly 设计的目的是为了提升前端代码的运行效率,而在这方面从实践上 4 来看 Rust 更受青睐。

客户端

在 Apple 和 Google 两大移动阵营中,iOS(iPadOS 和 tvOS)系统的首选语言为 Swift,Android 系统的首选语言为 Kotlin。在 Apple 和 Microsoft 两大桌面阵营中,macOS 系统的首选语言为 Swift,Windows 系统的首选语言为 .Net。原生语言可以让应用更好的适配对应的系统,但引入的问题就是相同应用针对不同系统适配的成本增加。针对大公司的核心应用确实有这个必要,但是普通场景,「跨平台」则会更吸引人。

在之前的博客中有对桌面端的跨平台框架作简要分析,但结合当下移动端的市场占比,基于前端技术的跨平台解决方案会是一个不错的选择。

脚本

脚本可以说就只是程序员自己为了方便而产生的需求,不出意外它应该只会出现在命令行的黑框框中。在类 Unix 系统中,Shell 是一个通用且不需要额外安装扩展的不错之选。除此之外,什么 Python、Perl、Ruby、Lua 都在不同的场景中发光发热,相信我们将处于并将长期处于脚本语言的五代十国中。

嵌入式

早期的嵌入式我认为是一个相对专业垂直的领域,由于操作的对象更加底层,所以使用的语言也会更底层一些,例如 C/C++。随着硬件的不断发展,开发板、智能设备、机器人都在逐步走入更多程序员的视野,硬件性能的提升也使得 Python 等高级语言可以作为嵌入式开发的工具。

个人偏好

领域偏好是站在客观的角度指导我们如何选择语言,但谁还没有些小脾气呢?以自己为例,最早接触的数据分析语言是 Matlab,我的本科和研究生的论文都是用 Matlab 完成的,当时是由于实验室都在用它(领域偏好),但我个人并不很喜欢它。虽然上面我也承认在 Python 和 R 的大战中,如果非让我选一个我会选择 Python,但这也剥夺不了我对 R 的钟爱。R 的管道符 |> 用起来就是舒服,虽然 Python 第三方也提供了类似的扩展包 siuba,但模仿终归还是模仿。

在一定程度上坚持自己的个性还是可能会有些益处的。仍以自己为例,钟爱 R 的我在了解到 RMarkdown 之后让我喜欢上了可重复性研究(更多细节可参见之前的博客)。再到之后的 Quarto,最终我将这些内容融入工作之中,开发工作中适用的产品。可以说如果仅是基于领域偏好彻底拥抱 Python 而抛弃 R,那么我会错过不少优秀的项目和工具。还有不得不提的 ggplot2,图形语法的最好践行者,Python 中 Matlab 的遗留瑰宝 matplotlib 我只能说很强大但我不喜欢用,至于 Python 第三方提供的类似扩展包 plotnine,模仿终归还是模仿。

真实需要

聊了这么多,到底我们需要多少种编程语言?这取决于你到底想要啥?

如果想作为一个安分守己的数据科学工作者,Python 能撑起你 90% 的需求,多花一些时间去了解业务可能比你多学一门编程语言的收益要大得多。

如果你不够安分,不想深藏于后端,甚至有技术变现的想法,那么前端交互自然不可缺失。此时 JavaScript/TypeScript 应该是个不错的选择,毕竟你不是要成为一个专业的前端或 APP 开发人员,跨平台才应该是我们应该偏好的重点,毕竟一套代码处处可用,变现的速度就可以杠杠的了。

当然我相信除了眼前的这些苟且,大家还是有更远大抱负的,那么在这条布满荆棘的路上你会遇到更复杂的问题。性能的提升我会选择 Rust,毕竟人家除了能提升后端还能提升前端,何乐而不为呢?

综上所述,作为数据科学工作的从业者,我将以并将长期以如下四句作为我编程语言选择的重要指导方针:

人生苦短,我用 Python

想要甜些,得上 JavaScript / TypeScript

开心的话,就用 R

再想更屌,恶补 Rust