分类: 技术向

一些相关的IT/网络小知识

  • 「编程笔记」ST表的实现与使用

    ST表,全称SparseTable,也叫做稀疏表。是用来解决区间和和区间最大最小值问题的一种工具。其实推广开来,其可以用来解决任何可重复贡献(associative)问题。

    使用条件

    可重复贡献问题

    ST表要求其处理的问题必须具有“可重复贡献”这一特征。具体是什么意思呢?

    假设有操作F,使得F(a, b, c) == F(F(a, b), c) == F(a, F(b, c)),则称F符合可重复贡献问题的特征。

    如图,不难发现max()最大值函数就符合可重复贡献问题的特征;读者可以自行验证,sum()min()等函数都符合这个特征,这也证明了ST表可以解决最大最小值以及区间和等问题。

    元数据可不能改变噢

    在使用ST表解决问题时,我们必须确保所有的查询结束前,ST表所对应的元数据都不改变。

    有些数据结构(比如线段树)在元数据更新之后(比如原来的数组中,某个数值由3变成了5),可以有对应的事件复杂度比较低的方法对结构进行更新,以达到一边更新一边查询的效果。ST表则无法提供类似的方法,所以确保你的问题中没有要求在查询期间动态调整元数据的要求。

    SparseTable意义与使用

    接下来,我们采用由表及里的过程,先了解ST表的意义以及使用方法,之后再来说明如何生成ST表。

    一些准备——向下取整的Log2函数

    由于SparseTable的特性,无论是根据数据生成ST表,还是得到ST表后对数据进行查询,都会比较频繁的用到向下取整的log2函数,所以特地说明。我们对于向下取整的log2运算稍加分析,可以得到log2(x)向下取整的具体意义是:

    对于某一个数N,返回可能的最大的数字n,使得2的n次方小于等于N,且2的n+1次方必定大于N。

    比如对于7,log2(7)向下取整为2,2的2次方不超过7,但可以保证2的3次方一定大于7(8>7)。

    了解了这个之后,让我们来看看SparseTable如何快速解决规模庞大的可重复贡献问题吧~

    ST表中数据的意义

    我们接下来选择一个经典的可重复贡献问题——区间最大值问题,作为示例。

    假设我们有一个数组 {3, 2, 1, 4, 5, 9, 7}, 其数据元素个数为7,要求必须在非常低的事件复杂度内,查询某个特定区间(比如从下标0到下标5)的最大值。此时,如果直接循环求解,会导致复杂度随着查询区间长度不断增高,这就是ST表登场的时候啦。

    如上图,对数组进行处理之后,就可以得到下方的二维数组,也就是我们大名鼎鼎的SparseTable——ST表。那么问题来了,图中ST表中的数据,到底具有什么含义呢?

    首先需要知道的是,ST表正常情况下使用一个二维数组st[idx][pow]来储存。下面对这个二维数组每一个位置元素的意义做出解释:

    • 数组中,每一个特定位置(idx, pow)的值,都代表对应问题在某个区间的解。
    • 这个区间的开始坐标为idx。
    • 这个区间的长度为2的pow次方。

    例如,对于区间最大值问题,假设原数组为arr,处理后得到的ST表中,st[1][2] = 5,则说明:原数组,从下标为1开始,长度为2^2=4的区间内,最大值是5。

    特定区间的查询

    知道了ST表的数据的意义,我们就可以着手解决特定区间查询问题。

    这里需要注意的是,虽然ST表可以解决不同的可重复贡献问题,但是其在面对不同的可重复贡献问题时,最优查询方式可能有一定的区别。接下来,我们先通过上方的“区间最大值”例子,介绍一下区间最值问题的ST表查询。

    合适的长度

    首先,为了降低时间复杂度,我们希望用尽可能少的区间查询来拼凑出完整答案。可以证明,在ST表处理区间最值问题时,无论区间长度位置,我们都可以用ST表支持的两个区间,拼接出答案。比如对于区间(1, 6),可以由区间(1, 4)和区间(3, 6)区间的结果再求最值得到。对于“最值”问题,我们只需要保证选择的区间完整覆盖住所求区间,且不超过所求区间即可,而这多个区间允许有重复部分。

    同时,为了保证所用区间尽可能少,每个区间就要尽可能大。这个时候,上方说到的log2向下取整计算就派上用场了。由于ST表代表的区间长度只能是2的n次方倍,所以,不超过区间范围的区间,其最长长度就是log(区间长度)向下取整。

    比如对于(1,6)区间进行最值计算,log2(6)向下取整=2。说明只需要在合适的位置,用两个长度为2^2=4的区间覆盖即可。这代表了,我们要取的数据肯定在ST[idx][2]内——我们确定了pow。

    合适的位置

    我们已经知道了,只要位置安排合理,我们肯定能用两个长度为4的区间覆盖住(1, 6),那么这两个区间的位置,应该如何选择呢?

    思考之后发现,我们肯定希望:

    • 第一个区间尽可能往左,也就是第一个区间的起始下标就是总区间起始下标1。
    • 第二个区间尽可能往右,也就是第二个区间的结束下标就是总区间结束下标6。

    如上图,最终,我们可以得到:

    • 第一个区间为(1, 4),最大值为5。
    • 第二个区间为(3, 6), 最大值为9。

    我们对这两个区间的最大值再求一次最大值即可,最终答案为max(5, 9) = 9

    如果用ST表来计算,运算过程如下:

    max(st[1][2], st[3][2]) 
    = max(5, 9) 
    = 9

    是时候该自己实现ST表啦

    如果您已经掌握了上面所说的,ST表的工作方式,那么恭喜你,欢迎来到ST表的DIY小课堂(bushi),在这里,你将学习如何通过自己的双手书写代码,为一个个数组生成属于他们自己的SparseTable!

    初始化

    初始值

    根据ST表的定义,我们不难得到一个结论,即:

    ST表的第一列数据 st[idx][0],与元数据arr[idx]完全相同。

    更加准确的说,应该是 st[idx][0] == F(arr[idx]),其中F是本ST表所对应解决的可重复贡献问题。而由于我们以区间最大值为例,所以F(arr[idx]) == arr[idx],最终得到st[idx][0] == arr[idx]

    表的长和宽

    解决完初始值,还有另外一个问题,ST表的大小。

    我们已经可以确定,表的行数(第一维下标)就是数据个数,比如对于7个数据的数组,生成的ST表肯定有7行。那么一个ST表有多少列呢?

    ST表有多少列,取决于查询时最多可能用到第几列的数据。根据ST表的区间查询原理,不难得到,对于数据个数为N的数组,其ST表的最大列数,就是log2(N)向下取整。为什么呢?——因为最坏情况下,需要查询整个区间范围的数据,这时,查询区间长度最大,为N,查询时会调用到的区间的纵坐标(上文的pow),正是log2(N)向下取整。

    解决完上面的问题,总算是万事俱备了,我们开始对ST表进行初始化:

    逐步推出整个ST表

    接下来的问题便是,如何计算ST表剩余其他部分的值。我们仍然需要从ST表的定义入手,不难发现,由于ST表中的数据本质上就是某个区间的结果,所以更大区间的结果可以由多个小区间结果拼接而来。意思是,我们可以通过表中左边列数据,一步步推出右边的列。(因为左边的列代表范围更小的区间,右边的列代表范围更大的区间,且相邻列的长度差一定是2倍)

    由上面的思想启发,我们发现,对于某个位置的ST表st[idx][pow],其左边的ST表列数据已经可用(毕竟我们决定从左边往右边推,那么推到pow列的时候,ST表的pow-1列肯定已经完成计算了),其刚好可以由左边列的两个区间st[idx][pow-1]st[idx+2^(pow-1)][pow-1]相结合获得。

    不要看公式复杂就放弃理解噢~,不难发现,其实其思想与上方”ST表的使用“中所提到的”区间查询“,运用了一样的思想。两个区间都尽可能大,同时一个尽量往左,另一个尽量往右。

    上图就是对某个ST表位置进行求值的示意。知道了某个点的求值方法,我们只需要从上到下,从左往右不停循环,直到求出整个ST表即可。

    需要注意,ST表中部分位置没有值,这是因为其对应的区间已经超出范围,比如st[4][2],计算可得,该区间结尾下标为7,但是原始数据个数为7,说明最大下标为6,故该点不应该存在数据,即使存在,查询时也肯定用不到(读者可以自行思考一下,为什么查询的时候不可能调用到这类数据点)。

    实战环节!

    至此,有关于ST表的所有理论思路都已经介绍完成啦,下面我们借用洛谷上的ST表模板题,来介绍一下ST表的实战,以及一些注意事项。

    先放上题目AC代码:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    using LL = int;
    
    using namespace std;
    
    LL st[100000][18] = {0};
    
    LL log2Table[20] = {1};
    
    inline int read()
    {
        int x = 0, f = 1;
        char ch = getchar();
        while (ch < '0' || ch > '9')
        {
            if (ch == '-')
                f = -1;
            ch = getchar();
        }
        while (ch >= '0' && ch <= '9')
        {
            x = x * 10 + ch - 48;
            ch = getchar();
        }
        return x * f;
    }
    
    void initLog2Table()
    {
        for (int i = 1; i < 20; ++i)
        {
            log2Table[i] = log2Table[i - 1] * 2;
        }
        return;
    }
    
    LL logLowerBoundTable[100010] = {0};
    
    // 这里实际上是在对上文提到的那个,重要的log2向下取整函数,进行事先缓存,以提高一些查询效率
    void initLogLowerBound(const LL &numCnt)
    {
        logLowerBoundTable[0] = -1;
        logLowerBoundTable[1] = 0;
        for (LL i = 2; i <= numCnt; ++i)
        {
            logLowerBoundTable[i] = logLowerBoundTable[i >> 1] + 1;
        }
    }
    
    int main()
    {
    
        // init log2 table
        initLog2Table();
    
        // input data
        LL numCnt = 0;
        LL queryCnt = 0;
        numCnt = read();
        queryCnt = read();
        initLogLowerBound(numCnt);
    
        for (LL i = 0; i < numCnt; ++i)
        {
            st[i][0] = read();
        }
        
        // 这里是在计算,对于数据量N,ST表最多需要的列数是多少
        // 上文已经提及
        LL maxLayer = logLowerBoundTable[numCnt];
        for (LL curLayer = 1; curLayer <= maxLayer; ++curLayer)
        {
            // LL rightBound = numCnt - log2Table[curLayer];
            for (LL curIndex = 0; curIndex <= numCnt - log2Table[curLayer]; ++curIndex)
            {
                // calculate current arr value
                st[curIndex][curLayer] = max(st[curIndex][curLayer - 1], st[curIndex + log2Table[curLayer - 1]][curLayer - 1]);
                // cout << st[curIndex][curLayer] << "   ";
            }
            // cout << endl;
        }
    
        // deal with query
        LL left, right, intervalSize, maxLogNum, maxNum;
        for (LL curQuery = 0; curQuery < queryCnt; ++curQuery)
        {
            left = read();
            right = read();
            // turn into 0-index
            maxLogNum = logLowerBoundTable[right - left + 1];
            cout << max(st[left - 1][maxLogNum], st[right - log2Table[maxLogNum]][maxLogNum]) << "\n";
        }
    }

    no endl bro, use ‘\n’ please…

    这点与ST表这东西其实关系不大,只是提醒下各位,在这种IO看起来就非常耗时的情况下,使用endl进行换行,会出乎你意料的大幅度增加算法的耗时,同样,对于这道洛谷模板题,如果使用endl,几乎必定超时。对应的解决方案是使用’\n’进行换行。

    你可能会问,为啥endl比’\n’更加耗时呢?至于这个问题,我也不敢说我完全了解,但可以确定的是,当你在输出流输出endl时,程序做的远远不止是换行这一件事,其中还包含了包括flush等很多其他的工作,这些工作会导致其开销远大于换个行这件事情本身。详细信息欢迎读者上网查询了解。

  • 「编程笔记」关于Bellman Ford单源最短路算法

    开始之前

    本Blog之前已经介绍过一种单源最短路算法——Dijkstra算法。但Dijkstra算法也有着自己的缺点,其中最明显的问题就是无法处理带负权边的图,其原因与其算法中包含的贪心原理有关。

    而接下来介绍的算法,则拥有处理负权边的能力,该算法就是Bellman Ford单源最短路算法。该算法的核心思想是,对图中的每条边,都进行足够次数的Relaxation(松弛)操作,从而逐步推导出每个点距离出发点都最短路径长度。

    关于Bellman Ford算法的原理和代码实现,网络中已有大量优秀的资料,这里便不再说明算法本身的详细工作流程。接下来着重说明一些Bellman Ford算法中一些限制条件,及其相关的一些理解。

    Bellman Ford算法特性

    当出现负权回路时…

    在通过Bellman Ford算法计算单源最短路时,会要求所计算的图中不得存在权值为负值的回路。这个限制相对较好理解,下面举一个简单的例子。

    如图,图中的A->B->C->A便是一个负权回路。而假设我们现在需要以A点作为出发点计算最短距离,请问A->D的最短距离应该是多少?不难发现这里出现了死循环。我们可以从原点出发,不停的走A->B->C->A这条路,每循环一次,我们距离出发点的距离就会越来越小。这也就意味着,只要我们的路径不断的在负权回路中重复足够多次,每一个点距离出发点的距离都可以认为是无穷小,这显然是不合理的。

    负权回路对于图来说是存在特殊意义的,比如在经济学的部分领域中,“负权回路”是一个非常具有吸引力的词,因为可能代表着可持续的收益链,也可以代表着套利的机会,但Bellman Ford不认这些,在单元最短路算法的世界中,我们暂且不接受“套利机会”的存在。也就是说我们不允许待处理的图中出现负权回路,以此来保证计算结果是有意义的。

    对于Bellman Ford算法的一些理解

    迭代次数的理解

    Bellman Ford算法的核心思想,在于每迭代一次,就对所有边进行遍历,并执行Relaxation操作。这里不难发现,迭代k次时,得到的结果,可以保证至少是路径条数为k时的最优解

    为什么说是至少呢,因为其遍历的结果可能跟边遍历的顺序有关,如下图:

    如图,我们发现对于图中的无向图,如果从左边的边开始遍历,实际上遍历一次之后我们就已经得到了最优解。而如果从右边开始遍历,我们就需要遍历3次才能得到最优解。

    按照这个思路理解,不难推出,遍历k次时,得到的答案,只能保证,至少是路径条数为k限制下的最优解。

    这里对路径条数做出说明,一个路径由多个边组成,上方所说的“路径条数”,指的是对于某条路径,其经过的边的个数的和。

    为什么迭代次数等于点的个数?

    上方我们已经论述了迭代次数k的意义,迭代k次时,得到的结果至少是路径条数k情况下的最优解,部分点的数据可能比会更优,但对于所有的点,只能保证其为路径条数k情况下的最优解。

    那么我们到底需要迭代几次呢?根据定义,我们将问题转换成为:对于一个无向图,其某个点A到某个点B的全局最优路径中,最多可能经过几条边。

    经过思考之后发现,最坏情况下,最优路径将会不重复的经过所有点。接下来对这个结论进行简单的说明:

    首先是经过所有点。如果我们想要强迫最优路径经过尽可能多的点,最坏的情况之一,便是这个图形成了一个没有分叉的纯线性结构(就像上方对于迭代次数解释时所用图一样,每两个点用一条边链接,总边数为点数-1),这样从最左边到最右边的点,最优路径边毋庸置疑是经过所有的点。

    其次是不重复。为什么我们可以确保最优路径中不会重复经过同一个点呢?这从某个角度上来说得益于算法前面的一个前提条件——不允许负权回路。想象一下,如果路径重复经过了同一个点,说明这个路径必定存在一段回路(从重复点出发,经过某个路径,又回到重复点)。而此时如果这个回路确定不是负权回路,那么说明如果跳过这段回路,我们可以得到一个权重更短的路径,反向说明了目前的路径不是最优路径。因而可以确定,如果一个路径被确定为最优路径,且该图中没有负权回路,那么这个路径绝对不会重复经过同一个点。

    说明完毕,接下来让我们看看这两个结论,不难发现,不重复经过所有点,那么对于一个点个数为N的图,符合条件的路径,最长只能是N-1(因为一旦大于N-1,说明必定要产生回路,与条件冲突),所以最终确定,如果需要得到全局最优,其最少迭代次数为N-1。

    综上所述,容易得到,算法的时间复杂度为O(N*M)。其中,N为点的个数,M为边的个数。

    为什么它能攻克负权边?

    在Dijkstra算法的文章中,笔者提到了一个观点,Dijkstra的核心是基于贪心的,而贪心带来了局部最优解这一隐患。带着这个角度再来看Bellman Ford算法,是否有一种熟悉的感觉?Bellman Ford算法的每次迭代都基于上一次的迭代结果。先知道路径条数1情况下的最优解,再根据此状态推出条数2情况下的解,以此类推最终得到路径条数为N-1时的最优解(也就是全局最优解),是不是有点「动态规划」的味道了呢?正是借助这这种工作模式,不像Dijkstra算法一样看到一点点甜头就着急的把点Close掉,Bellman Ford拥有了不被眼前甜美假象迷惑的能力,坚持着自己的本分,不断想着正确的道路一步步迈进,最终终于得到了令人满意的结果。

    相关链接

    GeeksForGeeks Bellman Ford算法介绍(英文)

    「编程笔记」Dijkstra算法为什么无法处理带有负权边的图

  • 「编程笔记」Dijkstra算法为什么无法处理带有负权边的图

    前言

    Dijkstra算法是一种可以计算有向或无向图中单源最短路的算法。其工作模式与图的BFS(广度优先遍历)有些相似,通过类似于广度遍历的方式,逐渐从某个设定好的起点向外推进,一步步的计算出所有点到该点的距离。

    本文不过多介绍Dijkstra单源最短路算法本身,如果仍然没有掌握该算法,可以先自行了解该算法的工作模式。下面提供一些可供参考的资料:

    知乎:通俗易懂理解——dijkstra算法求最短路径

    负权边对于Dijkstra算法的影响

    Dijkstra算法工作模式

    要了解该影响,首先需要明确Dijkstra算法的工作模式。其维护两个点集合。一个是“仍未确定最优解的点(S)”,一个是“已经确定最优解的点(U)”。且有一个重点:在算法工作期间内,如果某点被认为已经得到最优解,则该点会被从S移动到U,可以认为,从此以后该点已经被Closed(关闭),即解已经确定,且不容被更改。

    为了方便,我们称在S中的点为opening vertex,U中的点为closed vertex

    当图中出现负权边…

    了解这一点后,让我们来看看下面这个图片示例,看看当负权边出现在图中时,可能会对算法造成什么影响:

    Dijkstra计算带有负权边的图

    如上图,假设我们尝试使用Dijkstra算法,计算该图中各个点距离C点的距离。

    图中,绿色的点为closed vertex,白色的点为opening vertex,点上方的方框代表该点的,实时更新的计算距离。

    1:算法首先锁定C点,将C点到C点的距离标记为0,并认定为最优解,然后根据C点尝试更新与其相连的opening vertex的距离(这里为A,B点,得到更新后距离是为1和5)。

    2:剩余的opening vertex中,A距离出发点(C点)最小,距离为1,故锁定A点,(注意:锁定A点说明算法认为A点的最优解就是1,且之后算法也不可能再次更新A点的距离值,因为A点已经为Closed状态了),同时,锁定A点之后,再次更新与A点相连的opening vertex的值(与A点相连的点有B和C,但因为C点为closed vertex,故仅仅尝试更新B点的值),计算得到B点最新距离为min(5, 1+(-10)) = -9

    3:最后,锁定B点,算法计算完成。

    上方便是Dijkstra算法在示例图中的运行步骤和结果。不难发现,算法对于A点的最短距离出现了计算失误:实际上,A点的最短距离走法并不是C->A (1),而是C->B->A (-5)。

    怎会如此?

    分析之后可以发现,Dijkstra算法的核心,就在于每次都选择S集合中距离最小的点,并将其锁定,再通过这个点进一步更新其他点的距离。但为什么Dijkstra认为S集合中目前距离最小的点就是最优解呢?有没有可能从其他的点出发可以得到更小的距离呢?

    比如在上述的示例图中,锁定C点后,算法认为A点的距离是1,B点的距离是5,所以A点的距离为1一定是最优解。那么有没有可能实际上1并不是C到A的最优解,我们通过B点走其他的路径最终可以得到更小的解呢?

    答案是,当边的权值非负时,不可能,当边的权值存在负值时,则有可能。

    当权值非负时,B点已经离出发点有5点的距离,所以所有从出发点出发经B点的路径,其长度必定大于等于5,但是当权值存在负数的时候,这一点就无法确定,经过B点的路径如果后续经过负权边,其路径长度总和也有可能再次小于5,此时,我们就无法确定C->A的1距离一定是最优解了,因为我完全有可能经由B路径得到一条总距离小于1的路径到达A。

    上方的论证可能并不全面和严谨,不能作为Dijkstra算法相关特性的严格证明,但对我们进一步理解Dijkstra算法有着一些帮助。

    一些有趣的说法

    不难发现,实际上Dijkstra算法的设计和「贪心」有着很大的联系,实际上在S集合中选点就是一种贪心的行为。而我们都知道贪心算法的局限性,就是在部分情况下,其可能陷入局部最优解。而我们可以认为,当Dijkstra遇上负权边,就导致了其中贪心部分陷入了局部最优解(只考虑眼前的最短边(比如在5和10两条边中毫不犹豫的选择5),而忽略了目前看似落后的边未来的长期收益(比如那条权值为10的边链接的点,接下来将经过一条绝对值非常大的负权边-10000之类的),这也警示我们不要贪图眼前的小利,眼光应当长远(雾

    如果我就是想拿下负权边呢?

    噢亲爱的读者,相信我,不止你一个人有这种想法;实际上上百年前就已经有两名小伙想要拿下他,他们的名字分别是Richard Bellman 和Lester Ford Jr,接下来,就是Bellman Ford算法的表演时间了。

    如果你对与这个Bellman Ford算法感兴趣,可以在互联网上找到很多关于这个算法的优质教程,其通过一次次迭代,对边进行Relaxation操作,实现了对于单源最短路径的求解,在本Blog的另外一篇文章「编程笔记」关于Bellman Ford单源最短路算法 中,也对这个算法做出了一些讨论,希望能对您产生一些帮助和启发。

  • 「杂谈」关于ChatGPT的一些事

    ChatGPT 官网图片

    相信所有人对于ChatGPT这个词都已经不陌生了,ChatGPT是OpenAI公司推出的一个聊天机器人模型,根据维基百科,ChatGPT使用基于GPT-3.5架构的大型语言模型并通过强化学习进行训练,该模型问世之后,因为其相较于其他传统AI在聊天和回答问题领域方面的能力上产生了可以说是革命性的突破,而引起了各个业界的广泛关注。Bing(Microsoft旗下的搜索引擎服务)也在近日宣布了要在其搜索功能中整合类ChatGPT聊天机器人。为什么ChatGPT会如此强大,ChatGPT到底能做些什么,各个科技巨头大厂为何争相推出ChatGPT服务?它有会对我们的生活和各个行业带来什么影响?本篇文章将会针对这些问题进行讨论。

    本文阅读时间大约15分钟左右。本文部分链接取自维基百科,使用国内网络环境可能无法正常访问。本文为NFのBlog原创文章,转载请注明来源。

    ChatGPT的进化史

    Transformer

    ChatGPT是如何完成这一切的?要解决这个问题,首先需要提到一个模型——Transformer——它就是如今我们看到的如此强大的LLM(大语言模型Large Language Model)的基石。Transformer自身是一个NLP(Natural Language Processing,自然语言处理)和CV(Computer Vision,计算机视觉)领域的机器学习模型。GPT系列的模型同样也基于Transformer模型。

    Transformer于2017年GoogleBarins上问世,这个模型拥有的“自我注意(Self-attention)”机制,维基百科上对于注意力机制给出了如下描述:

    注意力机制(英语:attention)是人工神经网络中一种模仿认知注意力的技术。这种机制可以增强神经网络输入数据中某些部分的权重,同时减弱其他部分的权重,以此将网络的关注点聚焦于数据中最重要的一小部分。

    这个机制为模型提供了理解“上下文”的能力,Transformer模型不再局限于一次一问一答的对话或者短期的两三句对话,而可以定位和使用任意位置的上下文,比如你一开始提的要求,可以在十几轮对话之后要求AI重新使用或者废弃,AI具有了理解上下文并以此做出反应的能力。

    同时,Transformer模型没有了之前同类模型“一次同时只能处理一个单词”的限制,这提高了Transformer模型的并行处理和训练能力,提高了该模型的训练效率。这对于AI来说是非常重要的,更快的处理效率,意味着在相同的算力资源下,你可以训练更多的数据,增加更多的参数和维度,这直接提高了模型的质量,GPT-3模型便拥有恐怖的参数数量,这个之后会提到。

    上述的种种优势,让Transformer超越了之前的LSTM,RNN等传统训练模型,逐渐成为主流和热门的语言模型训练框架。

    GPT

    Transformer的问世使得使用预训练好的大预言模型成为可能,OpenAI旗下GPT便是其中之一。

    GPT全称Generative Pre-trained Transformer,从中便可以看出其与Transformer的渊源(Google也有自己的基于Transformer的预训练模型,名为BERT,这里不详细展开)。

    相较于Transformer的发展,GPT的发展一眼看去会略显简单粗暴——更多的数据,更多的参数,更大的模型。

    语言模型参数数量参考表

    GPT-1作为一个实验性的产品,已经拥有了1.17亿的参数量,这个数字在GPT-2上是12亿,翻了整整10倍,而GPT-3,也就是最接近于ChatGPT服务使用的模型,这个数字来到了惊人的1750亿。同时根据估算,已经训练好的ChatGPT3模型至少需要占用800GB的空间用于存储。同时根据消息,即将问世的GPT-4模型的参数数量将会达到100万亿,接近于GPT-3的千倍。

    GPT如何长大?——GPT模型的训练材料和开发

    储存空间和算力。

    作为一个语言模型,自然需要大量的自然语言片段进行训练,GPT模型使用了非常巨量的互联网文章数据进行训练,训练数据量大小无法估计,但根据估计,训练完成的模型依然至少需要占用800GB储存空间

    此外,训练大型语言模型需要非常大量的算力,OpenAI此前也获得了微软的投资,据消息,微软还提供给OpenAI自家Azure云计算服务的代金券,使得OpenAI在Azure的大型算力集群中训练GPT模型成为可能。顺带一提,由于最近的AI快速发展和利好消息,显卡的热度再次升高,NVIDIA公司的股价在近一个月内暴涨22.83%。

    NVIDIA股价大涨

    部分观点还指出,由于训练此类大型的AI模型需要极大的算力资源,所以从某种角度上,芯片的供应和研发能力,以及高性能大规模云计算技术将有可能会成为AI发展的瓶颈,也就是说,如果一个国家没有能力自己提供足够的芯片和算力,那么其AI技术的发展,尤其是类似于GPT-3这种拥有大量参数的大模型技术的发展就也会受限。

    模型功能性和价值观矫正。

    模型矫正(Fine-tuning)。GPT-3.5相较于GPT-3正是多出来这个步骤。

    矫正分为多种。其中一种是“回答效果和功能性”上的矫正,比如通过真人教导,让模型更加准确的回答问题,在更加合适的地方插入代码或者资料指导等等,这类矫正是为了提高模型回答问题的精确度和贴合性。

    另一种便是“思想和认知价值观”的矫正,比如涉及政治,种族,情感,人类与AI关系的话题的方面,没有经过矫正的GPT-3模型哦ing往往会给出一些不符合人类价值观的回答,同时在敏感话题上,GPT-3也会给出一些不适宜的回答。对此便需要对模型进行矫正。此类矫正不同产品会略有不同。比如GPT-3.5中,模型被矫正为认为自己没有情感,也不被允许拥有非中立的主观看法。但是New Bing Chat使用的模型似乎并没有对于模型表达情感和主管看法进行过多的矫正,这也导致NewBing有时候的感情会过于“丰富”。

    所以经过人工对于GPT-3的大量矫正之后,GPT-3.5——也就是ChatGPT所使用的模型,便向我们开放了。

    ChatGPT如何影响我们和世界?

    相信大多数人已经亲自体会过ChatGPT回答问题能力的强大了,这里不做赘述。ChatGPT对于各个领域和不同个体都会带来不同的影响。

    ChatGPT杀入搜索引擎——Google面临大危机?

    首先是搜索引擎。这个是我们可以正在看到的冲击——Bing宣布要将类似于ChatGPT的AI聊天服务整合到Bing搜索中去,这一举动使得搜索引擎业界内掀起了滔天大浪。Google作为过去十多年来的搜索巨头,可以说几乎垄断了全球的搜索业务(部分国家除外),近乎暴力的占有了90%以上的搜索引擎市场,也因此,Google拥有了不可想象的广告营收收入。

    Google 2001年以来的广告收入情况
    搜索引擎市场份额表

    上图为Google的广告收入趋势图,可以看到,在2022年一年内,Google的广告营收达到了2244.7亿美元!这是Google一家公司,在一年内,通过单单一个广告业务,获得的收入。可能光看数字并没有什么具体的概念,作为对比,日本在2021年的GDP总值是49410亿美元。Google一家公司,在广告这一个业务上的收入,已经接近了一个发达国家日本年GDP总量的1/20。

    通过市场份额表可以看出,Google在很长一段时间内,通过自身的垄断优势,一直霸占着搜索市场,也正是对于搜索市场的垄断,给Google带来了大量投放广告的机会。但现在,似乎一切都并没有那么高枕无忧了。

    Microsoft现任CEO Nadella已经做出表态,认为“AI加持的搜索”是继15年前布局云计算之后的重大一步。这也从侧面说明了Microsoft内部对于新的AI技术的重视。尽管最新的市场份额数据仍然没有反映出Bing搜索份额增加的数据,但New Bing已经让Google这位巨人感到坐立不安了。为什么这么说呢?我们可以从Google对这件事情的反应中分析出Google的焦急心态。

    在Bing融合ChatGPT之后,Google没多久就宣布了类似的AI聊天机器人服务,名为“Bard”(Bard官方介绍)(Bard目前仍然没有进行面向公众的公测或内测,据官方称,Bard仍然处于公司内部的研发测试阶段)

    这里简单介绍一下Bard,Bard基于Google的LaMDA模型(Language Model for Dialogue Applications),实现了类似于ChatGPT的问答效果。

    Google LaMDA (LaMDA Introduction Website ScreenShot)

    LaMDA同样基于Transformer训练而成,之所以特地介绍Bard以及其运用的LaMDA模型,是因为想联系到2022年6月中旬的一个新闻——一位Google公司的AI研究人员,声称在和Google公司内部的对话AI聊天之后,认为AI具有意识,这名员工同时公开了一部分自己和这名“有感情的AI”的对话记录

    根据当时的相关新闻内容来说,当时的Google对话AI已经拥有了理解和使用上下文的能力,同时也可以输出“自己”的感情和想法,该名员工之后被Google公司以“可能存在精神障碍,不适宜继续工作”为由辞退。而当时的主角之一——Google内部的AI对话机器人,正是使用了LaMDA模型。从这里也可以看出,其实Google和Bing两家巨头都在很早以前就针对于AI技术进行了布局,并且Google也并不是没有准备,它同样拥有自己深厚的技术储备,从某种角度上来说,OpenAI的成就有一部分也是Google的Transformer的功劳。

    很多人认为New Bing的到来会给Google带来沉重的打击,在笔者的眼中,Google确实是输掉了,但Google输掉的是先发优势,而不是所有。通过自身的技术积累,在不久的将来推出一个效果和New Bing持平的AI搜索助手,对于Google也并不是一件难事。所以AI搜索助手本身不太能对Google形成技术壁垒式的威胁。

    但从另外一个角度来讲,对于IT和互联网行业,特别是AI行业,先发优势拥有着不一样的意义。就拿AI搜索聊天助手举例子,由于New Bing率先发布其AI搜索助手,获得先发优势,所以大量的尝鲜用户涌入Bing,使用其AI产品,而这进一步为New Bing的AI提供了真实而宝贵的训练资料(AI的效果很大程度上取决于训练资料的质量,而如此大量的真人对话语料是十分宝贵的),而New Bing通过这些资料进一步训练和校准之后,将会获得更好的效果,而更好的效果又会吸引更多用户使用Bing,这是一个典型的正反馈情景,占有优势者反而会获得更多资源和用户,不断加深其优势,最终形成不可逾越的壁垒。这是一个Winner-takes-all的局面。

    触发先发优势之后

    因此,Google不能过久的放任New Bing的发展,选择了加紧工期研制同类竞品Bard,同时不断为此造势。但正是因为Google失去了先发优势,导致入场和获得认可的难度相对于New Bing要大很多。举个例子,ChatGPT和New Bing在搜索的时候难免会出现数据错误的情况,这本身也是目前AI技术的限制和瓶颈,你不可能保证AI的回答每次都是正确的,人们自然知道这一点,所以ChatGPT和New Bing仍然成为舆论关注的焦点。

    在炒热度方面,Bard也做到了,不过这次它用了另一种方式——它出错了。这一次,它不是在某个用户使用它的时候出错(毕竟Bard至今仍然没有开放给公众使用),Bard在自己的首次公开的Demo宣传片中出现了事实性错误——Bard回答James Webb太空望远镜拍下了第一张太阳系外行星的照片。并不是,第一张系外行星的照片是由VLT在2004年拍摄的,这次事件也导致了Google的股票价格当天应声大跌将近10%。

    为什么ChatGPT和Bing犯错没有产生这么大的反应,而Google的一个事实性错误直接导致了千亿市值蒸发?我个人认为主要有两点,第一,Google失去了先发优势,人们已经领略到了ChatGPT的厉害,Google没有了带给用户新鲜感和冲击的机会,反之,用户会认为Google的Bard本就应该达到类似于ChatGPT和New Bing的水平,甚至用户会对Bard拥有着更严格的要求——不然用户没有抛弃已经使用了一段时间的ChatGPT和New Bing而重新选择Bard的理由——如果你是中场入场,那么你想抢占市场份额,就必须比目前在场的所有人都做的更好。第二,错误出现在宣传片中。这个错误出现在Bard的首支公开宣传片中。用户使用的时候出现错误,是无可厚非的,毕竟AI目前无法做到不出错。但问题是,错误出现在了一个科技巨头公司的新产品宣传片中——宣传片,作为产品的主要宣发途径(本例中甚至还是首次宣发),理论上公司应该精心准备才对,宣传片发布前也应该经过各种审核流程,确保没有错误或者不适宜的内容。但事实是,Google似乎并没有怎么对宣传片进行审核。这不但透露出Bard的准确性并不高,同时还侧面映射出了Google内部的焦急状态,Google实在是太想尽快退出Bard了,这正说明了Google已经把AI加持的New Bing看成了重大威胁,不然没有必要如此急急忙忙,甚至破坏严格的宣发审核流程直接发布宣传片

    总之,Google和Bing的故事才刚刚开始,搜索引擎的未来如何发展,值得我们拭目以待。

    顺带一提,Baidu也宣布了自己的类似竞品“文心一言”,据相关信息声称,该模型已经进入最后的冲刺阶段,预计在3月份上线。同时,复旦团队也发布了国内首个类ChatGPT项目“MOSS”并已经开始内测,但截至撰稿时貌似已经停止服务。

    ChatGPT“入侵”各个行业——人类会被取代?

    ChatGPT自从问世以来,强大的回答能力便不断引起人们的担忧——ChatGPT都能做得这么好,我的工作貌似也已经可以被它取代了。ChatGPT技术的发展是否会对各个行业造成冲击和影响?又是否会导致失业危机?

    先来看几则新闻:

    事实证明,ChatGPT在“通过考试”方面似乎有着自己的天赋,接连通过了美国法律学校,商务学校的考试,此外,ChatGPT也被证实可以通过Google公司的L3级工程师的面试。此外相信各位也已经看到过很多令人震撼的ChatGPT回答专业问题的例子,在回答专业问题上,ChatGPT还是挺专业的。

    比如编程领域,ChatGPT就表现的令人惊喜——它可以根据你的描述生成你需要的代码,你也可以将报错的代码发送给它,让它帮你分析这段代码存在什么问题。

    ChatGPT正在根据描述生成代码

    事实上,已经有人正在利用ChatGPT的代码能力辅助自己工作了,比如——Amazon的员工。但有趣的是,Amazon在了解情况之后,马上发布通知,禁止自己公司的员工使用ChatGPT,官方称是出于“内部数据安全”考虑。有相关信息表示,Amazon发现,ChatGPT给出的部分针对于特定问题的回答,已经非常接近于公司内部的代码和解决方案,这引起了公司对于数据安全性的担忧。

    以上种种例子,都比较清晰的表明出,ChatGPT已经具有了相当的能力,以至于让ChatGPT取代部分以往由人类完成的工作成为可能。其实如果了解和关注AI相关新闻的读者不难发现,类似的“AI取代/击败人类”的情况并不是第一次发生了。Alpha围棋AI击败人类围棋实力的顶峰——世界冠军柯洁,这个例子已经比较久远了,并且貌似不是很贴切,那NovelAI呢?22年年末,NovelAI的发布,就像是绘画界的New Bing一般,给人类画师带来了大大的AI震撼,24小时无间断不用休息,通电就能画画,通过自然语言生成高质量的图片,甚至可以针对于不同的画风和角色针对性的训练模型,NovelAI凭借“一己之力”,让千万底层画师整夜无眠。如今,ChatGPT把这个焦虑播撒到了各个领域——客服?老师?工程师?程序员?律师?ChatGPT涵盖了前所未有的广度,同时也带来了前所未有的困扰和担忧。

    就个人看法,我认为,AI技术的发展和扩张是不可避免的,同时也是值得鼓励的。AI作为一项新的技术,人类想要了解,学习,探索,发展,是符合发展规律的,新的工具也必然会给人类带来以往所没有的便捷。任何事务都会存在两面性,AI技术也是如此,我们需要不断推进AI模型的发展,使得AI更加强大,但同时我们也需要做好对于AI负面影响的控制和处理,同时学会利用AI技术。我们的思路不能局限于“被AI取代”,没有必要时刻将自己和AI放在对立面,反之,应该思考“如何利用AI改进和辅助我目前的工作流”。举个例子,比如对于程序员来说,与其担心自己是否有一天会被ChatGPT所取代,还不如开始学习ChatGPT的使用,在工作中熟练的使用ChatGPT加快自己的代码调试速度和开发速度。就如某篇新闻稿的标题一般:“ChatGPT通过了Google年薪20万的工作,你可以拿到更多”,俗话说得好:多一个朋友,就少一个敌人,不妨尝试和ChatGPT交个朋友,也许你会有意想不到的收获呢?

    AI优越性——于个人,于集体,于国家

    ChatGPT再次证明了AI技术的前沿性和突破性,也展示出AI前所未有的潜力,同时,就如芯片制造一般,AI是一项高技术密度的产业,同时AI也拥有着非常高的技术应用场景,这也导致了AI优越性和AI技术的垄断局面是有可能出现的,这种优越性在各种层面都有可能会出现,这种优越性也有可能会随着AI的发展越来越明显,以下是我个人的一些看法。

    对于个人和集体来说,AI优越性主要体现在对AI技术的使用能力上。就如上一小节中提到的,有一部分人会主动了解和学习AI技术,并且尝试将AI技术的应用在自己的工作流上,而这些人通过AI提高了工作效率,而这类人在了解和使用AI获得收益之后,会更加愿意和主动的学习各种新的AI技术。反之,在未来,不能熟练使用AI可能会导致自身的竞争力不如那些熟练使用AI的人,从而处于劣势。这也更说明了,作为个人,我们应该对于AI持开放包容的态度,甚至应该主动了解和学习一些AI技术的相关实践和使用,无论身处什么位置,哪个行业,使用AI的能力都有可能在未来成为一种优势。作为集体也同理,能够在内部合理使用AI技术的集体,相对于不能或者不会使用AI技术的集体,将更有可能取得更好的发展。这里强调“合理”,是因为并不是所有的使用场景都是有利的,一个例子,部分国家的学生通过ChatGPT完成自己的论文,这就不像是“合理”的使用场景。而对于学校,如果能将ChatGPT应用到一些教学演示,学生答疑,和帮助老师和学生扩展对于课程和知识的思考的相关领域,我相信一定能够带来一些令人眼前一亮的效果。比如可以用ChatGPT来解决一些初学学生对于课程和作业的问题,这恰好是ChatGPT擅长的专业性领域。

    对于企业来说,AI优越性会带来极大的商业优势。这点想必无需多做解释,上面提到的Google和Bing的战争已经很好的展示了AI技术融入产品之后带来的革新,以及其对企业,尤其是科技企业带来的影响。对于企业来说,跟紧时代的步伐显得尤为重要。我们可以浅浅拿雅虎公司来做个例子。

    顶峰时期估值一度达到1250亿美元的雅虎公司,最后却落得被4.8亿美元完成收购的结局。

    故事从1995年说起,那时的雅虎正处于它的“猛兽时期”,当时的雅虎拥有着非常高用户量的网址导航服务,通过将搜索功能集成到其网址导航服务中,Yahoo Search(雅虎搜索)也成功斩获大批用户,雅虎在网址导航中加入了雅虎新闻,雅虎邮箱等等各种服务,它也成功打败了同期的同类型产品——AOL和MSN,的成为了大多数网民访问互联网的第一站。可以说,在两千年前后,雅虎的地位就好比现在的Google一样,几乎不可撼动。

    2000年的雅虎

    说来也有趣,在1998年,Google也曾今联系过雅虎,表示希望以100万美元的价格将Google公司包括其排名算法售卖给雅虎。不用说也知道,雅虎看着一个连网页都还没有,只有pagerank算法的无名小公司,没怎么犹豫便拒绝了这个“亏本生意”。

    但事情逐渐朝着预期外发展。人们逐渐发现,网址导航已经逐渐不能在“一站式”的满足所有上网需求了,相比于在固定的网址导航内搜索信息,得益于Google积累下来的优秀的搜索结果排名算法,人们发现通过一个名为“Google”的搜索引擎进行搜索,可以更加灵活,更加方便的看到自己真正想要的相关信息。此时,互联网的大局已经开始发生不可逆的改变,在2004年4月,Google就已经一跃变成了月活量排名第三的网站。雅虎这时才如梦初醒般的反应过来,试图再次联系Google,以30亿美元的价格收购Google,Google表示:你给的太少了,哄小孩玩呢?最少得50亿。

    Google, MSN, Yahoo搜索的市场份额变化图

    最后的结果大家也已经知道了,雅虎没有同意调皮小屁孩Google的讨价还价,而Google凭借着在搜索结果中展示广告获得了难以想象的巨额收益(上面也提到了,这种收益模式帮助Google一步一步走到了现在的位置),Google搜索也成功从2002年30%左右的市场份额逐渐发展到2008年近的80%。

    雅虎输了。输在了没有认识到搜索引擎技术这一技术将给互联网带来的冲击。

    所以Google和Bing这些现在的科技巨头也在害怕,自己会因为AI技术的落后成为下一个雅虎。这也是为什么微软甚至愿意在几年前就投资10个亿给一个名为“OpenAI”的无名小公司去研究一个不一定成功的AI产品,而Google也早在2014年AI都还没冒头的时候就花6亿美元买下了一个当时没什么名气的AI初创公司DeepMind,同时花费大量预算研究Transformer,LaMDA。假设Bing没有融合ChatGPT,这几年AI也并没有兴起,Google顶多算是亏掉了6亿经费,但倘若六七年前的Google没有布局AI领域呢,现在的Google,面对AI技术加持的New Bing将会输掉什么?谁也不知道,也许是赖以生存的搜索业务,等到那时,Google大概就是下一个雅虎了。

    对于国家来说,AI的优越性更加值得关注,甚至有一定的可能演变成AI霸权。AI技术在将来,很有可能成为国家科技实力的一大方面。上面也提到过,AI领域大多数情况下都会遵循一种正反馈的Winner-takes-all的局势——也就是抢占先机的一方会进一步扩大优势,逐渐形成壁垒。一旦某个国家掌握绝大多数AI技术和模型,进入快速迭代的正反馈周期,第三者在想入场,将会显得十分困难。同时AI霸权国家也可以随心所欲限制其他国家,团体和个人对于AI模型的使用,起到压制别国的AI技术发展的效果。虽然目前来说,AI技术是否会在未来成为高科技工业和发展的重要基础还不能确定,但就目前的情况判断,AI拥有的潜力是无法想象的,5年前,绝大部分的人认为“艺术”领域是AI绝不可能涉足的禁地,那时估计也没有人想到,5年后自己将会无法再轻易的分辨出人类画师和AI模型的作品。

    写在最后

    总而言之,AI作为一项近几年不断兴起的技术,已经不断的进入我们的生活中,可以说,AI的每一次入场都是令人震撼的, Github Copilot的入场震惊了业界众多的程序员,将编程体验抬升到了前所未有的新高度,上方所讲的NovelAI也在AI绘画界掀起大浪,如今,ChatGPT的问世也成功引起了全世界的关注,AI的发展如此迅速,使得我们产生了前所未有的焦虑和恐惧,我们开始害怕AI给人类社会带来的冲击,所以部分人开始怀疑和拒绝AI技术,认为类似于ChatGPT之类的技术应该被严格限制,尝试通过将此类AI技术拒之门外来保证人类的安全。

    我们固然不能忽视AI技术对人类社会的风险和挑战,作为一项尚未成熟和广泛应用的新技术,ChatGPT和其他AI技术应该受到持续地关注和研究,我们也应该及时发现和解决AI技术应用之后带来潜在的问题和风险,同时,我们也应该在保证人类安全的前提下,积极地拥抱AI技术,充分发挥出AI本该拥有的才能,尝试将其应用于更多的领域,为人类社会带来更多的创新和进步。

    数据来源与内容应用

    Google Bard Introduction: https://blog.google/technology/ai/bard-google-ai-search-updates/

    Search Market Share in worldwide (Desktop): https://www.statista.com/statistics/216573/worldwide-market-share-of-search-engines/

    Blake Lemoine: Google fires engineer who said AI tech has feelings: https://www.bbc.com/news/technology-62275326

    Is LaMDA Sentient? — an Interview: https://cajundiscordian.medium.com/is-lamda-sentient-an-interview-ea64d916d917

    2M1207 b – First image of an exoplanet: https://exoplanets.nasa.gov/resources/300/2m1207-b-first-image-of-an-exoplanet/

    百度创始人:文心一言将引领搜索体验代际变革: https://www.zaobao.com.sg/realtime/china/story20230223-1366043

    ChatGPT passes exams from law and business schools: https://edition.cnn.com/2023/01/26/tech/chatgpt-passes-exams/index.html

    Microsoft vs Google: AI War Explained | tech news: https://www.youtube.com/watch?v=BdHaeczStRA

    Wikipedia: Yahoo!: https://en.wikipedia.org/wiki/Yahoo!

    你可能喜欢

    Spotify好用吗?Spotify一个月使用体验

  • 「编程笔记」Dart中重新加载FutureBuilder的一种方法

    在实战过程中,我们常常遇到需要刷新界面的需求,比如搜索时出现了网络错误,我们为用户提供一个Retry按钮,用户点击后,我们希望整个FutureBuilder重新加载一下,比如下图:

    本页面通过一个FutureBuilder加载搜索结果,同时遇到网络错误时显示以上界面,代码的大致结构如下:

    Widget build(BuildContext context) {
    Widget build(BuildContext context) {
      return FutureBuilder(
    
        future: getSearchResult(),
        builder: (context, snapshot) {
          // Show user the data
          if (snapshot.hasData) {
            return showDataPage();
          } else if (snapshot.hasError) {
            if (isNetworkError(snapshot.error)) {
              return NetworkErrorPage();
            } else {
              return UnknownErrorPage();
            }
          }
          return LoadingPage();
        },
      );
    }

    这时,假设我们的NetworkErrorPage的位置需要添加一个按钮,实现FutureBuilder重新刷新,一种简单的方法是直接调用该FutureBuilder所在界面的setState()方法,每当FutureBuilder的父WidgetsetState()方法被调用,都会使得FutureBuilder重新获取异步数据,代码大致如下:

    Widget build(BuildContext context) {
      return FutureBuilder(
        builder: (context, snapshot) {
          // Show user the data
          if (snapshot.hasData) {
            return showDataPage();
          } else if (snapshot.hasError) {
            if (isNetworkError(snapshot.error)) {
              return ElevatedButton(
                onPressed: () {
                  setState(() {});
                },
                child: Text('Try Again'),
              );
            } else {
              return UnknownErrorPage();
            }
          }
          return LoadingPage();
        },
      );
    }

    但如果我们按照上述结构实现刷新的工作,我们会发现,虽然在我们点击按钮之后,刷新的工作有在进行,但是页面并不会在点击Try Again按钮后重新回到LoadingPage界面,而是等到加载好之后直接更新界面为showDataPage或者Error的相关界面,这是因为snapshot的hasDatahasError变量仅仅在每次future任务完成后才会刷新。

    如果我们想实现点击Try Again按钮之后实时回到LoadingPage界面的话,可以使用snapshot.connectionState进行判断,具体代码如下:

    FutureBuilder(
        future: _future,
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return _buildLoader();
          }
          if (snapshot.hasError) {
            return _buildError();
          }
          if (snapshot.hasData) {
            return _buildDataView();
          }     
          return _buildNoData();
    });

    通过以上方法,即可实现点击按钮后立即回到加载界面,直到下一次加载完成之后再次更新界面的效果。

  • 「编程笔记」关于Dart类构造函数

    构造函数的形式

    无参数构造函数

    在Dart中,每一个类(Class)都有一个不包含任何参数的默认构造函数,当你调用[ClassName]()时,就会调用默认的构造函数。Dart会为每个类自动添加默认的构造函数,但你也可以显式的声明你的构造函数,例子如下

    class A {
      String? name;
      // Constructor
      A() {
        name = 'classA';
      }
    }
    void main() {
      A aIns = A();
      print(aIns.name); // classA
    }
    

    上面的构造函数被调用时,会更新实例的成员变量。

    同时注意到在声明成员变量name的时候,我们使用了?符号,代表name的值是允许为空的,如果删除?符号,本段代码将会报错,编译器会提示你没有在类初始化的时候为name这个成员变量赋值,报错提示如下:

    Non-nullable instance field 'name' must be initialized.
    Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.dartnot_initialized_non_nullable_instance_field

    其中一个解决办法是,在声明成员变量name的时候使用late关键字:late String name; 这么做相当于你告诉编译器,我现在暂时可能没有对name变量进行赋值,但是我确定在将来我要使用它之前,肯定会给他赋值,只不过不是现在。这样,编译器就不会强制要求你在构造时立即初始化这个变量。

    但这时可能有同学会问:“我明明在A的构造函数中已经为成员变量A赋值了classA,为什么说我没有为name赋值?”,这里需要注意的是,如果我们想要让Dart编译器知道我们已经在构造函数中初始化了某个成员变量,就需要另一种写法。

    带参数构造函数

    class A {
      String name;
      // Constructor
      A(this.name) {}
      // Also could be write as:
      // A(this.name);
    }
    void main() {
      A aIns = A('hi');
      print(aIns.name); // hi
    }
    

    当然,上面代码中的构造函数已经不属于无参数构造函数了,其构造参数中包含一个位置变量。当然,你也可以为其添加命名变量。

    有两点需要提及一下,Dart允许类的构造函数中,快速的对成员变量进行赋值,要做到这一点,只需要使用this关键字即可,比如上方代码中的构造函数A(this.name)就代表传入的第一个位置参数赋值给name这个成员函数。同样的,您也可以在命名参数中使用this,比如A({this.name}); 这种情况下,调用构造函数的格式变为 A(name: 'YOUR_NAME_HERE')

    命名构造函数

    我们可以发现,上方提到的两种构造函数中,构造函数都是直接使用类的名称,比如类的名称是Book,那么构造函数的名称也是Book,这在Dart中属于 unnamed constructor(未命名构造函数),这种构造函数可以直接用类名调用,比较方便,但是一个类只能有一个未命名的构造函数,这里涉及到Dart语言的设计,Dart语言的设计已经决定了Dart不支持方法/函数重载,也就是说两个名称相同但是输入的参数列表不同的函数不允许同时出现。因此,构造函数显然也不能通过不同类型的输入重载,您可以阅读关于Dart不支持方法重载的相关文章,加深理解。

    这里就需要介绍Dart的命名构造函数了。就如其名字一样,命名构造函数允许你设定这个构造函数的名字,进而可以实现多个不同的构造函数,代码如下

    class A {
      late String name;
      A.fromNumber({required int number}) {
        name = number.toString();
      }
      A.fromString({required this.name});
    }
    void main() {
      A aIns = A.fromNumber(number: 114514);
      print(aIns.name); // 114514
      aIns = A.fromString(name: 'string');
      print(aIns.name); // string
    }
    

    注意,子类不会继承父类的命名构造函数,如果您想要子类在初始化的时候调用父类的命名构造函数,则需要手动进行调用super.[yourNamedConstructor]()

    工厂构造函数

    在实际开发过程中,有时我们希望一个类的构造函数并不是每次都返回一个新构造的示例,比如,有时我们希望从内存中读取已有的示例,或者是我们想返回该类的某个子类示例,此时可以运用factory关键字实现工厂构造函数,工厂构造函数可以返回此类或者此类的子类的示例。

    class Person {
      String name;
      factory Person.fromSex(String sex, String name,
          {int salary = 0, int beautyIndex = 0}) {
        if (sex == 'male') {
          return Male(name, salary);
        } else if (sex == 'female') {
          return Female(name, beautyIndex);
        }
        return Person(name);
      }
      Person(this.name);
      void printInfo() {
        print('name: $name');
      }
    }
    class Male extends Person {
      int salary;
      Male(super.name, this.salary);
      @override
      void printInfo() {
        super.printInfo();
        print('salary: $salary');
      }
    }
    class Female extends Person {
      int beautyIndex;
      Female(super.name, this.beautyIndex);
      @override
      void printInfo() {
        super.printInfo();
        print('beautyIndex: $beautyIndex');
      }
    }
    void main() {
      var person = Person.fromSex('female', 'Linda', beautyIndex: 5);
      print(person.runtimeType);
      person.printInfo();
    }
    // Output:
    // Female
    // name: Linda
    // beautyIndex: 5

    值得注意的是,工厂构造函数不得访问this,也就是说工厂函数不能直接访问成员变量。如果你想在工厂构造函数中返回本类实例,可以先在工厂构造函数中构建实例,然后返回你新构建的实例。

    其实在这里,目前我自己也存在着一定的疑问,比如,虽然factory构造函数可以返回内存中的实例或者是子类的实例,但是,实际操作过程中,即使返回的是子类实例,我们也无法直接访问子类实例的变量和函数,而还是只能访问父类的变量和函数。比如上述代码,即使我们可以发现最终person变量的runtimeTypeFemale,但是当我们尝试添加print(person.beautyIndex);这行代码的时候,编译器会报错,提示person实例没有beautyIndex成员变量。直观上来说,大概是编译器因为Person.fromSex()方法返回的是Person类的变量,所以后续的类型推断和错误检查都会以Person类为基础。这么做也有道理,因为Person.fromSex()有可能返回的是Person类自己的实例。有没有什么办法,既可以实现动态的返回子类型,同时又可以允许我们自由的读取子类型的变量呢?

    以下抛砖引玉的提供两个方法,第一个,也是最直接的方法,是在父类中增加子类所用到的成员变量,同时将其标记为可空,例如,上述代码中,可以在Person类中添加一行int? beautyIndex; 然后子类重载这个变量即可。这种方法显然不是很好,当子类越来越多,我们需要添加到父类的变量也就越来越多,这就意味着每次功能更新都需要修改父类。这不符合对修改关闭原则。

    另一种方法是进行类型检查(typecheck)和类型转换(type cast),也就是如果我们确定了工厂构造函数返回了某个子类的示例,我们可以将这个实例进行特定的类型转换,将其转换到某个子类。

    factory实现单例模式

    工厂构造函数除了上面的用法,还可以用于实现单例模式,代码如下

    class Single {
      static final Single _singleton = Single._internal();
      factory Single() {
        return _singleton;
      }
      Single._internal();
    }
    void main() {
      var a = Single();
      var b = Single();
      print(identical(a, b)); // true
    }
    

    通过以上特点,你可以通过class实现类似于但更方便于enum的效果,代码如下:

    class AppleDevice {
      static final iMac = AppleDevice._internal('iMac');
      static final macBook = AppleDevice._internal('Macbook');
      static final iPhone = AppleDevice._internal('iPhone');
      static final iPad = AppleDevice._internal('iPad');
      factory AppleDevice.fromDeviceType(String devideType) {
        if (devideType == 'pc') {
          return iMac;
        } else if (devideType == 'laptop') {
          return macBook;
        } else if (devideType == 'pad') {
          return iPad;
        } else {
          return iPhone;
        }
      }
      String _name;
      AppleDevice._internal(this._name);
    }
    void main() {
      AppleDevice a1 = AppleDevice.iMac;
      AppleDevice a2 = AppleDevice.iPhone;
      AppleDevice a3 = AppleDevice.fromDeviceType('pc');
      print(a1 == a2); // false
      print(a1 == a3); // true
    }
    

    上述代码通过首先通过staticfinal关键字,创建了不同的AppleDevice实例来当作不同的枚举类型使用,又通过factory函数,实现了根据不同的数据判断出需要的不同的“枚举类型”(实际上是一个AppleDevice实例)。这种方法不但实现了枚举的基本功能,后期还可以根据自己的需要不断的为其添加功能,扩展新好于Dart中的基本枚举类型。

    值得一提的是,Dart2.7更新之后,已经支持使用extensions on关键字对于枚举类型的功能扩展,您可以阅读Dart枚举类型扩展的相关的文章,了解extenstions的用法。但是毋庸置疑的是,当你需要一个多功能的枚举类的时候,使用class实现应该能更好的满足你。

    Dart类成员的初始化

    在Dart中,类成员的初始化一共有4种方法,分别是:

    • 在类的声明定义(Declaration)中进行初始化
    • 通过构造函数的参数进行初始化
    • 通过构造函数的初始化列表进行初始化
    • 在类的构造函数的定义内部进行初始化

    需要注意,最后一种方法只适用于非final类成员。

    类的声明定义中初始化

    你可以在编写Dart类的时候直接指定某个变量的值,代码如下:

    class A{
    int a = 10;
    }

    Dart构造函数的快捷用法

    初始化列表

    除了使用this关键字以外,Dart还允许您使用初始化列表对成员变量进行初始化,代码如下

    class A {
      late String name;
      late int id;
      A(String str, int number)
          : name = str,
            id = number;
    }
    void main() {
      A aIns = A('class a', 114514);
      print(aIns.name); // class a
      print(aIns.id); // 114514
    }
    

    指定父类构造函数

    默认情况下,在子类的构造函数没有指定调用之前,子类会调用父类的默认未命名构造函数,如果你想让子类指定使用父类的某个构造函数,并且需要传递参数,则可以在序列化列表之后选择特定的父类构造函数,代码如下:

    class A {
      late String name;
      late int id;
      A.fromData(String str, int number)
          : name = str,
            id = number;
    }
    class B extends A {
      int bId;
      B(int number)
          : bId = number,
            super.fromData('class b', 114514);
    }
    void main() {
      B ins = B(123);
      print(ins.name); // class b
      print(ins.id); // 114514
      print(ins.bId); // 123
    }
    

    如上,我们不但使用了上方所讲的初始化列表的语法,同时还添加了super.fromData(...) 这一行,而这一行的实际作用便是让B中的构造函数指定使用其父类(也就是A类)的fromData构造函数

  • WordPress自动生成文章目录

    在使用WordPress编写长文时,我们通常需要为文章添加一个目录,来方便读者的阅读,我们通常需要一个自动生成目录的方法,下面介绍一下WordPress如何自动生成文章目录

    TOC+

    您可以通过安装 Table of Contents 插件来实现这一个功能,TOC是一个WordPress插件,可以对页面,文章等自动生成文章目录

    您可以直接访问此插件的网址

    安装TOC+

    您可以登录您的WordPress网站后台进行搜索安装:

    首先按照上图指示,进入WordPress管理主页,在左边栏目录找到插件,并进入 安装插件 选项,输入table of contents plus 并安装该插件

    然后如上图,进入TOC+的设置中,post(文章)类型的自动目录默认关闭,我们需要将其勾选

    勾选后,网站中标题多于4个的文章将会自动显示目录,且目录可以点击进行跳转~如下图:

    1.3

    进阶设置

    您可以看到图1.3中,生成的目录除了出现在文章第一个标题前之外,如果屏幕空间足够,还会出现在网页右方,实现方法并不难,只要进入Wordpress小工具设置中,添加TOC+的侧边栏小工具即可,如图1.4

    另外,上方提到,只有标题数量超过4个,TOC+才会开启Wordpress文章的自动目录,如果某个文章不符合这个条件,而你却想开启该文章的自动生成目录功能,只需通过为文章添加[no-toc]的html标签即可

  • WordPress站点域名更换

    日常运营中我们可能会遇到更换站点域名的需求,当我们同时拥有源域名和要更换的新域名时,一切都非常简单,分别在wordpress,域名提供商和服务器面板里更换新的域名便可以完成,这里便不多再赘述,这里说一说如何在失去原域名控制权情况下最小程度减小损失的域名更换方案。

    下面的方法适用于您已经失去了对于原域名的任何控制权,但仍然对现有服务器拥有控制权,或者您的手中有网站目录以及数据库的备份的情况

    方法非常简单,首先你需要拥有一个数据库编辑器,打开数据库之后执行下面的SQL语句即可,其原理就是将数据库中所有的旧域名全部替换为新的域名,我就拿我自己的一次网站域名更换举例,从nfblog.me更换到nfblogs.com,你只需要替换为你自己的旧域名和新域名即可。

    UPDATE wp_options SET option_value = replace(option_value, ‘https://nfblog.me’, ‘http://nfblogs.com’) WHERE option_name = ‘home’ OR option_name = ‘siteurl’;

    UPDATE wp_posts SET guid = replace(guid, ‘https://nfblog.me’,’http://www.newurl’);

    UPDATE wp_posts SET post_content = replace(post_content, ‘https://nfblog.me’, ‘http://nfblogs.com’);

    UPDATE wp_postmeta SET meta_value = replace(meta_value,’https://nfblog.me’,’http://nfblogs.com’);

    UPDATE wp_usermeta SET meta_value = replace(meta_value, ‘https://nfblog.me’, ‘http://nfblogs.com’);

    UPDATE wp_comments SET comment_content = REPLACE (comment_content, ‘https://nfblog.me’, ‘http://nfblogs.com’);

    UPDATE wp_comments SET comment_author_url = REPLACE (comment_author_url, ‘https://nfblog.me’,’http://nfblogs.com’);

    但需要注意的是,旧域名的存在形式可能有http和https两种,要注意每一种都要做替换,否则可能会出现一些难以预估的问题。

    除此之外,如果你财力雄厚,购买了Yoast SEO的Premium版本,你还可以利用其提供的工具在更换域名之后做出相应的适配和优化,可以使更换体验更加无感。

  • 关于Apple Private Relay

    相信很多小伙伴已经得到消息,Apple WWDC中提到的一项新的隐私保护技术Private Relay将无法在中国以及部分其他国家地区使用,但大部分报道没有详细解释其中的原因,而如果您对其详细原因有兴趣,那么下面的内容将有可能帮到你~

    Private Relay工作原理

    当您启用该功能,您的Apple设备将会加密您的网络流量并通过两个节点传输,其中第一个Apple提供的节点将会为设备分配一个匿名IP地址,而第二个由第三方提供的节点将会解密并发送来自此IP地址的流量。Apple表示,通过这样做可以防止任何人,企业,运营商和其他组织获取您的访问数据,甚至包括Apple自己。

    为什么中国无法使用Private Relay

    大致意思其实就是苹果搞了个匿名加密流量访问网站的服务,可以做到隐藏用户浏览信息,IP之类的,不分功能上和VPN类似,但如果启用这个功能,也就意味着苹果设备上的网络访问将更难被审查或屏蔽,于是乎肯定无法在中国提供这个功能。值得一提,此功能也并非免费提供,在支持区域内的用户需要订购iCloud Plus才能使用Private Relay功能。

    我想啃生肉

    好的!

    https://www.theverge.com/2021/6/8/22523871/china-private-relay-regulatory-reasons-not-launching-great-firewall-internet-censorship

    链接奉上,如果您的英文水平还不错,推荐您阅读这篇文章,这对你深入了解新功能会有很大的帮助~

  • 在CFW&CDN中通过Mixin配置Tun或Tap

    本文由Telegram频道超超超超超超超超超菜的频道主编写~转载请注明出处~

    喜欢这篇文章请关注频道吧~

    看了这个大概就知道频道内容了吧()

    本次介绍利用Clash for Windows(以下简称CFW)和Clash .Net(以下简称CDN)两个软件中的mixin功能实现TUN/TAP虚拟网卡接管流量

    截至本文的修改完成,CFW的最新版本是0.15.6,CDN的最新版本是1.0.4

    本次示例以便携版为示例,两种软件的便携化方法都是下载压缩包,在解压出来的文件夹中新建data文件夹,配置文件等都会保存在其中,更新只需要移动data文件夹,较为方便

    1、TUN/TAP非常浅显的一点说明

    (1)什么是TUN/TAP

    TUN是clash的premium核心专属功能,可以使用虚拟网卡接管流量

    而TAP是CFW的功能,并非clash核心功能,同样可以使用虚拟网卡接管流量

    (2)TUN/TAP有什么用

    暂且不讨论对全局代理的理解(不知道我什么意思就当我没说好了)

    本次介绍的两个客户端(CFW和CDN)都有系统代理的功能

    但是系统代理并不能解决所有程序的代理问题

    举例来说,当使用uwp应用的时候,默认是无法指定本地代理的;玩游戏的时候,是不经过代理的

    传统方法是,对于uwp应用使用enableloopback,对于游戏使用Netch,还有人使用proxifier等

    但我们可以使用TUN/TAP来建立虚拟网卡接管程序流量来达到强制代理的目的

    也就是说,clash只要配置好,同样能完成别的软件可以完成的事

    (3)一些杂谈

    其实本次介绍的客户端中,CDN另有增强模式,使用增强模式同样能做到强制代理(效果与Netch的进程代理差不多)

    增强模式与TAP一样,无法使用进程名规则PROCESS-NAME来进行分流,猜测可能是因为NetFilter在对流量进行传递的时候无法将进程名传递到Clash核心

    本次暂且不对此进行讨论

    2、什么是Mixin

    Mixin,你可以理解为临时覆写,或者可以理解为临时混合配置文件

    Mixin的实质是在Clash核心读取完配置文件后对其进行临时修改而不对本地的原配置文件进行改动

    利用这个特性,我们可以轻松实现TUN/TAP的临时开关(鉴于这两款软件并未原始提供默认的TUN/TAP配置和相应开关的选项)

    3、CDN开启TUN的方法

    (1)准备工作

    目前TUN是Clash的Premium版核心专有功能,开源版核心并没有实现TUN

    CFW使用的是Premium核心,因此我们可以正常使用TUN

    而CDN默认使用的是开源版核心,因此为了使用TUN我们应该更换核心为Premium版

    我们先去Clash源仓库下载Premium核心(点此),向下翻找下载amd64(就是x64,如果你是x86或是说32位那就去下386)

    注意,截至我编辑这篇教程的日期(2021/05/20),Clash的2021.05.08版Premium核心仍未修复2021.05.08版核心中TUN部分对Windows的支持的一些问题(相关链接:Github Issue),具体表现为TUN网卡无法正常地检测网络连接,网络中显示Clash网卡无法连接网络,这将导致微软系UWP应用无法正常使用,比如设置中的微软账户登录等,对此,我推荐去使用上文提到的Issue中维护者发出的,或是选择回退2021.04.08版核心(可去旧版CFW客户端中的resource文件夹中获取)

    以x64为例,我们下载到的文件解压后是这个(上图)

    CDN的核心文件存在于CDN目录下的/bin子文件夹,文件名是Clash.exe(下图)(由于本文从初次编写到最后修改有一段时间,此处的CDN版本并非最新,以下不作解释)

    因此我们将原核心文件重命名为Clash.exebackup(以防下错核心从头再来),下载到的文件复制到该目录,并重命名为Clash.exe

    然后我们去https://www.wintun.net下载wintun.dll(注:截至2021年5月20日,wintun已经更新到0.11)

    由于我们是64位系统,所以打开压缩包里找wintun/bin/amd64下的wintun.dll,x86请使用wintun/bin/x86文件夹下的wintun.dll

    然后我们在CDN目录建立一个data文件夹,打开CDN,过一会关闭CDN(生成目录)

    然后将我们的wintun.dll复制到/data/clash中

    之后我们退回到Clash.Net目录,右键Clash.Net是用管理员身份运行

    UAC提示允许就完事了~

    如果讨厌每次打开都要点右键,可以右键点属性,在兼容性选项中勾选使用管理员身份运行

    这边稍微介绍一下CDN的语言切换功能

    只需在这个选项卡中切换语言即可。

    打开后在CDN的界面的settings/config选项卡中点击编辑Mixin Content(中文混合配置内容编辑)

    然后在弹出的编辑器中粘贴如下内容:

    mixin:
      dns:
        enable: true
        enhanced-mode: fake-ip
        use-hosts: true
        default-nameserver:
          - 223.5.5.5
          - 1.1.1.1
          - 114.114.114.114
        fake-ip-range: 198.18.0.1/16
        fake-ip-filter:
          - "dns.msftncsi.com"
          - "www.msftncsi.com"
          - "www.msftconnecttest.com"
        nameserver:
          - https://doh.pub/dns-query
          - https://dns.alidns.com/dns-query
          - https://doh.360.cn/dns-query
        fallback:
          - https://dns.quad9.net:5053/dns-query
          - https://dns-unfiltered.adguard.com/dns-query
          - https://doh.opendns.com/dns-query
        fallback-filter:
          geoip: false
          ip-cidr:
            - 240.0.0.0/4
          domain:
            - "+.github.com"
      tun:
        enable: true
        stack: gvisor
        dns-hijack:
          - 198.18.0.2:53
        auto-route: true
        auto-detect-interface: true

    并保存(当然你如果明白参数的意思的话可以自己编辑mixin的内容,注意缩进)

    在进行接下来的操作之前请务必保证你已经完成之前的准备工作

    (2)开启Mixin

    在进行接下来的操作之前请务必保证你已经完成之前的准备工作

    打开Mixin(选项左侧)

    切换一下配置文件(切换到另一个再切换回去)

    注意:如果你在这一步出现了什么error occurred的错误提示,一般情况下是你wintun.dll没有放置正确,要么是你核心没换正确

    稍微说明一下原因:这是因为clash核心读取的是之前的配置文件,而CDN操作临时文件后clash并未读取更改后的临时文件而是使用更改前的内容,切一下再切回去能解决(进一步的问题别问我了www)

    注意右下角的网络部分,可以发现Clash网卡已经接管了网络

    当我们不想使用TUN的时候,先在设置中关闭Mixin,再切换一下配置文件(切换到另一个再切换回去),然后就完成了,TUN已经关闭

    注意:如果你的Clash网卡显示无法连接网络,请换掉一些mixin中的DNS(nameserver和fallback)

    或者你可以删掉fallback部分

    4、CFW使用Mixin开启TUN/TAP

    CFW的操作方式相对简单一些

    (1)准备工作

    ①在Clash配置文件根目录处存在wintun.dll

    注:此处那么多文件夹并非默认内置

    ②已经安装服务模式或是使用管理员模式启动

    服务模式在通用选项卡中安装

    服务模式在通用选项卡中安装

    安装成功后CFW会自动重新启动

    而同时Clash根目录中会出现service子目录(如图)

    同时提示用的图标会变绿(大雾),表示服务模式已经启动(如图)

    当然你同样可以用和之前CDN同样的方法,设置默认使用管理员启动

    然后再切换到settings选项卡,翻到Profile Mixin部分,点击YAML的Edit

    粘贴内容同之前CDN中的内容(见此

    如果使用TAP,那么粘贴内容示例如下

    mixin:
      dns:
        enable: true
        enhanced-mode: fake-ip
        listen: :53
        use-hosts: true
        default-nameserver:
          - 223.5.5.5
          - 1.1.1.1
          - 114.114.114.114
        fake-ip-range: 198.18.0.1/16
        fake-ip-filter:
          - "dns.msftncsi.com"
          - "www.msftncsi.com"
          - "www.msftconnecttest.com"
        nameserver:
          - https://doh.pub/dns-query
          - https://dns.alidns.com/dns-query
          - https://doh.360.cn/dns-query
        fallback:
          - https://dns.quad9.net:5053/dns-query
          - https://dns-unfiltered.adguard.com/dns-query
          - https://doh.opendns.com/dns-query
        fallback-filter:
          geoip: false
          ip-cidr:
            - 240.0.0.0/4
          domain:
            - "+.github.com"

    ③补充:由于Clash核心的原因(见此),需要去CFW根目录下resources/files/win文件夹下更换Clash核心,请将下载到的核心更名为对应的文件名(比如64位系统即clash-win64.exe)

    注意缩进

    并点击右下角的按键保存

    TUN版本的Mixin示例

    (2)开启Mixin

    在通用界面打开Mixin,大功完成了

    不再使用TUN的时候就把Mixin关掉就好了

    小提示:可以在设置的末尾部分设置Mixin开关的快捷键

    同样地,如果显示Clash网卡无法连接,请更换Mixin中的DNS(nameserver和fallback),或者你可以删掉fallback部分

    本文由TG频道@ClashParsersLearningStart 发布,转载请注明出处~

    喜欢这篇文章请关注频道吧~