(碎碎念环节之 诈尸 ) 之前在读书的时候,就好奇有些巨佬的博客,怎么写着写着过几年就不更新了,还挺可惜的。直到自己上班才发现,这b班上的是真忙。本来空闲时间就不多,下班的时间还都拿来休息回血了,最多也就抽空临幸下老爱好或者填坑了,谁还有时间跟闲着没事一样一周写两份周报啊。再加上坚持写博客的人大多是有追求有讲究的,总不能内容越写越烂,于是为了精品内容就变成了经典爷爷up主,隔几个月甚至几年才突然诈尸一次,而某些精品博客可能就在这个过程中逐渐消亡了。虽然伤感但是非常现实,人总是要先吃饱饭的,哪里有其他精力空出来搞这些花里胡哨的事情。


最近在研究あいミス的解包汉化,好不容易花了一个星期把加密逻辑梳理走通了,然后开始看文本怎么提取、翻译、重新打包。(此处不得不提某个头铁憨憨,放着Android+IL2CppDumper不用,头铁用f12去看wasm汇编,折磨了小一个星期差点弃坑。)留个坑吧,有空考虑要不要写篇文章,记录下分析拆包是怎么做的。

看了半天发现,发现很多解包工具都不更新了,比如经典的AssetStudioUABE,想着靠谱起见,至少找一个在更新的工具吧。找了半天发现还得是LLM,ChatGPT马上就提供了有效信息,有个叫UnityPy的工具还在更新。一看,是个python程序库而不是界面软件,正好拿来给我批量处理当API调用,免得自己还要去改源程序当库用了。

然后天就塌了。由于不想装VSCode,就在linux上部署了code-server开发。装了python插件之后发现,代码高亮没了,怎么按快捷键都提示没有补全项可用。看了下控制台输出,发现pylance没有调用日志,以为是python插件的版本问题,就去折腾怎么降级python插件版本。一路从2025年试到2022年都没用,实在没辙了,就去问ChatGPT。好嘛,不问不知道,原来微软这鸡贼小子在pylance插件了加了史,只有微软自家发布的VSCode可以正常调用pylance插件,其他基于开源VSCodium方案的发行版会调用失败。我拿2024年的pylance插件试了下,服务确实可以启动起来,但是会输出一大堆车轱辘话。一开始没在意,以为是什么EULA一类的,后面看了社区讨论才知道,这是暗戳戳在内涵用户不是在官方环境下安装的插件来着。

想着几年前,最开始接触code-server的时候,安装python全家桶插件是使用正常的,于是决定回溯看是哪个版本加的史。二分找了一下,最后可用的版本大约在2023.6.30这里,高于这个版本的pylance插件就会开始bb并拒绝工作了。

后续是搜了一下,发现推荐的用法是,用开源的basedpyright代替pylance,其他插件正常安装就行,与vscode默认配置功能基本一致。唯一比较明显的是,由于缺少了巨硬魔法的加持,生成代码补全的速度略慢了一些,能够感受到从输入到出现提示的明显延迟,要是补全的tab或者回车敲得稍微快一些,能直接打断LSP施法。

前几年人工智能还没火起来的时候,为了打比赛学了一点,结果当时也没找到什么好的门路,学的一知半解,模型倒是会用了,但对原理还是一知半解。最近因为某些原因,决定重新捡起来精学一下,或许是因为人工智能成功火了起来,优质课程越来越多,也越来越好寻找。找了半天之后,决定看李宏毅老师的机器学习课;第一堂花了两个小时从线性拟合讲到深度学习,直接给我整不会了。好痒啊,要长脑子了.jpg

虽然一直以来对深度学习的各概念有所了解,但是毕竟古话说得好,“知其所以然”。我比较确信,自己正处在一种知道这个东西,并且大概知道怎么用,但并不了解原理的状态之中。换句话说,扔给我一个模型,我能让它跑起来,但如何让它结果更好、跑的更快,就超出能力范围了。这时候恰好遇到了李老师的机器学习课,以一种开了全图挂的美妙方式,在讲述神经网络数学原理的同时,还介绍了拟合函数是如何一步步从单调的线性拟合发展成灵活的,个中思路或许并不是其真实的发展路程,然而足够合理,让我能够理解并记住神经网络为什么会发展为如此形态,故在此记录,防止再次忘记。

1. 最基础的模型:线性拟合

首先从最广义的角度来看,数学建模,就是一类寻找特定函数y=f(x),以能够通过已知量x,准确或近似预测未知量y的函数f(x)的过程。无疑,在假设xy存在关联的情况下,最简单的近似函数即为f(x)=x,即x本身。在考虑一般场景下xy不会简单地为同一个值的问题后,对f(x)=x进行泛化,得到线性拟合的基本形式:f(x)=wx+b,其中w为weight,代表yx影响的程度,即直线的斜率;b为bias,代表xy值之间的固定修正,即直线的截距。

2. 更多的信息:线性组合的线性叠加

通过简单地更换函数的类型,比如将线性函数f(x)=x换成指数函数f(x)=e^x,或者周期函数f(x)=\sin{x},并不能应对绝大多数场景,故在此不纳入讨论范围。

俗话说得好,力大砖飞。正如人懂得越多越聪明一样,为了让数学模型更加准确,一种最为直接的方法是向模型中添加更多的自变量。这种修改使得模型中的自变量从单个x变成了一组变量x_1, x_2, ..., x_n,对应地,数学模型变成了f(x_1, x_2, ..., x_n)=\sum_{i=1}^{n}{w_{i}x_{i}}+b,向量化简写后就是经典的f(\textbf{\textit{x}})=\textbf{\textit{w}}^{T}\textbf{\textit{x}}+b。李老师在22年课程中,使用油管播放量预测任务检验了这一方法的有效性——模型效果确实有所提升,但离实际可用仍然有较大的差距。

3. 更精致的拟合:激活函数

模型预测值与实际值之间的差距被称为模型偏差model bias。显然,模型偏差越小,其预测效果越好。然而,线性拟合,或者说是大部分基本函数,在广阔的定义域上,通常并不具有一个稳定的值,调整参数时会使其在整个定义域上的函数值发生明显变化,我称其为“按下葫芦起了瓢”——通过梯度下降等优化算法调整参数后,某些预测效果特别差的样本得到了提升,然而同时可能存在某些预测效果较好的样本,因为其参数被修改,反而预测效果变差了。换句话说,当模型效果无论怎么训练都无法提升时,其最小偏差即可视为模型误差。

面对基本函数“牵一发而动全身”从而产生的模型偏差问题,最直接的应对思路无疑就是寻找不具有这类特征的函数。换言之,我们可以使用定义域有限的函数进行更加细致的准确度优化。考虑到定义域有限制,而表达方式最为通用的函数,无疑就是分段函数了:取两个点,将函数分为三段,左右侧均为常数值,中间为单调变化的曲线,从左端点值过渡到右端点值:

\alpha(x)= \begin{cases} c_1 &, x < x_L \\ wx+b, &, x_L \le x < x_R \\ c_2, &, x \ge x_R \end{cases}

这样就得到了一个相对完美的函数:在给定的定义域内为变量,在给定定义域之外为常量,满足了对模型更加精细调控的要求。在此将这种函数称为斜坡形函数。

通过将两个斜坡形函数叠加,还能实现更加优雅的函数值调控。构造具有相同形状、但相位不同的两个斜坡形分段函数:

\alpha_1(x)= \begin{cases} c_L &, x < x_{L1} \\ wx+b &, x_{L1} \le x < x_{R1} \\ c_R &, x \ge x_{R1} \end{cases} \alpha_2(x)= \begin{cases} c_L &, x < x_{L2} \\ w(x-(c_R-c_L))+b &, x_{L2} \le x < x_{R2} \\ c_R &, x \ge x_{R2} \end{cases}

将两式相减,得到

\alpha(x)=\alpha_1(x)-\alpha_2(x)= \begin{cases} 0 &, x < x_{L1} \\ h(x) &, x_{L1} \le x < x_{R2} \text{(偷懒懒得展开了)} \\ 0 &, x \ge x_{R2} \end{cases}

,直接将\alpha(x)变成了一个只在指定范围内有非零值的函数“片段”,彻底消灭了该函数对定义域其他范围的值影响。

(ps:md个烂typecho,写个css乱用通配符,害得我公式都变形了,调了半天才搞好)

此时的\alpha(x)仅仅只是一个应用于x之上的分段函数。而使其通用泛化的方式非常直接:直接把wx+b塞进\alpha(x)里,即可将原本的线性拟合模型转化为“定义域限定函数”。通过这种方式,近似地实现了使用多个互不干扰的函数构建数学模型的能力,提升了模型的泛化能力,使模型具有更小的偏差下限,能够更好地拟合真实情况。

那么如何构建这种形式的函数呢?答案是对\alpha(x)做进一步分解,使用惊人的注意力不难发现,可以将斜坡形函数拆分为两个更加简单的折线形函数:

(x)^+= \begin{cases} 0 &, x < 0 \\ x &, x \ge 0 \end{cases}

是不是眼熟起来了?是的,芝士ReLU函数(Rectified Linear Unit,带修正的线性单元)。由于在计算机中可以较为轻松地依据数据符号进行条件运算,因而ReLU函数不仅具有良好的数学性能,在实际的模型演算过程中也有得到广泛应用。

根据我个人理解,\alpha(x)这种能将任意输入值映射到具有特定特征(在特定定义域上,其值域范围有限)的函数被统称为激活函数activation function。估计激活这个名字来源于神经网络,意味着神经元对任意外部条件做出的响应,被映射(编码)为一种固定的表达方式。

既然是“统称”,那么自然还有其他的激活函数。不知道是不是出于梯度计算的考虑,其余常见的激活函数都是在定义域上连续可导的,如Sigmoid和tanh:

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

以Sigmoid为例,引入了激活函数的线性回归模型现在演化成了这个样子:

\begin{aligned} f(x)&=\sum_{i=1}^{n}{c_i\sigma(w_ix+b_i)}+b \\ &=\sum_{i=1}^{n}{c_i\frac{1}{1+e^{-(w_ix+b_i)}}}+b \end{aligned}

4. “我全都要”:神经网络

(这是书呆子 这是手指.jpg)诶!如果我们结合第二步跟第三步的思路,既引入多个自变量增加已知信息,又引入激活函数降低模型偏差,岂不美哉?

没错,这就是神经网络。按上述思路改进后,得到如下形式的数学模型:

\begin{aligned} f(x)&=\sum_{i=1}^{n}{c_i\sigma(\sum_{j=1}^{n}{w_{ij}x_{i}}+b_i)}+b \\ &=\textbf{\textit{c}}^T\sigma(\textbf{\textit{W}}\textbf{\textit{x}}+\textbf{\textit{b}})+b \end{aligned}

可以看到,模型由多个包含激活函数的子模型(“神经元”)构成,每个神经元将所有自变量作为输入,经过激活函数计算后进行累加汇总,得到最终结果。借用李老师公开课上的图来看,现在的数学模型长这个样子:

nn.png

可以看到,相比于传统的单函数模型,这种模型可近似看作多个子模型独自处理输入,然后汇总成输出,更加贴近人类,或者是神经元处理事件的模型。或许这就是其被称作神经网络的原因吧。从李老师的实验可以看出,使用(单层)神经网络相对于线性回归模型,其准确度有所提升——但显然还不够。

5. “大力出奇迹”:多层神经网络

上述数学模型的发展过程无疑验证了一个道理:大力出奇迹。随着入参和数学模型的规模和复杂度提升,其模型准确度也在或多或少、但是稳定地提升。那么问题来了:如何在神经网络的基础上,进一步提升数学模型的准确度?

扩大输入规模不失为一种有效的方法。然而,在数据集规模固定,或是出于性能要求限制了输入规模的情况下,就需要依靠更加优质的模型结构来提升其精准度了。在今天——AI进入人们——或者至少我的视野近八年的时间内,不论是其神经元结构,还是激活函数的构造,都大同小异,但是模型的性能却得到了质的飞跃。究竟是什么改变了神经网络?

答案是神经元间的拓扑结构。在第4节的数学模型中,“神经元”之间仅仅是并列关系,都只是按照自己的喜好对输入数据赋不同的权重,然后给出输出值;神经元之间没有任何“交流”。

从第4节的图中不难看出,神经网络具有“多输入、单输出”的特征。什么概念具有类似的结构呢?我想,答案应该是逻辑门。在数字逻辑电路的世界里,多个二元值进入逻辑门后,根据给定的真值表,将会得出一个固定的输出值;而这个输出值作为输入值在逻辑门上的映射,将带着输入值的特征,作为输入值进入下一个逻辑门;通过这种方法,计算机科学家们使用逻辑门构建出了当今千姿百态的处理器世界。相似地,如果提升现有的单层神经网络的维度,即取多个神经网络,并计算它们的输出值;然后,再取一个神经网络,将上一级每个神经网络的输出作为输入值,得到一个新的输出值,像下图所示,不就在神经网络的基础上,实现了模型结构的又一次进化吗?

dnn.png

反向传播back propagation为多层神经网络的实现提供了可能。但是效果好不好呢?尝试一下总是没有损失的。这一试可不得了:DNN,CNN,RNN,GAN,LSTM,BERT,Transformer,GPT... 一个个熟悉的名词如井喷式涌出。是的,仅仅是通过搭积木似的将神经网络进行二维组合,学术界就能拿着玩好几年,直到现在大火的各种AI聊天、按要求生成图片/音频/视频等等,无一不是建立在深度神经网络的基石之上。

那么,引用The Fabric of Cosmos里一段非常经典的话:

How could this be?
Does it bother us? Absolutely.

是的,为什么深度神经网络能够做到传统模型做不到的事情,它到底能做到哪些事情,甚至每一个“神经元”动作的含义,对我们而言都是需要了解的、甚至是未知的。那我们要做的,就是走进迷雾,掀开深度神经网络神秘的面纱。

以防万一有人不记得(笑死,搞得跟真的有人看一样),本站第一个域名是esperz.tk

.tk域名暴雷一事,在2023年中就初见端倪。当时在某交流频道里看到有人说注册的.tk域名无缘无故被收回,当时我还在想这人肯定是拿域名做了什么,然后在跟大伙春秋笔法。然后下半年freenom的名声就开始不对劲了,说是因为域名滥用问题在跟ICANN扯皮,新域名都不让注册了。还好这个时候已经把本站域名的有效期延长到24年了,在freenom没倒之前先将就着用吧。

然而不出意外的话,也该出意外了。24年,也就是今年春节,在家里用自己域名测试网络的时候,突然发现打不开网站了。对,不是提示证书过期,直接找不到网站了;nslookup一查,一个大大的Non-existent domain直接拍在我脸上。在freenom上确认发现,域名变成了PENDING状态,不能再进行任何操作了。好嘛,这下我就知道freenom是真的摆烂,开始拿用户数据乱玩了。

然后就准备开始找下家,来到了经典的eu.org环节。一看,哦豁,也是23年底因为请求太多处理不过来,不让申请新域名了。至此,可靠的白嫖手段用尽,不得不进入花钱环节了。在TLD-List上逛了半天之后,最终还是选择向钱包屈服,搞了个长期的.top域名,也就是现在的esperz.top。依稀想起了数年前室友怂恿我搞个.top域名玩,哎,最终还是没能逃出真香定理。

涨价(应该是)不可能涨价的,希望.top域名不要轻易倒闭吧,不然到时候还要去加价买.cc一类的野鸡域名了QAQ

这一年中也没玩太多新花样,主要是花了不少精力把bustub的lab做完了,但因为没搞新域名就一直搁置了,有时间整理下把思路和心得拿出来分享下吧。

起因是换了电脑之后懒得重装系统,但是为了装新驱动而不得不对系统进行了一个新的更。然而用了一两年之后,重新打开DiskGenius的时候才发现盘上有两个Recovery分区,估计巨硬是在安装程序里,从我原有系统盘的末尾切了1个G出来,又做了一个Recovery分区。然而咱们消费级电脑并没有那么高的可用性要求,不需要一个系统配一个Recovery分区,所以打算把分区表更新下,删掉新加的Recovery,让所有现存Windows系统共用一个Recovery分区。

大体流程参考这篇回答,简单来说原理就是分区备份还原,然后用reagentc命令将Windows配置为用我们设置的新Recovery分区:

  1. 给两个Recovery分区分配盘符;
  2. 如果需要,用dism备份和还原分区数据(比如说要删除的Recovery版本更新,想保留);
  3. reagentc /disable
  4. 删除不要的Recovery分区;
  5. reagentc /setreimage /path Z:\Recovery\WindowsRE配置WindowsRE,假设Recovery挂载到Z盘;
  6. reagentc /enable

这个时候理论上已经完成了WindowsRE分区的更换,用reagentc /info可以看到,Recovery分区已经被更换为了目标分区。但是如果直接用diskpart或者DiskGenius一类的工具卸载Recovery分区的话,会发现这么一个问题:要么卸载成功,但是重新用reagentc /info看WindowsRE配置又变回了Disabled,要么就卸载失败,任务管理器里还能看到盘符,或者重启之后分区又重新自动挂载上了。

然而天无绝人之路。在网上一顿乱翻,发现也有人有这个疑惑,一看原来Windows还有一个命令可以删除盘符:

mountvol Z: /d

经过测试,使用这个指令卸载盘符可以保证reagentc配置不变、消除盘符的同时,再重启后也没有自动挂载的现象复发了,可以认为达到了与Windows安装时自己配置相同的效果。

啊?这玩意还能有第三期?

起因是想玩玩ChatGLM3,然后发现这个库用的pytorch版本特别新(说是要求2.0以上,实测1.12也行),于是就打算自己编一个新版本玩玩。然后人就傻眼了:编了几天,编译过程是挺顺利的,结果编出来的whl用不了,一import就报OSError 1114,说是某个DLL的初始化函数出了问题,一开始以为是编译器坏了(因为就在当天一次重启把我CUDNN炸了,也不知道是不是SSD的问题 汰渍全责!),前后更换配置(Python版本/MSVC版本/是否启用CUDA,甚至换了电脑)编了几天,SSD都写了快1TB了,还是没有任何进展。

然后也是因为各种配置问题没文档,编不出Debug版的wheel来,就着RelWithDebInfo版用regsvr32看了半天,发现好像是因为某个全局静态变量没有初始化还是咋的,导致DLL加载过程中抛出了c10库的异常,没给任何有用信息就直接把主程序炸了;然而由于该方法只是简单地加载了一个DLL,并不一定遵循PyTorch应有的初始化逻辑,所以也不知道有没有参考价值。

最后实在想不通了,为什么官方编出来的装了可以直接用,于是决定去偷学官方ci的编译脚本,看看编译流程上和自己的到底有啥区别,结果最后发现,好像最大的差别是他用的是Ninja生成,而我用的是MSBuild。虽然认为本质上应该没啥区别,但是抱着死马当活马医的心态,装了VS自带的CMake工具(对,这玩意其实也自带Ninja的),然后有样学样,用Ninja来编译PyTorch。虽然不是很想用咕果家的东西,但是不得不承认的是,Ninja的TUI还是做得比MSBuild好的,至少可以大致了解到编译流程进行了多少= =

这里贴一下中间需要设置的变量:

  1. set CMAKE_GENERATOR=Visual Studio 16 2019(因为我没把Ninja加到PATH,以及VS2017编译会有报错);
  2. set DISTUTILS_USE_SDK=1,不然CUDA配置那里可能报一些奇奇怪怪的错误;
  3. python setup.py bdist_wheel,然后理论上他configure完会出现异常终止,因为MSBuild内部调的Ninja和我们设置的环境变量不符合;
  4. set CMAKE_GENERATOR=,取消generator的设置;
  5. 重新执行3,理论上应该不出现任何问题,直到编译完成。

然后就是经典环节了。pip安装编完的whl,import,没有报错,直接成功了,是的,成功了。

很喜欢计算机人的一句话:啊?

只能说是在摸清问题之前解决了问题了罢。咱又不开发AI框架,既然问题解决了,那探究先到此为止吧。