2025-05-25 08:00:00
为了在本地局域网环境中摆脱 IP 用上域名(纯属闲来无事瞎鼓捣),购入了 leovan.dev
域名。想着把各种服务都映射到不同的二级域名上,这样就可以不用 IP 和端口了,岂不完美。然,问题这就来了。
域名是在 Cloudflare 上申请的,在 Cloudflare 上使用 Page 服务部署网站就可以白嫖他家的证书,还能自动帮你续期,比如当前的站点就是使用 Page 进行部署的。但你要是想生成证书下载下来使用,就会很麻烦,因为证书的有效期只有三个月,手动续期再加上各种替换操作就不太方便了。
这时候就要请出我们的 acme.sh 了,除了支持各种桌面和服务器操作系统外,还支持 OpenWrt 路由器系统。
使用 acme.sh 申请免费证书需要使用 DNS 验证对域名的所有权,本文以 Cloudflare 为例,其它 DNS 请参考官方文档。Cloudflare 支持两种方式,一种是使用 API Token,另一种是使用全局 API Key,这里我们以 API Token 为例。
进入 API Token 页面,单击 按钮,在 API 令牌模板中选择 编辑区域 DNS
,单击 按钮。在 权限
中添加 区域 - DNS - 编辑
和 区域 - 区域 - 读取
的权限,在 区域资源
中根据你的需求选择对应的 特定区域
,例如 leovan.dev
,或为了省事选择 所有区域
也可以,如下图所示:
创建完毕后会生成 Token,将 Token 保存为 CF_Token="xxxxxxxxx"
,注意该 Token 在 Cloudflare 中不会再展示。
之后从 Cloudflare 账户主页进入对应的域名详情页面,在右下角可以找到 API 的区域 ID 和账户 ID 两个代码,如下图所示:
将区域 ID 保存为 CF_Zone_ID="xxxxxxxxx"
,将账户 ID 保存为 CF_Account_ID="xxxxxxxxx"
。
Opwnert 使用 uHTTPd 作为默认的 Web 服务器。正如官网上说的,这是一个轻量极了的 Web 服务器,以至于不支持反向代理。
警告
那就安装一个 Nginx 吧,不是说不行,只是 Nginx 和 uHTTPd 存在冲突,你需要把路由器的 LuCI 也切换到 Nginx 上,麻烦不说,后续如果有更新还有可能又会变回 uHTTPd。自己搞了下,差点登录不进去 Web 页面,遂放弃。
但这不影响我们先把路由器的域名 router.leovan.dev
映射到 192.168.100.1
上先用起来。
通过 系统 - 软件包
或命令行安装 acme.sh 相关软件包:
opkg install acme acme-acmesh-dnsapi luci-app-acme luci-i18n-acme-zh-cn luci-ssl-openssl
安装完毕后可以在 服务
菜单下找到 ACME 证书
子菜单,进入后在 ACME 全局配置
中输入 电子邮件帐户
,勾选 启用调试日志记录
,如下图所示:
在 证书配置
中删除默认的配置,在下方输入框中输入配置名称,例如 leovan_dev
,如下图所示:
单击 按钮打开配置对话框。在 常规设置
中勾选 已启用
,输入所需的域名,选择验证方式为 DNS
,其它保持默认,如下图所示:
在 DNS 质询验证
中选择对应的 DNS API(本文使用 CloudFlare.com
),并将上文中的 CF_Token="xxxxxxxxx"
、CF_Zone_ID="xxxxxxxxx"
和 CF_Account_ID="xxxxxxxxx"
填写到对应的位置,其它保持默认,如下图所示:
在 高级设置
中根据自己的需求选择 密钥长度
(本文使用 ECC 256 位
),其它保持默认,如下图所示:
单击 按钮,并在 ACME 证书
页面单击 按钮。
稍等片刻后,如果运行正常则可以在 证书
中看到对应域名的证书,如下图所示:
同时,系统会启动自动续签,在 系统 - 计划任务
中可以看到添加了如下一条记录:
0 0 * * * /etc/init.d/acme renew
通过 系统 - 软件包
或命令行安装 uHTTPd 的管理界面:
opkg install luci-app-uhttpd luci-i18n-uhttpd-zh-cn
安装完毕后可以在 服务
菜单下找到 uHTTPd
子菜单,进入后在 MAIN - 常规设置
中添加 HTTPS 监听,如下图所示:
在 HTTPS 证书
中选择上文中生成的证书(本文为 /etc/ssl/acme/leovan.dev.fullchain.crt
),如下图所示:
在 HTTPS 私钥
中选择上文中生成的私钥(本文为 /etc/ssl/acme/leovan.dev.key
),如下图所示:
在 uHTTPd
页面单击 按钮。
在 Cloudflare 中将 router.leovan.dev
解析到 192.168.100.1
上后,分别通过 http://192.168.100.1
、https://192.168.100.1
和 https://router.leovan.dev
访问路由器,如下图所示:
可以看出通过 router.leovan.dev
域名进行访问已经实现了 HTTPS 安全访问。
由于在 OpenWrt 上搞 Nginx 有些麻烦,此时此刻,恰巧手里还有一台群晖的 NAS,恰巧群晖默认支持反向代理服务器,这一切的一切不就又双叒叕完美了。
稍显遗憾的是在群晖中没有像 OpenWrt 那样的工具可以直接使用,这里就只能用脚本的方式手搓部署了。首先通过命令行 SSH 登录群晖,并切换到 root 用户:
sudo su
cd /root
由于群晖没有 crontab,因此需要使用如下命令强制安装,根据实际情况修改命令中的电子邮箱:
curl https://get.acme.sh | sh -s email=[email protected] --force
当控制台显示 Install success!
后表示安装成功。进入 /root/.acme.sh
目录,修改 account.conf
文件:
cd /root/.acme.sh
vi account.conf
account.conf
文件示例如下,请根据上文中的内容修改 CF_Token
、CF_Zone_ID
和 CF_Account_ID
配置项:
export CF_Token="xxxxxxxxx"
export CF_Zone_ID="xxxxxxxxx"
export CF_Account_ID="xxxxxxxxx"
LOG_FILE="/root/.acme.sh/acme.sh.log"
LOG_LEVEL=1
AUTO_UPGRADE="1"
ACCOUNT_EMAIL="[email protected]"
UPGRADE_HASH="xxxxxxxxx"
运行如下命令申请证书:
./acme.sh --set-default-ca --server letsencrypt
./acme.sh --issue --dns dns_cf --keylength ec-256 -d leovan.dev -d *.leovan.dev
正常情况下,申请的证书将保存在 /root/.acme.sh/leovan.dev_ecc
目录下。
运行如下命令将证书部署到群晖系统中:
export SYNO_USE_TEMP_ADMIN=1
./acme.sh --deploy --deploy-hook synology_dsm -d leovan.dev -d *.leovan.dev
此时进入群晖的 控制面板 - 安全性 - 证书
中,可以看到 leovan.dev
证书已经部署到系统中并作为默认证书,如下图所示:
在群晖中创建计划任务来实现自动更新并部署证书,在 控制面板 - 计划任务
中选择 新建 - 计划的任务 - 用户自定的脚本
。在 常规
中设置 任务名称
,选择 用户账号
为 root
,如下图所示:
在 计划
中设置执行的周期,由于 acme.sh 在证书到期前一个月会发起重新申请,因此可以将计划任务周期设置为每周,如下图所示:
在 任务设置
中设置 用户自定义的脚本
:
/root/.acme.sh/acme.sh --cron --home /root/.acme.sh
根据个人需要可以勾选 通过电子邮件发送运行详情
,如下图所示:
在 Cloudflare 中将 nas.leovan.dev
解析到 192.168.100.10
上后,在群晖的 登录门户 - 高级
单击 按钮打开对话框,单击 按钮,根据下图添加配置:
分别通过 http://192.168.100.10:500 和 https://nas.leovan.dev 访问群晖,如下图所示:
可以看出通过 nas.leovan.dev
域名进行访问已经实现了 HTTPS 安全访问。
提示
针对局域网其它机器上的 Web 服务,可以先将域名解析到群晖的 IP 上,再利用群晖的反向代理转发到对应机器的 Web 服务上。对于非 Web 服务,将域名直接解析到对应的机器上即可。
2024-09-22 08:00:00
最近在没有快进的情况下看完了一部剧《凡人歌》,你说有多好看吗,也不是,只是共鸣多些,剧中的种种都好似在“点”我。这一度让我陷入了第二次职业焦虑,但比第一次职业焦虑(大概 5 年前)会显得没那么严重,没那么慌张。
我是一个做事比较喜欢先考虑最坏情况的人,或者说是一个对风险敏感的保守型人,我理解这算是一种悲观。但我也乐观,因为我认为只要努力总还是能够进步的,不是对自己的能力由多自信,只是感觉在合适的地方总会有一席用武之地。说好听了这可能是乐观,说不好听了这可能是自我感觉良好,甚至有种逆风而上的狂妄自大。不过实话实说狂妄应该不至于,年纪大了,各种情绪都会收敛很多,会觉得容忍比自由更重要。为什么聊到悲观和乐观,因为看下来这可能是引起我第二次职业焦虑的根因:对宏观(不够悲观)和微观(过于乐观)的认知偏差。
大学学了七年的管理,两门经济学基础课宏观经济学考的算是所有科目中最烂的,微观经济学到着实不赖。宏观经济学确实会比微观经济学难一些,毕竟有一只看不见的手老在调控这儿调控那儿的 。后疫情时代已经有三年左右,经济确实恢复的不理想,这点大家都有目共睹。但可能我仍是一叶障目,背后到底有多少问题可能也只是片面了解。看了付鹏在凤凰湾区财经论坛上的演讲,让我这个没啥宏观认知的人也能清晰地直面市场现状。
还有就是近期在招人,骚扰了很多有段时间(短则个把月,长则一两年)没太联系的前同事。聊下来,感觉能维持现状就算不错的了,还有些是在走下坡路。或许在当前的职位供需下,对大部分人来说可能最好的就是保持不变,对于发生变化的也只有降低个人预期才不至于出现空档。
写本文前也简单看了看豆瓣上的剧评,里面有不少提到这里面哪一个都不算是“凡人”。没有错,但剧的背景在北京,再加上编剧可能也会有一些夸张的描述,所以同更多人理解的“凡人”有很大差距,但对于我们这些在一线城市打拼的人来看,距离就没有那么远了。至少就我而言,剧中每个人的好我可能粘不上,但每个人的烦我却都似曾相识。
那伟从买了 50 万的宝马,到被裁员,到最后用宝马换了哈弗是第一个触动我的点。因为我最近也再考虑买车,想买一辆越野车,所以价格上也不算便宜。在我看来,虽然车是一个消费品,但他能够带来的快乐会高于我付出的金钱,我的小摩托就是这样的。有了车至少不会被谢美蓝嫌弃沈磊用小电驴送他上班,不过北京的车牌确实是一个难受的客观因素,摇又摇不中,租又不好租。但那伟后面遭遇的一连串不幸,虽然感觉有点过了,但也确实让我会重新审视多攒些钱早点把房贷还完是不是更好的选择。
那隽是我羡慕的对象,学历好、工作好、房子好、女朋友好。但这些在他女朋友看来可能都不算好,但我很能理解那隽,因为这些会让他感觉很安全。剧中给到介绍那伟和那隽小时候家庭条件一般,那隽通过自己的努力走到这样一个位置,已经算是相当的成功了。最后没能和李晓悦走到一起倒不是工作和生活的平衡的问题,反到他身体的问题是,我认为他俩的问题在于人生观的差异过大。在我换到第二份工作之后,体重涨的速度应该超过了我挣钱的速度,Work Life Balance 这个道理谁人不懂呢,但又有哪些人能做到呢。之前会开玩笑的说:“若不是生活所迫,谁愿意把自己搞得一身才华!”,但殊不知这一切还不都是为了碎银几两。
那隽在那伟失业的时候讽刺他没有核心竞争力,看似狂妄却也实在,我感觉也也是他在发现后浪越来越优秀的时候对自己的质问。上面提到我的这次的职业焦虑没那么严重和慌张,就是当我在问自己“你的核心竞争力是什么?”时,我能够更快的发现问题并尽可能快的去改变。上周在和 HR 同学聊时,一个观点也给我带来了一些启发,他说核心竞争力应该是一个组合,上面说的我去改变具体一点是想在技术方面多下功夫,但多下功夫了就真的有核心竞争力了吗?我们总在谈短板和长板,那么核心竞争力到底是短板不那么短,还是长板要足够长?或许都不是,核心竞争力应该是你真的去思考这个问题了,然后也付诸行动去改变了,不奏效大不了再换一个思路去改变。
沈磊被谢美蓝诟病最大的就是在他母亲生命最后的阶段他没有去麻烦别人,看起来是他的理性在驱使,但如果事情发生在他自己的父母身上呢,这里不聊谁对谁错,选择是困难的,尤其是在不得不选的时候。不喜欢麻烦别人这个事我也是这样,我甚至把能不麻烦就不麻烦别人作为我的人生信条。但我现在有了些许改变,有些人值得去麻烦,因为我知道他不图有一天可以麻烦回我。我称这样的人为知己,人生有知己,三两足矣。
终其一生,不为牛马,能做到的就真的不是凡人了,大多数人可能是牛马一生。当下流行的说法是“牛马三件套”:房贷、车贷、传宗接代,好多鸡汤都在说让大家活出自我,其实没必要把两者放在对立面。牛马一生可能才是常态,每个人都有欲望,或大或小,但终会有两难全的事情,所以自我和解会更可取些。
每个人的情况各有不同,每个时段的情况也各有不同,审时度势,不把自己玩死,才会有追求更好的机会。所以在一个时间点大胆地假设小心地求证,事后不要以“都是为了什么或谁”的理由去懊悔,别绑架他人也别绑架自己,向前看就好。凡人,但不要烦心。
2024-05-18 08:00:00
在 Linux 中调用一个脚本有多种方式,例如 fork,exec 和 source。其中 fork 为 Linux 系统调用,exec 和 source 均为 bash 内部命令。下面以 parent.sh
和 child.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 之后,子进程拥有父进程的副本,但两者的 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 与 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 同 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
--------------------------------------------------
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.log
和 fail.log
两个文件。其中 1> success.log
表示将 stdout 重定向至 success.log
,2> fail.log
表示将 stderr 重定向至 fail.log
。因此 success.log
中的内容为 yes.txt
,fail.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 是一种特殊的重定向方式,可以用来将多行输入传递给命令,使用方式如下:
cmd << delimiter
...
delimiter
这会将中间的内容 ...
传递给命令。需要注意结尾处的 delimiter
的前后均不能包含任何字符,起始处的 delimiter
的前后空白字符将被忽略。最为常用的 delimiter
为 EOF
,但这不是必须的,例如:
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"
2024-04-14 08:00:00
随着深度神经网络模型的复杂度越来越高,除了训练阶段需要大量算力外,模型推理阶段也较多的资源。在深度学习落地应用中,受部署环境的影响,尤其是在边缘计算场景中,有限的计算资源成为了复杂模型的应用壁垒。
复杂模型的部署问题突出表现在三个方面,如下图所示:
针对上述三类问题,可以从模型压缩和推理加速两个角度出发,在保持一定模型精度的情况下,让模型速度更快、体积更小、能耗更低。
常用的模型压缩方法有如下几种类型:
剪裁(Pruning)的核心思想是在尽量保持模型精度不受影响的前提下减少网络的参数量,例如减少网络中连接或神经元的数量,如下图所示:
剪裁最常用的步骤如下:
对网络进行剪裁的具体方法可以分为非结构化剪裁和结构化剪裁。
非结构化剪裁是细粒度的剪裁方法,一般通过设定一个阈值,高于该阈值的权重得以保留,低于该阈值的权重则被去除。非结构化剪裁虽然方法简单、模型压缩比高,但也存在诸多问题。例如:全局阈值设定未考虑不同层级的差异性,剪裁信息过多有损模型精度且无法还原,剪裁后的稀疏权重矩阵需要硬件层支持方可实现压缩和加速的效果等。
结构化剪裁是粗粒度的剪裁方法,例如对网络层、通道、滤波器等进行剪裁。在滤波器剪裁中,通过评估每个滤波器的重要性(例如:Lp 范数)确定是否保留。结构化剪裁算法相对复杂、控制精度较低,但剪裁策略更为有效且不需要硬件层的支持,可以在现有深度学习框架上直接应用。
神经网络中的计算通常采用浮点数(FP32)进行计算,量化(Quantization)的基本思想是将浮点计算替换为更低比特(例如:FP16,INT8 等)的计算,从而降低模型体积加快模型推理速度。
数值的量化可以看做一个近似过程,主要可以分为两类:
范围近似又可以分为线性映射和非线性映射两种。
线性映射将浮点数映射到量化空间时采用如下计算公式:
$$ \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$
为多头自注意中的头数。
模型量化分为两种:
根据是否进行训练可以将量化方法分为两大类,如下图所示:
三种不同量化方法之间的差异如下图所示:
神经结构搜索(Network Architecture Search,NAS)旨在以一种自动化的方式,解决高难度的复杂神经网络设计问题。根据预先定义的搜索空间,神经结构搜索算法在一个庞大的神经网络集合中评估结构性能并寻找到表现最佳的网络结构。整个架构如下图所示:
近年来基于权重共享的结构搜索方法受到广泛关注,搜索策略和性能评估高度相关,因此两者往往合为一体表示。
搜索空间包含了所有可搜索的网络结构,越大的搜索空间可以评估更多结构的性能,但不利于搜索算法的收敛。搜索空间从搜索方式角度分为两种:
搜索策略即如何在搜索空间根据性能评估选择最优的网络结构。具体包含随机搜索、贝叶斯优化、进化算法、强化学习和基于梯度的方法。
性能评估最简单的方法就是对数据划分验证集,针对不同的网络结构重新训练并评估其在验证集上的表现。但这种方法所需的计算成本很高,无法在实践中落地应用。一些基于权重共享的结构搜索方法能够一定程度地加速搜索,因此考虑搜索策略和性能评估任务的相关性,在当前架构中往往将这两部分统一表述为搜索策略。
知识蒸馏(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$
的类概率分布越平滑,这可以让学生模型学习到教师模型对负标签的归纳信息。
可供使用的模型压缩库有:
硬件加速是指将计算交由专门的硬件以获得更快的速度。在深度学习领域最简单的体现就是利用 GPU 进行推理会比利用 CPU 更快。除此之外,在给定的硬件环境中,利用针对性优化的推理框架可以更充分的利用硬件特性提升预测效率。
并行计算是指将计算的过程分解成小部分,以并发方式运行实现计算效率的提升。在模型训练和推理阶段,主流的并行方式有:
可供使用的推理加速库有:
库 | 平台 | 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。
Hinton, G., Vinyals, O., & Dean, J. (2015). Distilling the knowledge in a neural network. arXiv preprint arXiv:1503.02531. ↩︎
模型压缩概述:https://paddlepedia.readthedocs.io/en/latest/tutorials/model_compress/model_compress.html ↩︎
Large Transformer Model Inference Optimization:https://lilianweng.github.io/posts/2023-01-10-inference-optimization/ ↩︎
深度学习模型压缩方法:剪枝:https://zhuanlan.zhihu.com/p/609126518 ↩︎
模型量化原理与实践:https://robot9.me/ai-model-quantization-principles-practice/ ↩︎
李航宇, 王楠楠, 朱明瑞, 杨曦, & 高新波. (2021). 神经结构搜索的研究进展综述. 软件学报, 33(1), 129-149. ↩︎
黄震华, 杨顺志, 林威, 倪娟, 孙圣力, 陈运文, & 汤庸. (2022). 知识蒸馏研究综述. 计算机学报, 45(3). ↩︎
2024-02-09 08:00:00
编程语言「只是」达成目标的工具,这是我一直推崇的说法,因为我认为达成目标更重要的在于个人思考,编程语言不过是个「工具」,选哪个并没有那么重要。现在我越来越认为这个「工具」的选择还是很重要的,因为编程语言之于目标实现的可能性和效率都制约了目标的最终达成。
以个人数据科学的工作背景,结合我的编程语言学习路径,尝试回答一下我们需要多少种编程语言这个问题。我的编程语言学习路径大致如下:
在我的编程语言学习路径中,各个编程语言之间还是存在很大差异的,以最早接触的 Logo 为例,其除了教学目的之外似乎就真的没有什么大用途了。这些编程语言可以大致划分为如下三种类型:
领域特定语言和标记语言就不用多说了,其应用范围有限。通用编程语言虽然定义为通用,但各自也有擅长和不擅长的领域,需要针对实际场景进行选择。
领域偏好是指你所从事的领域内大家对编程语言的使用偏好,这对于语言的选择至关重要,因为靠一己之力改变领域内大多数人的选择还是相当有难度的,从众可以极大地充分利用前人的成果降低工作的成本。
数据科学可选的编程语言有 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。