知也无涯

吾生也有涯,而知也无涯,能学一点算一点…

  • 存储引擎在存储整数时,一般会使用最高位作为标志位,标记存储的整数是正数还是负数(参考),最高位也被称为“most significant bit (MSb)”。通常,最高位为1则表示正数,最高位为0,则表示负数。更进一步的,负数则会通过补码(参考:two’s complement)的方式表示。但是,InnoDB没有使用这种方法。

    InnoDB 的整数存储

    在死锁诊断时,偶然注意到,InnoDB 在存储整数时,与一般的系统是不同的。例如,int 类型存储 1 的时候,使用的表示是:0x80000001。更多的示例可以参考右图:

    整数值InnoDB 表示
    10x80000001
    -10x7fffffff
    70x80000007
    -70x7ffffff9

    可以看到,这与一般的有符号型的整数存储是相反的。即:

    • 正数表示时,最高位(MSb)为1
    • 负数表示时,最高位(MSb)为0

    关于这个问题,在 Stackoverflow上也有看到有部分用户有类似的疑问:

    本文将讨论为什么会这样。

    考虑 8-bit 场景下的

    这里来回顾一下“体系结构”中的最为基础的一些知识吧。

    整数值绝对值绝对值的二进制原码2-补码Offset binary(“移码”)
    110000-00010000-00010000-00011000-0001
    -110000-00011000-00011111-11110111-1111
    770000-01110000-01110000-01111000-0111
    -770000-01111000-01111111-100101111001

    说明:

    移码有两种计算方式,结果是等价的,即:

    • 直接将原始数据加上2(n-1),然后转化为二进制即可以
    • 将其补码,最高位进行一次翻转,即 “补码 XOR 2(n-1)

    验证存储方式

    为了确认 InnoDB 的整数处理,再MySQL 8.4.4的源码中找到如下 InnoDB 处理整型数据的代码:

      if (type == DATA_INT) {
        /* Store integer data in Innobase in a big-endian format,
        sign bit negated if the data is a signed integer. In MySQL,
        integers are stored in a little-endian format. */
    
        byte *p = buf + col_len;
    
        for (;;) {
          p--;
          *p = *mysql_data;
          if (p == buf) {
            break;
          }
          mysql_data++;
        }
    
        if (!(dtype->prtype & DATA_UNSIGNED)) {
          *buf ^= 128;
        }
    
        ptr = buf;
        buf += col_len;

    这段代码中,先将字节序做了颠倒(从最高字节位开始,逐个字节进行拷贝存储),即将 MySQL 层面的小端(little-endian)转化为了InnoDB层面的(big-endian)存储。而后,再对最高位进行了一次翻转,即这里的:*buf ^= 128操作。

    即:先将数据在MySQL层面的表示做了大小端的转化并拷贝过来,然后,将最高位进行翻转。即,先将2补码的表示模式拷贝过来,再将最高位进行翻转。

    什么要这么存储

    在 MySQL/InnoDB 官方文档或者代码中,并没有关于该实现的说明。不过这么做,有一个非常明显的好处,即所有的整数表示的大小关系,与实际存储的数据(当中无符号型对待)的大小关系是一致的。

    即,在上述的例子中:7 > 1 > -1 > -7,而对应的编码表示,也有对应的大小关系:

    0x80000007 > 0x80000001 >0x7fffffff > 0x7ffffff9

    这里对这个问题做一个简单探讨。先说结论吧,这是一种较为典型的整数编码方式:Offset binary(“移码”)。即,将需要表示的整数,加上一个数值,让所有的整数映射到自然数空间。例如,在MySQL中使用32位的int类型,需要表示的整数范围为[-231,231]。那么,实际表示时,则加上231。更为一般的,对于[-2(n-1), 2(n-1)]之间的所有整数在表示时,都加上了2(n-1)。即,建立的映射关系是:

    f(i) = i + 2(n-1)

    即对于任何要存储的整数i,实际存储时都存储上述的f(i)。而在实际运算时,则是,将补码的最高位进行一次翻转即可。

    关于补码

    例如,在 8 位二进制中,00000001 表示 +1,而 11111111 代表 -1。具体的,在表示-3 时,先取 3 的二进制 00000011,再逐位取反 11111100,最后加 1 得到 11111101,即 -3 的补码表示。这种方式让计算机能够高效地进行整数运算,是典型的正负数的方法,该方法的更多优势可以参考:two’s complement

    补充说明

    MySQL 层面的整数表示和 InnoDB 的整数存储是不同的。在“验证存储方式”小结中的代码中可以看到:

    • MySQL使用了小端(little-endian),InnoDB层面使用了大端(big-endian)存储
    • 在 MySQL 层面使用2-补码做有符号整数类型存储;而InnoDB层面使用了“移码”存储

    参考文档

  • InnoDB 的锁诊断是一个比较困难的事情。首先,锁机制是一种较为复杂的资源竞争管理机制,如果涉及的资源类型比较多,锁类型又比较多,那么锁机制就会看起来比较复杂。具体到,MySQL/InnoDB 上,确实也就非常复杂了。这里主要关注 InnoDB 层面的锁,涉及的内容则包括了记录锁(record)、间隙锁(gap)、索引缝隙锁(next-key),而为什么要加锁,则又涉及到隔离级别、MVCC的实现,而锁的实现,则又与 InnoDB 底层的数据存储结构有有一定的关系,总得来说涉及的面比较多,如果对于这些概念没有了解,则比较难理解 InnoDB 的锁机制,也就比较难去排查 InnoDB 锁出现的问题。

    另一个层面是排查手段。MySQL/InnoDB在早期的版本中,对于锁问题的排查手段是比较有限的,而且与很多的配置参数有关,所以了解这些参数,熟悉MySQL/InnoDB锁信息查看的一些方法,则是另一个需要了解的。

    所以,关于 InnoDB 锁问题的也并不是一两个话题能够说清楚的。本文可能是一个系列(给自己挖坑),一个自己学习以及锁问题排查经验的分享。

    构造主键锁竞争

    记录锁,应该是 InnoDB 锁类型中较为常见,也是在READ-COMMITTED事务级别下,比较容易遇到的死锁类型(如果有死锁的话)。这里通过观察主键死锁、唯一键死锁,初步了解 InnoDB 锁信息内容,以及结构。

    查看当前的隔离级别

    mysql> show global variables like '%iso%';
    +-----------------------+----------------+
    | Variable_name         | Value          |
    +-----------------------+----------------+
    | transaction_isolation | READ-COMMITTED |
    +-----------------------+----------------+
    1 row in set (0.01 sec)
    
    mysql> show session variables like '%iso%';
    +-----------------------+----------------+
    | Variable_name         | Value          |
    +-----------------------+----------------+
    | transaction_isolation | READ-COMMITTED |
    +-----------------------+----------------+
    1 row in set (0.00 sec)

    准备表结构

    DROP TABLE IF EXISTS t1;
    
    CREATE TABLE t1 ( 
      id int unsigned, 
      nick varchar(32),
      age int,
      primary key (id)
    )
    mysql> desc t1;
    +-------+--------------+------+-----+---------+-------+
    | Field | Type         | Null | Key | Default | Extra |
    +-------+--------------+------+-----+---------+-------+
    | id    | int unsigned | NO   | PRI | NULL    |       |
    | nick  | varchar(32)  | YES  |     | NULL    |       |
    | age   | int          | YES  |     | NULL    |       |
    +-------+--------------+------+-----+---------+-------+

    构建锁等待

    在两个会话中,按从上到下,执行下述的SQL:

    时间顺序Session ASession B
    1START TRANSACTION;
    2INSERT INTO t1 VALUES ( 1, "a",12 );
    3START TRANSACTION;
    4INSERT INTO t1 VALUES ( 1, "x",23 );

    这时候,Session B会陷入等待。

    观察锁信息

    通过 SHOW INNODB STATUS 观察

    此时查看 InnoDB 锁信息,则有如下数据:

    ---TRANSACTION 10094, ACTIVE 14 sec inserting
    mysql tables in use 1, locked 1
    LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s)
    MySQL thread id 84, OS thread handle 140453961758272, query id 9262 10.88.0.1 sysb update
    INSERT INTO t1 VALUES ( 1, "x",23 )
    ------- TRX HAS BEEN WAITING 14 SEC FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `sysbenchdb`.`t1` trx id 10094 lock mode S locks rec but not gap waiting
    Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 00000001; asc     ;;
     1: len 6; hex 00000000276d; asc     'm;;
     2: len 7; hex 810000008d0110; asc        ;;
     3: len 1; hex 61; asc a;;
     4: len 4; hex 8000000c; asc     ;;
    
    ------------------
    ---TRANSACTION 10093, ACTIVE 23 sec
    2 lock struct(s), heap size 1128, 1 row lock(s), undo log entries 1
    MySQL thread id 83, OS thread handle 140454159017536, query id 9260 10.88.0.1 sysb

    SHOW ENGINE INNODB STATUS\G中仅打印了处于等待授予状态的锁信息,即这里仅打印了事务10094的等待的锁详情。

    详解锁信息

    这里详细看看其中的内容:

    RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `sysbenchdb`.`t1` trx id 10094 lock mode S locks rec but not gap waiting
    “RECORD LOCKS”这是一个记录锁
    space id 27 page no 4该记录处于物理位置,包括页面编号,以及页面所处的物理文件编号
    n bits 72该页面有72个记录对应标记位
    index PRIMARY of table sysbenchdb.t1 对应表
    trx id 10094所在的事务 ID
    lock mode S锁类型,为 S ,即共享锁
    locks rec but not gap这是一个简单记录锁,无需对记录前的“间隙”加锁
    Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 00000001; asc     ;;
     1: len 6; hex 00000000276d; asc     'm;;
     2: len 7; hex 810000008d0110; asc        ;;
     3: len 1; hex 61; asc a;;
     4: len 4; hex 8000000c; asc     ;;
    Record lock(等待的)记录锁
    heap no 2在页面中,记录以堆的方式存放,该记录的堆编号
    PHYSICAL RECORD: n_fields 5;记录共五个字段
    0: len 4; hex 00000001; asc ;;id 字段值,为1
    1: len 6; hex 00000000276d; asc 'm;;该记录的DB_TRX_ID,即为0x276d10093
    2: len 7; hex 810000008d0110; asc ;;DB_ROLL_PTR
    3: len 1; hex 61; asc a;;字段nick取值 a
    4: len 4; hex 8000000c; asc ;;字段age取值 0x8000000c,即12

    这里需要注意的是,这里一共有五个字段(n_fields 5; )。那实际这个表,只有三个字段,为什么这里会有五个字段?原因在于,InnoDB 在存储数据信息的时候,会额外的存储两个信息:DB_TRX_IDDB_ROLL_PTR。这是两个InnoDB的较为底层的概念,具体的可以参考该文档:17.3 InnoDB Multi-Versioning

    注意到,这里的 DB_TRX_ID 取值为 0x276d,转化为10进制则为:10093。即,该条记录最后一次被修改是被事务10093所修改,即上述表格中的Session A所执行的SQL所修改。

    在内置视图中查看锁信息

    mysql> SELECT
        ->     ENGINE_TRANSACTION_ID AS TRX_ID,
        ->     OBJECT_NAME,
        ->     INDEX_NAME,
        ->     LOCK_TYPE,
        ->     LOCK_MODE,
        ->     LOCK_STATUS,
        ->     LOCK_DATA
        ->   FROM performance_schema.data_locks;
    +--------+-------------+------------+-----------+---------------+-------------+-----------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
    +--------+-------------+------------+-----------+---------------+-------------+-----------+
    |  10224 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
    |  10225 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
    |  10224 | t1          | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 1         |
    |  10225 | t1          | PRIMARY    | RECORD    | S,REC_NOT_GAP | WAITING     | 1         |
    +--------+-------------+------------+-----------+---------------+-------------+-----------+

    (注:这里的TRX_ID与上述不同,因为这是重新执行了上述的冲突事务)

    补充说明

    在本示例中,写入的数据仅为主键(也是一种唯一键),故无论是READ-COMMITTED、还是REPEATABLE-READ,其所需要的锁都相同的。

    相关参考资源

    1. InnoDB Locking and Transaction Model@MySQL 8.4 Reference Manual
    2. InnoDB Data Locking – Part 1 “Introduction”@MySQL Blog Archive
    3. InnoDB Data Locking – Part 2 “Locks”@MySQL Blog Archive
    4. InnoDB Data Locking – Part 2.5 “Locks” (Deeper dive)@MySQL Blog Archive
    5. InnoDB Data Locking – Part 3 “Deadlocks”@MySQL Blog Archive
    6. Understanding InnoDB Locks and Deadlocks@2015 Percona Live
    7. Introduction to Transaction Locks in InnoDB Storage Engine@2013
  • 我的 Docker 常见命令

    ·

    查看当前容器列表

    docker ps -a

    可能看到输出如下:

    docker ps -a
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    CONTAINER ID  IMAGE                                                     COMMAND     CREATED       STATUS                   PORTS       NAMES
    c84f74eedd65  container-registry.oracle.com/mysql/community-server:9.1  mysqld      5 months ago  Exited (0) 4 months ago              mysql91
    13985429990f  container-registry.oracle.com/mysql/community-server:9.1  mysqld      2 months ago  Created                              mysql91_n

    启动/关闭/重启某个容器

    docker start/stop/restart <CONTAINER NAMES>
    docker start/stop/restart <CONTAINER ID>

    例如:

    docker start mysql91

    查看容器的运行日志

    docker logs -f <CONTAINER ID>

    这里的参数 -f则类似于 tail-f的参数。

    在容器中执行命令

    docker exec -it <CONTAINER NAMES> COMMAND

    例如

    docker exec -it mysql91 mysql -uroot -p

    这里参数,可以通过 man docker exec去查看。-it是两个参数,表示分配一个可交互式操作的终端。

    进入容器并启动一个Bash

    类似的,使用上面的命令,可以在容器环境下启动一个Bash

    docker exec -it mysql91 /bin/bash

    将容器中的文件拷贝到宿主机

    有时候,在容器中,工具和命令比较有限,需要将对应的容器中的文件拷贝出来并进行分析或存档。则可以使用如下命令:

    docker cp <CONTAINER NAMES>:<PATH_OF_FILE> <PATH_OF_HOST>

    例如:

    docker cp mysql91:/var/lib/mysql/binlog.000005 ./

    查看当前的本地的镜像列表

    docker image ls
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    REPOSITORY                                            TAG         IMAGE ID      CREATED       SIZE
    container-registry.oracle.com/mysql/community-server  9.1         f1f889678a73  6 months ago  606 MB
    container-registry.oracle.com/database/express        18.4.0-xe   364598d20118  4 years ago   6.03 GB

    其他资源

  • “六国都为秦所并,读史的人,往往以为一入战国,而秦即最强,这是错误的”

    《中国通史-春秋战国的竞争和秦国的统一》 吕思勉

    战国七雄都对得起“雄”字,在不同的时期都曾经雄霸一方,在战国早期,很多国家都强于当时的秦国。其中,在三家灭智分晋之后,魏斯正式被册封为侯,是为魏文侯。之后,魏文侯、魏武侯、魏惠王三代通过用贤良、施变法,魏国变得非常强大,通过“逢泽之会”称王。这期间,魏国任用了“魏成子、翟璜、李悝、乐羊、吴起、西门豹”等人。而,由吴起为主要将领,与秦国之间长达数年的河西之战中,屡次破秦,尽取秦国的河西地区,成就魏国一时的霸业。这里就来系统的看看战国早期的秦魏之间的长达数十年的河西之战。也借此可以更深入的体会一下,吕思勉所说的“往往以为一入战国,而秦即最强,这是错误的”。

    “秦魏”的地理位置

    我们可以通过如下两幅地图,可以大致了解秦魏的地缘关系。下右图,高度经过“夸张”,可以更好的反应出,两国的地形关系。秦国核心土地处于秦岭以北、黄土高坡以南,东北方向的出路是魏、赵两国。东出函谷关则需要经过韩魏两国(苏辙《六国论》)。秦魏两国之间,“黄河”可以认为是较为天然的分界线。

    河西之战的前中期

    河西之战 (战国)”就是发生在黄河以西,由当时最为强大的“魏国”主动挑起,经过多长战争,最终占领了黄河以西大量原本属于秦国的土地,并在这里设置了“西河郡”,由魏国军队主将“吴起”任首任郡守。主要的战役的发生地点和时间如下所示,此阶段,魏国胜多败少:

    具体的战役详情:

    • 前419年,魏军突然在河西地区筑城池,秦国派兵进攻,交战两年。前417年,魏军击败秦军。
    • 前413年,魏国大举进攻秦国,魏国李悝在郑县大败秦军。次年,魏文侯派太子击包围并占领了秦国的繁庞
    • 前409年,魏文侯任命吴起为主将,攻克秦国临晋、元里并筑城。次年,吴起攻打秦国至郑县,攻克洛阴、郃阳。

    至此,魏国全部占有原本属于秦国的河西地区,并在此设立西河郡。吴起担任首任郡守。

    河西之战的中后期

    在中期,秦国虽多次战争尝试收付河西地区,均未成功。在前389年的阴晋之战,最为有代表性。当时,秦惠公出兵号称五十万进攻魏国的阴晋,被吴起以步兵五万、战车五百辆、骑兵三千所击败。

    在战争的后期,魏惠王称王,迁都大梁。同时,秦国重用商鞅,逐渐强盛。河西之势也开始发生了变化。最终,在经过吴城之战魏国便逐渐势弱。在前330年,秦军再攻魏国上郡,大胜,最终收付了全部河西地区。

    具体的:

    • 前361年,魏惠王迁都大梁,魏国的战争中心转向了宋、卫、韩、赵等国。之后在元里、安邑、固阳都发生过争夺战,双方互有来回。
    • 而在前340年,吴城之战,魏国被秦商鞅击败
    • 前330年,秦军再攻魏国上郡,此役魏国防卫西河、上郡的主力全军覆没,主将龙贾被俘,魏惠王被迫于次年将西河郡全部献给秦国。至此,秦全部收复了被魏夺占的河西地区。

    魏国的强盛与衰落

    早期在三家灭智分晋之后,魏文侯、魏武侯两代人用贤良、施变法使得魏国非常强大。期间最为有代表性的人物包括李悝、吴起、西门豹等。其中李悝变法使国家强盛富足、吴起攻城略地,使得魏国声名大噪,而西门豹善治水…哦,上了小学语文课本。

    不合时宜的称王与“东扩”

    魏惠王不合时宜的称王与“东扩”,现在来看,是有违天时与地利的。在魏国最为强大时候,魏惠文王决定迁都,并秦国在游说之下称王。但此时,河西之地并未真正稳固,原本地处边陲的秦国,经过历代图治与变法(“秦国之强起于献公而成于孝公”),变得非常强大。在魏国东扩的时候,西边被秦国完全打败。不仅河西土地尽失,就连原本的旧都安邑也被占领。

    不合时宜的称王与“东扩”为后续魏国的衰落埋下种子。

    用人失误

    而后期,将相不和,魏惠王受谗言弃用吴起,也使得河西守势变弱。关于这一点,公孙痤在后来取得某次战役胜利的时候,依旧是非常认可吴起的功劳的,可见吴起之于魏国的重要。而忽视卫鞅,并任由其前往秦国,则更是此消彼长。

    再观秦国

    而秦国经过数代君王努力,最终不仅取回河西,并继续攻下河东,包括原魏国国都(安邑)。同时,拿下了东出函谷关的要到,以及部分“关东“的土地,为后续东出函谷关,一统六国做好了准备。

    最后

    这场战争来看,任用贤才、实施变法改革,使得国盛民强,是战争胜利的基础。利用地形、政策等巩固战果,才能够考虑进一步拓展疆域。通观全国之势,当时魏国虽然强大,但还不具备统一的实力,此时称王,反而成为众矢之的,而关中之地此时有没有守住,最终导致魏国衰落。

  • 在2015年,由 OpenAI 的 DP Kingma 等发布了 《ADAM: A METHOD FOR STOCHASTIC OPTIMIZATION》算法后,由于其迭代效率提升非常明显,所以 ADAM(或其变种)就被广泛的采用。本文将继续对上一篇介绍的梯度下降算法进行优化,并介绍 ADAM 算法(一种对随机梯度下降算法的优化算法)的实现以及效果。

    Stochastic Gradient Descent 或者说 mini-batch解决了样本量巨大时,梯度下降迭代的问题。但是,也带了一些新的问题。最为主要的是,因为样本数据的波动,而导致每次梯度下降计算时,梯度方向的波动,从而降低了梯度下降迭代的效率。

    在前面的《Mini-batch Gradient Descent和随机梯度下降(SGD)》文章中,我们对比了 mini-batch 和 batch gradient descent 的在迭代时,目标函数下降的速度。

    可以看到,batch gradient descent 的目标函数下降非常稳定,而 Mini-batch 的实现则会有明显的波动。为了尝试修正这个问题,从而提高迭代效率,在神经网络算法上,逐渐探索出了一些较为高效的优化算法:Adam SGD。该算法将 RMSprop 和 “Exponential smoothing”的想法结合在一起,形成了一个较为高效的算法,在实践中被广为使用。


    Stochastic Gradient Descent 与 Momentum

    SGD 会在每次迭代时根据样本的偏差,展现出不同的偏差,所以,在使用SGD进行迭代时,观察其 cost函数下降,应该会有更加明显的波动(后续吧自己实现的程序改造后,尝试观察一下)。

    为了加快迭代的速度,一个折中的思路是,引入一个均值替换当前的梯度方向。该如何引入这个均值呢?梯度是一个随时计算推进,不断推进的变量,常用的均值计算可以参考:Moving average。最为常见的实现是使用“Exponential moving average”,这种平均值的计算,在迭代计算时实现非常简单。

    Momentum 就是 “Exponential moving average”实现时的参数“smoothing factor”,在神经网络中,经常使用 \( \beta \)表示(原因是 \( \alpha \) 已经表示学习率了 )。

    而这里的 Momentum ,也是 TensorFlow 在构造 SGD 算法时需要的另一个参数。

    关于Exponential moving average

    或者叫“Exponential smoothing”。我们看看这个算法的具体实现是怎样的?

    原始的迭代:\( w = w – \alpha \frac{\partial J}{\partial w} \)

    使用 “Exponential smoothing” 后的迭代:

    $$
    \begin{align}
    v_0 & = 0 \quad \partial{w}_t = \frac{\partial J}{\partial w}|_{(for \, sample \, t)} \\
    v_{t} & = \beta*v_{t-1} + (1-\beta)\partial{w}_{t} \\
    w & := w – \alpha v_t
    \end{align}
    $$

    考虑 \( \beta = 0.9 \),如果数学直觉比较好的话,可以看出,原本使用梯度\( \partial{w} \)进行迭代的,这里使用了一个梯度的“Exponential smoothing” \( v_t \)去替代。上面的式子中,\( v_t \) 如果展开有如下表达式:

    $$
    \begin{align}
    v_t & = (1-\beta)\partial{w}_{t} + \beta(1-\beta)\partial{w}_{t-1} + \beta^2(1-\beta)\partial{w}_{t-2} … \\
    & = \sum\limits_{i=0}^{t} \beta^{i}(1-\beta)\partial{w}_{i}
    \end{align}
    $$

    使用“Exponential smoothing” 之后,新的迭代方向 \( v_t \),可以理解为一个前面所有梯度方向的加权平均。离得越近的梯度,权重越高,例如,\( \partial{w}_{t} \)的权重是\( (1-\beta) \);而之前的梯度,则每次乘以一个 \( \beta \)衰减。

    Exponential moving average的“冷启动问题”与修正

    仔细观测上诉的 “Exponential moving average” 公式,可以注意到一个问题,就是其最初的几个点总是会偏小。其原因是,当前值的权重总是为 \( 1- \beta \),而因为是初始的几个值,并没有更前面的数据去“平均”当前值,也就会出现,初始值总是会偏小的问题。

    通常,如果样本量很大的事时候,则可以忽略这个问题,因为初始值偏小的点占比会非常少,可以忽略。如果要一定程度上解决这个问题,也有继续对上述的 “Exponential moving average”做了一些修正,可以考虑对 \( v_t \)的结果值做一个修正:\( v_t := \frac{vt}{1-\beta^t} \)。

    一般的,因为样本的数量总是比较大的,所以我们可以忽略这个问题,而无需做任何修正。

    RMSprop

    在前面的“Gradient Descent with Momentum”中,我们看到为了解决梯度波动较大的问题,使用了 “Exponential moving average” 去尝试将一些比较偏的梯度,拉倒一个较为平均的方向上来。RMSprop的想法也是类似的,这里通过了root mean square的想法进行平均值的计算。具体的,在进行 SGD 时,每次更新梯度,按照如下的方法进行更新:

    $$
    \begin{align}
    s_0 & = 0 \quad \partial{w}_t = \frac{\partial J}{\partial w}|_{(for \, sample \, t)} \\
    s_{t} & = \beta*s_{t-1} + (1-\beta)(\partial{w}_{t})^2 \\
    w & := w – \alpha \frac{\partial w}{\sqrt{s_{t}}}
    \end{align}
    $$

    说明:这里对梯度进行平方时,如果在程序中是一个梯度向量,那么这里“平方”也就是对梯度的每一个分量进行一次平方。

    在“Exponential smoothing”的实现中,是将当前值,使用一个加权平均替代。与“Exponential smoothing”类似的,原本的梯度方向,现在使用如下的方向去替代了:

    $$
    \begin{align}
    s_t & = \frac{\partial{w}_{t}}{\sqrt{(1-\beta)(\partial{w}_{t})^2 + \beta(1-\beta)(\partial{w}_{t-1})^2 + \beta^2(1-\beta)(\partial{w}_{t-2})^2 + \cdots }} \\
    & = \frac{\partial{w}_{t}}{\sqrt{\sum\limits_{i=1}^{t}\beta^i(1-\beta)(\partial{w}_{i})^2}} \\
    \end{align}
    $$

    Adam Gradient Descent

    这可能是实际使用最多的算法,全称是 Adaptive Moment Estimation 。该实现,将 “Momentum” 和 “RMSprop” 做了一定的融合,形成了新的“最佳实践” Adam。在融合上,具体的实现与两个细节点:

    (1) 在 Adam 中均使用了“修正”计算,即 \( \hat{v_t} = \frac{v_t}{1-(\beta_1)^t} \quad \hat{s_t} = \frac{s_t}{1-(\beta_1)^t} \)

    (2) 参数更新公式,使用了两个算法的融合: \( w := w – \alpha \frac{\hat{v_t}}{\sqrt{\hat{s_t}}} \)

    Adam optimization的效果对比

    在 Adam 的论文中对于效果做了非常多的评估,感兴趣的可以参考相关论文。

    这里根据之前完成的训练程序,也进行了优化,实现了Adam算法。在 MNIST 数据集的训练上,我们来看看 Adam 的效果:

    从右图可以看到,Adam(蓝色)明显的提升了迭代效率。依旧一定程度存在 mini-batch(绿色) 的梯度波动的问题。相比于,batch gradient descent (红色)算法,迭代效率大大增加,约在第10次迭代,即在第一个epoch 的第十批样本进行训练时,cost 就下降到了比较低的程度。

    关于 root mean square

    root mean square也叫二次平均值,考虑一组数据:\( {x_1,x_2, \cdots , x_n } \),其RMS则为:

    $$ x_{rms} = \sqrt{\frac{1}{n} \sum_{i=1}^n x_i^2} = \sqrt{\frac{1}{n} (x_1^2 + x_2^2 + \cdots + x_n^2)} $$

    补充说明

    可以看到,所有的这些优化都是面向“最优化”问题的。梯度下降是一个一阶优化(First-order Optimization)的方法,其核心就在与每次迭代时,应该如何去更新响应的参数值,在梯度下降中也就是如何去选择合适的学习率。

    牛顿法是典型的二阶优化(Second-order Optimization),在迭代时使用了二阶导数,所以,通常可以获得更好的迭代效率。但是因为二阶导数的计算复杂度会上升非常多(对应的矩阵可能是所有参数的平方,应该也有人尝试去算过了…)。这也是为什么在这个场景下,依旧是使用一阶优化方法的原因。

    如果想比较好的理解学习率、Momentum、RMSprop、Adam等内容,建议先了解梯度、数值方法、最优化问题等数学方法。

    到这里这个系列算是一个小阶段了,这是一个个人学习的笔记,从数学的梯度概念开始,逐步到神经网络训练的Adam优化算法,也包含部分动手实践的神经网络算法实现。完成的系列包括了:

  • Terraform 可以自动化的创建云端的资源,但是要想实现更高的灵活度,则需要更为灵活的使用Terraform的“Data Sources”能力。例如,在自动化的创建数据库时,云厂商允许创建的版本号是在动态变化的,例如,当前最新的允许的创建的MySQL版本通常是 8.0.40,但通常过了一个季度之后,就变成了 8.0.41。这时,对应的 Terraform 的脚本就需要调整或者传递参数就需要发生变化。而 Terraform 提供的 “Data Sources” 能力则可以很好的解决这个问题。

    在 Oracle 的 Terraform 中可以使用 “Data Source: oci_mysql_mysql_versions” 实现该能力。

    示例

    首先使用 data 命令定义该对象:

    data "oci_mysql_mysql_versions" "gmv" {
        compartment_id = oci_identity_compartment.oic.id
    }

    这里会获取该租户环境下支持的所有MySQL版本。

    然后,再使用 output 命令就可以获取并输出这些版本信息。详细的output命令如下:

    output "mysql_version" {
      value       = data.oci_mysql_mysql_versions.gmv.versions
    }

    详细的输出示例如下:

    mysql_version = tolist([
      {
        "version_family" = "8.0"
        "versions" = tolist([
          {
            "description" = "8.0.36"
            "version" = "8.0.36"
          },
          {
            "description" = "8.0.37"
            "version" = "8.0.37"
          },
          {
            "description" = "8.0.38"
            "version" = "8.0.38"
          },
          {
            "description" = "8.0.39"
            "version" = "8.0.39"
          },
          {
            "description" = "8.0.40"
            "version" = "8.0.40"
          },
          {
            "description" = "8.0.41"
            "version" = "8.0.41"
          },
        ])
      },
      {
        "version_family" = "8.4 - LTS"
        "versions" = tolist([
          {
            "description" = "8.4.0"
            "version" = "8.4.0"
          },
          {
            "description" = "8.4.1"
            "version" = "8.4.1"
          },
          {
            "description" = "8.4.2"
            "version" = "8.4.2"
          },
          {
            "description" = "8.4.3"
            "version" = "8.4.3"
          },
          {
            "description" = "8.4.4"
            "version" = "8.4.4"
          },
        ])
      },
      {
        "version_family" = "9 - Innovation"
        "versions" = tolist([
          {
            "description" = "9.1.0"
            "version" = "9.1.0"
          },
          {
            "description" = "9.1.1"
            "version" = "9.1.1"
          },
          {
            "description" = "9.1.2"
            "version" = "9.1.2"
          },
          {
            "description" = "9.2.0"
            "version" = "9.2.0"
          },
        ])
      },
    ])

    获取特定大版本的各小版本

    可以通过 data资源中新增filter模块以过滤出需要的对象。

    在 Terraform 中,关于 data 资源是否可以使用 filter,以及filter支持的完整度视乎并没有明确的说明。这需要更具不同的供应商的实现。常见的,在data resourcefilter可以支持“列表匹配”、“通配符匹配”或者“正则匹配”。具体的匹配方式,则需要通过文档、或者测试区验证。

    添加带正则匹配的 filter
    data "oci_mysql_mysql_versions" "gmv" {
        #Required
        compartment_id = oci_identity_compartment.oic.id
        filter {
            name = "version_family"
            values = ["8.0.*"]
            regex  = true
        }
    }

    通过 HCL 语言获取最新的版本
    output "latest_versions" {
      value = {
        for db_version in data.oci_mysql_mysql_versions.gmv.versions : db_version.version_family => sort([
          for v in db_version.versions : v.version
        ])[length(db_version.versions) - 1] // 取排序后的最后一个版本
      }
    }

    最后的输出如下:

    latest_versions = {
      "8.0" = "8.0.41"
      "8.4 - LTS" = "8.4.4"
      "9 - Innovation" = "9.2.0"
    }

    参考链接