阅读视图

阿胶块到底要怎么吃才行?

阿胶作为传承3000年的滋补上品,其驴皮熬制工艺被列入国家级非物质文化遗产。传统阿胶块需经"九九八十一天"熬制,具有补血滋阴、润燥止血的功效,《本草纲目》记载其"疗吐血衄血,血淋尿血"。真的是好处多多啊。我顺便也查了下阿胶方面的知识,基础形态的阿胶就有很多种:

阿胶块‌

  • 传统黑色硬长方体,由驴皮经40余天熬制、切胶而成46
  • 需二次加工(打粉或熬膏),营养保留最完整但直接食用口感差14
  • 胶原蛋白含量≥80%,代表品牌:东阿阿胶、福牌24

阿胶糕‌

  • 阿胶块融化后加入核桃、黑芝麻、黄酒等配料冷却切块45
  • 即食便捷,含阿胶比例约10%-40%(品质差异大)16
  • 口感香甜,适合日常滋补67

阿胶粉‌

  • 阿胶块研磨或喷雾干燥制成46
  • 含阿胶40%以上1,85℃以上热水冲服吸收快412
  • 现代创新速溶型(如添加红枣萃取物)增速显著11

即食鲜阿胶‌

  • 粉末状创新形态,可直冲水/热牛奶饮用17
  • 主打"免煮即食",迎合年轻消费需求11
除了基础形态外,还有不少复合形态,比如:阿胶浆、‌阿胶膏 一类的。

由于前段时间老婆流产了,姐姐就买了两盒阿胶用于滋补身体,上次我也吃了一盒,不过是 阿胶糕 ,拆袋即食那种,里面有很多小包装,我吃了一盒,口感一般,毕竟是滋补身体的。这次买的是 阿胶块 ,类似巧克力一样,真是"坚硬如铁",第一晚上我用铁腕蒸了一小块,结果半小时过去了,还没完全融化,看样子这东西必须要熬制了,不过也不是个容易的事儿,所以就考虑直接打成粉末,为了这个去买个专门打阿胶机器也不显示,所以就开始找药店问问能不能处理。

后来找了一家,说是可以免费打粉,午饭后我顶着大太阳出发了,结果到药店人却不在,说有事儿出去了,这不是坑人么?约好的,没办法只能重新找了,还好又找到了一家,然后立马就动身过去了,也是个药店,交流一番后,就开始打磨了,整个过程30分钟左右,没想到这么硬的东西,打完就成了豆浆粉一样的东西,满打满算也就两袋吧,整个过程还是挺顺利的,不得不为帮忙的小哥点个赞。

小哥反复说,阿胶夏天不要吃,等冬天吃,不然会上火的,毕竟是大补的东西,所以只能放着了,早知道就先不打粉了。
  •  

路边停车收费,合理吗?

路边停车收费真的合理么?随便搞一条马路,竖一个收费标志牌,就可以开始收费了,总感觉有一种,"圈地为王"的感觉。有的甚至连停车线也不画,收费指示牌也看不到,你过来停车没人管,等你走的时候,收费的人就来了,关键是收费还不低,甚至比停车场还要高,对于此类行为我是相当反感的。

这是我六一那天,带闺女去游乐园玩的时候被贴的,严格意义上来说,这并不是交警贴的,而是路边穿黄马褂收费员贴的,他们天天骑着电动车沿着路边拍车辆,然后上到系统,就开始计费了,当然也有更智能一点的,你停车后,有仪器自动识别,如果你走的时候,没有收费员正好在场,你当时就不用缴费,但是停车记录已经产生,下次碰到了,还是会让你补齐费用的。

还好这一次路边是有停车线,而且正好有收费标识牌,然后收费还算合理(2元/小时),后来收了6块,后来说系统查到我还有5笔待支付记录,看了下最早一笔在2019年,在汉口汉阳那块,当时并不知道它计费了,而且费用也不低,大概是5元一小时,现在就开始拦下我,让我补齐费用,我当时就和工作人员争执了,你们路边有的连标识都没有,随便搞一条马路,就开始拍车牌计费,谁给你们的权利,给我说清楚,不然这费用我不会补,然后那个人支支吾吾也没买说个所以然,然后让我把这次停车的6块补缴了,就让我走了。

之前和朋友也聊到过类似话题,他回公司一般都停楼下巷子里的路边,偶尔会被计费,收费的人说是合法路边车位计费,朋友当时也没多想,就给了,直到后来被贴了罚单,说是违法停车,自那之后,再也没给所谓路边收费的人交过钱了。

有没有过类似经历的,你们都咋处理的?一起交流交流!
  •  

真是离谱,家门口的垫子也有人偷

昨天家里人说中午出去玩会儿的功夫,门口的垫子不见了,被人偷了,之前也被偷过一次,因为那会儿还没正式住进来,就没追求这事儿,没想到又来这一招,门口垫子就那么大点儿,也不值个啥钱,真不知道这些人是咋想的,这东西也偷。

得知消息,立马就反馈到物业,让帮忙查下监控,看有没有什么线索,后来得到反馈,监控没有啥异常,估计要么是同楼栋的人拿走了,要么就是其他楼栋的人,把东西装起来带走了,自认为小区里人的素质还可以,90%都是年轻人和小孩儿,感觉不至于吧。

之前小区有人在业主群里说,自己放楼顶晒太阳的花盆被拿走了,花和土都倒出来了,然后把花盆拿走了,闹得沸沸扬扬,后来监控查到了,是个保洁拿走了,但是物业不愿公开监控,也不知道具体谁拿走了,只是把东西送回来了,现在的物业、保洁、保安普遍素质不高,反正收钱第一名,其它所有事儿都懒得管,把业主都当傻子,看样子得把不交物业费进行到底了。

后面考虑换个带监控的门锁,或者门口按个监控,不然门口还不能放东西了,谁都可以拿走,还找不到人,真是离谱。

  •  

满心记更换域名啦,顺便送几个域名

经过慎重考虑,我决定对博客域名进行更换。原域名qq.mba因续费价格从首年百余元暴涨至292元/年,虽仍可使用至2027年,但本着务实原则,决定提前启动域名迁移计划。

域名更换:启用全新顶级域名 zhoutian.com

  • 中文双拼"周天"既朗朗上口,又蕴含"假日休憩"之意
  • 与博客记录生活、分享思考的定位高度契合
  • 更符合中文用户记忆习惯

博客名称从"满心记"正式更名为"周天记"

  • 新名称既保留原有记录生活的内核
  • 又赋予"周期性沉淀"的深层含义

这次调整看似突然,实则是三年建站经验的自然选择。早前使用qq.mba时主要考虑简短易记,如今更注重域名与内容气质的统一。原域名将逐步停用。

域名切换期间会做好301重定向,确保原有内容可访问。

域名留之无用,弃之可惜,留给有需要的人,准备赠送几个出去,先到先得

  1. nzqq.cn(阿里云)- 已送出
  2. nih.cc(阿里云)- 已送出
  3. says.top(阿里云)- 已送出
  4. ere.cc(西部数码)- 已送出
  •  

用百度地图做个足迹地图

今天看到 小生博客的文章,就花了点时间整到我的博客里了,考虑要不要单独做成页面,想了下,还是放 关于页里面,后续看情况,要不要独立出来。

预览效果,有兴趣的可以往下看,比较简单:

一、申请百度地图账号,创建应用

百度地图,百度要求实名认证。

注意:创建应用要选择【浏览器端】

以上全部通过后,获取 访问应用(AK)

二、创建map.php

<?php
?>

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的旅行足迹</title>
    <style>
        #container {
            flex: 1;
            width: 100%;
            height: 100%;
            border-radius: 10px;
        }

        /* 信息窗口样式 */
        .info-window {
            padding: 0;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
            text-align: center;
            border: none;
            background: #fff;
            overflow: hidden;
        }

        .info-header {
            text-align: center;
        }

        .info-header h3 {
            margin: 0;
            font-size: 20px;
            font-weight: 600;
            color: #333;
            text-align: center;
        }

        .info-content {
            padding: 0 15px 15px;
            text-align: center;
        }

        .info-content p {
            margin: 0 0 15px;
            font-size: 14px;
            line-height: 1.6;
            color: #666;
            text-align: center;
        }

        .info-content .photos {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 10px;
        }

        .info-content img {
            width: 100px;
            height: 100px;
            object-fit: cover;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            transition: transform 0.3s ease, box-shadow 0.3s ease;
        }

        .info-content a:hover img {
            transform: scale(1.05);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        }

        .BMap_cpyCtrl,
        .anchorBL {
            display: none !important;
        }
    </style>
</head>

<body>
    <!-- 地图容器 -->
    <div id="container"></div>

    <!-- 引入百度地图API -->
    <script type="text/javascript"
        src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak= 你的KEY"></script>

    <!-- 引入足迹点数据 -->
    <script src="<?php $this->options->themeUrl('style/markers.js'); ?>"></script>

    <!-- 地图初始化和足迹点添加 -->
    <script>
        // 地图初始化
        var map = new BMapGL.Map("container");
        var point = new BMapGL.Point(108.219771, 34.933863);
        map.centerAndZoom(point, 5); // 初始化地图,设置地图级别为5
        map.enableScrollWheelZoom(true); // 开启鼠标滚轮缩放
        console.log(map);
        // 设置地图样式
        map.setMapStyleV2({
            styleId: 'e538ad167219263086d18744cc59cd32'
        });

        // 添加足迹点和信息窗口
        markers.forEach((element) => {
            let point = new BMapGL.Point(element.latLng[0], element.latLng[1]); // 创建坐标点
            var marker;

            // 如果有自定义图标,则使用自定义图标
            if (element.icon) {
                var myIcon = new BMapGL.Icon(element.icon, new BMapGL.Size(26, 26));
                marker = new BMapGL.Marker(point, { icon: myIcon });
            } else {
                marker = new BMapGL.Marker(point); // 创建默认标记
            }

            map.addOverlay(marker); // 将标记添加到地图上

            // 创建信息窗口
            let opts = {
                width: 320, // 信息窗口宽度
                height: 0, // 信息窗口高度
                enableMessage: false, // 禁用默认的关闭按钮
                enableCloseOnClick: true, // 点击地图关闭信息窗口
            };

            // 构建信息窗口内容
            let info = `
                <div class="info-window">
                    <div class="info-header">
                        <h3>${element.name}</h3>
                    </div>
                    <div class="info-content">
                        <p>${element.desc}</p>
                        <div class="photos">`;
            if (element.photo && element.photo.length > 0) {
                element.photo.forEach((photoUrl, index) => {
                    // 获取对应的链接
                    let linkUrl = element.links ? element.links[index] : "#";
                    // 添加图片链接
                    info += `<a href="${linkUrl}" target="_blank"><img src="${photoUrl}" alt="Image ${index + 1}"></a>`;
                });
            }
            info += `
                        </div>
                    </div>
                </div>`;

            let infoWindow = new BMapGL.InfoWindow(info, opts); // 创建信息窗口对象

            // 为标记点添加点击事件
            marker.addEventListener("click", function () {
                map.openInfoWindow(infoWindow, point); // 打开信息窗口
            });
        });
    </script>
</body>
</html>
<?php
?>

三、创建marker.js

// 坐标查询:https://api.map.baidu.com/lbsapi/getpoint/index.html
var markers = [{
        latLng: [89.255025, 42.99805],
        name: "葡萄沟",
        icon: "zj.png",
        desc: "转车的时候下去看了看,确实和书本上描述的一样。"
    },
    {
        latLng: [75.996862, 39.476993],
        name: "喀什",
        icon: "zj.png",
        desc: "一出生就过去了,从小长大的地方,呆过十几年,不过现在已经没什么印象了。"
    },
    {
        latLng: [116.280592, 40.004567],
        name: "颐和园",
        icon: "zj.png",
        desc: "有点古典风格,到此处,感觉自己也有点儿儒雅的气质。"
    },
    {
        latLng: [116.024067, 40.362639],
        name: "八达岭长城",
        icon: "zj.png",
        desc: "不到长城非好汉,我去了五六次,应该是绝对的好汉了吧!"
    },
    {
        latLng: [116.403414, 39.924091],
        name: "故宫",
        icon: "zj.png",
        desc: "记得那会儿应该是20年前的事儿了。"
    },
    {
        latLng: [116.079068, 40.296759],
        name: "居庸关长城",
        icon: "zj.png",
        desc: "体验感一般,去过的人都不会选择再去了。"
    },
    {
        latLng: [120.127813, 30.228902],
        name: "西湖",
        icon: "zj.png",
        desc: "之前工作住周边,有事儿没事儿就去西湖转转,估摸着应该转了几十圈了吧。"
    },
    {
        latLng: [120.155526, 30.236867],
        name: "雷峰塔",
        icon: "zj.png",
        desc: "其它区域都是免费,维度雷峰塔要收费,原因是要坐船过去。"
    },

    {
        latLng: [124.831767, 45.148014], // 图文带跳转的
        name: "松原市",
        icon: "zj.png",
        desc: "松原市。",
        photo: [
            "https://img0.baidu.com/it/u=3915829036,420838185&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=1083",
            "https://img0.baidu.com/it/u=3915829036,420838185&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=1083",
            "/usr/uploads/2025/01/1454283446.jpg",
            "/usr/uploads/2025/01/3919603999.jpg",
        ],
        links: [
            "/index.php/archives/138/",
            "/index.php/archives/138/",
            "/index.php/archives/138/",
            "/index.php/archives/138/",
        ]
    },
];

四、嵌入使用

我这里是直接在关于页面引入,选择引入地方直接写好地址即可,我这里没有单独做页面,有需要做页面也可以单独改一下。

<?php $this->need('parts/map.php'); ?>
我简单实现了下,可以根据自己主题进行调整,特别是样式,需要根据自己主题进行适配;

参考文章:利用百度地图做博客足迹地图HTML源码

  •  

陪老婆去做了流产

与我而言,这个话题是沉重的。考虑再三,还是决定记录下来。

其实什么事儿都不是空穴来风,上月末发了一篇文章 聊聊我对生二胎的一些想法 ,其实关于这个问题,我一直在思考,从还没有生一胎的时候,一直到现在,我都有考虑。上次我老婆突然跟我说,可能怀了二胎,我还挺诧异,因为我们没有严格意义上要二胎计划,可以说是意外之喜,然后就买了试纸测量了,发现确实怀了,然后就找了个周末的时间去医院检查,由于周数比较小,所以也没查出来个啥,就是各项指数(孕酮)偏低,让多注意休息和补补孕酮啥的,让每周都过来看看。

经过了心里的一些挣扎后,既然孩子来了,那肯定是和我们家有缘分的,所以大家也都接受了,也开始按照从饮食起居上逐步调整,已经做好心里准备,迎接新宝宝的到来,为此老婆还规划了预产期,以及宝宝名字之类的。

连着去了两周多,孕酮指标一直上不去,而且做了B超,医生说没看到胎心,有可能是孕囊有问题,也有可能是空孕囊,当然也可能是怀孕周数不准,孩子太小等原因,我们潜意识里不愿意相信这样的结果,所以有换了个妇幼,结果仍然如此,虽然结果都一样,但诊断方面缺有不同,有的医生建议尽早流掉,这样对身体损伤最小,有的医生建议再等一周看看,可能孩子太小,查不出来也正常,后来找了一个朋友把片子给他看了,他建议直接流掉,说这个孩子即使过几周胎心出来了,也感觉是个发育缓慢的胎儿,且不说再以后过程中,会不会死亡,总的来说就是胎儿质量不高,不建议留下,留下以后可能也是伤人伤己。

得知这个结果之后,一时之间心里还是很难接受的,毕竟全家已经接受了这个小生命,突然又要舍弃掉它,真的是有点残忍,但为了以后,还是决定不要了,期间老婆很伤心,我也一直给她做心里疏导(其实就是陪着说说话),在做了大量检查后,最终安排入院,接受流产手术。

手术很成功,现在已经在家调养,不管怎样,身体还是需要调理好,不管以后还考不考虑二胎的事儿,也许某一天,我的博客里会分享二胎的喜讯,也有可能永远不会。

  •  

给小主机做一次大扫除

去年整个了低功耗的小主机,借助 Frps 做映射,实现外网访问,我把一些有大量存储需求的应用,都搬到小主机上了,本地也搭建了影音库和一些常用的应用,我的小主机配置还是比较高的,功耗较低,一天大概0.3 ~ 0.4度电的样子,当然存储最多只能扩展到3T,不过也基本够用了。

小主机真的蛮小的,比巴掌还小,也不重,散热方面感觉还可以,即使大热天,机器也没有明显发热现象,迄今为止,还没出现过问题,这点儿还是值得肯定的。

小主机从去过年到现在,距离一年也不久了,最近发现小主机风扇转的厉害,后来发现出风口和风扇都积灰了,也好久没关注它了,今天正好有点儿时间,就给简单清理一下吧。

拆卸还是比较简单的,底部四颗螺丝卸掉,就可以拿掉顶部机盖了,然后把电路板四颗螺丝也卸掉,就看到底部风扇了,主要是风扇和出风口积灰了,还有一些UBS、网口等等也有灰尘,我就用小刷子和湿巾简单清理了下,确实要干净了不少,弄好了就开始安装回去了,还是挺方便的。

底座安装回去之后的样子,一个512G的SATA硬盘,一个16G + 32G的内存条,一个2T m2固态硬盘,还可以扩展高速TF卡,各方面都很满意。

家里有小孩儿,好多地方不能放,只能放门口玄关位置,好了,小主机满血复活了!

  •  

一杯酒,一家人

不得不感叹,时间过得可真快,感觉才刚到25年,没想到已经距离年中不远了,这不生日也到了,由于是工作日,所以晚上到家也七点了,妈妈给做了菜,然后媳妇儿从公司点了两个菜,买了蛋糕,一家人一起吃个饭,既简单又实在,美中不足的是父亲没到场(上班,没有假期)。

也是开了瓶12年的白云边庆祝下,虽只有我一人饮酒,其它人也都喝饮料作陪,给闺女准备了金豆芽饮品,她也频繁跑上来跟我碰杯,虽然她还不太会表达,但看得出来,她很开心,不光有喝的,一会儿还有蛋糕可以吃,我们吃着饭,她围着桌子蹦蹦跳跳的,气氛格外融洽。

按道理说,今天也是闺女阳历生日,碰到一起了,所以就一起过了,自然蛋糕也要偏向于孩子风格,所以选了这个粉色系列,插上18岁的两根蜡烛,今年比较贪心,许了很多愿望,不敢奢求太多,只求一家人在往后的日子里 平安喜乐 即可。

我是一个感性的人,有时候看一些短视频,看着看着眼眶就湿润了,我也不知道为什么。

我一直觉得人的一生其实很短暂,悄悄把我们从看故事的人,变成了故事里的人。那些曾经让我们落泪的画面,如今正在我们自己的人生里一帧帧上演。这大概就是成长的滋味吧——既熟悉又陌生,既温暖又带着些许酸涩。
  •  

公司不续签劳动合同了,是否有赔偿?

最近有朋友聊到关于员工合同期满之后,如果公司单方面不续约情况下,是否有赔偿问题,就这方面拿出来聊聊,说实话,没经历过,我也不太懂,所以就找了各方面资料,汇总在一起,看是否有经历过的朋友,能给给建议。

大概情况我简单描述下,朋友在现在公司已经四年多了,前不久刚续签了一次合同,每次合同期为两年,两年到期后,公司HR会找合同期满员工续签合同,然后到上级部门审批,通过后,合同生效,合同内部没有明确说明续约相关条款,我那朋友也算是人到中年了吧,担心公司单方面不续约,导致失业,那么这种情况下是否有赔偿呢?他问我的时候,我也有点懵,因为确实没碰到过,按道理来说,公司不续约了,应该就不存在赔偿问题,由于不确定,所以我找了AI询问了下,大概结果如下:

OPENAI给出的回答:

  1. 劳动合同的性质:如果你的劳动合同是固定期限合同,并且在合同到期时公司不续签,通常情况下,雇主不需要支付额外的赔偿或补偿。
  2. 公司政策和规定:有些公司可能会根据内部政策提供一定的离职补偿或遣散费,虽然这并不是法律要求,但可以向人力资源部门询问相关政策。
  3. 劳动者的工作年限:在某些情况下,如果劳动者在公司工作了较长时间,可能会根据公司政策或集体合同获得一定的经济补偿。
  4. 法律法规:根据《中华人民共和国劳动合同法》,如果公司在合同到期后不续签且不符合特定的解雇条件,劳动者通常没有权利要求赔偿。但如果公司不合法解除劳动合同,劳动者可以要求赔偿。
    协商结果:在合同到期前,劳动者可以与公司进行协商,看看是否能够达成协议,获得一定的补偿。

豆包给出的回答:

  1. 需要支付经济补偿的情况:如果公司降低劳动合同约定条件续订劳动合同,劳动者不同意续订的,或者公司在劳动合同到期后直接决定不续签的,公司应当向劳动者支付经济补偿。经济补偿按劳动者在本单位工作的年限计算,每满一年支付一个月工资的标准向劳动者支付。六个月以上不满一年的,按一年计算;不满六个月的,向劳动者支付半个月工资的经济补偿。月工资是指劳动者在劳动合同解除或者终止前十二个月的平均工资。例如,劳动者在公司工作了 3 年 8 个月,劳动合同到期公司不续签且不存在法定续延情形,公司应支付 4 个月工资的经济补偿。
  2. 无需支付经济补偿的情况:公司维持或者提高劳动合同约定条件续订劳动合同,劳动者不同意续订的,此种情形下公司无需支付经济补偿。比如,原工资待遇是每月 5000 元,公司提出以每月 6000 元续签,劳动者拒绝,公司就不用支付补偿。

DeepSeek给出的回答:

  1. 公司主动不续签‌:必须按工作年限支付经济补偿,每满1年支付1个月工资,6个月以上不满1年按1年算,不满6个月支付半个月工资。
  2. 公司降低续签条件‌:若新合同存在降薪、调岗等不利变更,员工拒绝续签的,公司仍需支付补偿。
  3. 员工在医疗期、孕期、产期、哺乳期,或工伤治疗期间的,合同自动延续至情形消失,公司不续签属违法,需双倍赔偿。
  4. 合同到期后继续用工超1个月未签新合同的,从第2个月起需支付双倍工资(最长11个月)。

综合相关AI给出的结果,以及结合度娘的相关内容,综合到一起,基本上就是以下内容:

  1. 公司维持或提高原合同条件(如薪资、岗位不变或更好),员工拒绝续签的,公司无需补偿;
  2. 公司如果主动不续签情况下,按工龄执行 N 的赔偿机制,公司工作了4年,那么N就是4;
当然除了以上情况外,也有的人说,公司不做任何赔偿,但目前我没有找到实际经历过此事的朋友,希望有此类遭遇的朋友,能明鉴。。。
  •  

聊聊我对生二胎的一些想法

每次和老婆坐在客厅算家庭账本,算到最后总会陷入沉默。备孕二胎的念头像颗野草,时不时在心里冒头,但一想到现实,又赶紧把这火苗掐灭。产检费、顺产的几千块钱倒是小钱,可孩子出生后,乃至以后培养教育确实是一笔不小的投资。

隔壁王姐家老二刚出生,光是奶粉钱一个月就得两千多。原本给老大报乐高课的钱、全家出去旅游的预算,统统都得砍掉。现在老大上幼儿园,光校服费就分春夏秋冬四套,更别提兴趣班了。小区里的孩子不是学钢琴就是练舞蹈,当家长的哪能看着自家孩子啥特长没有?就算不报热门的,报个普通绘画班,一年下来也得小一万。​这是从邻居聊天得知的。

白天在公司对着电脑敲代码,眼睛酸得睁不开还得改方案。领导一个电话,周末就得加班。好不容易下班回家,老大缠着要陪着玩,骑车、遛弯,哄睡完自己累得倒头就睡。要是再来个小的,半夜喂奶、换尿布,第二天还得强撑着去上班,想想都觉得喘不过气。上个月部门裁员,好几个三十多岁的同事都走了,现在在公司每天都提心吊胆,生怕哪天就轮到自己。​

随着父母年龄大了,身体也或多或少会出些毛病,上周父亲说脖子后总是很胀痛,我们没有时间,他自己去医院拍了片子,医生说可能是血液太粘稠导致的,具体不敢确定,要进一步检查,妈妈身体也不是很好,一直说生了二胎全家人都被绑一起了,一个人也看不过来两个孩子,毕竟孩子到处跑啊,闹啊什么的。好不容易把第一个孩子养大了,还敢带着全家再走一遭吗?说实话,我是真没这个勇气。

家里现有房子比较小,目前住正好,如果有二胎了,可能就住不下,要考虑换房子的事儿了,都奔35的人了,谁都不敢说工作会一直稳定,还敢继续背房贷吗?虽然现在房价降了些,政策也有所调整,看似一切条件都有利于购房,但事实真的如此吗?我真的不相信。

现在的社会卷得太厉害,孩子从上幼儿园就开始 "拼爹妈" 。朋友家孩子才上小学,周末时间表排得比上班族还满,奥数、英语、编程轮着来。都说不能让孩子输在起跑线上,可这起跑线到底在哪儿?就算咬着牙供孩子读完大学又怎样?现在本科生遍地走,找工作照样难。我堂哥名牌大学毕业,还不是天天加班,工资也就勉强够生活,我也实在是不想自己辛苦培养出来的孩子,最终沦为资本家的牛马一辈子。​

更现实的是,老婆也三十好几了,要是再生二胎,身体能不能吃得消先不说,现在干的互联网工作肯定保不住。这个行业更新换代太快,二十多岁的年轻人一茬接一茬,等老婆休完产假,哪还有她的位置?转行又能做什么?超市收银、家政保洁,工资低不说,以后养老都成问题,当然也少不了对孩子的帮衬。​

有时候老婆和我开玩笑说 "再生一个,以后老了多个人照顾" ,话刚出口自己都笑了。想想我们现在,一年到头回不了几次老家,给父母的陪伴少得可怜,哪敢指望孩子以后能多孝顺?可看着老大一个人在家玩,又觉得孤单。小区里那些两个孩子的家庭,虽然鸡飞狗跳,但热热闹闹的也让人羡慕。​

和朋友聊过之后,分享出来的故事,然后结合自己的一些感触,写下了这篇文章。
  •  

Xc-Three主题 前台增加注册登录按钮

找到以下目录文件内的文字:<!-- 这里可以放很多东西 --> 这句文字注释,代码就放到这句文字下面(位子差不多在最底下)

默认左侧栏:Xc-Three/Miss/aside-left01.php
自定义左侧栏:Xc-Three/Miss/aside-left02.php

<!-- 登入 -->
<li>
    <?php if ($this->user->hasLogin()) : ?>
        <a class="link panel" href="#" rel="nofollow">
            <span class="Xc_balance"><?php $this->user->screenName(); ?></span>
            <svg class="icon-xl" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="15" height="15">
                <path d="M624.865 512.247L332.71 220.088c-12.28-12.27-12.28-32.186 0-44.457 12.27-12.28 32.186-12.28 44.457 0l314.388 314.388c12.28 12.27 12.28 32.186 0 44.457L377.167 848.863c-6.136 6.14-14.183 9.211-22.228 9.211s-16.092-3.071-22.228-9.211c-12.28-12.27-12.28-32.186 0-44.457l292.155-292.16z"></path>
            </svg>
        </a>
        <ul class="slides panel-body">
            <li>
                <?php if ($this->user->group == 'administrator' || $this->user->group == 'editor' || $this->user->group == 'contributor') : ?>
                    <a class="link" rel="noopener noreferrer nofollow" target="_blank" href="<?php $this->options->adminUrl("manage-posts.php"); ?>">管理文章</a>
                <?php endif; ?>
            </li>
            <li>
                <?php if ($this->user->group == 'administrator' || $this->user->group == 'editor') : ?>
                    <a class="link" rel="noopener noreferrer nofollow" target="_blank" href="<?php $this->options->adminUrl("manage-comments.php"); ?>">管理评论</a>
                <?php endif; ?>
            </li>
            <li>
                <?php if ($this->user->group == 'administrator') : ?>
                    <a class="link" rel="noopener noreferrer nofollow" target="_blank" href="<?php $this->options->adminUrl("options-theme.php"); ?>">修改外观</a>
                <?php endif; ?>
            </li>
            <li>
                <a class="link" rel="noopener noreferrer nofollow" target="_blank" href="<?php $this->options->adminUrl(); ?>">进入后台</a>
            </li>
        </ul>
    <?php else : ?>

<li>
    <a class="link panel" href="#" rel="nofollow" target="">
        <span class="Xc_balance">用户登录</span><svg class="icon-xl" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="15" height="15">
            <path d="M624.865 512.247L332.71 220.088c-12.28-12.27-12.28-32.186 0-44.457 12.27-12.28 32.186-12.28 44.457 0l314.388 314.388c12.28 12.27 12.28 32.186 0 44.457L377.167 848.863c-6.136 6.14-14.183 9.211-22.228 9.211s-16.092-3.071-22.228-9.211c-12.28-12.27-12.28-32.186 0-44.457l292.155-292.16z"></path>
        </svg></a>
    <ul class="slides panel-body panel-box" style="display: none;">
        <li>
            <a class="link" href="<?php $this->options->adminUrl('login.php'); ?>" target="_blank" rel="noopener noreferrer nofollow">登录</a>
            <?php if ($this->options->allowRegister) : ?>
                <a class="link" href="<?php $this->options->adminUrl('register.php'); ?>" target="_blank" rel="noopener noreferrer nofollow">注册</a>
            <?php endif; ?>
        </li>
    </ul>
</li>

<?php endif; ?>
</li>
  •  

Typecho主题模板 Xc-Three主题

Xc-Three

主题名字:Xc-Three
支持环境:PHP 8.0 8.1 8.2
支持全站Pjax无刷新加载
使用服务器搭建,虚拟主机不可用

{collapse}
{collapse-item label=" 授权方法" open}

主题采用单域名授权,需绑定一个授权域名,无限每7天可自助免费更换一次,后续升级版本均为免费

已购买用户 推荐人购买可获得30元奖励,未购买用户 推荐人购买可获得10元奖励,(提供推荐聊天截图)

当前主题售价128RMB,联系QQ:70027750(同微信)没想好购买不要添加

{/collapse-item}
{/collapse}

{collapse}
{collapse-item label=" 主题定制" open}

如果你觉得主题有加密授权的存在让你没有安全感

可以找我谈,我可以帮你写主题,功能外观也可与本站相同,加钱世界可及

接typecho主题定制,功能定制,主题美化

{/collapse-item}
{/collapse}

{collapse}
{collapse-item label=" 使用须知" open}

售后服务可帮你解决在使用本主题时遇到的问题,但不包含主机及 Typecho 安装、Typecho 的使用教程,如果你不会搭建,外加20¥可以帮你直接搭建好,加钱世界可及,避免扯皮。

因主题可复制性,个人使用可随意修改主题文件,还请勿泄露主题文件给他人,发现将不再提供后续服务,敬请谅解!

{/collapse-item}
{/collapse}

B站演示视频: https://www.bilibili.com/video/BV1X6zsYFE7T/

功能介绍
主题样式内置7种主题文章样式
多样式化顶部 侧栏 均可自定义内容
付费可见内置了一款免费的付费阅读插件,可对接当面付
评论样式内置2种评论区样式
邮箱推送有人评论回复都会给对方和站长发送邮件通知
置顶文章可在后台设置需要置顶显示的文章
pjax加载页面切换流畅度非常丝滑,几乎感觉不到卡顿
全局设置导航栏丶内容栏丶侧边栏都可以自定义宽度
侧边栏侧边栏可自定义显示和开关
字体内置多种页面字体和自定义字体
缩略图默认显示内置缩略图,也可自定义随机缩略图
懒加载图设置一个加载图,等图片完全加载出来了再显示文章的图
静态CDN可自定义网站的静态文件cdn加速
独立页面内置多种独立页面,留言板丶闪念丶友链丶壁纸丶导航丶归档
短代码编辑器多达40种丰富的短代码功能
其他功能还有很多其他功能,已本站为准,本站有的功能都有

 

{dotted startColor="#ff6c6c" endColor="#1989fa"/}

警告提示

{alert type="info"}警告提示{/alert}{alert type="success"}警告提示{/alert}{alert type="warning"}警告提示{/alert}{alert type="error"}警告提示{/alert}

消息提示

{message type="success" content=""/}
{message type="info" content=""/}
{message type="warning" content=""/}
{message type="error" content=""/}

云盘下载

{cloud title="默认网盘" type="default" url="" password=""/}

{cloud title="360网盘" type="360" url="" password=""/}

{cloud title="百度网盘" type="bd" url="" password=""/}

{cloud title="天翼网盘" type="ty" url="" password=""/}

{cloud title="诚通网盘" type="ct" url="" password=""/}

{cloud title="微云网盘" type="wy" url="" password=""/}

{cloud title="Github仓库" type="github" url="" password=""/}

{cloud title="蓝奏网盘" type="lz" url="" password="123456"/}

超链按钮

自定义自己想要的颜色  {abtn color="#73aaff" href="https://www.baidu.com" radius="" content="超链按钮"/}

自定义颜色按钮 {abtn color="#20c6a7" href="https://www.baidu.com" radius="" content="超链按钮"/}

彩色虚线

{dotted startColor="#ff6c6c" endColor="#1989fa"/}
自定义虚线颜色,支持任意颜色

{dotted startColor="#1772e8" endColor="#4cd327"/}

回复可见

隐藏内容,请前往内页查看详情

进度条

{progress percentage="" color="#ff6c6c"/}

自定义颜色

{progress percentage="30%" color="#3a9aee"/}

3行3列的表格

表头表头表头
表格表格表格
表格表格表格
表格表格表格

代码块

const obj = {
    name: 'hi',
    age: 18
}
// 判断某个属性是否在对象里
console.log('name' in obj)
// 删除对象某个属性
console.log(delete obj.name)
// 将对象的属性名提取成数组
console.log(Object.keys(obj))

Tabs标签页

{tabs}{tabs-pane label="标签一"}啊哈哈哈哈我是大傻逼!{/tabs-pane}{tabs-pane label="标签二"}啊啊啊啊啊!哦哦哦哦哦?哈哈哈哈哈!{/tabs-pane}{/tabs}

时间轴

{timeline}{timeline-item color="#19be6b"} 1.0.0版本正式上线{/timeline-item}{timeline-item color="#19be6b"} 更新2.0.0版本{/timeline-item}{timeline-item color="#ed4014"} 删库跑路{/timeline-item}{/timeline}

跑马灯

{lamp/}

折叠面板

{collapse}{collapse-item label="折叠标题一" open} 折叠内容一{/collapse-item}{collapse-item label="折叠标题二"} 折叠内容二{/collapse-item}{/collapse}

  •  

2024最新QQ昵称获取 官方接口

官方接口最新获取QQ昵称,解决编码问题

<?php

// 设置跨域和返回格式
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET");
header("Access-Control-Allow-Headers: Content-Type");
header('Content-type: application/json;charset=utf-8');

// 如果没有通过 GET 参数传入则使用此默认值
$qq = isset($_GET['qq']) ? $_GET['qq'] : '70027750';

function getUserInfo($qq)
{
  $curl = curl_init();
  curl_setopt_array($curl, array(
    CURLOPT_URL => 'https://users.qzone.qq.com/fcg-bin/cgi_get_portrait.fcg?uins=' . $qq,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false,
    CURLOPT_ENCODING => '',
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_POSTFIELDS => '------WebKitFormBoundaryYTwvlk5brGmyD3Mn',
    CURLOPT_HTTPHEADER => array(
      'Content-Type: multipart/form-data; boundary=---012345678912345678912312',
    ),
  ));
  $response = curl_exec($curl);
  $encode = mb_detect_encoding($response, array("ASCII", 'UTF-8', "GB2312", "GBK", 'BIG5'));
  $response = mb_convert_encoding($response, 'UTF-8', $encode);
  $data = json_decode(substr($response, 17, -1), true);

  // 返回的用户信息
  $userInfo = array(
    'name' => isset($data[$qq][6]) ? $data[$qq][6] : '',
    'mail' => $qq . '@qq.com',
    'avatar' => isset($data[$qq][0]) ? str_replace('http://', 'https://', $data[$qq][0]) : '',
    'qzone' => 'https://user.qzone.qq.com/' . $qq,
    'imgurl' => 'https://q1.qlogo.cn/g?b=qq&nk=' . $qq . '&s=40',
    'imgurl1' => 'https://q1.qlogo.cn/g?b=qq&nk=' . $qq . '&s=100',
    'imgurl2' => 'https://q1.qlogo.cn/g?b=qq&nk=' . $qq . '&s=140',
    'imgurl3' => 'https://q1.qlogo.cn/g?b=qq&nk=' . $qq . '&s=640',
    'by' => 'Miss君',
    'blog' => '博客:www.tmetu.cn',
  );

  return $userInfo;
}

// 获取用户信息
$userInfo = getUserInfo($qq);

// 构建返回的结果数组
$result = array(
  'code' => 200,
  'qq' => $qq,
  'data' => $userInfo,
  'time' => date('Y-m-d H:i:s') // 添加当前时间戳
);

// 输出 JSON 格式的结果
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

输出格式

{
    "code": 200,
    "qq": "70027750",
    "data": {
        "name": "Miss",
        "mail": "70027750@qq.com",
        "avatar": "https://qlogo3.store.qq.com/qzone/70027750/70027750/100",
        "qzone": "https://user.qzone.qq.com/70027750",
        "imgurl": "https://q1.qlogo.cn/g?b=qq&nk=70027750&s=40",
        "imgurl1": "https://q1.qlogo.cn/g?b=qq&nk=70027750&s=100",
        "imgurl2": "https://q1.qlogo.cn/g?b=qq&nk=70027750&s=140",
        "imgurl3": "https://q1.qlogo.cn/g?b=qq&nk=70027750&s=640",
        "by": "Miss君",
        "blog": "博客:www.tmetu.cn"
    },
    "time": "2024-08-30 00:11:20"
}
  •  

简约系PHP授权系统

Test

一款简约系PHP授权系统

  1. 支持用户等级判断,不一样的等级添加授权需要不一样的价格。
  2. 后台生成余额卡密,卡密可充值余额,余额可添加授权。
  3. 后台生成授权卡密,卡密可兑换添加授权。
  4. 前台支持查询授权是否为正版授权。
  5. 前台支持输入域名+qq+授权码下载源码。
  •  

思源宋,小米官方字体 css调用

偶然发现小米官网的字体无防盗链 可以直接使用,而且小米还储存了思源宋体

为什么用别人的字体?本地储存字体加载起来很慢,自己服务器慢的话那就可想而知了,所以我们可以利用小米官网储存的字体加速,看链接就知道这是小米官网,放心使用~

链接中的400,600,700 可以去除其中一个,分别对应字体的粗细,看自己需求

演示站: https://tmetu.cn/

小米字体

//css引用 
<link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,700:MiSans" />

//css调用
* {font-family: MiSans}

思源宋字体

//css引用 
<link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=Source_Han_Serif:400,600:Source_Han_Serif" />

//css调用
* {font-family: Source Han Serif}

程序员专用字体

//css引用 
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,700,700i" />

//css调用
* {font-family: Open Sans}
  •  

给网站顶部加一个进度条(适用于Pjax)

很多网站使用了Pjax无刷新加载 顶部没有页面加载进度,为了美观丶流畅感觉可以加进去,分享写个教程给大家

首先创建一个js文件引入(把以下js代码放进去)
注意:引入到jquery.js的下面

!(function (n, e) {
  "function" == typeof define && define.amd
    ? define(e)
    : "object" == typeof exports
    ? (module.exports = e())
    : (n.NProgress = e());
})(this, function () {
  function n(n, e, t) {
    return e > n ? e : n > t ? t : n;
  }
  function e(n) {
    return 100 * (-1 + n);
  }
  function t(n, t, r) {
    var i;
    return (
      (i =
        "translate3d" === c.positionUsing
          ? { transform: "translate3d(" + e(n) + "%,0,0)" }
          : "translate" === c.positionUsing
          ? { transform: "translate(" + e(n) + "%,0)" }
          : { "margin-left": e(n) + "%" }),
      (i.transition = "all " + t + "ms " + r),
      i
    );
  }
  function r(n, e) {
    var t = "string" == typeof n ? n : o(n);
    return t.indexOf(" " + e + " ") >= 0;
  }
  function i(n, e) {
    var t = o(n),
      i = t + e;
    r(t, e) || (n.className = i.substring(1));
  }
  function s(n, e) {
    var t,
      i = o(n);
    r(n, e) &&
      ((t = i.replace(" " + e + " ", " ")),
      (n.className = t.substring(1, t.length - 1)));
  }
  function o(n) {
    return (" " + (n.className || "") + " ").replace(/\s+/gi, " ");
  }
  function a(n) {
    n && n.parentNode && n.parentNode.removeChild(n);
  }
  var u = {};
  u.version = "0.2.0";
  var c = (u.settings = {
    minimum: 0.08,
    easing: "ease",
    positionUsing: "",
    speed: 200,
    trickle: !0,
    trickleRate: 0.02,
    trickleSpeed: 800,
    showSpinner: !0,
    barSelector: '[role="bar"]',
    parent: "body",
    template:
      '<div class="bar" role="bar"><div class="peg"></div></div></div>'
  });
  (u.configure = function (n) {
    var e, t;
    for (e in n) (t = n[e]), void 0 !== t && n.hasOwnProperty(e) && (c[e] = t);
    return this;
  }),
    (u.status = null),
    (u.set = function (e) {
      var r = u.isStarted();
      (e = n(e, c.minimum, 1)), (u.status = 1 === e ? null : e);
      var i = u.render(!r),
        s = i.querySelector(c.barSelector),
        o = c.speed,
        a = c.easing;
      return (
        i.offsetWidth,
        l(function (n) {
          "" === c.positionUsing && (c.positionUsing = u.getPositioningCSS()),
            f(s, t(e, o, a)),
            1 === e
              ? (f(i, { transition: "none", opacity: 1 }),
                i.offsetWidth,
                setTimeout(function () {
                  f(i, { transition: "all " + o + "ms linear", opacity: 0 }),
                    setTimeout(function () {
                      u.remove(), n();
                    }, o);
                }, o))
              : setTimeout(n, o);
        }),
        this
      );
    }),
    (u.isStarted = function () {
      return "number" == typeof u.status;
    }),
    (u.start = function () {
      u.status || u.set(0);
      var n = function () {
        setTimeout(function () {
          u.status && (u.trickle(), n());
        }, c.trickleSpeed);
      };
      return c.trickle && n(), this;
    }),
    (u.done = function (n) {
      return n || u.status ? u.inc(0.3 + 0.5 * Math.random()).set(1) : this;
    }),
    (u.inc = function (e) {
      var t = u.status;
      return t
        ? ("number" != typeof e &&
            (e = (1 - t) * n(Math.random() * t, 0.1, 0.95)),
          (t = n(t + e, 0, 0.994)),
          u.set(t))
        : u.start();
    }),
    (u.trickle = function () {
      return u.inc(Math.random() * c.trickleRate);
    }),
    (function () {
      var n = 0,
        e = 0;
      u.promise = function (t) {
        return t && "resolved" !== t.state()
          ? (0 === e && u.start(),
            n++,
            e++,
            t.always(function () {
              e--, 0 === e ? ((n = 0), u.done()) : u.set((n - e) / n);
            }),
            this)
          : this;
      };
    })(),
    (u.render = function (n) {
      if (u.isRendered()) return document.getElementById("nprogress");
      i(document.documentElement, "Progress-bar");
      var t = document.createElement("div");
      (t.id = "nprogress"), (t.innerHTML = c.template);
      var r,
        s = t.querySelector(c.barSelector),
        o = n ? "-100" : e(u.status || 0),
        l = document.querySelector(c.parent);
      return (
        f(s, {
          transition: "all 0 linear",
          transform: "translate3d(" + o + "%,0,0)"
        }),
        c.showSpinner || ((r = t.querySelector(c.spinnerSelector)), r && a(r)),
        l != document.body && i(l, "nprogress-custom-parent"),
        l.appendChild(t),
        t
      );
    }),
    (u.remove = function () {
      s(document.documentElement, "Progress-bar"),
        s(document.querySelector(c.parent), "nprogress-custom-parent");
      var n = document.getElementById("nprogress");
      n && a(n);
    }),
    (u.isRendered = function () {
      return !!document.getElementById("nprogress");
    }),
    (u.getPositioningCSS = function () {
      var n = document.body.style,
        e =
          "WebkitTransform" in n
            ? "Webkit"
            : "MozTransform" in n
            ? "Moz"
            : "msTransform" in n
            ? "ms"
            : "OTransform" in n
            ? "O"
            : "";
      return e + "Perspective" in n
        ? "translate3d"
        : e + "Transform" in n
        ? "translate"
        : "margin";
    });
  var l = (function () {
      function n() {
        var t = e.shift();
        t && t(n);
      }
      var e = [];
      return function (t) {
        e.push(t), 1 == e.length && n();
      };
    })(),
    f = (function () {
      function n(n) {
        return n
          .replace(/^-ms-/, "ms-")
          .replace(/-([\da-z])/gi, function (n, e) {
            return e.toUpperCase();
          });
      }
      function e(n) {
        var e = document.body.style;
        if (n in e) return n;
        for (
          var t, r = i.length, s = n.charAt(0).toUpperCase() + n.slice(1);
          r--;

        )
          if (((t = i[r] + s), t in e)) return t;
        return n;
      }
      function t(t) {
        return (t = n(t)), s[t] || (s[t] = e(t));
      }
      function r(n, e, r) {
        (e = t(e)), (n.style[e] = r);
      }
      var i = ["Webkit", "O", "Moz", "ms"],
        s = {};
      return function (n, e) {
        var t,
          i,
          s = arguments;
        if (2 == s.length)
          for (t in e)
            (i = e[t]), void 0 !== i && e.hasOwnProperty(t) && r(n, t, i);
        else r(n, s[1], s[2]);
      };
    })();
  return u;
});

在创建一个css文件 引入(把以下css代码放进去)

#nprogress{pointer-events:none}
#nprogress .bar{background:#73aaff;position:fixed;z-index:99999;top:0;left:0;width:100%;height:2px;}
#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;opacity:1;transform:rotate(3deg) translate(0,-4px);}

最后加入pjax 重载函数

$(document).on('pjax:send', function() {
NProgress.start()
});

$(document).on("pjax:complete", function () {
NProgress.done();
});
  •  

让KDE输入法更好用的配置Fcitx5 + Rime(中州韵)

✅ 在 Ubuntu 的 KDE 桌面环境下 KDE输入法推荐:Fcitx5 + Rime(中州韵) 📌 优点:/ 系统集成好:Fcitx5 在 KDE(Plasma)下兼容性非常好,界面美观,响应迅速。 Rime 输入法引擎:支持拼音、双拼、仓颉、五笔等各种方案,极其灵活,支持自定义词库、输入风格。 KDE 配套支持完善:托盘图标清晰,输入法切换稳定。 🔧 安装命令(适用于 Ubuntu 20.04/22.04/24.04): sudo apt update sudo apt install fcitx5 fcitx5-chinese-addons fcitx5-rime 然后设置环境变量: im-config -n fcitx5 注销或重启。 🚀 启动配置工具(可视化设置): bash fcitx5-configtool 本文永久更新地址: https://v2fy.com/p/2025-06-03-09-52-29-config-ubuntu-kde/
  •  

让Linux Ubuntu KDE桌面smb拷贝更快,提升Plex server影视服务器读取效率

安装软件包 sudo apt-get install cifs-utils -y 创建挂载点 mkdir -p ~/home-smb 创建密码凭据文件 sudo vim /etc/credentials 在凭据文件中添加内容 username='smb登陆用户名' password='smb登陆密码' 修改权限 sudo chmod 600 /etc/credentials 挂载smb服务路径文档 sudo mount -t cifs //192.168.66.217/root ~/home-smb -o credentials=/etc/credentials 有用的挂载选项: uid=1000,gid=1000 – 设置挂载后的文件所有者 dir_mode=0755,file_mode=0644 – 设置目录和文件权限 vers=3.0 – 指定SMB协议版本 iocharset=utf8 – 设置字符编码 实现开机自动挂载 我在ubuntu系统的管理员用户名zhaoolee, 也会创建同名用户组zhaoolee 在 /etc/fstab 尾部添加以下内容 //192.168.66.217/root /home/zhaoolee/home-smb cifs credentials=/etc/credentials,iocharset=utf8 0 […]
  •  

二〇二五年五月总结,不思不虑寻找快乐

时间过得很快,不知不觉5月过完了。月初五一假期,月末端午假期,这样的组合真不错。

总体来看这个月没有任何成就,但也谈不上浑浑噩噩。希望时光慢一些,可以好好享受这不思不虑的生活。

寻找快乐的方法很简单,刷刷短视频看看短剧,嘻嘻哈哈一天很快就过去了。在短暂的快乐不需要审视当下和未来,快乐就好。寻找快乐永远是我们乐于做的事情,更何况是用简单而慵懒的方法就可以找到快乐。

辰山植物园搭帐篷

五一假期逛了逛辰山植物园。找了一块空地,支天幕,搭帐篷,不为赶路不为体验项目,坐在树下吹吹风看看景。渴了喝点水,饿了吃点零食,就这样静静呆着十分惬意。

离我们比较近的是月季园,远远看去很多人在那里游玩观赏拍照,我也过去凑凑热闹。

没想到月季花的品种竟有这么多,花瓣的形状、颜色、大小各异。月季花的生命力如火焰那样强烈。

工会休养杭州3日游

工会休养比较轻松,每天只安排半天行程,项目比较少。第一天去湘湖坐船,第二天爬北高峰,第三蹬雷峰塔。

徒步盐帮古道十八渡

月末端午节跟团徒步盐帮古道和十八渡,早上6点多起床赶大巴,中途停靠一次,大约4个小时到地方。

端午节徒步盐帮古道的人排起长队,不同颜色的绑带混杂在一起,沿着山路往上爬。到达山顶休息补给,这里有冷水、冰糕、泡面。继续往前下山是去十八渡的路,跟团可以不走回头路,第一渡到第十八渡,然后去大巴停车场。

看书

碎片化的内容给予不了养分看过就忘,但是看过的书籍何尝不是这样。从外而内的改变不在一朝一夕,本质不想改变再长的时间也是浪费生命。

深知当下的自己跟不上时代的浪潮,又祈求通过外力改变自己。看完一个有知识有经验的视频,读完一本深入有方法的书籍,我还是我,内心得不到改变行动永远停留在那里。

5月份没有正儿八经看书,但要说起来……通过蚂蚁摄影APP看了一本《用索尼相机拍好51个生活场景》。

想拍好照片不容易,书中说第一时间拍出照片,觉得很有道理。拍好照片,动手比动脑重要。

骑车

总里程341.21km
总时长15:52:09
骑行均速21.50km/h
骑行次数26

骑车是目前唯一坚持的运动,这样的运动虽然不能使我肌肉凸显,但能使我保持较好的体能。

继续有氧骑行,当下目标将踏频稳定在90以上。目前踏频可以稳定在80以上,再接再厉。为提高速度,提高踏频是相对容易做到的。

  •  

一次借助ChatGPT抵御恶意攻击的经历,为个人服务器添加自动防御系统fail2ban

我有一台个人服务器,托管着自己的WordPress网站,也放了RustDesk这种私有化的远程桌面工具,最近我发现RustDesk特别卡,登录服务器才发现WordPress消耗了大量的资源和带宽 然后我把现象丢给了GPT4o, GPT4o要求查看我的Nginx日志 sudo tail -f /var/log/nginx/access.log 我将日志贴给gpt后,gpt快速分析了问题 一、 使用 Nginx 限速保护登录接口 为了避免保留破解,我们需要对wp-login.php进行限流 1.1 编辑 nginx.conf,添加限流配置 在 http {} 区块中添加以下内容: limit_req_zone $binary_remote_addr zone=login_zone:10m rate=5r/m; 解释如下: $binary_remote_addr:按 IP 建立限流; login_zone:10m:设置一个 10MB 内存的限流区域; rate=5r/m:每个 IP 每分钟最多允许 5 次请求。 📌 注意:必须写在 http {} 块中! 1.2 修改虚拟主机配置,启用登录页限速 location = /wp-login.php { limit_req zone=login_zone burst=3 nodelay; include fastcgi_params; fastcgi_pass unix:/run/php/php8.2-fpm.sock; […]
  •  

家庭数据中心系列 数据变更感知到自动导出:构建 WordPress 双活同步自动化运维的最后一跳

家庭数据中心系列 数据变更感知到自动导出:构建 WordPress 双活同步自动化运维的最后一跳 无敌的个人博客 tangwudi

1 前言 我之前的博客架构是家庭数据中心(主节点+热备节点) + 腾讯云(容灾节点),属于比较典型的”单节点读写”方案。由于日常只有主节点负责处理数据库的读写请求,所以数据库之间并不需要实时同步:每当我新增文章、修改内容,或者批准、回复评论,导致 WordPress 的数据库发生变化时,如果刚好有心情,我会手动将主节点的 MariaDB 中的 wordpress 库导出为 wordpress.sql 文件,并放进 Syncthing 的同步目录里。接下来,Syncthing 会将这个文件同步到热备节点和容灾节点的指定目录,这2个节点上的 inotify 脚本监测到目录发生变化后,自动触发数据库导入脚本,将 wordpress.sql 文件导入对应的 MariaDB 中,从而完成主节点和其他节点之前的数据同步。 不过现在,博客架构升级成了家庭数据中心”主写 […]

<p>The post 家庭数据中心系列 数据变更感知到自动导出:构建 WordPress 双活同步自动化运维的最后一跳 first appeared on 无敌的个人博客.</p>

  •  

自建”非直连”科学节点的架构与原理:sing-box 与 Cloudflare 的组合实践

自建”非直连”科学节点的架构与原理:sing-box 与 Cloudflare 的组合实践 无敌的个人博客 tangwudi

1 前言 自从购买了Racknerd的VPS,并完成了将容灾节点从腾讯云轻量服务器搬家到Racknerd芝加哥VPS的大工程之后(参见文章:家庭数据中心系列 博客架构的第二次重构:VPS搬家引发的服务迁移与双活容灾实践,数据中心搬家可不就是大工程),我还在思考如何能够充分利用这高价购买(39.88美金/年)的芝加哥节点(Racknerd_高配1_882),毕竟现在芝加哥节点的资源还大量闲置着: 转念一想,其实除了硬件资源,芝加哥节点每月流量高达8500G,而我现在主要是将它作为平时核心应用(博客)对Cloudflare的主回源站,这种用法根本用不了多少流量,等于说这么多月流量完全浪费了。 既然想充分利用流量,那自建科学节点就是首当其冲的用法了。可是因为我之前并没有境外的VPS,对这方面的知识也不怎么了解,所以,需要先梳理一下相关的知识点。 2 背景知识:现实网络环境下的访问困境 在当今的网 […]

<p>The post 自建”非直连”科学节点的架构与原理:sing-box 与 Cloudflare 的组合实践 first appeared on 无敌的个人博客.</p>

  •  

人生不过三万天,别怕做没有意义的事

陷入能量低谷,刷视频,看短剧,深陷其中无法自拔。虽然每看一个视频,都会告诫自己,“焦虑无止境,只能自己就自己”,但是身体仍旧躺在床上,手指划过屏幕。点击“2.0x”倍速,虽然有时快到听不清楚,有时快到看不清一闪而过的画面,但我未曾想过调回“1.0x”的正常速度。

有时积极,有时忧郁,似乎进入了双重人格。我清楚的知道自己并非如此,但我仍需要调节情绪,看淡一切,财富、功利、情感。也许我正在通过这种心理暗示将易碎的心灵包裹。

短短两天抖音又关注了一些人:电烙铁大师兄、郑哥哥聊心理、魏姐姐谈心理、精神心理科王昕主任、心一小道、道·小生。

电烙铁大师兄1——星黎是一个聊天智能体,目前这款产品还未上市。最近的一条视频,大师兄使用豆包克隆自己与星黎对话,竟然被星黎开启力图灵测试,接下来对话变得诡异,星黎的似乎也有情绪。

男性重度抑郁患者的死亡率竟然是女性的三倍2,从没想过抑郁症会给患者带来那么大的痛苦。自残、自杀,因抑郁症症而换心血管疾病的人也很多。本以为抑郁症只是一个简单的心理问题,没想到会反应在身体上。关注了郑哥哥3、魏姐姐4,才知道不是所有问题都能迎刃而解。

心一小道5、道·小生6,没想到我竟然会认同他们说的一些话,感觉自己正向道系网友发展。不内耗,随自己心意,想努力便努力,想躺平就躺平,不急功近利,品行与财富同行。“无用之用,方为大用。人生不过三万天,别怕做没有意义的事。”

  1. 电烙铁大师兄的抖音 – 抖音 ↩︎
  2. 中国人群抑郁症与全因、特定原因死亡率之间的关联 — 中国,2010-2022 年 ↩︎
  3. 郑哥哥聊心理的抖音 – 抖音 ↩︎
  4. 魏姐姐讲心理的抖音 – 抖音 ↩︎
  5. 心一小道的抖音 – 抖音 ↩︎
  6. 道·小生的抖音 – 抖音 ↩︎

  •  

《树莓派不吃灰》033:为ubuntu server 24.04添加xfce4轻量化桌面配合xrdp实现图形化控制

最近想在树莓派挂机一些网页,比如上一期提到的《树莓派不吃灰》032:基于Deepseek每天自动算八字,自动生成最合适的摆件显示在办公桌 https://github.com/zhaoolee/pi/blob/main/_posts/2025-05-10-11-29-25-fortune.md 但我的树莓派是安装的ubuntu server 24.04,没有桌面环境,于是本期为ubuntu server24.04添加一个桌面环境。 为什么选择xrdp而不是vnc xrdp对比vnc的优势是在弱网条件下依然有更流畅的表现,rdp传输的是“绘图指令”(逻辑信息),在客户端重建图像,数据量小, 而VNC传输的是“整幅像素图”(图像帧),尤其屏幕变化时数据量巨大。 Windows系统自带rdp的客户端,免费,成本更低。 macOS客户端也有微软提供的rdp的官方客户端,依然免费。 同步时间 sudo apt install ntpdate -y sudo ntpdate time.windows.com 切换到非root用户 su - zhaoolee 安装xfce4 sudo apt install xfce4 xfce4-goodies -y 配置xrdp 什么是RDP(Remote Desktop Protocol)? RDP是微软(Microsoft)开发的远程桌面协议,用于远程连接 Windows 系统的桌面环境。默认端口是 3389,使用的是 Windows 原生的远程桌面服务(Remote Desktop Services / Terminal Services),支持图形加速、剪贴板共享、打印重定向、音频传输等功能客户端:Windows 内置的远程桌面(mstsc.exe),macOS、Linux 也有对应客户端(如 Remmina、Microsoft Remote Desktop) 你在办公室用 Windows 电脑,回家后用笔记本通过远程桌面连接,看到的就是办公室电脑的界面,这背后用的就是 RDP […]
  •  

dnscrypt-proxy(v2.1.8) 多场景配置指南:从上游部署到下游集成

dnscrypt-proxy(v2.1.8) 多场景配置指南:从上游部署到下游集成 无敌的个人博客 tangwudi

1 前言 其实,我一直想在内网搭建一个稳定、无污染的 DNS 服务,专门用于解决 DNS 污染问题,供部分仅需解决DNS污染的应用使用,比如emby的tmdb插件,不能正常刮削影视信息,仅仅是因为其API地址”api.themoviedb.org”被国内DNS进行了污染而已,比如这是用国内DNS的解析结果: 而正常的解析应该是解析到AWS cloudfront的Anycast地址: 所以,这部分应用其实并不需要全局代理,需要的仅仅是一个”无污染”的DNS而已。 过去,解决DNS污染问题,我是依赖于 AC86U 路由器上 Merlin 固件内置的 Clash 插件来实现,通过它劫持 UDP 53 端口来将 DNS 请求转发到外部加密 DNS 服务。这种方式虽然也能解决DNS污染,但却存在着较大的限制: 对服务稳定性要求高:一旦 Clash 插件 […]

<p>The post dnscrypt-proxy(v2.1.8) 多场景配置指南:从上游部署到下游集成 first appeared on 无敌的个人博客.</p>

  •  

家庭数据中心系列 博客架构的第二次重构:VPS搬家引发的服务迁移与双活容灾实践

家庭数据中心系列 博客架构的第二次重构:VPS搬家引发的服务迁移与双活容灾实践 无敌的个人博客 tangwudi

1 前言 随着腾讯云轻量服务器到底时间越来越近,我一直在犹豫是否续费,第一年是99元/年,第二年我记得是300元/年(记不太清楚了,左右吧),而第三年的价格是多少呢?看了一下,好家伙: 本来我就觉得备案没啥用了(电信在打击有入向HTTP流量的、有公网IP的家宽,这样一来,国内CDN回源主机直接指向家宽公网IP对应的动态域名这种方式就没用了),加上国内VPS平时使用中的各种不变(不能正常访问docker、git、apt等等,非要折腾,烦死了),早就想放弃了,只是想着保留着备案,万一哪天排上用场了呢。 可是加上这次的续费价格,直接打破了我最后一点念想,这让我最终下定决心:域名注销备案,也迁到cloudflare;同时我的云上冗余数据中心也同步搬迁到境外的VPS,那么,接下来面临的第一个问题,就是往哪儿搬呢? 2 VPS的选购 2.1 选购标准 境外可选的VPS供应商实在是太多了,按理说选择面应 […]

<p>The post 家庭数据中心系列 博客架构的第二次重构:VPS搬家引发的服务迁移与双活容灾实践 first appeared on 无敌的个人博客.</p>

  •  

《树莓派不吃灰》032:基于Deepseek每天自动算八字,自动生成最合适的摆件显示在办公桌

我有个朋友,喜欢在桌面搞点风水摆件,提升运势,我感觉这东西虽然玄学,但确实能提供心理安慰的作用,让人心情愉悦。 于是,我打算搞一个电子风水摆件,录入自己的八字信息,每天自动调用满血版DeepSeek,计算今天最适合的风水摆件,并通过屏幕展示在桌面上。为了避免过于单调,还可以让Deepseek大模型把今天中午适合吃什么,今天适合联系哪些朋友,今天幸运数字是什么,今天的幸运色是什么,变成一个个小建议轮播到屏幕上! 风水摆件所需原料及售价: 能运行浏览器的开发板(树莓派,香橙派都可以,小黄鱼树莓派3b基本100块搞定,丰俭由人) 带触控的屏幕(拼夕夕一手7寸5点屏幕售价100块,小黄鱼还能再打8折) DeepSeek按量付费的token(充10块钱,一年用不完) 经过五一劳动节的劳动,已经搞得差不多 在线体验地址 http://fangyuanxiaozhan.com:4000/register 进入网页后,需要录入出生日期和时间,方便大模型八字获取八字信息(点击圆形头像,有惊喜🕶) 点击注册后,程序会自动跳转到一个url,这个网页的url可以放到树莓派浏览器打开,每天的零点后,浏览器会自动刷新,重新计算当天运势;(底部有个输入框,里面有塔罗占卜,今天适合听什么歌的预制对话,也可以随意提问,和大模型Chat的玩法基本一样) 点击右上角的「进入玄修」,就会进入风水摆件页面,风水摆件会有一个闪着光晕的细腻动画。 实机运行效果如下(画面被压缩了,实际效果好很多,一度引起办公室众多玄学爱好者的围观) 运维老哥点了一根烟,说起了不足为外人道的绝密往事 去年底阿里云状况频发,新加坡的某个机房着火,两个星期才勉强恢复,这可能不是技术问题,而是风水问题。 如果你是一个二手电子垃圾爱好者,或者运维老哥,也可以将风水摆件放到机房,机魂大悦,让你一觉到天明。 我为我的二手硬件小机房,添加了一个风水摆件,内网穿透的成功率变高了很多😁 (信则有,不信则无)。 后续计划,搞个更酷的电子潮玩版本 我打算用分光棱镜做个更酷的简化版本,Demo如下图所示,可以显示有限的文字,依然是每天占卜,给出建议,成本基本在100块以内,而且会非常省电。作为电子潮玩售卖,图一乐! 小结 如果把制作玄学摆件作为一个创业项目,应该被归类为图一乐的类别,他能为使用者带来乐子,也能给创业者带来乐子! 如果有人愿意为玄学摆件付费,那真的是非常理想的创业项目;世界上从不缺少让人紧绷的创业项目,这些项目大多也会失败,玄学摆件不需要参与者紧绷,疗效信则有,不信则无。 只是单纯的好玩(^-^)V。 本文永久更新地址: https://v2fy.com/p/2025-05-10-11-29-25-fortune/
  •  

2025年4月阅读书摘

✇Dennis
作者Domon

4月阅读记录

  • 《悉达多》Done
  • 《每一句话语都坐着别的眼睛》Done
  • 《如何度过每天的 24 小时》Done
  • 《东京贫困女子》80%

4月阅读书摘

《每一句话语都坐着别的眼睛》

每一句话语都坐着别的眼睛

2025年4月阅读书摘

我不想被这盛开的、铺张着所有颜色的陈列馆俘虏,我不要将自己的身体奉献给这贪婪的、用鲜花伪装的燃烧的夏天。我要离开花边,走上地毯,脚下是坚实的柏油路,死亡就无法从地下爬上脚踝。

我挑衅赤身迎面而来的无常,却无力找到可以勉强自己顺应世俗的尺度。

最关键的东西往往无法言说,而言说的冲动却总在旁流淌。

远走他乡的树像背井离乡的人,在恰当的时刻离开危险的地方,找到一块不很恰当的土地,在一个错误的地方停下来,无法决心继续走下去。

国王鞠躬,国王杀人

我把他吃了。 所有事物都有它们自己的(国)王。每个王出场时,都会向别的王点头示意。王们不会离开自己的物体,但他们互相认识,在我的脑子里相遇后合为一体。他们其实是一个王,被遣到各处挑选赖以生存的新物质:象棋里的木王,风信鸡里的铁王,公鸡里的肉王。组成这些物体的物质,在仔细观望时发现大脑中发生迷失的起点。事物的平凡处暴露出来,物质变成了人。同类事物间出现了不同等级,我和它们之间的差距更大。我必须应对自己展开的对比,却不得不败下阵来。和木头、铁皮或羽毛相比,皮肤是最脆弱的物质,我只得依赖国王时好时坏的权力。

在他头脑中,所有事情都联系在一起对我很不利,但我脑子里联系的是其他东西:如象棋子中站着一个王,微微鞠躬,审问者体内也有个王,是杀人的王。那是我刚刚开始受审的一次,一个夏日午后,刨子幽灵也来了。窗玻璃在阳光下泛着波浪形微光,在地板洒下一圈圈白色的光环。在审问者横穿房间时,这光环爬上他的裤腿。我暗自希望他蹒跚一下,让光环爬进他的鞋,穿过脚掌将他杀死。

照片上关于我的信息很少,更多是关于母亲的。从照片可以推断出以下三种状况。第一种:头发的中缝是歪的,两条辫子在耳后高低一样。说明父亲在前一天晚上只是微醉,母亲给我编辫子时心境淡泊,想着自己的事,手指习惯性地动作。婚姻总体可以,生活还能够忍受。第二种情况:头缝和辫子歪歪扭扭,我的头看上去像被挤过,脸颊错位。这说明父亲头天晚上喝得酩酊大醉,母亲一边梳头一边流眼泪。我成了一块多余的木头,像她常说的,如果不是因为我,她早就离婚了。第三种状况:头缝和辫子都很正,左右脑和脸完全对称。说明父亲前一天晚上回家时是清醒的,母亲心情轻松愉快,也能喜欢我了。不过显示第三种情况的照片很少。因为摄影师只在节假日来,平日里,我父亲在工作时间也会喝点酒,但节假日他唯一的消遣就是大醉一场。他没有别的爱好,不像别的男人那样喜欢下棋、打牌、玩保龄球,他也不会跳舞,只是端着酒瓶站在一边看别人玩,直喝到眼睛臃肿,舌头变大,双腿发软。从我的相片也能倒推出他的三种状况,第二天通过梳齿钻进我的发型里。

沉默使我们令人不快,说话让我们变得可笑

表面看来,写作和说话很类似,但实际上,写作是一种独处。落在纸上的文字之于经历的事件,相当于沉默之于说话。我将经历转化为句子时,一个幽灵般的迁徙开始了。事实的内脏被打包进词语,学着跑步,跑向迁徙开始时还未知的目的地。为了停留于这样的意象,我在写作时,仿佛在森林里支了张床,苹果中放一把椅子,街上跑来一只手指。或者相反:手提包变得比城市还大,眼白比墙大,手表比月亮大。经历中有地点,头顶和大地之上有天空,或晴空万里或乌云密布,脚下有柏油路或地板;经历中有时间环绕,眼前是光明或者暗夜;对面有人或物。事件有开端、过程和结束;皮肤能感觉到时间的长短。所有这一切都不会因词语而发生。经历作为一个过程嘲弄写作,与词语无法兼容

人们提到“手帕”时,他们指的是哪条手帕呢?哭泣时用的手帕,不是告别时挥舞的手帕,不是包扎伤口的手帕,不是人们伤风时擦鼻子的手帕,不是打结记事的手帕,不是怕丢钱把它们包起来的手帕,也不是在街边丢失或被扔掉的手帕。手帕永远不会是同一条。在简单的一句“那女人把手帕塞进衣袋”中,会潜伏着多少种可能性?

诗歌一直向我印证,我的人生没有出路。没人能说服外公放弃去填充那些表格。直到我进了城,背诵诗歌成了一种习惯,我才终于理解,外公的发票表格不是他的祈祷,而是他的诗歌,或者说是他的大丽花。

一次触摸,两次释放

我发现,是事物决定着一个人,什么时候,以什么方式,在哪里忆起过去的人或场景。那些由坚不可摧的、没有生命因而更持久的、与我们自身完全不同的物质组成的事物,决定着它们在大脑的回归。

因此这里的人们大多以为,我们必须与现实打足够多的交道,才能真正忘记过去。而我的经验是,人们愈是认真地参与当下,过去就愈加清晰地回到我们身边。

我越是仔细观察当下,它越是急切地想成为过去的范式。我脑子里如果没有当下,也就不会拥有过去。

从她们的行为方式上,我无法判断谁更不自信,我也不知道自己在生人面前吃东西会是什么样。我仔细观察她们,是在她们处理面包屑的动作中寻找意义,还是说明我对自己缺乏了解,坐在别人对面缺乏自信,总希望在琐事中找出对与错?

红花与棍子

这个国家的年轻干部是最老的。因为他们模仿独裁者时毫不费力,比年长的人更加惟妙惟肖。当然,这是他们事业刚起步时必备的技能。后来,当了几天幼儿园教师,我才明白那不是模仿,他们其实是在扮演自己,因为除此之外他们没有属于自己的姿态动作。

每个人眼中飘落的雪花应该有不同的美,在这个国家却不能成为主题。

文明社会对个性的培养,从个体出发去理解自身及周围的事物,在这里不存在。这正是国家所需要的——软弱性格的培养要在皮肤还稚嫩的时候开始。将来要想克服自己的软弱,唯一的办法就是巴结权势,否定自我,委曲求全,惟其如此才有机会。无须逃避的自我意识,不是这个国家所需要的。

客观上不应该给三岁幼儿灌输任何个人的东西,但主观上他们有这个潜质。到了五岁,主观的也不行了,已经为时太晚。这一点一天比一天更清晰地摆在我眼前。对人类本质的滥用在内视,在贪婪地延续。毁灭在幼年业已完成。

大脑直觉产生的文字,我们自然而然地援引并说出它们,其实并不是与生俱来的。它们可以通过学习获得,也可以被阻隔。独裁统治下的社会,它在孩子们的教育中被阻隔。在成人世界,它在记忆中被剔除。

岛在内,国界在外

  • 孤独横穿日子,让生活中的一切变得毫无意义,那是我自己的疏忽和失败。
  • 然而,逃跑的愿望却越来越强烈,上升到一种歇斯底里。对毫无意义的日常生活的厌倦,变成一种病态的希望,希望通过冒险在陌生的地方创造全新的生活。逃跑意识成为伴随日常生活的本能,人们把自己的国家看成临时居住地,早晚能逃出去的信念成为他们活下去的唯一精神支柱。这造就了大量的机会主义者。一方面,在事情搞定之前不能引起别人的注意;另一方面,要努力做到事业有成:爬得越高,机会越大。利用别人对自己的依赖和影响力,利用对下级的压迫去谄媚和贿赂上级。很多干部都用“上台”作伪装准备逃亡,他们最终能够定居国外并非偶然,是长期努力的结果。他们自嘲地告诉别人,外逃是他们人生最大的奢侈,他们都曾是“具有高度社会主义觉悟”的人。众多高官逃跑后恐怕得重新定义政治觉悟这个概念了:社会主义觉悟的最高发展形式就是逃往资本主义。高干的外逃和普通老百姓绝望的逃亡不能同日而语,那是一种保险的交易,死亡风险为零。虽然大众没有这样的幸运,虽然逃离之前自由从未真正属于过他们,但是,看到高官与国家统治者背道而驰,他们还是会在一旁幸灾乐祸。
  •  

二〇二五年四月总结:倔强与坚持,向阳才能生长

时间过得很快,已经到了五月,感觉上半年很快就会结束了。春暖花儿竞相开放,一朵朵交替盛开。虽然花期短暂,但是各种花交替开放给原本暗淡的时光增添了不少色彩。

“GOOD MORNING”,是花盆的寄语,承载着绿色生命,人们为了霸占绿色生命之美,想尽办法将其移栽之室内,希望早上醒来时能即刻看到鲜活的绿色。然而不是所有的绿色生命选择妥协,宁可玉碎不求瓦全的绿色生命在这场博弈中殒命,但是他的同类在这场博弈中向阳而生,向风而立,摇曳的枝干和叶子像是对着天空说,“你好”。

公众号

上半月精力主要投入到[博客研究社]公众号,找内容方,向写文章,查看流量。然而开通了赞赏功能之后,推荐流量直接腰斩,甚至没有推荐。不确定是否跟文章赞赏有关系,好赖搜一搜有一些流量。但是针对搜一搜写文章,很容易陷入堆砌内容的死胡同,搜一搜就像SEO关键词优化。

后半月有意无意的停止了更新,没有更新的日子还有流量,也有也有用户关注。随后查看了流量分析,发现大部分文章推荐曝光量大约在210次左右,但是推荐阅读转化量不高。也有一些推荐曝光为0的文章,显然这些文章不受公众号喜欢,或是触发了隐形规则。

看书

终于把《认知觉醒:开启自我改变的原动力》看完了,有所受益。从思想上去改变,行为上自然会有所改变。这本书适合大多数人阅读,另外作者还有一本青少年学习版《认知觉醒:伴随一生的方法论》。

每月一本书,一年十本书。继续坚持,前路漫漫总有始终。谨记,书中自有黄金屋,书中自有颜如玉。

《劝学诗》
宋朝·赵恒

富家不用买良田,书中自有千钟粟。
安居不用架高堂,书中自有黄金屋。
出门无车毋须恨,书中有马多如簇。
娶妻无媒毋须恨,书中有女颜如玉。
男儿欲遂平生志,勤向窗前读六经。

骑车

总里程316.61km
总时长14:34:07
骑行均速21.73km/h
骑行次数27

很久没清洗自行车了,犄角旮旯落满了灰尘,前刹车片有点异响,像是沾染了油污。这个月变得懒惰,没有抽时间去清洗自行车。

4月骑了316公里,算不上多,距离500公里的月目标差很多。天气渐暖,应该多出去骑骑车,不仅能锻炼身体还能欣赏美景陶冶情操。

拍照

[可颂]是个非常不错的App,凭借抖音强大的社区,在可颂不仅仅可以学习拍照技巧和姿势,还能找到拍摄机位。可颂有个很赞的功能——灵感跟拍,扫描场景、AI辅助,跟着引导就能拍出很好看的照片。

索尼ZV-E10Ⅱ虽然没有吃灰,但拿出来拍照的次数极少。走在公园里拍拍花花草草,但不知道为什么总有种陌生感,也许是“摄影眼”的能力太弱,眼睛所到之处都觉得普通。

慢慢熟悉了索尼的操作方式,目前大多时候用A、S档,偶尔用用M档。令人惊喜的是微单的连拍速度真的很快,速度好比加特林。

特别声明:照片经过压缩,非原图。

  •  

家庭数据中心系列 我的博客安全性评估(上):测试工具准备篇之Kali Linux安装及初始化

家庭数据中心系列 我的博客安全性评估(上):测试工具准备篇之Kali Linux安装及初始化 无敌的个人博客 tangwudi

1 前言 这篇文章算是我兑现好多年前自己给自己下的一个目标。起因是,当时在测试公司防 D 产品时,需要写一篇常用流量模拟工具(用于模拟各种网络攻击流量)的使用手册,我就想:干脆我来写一篇吧。结果后来拖拖拉拉,最终也没写出来(当时可没有现在写博客文章的这种主动劲,都是能拖就拖,能不写就不写~)。 再加上,现在的简中互联网环境下,这类文章已经很难找到了,就算还能零零星星找到一些,很多工具的下载方式、安装方法甚至使用步骤都已经发生了变化,导致想要了解和学习的朋友不仅难以获取完整的资料,还容易被过时的信息误导。既然如此,我索性就整理一篇,把这些工具的安装、配置和基本使用方法系统地记录下来,希望能帮到有需要的人,也顺便填补这一块内容的空缺。同时,也算是给当年的自己一个交代吧。 注:说得好听,其实是升级成了Cloudflare Pro之后,我想看看WAF的托管规则和对绝对自动流量的防御效果到底如何,权 […]

<p>The post 家庭数据中心系列 我的博客安全性评估(上):测试工具准备篇之Kali Linux安装及初始化 first appeared on 无敌的个人博客.</p>

  •  

《认知觉醒》周岭:提升改变应在舒适区边缘,要觉醒先觉知

书名认知觉醒:开启自我改变的原动力
作者周岭

历时17天共10小时45分钟读完这本《认知觉醒》,现在已经记不清这本书的开头讲了什么,只觉得非常有用。

对书中改变和提升的方法大致有了了解,如果直接按照书中方法执行想必会遇到一些困难,因为改变的动力容易受环境影响,反反复复看不到正向反馈时更容易摆烂停止不前。

这本书讲的就是,开启自我改变的原动力。不能一蹴而就,那就慢慢学,慢慢改变。

大脑结构并不是我所关心的,而我希望通过这本书能切合实际的改变什么。也许是改变自己目的光短浅,也许是改变自己的行动力不足,也许是改变自己看似清醒实则迷蒙的状态。

要想觉醒先要觉知,而觉知后的觉醒需要遵循某些方法。本能脑、情绪脑、智慧脑,各有特点,如何让本能脑和情绪脑与智慧脑一同协作是个难题。

上帝视角看自己,正是第三视角,跳出自我看自己,从多个方面分析当下的行为和情绪,能更好的控制自己,找到更好的处理方法。

最近我总是在客厅里踱来踱去,无所适从,知道该干啥却没办法去做。当我用第三视角看自己,才恍然大悟。近期家里的事情比较多,没有按照计划时间推进,而影响了自己的情绪和行为,加上长辈的唠叨,使我进入恶性循环。焦虑因此生成,不知道该怎么办,又怕做的不对,更怕没有结果,进而影响到行为,逃避焦虑。

书中引入“心智带宽”的概念,对提升专注力十分有帮助。想必大家听说过这句话:“多即是少,少即是多”。虽然我们的大脑能同时处理很多事情,但是多线程的大脑更容易分心,思想抛锚正是多线程大脑带来的结果。

专注只做一件事,不仅仅可以提高效率,更能深入剖析事情的起因,问题所在,也能更好的制定策略,等等。

本能脑会让我们趋利避害,可以很好的解决眼前问题;情绪脑掌管情绪,可能会让我们失去耐心;理智脑的能量太小,很难说服本能脑和情绪脑。因此,我们的理智脑要学会使用智慧调动本能脑和情绪脑,带领他们去往更好的环境。让本能脑遇见更远的未来,让情绪脑理理解当下处境应该保持镇静还是愤怒。

《认知觉醒》插图

为了让理智脑更好的带领本能脑和情绪脑,书中提到了“舒适区”、“拉伸区”、“困难区”。在舒适区边缘刻意练习,并利用复利效应,使某些事情越做越简单,越做越得心应手。这种方法可以使本能脑和情绪脑保持良好的状态,对学习效果有明显的提升作用,也使我们学得更快。

早起、冥想、读书、写作、跑步,成本最低的成长之道。早起,实则是时间管理,将一天分成3分,早起规划一天的安排,并做一些正向引导。冥想,要善用思考,可以帮助我们找到问题的原因和答案。读书,获取知识的途径,将书中的知识与我关联,把一本书看做一个人,阅读就是与智者聊天。写作,将知识、经验转换成自己的学识和见解并输出,验证自己是否真的学会了。跑步,健康是一切的根基,没有健康都是空谈。运动后冥想、读书,有益于大脑神经元的生长。

这本书还提到了其他书籍,简单做了笔记,提升或改变自己的书单就有了。罗列一些书名《卡片笔记写作方法》、《好好学习》、《刻意学习》、《美好人生运营指南》。

  •  

家庭数据中心系列 基于 HTTP 代理的轻量旁路网关搭建:redsocks2 + iptables 实战指南

家庭数据中心系列 基于 HTTP 代理的轻量旁路网关搭建:redsocks2 + iptables 实战指南 无敌的个人博客 tangwudi

1 前言 说到我家的科学环境,在经过三次大的优化之后,结构是这样: 目前这个结构运行良好,如果内网某台设备需要具备”全局”科学的能力,只需要在爱快上的”流控分流”-“分流设置”-“端口分流”中将该设备的源IP地址关联到爱快的wan2口(连接着AC86u的lan口)即可: 甚至为了应对AC86u故障时特殊情况,还在内网PVE里部署了一个安装了opencalsh的openwrt的虚拟机来作为应急设备: 详细搭建过程参见文章:软路由系列 爱快+openwrt最佳部署方案探讨(再见吧,旁路由)。 然而,现有的方案也存在一些局限性: 依赖特定硬件的路由器 当前方案依赖爱快和 AC86u(或者openwrt)这类具备科学上网能力的路由器,而不管是爱快还是具备科学能力的路由器,拥有的家庭都是少数。因此,对于没 […]

<p>The post 家庭数据中心系列 基于 HTTP 代理的轻量旁路网关搭建:redsocks2 + iptables 实战指南 first appeared on 无敌的个人博客.</p>

  •  

家庭数据中心系列 使用Sassy Social Share插件为WordPress添加社交分享按钮

家庭数据中心系列 使用Sassy Social Share插件为WordPress添加社交分享按钮 无敌的个人博客 tangwudi

1 前言 最近在浏览一些网站的时候,看到别人都提供了分享网站内容到主流社交软件的按钮,我一琢磨,感觉这个还是有用处的:有人愿意分享你的文章,结果还需要让别人去浏览器地址栏手动复制文章的地址链接才行,说不定这一步操作就直接打消了人家愿意分享的念头呢?想了一下,感觉还是应该给我的博客也添加上主流社交媒体的分享按钮。 大概的搜索了一下,WordPress上可用的提供社交媒体分享按钮的插件很多,有些插件集成了社交媒体分享功能之外,还包含了其他一堆功能,比如 Jetpack,这个插件虽然功能丰富,但也比较臃肿,加载的资源较多,可能会影响网站的加载速度。而另一些插件则专注于社交分享功能,比如 Sassy Social Share,这类插件功能更简洁,专注于提供轻量的社交分享按钮。 由于我不愿意轻易在 WordPress 里安装插件,尤其是那些功能繁杂、可能拖慢加载速度的”重”插 […]

<p>The post 家庭数据中心系列 使用Sassy Social Share插件为WordPress添加社交分享按钮 first appeared on 无敌的个人博客.</p>

  •  

策略 II

从模仿学习到强化学习

模仿学习(IL)使用固定的专家数据进行离线学习(Offline Learning),通过行为克隆(BC)等方式模仿专家策略。其主要局限在于难以处理专家数据未覆盖的状态(OOD)

如果专家演示也有对错误状态或偏离专家轨迹情况的处理,那也能学的不错。

强化学习(RL)允许智能体与环境在线交互,通过试错和环境反馈(奖励)学习。这使得 RL 能够探索更广泛的状态空间并学习处理未知情况。

离线学习(Offline Learning):指学习过程无法干预数据的产生过程。我们只能使用一个预先收集好的、固定的数据集进行学习。模仿学习中的 BC 就是典型的离线学习。

在线学习(Online Learning):指智能体在学习过程中可以主动与环境交互,实时产生新的数据,并利用这些新数据更新自己的策略。强化学习通常可以在线进行。

与 BC 不同,RL 允许智能体与环境进行交互(从而可以探索到状态空间中更广泛的区域),可以做 Online 学习(但不是所有的 RL 算法都是 Online 的)。

强化学习基础与目标

强化学习的目标是找到一个最优策略参数 $\theta^*$,使得在该策略下产生的轨迹的期望回报最大化。即优化目标函数 $J(\theta)$:

$$ J(\theta) = \mathbb{E}{\tau \sim p\theta(\tau)} [R(\tau)] = \mathbb{E}{\tau \sim p\theta(\tau)} \left[ \sum_{t=0}^{T} r(s_t, a_t) \right] $$

这里,$p_\theta(\tau)$ 表示由策略 $\pi_\theta$ 与环境交互产生的轨迹 $\tau$ 的概率分布,这个分布由策略 $\pi_\theta$ 和环境共同决定。

由于策略和环境都可能具有随机性,单次轨迹的回报 $R(\tau)$ 可能不同。因此,我们的目标是在所有可能轨迹的分布上,最大化期望回报。我们主要关注 有限时间步(finite horizon) 的情况,即任务在 $T$ 步内完成。

策略梯度(Policy Gradient)

直接优化 $J(\theta)$ 通常很困难,因为期望的计算涉及到对所有可能轨迹的积分或求和,这在连续或高维状态动作空间中是难以处理的。

蒙特卡洛近似(Monte Carlo Approximation)

蒙特卡洛(Monte Carlo):多次采样求平均,从而近似地计算期望。

使用当前的策略 $\pi_\theta$ 与环境交互,生成 $N$ 条轨迹 $\tau^{(1)}, \tau^{(2)}, \ldots, \tau^{(N)}$。然后用这些样本的平均回报来近似期望回报:

$$ J(\theta) \approx \frac{1}{N} \sum_{i=1}^{N} R(\tau^{(i)}) = \frac{1}{N} \sum_{i=1}^{N} \sum_{t=0}^{T} r(s_t^{(i)}, a_t^{(i)}) $$

虽然我们可以近似 $J(\theta)$ 的值,但为了使用梯度上升(Gradient Ascent)方法来优化 $\theta$,我们需要计算目标函数关于参数 $\theta$ 的梯度 $\nabla_\theta J(\theta)$。

直接对蒙特卡洛近似形式求梯度是困难的,因为轨迹的生成过程 $\tau \sim p_\theta(\tau)$ 本身就依赖于 $\theta$。

策略梯度定理(Policy Gradient Theorem)

从期望的定义出发:

$$ J(\theta) = \int p_\theta(\tau) R(\tau) \mathrm{d}\tau $$

对其求梯度:

$$ \nabla_\theta J(\theta) = \nabla_\theta \int p_\theta(\tau) R(\tau) \mathrm{d}\tau = \int \nabla_\theta p_\theta(\tau) R(\tau) \mathrm{d}\tau $$

这里用到了梯度和积分可以交换顺序的假设。

引理(对数导数技巧):对于任何概率密度函数 $p_\theta(x)$,有 $\nabla_\theta p_\theta(x) = p_\theta(x) \nabla_\theta \log p_\theta(x)$。

证明:

应用链式法则于 $\log p_\theta(x)$:

$$ \begin{aligned} \nabla_\theta \log p_\theta(x) &= \left( \frac{\mathrm{d}}{\mathrm{d} p_\theta(x)} \log p_\theta(x) \right) \nabla_\theta p_\theta(x) \ &= \frac{1}{p_\theta(x)} \nabla_\theta p_\theta(x) \end{aligned} $$

这个等式成立的前提是 $p_\theta(x) > 0$。因为我们通常在概率密度函数的支撑集(support)上进行计算,这些地方的概率值是正的,所以这个假设通常是合理的。

现在,我们只需要将上式两边同时乘以 $p_\theta(x)$ 即可得到我们想要证明的公式:

$$ \begin{aligned} p_\theta(x) \nabla_\theta \log p_\theta(x) &= p_\theta(x) \left( \frac{1}{p_\theta(x)} \nabla_\theta p_\theta(x) \right) \ &= \nabla_\theta p_\theta(x) \end{aligned} $$

也即:

$$ \nabla_\theta p_\theta(x) = p_\theta(x) \nabla_\theta \log p_\theta(x) $$

将这个技巧应用于 $p_\theta(\tau)$:

$$ \nabla_\theta p_\theta(\tau) = p_\theta(\tau) \nabla_\theta \log p_\theta(\tau) $$

代入梯度表达式:

$$ \begin{aligned} \nabla_\theta J(\theta) &= \int \nabla_\theta p_\theta(\tau) R(\tau) \mathrm{d}\tau \ &= \int p_\theta(\tau) \nabla_\theta \log p_\theta(\tau) R(\tau) \mathrm{d}\tau \ &= \mathbb{E}{\tau \sim p\theta(\tau)} [\nabla_\theta \log p_\theta(\tau) R(\tau)] \end{aligned} $$

这个结果非常重要,它表明,目标函数的梯度可以表示为一个期望 (蒙特卡洛:来了嗷!)。

这意味着我们可以再次使用蒙特卡洛方法来估计这个梯度:采样 $N$ 条轨迹 $\tau^{(i)} \sim p_\theta(\tau)$,然后计算:

$$ \nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^{N} \nabla_\theta \log p_\theta(\tau^{(i)}) R(\tau^{(i)}) $$

请注意,这个梯度表达式中并没有出现奖励函数 $R(\tau)$ 关于 $\theta$ 的梯度 $\nabla_\theta R(\tau)$。

梯度是通过 $\nabla_\theta \log p_\theta(\tau)$ 传入的。这意味着强化学习不需要奖励函数本身是可导的(极其重要!!!),甚至不需要知道奖励函数的具体形式。我们只需要能够从环境中获得每个时间步的奖励值 $r(s_t, a_t)$ 即可。

这极大地扩展了强化学习的应用范围,可以处理奖励是稀疏的、非连续的 (例如,任务成功为 1,失败为 0)等复杂情况。

利用马尔科夫性:

$$ p_\theta(\tau) = p(s_0) \prod_{t=0}^{T-1} \pi_\theta(a_t | s_t) p(s_{t+1} | s_t, a_t) $$

其中:

  • $p(s_0)$ 是初始状态分布的概率
  • $\pi_\theta(a_t | s_t)$ 是策略在状态 $s_t$ 选择动作 $a_t$ 的概率
  • $p(s_{t+1} | s_t, a_t)$ 是环境的状态转移概率,即在状态 $s_t$ 执行动作 $a_t$ 后转移到状态 $s_{t+1}$ 的概率

取对数:

$$ \log p_\theta(\tau) = \log p(s_0) + \sum_{t=0}^{T-1} \left( \log \pi_\theta(a_t | s_t) + \log p(s_{t+1} | s_t, a_t) \right) $$

现在对 $\theta$ 求梯度 $\nabla_\theta$:

$$ \nabla_\theta \log p_\theta(\tau) = \nabla_\theta \log p(s_0) + \sum_{t=0}^{T-1} \left( \nabla_\theta \log \pi_\theta(a_t | s_t) + \nabla_\theta \log p(s_{t+1} | s_t, a_t) \right) $$

注意到:

  1. 初始状态分布 $p(s_0)$ 通常与策略参数 $\theta$ 无关,所以 $\nabla_\theta \log p(s_0) = 0$
  2. 环境的动态 $p(s_{t+1} | s_t, a_t)$ 描述的是环境模型中的状态转移概率,它也不依赖于我们正在学习的策略参数 $\theta$,因此 $\nabla_\theta \log p(s_{t+1} | s_t, a_t) = 0$

环境模型:包括状态转移概率 $p(s_{t+1} | s_t, a_t)$ 和奖励函数 $r(s_t, a_t)$,真实世界一般都拿不到。

  • Model-Free:我们不需要知道(甚至不需要学习)环境的模型。我们只需要能够与环境交互并从中采样即可(本课程主要是这个,在模拟器里可以随便模拟,也不需要显式建模)
  • Model-Based:会尝试利用神经网络去学习环境的模型,并利用模型进行规划或生成模拟数据(真实世界的 RL 一般需要用这个)

由此,梯度表达式简化为:

$$ \nabla_\theta \log p_\theta(\tau) = \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t | s_t) $$

所以:

$$ \begin{aligned} \nabla_\theta J(\theta) &= \mathbb{E}{\tau \sim p\theta(\tau)} [\nabla_\theta \log p_\theta(\tau) R(\tau)] \ &= \mathbb{E}{\tau \sim p\theta(\tau)} \left[ \left( \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t | s_t) \right) R(\tau) \right] \end{aligned} $$

由此,我们得到 最终的蒙特卡洛策略梯度估计

使用 $N$ 条采样轨迹 $\tau^{(1)}, \ldots, \tau^{(N)}$,其中 $\tau^{(i)} = (s_0^{(i)}, a_0^{(i)}, \ldots, s_T^{(i)}, a_T^{(i)})$ 且 $R(\tau^{(i)}) = \sum_{t=0}^{T} r(s_t^{(i)}, a_t^{(i)})$,策略梯度可以近似为:

$$ \hat{g} = \frac{1}{N} \sum_{i=1}^{N} \left[ \left( \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t^{(i)} | s_t^{(i)}) \right) R(\tau^{(i)}) \right] $$

这个估计值 $\hat{g}$ 就是我们用来更新策略参数 $\theta$ 的梯度方向。

基础策略梯度算法(REINFORCE)

基于上述推导,我们可以得到一个基础的策略梯度算法流程(REINFORCE 算法):

  1. 初始化策略参数 $\theta$(例如,随机初始化神经网络的权重)。
  2. 循环以下步骤:
    1. 使用当前的策略 $\pi_\theta$ 与环境交互,采样 $N$ 条轨迹 ${\tau^{(i)}}_{i=1}^N$。
    2. 对于每条轨迹 $\tau^{(i)}$,计算其总回报 $R(\tau^{(i)}) = \sum_{t=0}^{T} r(s_t^{(i)}, a_t^{(i)})$。
    3. 计算策略梯度估计值 $\hat{g} = \frac{1}{N} \sum_{i=1}^{N} \left[ \left( \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t^{(i)} | s_t^{(i)}) \right) R(\tau^{(i)}) \right]$。
    4. 使用梯度上升更新策略参数:$\theta \leftarrow \theta + \alpha \hat{g}$,其中 $\alpha$ 是学习率。

这个算法的直观意义是:

  • 对于回报 $R(\tau^{(i)})$ 较高的轨迹,我们会增大该轨迹中采取的动作 $a_t^{(i)}$ 在对应状态 $s_t^{(i)}$ 下被选中的概率(通过增大 $\log \pi_\theta(a_t^{(i)} | s_t^{(i)})$)
  • 对于回报较低的轨迹,则会减小其中动作被选中的概率。
  • 更新的幅度由整条轨迹的总回报 $R(\tau^{(i)})$ 来加权。

同策略(On-Policy):用于计算梯度 $\hat{g}$ 的轨迹 ${\tau^{(i)}}$ 必须是由当前正在优化的策略 $\pi_\theta$ 生成的。一旦策略参数 $\theta$ 被更新(步骤 d),之前采样得到的轨迹就不能再用于下一次的梯度计算了,因为它们是由旧策略生成的,不再符合新策略 $\pi_{\theta_{new}}$ 下的轨迹分布 $p_{\theta_{new}}(\tau)$。因此,在每次迭代中,我们都需要重新采样一批新的轨迹。

这种 On-Policy 的特性导致了策略梯度方法通常具有较高的 样本复杂度 (Sample Complexity),即需要大量的与环境交互的样本才能学习好策略,因为每次更新后数据就被丢弃了。这也是后续算法(如 PPO)试图改进的一个重要方面。

试错学习(Trial-and-Error):REINFORCE 体现了强化学习的核心思想 —— 试错。智能体尝试不同的动作,环境根据结果给出奖励。算法通过梯度更新,使得带来高奖励的动作(“好的尝试”)在未来更有可能被选中,而带来低奖励或惩罚的动作(“坏的尝试” 或 “错误”)则被抑制。

这个过程就像学习骑自行车,通过不断尝试和调整,逐渐学会保持平衡(获得 “不摔倒” 这个隐含的高奖励)。

策略梯度与行为克隆的对比

策略梯度(Policy Gradient, PG)方法和行为克隆(Behavior Cloning, BC)都是学习一个从状态 $s$ 到动作 $a$ 的映射(策略 $\pi_\theta(a|s)$),通常使用神经网络作为参数化模型 $\theta$。然而,它们的学习目标和更新规则有本质区别。

行为克隆的目标是最大化专家演示数据 $D_{expert} = {(s_i, a_i)}$ 的对数似然,可以通过蒙特卡洛估计来近似:

$$ \begin{aligned} \arg \max_\theta J_{BC}(\theta) &= \sum_{(s, a) \in D_{expert}} \log \pi_\theta(a|s) \ &\approx \arg \max_\theta \frac{1}{N} \sum_{i=1}^{N} \left[ \sum_{t=0}^{T-1} \log \pi_\theta(a_i^{(t)}|s_i^{(t)}) \right] \end{aligned} $$

其梯度为:

$$ \begin{aligned} \nabla_\theta J_{BC}(\theta) &= \sum_{(s, a) \in D_{expert}} \nabla_\theta \log \pi_\theta(a|s) \ & \approx \frac{1}{N} \sum_{i=1}^{N} \left[ \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_i^{(t)}|s_i^{(t)}) \right] \end{aligned} $$

行为克隆试图让策略网络在专家访问过的状态 $s$ 下,输出专家采取的动作 $a$ 的概率尽可能高。它假设专家演示中的所有状态 - 动作对都是最优且等价重要的

策略梯度的目标是最大化期望回报 $J(\theta) = \mathbb{E}{\tau \sim p\theta(\tau)} [R(\tau)]$,其梯度(使用蒙特卡洛估计)为:

$$ \nabla_\theta J(\theta) \approx \hat{g} = \frac{1}{N} \sum_{i=1}^{N} \left[ \left( \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t^{(i)} | s_t^{(i)}) \right) R(\tau^{(i)}) \right] $$

策略梯度也通过 $\nabla_\theta \log \pi_\theta(a_t | s_t)$ 项来调整动作的概率,但它引入了一个关键的权重因子:整条轨迹的回报 $R(\tau^{(i)})$

行为克隆可以看作是策略梯度的一种特殊情况,即假设所有演示轨迹的回报 $R(\tau)$ 都等于 1(或者某个常数)。它平等地对待演示数据中的每一个动作,试图无差别地模仿。

策略梯度则根据动作实际带来的结果也即 $R(\tau)$ 来调整策略。

  • 回报高的轨迹中的 $(s_t, a_t)$ 对会被赋予更大的权重,使得这些 “好” 动作的概率增加
  • 回报低的(甚至可能是负回报的)轨迹中的 $(s_t, a_t)$ 对会被赋予较小的(或负的)权重,使得这些 “坏” 动作的概率降低。

行为克隆的问题:由于无差别模仿,行为克隆会学习演示数据中的所有行为,包括专家可能存在的噪声、次优动作或不必要的习惯(例如演示者操作时手部的轻微抖动)。它无法区分哪些动作对于完成任务是关键的,哪些是无关紧要甚至有害的。

此外,如果演示数据过于 “完美”,只包含最优轨迹,那么策略在遇到训练时从未见过的、略微偏离的状态时,可能会因为缺乏相应的纠错经验而表现很差(Distribution Shift)。

如果你想让 BC 足够好:

  1. 正确覆盖所有的完美轨迹,且你训练的模型能够正确地 follow 这些轨迹
  2. 对各种 error 的 corner case 都有拽回来的部分覆盖,但不要有导致 error 发生的部分
  3. 省流就是尽最大可能避免与真实世界的 Distribution Shift

显然这比较困难。

  • BC:不断调 Demenstration,尝试满足上述条件
  • RL:不断地在环境中尝试

策略梯度(REINFORCE)的挑战

基础的策略梯度算法(REINFORCE)虽然原理简洁且不依赖模型和可导奖励,但在实际应用中面临严峻挑战:

高方差(High Variance)/ 嘈杂(Noisy)

蒙特卡洛方法通过采样 $N$ 条轨迹来估计梯度 $\nabla_\theta J(\theta)$。然而,由于环境和策略的随机性,单条轨迹的回报 $R(\tau^{(i)})$ 可能有很大波动。尤其是在复杂任务和长时序(large $T$)问题中,轨迹空间极其巨大,有限的 $N$ 条样本可能远不足以精确估计期望梯度。

这导致每次计算出的梯度估计值 $\hat{g}$ 噪声很大,围绕真实梯度方向剧烈波动。虽然理论上这个估计是 无偏 的(当 $N \to \infty$ 时收敛到真值),但在 $N$ 有限时,高方差会使得训练过程不稳定,收敛缓慢,甚至可能发散。

更直白的讲,梯度估计的随机性大,会导致即使使用相同的超参数,仅因采样轨迹不同,多次训练的结果(性能、学习曲线)也可能差异巨大,缺乏稳定性。这与结果通常更一致的监督学习不同,导致需要进行大 Batch Size 以及对超参数的充分试错。

样本效率低下(Low Sample Efficiency)

REINFORCE 是 On-Policy (同策略)算法。一旦策略参数 $\theta$ 更新,之前采集的数据就 “过时” 了,不能用于下一次梯度计算。这导致算法需要大量的交互样本才能学习,尤其对于交互成本高昂的环境(如真实机器人),这种样本效率是难以接受的。

On-Policy 与 Off-Policy 学习

On-Policy 和 Off-Policy 都属于 Online Learning,因为你需要持续地和环境交互,然后根据交互数据来更新策略。

  • On-Policy(同策略):学习算法使用的数据必须由当前正在优化的策略产生。每次策略更新后,旧数据失效。
    • 例如:REINFORCE、SARSA
    • 通常效果更好,直接优化当前策略的表现
    • 样本效率低 (贵)
  • Off-Policy(异策略):学习算法可以使用由不同策略(例如过去的策略、专家策略或其他探索策略)产生的数据。通常会使用重要性采样(Importance Sampling)等技术来修正数据分布不匹配的问题。
    • 例如:Q-Learning、DDPG、SAC
    • 样本效率高,可以利用历史数据(通常存储在 Replay Buffer 中)
    • 缺点是效果不一定好,优化目标与数据生成分布不一致可能导致问题(老是去学以前已经改正的)

高斯策略(Gaussian Policy)

随机策略(stochastic policy):输出的是一个概率分布而不是一个确定的动作。

高斯策略:实际执行的动作 $a_t$ 则从一个以 $\mu_\theta(s_t) = f(s_t)$ 为均值、协方差矩阵为 $\Sigma$ 的高斯分布中采样得到:

$$ \pi_\theta(a_t|s_t) = \mathcal{N}(\mu_\theta(s_t); \Sigma) = \mathcal{N}(f(s_t); \Sigma) $$

我们约定,$k$ 是动作空间的维度,$p$ 是参数的维度。

对于多元高斯分布,其概率密度函数的对数为:

$$ \begin{aligned} \log \pi_\theta(a_t|s_t) &= -\frac{1}{2} (a_t - \mu_\theta(s_t))^\top \Sigma^{-1} (a_t - \mu_\theta(s_t)) - \frac{k}{2}\log(2\pi) - \frac{1}{2}\log|\det(\Sigma)| \ &= -\frac{1}{2} | \mu_\theta(s_t) - a_t |^2_{\Sigma} + \text{const} \end{aligned} $$

$$ \nabla_\theta \log \pi_\theta(a_t|s_t) = \left(\frac{\partial \mu_\theta(s_t)}{\partial \theta}\right)^\top \Sigma^{-1} (a_t - \mu_\theta(s_t)) $$

其中,$| \mathbf{x} |^2_{\Sigma} = \mathbf{x}^\top \Sigma^{-1} \mathbf{x}$。如果协方差矩阵 $\Sigma$ 是一个对角矩阵,并且所有对角线元素都相等,即 $\Sigma = \sigma^2 I$,那结果就是 L2。

证明:

引理:

  1. 链式法则:令 $\mathbf{y}(\theta) = f(\mathbf{s}t) - \mathbf{a}t$,$g(\mathbf{y}) = \mathbf{y}^\top \Sigma^{-1} \mathbf{y}$,则 $\nabla\theta g(\mathbf{y}(\theta)) = \left(\frac{\partial \mathbf{y}}{\partial \theta}\right)^\top \nabla\mathbf{y} g(\mathbf{y})$
  2. 对于对称矩阵 $A$,$\nabla_\mathbf{x} (\mathbf{x}^\top A \mathbf{x}) = 2 A \mathbf{x}$。

所以,

$$ \begin{aligned} \nabla_\theta \log \pi_\theta(a_t|s_t) &= \nabla_\theta \left( -\frac{1}{2} (a_t - \mu_\theta(s_t))^\top \Sigma^{-1} (a_t - \mu_\theta(s_t)) \right) \ &= -\frac{1}{2} \nabla_\theta \left( (\mu_\theta(s_t) - a_t)^\top \Sigma^{-1} (\mu_\theta(s_t) - a_t) \right) \ &= -\frac{1}{2} \nabla_\theta \left( \mathbf{y}(\theta)^\top \Sigma^{-1} \mathbf{y}(\theta) \right) \quad (\text{令 } \mathbf{y}(\theta) = \mu_\theta(s_t) - a_t) \ &= -\frac{1}{2} \left(\frac{\partial \mathbf{y}}{\partial \theta}\right)^\top (\nabla_\mathbf{y} (\mathbf{y}^\top \Sigma^{-1} \mathbf{y})) \quad (\text{应用链式法则}) \ &= -\frac{1}{2} \left(\frac{\partial (\mu_\theta(s_t) - a_t)}{\partial \theta}\right)^\top (2 \Sigma^{-1} \mathbf{y}) \quad (\text{应用引理 2}) \ &= -\frac{1}{2} \left(\frac{\partial \mu_\theta(s_t)}{\partial \theta}\right)^\top (2 \Sigma^{-1} (\mu_\theta(s_t) - a_t)) \ &= - \left(\frac{\partial \mu_\theta(s_t)}{\partial \theta}\right)^\top \Sigma^{-1} (\mu_\theta(s_t) - a_t) \ &= \left(\frac{\partial \mu_\theta(s_t)}{\partial \theta}\right)^\top \Sigma^{-1} (a_t - \mu_\theta(s_t)) \end{aligned} $$

部分可观测性(Partial Observability)

在许多现实场景中,智能体无法获取环境的完整状态 $s_t$,只能得到一个观测值 $o_t$(例如,来自摄像头的图像)。这种情况被称为部分可观测马尔可夫决策过程(Partially Observable Markov Decision Process, POMDP)。此时,策略变为基于观测值的 $\pi_\theta(a_t|o_t)$。

一个重要的结论是:即使在部分可观测的情况下,策略梯度的基本形式依然成立。我们可以将推导过程中的 $s_t$ 替换为 $o_t$,得到:

$$ \nabla_\theta J(\theta) = \mathbb{E}{\tau \sim p\theta(\tau)} \left[ \left( \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t | o_t) \right) R(\tau) \right] $$

其中 $\tau = (o_0, a_0, o_1, a_1, \ldots)$。这是因为策略梯度的推导并不依赖于状态的马尔可夫性质。

注意:虽然公式形式不变,但策略的学习效果现在受限于观测 $o_t$ 所包含的信息量。如果 $o_t$ 缺失了做出最优决策所必需的关键状态信息,那么即使使用策略梯度,也无法学到最优策略

在这种情况下,一种常用的方法是 利用历史信息,例如使用循环神经网络(RNN)作为策略网络,输入 $o_t$ 和之前的隐藏状态,以捕捉时间上的依赖关系。

降低策略梯度方差的技术

为了缓解 REINFORCE 的高方差问题,可以采用以下技巧:

奖励转置(Reward-to-Go)

原始的 REINFORCE 算法中,在计算 $t$ 时刻的梯度项 $\nabla_\theta \log \pi_\theta(a_t | s_t)$ 时,使用了整条轨迹的总回报 $R(\tau) = \sum_{t'=0}^{T} r_{t'}$ 作为权重。

思考:在 $t$ 时刻采取的动作 $a_t$ 只能影响从 $t$ 时刻及之后获得的奖励 $(r_t, r_{t+1}, \ldots, r_T)$,而无法影响 $t$ 时刻之前的奖励 $(r_0, \ldots, r_{t-1})$。因此,将过去的奖励也包含在权重中,引入了与当前决策无关的噪声。

改进:只使用从当前时刻 $t$ 开始到轨迹结束的累积奖励,即 奖励转置(Reward-to-Go),作为权重:

$$ \hat{Q}(s_t, a_t) = \sum_{t'=t}^{T} r(s_{t'}, a_{t'}) $$

修改后的策略梯度估计变为:

$$ \hat{g}{rtg} = \frac{1}{N} \sum{i=1}^{N} \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t^{(i)} | s_t^{(i)}) \hat{Q}(s_t^{(i)}, a_t^{(i)}) $$

这种方法考虑了动作的因果影响,即一个动作只对未来的奖励负责

理论上可以证明,使用 Reward-to-Go 仍然是 $\nabla_\theta J(\theta)$ 的无偏估计,并且通常具有比使用总回报 $R(\tau)$ 更低的方差。

基线(Baseline)

另一个问题是,策略梯度对奖励的绝对值敏感。如果所有轨迹的回报都是正的(即使有好有坏),那么所有动作都会在一定程度上被 “鼓励”(梯度项为正)。我们更希望的是:比平均水平好的动作被鼓励,比平均水平差的动作被抑制。这可以同时降低方差,增强训练稳定性。

思路:从回报项中减去一个只依赖于状态 $s_t$ 的基线 $b(s_t)$。这个基线不依赖于具体采取的动作 $a_t$。

$$ \nabla_\theta J(\theta) = \mathbb{E}{\tau \sim p\theta(\tau)} \left[ \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t | s_t) (\hat{Q}(s_t, a_t) - b(s_t)) \right] $$

可以证明,只要基线 $b(s_t)$ 不依赖于动作 $a_t$,减去它不会改变梯度的期望值(即估计仍然是无偏的),也即:

$$ \mathbb{E}{a_t \sim \pi\theta(\cdot|s_t)}[\nabla_\theta \log \pi_\theta(a_t|s_t) b(s_t)] = 0 $$

证明:

$$ \begin{aligned} \mathbb{E}{a_t \sim \pi\theta(\cdot|s_t)}[\nabla_\theta \log \pi_\theta(a_t|s_t) b(s_t)] &= b(s_t) \mathbb{E}{a_t \sim \pi\theta(\cdot|s_t)}[\nabla_\theta \log \pi_\theta(a_t|s_t)] \ &= b(s_t) \int \pi_\theta(a_t|s_t) \nabla_\theta \log \pi_\theta(a_t|s_t) \mathrm{d}a_t & & \text{(期望定义)} \ &= b(s_t) \int \pi_\theta(a_t|s_t) \frac{\nabla_\theta \pi_\theta(a_t|s_t)}{\pi_\theta(a_t|s_t)} \mathrm{d}a_t & & \text{(对数导数技巧)} \ &= b(s_t) \int \nabla_\theta \pi_\theta(a_t|s_t) \mathrm{d}a_t \ &= b(s_t) \nabla_\theta \int \pi_\theta(a_t|s_t) \mathrm{d}a_t \ &= b(s_t) \nabla_\theta (1) & & \text{(概率密度积分为 1)} \ &= b(s_t) \times 0 \ &= 0 \end{aligned} $$

目标:选择合适的基线 $b(s_t)$ 来最小化梯度估计的方差。

最优基线:虽然减去任何有效的基线都不会引入偏差,但不同的基线对降低方差的效果不同。最优的基线通常难以计算。

证明:我们可以分析梯度估计的方差。

令 $g(\tau, b) = \nabla_\theta \log p_\theta(\tau) (R(\tau) - b)$。

$$ \mathrm{Var}[g(\tau, b)] = \mathbb{E}[g(\tau, b)^2] - (\mathbb{E}[g(\tau, b)])^2 $$

由于 $\mathbb{E}[g(\tau, b)] = \mathbb{E}[\nabla_\theta \log p_\theta(\tau) R(\tau)]$(因为基线项期望为 0),它不依赖于 $b$。因此,最小化方差等价于最小化 $\mathbb{E}[g(\tau, b)^2]$:

$$ \mathbb{E}[g(\tau, b)^2] = \mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2 (R(\tau) - b)^2] $$

对 $b$ 求导并令其为 0:

$$ \frac{\mathrm{d}}{\mathrm{d}b} \mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2 (R(\tau) - b)^2] = \mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2 \times 2(R(\tau) - b) \times (-1)] = 0 $$

$$ \mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2 (R(\tau) - b)] = 0 $$

$$ \mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2 R(\tau)] = b , \mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2] $$

解出最优基线 $b^*$:

$$ b^* = \frac{\mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2 R(\tau)]}{\mathbb{E}[(\nabla_\theta \log p_\theta(\tau))^2]} $$

这个最优基线 $b^*$ 可以看作是回报 $R(\tau)$ 的期望,但使用梯度幅度的平方 $(\nabla_\theta \log p_\theta(\tau))^2$ 进行了加权。

采样均值基线

$$ b = \frac{1}{N} \sum_{i=1}^N R(\tau^{(i)}) $$

这里也可以使用平均 Reward-to-Go 作为基线。

这虽然不是最优的,但通常也能提供不错的方差降低效果。

注意,如果使用蒙特卡洛算法,不同的 $b$ 的选择的确会影响采样计算出的 $\nabla_\theta J(\theta)$ 近似值,但是这是由于采样不足,$N$ 不够大造成的。

状态价值函数基线

状态价值函数 $V^{\pi_\theta}(s_t)$:表示从状态 $s_t$ 开始,遵循策略 $\pi_\theta$ 之后所能获得的期望(折扣)Reward-to-Go 回报,它只依赖于状态 $s_t$ 和策略 $\pi_\theta$。

$$ V^{\pi_\theta}(s_t) = \mathbb{E}{\tau \sim p\theta(\tau)} \left[ \sum_{t'=t}^{T} \gamma^{t'-t} r_{t'} \middle| s_t \right] = \mathbb{E}{a_t \sim \pi\theta(\cdot|s_t)} [Q^{\pi_\theta}(s_t, a_t)] $$

动作价值函数 $Q^{\pi_\theta}(s_t, a_t)$:表示在状态 $s_t$ 采取动作 $a_t$ 后,再遵循策略 $\pi_\theta$ 所能获得的期望(折扣)Reward-to-Go 回报,它依赖于状态 $s_t$、动作 $a_t$ 和策略 $\pi_\theta$。

$$ Q^{\pi_\theta}(s_t, a_t) = \mathbb{E}{\tau \sim p\theta(\tau)} \left[ \sum_{t'=t}^{T} \gamma^{t'-t} r_{t'} \middle| s_t, a_t \right] = r(s_t, a_t) + \gamma \mathbb{E}{s{t+1} \sim P(\cdot|s_t, a_t)} [V^{\pi_\theta}(s_{t+1})] $$

优势函数(Advantage Function) $A^{\pi_\theta}(s_t, a_t)$:在状态 $s_t$ 采取特定动作 $a_t$ 相对于平均动作(也就是 $V^{\pi_\theta}(s_t)$ 作为基线)的好坏程度

$$ \begin{aligned} A^{\pi_\theta}(s_t, a_t) &= Q^{\pi_\theta}(s_t, a_t) - V^{\pi_\theta}(s_t) \ &= r(s_t, a_t) + \gamma \mathbb{E}{s{t+1} \sim P(\cdot|s_t, a_t)} [V^{\pi_\theta}(s_{t+1})] - V^{\pi_\theta}(s_t) \end{aligned} $$

这里引入了折扣因子 $\gamma \in [0, 1)$,它的作用是:

  1. 确保在无限时间步长问题中,累积回报是有限的。
  2. 表示对未来奖励的不确定性或对即时奖励的偏好。$\gamma$ 越小,越看重眼前的奖励。
  3. 隐式地鼓励尽早完成任务:因为越往后的奖励会被 $\gamma$ 折扣得越多,所以总回报最高的方式通常是尽快获得奖励。

现在,策略梯度现在可以写为:

$$ \nabla_\theta J(\theta) = \mathbb{E}{(s_t, a_t) \sim \pi\theta} [ \nabla_\theta \log \pi_\theta(a_t | s_t) A^{\pi_\theta}(s_t, a_t) ] $$

使用 $V(s_t)$ 作为基线后,权重项变为:

$$ \begin{aligned} \hat{A}(s_t, a_t) &= \hat{Q}(s_t, a_t) - \hat{V}(s_t) \ &= r(s_t, a_t) + \gamma \hat{V}(s_{t+1}) - \hat{V}(s_t) \ \end{aligned} $$

这里直接暴力地对期望 $\mathbb{E}{s{t+1} \sim P(\cdot|s_t, a_t)} [V^{\pi_\theta}(s_{t+1})]$ 进行蒙特卡洛估计。

$\hat{A}(s_t, a_t)$ 是优势函数的估计值。

  • $\hat{A}(s_t, a_t) > 0$:动作 $a_t$ 比平均表现要好,应该增加其概率
  • $\hat{A}(s_t, a_t) < 0$:动作 $a_t$ 比平均表现要差,应该降低其概率

估计 $V(s_t)$ 的方法

蒙特卡洛

计算在所有 $N$ 条轨迹中经过状态 $s_t$ 的样本的平均 Reward-to-Go 回报:

$$ \hat{V}(s_t) = \frac{1}{N} \sum_{i=1}^{N} \sum_{t'=t}^{T} \gamma^{t' - t} r(s_{t'}, a_{t'}) $$

神经网络

使用另一个神经网络(称为 Critic)来学习并预测 $V(s_t)$:

$$ \hat{V}(s) = \hat{V}_{\phi}(s) $$

不要被形式迷惑,这里就是要设法学一个 $s_t$ 的值函数。

所以,我们可以准备数据集:

$$ \mathcal{D} = { (s_{i,t}, \underbrace{r(s_{i,t}, a_{i,t}) + \gamma \hat{V}{\phi}^{\pi}(s{i,t+1})}{y{i,t}}) } $$

其中,$s_{i,t}$ 是在第 $i$ 条轨迹、时刻 $t$ 遇到的状态。

然后,使用神经网络来监督学习就行。

自举(Bootstrap):使用了一个基于当前函数估计的值 $\hat{V}{\phi}^{\pi}(s{i,t+1})$ 来更新同一个函数在另一个点 $s_{i,t}$ 的估计 $\hat{V}{\phi}^{\pi}(s{i,t})$。

关于自举有一个很形象的例子:在河里拽自己的鞋带把自己拽起来。

Actor-Critic

重新回顾 “基线” 这一概念,再结合使用神经网络来估计 $V(s_t)$ 的方法以及策略梯度的公式:

$$ \nabla_\theta J(\theta) = \mathbb{E}{(s_t, a_t) \sim \pi\theta} [ \nabla_\theta \log \pi_\theta(a_t | s_t) A^{\pi_\theta}(s_t, a_t) ] $$

我们就可以很自然的想到 Actor-Critic 方法。

  • Actor(演员):指策略网络 $\pi_\theta(a_t|s_t)$,负责根据状态 $s_t$ 做出动作决策,决定此步的 $r(s_t, a_t)$ 进而影响 $A(s_t, a_t)$
  • Critic(评论家):指价值网络($V_{\phi}(s_t)$ 或者 $Q_{\phi}(s_t, a_t)$,$\phi$ 表示其参数),负责评估 Actor 所处的状态 $s_t$ 或采取的动作 $a_t$ 的好坏(即估计 $V$ 值或 $Q$ 值,进而计算优势 $A$ 值)

在训练完成后,真正推理(干活)的时候,不用 Critic,只用 Actor。

Batch Actor-Critic

循环:

  1. 收集一批完整的轨迹数据
  2. 用这批数据一次性或多次迭代地更新 Critic $\hat{V}_\phi^\pi$(拟合蒙特卡洛回报或 TD 目标)
  3. 用更新后的 Critic 计算整批数据的优势: $$ \hat{A}^\pi(s_t, a_t) = r(s_t, a_t) + \gamma \hat{V}\phi^\pi(s{t+1}) - \hat{V}_\phi^\pi(s_t) $$
  4. 计算整批数据的平均策略梯度: $$ \nabla_\theta J(\theta) = \mathbb{E}{(s_t, a_t) \sim \pi\theta} [ \nabla_\theta \log \pi_\theta(a_t | s_t) \hat{A}^\pi(s_t, a_t) ] $$
  5. 更新 Actor: $$ \theta \leftarrow \theta + \alpha \nabla_\theta J(\theta) $$

Online Actor-Critic

循环:

  1. 在当前状态 $s$,根据策略选择动作 $a \sim \pi_\theta(a|s)$
  2. 执行动作 $a$,观察到奖励 $r$ 和下一个状态 $s'$ 获得一个转换 $(s, a, r, s')$
  3. 立即使用这个转换来更新 Critic $\hat{V}\phi^\pi$(通常使用 TD 目标 $\delta$) $$ \delta = r + \gamma \hat{V}\phi^\pi(s') - \hat{V}\phi^\pi(s) \ L(\phi) \doteq \frac{1}{2} \delta^2 = \frac{1}{2} \left( (r + \gamma \hat{V}\phi^\pi(s')) - \hat{V}\phi^\pi(s) \right)^2 \ \nabla\phi L(\phi) = \frac{\partial L(\phi)}{\partial \delta} \frac{\partial \delta}{\partial \hat{V}\phi^\pi(s)} \nabla\phi \hat{V}\phi^\pi(s) = - \delta \nabla\phi \hat{V}\phi^\pi(s) \ \hat{V}\phi^\pi(s) \leftarrow \hat{V}\phi^\pi(s) + \beta \nabla\phi L(\phi) $$
  4. 立即计算优势函数的估计值,通常就是 TD 误差本身: $$ \hat{A}^\pi(s, a) = \delta = r + \gamma \hat{V}\phi^\pi(s') - \hat{V}\phi^\pi(s) $$
  5. 立即更新 Actor: $$ \theta \leftarrow \theta + \alpha \nabla_\theta J(\theta) \approx \theta + \alpha \nabla_\theta \log \pi_\theta(a|s) \hat{A}^\pi(s, a) $$

Online vs. Batch

  • Online:更新更频繁(每一步都可能更新),数据利用率可能更高(效率高),能适应非平稳环境但单步更新可能带来高方差
  • Batch:更新基于更多数据(如走完一整条轨迹才更新),梯度估计更稳定(方差较低)但需要存储更多数据,更新频率较低

网络架构

ac_arch

  • 分离网络:Actor 和 Critic 使用独立的神经网络。简单稳定,但无特征共享。
  • 共享网络:Actor 和 Critic 共享部分底层网络。参数效率高,但训练可能更复杂。

同步 / 异步

parallel

即使在 Online AC 中,也常常收集一个小批量数据来更新 Critic $\hat{V}_\phi^\pi$ 和 Actor $\theta$,因为这有助于稳定学习过程,降低梯度估计的方差。

并行化(Parallelization):使用多个并行的 Actor(workers)同时在环境中收集经验,可以显著提高数据采集速度和多样性,进一步稳定训练。

并行又可分为同步(Synchronous)和异步(Asynchronous)。同步并行存在同步点,整体速度受限于最慢的 worker。异步并行则没有同步点,会更快。

💾

  •  

策略 I

条件抓取生成模型(Conditional Grasp Generative Model)

问题定义与挑战

目标:在一个包含多个物体的杂乱场景(例如,一个箱子里的物品)中,规划灵巧手的抓取动作。

核心挑战:

  1. 避免碰撞:抓取目标物体的同时,要尽量避免与场景中的其他物体或环境发生不必要的碰撞。
  2. 泛化性:模型需要能够泛化到新的物体(不同的几何形状)和新的场景布局(不同的物体分布)。

与单物体抓取的区别:相比于抓取一个孤立的物体,杂乱场景中的抓取规划要复杂得多,因为它需要同时考虑物体间的相互作用和潜在的碰撞。

进阶问题:有些研究会先通过 非抓握操作(Non-prehensile Manipulation,如推、拨) 将目标物体分离出来,简化后续抓取。

DexGraspNet

核心是利用大规模 合成数据(Synthetic Data) 进行训练,并通过深度学习模型来学习抓取策略。

dex_grasp_net

场景理解模块(Scene Understanding)

输入:场景的点云数据。

任务:

  • 预测场景中每个点的 抓取可能性(Graspness):哪些区域适合进行抓取。
  • 区分前景 物体(Objectness) 与背景(如桌面)。

方法:使用一个点云处理网络(如基于稀疏卷积的网络)进行监督学习,标签(Graspness, Objectness)从合成数据中自动生成(由合成数据提供监督信号)。

局部区域提议与特征提取(Local Region Proposal & Feature Extraction)

动机:直接使用整个场景的全局特征(Global Feature)来指导抓取生成存在困难

  • 弱关联性:全局特征与特定位置的抓取动作之间的关联可能不够强,导致以之为条件的条件生成模型学习效果不佳,甚至退化为无条件生成

    老师提到,conditon 最好要和输出结果有很强的 correlation,这样效果更好,且更好建模、泛化。

  • 泛化性差:新场景的全局特征可能与训练数据差异巨大,导致模型难以迁移。

方法:

  • 根据第一步预测的 Graspness Score,选择得分最高的点 (例如 Top 1%)。
  • 围绕这些高分点,提取局部区域(Local Region)的点云。
  • 从这些局部点云区域中提取局部特征(Local Feature)。这些局部特征(如平坦表面、边缘、角落等 几何信息 )在不同场景中更可能重复出现,有助于提升泛化性。

条件抓取生成模块(Conditional Grasp Generation)

输入:上一步提取的局部特征。

任务:生成有效的抓取位姿,包括末端执行器的 6D 位姿(位置 $T$ 和姿态 $R$)以及手的形态(手指配置 $\theta$)。

面临的挑战:抓取的 多模态性(Multi-modality)。对于同一个物体或区域,通常存在多种有效的抓取方式(多峰分布)。如果直接使用回归(Regression)预测单一抓取,模型倾向于输出所有可能抓取的 “平均值”,而这个平均抓取往往是无效的(Mode Average 问题)。

开车避障时,你可以选择左打方向盘或者右打方向盘,但模型为了降低 Loss,会输出平均值 —— 啥都不动,直直撞上去。

解决方案:解耦建模,将抓取生成分解为两个步骤。

  1. 建模末端 6D 位姿的分布:认为末端位姿 $(T, R)$ 的选择具有明显的 多模态特性

    所以,使用一个 条件生成模型 (如 Diffusion Model)来学习在给定局部特征条件下的 位姿分布 $p(T, R | \text{local_feature})$,并从中采样得到 $(T,R)$

  2. 预测手型:假设当末端位姿 $(T, R)$ 固定后,最优的手指形态 $\theta$ 的不确定性大大降低(近似单峰分布)。

    因此,可以使用一个 回归模型,根据采样得到的 $(T, R)$ 和局部特征来预测手型 $\theta = f(T, R, \text{local_feature})$。

生成过程:先从学习到的分布 $p(T, R | \text{local_feature})$ 中采样一个或多个候选的末端位姿 $(T, R)$,然后对每个采样出的位姿预测对应的手型 $\theta$。

实验结果与分析

消融实验证实,使用局部特征作为条件相比于使用全局特征,抓取成功率有显著提升,这验证了局部特征在增强关联性和泛化性方面的关键作用。

Scaling Law:抓取性能与训练数据的规模(抓取样本数量和场景多样性)显著相关,使用合成数据可以大幅提升成功率(10 万~ 1000 万),但存在边界收益递减的问题。

优点

  1. 有效的数据合成管线
  2. 设计了一个 端到端 的框架

局限性

  1. 抓取类型:仅处理包覆式抓取(Power Grasp),没处理指尖抓取(Precision Grasp),如用指尖捏取小物体
  2. 抓取闭合类型:主要使用力封闭抓取(Force-Closure Grasp),但是存在非力封闭的场景(如托起物体)

透明与高反光物体

问题概述

尽管像 GraspNet 这样的方法在许多物体上表现良好,但它们在处理透明(Transparent)或高反光(Highly Specular/Shiny)物体时会遇到巨大挑战。

主要原因在于,目前商用的深度传感器(Commercial Depth Sensor),如基于飞行时间(Time-of-Flight, ToF)或结构光(Structured Light)的传感器,其工作原理依赖于对光线传播的特定假设,例如:

  • 它们假设光线照射到物体表面后会直接反射回来。
  • 结构光方法假设投射的特定光图案(Pattern)在物体表面会发生可预测的漫反射(Diffuse Reflection),通过观察反射图案的变形来推算深度。

然而,对于透明物体,大部分光线会发生折射(Refraction)并穿透物体,而不是反射。对于高反光物体,光线会发生镜面反射(Specular Reflection),形成高光区域,这与传感器通常假设的漫反射模型不符。

这些问题会 导致点云的质量(quality)变差,所以在深度传感器看来,透明或高反光物体的几何结构往往是残缺不全的。

transparent_and_shiny

由于输入的点云质量低下,依赖于几何信息进行抓取规划的方法自然难以有效工作。

ASGrasp

asgrasp

核心目标:深度修复,获得高质量的深度信息。

ASGrasp 采用基于学习的深度感知(Learning-based Depth Sensing)方法,而不依赖固定物理模型的传统方法。

  1. 合成数据驱动:利用图形学渲染技术生成大量的合成数据。每条数据包含一个渲染出的场景图像(RGB Image)和与之对应的 “完美” 深度图或点云(Ground Truth Depth/Point Cloud)。
  2. 监督学习:将(图像,真实深度)作为 配对的监督信号,训练一个深度学习网络。这个网络学习从输入的(可能有问题的)传感器图像直接预测出准确的深度信息 $f: \text{Image} \rightarrow \text{Depth}$。

显然,依赖于合成数据的方法主要挑战在 泛化性(Generalization) 问题。

为了解决泛化性问题,合成数据必须具有足够的 多样性(Diversity)

域随机化(Domain Randomization):在生成合成数据时,尽可能地随机化各种环境和物体参数,使得训练数据覆盖足够广泛的分布,防止对特定条件产生过拟合(overfit),从而让模型对真实世界中未曾见过的变化更具鲁棒性。

域随机化的方面包括:

  1. 物体和布局(Objects and Layout)
  2. 材质(Materials)
  3. 背景(Backgrounds)
  4. 光照(Illumination)
  5. 相机视角(Camera Viewpoints)

除了使用多样化合成数据进行训练。ASGrasp 另一核心是 多模态立体视觉 (Multi-modal Stereo Vision)方案,它同时利用了红外图像(Infrared,IR)和彩色图像(RGB)来估计深度。且其中使用了类似双目视觉的 立体匹配(Stereo Matching) 方法来得到深度信息。

ASGrasp 的独特之处在于其 混合匹配策略

  1. 它首先利用 红外图像对(IR Image Pair) 进行双目立体匹配,红外成像对于某些在可见光下难以处理的材质(如透明、高反光)可能提供更稳定的特征。
  2. 同时,它将 彩色图像(RGB Image) 作为 额外的上下文信息(Additional Context) 融入匹配过程。RGB 图像提供了丰富的颜色和纹理信息,可以帮助消除歧义(disambiguate),或者在 IR 信息不足时提供补充。

在网络结构层面,ASGrasp 采用了在立体匹配领域常见的技术:

  1. 相关性金字塔 / 代价体(Correlation Pyramid / Cost Volume):编码不同视差下的匹配代价。
  2. 由粗到精(Coarse-to-Fine)的优化策略:逐步细化深度图,提高精度。

通过这种方式,ASGrasp 能够生成高质量的深度图,尤其是在处理传统方法难以应对的透明和高反光物体时表现出色。

而拥有了更准确的深度图后,就可以将其输入到后续的抓取规划块中,能够为这些原本难以感知的物体生成有效的抓取位姿。

可供性(Affordance)

前面的讨论主要集中在如何通过视觉感知来抓取物体。然而,机器人的能力不应仅限于抓取,更进一步需要执行各种 操作(Manipulation / Operation)

可供性(Affordance):指一个物体所能支持或提供的交互方式或操作可能性。

它描述了环境或物体向交互者(人或机器人)提供的潜在行动可能性。

例如:

  • 一个抽屉的可供性是它可以被拉出(Pullable)或推入(Pushable)。
  • 一扇门的可供性在于它的门把手可以被抓住(Graspable),并且门可以被打开(Openable)。
  • 一把刀作用于一个水果时,水果的可供性在于它可以在某些区域被切割(Cuttable)。

在机器人学中,我们关注的可供性通常是指:为了让机器人完成某个特定的操作任务,它应该与物体的 哪个区域(Where) 进行交互,以及应该 以何种方式(How) 进行交互。

可供性通常被表示为热力图,也称 可供性图(Affordance Map)。对于不同的动作会有不同的可供性图,指示那个地方适合此类动作。

因此,通过预测这样的可供性地图,机器人就能知道哪些区域是执行特定操作的有效接触点。

Where2Act

利用学习方法来预测物体可供性的工作。

  1. 数据收集:让机器人在仿真或真实环境中对各种物体(尤其是带有可活动部件的铰接物体,articulated objects)进行大量的随机交互尝试(推、拉、拽等)。
  2. 标注:记录下哪些尝试成功了(如成功打开抽屉),哪些失败了。成功的交互区域和方式就构成了正样本训练数据。
  3. 模型训练:训练一个深度学习模型,输入物体的视觉信息(如图像和 / 或点云),输出其可供性图。

Pipeline

where_2_act

输入:2D / 3D

特征融合:将 2D 和 3D 特征进行融合,得到每个点的综合特征 $f_p$。

输出预测:基于融合后的特征 $f_p$,模型会预测多个信息:

  • 交互点(Contact Point):预测哪些点适合进行交互(输出表示可交互性的分数 $a_p$,affordance)。
  • 交互方向(Interaction Direction):预测在某个点上,应该沿着哪个方向进行交互(输出方向 $R_{z|p}$)。这可能需要对方向进行离散化或直接回归。
  • 成功置信度(Success Confidence):预测在该点以预测方向进行交互的成功概率或置信度(输出成功得分 $s_{R|p}$)。

VAT-Mart

VAT-Mart 进一步扩展了可供性的概念,认为仅仅预测交互点和初始方向可能不足以完成复杂的、需要遵循特定路径的操作。

例如,打开一个旋转门,如果只是沿着一个固定方向拉门把手,很快就会因为运动轨迹不匹配而失败。正确的操作需要沿着门转动的弧线运动。

VAT-Mart 不仅预测可供性区域(affordance),还预测出一整条 操作轨迹(trajectory)

视觉驱动的开环方法总结

应用

利用视觉输入进行预测:

  • 预测 物体位姿(Object Pose):通常需要物体的 CAD 模型和抓取标注。
  • 预测 抓取位姿(Grasp Pose):可以直接预测抓取点和姿态,无需 CAD 模型或预定义抓取。
  • 预测 可供性(Affordance):超越简单抓取,指导更广泛的交互操作。

运动规划(Motion Planning):利用预测出的目标(如抓取位姿或交互点 / 轨迹),结合环境信息(避障),规划出机器人手臂的运动路径。

实际执行中,运动规划往往需要结合一些启发式(heuristics)规则或技巧(tricks)来提高成功率和鲁棒性。例如 预抓取(Pre-grasp)位置,先移动到目标抓取点附近的一个安全位置,再直线接近并闭合夹爪,可以避免不必要的碰撞。

局限性

操作复杂度有限:通常只能处理一些预定义好的、相对简单的操作(如开抽屉、开柜门)。更复杂的操作(如转笔)超出了当前基于可供性预测和运动规划的框架能力。主要瓶颈在于启发式规则的设计。

开环执行:规划一次,执行到底。系统根据初始的视觉观测进行规划(抓取位姿、运动轨迹等),然后执行这个预先计算好的计划,在执行过程中 不再接收新的视觉反馈 来调整动作。就像闭着眼睛做事一样。

显然,对于开环来说,一旦执行过程中出现预期之外的情况,例如物体被意外碰到、滑动,或者初始感知 / 规划存在误差,整个任务很可能失败,因为系统无法根据实时变化进行调整。

但是,通过 高频率地重复 “感知 - 规划 - 执行” 的循环,将开环系统近似转化为闭环系统。

策略学习

策略学习(Policy Learning) 旨在解决开环抓取和规划中动态特性不足、无法及时根据环境状态调整的问题。

策略学习的核心在于构建一个能够根据环境状态变化采取合理策略的方案,本质上就是一个 Policy。Policy 拥有闭环执行的潜力,能够更好地适应场景状态的变化,从而使机器人操作更加鲁棒和高动态。

基础约定

  • 状态(State)$s_t$:环境的状态,这些状态通常隐藏在观测之下
  • 观测(Observation)$o_t$:对环境的观测,例如点云或图像。观测蕴含状态的信息,但通常是局部的、片面的。观测是状态的体现,状态是观测的本质
  • 动作(Action)$a_t$:在特定状态或观测下采取的策略,Policy 的目标就是根据场景中的状态变化动态地做出响应
  • 策略(Policy)$\pi(a_t|s_t)$ / $\pi(a_t|o_t)$:Policy 定义了在特定状态 / 观测下应该采取什么样的动作。通常用参数 $\theta$ 来参数化 Policy,记作 $\pi_\theta$

如果 Policy 基于状态 $s_t$ 来决定动作 $a_t$,则称该 Policy 为 Fully Observed 的策略。这意味着所有环境状态都是可观测的,虽然在现实中,我们通常只能获得部分观测。

我们的目标即为学习到这个策略。

模仿学习(Imitation Learning)

策略学习中最简单的方法就是监督学习,即模仿专家的行为。专家在每个状态或观测下给出正确的动作,然后通过监督学习训练 Policy。

示例:Point Goal Navigation

目标:在场景中导航到一个目标点。

传统方法(如 A* 算法):在已知地图的情况下可以找到最短路径。

策略学习:将传统路径规划算法作为老师(提供监督信号),指导 Policy 在每一步应该如何走才能更快到达目标点。

与传统方法的区别:即使没有地图,也可以通过训练一个基于视觉观测的 Policy,从而将策略应用到未建立地图的新场景中。

这是一种典型的模仿学习策略。

模仿学习的执行过程

策略执行过程可以概括如下:

  1. 观测(Observation)$o_t$:观测环境,可能蕴含环境状态的描述。
  2. 策略(Policy)$\pi(a_t|o_t)$:根据观测,Policy 决定采取的动作。
  3. 状态转移(State Transition):采取动作后,环境状态发生改变。

通过这样的方式,逐步迭代执行。

Markov 假设

定义:在任何状态下做判断时,只需根据当前状态来决定接下来应该采取什么动作,无需考虑过去经历了哪些状态。当前状态已经包含了过去历史的充分信息。

Markov 假设并非总是成立。 例如,司机超车时会根据自己的一些历史信息(如右后方是否有车辆)来决定是否变道。

Behavior Cloning

Behavior Cloning (BC) 是一种基本的模仿学习方案。根据观测,数据集包含专家在特定状态下采取的动作。通过监督学习,建立从观测到动作的映射关系,并使用动作层面的监督信号进行梯度回传,从而训练 Policy。

BC 将模仿学习问题视为一个监督学习问题。给定专家在状态 $s$ 下采取的动作 $a$ 的数据集 $D = {(s_i, a_i)}{i=1}^N$,行为克隆的目标是学习一个策略 $\pi\theta(a|s)$,使得在给定状态 $s$ 时,策略输出的动作 $a$ 尽可能接近专家动作 $a^*$。通常通过最小化预测动作与专家动作之间的差异来实现,例如使用均方误差损失:

$$ \theta^* = \arg \min_\theta \sum_{(s_i, a_i^) \in D} || \pi_\theta(s_i) - a_i^ ||^2 $$

BC 历史

1989 年,研究人员使用神经网络处理视觉输入,并将其映射到车辆的行为(方向盘转动角度、油门、刹车等)。这是 Behavior Cloning 的雏形。

2016 年,研究人员尝试使用深度学习方案改进 Behavior Cloning,用于自动驾驶。相比于早期的思路,这些改进包括更深的网络、更多的数据,以及更好的校正机制。即使使用基本的 Behavior Cloning,也能展示出不错的自动驾驶能力。

Distribution Shift

定义:模仿学习依赖于训练数据和测试数据具有较好的分布一致性。当这种分布一致性被打破时,模型很难泛化到测试集。而且,随着时间的推移,偏差会不断增大,尤其是在长序列任务中。一开始可能与训练分布一致,但执行步数越多,偏差越大,最终完全不可回头。

distribution_shift

这是因为:

  1. 专家演示数据通常只覆盖了状态空间中很小的一部分,即专家成功执行任务时所经历的状态。
  2. 学习到的策略 $\pi_\theta$ 不可能完美复制专家策略 $\pi^*$。即使是很小的误差,也会导致智能体在执行过程中逐渐偏离专家的状态分布。
  3. 一旦智能体进入训练数据中未曾出现过的状态,行为克隆训练出的策略可能无法做出正确的决策,导致错误累积,最终可能完全失败。

就像在一个陌生的环境中,有人带路,但之后开始乱走,走到一个完全陌生的环境,就会迷路。除非有专家重新指导,否则无法回到正确的轨迹。

BC 的实际应用

尽管有局限性,但当数据足够大时,Behavior Cloning 仍然可以表现出不错的性能。

遥操作可以通过动捕或机器人主从同步等方式获取专家数据。有了这些数据,就可以通过 BC 进行学习。

  • 合成数据:量大,但可能存在外观(Appearance)和物理(Physics)方面的差异,导致从虚拟环境学习的技能难以迁移到真实场景。需要进行充分的 域随机化(Domain Randomization)
  • 遥操作数据:在真实 / 虚拟场景(虚拟场景中便于数据增强)中采集,可以减少 Appearance 和 Physics 的差异。但代价高昂,且仍然可能存在泛化问题

解决 Distribution Shift 的思路

既然 Distribution Shift 来自于分布的不同,那么解决 Distribution Shift 的核心在于让这两个分布更加对齐(Alignment)。

有两种主要思路:

  1. 改变 $p_{\text{data}}(o_t)$:扩充专家数据的轨迹,使其能够覆盖策略执行过程中可能出现的状态空间。
  2. 改变 $p_{\pi}(o_t)$:给定专家轨迹,更好地拟合专家的轨迹,避免偏离专家的路线。

Dataset Aggregation

Dataset Aggregation(DAgger)是一种改变 $p_{\text{data}}(o_t)$ 的方法,旨在扩充训练数据,使其能够覆盖策略执行过程中可能出现的状态空间。

其核心思想是在训练过程中主动收集策略在执行时遇到的状态,并向专家请教这些状态下的正确动作,然后将这些新的数据加入训练集中。

  1. 初始化:使用初始的专家数据集 $D$ 训练一个初始策略 $\pi_1$。
  2. 迭代执行 (对于 $i = 1, 2, ..., N$):
    1. 执行策略:让当前的策略 $\pi_i$ 在环境中执行,收集遇到的状态序列 $s_1, s_2, ...$ (Rollout)
    2. 专家标注:对于收集到的状态 $s_t$,查询专家策略 $\pi^$,得到专家会采取的动作 $a_t^ = \pi^*(s_t)$。
    3. 数据聚合:将新的状态 - 动作对 $(s_t, a_t^)$ 加入到数据集 $D$ 中,即 $D \leftarrow D \cup {(s_t, a_t^)}$。
    4. 重新训练:使用聚合后的数据集 $D$ 重新训练策略,得到 $\pi_{i+1}$。
  3. 输出:最终得到的策略 $\pi_N$。

通过这种方式,监督数据集不断增长,覆盖实际执行过程中可能看到的各种状态,从而使 Policy 更加可控。

问题:出错了再标注的话,可能会对策略的准确性有所伤害(因为这种情况下你学到的不是完美的 Policy),但通常因为看到新的状态带来的学习经验收益更大。

从最优解中获取(From optimal solution)

利用传统算法,来构建一个传统的最优求解器(如 A* 搜索)。当学习的策略偏离最优路径时,可以使用这个求解器来提供完美的纠正动作,指导策略回到正轨。

从教师策略中学习(From a teacher solution)

我们可以假设存在一个 教师策略,它拥有比 学生策略 更多的 特权信息(Privileged Knowledge),例如仿真环境中的真实状态、物体精确的姿态或物理属性等。

利用这些特权信息,教师策略可以更容易地规划出最优动作。然后,让只能看到部分观测(如图像、点云)的学生策略去模仿教师策略的行为。

这样,即使学生策略偏离了,教师策略也能根据当前状态(利用其特权信息)给出在线的、正确的指导动作。这与仅提供固定的专家轨迹不同,教师策略具有在线适应和纠错能力。

非马尔可夫性与历史信息

传统的行为克隆通常假设环境满足马尔可夫性(Markov Property),即当前动作仅依赖于当前观测状态 $o_t$。然而,在现实世界中,我们通常只能获得 部分观测(Partial Observation) $o_t$,它并不包含环境的完整状态 $s_t$。

例如,一个经验丰富的司机在超车时,其决策可能依赖于几秒前看到的后视镜信息(即历史观测),即使当前观测 $o_t$ 中并没有显示那辆车。在这种情况下,仅根据当前观测 $o_t$ 学习动作 $a_t$ 的策略 $\pi(a_t|o_t)$ 会遇到困难:对于相同的观测 $o_t$,由于历史信息的不同,专家可能采取不同的动作(例如有时超车,有时不超车)。模型试图拟合这种一对多的映射关系,可能会学到一个无效的 “平均” 行为。

一个自然的解决方案是 引入历史信息,即将过去的观测序列 $(o_{t-k}, ..., o_t)$ 作为策略的输入,学习 $\pi(a_t | o_{t-k}, ..., o_t)$。这通常可以通过循环神经网络(RNN)或 Transformer 等序列模型实现。

然而,引入历史信息也带来了新的问题:

  1. 过拟合(Overfitting):输入维度大大增加,模型更容易在训练数据上过拟合,学到一些 spurious correlations(虚假关联),导致泛化能力下降。

  2. 因果混淆(Causal Confusion):模型在学习时,可能错误地将相关性当成了因果关系。

    例如,在自动驾驶数据中,每次踩刹车(action)都伴随着前方出现行人(cause)和刹车灯亮起(effect/correlation)。模型如果只学习到了 “观测到刹车灯亮起” 与 “踩刹车” 之间的关联,而忽略了 “看到行人” 这个真正的原因,就会做出错误的决策。它可能会认为只要刹车灯没亮,就不需要刹车,即使前方有行人。引入历史信息会使得输入维度更高,潜在的虚假关联更多,从而加剧因果混淆的风险。

多峰行为

和之前 DexGrapsNet 提到的一样,专家在面对同一个状态时,可能会有多种同样合理的行为选择。例如,在避障时,专家可能有时选择从左边绕过障碍物,有时选择从右边绕过。这种行为被称为 多峰行为

multi_task_learning

如果使用标准的行为克隆(例如,一个简单的多层感知机 MLP 直接回归动作),模型会试图拟合所有这些不同的专家动作。

  • 对于离散动作,这可能导致模型在不同动作间犹豫不决

  • 对于连续动作(如方向盘角度),模型可能会输出所有专家动作的平均值。

在上面的避障例子中,如果专家演示中左右绕行的概率各半,模型的平均输出可能是 “直行”,直接撞上障碍物。

多峰行为解决方案:对动作分布进行建模

为了解决多峰行为问题,我们需要使用更强大的模型来显式地建模动作的 分布 $\pi(a|s)$,而不是仅仅预测一个单一的确定性动作。

高斯混合模型(Gaussian Mixture Models, GMM)

假设动作分布可以由多个高斯分布的加权和来表示。策略网络输出每个高斯分量的均值(mean)、方差(variance)以及它们的权重(weight)。

$$ p(a|s) = \sum_{k=1}^K w_k(s) \mathcal{N}(a | \mu_k(s), \Sigma_k(s)) $$

其中 $K$ 是预先设定的模式(mode)数量。这种方法的优点是简单直观,但难点在于如何预先确定合适的 $K$ 值。

基于隐变量的模型(Latent Variable Models)

例如变分自编码器(Variational Autoencoder, VAE)。

将动作的生成过程建模为一个包含随机隐变量 $z$ 的条件生成模型 $p(a|s, z)$。通过从隐空间 $z$ 中采样,可以生成多样的动作。

例如,ALOHA 工作就使用了条件 VAE(CVAE)来建模动作分布,其策略 $\pi(a|s)$ 通过先从一个条件先验 $p(z|s)$ 中采样隐变量 $z$,再通过解码器 $p(a|s, z)$ 生成动作。训练时通过最大化证据下界(ELBO)来学习。

$$ \log p(a|s) \ge \mathbb{E}{q(z|s,a)}[\log p(a|s,z)] - D{KL}(q(z|s,a) || p(z|s)) $$

扩散模型(Diffusion Models)

扩散模型可以用来建模复杂的动作分布。

其核心思想是通过一个逐步去噪的过程从纯噪声中生成目标数据(这里是动作)。Diffusion Policy 这项工作就是将扩散模型应用于模仿学习。给定状态 $s$,模型学习一个去噪网络,该网络可以迭代地将一个随机噪声向量转化为符合专家行为分布的动作 $a$。

generate_model

diffusion

自回归建模(Autoregressive Modeling)

对于高维度的动作空间(例如,机械臂的多个关节角度),可以将动作 $a = (a_1, a_2, ..., a_d)$ 的联合分布分解为一系列条件概率的乘积:

$$ p(a|s) = p(a_1|s) p(a_2|s, a_1) \cdots p(a_d|s, a_1, ..., a_{d-1}) $$

然后,对每一维的条件概率 $p(a_i | s, a_1, ..., a_{i-1})$ 进行建模。

一个常用的技巧是先将每一维的连续动作 $a_i$ 进行 离散化(Discretization),将其值域划分为若干个区间(bins)。然后,将建模问题转化为预测在给定条件(状态 $s$ 和之前的动作维度 $a_1, ..., a_{i-1}$)下,当前动作维度 $a_i$ 属于哪个离散区间的概率分布。这变成了一个分类问题,可以用神经网络输出每个区间的概率。通过这种自回归和离散化的方式,可以将复杂的高维连续动作分布建模问题,转化为一系列相对简单的、一维离散概率分布的建模问题。在生成动作时,按顺序依次对每一维进行采样。

多任务学习(Multi-task Learning)

在许多实际场景中,我们收集到的专家数据可能包含执行不同任务(或同一任务的不同目标)的轨迹。例如,导航数据可能包含去往不同目的地的轨迹。

与其为每个任务单独训练一个策略(这会减少每个任务可用的数据量),不如采用 多任务学习 的思路。我们可以训练一个 目标条件化(Goal-conditioned) 的策略 $\pi(a|s, g)$,该策略不仅依赖当前状态 $s$,还依赖于当前要完成的目标 $g$。

这样做的好处是:

  1. 数据效率:所有任务的数据可以一起用来训练一个共享的模型,增加了有效训练数据量。
  2. 知识共享:不同任务之间可能存在共享的子结构或技能(例如,从北大无论开车去哪里,都得先开出北大东门)。多任务学习使得模型可以学习这些共享的知识,并互相促进,可能比单任务学习效果更好。

然而,多任务学习也引入了 目标空间的分布偏移:除了状态空间 $s$ 可能存在分布偏移外,目标空间 $g$ 也可能存在分布偏移。如果在测试时遇到一个训练时从未见过的目标 $g$,策略的泛化能力就面临考验。

模仿学习的局限性

尽管模仿学习(尤其是结合了 DAgger 和先进模型结构后)非常强大,甚至催生了许多成功的应用(如一些基于大模型的机器人控制),但它仍然有其局限性:

  1. 依赖专家数据:需要大量高质量的专家演示数据,获取成本可能很高。
  2. 无法超越专家:策略的性能上限受限于专家的水平。
  3. 不适用于高度动态或不稳定的任务:对于那些需要精确反馈和快速调整的任务(例如,让机器人用指尖转笔),微小的误差就可能导致失败。在这种情况下,仅仅模仿轨迹可能不足以学习到鲁棒的策略,因为系统对状态扰动非常敏感,而专家数据可能无法覆盖所有可能的微小扰动及其纠正措施。

强化学习

懒得详细写了,当年学过强化学习课程已经被狠狠摧残过一遍了。

推荐参照 动手学强化学习 自学。

马尔可夫决策过程(Markov Decision Process,MDP)

$$ \mathcal{M} = {S, \mathcal{A}, \mathcal{T}, r} $$

其中:

  • $\mathcal{A}$:动作空间 (Action Space),智能体可以采取的动作。
  • $\mathcal{T}$:状态转移算子 (Transition Operator),现在依赖于状态和动作,$p(s_{t+1}|s_t, a_t)$。
  • $r$:奖励函数 (Reward Function),$r:S \times \mathcal{A} \to \mathbb{R}$,表示在状态 $s_t$ 执行动作 $a_t$ 后获得的即时奖励 $r(s_t, a_t)$。

部分可观测马尔可夫决策过程(POMDP)

部分可观测马尔可夫决策过程(Partially Observable Markov Decision Process, POMDP)是 MDP 的扩展,其中智能体只能观测到部分状态。

$$ \mathcal{M} = {S, \mathcal{A}, \mathcal{O}, \mathcal{T}, \mathcal{E}, r} $$

其中:

  • $\mathcal{O}$:观测空间 (Observation Space),智能体可以观测到的状态
  • $\mathcal{E}$:观测概率 (Observation Probability),$p(o_t|s_t, a_t)$,描述在真实状态 $s_t$ 下,观测到 $o_t$ 的概率 $p(o_t|s_t)$
  • $\mathcal{T}$:状态转移算子 (Transition Operator),$p(s_{t+1}|s_t, a_t)$

此时,智能体 无法直接知道 当前的真实状态 $s_t$,只能得到一个与 $s_t$ 相关的观测 $o_t$。

强化学习的目标

强化学习:学习一个策略(policy) $\pi_\theta(a|s)$ (由参数 $\theta$ 决定),使得在一个轨迹(trajectory) $\tau =(s_1, a_1, s_2, a_2, ...)$ 上的累积奖励期望最大化。

$$ \begin{aligned} \theta^* &= \arg\max_\theta \mathbb{E}{\tau \sim p\theta(\tau)} \left[ \sum_t r(s_t, a_t) \right] \ &= \arg\max_\theta \mathbb{E}{(s_t, a_t) \sim p\theta(s_t, a_t)} \left[ r(s_t, a_t) \right] \end{aligned} $$

其中,$p_\theta(\tau) = p(s_1) \prod_{t=1}^T \pi_\theta(a_t|s_t) p(s_{t+1}|s_t, a_t)$ 是轨迹 $\tau$ 出现的概率。

注意这个式子暗含了马尔可夫性,因为状态转移概率 $p(s_{t+1}|s_t, a_t)$ 只依赖于 $s_t$ 和 $a_t$)。

有限时间界(Finite Horizon):最大化固定步数 $T$ (有限时间)内的总奖励期望。

$$ \theta^* = \arg\max_\theta \sum_{t=1}^T \mathbb{E}{(s_t, a_t) \sim p\theta(s_t, a_t)} [r(s_t, a_t)] $$

其中 $p_\theta(s_t, a_t)$ 是在 $t$ 时刻访问状态 - 动作对 $(s_t, a_t)$ 的概率(边际分布)。

RL 优化的是期望奖励:即使奖励函数本身不平滑,期望奖励 $\mathbb{E}{\pi\theta}[r(x)]$ 通常是关于策略参数 $\theta$ 平滑的,这使得基于梯度的优化方法成为可能。

💾

  •  

家庭数据中心系列 Cloudflare 监控告警组合实战:运行状况检查 + 事件警报带来的轻量化运维体验

家庭数据中心系列 Cloudflare 监控告警组合实战:运行状况检查 + 事件警报带来的轻量化运维体验 无敌的个人博客 tangwudi

1 前言 关于群站应用的健康检查和即时通知,之前我曾经写过一套方案:自建 Uptime 搭配 Bark,用来自行监控服务状态,并将异常推送到我的苹果设备(iPhone、iPad、macOS)上(具体的搭建步骤参看文章:docker系列 搭建基于uptime和bark的应用实时健康监测及报警系统)。除了只能支持苹果生态这一点之外,这套方案在功能性上其实相当完整。 不过,用了一段时间后我还是放弃了,不是因为技术层面出问题,而是从实际使用体验来看,性价比确实不高,原因大致有以下几点: 图形界面浪费资源:Uptime 的图表页面对我来说几乎没用,我也不会没事去盯着看,但后台渲染这些内容却占用不少轻量服务器的资源——而腾讯云的轻量服务器本来也不算性能强悍。 WAF 规则妥协安全性:为了让 Uptime 的探测请求能成功回源,我还得把腾讯云服务器的 IP 加入 Cloudflare 的 WAF 白名单 […]

<p>The post 家庭数据中心系列 Cloudflare 监控告警组合实战:运行状况检查 + 事件警报带来的轻量化运维体验 first appeared on 无敌的个人博客.</p>

  •  

养猫的烦恼

#吐槽 去年从老家带回来的那只中华田园猫,不知不觉已经养了半年多了。最近,它开始频繁叫春,声音又尖又长,尤其半夜三更的时候突然一声“嗷~~~”,简直惊悚到魂都快飞出来。每天夜里都像在演鬼片,我被折腾得严重失眠,白天没精神,整个人都快崩溃了。本来想直接带它去做绝育的,省得它继续上演“午夜惊魂”。可是一打听才知道,猫咪在做手术前最好先接种疫苗,特别是要防猫瘟。小猫抵抗力弱,一旦感染,后果不堪设想。左思右想,还是决定先给它打一针疫苗再说吧,这也意味着我们还得多忍受一段时间它的嚎叫。唉,早知道养猫这么折腾,当初就不该把它带回来......

另外抽时间将主题 vue 组件由直接在客户端渲染改为了服务端预渲染,大幅提高了 vue 页面的流畅度。至此当切换到笔记首页、展开右侧边栏、进入笔记内页、待办事项页面等含有 vue 组件的页面时彻底杜绝了闪烁问题!

  •  

2025年3月阅读书摘

✇Dennis
作者Domon

3月阅读记录

  • 《猫鱼》Done

3 月阅读书摘

猫鱼

  • 2025年3月阅读书摘
  • 书名: 猫鱼
  • 作者: 陈冲
  • 简介:
  • 出版时间
  • ISBN:
  • 分类:
  • 出版社:

悲伤是黑镜中的美

  • 2025年3月阅读书摘

    📌 这话让我想到,创作的饥渴和激情,常常来自某种基于哀思的记忆和想象——那个用清澈双眼望着你说“我爱你”的孩子,终将长大离家去寻找别的爱;那段令你神魂颠倒死而后已的恋情,终将这样或者那样地结束;那个晨光里完美的蜘蛛网、蒲公英、凤尾蝶,那道划过夜空的火流星……一切穿刺到你灵魂的美都与母亲一样,终将逝去。这不可名状、无法安慰的渴望和骚动便是艺术的源泉。

  • 📌 写到一位垂死的老妇人,看到自己美丽而艰难的一生像电影那样闪回,她无法相信这就是一切,这就是尽头。然而在死去那一瞬间,老妇人脸上露出了一丝神秘的微笑,也许她瞥见了宇宙与时间之前的虚无,知道了生命的奥妙。
    当时,她的体内有31470103497276—498750108327个原子,她的实质中,63.7%是氧气,21%是碳,2.6%是氮,1.4% 是钙,1.1%是磷,外加少量在恒星中产生的九十种其他化学元素。火化时,她身体里的水分蒸发了;她的碳与氧结合后,形成了气体一氧化碳与二氧化碳,飘浮起来跟空气混合;她的大部分钙和磷燃烧成了红棕色的灰烬,随风散落在土壤里。
    曾经属于她的原子就这样被释放和蔓延开来。六十天内,它们便波及全球的空气;一百天内,她的部分原子——那些火化时蒸发了的水分——便凝结成雨水降落下来,被动物和植物酣饮吸收,转化成器官、骨骼、枝叶和花朵;孕妇们吃了那些动物和植物,十个月后,含有她原子的婴儿们便呱呱坠地……
    在老妇人去世的几年后,地球上会有数百万含有她原子的孩子;再过几十年,那些孩子的孩子身上也将包含她的一部分原子,他们的思想将包含一部分她的思想……曾经暂时属于她的那些原子,将永远循环在风里水里土壤里,在世世代代的生命与思想里。他们能传承她的记忆,感受她经历的痛苦与欢乐吗?当然不能,但也许我们每个人,都积累和融汇了所有生命的记忆;也许我们所体验的无常,从来就是永恒。
    母亲将存在于万物中——这个想法给我带来安慰。

无法实现的梦想

  • 📌 我想起作家斯坦贝克的一句话:世上每个人,都有一个他冥冥中知道无法实现的梦想,但他会用毕生去希望和等待它的到来。人类因此而悲哀,也因此而伟大和辉煌

被遗忘的爱之夜

  • 📌 那时我还没有相机,时间没有从绵延的生命中被切割成一百分之一秒的单位,夹到相册里。那些没有被相机拍过的记忆——人脸、人声、语言、地方,熟悉的和不认识的,似曾相识的和梦里的,欣喜若狂或绝望无底的——像时间河流里的一块块石头,被岁月磨成了卵石,上面长出一层毛茸茸的青苔,边上沉淀了淤泥砂石。隔着漂动的水草和水波看它们,恍恍惚惚,阳光里一个样子,月光里又是另一个样子……

一点心

  • 📌 你在向外看,这正是你不该做的事情。没有人能给你建议和帮助,没有人;唯一能帮助你的是走进自己的灵魂深处,审视你写作的动机,是否扎根于内心最深处,向自己坦白,如果无法写作,你是否会死;在夜深人静时问自己:我必须写吗?如果你可以用一个强烈而简单的“我必须”来回答这个庄严的问题,那么就根据这一必须来构建你的生活;哪怕在最不重要和最微不足道的时刻,你的生活都必须成为这个回答的象征和见证。

  • 📌 把回想留给未来吧,就像把梦留给夜,泪留给海,风留给帆。

幻想博物馆

  • 📌 好像是博尔赫斯说的,我们是我们的记忆……那个不断变形的幻想博物馆,那堆破碎的镜子。从逝去的时间里,记忆只选择某些碎片,我们似乎总是在企图用碎片拼凑出一个完整的现实,而记忆的选择又往往不是在发现,而是在隐藏事实。

停留在荒芜和黑暗的地方

  • 📌 父亲关起门来跟我们说,歌里面唱哪个地方是个好地方,就不能去那个地方。

将美丽带回人间

  • 📌 我想起剧本中王亚军念的莎士比亚台词,“To be,or not to be,that is the question”,人到底只有这样一个选择。我只有继续。摇滚歌手大卫·鲍伊在一次采访里说过,最令人兴奋的创作,往往产生于你觉得脚尖够不到水底的时候,觉得自己要被淹没的时候。艺术创作是一种求生,是把全部的、最纯粹的注意力集中在一个问题上,而这个问题就是答案本身。

几粒金色的麦穗

  • 📌 我想起《霍乱时期的爱情》中的一句话,“说到底爱情是一种本能,要么第一次就会,要么就一辈子也不会。”

我们将死于梦醒

  • 📌 飞机开始升高,窗外渐远的灯火和渐厚的云层仿佛奇妙的海底世界,父亲大红色的泳帽出现在我的脑海,它在水里时而浮起时而沉没,不管池子里人多人少,不管他游到哪个角落,我都能从眼梢看见那团红色。不知父亲有没有留意我的蓝泳帽,感觉到某种心照不宣的亲情?

猫鱼

  • 📌 美国摄影师沙丽·曼在《留住这一刻》中这样写道:“早在 1901 年,爱弥尔·佐拉就指出了摄影对记忆的威胁,他说,如果你没有拍下来,就不能声称你真正看到了某物。然而,一旦被拍了下来,无论你‘真正看到’的是什么,都永远不再会被记忆的眼睛看到。”

  • 📌 沙丽·曼称之为“照片的背叛”。我们总以为照片能保存过去,其实它们把某些瞬间从人生长河中截出来,取代并腐蚀了真相,同时创造了它们自己的记忆。

  • 📌 每一个艺术家都有自己童年的“猫鱼”,它是“一种象征性的语言”“本性中被遗忘或隐藏了的真相”;它是我们余生创作最汹涌的源泉,也是我们在日常生活中体验到的每一个“奇迹”。我很难想象任何创作者的想象力与核心图像,不是潜意识中来自童年的、某个强烈的视觉感知或幻想。

孤独和欲望的颜色

  • 📌 忘了是哪位作家说的,艺术家必是诗人,他不一定写诗,但是他眼睛里看到诗。
  •  

二〇二五年三月总结,我与平庸之间的关系

人的精力是有限的,能做的事情不多,然而我总想做更多的事情,甚至不加排序的做这些事情,最终碌碌无为。

我与平庸同行,虽然厌恶他,但平庸从未嫌弃我的能力,始终保持着最平易近人的方式环抱着我。

平庸没有给我带来负面情绪,在我焦虑几近崩溃时积极调整,在我决心要做某些事情时他默默退出从未说过你不行。

但是我的潜意识里有很多他的影子,保持乐观不要过度悲伤,保持善良不要急功近利,保持健康不要没了青山。

已然接受了自己的平庸,在今后的道路上也许会有闪光,但平庸才是最长情的那个。

写公众号文章

现在写公众号晚不晚,我觉的是有些晚的。但无论是什么时候开始都会遇到各种各样的问题。公众号推荐模式早已发生改变,给普通人留了很多机会。

机会有了,就去做。没有什么经验的我,运营公众号表现的依然是个小白。目前还停留在内容运营,但我却选了如此小众的方向——独立博客。

在公众号里面讲独立博客似乎是逆势而为,发布的文章阅读量从未突破两位数,看了很多账号差不多如此,等待着掉入流量池。做公众号有点玄学味道,但是也看到有人动不动就注销关注数过万的账号,另起炉灶也能很快有所成。如此来看,做公众号还是有一套方法的,至于是啥要付费才能明白。公众号还是卖课的比较多啊。

而我现在要做的是积累内容,但精力有限,每周仅发布3篇文章,这样算下来要积累100篇文章也需要200多天。现在写作的能力有限,光是找选题都能用很多时间,再列大纲,再写正文,再优化语言,这一套流程下来需要七八个小时。

接下来是想办法优化时间。

修整自行车骑行

3月份骑行次数和里程都不多,主要原因是后拨变速异常,拖了很久才处理。调整尾钩变形、后拨更换了导轮,终于把后拨变速搞好了,骑上去明显感觉不一样。

不多说,看看数据:

总里程310.33km
总时长15:10:29
骑行次数28
月均速20.45km/h

这数据没眼看,到目前还没有恢复去年的水平。有点动摇,要不要再升级自行车,中轴、轮组都给换掉,还是直接买个入门公路车。

但有考虑到骑车健身,不能一味追求速度,当前的问题是骑车锻炼时间的不足,根据精力合理安排骑行时间。

相机到手新开始

从产生买相机的念头,到买相机,前前后后有一个多月。这个过程潜意识在帮我做决定,入门相机还是进阶相机,潜意识帮我选择入门型。

入门相机确实符合我的需求,毕竟技术水平在那里,拿再好的相机很难拍出惊艳的照片。入门相机则可以让我快速上手,多练习拍照技巧,找找大师机位,看看能不能拍出大师的感觉。

虽然不拍视频,但我还是入手了更适合拍视频的索尼ZV-E10Ⅱ,使用固定机位录个口播也不错,怪不得导购说拿来做直播也是不错的选择。

相机到手,接下来要练习拍照,找找教程跟着学习,第一时间拍出第一张照片。

  •  

家庭数据中心系列 通过 Cloudflare 实现精准缓存清除:Cache-Tag 与前缀方式实战

家庭数据中心系列 通过 Cloudflare 实现精准缓存清除:Cache-Tag 与前缀方式实战 无敌的个人博客 tangwudi

1 前言 很多使用 Cloudflare CDN 的朋友经常会遇到这样一个问题:修改了网站的某些内容,但刷新网页后却发现更改并没有立即生效。这时,大家的第一反应往往是去 Cloudflare 仪表盘里点一下 “清除所有内容”: 虽然这样能立刻解决问题,但却有个很大的副作用——所有的静态资源(HTML、CSS、JS、图片等)都会被清除,访客访问网站时不得不重新从源站加载所有内容,这会导致页面变慢、流量浪费,甚至影响 SEO。 实际上,我们很多时候真正想要清除的只是HTML 页面,比如修改了 functions.php、header.php 或 footer.php,导致页面结构发生变化,但 Cloudflare 仍在提供旧的 HTML 缓存,这时候只清除 HTML,而不动 CSS、JS、图片,才是最合理、最精确的做法。 虽然 Cloudflare 仪表盘本身没有提供& […]

<p>The post 家庭数据中心系列 通过 Cloudflare 实现精准缓存清除:Cache-Tag 与前缀方式实战 first appeared on 无敌的个人博客.</p>

  •  

视觉与抓取 III

抓取

Form Closure 与 Force Closure

  • Form Closure(形闭合):这是一种纯粹基于几何的定义。指的是接触点(contact points)形成了一个 “笼子”,将物体完全包住。在不移动接触点的情况下,物体从几何上无法从这个 “笼子” 中逃逸。可以认为这是一种最理想、最稳固的包裹式抓取接触状态。其不依赖于摩擦力
  • Force Closure(力闭合):这个概念考虑了接触点的力和摩擦力。它指的是,虽然接触点可能没有形成几何上的 “笼子”,但通过在这些接触点上施加适当的力(利用摩擦力),可以抵抗施加在物体上的任意方向的力(force)和力矩(torque)。换句话说,只要夹爪(或手指)能提供足够大的力,理论上就能抵抗任何外来的扰动,或者能让物体产生任意方向的加速度和角加速度。其依赖于摩擦力

它们之间存在一个重要的关系:

$$ \text{Form Closure} \subset \text{Force Closure} \subset \text{Successful Grasp} $$

也即,严苛程度上:

$$ \text{Successful Grasp} \leq \text{Force Closure} \leq \text{Form Closure} $$

这意味着:

  • 如果一个抓取是 Form Closure,那么它一定也是 Force Closure。
  • 如果一个抓取是 Force Closure,那么它在理想情况下(夹爪力量足够)一定能成功抓起物体。
  • 但是反过来不一定成立。
    • 一个成功的抓取不一定是 Force Closure,比如轻轻托起一个物体,它只抵抗了垂直方向的力,如果施加一个水平方向的力,它就会滑动
    • 一个 Force Closure 也不一定是 Form Closure,比如用两个手指平行夹住一个方块的两侧,这不是 Form Closure,因为物体可以上下滑动。但如果考虑摩擦力,只要能施加足够的夹紧力,它可能是一个 Force Closure,能够抵抗各个方向的外力。

摩擦锥(Friction Cone)

为了理解 Force Closure,我们需要引入摩擦锥的概念。

考虑一个简单的物理场景:一个滑块放在水平面上,两者之间的静摩擦系数为 $\mu$。

显然,如果我们对滑块施加一个法向力(正压力) $N$,就能利用摩擦力将之固定在平面上。

现在考虑如下情形:如果施加一个与法线方向成 $\theta$ 角的力 $F$ 作用在接触点上。

friction_cone

这个力 $F$ 可以分解为法向分量 $F_{\perp} = F \cos \theta$ 和切向分量 $F_{\parallel} = F \sin \theta$。

为了使滑块不发生滑动,切向力必须小于等于最大静摩擦力,即:

$$ F_{\parallel} \le \mu F_{\perp} $$

代入分解后的力,得到:

$$ F \sin \theta \le \mu (F \cos \theta) $$

假设 $F \cos \theta > 0$,我们可以得到:

$$ \tan \theta \le \mu $$

令摩擦角 $\alpha = \arctan \mu$。这意味着,只要施加的力 $F$ 与接触面法线方向的夹角 $\theta$ 不超过 $\alpha$,无论这个力 $F$ 有多大(在理想情况下,假设物体和接触面都是刚体且不会被破坏),滑块都不会发生滑动。这种情况称为 自锁(self-locking)

在三维空间中,所有满足这个条件的力 $F$ 的方向构成了一个圆锥,称为 摩擦锥(Friction Cone)。这个锥体的轴线是接触点的法线方向,其半顶角就是摩擦角 $\alpha = \arctan \mu$。

任何作用在接触点且方向向量位于此摩擦锥内部(或边界上)的力,都不会导致该接触点发生滑动(不会有滑动摩擦,都是静摩擦)。

Force Closure 的数学定义

定义:一组摩擦接触实现 力闭合(force closure),如果其 力旋量锥(wrench cones)正向张成(positive span) 是整个 力旋量空间(wrench space)

思考一下,作用在一个刚体上的力的效果,它不仅会使物体 平移(力),还会使物体 旋转(力矩),而这就引入了六个自由度。

而为了同时描述作用在刚体上的力和力矩的 整体效果,我们将力和力矩组合成一个单一的向量,称为 力旋量(Wrench)

  • 在二维平面中,物体有 2 个平移自由度(在平面内)和 1 个旋转自由度(绕垂直于平面的轴)。因此,力旋量是一个 3 维向量:

    $$ \mathcal{F} = \begin{bmatrix} f_x \ f_y \ \tau_z \end{bmatrix} \in \mathbb{R}^3 $$

    前两个分量是平面内的力,最后一个分量是绕垂直轴的力矩。

  • 在三维空间中,物体有 3 个平移自由度和 3 个旋转自由度。因此,力旋量是一个 6 维向量: $$ \mathcal{F} = \begin{bmatrix} \mathbf{f} \ \boldsymbol{\tau} \end{bmatrix} = \begin{bmatrix} f_x \ f_y \ f_z \ \tau_x \ \tau_y \ \tau_z \end{bmatrix} \in \mathbb{R}^6 $$ 前三个分量是力,后三个分量是力矩。

现在,我们可以更精确地定义 Force Closure。一个抓取被称为 Force Closure,是指所有接触点的摩擦锥组合起来,能够产生抵抗任意施加于物体的 力旋量(Wrench) 的能力(和最初那个定义等价)。

我们将空间中的每个摩擦锥用一定数量(记为 $k$,课中选择为 $k = 6$)的力旋量组成的多面体锥来近似,从而摩擦锥可以表示为这 $k$ 个力旋量的线性组合。

接触点决定力,方向决定力矩

如此考虑所有的摩擦锥,我们定义 抓取矩阵 F(Grasp Matrix F)

$$ F = \begin{bmatrix} \mathcal{F}_1 & \cdots & \mathcal{F}_j \end{bmatrix} \in \mathbb{R}^{n \times j},\ n = 3 \text{ or } 6,\ j = k \times C $$

其中,$C$ 是接触点(摩擦锥)的数量,$k$ 是为了近似每个摩擦锥所使用的力旋量数量(也即用多少面体锥来近似摩擦锥)。

那么,力闭合的数学化表达(充要条件)就是:

$$ \text{rank}(F) = n \text{ (3 or 6)} \ Fk = 0 \text{ for some } k \in \mathbb{R}^j, k_i \ge \epsilon > 0 \text{ for all } i $$

第一个条件

$$ \text{rank}(F) = n \text{ (3 or 6)} $$

这个条件意味着 $F$ 的 $j$ 个列向量 $\mathcal{F}_1, \dots, \mathcal{F}_j$ 能够张成整个 $n$ 维的任务空间 $\mathbb{R}^n$。

物理意义:为了能够抵抗任意方向的外部扰动(力 / 力矩),我们施加的接触力 / 力旋量的组合必须能够产生任意方向的合力 / 合力旋量。如果 $\mathrm{rank}(F) < n$,那么 $F$ 的列向量只能张成 $\mathbb{R}^n$ 的一个子空间。这意味着存在某些方向的外部扰动,无论我们如何调整接触力的大小(即对 $\mathcal{F}_i$ 进行线性组合),都无法产生一个能够与之平衡的合力 / 合力旋量。

第二个条件

$$ Fk = 0 \text{ for some } k \in \mathbb{R}^j, k_i \ge \epsilon > 0 \text{ for all } i $$

这个条件意味着存在一个线性组合,使得合力 / 合力矩为零,并且这个组合中的 所有系数 $k_i$ 都必须是严格正的 (大于某个很小的正常数 $\epsilon$)。这意味着我们可以通过同时施加正向的力(或在摩擦锥内的力)来实现力的平衡。

为什么需要严格大于零($>\epsilon$)?这保证了原点不在凸锥的边界上。如果原点在边界上,可能存在某些方向的扰动,虽然理论上可以被平衡,但在实际中(考虑到力的限制、接触的不确定性等)可能无法稳定地抵抗。严格大于零提供了鲁棒性,使得抓取更加稳定,并且能够抵抗微小的扰动。

物理 / 几何意义:这个条件与凸包(Convex Hull)或锥组合(Conic Combination)的概念紧密相关。具体来说,它等价于零向量(原点)严格位于由接触力旋量向量 ${\mathcal{F}_1, \dots, \mathcal{F}_j}$ 生成的凸锥(Convex Cone)的内部。

合并条件

如果这两个条件都满足,那么对于施加在物体上的任何外部力旋量 $w_{ext}$(或者等价地,对于想要让物体产生的任何加速度 $a$ 和角加速度 $\alpha$,它们对应一个需要施加的力旋量 $w_{req}$),我们都能找到一组非负的系数 $k' = [k'_1, k'_2, \ldots, k'J]^\top$ ($k'i \ge 0$),使得 $Fk' = -w{ext}$ (或 $Fk' = w{req}$)。

因为所有 $k'_i \ge 0$,这意味着所需的接触力都在各自(近似的)摩擦锥内,因此不会发生滑动。

不过,这个理论推导假设接触点可以施加任意大的力。在实际机器人中,执行器(电机)的力 / 力矩是有限的。所以,即使一个抓取满足 Force Closure 条件,如果需要抵抗的外力过大或需要产生的加速度过大,超出了机器人的能力范围,抓取仍然会失败。

Force Closure 应用

Force Closure 的概念是合成大规模抓取标注数据集(Grasp Data Synthesis)的关键技术之一。

合成抓取数据集的两个经典方法:

  • 利用 Force Closure 大量生成抓取标签
  • 在 Simulater 中设置不同的重力方向($x,y,z,-x,-y,-z$),看会不会掉出来,来近似判断

GraspNet-1B 数据集

GraspNet-1B 数据集的生成流程大致如下:

  1. 获取物体模型:通过 3D 扫描收集一批物体的三维模型。
  2. 物体上抓取姿态采样:对每个物体模型,在其表面采样大量的候选抓取位姿(gripper pose),包括位置和朝向。例如,可以在物体表面均匀采样点(FPS 算法),然后将夹爪中心对准采样点,朝向可以基于表面法线并加入随机旋转。
  3. Force Closure 筛选:对每个采样得到的抓取姿态,给定一个摩擦系数 $\mu$(例如 $\mu=0.8$),使用前面所述的数学条件判断它是否满足 Force Closure。只保留满足条件的抓取姿态作为该物体的有效抓取标签。
  4. 场景生成与物体位姿标注:创建包含多个物体的三维场景(例如,将物体随机摆放在桌面上)。需要知道场景中每个物体的精确 6D 位姿。GraspNet 最初通过将真实物体摆放在桌面上,然后使用 RGB-D 传感器数据和物体模型进行匹配来标注位姿。(现在可以完全在仿真环境中生成场景和物体的精确位姿)。
  5. 抓取标签转换与碰撞检测:将步骤 3 中得到的物体中心坐标系下的有效抓取标签,利用步骤 4 中得到的物体位姿,转换到场景坐标系下。然后,检查在这个场景中,当夹爪移动到抓取位置(以及接近过程)时,是否会与场景中的其他物体发生碰撞。去除会发生碰撞的抓取标签。
  6. 多视角渲染:对于每个生成好的带有有效、无碰撞抓取标签的场景,从多个不同的虚拟相机视角进行渲染,生成 RGB 图像、深度图、点云等数据,从而在人工参与恒定的情况下扩大数据集。每一个数据点就构成了一个(输入数据,有效抓取标签)的配对。

关于摩擦系数的讨论

GraspNet 数据集实际上为不同的 $\mu$ 值(如从 0.8 到 0.1)都进行了筛选并存储了标签。

$\mu$ 值越低,对抓取的要求越高(更接近 Form Closure),这样的抓取在低摩擦表面上更可能成功。

训练时,有时会选择使用在较低 $\mu$(如 0.1)下仍然满足 Force Closure 的标签,认为这些是更高质量、更鲁棒的抓取,在真实世界中会拥有最好的泛化性,尽管这会大大减少标签数量(从 10 亿减少到几百万)。

这是一个标签数量和质量之间的权衡(Trade-off)。

意义

GraspNet-1B 的生成流程在当时是开创性的,但也有其局限性。例如,它依赖于扫描的真实物体和在真实桌面上进行的位姿标注,限制了物体种类和场景背景的多样性。

如今,随着高质量三维模型库(如 ObjectVerse XL 包含千万级模型)和逼真渲染技术的发展,完全可以在仿真环境中生成更大规模、更多样化的抓取数据集。物体模型、场景布局、纹理、光照等都可以程序化生成,无需依赖真实扫描和物理摆放,这大大提高了效率和数据的泛化潜力(还是王老师一直强调的观点,合成数据的潜力是巨大的 )。

尽管 GraspNet-1B 的物体和背景多样性有限,但它证明了使用基于三维几何信息(如点云)作为输入的模型,即使只在相对有限的数据上训练,也能学到在杂乱场景中进行抓取的有效策略。这说明三维几何本身提供了强大的先验信息。 然而,若要训练能直接从二维图像(RGB 或 RGB-D)输入的模型,并使其泛化到未见过的物体和环境,就需要更大规模、更多样性的合成数据。

抓取检测问题(Grasp Detection)

将抓取问题形式化(Formulate)为一个检测问题,是解决机器人抓取的一种常用方法。

目标:给定场景的某种表示(如点云、RGB-D 图像、体素网格),算法需要输出一系列候选的抓取姿态(Grasp Poses)。每个姿态通常包含位置(3 DoF)、朝向(3 DoF)和夹爪宽度(1 DoF),并附带一个质量评分(Quality Score)或成功概率。

输入模态

三维几何表示

举例:点云(Point Cloud)、体素网格(Voxel Grid)、截断符号距离场(TSDF - Truncated Signed Distance Function)。

  • 点云:最直接的表示方式,每个点包含位置和法线信息。
  • 体素网格:体素就是三维空间中的像素(小方格),通过将空间均匀划分,就得到体素网格。
  • TSDF:一种常见的体素网格表示。每个体素存储一个值,表示该体素中心到最近物体表面的有符号距离,并且这个距离值通常会被截断在一个范围内(例如 -10cm 到 +10cm)。正值表示在表面外,负值表示在表面内,0 表示在表面上。

由于抓取的物理稳定性主要取决于物体的局部几何形状(决定了接触点、法线、曲率等),而不是颜色或纹理,所以直接使用几何信息作为输入被认为更直接、更有效,尤其是在 GraspNet-1B 这类几何信息丰富但视觉外观多样性有限的数据集上(意思就是 3D 比 2D 信息更好,不需要用 RGB 反推几何信息,而是直接就是和任务密切相关的几何信息)。

这里老师还提到了一个 Partical / Complete 的说法挺有意思的,就是说你想建模完整的三维场景,那就需要多视角的数据,否则单视角会因为重叠而导致信息缺失。

二维图像表示

举例:RGB 图像、深度图像(Depth Image)。

2D 信息往往隐式包含几何信息。

基于 TSDF 的抓取(VGN)

VGN

VGN 直接在三维体素空间中对抓取位姿进行预测。

输入:一个表示了场景几何的 3D 体素网格,例如一个 40x40x40 的 TSDF 网格。

网络结构:通常采用类似 U-Net 的 3D 全卷积网络结构。

输出:总体预测三个体素网格,即对于输出网格中的每一个体素,网络预测:

  1. 抓取质量(Grasp Quality/Score):一个标量值,表示以该体素为中心的抓取成功的概率或质量。

  2. 抓取朝向(Grasp Orientation):描述夹爪应该如何旋转。通常采用四元数格式。

  3. 抓取宽度(Grasp Width):一个标量值,表示执行抓取时夹爪需要张开的宽度

    预测宽度的主要目的是防止夹爪过宽向外碰撞,而不是为了确定要多宽才能夹,实际操作都是直接夹到不能继续为止(工程 Trick)。

抓取任务评估

常用任务:清理桌面(Table Clearing)或箱中取物(Bin Picking)。这类任务的目标是将一个杂乱堆叠的物体集合逐一抓取并移除。

评估指标:

  • 抓取成功率(Success Rate):成功抓取次数 / 总尝试抓取次数。
  • 清理率(Percentage Cleard):成功移除的物体数量 / 场景中总物体数量。
  • 规划时间(Planning Time):接收输入与返回抓取之间的时间间隔

特点:

  • 非特定对象(Object Agnostic):算法通常不区分物体身份,哪个物体看起来最好抓(预测得分最高)就先抓哪个。
  • 非任务导向(Non-Task-Oriented):不关心抓取物体后的具体用途(即无语义信息,不关心是递给别人、是用来倒水、还是装配),只关心能否稳定地把物体 “提起来”。
  • 过程简化:评估时,抓取后的放置阶段可能被简化,例如直接移动到一个固定区域放下,甚至允许在移动过程中发生碰撞,只要物体被成功从初始位置拿起就算成功。

后处理

  • 通过 高斯平滑 提升预测的鲁棒性和区域一致性。

  • 通过 距离掩膜 保证抓取的物理和运动学可行性,如果 TSDF 值高过阈值,那就认为距离表面太深了手指不可达,将其 Mask 掉。

  • 通过 NMS 以抓取质量分数指标去除冗余预测,得到精简且有代表性的抓取候选集。

    但这里老师也说了,仅仅这样不够好,因为光看 Grasp Quality 的话,没考虑 Orientation / Width,即使前面这个准了后面不准也没用,所以光靠前面抑制其实也不太好

损失函数

VGN 的损失函数通常是针对前文所述三个输出分别计算,然后加权求和。

  • 质量损失:通常使用二元交叉熵损失(Binary Cross-Entropy Loss),因为这里是一个 0/1 二分类变量

    但老师后面又说了这个抓取质量的指标显然不是一个阶跃的,而是 具有一定平滑性 的,在一个点能抓起来,其附近也应当能抓起来,这就是为什么要进行高斯核平滑后处理

  • 方向损失:L2,但只对那些真实抓取标签为正的体素(就是真的能抓起来的地方)计算。

  • 宽度损失:同上

Sim2Real Gap

VGN 使用了大量合成数据进行训练,但能够在真实机器人上较好地工作(Sim2Real Transfer),关键原因在于其依赖的是几何表征,它不考虑颜色、纹理等视觉信息,只关注物体的形状和抓取器的几何匹配。

Sim2Real 的工作都会有 Gap,但是否 Work 要看 Gap 重不重要,影响大不大。

  • 合成数据中的深度信息是完美的,而真实传感器采集的深度图存在噪声
  • VGN 使用的 TSDF 表征,特别是当体素分辨率不高时(例如,40x40x40 的格子,每个格子边长可能达到厘米级),对几毫米级别的深度噪声不敏感。小的表面凹凸或噪声在体素化后会被平滑掉,不会显著改变 TSDF 的值。
  • 因此,即使训练于完美深度数据,模型在面对带噪声的真实深度时,性能下降有限。

对于夹爪式抓取,现实中的成功率往往不低于甚至高于仿真(Sim2Real 甚至可以是负的!)

  • 力闭合与变形:夹爪在闭合时通常会持续施力直至完全闭合或达到力 / 行程限制。这个过程可以轻微移动物体、压紧物体,甚至使软性物体发生形变,从而形成更稳固的接触面。这些物理效应在标准仿真中可能未被完全模拟,但在现实中是有利的。
  • 摩擦力问题:如果担心仿真中摩擦系数不准(如仿真中认为能抓住,现实中太滑抓不住),可以通过简单的工程手段解决,例如在夹爪指尖贴上高摩擦系数的材料(如橡胶垫)。
  • 仿真中的 Artifacts:有时仿真环境自身的问题(如碰撞检测不准、物理模拟不稳定)反而会导致仿真中抓不住,而现实中没问题。

机器人学是一个应用学科,最终目标是解决问题,真机表现是最终检验标准。

VGN 的局限性

  • 多视角依赖:对于相互遮挡严重的场景(cluttered scene),单视角可能无法看到被遮挡物体的完整几何形状,导致 TSDF 不准确,进而无法规划出好的抓取。VGN 需要较好的多视角观测来构建完整的场景 TSDF。
  • 精度限制:由于使用体素表示,其抓取位姿(特别是平移)的精度受限于体素大小。理论上的最高平移精度约为半个体素边长。这对于需要高精度操作的任务可能是个问题。
  • 计算 / 内存与精度的权衡:提高体素分辨率可以提升精度,但会导致内存和计算量急剧增加(通常是分辨率的三次方)。

基于点云的抓取(GSN / GraspNet)

点云是另一种重要的三维表示。

  • 轻量级 / 高效性:点云只表示物体表面,不像体素需要表示整个空间(包括空白区域)。对于同样场景,点云的点数通常远少于体素数(几万点 vs 几十万体素)。
  • 高分辨率 / 精度:理论上,点云中每个点的坐标可以是连续值,可以达到很高的空间分辨率,只要相机 / LiDAR 精度足够。

GraspNet 架构

GraspNet

GraspNet 将复杂的六自由度抓取姿态预测分解为一系列更简单的问题。

先大致说一下方法:

  1. 在表面选择接触点
  2. 在接触点为中心的一个半球面上均匀采样 256 个方向,得到一个旋转轴
  3. 绕旋转轴旋转夹爪
  4. 沿旋转轴深入夹爪

整个过程将原本抓取位姿的 6 DoF 自由度进行了多阶段划分

  1. 位移的 3 DoF
    1. 接触点的选择带来了 2 DoF
    2. 深入夹爪带来了 1 DoF
  2. 旋转的 3 DoF
    1. 旋转轴的轴向带来了 2 DoF
    2. 夹爪绕旋转轴旋转角度带来了 1 DoF

真实操作流程:

  1. 网络首先在输入点云的每个点上预测一个 “可抓取性” 分数(Graspness Score),表示该点作为抓取接触点的优劣程度。

  2. 保留分数高的点作为候选抓取中心点(从 N 个点降到 M 个点)。

  3. 对于每个候选点,预测最佳的抓取器接近方向(Approach Vector / View)。这通常是在以该点为中心的半球面上采样多个方向进行评估。

  4. 对于选定的 “点 - 方向” 对,需要确定绕着接近方向的旋转角(In-plane Rotation Angle)以及夹爪最终的张开宽度或深入深度(Depth)。

    Cylinder Grouping:在候选点附近,沿着接近方向定义一个圆柱体区域,聚合该区域内所有点的特征。这个聚合后的特征被用来预测最佳的旋转角和深度 / 宽度。

GraspNet 成功的本质

  1. 点云的优良性质:准确、轻量、效率高、精度高
  2. 架构:端到端网络,多阶段设计,每个阶段都有监督信号,稳定
  3. 泛化性:局部性与平移等变性
    1. 局部性:Cylinder Grouping 聚合,依赖候选点周围的局部几何信息判断,而不太关心场景中其他远处的物体。
    2. 平移等变性(Translation Equivariance):类似二维情形,模型学习到的几何模式识别能力不随物体在空间中的位置变化而失效。

GraspNet 的核心在于学习 局部几何特征(Local Geometric Features) 与抓取成功的关系。

例如,一对平行的小平面、一个合适的边缘或角落,这些局部形状无论出现在哪个物体上,都可能指示一个好的抓取点。当模型在训练数据(如 GraspNet-1B 的数百个物体)中见识了足够多样的局部几何模式后,就能泛化到包含相似局部几何的新物体上,即使整体形状从未见过。

这个局部泛化是非常本质的,因为它对某一位置是否适合抓进行了深入学习。

抓取的条件生成模型

无论是 VGN 还是 GraspNet,它们本质上是 检测(Detection) 方法。它们从场景中预测(检测)出一系列离散的、得分较高的抓取候选。最后通常还需要进行非极大值抑制(NMS)来去除冗余的候选。然而,理论上一个物体可能有无限多种抓取方式,检测式方法只能给出有限的几个解。

随着生成模型(如 GANs、VAEs、Diffusion Models)的发展,研究者开始探索直接生成抓取姿态的方法。目标是学习抓取姿态的 分布(Distribution),然后从中采样。

动机:对于具有高自由度(如 20+ DOF)的灵巧手(Dexterous Hand),抓取姿态空间巨大,传统的采样 + 评估或检测方法变得非常困难。生成模型提供了一种直接建模和采样高维复杂分布的途径。

不过,训练强大的生成模型通常需要极大规模的数据集(DexGraphNet 使用了一个包含 10 亿级别抓取样本的数据集)。生成如此规模的灵巧手抓取数据本身就是一个挑战,需要专门设计的抓取规划与优化管线。

该工作采用 条件扩散模型

  1. 首先,类似 GraspNet,在点云上识别出潜在的、适合抓取的接触点
  2. 在选定的接触点周围(用一个球形区域) 提取局部几何特征 $F$。这个特征 $F$ 编码了该点附近的形状信息
  3. 条件扩散,逐步去噪,学习出条件概率分布

如果你对扩散模型 / 条件扩散模型 / DDIM 的推导感兴趣,笔者推荐如下内容:

💾

  •  

佳能R50和索尼ZV-E10Ⅱ,最终6120块选了ZV-E10Ⅱ

一直想要一个轻巧方便携带的相机,主要用来练手,出门拍拍花草山水之类的。看了佳能R50和索尼ZV-E10Ⅱ,都符合我的要求。

本打算买个佳能R50,以后能无缝衔接,毕竟佳能给我的感觉十分好。又看了索尼ZV-E10Ⅱ,感觉这个挺好看的尤其是白色。

总结一下我眼中佳能R50与ZV-E10Ⅱ的一些区别:

设备/功能佳能R50ZV-E10Ⅱ
取景器电子取景器
对焦速度尚可相对稍快
视频拍摄尚可相对较好
套机镜头焦段18-45mm16-50mm
套机镜头调焦手动调焦手动+电动调焦
人像拍摄面部皮肤自然面部皮肤稍白

这两个相机,最终选了索尼ZV-E10Ⅱ,到店那样机把玩试试,总体感觉可以。国补后6120元,送了相机包、屏幕钢化膜、相机套、充电器。

刚开始冲着白色去的,因为白色外观视觉效果十分抢眼,很好看。当我看到样机外观的小瑕疵时决定买黑色,因为这些小瑕疵在白色机壳上十分明显,黑色却看不出这样的瑕疵。这点瑕疵不影响功能,但作为二手机出售时会影响价格。

又一个可能吃灰的设备到手,相机到手还有其他东西要买,偏光镜、滤镜、补光灯、镜头。还好索尼的镜头比较多,价格适中,以后有需要了再添置。

  •  

家庭数据中心系列 “API Shield” 深度解析:构建更安全的 Cloudflare API 防护体系

家庭数据中心系列 “API Shield” 深度解析:构建更安全的 Cloudflare API 防护体系 无敌的个人博客 tangwudi

1 前言 在Cloudflare仪表盘的”安全性”选项卡下,有一个”API Shield(API防护)”的功能: 这个功能一看就是专门用来保护API的,不过之前我也没有自建的、需要保护的API,所以也没机会白嫖研究这个功能。 但是,随着最近这段时间我写了2篇涉及自建API的文章(参见家庭数据中心系列 构建高效且安全的随机图片API:Cloudflare Worker + R2 + KV 实战指南和家庭数据中心系列 Cloudflare Worker + KV:打造 WordPress 云端文章阅读统计),忽然发现我已经具备研究这个功能的素材了,这周的研究目标就选”API Shield”吧。 注:最近Cloudflare已经推出了新的仪表盘,功能布局变化非常之大,就比如这个”API Shield” […]

<p>The post 家庭数据中心系列 “API Shield” 深度解析:构建更安全的 Cloudflare API 防护体系 first appeared on 无敌的个人博客.</p>

  •  

PageSpeed Insights 94分,目前测试的最高分,特此发贴纪念一下,哈哈

PageSpeed Insights 94分,目前测试的最高分,特此发贴纪念一下,哈哈 无敌的个人博客 tangwudi

今天忽然兴起,又用PageSpeed Insights跑了一下分,结果居然到了94分(之前最高91分),按PageSpeed Insights这种对使用了CDN的站点运气型测试的尿性,以后大概率也很难人品爆棚的出现这么高的分了,所以特此发个说说纪念一下我的人生巅峰~:

<p>The post PageSpeed Insights 94分,目前测试的最高分,特此发贴纪念一下,哈哈 first appeared on 无敌的个人博客.</p>

  •  

VLA Frontier

AIGC Declaration

本文使用了 AIGC 来提高效率,其中可能存在谬误,我已尽力检查并校对,但仍不保证完全准确,欢迎指正。

本文依赖于我编写的 arXiv Tex 源码获取 Pipeline,这里是 Repo,欢迎使用!

HybridVLA

Paper

hybirdvla

Insight

  1. 传统自回归(AR,RT-2/OpenVLA)方法为了将动作作为 token 用 LLM 去预测,将动作离散化,破坏了动作连续性
  2. 扩散方法(Diffusion,CogACT/DiVLA)的扩散头独立于 LLM,无法利用语言模型的推理能力
  3. 设计一种办法协同 AR 和 Diffusion,从而兼顾两者的优点,同时充分利用 LLM

Method

Arch

Backbone:

  1. Vison Encoder:DINOv2(语义特征)+ SigLIP(细粒度特征)
  2. Prompt Encoder:LLAMA-2 (7B) / Phi-2 (2.7B)

整体 Token 序列结构:

$$ \text{Input Tokens} = \underbrace{[V_1,...,V_N]}{\text{视觉}} \oplus \underbrace{[L_1,...,L_M]}{\text{语言}} \oplus \underbrace{[R]}_{\text{机器人状态}} \oplus \underbrace{[\text{}, a^{i}t, i, \text{}]}{\text{扩散部分}} \oplus \underbrace{[A^{ar}_1,...,A^{ar}K]}{\text{AR 动作}} $$

  1. 编码后(V,L,R),插入一个特殊的扩散开始 Token $\text{}$ 与掩码 $\text{}$ $$ \text{Input Tokens} = \underbrace{[V_1,...,V_N]}{\text{视觉}} \oplus \underbrace{[L_1,...,L_M]}{\text{语言}} \oplus \underbrace{[R]}_{\text{机器人状态}} \oplus \text{} \oplus \text{} $$

  2. 然后进行扩散 Token 预测,使用得到的 Token 进行去噪,得到扩散动作 $a^d$

    $$ a^d = a^0 = [\Delta x, \Delta y, \Delta z, \text{Roll}, \text{Pitch}, \text{Yaw}, \text{Gripper(0/1)}] $$

  3. 对得到的扩散动作 $a^d$,重新使用 MLP 映射回 LLM,得到 $e_{a^d}$,插入特殊的扩散结束 Token $\text{}$,重构得到序列

    $$ [V][L][R][\text{}][e_{a^d}][\text{}][\text{}] $$

  4. 基于新序列预测 AR Token,再经过 Detokenizer,得到动作 $a^{ar}$(动作离散到 256 个动作区间,概率值)

  5. 计算 AR 动作置信度 $c^{ar}$

    $$ c^{ar} = \frac{1}{7}\sum_{k=1}^7 \max(p(A_k)) $$

  6. 根据置信度,判断是要融合 AR 动作与扩散动作还是直接使用扩散动作 $$ a_{final} = \begin{cases} 0.5a^d + 0.5a^{ar}, & \text{if } c^{ar} > 0.96 \ a^d, & \text{otherwise} \end{cases} $$

直观理解

  1. 扩散模式:自动驾驶(精确控制油门 / 刹车)
  2. AR 模式:语音导航("前方路口左转")
  3. 当导航指令清晰时(高置信度),自动驾驶会参考语音提示;当导航模糊时,完全依赖自动驾驶

现在,HybridVLA 既保持了语言模型的强推理能力,又获得了物理级的动作连续性,突破了传统 VLA 模型的性能瓶颈。

Loss function

$$ \mathcal{L}{dif}=E{a,i,c}||\epsilon-\epsilon_\pi(a_t^i,i,c)||^2 \ \mathcal{L}{hybrid}=\mathcal{L}{dif}+\mathcal{L}_{ce} $$

Trick

  • KV 缓存加速
  • 降低 Diffusion 去噪步数以加速生成

Question

为什么 AR 不加 diffusion,难道没语义了吗

ManipLLM

Paper

manipllm

Why

  • 基于有限数据集学习的方法见过的物品类别是有限的,难以泛化到现实世界
  • 过往的模型无法解释自身的结果(可解释性差),是个黑箱

Insight

  1. 通过 类别 → 区域 → 位姿 的渐进式训练将 MLLM(多模态大语言模型,Multimodal Large Languege Model)基于互联网级别数据所习得的常识和推理能力与之前看似黑箱的机器人操作去逐渐对齐,类似 COT 思维链完成渐进式思考,从而得到由粗到细的高可解释性动作预测
  2. 直接让 MLLM 去对图片进行预测哪里可以动可能效果是不 OK 的,但根据 Affordance Map 生成若干个点来让 MLLM 进行选择(选择题比填空题好做)是 OK 的。

Method

Arch

Backbone:

  • 视觉编码器:CLIP 的 ViT
  • 文本编码器:LLaMa 的 Tokenizer
  • 多模态对齐:通过适配器(Adapter)将视觉特征与 LLaMa 的文本空间对齐,仅微调适配器参数,保留 MLLM 原有知识。

Loss Function

$\mathcal{L}_A$ 可供性损失

目标:教会模型识别物体表面可操作区域

训练方式:

  1. 首先根据可供性图 $\mathcal{A}$ 来在图片中随机选择一系列点,包括 $n$ 个正样本($\mathcal{A} \geq 0.8$)、 $n$ 个负样本($\mathcal{A} \geq 0.8$),分别标记为 1、0
  2. 将点的位置送入 MLLM,进行提问:“确定在以下每个点上操作是否可以有效地操纵图像中的对象?” + ${x_i, y_i}^{2n}_{i=1}$
  3. 获得模型输出词元概率序列 ${p_i}^n_{i=1}$,注意这里不是 0/1,而是 LLM 输出此处为 True 这个词元的概率
  4. 计算交叉熵损失: $$ \mathcal{L}A = -\frac{1}{2n} \sum{i=1}^{2n} \left[ y_i \log p_i + (1 - y_i) \log (1 - p_i) \right] $$
$\mathcal{L}_M$ 语言建模损失

目标:通过 “填空” 训练模型预测被遮挡的位姿参数

训练方式 MLM(Mask Language Modeling,完形填空) :

  1. 随机遮挡坐标或方向分量,如将 “接触点是 $(80,120)$” 改为 “接触点是 $(\text{[MASK]},120)$”
  2. 每个被遮挡值离散化为 100 个区间
  3. 模型预测被遮挡位置的类别概率分布 $q_j$,计算交叉熵(真实标签以 one-hot 编码,$c_j$ 为真实类别编号): $$ \mathcal{L}M = -\sum{j \in \text{masked}} \log q_j[c_j] $$
$\mathcal{L}_F$ 位姿预测损失

目标:直接训练模型预测完整位姿参数,包括:

  • 接触点坐标 $(x, y)$
  • 夹爪上方向 $(x_u, y_u, z_u)$
  • 夹爪前方向 $(x_f, y_f, z_f)$

注:三维空间坐标由深度图投影得到。

训练方式:类似 $\mathcal{L}_M$,用 MLM 方式来计算损失

总损失

$$ \mathcal{L} = \mathcal{L}_A + \mathcal{L}_M + \mathcal{L}_F $$

注意这里,$\mathcal{L}_A$ 提供的区域先验可以帮助 $\mathcal{L}_M$ 和 $\mathcal{L}_F$ 更准确定位接触点。

  1. $\mathcal{L}_A$ 先教会模型 “哪里能操作”
  2. $\mathcal{L}_M$ 再训练 “如何补全参数”
  3. $\mathcal{L}_F$ 最终实现 “端到端预测”

主动阻抗适应策略

问题:方向预测可能存在误差

解决办法:在初始方向附近随机添加多个扰动方向,随后挨个试,每个施加一个固定的阻抗力,测量位移,选择最大的位移方向。

测试时适应(TTA)

问题:Sim-to-Real 差异(如光照、纹理变化)导致位姿预测偏移。

策略:在线更新视觉适配器(Visual-Adapter,连接 CLIP 视觉编码器和 LLaMa 语言模型,参数很少,就是一个轻量 MLP)参数

  1. 输入当前测试样本的位姿预测结果 $(x,y)$
  2. 根据实际操作成败生成二元标签(成功 → “yes”,失败 → “no”)
  3. 通过 $\mathcal{L}_A$ 微调视觉适配器,适应真实场景的视觉特征。

π0

pi0

Why

  1. 现有数据集太少,无法习得通用能力
  2. 基于 AR 的动作生成方法难以实现高频控制(但现有的基于扩散的模型已经改进了一些),流匹配是扩散的一种变体,适合生成高频、复杂、精细的动作块

Insight

  1. VLM + Flow Matching = new VLA
  2. 不能只在高质量数据集上训练,否则鲁棒性(容错性)不强,无法在真实世界中使用,解决方案是先在低质量、大量的混合机器人数据上学习,然后再在高质量数据集上进行微调,精进技能

Method

基本就是引入了流匹配来替换扩散模型,这是一种相较于扩散更直观的生成模型,关于流匹配的推导、代码和直观讲解可以参见 Meta 的综述

flow_matching

flow_matching_with_cond

RoboFlamingo

Paper / 作者解读

roboflamingo

Insight

感觉没啥新的,可能是我看的顺序问题,先看了今年 / 去年的,潜移默化地感觉这个结构似乎已经是一个范式了。

Method

Arch

  1. ViT(预训练) + Resampler 下采样(通过自注意力机制实现)降低 Token 数量,得到视觉 Token

    $$ \hat{X}_t^v=\text{ViT}(I_t,G_t) \ \text{Resampler: }K_R=\hat{X}_t^vW_K^R, V_R=\hat{X}_t^vW_V^R, X_t^v=\text{softmax}(\frac{Q_RK_R^T}{\sqrt{d}})V_R $$

  2. LLM(预训练)得到文本 Token

    $$ X = X_t^1=\text{LLM}(L_t) $$

  3. 特征融合:堆叠 $L$ 层解码器,每层结构包括:

    1. 使用交叉注意力,以 Text Token 做 Query,Visual Token 做 Key / Value,进行残差连接
    2. 随后进行自注意力,依旧进行残差连接,从而完成视觉与语言特征的融合

    $$ \begin{aligned} &\hat{X}_t^l=\text{Tanh}(\alpha)\cdot\text{MLP}(A(X_t^lW_Q^C,X_t^vW_K^C,X_t^vW_V^C))+X_t^l,\ &X_t^{l+1}=\text{MLP}(A(\hat{X}_t^lW_Q^S,\hat{X}_t^lW_K^S,\hat{X}_t^lW_V^S))+\hat{X}_t^l \end{aligned} $$

  4. max pooling 后送入策略头,以一个循环模型(LSTM)进行时序建模,直接预测 7 DoF 动作 $$ \tilde{X}_t=\mathrm{MaxPooling}(X_t)\ h_t=\mathrm{LSTM}(\tilde{X}t,h{t\boldsymbol{-}1})\ a_t^{pose},a_t^{gripper}=\mathrm{MLP}(h_t) $$

Train

监督信号:专家示范动作

  • 位姿预测:MSE 损失
  • 夹爪状态:BCE 损失
  • 总损失: $$ \mathcal{L} = \sum_t |a_t^{pose} - \hat{a}_t^{pose}|^2 + \lambda \cdot \text{BCE}(a_t^{grip}, \hat{a}_t^{grip}) $$

微调策略

  • 仅训练:重采样器参数 + 交叉注意力层 + 策略头
  • 冻结:ViT 参数 + 语言模型参数
  • 结果:参数量 <1% 的微调,高效且防过拟合

RoboMamba

Paper

Mamba

mamba

Mamba Youtube 讲解 / CSDN

传统模型的问题:

  1. Transformer 自注意力机制的计算复杂度为 $O(L^2)$($L$ 为序列长度),资源需求量大

  2. RNN 等在反向传播的时候需要沿着时间维度逐步进行(Backpropagation through time),无法并行训练;且长程依赖关系容易造成梯度消失 / 爆炸,尽管 LSTM 等通过门控机制缓解,但并未完美解决。

    RNN 的本质是一个这样的函数:

    $$ h_{t+1} = f(h_t, x_{t+1}) $$

SSM

  1. 本质类似 RNN,但是在训练的时候无需像 LSTM 一样总要等到隐状态沿着时间维度完整前传,而是类似 Transformer,可以并行地处理所有 Token
    1. 隐状态之间没有非线性,而是具有了很好的线性性质,可以直接化为一个完整的矩阵乘法
    2. 没有时间依赖性(线性非时变系统),$A$ 和 $B$ 在整个前向推理过程中不变,从状态 1 转到状态 2,和从状态 2 转到状态 3 是一样的,换句话说聚合信息的方式是恒定的
  2. 推理时像无隐状态的线性 RNN,可以并行地推导所有步骤的输出,而无需像 Transformer 一样以自回归地形式一个 Token 一个 Token 地输出(因为 Transformer 在推理过程中的注意力矩阵是动态构建的)

以下为 S4 的数学推导,摘录整理自 这里,补全了最后一步的跳步。

状态空间模型将系统的状态、输入和输出关系表示为:

$$ \begin{aligned} \dot{x}(t) &= A(t)x(t) + B(t)u(t)\ y(t) &= C(t)x(t) + D(t)u(t) \end{aligned} $$

其中,$A,B,C,D$ 是系数矩阵,$x(t)$ 是状态向量,$u(t)$ 是输入向量,$y(t)$ 是输出向量。

假定系数矩阵不随时间变化,这可以简化为线性非时变系统:

$$ \begin{aligned} \dot{x}(t) &= Ax(t) + Bu(t)\ y(t) &= Cx(t) + Du(t) \end{aligned} \tag{1} $$

容易发现,核心其实是第一个式子,但若直接对状态方程积分:

$$ x(t) = x(0) + \int_0^t (Ax(\tau) + Bu(\tau))\mathrm{d}\tau $$

积分项包含 $x(\tau)$ 本身,但我们无法获取连续时间内所有 $x(\tau)$ 值,导致积分无法完成。

所以,我们将上式转换为离散形式:

$$ x(k+1) = x(k) + \sum_{i=0}^k (Ax(i) + Bu(i))\Delta t $$

但这仍需要改造原方程,消除 $\dot{x}(t)$ 表达式中的 $x(t)$ 从而可以积分。

构造辅助函数 $\alpha(t)x(t)$ 并求导:

$$ \frac{\mathrm{d}}{\mathrm{d}t}[\alpha(t)x(t)] = \alpha(t)\dot{x}(t) + x(t)\frac{\mathrm{d}\alpha(t)}{\mathrm{d}t} $$

代入状态方程 $(1)$:

$$ \frac{\mathrm{d}}{\mathrm{d}t}[\alpha(t)x(t)] = \alpha(t)(Ax(t) + Bu(t)) + x(t)\frac{\mathrm{d}\alpha(t)}{\mathrm{d}t} $$

合并 $x(t)$ 的相关系数:

$$ \frac{\mathrm{d}}{\mathrm{d}t}[\alpha(t)x(t)] = \left(A\alpha(t) + \frac{\mathrm{d}\alpha(t)}{\mathrm{d}t}\right)x(t) + B\alpha(t)u(t) \tag{2} $$

为消除导数中的 $x(t)$,令其系数为 $0$:

$$ A\alpha(t) + \frac{\mathrm{d}\alpha(t)}{\mathrm{d}t} = 0 $$

解得:

$$ \alpha(t) = e^{-At} $$

代入 $(2)$:

$$ \frac{\mathrm{d}}{\mathrm{d}t}[e^{-At}x(t)] = Be^{-At}u(t) $$

对此式积分:

$$ e^{-At}x(t) = x(0) + \int_0^t e^{-A\tau}Bu(\tau)\mathrm{d}\tau $$

整理得到:

$$ x(t) = e^{At}x(0) + \int_0^t e^{A(t-\tau)}Bu(\tau)\mathrm{d}\tau $$

在离散系统中:

  • 定义采样时刻 $t_k$ 和 $t_{k+1}$,采样间隔 $T = t_{k+1} - t_k$
  • 将连续时间积分区间分成离散子区间:

$$ x(t_{k+1}) = e^{A(t_{k+1}-t_k)}x(t_k) + \int_{t_k}^{t_{k+1}} e^{A(t_{k+1}-\tau)}Bu(\tau)\mathrm{d}\tau \tag{3} $$

采用零阶保持法,假设 $u(t)$ 在采样间隔内保持恒定:

$$ \int_{t_k}^{t_{k+1}} e^{A(t_{k+1}-\tau)}Bu(\tau)\mathrm{d}\tau = \int_{t_k}^{t_{k+1}} e^{A(t_{k+1}-\tau)}\mathrm{d}\tau \cdot Bu(t_k) $$

代入 $(3)$,并使用 $T = t_{k+1} - t_k$:

$$ x(t_{k+1}) = e^{AT}x(t_k) + \int_{t_k}^{t_{k+1}} e^{A(t_{k+1}-\tau)}\mathrm{d}\tau \cdot Bu(t_k) $$

引入变量替换 $\lambda = t_{k+1} - \tau$:

$$ x(t_{k+1}) = e^{AT}x(t_k) + Bu(t_k)\int_0^T e^{A\tau}\mathrm{d}\tau $$

原文这里略有跳步,只需要展开矩阵指数然后假设 $A$ 可逆从而合并系数再重新合并成矩阵指数即可:

$$ e^{A\tau} = I + A\tau + \frac{(A\tau)^2}{2!} + \frac{(A\tau)^3}{3!} + \dots $$

$$ \begin{aligned} \int_0^T e^{A\tau} \mathrm{d}\tau &= \int_0^T \left( I + A\tau + \frac{(A\tau)^2}{2!} + \frac{(A\tau)^3}{3!} + \dots \right) \mathrm{d}\tau \ &= \int_0^T I \mathrm{d}\tau + \int_0^T A\tau \mathrm{d}\tau + \int_0^T \frac{(A\tau)^2}{2!} \mathrm{d}\tau + \dots \ &= T \cdot I + \frac{A T^2}{2} + \frac{A^2 T^3}{3 \cdot 2!} + \frac{A^3 T^4}{4 \cdot 3!} + \dots \ &= \sum_{k=0}^{\infty} \frac{A^k T^{k+1}}{(k+1)!} \ &= \sum_{m=1}^{\infty} \frac{A^{m-1} T^m}{m!} \quad \text{换元:} m = k+1 \ &= A^{-1} \sum_{m=1}^{\infty} \frac{(A T)^m}{m!} \ &= A^{-1} (e^{A T} - I) \end{aligned} $$

最终离散时间状态方程:

$$ x(t_{k+1}) = e^{AT}x(t_k) + (e^{AT} - I)A^{-1}Bu(t_k) $$

容易想到这里还是会存在类似 RNN 的长程依赖问题,Mamba 最终其实相对于 S4 做了很多改进,包括 HiPPO(处理远程依赖性)等,这里就没详细去看了(逃)

Insight

robomamba

Training

robomamba_training

对齐预训练

数据:LLaVA 图像 - 文本对

目的:使用单一 MLP 对齐视觉特征编码与 Mamba 词嵌入

冻结 CLIP、Mamba,仅微调 Project MLP 投影层。

令对齐预训练数据集为 $\mathcal{D}a = {(I_k, T_k)}{k=1}^N$,其中:

  • $I_k \in \mathbb{R}^{W \times H \times 3}$:图像输入
  • $T_k = [t_1^{(k)}, t_2^{(k)}, ..., t_L^{(k)}]$:对应的文本描述(token 序列)

那么:

$$ p(y|I) = \text{Softmax}(\text{Mamba}([\text{Proj}(\text{Emb}(I)); \text{}])) \ \mathcal{L}a = -\sum{k=1}^N \sum_{t=1}^{L_k} \log p(t_t^{(k)} | t_{<t}^{(k)}, I_k) $$

指令协同训练

目的:学习长程规划、物理常识等技能

数据:$\mathcal{D}c = \mathcal{D}{gen} \cup \mathcal{D}_{robot}$ 为混合指令数据集

  • $\mathcal{D}_{gen}$:通用视觉指令数据(如 ShareGPT4V)
  • $\mathcal{D}_{robot}$:高级机器人指令数据(如 RoboVQA)

冻结 CLIP,微调 Project MLP 投影层、Mamba。

先在通用的上面训练,然后再在高级数据集上训练,损失函数为交叉熵。

这里不知道有没有采用渐进式混合 $\mathcal{L}c = \lambda \mathcal{L}{gen} + (1-\lambda)\mathcal{L}_{robot}$

这个阶段挺重要的,原文说跳过此处训练直接进行动作微调时,成功率从 82.3% 骤降至 47.1%。

动作微调

目的:训练动作策略头,获得操作能力

冻结 CLIP、Project MLP 投影层、Mamba,仅调整策略头。

$$ \begin{align} \mathcal{L}{pos} &= \frac 1N {\sum{i=1}^N |a_\mathrm{pos} - a^{gt}\mathrm{pos}|} \ \mathcal{L}{dir} &= \frac 1N {\sum_{i=1}^N \arccos\left (\frac{{\text{Trace}\Big(a^{gt}\mathrm{dir}}^\top a\mathrm{dir}\Big)-1}{2}\right )} \end{align} $$

注:两个旋转矩阵的乘积 $R^\top R_{gt}$ 表示相对旋转;对于旋转矩阵 $R$,其迹与旋转角度 $\theta$ 满足:

$$ \text{Trace}(R) = 1 + 2\cos\theta $$

从而通过迹可直接计算两个旋转矩阵之间的角度差异。

GR-1

Paper / Project Page

Generative Robot-1

gr1

Insight

  1. 数据瓶颈突破:传统视觉机器人操作受限于小规模机器人数据(高采集成本),而视频数据与机器人轨迹具有内在一致性(时间序列 + 多模态)
  2. 统一建模优势:GPT-style Transformer 可同时处理语言、图像、机器人状态,避免传统方法中多模块拼接的复杂性
  3. 预训练 - 微调协同:视频预测任务(预测未来帧)隐式学习物理规律,迁移到机器人动作推理时提升泛化能力

核心贡献:首次证明大规模视频生成预训练可迁移到机器人操作,统一 GPT 架构实现多模态 - 多任务端到端学习。

Method

Arch

Backbone:

  1. Vision Encoder:MAE 预训练的 ViT(图像 → patch tokens + CLS token)
  2. Language Encoder:冻结的 CLIP 文本编码器
  3. State Encoder:MLP 编码机器人末端位姿(6D)和夹爪状态(二进制)

Token 序列构造:

首先,所有模态的嵌入(图像、语言、状态)都通过线性变换映射到同一维度 $d$,然后将所有模态的 Token 拼接成一个序列。

视频生成预训练时:

$$ \text{Input Tokens} = \underbrace{[l]}{\text{语言}} \underbrace{[o{t-h}]}{\text{图像}} \underbrace{[\text{OBS}]}{\text{视频预测 cls}} \oplus \cdots \oplus [l][o_t][\text{OBS}] $$

使用机器人数据微调时:

$$ \text{Input Tokens} = \underbrace{[l]}{\text{语言}} \underbrace{[s{t-h}]}{\text{状态}} \underbrace{[o{t-h}]}{\text{图像}} \underbrace{[\text{OBS}]}{\text{视频预测 cls }} \underbrace{[\text{ACT}]}_{\text{动作预测 cls}} \oplus \cdots \oplus [l][s_t][o_t][\text{OBS}][\text{ACT}] $$

  1. 模态对齐:语言 Token $l$ 在每个时间步重复,防止被其他模态掩盖
  2. 因果注意力掩码:只能往前看,不能往后看
    • 预训练时掩码未来 $\text{[OBS]}$ Token
    • 微调时同时掩码 $\text{[OBS]}$ 和 $\text{[ACT]}$ Token
  3. 时间嵌入:每个时间步添加可学习的时间戳编码

训练流程

gr1_encoder_decoder

预训练阶段(视频生成)

输入:语言描述 + 历史帧序列

输出:未来帧预测(MSE 损失,和 MAE 重构损失一样,直接就是判断像素差)

$$ \mathcal{L}{\text{video}} = \frac{1}{H \times W} \sum{i=1}^H \sum_{j=1}^W \left( \hat{o}{t+\Delta t}(i,j) - o{t+\Delta t}(i,j) \right)^2 $$

  • $\hat{o}_{t+\Delta t}$:预测的未来帧
  • $o_{t+\Delta t}$:真实的未来帧
微调阶段(机器人操作)

输入:语言指令 + 历史状态 / 图像序列

输出:动作(连续位移 + 夹爪开合) + 未来帧预测

动作损失(Smooth L1):

$$ \mathcal{L}{\text{arm}} = \frac{1}{N} \sum{i=1}^N \begin{cases} 0.5 (a_{\text{arm}}^i - \hat{a}{\text{arm}}^i)^2, & \text{if } |a{\text{arm}}^i - \hat{a}{\text{arm}}^i| < 1 \ |a{\text{arm}}^i - \hat{a}_{\text{arm}}^i| - 0.5, & \text{otherwise} \end{cases} $$

  • $N$:批量大小(Batch Size)
  • $a_{\text{arm}}$:真实动作,$\hat{a}_{\text{arm}}$:预测动作,就是位移和旋转那六个自由度的数值

Smooth L1 Loss 是回归任务中常用的损失函数,结合了 L1 Loss 和 L2 Loss 的优点。其公式为:

$$ \text{SmoothL1}(x) = \begin{cases} 0.5x^2 & \text{当 } |x| < 1 \ |x| - 0.5 & \text{其他情况} \end{cases} $$

其中 $x = y_{\text{pred}} - y_{\text{true}}$ 表示预测值与真实值的差。

特点

  1. 在 $|x| < 1$ 时使用二次函数(类似 L2 Loss),梯度平缓,避免离群值梯度爆炸;
  2. 在 $|x| \geq 1$ 时使用线性函数(类似 L1 Loss),降低大误差时的梯度幅值;
  3. 在 $x=0$ 处可导,优化更稳定。

夹爪动作损失(Binary Cross-Entropy):

$$ \mathcal{L}{\text{gripper}} = -\frac{1}{N} \sum{i=1}^N \left[ y_i \log p_i + (1 - y_i) \log (1 - p_i) \right] $$

  • $y_i$:真实标签(0 或 1)
  • $p_i$:预测为张开状态的概率

总损失:

$$ \mathcal{L}{\text{finetune}} = \mathcal{L}{\text{arm}} + \mathcal{L}{\text{gripper}} + \mathcal{L}{\text{video}} $$

TinyVLA

Paper / Project Homepage

Insight

  1. 传统 VLA 模型依赖大型 VLM + AR,速度慢、推理延迟高
  2. 数据依赖问题

Method

  1. 使用小型 VLM Backbone
  2. 冻结预训练权重,仅微调部分参数(LoRA),保留多模态理解能力,减少数据依赖
  3. 使用扩散策略头来生成最终动作,以多模态主干输出的嵌入(图像 + 语言指令)作为扩散过程的控制条件

DiffusionVLA

Paper

没看懂他的 FiLM 注入模块是如何实现的。

推理标记通过 FiLM 层注入策略模型,FiLM 层对策略内部投影层的参数进行缩放和偏移。

film_vs_transformer

Reference

CogACT

Paper

Condition and Action

cogact

Insight

  1. VLM 直接将动作离散化为 Token 预测,忽略了动作的连续性和多模态性,导致成功率差、精度低
  2. 动作信号具有连续性、多模态性(同一任务有多个可行轨迹)、时序相关性,与语义 Token 有本质不同
  3. 模仿人脑功能划分,用 VLM 处理认知(理解任务),DiT 处理动作生成

Method

Arch

Backbone:

  1. Vision Encoder:DINOv2 + SigLIP
  2. LLM:LLaMA-2 7B
  3. Action:DiT

整体 Token 序列结构:

$$ \text{Input Tokens} = \underbrace{[V_1,...,V_{N_v}]}{\text{视觉}} \oplus \underbrace{[L_1,...,L{N_l}]}{\text{语言}} \oplus \underbrace{[C]}{\text{认知}} $$

使用因果注意力机制聚合信息后,得到认知特征 $f_t^c \in \mathbb{R}^{d_c}$。

$f_t^c, (a_t^i, a_{t+1}^i, ..., a_{t+N}^i)$ 作为动作模块的条件,进行条件扩散生成。

也即,训练网络学会从带噪声(人为加噪)的动作序列 $(a_t^i, a_{t+1}^i, ..., a_{t+N}^i)$ 中恢复出干净的动作序列 $(a_t, a_{t+1}, ..., a_{t+N})$。

其中:

  • 符号 $i$ 表示去噪步骤的索引,会通过位置编码加入到认知特征 $f_t^c$ 中
  • $t$ 表示时间步

Loss function

$$ \mathcal{L}_{\text{MSE}} = \mathbb{E}||\boldsymbol{\hat{\epsilon}}^i - \boldsymbol{\epsilon}||_2 $$

其中:

  • $\boldsymbol{\epsilon}$:扩散过程添加的高斯噪声
  • $\boldsymbol{\hat{\epsilon}}^i$:第 i 步去噪时预测的噪声

扩散模型通过预测噪声间接建模动作分布,避免直接回归的模态坍缩问题。

AAE (Adaptive Action Ensemble)

可以看到,我们每步根据观测信息最终会预测一个 Action Chunk,但推理的时候它们会彼此重叠,没有充分利用信息;而如果每个时间步都只用 Action Chunk 的最开始一部分,又会导致动作不平滑。

为此,作者提出了一种自适应动作聚合的方式,通过余弦相似度来为不同时间步预测的同一时刻的动作进行加权:

$$ \hat{\boldsymbol{a}}t = \sum{k=0}^{K} w^{\text{ada}}k \cdot \boldsymbol{a}{t}|\boldsymbol{o}_{t-k} $$

其中:

  • $\hat{\boldsymbol{a}}_t$:最终预测的动作
  • $w^{\text{ada}}_k$:加权系数
  • $\boldsymbol{a}{t}|\boldsymbol{o}{t-k}$:第 $t-k$ 步预测的第 $t$ 步动作
  • $\boldsymbol{o}_{t-k}$:第 $t-k$ 步的观测信息
  • $K$:采用最近几次的历史动作预测,基于训练集动作的标准偏差来确定

cogact_aae

加权系数的计算方式:

$$ w_k^{\text{ada}} = \exp(\alpha \cdot \langle \boldsymbol{a}_t|\boldsymbol{o}_t, \boldsymbol{a}t|\boldsymbol{o}{t-k} \rangle) $$

  • $\langle \cdot,\cdot \rangle$:余弦相似度(取值范围 $[-1,1]$)
  • $\alpha$:温度系数,超参数
  • $\boldsymbol{a}t|\boldsymbol{o}{t-k}$:基于历史观测 $\boldsymbol{o}_{t-k}$ 预测的当前时刻动作

实际使用时会进行 softmax 归一化:

$$ \hat{w}k = \frac{w_k^{\text{ada}}}{\sum{j=0}^K w_j^{\text{ada}}} $$

本质就是 相似度越高 → 权重越大,从而保留相同动作模式,并且实现平滑过渡。

PointVLA

Paper

pointvla

Insight

  • 现有 VLA 模型(如 OpenVLA、DexVLA)依赖 2D 图像输入,难以处理需要深度感知的任务;重新训练包含 3D 数据的 VLA 模型成本高昂,而丢弃已有的大规模 2D 数据集会造成资源浪费
  • 所以,选择将 3D 点云信息嵌入后注入动作专家模块,然而直接微调 VLM 主干会引发灾难性遗忘,不加选择的注入动作专家模块也会引发性能暴跌
  • 通过这种方式,作者实现了不破坏预训练 VLA,同时高效融合 3D 点云信息

Method

Arch

Backbone:

  • VLM:Qwen2-VL,2B
  • Action:ScaleDP,1B,Diffusion 变体

3D injector:在选定层执行 $h_{\text{new}} = h_{\text{2D}} + \text{MLP}(f_{\text{3D}})$,其中 $h_{\text{2D}}$ 为原动作专家选定的几个层的隐藏状态。

这里 $f_{\text{3D}}$ 有一个分层卷积设计,而且是从头开始训练的。作者发现,预训练的 3D 视觉编码器会阻碍性能,往往在新环境中难以成功学习机器人行为。

Skip-Block

由于要额外引入 3D 注入,所以作者探究了一下在动作专家模块中模块对性能的影响,从而选择影响较小的层去注入信息(这个思想类似于模型剪枝的时候的操作)。

作者发现,动作专家模块的前 11 层影响很大,后续层则可以进行替换或注入,这比较符合直接,前期对 2D 及语义特征的处理还相对低级,自然会比较重要,对性能影响大。

DexVLA

Paper

dexvla

Insight

  • 之前的 VLA 明显在 LLM 和 Action 部分大小失衡,过度扩展视觉语言模块(VLM 参数达 3B-7B),而动作专家部分(action expert)仍停留在百万参数级别,成为性能瓶颈

Method

Arch

Backbone:

  • VLM:Qwen2-VL,2B
  • Action:ScaleDP,1B,Diffusion 变体,具有多个策略头,可以适配多种不同的下游机型

Training

很怪的训练方法,分阶段训练不是没见过,但都是整体 pipeline 不变,只改变冻结部分的,本文的训练在不同阶段的 pipeline 都变了,前一阶段用的部分再后续阶段直接丢掉了。

  1. 阶段 1:仅用跨形态数据预训练动作专家,也就是扩散部分,学习低级运动技能(如抓取、移动)。语义部分 不是靠 VLM,而是暂时性靠另外一个 ViT/DistilBERT 编码,随后经过 FiLM+ResNet 来进行整合,送入扩散部分。
  2. 阶段 2:绑定 VLM 与动作专家,冻结 VLM 的视觉编码器,联合训练视觉到 Token 的投影层以及扩散专家,用特定形态数据对齐视觉 - 语言 - 动作映射。舍弃上一阶段的 FiLM+ResNet 不分。
  3. 阶段 3:全模型微调,微调时引入 高质量子步骤推理标注数据,使模型能自动分解长期任务(如 “叠衣服” 分解为展平、对齐袖子等)。

💾

  •  

访客统计插件 Visitor Statistics

访客统计(Visitor Statistics)是一款功能强大的WordPress插件,旨在帮助网站管理员精确跟踪和分析网站访问数据。它能够记录访客的IP地址、访问时间、设备类型、浏览器信息以及来源页面等关键数据,并通过内置的IP地理位置数据库提供精确到城市级别的访客位置识别。

该插件提供直观的图表化展示界面,让您轻松掌握网站访问趋势和用户行为特征。其优化的异步记录机制确保对网站性能影响微乎其微,同时支持多种筛选选项,如可选择性忽略管理员访问、搜索引擎爬虫、404页面访问等。插件还支持自定义数据保留时间和独立访客判定规则,为您提供了灵活而全面的访客统计解决方案。

  •  

视觉与抓取 II

迭代最近点算法(ICP)

动机

在机器人抓取任务中,物体的位姿估计精度直接影响抓取成功率。

以 YCB 数据集为例,当预测位姿的平移误差超过 2cm 时,抓取成功率会显著降低至 60% 以下。对于细长物体(如粉笔、剪刀),即使 2.5mm 的误差也可能导致抓取失败。

这种敏感性源于:

  1. 机械臂运动误差:沿特定方向的平移误差可能推翻物体
  2. 夹爪闭合策略:夹爪开合宽度需要与物体尺寸精确匹配
  3. 旋转容错性:旋转误差(如绕 Z 轴 30°)通常比平移误差更宽容

而对于 PoseCNN,仅 32% 的预测能达到 2cm 内的平移精度。这种误差水平难以满足实际抓取需求,因此需要后续优化。

posecnn_with_without_icp

算法原理与流程

ICP 用于优化初始位姿估计,通过迭代优化使源点云和目标点云对齐。

  • 源点云(Source/Moved Data): $P = {p_1, p_2, \dots, p_n}$,其中每个 $p_i \in \mathbb{R}^3$。点云可以表示为矩阵 $P \in \mathbb{R}^{3 \times n}$。
  • 目标点云(Target/True Data): $Q = {q_1, q_2, \dots, q_m}$,其中每个 $q_j \in \mathbb{R}^3$。点云可以表示为矩阵 $Q \in \mathbb{R}^{3 \times m}$。

注意:$n$ 和 $m$ 分别是源点云和目标点云中点的数量,它们可以不相等($n \neq m$)。

ICP 通过迭代优化寻找最佳的旋转矩阵 $\hat{R} \in \mathbb{SO}(3)$ 和平移向量 $\hat{T} \in \mathbb{R}^{3 \times 1}$,使得变换后的源点云 $P$ 与目标点云 $Q$ 尽可能对齐。

算法迭代步骤如下:

  1. 数据中心化(Make data centered)

    • 计算点云 $P, Q$ 的质心(均值):$\bar{P} = \frac{1}{n} \sum_{i=1}^n p_i, \bar{Q} = \frac{1}{m} \sum_{j=1}^m q_j$。
    • 将点云中心化:$\tilde{p}_i = p_i - \bar{P}, \tilde{q}_j = q_j - \bar{Q}$。得到中心化后的源点云矩阵 $\tilde{P} = [\tilde{p}_1, \dots, \tilde{p}_n] \in \mathbb{R}^{3 \times n}$ 和目标点云矩阵 $\tilde{Q} = [\tilde{q}_1, \dots, \tilde{q}_m] \in \mathbb{R}^{3 \times m}$。
    • 这一步的目的是先去除位移 $t$ 的影响
  2. 对应点匹配(Correspondence Search)

    • 对于当前源点云 $P$ 中的每一个点 $p_i$,在目标点云 $Q$ 中找到其最近邻点 $q_{j_i}$:

      $$ q_{j_i} = \operatorname{argmin}_{q_j \in \tilde{Q}} | \tilde{q}_j - \tilde{p}_i |^2_2 $$

    • 形成一个与 $P$ 点一一对应的目标点子集(correspondences)$P_{corr_pts} = { q_{j_1}, q_{j_2}, \dots, q_{j_n} }$

    • 得到对应目标点云矩阵 $\tilde{P}{corr} = [\tilde{q}{j_1}, \dots, \tilde{q}_{j_n}] \in \mathbb{R}^{3 \times n}$。

  3. 位姿求解(Pose Estimation using Orthogonal Procrustes)

    • 目标是找到最优旋转 $\hat{R}$,最小化中心化点云之间的距离:

      $$ \hat{R} = \operatorname{argmin}{R \in \mathbb{SO}(3)} |\tilde{P}{corr} - R\tilde{P}|_F^2 $$

    • 计算协方差矩阵 $K = \tilde{P}{corr} \tilde{P}^\top = \sum{i=1}^n \tilde{q}_{j_i} \tilde{p}_i^\top$,这是一个 $3 \times 3$ 矩阵。

    • 对 $K$ 进行 SVD 分解:$K = U D V^\top$。

    • 计算最优旋转矩阵 $\hat{R}$ (确保是旋转矩阵,处理可能的反射情况):

      $$ \hat{R} = U \begin{bmatrix} 1 & 0 & 0 \ 0 & 1 & 0 \ 0 & 0 & \det(UV^\top) \end{bmatrix} V^\top $$

    • 计算最优平移向量 $\hat{T}$: $$ \hat{T} = \bar{P}_{corr} - \hat{R} \bar{P} $$

    • 这里的详细推导可以参见前一章笔记的正交 Procrustes 问题

  4. 更新与迭代(Update P and Iterate)

    • 使用求得的 $\hat{R}, \hat{T}$ 更新 原始 源点云 $P$ 的位姿:

      $$ P_{new} = \hat{R} P + \hat{T} $$

      (这里 $P$ 是 $3 \times n$ 矩阵,$\hat{T}$ 是 $3 \times 1$ 向量,需要广播加到 $P$ 的每一列)

    • 将 $P_{new}$ 作为下一次迭代的输入源点云。

    • 重复步骤 2-4,直到满足收敛条件($\hat{R}, \hat{T}$ 变化足够小,或者达到最大迭代次数)。

ICP 收敛性

由于计算对应点匹配的时候 可能会导致非一一映射问题(好几个点离同一个点最近),此时必然无法找到一个完美的变换(不可能两个不同的点经过仿射变换到了同一个点)。

所以,ICP 并没有收敛保证,可能卡在局部最优(local minimum),但其对于 PoseCNN 的性能表现还是有很强的提升。

ICP 算法的问题

优点

  • 操作简便,无需进行点云分割或特征提取。
  • 当初始估计较为准确时,具有不错的精度和收敛性。

缺点

  • 寻找最近对应点计算成本高(可通过下采样密集点云或采用小样本匹配以加快迭代速度来降低)。
  • ICP 每次迭代太耗时,还会迭代很多次,所以后来提出了一些算法来加速。
  • 仅考虑点对点距离,未充分利用点云结构信息。
  • 对初始估计的准确性高度依赖。

类别级位姿估计(Category-Level Pose Estimation)

实例级别(Instance-Level)的位姿估计都要求我们知道物体的完整建模,否则我们缺乏目标,无法进行估计。

不过,对于一些有自然定义的 pose,它会具有一个天然的参考性(从而提供一个类别级的参考系),从而可以从 Instance level 延拓到 Category level,直接对这一类别的物体的 pose 进行预测。

这是王鹤老师在 CVPR 2019 Oral 的工作,原始论文可以参见 这里

这是如何做到的?当物体缺乏实例级别的 CAD 模型时,那就建立类别级的统一参考系。

这里的核心思想是 通过归一化操作定义标准化物体空间 Normalized Object Coordinate Space(NOCS)

  1. 旋转对齐 (Rotation Alignment):通过先验,使用物体的方向,对齐坐标系
  2. 平移归一化 (Translation Normalization):计算 Bounding box,将包围盒中心平移至坐标系原点
  3. 尺寸归一化 (Scale Normalization):通过对角线长度限制 Bounding box 的大小(限制对角线长度为 $1$,那么一定能装到 $1\times 1\times 1$ 的 Bounding box 内)

举个栗子 🌰

对于茶杯我们总是知道其大致形状的(先验)。

  1. 然后对齐物品的朝向,如把茶杯手柄的方向统一规定为某一轴的正方向,从而对齐 $R$
  2. 使用一个正方体的 bounding box 来框起来物体,然后强制把其中心定位在 $(0,0,0)$,从而对齐 $t$
  3. 归一化 Bounding box 的大小,从而对齐同一类物体的 size

nocs_1

nocs_2

nocs_3

好,现在有了参考系,那怎么用呢?

首先,我们指出一下该算法和 ICP 算法的本质区别:

  1. ICP 算法需要很强的先验知识,我们需要完整的知道物体的本身建模,然后以 RGBD 或者 RGB 重建得到的点云去与物体本身建模点云配准,由于求位姿的算法需要一个变换前后的坐标对,所以我们需要先进行最近邻匹配(也就是这一步导致了收敛性的缺失以及迭代速度的变慢),然后据此迭代得到物体位姿 $(R,t)$
  2. NOCS 算法不再需要完整的知道知道物体的本身建模,而是通过标准化的 NOCS 空间隐式地引入了对于某一类物体的、相较于 ICP 算法更粗粒度的几何先验,降低了对于高精建模的依赖,我们(使用合成数据)训练得到一个神经网络,可以从 RGB 图像直接为每一个像素预测其在 NOCS 中的对应点 $(x,y,z)$,随后将其与 RGBD 重建得到的点云信息进行配准,这里根据像素关系,可以天然形成数量相同的变换前后的坐标对,所以不再需要找到最近邻(Correspondence)。而后,我们可以直接用 Umeyama 算法(和 ICP 去除最近邻匹配的后半段类似)来重建得到 7 DoF 物体位姿 $(s,R,t)$

整个 NOCS 过程可以被建模为如下数学形式:

给定两组对应点云:

  • 规范空间点 $\mathbf{p}_i \in \mathbb{R}^3$(来自 NOC Map 预测)
  • 真实空间点 $\mathbf{q}_i \in \mathbb{R}^3$(来自深度图反投影)

寻找相似变换参数 $(s, R, t)$ 使得:

$$ sR\mathbf{p}_i + t = \mathbf{q}_i \quad \forall i $$

接着,我们给出算法的过程。

nocs_arch

nocs_arch_2

  1. 输入 RGBD 图像,提取 RGB 信息,使用 Mask R-CNN(如果没学过,可以参见我在 AI 基础写的 这篇笔记)获得 ROI(感兴趣区域,Region of Interest),分割物体
  2. 对于分割出的物体,对其每个像素预测其对应的 NOCS 空间坐标 $(x,y,z)$,得到 NOCS Map
  3. 利用 Depth 图像和相机内参,将 NOCS Map 中的点反投影(Back Projection)到三维空间中,得到点云数据
  4. 通过 NOCS Map 和 Depth 图像得到的点云数据,进行 Pose Fitting,利用 Umeyama 算法,计算得出物体的 7DoF 位姿(缩放 + 旋转 + 平移),缩放系数的计算就是简单的用 NOCS Map 的各轴向长度与物体实际点云各轴向作了一个除法。而反过来计算 Bounding Box 的时候,则利用了 NOCS 建模时令物体中心处在原点从而具有的对称性,以预测出的 NOCS Map 各轴向最大绝对值乘 2 再乘缩放系数作为了 Bounding Box 的各轴向尺寸

Umeyama 算法和前文类似,再次不再赘述。

了解了过程之后,一个很自然的问题就是:为什么不能直接用神经网络去根据 RGB 图像和 RGBD 反投影得到的深度图预测 6DoF 位姿?

  1. 首先,实验能证明这种方法比直接回归要好;
  2. 其次,直观的理解上可以想到,回归是一个从 3D $\to$ 6D 的直接预测,而 NOCS 是首先建立了 2D $\to$ 3D 的对应关系,然后将 6D 的位姿变换成了从 NOCS 3D 到 Depth 3D 的一个几何优化问题,明显后者比前者更符合直觉。
  3. 除此之外,NOCS 方法还充分利用了形状 / 几何先验,通过规范空间强制同类物体共享几何分布特征,使网络能学习类别级别的形状规律,学习起来会具有协同效应(Synergy),提升了对未见物体的泛化能力。

合成数据

刚才介绍过了 NOCS 方法,那么现在最大的问题就在于如何去训练这样一个从二维 RGB 图像重建到 NOCS 空间的神经网络了。

在类别级物体姿态估计任务中,真实数据标注面临两大挑战:

  1. 标注成本过高
  2. 类别泛化性不足

因此,直接去使用真实数据是很难成功的,所以很自然地,我们想要使用合成数据来进行训练。

但是,模型在合成数据($\mathcal{D}{syn}$)和真实数据($\mathcal{D}{real}$)上的往往存在差异,也即 Sim2Real Gap,这是由于这二者的分布是不同的,直接用真实数据去测试在合成数据上 Work 的方法,往往会导致性能暴跌。

为此,王老师提出了一种新的数据合成办法,也就是 Mixed Reality Data

mixed_reality_data

这种数据中,背景是真实的,而需要分割的前景是合成的(从而我们可以直接获得训练 NOCS 模型所需的监督信号),从而可以很轻易地获取到几十万量级的数据。

但是,在实践过程中,发现简单地使用这个方法还是会存在较大的 Sim2Real Gap,这是由于合成背景和前景照片的时候,分界太过明显,从而导致分割的 Mask R-CNN 学习到的经验难以应用到真实世界。

为了解决这个问题,王老师又提出了使用 Co-Training 的方案,即同时结合过往 Image Segmentation 领域收集的真实数据集(Coco)与我们的合成数据集来一同对 Mask R-CNN 进行 混合训练,但前者不参与后续的 NOCS 映射训练,只为分割提供监督信号。

王老师认为,这种合成数据的使用在具身智能领域是必不可少的,因为训练学习所需的真实数据很难大规模、轻易地获取到。

王老师还提到,目前 Pose Estimation 领域最 work 的模型(FoundationPose)就是纯合成数据训练出来的,不过他们的合成过程会更加精细

sota_pose_estimator

对于预测得到的位姿,有时候还需要 Refinement,比如之前介绍的 ICP 算法。

然而,ICP 算法同时需要点云与物体表面 mesh,真实情况下可能两者都没有,所以现在这个问题完全用神经网络来做,而其训练的数据全靠合成。

运动规划的层级

$$ \text{pose} \to \text{grasp} \to \text{motion planning} \to \text{control} $$

  1. 一代技术:工业机器人,完全的轨迹重放,无环境感知能力
  2. 二代技术:位姿预测,但需要物体预先定义,轨迹通过位姿进行预测规划得到
  3. 三代技术:抓取预测
  4. 四代技术:动作规划预测,神经网络端到端直接输出动作轨迹 Action / Trajectory,可以进行闭环纠错
  5. 五代技术:完全的闭环控制,大语言模型指导进行语义推理

开环控制如果 pose estimation 足够快,也能搞成闭环。

抓取(Grasp)

抓取:指通过在接触点施加力和力矩,以期望的方式约束物体运动的过程。

Force Closure

定义:通过摩擦力 维持平衡的约束状态,如果施加在摩擦接触点上的一组力足以补偿施加在物体上的任何外部力,则称为力闭合。

王鹤老师原话:以某一组力在某一组接触点(Contact Point)抓取起来后,物体需要任意方向的加速度,都可以提供。

Force Closure 是判断抓取质量的一个重要指标。

Form Closure

定义:仅仅通过 几何约束 完全限制刚体运动的状态( 不依赖摩擦力 )。

根据定义,不难推知,严苛程度上:抓起来 ≤ force closure ≤ form closure

在规划机器人手的抓取时,力闭合是一个很好的最低要求。形闭合通常过于严格,需要太多接触点。

回归与生成

传统的抓取问题可以看作是一个回归问题,即预测唯一的抓取位姿。然而,由于遮挡和对称性,一个物体通常存在多个可行的抓取(多峰分布)。因此,将抓取建模为一个生成问题更为合适。

💾

  •  

修整自行车,升级后拨导轮。自己动手的乐趣在于及时解决问题

自从开通了两个公众号,博客已经很久没更新了。和预想的差不多,公众号的数据流量不会太好,但有希望。首要目标是500粉丝,现在还差很远。

按照现在的能力和精力只能主做其中一个博客研究社,另一个公众号还在等待时机,当有多余的精力时再打造FENG.PUB

不过当务之急还是修整自行车,自从发现自行车问题将近一个月了。虽然还能骑,但是骑着不舒心。

自行车修整升级

通过这次修整,对自行车一些问题又有了新的认识。

之前以为后拨变速器坏了需要更换,没想到是后拨调节螺丝滑了,达不到调节效果,只需要更换这个螺丝就行。

还有,尾钩变形,导致后拨导轮与飞轮不成一条线。我以为需要更换尾钩,没成想只要把尾钩撬一撬就可以调整了。

除了修整还有升级,这次对后拨导轮进行升级,更换为带有轴承的导轮。

虽然通过网络得知更换后拨大鸡腿可以更省力,但我还是保守一点,保持11齿导轮大小不变,更换带轴承的导轮。淘宝这样的导轮一个只需要8块钱,果断买了2个。不知道换过导轮能省多少力气,速度会不会有所提升。

没有什么经验,所以我预留了充足的时间用于修整和升级自行车。过程是反反复复,感觉不对拆了重装。更换导轮、后拨拆装、尾钩拆装、调整尾钩、调节变速、拆装后刹车片,用时近4小时。

为什么要自己动手

两个直接原因,致使自己动手修整升级自行车。

了解自行车结构,快速解决一些小问题。

2千出头的自行车,买的时候就看到评论区有人这样评价:“除了车架,其他的都换掉了”、“刹车异响”、“脚蹬异响”。不过也看到店家积极回复,可以帮着解决这些问题。

买回来先是解决了刹车异响的问题,骑着感觉还行,毕竟比共享单车要好太多了。旅行车兼顾了公路和越野,骑了一段时间才知道这种车是为了装货,什么前车架后尾架,驮个前包后包十分方便。

自从有了这个车,开始关注前拨后拨、刹车、飞轮、车架、轮胎,也是买车之后才对自行车有了新的认识。

在bilibili搜索教程,尝试调整变速器。后又因为看到一句话,“链条清洗十分干净的自行车,绝对大佬”,便清洗了牙盘和飞轮,也清洗了链条,但是链条始终油乎乎的。

之后自行车有什么小问题,自己能很快解决。随后在车座下方加装了小包,放了常用的内六角、补胎片和打气筒,以备不时之需。

省工时费,买配件和工具。

当我把自行车骑到车店,表示断了一根辐条时,店家看过自行车辐条,表示没有这么长的。又找了一家店,店家正在忙需要等待,那时天色渐晚,决定先回家再说。

大致了解更换辐条的步骤,虽然简单,但是工时可能会比较多。轮子拆下来,内胎、外胎都要拆下来,装好辐条还要调整偏摆,装好内外胎,再把轮子装好。

这一套流程下来要花费不少工时,更何况我这车子的辐条材质是钢的,比不上碳纤维的价格,计时更换整个轮子的辐条也花不了多少钱。

剩下的工时费,可以买配件和工具,而我又喜欢自己动手,所以开始在网上买工具买配件。

  •  

家庭数据中心系列 WordPress 图标字体优化:本地托管 FontAwesome,告别 API 限制

家庭数据中心系列 WordPress 图标字体优化:本地托管 FontAwesome,告别 API 限制 无敌的个人博客 tangwudi

1 前言 之前,我博客外链以及菜单的图标都是配合FontAwesome(Free版)的图标字体来使用,效果也蛮不错: 不过,当时是使用的FontAwesome官方的CDN,需要在WordPress中插入如下代码: <script defer src="https://kit.fontawesome.com/YOUR_KIT_ID.js" crossorigin="anonymous"></script> Font Awesome 是一个基于CSS和LESS的字体和图标工具包,它由Dave Gandy制作,用于Twitter Bootstrap,后来被集成到BootstrapCDN 中。Font Awesome在使用第三方Font Scripts的网站中占有20%的市场份额,排在Google字体之后的第二位。 关于FontAwesome图标字体的详细介绍和使用参见文章 […]

<p>The post 家庭数据中心系列 WordPress 图标字体优化:本地托管 FontAwesome,告别 API 限制 first appeared on 无敌的个人博客.</p>

  •  

生日快乐

今天是孩子们十四周岁的生日。往年我们都是在家里简单庆祝:订个蛋糕,孩子们约上一两个同学,我和老婆炒几个家常菜,再煮几碗长寿面,简简单单就把生日过了。由于今年刚好赶上周末,老婆又临时要加班,家里只剩下我和孩子们仨。想了想,干脆带她们去外面过一次生日吧,换个花样也挺好。

于是就有了两位小寿星人尴尬至极的一次生日体验。😂

  •  

RL in VLA

iRe-VLA

Paper

ire_vla

Insight

  1. RL 只用以更新少部分参数,即 Action 头,从而避免 RL 大规模更新参数的不稳定。
  2. SFT 来更新 LLM,更加稳定
  3. 训练过程:先 SFT,然后迭代进行 RL(PPO,on-policy)和 SFT

Intresting

  1. LLM 用以高层规划(分解任务,无法直接应用于物理世界)或者低层控制信号(LLM 中引入 Action Token 或者后接动作头)
  2. RL 直接用以提升 VLA 输出的低层控制信号
  3. RL 得到的新成功轨迹加入数据集,on-policy
  4. RL 用以探索,SFT 用以记忆

Arch

Backbone:BLIP

Componentes:LoRA,TokenLearner(压缩多 token 到单 token)

Reward Signal:MSE (SFT), 01 Sparse (RL)

Result

当在线数据 $|D_{\text{RL}}| > 0.3|D_e|$ 时,超越纯模仿学习的涌现能力(应对遮挡、动态干扰)。

RLPD

Paper

Efficient Online Reinforcement Learning with Offline Data

rlpd

Insight

  1. 对称采样:50% 在线数据 + 50% 离线数据,去除对于离线数据质量的假设
  2. LayerNorm 约束价值函数 $Q$,抑制 OOD 时的过度自信(价值外推),稳定值函数
  3. 高效采样:增加数据回放比 UTD,采用随机集成蒸馏(见下述算法)

Algorithm

$$ \begin{array}{l} \hline \textbf{算法} \ \text{在线强化学习结合离线数据 RLPD} \ \hline \text{初始化:} \ \quad \text{层归一化,集成规模 } E,\ \text{梯度步数 } G,\ \text{网络架构} \ \quad \text{评论家参数 } \theta_1,...,\theta_E\ (\theta'i \leftarrow \theta_i),\ \text{策略参数 } \phi \ \quad \text{折扣因子 } \gamma,\ \text{温度系数 } \alpha,\ \text{EMA 权重 } \rho,\ \text{目标子集 } Z \in {1,2} \ \quad \text{经验池 } \mathcal{R} = \varnothing,\ \text{离线数据集 } \mathcal{D} \ \hline \text{主循环:} \ \quad \text{获取初始状态 } s_0 \ \quad \text{循环 } t=0 \text{ 至 } T: \ \qquad \text{执行动作 } a_t \sim \pi\phi(\cdot|s_t),\ \text{存储转移 } (s_t, a_t, r_t, s_{t+1}) \text{ 至 } \mathcal{R} \ \hline \qquad \text{训练步骤 (重复 } G \text{ 次):} \ \qquad\quad \text{采样 } b_R \leftarrow \frac{N}{2} \text{ 自 } \mathcal{R},\ b_D \leftarrow \frac{N}{2} \text{ 自 } \mathcal{D} \ \qquad\quad \text{合并批次 } b = b_R \cup b_D \ \qquad\quad \text{计算目标值:} \ \qquad\qquad \mathcal{Z} \leftarrow \text{随机选取 } Z \text{ 个索引(从 } {1,...,E} \text{)} \ \qquad\qquad y = r + \gamma \big[\min_{i\in\mathcal{Z}} Q_{\theta'i}(s', \tilde{a}')\big] + \gamma\alpha \log \pi\phi(\tilde{a}'|s') \ \qquad\qquad \text{其中 } \tilde{a}' \sim \pi_\phi(\cdot|s') \ \hline \qquad\quad \text{评论家更新:} \ \qquad\qquad \text{循环 } i=1 \text{ 至 } E: \ \qquad\qquad\quad \theta_i \leftarrow \arg\min \frac{1}{N}\sum (y - Q_{\theta_i}(s,a))^2 \ \qquad\qquad \theta'i \leftarrow \rho\theta'i + (1-\rho)\theta_i \ \hline \qquad\quad \text{策略更新:} \ \qquad\qquad \phi \leftarrow \arg\max \frac{1}{E}\sum{i=1}^E Q{\theta_i}(s,\tilde{a}) - \alpha \log \pi_\phi(\tilde{a}|s) \ \qquad\qquad \text{其中 } \tilde{a} \sim \pi_\phi(\cdot|s) \ \hline \end{array} $$

Result

收敛变快(300k vs 1M),效果提升。

HIL-SERL

Paper / Homepage / Code

Human in Loop SERL,双臂任务

hil_serl

Insight

主动学习、人在回路:系统向模型请求可能的修正,offline 更新

Arch

Backbone:ResNet-10

Reward:01 Sparse (MLP)

AC 架构:

  • Actor:采样,送到 replay buffer,可以人为干预
  • Learner:学习,RLPD 均等采样

两个缓冲区:

  • 人类示范(离线)
  • 策略实施(RL buffer)

对于人类产生的干预数据:

  • actions 同时放到两个缓冲区(RL buffer + Demo buffer)
  • P 概率转移只放到 RL buffer

单独用 DQN 学习抓握(夹爪建模为离散动作),输出动作基于 EEF 当前坐标系,抗干扰。

RLDG

Paper

Reinforcement Learning Distilled Generalist

rldg

Insight

  1. 使用 RL 生成高质量微调数据,微调 HIL-SERL
  2. 数据质量 > 数据数量

ConRFT

Paper

Consistency-based Reinforced Fine-Tuning

conrft

Math

离线 Critic 损失

$$ \mathcal{L}{Q}^{offline}(\theta) = \alpha\left(\mathbb{E}{s\sim\mathcal{D},a\sim\pi}[\max(Q_{\theta},V^{\mu})] - \mathbb{E}{s,a\sim\mathcal{D}}[Q{\theta}]\right) + \frac{1}{2}\mathbb{E}[(Q_{\theta}-\mathcal{B}^{\pi}\overline{Q})^2] $$

  • $\max(Q_{\theta},V^{\mu})$:防止 OOD(分布外)动作的高估
  • $\mathbb{E}[(Q_{\theta}-\mathcal{B}^{\pi}\overline{Q})^2]$:稳定 Q 值估计,防止离线数据不足导致的过拟合

一致性策略

$$ \pi_{\psi}(a|s) = f_{\psi}(a^k, k | E_{\phi}(s)) $$

  • $f_{\psi}$ 一致性策略是一个基于扩散模型的策略,负责去噪并生成最终动作。其目标是学习从单位高斯分布 $\mathcal{N}(0,I)$ 的随机噪声动作 $a^k$ 到专家动作分布 $a \sim \pi^*(a|s)$ 的映射。映射过程以当前状态编码 $E_{\phi}(s)$ 为条件。
  • $a^k \sim \mathcal{N}(0, kI)$ 是第 $k$ 步的含噪声动作将扩散时间步 $[\epsilon, K]$ 划分为 $M$ 个子区间(边界为 $k_1=\epsilon \le \dots \le k_M=K$),每个子区间对应一个噪声尺度 $k_m$。例如,$\epsilon=0.002$ 表示初始噪声尺度极小,$K$ 为最大噪声尺度。

$$ \mathcal{L}{\pi}^{offline}(\psi) = -\eta\mathbb{E}[Q(s,a)] + \beta\mathbb{E}[d(f{\psi}(a+k_mz),a)] $$

  • $-\eta\mathbb{E}[Q(s,a)]$:引导策略朝高回报方向优化

  • $\beta\mathbb{E}[d(f_{\psi}(a+k_mz),a)]$:迫使策略在不同噪声尺度下保持动作预测的一致性,也即约束动作与演示数据的一致,解决人类演示的次优问题

    对任意中间扩散步 $k_m$,若向专家动作 $a$ 添加噪声 $k_m z$ 得到扰动动作 $a + k_m z$,一致性策略 $f_{\psi}$ 应能将其映射回原始专家动作 $a$。

Insight

  • 人在回路
  • 一致性策略保证鲁棒性,但在线学习阶段逐步降低 $\beta$(BC 权重),实现从模仿到自主探索的平滑过渡
  • 反馈信号中存在时间惩罚,引导快速完成任务

GRAPE

Paper

Generalizing Robot Policy via Preference Alignment

grape

Math

TPO 轨迹偏好优化损失(Trajectory-wise Preference Optimization Loss,类似 DPO): $$ \mathcal{L}{\text{TPO}} = -\mathbb{E} \left[ \log \sigma \left( \beta \left( \log \frac{\pi\theta(\zeta_w)}{\pi_{\text{ref}}(\zeta_w)} - \log \frac{\pi_\theta(\zeta_l)}{\pi_{\text{ref}}(\zeta_l)} \right) \right) \right] $$

  • $\beta$:温度系数,调节策略更新的强度(类比 “学习率”),越大这个 Loss 也越大,策略对比越强,更关注优选 / 劣选轨迹的差异;越小越保守更新,这项损失不重要。
  • $\pi_\theta$:待优化的策略(参数为 $\theta$)
  • $\pi_{\text{ref}}$:参考策略(预训练的初始策略)
  • $\zeta_w, \zeta_l$:优选轨迹(winning)和劣选轨迹(losing)

Insight

  • 对比学习,增大优选轨迹概率比,降低劣选轨迹概率比

  • 存在外部 Critic,由强大 LLM(GPT4o)给出,而非手动设计,某一时刻的成本为后续成本的乘积: $$ R_{\text{ext}}(\zeta) = \prod_{i=1}^{\mathbf{S}} e^{-C^{S_i}({\kappa_{S_i}})} $$ 其中:

    • $\mathbf{S}$:子系统的总数
    • ${\kappa_{S_i}}$:子任务 $S_i$ 的动态参数集合,如关节角度、速度、接触力等实时状态
    • $C^{S_i}$:子任务 $S_i$ 的成本函数,由 LLM 给出
  • 完整的 Reward 同时包括外部 Critic、模型自身、以及成功与否信息加权,用以判断 $\zeta_w, \zeta_l$: $$ R_{\text{GCPG}}(\zeta) = \lambda_1 R_\text{self}(\zeta) + \lambda_2 R_\text{ext}(\zeta) + \lambda_3 I_{\text{success}}(\zeta) $$ 其中: $$ R_\text{self}(\zeta) =\log(\pi(\zeta, q)) = \log(\prod_{i=1}^T\pi(a_i \mid(o_i, q))) $$

Algorithm

$$ \begin{array}{l} \hline \textbf{算法} \ \text{迭代偏好优化算法} \ \hline \text{初始化:} \ \quad \text{基础 VLA 策略 } \pi_\theta,\ \text{任务指令集 } Q = {q_i},\ \text{阶段分解器 } \mathcal{M}D \ \quad \text{最大迭代次数 } K,\ \text{奖励权重 } {\lambda_1, \lambda_2, \lambda_3} \ \quad \text{阶段关键点 } {\kappa{S_i}},\ \text{成本函数 } {C^{S_i}j}\ \text{及阈值 } {\tau^{S_i}j} \ \hline \text{主循环:} \ \quad \text{循环 } k=1 \text{ 至 } K: \ \qquad \text{用 } \pi\theta \text{ 和 } Q \text{ 采样轨迹集 } \mathcal{D}^k = {\zeta_i}{i=1}^M \ \qquad \text{循环轨迹 } \zeta \in \mathcal{D}^k: \ \qquad\quad \text{分解 } \zeta \text{ 为多阶段 } S\ \text{(阶段分解)} \ \qquad\quad \text{计算各阶段成本 } C_{S_i}\ \text{(阶段成本)} \ \qquad\quad \text{计算外部奖励 } R_{\text{ext}}(\zeta)\ \text{(全局成本)} \ \qquad\quad \text{计算策略自奖励 } R_{\text{self}}(\zeta)\ \text{(轨迹自评估)} \ \qquad\quad \text{验证任务成功指标 } I_{\text{success}}(\zeta)\ \text{(成功判别)} \ \qquad\quad \text{聚合 GCPG 奖励 } R_{\text{GCPG}}(\zeta)\ \text{(综合奖励)} \ \hline \qquad \text{按 } R_{\text{GCPG}}(\zeta) \text{ 排序 } \mathcal{D}^k \ \qquad \text{从 top-}m \text{ 和 bottom-}m \text{轨迹生成配对 } {\zeta_w, \zeta_l} \ \qquad \text{用 TPO 损失更新 } \pi_\theta\ \text{(偏好对齐)} \ \hline \text{返回:优化策略 } \pi^* \ \hline \end{array} $$

ASAP

Paper

Aligning Simulation and Real-World Physics

asap

Insight

  1. 预训练得到基础策略(模拟环境中)

  2. 后训练收集现实数据,模拟重放,获取跟踪误差,训练 delta 模型来补偿差异,形成残差校正项,通过动作空间修正隐式补偿,而不是像 SysID 一样显式建模物理参数来修正差异

    骑自行车时,人脑自动补偿重心偏移,而非计算力学方程

    $$ s_{t+1} = f^{\text{ASAP}}(s_t, a_t) = f^\text{sim}(s_t, a_t + \pi^\Delta(s_t, a_t)) $$

  3. 非对称 AC 架构

    1. Actor 网络仅依赖本体感知输入(关节位置 / 速度、基座姿态、时间相位)
    2. Critic 网络额外访问特权信息(参考动作轨迹、全局位置)

Arch

  1. PPO,AC
  2. Reward:$r_t = r_{\text{task}} + r_{\text{penalty}} + r_{\text{regularization}}$
    • 任务奖励(身体位置 / 旋转 / 速度匹配)
    • 惩罚项(关节极限、扭矩超限)
    • 正则化(动作平滑性)

💾

  •  

WP Simple EXIF

WP Simple EXIF 是一款专为摄影爱好者设计的 WordPress 插件,能够全面提取并优雅展示照片的 EXIF 拍摄信息,包括相机品牌、型号、镜头参数、曝光数据、拍摄日期、GPS位置等丰富内容。用户可根据需要自由选择显示的字段,并决定是否为所有图片或仅特定图片显示信息。插件采用响应式设计,桌面端悬停显示、移动端点击查看,兼顾视觉美感与交互体验,适配各类设备。

安装和使用极为简单,无需复杂配置即可启用。插件采用轻量级实现,对网站性能影响极小,并通过智能图片处理技术,自动从原图获取完整 EXIF 数据,即便图片使用缩略图显示也不影响信息提取。WP Simple EXIF 不仅提升了照片展示的专业度,也为访客提供了更深入的观赏体验,是摄影博客、作品集网站的理想选择。

后台设置界面
尺寸:2560×1920
测试图

说明:因精力有限,这里所提供的插件版本不能保证及时更新,如需最新版请联系我索取。

  •  

插件-WP Simple SMTP

#插件 WP Simple SMTP是一款专为WordPress网站开发的SMTP邮件配置插件,它通过提供简单易用的界面,帮助用户快速配置网站的邮件发送功能。该插件支持所有常见的SMTP服务器(如Gmail、QQ邮箱、163邮箱等),并提供完整的SSL/TLS加密连接支持,确保邮件传输安全。它具有测试邮件发送功能,让用户可以即时验证配置是否正确,并提供详细的错误日志记录,帮助用户排查问题。通过使用这款插件,WordPress网站管理员能够有效避免网站邮件被归类为垃圾邮件,提高邮件送达率,而无需复杂的技术设置。

## 功能特点

- 简单的设置界面,易于配置

- 支持常见的SMTP服务器(如Gmail, QQ邮箱, 163邮箱等)

- 支持SMTP验证

- 支持SSL/TLS加密连接

- 支持测试邮件发送功能

- 完整的错误日志记录

## 使用方法

1. 在WordPress后台插件页面激活插件

2. 在"设置" > "SMTP设置"中配置以下信息:

   - SMTP服务器地址

   - SMTP端口

   - 发件人邮箱

   - 发件人名称

   - SMTP用户名

   - SMTP密码

   - 加密方式(None/SSL/TLS)

3. 点击"保存设置"

4. 可以使用"发送测试邮件"功能验证配置是否正确

## 安全说明

- 所有密码信息都经过加密存储

- 建议使用SSL/TLS加密连接

- 请不要在公共场合泄露你的SMTP配置信息

## 常见问题

1. 如果发送测试邮件失败,请检查:

   - SMTP服务器地址和端口是否正确

   - 用户名和密码是否正确

   - 是否选择了正确的加密方式

   - 服务器是否支持外部SMTP连接

2. 如果正式邮件发送失败,请查看错误日志获取详细信息。 

说明:因精力有限,这里所提供的插件版本不能保证及时更新,如需最新版请联系我索取。

  •  

插件-Simple Debug Log

#插件 Simple Debug Log 插件提供了一种简单而有效的方式来管理 WordPress 的调试功能,让开发者和网站管理员可以轻松查看系统中出现的错误和警告,而无需直接编辑配置文件或通过FTP访问服务器。这对于排查 WordPress 网站问题、插件冲突或主题错误非常有用,尤其是对于那些可能不太熟悉服务器文件系统或没有直接文件访问权限的用户。插件的设计理念是"简单易用",无需复杂配置,安装后即可使用,同时提供了足够的功能来满足日常的 WordPress 调试需求。

# Simple Debug Log

简单易用的WordPress调试日志工具,帮助开发者和站长轻松管理和查看WordPress调试信息。

## 插件介绍

Simple Debug Log是一款专为WordPress开发者和网站管理员设计的调试工具。它提供了一种简单而直观的方式来启用调试模式、查看和管理调试日志,无需直接编辑配置文件或通过FTP访问服务器,大大简化了WordPress的调试流程。

## 主要功能

- **在WordPress管理后台查看调试日志**:直接在管理界面查看完整的debug.log内容

- **一键开启/关闭WordPress调试模式**:无需手动编辑wp-config.php文件

- **一键清除调试日志**:轻松重置您的调试记录

- **仪表盘小工具快速访问**:从仪表盘直接查看最新的调试信息

- **智能检测调试文件位置**:自动查找并使用您WordPress环境中的debug.log文件

- **大文件智能处理**:对于大型日志文件,只加载最近的内容,避免性能问题

- **安全的权限控制**:确保只有管理员可以查看和操作调试信息

## 使用场景

- **开发和调试WordPress插件**:轻松查看插件运行时产生的错误和警告

- **主题开发和测试**:快速发现并解决主题中的问题

- **网站故障排查**:当网站出现异常行为时,通过查看调试日志找出原因

- **性能优化**:识别可能导致性能下降的警告和通知

- **服务器环境诊断**:了解PHP配置问题或服务器限制

## 安装方法

### 通过WordPress后台安装(推荐)

1. 登录到您的WordPress管理后台

2. 进入"插件" > "安装插件"

3. 点击"上传插件"按钮

4. 选择下载的zip文件并上传

5. 安装完成后点击"启用插件"

### 手动安装

1. 下载插件并解压

2. 通过FTP将插件文件夹上传到`/wp-content/plugins/`目录

3. 在WordPress后台的插件页面中激活"Simple Debug Log"

4. 安装完成后,通过 "工具 > 调试日志" 访问插件功能

## 详细使用指南

### 切换调试模式

1. 进入"工具" > "调试日志"页面

2. 点击页面顶部的"开启调试模式"或"关闭调试模式"按钮

3. 系统会自动修改wp-config.php文件中的调试设置

4. 操作完成后会显示成功提示

### 查看调试日志

1. 确保调试模式已开启

2. 在"工具" > "调试日志"页面中,您可以查看完整的调试日志内容

3. 对于大型日志文件,系统会自动只显示最近的内容(约1MB)

4. 日志内容会按时间顺序显示,最新的信息在底部

### 清除调试日志

1. 在调试日志页面中,点击"清除日志"按钮

2. 确认操作后,系统会清空debug.log文件内容

3. 操作完成后会显示成功提示

### 使用仪表盘小工具

1. 插件安装后,会自动在仪表盘添加"调试日志"小工具

2. 小工具会显示最近的5条调试信息和当前调试模式状态

3. 点击"查看全部"可以快速进入完整的调试日志页面

## 常见问题解答

### 修改wp-config.php文件失败怎么办?

确保WordPress的wp-config.php文件具有适当的写入权限。通常,文件权限设置为644或640应该足够,同时确保文件的所有者与运行Web服务器的用户相匹配。

### 找不到调试日志文件?

插件会自动在WordPress内容目录(通常是wp-content)下查找或创建debug.log文件。如果自动检测失败,请确保您的WordPress安装具有在内容目录中创建文件的权限。

### 调试日志显示不完整?

对于超过1MB的大型日志文件,插件会只显示最后部分内容,以避免浏览器性能问题。如果需要查看完整日志,您可以直接通过FTP或主机控制面板下载debug.log文件。

### 安全性问题?

插件严格限制只有具有管理员权限(manage_options)的用户才能访问调试功能,并使用WordPress的nonce机制防止CSRF攻击。我们建议仅在开发或故障排查时启用调试模式,并在生产环境中保持禁用状态。

## 版本历史

### 1.0.0

- 初始版本发布

- 基本的调试日志查看和管理功能

- 调试模式切换功能

- 仪表盘小工具集成

## 许可证

本插件采用 GPL v2 许可证。

说明:因精力有限,这里所提供的插件版本不能保证及时更新,如需最新版请联系我索取。

  •  

家庭数据中心系列 Cloudflare Worker + KV:打造 WordPress 云端文章阅读统计

家庭数据中心系列 Cloudflare Worker + KV:打造 WordPress 云端文章阅读统计 无敌的个人博客 tangwudi

1 前言 在 WordPress 里,实现文章阅读统计的方法有很多,常见的做法通常是借助插件,在主题文件(通常是 functions.php)中插入代码,并将阅读次数存放在本地数据库中。根据实现方式,大致可以分为两类:服务端统计 和 前端统计。 • 服务端统计:一般是通过 PHP 代码直接记录文章的访问量,但如果网站启用了 CDN 缓存,很多请求根本不会真正到达服务器,导致统计数据不准确。例如,WordPress 的 WP-PostViews 插件 就是这么做的,这种方式已经逐渐被淘汰。 • 前端统计:通过 JavaScript 在浏览器端直接向 WordPress 发送请求,调用 admin-ajax.php 记录访问量。虽然这种方式可以绕开 CDN 缓存的干扰,但 admin-ajax.php 是同步执行的,性能并不算高,流量一大就可能拖慢 WordPress 的整体响应速度。 说到底 […]

<p>The post 家庭数据中心系列 Cloudflare Worker + KV:打造 WordPress 云端文章阅读统计 first appeared on 无敌的个人博客.</p>

  •  

插件-Simple APCu Cache

#插件 Simple APCu Cache 是一款轻量级 WordPress 插件,它利用APCu缓存技术来提高网站性能。该插件通过在PHP进程之间共享数据来减少数据库查询次数,从而显著加快网站加载速度。它能自动检测服务器是否支持APCu,在激活时自动设置对象缓存,并提供简洁的管理界面让用户查看缓存状态和统计信息。使用非常简单,激活后即可自动工作,无需额外配置,还可以通过"设置 > 简单对象缓存"菜单随时清除缓存,是解决WordPress健康站点检查中对象缓存提示的理想解决方案。

## 功能

- 使用APCu作为WordPress对象缓存的后端

- 提供简单的管理界面,显示缓存状态和统计信息

- 支持一键清除缓存功能(直接在仪表盘清除,无需跳转)

- 自动检测APCu是否可用

- 在插件激活时自动设置对象缓存,在停用时自动清除

- 使用AJAX技术实现无刷新缓存管理

## 要求

- WordPress 5.0或更高版本

- PHP 7.0或更高版本

- 启用了APCu扩展的PHP环境

## 安装

1. 将`simple-apcu-cache`目录上传到`/wp-content/plugins/`目录

2. 在WordPress管理后台激活插件

3. 插件会自动设置对象缓存

## 使用方法

激活插件后,它会自动开始工作,无需额外配置。

你可以在"设置 > 简单对象缓存"中查看缓存状态和统计信息,也可以手动清除缓存。

### 如何手动清除缓存?

有两种方式可以清除缓存:

1. 在WordPress仪表盘中,找到"简单对象缓存状态"小工具,直接点击"清除缓存"按钮。

2. 在"设置 > 简单对象缓存"页面中点击"清除缓存"按钮。

## 更新日志

### 版本 1.1.0

- 优化仪表盘小工具,现在可以直接通过按钮清除缓存,无需跳转到设置页面

- 添加AJAX处理功能,清除缓存后实时更新统计信息

- 改进用户体验,清除缓存操作完成后显示即时反馈

- 添加状态提示和视觉反馈,让用户更清楚操作结果

### 版本 1.0.0

- 初始版本发布

## 许可证

GPL v2或更高版本 

说明:因精力有限,这里所提供的插件版本不能保证及时更新,如需最新版请联系我索取。

  •  

视觉与抓取 I

抓取

抓取(grasping):通过末端执行器(end-effector)对物体施加约束(力和扭矩),以期望的方式限制物体运动的过程。

抓取合成(grasping synthesis):对于夹爪位姿或者关节控制的高维搜索 / 优化问题。

vision_grasping_robot_sequence

  • 抓握式操作 (Prehensile Manipulation):通过完全约束物体自由度实现精确控制
  • 非抓握式操作 (Non-prehensile Manipulation):利用推、滑等接触力学原理调整物体状态,适用于薄片状物体或预处理场景,不是所有动作都需要抓取

抓取的自由度

抓取姿势(Grasp Pose):手的位置、方向和关节状态

  • 4-DoF 抓取:仅需平移和绕重力轴旋转,适用于结构化环境、固定位置(如流水线物料分拣)

    $$ (x, y, z, \theta_z) $$

    rpy

    yaw

  • 6-DoF 抓取:允许任意方向接近,处理非结构化场景(即更复杂的任务如杂乱堆叠物体) $$ (x, y, z, \theta_x, \theta_y, \theta_z) $$

  • 手指自由度

    • 平行夹爪:开 / 关,1 DoF
    • 灵巧手(Dexterous Hand):21 DoF

开环抓取与闭环抓取

开环控制是指 不使用反馈机制 的控制系统。

  1. 控制命令直接发送给系统,不基于系统当前状态或结果进行调整
  2. 输入与输出之间没有信息回路
  3. 系统不会根据执行结果来自动修正控制信号

开环抓取:基于视觉位姿估计,预测抓取位姿,执行抓取,视觉只会用到一次,如果失败(如掉落、没抓起来),不会尝试修正,“蒙着眼睛做事情”。

闭环抓取:基于视觉位姿估计,预测抓取位姿,执行抓取,如果抓取失败,则调整抓取位姿,重新抓取。

开环抓取系统

一般处理流程:

  1. 视觉感知
  2. 位姿估计
  3. 运动规划
  4. 控制执行

对已知物体的抓取

由于物体信息已知,可以通过对物体的位姿进行预测。也就是在物体自身坐标系下进行抓取标注,然后转换到世界坐标系下。

  1. RGB 图像,若满足

    1. 相机内参(将三维空间点投影到二维图像平面的关键参数,包括焦距、主点灯)已知:逆向的关键

    2. 物体大小已知:避免歧义(ambiguity)

      why_pigeon_so_big

      道理我都懂,但是这个鸽子怎么这么大?

    3. 物体无对称性

    那么其可以唯一对应一个位姿

  2. 点云(Point Cloud)图像,只需满足物体 无对称性,那么就可以唯一对应一个位姿。

Iterative Closest Point (ICP) 算法

流程:

  1. 初始化变换估计 $T_0 = (R_0, t_0)$

  2. 迭代直至收敛:

    1. 数据关联:确立变换后最近邻点对,建立模板点云 $M$ 与场景点云 $S$ 对应关系

      $$ C = { (m_i, s_j) | s_j = \arg \min_{s \in S} | T_k m_i - s | } $$

    2. 变换求解:最小化对应点距离 $$ T_{k+1} = \arg \min_T \sum_{(m,s) \in C} | Tm - s |^2 $$

问题:比较怕物体被挡住造成 点云缺失

对未知物体的抓取

直接预测抓取位姿。

也有算法可以从见过同一类别物体进行泛化。

旋转回归(Rotation Regression)

回归:估计连续变量。

旋转回归:一种特殊的回归任务,对于输入信号,经由神经网络估计连续的旋转变量。

Typora 2025-03-18 16.13.52

其中,表示方式空间 $R$ 可以是四元数、欧拉角等,而 $X$ 是 $\mathbb{SO}(3)$ 群。

回顾一下 $\mathbb{SO}(3)$ 群的定义,$\mathbb{SO}(3)$ 是特殊正交群(Special Orthogonal group),由所有三维旋转矩阵组成。

3D 旋转矩阵 $R$ 是一个 $3\times3$ 矩阵,满足以下条件:

  • $R^{\top}R = I$ (正交性)
  • $\det R = +1$ (保持右手坐标系)

$\mathbb{SO}(2) / \mathbb{SO}(3)$ 具有很好的连续性,没有跳变点的存在。

与普通回归不同,旋转表示在非线性空间、非欧空间中,所以对于之前所讲过的所有旋转的表达方式,简单地使用 MSE 来作为监督信号都会不够理想。

这是因为,CNN 理应具有连续性,对于输入的微小变动,其输出不应当造成很大的改变。

而如果对于某一旋转表达方式,存在这种 Ground Truth 监督信号的跳变,神经网络为了拟合这种跳变点,就会导致其权重矩阵 $W$ 出现一些很大的参数,造成数值不稳定性的同时,为之消耗大量的注意力,大部分的训练过程都去拟合这种跳变而不是其他占比更多、更泛用的部分,这是非常不好的。并且这一过程是 Loss 无关的,是由于选择了不好的表达方式造成的本质性问题。

所以,理想的表达方式,应当满足:

  1. 双射,表达方式到 $\mathbb{SO}(3)$ 群是一一映射的,否则特定旋转时可能出现多种等价表示,这使得神经网络难以学习
  2. 连续, $\mathbb{SO}(3)$ 群中任何一点附近的连续变化,其对应的表达方式应当也是连续变化,也即不存在性质不好的 奇点(Singularities)

欧拉角

欧拉角使用三个角度(通常表示为 $\alpha$、$\beta$、$\gamma$)来描述绕三个主轴的连续旋转,完整的旋转矩阵可以通过组合这些基本旋转得到:

$$ R = R_x(\alpha)R_y(\beta)R_z(\gamma) $$

问题:欧拉角的表达方式天然存在非双射、万象节锁的问题

举例:考虑 2D 的情况,此时使用单一自由度 $\theta$ 来代表绕轴旋转的角度。

euler_angle_rotation_discontinuity

绕旋转轴转 $0$ 和 $2\pi$ 是一样的,但是在实际的 $\mathbb{SO}(2)$ 中是连续的。

一个解决方法是 引入冗余维度,把低维空间中的的不连续改成高维空间中的连续,如 $\theta \to (x,y)$,后者是连续的,且能反向求解出前者。

轴角

轴角表示由一个单位向量 $\mathbf{e} = [e_x, e_y, e_z]^{\top}$(表示旋转轴)和一个标量 $\theta$(表示旋转角度)组成:

$$ (\text{axis}, \text{angle}) = (\mathbf{e}, \theta) $$

可以使用罗德里格旋转公式(Rodrigues' rotation formula)将轴角表示转换为旋转矩阵:

$$ R = I + (\sin\theta)K + (1-\cos\theta)K^2 $$

其中 $K = [\mathbf{e}]_\times$ 是其叉乘矩阵:

$$ K = \begin{bmatrix} 0 & -e_z & e_y \ e_z & 0 & -e_x \ -e_y & e_x & 0 \end{bmatrix} $$

问题: 当 $\theta = 0$ 时,任何轴都表示单位旋转(即不旋转);当 $\theta = \pi$ 时,绕某个轴的旋转 $(\mathbf{e}, \pi)$ 和绕它的反方向 $(-\mathbf{e}, \pi)$ 表示相同的旋转。

四元数

四元数是复数的一种推广,形式为:

$$ q = w + xi + yj + zk $$

其中 $w$ 是实部,向量 $\mathbf{v} = (x, y, z)$ 是虚部,且 $i^2 = j^2 = k^2 = ijk = -1$。

任何一个旋转,即绕某个单位向量 $\hat{\omega}$ 旋转 $\theta$ 角度,对应的四元数可以表示为:

$$ q = \left[\cos\frac{\theta}{2}, \sin\frac{\theta}{2} \hat{\omega}\right] $$

问题:四元数存在 “双重覆盖” 关系。

我们可以很容易地发现:

$$ \begin{aligned} q &= \left[\cos\frac{\theta}{2}, \sin\frac{\theta}{2} \hat{\omega}\right] \ -q &= \left[-\cos\frac{\theta}{2}, -\sin\frac{\theta}{2}\hat{\omega}\right] \ &= \left[\cos(\pi - \frac{\theta}{2}), \sin(\pi - \frac{\theta}{2}) (-\hat{\omega})\right] \end{aligned} $$

是等价的($-q$ 意味着同一旋转轴但是翻转正方向,然后旋转 $2\pi - \theta$)。

double_coverage

为此,我们通常约束四元数位于上半球(即 $w \geq 0$),但这又会引入新的不连续性:

  1. 临近球大圆的不连续性

    quaternion_double_coverage_fix_issue

  2. 球大圆上的不连续性:由于双重覆盖,我们只能取一个半圆,但是在这个切面圆的直径上,我们还是只能选取两个切点中的一个(否则又存在双重覆盖问题,$q = -q$),而这么选取的话,在这个点附近,依旧有类似欧拉角的跳变存在(还是那个原因,在这个点附近的微小变动会引发跳变)

    quaternion_issue

6D 表示

为了解决不连续性问题,我们放弃了选择上述方法,改为回到旋转矩阵本身。

直接尝试拟合旋转矩阵,会引入 9 个数的自由度,我们还需要映射到 $\mathbb{SO}(3)$,所以引入进行施密特正交化以满足旋转矩阵条件:

  1. 第一列标准化
  2. 第二列只保留垂直于第一列的分量,然后标准化
  3. 第三列通过第一列和第二列的叉乘确定

形式化表示为:

$$ f_{GS}\left(\begin{bmatrix} \mathbf{a}_1 & \mathbf{a}_2 \end{bmatrix}\right) = \begin{bmatrix} \mathbf{b}_1 & \mathbf{b}_2 & \mathbf{b}_3 \end{bmatrix} $$

其中:

$$ \mathbf{b}_i = \begin{cases} N(\mathbf{a}_1) & \text{if } i = 1 \ N(\mathbf{a}_2 - (\mathbf{b}_1 \cdot \mathbf{a}_2)\mathbf{b}_1) & \text{if } i = 2 \ \mathbf{b}_1 \times \mathbf{b}_2 & \text{if } i = 3 \end{cases} $$

其中 $N(\mathbf{v})$ 表示向量 $\mathbf{v}$ 的归一化。

这种表示实际上只有 6 个自由度,所以我们叫它 6D 表示方法。

然而,这个方法固然简单,但是他引入了新的问题:拟合得到的 9 个数彼此并不等价。

  1. 对于第一列,是一等公民,直接归一化
  2. 对于第二列,是二等公民,需要移除平行于第一列的分量
  3. 对于第三列,甚至完全不考虑它的数值,正交系的三个向量直接由前两个叉乘得到

所以,这种表示方式与传统的 L2 Norm 的损失函数并不协调。

当然我们可以相对应地分优先级,第一列直接算,第二列需要加权,第三列直接排除在损失函数之外,但直觉上就会感觉到不平衡的存在 —— 神经网络输出的各个神经元本应等价,但是你算 Loss 的时候还要排除,哪有这样的道理?

9D 表示

9D 表示直接使用完整的旋转矩阵(9 个元素)作为表示。为将神经网络的欧几里得输出映射到 $\mathbb{SO}(3)$,同时满足前述要求:

  1. 双射
  2. 连续
  3. 等价

我们使用奇异值分解(SVD)对之进行正交化:

$$ \hat{R} = U\begin{bmatrix} 1 & 0 & 0 \ 0 & 1 & 0 \ 0 & 0 & \det(UV) \end{bmatrix}V^{\top} $$

其中 $U$ 和 $V$ 是对神经网络预测除的矩阵进行 SVD 分解得到的正交矩阵,$\det(UV)$ 项确保结果矩阵的行列式为 +1,满足旋转矩阵的性质。

SVD 的基本过程

给定任意矩阵 $M \in \mathbb{R}^{3 \times 3}$,其奇异值分解(SVD)为:

$$ M = U \Sigma V^{\top} $$

其中:

  • $U$ 和 $V$ 是正交矩阵($U U^{\top} = V V^{\top} = I$)
  • $\Sigma$ 是对角矩阵,对角线元素为奇异值 $\sigma_1 \geq \sigma_2 \geq \sigma_3 \geq 0$

对于我们预测的旋转矩阵而言,这里分解得到的奇异值会很接近 1,但不一定就是 1,所以直接换掉它来使之满足正交化条件。

优势:CNN Friendly

  • 不区分对待矩阵的每一行,实现完全连续、一一映射的表示
  • 与神经网络的欧几里得输出空间兼容

增量旋转预测

对于预测增量旋转(delta rotation),即 $\mathbb{SO}(3)$ 在单位矩阵 $I$ 附近的小范围旋转,前面几种表示方式实际上都可以,因为此时在这个邻域没有了我们考虑了半天的奇点(Singularities)问题。

而且,此时由于四元数等表示方式需要预测参数更少,学习起来甚至可能更快。

Rotation Fitting

使用神经网络先预测物体坐标或对应关系,然后解算旋转。具体步骤包括:

  1. 对物体表面的每个像素,预测其在物体建模模型上的 3D 坐标
  2. 基于这些对应关系拟合旋转矩阵

这种方法建立了模型坐标系(model) $(x_i^M, y_i^M, z_i^M)$ 和相机坐标系(camera) $(x_i^C, y_i^C, z_i^C)$ 两个坐标系之间的对应关系。

我们的目标是找到将模型坐标系转换到相机坐标系的最优变换矩阵(要求物体大小不变)。

model_to_camera_coordinates

这要求物体是见过的、标注过的,不然没法比对(缺乏 $(x_i^M, y_i^M, z_i^M)$ 模型坐标系基础定义)。

  • 有 Depth 信息:3d to 3d,$(u,v, d) \to (x_i^M, y_i^M, z_i^M)$
  • 没有 Depth 信息:2d to 3d,$(u,v) \to (x_i^M, y_i^M, z_i^M)$

正交 Procrustes 问题

给定两组对应的 3D 点集,不考虑位移 $t$ 的纯旋转拟合(求解它们之间的最优旋转矩阵)可以形式化为正交 Procrustes 问题,这是一个矩阵逼近问题。

定义:给定矩阵 $\mathbf{M} \in \mathbb{R}^{n \times p}$ 和 $\mathbf{N} \in \mathbb{R}^{n \times p}$,我们需要求解:

$$ \hat{\mathbf{A}} = \arg\min_{\mathbf{A} \in \mathbb{R}^{p \times p}} |\mathbf{M}^{\top} - \mathbf{AN}^{\top}|F^2 = \arg\min{\mathbf{A} \in \mathbb{R}^{p \times p}} |\mathbf{M} - \mathbf{NA}^{\top}|_F^2 \ \text{subject to} \quad \mathbf{A}^{\top}\mathbf{A} = \mathbf{I} $$

其中,$|\cdot|_F$ 表示 Frobenius 范数,定义为:

$$ |X|F = \sqrt{\text{trace}(X^{\top}X)} = \sqrt{\sum{i,j} x_{ij}^2} $$

这里:

  • $\mathbf{M}$ 可以表示目标坐标系中的点集(例如相机坐标系)

  • $\mathbf{N}$ 表示源坐标系中的对应点集(例如模型坐标系)

  • 求解的 $\mathbf{A}$ 即为从 $\mathbf{N}$ 到 $\mathbf{M}$ 的最优旋转矩阵

  • 约束条件 $\mathbf{A}^{\top}\mathbf{A} = \mathbf{I}$ 确保 $\mathbf{A}$ 是正交矩阵,保证了纯旋转变换(不包含缩放或剪切)。

正交 Procrustes 问题有一个优雅的解析解,可以通过奇异值分解(SVD)获得。如果我们对矩阵 $\mathbf{M}^{\top}\mathbf{N}$ 进行 SVD 分解:

$$ \mathbf{M}^{\top}\mathbf{N} = \mathbf{UDV}^{\top} $$

那么最优旋转矩阵为:

$$ \hat{\mathbf{A}} = \mathbf{UV}^{\top} $$

数学证明

首先回顾迹运算的性质:

  1. 线性性质:$\text{tr}(A + B) = \text{tr}(A) + \text{tr}(B)$
  2. 循环性质:$\text{tr}(ABC) = \text{tr}(BCA) = \text{tr}(CAB)$
  3. 转置性质:$\text{tr}(A^{\top}) = \text{tr}(A)$
  4. 标量提取:$\text{tr}(cA) = c·\text{tr}(A)$,其中 $c$ 为标量
  5. 与 Frobenius 范数的关系:$|A|_F^2 = \text{tr}(A^{\top}A) = \text{tr}(AA^{\top})$

利用迹运算的性质和 $\mathbf{A}$ 是正交矩阵的条件($\mathbf{A}^{\top}\mathbf{A} = \mathbf{I}$):

$$ \begin{aligned} |\mathbf{M} - \mathbf{NA}^{\top}|_F^2 &= \text{tr}((\mathbf{M} - \mathbf{NA}^{\top})^{\top}(\mathbf{M} - \mathbf{NA}^{\top}))\ &= \text{tr}(\mathbf{M}^{\top}\mathbf{M} - \mathbf{M}^{\top}\mathbf{NA}^{\top} - \mathbf{AN}^{\top}\mathbf{M} + \mathbf{AN}^{\top}\mathbf{NA}^{\top}) \ &= \text{tr}(\mathbf{M}^{\top}\mathbf{M}) - \text{tr}(\mathbf{M}^{\top}\mathbf{NA}^{\top}) - \text{tr}(\mathbf{AN}^{\top}\mathbf{M}) + \text{tr}(\mathbf{AN}^{\top}\mathbf{NA}^{\top}) \ &= \text{tr}(\mathbf{M}^{\top}\mathbf{M}) - \text{tr}(\mathbf{M}^{\top}\mathbf{NA}^{\top}) - \text{tr}((\mathbf{M}^{\top}\mathbf{NA}^{\top})^{\top}) + \text{tr}(\mathbf{N}^{\top}\mathbf{N}\mathbf{A}^{\top}\mathbf{A}) \ &= \text{tr}(\mathbf{M}^{\top}\mathbf{M}) - 2\text{tr}(\mathbf{M}^{\top}\mathbf{NA}^{\top}) + \text{tr}(\mathbf{N}^{\top}\mathbf{N}) \end{aligned} $$

注意到第一项 $\text{tr}(\mathbf{M}^{\top}\mathbf{M})$ 和第三项 $\text{tr}(\mathbf{N}^{\top}\mathbf{N})$ 都不依赖于 $\mathbf{A}$,因此最小化目标函数等价于最大化第二项 $\text{tr}(\mathbf{M}^{\top}\mathbf{NA}^{\top})$。

当我们有 SVD 分解 $\mathbf{M}^{\top}\mathbf{N} = \mathbf{UDV}^{\top}$ 时,可以将迹运算展开:

$$ \begin{aligned} \text{tr}(\mathbf{M}^{\top}\mathbf{NA}^{\top}) &= \text{tr}(\mathbf{UDV}^{\top}\mathbf{A}^{\top}) \ &= \text{tr}(\mathbf{UD}(\mathbf{AV})^{\top}) \ &= \text{tr}((\mathbf{AV})^{\top}\mathbf{UD}) \quad (\text{循环性质,左乘正交矩阵逆,右乘正交矩阵}) \ &= \sum_{i=1}^{d}[(\mathbf{AV})^{\top}\mathbf{U}]_{ii}d_i \end{aligned} $$

其中 $d_i$ 是矩阵 $\mathbf{D}$ 对角线上的第 $i$ 个元素,$d$ 是 $\mathbf{M}^{\top}\mathbf{N}$ 的非零奇异值的数量。

为了最大化上述和式,我们需要使 $(\mathbf{AV})^{\top}\mathbf{U}$ 的对角元素尽可能大。由于 $\mathbf{AV}$ 和 $\mathbf{U}$ 都是正交矩阵,因此 $(\mathbf{AV})^{\top}\mathbf{U}$ 也是正交矩阵,其对角元素的绝对值不能超过 1(否则对应的列 / 行的 $L_2$ 范数会超过 1)。

因此,该和式在所有 $(\mathbf{AV})^{\top}\mathbf{U}$ 的对角元素都等于 1 时达到最大值,即:

$$ \begin{aligned} (\mathbf{AV})^{\top}\mathbf{U} &= \mathbf{I} \ \mathbf{AV} &= \mathbf{U} \ \mathbf{A} &= \mathbf{UV}^{\top} \end{aligned} $$

后处理

正交 Procrustes 问题的基本约束 $\mathbf{A}^{\top}\mathbf{A} = \mathbf{I}$ 保证了 $\mathbf{A}$ 是一个正交矩阵。但正交矩阵即可以是旋转($\det \mathbf{A} = +1$),也可以是 反射 (改变手性,$\det \mathbf{A} = -1$)

所以,如果计算出的 $\det(\mathbf{UV}^{\top}) = -1$,表明 $\mathbf{UV}^{\top}$ 是一个反射。为了得到最接近的纯旋转,我们通过修改 SVD 中间对角矩阵 $\mathbf{D}$ 的最后一个元素符号来 “翻转” 这个反射。具体做法就是将解修正为:

$$ \hat{\mathbf{A}} = \mathbf{U}\begin{pmatrix} 1 & 0 & 0 \ 0 & 1 & 0 \ 0 & 0 & \det(\mathbf{UV}^{\top}) \end{pmatrix}\mathbf{V}^{\top} $$

直观上,这代表选择翻转关联性最弱的方向,是因为这样做对整体对齐效果(即 Frobenius 范数或等价的迹最大化目标)的影响是最小的。

位移求解

可以想到,一旦旋转矩阵确定,那么位移向量 $t$ 就非常好解了(计算变换前后差值即可)。

将一个变换矩阵转换为刚才说的正交 Procrustes 问题,也只需要对两个原始点集 $\mathbf{M}$ 和 $\mathbf{N}$ 分别减去各自的几何中心即可。

步骤:

  1. 中心化

    • 计算两个点集的质心:$\overline{\mathbf{M}}$(M 的均值), $\overline{\mathbf{N}}$(N 的均值)。
    • 得到中心化后的点集:$\tilde{\mathbf{M}} = \mathbf{M} - \overline{\mathbf{M}}$, $\tilde{\mathbf{N}} = \mathbf{N} - \overline{\mathbf{N}}$。
  2. 求解旋转 $\hat{\mathbf{R}}$:对中心化后的点集 $\tilde{\mathbf{M}}$ 和 $\tilde{\mathbf{N}}$ 应用 带约束的正交 Procrustes 算法 (要求 $\det(\mathbf{R})=+1$),求解最优旋转 $\hat{\mathbf{R}}$,使得 $\tilde{\mathbf{M}}^{\top} \approx \hat{\mathbf{R}}\tilde{\mathbf{N}}^{\top}$。

  3. 求解平移 $\hat{\mathbf{T}}$:利用已求得的 $\hat{\mathbf{R}}$ 和原始点集的质心计算最优平移: $$ \hat{\mathbf{T}} = \overline{\mathbf{M}^{\top} - \hat{\mathbf{R}} \mathbf{N}^{\top}} $$

问题

草,刚上完的计算机视觉导论还在追我!

对于 Outlier 较为敏感,使用 RANSAC 算法即可。

以下内容直接摘录自 CV 导论笔记,看过的可以直接跳。

最小二乘法(Least Square Method)

定义:假设有一组数据点 $(x_i, y_i)$,我们希望通过直线 $y = mx + b$ 拟合这些点。

其能量函数(损失函数)为:

$$ E = \sum_{i=1}^n (y_i - mx_i - b)^2 $$

not_roboust_outliner

最小二乘法的一个问题是对细微噪声 鲁棒(robust),但是对于 离群点(Outliers) 敏感。如图,为了照顾一个离群点,整个直线发生了很大的旋转。

RANSAC(RANdom SAmple Consensus,随机抽样一致算法)

动机:我们想要一个自动化的算法,可以确定离群点(outliers)并排除之。

想法:我们希望找到一条直线,使得这条直线有最多的内点(inliner)。

RANSAC loop:假设这条直线需要 2 个点(或选择 $n$ 个点,但选择最少的 2 个点可以保证这些点中没有 outliers 的概率最大)来确定:

  1. 随机选择 $k$ 组能确定这条直线的点(即从所有点中选出一个 $k \times 2$ 的矩阵)。
  2. 对每一组点计算出一条直线(使用 SVD)。
  3. 对每一组点的直线,计算所有点到这条直线的距离;若距离小于阈值,则认为该点是这条直线的内点(inliner)。
  4. 找到内点数量最多的直线,若数量超过阈值,则认为这条直线是最优的。
  5. 对最优直线,用其所有内点重新计算一次直线。
  6. 重复上述步骤,直到内点数量不再增加。

注意:此过程可推广到 $n$ 维空间,只需选择 $\geq n$ 个点来确定一个 $n-1$ 维的超平面。

实际上,从今天的视角来看,此循环(loop)不再必需,因为我们可以并行地提出所有假设(Hypothesis),CV 导论中将此留作作业。

Instance level

对物体级别的位姿变换预测,要求每个物体都已知(完整建模),典型算法如 PoseCNN,如果结合 ICP 算法可以在位移幅度较小的情况下更快的提升准确率(下节课详细讲)。

posecnn_result

Catagory level

对同一类别物体的位姿变换预测,这类物品通常具有共有结构,如茶杯具有相近的几何形状,可以用于定位(下节课详细讲)。

在同类别物体中进行泛化,但也因此受限,没见过的类别不行。

大小不知道,能给出旋转 Rotation 不能给平移 Translation,因为可能沿着物体光轴走,还是那个鸽子为什么这么大的问题,所以 Catagory level 必须要知道大小。

why_pigeon_so_big_2

那么如何在同一类别物体的不同尺寸之间进行泛化呢,答案是类似归一化的想法,把同一类别的东西缩放到一个标准的 1x1x1 box 内,将其几何中心归一化到 box 中心,从而统一他们的尺度。

💾

  •  

插件-simple-redis-cache

#插件 Simple Redis Cache (已停止使用)是一款功能实用且易于配置的 WordPress 插件,旨在通过将对象缓存存储到 Redis,有效减少数据库查询次数,加快页面加载速度,全面提升网站性能与响应效率。插件支持自定义 Redis 主机、端口、密码、数据库索引及连接和读取超时设置,适配多种服务器环境。同时,插件可自动生成和管理 object-cache.php 文件,提供缓存连接状态监测、缓存统计信息及一键清除缓存功能,确保缓存机制稳定高效运行。兼容 WordPress 4.0 及以上版本,适合追求网站加速与优化的用户使用。

## 主要功能

- 简单配置 Redis 连接参数

- 自动生成 object-cache.php 文件

- 支持密码保护的 Redis 服务器

- 支持多个数据库索引

- 可配置连接超时和读取超时

- 提供缓存统计信息

- 支持清除缓存功能

- **新增:WordPress 后台缓存排除功能,避免后台操作问题**

## 系统要求

- WordPress 4.0 或更高版本

- PHP 5.6.0 或更高版本

- PHP Redis 扩展

## 安装方法

1. 上传 `simple-redis-cache` 文件夹到 `/wp-content/plugins/` 目录

2. 在 WordPress 后台激活插件

3. 访问 "设置 > Redis缓存" 配置 Redis 连接信息

4. 点击 "生成 object-cache.php" 按钮创建缓存文件

## 配置选项

### Redis 主机

Redis 服务器的主机名或 IP 地址。默认为 127.0.0.1(本地主机)。

### Redis 端口

Redis 服务器的端口号。默认为 6379。

### Redis 密码

Redis 服务器的密码。如果没有设置密码,请留空。

### Redis 数据库

Redis 数据库索引(0-16)。默认为 0。

### 连接超时

连接到 Redis 服务器的超时时间(秒)。默认为 1 秒。

### 读取超时

从 Redis 服务器读取数据的超时时间(秒)。默认为 1 秒。

## 使用方法

1. 安装并激活插件后,访问 "设置 > Redis缓存" 页面

2. 配置 Redis 连接信息

3. 点击 "保存更改" 按钮保存设置

4. 点击 "生成 object-cache.php" 按钮创建缓存文件

5. 插件会自动测试 Redis 连接,并显示连接状态

## 常见问题

### 如何检查 Redis 缓存是否正常工作?

在插件设置页面可以看到 Redis 连接状态和缓存统计信息。如果显示 "连接成功",则表示 Redis 缓存已正常工作。

### 如何清除 Redis 缓存?

在插件设置页面点击 "清除对象缓存" 按钮即可清除所有 Redis 缓存数据。

### 如何卸载插件?

停用插件前,建议先点击 "删除 object-cache.php 文件" 按钮,然后再停用和删除插件。

### 为什么需要 PHP Redis 扩展?

本插件使用 PHP Redis 扩展与 Redis 服务器通信,这比使用纯 PHP 实现的客户端更高效。

### 后台操作出现问题怎么办?

最新版本已添加后台缓存排除功能,自动检测 WordPress 后台请求并禁用 Redis 缓存,确保后台操作正常。如果您仍然遇到问题,请尝试以下解决方法:

1. 更新插件到最新版本

2. 重新生成 object-cache.php 文件

3. 如果问题仍然存在,可以临时禁用 Redis 缓存(点击"删除 object-cache.php 文件"按钮)

## 故障排除

### Redis 连接失败

- 确保 Redis 服务器已启动并运行

- 检查主机名和端口是否正确

- 如果设置了密码,确保密码正确

- 检查防火墙设置是否允许连接

### 插件无法生成 object-cache.php 文件

- 确保 wp-content 目录可写

- 检查服务器权限设置

- 尝试手动创建文件

### 插件无法停用

- 使用插件设置页面中的 "删除 object-cache.php 文件" 按钮

- 如果仍然无法停用,手动删除 wp-content/object-cache.php 文件

### 删除插件后重新安装出现问题

如果删除插件后重新安装,点击生成 object-cache.php 时出现插件被停用的情况,可能是因为旧的 object-cache.php 文件仍然存在。解决方法:

- 手动删除 wp-content/object-cache.php 文件后再重新安装插件

- 插件已更新,现在会强制删除旧文件并生成新文件,避免此类问题

### WordPress 后台操作问题

如果在 WordPress 后台进行操作时遇到问题(如保存文章失败、设置无法更新等),这可能是由于 Redis 缓存导致的。最新版本已添加后台缓存排除功能,自动检测后台请求并禁用 Redis 缓存,确保后台操作正常。

## 更新日志

### 1.0.3

- 添加 WordPress 后台缓存排除功能,自动检测后台请求并禁用 Redis 缓存

- 修复后台操作可能出现的问题

- 优化缓存处理逻辑,提高稳定性

### 1.0.2

- 修复生成object-cache.php文件后插件被停用的问题

- 增强插件与WordPress的兼容性

- 添加插件冲突检测和自动处理机制

- 改进文件生成和删除逻辑

- 添加文件生成时间戳和唯一标识符

### 1.0.1

- 修复删除插件后重新安装时的兼容性问题

- 增强文件版本校验功能

- 改进插件停用和卸载逻辑

- 强化 object-cache.php 文件生成机制

### 1.0.0

- 初始版本发布

## 许可证

本插件采用 GPL v2 或更高版本许可证。 

(该插件已不使用,暂停更新!)

  •  

插件-simple-page-cache

#插件 Simple Page Cache 是一款轻量、高效的 WordPress 页面缓存插件,旨在提升网站加载速度并优化站点健康检查中的缓存表现。插件激活后自动为访客生成页面缓存,并添加必要的 HTTP 缓存头,无需复杂配置即可使用。它支持移动设备的单独缓存,并在发布文章、更新内容或收到评论时自动清除相关缓存,确保内容实时更新。同时,插件通过正则处理评论表单,防止缓存用户的姓名、邮箱等个人信息,有效保护用户隐私。简洁的管理界面还支持手动清除缓存,为站长提供灵活控制。

## 功能

- 自动为访客创建页面缓存

- 添加必要的 HTTP 缓存头信息

- 在内容更新时自动清除相关缓存

- 提供简单的管理界面

- 支持移动设备的单独缓存

- 缓存包含评论表单的页面,但评论表单通过AJAX动态加载

- 仪表盘小工具支持一键清除缓存功能,无需跳转

- 显示缓存命中率统计,帮助评估缓存效果

## 安装

1. 下载插件并解压

2. 将插件文件夹上传到 `/wp-content/plugins/` 目录

3. 在 WordPress 管理后台激活插件

## 使用方法

插件激活后会自动开始缓存页面,无需额外配置。

如果需要手动清除缓存,可以直接在仪表盘小工具中点击"清除缓存"按钮,或在"设置" > "简单页面缓存"中操作。

您可以在仪表盘小工具和设置页面中查看缓存命中率统计,了解缓存的实际效果。

## 注意事项

- 此插件不会为登录用户缓存页面

- 搜索结果、404页面和Feed不会被缓存

- 包含评论表单的页面会被缓存,但评论表单会通过AJAX动态加载,确保评论功能正常

- 当发布新文章、更新内容或收到新评论时,相关页面的缓存会自动清除

## 技术说明

插件使用了创新的缓存策略来处理评论表单:

1. **智能缓存处理**:缓存页面内容,但移除评论表单,替换为占位符

2. **AJAX动态加载**:通过JavaScript在页面加载后动态获取评论表单

3. **HTTP缓存头优化**:提供标准的缓存头信息,增强浏览器缓存效果

4. **缓存清理机制**:在内容更新时自动清除相关页面的缓存

5. **缓存命中率统计**:记录并显示缓存命中率,帮助评估缓存效果

6. **AJAX缓存管理**:使用AJAX技术实现一键清除缓存,提升用户体验

这种方法既保证了缓存的全面覆盖(包括评论页面),又确保了评论功能的正常运行,是一种兼顾性能和功能的优化解决方案。

## 性能优化

本插件采用了多种技术来确保最佳性能:

1. **条件检查优化**:使用精简的条件检查,快速判断是否需要缓存

2. **代码结构优化**:移除冗余逻辑,提高执行效率

3. **缓存文件管理**:使用专用方法管理缓存文件的创建和删除

4. **CSS优化**:压缩内联CSS,减少管理界面加载时间

5. **HTTP头优化**:提供标准化的缓存头信息,增强浏览器缓存效果

6. **评论表单AJAX加载**:确保页面主体内容快速加载,评论表单延迟加载

7. **AJAX后台操作**:使用AJAX技术优化后台操作,无需页面刷新

8. **命中率统计**:提供缓存命中率统计,帮助评估缓存效果

## 更新日志

### 版本 1.4.1

- 添加缓存命中率统计功能,在仪表盘小工具和设置页面中显示

- 显示缓存命中与未命中次数,便于分析缓存效果

- 优化仪表盘小工具布局,改为三列显示

- 清除缓存后AJAX更新包含最新命中率信息

- 当清除所有缓存时,重置命中率统计

### 版本 1.4.0

- 优化仪表盘小工具,现在可以直接通过按钮清除缓存,无需跳转到设置页面

- 添加AJAX处理功能,清除缓存后实时更新统计信息

- 改进用户体验,清除缓存操作完成后显示即时反馈

- 添加状态提示和视觉反馈,让用户更清楚操作结果

### 版本 1.3.0

- 改进缓存策略,现在可以缓存包含评论表单的页面

- 添加评论表单AJAX动态加载功能,确保评论功能正常工作

- 使用占位符标记评论表单位置,提高用户体验

- 优化JavaScript代码,使用更安全的DOM操作方法

- 提高页面加载速度,同时保持评论功能完整可用

### 版本 1.2.2

- 修复评论提交后再次提交无法成功的问题

- 改进评论表单nonce处理,确保每次提交都有新的有效nonce值

- 添加评论提交后的临时cookie标记,优化缓存清理逻辑

- 保留父评论ID信息,修复评论回复功能

- 在缓存页面中添加时间戳标记,确保缓存总是最新的

### 版本 1.2.1

- 修复评论提交后再次提交评论失败的问题

- 添加评论提交后自动清除用户特定缓存功能

- 优化评论提交过程,防止POST请求被缓存

- 改进缓存清除机制,确保用户能看到最新状态

- 增强评论页面处理逻辑,支持连续多次评论提交

### 版本 1.2.0

- 添加用户评论信息识别功能,允许同一用户查看自己的评论信息

- 为用户评论信息创建独立缓存,防止信息泄露给其他访客

- 改进缓存文件命名机制,支持用户特定缓存

- 优化评论表单处理逻辑,提升用户体验

- 更新了技术文档和说明

### 版本 1.1.1

- 优化代码结构,减少代码冗余,提高执行效率

- 改进条件判断逻辑,使用统一的跳过缓存条件数组

- 抽取公共方法,如缓存统计、缓存文件删除等

- 压缩内联CSS,减少管理界面加载时间

- 优化评论表单处理逻辑,提高处理效率

### 版本 1.1.0

- 增强了评论表单处理机制,使用DOM解析和正则表达式双重保障

- 新增支持缓存包含评论表单的页面,同时保护用户隐私

- 改进了评论提交后的缓存清除机制,包括评论分页和相关小工具

- 处理JavaScript存储的评论者信息,防止通过脚本恢复个人信息

### 版本 1.0.0

- 初始版本发布 说明:因精力有限,这里所提供的插件版本不能保证及时更新,如需最新版请联系我索取。

  •  

为了站点健康的笑脸

昨天查看了一下 WordPress 站点健康状态,有两条建议:一个是关于未检测到页面缓存问题的,另一个是关于未使用对象缓存问题的。其实很早就知道有这两项需要优化,但以前经常折腾博客改代码。一是没顾上,二是觉得折腾主题时还得记着随时清除缓存才能看到修改后的效果,嫌麻烦就没动它。等今天再看到时,强迫症发作了,看不得后台有这烦人的提示,于是开始在 WordPress 插件市场寻找能够解决这两个问题的插件。

然而,体验了几个插件后,其实对现在流行的 WordPress 插件挺无语的。感觉插件市场上大多数插件似乎都趋向于功能大杂烩的模式,集成了过多附加功能,而且多少有点知名度的插件还常常通过额外付费升级来赚取收益。还有就是很多的轻量化插件却由于多年没有维护,已经无法兼容。

无奈之下,只好借助 cousor 自己动手折腾了一番,最终鼓捣出来两个插件,分别用于对象缓存和页面缓存。这两个插件要说优点,可能只有一个:那就是简单到只针对单一问题的插件!有需要的可以在稍后发布的笔记-插件话题中下载尝试。

  •  

机器人学 III

运动规划

配置空间(Configuration Space)

定义:机器人的所有可能关节状态构成的抽象空间,记为 $\mathcal{C}-\text{space}$。

  • Q 表示法:$Q = (q_1, q_2, \ldots, q_n)$,其中 $q_i$ 为第 $i$ 个关节的位置参数(如角度或位移)。
  • 自由空间(Free Space)$\mathcal{C}_{\text{free}}$:不与障碍物碰撞的合法配置集合。
  • 障碍空间(Obstacle Space)$\mathcal{C}_{\text{obs}}$:与障碍物发生碰撞的非法配置集合。

路径规划问题:在 $\mathcal{C}{\text{free}}$ 中寻找从起点 $Q{\text{start}}$ 到目标 $Q_{\text{goal}}$ 的连续路径。

挑战:避障、长程规划、高维空间规划

碰撞检测(Collision Detection)

基本挑战

问题定义:给定一个 $q_{\text{pose}}$,判断机械臂是否与环境发生碰撞(collision)。也即判断其是在 $\mathcal{C}{\text{free}}$ 中还是在 $\mathcal{C}{\text{obs}}$ 中。

几何复杂度:机械臂与环境的高精度三维模型(如三角网格 / 面片,mesh)直接检测碰撞计算量很大。

计算瓶颈:检测两个含 $10^5$ 三角面片的模型是否碰撞需 $O(10^{10})$ 次面片相交判断。

球体包裹法(Bounding Spheres)

思想:用球体序列近似机械臂连杆(如下图)。

bounding_spheres

碰撞检测公式:两球体中心 $\mathbf{p}_i, \mathbf{p}_j$ 满足 $|\mathbf{p}_i - \mathbf{p}_j| \leq (r_i + r_j)$ 时碰撞。

优缺点:

  • 优点:计算高效($O(n^2)$ 复杂度,$n$ 为球体数)。
  • 缺点:保守性导致可行解丢失,限制了模型对于更精细物体的操作能力
    • 你不能通过球体近似抓起来一个很小的面片
    • 球体近似还可能导致虚假自碰撞(self-collision,即不同连杆之间的碰撞)

凸包分解(Convex Decomposition)

思想:将凹几何体分解为多个凸包(Convex Hull),利用凸包相交检测算法加速。

原因:检测多个凸起来的物体之间是否发生碰撞是很很高效的(类似之前的球体近似),但是检测凸起来的物体和凹进去的物体之间是否发生碰撞是比较困难的。

分类:

  • 凸包(Convex-Hull):生成单一的凸网格,效率最高但精度较低。
  • 精确凸分解(Exact Convex Decomposition):属于 NP-hard 问题,不实用,因为会产生大量的聚类。
  • 近似凸分解(Approximate Convex Decomposition, ACD):确定网格三角形的划分,使用最少的聚类数量,同时确保每个聚类的凹度低于用户定义的阈值。

convex_hull_mesh_decomposition

优缺点:

  • 优势:比球体更精确,减少保守性误差。
  • 缺点:凹形物体的高效凸分解仍是几何处理中的待研究问题。

insight:问题做不出来不一定是自己的问题,也有可能是更底层的 simulation 有问题。

运动规划算法

问题定义:既然已经有了检测 $q_{\text{pose}}$ 是否与环境发生碰撞的算法,那么接下来的任务就是在 $\mathcal{C}{\text{free}}$ 中找到一条从 $Q{\text{start}}$ 到 $Q_{\text{goal}}$ 的路径(路径上所有点都在 $\mathcal{C}_{\text{free}}$ 中)。

局限性

运动规划具有局限性,因为有些情况我们是可以容忍的,但会被之排除。

比如,我们的操作是具有弹性的,如用手去抓东西,尽管手会变形,但不影响可行性,然而基于碰撞检测的方法会将解排除。

即便如此,运动规划算法仍然具有其价值,因为对于很多基础问题,基于模拟的采样效率优于去真实环境中采集数据(RL),这能提供大量可行的轨迹数据,从而为 RL 提供数据来源。

概率路图法(Probabilistic Roadmap, PRM)

步骤:

  1. 采样:在 $\mathcal{C}_{\text{free}}$ 中随机生成 $N$ 个配置点 ${Q_1, Q_2, \ldots, Q_N}$。通常会在 $\mathcal{C}-\text{space} \subset \mathbb{R}^n$ 中对各个维度进行均匀离散化,然后随机采样。

    注意,这里暗含了对 $\mathcal{C}-\text{space}$ 的均匀采样必然也是对 $\mathcal{C}_{\text{free}}$ 的均匀采样(因为概率密度函数 PDF 恒为常数)。

  2. 建图:连接邻近点形成图结构,剔除与 $\mathcal{C}_{\text{obs}}$ 碰撞的边。

  3. 查询:在图搜索(如 A* 算法)中寻找 $Q_{\text{start}}$ 到 $Q_{\text{goal}}$ 的路径。

特点:预计算路图可复用,适合多查询场景。

伪代码(注意符号 $N,n$ 的定义与上文有所出入):

$$ \begin{array}{l} \textbf{function} \ \text{概率路线图}(n, k, q_{start}, q_{goal}) \ \textbf{returns} \ \text{一条从起点到目标的路径} \ \quad \text{// 输入:} n: \text{路线图中采样节点的数量}, k: \text{为每个配置检查的最近邻居数量}, q_{start}, q_{goal} \ \quad V \leftarrow {q_{start}, q_{goal}} \ \quad E \leftarrow \varnothing \ \quad \textbf{while} \ |V| < n \ \textbf{do} \ \quad \quad \textbf{repeat} \ \quad \quad \quad q \leftarrow \text{在}\ C \ \text{中的一个随机配置} \ \quad \quad \textbf{until} \ q \ \text{在} \ C_{free} \ \text{中} \ \quad \quad V \leftarrow V \cup {q} \ \quad \textbf{end} \ \quad \textbf{for each} \ q \in V \ \textbf{do} \ \quad \quad N_q \leftarrow \text{根据距离函数从} \ V \ \text{中选择的} \ q \ \text{的} \ k \ \text{个最近邻居} \ \quad \quad \textbf{for each} \ q' \in N_q \ \textbf{do} \ \quad \quad \quad \textbf{if} \ (q, q') \notin E \ \text{and} \ (q, q') \in C_{free} \ \textbf{then} \ \quad \quad \quad \quad E \leftarrow E \cup {(q, q')} \ \quad \quad \quad \textbf{end} \ \quad \quad \textbf{end} \ \quad \textbf{end} \ \quad \textbf{return} \ \text{使用 Dijkstra 算法寻找从} \ q_{start} \ \text{到} \ q_{goal} \ \text{的路径} \ \end{array} $$

如何判断一条线是否全在 $\mathcal{C}{\text{free}}$ 中,即 $(q, q') \in C{free}$?

答:在其上线性采样一些点(可以采用二分法加快尝试效率),然后判断这些点是否在 $\mathcal{C}{\text{free}}$ 中。如果都是,则认为这条线全在 $\mathcal{C}{\text{free}}$ 中。如果有任何一个点不在 $\mathcal{C}{\text{free}}$ 中,则认为这条线不在 $\mathcal{C}{\text{free}}$ 中。

高斯采样

考虑如下情形:

rpm_not_applicable

在这种情况下,如果仍然使用均匀采样,那么狭窄路径由于所占面积比例较小,其中点被采样到的概率也会非常小,导致难以求解。

所以我们需要使用 高斯采样

  1. 首先均匀生成样本点:在配置空间中均匀随机生成一个样本点 $q_1$
  2. 高斯分布生成第二个点:以 $q_1$ 为均值,$\sigma^2$ 为方差,从高斯分布 $\mathcal{N}(q_1, \sigma^2)$ 中生成第二个样本点 $q_2$
  3. 筛选添加条件:如果 $q_1 \in C_{\text{free}}$ 且 $q_2 \notin C_{\text{free}}$,则添加 $q_1$ 到图中

高斯采样中节点 $q_2$ 由节点 $q_1$ 的高斯分布 $\mathcal{N}(q_1, \sigma^2)$ 生成,避免了在 C 空间中的多次插值和碰撞检测,提高了采样效率

  • 太大的 $\sigma$ 难以对狭窄通道采样
  • 太小的 $\sigma$ 采样效率不高,且得到的采样点距离障碍物太近,容易和障碍物发生碰撞。

uniform_vs_gaussian_sampling

可以看到,这么采样之后,我们得到的点大多会分布在自由空间的边界附近,也即 边界偏好。通过这种方法,我们可获取地图中的连通信息,有更大的概率找到关键通路。

但是这种方式的弊端在于其 采样效率也有可能会降低,我们可能需要采样更多的次数才能找到足够多的、满足条件的点。而且仍然存在冗余,如凹陷、障碍物转角区域的路标点。

桥采样

桥采样是高斯采样的一种变体:

  1. 均匀生成 $q_1$
  2. 从高斯分布 $\mathcal{N}(q_1, \sigma^2)$ 生成 $q_2$
  3. 计算中点 $q_3 = (q_1 + q_2) / 2$
  4. 当 $q_1, q_2 \in C_{\text{obs}}$ 而 $q_3 \in C_{\text{free}}$ 时,添加中点 $q_3$

bridge_sampling

这种采样方式更适合在狭窄通道处构建 “桥梁”,但是问题是非窄桥的地方采样会更少了。

总结

上述采样方法各有优劣,所以一般情况下,我们会结合这几种采样方法,从而尝试尽可能的提高获得可行解的概率。

PRM 更适合场景是静态的情况,因为它对空间的覆盖很好,而这种情况下,任意重新给定起点和终点(如果不在图中,我们找到其最近的点然后尝试建边),我们就可以很快得到新的路径。

但如果场景是动态的,那么我们需要重新构建路图,效率就会降低。

快速扩展随机树(Rapidly-exploring Random Tree, RRT)

步骤:

  1. 生长树:从 $Q_{\text{start}}$ 出发,向随机采样点扩展树分支。
  2. 目标偏置:以 $1 - \beta$ 的概率向 $Q_{\text{goal}}$ 方向尝试扩展树,以 $\beta$ 的概率向随机采样点扩展树。
  3. 终止条件:树分支到达 $Q_{\text{goal}}$ 邻域。

这里利用了一些 RL 中的思想,即 平衡探索与利用(exploration vs exploitation)。我们固然希望更快的找到目标,但是如果我们只向目标扩展,那么我们可能会错过一些更好的路径,甚至根本找不到路径。这就要求我们在其中寻得一个平衡。

这也是为什么我们在算法中引入了一个参数 $\beta$,它控制了我们向目标扩展的概率。

rrt_pathfinding_algorithm_diagram

伪代码:

$$ \begin{array}{l} \textbf{function} \ \text{RRT 扩展算法}(n, \epsilon, \beta, q_{start}, q_{goal}) \ \textbf{returns} \ \text{一条从起点到目标的路径} \ \quad \text{// 输入:} n: \text{树中采样节点的数量}, \epsilon: \text{步长}, \beta: \text{采样目标点的概率}, q_{start}, q_{goal} \ \quad V \leftarrow {q_{start}} \ \quad E \leftarrow \varnothing \ \quad \textbf{for} \ i = 1 \rightarrow n \ \textbf{do} \ \quad \quad \textbf{if} \ rand(0, 1) < \beta \ \textbf{then} \ \quad \quad \quad q_{target} \leftarrow q_{goal} \ \quad \quad \textbf{else} \ \quad \quad \quad q_{target} \leftarrow \text{从} \ C_{free} \ \text{中均匀随机采样} \ \quad \quad \textbf{end} \ \quad \quad q_{near} \leftarrow \text{V 中离} \ q_{target} \ \text{最近的邻居} \ \quad \quad q_{new} \leftarrow q_{near} + \frac{\epsilon}{|q_{near}-q_{target}|}(q_{target} - q_{near}) \ \quad \quad \textbf{if} \ q_{new} \notin V \ \text{and} \ q_{new} \in C_{free} \ \text{and} \ (q_{near}, q_{new}) \in C_{free} \ \textbf{then} \ \quad \quad \quad V \leftarrow V \cup {q_{new}} \ \quad \quad \quad E \leftarrow E \cup {(q_{near}, q_{new})} \ \quad \quad \textbf{end} \ \quad \textbf{end} \ \quad \textbf{return} \ \text{使用 Dijkstra 算法寻找从} \ q_{start} \ \text{到} \ q_{goal} \ \text{的路径} \ \end{array} $$

RRT 方法需要根据问题和经验进行参数调节,这包括探索参数 $\beta$、步长 $\epsilon$ 和采样点数量 $n$。

  • 较大的 $\epsilon$:
    • 优点:加快树的扩展速度
    • 缺点:可能会跳过狭窄通道,导致路径不可行,导致难以在复杂环境中生成有效的新样本
  • 较小的 $\epsilon$:
    • 优点:更精确地探索空间
    • 缺点:扩展速度慢,生成大量的节点增加计算负担,增加迭代次数

RRT-Connect

RRT-Connect 是对基本 RRT 算法的一种改进,具有以下特点:

  1. 双向树生长:同时从起点 $q_{start}$ 和目标点 $q_{goal}$ 分别生长两棵树,而不是只从起点生长一棵树,这样可以加快搜索效率。
  2. 定向生长策略:让两棵树相向生长,每棵树扩展的目标会选择另一棵树最近更新的叶子节点而不是根节点,这大大提高了两棵树相连接的效率
  3. 贪婪扩展:使用多种 $\epsilon$ 步长进行更贪婪的树扩展,而不是单步扩展,加速树的生长速度

这种双向搜索策略显著提高了路径规划的效率,尤其是在复杂环境中。

捷径算法(Shortcutting)

RRT 和 RRT-Connect 不是渐近最优的(即使采样无限多,也不能保证找到最优路径)

  • PRM(概率路线图)算法具有渐近最优性,但需要海量采样才能实现
  • PRM 和 RRT 常产生不自然的 "抖动" 路径(下图图 1),缺乏平滑性

shortcutting

捷径算法:通过直接连接路径上不相邻的点(如果连线在自由空间中),尝试消除不必要的弯路,是一种已经得到了可行路径的后处理方法。

多次重启

单次 RRT 之后多次 Shortcutting,效果不一定会变好,因为这可能仅仅是平滑了一下路径,但是没有根本性地优化掉冗余的主干路径。

所以,我们可以尝试多次 RRT,并对多条可行路径并行地进行优化(即 Shortcutting),然后再从中选择最优的路径,从而规避局部最优解。

比如下面这张图,实际上上面存在更优解,但是单次 RRT/RRT-Connect 找到的是下面的次优解。这种情况下单纯使用 Shortcutting 是无效的。

shortcutting_not_applicable

控制系统

控制系统的核心目标

在机器人系统中,控制论的核心任务是 将已知的理想行为完美执行。而控制系统本质是对一些你不知道、无法避免的 error 进行一种反馈。因为现实不存在说到做到,总是会有误差的存在。

开环与闭环控制

开环控制(Feedforward, FF):直接执行预设动作,认为 FK(前向运动学)是没有误差的,所以它依赖精确建模但缺乏误差修正能力。

简而言之:就像闭着眼睛做事一样

  • 不使用状态估计器,即不会估计系统当前的真实状态
  • 没有反馈机制,因此容易受到噪声和外部干扰影响
  • 依靠 预先设定 的启发式方法来尝试达到目标状态

ff

闭环控制(Feedback, FB):引入实时反馈,构建反馈回路。

  • 能够有效地达到并维持期望状态
  • 可以主动抵抗外部干扰的影响,稳定本来不稳定的系统

fb

控制系统的性能评价

我们总是希望能够尽快达到理想状态并保持在该状态。

  • 最小化稳态(Steady-State)误差
  • 最小化调节时间,快速达到稳态
  • 最小化稳态附近的振荡

性能评价指标

首先,定义误差函数(Error Function):

  • 期望状态:$\theta_d$(destination)
  • 当前状态:$\theta$
  • 误差:$\theta_e = \theta_d - \theta$

然后,就可以定义性能评价指标:

  1. 稳态误差(Steady-State Error):表示系统到达稳态后的残余误差

    $$ e_{ss} = \lim_{t\to\infty} \theta_e(t) $$

    理想系统应满足 $e_{ss}=0$

  2. 调节时间(Settling Time):误差首次进入并保持在 $\pm 2%$ 误差带所需时间

  3. 超调量(Overshoot):系统响应超过稳态值的程度,最开始过去不算 $$ \text{overshoot} = |a/b| \times 100% $$ 其中,$a$ 表示最大偏移量,$b$ 表示最终稳态值

performance_evaluation_metrics

P 控制(Proportional Control)

在控制系统中,P 控制是将错误信号转换为命令的基本方法,控制信号与误差大小成正比。

  • $\theta(t)$:$t$ 时刻系统实际状态
  • $\theta_d(t)$:期望状态(目标状态)
  • $\theta_e(t)$:误差状态,$\theta_e(t) = \theta_d(t) - \theta(t)$
  • $K_p$:比例系数

比例控制的基本表达式

$$ P = K_p\theta_e(t) $$

一阶形式

当控制信号改变状态的导数(即控制速度信号)时:

$$ \dot{\theta}(t) = P = K_p\theta_e(t) $$

根据误差定义和状态导数关系:

$$ \theta_e(t) = \theta_d(t) - \theta(t) \ \dot{\theta}_e(t) = \dot{\theta}_d(t) - \dot{\theta}(t) $$

将控制方程代入:

$$ \dot{\theta}_e(t) = \dot{\theta}_d(t) - K_p\theta_e(t) $$

如果期望状态以恒定速度移动:

$$ \dot{\theta}_d(t) = c $$

则误差动态方程为:

$$ \dot{\theta}_e(t) + K_p\theta_e(t) = c $$

首先求解特征方程:

$$ \dot{\theta}_e(t) + K_p\theta_e(t) = 0 $$

求解过程(以防有同学已经忘光了 ODE):

$$ \begin{aligned} \dot{\theta}_e(t) &= -K_p\theta_e(t) \ \frac{\mathrm{d}\theta_e(t)}{\mathrm{d}t} &= -K_p\theta_e(t) \ \frac{\mathrm{d}\theta_e(t)}{\theta_e(t)} &= -K_p \mathrm{d}t \ \int \frac{\mathrm{d}\theta_e(t)}{\theta_e(t)} &= -K_p \int \mathrm{d}t \ \ln|\theta_e(t)| &= -K_p t + C_1 \ |\theta_e(t)| &= e^{-K_p t + C_1} = e^{C_1} \cdot e^{-K_p t} \ C &= e^{C_1} \ |\theta_e(t)| &= C \cdot e^{-K_p t} \ \end{aligned} $$

得到齐次方程的通解:

$$ \theta_e(t) = Ce^{-K_pt} $$

其中 $C$ 为常数。

然后观察原始方程,容易发现特解:

$$ \theta_{A} = \frac{c}{K_p} $$

所以通解为:

$$ \theta_e(t) = \frac{c}{K_p} + Ce^{-K_pt} $$

应用初始条件 $\theta_e(0)$ 确定常数 $C$:

$$ \theta_e(0) = C + \frac{c}{K_p} \Rightarrow C = \theta_e(0) - \frac{c}{K_p} $$

最终,我们得到:

$$ \theta_e(t) = \frac{c}{K_p} + \left(\theta_e(0) - \frac{c}{K_p}\right)e^{-K_pt} $$

结论分析

  1. 当 $c=0$(目标静止)时:

    $$ \theta_e(t) = \theta_e(0)e^{-K_pt} $$

    误差呈指数衰减至零,系统最终收敛到目标状态。

  2. 当 $c\neq0$(目标移动)时:

    • 随着 $t\rightarrow\infty$,$e^{-K_pt}\rightarrow0$
    • 稳态误差:$\lim_{t\rightarrow\infty}\theta_e(t) = \frac{c}{K_p}$
    • 系统存在永久稳态误差,误差大小与目标速度 $c$ 成正比,与比例增益 $K_p$ 成反比,所以增大 $K_p$ 可以减小稳态误差

二阶形式

如果控制信号改变状态的二阶导数(力或力矩信号):

$$ \ddot{\theta}(t) = P = K_p\theta_e(t) $$

则会导致状态振荡且不稳定。

PI 控制(Proportional-Integral Control)

PI 控制结合了比例控制和积分控制:

$$ PI = K_p \theta_e(t) + K_i \int_0^t \theta_e(\tau) \mathrm{d}\tau $$

其中:

  • $K_p$:比例系数
  • $K_i$:积分系数
  • $\theta_e(t)$:误差

如果控制信号作用于状态导数(如速度信号):

$$ \dot{\theta}(t) = PI = K_p \theta_e(t) + K_i \int_0^t \theta_e(\tau) \mathrm{d}\tau $$

定义误差导数 $\dot{\theta}_e(t) = \dot{\theta}_d(t) - \dot{\theta}(t)$,也即 $\dot{\theta}_d(t) = \dot{\theta}_e(t) + \dot{\theta}(t)$,两边求导得到:

$$ \ddot{\theta}_d(t) = \ddot{\theta}_e(t) + K_p \dot{\theta}_e(t) + K_i \theta_e(t) $$

如果 $\ddot{\theta}_d(t) = 0$(目标加速度为零),动态方程化为:

$$ \ddot{\theta}_e(t) + K_p \dot{\theta}_e(t) + K_i \theta_e(t) = 0 $$

这是一个二阶常系数齐次微分方程。

PPT 上没有,回忆一下高数:

对于齐次线性常系数二阶微分方程:

$$ y'' + py' + qy = 0, $$

其特征方程为:

$$ \lambda^2 + p\lambda + q = 0, $$

特征根 $\lambda_1, \lambda_2$ 的不同情况对应微分方程的通解如下:

  1. 两相异实根 $\lambda_1, \lambda_2$:

    $$ y = C_1 e^{\lambda_1 x} + C_2 e^{\lambda_2 x}. $$

  2. 二重根 $\lambda_1$:

    $$ y = (C_1 + C_2 x)e^{\lambda_1 x}. $$

  3. 共轭复根 $\lambda_{1,2} = a \pm i\beta$: $$ y = e^{ax}(C_1 \cos \beta x + C_2 \sin \beta x). $$

解的形式由方程特征根决定,特征方程为:

$$ r^2 + K_p r + K_i = 0 $$

其解的形式决定系统的阻尼特性:

  1. 过阻尼 (Overdamped,下图 I):两个实根,系统缓慢收敛。
  2. 临界阻尼 (Critically damped,下图 II):双重实根,快速无振荡收敛。
  3. 欠阻尼 (Underdamped,下图 III):共轭复根,系统振荡收敛。

overdamped_critical_underdamped

P 控制与 PI 控制比较

  1. P 控制

    • 仅能消除静态误差(目标静止时)。
    • 对于目标移动(如恒定速度),存在稳态误差,在下图中,可以看到 P 控制没有最后和 $\theta$ 存在一个恒定差距,$\theta_e \to c \neq 0$
  2. PI 控制

    • 通过积分项消除稳态误差,在下图中,可以看到 PI 控制没有最后可以和 $\theta$ 重合,$\theta_e \to 0$
    • 对恒定速度目标控制效果更好(可以消除稳态误差),但对复杂轨迹不能完全消除。

pi_vs_p_control

PI 控制通过引入积分项,解决了 P 控制中的稳态误差问题,但会引入更多复杂性(如可能的振荡)。调整 $K_p$ 和 $K_i$ 的值可改变系统性能,如响应速度和稳定性。

PD 控制(Proportional-Derivative Control)

PD 控制结合了比例控制和微分控制:

$$ PD = K_p \theta_e(t) + K_d \frac{\mathrm{d}}{\mathrm{d}t}\theta_e(t) $$

其中:

  • $K_p$:比例系数
  • $K_d$:微分系数
  • $\theta_e(t)$:误差
  • $\frac{\mathrm{d}}{\mathrm{d}t}\theta_e(t)$:误差变化率

根据误差定义 $\theta_e(t) = \theta_d(t) - \theta(t)$,可得:

$$ \ddot{\theta}_e(t) = \ddot{\theta}_d(t) - \ddot{\theta}(t) $$

将误差加速度表达式代入控制方程:

$$ \ddot{\theta}_e(t) = K_p \theta_e(t) + K_d \dot{\theta}_e(t) $$

重新整理得到:

$$ \ddot{\theta}_e(t) + K_d \dot{\theta}_e(t) + K_p \theta_e(t) = \ddot{\theta}_d(t) $$

如果 $\ddot{\theta}_d(t) = 0$(目标加速度为零),动态方程简化为:

$$ \ddot{\theta}_e(t) + K_d \dot{\theta}_e(t) + K_p \theta_e(t) = 0 $$

后续类似 PI 控制,但 $K_p$ 位置有所改变。

解的形式由方程特征根决定,特征方程为:

$$ r^2 + K_d r + K_p = 0 $$

根据特征根的性质,系统表现出不同的动态行为:

  1. 过阻尼:两个实根,系统无振荡地缓慢收敛。
  2. 临界阻尼:二重实根,系统以最快速度无振荡收敛。
  3. 欠阻尼:一对共轭复根,系统呈振荡收敛状态。

PID 控制(Proportional-Integral-Derivative Control)

PID 控制结合了 P、I、D 三种控制方式:

$$ PID = K_p \theta_e(t) + K_i \int_0^t \theta_e(\tau)\mathrm{d}\tau + K_d \frac{\mathrm{d}}{\mathrm{d}t}\theta_e(t) $$

比例项(Proportional)

$K_p$ 控制当前状态

$$ u_P(t) = K_p \theta_e(t) $$

  • $K_p$ 增大可 加快响应速度,因为我们会更希望快速减少 $\theta_e(t)$
  • 单独使用会产生稳态误差(P 控制),如机械臂关节受摩擦力时无法完全归零

积分项(Integral)

$K_i$ 控制历史累积

$$ u_I(t) = K_i \int_0^t \theta_e(\tau)\mathrm{d}\tau $$

  • 对持续误差进行累积补偿,消除稳态误差

微分项(Derivative)

$K_d$ 预测未来趋势

$$ u_D(t) = K_d \frac{\mathrm{d}}{\mathrm{d}t}\theta_e(t) $$

  • 与误差的变化率成正比,抑制超调和振荡
  • 当误差增加时提供更强的控制作用
  • 当误差减小时提供更温和的控制作用

总结

调高各个系数的影响:

| 参数(Parameter) | 上升时间(Rise time) | 超调量(Overshoot) | 调节时间(Settling time) | 稳态误差(Steady-state error) | 稳定性(Stability) | | ----------------- | --------------------- | ------------------- | ------------------------- | ------------------------------ | ------------------- | | $K_p$ | 减小 | 增大 | 小变化 | 减小 | 变差 | | $K_i$ | 减小 | 增大 | 增加 | 消除 | 变差 | | $K_d$ | 小变化 | 减小 | 减小 | 理论上无影响 | 如果 $K_d$ 小则改善 |

仿真实现

$$ \text{force} = \text{stiffness} * (\text{targetPosition} - \text{Position}) + \text{damping} *((\text{targetVelocity} - \text{Velocity})) $$

  • Stiffness(刚度) 类似于 $k_p$ (比例增益),用于调整位置误差的影响。
  • Damping(阻尼) 类似于 $k_d$ (微分增益),用于调整速度误差的影响。

💾

  •  

重回公众号,弄了两个微信公众号

做了一件不大不小的事情,重回微信公众号。搞了两个微信公众号,「博客研究社」和「FENG.PUB」。一下子搞两个公众号,着实有点压力,能不能运营号这两个号还是未知。

FENG.PUB

微信公众号「FENG.PUB」,正是我的博客域名,本想与注册同名公众号,想了想,我可能会将公众号引流至博客,干脆直接使用域名作为公众号的名称。

在内容方面,该公众号与博客内容会有差异,毕竟公众号阅读要求速度快,也许谋篇文章在公众号上会简单叙述,尽可能的简洁让公众号读者能快速看完。也许会删减博客文章的一些内容在发布到公众号上。

另外可能会有些不适宜在公众号上发的文章只会在博客里面发,小而精的「FENG.PUB」也许不那么完美,但该有的都会有。

3月10日,「FENG.PUB」微信公众号诞生日,还是开通了个人微信公众号

终于又一次开通了微信公众号「FENG.PUB」,但这次不太一样,也许你不知道什么是「FENG.PUB」,其实它是一个个人博客域名。域名虽然简洁,但域名后缀不像.COM或.CN那样被大众所知。

再说说为什么开通「FENG.PUB」微信公众号。起初我只在个人独立博客发布一些文章,这些文章很杂很乱,也不能说烂,主要是不同时期涉及的内容不同,所以文章没有特定的主题。

一直以来都认为公众号需要定位的,没有定位的公众号不叫公众号。但这次一改常态,做一个没有定位的个人公众号,像做个人博客那样。因此你会在这个公众号看到不同定位的内容。

顺便说一句,虽然AI能够帮我写内容,但该公众号将摒弃AI生成的一切。虽然公众号「FENG.PUB」不使用AI,但不代表我讨厌AI,而是希望更完整的展示个人特色。

3月10日,「FENG.PUB」微信公众号诞生日。不知道来年的我是否记能记得这是什么日子。

博客研究社

说实话,「博客研究社」这个名头对我来说有点大,独立博客包罗万象,能将博客研究到极致甚是不易。

这个名字很直接,看名称就知道是博客相关,但我的能力有限,只能做一些小白做不到的事情,所以微信公众号「博客研究社」目前只能做一些边边角角的事情。

很期望大家来「博客研究社」转转,提提建议。当然我不敢说什么建议都能执行,只要能触及到的事情回去尝试,成不成做了再说。

  •  

家庭数据中心系列 Cloudflare APO 缓存失效解析:Cf-Cache-Status BYPASS 的原因与解决方案

家庭数据中心系列 Cloudflare APO 缓存失效解析:Cf-Cache-Status BYPASS 的原因与解决方案 无敌的个人博客 tangwudi

1 前言 这周原本计划写的文章不是这篇,但在由于在研究 “APO对 WordPress 内容缓存的影响”时,无意间发现了一个异常的情况:访问博客请求的响应标头中出现了 “Cf-Cache-Status: BYPASS“: 最关键的是,不管怎么刷新都是”BYPASS”,这意味着APO对我博客的访问请求处于失效的状态(不知道问题的实际影响范围,只知道至少对我的访问请求是如此)。这让我感到非常不爽,毕竟现在已经是高贵的Cloudflare Pro用户,而APO功能可是单独拿出来卖5美金一个月的昂贵服务,怎么能不生效呢?这事必须得重视! 为了弄清楚背后的原因,我不得不中途调整方向,深入研究这一现象,并尝试找出问题的触发因素。 2 启用APO后WordPress站点响应头常规内容解析 在正常情况下,使用 APO 时,响应头应类似 […]

<p>The post 家庭数据中心系列 Cloudflare APO 缓存失效解析:Cf-Cache-Status BYPASS 的原因与解决方案 first appeared on 无敌的个人博客.</p>

  •  

机器人学 II

四元数

[!TIP]

强烈推荐参考 Krasjet / Quaternion 以获得直观且详细的性质证明推导。

小小的吐槽:王老师上节课明明刚说四元数不重要不要求掌握,结果这节课花了绝大部分时间来推导 hhh。

定义

四元数是复数的推广,表示为:

$$ q = w + xi + yj + zk $$

其中:

  • $w$ 是实数部分;
  • $x, y, z$ 是虚数部分;

$i, j, k$ 是虚数单位,满足以下关系:

$$ i^2 = j^2 = k^2 = ijk = -1 $$

反交换性质:

$$ ij = k = -ji, \quad jk = i = -kj, \quad ki = j = -ik $$

向量形式

$$ q = (w, \bold{v}), \quad \bold{v} = (x, y, z) $$

运算性质

乘法:对于两个四元数 $q_1 = (w_1, \bold{v}_1)$ 和 $q_2 = (w_2, \bold{v}_2)$,其乘法定义为:

$$ \begin{aligned} q_1 q_2 &= (w_1 w_2 - \bold{v}_1^{\top} \bold{v}_2, , w_1 \bold{v}_2 + w_2 \bold{v}_1 + \bold{v}_1 \times \bold{v}_2) \ &= (w_1 w_2 - \bold{v}_1 \cdot \bold{v}_2, , w_1 \bold{v}_2 + w_2 \bold{v}_1 + \bold{v}_1 \times \bold{v}_2) \end{aligned} $$

这被称为 Graßmann 积。

注意:四元数的乘法 不可交换,即 $q_1 q_2 \neq q_2 q_1$。

共轭

$$ q^* = (w, -\bold{v}) $$

模长

$$ |q|^2 = w^2 + \bold{v}^{\top} \bold{v} = qq^* = q^*q $$

$$ q^{-1} = \frac{q^*}{|q|^2} $$

这是模长的直接推论。

几何意义与应用

单位四元数:若四元数的模长为 $1$,即 $|q| = 1$,则称其为单位四元数。单位四元数可表示三维空间中的旋转。其还具有性质 $q^{-1} = q^*$。

纯四元数:若四元数的实部为 $0$,即 $q = (0, \bold{v})$,则称其为纯四元数。纯四元数可以表示三维空间中的向量。

旋转表示:任何一个旋转,都可以表示为绕某个单位向量 $\hat{\omega}$ 旋转 $\theta$ 角度(证明见后)。

那么,对应的四元数可以表示为:

$$ q = \left[\cos\frac{\theta}{2}, \sin\frac{\theta}{2} \hat{\omega}\right] $$

注意,旋转到四元数存在 “双重覆盖” 关系,我们可以很容易地发现:

$$ \begin{aligned} q &= \left[\cos\frac{\theta}{2}, \sin\frac{\theta}{2} \hat{\omega}\right] \ -q &= \left[-\cos\frac{\theta}{2}, -\sin\frac{\theta}{2}\hat{\omega}\right] \ &= \left[\cos(\pi - \frac{\theta}{2}), \sin(\pi - \frac{\theta}{2}) (-\hat{\omega})\right] \end{aligned} $$

是等价的($-q$ 意味着同一旋转轴但是翻转正方向,然后旋转 $2\pi - \theta$)。

double_coverage

相应地,从四元数恢复轴角表示:

$$ \theta = 2 \arccos(w), \quad \hat{\omega} = \begin{cases} \frac{\bold{v}}{\sin(\theta/2)}, & \theta \neq 0 \ 0, & \theta = 0 \end{cases} $$

其中,$w$ 是单位四元数的实部,四元数的一种常见表示就是 $(w,x,y,z)$。

四元数与旋转

向量旋转:任意向量 $\mathbf{v}$ 沿着以 单位向量 定义的旋转轴 $\mathbf{u}$ 旋转 $\theta$ 度得到 $\mathbf{v}'$,那么:

令向量 $\mathbf{v}$ 的四元数形式 $v = [0, \mathbf{v}]$,旋转四元数 $q = \left[\cos\left(\frac{\theta}{2}\right), \sin\left(\frac{\theta}{2}\right)\mathbf{u}\right]$

则旋转后的向量 $\mathbf{v}'$ 可表示为:

$$ \mathbf{v}' = qv q^* = qv q^{-1} $$

如果是给定四元数 $q$ 旋转向量 $\mathbf{v}$ ,那么设 $q = [w, \mathbf{r}]$ 是单位四元数(即 $w^2 + |\mathbf{r}|^2 = 1$),向量 $\mathbf{v}$ 的四元数形式为 $v = [0, \mathbf{v}]$。

则:

$$ \begin{aligned} qvq^* &= [w, \mathbf{r}][0, \mathbf{v}][w, -\mathbf{r}] \ &= [ - \mathbf{r} \cdot \mathbf{v}, w\mathbf{v} + \mathbf{r} \times \mathbf{v} ][w, -\mathbf{r}] \ &= [0, (1-2|\mathbf{r}|^2)\mathbf{v} + 2(\mathbf{r} \cdot \mathbf{v})\mathbf{r} + 2w(\mathbf{r} \times \mathbf{v})] \end{aligned} $$

最后一个等式的展开计算如下

实部:

$$ \begin{aligned} &= (- \mathbf{r} \cdot \mathbf{v})w - (w\mathbf{v} + \mathbf{r} \times \mathbf{v}) \cdot (-\mathbf{r}) \ &= -w (\mathbf{r} \cdot \mathbf{v}) + w (\mathbf{v} \cdot \mathbf{r}) + (\mathbf{r} \times \mathbf{v}) \cdot \mathbf{r} \ &= 0 \quad \end{aligned} $$

虚部:

$$ \begin{aligned} &= (- \mathbf{r} \cdot \mathbf{v})(-\mathbf{r}) + w (w\mathbf{v} + \mathbf{r} \times \mathbf{v}) + (w\mathbf{v} + \mathbf{r} \times \mathbf{v}) \times (-\mathbf{r}) \ &= (\mathbf{r} \cdot \mathbf{v})\mathbf{r} + w^2 \mathbf{v} + w (\mathbf{r} \times \mathbf{v}) - w (\mathbf{v} \times \mathbf{r}) - (\mathbf{r} \times \mathbf{v}) \times \mathbf{r} \ &= (\mathbf{r} \cdot \mathbf{v})\mathbf{r} + w^2 \mathbf{v} + 2w (\mathbf{r} \times \mathbf{v}) - \big[ (\mathbf{r} \cdot \mathbf{r})\mathbf{v} - (\mathbf{v} \cdot \mathbf{r})\mathbf{r} \big] \ &= (1 - 2|\mathbf{r}|^2)\mathbf{v} + 2(\mathbf{r} \cdot \mathbf{v})\mathbf{r} + 2w (\mathbf{r} \times \mathbf{v}) \end{aligned} $$

其中利用了叉乘展开式:

$$ a \times b \times c = (a \cdot c)b - (a \cdot b)c $$

以及单位四元数约束条件 $w^2 + |\mathbf{r}|^2 = 1$,将 $w^2 = 1 - |\mathbf{r}|^2$ 代入后合并同类项。

接下来证明这个结果与罗德里格旋转公式等价即可。

$$ qvq^* = [0, (1-2|\mathbf{r}|^2)\mathbf{v} + 2(\mathbf{r} \cdot \mathbf{v})\mathbf{r} + 2w(\mathbf{r} \times \mathbf{v})] $$

我们有:

  • $w = \cos(\frac{\theta}{2})$
  • $\mathbf{r} = \sin(\frac{\theta}{2})\mathbf{u}$,且 $\mathbf{u}$ 是单位向量,$|\mathbf{u}| = 1$。

所以:

$$ \begin{aligned} 1 - 2|\mathbf{r}|^2 &= 1 - 2\sin^2\left(\frac{\theta}{2}\right) = \cos(\theta) \ \ 2(\mathbf{r} \cdot \mathbf{v})\mathbf{r} &= 2 \left(\sin\left(\frac{\theta}{2}\right)(\mathbf{u} \cdot \mathbf{v})\right) \left(\sin\left(\frac{\theta}{2}\right)\mathbf{u}\right) \ &= 2 \sin^2\left(\frac{\theta}{2}\right) (\mathbf{u} \cdot \mathbf{v}) \mathbf{u} \ &= (1 - \cos(\theta)) (\mathbf{u} \cdot \mathbf{v}) \mathbf{u} \ \ 2w(\mathbf{r} \times \mathbf{v}) &= 2 \cos\left(\frac{\theta}{2}\right) \left(\sin\left(\frac{\theta}{2}\right)(\mathbf{u} \times \mathbf{v})\right) \ &= \left(2 \sin\left(\frac{\theta}{2}\right) \cos\left(\frac{\theta}{2}\right)\right) (\mathbf{u} \times \mathbf{v}) \ &= \sin(\theta) (\mathbf{u} \times \mathbf{v}) \end{aligned} $$

将以上结果代回到 $\mathbf{v}'$ 的表达式中:

$$ \begin{aligned} \mathbf{v}' &= (1-2|\mathbf{r}|^2)\mathbf{v} + 2(\mathbf{r} \cdot \mathbf{v})\mathbf{r} + 2w(\mathbf{r} \times \mathbf{v}) \ &= (\cos(\theta))\mathbf{v} + (1 - \cos(\theta)) (\mathbf{u} \cdot \mathbf{v}) \mathbf{u} + (\sin(\theta)) (\mathbf{u} \times \mathbf{v}) \end{aligned} $$

正是罗德里格旋转公式的结果。

旋转组合:两个旋转 $q_1$ 和 $q_2$ 的组合等价于四元数的乘法:

$$ q_2 (q_1 x q_1^) q_2^ = (q_2 q_1) x (q_1^* q_2^*) $$

虽然四元数不满足交换律,但其满足结合律(可以证明四元数存在对应的四维矩阵,所以矩阵的性质也是四元数的性质)。

注意:

  • 四元数的旋转表示具有 $3$ 个自由度(四个参数加一个单位模长约束)。
  • 几何上,单位四元数可以看作 $4$ 维球面 $S^3$ 的壳。

四元数与旋转矩阵

从四元数到旋转矩阵

因为我们有 $\mathbf{v}' = q \mathbf{v} q^{-1}$ (这里假设 $\mathbf{v}$ 是向量, $q$ 是单位四元数, $\mathbf{v}'$ 是旋转后的向量,并且我们将向量 $\mathbf{v}$ 视为纯四元数 $[0, \mathbf{v}]$ 进行运算),我们可以计算出对应的旋转矩阵为:

令单位四元数 $q = w + x\mathbf{i} + y\mathbf{j} + z\mathbf{k} = [w, (x, y, z)]$,则旋转矩阵 $R(q)$ 为:

$$ R(q) = \begin{bmatrix} 1 - 2y^2 - 2z^2 & 2xy - 2zw & 2xz + 2yw \ 2xy + 2zw & 1 - 2x^2 - 2z^2 & 2yz - 2xw \ 2xz - 2yw & 2yz + 2xw & 1 - 2x^2 - 2y^2 \end{bmatrix} $$

证明:使用三个基向量挨个求就行。

令 $\mathbf{r} = (x, y, z)$。

令 $\mathbf{v} = \mathbf{e}_1 = (1, 0, 0)$。

  • $|\mathbf{r}|^2 = x^2 + y^2 + z^2$
  • $\mathbf{r} \cdot \mathbf{e}_1 = x$
  • $\mathbf{r} \times \mathbf{e}_1 = (x, y, z) \times (1, 0, 0) = (0, z, -y)$

$$ \begin{aligned} \mathbf{v}'_1 &= (1-2(x^2+y^2+z^2))\mathbf{e}_1 + 2x\mathbf{r} + 2w(\mathbf{r} \times \mathbf{e}_1) \ &= (1-2x^2-2y^2-2z^2)(1, 0, 0) + 2x(x, y, z) + 2w(0, z, -y) \ &= (1-2x^2-2y^2-2z^2 + 2x^2, 2xy + 2wz, 2xz - 2wy) \ &= (1 - 2y^2 - 2z^2, 2xy + 2zw, 2xz - 2yw) \end{aligned} $$

这就是矩阵 $R$ 的第一列。

令 $\mathbf{v} = \mathbf{e}_2 = (0, 1, 0)$。

  • $\mathbf{r} \cdot \mathbf{e}_2 = y$
  • $\mathbf{r} \times \mathbf{e}_2 = (x, y, z) \times (0, 1, 0) = (-z, 0, x)$

$$ \begin{aligned} \mathbf{v}'_2 &= (1-2(x^2+y^2+z^2))\mathbf{e}_2 + 2y\mathbf{r} + 2w(\mathbf{r} \times \mathbf{e}_2) \ &= (1-2x^2-2y^2-2z^2)(0, 1, 0) + 2y(x, y, z) + 2w(-z, 0, x) \ &= (2xy - 2wz, 1-2x^2-2y^2-2z^2 + 2y^2, 2yz + 2wx) \ &= (2xy - 2zw, 1 - 2x^2 - 2z^2, 2yz + 2xw) \end{aligned} $$

这就是矩阵 $R$ 的第二列。

令 $\mathbf{v} = \mathbf{e}_3 = (0, 0, 1)$。

  • $\mathbf{r} \cdot \mathbf{e}_3 = z$
  • $\mathbf{r} \times \mathbf{e}_3 = (x, y, z) \times (0, 0, 1) = (y, -x, 0)$

$$ \begin{aligned} \mathbf{v}'_3 &= (1-2(x^2+y^2+z^2))\mathbf{e}_3 + 2z\mathbf{r} + 2w(\mathbf{r} \times \mathbf{e}_3) \ &= (1-2x^2-2y^2-2z^2)(0, 0, 1) + 2z(x, y, z) + 2w(y, -x, 0) \ &= (2xz + 2wy, 2yz - 2wx, 1-2x^2-2y^2-2z^2 + 2z^2) \ &= (2xz + 2yw, 2yz - 2xw, 1 - 2x^2 - 2y^2) \end{aligned} $$

这就是矩阵 $R$ 的第三列。

将 $\mathbf{v}'_1, \mathbf{v}'_2, \mathbf{v}'_3$ 作为列向量组合起来,就得到了图片中给出的旋转矩阵 $R(q)$:

$$ R(q) = \begin{bmatrix} 1 - 2y^2 - 2z^2 & 2xy - 2zw & 2xz + 2yw \ 2xy + 2zw & 1 - 2x^2 - 2z^2 & 2yz - 2xw \ 2xz - 2yw & 2yz + 2xw & 1 - 2x^2 - 2y^2 \end{bmatrix} $$

证毕。

从旋转矩阵到四元数

根据上一步结果,旋转矩阵 $R$ 的迹(trace)满足:

$$ \text{tr}(R) = 3 - 4(x^2 + y^2 + z^2) = 4w^2 - 1 $$

我们可以计算四元数的分量为:

$$ \begin{aligned} w &= \frac{\sqrt{\text{tr}(R)+1}}{2} \ x &= \frac{R_{32}-R_{23}}{4w} \ y &= \frac{R_{13}-R_{31}}{4w} \ z &= \frac{R_{21}-R_{12}}{4w} \end{aligned} $$

其中 $R_{ij}$ 表示矩阵 $R$ 的第 $i$ 行第 $j$ 列的元素。这些公式在 $w \neq 0$ 时有效。

四元数的距离

这部分证明亦可参见 Krasjet / Quaternion 第 4 节・四元数插值(第 37 页)。

在单位三维球面 $S^3$ 上,或两个四元数 $(q_1, q_2)$ 之间的角度:

$$ \langle p, q \rangle = \arccos(p \cdot q) $$

证明:设 $p = (p_w, \mathbf{p}_v)$ 和 $q = (q_w, \mathbf{q}_v)$,那么显然,从 $p$ 旋转到 $q$ 的相对旋转可以由四元数乘法 $\Delta q = q p^*$ 表示。

$$ \begin{aligned} \Delta q &= q p^* \ &= (q_w, \mathbf{q}_v)(p_w, -\mathbf{p}_v) \ &= (q_w p_w - \mathbf{q}_v \cdot (-\mathbf{p}_v), q_w(-\mathbf{p}_v) + p_w \mathbf{q}_v + \mathbf{q}_v \times (-\mathbf{p}_v)) \ &= (q_w p_w + \mathbf{q}_v \cdot \mathbf{p}_v, \dots) \end{aligned} $$

所以,$\Delta q$ 的实部 $\text{Re}(\Delta q) = q_w p_w + \mathbf{q}_v \cdot \mathbf{p}_v$。

这正好是 $p$ 和 $q$ 作为 4D 向量的点积 $p \cdot q$。

$$ \text{Re}(\Delta q) = p \cdot q = \cos \langle p, q \rangle\ \langle p, q \rangle = \arccos(p \cdot q) $$

对应旋转之间的距离:

$$ \text{dist}(p, q) = 2 \arccos(|p \cdot q|) $$

或等价地:

$$ \text{dist}(p, q) = 2 \min {\langle p, q \rangle, \langle p, -q \rangle} $$

这里需要在两个值之间取最小值的原因也可以参见 Krasjet / Quaternion 第 5.4 节・双倍覆盖带来的问题(第 46 页)。

回顾之前四元数与旋转的关系,不难得知两个旋转 $(R_1, R_2)$ 的距离与其对应四元数 $q(R_1)$ 和 $q(R_2)$ 在球面上的距离成线性关系(前者是后者的两倍)。

unit_circle_and_rotation_diagram

四元数插值

这部分证明可以参见 Krasjet / Quaternion 第 5 节・四元数插值(第 41 页)。

线性插值(Linear Interpolation, Lerp)

$$ q(t) = (1-t)q_1 + tq_2 $$

lerp

归一化线性插值(Normalized Linear Interpolation, Nlerp)

$$ q(t) = \frac{(1-t)q_1 + tq_2}{|(1-t)q_1 + tq_2|} $$

省流:就是除个模长,让他恢复为单位四元数。

nlerp

球面线性插值(Spherical Linear Interpolation, Slerp)

以上两种插值都有问题,他们实际上是线性切分了弦长,而不是弧长,这会导致在转动的时候的角速度不均匀:

nlerp

所以,我们要引入新的插值方式,这就是球面线性插值(Spherical Linear Interpolation, Slerp):

$$ q(t) = \frac{\sin((1-t)\theta)}{\sin(\theta)} q_1 + \frac{\sin(t\theta)}{\sin(\theta)} q_2 $$

其中 $\theta$ 是 $q_1$ 和 $q_2$ 之间的夹角,$\theta = \arccos(q_1 \cdot q_2)$。

slerp

证明的一个方法在 Krasjet / Quaternion 第 5.3 节・球面线性插值(第 43 页)。

不过老师的 Slide 上有另一种更简单直观的利用三角函数性质的证明方法:

vector_geometry_angle_diagram

$$ \begin{aligned} \alpha+\beta&=\psi\ \mathbf{v}(t)&=w_0\mathbf{v}0+w_1\mathbf{v}1\ \frac{\sin\alpha}{w_1}&=\frac{\sin\beta}{w_0}=\frac{\sin(\pi-\psi)}1=\sin\psi\ w{0}&=\frac{\sin\beta}{\sin\psi}\ w{1}&=\frac{\sin\alpha}{\sin\psi}\ \psi&=\cos^{-1}(\mathbf{v}_0\cdot\mathbf{v}_1) \end{aligned} $$

第三个式子利用了三角形的性质:

$$ \frac{A}{\sin\alpha}=\frac{B}{\sin\beta}=\frac{C}{\sin\gamma} $$

球面均匀采样

考虑我们如何随机采样一个旋转。

引理:在 $\mathbb{SO}(3)$ 中均匀采样旋转矩阵等价于从单位四元数的集合 $\mathbb{S}(3)$ 中均匀采样。

原因:两个旋转之间的距离与对应的四元数在单位球面上的距离成线性关系。

那么,如何均匀采样 $\mathbb{S}(3)$ 呢?

方法:从四维标准正态分布 $\mathcal{N}(0, I_{4 \times 4})$ 中随机采样一个变量,并将其归一化,从而得到(直接解释为)单位四元数。

原因:由于标准正态分布是各向同性的(即在所有方向上均匀分布),所以采样得到的单位四元数在 $\mathbb{S}(3)$ 中也是均匀分布的。

随后,采样得到的单位四元数也就可以转换为对应的旋转矩阵(如果需要)。

有趣的事实

对于神经网络来讲,最好的旋转表示方法是 9 个数的旋转矩阵。因为其他的表示方法均可能出现对于输入的微小扰动,即一个小的旋转,出现一个跳变,而只有最初最冗余的 $\mathbb{R}^{3\times3}$ 旋转矩阵保证其必然是连续的( 即连续性 ),而这对于神经网络是很好的性质。

各旋转表示方式对比

| Representation | Inverse? | Composing? | Any local movement in SO(3) can be achieved by local movement in the domain? | | --------------- | ----------- | ----------- | ---------------------------------------------------------------------------- | | Rotation Matrix | ✔️ | ✔️ | N/A | | Euler Angle | Complicated | Complicated | No | | Angle-axis | ✔️ | Complicated | ? | | Quaternion | ✔️ | ✔️ | ✔️ |

  • 旋转矩阵:可逆、可组合(矩阵连乘)、但在 $\mathbb{SO}(3)$ 上移动不直接(9 D - 6 约束 = 3DoF)
  • 欧拉角:逆向复杂、组合复杂、因为 Gimbal lock 的存在,与 $\mathbb{SO}(3)$ 不能平滑映射
  • 轴角:可逆、组合复杂、大部分情况下可以与 $\mathbb{SO}(3)$ 平滑映射,但是在边界情况(如旋转 $0$ 度时)不行
  • 四元数:完美

运动规划

形式化表述

配置空间 (Configuration Space)

定义:配置空间(Configuration spcae,C-space)是 $ \mathbb{R}^n $ 的一个子集,包含系统的所有可能状态(即状态空间)。

  • $C$:配置空间,表示所有可能状态的集合。
  • $C_{\text{free}} \subseteq C$:自由空间,包含所有有效状态(无碰撞)。
  • $C_{\text{obs}} \subseteq C$:障碍空间,表示有障碍的无效状态。
  • $C_{\text{free}} \cup C_{\text{obs}} = C$
  • $C_{\text{free}} \cap C_{\text{obs}} = \varnothing$

问题定义

configuration_space_pathfinding

给定:

  • 自由空间 $C_{\text{free}}$。
  • 起始状态 $q_{\text{start}} \in C_{\text{free}}$。
  • 目标状态 $q_{\text{goal}} \in C_{\text{free}}$。

目标:计算一系列动作,使机器人从 $q_{\text{start}}$ 移动到 $q_{\text{goal}}$。

注意,这里的符号 $q$ 不是四元数(quaternion)的意思,其是配置空间中的一个点,即状态。

例如,对于一个机械臂,其配置空间可能是 $\mathbb{R}^n$,那么 $q$ 就是关节的角度组合之一 $(\theta_1, \theta_2, \dots, \theta_n)$。

挑战

  1. 避免障碍物:确保路径始终在 $C_{\text{free}}$ 内。
  2. 长规划时间:路径可能较长,需要优化。
  3. 高维空间:配置空间维度可能很高(例如多关节机器人)。

💾

  •  

2025年2月阅读书摘

✇Dennis
作者Domon

2月阅读记录

  • 《猫鱼》60%
  • 《阅读不息》51%
  • 《达美乐——创意比萨巨头如何用科技创新客户体验》10%

  •  

二〇二五年二月总结,AI加持下进入快节奏的生活

又到了写总结的日子,而我并没有心情去写。都说AI爆发元年,机会来了。我尝试用AI做一些事情,但总是差强人意,在AI应用方面完全摸不着头脑。

prompt工程来看是不错的选择,然而这里的难点可不仅仅是提示词,譬如项目的部署配置,外行人学这些内容也不容易。

虽然AI写作方面确实厉害,但总感觉抓不住重点达不到想要的结果。如果抛开最初的主题,那AI写的内容绝对称得上高水平。我思考片刻,也许是因为我的提示词让AI在理解上产生偏差。

还有很多方面AI做的都不错,看过AI看图做诗,我真想让他好好教我如何做出曼妙而又易懂的诗句。

也许AI可以作为我的老师,但我还不知道应该让AI用什么方式来教学。毕竟目前的AI是一问一答,用AI学习不仅要主动还要时刻做好课程规划。让AI当老师,不知道会不会越学越像AI。

用AI做了什么

开篇关于AI的内容,但这篇文章的主题是月度总结,然而这并不冲突,因为2月份的大部分时间都在探究如何使用AI。

当然你无需怀疑这篇总结是不是AI撰写,毕竟坚守个人博客网站要尽情的自我表达而非AI撰写或润色,这是我的信条,因为AI代表不了我内心深处的感受。

自从手机装了DeepSeek,总是出现“服务器繁忙”,它实在是太火了。而后寻找DeepSeek替代品。DeepSeek满血版实在是太多了。

先是下载了360的“纳米AI搜索”,虽然能满足我的需求,但首屏给我的感觉很乱,尤其是红衣大叔送车的广告,我只是想简简单单使用DeepSeek,虽然如此,不过纳米AI集成了很多大模型,值得下载。

随后又下载了“腾讯元宝”,这界面清爽比较符合我对简洁的要求。不仅有DeepSeek,还有自己的Hunyuan,这两个大模型非常聪明。目前“腾讯元宝”成了我首选的AI工具。

生活上的很多事情,都会向DeepSeek提问。譬如:“4000元左右的相机哪个值得买”,“预防老寒腿的方法”,“10万块创业有什么建议”。DeepSeek都能给出很好的分析,最后还有好的建议。

DeepSeek太好用了,我把这个应用推荐给同事。听闻他有意向在老家贵阳买房子,我问DeepSeek,贵阳哪里的房子比较适合居住。DeepSeek罗列了各区的特点以及参考房价,最终给了建议。同事感觉太神奇了,比中介还要专业,立刻下载了DeepSeek。

但是目前我只停留在向DeepSeek提问的层面,AI还能做很多事情,譬如AI写作、AI生图、AI生成视频、AI编程,越后尝试越觉得难。想要精通AI不是一朝半夕。了解AI的思考方式,预设AI输出的结果框架,让AI能够明白我要表达什么我想做什么,让AI为我做好一切。虽然很难,但是能够实现。

通勤漫骑

自行车后拨出现故障,其中一颗调节螺丝的内丝坏了,导致后拨及极不稳定,异响并且时不时地自动跳档。还好最近骑得不快,上下班将就骑骑还可以。

网上搜索蓝图后拨,找到一样的后拨,价格不贵。等有空了或是想换的时候再更换。

总里程353.36
总时长17:14:08
骑行次数29
月均速20.50km/h

最近速度提不上来也许跟后拨有关系,但也有可能是太累了。低头看看肚皮,过了年发福不少,该减重了,不然更骑不动了。现在有些许担心,这样反反复复,想要突破实在是太难了。

环梦·AI科普展

老姐给了一张AI机器人的展票,刚好周末去逛逛。春晚AI智能机器人上台表演,人形机器人十分灵活,勾起我想要一探究竟的欲望。

“2025环梦·AI智能机器人科普展览”在上海世贸展馆举办。进门比较抢眼的是人形G1机器人,大家都争相与机器人握手。虽然G1人个头不高,走起路来别有感觉。

然后是更加抢眼的仿真机器人,围观的人比较多。这些仿真机器人虽然生的好看,但是肢体算不上灵活,表情不够丰富,也许是仿生组织封印了他的灵动感。

而后有智能机器狗,小米的机械狗也在其列,还有大名鼎鼎的Go2机器狗。看到Go2能够灵活自如的蹦跳,身手敏捷,十分活跃。心想日后导盲犬可以放飞自我,放下这份工作了。

虽然这些机器人机器狗都有AI交互,但展会现场环境十分嘈杂,机器人无法听清或听不到我们在说什么,显得不够智能。

这次展会上还有很多其他机器人,譬如说财神迎宾机器人、酒店引导机器人、舞蹈机器人、格斗机器人。

这次展会没有耳目一新的感觉,也许是我对他们的期望太高,但是智能机器人能发展成现在的墨阳已经很了不起了。以后应该会有更多场景的智能机器人,就像电影《机器人总动员》里的机器人一样,都有特殊功能适应不同岗位。

每月一本书

赶在月末最后几天把《蛤蟆先生去看心理医生》看完了。蛤蟆从萎靡不振到重新振作起来,通过心理医生苍鹭的引导,不断认识自我,脱离原来的自己,完成了自救。

不仅仅是蛤蟆先生内心和性格发生了变化,他的小伙伴们也发生了变化,河鼠、鼹鼠有了新的人生目标,年纪较大的獾也有微妙的心理变化。

这本书比较适合精神上深陷泥潭并想要改版现状寻求自救的人,也许书中会有想要的答案和明灯。

写在最后

最近想要做点项目,但不知道做什么好。公众号、网站、社区运营,对我来说都有知识盲区,即使有时候知道该怎么做,执行的时候却发现自己做不了。我的水平也只停留在建设初期,找不到方法或是找错方法,失败感常常让我望而却步。

当然很多人说,先做了再说。遇神杀神,佛挡杀佛,魔来斩魔。这种大无畏的精神正是我的薄弱,而我总是瞻前思后。为此做了心理小测试,结果是我适合做协助工作。

看来想要做成就一些事情,跟性格是有一定的关系。当然我知道性格是可以重塑。针对特定环境进行训练,以此达到某种条件反射。虽然认知根本没发生变化,但是用这样的方法可以达成某种目的。

  •  

家庭数据中心系列 Cloudflare Pro 深度体验:从 Free 到 Pro,到底值不值得升级?

家庭数据中心系列 Cloudflare Pro 深度体验:从 Free 到 Pro,到底值不值得升级? 无敌的个人博客 tangwudi

1 前言 我大约是去年年初的时候知道的Cloudflare,距今已经差不多一年了,这一年中我使用Free账户白嫖了Cloudflare的众多功能(CDN缓存加速、WAF、DDoS攻击防护、Tunnel、worker、R2等等),说实话,我心情一直很复杂:一方面白嫖得很爽,一方面又觉得白嫖得很惭愧(当年因为一直使用盗版感觉亏欠了微软,我可是自费购买surface pro 4来还债的~)。同时,我对pro订阅用户多出的功能也很好奇,毕竟pro用户年付费的话,20美金/月也不算贵,每个月少吃一顿大餐就省出来了,关键在于,相对于普通Free用户来说,pro订阅用户多出的功能到底值不值20美金(我相信绝大部分使用Cloudflare Free账户的站长都有这个疑惑)? 带着疑惑,在网上搜了半天,我发现几乎没有详细验证这个问题的文章,一般也是一些论坛里有人提出问题,然后有人简单的回答几句,并且主观性也 […]

<p>The post 家庭数据中心系列 Cloudflare Pro 深度体验:从 Free 到 Pro,到底值不值得升级? first appeared on 无敌的个人博客.</p>

  •  

机器人学 I

基础概念

连杆(Link):按照顺序连接的刚体。

关节(Joint):连接连杆的部件,决定了相邻连杆之间的运动自由度(DoF,Degree of Freedom)。

自由度(DoF,Degree of Freedom):机械臂的自由度是指机械臂能够自由运动的维度。

刚性变换(Rigid Transformation)

点的表示与坐标系

约定:

  • 任意点 $p$ 的位置由一个参考系 $\mathcal{F}_s$ 记录。
  • 点的坐标记为普通字母(如 $p$),向量用粗体字母表示(如 $\mathbf{v}$)。
  • s 代表 space,b 代表 body。

记录公式包含参考系的上标,例如:

coordinate_axes_vector_representation

$$ o_b^s = o_s^s + \mathbf{t}_{s \to b}^s $$

这个公式表示:在坐标系 $\mathcal{F}s$ 中,点 $o_b$ 的位置是 $o_s$ 的位置加上平移向量 $\mathbf{t}{s \to b}^s$。

刚体的位姿变换

刚体自身会绑定一个坐标系 $\mathcal{F}_b$,当刚体移动时,此坐标系也会移动。

所以,刚体的 位姿(位置与姿态,pose) 变化,就是通过 坐标系变换 来对齐两个坐标系。也即将 $\mathcal{F}_s$ 通过旋转和平移变换,使其与 $\mathcal{F}_b$ 重合。

coordinate_frame_transformation_diagram

  • 转动矩阵(rotation):$R_{s \to b}$,用于对齐坐标轴 ${x_i, y_i, z_i}$,代表 “朝向”
  • 平动向量(translation):$\mathbf{t}_{s \to b}$,用于对齐原点 $o_s$ 和 $o_b$,代表 “位置”

$(R_{s \to b}^s, \mathbf{t}_{s \to b}^s)$ 合在一起,就描述了一个刚体的位姿,其拥有 6 个自由度,转动和平动各自拥有 3 个自由度。

  • 原点变换: $$ o_b^s = o_s^s + \mathbf{t}_{s \to b}^s $$
  • 坐标轴变换: $$ [\mathbf{x}_b^s, \mathbf{y}_b^s, \mathbf{z}b^s] = R{s \to b} [\mathbf{x}_s^s, \mathbf{y}_s^s, \mathbf{z}_s^s] $$

如果观察者使用 $\mathcal{F}_s$:

$$ o_s^s = 0, \quad [\mathbf{x}_s^s, \mathbf{y}_s^s, \mathbf{z}s^s] = I{3 \times 3} $$

则:

$$ \mathbf{t}{s \to b}^s = o_b^s, \quad R{s \to b} = [\mathbf{x}_b^s, \mathbf{y}_b^s, \mathbf{z}_b^s] \in \mathbb{R}^{3 \times 3} $$

相对的,如果观察者使用 $\mathcal{F}_b$:

假设刚体上的点 $p$ 在 $\mathcal{F}_b$ 中的坐标为 $p^b$(随刚体运动,所以相对于坐标系 $\mathcal{F}_b$ 固定不变),其在 $\mathcal{F}_s$ 中的坐标为 $p^s$,则有:

  1. 初始时,$\mathcal{F}_s = \mathcal{F}_b$,$p^s = p^b$。
  2. 刚体发生运动,相对于参考系 $\mathcal{F}s$,此运动可以描述为 $(R{s \to b}^s, \mathbf{t}{s \to b}^s)$,则: $$ p^s = R{s \to b}^s p^b + \mathbf{t}_{s \to b}^s $$
  3. 同理,对于任意点 $x^s$,变换后的点 $x'^s$ 表示为: $$ x'^s = R_{s \to b} x^s + \mathbf{t}_{s \to b} $$

值得注意的是,当 $\mathbf{t}{s \to b}^s \neq 0$ 时, $(R{s \to b}^s, \mathbf{t}{s \to b}^s)$ 这个变换并不是线性的。反之,当 $\mathbf{t}{s \to b}^s = 0$ 时,变换是线性的。

齐次坐标

在三维空间中,齐次坐标系将一个点 $x \in \mathbb{R}^3$ 表示为:

$$ \tilde{x} := \begin{bmatrix} x \ 1 \end{bmatrix} \in \mathbb{R}^4 $$

对应的,齐次变换矩阵具有以下形式:

$$ T^s_{s\rightarrow b} = \begin{bmatrix} R^s_{s\rightarrow b} & t^s_{s\rightarrow b} \ 0 & 1 \end{bmatrix} $$

其中 $R^s_{s\rightarrow b}$ 是旋转矩阵,$t^s_{s\rightarrow b}$ 是平移向量。

这么做的原因是,在传统的笛卡尔坐标系中,平移和旋转是两种不同性质的变换:

  • 旋转是线性变换:$x' = Rx$
  • 平移是仿射变换:$x' = x + t$

这导致无法用单一矩阵乘法表示同时包含旋转和平移的变换。而在齐次坐标系中,两种变换统一为:

$$ \begin{bmatrix} x' \ 1 \end{bmatrix} = \begin{bmatrix} R & t \ 0 & 1 \end{bmatrix} \begin{bmatrix} x \ 1 \end{bmatrix} = \begin{bmatrix} Rx + t \ 1 \end{bmatrix} $$

注意,这种变换保持刚体的形状和大小不变,只改变其位置和方向。

通过引入齐次坐标,我们恢复了线性,此时多个变换的组合可以通过矩阵乘法简洁表示,且满足传递性、可逆性:

$$ T_3 = T_2 \cdot T_1 \ T_{2\to1}^2=\left(T_{1\to2}^1\right)^{-1} $$

这极大地简化了计算复杂变换序列的过程,现在,坐标变换遵循一般规则:

$$ x^1 = T^1_{1\rightarrow 2}x^2 $$

直观上容易记混淆这个公式。请记住,这个 $x$ 是随着刚体变动的,$x^2$ 是其在变换后坐标系下的坐标,亦是变换前的坐标,经过固定坐标系下的变换矩阵 $T^1_{1\to2}$ ,就得到了变换后的、在原始固定坐标系下的坐标 $x^1$。

同时,我们显然有:

$$ x^{2}=(T_{1\to2}^{1})^{-1}x^{1}=T_{2\to1}^{2}x^{1} $$

在后文中,我们忽略 $\tilde{}$ ,默认在齐次坐标系下写公式。

多连杆刚体几何

基本关节类型

  1. Revolute Joint(旋转关节 / 铰链关节)

    • 允许绕单一轴线的旋转运动。

    • 1 DoF

      revolute_joint_1_dof

  2. Prismatic Joint(滑动关节 / 平移关节)

    • 允许沿单一方向的平移运动。

    • 1 DoF

      prismatic_joint_1dof_diagram

  3. Helical Joint(螺旋关节)

    • 螺旋运动,即旋转与平移的组合运动,旋转和平移之间存在固定比率。

    • 1 DoF

      helical_one_dof_diagram

  4. Spherical Joint(球形关节 / 球窝关节)

    • 允许绕球心进行任意方向的旋转。

    • 3 DoF

      spherical_joint_ball_socket

总结:

| 关节类型 | 英文名称 | 自由度(DoF) | 运动描述 | | -------- | ------------- | ------------- | ----------------------- | | 旋转关节 | Revolute (R) | 1 | 绕单一轴线旋转 | | 滑动关节 | Prismatic (P) | 1 | 沿单一方向平移 | | 螺旋关节 | Helical (H) | 1 | 螺旋运动(旋转 + 平移) | | 球形关节 | Spherical (S) | 3 | 任意方向旋转 |

基座连杆和末端执行器

基座连杆 (Base link / Root link)

  • 定义:第 0 号连杆。
  • 特点
    • 被视为 “固定” 参考。
    • 空间坐标系 $\mathcal{F}_s$ 附着于此。

末端执行器连杆 (End-effector link)

  • 定义:最后一个连杆。
  • 特点
    • 通常为抓手(gripper)。
    • 末端坐标系 $\mathcal{F}_e$ 附着于此。

robot_arm_kinematics_diagram

如何看坐标系:

  • $\color{red}{x}$ 是红
  • $\color{green}{y}$ 是绿
  • $\color{blue}{z}$ 是蓝

变换矩阵

robot_arm_revolute_joint_diagram

$$ T_{0\to1}^0=\begin{bmatrix}\cos\theta_1&-\sin\theta_1&0&-l_2\sin\theta_1\\sin\theta_1&\cos\theta_1&0&l_2\cos\theta_1\0&0&1&l_1\0&0&0&1\end{bmatrix} $$

要点:旋转矩阵没影响 $z$ 轴;平动向量在平面上也有变动,因为绕着 $l_2$ 左端点转了一下。

prismatic_joint_mechanism_diagram

$$ T_{1\to2}^1=\begin{bmatrix}1&0&0&0\0&1&0&l_3\0&0&1&\theta_2\0&0&0&1\end{bmatrix} $$

要点:转动矩阵为 $I$;平动向量只改了 $y,z$。

robotic_arm_link_end_effector

$$ T_{2\to3}^2=\begin{bmatrix}1&0&0&0\0&1&0&0\0&0&1&-l_4\0&0&0&1\end{bmatrix} $$

要点:转动矩阵为 $I$;平动向量只改了 $z$。

base_to_end_effector_diagram

$$ T_{0\to3}^{0}=T_{0\to1}^{0}T_{1\to2}^{1}T_{2\to3}^{2}=\begin{bmatrix}\cos\theta_{1}&-\sin\theta_{1}&0&-\sin\theta_{1}(l_{2}+l_{3})\\sin\theta_{1}&\cos\theta_{1}&0&\cos\theta_{1}(l_{2}+l_{3})\0&0&1&l_{1}-l_{4}+\theta_{2}\0&0&0&1\end{bmatrix}=\begin{bmatrix}R_{s\to e}^{s}&\mathbf{t}_{s\to e}^{s}\0&1\end{bmatrix} $$

旋转的参数化

参数化:用一组简单的数值参数来完整描述一个复杂系统或对象的过程。

假设我们已经为 Robot 的每个连杆(Link)分配了坐标系,那么我们可以使用相邻(adjacent)坐标系之间的 相对角度平移 来参数化每个关节。

而对于末端执行器(End-Effector),我们又有如下两种方式来表征其位姿:

关节空间表示(Joint space)

  • 这是一个向量空间,其中每个坐标是关节位姿的向量
  • 具体来说,是关节围绕关节轴的 角度 向量
  • 例如,一个 6 自由度机器人会有 6 个关节角度值 $(θ_1, θ_2, θ_3, θ_4, θ_5, θ_6)$

笛卡尔空间表示(Cartesian space)

  • 这是末端执行器刚体变换的空间
  • 用数学符号表示为:$(R_{s→e}, t_{s→e})$
    • 其中 $R_{s→e}$ 表示从基座坐标系到末端执行器坐标系的旋转矩阵
    • $t_{s→e}$ 表示从基座坐标系到末端执行器坐标系的平移向量
  • $\mathcal{F}_e$ 表示末端执行器的坐标系

对比

  • 关节空间 直观地反映了机器人各关节的实际物理状态,强调关节。
  • 笛卡尔空间 则描述了机器人末端在三维空间中的实际位置和方向,更符合人类思考方式,容易进行判断目标是否达成,强调末态。

联系

正向运动学 (Forward Kinematics,FK)

正向运动学将关节空间坐标 $\theta \in \mathbb{R}^n$ 映射到变换矩阵 $T$:

$$ T_{s \rightarrow e} = f(\theta) $$

也即,给定关节角度,计算末端执行器的位置和姿态。

这一映射可以简单地通过沿着运动链组合各个变换矩阵计算得出。

逆向运动学 (Inverse Kinematics,IK)

逆向运动学解决的问题:给定正向运动学 $T_{s \rightarrow e}(\theta)$ 和目标姿态 $T_{target} = \mathbb{SE}(3)$,求解满足以下条件的关节角度 $\theta$:

$$ T_{s \rightarrow e}(\theta) = T_{target} $$

过程:给定末端执行器的目标位置和姿态,计算需要的关节角度

逆向运动学比正向运动学更复杂,因为 $T^{-1}$ 可能很难计算,所以 通常可能有多个解或无解

robot_arm_kinematics_diagram

根据前文所述,三维空间中,任何刚体的完整位姿可以用 6 个独立参数完全描述,即 $(R,t)$。

因此,6 自由度是机械臂实现空间中任意位置和姿态所需的最小自由度数量。这也称为 "完全自由度" 配置。

至少 6 个自由度可以保证覆盖此空间,从而 IK 的方程有解(但有时候可能得不到解析解,只能得到数值解)。

引理:如果机械臂构型满足 Pieper Criterion,则有解析解(闭式解)。

实例:UR5 机械臂。

虽然 6 自由度保证了有解,但是这个解可能超出了可行空间(如碰撞解),所以额外增加 1 个冗余自由度形成 7 自由度,可以扩大解空间,更有可能找到可行解(非碰撞解)。

但我们不能一味增加自由度,因为这会带来工程复杂性并延长反应时间,所以目前工业界一般是 6 或者 7 DoF。

一个 IK 求解方式(cuRobo):

  1. 选定一个初始值 $\theta_0$

  2. 目标:最小化能量函数(Energy Function)

    $$ \arg \min_{\theta} ||T_{s \rightarrow e}(\theta) - T_{target}||_2 $$

  3. 迭代直到收敛

  4. 可以使用 GPU 并行迭代多个随机选定的初始值,加快速度,并尝试找到最优解

应用

假设我们已知机械臂现在状态,我们想要略微移动一点到达新的状态,我们该选择何种表征进行预测?

  1. 使用笛卡尔空间,优点是 $(\Delta R, \Delta t)$ 直观,容易预测,缺点是执行操作所需的 $\Delta \theta$ 难以计算(需要 IK),RT-2 选用的是这种。
  2. 使用关节空间,优点是预测得到 $\Delta \theta$ 后很容易操作,并计算移动后的 $(R, t)$ 以及 $(\Delta R, \Delta t)$ 易于计算(FK),缺点是 $\Delta \theta$ 难以求解,$\pi0$ 选用的是这种。

SE (3) 群与空间变换的表示方法

SE (3) 是 Special Euclidean group in 3 dimensions 的缩写,代表三维特殊欧几里得群。它描述了三维空间中所有的刚体变换(rigid transformations),包括旋转和平移,但不包括缩放、切变等变形。

SE (3) 群可以数学表示为:

$$ \mathbb{SE}(3):=\left{T=\begin{bmatrix}R&\mathbf{t}\0&1\end{bmatrix},R\in\mathbb{SO}(3),\mathbf{t}\in\mathbb{R}^3\right} $$

其中:

  • $\mathbb{SO}(3)$ 是三维特殊正交群,表示所有的三维旋转
  • $t$ 是三维空间中的平移向量

注意这里:

  • 所有三维正交矩阵是 $\mathbb{O}(3)$
  • 旋转矩阵是 $\mathbb{SO}(3) \subset \mathbb{O}(3)$,其满足行列式是 1,因为这样可以保证应用后手性不变,如果行列式是 -1,那么实际上是一个旋转加镜像的操作。

延伸:

  • $\mathbb{SO}(2)$ 是二维旋转矩阵,有 1 个自由度
  • $\mathbb{SO}(3)$ 是三维旋转矩阵,有 3 个自由度

drone_orientation_angles_axes

欧拉角

欧拉角(Euler Angles):描述三维旋转的一种方法,通过三个连续的旋转来表示任意旋转。

eular-angle

  • 绕 X 轴旋转 $\phi$(roll)

    roll

  • 绕 Y 轴旋转 $\theta$(pitch)

    pitch

  • 绕 Z 轴旋转 $\psi$(yaw)

    yaw

应用:相较于旋转矩阵 $R$,所需数值表示从 9 个降低到了 3 个。

$$ \begin{gathered} R_{x}(\alpha):=\begin{bmatrix}1&0&0\0&\cos\alpha&-\sin\alpha\0&\sin\alpha&\cos\alpha\end{bmatrix}\ R_{y}(\beta):=\begin{bmatrix}\cos\beta&0&\sin\beta\0&1&0\-\sin\beta&0&\cos\beta\end{bmatrix}\ R_{z}(\gamma):=\begin{bmatrix}\cos\gamma&-\sin\gamma&0\\sin\gamma&\cos\gamma&0\0&0&1\end{bmatrix} \end{gathered} $$

任意旋转均可拆为 $R=R_{z}(\alpha)R_{y}(\beta)R_{x}(\gamma)$。这个顺序可以变,但一般默认是这个顺序。

问题:

  1. 对于一个旋转矩阵,其欧拉角可能不唯一

    $$ \begin{aligned}R_z(45°)R_y(90°)R_x(45°)&=R_z(90°)R_y(90°)R_x(90°)\&=\begin{bmatrix}0&0&1\0&1&0\-1&0&0\end{bmatrix}\end{aligned} $$

  2. Gimbal Lock:如果三次旋转中第二次旋转 $\beta$ 的角度为 $\pi/2$,那么剩下 2 个自由度会变成 1 个。

    $$ R_z(\alpha) = \begin{bmatrix} \cos\alpha & -\sin\alpha & 0 \ \sin\alpha & \cos\alpha & 0 \ 0 & 0 & 1 \end{bmatrix} \ R_y(\beta) = \begin{bmatrix} \cos\beta & 0 & \sin\beta \ 0 & 1 & 0 \ -\sin\beta & 0 & \cos\beta \end{bmatrix} = \begin{bmatrix} 0 & 0 & 1 \ 0 & 1 & 0 \ -1 & 0 & 0 \end{bmatrix} \ R_x(\gamma) = \begin{bmatrix} 1 & 0 & 0 \ 0 & \cos\gamma & -\sin\gamma \ 0 & \sin\gamma & \cos\gamma \end{bmatrix} $$

    带入、合并计算:

    $$ R_y(\pi/2)R_x(\gamma) = \begin{bmatrix} 0 & 0 & 1 \ 0 & 1 & 0 \ -1 & 0 & 0 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \ 0 & \cos\gamma & -\sin\gamma \ 0 & \sin\gamma & \cos\gamma \end{bmatrix} = \begin{bmatrix} 0 & \sin\gamma & \cos\gamma \ 0 & \cos\gamma & -\sin\gamma \ -1 & 0 & 0 \end{bmatrix} $$

    $$ \begin{aligned} R &= R_z(\alpha) [R_y(\pi/2)R_x(\gamma)] = \begin{bmatrix} \cos\alpha & -\sin\alpha & 0 \ \sin\alpha & \cos\alpha & 0 \ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 0 & \sin\gamma & \cos\gamma \ 0 & \cos\gamma & -\sin\gamma \ -1 & 0 & 0 \end{bmatrix} \ &= \begin{bmatrix} 0 & \cos\alpha\sin\gamma - \sin\alpha\cos\gamma & \cos\alpha\cos\gamma + \sin\alpha\sin\gamma \ 0 & \sin\alpha\sin\gamma + \cos\alpha\cos\gamma & \sin\alpha\cos\gamma - \cos\alpha\sin\gamma \ -1 & 0 & 0 \end{bmatrix} \ &= \begin{bmatrix} 0 & -\sin(\alpha-\gamma) & \cos(\alpha-\gamma) \ 0 & \cos(\alpha-\gamma) & \sin(\alpha-\gamma) \ -1 & 0 & 0 \end{bmatrix} \end{aligned} $$

轴角表示法

欧拉定理:任意三维空间中的旋转都可以表示为绕一个固定轴 $\hat{\omega} \in \mathbb{R}^3$(单位向量,满足 $|\hat{\omega}| = 1$)旋转一个正角度 $\theta$ 的结果。

其中:

  • $\hat{\omega}$:旋转轴的单位向量。
  • $\theta$:旋转角度(正方向遵循右手定则)。
  • $R\in\mathbb{SO}(3):=\mathrm{Rot}(\hat{\omega},\theta)$:三维旋转矩阵,必然可以表示为绕 $\hat{\omega}$ 旋转角度 $\theta$ 的变换。

vector_rotation_angle_diagram

轴角表示法的问题:

  • 不唯一性:$(\hat{\omega}, \theta)$ 和 $(-\hat{\omega}, -\theta)$ 代表同一个旋转
  • 当旋转是单位矩阵 $R=I$ 时(即没有旋转),$\theta=0$,此时旋转轴 $\hat{\omega}$ 可以是任意方向。
  • 当旋转角度 $\theta = \pi$ 时,绕轴 $\hat{\omega}$ 和绕轴 $-\hat{\omega}$ 旋转 $\pi$ 得到的结果是相同的。这种情况对应 $\text{tr}(R) = -1$。

如果我们将旋转角 $\theta$ 限制在 $(0, \pi)$ 这个开区间内,那么对于大部分旋转,其轴角表示就是唯一的(不考虑不旋转、旋转 $\pi$)。

轴角表示到旋转矩阵

对于一个单位轴向量(axis)$\mathbf{u} = [x, y, z]^\top$,其对应的叉乘矩阵(cross product matrix)$K$ 定义为:

$$ K = \begin{bmatrix} 0 & -z & y \ z & 0 & -x \ -y & x & 0 \end{bmatrix} $$

其具有性质:当 $K$ 与任意向量 $\mathbf{v}$ 相乘时,运算结果等同于 $\mathbf{u}$ 和 $\mathbf{v}$ 的叉乘:

$$ K\mathbf{v} = \begin{bmatrix} 0 & -z & y \ z & 0 & -x \ -y & x & 0 \end{bmatrix} \begin{bmatrix} v_1 \ v_2 \ v_3 \end{bmatrix} = \begin{bmatrix} -z v_2 + y v_3 \ z v_1 - x v_3 \ -x v_2 + y v_1 \end{bmatrix} = \mathbf{u} \times \mathbf{v} $$

那么,绕单位轴 $\mathbf{u}$ 旋转 $\theta$ 的旋转矩阵 $R_\theta$ 可以表示为:

$$ \begin{aligned} R_\theta &= \cos\theta \cdot I + (1-\cos\theta)(\mathbf{u}\mathbf{u}^\top) + \sin\theta \cdot K \ &= I + (1-\cos\theta)(\mathbf{u}\mathbf{u}^\top - I) + \sin\theta \cdot K \ & = I + (1-\cos\theta) K^2 + \sin\theta \cdot K \end{aligned} $$

这就是 Rodrigues 旋转公式(矩阵形式)

为了证明它,我们先证明向量形式:

Rodrigues 旋转公式(向量形式):在 3D 空间中,任意一个向量 $\mathbf{v}$ 沿着单位向量 $\mathbf{u}$ 旋转 $\theta$ 角度之后的向量 $\mathbf{v}'$ 为:

$$ \mathbf{v}' = \cos(\theta)\mathbf{v} + (1 - \cos(\theta))(\mathbf{u} \cdot \mathbf{v})\mathbf{u} + \sin(\theta)(\mathbf{u} \times \mathbf{v}) $$

其详细证明参见 Krasjet / Quaternion 第 2 节・三维空间中的旋转(第 11 页)。

从向量形式稍加变形,我们就能得到矩阵形式:

$$ \begin{aligned} \mathbf{v}^{\prime}&=\cos(\theta)\mathbf{v}+(1-\cos(\theta))(\mathbf{u}\cdot\mathbf{v})\mathbf{u}+\sin(\theta)(\mathbf{u}\times\mathbf{v}) \ &=\cos(\theta)\mathbf{v}+(1-\cos(\theta))(\mathbf{u}^\top\mathbf{v})\mathbf{u}+\sin(\theta)(\mathbf{u}\times\mathbf{v}) \ &=\cos(\theta)\mathbf{v}+(1-\cos(\theta))\mathbf{u}(\mathbf{u}^\top\mathbf{v})+\sin(\theta)(\mathbf{u}\times\mathbf{v}) \ &=\begin{bmatrix}\cos(\theta)I+(1-\cos(\theta))(\mathbf{u}\mathbf{u}^\top)+\sin(\theta)K\end{bmatrix}\mathbf{v} \ &=R_\theta\mathbf{v} \end{aligned} $$

旋转矩阵 $R_\theta$ 也可以写成:

$$ R_\theta = e^{\theta K} $$

我们可以证明后者和前者是等价的:

$$ e^{\theta K} = I + \theta K + \frac{(\theta K)^2}{2!} + \frac{(\theta K)^3}{3!} + \cdots $$

而我们又有:

$$ K^2 = \begin{bmatrix} -z^2 - y^2 & xy & xz \ xy & -x^2 - z^2 & yz \ xz & yz & -x^2 - y^2 \end{bmatrix} $$

利用 $\mathbf{u}$ 是单位向量的性质($x^2 + y^2 + z^2 = 1$),可简化为:

$$ K^2 = \mathbf{u}\mathbf{u}^\top - I $$

所以:

$$ K^3 = K \cdot K^2 = K (\mathbf{u}\mathbf{u}^\top - I) = K \mathbf{u}\mathbf{u}^\top - K = -K $$

这里利用了叉乘性质 $K\mathbf{u} = \mathbf{u} \times \mathbf{u} = \mathbf{0}$。

所以:

$$ K^3 = -K, \quad K^4 = -K^2, \quad K^5 = K, \quad \dots $$

带回展开形式,合并同类项:

$$ \begin{aligned} e^{\theta K} &= I + \left(\theta - \frac{\theta^3}{3!} + \frac{\theta^5}{5!} - \cdots\right)K + \left(\frac{\theta^2}{2!} - \frac{\theta^4}{4!} + \cdots\right)K^2 \ &= I + \sin\theta K + (1 - \cos\theta)K^2 \ &= I + \sin\theta K + (1 - \cos\theta)(\mathbf{u}\mathbf{u}^\top - I) \ &= \cos\theta I + (1 - \cos\theta)\mathbf{u}\mathbf{u}^\top + \sin\theta K \ &= R_\theta \end{aligned} $$

从旋转矩阵 R 反求 $(\hat{\omega}, \theta)$

当 $\theta \in (0, \pi)$ 时,可以通过以下公式从旋转矩阵 $R$ 计算出 $\theta$ 和 $\hat{\omega}$:

  • $\theta = \arccos \frac{1}{2}[\text{tr}(R) - 1]$

  • $[\hat{\omega}] = \frac{1}{2 \sin \theta}(R - R^\top)$

    注意:$[\hat{\omega}]$ 表示与向量 $\hat{\omega}$ 相关联的反对称矩阵(skew-symmetric matrix)/ 叉乘矩阵

证明:$\theta = \arccos \frac{1}{2}[\text{tr}(R) - 1]$

对罗德里格公式两边取迹 (trace):

$$ \text{tr}(R) = \text{tr}(I + \sin \theta [\hat{\omega}] + (1 - \cos \theta) [\hat{\omega}]^2) $$

利用迹的线性性质 $\text{tr}(A+B) = \text{tr}(A) + \text{tr}(B)$ 和 $\text{tr}(cA) = c \cdot \text{tr}(A)$:

$$ \text{tr}(R) = \text{tr}(I) + \sin \theta \cdot \text{tr}([\hat{\omega}]) + (1 - \cos \theta) \cdot \text{tr}([\hat{\omega}]^2) $$

代入已知的迹的值:$\text{tr}(I)=3$, $\text{tr}([\hat{\omega}])=0$, $\text{tr}([\hat{\omega}]^2)=-2$。

$$ \begin{aligned} \text{tr}(R) &= 3 + \sin \theta \cdot 0 + (1 - \cos \theta) \cdot (-2) \ &= 3 - 2(1 - \cos \theta) = 3 - 2 + 2 \cos \theta \ &= 1 + 2 \cos \theta \end{aligned} $$

整理得到 $\cos \theta$:

$$ 2 \cos \theta = \text{tr}(R) - 1 \ \cos \theta = \frac{1}{2}[\text{tr}(R) - 1] \ \theta = \arccos \left( \frac{1}{2}[\text{tr}(R) - 1] \right) $$

证明:$[\hat{\omega}] = \frac{1}{2 \sin \theta}(R - R^\top)$

首先计算 $R$ 的转置 $R^\top$。

利用性质:

  • $[\hat{\omega}]^\top = -[\hat{\omega}]$

  • $([\hat{\omega}]^2)^\top = ([\hat{\omega}][\hat{\omega}])^\top = [\hat{\omega}]^\top [\hat{\omega}]^\top = (-[\hat{\omega}])(-[\hat{\omega}]) = [\hat{\omega}]^2$

    即 $[\hat{\omega}]^2$ 是对称矩阵。

$$ \begin{aligned} R^\top &= (I + \sin \theta [\hat{\omega}] + (1 - \cos \theta) [\hat{\omega}]^2)^\top \ &= I^\top + (\sin \theta [\hat{\omega}])^\top + ((1 - \cos \theta) [\hat{\omega}]^2)^\top \ &= I + \sin \theta [\hat{\omega}]^\top + (1 - \cos \theta) [\hat{\omega}]^2 \ &= I - \sin \theta [\hat{\omega}] + (1 - \cos \theta) [\hat{\omega}]^2 \end{aligned} $$

现在计算 $R - R^\top$:

$$ \begin{aligned} R - R^\top &= (I + \sin \theta [\hat{\omega}] + (1 - \cos \theta) [\hat{\omega}]^2) - (I - \sin \theta [\hat{\omega}] + (1 - \cos \theta) [\hat{\omega}]^2) \ &= (I - I) + (\sin \theta - (-\sin \theta)) [\hat{\omega}] + ((1 - \cos \theta) - (1 - \cos \theta)) [\hat{\omega}]^2 \ &= 0 + (2 \sin \theta) [\hat{\omega}] + 0 \ &= 2 \sin \theta [\hat{\omega}] \end{aligned} $$

当 $\theta \in (0, \pi)$ 时,$\sin \theta \neq 0$,所以我们可以两边同除以 $2 \sin \theta$:

$$ [\hat{\omega}] = \frac{1}{2 \sin \theta}(R - R^\top) $$

由此,我们可以定义两个旋转矩阵之间的 旋转距离

旋转距离:从姿态 $R_1$ 转到姿态 $R_2$ 所需的最小旋转角度。

易知,两个旋转的关系是:

$$ (R_2 R_1^\top) R_1 = R_2 $$

那么,旋转距离 $\text{dist}(R_1, R_2)$ 由以下公式给出(注意 $\theta(\cdot)$ 是上述欧拉定理中的函数):

$$ \text{dist}(R_1, R_2) = \theta(R_2 R_1^\top) = \arccos\left(\frac{1}{2} \big[\text{tr}(R_2 R_1^\top) - 1\big]\right) $$

四元数(Quaternion)

扩展内容,下节课详细推导。

参考:

  1. Krasjet / Quaternion
  2. Wiki / Quaternion

💾

  •  

初见具身智能

汽车工厂机器人

核心:预设计并计算轨迹,随后只是重放轨迹,实际上是不断 “重放”

问题:

  • 部署耗时
  • 无法灵活处理多任务

如果想要足够通用,则需要像人,才能实现 “通用机器人”(Task generalists),能够形成 perception-action loop(感知 - 动作循环)

实际上是形成了一个神经网络:

  • 输入:本体状态、控制信息、环境信息
  • 输出:下一步的关节控制

VLA (Vision Language Action Model)

神经网络:

  • 输入:V (vision) + L (language),现在有 VLM 模型
  • 输出:A (action)

思维活动:

  • 快系统(Faster system):动作生成
  • 慢系统(Slower system):复杂推理

人脑:

  • 大脑进行感知
  • 小脑控制动作

观点:没有具身智能,就没有 AGI。

困境

具身智能最大的问题:缺少真实数据,不能满足 Scaling Law 所需的数据量。

和智能驾驶不一样,在真实世界中快速采集到所需数据是几乎不可能的。

神经网络还有一个问题,就是泛化性,因为在真实世界中数据的分布可能会与训练集的分布不一致。

可能的解决方法:合成数据。

优点:

  • 无需注释
  • 高效节约时间
  • 可转移到现实世界

💾

  •  

修整自行车,骑行打卡天马山射电望远镜。37.07公里,均速20.14km/h

很久没出去骑车了,把自行车推出来修整一下,调整车把,后拨变速器,后轮辐条。抖音视频现学现用,虽然原理简单,但是要调整到理想的状态实在太难。最终后变速器还是会跳档,五档直接跳到七档,再减一档才能到六档。后轮辐条调整了很长时间,虽然好了一些,但是还有稍许偏摆。

下午阳光正好,适合出去骑车。穿上冲锋衣,戴好头盔,特意把心率带也穿戴好。漫无目的的骑车,计划骑40公里左右,均速20左右,当做适应性骑车。毕竟很久没有长距离骑行了。

漫无目的的骑车,着实无聊。现在方向往西,各个打卡点开始在脑海中浮现,哪些地点在我的骑行范围之内。首先想到了天马射电望远镜,看好路线,去那里看看。

天马射电天文望远镜
上海市松江区九江公路1703号

途径佘天昆公路,是一条很适合骑行的公路。不是主干道,没有什么红绿灯,来往车辆比较少,虽然路面不宽,但平实的路面不颠簸。

到天马射电望远镜附近,围着望远镜寻找最佳机位。每路过一种可能,停下开拍照,再找下一个机位并拍照。围着望远镜顺时针转了一圈,最终找到了最佳机位,拍照打卡。

自行车充当模特:

广角镜头下的射电望远镜:

里程37.07km
运动均速20.14km/h
全程均速14.31km/h
最快速度28.41km/h
运动时间1:50:27
全程时间2:35:28
累计上升108m
累计下降113m
最大心率148bpm
平均心率132bpm
最大踏频127rpm
平均踏频76rpm

共骑行37.07公里,总用时2小时35分钟,运动时间1小时50分钟,时间和里程在计划之内。

虽然这次心率数据正常,但是明显感到心跳的厉害。比起年前体能下降不少,日后还得慢慢恢复。多做有氧骑行,保持好心率。

  •  

家庭数据中心系列 优化网站加载速度:通过 Cloudflare Zaraz 实现第三方脚本的云端加载及管理

家庭数据中心系列 优化网站加载速度:通过 Cloudflare Zaraz 实现第三方脚本的云端加载及管理 无敌的个人博客 tangwudi

1 前言 在互联网时代,网页加载速度已经成为影响用户体验和SEO排名的关键因素。而其中,TBT(Total Blocking Time,完全阻塞时间)则是衡量网页性能的一个重要指标。 TBT 衡量的是浏览器在加载网页时,因脚本执行而无法响应用户交互的时间。显而易见,影响 TBT 的主要因素就是网页中的 JavaScript 脚本:浏览器需要加载并执行所有 JS 脚本,才能完成页面的渲染和其他内容的加载(CSS也影响渲染,只是相对JS来说影响得小一些)。因此,网页上脚本(和CSS)的数量和复杂性越高,加载完成的时间就越有可能受到影响。 在下图中,TBT时间超过710毫秒,直接导致谷歌的PageSpeed Insights评分大福下降: 而当TBT时间少的时候,评分则大幅上升,下图中,在其他几个参数都与上图中差不多的情况下,TBT大幅降低到90毫秒后,评分直接提高了到了80,可见在谷歌眼中TB […]

<p>The post 家庭数据中心系列 优化网站加载速度:通过 Cloudflare Zaraz 实现第三方脚本的云端加载及管理 first appeared on 无敌的个人博客.</p>

  •  

家庭数据中心系列 构建高效且安全的随机图片API:Cloudflare Worker + R2 + KV 实战指南

家庭数据中心系列 构建高效且安全的随机图片API:Cloudflare Worker + R2 + KV 实战指南 无敌的个人博客 tangwudi

1 前言 原本没想写这篇文章,之所以忽然插队来写,是因为我忽然对”纯手动定期更换博客的背景图片”这种行为有点倦怠了~。 我博客的背景图片一直是我定期手动更换的(图片存放在cloudflare R2上)。本来嘛,1-2个月更换一次背景图片倒也不算麻烦,但是随着用过的背景图片越来越多,之前用过的完全弃之不用我也有点舍不得(毕竟是我精挑细选出来的),但是让我主动的手动更换成以前用过的背景图片,主观上我又有点不情愿。 咋办呢?要不干脆搞个随机背景,这样一来就像皇帝侍寝翻牌子,翻到那个就是哪个,我也不用纠结了~。想到就开始做,刚好也可以水一篇文章~。 2 第二部分:实现方式的选择 在搭建随机图片 API 时,可以根据”是否需要 VPS”作为判断条件来选择适合自己实际条件的方案: 1. 需要 VPS 的方案(适合有 VPS 或使用 Cloudflare […]

<p>The post 家庭数据中心系列 构建高效且安全的随机图片API:Cloudflare Worker + R2 + KV 实战指南 first appeared on 无敌的个人博客.</p>

  •  

2025年1月阅读书摘

✇Dennis
作者Domon

1月阅读记录

  • 《置身事内》Done
  • 《控糖革命》Done
  • 《生活在低处》Done
  • 《廉价日本》Done
  • 《素食者》Done
  • 《如何带着三文鱼旅行》10%

1月阅读书摘

置身事内:中国政府与经济发展

  • 2025年1月阅读书摘
  • 书名: 置身事内:中国政府与经济发展
  • 作者: 兰小欢
  • 简介: 本书是复旦大学经济学院教授兰小欢多年教学与研究内容的凝练,将经济学原理与中国经济发展的实践有机融合,以地方政府投融资为主线,深入浅出地论述了中国经济的发展,广泛采纳各领域学者全新研究成果。全书分上下两篇。上篇解释微观机制,包括地方政府的基本事务、收支、土地融资和开发、投资和债务等;下篇解释这些微观行为与宏观现象的联系,包括城市化和工业化、房价、地区差异、债务风险、国内经济结构失衡、国际贸易冲突等。最后一章通过对中国政治经济体系的论述,作者简明地刻画了地方政府进行经济治理的基本方式,指出中国政府通过深度介入工业化和城市化的进程,在发展经济的同时逐步推动了市场机制的建立和完善。
  • 出版时间 2021-08-01 00:00:00
  • ISBN: 9787208171336
  • 分类: 经济理财-财经
  • 出版社: 上海人民出版社

前言 从了解现状开始

  • 📌 本书注重描述现实,注重解释“是什么”和“为什么”。当不可避免涉及“怎么办”的时候,则注重解释当下正在实施的政策和改革。对读者来说,了解政府认为应该怎么办,比了解“我”认为应该怎么办,重要得多。

第一节 分税制改革

  • 📌 中央重大政策出台的背后,也要经过很多轮的征求意见、协商、修改,否则很难落地。成功的政策背后是成功的协商和妥协,而不是机械的命令与执行,所以理解利益冲突,理解协调和解决机制,是理解政策的基础。

第一节 京东方与政府投资

  • 📌 创新当然是经济持续增长的源动力,但创新是买不来的,只能靠自己做。创新必须基于知识和经验的积累,所以只能自己动手“边做边学”,否则永远也学不会。只有自己动手,不是靠简单的模仿和引进,才能真正明白技术原理,才能和产业链上的厂商深入交流,才能学会修改设计以适应本土客户的要求,也才能逐步实现自主创新。若单纯依靠进口或引进,没有自己设厂和学习的机会,那本国的技术就难以进步,很多关键技术都会受制于人,这样的国际分工和贸易并不利于长期经济增长。

第三节 经济发展与贫富差距

  • 📌 这种现象被称为“隧道效应”(tunnel effect),形容隧道中两条车道一动一静时,静的那条的焦虑和难耐。(40)

第一节 债务与经济衰退

  • 📌 一个部门的负债对应着另一个部门的资产。债务累积或“加杠杆”的过程,就是人与人之间商业往来增加的过程,会推动经济繁荣。而债务紧缩或“去杠杆”也就是商业活动减少的过程,会带来经济衰退。举例来说,若房价下跌,老百姓感觉变穷了,就会勒紧裤腰带、压缩消费。东西卖不出去,企业收入减少,就难以还债,债务负担过高的企业就会破产,银行会出现坏账,压缩贷款,哪怕好企业的日子也更紧了。这个过程中物价和工资会下跌(通货紧缩),而欠的钱会因为物价下跌变得更值钱了,实际债务负担就更重了。

结束语

  • 2025年1月阅读书摘

    📌 我出生于1980年,长在内蒙古的边陲小镇,在北京、大连、上海、深圳、武汉都长期待过,除了在美国读书和生活的六七年,没离开过这片滚滚红尘。虽然见过的问题和麻烦可以再写几本书,但经历和见闻让我对中国悲观不起来。我可以用很多理论来分析和阐述这种乐观,但从根本上讲,我的乐观并不需要这些头头是道的逻辑支撑,它就是一种朴素的信念:相信中国会更好。这种信念不是源于学术训练,而是源于司马迁、杜甫、苏轼,源于“一条大河波浪宽”,源于对中国人勤奋实干的钦佩。它影响了我看待问题的角度和处理信息的方式,我接受这种局限性,没有改变的打算。

  • 📌 我是个经济学家,基于专业训练的朴素信念也有一个:生活过得好一点,比大多数宏伟更宏伟。

廉价日本:什么都涨为何薪资不涨?

  • 2025年1月阅读书摘
  • 书名: 廉价日本:什么都涨为何薪资不涨?
  • 作者: 【日】中藤玲
  • 简介: 曾经以“高品质”与“高价格”为标签的日本,正逐渐成为物价和工资都“便宜”的国家,也让“日本的工资在近30年间完全没有增长”成为不容忽视的现实。
    日经记者中藤玲从物价、人才、房地产等各个方面进行了采访,从采访和调查中传达日本现状,并针对企业对低价的成因、影响的看法,学者提出的见解,消费者遭遇的两难困境,进行深入且详实的探究。
    便宜当然好。若便宜还有好货,那就更好了!但当企业唯一的促销手段只剩下降价,是否也在促使整个社会陷入恶性循环呢?打工人、消费者,甚至日本将会面临什么样的困境呢?
  • 出版时间 2024-05-24 00:00:00
  • ISBN:
  • 分类: 经济理财-财经
  • 出版社:

前言 正视日本的廉价

  • 📌 从民众的角度来看,廉价无疑会让“生活更轻松”,但是站在供给方的立场上看,则会导致收益无法提升。最终结果是薪资原地踏步、消费难以带动、需求增长无力,社会经济陷入恶性循环之中。
  •  

2024年度书籍回顾

✇Dennis
作者Domon

总结

2024年度书籍回顾

24 年感觉同样是没有多少阅读的时间,微信读书里显示的是 145 个小时,今年还在图书馆借了一些实体书来读,同时也把吃灰的 kindle 拿来当作小说阅读器来使用。但肯定是不会像去年那样达到了 160 小时的水平。在跑步计划没有大变化的今年,这样的结果至少可以表明,中午和晚上的一些时间都用来耍手机了。

另外一个值得深思的问题就是,看长文章(10-20 分钟)的频次在骤降,没有看过几篇,能够有些许记忆的就更少了。原因或许是两个方面,首先肯定是自己的耐心在降低,经常是看了一小会就切出去玩其他 App 了。另外一个方面,便是优质内容的萎缩,或许也没有萎缩,而是我的信息源出了问题。在我看来,前者的问题要比后者好解决,根源性,结构性的问题才更加让人难受。

还有一个问题需要解答,在 AI 井喷的 2024 年,它对于我的阅读流有没有什么改进?坦率的说,没有。我唯一用的 AI 相关的功能,都是和搜索有关的,鲜少有和 ta 对谈书籍内容。更别说串联不同书籍的内容,用探讨式的对话去聊某个 topic,我都没有尝试过。这一点多少让我感到一些失望,当然不是对于 AI 工具的,而是针对我自己的。手头上有了那么多锤子,但是我却找不到钉子了

希望 2025 年,大家,当然也包括我自己能够沉下心来多看一点书。以前可能在书里想要探求某个答案,现在还是重新专注回「看」和「内容」上吧。在嘈杂的世界里,能够用文字和想象力构造出另一个世界,已经足够浪漫了。

数据

总览

2024年度书籍回顾

去年就吐槽过微信读书的年报,说它做的很敷衍,没有前两年用心。得,今年直接摆烂,连个年报都没了,变成了食之无味的「阅历」。

10 月份阅读时常骤减,似乎是开始用手机玩一些游戏,记得有个周末第一次接触到「小丑牌」,实实在在的在床上打了一天。这样说,我好像变成了在文章里看到的那群人,自控力极差,稍有不慎就坠入手机的黑洞。

总体上来看下半年看书时间是低于上半年的,有一部分工作的原因,但更多的情况是拿起手机,还是优先的选择了其他 App,即使两台手机的 Dock 上都有微信读书。

内容上还是以小说,散文,社科(非虚构)三分了天下,其余的空隙被一点点工具书所填满。这一点也是 2025 年需要优化的方向,从文学往工具书上匀一点点。

书籍推荐(排名不分先后)

  • 《局外人》:在熟悉的世界里当个局外人
  • 《房思琪的初恋乐园》:房思琪的痛,我只敢翻开这一次。
  • 《夜莺与玫瑰》:王尔德的爱情童话是成年人的挽歌。
  • 《推拿》:他们和我们一样,认真的活着。他们看不见我们,我们也难以看见他们。
  • 《冬牧场》:地窖里的羊粪,封存了冬牧场的寒冷,当然也包括时间。
  • 《最后的耍猴人》:时代的拐点,手艺人的末路。
  • 《东京八平米》:用四张榻榻米,过《完美的日子》。
  • 《我用中文做了场梦》:亚历的梦是我们视而不见的生活。
  • 《我的二本学生》:写作是一盏明灯,照亮了平凡的普通学生。
  • 《世上为什么要有图书馆》:理想主义者的胜利,即便是暂时的,它的意义也十分重大。
  • 《银河系边缘的小失常》:如果可能,请让大炮再发射我一次。

书籍列表(按读完时间排序)

  • 《东京八平米》
  • 《房思琪的初恋乐园》
  • 《那些忧伤的年轻人》
  • 《春潮》
  • 《我们正年轻:百年青春影像志》
  • 《时间贫困 : 如何利用时间,决定了我们是谁》
  • 《冬牧场》
  • 《打造第二大脑》
  • 《最后的耍猴人》
  • 《菊次郎与佐纪》
  • 《追光聚焦·深圳特区报 30 年影像选萃》
  • 《手机大脑》
  • 《每周工作 4 小时》
  • 《阿根廷婆婆》
  • 《小而美》
  • 《推拿》
  • 《你想活出怎样的人生》
  • 《跑者脑力训练》
  • 《赶时间的人》
  • 《爱吃沙拉的狮子》
  • 《银河系边缘的小失常》
  • 《鱼翅与花椒》
  • 《How bad do you want it》
  • 《夜莺与玫瑰》
  • 《白色绵羊里的黑色绵羊》
  • 《遇见未知的自己》
  • 《记一忘三二》
  • 《我的二本学生》
  • 《世上为什么要有图书馆》
  • 《局外人》
  • 《我在印度的 701 天》
  • 《想成为神的巴士司机》
  • 《柳林木风》
  • 《豆子,芝麻,茶》
  • 《学习之道》
  • 《李光耀观天下》
  • 《我用中文做了场梦》
  • 《一生之敌》
  • 《为你的生活写作》
  • 《李诞脱口秀工作手册》
  • 《生活在低处》

月份阅读书摘

往年回顾

  •  

家庭数据中心系列 Roxy-WI部署与 HAProxy 实战:图形化管理的全新体验

家庭数据中心系列 Roxy-WI部署与 HAProxy 实战:图形化管理的全新体验 无敌的个人博客 tangwudi

1 前言 在日常使用中,我们常接触到一些以配置文件方式管理的优秀软件,例如 HAProxy、Nginx、 Apache和Keepalived。这些软件以其高性能和灵活性闻名,但它们的配置文件通常需要直接使用文本编辑器进行修改,对于那些熟悉这些软件配置文件格式的技术人员来说,使用文本编辑器直接编辑配置文件是一种高效且习惯性的操作方式。 然而,对于更习惯于图形化界面的用户来说,这种纯文本的操作方式可能显得复杂甚至令人望而却步。为了弥补这一点,社区中出现了许多提供图形界面(GUI)管理功能的项目,这些工具大大降低了操作门槛,使更多人能够轻松地使用这些强大的工具,例如,我在之前的一篇文章中曾介绍过一个为 Nginx 提供图形化管理功能的项目:nginxWebUI(参看文章:docker系列 使用docker基于nginxWebUI搭建图形化的nginx),就是专门用于nginx图形化管理的。 而今 […]

<p>The post 家庭数据中心系列 Roxy-WI部署与 HAProxy 实战:图形化管理的全新体验 first appeared on 无敌的个人博客.</p>

  •  

马上过农历新年了,祝大家农历新年”尽量”快乐!同时也给自己放个假吧。

马上过农历新年了,祝大家农历新年”尽量”快乐!同时也给自己放个假吧。 无敌的个人博客 tangwudi

明天就是除夕了,提前祝大家新年尽量快乐,至于为什么说”尽量”,那是因为这年头,普通人未必快乐得起来,所以只能”尽量”了。 我呢,也准备给自己放个假,好好休息一下,毕竟保持每周一更(这几个月撑不住了才开始每周一更,之前可是每周2-3更,更吓人)还是很累人的,也正好让大脑休息一下,放空一下,2月10号恢复更新,敬请期待。

<p>The post 马上过农历新年了,祝大家农历新年”尽量”快乐!同时也给自己放个假吧。 first appeared on 无敌的个人博客.</p>

  •  

家庭数据中心系列 HAProxy 实战教程:多场景部署与常用功能配置解析

家庭数据中心系列 HAProxy 实战教程:多场景部署与常用功能配置解析 无敌的个人博客 tangwudi

前言 如之前一篇文章末尾所说(参见文章:docker系列 Traefik文件动态配置实战:本地网络负载均衡的高效实现),我对Traefik来实现家庭数据中心内网里主博客站点和备份博客站点的负载均衡(热备)还是不太满意,考虑了一下,还是准备尝试一下HAProxy,毕竟这才是传统的对(非docker环境、非k8s环境的)应用进行负载均衡的专业解决方案:Traefik设计的初衷就是为微服务和容器化环境设计的,让它来干传统的负载均衡本来就有点水土不服。当然,最关键是又可以水一篇文章~。 注:阅读本文需要对传统负载均衡(另一个称呼为”应用交付”)有一定了解,因为需要该领域的很多基础知识,否则阅读起来可能会有一定不适(头晕、瞌睡等副作用~)。 HAproxy介绍 HAProxy 是一款高性能、可靠的开源负载均衡器和反向代理软件,广泛应用于 Web 服务、数据库和其他高并发场景。 […]

<p>The post 家庭数据中心系列 HAProxy 实战教程:多场景部署与常用功能配置解析 first appeared on 无敌的个人博客.</p>

  •  

Koopa IR 人话版

切片 Slice

slice 是存储一系列元素的数组。具体来说,koopa_raw_slice_t 是一个结构体,用于表示一个元素数组及其长度。其定义如下:

typedef struct {
  // 数组指针
  const void **buffer;
  // 数组长度
  uint32_t len;
  // 数组中元素的类型
  koopa_raw_slice_item_kind_t kind;
} koopa_raw_slice_t;
  • buffer:指向元素数组的指针。数组中的每个元素都是 const void * 类型,这意味着它可以指向任何类型的对象。
  • len:数组的长度,即数组中元素的数量。
  • kind:数组中元素的类型,用 koopa_raw_slice_item_kind_t 枚举类型表示,可以是类型、函数、基本块或值等。

基本块 Basic Block

basic block 就是一系列的指令集合,在其内逻辑流不会发生跳转。

需要注意的是,基本块的结尾必须是 brjumpret 指令其中之一 (并且,这些指令只能出现在基本块的结尾)。也就是说,即使两个基本块是相邻的,例如上述程序的 %else 基本块和 %end 基本块,如果你想表达执行完前者之后执行后者的语义,你也必须在前者基本块的结尾添加一条目标为后者的 jump 指令。这点和汇编语言中 label 的概念有所不同。

比如一段代码:

int main() {
  int b = 1;
  if (b == 1) {
    return 1;
  }
  else {
    return 2;
  }
}

其被翻译为如下的 KoopaIR:

fun @main(): i32 {
%main_entry:
	@b_2 = alloc i32
	store 1, @b_2
	%0 = load @b_2
	%1 = eq %0, 1
	br %1, %then_0, %else_0
%then_0:
	ret 1
%jump_0:
	jump %end_0
%else_0:
	ret 2
%jump_1:
	jump %end_0
%end_0:
	ret 0
}

这里,每个标号分开的区域就是一个基本块。

value

value 对应 koopa_raw_value_t 类型,表示指向一个值的指针,多数情况下,你可以认为它是一个指令的相关信息,因为我们知道,在 KoopaIR 中是静态单赋值,它总是类似下面这种只赋值一次的指令:

@b_2 = alloc i32
store 1, @b_2
@c_2 = alloc i32
store 2, @c_2
@d_2 = alloc i32
store 3, @d_2
@e_2 = alloc i32
store 4, @e_2
%0 = load @b_2
%1 = load @c_2
%2 = add %0, %1
%3 = load @d_2
%4 = add %2, %3
%5 = load @e_2
%6 = add %4, %5
ret %6

所以,这里你会发现,大多数指令就是一个 ,他们总是引用了一些别的值,做了一些事情。

比如,%0 = load @b_2,就会引用两个值,一个是 @b_2,另一个是 %0

这些引用的值记作这个 valuedata,根据操作的不同,其会有不同的字段,比如对于 load 指令,你需要如下访问到他的 data:

value->kind.data.load

它会有两个属性:

  • src:代表被加载的值
  • dest:代表存放加载结果的值

这两者类型也都为 koopa_raw_value_t

value 所谓的这个值也可以是代表 KoopaIR 中用到的一些“东西”,如 interger 代表一个数,又如 aggregate 代表一个初始化列表。其的定义链如下:

typedef const koopa_raw_value_data_t *koopa_raw_value_t;
typedef struct koopa_raw_value_data koopa_raw_value_data_t;

所以,对其解引用后就会得到 koopa_raw_value_data 类型,其定义如下:

struct koopa_raw_value_data {
  // 值的静态类型
  koopa_raw_type_t ty;
  // 值的名称
  const char *name;
  // 值被哪些值使用
  koopa_raw_slice_t used_by;
  // 值的具体种类,代表值的动态行为
  koopa_raw_value_kind_t kind;
};
  • ty:值的类型信息,用于描述值的静态类型,对应 koopa_raw_type_t 类型,一个值可以是 int32pointerfunction 等。
  • name:值的名称,如 @a_0@main 等,只对 funcalloc 指令有意义。
  • used_by:值被哪些值使用,对应 koopa_raw_slice_t 类型,表示值被哪些指令使用。
  • kind:值的类型和依赖关系,用于描述值的动态行为,对应 koopa_raw_value_kind_t 类型,表示指令的类型,如 integeraggregate 等。
typedef struct {
  koopa_raw_value_tag_t tag;
  union {
    koopa_raw_integer_t integer;
    koopa_raw_aggregate_t aggregate;
    koopa_raw_func_arg_ref_t func_arg_ref;
    koopa_raw_block_arg_ref_t block_arg_ref;
    koopa_raw_global_alloc_t global_alloc;
    koopa_raw_load_t load;
    koopa_raw_store_t store;
    koopa_raw_get_ptr_t get_ptr;
    koopa_raw_get_elem_ptr_t get_elem_ptr;
    koopa_raw_binary_t binary;
    koopa_raw_branch_t branch;
    koopa_raw_jump_t jump;
    koopa_raw_call_t call;
    koopa_raw_return_t ret;
  } data;
} koopa_raw_value_kind_t;
  • tag:值的种类,对应 koopa_raw_value_tag_t 枚举类型。
  • data:值的具体数据,根据 tag 的不同,有不同的结构体。如上文所说的 load 指令,其 tagKOOPA_RVT_LOAD,其 datakoopa_raw_load_t 类型,而 koopa_raw_load_t 又会有 load 指令所需的 srcdest 两个指令字面字段。

类型 type

类型 type 对应 koopa_raw_type_t 类型,表示指向一个 koopa_raw_type_kind_t 类型的指针,用于描述值的静态类型,一个值可以是 int32pointerfunction 等。

typedef const koopa_raw_type_kind_t *koopa_raw_type_t;
typedef struct koopa_raw_type_kind {
  koopa_raw_type_tag_t tag;
  union {
    struct {
      const struct koopa_raw_type_kind *base;
      size_t len;
    } array;
    struct {
      const struct koopa_raw_type_kind *base;
    } pointer;
    struct {
      koopa_raw_slice_t params;
      const struct koopa_raw_type_kind *ret;
    } function;
  } data;
} koopa_raw_type_kind_t;
  • tag:类型标签,表示类型的种类,对应 koopa_raw_type_tag_t 枚举类型。
  • data:类型数据,根据 tag 的不同,有不同的结构体,也可能没有数据只是分配了空间。
typedef enum {
  // 32 位整数
  KOOPA_RTT_INT32,
  // 空类型
  KOOPA_RTT_UNIT,
  // 数组
  KOOPA_RTT_ARRAY,
  // 指针
  KOOPA_RTT_POINTER,
  // 函数
  KOOPA_RTT_FUNCTION,
} koopa_raw_type_tag_t;

初始化列表 aggregate

一个数组:

int a[2][2] = {1, 2, 3, 4};

会被 KoopaIR 翻译为:

global @a_0 = alloc [[i32, 2], 2], {{1, 2}, {3, 4}}

那么 {{1, 2}, {3, 4}} 就是一个 aggregate,它的元素是两个 aggregate,即 {1, 2}{3, 4},其中每个 aggregate 的元素是两个 integer

指针 get_ptr / get_elem_ptr

若一个 valuekind.tagKOOPA_RVT_GET_PTRKOOPA_RVT_GET_ELEM_PTR,则其 kind.datakoopa_raw_get_ptr_tkoopa_raw_get_elem_ptr_t,里面存放了值的依赖关系:

typedef struct {
  // 源
  koopa_raw_value_t src;
  // 索引
  koopa_raw_value_t index;
} koopa_raw_get_ptr_t;

typedef struct {
  // 源
  koopa_raw_value_t src;
  // 索引
  koopa_raw_value_t index;
} koopa_raw_get_elem_ptr_t;

举例说明,对于指令:

%0 = get_ptr @a_0, 1
%0 = get_elem_ptr @a_0, 1
  • @a_0src
  • 1index

那么,有时候我们还会想要获得指针所指向范围的大小,那么我们就可以使用 get_alloc_size 函数。

int get_alloc_size(const koopa_raw_type_t ty) {
    switch (ty->tag) {
        // 空类型不占用空间
    case KOOPA_RTT_UNIT:
        return 0;
        // 函数类型不占用空间
    case KOOPA_RTT_FUNCTION:
        return 0;
        // 32 位整数占用 4 字节
    case KOOPA_RTT_INT32:
        return 4;
        // 指针类型占用 4 字节
    case KOOPA_RTT_POINTER:
        return 4;
        // 数组类型占用空间为数组长度乘以数组元素类型占用空间
    case KOOPA_RTT_ARRAY:
        return ty->data.array.len * get_alloc_size(ty->data.array.base);
    default:
        printf("Invalid type: %s\n", koopaRawTypeTagToString(ty->tag).c_str());
        assert(false);
    }
}
  • 对于一个 value.kind.tag = KOOPA_RVT_GET_PTRvalue,我们使用 get_alloc_size(value.kind.data.get_ptr.src->ty->data.pointer.base) 来获得指针的步长
  • 对于一个 value.kind.tag = KOOPA_RVT_GET_ELEM_PTRvalue,我们使用 get_alloc_size(value.kind.data.get_elem_ptr.src->ty->data.pointer.base) 来获得指针的步长

一时间也想不到什么很好的说法来通俗的说明...

💾

  •  

编译原理大作业的奇妙测试点们

Lv3

Riscv

发现 27_complex_binary 寄存器超过使用限制了,得复用寄存器才行,不能每个中间结果都开一个新的。

但其实无所谓,Lv4 会将寄存器完全改为使用栈来存储,所以不用 care,写完 Lv4 自然就过了。

Lv4

Koopa

在测试点 18_multiple_returns2 中,存在多条 return 语句,形如:

int main(){
    return 0; return 1;
}

我们应当只处理到第一个 return 语句后,就停止处理(或者提前看后续 Lv6 的实现办法)

Riscv

发现过不去 21_decl_after_decl3 测试点,遂检查了一下代码,发现是不能仅仅只在 loadstore 指令中重置寄存器计数,对于 binary 指令,也需要重置寄存器计数。

来自写完之后的补充:后来就是在每次 void visit(const koopa_raw_value_t& value) 时,都会先重置寄存器计数了。

Lv6

Koopa

惨不忍睹:

Lv5:07_empty_ block1 09_summary1 11_ret_in_block2 14_ret_in_block3

Lv6:13_branch2

这些点都是因为最后一条语句的处理问题。

需要注意的是,基本块的结尾必须是 br,jump 或 ret 指令其中之一 (并且,这些指令只能出现在基本块的结尾)。也就是说,即使两个基本块是相邻的,例如上述程序的 % else 基本块和 % end 基本块,如果你想表达执行完前者之后执行后者的语义,你也必须在前者基本块的结尾添加一条目标为后者的 jump 指令。这点和汇编语言中 label 的概念有所不同。

值得一提的是,如下 Koopa IR 是合法的:

%then_0:
	ret 1
%return_end_0:
	jump %end_0

所以,我们得到一个弱智但有效的做法:给所有 ret 语句后都添加一个新的标签,保证每个 print 函数的末尾不是一条 br / jump / ret,就可以了。

另外,在同一函数体内出现多个同名 alloc 指令是不合法的。

Lv6:14_else_match2,检查是否正确处理了 if else 的 label。

Riscv

这里发现始终过不去 11_logical1 测试点,先 AEWA

首先是 AE,发现是我在处理 12 位立即数偏置的时候,错误地使用了 reg(sp) 的形式。

实际上偏移量不是指做成 t1(sp),而是先做 t1 = bias; t1 = sp + t1,然后再 lw t0, (t1)。对 sw 指令同理。

接着是 WA,发现是我在处理 12 位立即数的时候,寄存器分配出现了问题,我手动调整了 context.stack_used 的值为临界值 2040 后,发现是我原先对于 load 处的寄存器分配有问题,我使用了 cur_reg 而不是 new_reg,这会导致如果 load 指令目标偏置超过 12 位立即数限制,那么在 riscv._lw(reg, "sp", context.stack_map[load.src]); 中,会隐式地发现偏置大于 2048 并再次分配 t0 来存储偏置,从而造成一句 lw t0, (t0) 的指令。修改为 new_reg 后,即可 AC。

Lv7

Koopa

关于短路求值的一个测试点:需要注意一下,对于逻辑表达式,其返回值一定是一个布尔类型,所以你需要考虑如下的测试点,其不能被用常量传播直接求出:

int main() {
    int x = 2;
    putint(0||x);
	putch(10);
}

这个点的输出应当是 1,而不是 2。这个测试点甚至在全部的本地/在线测试中都不存在类似的,导致我直到通过了所有测试开始逐行加注释改善代码质量的时候才发现。

Lv8

Koopa

发现在 16_summary1 测试点上 AE 了,仔细检查尝试,发现了问题,即在不同函数体内可能声明同样的变量:

int f() {
  int a = 1;
}

int g() {
  int a = 2;
}

这意味着你需要在每次进入函数体时清空 is_symbol_allocated,在两个函数体内各自生成一次 alloc 指令。

Lv9

Koopa

你需要考虑形如 {} 的初始化,这个东西只要出现,就至少会初始化掉一个步长。

如果你发现在 22_arr_init1 测试点 WA / AE,那么就很有可能是此原因导致的,你可以本地测试如下测试点:

const int buf[3][3][1] = { 1,{},2 };

这个测试点的输出应当是

alloc [[[i32, 1], 3], 3], {{{1}, {0}, {2}}, {{0}, {0}, {0}}, {{0}, {0}, {0}}}

如果你仅仅在处理第二个 {} 的时候检查对齐,而不考虑其 init_values 为空,一上来就对齐导致完全没有补 0 进而被直接跳过,那么很容易得到:

alloc [[[i32, 1], 3], 3], {{{1}, {2}, {0}}, {{0}, {0}, {0}}, {{0}, {0}, {0}}}

另外一个测试点是:

const int buf[2][3] = {{}, 1};

这个测试点输出应当是:

global @buf_0 = alloc [[i32, 3], 2], {{0, 0, 0}, {1, 0, 0}}

一个数组表达式的 LVal 出现的位置是不确定的,其既可以作为值,也可以作为指针参数去调用函数,我们必须判断输出它时究竟是哪种情况,进而输出不同的 Koopa IR。

而判断的方法,就是看我们调用他们所使用的维度个数,相对于我们初始化他们时的维度个数的关系。

  • 若调用时使用的维度个数等于初始化时知道的维度个数,则其为值,我们最后补上的应当是一句 load 指令
  • 若调用时使用的维度个数小于初始化时知道的维度个数,则其为指针,我们最后补上的应当是一句 getelemptr 指令

注:对于指针的情况,你要记录他的维度为表达式 + 1。

Riscv

一个测试点:

int main() {
  int b[2][3] = { 1, 2, 3, 4 };
  putint(b[0][1]);
  putch(10);
  return 0;
}

测试输出是 4 还是 2?如果是 4,那么说明你对于 getelemptr 指令的翻译有问题。

这是因为,getelemptr %0, 1 指令的翻译并不一定是 +4,而是也需要像之前一样,使用 get_alloc_size 函数来计算 %0 的偏移步长。

如果你本地所有测试、远程的从 Koopa 也能过,但是 Riscv 差一个点,那么可能是存在长跳转的问题,即跳转范围超过了 bnezbeqz 的跳转范围,所以需要使用 jump 指令。

只需要修改一下这两条的指令的实现,在 bnezbeqz 旁边添加新的标号,然后将原先的短跳转转为长跳转 jump 指令即可。

性能测试

发现是没有在 main.cpp 中允许 -perf 的模式(其实就是和 -riscv 一样),添加一下就行。

💾

  •  

程序优化

代码优化概述

代码优化的原则:

  • 保证安全(确保语义 / 可观察行为不变)
  • 提高效率(二八法则:80% 时间在 20% 代码上,主要优化这 20% 代码)

优化方式:

  • 算法设计阶段
  • 编译阶段
  • 语义分析:根据静态检查,优化 源程序
  • 中间代码生成:机器无关优化
  • 目标代码生成:机器有关优化
  • 链接时刻优化

代码优化器的结构

42345

代码优化的范围

  • 局部优化:基本块内(即标号分割的块)
  • 区域性优化:若干个基本块构成的区域
  • 全局优化:一个过程内所有基本块
  • 过程间优化:一个程序所有过程及其基本块

代码优化的常用方法

  • 公共子表达式消除
  • 复写传播(消除 a=b)
  • 死代码消除
  • 常量折叠 / 常量传播:直接推导出表达式的值是否为常量,若为常量则直接用其替换该表达式
  • 代码外提(循环中不变量外提)
    • 循环不变式:不管循环执⾏多少次都得到相同结果的表达式
  • 强度消减:减少操作次数、操作强度(如将二的幂次乘除法转换为移位操作)
    • 归纳变量:每次循环都增加恒定常数的变量
    • 如果一组归纳变量变化步调一致,考虑消除一些
  • 数据流分析

数据流分析

数据流分析是一种静态代码分析技术,用于在程序编译时推导出程序各部分可能的行为。它通过分析变量和表达式在程序中的流动情况,帮助我们理解程序在不同点上的状态。

一些基本概念:

  1. 基本块(Basic Block):一个基本块是一段没有分支和跳转的连续代码。换句话说,它是一个入口和一个出口之间的代码段,只有在入口处进入,并且在出口处离开。
  2. 控制流图(Control Flow Graph, CFG):控制流图是由基本块作为节点,控制流作为边构成的有向图。它展示了程序执行的所有可能路径。

通过数据流方程,计算每个基本块的入口和出口状态。常见的数据流方程包括:

  • 到达定义(Reaching Definitions):哪些变量定义可以到达这个基本块。
  • 活跃变量(Live Variables):哪些变量在基本块之后仍然需要使用。
  • 可用表达式(Available Expressions):哪些表达式在基本块入口处已经计算过且没有被修改。

数据流抽象

基本概念:

  • 程序点(program point):每条语句对应其前、后两个程序点
    • 基本块内两条语句 $s_1, s_2$,$s_1$ 后的程序点与 $s_2$ 前的程序点相同
  • 路径(path):程序点 $p_1, p_2, \ldots, p_n$ 构成的序列,对于任意 $1 \leq i < n$,必然有二者之一(人话就是他们连着):
    • 点 $p_i$ 和点 $p_{i+1}$ 是一条语句前、后的两个程序点
    • 点 $p_i$ 指向基本块的结尾,点 $p_{i+1}$ 指向该基本块后继的开头(连接不同基本块)

数据流分析推导

对于每个程序点 $p$:

  • 前向(forward)分析:以 $p$ 为终点的所有路径的集合的性质(人话就是顺着逻辑流走)
  • 后向(backward)分析:以 $p$ 为起点的所有路径的集合的性质(人话就是逆着逻辑流走)

前向分析模式

  • 数据流分析的域 $V$,交汇运算 $\wedge: V \times V \rightarrow V$,顶值 $T \in V$
  • 每个基本块 $B$ 的传递函数 $f_B: V \rightarrow V$ (从入口到出口)
  • 边界条件:$\text{OUT}[\text{ENTRY}] = \nu_{\text{ENTRY}}$
  • 初始值:$\text{OUT}[B] = T \quad (B \neq \text{ENTRY})$
  • 方程组:对任意 $B \neq \text{ENTRY}$,有 $$ \begin{aligned} &\text{IN}[B] = \bigwedge_{P \text{是} B \text{的前驱}} \text{OUT}[P] \ &\text{OUT}[B] = f_B(\text{IN}[B]) \end{aligned} $$

后向分析模式

  • 数据流分析的域 $V$,交汇运算 $\wedge: V \times V \rightarrow V$,顶值 $T \in V$(即不清楚值时的默认输入)
  • 每个基本块 $B$ 的传递函数 $f_B: V \rightarrow V$ (从出口到入口)
  • 边界条件:$\text{IN}[\text{EXIT}] = \nu_{\text{EXIT}}$
  • 初始值:$\text{IN}[B] = T \quad (B \neq \text{EXIT})$
  • 方程组:对任意 $B \neq \text{EXIT}$,有 $$ \begin{aligned} &\text{OUT}[B] = \bigwedge_{S \text{是} B \text{的后继}} \text{IN}[S] \ &\text{IN}[B] = f_B(\text{OUT}[B]) \end{aligned} $$

活跃变量分析

活跃变量:在程序点 $p$ 之后仍然需要使用的变量

  • 分析模式:后向分析模式
  • 基础定义:
    • $\text{def}_B$:基本块 $B$ 中定义的变量
    • $\text{use}_B$:基本块 $B$ 中使用的变量
  • 分析域 $V$:变量集
  • 交汇运算 $\land$: $$ O_1 \land O_2 = O_1 \cup O_2 $$ 即:在任意后继中活跃则认为是活跃的。
  • 顶值 $T$:$\varnothing$
  • 传递函数 $f_B$:$f_B(O) = (O - \text{def}_B) \cup \text{use}_B$
  • 方程组: $$ \begin{aligned} &\text{OUT[B]} = \bigcup_{\text{s是B的后继}} \text{IN[S]} \ &\text{IN}[B] = \text{OUT}[B] \cup \text{use}_B \end{aligned} $$

注意传递函数实际上是指令级一条套一条推得的:

$$ f_B(O) = f_{s_1}(f_{s_2}(f_{s_3}(O))) $$

所以如果你是直接根据块级别去做题的话,需要额外注意各条指令之间的依赖关系,判断到底是先用还是先定义。

比如说:

$$ \begin{aligned} &s_1: a = b * d \ &s_2: b = a - d \ \end{aligned} $$

这个基本块中,我们不仅定义了 $a$,还使用了 $a$,但是由于我们是先定义的,所以 $a$ 不在这个块最终输出的活跃变量中。

又比如:

$$ \begin{aligned} &s_1: a = a + 1 \ \end{aligned} $$

这个基本块中,我们也是既定义了 $a$,又使用了 $a$,但是仔细观察会发现我们是先使用的 $a$,再定义的 $a$,所以 $a$ 在这个块最终输出的活跃变量中。

后续分析同,不再赘述。

到达定值分析

到达定值(可达定义):在程序点 $p$ 处,变量 $v$ 的定值(即赋值语句,$v = \text{exp}$)可以到达 $p$

  • 分析模式:前向分析模式
  • 基础定义:
    • $\text{gen}_B$:基本块 $B$ 中生成定值的集合
    • $\text{kill}_B$:基本块 $B$ 中杀死定值的集合,即对于基本块中定值的 $v$,杀死所有其他对 $v$ 的定值
  • 分析域 $V$:变量集
  • 交汇运算 $\land$: $$ I_1 \land I_2 = I_1 \cup I_2 $$ 即:在任意前驱可达则认为是可达的。
  • 顶值 $T$:$\varnothing$
  • 传递函数 $f_B$:$f_B(I) = (I - \text{kill}_B) \cup \text{gen}_B$
  • 方程组: $$ \begin{aligned} &\text{IN}[B] = \bigwedge_{P \text{是} B \text{的前驱}} \text{OUT}[P] \ &\text{OUT}[B] = f_B(\text{IN}[B]) \end{aligned} $$

可用表达式分析

可用表达式:到达一个程序点的每条路径都对表达式 $E$ 求值,并且该表达式最近一次求值后其使用的变量没有被修改。

  • 分析模式:前向分析模式
  • 基础定义:
    • $\text{e_gen}_B$:基本块 $B$ 中生成的表达式的集合
    • $\text{e_kill}_B$:基本块 $B$ 中杀死的表达式的集合,若基本块中有语句 $s$ 对 $x$ 赋值,则杀死所有使用 $x$ 的表达式,如 $z = x + y$ 会杀死 $z + 1$,又如 $x = x + y$ 会杀死 $x + y$
  • 分析域 $V$:表达式集
  • 交汇运算 $\land$: $$ I_1 \land I_2 = I_1 \cap I_2 $$ 即:要求任意前驱中都要可用才认为可用
  • 顶值 $T$:全集
  • 传递函数 $f_B$:$f_B(I) = (I - \text{e_kill}_B) \cup \text{e_gen}_B$
  • 方程组: $$ \begin{aligned} &\text{IN}[B] = \bigwedge_{P \text{是} B \text{的前驱}} \text{OUT}[P] \ &\text{OUT}[B] = f_B(\text{IN}[B]) \end{aligned} $$

注意:

$$ \begin{aligned} &s_1: a = a + 1 \ \end{aligned} $$

这个基本块中,我们也是既计算了 $a + b$,又定值了 $a$,后来的定值杀死了前面的计算,所以 $\text{e_gen}_B$ 不包括 $a+1$,但是 $\text{e_kill}_B$ 包括 $a+1$(这样做能满足传递函数定义)。

总结

| 域 | 活跃变量 | 到达定值 | 可用表达式 | | -------- | ------------------------------------- | ------------------------------------- | ------------------------------------- | | 方向 | 后向 | 前向 | 前向 | | 传递函数 | $(O - \text{def}_B) \cup \text{use}_B$ | $(I - \text{kill}_B) \cup \text{gen}B$ | $(I - \text{e_kill}B) \cup \text{e_gen}B$ | | 边界条件 | $\text{IN}[\text{EXIT}] = \varnothing$ | $\text{OUT}[\text{ENTRY}] = \varnothing$ | $\text{OUT}[\text{ENTRY}] = \varnothing$ | | 交汇运算 | $\cup$ | $\cup$ | $\cap$ | | 方程组 | $\text{OUT}[B] = \bigcup{S, succ(B)} \text{IN}[S] \quad$ | $\text{IN}[B] = \bigcup{P, pred(B)} \text{OUT}[P] \quad$ | $\text{IN}[B] = \bigcap{P, pred(B)} \text{OUT}[P] \quad$ | | | $\text{IN}[B] = f_B(\text{OUT}[B])\quad$ | $\text{OUT}[B] = f_B(\text{IN}[B])\quad$ | $\text{OUT}[B] = f_B(\text{IN}[B])\quad$ | | 初始值 / 顶集 | $\text{IN}[B] = \varnothing$ | $\text{OUT}[B] = \varnothing$ | $\text{OUT}[B] = \text{全集}$ |

其中:

  • $B$ 表示基本块,$S$ 表示后继块,$P$ 表示前驱块
  • $\text{def}_B$ 表示在块 $B$ 中定义的变量集合
  • $\text{use}_B$ 表示在块 $B$ 中使用的变量集合
  • $\text{kill}_B$ 表示在块 $B$ 中被覆盖的定义集合
  • $\text{gen}_B$ 表示在块 $B$ 中生成的定义集合
  • $\text{e_kill}_B$ 表示在块 $B$ 中被覆盖的表达式集合
  • $\text{e_gen}_B$ 表示在块 $B$ 中生成的表达式集合

习题

31746

做数据流分析的结果:

63523

路径表达式

  • 有向图 $G = (V, E)$:其中 $V$ 是顶点集合,$E$ 是边的集合。
  • 路径表达式 (path expression):一个以 $E$ 为字母表的正则表达式 $R$,且 $R$ 识别的每个符号串都是图 $G$ 中的一条路径。

98139

基于路径表达式的数据流分析

  • 数据流分析的域 $V$,交汇运算 $\wedge : V \times V \to V$
  • 每个基本块 $B$ 的传递函数 $f_B : V \to V$
  • 用 $F(R) : V \to V$ 表示 $R$ 能识别的路径的数据流抽象

以前向分析为例

  • $F(\varepsilon)$ = 恒等函数
  • $F(e) = f_{h(e)}$,其中 $h(e)$ 是边 $e$ 的起点基本块
  • $F(R_1 \mid R_2) = F(R_1) \wedge F(R_2)$
  • $F(R_1 R_2) = F(R_2) \cdot F(R_1)$
  • $F(R_1^*) = \bigwedge_{i \geq 0} F(R_1)^i$,不过有时能找到更高效的算法

💾

  •  

目标代码生成

目标机模型

  • 类 RISC 计算机,按字节寻址,以 4 个字节为 1 个字(word)

  • 通用寄存器 $R_1, R_2, ⋯, R_n$

  • 使用如下机器指令,每条指令的长度为 8 字节:

    • $\text{LD} ; \text{dst}, \text{addr}$:把位置 $\text{addr}$ 上的值加载到位置 $\text{dst}$(load)

      $\text{LD} ; r_1, r_2$:寄存器到寄存器的拷贝

    • $\text{ST} ; x, r$:把寄存器 $r$ 中的值保存到位置 $x$(store)

    • $\text{OP} ; \text{dst}, \text{src}_1, \text{src}_2$:把位置 $\text{src}_1$ 和 $\text{src}_2$ 中的值运算后将结果放到位置 $\text{dst}$ 中(operation)

      $\text{OP}$ 是诸如 $\text{ADD}$ 或 $\text{SUB}$ 的运算符

    • $\text{BR} ; L$:控制流转向标号为 $L$ 的指令(branch)

    • $\text{Bcond} ; r, L$:对寄存器 $r$ 中的值进行测试,如果为真则转向标号 $L$(branch condition)

      $\text{cond}$ 是诸如 LTZ(判断是否小于 0)或 NEZ(判断是否不等于 0)的常见测试

目标机的寻址模式

  • contents(addr) 表示 addr 所代表的位置中的内容
  • lvalue(x) 表示分配给变量 x 的内存位置

| 位置形式 | 汇编表示 | 地址 | | -------------- | -------- | --------------------------- | | 变量名 | x | lvalue(x) | | 数组索引 | a(r) | lvalue(a) + contents(r) | | 直接常数 | #M | M | | 寄存器 | r | r | | 间接寄存器 | *r | contents(r) | | 索引 | M(r) | M + contents(r) | | 间接寄存器索引 | *M(r) | contents(M + contents(r)) |

target_machine_addressing_mode

进行栈式管理的目标代码

生成支持栈式存储管理的目标代码:

  • 生成过程调用和返回的目标代码序列
  • 将 IR 中的名字转换成为目标代码中的地址

简化调用 / 返回的三地址代码:

  • call callee
  • return

过程 callee (被调用者)的属性(编译时确定):

  • callee.codeArea:运行时代码区中 callee 的第一条指令的地址
  • callee.recordSizecallee 的一个活动记录的大小

过程的调用和返回

简化场景下的活动记录:

  • 只需考虑在活动记录中保存返回地址
  • 假设寄存器 SP 中维持一个指向栈顶的指针

调用指令序列

调用者

  • ST -4(SP), #here + 16:计算返回地址,当前指令地址加上 16(偏移掉 2 条指令,即当前 ST 和下一条 BR),地址是 4 字节的(32 位)
  • BR callee.codeArea:跳转到被调用者的代码

被调用者

  • SUB SP, SP, #callee.recordSize:为活动记录分配空间

返回指令序列

被调用者

  • ADD SP, SP, #callee.recordSize:释放活动记录
  • BR *-4(SP):跳转到返回地址

指令选择

控制流图

基础定义:

  1. 基本块(Basic Block):一个基本块是一段没有分支和跳转的连续代码。换句话说,它是一个入口和一个出口之间的代码段,只有在入口处进入,并且在出口处离开。

    具有线性结构,其中最后一条语句为跳转或者过程 / 函数返回 (br /jump/ret)。

  2. 控制流图(Control Flow Graph, CFG):控制流图是由基本块作为节点,控制流作为边构成的有向图。它展示了程序执行的所有可能路径。

    有向图,图中结点为基本块,边为控制流跳转。控制流只能从基本块的第一条指令进入。

示例代码:

n = 10; a = 1; b = 1;
while (!(n == 0)) {
    t = a + b; a = b; b = t;
    n = n - 1;
}
return a;

对应的控制流图:

62038

控制流图 + 三地址代码

三地址代码:控制流图的每个基本块内部为三地址代码。

  • 跳转指令的目标为基本块(而不是指令标号)。
  • 一种常见的 混合 IR
  • 上图 BB1 的指令并不完全是三地址形式,因为(BB2)和(BB3)都不是真实指令标号。

控制流图中的循环

循环的定义

  • 一个 结点集合 $L$
  • 存在一个 循环入口 (loop entry)结点,唯一的前驱可以在 $L$ 之外的结点
  • 每个结点都有到达入口结点的非空路径,且该路径都在 $L$ 中

30799

对应的控制流图中的循环:

  • 循环 1:${BB3}$
  • 循环 2:${BB6}$
  • 循环 3:${BB2, BB3, BB4}$(BB2 为入口结点)

划分基本块的算法

输入:三地址指令序列。

输出:基本块的列表。

方法:

  1. 确定 首指令 (leader,基本块的第一条指令):
    • 第一条三地址指令。
    • 任何一个条件或无条件跳转指令的 目标指令
    • 紧跟在一个条件或无条件跳转指令 之后的指令
  2. 确定基本块:每条首指令对应一个基本块:从首指令开始到下一个首指令。

21694

基于三地址跳转指令的流图:两个基本块 $B$ 和 $C$ 之间存在一条有向边当且仅当基本块 $C$ 的第一条指令可能在 $B$ 的最后一条指令之后执行。

  • 情况 1:$B$ 的结尾跳转到 $C$ 的开头。
  • 情况 2:$B$ 的结尾不是无条件跳转,且 $C$ 在原来的序列中紧跟 $B$ 之后。

可以额外添加 入口(entry)和出口(exit)结点,这些结点不包含指令。

64145

指令选择

主要问题:最大限度地利用寄存器,减少与内存交互的加载与保存。

代码生成算法的基本思想:

生成机器指令的规则

  • 只有当运算分量(参与计算的变量或常数)不在寄存器中,才从内存载入
  • 尽量保证只有当寄存器中的值不被使用时,才把它覆盖掉(延迟到最后一刻)

记录各个值对应的位置的数据结构:

  • 寄存器描述符(register descriptor)
    • 为每个寄存器维护,key 为寄存器名 $R_n$,value 为变量名
    • 跟踪哪些变量的当前值放在该寄存器内
  • 地址描述符(address descriptor)
    • 为每个程序变量维护,key 为变量名 $a,b,\cdots$,value 为变量名或寄存器名
    • 跟踪哪些位置(寄存器、栈中位置等)可以找到该变量的当前值

30399

三地址指令生成

代码语句

$x = y ; \text{op} ; z$

  1. 调用 $\text{getReg}(x = y ; \text{op} ; z)$,给 $x, y, z$ 选择寄存器 $R_x, R_y, R_z$。
  2. 查 $R_y$ 的寄存器描述符,如果 $y$ 不在 $R_y$ 中则生成指令 $\text{LD} ; R_y, y'$,其中 $y'$ 是某个存放了 $y$ 的值的内存位置。
  3. 对 $z$ 做与上述类似的处理。
  4. 生成指令 $\text{OP} ; R_x, R_y, R_z$,其中 $\text{OP}$ 对应 $\text{op}$(比如 $\text{ADD}$ 对应 +)。
  5. 更新寄存器和地址描述符。

$x = y$

  1. 调用 $\text{getReg}(x = y)$ 总是为 $x$ 和 $y$ 选择相同的寄存器。
  2. 如果 $y$ 不在 $R_y$ 中,那么生成指令 $\text{LD} ; R_y, y'$`,其中 $y'$ 是存放 $y$ 的位置。
  3. 更新寄存器和地址描述符。
    1. 如果生成了 $\text{LD}$ 指令,则先按照 $\text{LD}$ 的规则处理。
    2. $R_y$ 的寄存器描述符:把 $x$ 加入变量集合。
    3. $x$ 的地址描述符:只包含 $R_y$。

三地址指令

$\text{LD} ; R, x$

  1. $R$ 的寄存器描述符:只包含 $x$。
  2. $x$ 的地址描述符:$R$ 作为新位置加入 $x$ 的位置集合。
  3. 任何不同于 $x$ 的变量的地址描述符中删除 $R$。

$\text{OP} ; R_x, R_y, R_z$

  1. $R_x$ 的寄存器描述符:只包含 $x$。
  2. $x$ 的地址描述符:只包含 $R_x$。
  3. 任何不同于 $x$ 的变量的地址描述符中删除 $R_x$。

$\text{ST} ; x, R$

  1. 生成这种指令时 $R$ 一定存放了 $x$ 的当前值。
  2. $x$ 的地址描述符:把 $x$ 自己的内存位置加入位置集合。

三地址指令的活跃变量分析

活跃变量分析:基本块的结尾

  1. 如果变量 $x$ 在出口处活跃(其值在后续的控制流中会被用到),且查 $x$ 的地址描述符发现其不在自己的内存位置上,则生成指令 $\text{ST} ; x, R_x$。
  2. 更新寄存器和地址描述符。

如果不想维护这些描述符,可以在任何一条语句结束后都立即把值都写回内存位置

活跃变量分析

目的:研究哪些变量 “接下来马上会用到”。如果用不到,可以从寄存器里踢出。

活跃变量:如果对于两条语句 $i,j$,满足 $\text{def}(i, x)$ 且 $\text{use}(j, x)$,并且 $i\to j$ 存在一条路径没有其他的对变量 $x$ 的赋值,那么 $j$ 使用了 $i$ 处计算的 $x$,称为 $x$ 在语句 $i$ 处活跃,记作 $\text{live}_{out}(i, x)$。

  • 定值 $\text{def}(i, x)$:语句 $i$ 给变量 $x$ 进行了赋值
  • 使用 $\text{use}(i, x)$:语句 $i$ 使用了变量 $x$ 的值
  • 活跃变量 $\text{live}_{out}(i, x)$:变量 $x$ 在语句 $i$ 后的程序点上活跃(live)

活跃变量信息的用途:实现寄存器选择函数 $\text{getReg}()$。

  • 如果一个寄存器只存放了 $x$ 的值,且 $x$ 在 $i$ 处不活跃,那么这个寄存器在 $i$ 处可以用于其它目的。

分析算法

基本原则:设 $i$ 的下一条语句为 $j$:

  1. 若 $\text{use}(j, x)$,则 $\text{live}(i, x)$: 若在语句 $j$ 处使用了变量 $x$,则在语句 $i$ 处($i$ 是 $j$ 的前一个语句)$x$ 是活跃的。
  2. 若 $\text{live}(j, x)$ 且 $\neg \text{def}(j, x)$,则 $\text{live}(i, x)$: 若在语句 $j$ 处 $x$ 是活跃的,并且 $x$ 不是在语句 $j$ 处定义的,则在语句 $i$ 处 $x$ 也是活跃的。

活跃变量分析通常通过反向扫描程序的语句来进行,具体步骤如下:

  1. 初始化:假设在基本块出口处,所有非临时变量均活跃。

  2. 反向扫描

    • 从最后一个语句开始反向扫描基本块中的每个语句。
    • 对于形如 $x = y \ \text{op} \ z$ 的语句 $i$:
      • 将 $x, y, z$ 到目前为止更新过的活跃信息关联到 $i$。
      • 设置 $x$ 为 “不活跃”(因为它刚刚被定义)。
      • 设置 $y$ 和 $z$ 为 “活跃”(因为它们在这里用了)。

    注意:上述步骤中,设置 $x$ 为不活跃和设置 $y$、$z$ 为活跃的顺序(即后两步顺序)非常重要,因为 $x, y, z$ 可能会重复出现,如 $x=x+y$。

实际上为了跨基本块进行活跃变量分析,应当使用下节课的数据流分析去递归调用。

寄存器分配

getReg 函数

目标:减少 LD 和 ST 的指令数目。

任务:对一条指令 $x = y ; \text{op} ; z$ ,为运算分量 $y$ 和 $z$ 以及结果 $x$ 选择寄存器。

给运算分量选择寄存器:

  1. 如果已经在寄存器中,则选择该寄存器。
  2. 否则,如果有空闲寄存器,则选择一个空闲寄存器。
  3. 否则,设 $R$ 是一个候选寄存器,其存放了 $v$ 的值:
    • 如果 $v$ 的地址描述符包含其它位置,则可以用 $R$(还有别的地方存了,可以覆盖)。
    • 如果 $v$ 就是 $x$ 且不为运算分量,则可以用 $R$($x$ 是结果,本就要覆盖)。
    • 如果 $v$ 在该语句后不是活跃变量,则可以用 $R$($v$ 不会再用到,可以覆盖)。
  4. 否则,进行溢出操作(spill)。

溢出操作(spill)

设 $R$ 是候选寄存器,它存放了变量 $v$ 的值:

  1. 生成指令 $\text{ST} ; v, R$,并更新 $v$ 的地址描述符(把寄存器的值驱逐到内存中去)。
  2. 如果 $R$ 中还存放了别的变量的值,则可能要生成多条 ST 指令。
  3. 然后,我们就可以使用 $R$ 了。

寄存器的分配与指派

分配:哪些值应该放在寄存器中

指派:各个值应该存放在哪个寄存器

两个不同时活跃的变量可以使用同一个寄存器。

寄存器冲突图

构造寄存器冲突图(register-interference graph)

  • 结点:在第一趟代码生成中使用的符号寄存器
  • :两个符号寄存器不能指派同一个物理寄存器(相互冲突)则用边连起来

构造方法:

  1. 先假设寄存器无限,构造一次
  2. 然后写出汇编代码,列出每步的活跃寄存器
  3. 将同时活跃的寄存器连线,构造出图染色问题,进行图着色后,相同颜色的结点可以分配同一个物理寄存器
  4. 如果最小能进行 n - 染色,则 n 个寄存器即可
  5. 如果不能进行 n - 染色,则需要增加寄存器或者进行溢出操作

冲突:$R_1$ 在 $R_2$ 被定值的地方是活跃的,也就是说如果存在在一个指令 $i$,使得 $\text{def}(i, R_2)$ 且 $\text{live}_\text{out}(i, R_1)$,这个时候我们不能将他们合并为一个寄存器,因为这两个值后续都要用。

image-20250106223232324

62805

图着色算法的启发式技术

定理:如果冲突图中每个结点的度数都 $< m$,则总是可以 $m$- 着色。

原因:每个结点邻居的颜色最多 $m - 1$ 种,总能对其着色。

算法

  1. 寻找度数 $< m$ 的结点,从图中删除,并把该结点压到一个栈中
  2. 如果所有结点的度数都 $\geq m$:
    • 找到一个溢出结点,不对它着色
    • 删除该结点。
  3. 当图为空的时候:
    • 从栈顶依次弹出结点。
    • 选择该结点的邻居没有使用的颜色进行着色。

如果有溢出:

  1. 为溢出结点生成代码,使用时加载到新的符号寄存器中
  2. 然后对新的代码重新进行活跃性分析和寄存器分配(反正大不了退化到一用一存,肯定能搞定)

溢出节点选择:降低溢出代价,即降低引入的额外指令的运行时开销,尤其是避免在循环中引入新代码。

拆分

定义:对一个节点的 活跃范围 进行拆分,从而降低其在冲突图中的度数

  • 把某个结点对应寄存器的值保存到内存中(故意加一句 $\text{ST} ; x, R_1$)
  • 在拆分的地方把值再加载回来

16928

24486

52353

合并

定义:如果 $R_1$ 和 $R_2$ 在冲突图中不相邻的话,那么就可以把它们合并(coalesce)成一个符号寄存器

75926

  • 生成代码时,有大量的寄存器之间的拷贝,如 $\text{LD} ; R_1, R_2$
  • 如果把 $R_1$ 和 $R_2$ 分配到同一个物理寄存器,就不需要执行该拷贝

问题:可能增加冲突边的数目,从而无法着色

  • 解决方案 1:合并时不要创建高度数($\geq m$)的结点
  • 解决方案 2:如果 $a$ 的每个邻居 $r$ 都满足下面的条件之一,才可以把 $a$ 与 $b$ 合并:
    • $r$ 与 $b$ 之间有冲突
    • $r$ 的度数比较低($< m$)

预着色

  • 有些指令有默认寄存器,不可更改
  • 当成特殊符号寄存器,在着色前就加入图中并染色
  • 不要在这些节点溢出

💾

  •  

运行时环境

运行时环境的作用

运行时环境的主要作用是实现 存储组织过程抽象

问题:运行时环境需要考虑源语言本身的特性

可执行文件 = 源程序代表的计算 + 通过体系结构 / 操作系统接口实现的运行时环境

虚拟机实现的接口:

  • vm_get(name)
  • vm_set(name, value)
  • vm_param(value)
  • vm_call(name, nargs)
  • vm_ret(value)

其中,vm 是 Virtual Machine(虚拟机)的缩写,后缀是三地址代码的指令。

基础示例

72589

27093

其实就是 Lab 那个 Koopa IR 的作用,翻译成与具体运行环境无关的代码。

  • 体系结构和操作系统提供了非常底层的操作
  • 运行时环境用这些操作来实现数据存储和过程调用

主要要关注一下状态:

  • pc:程序计数器,指向当前执行的指令
  • ra:返回地址,return address
  • a0:返回值
  • st:参数栈,调用 vm_param 时,参数入栈

运行时环境的设计

存储组织:在代码生成前,编译器需要进行 目标运行环境的设计数据空间的分配

编译器在操作系统 / 虚拟机规定的区域中存储生成的目标代码与代码运行时的数据空间

比如 RISC-V 中:

  • .text 段存储代码
  • .data 段存储全局变量等

区分程序的编译时刻和运行时刻

  • 编译时刻:对应静态分配
    • 编译器通过程序文本即可做出分配决定
    • 例如:常量、全局变量、静态变量(C 中的 static 变量)
  • 运行时刻:对应动态分配
    • 程序在运行过程中才能做出分配决定
    • 例如:局部变量、动态变量(C 中的 malloc 函数分配的数据)

注意:静态确定的存储空间大小并 不意味 静态分配(可以动态分配,回顾 ICS,其中有个 .bss 段节省空间)

很多时候空间大小可以由类型信息得出

纯静态存储分配

定义:所有分配决定都在编译时得到。

  • 优点:不需要运行时的支持,可以做分时复用优化
  • 缺点:不支持递归调用过程(过程调用次数不能静态确定),不能动态建立数据结构

实例:Fortran 语言。

动态存储分配

  • 栈式存储管理:随着过程调用分配,值与过程的生命周期相同,局部栈上自动变量
  • 堆式存储管理:不完全随过程调用分配,值的生命周期可能比过程更长, malloc 完不 free

栈式存储管理

活动树:表示程序运行的所有过程

  • 一个节点:一个过程活动
  • 根节点:主过程/入口过程
  • 前序遍历:得到过程调用的顺序
  • 后序遍历:得到过程返回的顺序

活动记录

活动记录 / 栈帧:地址连续的一个存储块。

一次活动:子程序 / 过程 / 函数的一次执行。

结构:

  • 实际参数:通常放在寄存器里,但有时放不下
  • 返回值:通常放在寄存器里,但是不绝对
  • 控制链:指向调用者的活动记录
  • 访问链:用于定位别处(非本活动记录)的某个数据
  • 保存的机器状态:此次调用前的信息,如返回地址
  • 局部数据:该过程的局部变量
  • 临时变量:中间代码 / 目标代码生成产生的临时值

注意,这里与 Linux 栈帧不太一样,压栈的参数是被调用者活动记录的一部分,而不是调用者的。

布局:

53649

访问链与控制链

  • 访问链:指向过程中要访问的 非局部数据 所在的活动记录,用于查找符号 / 过程定义
  • 控制链:指向调用者的活动记录,用于找到当前活动的调用者,是活动树的一条有效路径

注意区分定义和调用

  • 访问链:管的是定义,是某变量 / 函数在源代码中出现的顺序 / 层级组织
  • 控制链:管的是调用,是活动的调用顺序

再重复一遍:

  • 沿访问链找定义
  • 沿控制链找上级活动(过程)

活动记录指针

11632

  • ARP 在活动记录开始位置的高地址下定长,存 固定长度 信息,类比 rbp
  • TOP 就是栈顶指针,可变长度,类比 rsp

恢复:

  • ARP:控制链存在 ARP,恢复的时候从这里找到调用者的指针,恢复 ARP。
  • TOP:以 ARP + 活动记录起始的固定长度赋值即可。

注意,这里 ARP 指针以上虽然属于被调用者栈帧,但是是由调用者创建的。

静态作用域

静态作用域:也称词法(lexical)作用域,非局部名字的绑定在过程被 定义时决定。典型实例如 PASCAL 语言。

访问链法

人话:沿着 访问链 找到定义所在位置

假设嵌套深度为 $m$ 的过程 $q$ 调用嵌套深度为 $n$ 的过程 $p$:

  • 情况 $m < n$:

    • $p$ 直接声明在 $q$ 中,也就是说 $m + 1 = n$
    • 将 $p$ 的访问链指向 $q$ 的活动记录
  • 情况 $m \ge n$:

    • $q$ 和 $p$ 的嵌套深度从 1 到 $n-1$ 的外围过程是相同的
    • 追踪 $q$ 的访问链 $m - n + 1$ 步,到达直接包含 $p$ 的过程 $r$ 的最近的活动记录
    • 将 $p$ 的访问链指向这个 $r$ 的活动记录

显示表法

人话:访问链法太慢了,还要挨个找链表,但是我们知道活动调用自然形成了一个递增的深度顺序,所以我们利用这个,做一个指针表。

显示表(display):运行时环境维护一个数组 $d$,为每个嵌套深度记录一个指针。

  • 指针 $d[i]$ 指向最近的嵌套深度为 $i$ 的活动记录
  • 如果过程 $p$ 在运行中访问嵌套深度为 $i$(静态可确定)的过程 $q$ 的数据,则可以通过 $d[i]$ 找到 $q$ 的活动记录
  • 使用显示表可以提高效率,访问开销是常数

50053

过程作为参数传递

当一个过程 $p$ 作为参数传递给另一个过程 $q$,并且 $q$ 随后调用了这个参数时,有可能 $q$ 并不知道 $p$ 的上下文。

方法:调用者把 $p$ 作为参数传递时,同时传递其访问链。

问题:栈式管理下,访问链指向的活动记录有可能不在栈中,如下例。

def M(x):
    def R(y):
        i, j, k = 0, 0, 0
        def P(z):
            return i + j + k + z
        return P
    f = R(1)
    return f(2)
print(M(3))

发生 return P 时,R 就从活动记录的栈中扔掉了,再以其结果 f 调用的时候,就找不到定义 P 的记录 R 了。

87697

解决办法:

  1. 完全在堆中分配和管理活动记录,从而延长生命周期
  2. 闭包
闭包

发生上述情况的 “逃逸” 时,运行时在堆上分配空间,存储需要的外层函数的局部数据。

92830

这样,在 R 没了的时候,调用 P 依旧能找到 R 的东西。

动态作用域

动态作用域:非局部名字的绑定在过程被 调用时决定

被调用者的非局部名字 a 和其调用者中使用 相同 的存储单元,此时静态无法确定,只能在运行时确定。

用得少,运行时环境为每个名字维护一个全局的作用域栈。

比较

19742

  • 动态作用域:
    • 调用 dynamic->small->show,在 show 里要找 r 的时候,沿着控制流往上找,在 small 里找到,输出 0.125
    • 若直接调用 dynamic->show,则在 dynamic 里找到,输出 0.250
  • 静态作用域:只要是调用 show,就沿着访问链往上找(这是静态的过程),所以无论在哪里调用 show,都输出 0.250

运行时环境实现

过程抽象主要需要考虑如何创建和维护活动记录,生成目标代码需要与操作系统和体系结构一致。

总体策略:

  • 调用代码序列:precall(预调用)和 prologue(序言)
  • 返回代码序列:epilogue(尾声)和 postreturn(后返回)

过程链接

  • 调用代码序列:分配空间,填写记录信息

    分为 precall 和 prologue

  • 返回代码序列:释放记录,恢复状态,继续执行

    分为 epilogue 和 postreturn

分割方案的权衡:

  • 调用者工作多:代码较长,因为每次调用都需要重复生成
  • 被调用者工作多:冗余存储操作,如考虑被调用者保存寄存器

调用代码序列设计

调用者 precall

  • 计算实际参数值,存入记录
  • 保存状态信息(caller-saved)
  • 更新 ARP 指针

被调用者 prologue

  • 保存状态信息(如 callee-saved 寄存器)
  • 初始化局部数据并执行

返回代码序列设计

被调用者 epilogue

  • 设置返回值
  • 恢复 ARP 和状态(callee-saved)
  • 转移到调用者代码

调用者 postreturn

  • 获取返回值
  • 恢复状态(caller-saved)

堆式存储管理

基本上就是 Malloclab 那一套,需要合理分配 / 回收堆区空间。

要注意的问题包括:

  • 内存泄漏
  • 悬空指针解引用

垃圾回收

类型不安全的语言(比如 C 和 C++)不适合使用垃圾回收

主要依赖于可达性分析。

可达性分析

根集(rootset):不需要指针解引用就可以直接访问的数据,如静态字段成员

可达性(reachability)

  • 根集中的成员指向的对象都是可达的
  • 对于任意一个对象,如果指向它的一个指针被保存在可达对象的某字段中,那么这个对象也是可达的

性质:一个对象一旦变得不可达,它就不会再变成可达的

改变可达对象集合的操作

  1. 对象分配:返回一个指向新存储块的指针。
  2. 参数传递 / 返回值:对象指针从实在参数传递到形式参数 / 从返回值传递给调用者。
  3. 引用赋值:对于指针 $u$ 和 $v$,赋值 $u = v$ 将 $u$ 指向 $v$ 指向的对象,可能使得 $u$ 原来指向的对象变得不可达,并递归使得更多对象不可达。
  4. 过程返回:活动记录出栈,局部变量从根集中移除,可能使得一些对象变得不可达。

垃圾回收算法

基本思想:寻找不可达的对象。

两种基本方法:

  1. 跟踪相关操作,捕获对象变得不可达的时刻,回收对象占用的空间。

    典型例子:基于引用计数的垃圾回收。

  2. 在需要时,标记出所有可达对象,然后回收其他对象。

    典型例子:基于跟踪的垃圾回收。

基于引用计数的垃圾回收器

每个对象有一个用于存放 引用计数 (reference counting)的字段,并按照如下方式维护:

  1. 对象分配:引用计数设为 1。
  2. 参数传递:引用计数加 1。
  3. 引用赋值:对于 $u = v$,$u$ 指向的对象引用计数减 1,$v$ 指向的对象引用计数加 1。
  4. 过程返回:每个局部变量指向对象的引用计数减 1。

2295

问题是会导致循环垃圾:

60853

循环垃圾的解决方法:弱引用,程序员手动声明一些指针不影响引用计数。

基于引用计数的垃圾回收器总结

优点:

  • 以增量方式完成,可以避免长时间停顿
  • 垃圾可以被及时回收
  • 易于实现
  • 可以与其它存储管理机制结合

缺点:

  • 空间代价:每个对象都要保存引用计数
  • 时间代价:每次指针更新都要做多次检查和修改
  • 循环数据结构会造成内存泄漏

标记 - 清扫式垃圾回收

以周期性的方式运行,在空闲空间耗尽或者低于某个阈值时启动,寻找不可达对象并回收其空间。

分成两个阶段:

  1. 标记:从根集开始,跟踪并标记出所有可达对象
  2. 清扫:遍历整个堆区,释放不可达对象

如果我们把数据对象看作顶点,指向关系看作有向边,那么标记的过程实际上是从根集开始的图遍历的过程

60418

算法优化

当前问题:基本算法需要扫描整个堆区

优化:用一个列表记录所有已经分配的对象不可达对象等于已分配对象去掉可达对象

  • 优点:只需要扫描这个列表就可以完成清扫
  • 缺点:需要维护这个额外的列表
标记 - 压缩式垃圾回收

对可达对象进行重定位(relocating)可以消除存储碎片

把可达对象移动到堆区的一端,另一端则是空闲空间空闲空间接合成单一块,更容易存储较大的对象提高应用程序的时间局部性和空间局部性

整个过程分成三个步骤:

  1. 标记:从根集开始,跟踪并标记出所有可达对象
  2. 计算新地址:计算可达对象的新地址
  3. 移动对象并更新其中的指针:移动可达对象并更新其中的指针

因为对象的位置发生改变,所以所有的指针都可能需要更新。

918

标记 - 清扫式垃圾回收小结

优点

  • 基本没有空间代价(一个内存块只需要若干个二进制位)
  • 可以正确处理循环数据结构

缺点

  • 应用程序必须全面停顿,不适用于实时系统
  • 可能会造成堆区的碎片化

改善措施

  • 可以采用 增量式回收部分回收 来改善
  • 可以用 标记并压缩 来解决

实际中可以同时使用 引用计数标记 - 清扫

拷贝回收器

标记并压缩的问题:压缩时需要扫描整个堆区

拷贝回收器:堆区空间被分为两个 半空间 (semispace)

  • From 半空间:在这里分配内存
  • To 半空间:拷贝可达对象到这里

策略:

  • 在 From 半空间里分配内存,当其填满后,开始垃圾回收
  • 回收时,把可达对象拷贝到 To 半空间
  • 回收完成后,把两个半空间的角色对换,应用程序继续

59230

世代垃圾回收器

设计原因:大多数对象生命周期都很短

策略:

  • 把堆区分成 不同的年龄区域 (代表不同的世代),对比较年轻的区域进行更加频繁的垃圾回收
  • 在一个回收周期内不用跟踪所有的内存单元
  • 周期性地对 “较老” 的区域进行回收

💾

  •  

中间代码生成 II

生成表达式代码的 SDD

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline S \to \text{id} = E ; & S.\text{code} = E.\text{code} \parallel \ & \quad \text{gen}(\text{top.get(id.lexeme)} = E.\text{addr}) \ \hline E \to E_1 + E_2 & E.\text{addr} = \text{new Temp()} \ & E.\text{code} = E_1.\text{code} \parallel E_2.\text{code} \parallel \ & \quad \text{gen}(E.\text{addr} = E_1.\text{addr} + E_2.\text{addr}) \ \hline \phantom{E \to \ }| - E_1 & E.\text{addr} = \text{new Temp()} \ & E.\text{code} = E_1.\text{code} \parallel \ & \quad \text{gen}(E.\text{addr} = \text{“minus”} E_1.\text{addr}) \ \hline \phantom{E \to \ }| ( E_1 ) & E.\text{addr} = E_1.\text{addr} \ & E.\text{code} = E_1.\text{code} \ \hline \phantom{E \to \ }| \text{id} & E.\text{addr} = \text{top.get(id.lexeme)} \ & E.\text{code} = \text{“”} \ \hline \end{array} $$

其中:

  • 综合属性 $\text{code}$ 表示代码
  • $\text{addr}$ 表示存放表达式结果的地址(临时变量)
  • $\text{top.get}(\cdots)$ 从栈顶符号表(符号是嵌套的,可以实现为栈)开始,逐个向下寻找 $\text{id}$ 的信息
  • $\text{new} , \text{Temp}()$ 可以生成一个临时变量
  • $\text{gen}(\cdots)$ 生成相应代码

这里实际上是一个增量式翻译,即要运算得到 c = a + b,先翻译出生成 ab 的代码,再翻译生成 c 的代码。

数组元素

数组元素的寻址

一维数组的寻址

假设数组元素被存放在连续的存储空间中,元素从 0 到 $n-1$ 编号,第 $i$ 个元素的地址为:

$$ \text{base} + i \cdot w $$

其中:

  • $\text{base}$ 为数据 $A$ 的内存块的起始地址,即 $A[0]$ 的相对地址
  • $w$ 为每个数组元素的宽度
  • $\text{base}$, $w$, $n$ 的值都可以从符号表中找到

k 维数组的寻址

假设数组按行存放,即首先存放 $A[0][i_2]...[i_k]$,然后存放 $A[1][i_2]...[i_k]$, ...

设 $n_j$ 为第 $j$ 维的维数,$w_j$ 为第 $j$ 维的每个子数组元素的宽度,$w_k = w$ 为单个元素的宽度:

$$ \begin{aligned} w_{k-1} &= n_k \cdot w_k = n_k \cdot w \ w_{k-2} &= n_{k-1} \cdot w_{k-1} = n_{k-1} \cdot n_k \cdot w \end{aligned} $$

多维数组 $A[i_1][i_2]...[i_k]$ 的地址为:

$$ \text{base} + i_1 \cdot w_1 + i_2 \cdot w_2 + ... + i_k \cdot w_k $$

或者:

$$ \text{base} + (((...((i_1 \cdot n_2 + i_2) \cdot n_3 + i_3)...) \cdot n_k) + i_k) \cdot w $$

多维数组的存放方法

  • 行优先(一般选择)
  • 列优先

求 $a[i]$ 的地址:

$$ \text{base} + (i - \text{low}) \cdot w = \text{base} - \text{low} \cdot w + i \cdot w $$

注意,这里 $\text{low}$ 是下界,其不一定为 0。

包含数组元素的表达式文法

添加新的文法产生式

  1. 数组元素 $L: L \rightarrow L[E] \ | \ id[E]$
  2. 以数组元素为左部的赋值 $S \rightarrow L = E$
  3. 数组元素作为表达式中的因子 $E \rightarrow L$

翻译方案

  1. 计算偏移量:对 $L$ 的代码计算偏移量,将结果存于 $L.\text{addr}$ 所指的临时变量中。

  2. 综合属性 $\text{array}$:记录相应数组的信息:元素类型,基地址等。

  3. 数组元素作为因子

    • $L$ 的代码只计算了偏移量

    • 数组元素的存放地址应该根据偏移量进一步计算,即 $L$ 的数组基地址加上偏移量

    • 使用三地址指令 $x = a[i]$

  4. 数组元素作为赋值左部

    • 使用三地址指令 $a[i] = x$。

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline L \to \text{id} [ E ] & L.\text{array} = \text{top.get(id.lexeme)} \ & L.\text{type} = L.\text{array.type.elem} \ & L.\text{addr} = \text{new Temp()} \ & \text{gen}(L.\text{addr} = E.\text{addr} * L.\text{type.width}) \ \hline \phantom{L \to \ }| L_1 [ E ] & L.\text{array} = L_1.\text{array} \ & L.\text{type} = L_1.\text{type.elem} \ & t = \text{new Temp()} \ & L.\text{addr} = \text{new Temp()} \ & \text{gen}(t = E.\text{addr} * L.\text{type.width}) \ & \text{gen}(L.\text{addr} = L_1.\text{addr} + t) \ \hline E \to E_1 + E_2 & E.\text{addr} = \text{new Temp()} \ & \text{gen}(E.\text{addr} = E_1.\text{addr} + E_2.\text{addr}) \ \hline \phantom{E \to \ }| \text{id} & E.\text{addr} = \text{top.get(id.lexeme)} \ \hline \phantom{E \to \ }| L & E.\text{addr} = \text{new Temp()} \ & \text{gen}(E.\text{addr} = L.\text{array.base} [ L.\text{addr} ]) \ \hline S \to \text{id} = E ; & \text{gen}(\text{top.get(id.lexeme)} = E.\text{addr}) \ \hline \phantom{S \to \ }| L = E ; & \text{gen}(L.\text{array.base} [ L.\text{addr} ] = E.\text{addr}) \ \hline \end{array} $$

注意:

  • 这里不是在算数组的类型大小,而是在算一个数组表达式的相对于基地址的偏移量。
  • $\text{addr}$ 是偏移量这一计算结果的存放地址,而不是数组的基地址,基地址应当在 $\text{array}$ 属性里面。
  • 建议结合例子观察一下 $\text{type}$ 的解码顺序
  • 这里省略了一些 $\text{gen}$ 的引号,请勿认为它完成了计算,这只是生成了计算的代码。

例子

56189

类型检查和转换

类型系统 (type system)

  • 给每一个组成部分赋予一个类型表达式
  • 通过一组逻辑规则来表示这些类型表达式必须满足的条件

设计类型系统的根本目的是用静态检查的方式来保证合法程序运行时的良行为。

类型检查规则

类型综合:根据子表达式的类型构造出表达式的类型

例如:

  • 如果 $f$ 的类型为 $s \rightarrow t$ 且 $x$ 的类型为 $s$
  • 那么 $f(x)$ 的类型为 $t$

类型推导:根据语言结构的使用方式来确定该结构的类型

例如:

  • 如果 $f(x)$ 是一个表达式,$f$ 的类型为 $\alpha \rightarrow \beta$,且 $x$ 的类型为 $\alpha$
  • 那么 $f(x)$ 的类型为 $\beta$
  • $\alpha, \beta$ 可以是未知类型

类型转换

假设在表达式 $x * i$ 中,$x$ 为浮点数、$i$ 为整数,则结果应该是浮点数

  • $x$ 和 $i$ 使用不同的二进制表示方式
  • 浮点数 * 和整数 * 使用不同的指令
  • 例如:
    • $t_1 = (\text{float}) i$
    • $t_2 = x ; \text{fmul} ; t_1$

处理简单的类型转换的 SDD:

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline E \to E_1 + E_2 & \text{if } (E_1.\text{type} = \text{integer} \text{ and } E_2.\text{type} = \text{integer}) E.\text{type} = \text{integer} \ & \text{else if } (E_1.\text{type} = \text{float} \text{ and } E_2.\text{type} = \text{integer}) E.\text{type} = \text{float} \ \hline \end{array} $$

类型拓宽和类型收缩

  • 编译器自动完成的转换为 隐式转换(coercion)
  • 程序员用代码指定的强制转换为 显式转换(cast)

处理类型转换的 SDT

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline E \to E_1 + E_2 & E.\text{type} = \max(E_1.\text{type}, E_2.\text{type}); \ & a_1 = \text{widen}(E_1.\text{addr}, E_1.\text{type}, E.\text{type}); \ & a_2 = \text{widen}(E_2.\text{addr}, E_2.\text{type}, E.\text{type}); \ & E.\text{addr} = \text{new Temp}(); \ & \text{gen}(E.\text{addr} \text{ “=” } a_1 \text{ “+” } a_2); \ \hline \end{array} $$

widen 函数用于将一个地址的值转换为指定的类型。其定义如下:

Addr widen(Addr a, Type t, Type w) {
    if (t == w) return a;
    else if (t == integer && w == float) {
        Addr temp = new Temp();
        gen(temp '=' '(float)' a); // 就是这里发生了隐式类型转换
        return temp;
    }
    else error;
}
  • max 函数用于查找两个类型的最小公共祖先。具体实现依赖于类型系统的定义。
  • widen 函数生成必要的类型转换代码,并返回转换后的地址。

函数/运算符的重载

通过查看参数来解决函数重载问题:

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline E \to f(E_1) & \text{if } f.\text{typeset} = {s_i \to t_i \mid 1 \leq i \leq k} \text{ and } E_1.\text{type} = s_k \text{ then } E.\text{type} = t_k \ \hline \end{array} $$

控制流的翻译

布尔表达式可以用于改变控制流/计算逻辑值:

文法:

$$ B \to B , || , B \mid B , && , B \mid !B \mid (B) \mid E , \text{rel} , E \mid \text{true} \mid \text{false} $$

短路求值:

  • $B_1 , || , B_2$ 中 $B_1$ 为真时,不用计算 $B_2$,整个表达式为真。
  • $B_1 , && , B_2$ 中 $B_1$ 为假时,不用计算 $B_2$,整个表达式为假。

短路代码通过跳转指令实现控制流的处理,逻辑运算符本身不在代码中出现。

98647

控制流语句的 SDD

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline P \to S & S.\text{next} = \text{newlabel()} \ & P.\text{code} = S.\text{code} \parallel \text{label}(S.\text{next}) \ \hline S \to \text{assign} & S.\text{code} = \text{assign.code} \ \hline S \to \text{if} (B) S_1 & B.\text{true} = \text{newlabel()} \ & B.\text{false} = S_1.\text{next} = S.\text{next} \ & S.\text{code} = B.\text{code} \parallel \text{label}(B.\text{true}) \parallel S_1.\text{code} \ \hline S \to \text{if} (B) S_1 \text{ else } S_2 & B.\text{true} = \text{newlabel()} \ & B.\text{false} = \text{newlabel()} \ & S_1.\text{next} = S_2.\text{next} = S.\text{next} \ & S.\text{code} = B.\text{code} \parallel \text{label}(B.\text{true}) \parallel S_1.\text{code} \ & \quad \parallel \text{gen}(\text{“goto”} S.\text{next}) \parallel \text{label}(B.\text{false}) \parallel S_2.\text{code} \ \hline S \to \text{while} (B) S_1 & \text{begin} = \text{newlabel()} \ & B.\text{true} = \text{newlabel()} \ & B.\text{false} = S.\text{next} \ & S_1.\text{next} = \text{begin} \ & S.\text{code} = \text{label}(\text{begin}) \parallel B.\text{code} \parallel \text{label}(B.\text{true}) \parallel S_1.\text{code} \parallel \text{gen}(\text{“goto”} \text{begin}) \ \hline S \to S_1 S_2 & S_1.\text{next} = \text{newlabel()} \ & S_2.\text{next} = S.\text{next} \ & S.\text{code} = S_1.\text{code} \parallel \text{label}(S_1.\text{next}) \parallel S_2.\text{code} \ \hline \end{array} $$

重点在于理解标号的顺序,明白基本块之间是怎么跳转的,其实如果自己做完 Lab Lv6 基本上就很简单了。

布尔表达式控制流翻译

生成的代码执行时跳转到两个标号之一:

  • 表达式的值为真时,跳转到 $B.\text{true}$。
  • 表达式的值为假时,跳转到 $B.\text{false}$。

$B.\text{true}$ 和 $B.\text{false}$ 是两个继承属性,根据 $B$ 所在的上下文指向不同的位置:

  • 如果 $B$ 是 if 语句的条件表达式,分别指向 then 分支和 else 分支
  • 如果没有 else 分支,则 $B.\text{false}$ 指向 if 语句的下一条指令
  • 如果 $B$ 是 while 语句的条件表达式,分别指向循环体的开头和循环的出口

下图的代码中同时考虑了短路求值

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline B \to B_1 || B_2 & B_1.\text{true} = B.\text{true}; B_1.\text{false} = \text{newlabel()}; \ & B_2.\text{true} = B.\text{true}; B_2.\text{false} = B.\text{false}; \ & B.\text{code} = B_1.\text{code} \parallel \text{label}(B_1.\text{false}) \parallel B_2.\text{code} \ \hline B \to B_1 && B_2 & B_1.\text{true} = \text{newlabel()}; B_1.\text{false} = B.\text{false}; \ & B_2.\text{true} = B.\text{true}; B_2.\text{false} = B.\text{false}; \ & B.\text{code} = B_1.\text{code} \parallel \text{label}(B_1.\text{true}) \parallel B_2.\text{code} \ \hline B \to ! B_1 & B_1.\text{true} = B.\text{false}; B_1.\text{false} = B.\text{true}; B.\text{code} = B_1.\text{code} \ \hline B \to (B_1) & B_1.\text{true} = B.\text{true}; B_1.\text{false} = B.\text{false}; B.\text{code} = B_1.\text{code} \ \hline B \to E_1 \text{ rel } E_2 & B.\text{code} = \text{gen}(\text{“if”} E_1.\text{addr } \text{rel.op } E_2.\text{addr } \text{“goto”} B.\text{true}) \parallel \text{gen}(\text{“goto”} B.\text{false}) \ \hline B \to \text{true} & B.\text{code} = \text{gen}(\text{“goto”} B.\text{true}) \ \hline B \to \text{false} & B.\text{code} = \text{gen}(\text{“goto”} B.\text{false}) \ \hline \end{array} $$

布尔值和跳转代码

程序中出现布尔表达式的目的也有可能就是求出它的值,例如 $x = a < b$

处理方法:首先建立表达式的语法树,然后根据表达式的不同角色来处理。

文法:

  • $S \rightarrow \text{id} = E; \ | \ \text{if (E) S} \ | \ \text{while (E) S} \ | \ S \ S$
  • $E \rightarrow E | E \ | \ E \ && \ E \ | \ E \ \text{rel} \ E \ | \ \ldots$

根据 $E$ 的语法树结点所在的位置:

  • $S \rightarrow \text{while (E) S1}$ 中的 $E$,生成跳转代码
  • $S \rightarrow \text{id} = E$,生成计算右值的代码

在写 Lab 的时候实际上是反正值肯定返回,但是怎么用(赋值还是条件跳转)就是上一级考虑的问题了。

回填

为布尔表达式和控制流语句生成目标代码的关键问题:某些跳转指令应该跳转到哪里?

例如: $\text{if (B) S}$

  • 按照短路代码的翻译方法,$B$ 的代码中有一些跳转指令在 $B$ 为假时执行,
  • 这些跳转指令的目标应该跳过 $S$ 对应的代码。生成这些指令时,$S$ 的代码尚未生成,因此目标不确定
  • 如果通过语句的继承属性 $\text{next}$ 来传递,当中间代码不允许符号标号时,则需要第二趟处理。

回填的基本思想

  1. 记录 $B$ 的代码中跳转指令 $\text{goto S.next}$,$\text{if ... goto S.next}$ 的位置,但是不生成跳转目标
  2. 这些位置被记录到 $B$ 的综合属性 $B.\text{falseList}$ 中
  3. 当 $S.\text{next}$ 的值已知时(即 $S$ 的代码生成完毕时),把 $B.\text{falseList}$ 中的所有指令的目标都填上这个值

回填技术

  • 生成跳转指令时暂时不指定跳转目标标号,而是使用列表记录这些不完整的指令
  • 等知道正确的目标时再填写目标标号
  • 每个列表中的指令都指向同一个目标,列表包括:$\text{truelist}$, $\text{falselist}$, $\text{nextlist}$

布尔表达式的回填翻译

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline B \to B_1 || M B_2 & \text{backpatch}(B_1.\text{falselist}, M.\text{instr}); \ & B.\text{truelist} = \text{merge}(B_1.\text{truelist}, B_2.\text{truelist}); \ & B.\text{falselist} = B_2.\text{falselist}; \ \hline B \to B_1 && M B_2 & \text{backpatch}(B_1.\text{truelist}, M.\text{instr}); \ & B.\text{truelist} = B_2.\text{truelist}; \ & B.\text{falselist} = \text{merge}(B_1.\text{falselist}, B_2.\text{falselist}); \ \hline B \to ! B_1 & B.\text{truelist} = B_1.\text{falselist}; \ & B.\text{falselist} = B_1.\text{truelist}; \ \hline B \to (B_1) & B.\text{truelist} = B_1.\text{truelist}; \ & B.\text{falselist} = B_1.\text{falselist}; \ \hline B \to E_1 \text{ rel } E_2 & B.\text{truelist} = \text{makelist(nextinstr)}; \ & B.\text{falselist} = \text{makelist(nextinstr + 1)}; \ & \text{emit}(\text{“if”} E_1.\text{addr } \text{rel.op } E_2.\text{addr } \text{“goto” } B.\text{true}); \ & \text{emit}(\text{“goto”} B.\text{false}); \ \hline M \to \varepsilon & M.\text{instr} = \text{nextinstr}; \ \hline B \to \text{true} & B.\text{truelist} = \text{makelist(nextinstr)}; \ & \text{emit}(\text{“goto”} B.\text{true}); \ & B.\text{falselist} = \text{null}; \ \hline B \to \text{false} & B.\text{falselist} = \text{makelist(nextinstr)}; \ & \text{emit}(\text{“goto”} B.\text{false}); \ & B.\text{truelist} = \text{null}; \ \hline \end{array} $$

首先注意:所有的语义规则都是在产生式末尾,这个表省略了大括号(后同),此时,你即将规约回产生式头,而且拥有了所有产生式体的属性(此时他们是综合属性),所以可以随便用了。

这里,引入两个综合属性:

  • truelist:包含跳转指令(位置)的列表,这些指令在取值 true 时执行
  • falselist:包含跳转指令(位置)的列表,这些指令在取值 false 时执行

辅助函数包括:

  • makelist(i):构造一个列表
  • merge(p1, p2):合并两个列表
  • backpatch(p, i):用 i 回填 p 指向的语句列表中的跳转语句的跳转地址

大概讲一下,以第一个产生式 $B \to B_1 || M B_2$ 为例:

  1. 当展开此步的时候,我们已经知道了当 $B_1$ 为假时,会继续判断 $B_2$,所以可以用 $M.\text{instr}$ 回填 $B_1.\text{falselist}$
  2. 但是此时还没有生成 $B_2$ 的代码,我们不知道 $B_1$ 为真的时候应该跳多远才能跳过 $B_2$,所以先把 $B_1.\text{truelist}$ 和 $B_2.\text{truelist}$ 合并,得到 $B.\text{truelist}$,这意味着如果二者任一为真,就代表 $B$ 为真,在回填 $B.\text{truelist}$ 的时候,也就可以回填到 $B_1.\text{truelist}$ 和 $B_2.\text{truelist}$
  3. 当然,如果 $B_2$ 为假(这隐含我们已经判断到了 $B_2$,也即 $B_1$ 为假),那么 $B$ 也为假,所以 $B.\text{falselist}$ 就是 $B_2.\text{falselist}$

控制流语句的回填翻译

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline S \to \text{if} (B) \ M_1 \ S_1 \ N \ \text{else} \ M_2 \ S_2 & \text{backpatch}(B.\text{truelist}, M_1.\text{instr}); \ & \text{backpatch}(B.\text{falselist}, M_2.\text{instr}); \ & \text{temp} = \text{merge}(S_1.\text{nextlist}, N.\text{nextlist}); \ & S.\text{nextlist} = \text{merge}(\text{temp}, S_2.\text{nextlist}); \ \hline S \to \text{if} (B) \ \text{then} \ M \ S_1 & \text{backpatch}(B.\text{truelist}, M.\text{instr}); \ & S.\text{nextlist} = \text{merge}(B.\text{falselist}, S_1.\text{nextlist}); \ \hline N \to \varepsilon & N.\text{nextlist} = \text{nextinstr}; \ & \text{emit}(\text{“goto”____}); /* 稍后回填 */ \ \hline M \to \varepsilon & M.\text{instr} = \text{nextinstr}; \ \hline S \to \text{while} \ M_1 \ (B) \ \text{do} \ M_2 \ S_1 & \text{backpatch}(S_1.\text{nextlist}, M_1.\text{instr}); \ & \text{backpatch}(B.\text{truelist}, M_2.\text{instr}); \ & S.\text{nextlist} = B.\text{falselist}; \ & \text{emit}(\text{“goto”} M_1.\text{instr}); \ \hline S \to { L } & S.\text{nextlist} = L.\text{nextlist}; \ \hline S \to A & S.\text{nextlist} = \text{null}; \ \hline L \to L_1 \ M \ S & \text{backpatch}(L_1.\text{nextlist}, M.\text{instr}); \ & L.\text{nextlist} = S.\text{nextlist}; \ \hline L \to S & L.\text{nextlist} = S.\text{nextlist}; \ \hline \end{array} $$

和之前大差不差。

Break 和 Continue 语句的处理方法

Break 语句:

  • 追踪外围语句 $S$
  • 生成一个跳转指令坯
  • 将这个指令坯的位置加入到 $S.\text{nextlist}$ 中

跟踪的方法:

  • 在符号表中设置 $\text{break}$ 条目,令其指向外围语句
  • 在符号表中设置指向 $S.\text{nextlist}$ 的指针,然后把这个指令坯的位置直接加入到 $\text{nextlist}$ 中

Switch 语句的生成式

为了构造 switch 语句的翻译方案,设置一个队列变量 $q$

$q$ 的元素是记录,包含 $c$(condition) 和 $d$(destination) 两个成员,分别用于存储 case 后面的常量值 $v$ 和各语句串中间代码第一个三地址语句地址,以便生成 test 后面的条件转移语句时使用

Switch 语句的翻译方案

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline S \rightarrow \text{switch} (E) H { M , \text{default}:F , L } & S.\text{nextlist} = \text{merge}(M.\text{nextlist}, L.\text{nextlist}, \text{makelist}(\text{nextinstr})) \ & \text{emit}(\text{goto-'}) \\ & \text{backpatch}(H.\text{list}, \text{nextinstr}) \\ & \text{for t in } q: \\ & \quad \text{gen}(\text{if'} , E.\text{addr } \text{==' } t.c \, \text{goto' } t.d) \ & \text{emit}(\text{goto' } F.\text{instr}) \\ \hline H \rightarrow \varepsilon & \text{set q as } \varnothing \\ & H.\text{list} = \text{makelist}(\text{nextinstr}) \\ & \text{emit}(\text{goto-'}) \ \hline F \rightarrow \varepsilon & F.\text{instr} = \text{nextinstr} \ \hline M \rightarrow \text{case} , C:F , L & t.c = C.\text{val} \ & t.d = F.\text{instr} \ & \text{insert } t , \text{into } q \ & M.\text{nextlist} = \text{merge}(L.\text{nextlist}, \text{makelist}(\text{nextinstr})) \ & \text{emit}(\text{goto-'}) \\ \hline M \rightarrow M_1 \, \text{case} \, C:F \, L & t.c = C.\text{val} \\ & t.d = F.\text{instr} \\ & \text{insert } t \, \text{into } q \\ & M.\text{nextlist} = \text{merge}(M_1.\text{nextlist}, L.\text{nextlist}, \text{makelist}(\text{nextinstr})) \\ & \text{emit}(\text{goto-'}) \ \hline L \rightarrow S & L.\text{nextlist} = S.\text{nextlist} \ \hline L \rightarrow L_1 , F , S & \text{backpatch}(L_1.\text{nextlist}, F.\text{instr}) \ & L.\text{nextlist} = S.\text{nextlist} \ \hline \end{array} $$

For 循环的翻译方案

(来自 22 年往年题)

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{语义规则} \ \hline S \to \text{for} \ (S_1 \ M_1 \ ; B ; \ M_2 \ S_2) \ N \ S_3 & \text{backpatch}(B.\text{truelist}, N.\text{instr}); \ & \text{backpatch}(N.\text{nextlist}, M_1.\text{instr}); \ & \text{backpatch}(S_1.\text{nextlist}, M_1.\text{instr}); \ & \text{backpatch}(S_2.\text{nextlist}, M_1.\text{instr}); \ & \text{backpatch}(S_3.\text{nextlist}, M_2.\text{instr}); \ & \text{emit}(\text{“goto”} \ M_2.\text{instr}); \ & S.\text{nextlist} = B.\text{falselist}; \ \hline M \to \varepsilon & M.\text{instr} = \text{nextinstr}; \ \hline N \to \varepsilon & N.\text{nextlist} = \text{makelist}(\text{nextinstr}); \ & \text{emit}(\text{“goto”} \ ____); \ & N.\text{instr} = \text{nextinstr}; \ \hline \end{array} $$ 注意这里,$B$ 默认是非顺序执行,一定跳转的,所以 $M_2$ 的位置不是 $N$,而 $S_2$ 默认接下来是顺序执行的,所以后面要跟个 $\text{goto}$。

💾

  •  

2024年12月阅读书摘

✇Dennis
作者Domon

12月阅读记录

  • 《为你的生活写作:布洛克小说一日课》Done
  • 《跑者常见疼痛手册》Done
  • 《李诞脱口秀工作手册》Done
  • 《生活在低处》Done
  • 《羊男的圣诞节》Done
  • 《置身事内》16%

12月阅读书摘

为你的生活写作:布洛克小说一日课

  • 2024年12月阅读书摘
  • 书名: 为你的生活写作:布洛克小说一日课
  • 作者: 劳伦斯·布洛克
  • 简介: 本书是著名侦探小说大师劳伦斯·布洛克的写作指南,心得总结,由布洛克于20世纪80年代创办的“为生活写作”培训班改编而来,是实用的写作心理指导和励志指南。该书不讲具体的写作技法,不教授诸如结构怎么搭建这样具体的问题,而是通过几项具有实操性的心理训练来进行写作前的准备工作,让你能有足够的勇气应对写作中产生的心理问题与负面情绪,让写作者不再自卑,不再拖延,找到打开想象力和灵感的门径。本书语言富有幽默感、轻松活泼:每个章节实际都是一篇解决写作心理问题的主题性散文,是欣赏作家幽默的文风的绝佳机会。你也可以把本书当作布洛克对自己举办的一日小说培训班,跟着指导做训练,不断提升写作“气场”。
  • 出版时间 2023-11-27 00:00:00
  • ISBN: 9787020182862
  • 分类: 文学-外国文学
  • 出版社: 人民文学出版社

开启冥想之门

  • 📌 但我建议人人都可以寻找一种与冥想功能类似的有效方法,让我们在写作之余短暂休息,与写作保持一点距离,同时不会完全中断与写作的联系。

作家的思想

  • 📌 你的内心状态是决定你能否成为成功的作家的关键。你对自己、写作和周围世界所持的信念将决定你的成败。思想是创造性的。你头脑中的思想,无论是有意识的还是无意识的,都会在生活中产生明显后果。心有所想,才能事有所成。

关于时间的几点思考

  • 📌 人们谈论稀缺性问题时——比如时间的稀缺或金钱的稀缺——往往言不由衷。“我没时间”意味着“我选择把时间花在别的事上”。同样,“我买不起”的意思是“我宁愿花钱干别的事”。

布丁好不好,吃了才知道

  • 📌 我猜,本培训班使人们更容易朝自己原本所想的目标迈进。我觉得所有这种性质的转型体验皆如此,它们帮助你成为内心深处真正的自己,让你记住那些你很久以前就知道但不知何故忘记的伟大真理。

李诞脱口秀工作手册

  • 2024年12月阅读书摘
  • 书名: 李诞脱口秀工作手册
  • 作者: 李诞
  • 简介: 李诞分享创作经验!创意是智力活儿,也是体力活儿,归根结底是苦力活儿!这是李诞写给所有创意工作者的一本工作手册。在本书中,李诞毫无保留地分享了从业以来所有的创作心得,既有具体的创作方法,写逐字稿、天天写稿,也有对待工作的态度,你的全部人生都理应要为你的创作提供养分。除此之外,他还悉心解答了创意工作者在创作、工作过程中遇到的困惑与难题。这些方法、习惯成就了李诞,这本书也会让你收获信念和实实在在的方法。翻开本书,像李诞一样熬出创意,磨出灵感!
  • 出版时间 2021-08-01 00:00:00
  • ISBN: 9787559461186
  • 分类: 个人成长-沟通表达
  • 出版社: 江苏凤凰文艺出版社

2 其次,这是一份和生活分不开的工作

  • 📌 越早丧失看剧的乐趣,越早发现观察剧的乐趣,越早成为一个合格的以创作为生的人。放弃看的乐趣,享受观察的乐趣——这样的测验不只在看剧时,在面对一切时都一样。

7 表演的目的始终是让人相信

  • 📌 关键是还原,不是表演。是还原你的观察,你的想象,不是表演你超强的模仿能力,肢体协调——那些都是技巧。

9 小情境与大情境

  • 📌 不是你去找观众,而是要让观众来找你。狠狠地操练自己,你越强,找到你的观众越多。而不是你能讨好的人越多,你的观众越多。

10 只有情绪是真的

  • 📌 工作是不能靠灵感的。专业人士追求的不是流星的高光,而是不管刮风下雨,都能有一点点收成。

12 比永恒更难的难题:到底什么是风格?

  • 📌 做一个真诚的人,尽可能善良,不掩藏痛苦,也不羞涩于快乐,放心地把自己交给舞台,交给同伴,交给世界,那怎么找都找不到的风格,也许就会来找你。

脱口秀 问与答

  • 2024年12月阅读书摘

    📌 眼高手低嘛,创作最忌讳的就是眼高手低。你眼高是个很好的事,不要因为眼高去惩罚自己。或者说,眼高手低是个客观现实,如果你是一个创作者,你大概率就是个眼高手低的人。所以,你接受就好了。你写出来的永远达不到你的眼高,那你就不写了吗?一点点去改嘛。你不开始,没有那篇烂的,后面什么都不会有的。尼尔·盖曼(Neil Gaiman)也说过类似的话,就是写作的时候,可能先写出来30万字,这30万字都是垃圾,但那30万字就是为了你后面真正要写的20万字作准备。

  • 📌 就比如,你在喝雪碧,如果是相声,很可能就会说“喝雪碧,那你得兑辣椒油啊,加完辣椒油还得泡面啊。那天啊,一大爷就直接……”这是很好笑的,我也爱听。但脱口秀演员讲这个就不行,但很多时候线下有些演员就是在讲这种东西。你真要讲,应该这么说,“昨天下午我和我同事在一块,我喝矿泉水,她喝雪碧,我说你怎么会喝雪碧呢,你放弃了生活了吗?”这是脱口秀。

生活在低处

  • 2024年12月阅读书摘
  • 书名: 生活在低处
  • 作者: 胡安焉
  • 简介: “和绝大多数人一样,我只是一个普通人,至少在四十岁之前,做过的都是再普通不过的工作,经济收入还拖了人均收入的后腿;从来没有人用‘优秀’来形容过我,也没有人真正关心我的内心世界。”
  • 出版时间 2024-08-01 00:00:00
  • ISBN: 9787572619830
  • 分类: 文学-散文杂著
  • 出版社: 湖南文艺出版社

自序 普通的事物

  • 📌 文学和哲学一样,无法直接应用于现实,它不负责解决实际问题,否则它将是极其低效的一种手段;但是文学可以影响人,这种影响并非即时和具体地发生,而是以一种更根本和深远的方式。

第一章 童年,暨我的家庭史

  • 📌 我站在餐厅外面,如果说往事在我脑海里一幕幕闪现,似乎是一种过于文艺乃至庸俗的修辞,可事实就是这么文艺乃至庸俗:往事在我脑海里一幕幕闪现。和家人一起喝早茶是一件快乐的事情,尤其是在我还小的时候。但此刻怀念这件快乐的事情,让我非常难过……

  • 📌 我发现自己有个奇怪的特点:当我遇到比自己更腼腆和内向的人时,我总是想消除这种差距,于是我会话多起来,甚至开始搞怪,去逗对方开心;但当我遇到比自己活泼和外向的人时,我却会变得拘谨和话少,似乎我希望巩固这种差距。

第二章 我为什么写作

  • 📌 这里面没有丝毫偶然的成分,她们并不冒昧而我也无须意外。这就是命运。或者说,命运常常给人这种感觉:就像我们并不是我们自己,而只是在扮演我们的一群演员。

6 出路

  • 📌 于是我又尝试为自己的写作辩护,试图给自己感兴趣的写作方向寻找观念依据。然而那些辩护是徒劳且缺乏说服力的,就像一个站在暴雨中的人,举起双手去遮挡雨水,终究还是免不了浑身湿透。在这种情况下,唯一可以在精神上支撑我、帮助我克服羞耻心的,就只有真诚了。无论是对待写作还是对待交流,我都竭尽所能地做到真诚。当我自觉真诚的时候,我就问心无愧、理直气壮,不再犹豫和畏缩,也不再害怕任何人的取笑,哪怕那些取笑只存在于我的想象之中。

7 非虚构

  • 📌 但话又说回来,内向的性格也有利于写作的方面。比如说,它给我的生活增加了很多阻力,这些阻力散布在日常的时时刻刻、方方面面,而人总是在受挫后才开始反思,并去寻因溯源——我遭遇的挫折越多,反思面也就越大,对自己的认识就越加全面和深入。

  • 📌 就我所见,一个人自己写自己,很容易留下虚妄之言,在真实的自己和理想的自己之间,绝大多数人无法始终保持清醒,恐怕我也不能例外。

第三章 活着,写着

  • 📌 “我实在地告诉你们,一粒麦子不落在地里死去,它仍然是一粒麦子。若它落在地里死去,就会结出许多麦子。”(《约翰福音》12:24)

日常记

  • 📌 此外我还会犯各种各样的错误,我自己清楚为什么会犯那些错误,但别人听了会觉得莫名其妙。不过我好像宁愿把菜做坏也不愿意买好点的食材以及稍微多放点油。我也不知道是什么苦难把我塑造成这样,实际上我什么苦难都没有经历过。或许我的问题归根结底是对吃这件事不讲究——我似乎不觉得吃好一点有多么重要。我对美食缺乏真正的热爱,对人生的态度好像也差不多。尽管我经常告诉别人什么好吃和什么不好吃,但在心里我其实觉得区别不大。

内心记

  • 📌 生物本来就是这个样子。只有对此不满足的人,或在“活着”这条路上走得磕磕绊绊且痛苦万分的人,才会感到精神追求的必要,才会思考人生的意义。而这种思考没有所谓的正确答案,它是务虚的、抽象的;它不是达到某个目的的手段,它就是目的本身。对人生没有这样的感悟,读诗就没有意义。诗不能满足人的基本需要,只能满足人的终极需要。

  • 📌 其实我也爱生活,但我不爱你们的生活,而你们总想否定我的生活,用你们的“生活”掠夺生活——因为你们睥睨我和你们不一样的方面,所以我憎恨我和你们一样的方面——有时候我也不知道界线在哪里:我不能判断,不敢求证,更不懂应对——只有迫使自己站到高处,我才获得孤独和痛苦的安全感。

  •  

2024年11月阅读书摘

✇Dennis
作者Domon

11月阅读记录

  • 《格里格外》Done
  • 《一生之敌》Done
  • 《我用中文做了场梦》Done
  • 《我们为什么要睡觉》48%
  • 《一团坚冰》36%
  • 《为你的生活写作:布洛克小说一日课》51%

11月阅读书摘

一生之敌

  • 2024年11月阅读书摘
  • 书名: 一生之敌
  • 作者: 史蒂文·普莱斯菲尔德
  • 简介: 我写了17年才赚到第一分钱(一个从未制作过的剧本的3500美元稿费)。
    我写了27年才出版了我的第一本小说(《重返荣耀》)。
    在那段时间里,我在11个州做了21份不同的工作。
    我在学校教书,开拖拉机拖车,做广告,在好莱坞做编剧,在海上石油钻井平台工作,像移民工人一样摘水果。
    有一个季节我住在这所房子里。它没有电,没有水,没有门,没有窗户。租金是每月15美元。
    在这段时间里,我一直在写作。
    我为什么要告诉你这些?
    因为这本书是为了能让你从我的错误中学习。
    因为这本书是为了让你避免那些我在成为作家之前所陷入过的所有绝境。
2024年11月阅读书摘

你本该成为一名画家、企业家、运动员;你本该从昨天开始穿上跑鞋、拿起画笔、出门旅行。你屈服于止痛药、流言蜚语和手机成瘾,你本该成为的人只存在于夜深人静时的幻想。你屈服于这位一生之敌,浪费着自己的天赋。作为一个曾跌入谷底的过来人,作者普雷斯菲尔德以其毕生的经验,为我们总结了造成这些困境的根源——内阻力,并指出这位敌人是我们过上另一种人生面临的zui大挑战,而战胜它的方法是付诸行动,成为职业选手。本书为每一个人而写,尤其是创作者、创业者、拖延症患者、浪费天赋、缺乏行动力犹豫不决的人群,作者在给出一记响亮耳光的同时,也给予了无比慷慨真诚的鼓励。

  • 出版时间 2024-07-01 00:00:00
  • ISBN: 9787553530215
  • 分类: 个人成长-励志成长
  • 出版社: 上海文化出版社

我所知道的

  • 📌 难的并不是写作。而是坐下来开始写。

内阻力会招募盟友

  • 📌 通常,夫妻或亲密的朋友,甚至整个家庭,都会进入一种心照不宣的协议模式,每个人都会(不知不觉地)尽力确保自己和所有亲朋好友困在同一个坑里,他和他所有的亲友就那样躺在坑里,无比舒适。一只螃蟹所能犯下的最严重的叛国罪,就是跳上桶口的边缘。

内阻力与拖延

  • 📌 拖延是内阻力最常见的表现,因为它最容易被合理化。我们不会坦然承认,“我永远不会动笔去写我的交响乐。”相反,我们会说:“我会写的——只是明天再开始。”

内阻力与自我治疗

  • 📌 我曾在纽约一家大型广告公司担任文案。那时,老板常常对我们说:去发明一种疾病。他说,只要造出个病来,我们就可以卖药了。

内阻力与不快乐

  • 📌 作为艺术家和专业人士,我们有义务发动我们自己内在的革命,在我们自己的头颅里掀起一场属于个人的起义。在这次起义中,我们要摆脱消费主义文化的暴政。我们要推翻广告、电影、游戏、杂志、电视和MTV编定的程序,这些东西从摇篮时期就开始催眠我们。我们拔掉电网插头,因为我们已经认识到,把可支配收入贡献给“狗屎股份有限公司”来完成它的底线目标,这永远无法疗愈我们的不安,唯一的办法,只有工作。

内阻力与合理化防御

  • 📌 合理化是内阻力的代言人,会帮助它掩饰藏在身后的大棒。它不展示我们的恐惧(这可能会让我们感到羞耻,进而促使我们去做我们的工作),而是为我们提供了一系列看似中肯、合理的正当理由来解释,为什么我们不应该去做我们的工作。

职业选手不找借口

  • 📌 他尊重内阻力。他知道,无论借口多么合理,只要今天屈服了,明天屈服的可能性就会是今天的两倍。

接近神秘

  • 📌 因为,只要我们能够每天都坐下来,不断地“磨”,一些神秘的事情就会开始发生。齿轮开始转动。在这个过程中,上天必定、必然会对我们伸出援手。会有看不见的力量加入我们的事业进程,会有意想不到的好运加持我们最终的目标。

我用中文做了场梦

  • 2024年11月阅读书摘
  • 书名: 我用中文做了场梦
  • 作者: 亚历
  • 简介: 《我用中文做了场梦》是意大利青年作家亚历用中文写下自己六年中国漫游的非虚构文学作品。
    2016年,23岁的毕业生亚历在衰老的意大利看不到出路,决心投奔冉冉升起的电影制作热土——中国。他来到北京电影学院学导演,出演瓜子和手机广告,在主旋律战争片中当46号群演,用蹩脚的普通话录电影播客,给纪录片当翻译,也参与过地下独立电影制作。
    六年间,亚历从零开始学中文,在豆瓣上写日记,在大城小镇与不同的人对话,在每一次微小的相遇中见证中国的广阔:在北京,和宿管阿姨学习怎么切菜;在广州拍广告,开工前喝早茶,拍完片喝断片;在上海,把客厅当成写作沙龙,创造一个临时的家;在四川农村,把白jiu当成暖气,跨越寒冬和方言的隔阂。
    亚历用冷静又不乏幽默的文字,记录自己在中国的观察和日常,书写近年的个体遭遇和时代变化,也写下无论全世界青年人共同面对的时代情绪和现实困境:在失序且孤独的时代,勇于拥抱生活的不确定,保持流动,渴望自由,跨过隔阂,与人连接。
  • 出版时间 2024-06-01 00:00:00
  • ISBN: 9787549642632
  • 分类: 文学-散文杂著
  • 出版社: 文汇出版社

前言

  • 📌 从差不多十岁的年纪,写作就是我最靠谱的朋友。写作能解答我的疑惑,挖掘我的感受,带来新的结论。它在我的生活中是一个很低调的存在:有时候,它会放你走,让你该忙忙、该玩玩,不会限制你的活动。它不急,因为知道你迟早不得不坐下来面对那张空纸慢慢说事。我试过忘记自己有这样的精神义务,却次次都回到了电脑前,仿佛被某种无形的力量所吸引。这是我的命运,和它较劲完全无效,我只能常年接受写作的召唤。

来中国才是正经事

  • 📌 中国的电视剧能创造一种独特的既和生活有关,又不反映现实的平行世界:现实中,没有那么多摆在房间各角落显眼的酸奶盒。

过日子的老外

  • 📌 这个世界无疑简单,有时狭窄又无聊,却熟悉到令人欣慰。

  • 📌 生活在别处,不熟悉的一切是每一天的挑战。我们选择抗拒和怨恨,还是包容和好奇,会决定我们的生活体验。我要努力做后者;如果发现做不到,就回家。

北京,北京

  • 📌 我感觉自己是一名到处抵抗邪恶寒气的孤独的战士。我没有明显的疾病,却陷入了深深的危机感。

  • 📌 我们的生活在平行的轨道上进行,却总是有交叉。

和人交流

  • 📌 说起各种疫情前的习惯,好像说的不是自己,而是曾经活着的某个人。

海边的老师

  • 📌 我们产生情感交流的速度远远超出我的想象。我以为年龄差距和生活经历的截然不同会阻挡双方对彼此的深度理解,像成年人一样,待在自己的同温层并排斥和层外的世界交流,似乎听什么播客可以决定你和他人日后交往的可能性。但是学生立刻习惯了我的存在,用简单的一句“老师再见”将我纳入他们的日常之中。那些我以为会成为沟通障碍的因素,反而促进了我们的交流。我们对彼此没有任何预设,像是在没有地图的情况下去探索一片未知的土壤。我适应了学生的思维——周日下午那些既能引用《愚公移山》,又能讲到在洛杉矶生活的东北博主的作文。在去食堂的路上遇到学生打招呼会给你一种归属感。我原本觉得自己只是来这里体验、观察、了解,却很快就动了心。

何处才是家?

  • 📌 我尽量推迟做决定,在漂泊的自在中躲避。北京像是一个前任,有美好的回忆,但是是回不去的。它又像一段离不成的婚姻,总以某些借口牵绊住你。我们已经分居了:我在上海参加电影节,出门在外有快一个月了;它在家等着,保管我的行李,默认我早晚要回来。

  • 📌 冰箱中看似多余的草本植物会营造一种家的感觉。在这一点上,我已经脱离了现实。我需要的不是草本植物,而是它们的氛围。我用实实在在的金钱去买感觉。也许只有当下的自己才会懂,一个没有罗勒叶的家有多么莫名其妙。

花园坊的春天

  • 📌 我特别珍惜自由职业所带来的安静,它让我回归社会的观众席,欣赏日常工作间隙的小剧场。

  • 📌 这个群确实改变不了世界。不过,在失序的生活中,小社群的存在给每个人带来某种更坚固的依靠。不管是用来发泄无力感或不解,表示无奈或迷茫,或只是为了求食用油,它说的是:在花园坊,你不是一座孤岛。

流动中的人

  • 📌 “格林豪泰酒店-大柏树店”,我这才意识到自己在哪里。加上摆在地上的洗衣篮,“大柏树”那三个字把我带回一个已经没落的时代。上下班、创业园区、奶茶、烧烤、火锅店、出差、同事、酒醉、开房。白色的防护服和棉签将我们虚无主义的人生一扫而尽。我们在这里从罪恶中赎回自己,怪不得不需要付房费。那是曾经的世界。现在,不用交钱。要交上被成功净化的灵魂。

唐先生的故事

  • 📌 在酒席上遇到年轻人时,我心里会渴望这种事情发生——一种抛开过节的场合规则、作为同龄人的精神连接,能让我们聊点彼此的想法和感受。他们有时候会坐我对面,但是一次又一次敬酒的节奏容不下字面意义上的闲聊。
  •  

中间代码生成 I

中间代码

中间代码是介于源代码和目标代码之间的一种代码形式,它既不依赖于具体的编程语言,也不依赖于具体的目标机。

  • 对不同的程序语言进行编译时,可以采用同一种形式的中间代码。
  • 同一种形式的中间代码,可以转换成不同目标机的目标代码。
  • 在中间代码上可以进行各种不依赖于目标机的优化,这些优化程序可以在不同的程序语言和不同的目标机的编译程序中重复使用。

编译器前端的逻辑结构:

image-20241118103823890

中间代码的表示形式

  • 抽象语法树(AST)
  • DAG(Directed Acyclic Graph 有向无环图)
  • 后缀式(也称逆波兰表示)
  • 三地址代码

AST

AST 抽象语法树的生成方式同前文章所述。

image-20241118104004274

DAG

image-20241118104025778

和 AST 的区别:尽可能的复用相同的节点。

这点在翻译上亦有体现:在产生表达式 DAG 的翻译方案中,每次调用 Leaf()Node() 的构造函数时,要检查是否已存在相同结构的节点,如果存在,则返回找到的已有节点,否则构造新节点(但这在下表中不体现,所以看上去和 AST 没有区别)。

$$ \begin{array}{|l|l|} \hline \text{产生式} & \text{语义动作} \ \hline E \to E_1 + T &{ E.\text{node} = \text{new Node (“+”}, E₁.\text{node}, T.\text{node}); } \ E \to T &{ E.\text{node} = T.\text{node}; } \ T \to T_1 * F &{ T.\text{node} = \text{new Node (“*”}, T₁.\text{node}, F.\text{node}); } \ T \to F &{ T.\text{node} = F.\text{node}; } \ F \to (E) &{ F.\text{node} = E.\text{node}; } \ F \to id &{ F.\text{node} = \text{new Leaf(ID, id.name); } } \ F \to num &{ F.\text{node} = \text{new Leaf(NUM, num.val); } } \ \hline \end{array} $$

由于这个检查的存在,DAG 生成效率降低了,但是运行效率提高了。

三地址代码

基本形式:

$$ x = y \text{ op } z $$

类别:

  1. 一元运算:$ x = \text{op } y $,$\text{op}$ 是一元运算符,如一元减、逻辑非等。

  2. 复制指令:$ x = y $

  3. 无条件跳转:$ \text{goto } L $

  4. 条件跳转:$ \text{if } x \text{ goto } L $ ($x$ 为真时跳转)或 $ \text{if } \text{False } x \text{ goto } L $ ($x$ 为假时跳转)

  5. 条件转移:$ \text{if } x \text{ ROP } y \text{ goto } L $,仅当 $x \text{ ROP } y$ 成立时跳转

    $\text{ROP}$ 是关系运算符,包括 $<$、$\leq$、$>$、$\geq$、$==$、$!=$ 等

  6. 参数传递与函数调用:

    1. 首先使用 $ \text{param } x_1$、 $\text{param } x_2$ $\cdots$ $\text{param } x_n $ 传递参数
    2. 然后使用 $ \text{call } p, n $ 调用函数,其中 $p$ 是函数名,$n$ 是参数个数
  7. 数组与地址操作

    1. 把数组元素 $y[z]$ 的值赋给 $x$:$x = y[z]$
    2. 把 $z$ 的值赋给数组元素 $x[y]$:$x[y] = z$
    3. 把 $y$ 的地址赋给 $x$:$x = &y$
    4. 把 $y$ 值为地址的存储空间的值赋给 $x$:$x = *y$
    5. 把 $y$ 值赋给 $x$ 值为地址的存储空间:$*x = y$

示例 1

语句

do i = i + 1; while (a[i] < v);

符号标号

L:  t1 = i + 1
    i = t1
    t2 = i * 8
    t3 = a[t2]
    if t3 < v goto L

位置号

100: t1 = i + 1
101: i = t1
102: t2 = i * 8
103: t3 = a[t2]
104: if t3 < v goto 100

三地址代码具体实现

对于表达式

$$ a = b * -c + b * -c $$

其三地址代码可以表示为如下几种方式。

四元式表示

$$ \begin{array}{|c|c|c|c|c|} \hline \text{inst} & \text{op} & \text{arg1} & \text{arg2} & \text{result} \ \hline (0) & \text{uminus} & c & & t_1 \ (1) & * & b & t_1 & t_2 \ (2) & \text{uminus} & c & & t_3 \ (3) & * & b & t_3 & t_4 \ (4) & + & t_2 & t_4 & t_5 \ (5) & \text{assign} & t_5 & & a \ \hline \end{array} $$

三元式表示

$$ \begin{array}{|c|c|c|c|} \hline \text{inst} & \text{op} & \text{arg1} & \text{arg2} \ \hline \text{(0)} & \text{uminus} & c & \ \text{(1)} & * & b & (0) \ \text{(2)} & \text{uminus} & c & \ \text{(3)} & * & b & (2) \ \text{(4)} & + & (1) & (3) \ \text{(5)} & \text{assign} & a & (4) \ \hline \end{array} $$

注:三元式中可以使用指向三元式语句的指针来表示操作数。

间接三元式表示

$$ \begin{array}{|c|c|} \hline \text{address} & \text{inst} \ \hline \text{0} & \text{(0)} \ \text{1} & \text{(1)} \ \text{2} & \text{(2)} \ \text{3} & \text{(3)} \ \text{4} & \text{(4)} \ \text{5} & \text{(5)} \ \hline \end{array} $$

$$ \begin{array}{|c|c|c|c|} \hline \text{inst} & \text{op} & \text{arg1} & \text{arg2} \ \hline \text{(0)} & \text{uminus} & c & \ \text{(1)} & * & b & (0) \ \text{(2)} & \text{uminus} & c & \ \text{(3)} & * & b & (2) \ \text{(4)} & + & (1) & (3) \ \text{(5)} & \text{assign} & a & (4) \ \hline \end{array} $$

间接三元式:三元式表 + 间接码表。

间接码表是一张指示表,按运算的先后次序列出相关三元式们在三元式表中的位置。

这样,修改语句顺序的时候,只需要修改间接码表,而不需要修改三元式表,方便优化。

不同表示方法的对比

  • 四元式需要利用较多的临时单元,四元式之间的联系通过临时变量实现
  • 中间代码优化处理时,四元式比三元式更为方便
  • 间接三元式与四元式同样方便,两种实现方式需要的存储空间大体相同

静态单赋值(SSA)

SSA(Static Single Assignment):每个变量在 SSA 形式中只赋值一次,每次赋值都对应一个不同的变量名。

示例

转换前:

p = a + b
q = p - c
p = q * d
p = e - p
q = p + q

转换后:

p1 = a + b
q1 = p1 - c
p2 = q1 * d
p3 = e - p2
q2 = p3 + q1

Phi 函数

作用:在不同路径中对同一个变量赋值时,使用 $\phi$ 函数来合并不同的赋值。

示例:

if (flag) x = -1;
else x = 1;
y = x * a;

转换前:

$$ \begin{array}{l} \text{if(flag)} \ \quad x = -1; \ \text{else} \ \quad x = 1; \ y = x * a; \ \end{array} $$

转换后:

$$ \begin{array}{l} \text{if (flag)} \ \quad x1 = -1; \ \text{else} \ \quad x2 = 1; \ x3 = \phi(x1, x2); \ y = x3 * a; \ \end{array} $$

类似于 ICS 中学到的条件转移 cmov 指令。

类型和声明

类型检查(Type Checking):利用一组规则来检查运算分量的类型和运算符的预期类型是否匹配。

语言类型

  • 类型化的语言:变量都被给定类型的语言。

    特点:表达式、语句等语法构造的类型都是可以静态确定的。

    例如,类型为 boolean 的变量 x 在程序每次运行时的值只能是布尔值,not(x) 总有意义。

  • 非类型化的语言:不限制变量值范围的语言。

    特点:一个运算可以作用到任意的运算对象,其结果可能是一个有意义的值,一个错误,一个异常或一个语言未加定义的结果。

类型表达式

类型表达式 (Type Expression):用来表示源程序中变量、常量、表达式、语句等语言成分的类型。

种类:

  • 基本类型:$\text{boolean}$, $\text{char}$, $\text{integer}$, $\text{float}$ 等
  • 类名
  • 数组类型:$\text{array}$
  • 记录(结构)类型:$\text{record}$
  • 函数类型:$\text{s} \rightarrow \text{t}$ 从 $\text{s}$ 到 $\text{t}$ 的函数表示为 $\text{s} \rightarrow \text{t}$
  • 笛卡尔积:用 $\times$ 表示列表或元组(例如函数参数)
  • 指针类型
  • 类型表达式的变量

类型表达式的例子

C 语言的类型:

struct {
    int no;
    char name[20];
}

类型表达式为:

$$ \text{record} \left( (\text{no} \times \text{integer}) \times (\text{name} \times \text{array} (20, \text{char})) \right) $$

类型等价 (Type Equivalence)

类型等价:两个类型的值集合相等并且作用于其上的运算集合相等。

特点:具有对称性

种类:

  • 按名字等价:两个类型名字相同,或者被定义成等价的两个名字
  • 按结构等价:两个类型的结构完全相同,但是名字不一定相同

按名字等价一定是按结构等价的。

类型兼容 (Type Compatibility)

类型兼容:两个类型可以替换而不会引起类型错误。

注意,其是针对某种运算而言,而且类型相容 不具有对称性

比如,在有的语言中,整型类型对实型值运算与实型类型相容。

即允许把整型的值赋给实型变量,但不允许把实型的值赋给整型变量。

声明语句

文法:

$$ \begin{array}{l} D \rightarrow T \ id \ ; \ D \ | \ \varepsilon \ T \rightarrow B \ C \ | \ \text{record} \ \text{“{”} \ D \ \text{“}”} \ B \rightarrow \text{int} \ | \ \text{float} \ C \rightarrow \varepsilon \ | \ [num] \ C \ \end{array} $$

含义:

  • D 生成一系列声明(Declaration)
  • T 生成不同的类型(Type)
  • B 生成基本类型 int/float
  • C 表示分量,生成 [num] 序列

注意 record 中用 D 嵌套表示各个字段的声明。

字段声明和变量声明的文法一致。

局部变量的存储布局

  • 变量的类型可以确定变量需要的内存(即类型的宽度)
  • 可变大小的数据结构只需要考虑指针
  • 函数的局部变量总是分配在连续的区间
  • 给每个变量分配一个相对于这个区间开始处的相对地址,变量的类型信息保存在符号表中

计算 T 的类型和宽度的 SDT

综合属性:$\text{type, width}$

全局变量 $t$ 和 $w$ 用于将类型和宽度信息从 $B$ 传递到 $C \to \varepsilon$

相当于 $C$ 的继承属性,因为总是通过拷贝来传递,所以在 SDT 中只赋值一次。

也可以把 $t$ 和 $w$ 替换为 $C.\text{type}$ 和 $C.\text{width}$(继承属性)

$$ \begin{array}{|ll|} \hline \text{产生式} & \text{动作} \ \hline T \to B & { t = B.\text{type}; w = B.\text{width}; } \ \phantom{ T \to \ } C & { T.\text{type} = C.\text{type}; T.\text{width} = C.\text{width} } \ \hline B \to \textbf{int} & { B.\text{type} = \text{integer}; B.\text{width} = 4; } \ B \to \textbf{float} & { B.\text{type} = \text{float}; B.\text{width} = 8; } \ \hline C \to \varepsilon & { C.\text{type} = t; C.\text{width} = w; } \ C \to [ \textbf{num} ] C_1 & { C.\text{type} = \text{array}(\textbf{num.value}, C_1.\text{type}); } \ & { C.\text{width} = \textbf{num.value} \times C_1.\text{width}; } \ \hline \end{array} $$

例子:

image-20241118143537446

💾

  •  

语法制导翻译 III

基础属性文法(SDD)

(后文中均以此为例说明)

这是一个 while 的常见语法:

$$ S \rightarrow \text{while} (C) \ S_1 $$

这里:

  • $S$ 是生成各种语句的非终结符号,我们假设这些语句包括 $\text{if}$ 语句、赋值语句和其他类型的语句
  • $C$ 表示一个条件表达式,也即一个值为真或假的布尔表达式

这个 $\text{while}$ 语句的含义是首先对条件表达式 $C$ 求值。

  • 如果 $C$ 是真,控制就转向 $S_1$ 的代码开始处(循环体)
  • 如果 $C$ 的值为假,那么控制就转向跟在这个 $\text{while}$ 语句的代码之后的代码(循环结束)

我们还必须设计 $S_1$ 的代码,使得它在结束的时候能够跳转到这个 $\text{while}$ 语句的代码开始处(也即 $C$ 处),继续下一轮循环条件判断。

为此,我们生成一些形式为 $\text{label } L$ 的指令,其中 $L$ 是一个标识符。这个指令表明后一句指令的标号是 $L$,这会方便我们定位语句。

从而,我们得到这个 L 属性 SDD (回忆:SDD 是上下文无关文法和属性 / 规则的结合)的属性计算:

$$ \begin{array}{|l|l|} \hline \text{产生式} & \text{语义规则} \ \hline S \rightarrow \text{while} (C) \ S_1 & L1 = \text{new()} \ & L2 = \text{new()} \ & S_1.\text{next} = L1 \ & C.\text{false} = S.\text{next} \ & C.\text{true} = L2 \ & S.\text{code} = \text{label} \ || \ L1 \ || \ C.\text{code} \ || \ \text{label} \ || \ L2 \ || \ S_1.\text{code} \ \hline \end{array} $$

(注:有一说一,第一次看这个的时候很迷惑,但是如果你写完了 lab 的 lv6、lv7 之后你会发现毫无难度)

我们使用下面的属性来生成正确的中间代码:

  1. 继承属性 $S.\text{next}$ 是必须在 $S$ 执行结束之后执行的代码的开始处的标号,在调用这个产生式推导之前就已经有了
  2. 综合属性 $S.\text{code}$ 是中间代码的序列,它实现了语句 $S$
  3. 继承属性 $C.\text{true}$ 是在 $C$ 为真时执行的代码的开始处的标号
  4. 继承属性 $C.\text{false}$ 是在 $C$ 为假时执行的代码的开始处的标号
  5. 综合属性 $C.\text{code}$ 是一个中间代码的序列,它实现了条件表达式 $C$

转换上述 SDD 为 SDT 的语义动作:

$$ \begin{array}{|r|l|} \hline \text{产生式} & \text{语义动作} \ \hline S \rightarrow \text{while} ( & {L1 = \text{new()}; L2 = \text{new()}; C.\text{false} = S.\text{next}; C.\text{true} = L2; } \ C) & { S_1.\text{next} = L1; } \ S_1& { S.\text{code} = \text{label} \ || \ L1 \ || \ C.\text{code} \ || \ \text{label} \ || \ L2 \ || \ S_1.\text{code}; } \ \hline \end{array} $$

注意,这里把原先的语义动作分开后插入到了产生式中。

为什么要这么做?

因为我们需要先根据依赖关系计算需要用到的属性,然后进行代码生成。

$L1 = \text{new()}; L2 = \text{new()}$:存放了在代码片段需要的标号(函数 $\text{new()}$ 生成了新的标号),后续定位代码时需要用到

  • $L1$ 存放了条件判断语句(同时也是 $\text{while}$ 语句)的开始标号,每次循环体 $S_1$ 结束时需要跳转至此
  • $L2$ 存放了 $S_1$ 的开始标号,当 $C$ 为真时需要跳转至此

这里在展开下一步对应的表达式前,设置了两个继承属性:

  • $C.\text{false} = S.\text{next}; C.\text{true} = L2$:计算 $C$ 的继承属性,$C$ 为真 / 假时应该跳转到哪里
  • $S_1.\text{next} = L1$:计算 $S_1$ 的继承属性,$S_1$ 结束后需要跳转至 $L1$

类似我们现在正在展开的 $S$,以上继承属性,在即将展开 $C$、$S_1$ 的时候会用到。

这样,再递归的调用 $C$ 和 $S_1$ 的语义动作,就可以生成正确的综合属性(中间代码) $C.\text{code}$ 和 $S_1.\text{code}$。

在最后,我们已经完成了 $S$ 产生式体内所有计算综合属性 $S.\text{code}$ 所需要的属性,从而可以在这个产生式的最后生成综合属性(中间代码) $S.\text{code}$。

L 属性 SDD 的实现方法

递归下降函数法

使用递归下降的语法分析器,为每个非终结符建立一个函数,在函数中计算属性。

$$ \begin{array}{l} \textbf{string } S(\text{label next}) { \ \quad \textbf{string } Scode, Ccode; \quad /* 存放代码片段的局部变量 / \ \quad \text{label } L1, L2; \quad / 局部标号 / \ \quad \textbf{if} ( 当前输入 == 词法单元\textbf{while} ) { \ \quad \quad 读取输入; \ \quad \quad 检查 \text{“(”} 是下一个输入符号, 并读取输入; \ \quad \quad L1 = \textbf{new}(); \ \quad \quad L2 = \textbf{new}(); \ \quad \quad Ccode = C(\text{next, L2}); \quad / 当条件为真时跳转为 S 最开始的 next 标号,否则跳转至 L2 / \ \quad \quad 检查 \text{“)”} 是下一个输入符号, 并读取输入; \ \quad \quad Scode = S(L1); \ \quad \quad \textbf{return}(\text{“label”} | L1 | Ccode | \text{“label”} | L2 | Scode); \ \quad } \ \quad \textbf{else} { / 其他语句类型 */ } \ } \end{array} $$

类似上述展开方式,只不过我们将 “展开” 这一动作实现为了函数调用,用中间值 $Scode$ 和 $Ccode$ 来存放递归调用得到的中间代码片段。

对于 $C()$ 和 $S()$ 的调用,我们将其所需要的继承属性作为参数传递过去。

递归下降、边扫描边生成(On-the-fly)

使用递归下降的语法分析,边扫描边生成代码(on-the-fly)。

递归下降函数法存在的问题:当属性值很大时,对属性值进行运算的效率很低

我们总需要用中间值 $Scode$ 和 $Ccode$ 来存放递归调用得到的中间代码片段,而且最后会返回 $S.\text{code}$。这些中间值可能是一个上百 KB 的串,对其进行并置等运算会比较低效。

所以,我们可以逐步生成属性的各个部分,并增量式添加到最终的属性值中。

可行性条件

  • 存在一个 主属性,且主属性是综合属性
  • 在各个产生式中,主属性是通过产生式体中各个非终结符号的主属性 连接(并置) 得到的。同时还会连接一些其它的元素
  • 各非终结符号的主属性的连接顺序和它在产生式体中的顺序相同

此时,只需要在适当的时候 “发出(emit)” 非主属性的元素,即把这些元素拼接到适当的地方就可以了。

人话:就是分开生成,只要生成顺序正确,那么最后结果也是正确的,和 lab 里一样。

举例

产生式:$S \to \text{while} (C) , S1$,目标 $S.code = \text{Label} , || , L1 , || , C.code , || , \text{Label} , || , L2 , || , S1.code$

SDT: $$ \begin{array}{|r|l|} \hline \text{产生式} & \text{语义动作} \ \hline S \rightarrow \text{while} ( & {L1 = \text{new()}; L2 = \text{new()}; C.\text{false} = S.\text{next}; C.\text{true} = L2; \text{print}(\text{“label”}, L1); } \ C) & { S_1.\text{next} = L1; \text{print}(\text{“label”}, L2); } \ S_1& \ \hline \end{array} $$

继续推导 $C$ 和 $S_1$ 的时候,会自动生成相应的代码,并且其继承属性已经提前设置。

为了避免刚才说的问题,我们可以在处理 $S$ 时,先调用 $C$,再调用 $S$(对应于 $S_1$)。

如果各个函数把属性 $code$ 打印出来,我们处理 $\text{while}$ 语句时,只需要:

  1. 先打印 $\text{Label} , L1$
  2. 再调用 $C$(打印 $C$ 的代码)
  3. 再打印 $\text{Label} , L2$
  4. 再调用 $S$(打印 $S1$ 的代码)

对于当前这个规则而言,只需要处理 1、3,即打印 $\text{Label} , L1$ 和 $\text{Label} , L2$,2、4 在 $C()$ 和 $S()$ 中处理。

$$ \begin{array}{l} \textbf{string } S(\text{label next}) { \ \quad \text{label } L1, L2; \quad /* 局部标号 / \ \quad \textbf{if} ( 当前输入 == 词法单元\textbf{while} ) { \ \quad \quad 读取输入; \ \quad \quad 检查 \text{‘(’} 是下一个输入符号, 并读取输入; \ \quad \quad L1 = \textbf{new}(); \ \quad \quad L2 = \textbf{new}(); \ \quad \quad print(\text{“label”} | L1); \ \quad \quad C(\text{next, L2}); \quad / 当条件为真时跳转为 S 最开始的 next 标号,否则跳转至 L2 / \ \quad \quad print(\text{“label”} | L2); \ \quad \quad 检查 \text{‘)’} 是下一个输入符号, 并读取输入; \ \quad \quad S(L1); \ \quad } \ \quad \textbf{else} { / 其他语句类型 */ } \ } \end{array} $$

注意:没有最后的 $\text{return}$ 语句了,改为分阶段 $\text{print}$。

自底向上语法分析

以 LL 文法为基础的 L 属性 SDD 可以在 LR 语法分析(自底向上) 过程中实现。

我们遵循如下的三个原则:

  1. 首先构造出 L 属性 SDD 的 SDT,这样的 SDT:

    • 在各个非终结符号之前放置语义动作来计算它的继承属性
    • 并且在产生式后端放置一个动作来计算综合属性
  2. 对 $A$ 的规则中每个内嵌的语义动作 $a$,向这个文法中引入一个标记非终结符号 $M$ 来替换它。每个这样的位置都有一个不同的标记,并且对于任意一个标记 $M$ 都有一个产生式 $M \rightarrow \varepsilon$

  3. 如果标记非终结符号 $M$ 在某个产生式 $A \rightarrow \alpha {a} \beta$ 中替换了语义动作 $a$,对 $a$ 进行修改得到 $a'$,并且将 $a'$ 关联到 $M \rightarrow \varepsilon$ 上。这个动作 $a'$:

    1. 将动作 $a$ 需要的 $A$ 或 $\alpha$ 中符号的任何属性作为 $M$ 的 继承属性 进行拷贝

      注:L 属性 SDD 保证了它计算的时候所需要的继承属性不包括右边 $\beta$ 的属性。

    2. 按照 $a$ 中的方法计算各个属性,但是将计算得到的这些属性作为 $M$ 的 综合属性

动作 $a'$ 必须设法找到相应的属性,因为产生式 $M \rightarrow \varepsilon$ 中没有 $A$ 的符号。

这个变换看起来是非法的,因为通常和产生式 $M \rightarrow \varepsilon$ 相关的动作将不得不访问某些没有出现在这个产生式中的文法符号的属性(如 $A.i$)。

然而,我们将在 LR 语法分析栈上实现各个语义动作。就像我们现在在栈中添加了 $M$ 一样,在规约到 $A$ 之前,我们亦会在栈中其前添加一个记录,其存放 $A$ 的一些属性。

所以,必要的属性总是可用的,它们位于栈顶之下的已知位置上。

所有这些属性的拷贝工作能够正确进行的原理是:所有拷贝都发生在对某个非终结符号的一次展开时创建的不同记录之间。

因此,这些记录中的每一个都知道其他各个记录在栈中离它有多远,因此可以安全地把值写到它下面的记录中。

举例 1

$$ A \rightarrow {B.i=f(A.i);}BC $$

引入 $M$ 后变为:

  • $A \rightarrow MBC$
  • $M \rightarrow \varepsilon {M.i=A.i; M.s=f(M.i);}$

以下给出对于栈结构说明的一个 基础约定(和 PPT 原文有变动)

  • 栈由各个记录组成,图中栈顶 / 栈上方在右,栈底 / 栈下方在左。
  • 每个记录包含多个域,每个域对应一个属性。

即:

    • 记录 X
      • 域 1(属性)
      • 域 2(属性)
    • 记录 Y
      • 域 1(属性)
      • 域 2(属性)

属性传递过程

  • 当执行到 $M$ 的归约时,$A.i$ 的值存放在 $M$ 记录的栈下方记录的域(当然,这个域的名字肯定不是 $A$)中
  • 如果产生式右部为 $KMBC$,那么在栈中,$M$ 记录的栈下方记录为 $K$,$K$ 的某个域中存放 $A.i$
  • $M.s$ 即 $B.i$,$M$ 记录的栈下方记录存放 $A.i$,即将归约到 $B$ 时,$B.i$ 存放在栈中归约位置的下方记录中

我觉得这里理解起来比较烦,但你可以这么想:我们随时都要确保进行一次规约之前,其所需要的继承属性都已经准备好了,所以对于 $A \rightarrow MBC$,我们未来要规约到 $A$,那么 $A$ 所需要的继承属性必然也要提前准备好,位置就在当前栈下方记录的域,这个域可以是当前产生体的一部分,比如上文的 $K$,也可能是不在产生体中的,比如还有一个 $S \rightarrow DA$,那么就会在 $D$ 记录里。

举例 2

原始规则:

$$ \begin{array}{|r|l|} \hline \text{产生式} & \text{语义动作} \ \hline S \rightarrow \text{while} ( & {L1 = \text{new()}; L2 = \text{new()}; C.\text{false} = S.\text{next}; C.\text{true} = L2; } \ C) & { S_1.\text{next} = L1; } \ S_1& { S.\text{code} = \text{“label”} \ || \ L1 \ || \ C.\text{code} \ || \ \text{“label”} \ || \ L2 \ || \ S_1.\text{code}; } \ \hline \end{array} $$

转换为:

$$ \begin{align*} S &\rightarrow \text{while}(M C) N S_1 \ M &\rightarrow \varepsilon \ N &\rightarrow \varepsilon \end{align*} $$

image-20241110130636625

按照此产生式归约,我们希望会首先规约 $\varepsilon \leftarrow M$,然后规约 $\varepsilon \leftarrow N$,最后将 $\text{while}(M C) N S_1$ 规约回 $S$(LR 从左到右读,然后逐渐归约)。

  • $S.\text{next}$ 位于栈中右部的栈下方记录的域中(它会在规约完 $S$ 的后续流程中被修改为正确的值,就像你现在还没规约到 $C$,但是使用了一个 $M$ 来存储)
  • $C$ 的继承属性 $\text{true}$、$\text{false}$ (即条件满足 / 不满足时的下一句指令的位置)位于栈中紧靠 $C$ 的下方记录 $M$ 的域中

当将 $\varepsilon$ 规约到 $M$ 时,由于我们需要将 $C.\text{false}$ 的值设为 $M$ 的一个域,所以我们在栈中 $M$ 的栈下方记录中找到 $S.\text{next}$ 的值,它就是 $C.\text{false}$ 的值。

由于此时栈顶指针在 $M$,所以这里执行的是 $C.\text{false} = \text{stack[top-3].L1}$。

这样在下一步规约 $C$ 时,其所需要的继承属性 $\text{true}$ 和 $\text{false}$ 都已经计算完毕,并存放在栈中紧靠在它栈下方记录 $M$ 的域中。

又一例:92339

  • 我们要为即将到来的规约到 $S_1$ 做准备,所以要准备 $S_1.\text{next}$,存放在 $N$ 的栈记录中,使得它恰好存放在紧靠 $S_1$ 的栈记录之下的记录 $N$ 的域中
  • $S_1.\text{next} = \text{stack[top-3].L1}$(此时 $\text{top}$ 指针在 $N$)

image-20241110130756724

  • 这个时候就要执行对 $S_1$ 的规约就可以用其继承属性 $S_1.\text{next}$ 了。

最终得到:

$$ \begin{array}{|l|l|} \hline \text{产生式} & \text{规约时动作} \ \hline S \to \text{while} ( M C ) N S_1 & \text{tempCode }= \text{label} , || , \text{stack}[\text{top} - 4].L1 , || \ & \quad \text{stack}[\text{top} - 3].\text{code} , || , \text{“label”} , || , \text{stack}[\text{top} - 4].L2 , || \ & \quad \text{stack}[\text{top}].\text{code}; \ & \text{top} = \text{top} - 6; \ & \text{stack}[\text{top}].\text{code} = \text{tempCode}; \ \hline M \to \varepsilon & \text{top} = \text{top} + 1; \ & L1 = \text{new()}; \ & L2 = \text{new()}; \ & C.\text{true} = L2; \ & C.\text{false} = \text{stack}[\text{top} - 3].\text{next}; \ \hline N \to \varepsilon & \text{top} = \text{top} + 1; \ & S_1.\text{next} = \text{stack}[\text{top} - 3].L1; \ \hline \end{array} $$

$$ \begin{array}{c} \hline 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 \ \hline ? & \text{while} & ( & M & C & ) & N & S_1 \ \hline S.\text{next} & & & C.\text{true} & C.\text{code} & & S_1.\text{next} & S_1.\text{code} \ & & & C.\text{false} & & & & \ & & & L_1 & & & & \ & & & L_2 & & & & \ \hline \end{array} $$

确定继承属性在分析栈中的位置

综合属性值很容易在栈中($\text{stack}[i].\text{val}$)找到,因此在自底向上分析中处理 L 属性定义的关键是 确定继承属性值在栈中的位置

实际使用中,$X$ 的继承属性 $X.i$ 通常和文法符号 $Y$ 的综合属性 $Y.s$ 有关:

  • 或者是 $Y.s$ 的直接拷贝
  • 或者是 $Y.s$ 值的函数值

例子

image-20241110131217312

image-20241110131245070

在原始文法中,属性 $A.s$ 需要传递给 $C.i$。

但是由于存在两个产生式 $S \to aA{C.i = A.s}C$ 与 $S \to bAB{C.i = A.s}C$,所以要规约到 $C$ 时,我们不知道 $A.i$ 应该在相对于栈顶的 $\text{stack}[\text{top}-1]$ 还是 $\text{stack}[\text{top}-2]$ 记录获得。

通过引入 $M$,可以将这个过程明确为:

$$ A.s \rightarrow M.i = M.s \rightarrow C.i $$

即:分解复杂的属性传递路径,使得属性的传递过程变得更清晰、可控。

image-20241110131312248

先一步一步传下来继承属性,然后再在 $M$ 内部完成计算,因为这样可以明确参数在栈中的位置。

即:通过分解复杂的计算过程,使得每一步都能够明确地进行属性传递和计算。

💾

  •  

语法制导翻译 II

构造抽象语法树的 SDD

抽象语法树 (Abstract Syntax Tree)

  • 每个 结点 代表一个语法结构,对应于一个 运算符
  • 结点的每个 子结点 代表其子结构,对应于 运算分量
  • 表示这些子结构按照特定方式组成更大的结构
  • 可以忽略掉一些标点符号等非本质的东西

语法树的表示方法

  • 每个结点用一个对象表示
  • 对象有多个域
    • 叶子结点中只存放词法值
    • 内部结点中存放 $\text{op}$(操作符)值和参数(通常指向其它结点)

抽象语法树的例子

产生式 $S \rightarrow \text{if } B \text{ then } S_1 \text{ else } S_2$ 的语法树

          if-then-else
          /     |     \
         B     S_1    S_2
class StmtIfAST : public BaseAST {
public:
    unique_ptr<BaseAST> exp;
    unique_ptr<BaseAST> then_stmt;
    optional<unique_ptr<BaseAST>> else_stmt;
    Result print() const override;
};

赋值语句的语法树

          assignment
          /        \
   variable    expression
class StmtAssignAST : public BaseAST {
public:
    unique_ptr<BaseAST> l_val;
    unique_ptr<BaseAST> exp;
    Result print() const override;
};

注意:在语法树中,运算符号和关键字都不在叶结点,而是在内部结点中出现

抽象语法树 vs 具体语法树

image-20241110005517160

image-20241110005541711

可以看到,AST 相较于具体语法树,不再包含标点符号等非本质的东西,而且不再像具体语法树一样表示为推导的完整过程,而是带有了一部分的语义信息。

抽象语法树的构造

定义:抽象语法树中的每个结点代表一个程序构造,子结点代表构造的组成部分。

例如,表达式 $E_1 + E_2$ 的语法树结点标号为 $+$,子结点分别代表 $E_1$ 和 $E_2$。

结点构造: 每个结点有一个 $\text{op}$ 字段表示结点标号,及以下字段:

  1. 叶子结点(Leaf)
    • 有一个附加域存储此叶子结点的词法值
    • 构造函数 $\text{Leaf}(\text{op}, \text{val})$ 创建叶子结点对象
    • 例如:$\text{Leaf}(\text{num}, 5)$ 表示一个叶子结点,标号为 $\text{num}$,值为 $5$
  2. 内部结点(Node)
    • 附加字段数量等于结点的子结点数量
    • 构造函数 $\text{Node}(\text{op}, c_1, c_2, ..., c_k)$ 创建内部结点对象
    • 例如:$\text{Node}(+, E_1, E_2)$ 表示一个内部结点,标号为 $+$,子结点为 $E_1$ 和 $E_2$

总结:

  • 抽象语法树结点通过 $\text{op}$ 字段表示 标号,叶子结点通过 $\text{val}$ 存储 ,内部结点通过 构造函数 $\text{Node}$ 连接子结点
  • 属性 $\text{E.node}$ 指向 $\text{E}$ 对应的这一块 以之为根节点的语法树 的一部分

image-20241110010050014

自顶向下的 AST 构造过程

$$ \begin{array}{|l|l|l|} \hline & \quad \textbf{产生式}&\textbf{语义规则}\ \hline \textbf{1)} & \quad E \rightarrow T , E' & E.\text{node} = E'.\text{syn} \ & &E'.\text{inh} = T.\text{node} \ \hline \textbf{2)} & \quad E' \rightarrow + , T , E'_1 & E'_1.\text{inh} = \text{new Node}('+', E'.\text{inh}, T.\text{node}) \ & & E'.\text{syn} = E'_1.\text{syn} \ \hline \textbf{3)} & \quad E' \rightarrow - , T , E'_1 & E'_1.\text{inh} = \text{new Node}('-', E'.\text{inh}, T.\text{node}) \ & & E'.\text{syn} = E'_1.\text{syn} \ \hline \textbf{4)} & \quad E' \rightarrow \varepsilon & E'.\text{syn} = E'.\text{inh} \ \hline \textbf{5)} & \quad T \rightarrow ( , E , ) & T.\text{node} = E.\text{node} \ \hline \textbf{6)} & \quad T \rightarrow \text{id} & T.\text{node} = \text{new Leaf}(\text{id}, \text{id.entry}) \ \hline \textbf{7)} & \quad T \rightarrow \text{num} & T.\text{node} = \text{new Leaf}(\text{num}, \text{num.val}) \ \hline \end{array} $$

对于式子 $a - 4 + c$,构造出语法分析树如下:

image-20241110011446937

注意:

  • 最后一列语义规则不一定是同时执行的,如在产生式(1)中,实际上先计算了继承属性 $E'.\text{inh} = T.\text{node}$,但是在最后才计算综合属性 $E.\text{node} = E'.\text{syn}$。
  • 一个文法符号可能对应多个结点,如图里结点 4、8 都对应同一个 $T$
  • 虚线部分构成的是一颗 语法分析树,而不是 抽象语法树

注意这张图中实际上有个关系:

  • 虚线:语法分析树
  • 黑线:依赖图,依赖图中的边表示的是依赖关系,而不是等于关系

非终结符号 $E'$ 有一个继承属性 $\text{inh}$ 和一个综合属性 $\text{syn}$。

属性 $\text{inh}$ 表示至今为止构造得到的部分抽象语法树

举例:$E'.\text{inh}$ 表示的是位于 $E'$ 的子树左边的输入串前缀所对应的抽象语法树的根

  1. 在图中的结点 5 处,$E'.\text{inh}$ 表示对应于节点 2($a$)的抽象语法树的根
  2. 在节点 6 处则对应节点 5($a - 4$)
  3. 在节点 9 处则对应节点 6($a - 4 + c$),因为没有更多的输入,所以在结点 9 处,$E'.\text{inh}$ 指向整个抽象语法树的根

继承属性可以把值从一个结构传递到另一个并列的结构,也可把值从父结构传递到子结构。

属性 $\text{syn}$ 把这个值沿着语法分析树向上传递,直到它成为 $E.\text{node}$ 的值

举例:

  1. 结点 10 上的属性值 $E'.\text{syn}$ 是通过产生式 4 所关联的规则 $E'.\text{syn} = E'.\text{inh}$ 来定义的
  2. 结点 11 处的属性值 $E'.\text{syn}$ 是通过产生式 2 所关联的规则 $E'.\text{syn} = E'_1.\text{syn}$ 来定义的
  3. 类似的规则还定义了结点 12 和 13 处的值

语法制导的翻译方案(SDT)

定义:语法制导的翻译方案(syntax-directed translation scheme,SDT)是对语法制导定义的补充,也称作语法制导的翻译模式。

  • 把 SDD 的 语义规则改写为计算属性值的程序片段,用花括号 ${}$ 括起来,插入到产生式右部的任何合适的位置上
  • 这种方法表示语法分析和语义动作交错,可以在按 深度优先 遍历分析树的过程中随时执行语义动作

说人话:

  • SDT 是在语法分析过程中附带语义动作(程序计算片段)
  • 语义动作可以放在产生式的任意位置,通常用大括号 ${}$ 包围

基础文法:原来的不含语义动作的文法称作基础文法。

举例

一个简单的 SDT (只包含 +/- 操作的表达式):

$$ \begin{aligned} E \rightarrow& TR \ R \rightarrow& \text{addop } T { \text{print(addop.lexeme)} } R_1 \ |& \varepsilon \ T \rightarrow& \text{num} { \text{print(num.val)} } \end{aligned} $$

image-20241110011826210

图中 pt 即 print,以深度优先搜索遍历这颗树的时候,即得到后缀表达式。

SDT 的实现方法

基本实现方法

  • 建立语法分析树
  • 将语义动作看作是虚拟的结点
  • 从左到右,深度优先地遍历分析树,在访问虚拟结点时执行相应动作

通常情况下在语法分析过程中实现,不需要真的构造语法分析树。

实现 SDD 的两种重要基础文法

  • 基础文法是 LR 的,SDD 是 S 属性的

    LR:自底向上、从左向右扫描、进行最右推导的逆操作,即从左边开始归约

  • 基础文法是 LL 的,SDD 是 L 属性的

    LL:自顶向下、从左向右扫描、进行最左推导

翻译方案的设计

原则

  1. 根据语法制导定义设计翻译方案
  2. 需要保证语义动作不会引用还没有计算的属性值

只需要综合属性的情况

操作:为每一个语义规则建立一个包含赋值的动作,并把这个动作放在相应的产生式右边的末尾

例如:

$$ \text{T} \rightarrow \text{T}_1 * \text{F} $$

所需动作:$\text{T.val} = \text{T}_1.\text{val} * \text{F.val}$

改写后:

$$ \text{T} \rightarrow \text{T}_1 * \text{F} {\text{T.val} = \text{T}_1.\text{val} * \text{F.val}} $$

既有综合属性又有继承属性

原则:

  • 产生式 右边 的符号的 继承属性 必须在 这个符号以前 的动作中计算出来(不然上哪里去继承?)

  • 一个动作 不能引用 这个动作 右边符号的综合属性 (还没算到右边符号呢)

    继承属性肯定得允许,不然你赋值 ${ \text{A}_1.\text{in} = 1 } \text{A}_1$ 都不行。

  • 产生式 左边非终结符号的综合属性 只有在它所 引用的所有属性都计算出来 以后才能计算

    计算这种属性的动作通常可放在产生式右端的末尾(同上文只需要综合属性的情况)

例如:

$$ \text{S} \rightarrow \text{A}_1 \text{A}_2 { \text{A}_1.\text{in} = 1;\text{A}_2.\text{in} = 2 } \ \text{A} \rightarrow a { \text{print(A.in)} } $$

此翻译方案不满足要求(违背原则 1,应该在 $\text{A}_1$ 出现之前,先算出其继承属性 $\text{A}_1.\text{in}$),可以改成如下的形式:

$$ \text{S} \rightarrow { \text{A}_1.\text{in} = 1 } \text{A}_1 { \text{A}_2.\text{in} = 2 } \text{A}_2 \ \text{A} \rightarrow a { \text{print(A.in)} } $$

后缀翻译方案

后缀 SDT:所有动作都在产生式最右端的 SDT

文法可以自底向上分析且 SDD 是 S 属性的,必然可以构造出后缀 SDT。

构造方法

  • 将每个语义规则看作是一个赋值语义动作
  • 将所有的语义动作放在规则的 最右端

举例

image-20241110013021198

后缀 SDT 的语法分析栈实现

实现方法:可以在 LR 语法分析的过程中实现。

  • 归约时 执行相应的语义动作
  • 定义用于记录各文法符号的属性的 union 结构
  • 栈中的每个文法符号(或者说状态)都附带一个这样的 union 类型的值

在按照产生式 $A \rightarrow XYZ$ 归约时,$Z$ 的属性可以在栈顶找到,$Y$ 的属性可以在下一个位置找到,$X$ 的属性可以在再下一个位置找到。

image-20241110013128864

image-20241110013327594

上图展示了一个规约的过程,把 $XYZ$ 规约回了 $A$,并在此过程中完成了属性的传递。

image-20241110013354457

再次强调:这是自底向上的分析过程,语义动作在规约时生效。

产生式内部带有语义动作的 SDT

$$ B \rightarrow X {a} Y $$

动作左边的所有符号(以及动作)处理完成后 ,就立刻执行这个动作。

  • 自底向上分析时,(最早就开始)在 $X$ 出现在栈顶(即刚刚规约出 $X$)时执行动作 $a$
  • 自顶向下分析时,(延迟到最晚)在试图展开 $Y$ 或者在输入中检测到 $Y$ 的时候执行 $a$

有问题的 SDT

并不是所有的 SDT 都可以在分析过程中实现。

比如,从中缀表达式到前缀表达式的转换:

$$ \begin{array}{rl}

  1. & L \rightarrow E ; n \
  2. & E \rightarrow { \text{print}(\text{“+”}); } ; E_1 + T \
  3. & E \rightarrow T \
  4. & T \rightarrow { \text{print}(\text{“*”}); } ; T_1 * F \
  5. & T \rightarrow F \
  6. & F \rightarrow (E) \
  7. & F \rightarrow \text{digit} ; { \text{print}(\text{digit.lexval}); } \ \end{array} $$

在自顶向下和自底向上的分析中都无法实现这种 SDT:

在这个 SDT 中,操作符(+ 和 *)需要在操作数之前打印,这就是将中缀表达式转换为前缀表达式的要求。然而:

  • 在自顶向下分析中:

    • 分析过程是从左到右进行的
    • 当遇到产生式 $E \rightarrow E_1 + T$ 时,必须先处理 $E_1$
    • 但是根据语义动作的要求,我们需要在处理 $E_1$ 之前就打印“+”
    • 这造成了时序上的矛盾
  • 在自底向上分析中:

    • 分析过程是按照规约顺序进行的
    • 当要规约 $E_1 + T$ 到 $E$ 时,$E_1$ 和 $T$ 的值已经计算完成
    • 此时再打印“+”就太晚了,因为操作数已经处理完毕

所以,对于这种一般的 SDT,可以先建立分析树(语义动作作为虚拟的结点),然后进行前序遍历并执行动作。

消除左递归时 SDT 的转换方法

如果动作不涉及属性值,可以 把动作当作终结符号 进行处理,然后消左递归。

原始的产生式:

$$ \begin{aligned} E & \rightarrow E_1 + T { \text{print(“+”)} } \ E & \rightarrow T \end{aligned} $$

转换后得到:

$$ \begin{aligned} E & \rightarrow T R \ R & \rightarrow + T { \text{print(“+”)} } R \ R & \rightarrow \varepsilon \end{aligned} $$

左递归文法翻译方案的转换

带左递归的文法:

$$ \begin{aligned} E & \rightarrow E_1 + T { E.\text{val} = E_1.\text{val} + T.\text{val} } \ E & \rightarrow E_1 - T { E.\text{val} = E_1.\text{val} - T.\text{val} } \ E & \rightarrow T { E.\text{val} = T.\text{val} } \ T & \rightarrow (E) { T.\text{val} = E.\text{val} } \ T & \rightarrow \text{num} { T.\text{val} = \text{num}.\text{val} } \end{aligned} $$

转换后的不带有左递归的文法:

$$ \begin{aligned} E & \rightarrow T { R.\text{i} = T.\text{val} } R { E.\text{val} = R.\text{s} } \ R & \rightarrow + T { R_1.\text{i} = R.\text{i} + T.\text{val} } R_1 { R.\text{s} = R_1.\text{s} } \ R & \rightarrow - T { R_1.\text{i} = R.\text{i} - T.\text{val} } R_1 { R.\text{s} = R_1.\text{s} } \ R & \rightarrow \varepsilon { R.\text{s} = R.\text{i} } \ T & \rightarrow (E) { T.\text{val} = E.\text{val} } \ T & \rightarrow \text{num} { T.\text{val} = \text{num}.\text{val} } \end{aligned} $$

image-20241110013841264

举例

原有文法:

$$ \begin{aligned} A & \to A_1 Y { A.a = g(A_1.a, Y.y) } \ A & \to X { A.a = f(X.x) } \ \end{aligned} $$

消除左递归之后,文法转换成

$$ A \to XR \ R \to YR | \varepsilon $$

消除左递归后的翻译方案:

$$ \begin{aligned} A &\to X { R.i = f(X.x) } R { A.a = R.s } \ R &\to Y { R_1.i = g(R.i, Y.y) } R_1 { R.s = R_1.s } \ R &\to \varepsilon { R.s = R.i } \ \end{aligned} $$

image-20241118010809822

image-20241118010827545

注意事项

  • 并不是所有的 SDT 都可以在分析过程中实现
  • 后缀 SDT 以及 L 属性 SDT 可以在分析时完成

💾

  •  

语法制导翻译 I

概述

  1. 语法分析器

    • 用于判断输入在语法上的正确性。
    • 语法分析完成后,通常还需将输入的源代码翻译为目标表示形式。
  2. 语法制导定义(Syntax-Directed Definition, SDD)

    • 将文法符号和某些属性相关联
    • 通过语义规则描述如何计算属性的值
  3. 语法制导翻译(Syntax-Directed Translation,SDT)

    在产生式体中加入语义动作,并在适当的时候执行这些语义动作

    • 编译器在分析过程中执行的工作
    • 包含语义分析和正确性检查,若正确,则翻译为中间代码或目标代码
    • 文法符号的属性描述其语义(如变量的类型、层次、存储地址等),通过对属性值的计算完成翻译任务

语法制导定义(SDD)

SDD 是上下文无关文法和属性 / 规则的结合。

规则定义: 对于 $\forall A \rightarrow X_1 X_2 \ldots X_n \in P$,每个规则的一般形式为:$c = f(c_1, c_2, \ldots, c_k)$

  • 综合属性: $c$ 是 $A$ 的一个属性,且 $c_1, c_2, \ldots, c_k$ 是 $A$ 的继承属性或是某个 $X_i$ 的属性(向上看继承的或者向下看子节点属性,综合全局看,通常自下而上进行传递)。
  • 继承属性: $c$ 是某个符号 $X_i$ 的属性,且 $c_1, c_2, \ldots, c_k$ 是 $A$ 或 $X_j$ 的属性(父节点、自己、兄弟节点的属性,向上看,通常自上而下或横向进行传递)。

不允许 $N$ 的继承属性通过 $N$ 的子结点上的属性来定义,但允许 $N$ 的综合属性依赖于 $N$ 本身的继承属性。

终结符号有综合属性(由词法分析器 lexer 获得),但是没有继承属性(它们的属性在词法分析阶段已经完全确定,不依赖于语法树中其他节点的属性)。

S 属性的 SDD

定义只包含综合属性 的 SDD 称为 S 属性的 SDD。

  • 每个语义规则都根据产生式体中的属性值来计算头部非终结符号的属性值
  • 如果我们可以给各个属性值排出计算顺序,那么注释分析树就可以计算得到属性值
  • S 属性的 SDD 一定可以按照 自底向上 的方式求值

S 属性:Synthesized Attributes。

实现:

  • S 属性的 SDD 可以和 LR 语法分析器(从左到右扫描,进行最右推导的逆操作,即从左边开始规约)一起实现
  • 栈中的状态可以附加相应的属性值
  • 在进行归约时,按照语义规则计算归约得到的符号的属性值(下一章会有实例)

无副作用:语义规则不应有复杂的副作用。

  • 受控副作用:在 SDD 中添加除求值之外的动作
  • 无副作用:要求副作用不影响其它属性的求值
  • 没有副作用的 SDD 称为 属性文法(attribute grammar)

适用于自顶向下分析的 SDD

若存在直接左递归,则无法使用自顶向下分析。

消除左递归后,可能无法直接使用自顶向下分析,比如,我们把一个:

$$ T \rightarrow T + E \mid E $$

拆成了:

$$ \begin{aligned} T &\rightarrow E T' \ T' &\rightarrow + E T' \mid \varepsilon \end{aligned} $$

此时,对于第二个产生式,$+$ 的左侧因子无法直接获得。

为此,我们需要引入继承属性。

$$ \begin{array}{|l|l|} \hline \text{产生式} & \text{语义规则} \ \hline T \rightarrow FT' & T'.\text{inh} = F.\text{val} \ & T.\text{val} = T'.\text{syn} \ \hline T' \rightarrow *FT_1' & T_1'.\text{inh} = T'.\text{inh} * F.\text{val} \ & T'.\text{syn} = T_1'.\text{syn} \ \hline T' \rightarrow \varepsilon & T'.\text{syn} = T'.\text{inh} \ \hline F \rightarrow digit & F.\text{val} = digit.\text{lexval} \ \hline \end{array} $$

image-20241117232452765

注意看黑实线的属性传递流,同一个产生式的语义动作可能不是同时执行的。

这些黑实线实际上构成了一个依赖图(后面会讲)。

L 属性的 SDD

定义:语义规则中的每个属性可以是:

  • 综合属性

  • 继承属性,且对于任意产生式 $A \rightarrow X_1 X_2 ... X_n \in P$,$X_j$ 的继承属性仅依赖于:

    • 产生式中 $X_j$ 左边 符号 $X_1, X_2, ..., X_{j-1}$ 的属性
  • $A$ 的继承属性

每一个 S 属性的 SDD 都是 L 属性的 SDD。

L 属性:Left-Attributed Definitions。

L 属性 SDD 的自顶向下语法分析

L 属性的 SDD 可用于按 深度优先 顺序来计算。

对于规则:

$$ A \rightarrow X_1 X_2 ... X_n $$

在递归子程序中实现 L 属性,则对于每个非终结符号 $A$ 或者 $X_i$,其对应的函数的参数为继承属性,返回值为综合属性。

在处理规则时:

  • 在调用 $X_i()$ 之前计算 $X_i$ 的继承属性值,然后以它们为参数调用 $X_i()$,得到 $X_i$ 的综合属性值
  • 在产生式对应代码的最后计算 $A$ 的综合属性值

注意: 如果所有的文法符号的属性计算按上述方式进行,计算顺序必然和依赖关系一致。

依赖图

使用 依赖图 表示计算顺序:

  • 结点:属性值
  • 有向边:属性依赖关系

若依赖图无环,则存在一个拓扑排序,确定属性值的计算顺序。

特定类型的 SDD 一定不包含环,且有固定的排序模式:

  • S 属性的 SDD:可用于 自顶向下自底向上 的语法分析
    • 每个属性都是综合属性
    • 都是根据子构造的属性计算出父构造的属性
    • 在依赖图中,总是通过子结点的属性值来计算父结点的属性值
  • L 属性的 SDD:可用于按 深度优先 顺序来计算

依赖图的边

  • 综合属性:从下到上
  • 继承属性:从左到右,从上到下

💾

  •  

语法分析 IV

LR (1) 文法

LR (k) 项

形式:$[A \rightarrow \alpha \cdot \beta, a_1a_2 \ldots a_k]$

  • 当 $\beta \neq \varepsilon$ 时,为移进或待归约项,$a_1a_2 \ldots a_k$ 不直接起作用
  • 当 $\beta = \varepsilon$ 时,即为归约项 $[A \rightarrow \alpha \cdot, a_1a_2 \ldots a_k]$,仅当前输入符串前 $k$ 个符号是 $a_1a_2 \ldots a_k$ 时,才能用 $A \rightarrow \alpha$ 进行归约
  • $a_1a_2 \ldots a_k$ 称为向前搜索符号串(展望项)

LR (1) 有效项

LR (1) 项 $[A \rightarrow \alpha \cdot \beta, a]$ 对于一个可行前缀 $\gamma$ 有效的条件是存在一个推导:

$$ S \Rightarrow_{rm}^* \delta Aw \Rightarrow_{rm}\underbrace{\delta \alpha}_{\gamma} \beta w $$

其中:

  1. $\gamma = \delta \alpha$
  2. 要么 $a$ 是 $w$ 的第一个符号,要么 $w$ 为 $\varepsilon$ 且 $a$ 等于 $$ $ $$

考虑文法 $G:S \rightarrow CC \quad C \rightarrow cC \mid d$

规范推导:

$$ S \Rightarrow_{rm}^* ccCcd \Rightarrow_{rm} cccCcd $$

项 $[C \rightarrow c \cdot C, c]$ 对可行前缀 $ccc$ 是有效的。

LR (1) 有效项的推导

若项 $[A \rightarrow \alpha \cdot B \beta, a]$ 对可行前缀 $\gamma = \delta \alpha$ 是有效的,则存在一个规范推导:

$$ S \Rightarrow_{rm}^* \delta Aax \Rightarrow_{rm} \delta \alpha B \beta ax $$

假定 $\beta ax \Rightarrow_{rm}^* by$,则对每一个形如 $B \rightarrow \xi$ 的产生式,有规范推导:

$$ S \Rightarrow_{rm}^* \delta \alpha B \beta ax \Rightarrow_{rm}^* \delta \alpha Bby \Rightarrow_{rm} \delta \alpha \xi by $$

从而项 $[B \rightarrow \cdot \xi, b]$ 对于可行前缀 $\gamma = \delta \alpha$ 也是有效的。

注意到 $b$ 必然属于二者之一:

  1. 从 $\beta$ 推出的第一个终结符号
  2. $\beta \Rightarrow_{rm}^* \varepsilon$ 而 $b = a$

这两种可能性结合在一起,则 $b \in \text{First}(\beta a)$。

LR (1) 项集的构造

构造有效 LR (1) 项集族的方法实质上和构造规范 LR (0) 项集族的方法相同。

我们只需要修改两个过程:Closure 和 Goto。

Closure

设 $I$ 是 $G$ 的一个 LR (1) 项集,$\text{Closure}(I)$ 是从 $I$ 出发用以下三条规则构造的项集:

  1. 每一个 $I$ 中的项都属于 $\text{Closure}(I)$
  2. 若项 $[A \rightarrow \alpha \cdot B \beta, a]$ 属于 $\text{Closure}(I)$ 且 $B \rightarrow \gamma \in P$,则对任何 $b \in \text{First}(\beta a)$,把 $[B \rightarrow \cdot \gamma, b]$ 加到 $\text{Closure}(I)$ 中
  3. 重复执行 (2) 直到 $\text{Closure}(I)$ 不再增大为止

Goto

设 $I$ 是 $G$ 的一个 LR (1) 项集,$X$ 是一个文法符号,定义:

$$ \text{Goto}(I, X) = \text{Closure}(J) $$

其中 $J = {[A \rightarrow \alpha X \cdot \beta, a] \mid [A \rightarrow \alpha \cdot X \beta, a] \in I}$

LR (1) 项集族的构造方法

输入:一个增广文法 $G'$。

输出:LR (1) 项集族,其中的每个项集对文法 $G'$ 的一个或多个可行前缀有效。

方法:过程 Closure 和 Goto,以及用于构造项集的主例程 items

过程 Closure

$$ \begin{aligned} &\text{SetOfItems } \textbf{Closure}(I) { \ &\quad \text{repeat} \ &\quad \quad \text{for (} [A \to \alpha \cdot B \beta, a] \in I \text{)} \ &\quad \quad \quad \text{for (} B \to \gamma \in G' \text{)} \ &\quad \quad \quad \quad \text{for (} b \in \text{First}(\beta a) \text{)} \ &\quad \quad \quad \quad \quad \text{将 } [B \to \cdot \gamma, b] \text{ 加入 } I \text{ 中;} \ &\quad \text{until 不能向 } I \text{ 中加入更多的项;} \ &\quad \text{return } I; \ &} \end{aligned} $$

过程 Goto

$$ \begin{aligned} &\text{SetOfItems } \textbf{Goto}(I, X) { \ &\quad J \leftarrow \varnothing; \ &\quad \text{for (} [A \to \alpha \cdot X \beta, a] \in I \text{)} \ &\quad \quad \text{将 } [A \to \alpha X \cdot \beta, a] \text{ 加入 } J \text{ 中;} \ &\quad \text{return } \textbf{Closure}(J); \ &} \end{aligned} $$

最后执行 $\text{Closure}$ 的原因是,$\text{Goto}$ 函数返回的是一个新的项集,需要对其进行闭包操作。

项集族 $C$

$$ \begin{aligned} &\text{void } \textbf{items}(G') { \ &\quad C \leftarrow { \textbf{Closure}({ [S' \to \cdot S, $] }) }; \ &\quad \text{repeat} \ &\quad \quad \text{for (每个项集 } I \in C \text{)} \ &\quad \quad \quad \text{for (每个文法符号 } X \text{)} \ &\quad \quad \quad \quad \text{if (} \textbf{Goto}(I, X) \neq \varnothing \text{ 且不在 } C \text{ 中)} \ &\quad \quad \quad \quad \quad \text{将 } \textbf{Goto}(I, X) \text{ 加入 } C \text{ 中;} \ &\quad \text{until 不再有新的项集加入到 } C \text{ 中;} \ &} \end{aligned} $$

构造 LR (1) 分析表

  1. DFA 状态对应分析表行

    DFA 中的每个状态对应分析表中的一行。

  2. DFA 状态转移

    对于 DFA 中的每一个从状态 $i$ 到状态 $j$ 的转移:

    • 如果转移符号为终结符 $a$:在表项 $M[i, a]$ 中填写 移进动作 $S_j$ (Shift,Action 列)
    • 如果转移符号为非终结符 $A$:在表项 $M[i, A]$ 中填写 转移到状态 $j$ (Goto 列)
  3. 包含归约项 $[A \rightarrow \alpha \cdot, a]$ 的状态 $i$

    在表项 $M[i, a]$ 中填写归约动作 $r_k$(Reduce),其中 $k$ 是产生式 $A \rightarrow \alpha$ 的编号

注意:如果每个单元格中只包含一个动作,则分析表合法

LR(1)分析表举例

文法:

$$ \begin{aligned} & \quad S' \rightarrow S \ & \quad S \rightarrow CC \ & \quad C \rightarrow cC \ & \quad C \rightarrow d \ \end{aligned} $$

项集族:

image-20241114185322182

分析表:

image-20241114185347887

LALR 文法

LALR:Look-Ahead LR

LR (1) 分析表:状态多,实际使用较少。

同心集:两个 LR (1) 项集 去掉搜索符后相同,称为 同心

LALR (1) 分析表:合并同心集(合并搜索符串)后构造出的 LR 分析表。

合并同心项集不会产生移进 / 归约冲突,但是有可能产生归约 / 归约冲突

因为合并的时候合并的是同心项的展望符,而展望符只在规约的时候起作用,在移入的时候是不起作用的,只要合并前各个同心项目集本身是没有移进 / 归约冲突的,就不会有移进 / 归约冲突(后文有证明)。

LALR 分析表的高效构造算法

通过先构造 LR (1) 分析表再合并得到 LALR (1) 分析表的过程太慢了。

  1. 内核项表示

    使用内核项表示 LR (0) 或 LR (1) 项集。

    内核项:$[S' \rightarrow \cdot S]$ 或 $$[S' \rightarrow \cdot S, $]$$,以及 $\cdot$ 不在最左边的项(这些项代表对于已经读入的符号完全没有要求)。

  2. 传播和自发生成

    通过传播和自发生成,获得向前看符号,得到 LALR (1) 内核项。

    传播 / 自发生成:向前看符号的传递过程。

    对于某个项 $[A \rightarrow \alpha \cdot B \beta, a]$ 执行闭包:

    传播:假设向前看符号是一个不在文法中的符号 $#$,即对 $[A \rightarrow \alpha \cdot B \beta, #]$ 进行闭包,若得到的某些项的向前看符号 就是 $#$,那么就认为这些项的向前看符号是传播得到的,直接复制 $a$,就行;

    自发生成:假设向前看符号是一个不在文法中的符号 $#$,即对 $[A \rightarrow \alpha \cdot B \beta, #]$ 进行闭包,若有些项的向前看符号 不是 $#$,那么就认为这些项的向前看符号是传播得到的,不改动这些项的向前看符号;

  3. Closure 函数

    使用 $\text{Closure}$ 函数求出内核项的闭包,得到 LALR 分析表。

由于传播和自发生成的表述比较抽象,这里给一个例子(书 P175)来自己悟:

文法:

$$ \begin{aligned} S' &\to S \ S &\to L = R \mid R \ L &\to * R \mid id \ R &\to L \end{aligned} $$

直接根据产生式,构建出只有内核项的项集族: $$ \begin{aligned} I_0 &: {S' \to \cdot S} \ I_1 &: {S' \to S\cdot} \ I_2 &: {S \to L\cdot = R, R \to L\cdot} \ I_3 &: {S \to R\cdot} \ I_4 &: {L \to * \cdot R} \ I_5 &: {L \to id\cdot} \ I_6 &: {S \to L = \cdot R} \ I_7 &: {L \to * R\cdot} \ I_8 &: {R \to L\cdot} \ I_9 &: {S \to L = R\cdot} \end{aligned} $$

使用如下算法来确定向前看符号:

输入:一个 LR (0) 项集 $I$ 的内核 $K$ 以及一个文法符号 $X$。$#$ 是一个不在文法中的符号。

输出:由 $I$ 中的项为 $\text{Goto}(I, X)$ 中内核项自生成的向前看符号,以及 $I$ 中将其向前看符号传播到 $\text{Goto}(I, X)$ 中内核项的项。

$$ \begin{aligned} &\text{for } (K \text{中的每个项 } A \to \alpha \cdot \beta) { \ &\quad J := \text{Closure}({[A \to \alpha \cdot \beta, #]}); \ &\quad \text{if } ([B \to \gamma \cdot X \delta, a] \text{ 在 } J \text{ 中,并且 } a \neq #) \ &\quad\quad \text{断定 } \text{Goto}(I, X) \text{中的项 } B \to \gamma X \cdot \delta \text{的向前看符号 } a \text{ 是自发生成的;} \ &\quad \text{if } ([B \to \gamma \cdot X \delta, #] \text{ 在 } J \text{ 中}) \ &\quad\quad \text{断定向前看符号从 } I \text{中的项 } A \to \alpha \cdot \beta \text{ 传播到了 } \text{Goto}(I, X) \text{中的项 } B \to \gamma X \cdot \delta \text{上}; \ & } \ \end{aligned} $$

当我们将算法应用于项集 $I_0$ 的内核时,我们首先计算 $\text{Closure}({[S' \rightarrow \cdot S, #]})$,得到:

$$ \begin{aligned} & S' \rightarrow \cdot S, # \ & S \rightarrow \cdot L = R, # \ & S \rightarrow \cdot R, # \ & L \rightarrow \cdot *R, # / = \ & L \rightarrow \cdot id, # / = \ & R \rightarrow \cdot L, # \end{aligned} $$

在这个闭包的项中,我们看到有两个项中的向前看符号 $=$ 是自发生成的;

而对于这个闭包结果,把 $#$ 替换为真实的闭包前原始项 $$[S' \rightarrow \cdot S, $]$$ 的向前看符号,即 $$ $ $$,我们认为此时的向前看符号 $$ $ $$ 是传播得到的。

LALR (1) 的讨论

  1. 核心依赖性

    由于 $\text{Goto}(I, X)$ 仅依赖于 $I$ 的核心,因此 LALR(1)项集合并后的转换函数 $\text{Goto}(I, X)$ 随自身的合并而得到

  2. 动作修改

    动作 $\text{action}$ 应当进行修改,以反映各被合并集合的既定动作

  3. 归约 - 归约冲突

    项集合合并时,可能会导致冲突。这种冲突不会是移进 - 归约冲突:

    $$ \begin{aligned} I_k: &{[A \rightarrow \alpha \cdot, u_1] \quad [B \rightarrow \beta \cdot ay, b]} \quad a \cap u_1 = \varnothing \ I_j: &{[A \rightarrow \alpha \cdot, u_2] \quad [B \rightarrow \beta \cdot ay, c]} \quad a \cap u_2 = \varnothing \ I_{kj}: &{[A \rightarrow \alpha \cdot, u_1 \cup u_2] \quad [B \rightarrow \beta \cdot ay, b/c]} \quad a \cap (u_1 \cup u_2) = \varnothing \end{aligned} $$

    但可能引起归约 - 归约冲突:

    $$ \begin{aligned} I_k: &{[A \rightarrow \alpha \cdot, u_1] \quad [B \rightarrow \beta \cdot, u_2]}\ I_j: &{[A \rightarrow \alpha \cdot, u_2] \quad [B \rightarrow \beta \cdot, u_1]}\ I_{kj}: &{[A \rightarrow \alpha \cdot, u_1 \cup u_2] \quad [B \rightarrow \beta \cdot, u_1 \cup u_2]} \end{aligned} $$

    此时,有两个展望符号相同、核心也相同的归约项,可能产生归约 - 归约冲突。

二义性文法的使用

  1. 二义性文法不是 LR 的
  2. 有用的二义性文法
    • 简洁描述某些结构
    • 隔离某些语法结构,对其进行特殊处理
  3. 处理某些二义性文法
    • 通过消除二义性规则,保证每个句子只有一棵语法分析树
    • 可以在 LR 分析器中实现这一规则

利用优先级 / 结合性消除冲突

  1. 二义性文法
    • $E \rightarrow E + E \mid E * E \mid (E) \mid \text{id}$
    • 等价于:$E \rightarrow E + T \mid T \quad T \rightarrow T * F \mid F \quad F \rightarrow (E) \mid \text{id}$
  2. 二义性文法的优点
    • 容易:修改算符的优先级和结合性
    • 简洁:多优先级无需引入大量非终结符
    • 高效:不需处理 $ E \rightarrow T $ 这样的归约

四种 LR 解析的对比

  1. 如果构造 LR (0) 的 DFA
    • 没有归约冲突就是 LR (0) 文法
    • 有冲突但可以通过 Follow 集合解决冲突就是 SLR 文法
    • 否则不是 SLR 文法
  2. 如果构造 LR (1) 的 DFA
    • 没有冲突就是 LR (1) 文法
    • 如果合并同心集之后也没有冲突,那么就是 LALR (1) 文法
  3. 包含关系
    • $\text{LR(0)} < \text{SLR} < \text{LALR} < \text{LR(1)}$

用途比较:

  • $\text{LR}(0)$:最简单,但只能用于最简单的文法
  • $\text{SLR}$:构造简单,易于实现,实用价值高(大多数上下文无关文法均可构造 SLR 分析表)
  • $\text{LR}(1)$:适用文法类最大(几乎所有上下文无关文法),但分析表体积过大,使用价值不大
  • $\text{LALR}(1)$:介于 $\text{SLR}$ 和 $\text{LR}(1)$ 之间,最实用(比 $\text{SLR}$ 适用更多,比 $\text{LR}(1)$ 更简单)

💾

  •  

语法分析 III

自底向上语法分析

自底向上语法分析:将一个串 $w$ 归约 回到文法开始符号 $S$ 的过程。

在每个归约(reduction)步骤中,一个与某 产生式右部相匹配的特定子串 被替换为该 产生式左部 的非终结符号。

下文中,对如下概念不加区分:

  • 产生式左部 / 产生式头
  • 产生式右部 / 产生式体

归约:是一个推导步骤的反向操作

  • 推导步骤:将句型中的一个非终结符号替换为该符号的某个产生式的体 $A \rightarrow \alpha$
  • 归约步骤:与某产生式体匹配的子串被替换为该产生式头部的非终结符号 $\alpha \leftarrow A$

自底向上语法分析的目标

目标:反向构造一个推导过程。

方法:对输入进行从左到右的扫描,并在扫描过程中进行自底向上语法分析,就可以反向构造出一个最右推导。

句柄(Handle)

句柄:是与某个 产生式体 匹配的 子串,对它的归约代表了相应的最右推导中的一个反向步骤(看接下来的形式定义会好理解一些)。

Ref:

  • 前缀(prefix):移走 $x$ 尾部的 零个 或多个连续的符号。
  • 后缀(suffix):移走 $x$ 头部的 零个 或多个连续的符号。
  • 子串(substring):从 $x$ 中删去一个前缀和一个后缀。

注意,和某个产生式体匹配的最左子串不一定是句柄( 需要归约后能回到开始符号 )。

形式定义

若有 $S {\Rightarrow}^{*}{\text{rm}} \alpha A w \Rightarrow{\text{rm}} \underbrace{\alpha \beta w}_{\gamma}$ ,那么紧跟在 $\alpha$ 之后的 $\beta$ 是 句柄

最右句型:所有在最右推导中出现的句型,其内句柄右边的串 $w$ 只包含终结符号。

将 $\beta$ 替换为 $A$ ( 规约 )之后得到的串($\alpha A w$)是 $\gamma$ 的某个 最右推导序列 中出现在位于 $\gamma$($\alpha \beta w$) 之前的最右句型。

句柄可能存在多个,如果一个文法是 无二义性 的,那么该文法的 每个最右句型都有且只有一个句柄

句柄的寻找方法

给定:

$$ S = \gamma_0 \stackrel{rm}{\Rightarrow} \gamma_1 \stackrel{rm}{\Rightarrow} \gamma_2 \stackrel{rm}{\Rightarrow} \cdots \stackrel{rm}{\Rightarrow} \gamma_{n-1} \stackrel{rm}{\Rightarrow} \gamma_n = w $$

为了以相反顺序重构这个推导,我们在 $\gamma_n$ 中寻找句柄 $\beta_n$,并将 $\beta_n$ 替换为相关产生式 $A \rightarrow \beta_n$ 的头部 $A$,得到前一个最右句型 $\gamma_{n-1}$。

移入 - 归约语法分析技术

移入 - 归约语法分析是一种 自底向上 的语法分析技术,主要操作包括 移入归约

组成

  • :存放已识别的文法符号,句柄通常出现在栈的顶部
  • 输入缓冲区:存放待分析的符号,通常显示在右侧

image-20241114152516345

主要操作

  1. 移入(shift):将下一个输入符号移到栈的顶部
  2. 归约(reduce):将栈顶符号串(右部)替换为相应的产生式左部
  3. 接受(accept):语法分析成功完成
  4. 报错(error):发现语法错误,并调用错误恢复工具

LR ($k$) 中的 $k$ 表示 在输入中向前看 $k$ 个符号

移入 - 归约的语法分析技术可以使用栈中离栈顶很远的信息(向前看符号)来引导语法分析过程。

移入 - 归约语法分析中的冲突

有些上下文无关文法不能使用移入 - 归约语法分析技术。

即使知道了栈中的所有内容以及接下来的 $k$ 个输入符号,我们仍然可能会遇到:

  1. 移入 / 归约冲突:无法判断应该进行移入还是归约操作
  2. 归约 / 归约冲突:无法在多个可能的归约方法中选择正确的归约动作

接下来,我们举例说明。

移入 / 规约冲突举例

定义:在某个状态下,分析器既可以进行移入操作,也可以进行归约操作,但无法确定应该选择哪一种。

考虑以下文法:

  1. $E \rightarrow E + E$
  2. $E \rightarrow id$

假设当前状态是:

  • 栈:$id$
  • 剩余输入:$+ id$

在这种情况下,分析器可以选择:

  1. 移入:将 $+$ 移入栈中
  2. 归约:根据 $E \rightarrow id$,将 $id$ 归约为 $E$

这是一个典型的移入 / 归约冲突,因为分析器无法确定是应该移入 $+$ 还是进行归约。

规约 / 规约冲突举例

定义:在某个状态下,分析器可以进行多种归约操作,但无法确定应当选择哪一种。

考虑以下文法:

  1. $S \rightarrow A$
  2. $S \rightarrow B$
  3. $A \rightarrow a$
  4. $B \rightarrow a$

假设当前状态是:

  • 栈:$a$
  • 剩余输入:空

在这种情况下,分析器可以选择:

  1. 根据 $A \rightarrow a$ 进行归约。
  2. 根据 $B \rightarrow a$ 进行归约。

这是一个归约 / 归约冲突,因为分析器无法确定是应该将 $a$ 归约为 $A$ 还是 $B$。

LR ($k$) 语法分析

LR ($k$) 语法分析的定义:

  • L 表示对输入进行从左到右的扫描
  • R 表示反向构造出一个最右推导序列
  • $k$ 表示在做出语法分析决策时向前看 $k$ 个输入符号(用于指导规约操作)

对于实际应用,$k = 0$ 和 $k = 1$ 具有重要意义,因此这里只考虑 $k \leq 1$ 的情况。当省略 $k$ 时,假设 $k = 1$。

LR (0) 项和 LR (0) 自动机

LR (0) 项

:一些状态,这些状态表示了语法分析过程中所处的位置。

一个文法 $G$ 的一个 LR (0) 项 是 $G$ 的一个产生式再加上一个位于它的右侧某处的点。

举例:$A \rightarrow XYZ$:

  • $A \rightarrow \cdot XYZ$
  • $A \rightarrow X \cdot YZ$
  • $A \rightarrow XY \cdot Z$
  • $A \rightarrow XYZ \cdot$

这里,$\cdot$ 标记了当前读到的位置,$\cdot$ 左边是已经读到的,$\cdot$ 右边是尚未读到的。

项表明了语法分析过程的给定点,我们已经看到一个产生式的哪些部分。

比如,$A \rightarrow X \cdot YZ$ 表明当前已经读到了 $X$,期望接下来在输入中看到一个从 $YZ$ 推导得到的串(从而可以规约回 $YZ$,再读入 $YZ$ 后即可规约回 $A$)。

LR (0) 项可分为四类:

  1. 移进项:$A \to \alpha \cdot a \beta, \quad a \in V_T$,表示当前可以读取符号 $a$ 并进行移入操作
  2. 待归约项:$A \to \alpha \cdot B \beta, \quad B \in V_N$,表示当前需要继续其他操作后(至少还要把 $B$ 给规约出来),才可以归约到 $A$
  3. 归约项:$A \to \alpha \cdot$,表示当前可以进行规约操作(已经把一个产生式体完全读入了),即将 $\alpha$ 规约为 $A$
  4. 接受项:$S' \to S \cdot$

对于产生式 $A \to \varepsilon$ 的唯一一项是 $A \to \cdot$,它是归约项。

项集:这些项的列表

我们还可以划分每个项为如下两类:

  1. 内核项:包括初始项 $S' \rightarrow \cdot S$ 以及点不在最左端的所有项(代表要么正在从头开始,要么已经有一些已读信息了)
  2. 非内核项:除了 $S' \rightarrow \cdot S$ 之外点在最左端的所有项(代表我们对于这个产生式完全没有任何已读信息)

规范 LR (0) 项集族的构造

为了构造一个文法的规范 LR (0) 项集族,我们定义了一个 增广文法 (augmented grammar)和两个函数:ClosureGoto

增广文法

如果 $G$ 是一个以 $S$ 为开始符号的文法,那么 $G$ 的增广文法 $G'$ 就是在 $G$ 中加上新开始符号 $S'$ 和产生式 $S' \rightarrow S$ 而得到的文法。

当且仅当语法分析器要使用规则 $S' \rightarrow S$ 进行归约时(即 $S' \rightarrow S \cdot$),输入符号串被接受(即表明已经完全规约回到了原开始符号)。

引入这个新的开始产生式的目的是使得文法开始符号($S'$)仅出现在一个产生式的左边,从而使得分析器只有一个接受状态

项集的闭包

如果 $I$ 是文法 $G$ 的一个项集,那么 $\text{Closure}(I)$ 就是根据下面的两个规则从 $I$ 构造得到的项集:

  1. 一开始,将 $I$ 中的各个项加入到 $\text{Closure}(I)$ 中
  2. 如果 $A \rightarrow \alpha \cdot B \beta$ 在 $\text{Closure}(I)$ 中,$B \rightarrow \gamma$ 是一个产生式,并且项 $B \rightarrow \cdot \gamma$ 不在 $\text{Closure}(I)$ 中,就将这个项加入其中。不断应用这个规则,直到没有新项可以加入到 $\text{Closure}(I)$ 为止

直观地讲,$\text{Closure}(I)$ 中的项 $A \rightarrow \alpha \cdot B \beta$ 表明在语法分析过程的某点上,我们认为接下来可能会在输入串中看到一个能够从 $B \beta$ 推导得到的子串。

这个可以从 $B \beta$ 推导得到的子串的某个前缀肯定可以从 $B$ 推导得到,而推导 / 逆向规约时必然要用某个 $B$ 产生式。

因此我们加入了各个 $B$ 产生式对应的项。也就是说,如果 $B \rightarrow \gamma$ 是一个产生式,那么我们把 $B \rightarrow \cdot \gamma$ 加入到 $\text{Closure}(I)$ 中。

Goto 函数

$\text{Goto}$ 函数形式为 $\text{Goto}(I, X)$,其中:

  • $I$ 是一个项集
  • $X$ 是一个文法符号

$\text{Goto}(I, X)$ 被定义为 $I$ 中所有形如 $[A \rightarrow \alpha \cdot X \beta]$ 的项所对应的项 $[A \rightarrow \alpha X \cdot \beta]$ 的集合的闭包,即:

$$\text{Goto}(I, X) = \text{Closure}({ [A \rightarrow \alpha X \cdot \beta] \mid [A \rightarrow \alpha \cdot X \beta] \in I })$$

直观地讲,$\text{Goto}$ 函数用于定义一个文法的 LR (0) 自动机中的移入单个符号( 终结符号或者非终结符号都可以 )的步骤,也即一类 状态转换

求 LR (0) 项集规范族的算法

$$ \begin{aligned} &\text{void } items(G') { \ &\quad C = \textbf{Closure}({[S' \to \cdot S]}); \ &\quad \text{repeat} \ &\quad \quad \text{for (}C \text{ 中每个项集 } I \text{)} \ &\quad \quad \quad \text{for (每个文法符号 } X \text{)} \ &\quad \quad \quad \quad \text{if (} Goto(I, X) \text{ 非空且不在 } C \text{ 中)} \ &\quad \quad \quad \quad \quad 将 Goto(I, X) 加入 C 中; \ &\quad \text{until 在某一轮中没有新的项集被加入到 } C \text{ 中;} \ &} \end{aligned} $$

从初始项集开始,不断计算各种可能的后继,直到生成所有的项集。

LR (0) 自动机的构造

  1. 规范 LR (0) 项集族中的项集可以作为 LR (0) 自动机的状态

  2. $\text{Goto}(I, X) = J$,则从 $I$ 到 $J$ 有一个标号为 $X$ 的转换

  3. 初始状态为 $\text{Closure}({ S' \rightarrow \cdot S })$ 对应的项集

  4. 接受状态:包含形如 $A \rightarrow \alpha \cdot$ 的项集对应的状态,即任何表示识别出了一个句柄的状态都是这个自动机的终态

    可以发现所有的终态都是规约动作,说明 LR 事实上就是一直规约句柄的过程

    对于整个 LR (0) 的编译过程而言,$S'\to S \cdot$ 当然是表示编译完成的终态

    但是 LR (0) 自动机只是我们构造 LR (0) 分析表的中间步骤

    要手动填 Action 和 Goto 表之后才构成整个 LR (0) 分析流程

移入 - 归约决策过程

假设文法符号串 $\gamma$ 使 LR (0) 自动机从开始状态运行到状态 (项集) $j$:

  1. 归约判断:如果 $j$ 中有一个形如 $A \rightarrow \alpha \cdot$ 的项,那么:
    • 在 $\gamma$ 之后添加一些 终结符号 可以得到一个最右句型
    • $\alpha$ 是 $\gamma$ 的后缀,且 $A \rightarrow \alpha$ 是这个句型的句柄
    • 表示 可能 找到了当前最右句型的句柄
  2. 移入判断:如果 $j$ 中存在一个项 $B \rightarrow \alpha \cdot X \beta$,那么:
    • 在 $\gamma$ 之后 添加 $X \beta$,然后再添加一个终结符号串 可以得到一个最右句型
    • 在这个句型中 $B \rightarrow \alpha X \beta$ 是句柄
    • 此时表示还没有找到句柄,至少还需要移进 $X$

LR 语法分析表

语法分析表由两个部分组成:

  • 一个语法分析动作函数 $\text{Action}$
  • 一个转换函数 $\text{Goto}$

Action 表

$\text{Action}$ 函数有两个参数:

  • 状态 $i$
  • 终结符号 $a$(或者是输入结束标记 $$$ $$)。

$\text{Action}[i, a]$ 的取值可以有下列四种形式:

  1. 移入(Goto) $S_j$:$j$ 表示一个状态,$S_j$ 表示移进(Shift)到 $j$。语法分析器的动作是将输入符号 $a$ 移入栈中,使用状态 $j$ 来代表 $a$
  2. 归约(Reduce) $r_j$:产生式 $j = A \rightarrow \beta$:语法分析器将栈顶的 $\beta$ 根据这个产生式归约为产生式头 $A$
  3. 接受(Accept):语法分析器接受输入并完成语法分析过程
  4. 报错(Error):语法分析器在输入中发现错误并执行某个纠正动作

Goto 表

我们把定义在项集上的 $\text{Goto}$ 函数扩展为定义在状态集上的函数:如果 $\text{Goto}[I_i, A]=I_j$,那么 $\text{Goto}$ 把状态 $i$ 和一个非终结符号 $A$ 映射到状态 $j$。

分析过程

  1. 把状态 0($S_0$)和符号 $$ $ $$ 压入初始为空的栈里。

  2. 设置栈顶元素中的状态为 $s$,当前读入的符号为 $a$。

  3. 反复执行以下各动作,直到分析成功或发现语法错误为止:

    1. 移进:若 $\text{Action}[s, a]=S_i$,(Shift,移进)则把 $a$ 和状态 $i$ 压进栈,读下一个输入符号到 $a$ 中

    2. 归约:若 $\text{Action}[s, a]=r_j$ (reduce,即产生式 $j=A \rightarrow X_{m-k+1} X_{m-k+2} \cdots X_m$),则出栈 $k$ 项,把 $A$ 和 $s_{new}=\text{Goto}[s', A]$ 进栈,其中 $s'$ 是出栈 $k$ 项后新的栈顶元素中的状态

    3. 接受:若 $$\text{Action}[s, $]=\text{accept}$$,则分析成功,结束

  4. 出错:若 $$\text{Action}[s, a]=\text{error}$$,则转由错误处理程序

举例说明

文法:

$$ \begin{aligned} E &\rightarrow T E' \ E' &\rightarrow + T E' | \varepsilon \ T &\rightarrow F T' \ T' &\rightarrow * F T' | \varepsilon \ F &\rightarrow ( E ) | id \end{aligned} $$

假设输入字符串为 $id + id * id$。

image-20241114155742253

image-20241114155750011

分析表结构

分析表的第一列是状态,第二列是 Action 部分,由 $|T|+1$ 列构成,第三列是 Goto 部分,由 $|V|$ 列构成。

$$ \text{Action}[s, a] = \begin{cases} 移进 S_i & a\ 和状态\ i\ 进栈 \ 归约 r_j & \text{出栈 k 项,然后}\ A\ \text{和 Goto[s',A] 进栈} \ 接受 & \text{接受} \ 出错 & \text{出错} \end{cases} $$

其中:

  • $s$ 是状态
  • $a$ 是读入的终结符(单词)或 $$ $ $$
  • $k$ 是 $j$ 号产生式 $A \rightarrow \beta$ 的长度 $|\beta|$
  • $s'$ 是出栈 $k$ 项后新的栈顶元素中的状态

LR (0) 分析表中的冲突

移进规约冲突

假设有一个项集 $I$ 包含以下项目:

  • $A \rightarrow \alpha \cdot a \beta$
  • $B \rightarrow \gamma \cdot$

在这种情况下,如果当前输入符号是 $a$:

  • 根据项目 $A \rightarrow \alpha \cdot a \beta$,分析器会尝试移进符号 $a$,以期待未来能够归约到 $A$
  • 根据项目 $B \rightarrow \gamma \cdot$,分析器会尝试进行归约操作,把当前栈顶的 $\gamma$ 归约为 $B$

这就导致了移进 - 归约冲突。

移进规约冲突解决方案:SLR 分析表

SLR:Simple LR。

依据 $\text{Follow}$ 集来选择是否进行归约。

如果 $I={X \rightarrow \alpha \cdot b \beta$,$A \rightarrow \alpha \cdot$,$B \rightarrow \alpha \cdot}$,且若 ${b}$、$\text{Follow}(A)$、$\text{Follow}(B)$ 两两不交,则面对应当前读入符号 $a$,状态 $I$ 的解决方法:

  1. 若 $a=b$,则移进
  2. 若 $a \in \text{Follow}(A)$,则用 $A \rightarrow \alpha$ 进行归约
  3. 若 $a \in \text{Follow}(B)$,则用 $B \rightarrow \alpha$ 进行归约
  4. 此外,报错

注:此处只举例了两个规约项、一个移入项,实际上可以有更多个规约项、移入项。

每个 SLR (1) 文法都是无二义性的,但是存在很多不是 SLR (1) 的无二义性文法。

SLR 原理:可行前缀(Viable Prefix)

不是所有的最右句型的前缀都可以出现在栈中,因为语法分析器在移入时 不能越过句柄

可行前缀 (Viable Prefix):某个最右句型的前缀,且没有越过该句型的句柄的右端。

有效项:如果存在 $S \Rightarrow \alpha Aw \Rightarrow \alpha \beta_1 \beta_2 w$,那么我们说项 $A \rightarrow \beta_1 \cdot \beta_2$ 对 $\alpha \beta_1$ 有效。

当我们知道 $A \rightarrow \beta_1 \cdot \beta_2$ 对 $\alpha \beta_1$ 有效:

  • 如果 $\beta_2$ 不等于空,表示句柄尚未出现在栈中,应继续移进或者等待归约
  • 如果 $\beta_2$ 等于空,表示句柄出现在栈中,应归约

如果某个时刻存在两个有效项要求执行不同的动作,那么就应该设法解决冲突。

冲突实际上表示可行前缀可能是两个最右句型的前缀,第一个包含了句柄,而另一个尚未包含句柄。

SLR 解决冲突的方法:假如要按照 $A \rightarrow \beta$ 进行归约,那么得到的新句型中 $A$ 后面跟着的是下一个输入符号。因此只有当下一个输入在 $\text{Follow}(A)$ 中时才可以归约。

  • 如果在文法 $G$ 的 LR (0) 自动机中,从初始状态出发,沿着标号为 $\gamma$ 的路径到达一个状态,那么这个状态对应的项集就是 $\gamma$ 的 有效项集

  • 回顾确定分析动作的方法,就可以知道我们实际上是按照有效项来确定的

    为了避免冲突,归约时要求下一个输入符号在 $\text{Follow}(A)$ 中,且 SLR 语法保证了 $\text{Follow}$ 集合两两不交

SLR 语法分析器的弱点

没有展望符号

没有展望符号,不能确定规约之后还是不是可行前缀(即使 $\text{Follow}$ 集合得到满足也不保证)

举例:

  1. 假设此时栈中的符号串为 $\beta \alpha$,输入符号是 $a$
  2. 如果 $\beta A a$ 不能是某个最右句型的前缀,那么即使 $a$ 在某个句型中跟在 $A$ 之后,仍然不应该按照 $A \rightarrow \alpha$ 归约。

不能提前确定信息

$A \rightarrow \alpha \cdot$ 出现在项集中的条件:

  1. 首先 $A \rightarrow \cdot \alpha$ 出现在某个项集中,然后逐步读入 / 归约到 $\alpha$ 中的符号,点不断后移,直到末端

  2. 而 $A \rightarrow \cdot \alpha$ 出现的条件是 $B \rightarrow \beta \cdot A \gamma$ 出现在项中

    期望首先按照 $A \rightarrow \alpha$ 归约,然后将 $B \rightarrow \beta \cdot A \gamma$ 中的点后移到 $A$ 之后

  3. 显然,在按照 $A \rightarrow \alpha$ 归约时要求下一个输入符号是 $\gamma$ 的第一个符号

  4. 但是从 LR (0) 项集中不能确定这个信息

💾

  •  

语法分析 II

文法的设计方法

消除二义性

一些二义性文法可以被改成等价的无二义性文法

例子:dangling-else

$$ \begin{aligned} \text{stmt} \rightarrow& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{stmt} \ |& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{stmt} \ \textbf{else} \ \text{stmt} \ |& \text{other} \end{aligned} $$

在这个语法下,$\textbf{if} \ \text{expr}_1 \ \textbf{then} \ \textbf{if} \ \text{expr}_2 \ \textbf{then} \ \text{stmt}_1 \ \textbf{else} \ \text{stmt}_2$ 有两棵语法树:

image-20241109131909671

即:这个 else 既可以和第一个 then 匹配,也可以和第二个 then 匹配。

消除 dangling-else 二义性

引入 matched_stmt 表示匹配好的语句,文法如下:

$$ \begin{aligned} \text{stmt} \rightarrow& \text{matched_stmt} | \text{open_stmt} \ \text{matched_stmt} \rightarrow& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{matched_stmt} \ \textbf{else} \ \text{matched_stmt} \ |& \text{other} \ \text{open_stmt} \rightarrow& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{stmt} \ |& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{matched_stmt} \ \textbf{else} \ \text{open_stmt} \end{aligned} $$

即:通过引入新的非终结符,来保证 else 与最近未匹配的 then 匹配。

例子:近对称符号串

文法 $G$ 的产生式如下:

$$ S \rightarrow aSb ,|, bSa ,|, SS ,|, ba ,|, ab $$

$L(G)$:

  • 最小单元:$abab,aabb,baba,bbaa$
  • 最小单元外侧可以对称着包 $a\cdots b$ 或者 $b\cdots a$
  • 然后包完了还可以重复

二义性示例

$G$ 是二义性的,例如 $ababab$ 有两个不同的最左推导:

  1. $S \Rightarrow SS \Rightarrow abS \Rightarrow abSS \Rightarrow ababab$
  2. $S \Rightarrow SS \Rightarrow SSS \Rightarrow abS \Rightarrow ababab$

等价上下文无关文法

$$ \begin{aligned} S &\rightarrow TS ,|, T \ T &\rightarrow aB ,|, bA \ A &\rightarrow a ,|, bAA \ B &\rightarrow b ,|, aBB \end{aligned} $$

消除文法中的左递归

文法左递归:$A\Rightarrow^+A\alpha$

  • 直接左递归:直接左递归经过一次推导就可以看出文法存在左递归 $$ A \rightarrow A \alpha \mid \beta $$
  • 间接左递归:间接左递归是指需多次推导才可以看出文法存在左递归 $$ S \rightarrow A a \mid b \ A \rightarrow S d \mid \varepsilon $$

消除直接左递归

将原始规则 $A \rightarrow A \alpha \mid \beta$ 转换为:

$$ \begin{aligned} A &\rightarrow \beta A' \ A' &\rightarrow \alpha A' \mid \varepsilon \end{aligned} $$

消除间接左递归

  1. 先转换成直接左递归:

    使用替换法,将 $S$ 的规则替换为 $A$ 的规则:

    $$ \begin{aligned} S &\rightarrow A a \mid b \ A &\rightarrow S d \mid \varepsilon \end{aligned} $$

    转换为:

    $$ \begin{aligned} S &\rightarrow A a \mid b \ A &\rightarrow A a d \mid b d \mid \varepsilon \end{aligned} $$

  2. 再消除左递归:

    $$ \begin{aligned} A &\rightarrow b d A' \mid A' \ A' &\rightarrow a d A' \mid \varepsilon \end{aligned} $$

消除所有左递归的算法

  1. 将文法 $G$ 的非终结符顺序整理为 $A_1, A_2, \cdots, A_n$。

  2. 逐步消除间接左递归

    1. 对于每个 $i$ 从 1 到 $n$,对于每个 $j$ 从 1 到 $i-1$,将形如 $A_i \rightarrow A_j r$ 的规则替换为:

      $$ A_i \rightarrow \delta_1 r \mid \delta_2 r \mid \cdots \mid \delta_k r $$

      其中,$A_j \rightarrow \delta_1 \mid \delta_2 \mid \cdots \mid \delta_k$ 是当前 $A_j$ 的所有产生式。

    2. 然后,消除 $A_i$ 规则中的直接左递归。

    理解:循环操作,先避免 $A_i \rightarrow A_j r\ (j < i)$ 的退化,再消除 $A_i$ 的左递归,从而避免了所有非终结符的左递归

  3. 化简得到的文法

预测分析法

预测分析法:试图从开始符号推导出输入符号串

  • 以开始符号 $S$ 作为初始的当前句型
  • 每次为最左边的非终结符号选择适当的产生式
    • 通过查看下一个输入符号来选择这个产生式
    • 有多个可能的产生式时预测分析法无能为力

问题:当两个产生式具有相同的前缀时无法预测

文法:

$$ \begin{aligned} \text{stmt} \rightarrow& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{stmt} \ \textbf{else} \ \text{stmt} \ |& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{stmt} \end{aligned} $$

处理办法:提取左公因子

新文法:

$$ \begin{aligned} \text{stmt} \rightarrow& \textbf{if} \ \text{expr} \ \textbf{then} \ \text{stmt} \ \text{elsePart} \ \text{elsePart} \rightarrow& \textbf{else} \ \text{stmt} \mid \varepsilon \end{aligned} $$

提取左公因子

含有左公因子的文法:

$$ A \rightarrow \alpha \beta_1 \mid \alpha \beta_2 $$

提取左公因子:

$$ \begin{aligned} A &\rightarrow \alpha A' \ A' &\rightarrow \beta_1 \mid \beta_2 \end{aligned} $$

自顶向下的语法分析

定义:自顶向下分析是从文法的开始符号出发,试构造出一个 最左推导,从左至右匹配输入的单词串。

步骤

  1. 推导替换
    • 当前被替换的非终结符号为 $A$
    • 当前从左至右读到的单词符号为 $a$
  2. 匹配产生式
    • 如果 $A$ 的产生式为:$A \rightarrow \alpha_1 \mid \alpha_2 \mid \cdots \mid \alpha_n$
    • 其中由 $\alpha_i(1 ≤ i ≤ n)$ 推导出的第一个终结符号为 $a$,则选择产生式 $A \rightarrow \alpha_i$ 构造最左推导
  3. 策略
    • 用 $\alpha_i$ 替换 $A$,进行预测分析
    • 如果匹配失败,则进行回溯尝试

关键点

  • 自顶向下分析通过 试探和回溯 来构造符合输入的句子结构。
  • 最左推导是核心策略,确保每一步都尽可能匹配输入的左边部分。

回溯的解决

对文法加什么样的限制可以保证没有回溯?

在自顶向下的分析技术中,通常使用向前看几个符号来唯一地确定产生式(这里只假定只看一个符号)。

  1. 假设当前句型是 $xA\beta$,而输入是 $xa\cdots$,那么选择产生式 $A \rightarrow \alpha$ 的 必要条件 是下列之一:

    • $\alpha \Rightarrow^* \varepsilon$ 且 $\beta$ 以 $a$ 开头(可以用更强的条件替代:在某个句型中 $a$ 跟在 $A$ 之后)
    • $\alpha \Rightarrow^* a\cdots$
  2. 如果按照这两个条件选择时能够保证唯一性,那么我们就可以避免回溯

总结:

  • 使用向前看符号(展望符号)来唯一确定产生式
  • 确保选择产生式时满足特定条件以避免回溯

First 和 Follow

First 集合

First:可以从某个符号串 $\alpha$ 推导出的串的首符号(终结符)的集合。

  • 形式化定义:

    $$ \text{First}(\alpha) = {a \mid \alpha \Rightarrow^* a\cdots, a \in V_T} $$

    其中,$V_T$ 是终结符的集合

  • 特别地,如果 $\alpha \Rightarrow^* \varepsilon$,即 $\alpha$ 可以推导出空串 $\varepsilon$,那么我们规定 $\varepsilon \in \text{First}(\alpha)$

简单来说,First 集合包含了 $\alpha$ 能够推导出的所有串的第一个终结符

Follow 集合

Follow:可能在某些句型中紧跟在非终结符 $A$ 右边的终结符的集合。

  • 形式化定义:

    $$ \text{Follow}(A) = {a \mid S \Rightarrow^* \cdots Aa\cdots, a \in V_T} $$

    其中,$S$ 是开始符号

  • 如果 $A$ 是某个句型的最右符号时,那么 $$ $ $$ 也属于 $\text{Follow}(A)$

简单来说,Follow 集合包含了在某些推导过程中可能出现在 $A$ 右边的终结符

计算 First 集合

计算单个符号 X 的 First 集合

终结符:如果 $X$ 是终结符,那么 $\text{First}(X) = {X}$。

非终结符

  1. 如果 $X$ 是非终结符,并且 $X \rightarrow Y_1Y_2\cdots Y_k$ 是一个产生式:

    • 如果某个 $a$ 在 $\text{First}(Y_i)$ 中,并且 $\varepsilon$ 在 $\text{First}(Y_1), \text{First}(Y_2), \cdots, \text{First}(Y_{i-1})$ 中,那么 $a$ 也在 $\text{First}(X)$ 中

      人话:如果 $\varepsilon$ 在这些 $\text{First}(Y_i)$ 中,那么就意味着 $Y_i \Rightarrow^* \varepsilon$,也就可以忽略前面的

    • 如果 $\varepsilon$ 在 $\text{First}(Y_1), \text{First}(Y_2), \cdots, \text{First}(Y_k)$ 中,那么 $\varepsilon$ 也在 $\text{First}(X)$ 中

      人话:所有的子部分都可以推出空串,那么 $X$ 也可以推出空串

  2. 如果 $X$ 是非终结符,并且 $X \rightarrow \varepsilon$ 是一个产生式,那么 $\varepsilon$ 在 $\text{First}(X)$ 中

计算产生式右部 $X_1 X_2 \cdots X_n$ 的 First 集合

  1. 向集合中加入 $\text{First}(X_1)$ 中所有非 $\varepsilon$ 的符号
  2. 如果 $\varepsilon$ 在 $\text{First}(X_1)$ 中,再加入 $\text{First}(X_2)$ 中的所有非 $\varepsilon$ 的符号
  3. 依次类推,直到所有 $X_i$ 被处理完
  4. 如果 $\varepsilon$ 在所有的 $\text{First}(X_i)$ 中,则将 $\varepsilon$ 加入 $\text{First}(X_1 X_2 \cdots X_n)$ 中

计算 Follow 集合

  1. 将右端结束标记 $$$ $$ 放到 $\text{Follow}(S)$ 中。

  2. 不间断迭代以下规则,直到所有的 Follow 集合都不再增长为止:

    • 如果存在产生式 $A \rightarrow \alpha B \beta$,那么 $\text{First}(\beta)$ 中所有非 $\varepsilon$ 的符号都在 $\text{Follow}(B)$ 中

      人话:此时即存在式子可以推导出 $Bx, x \in \text{First}(\beta)$

    • 如果存在产生式 $A \rightarrow \alpha B$,或者 $A \rightarrow \alpha B \beta$ 且 $\text{First}(\beta)$ 包含 $\varepsilon$,那么 $\text{Follow}(A)$ 中的所有符号都加入到 $\text{Follow}(B)$ 中

      人话:此时即  $\text{Follow}(A) \sub \text{Follow}(B)$,因为对于每个 $A$ 出现的式子,我们都可以执行这个替换,从而使得原本接在 $A$ 后面的字符接到 $B$ 后面

LL (1) 文法

定义:对于文法中任意两个不同的产生式 $A \rightarrow \alpha | \beta$:

  1. 不存在终结符号 $a$ 使得 $\alpha$ 和 $\beta$ 都可以推导出以 $a$ 开头的串

  2. $\alpha$ 和 $\beta$ 最多只有一个可以推导出空串

  3. 如果 $\beta$ 可以推导出空串,那么 $\alpha$ 不能推导出以 $\text{Follow}(A)$ 中任何终结符号开头的串

    理解:如果可以,那么产生了二义性:

    • 对于 $A$ 推导为 $\beta$,然后再推导得到空串 $\varepsilon$,接着后接 $\text{Follow}(A)$ 中的字符
    • 对于 $A$ 推导为 $\alpha$,然后再推导得到 $\text{Follow}(A)$ 中的字符

注:这里不一定只有 $\alpha$ 和 $\beta$ 两个产生式,而是所有可能的产生式,这里只是简写了(有 “任意两个” 这一条件)。

这里主要是为了自顶向下的语法分析的时候能确定找到唯一路径。

等价条件

对于文法中任意两个不同的产生式 $A \rightarrow \alpha | \beta$:

  • $\text{First}(\alpha) \cap \text{First}(\beta) = \varnothing$ (条件 1, 2)
  • 如果 $\varepsilon \in \text{First}(\beta)$,那么 $\text{First}(\alpha) \cap \text{Follow}(A) = \varnothing$ (条件 3)

LL (1) 文法的说明

输入串以 $$$ $$ 为结束标记,这相当于对文法作扩充,即增加产生式 $$S' \rightarrow S$ $$,所以 $\text{Follow}(S)$ 一定包含 $$$ $$。

预测分析表的构造方法

  • 输入:文法 $G$

  • 输出:预测分析表 $M$,用于指导预测分析器如何根据当前输入符号和栈顶符号做出解析决策

    其中每一项 $M[A, a]$ 表明当前栈顶是 $A$,输入符号是 $a$ 时,应该使用哪个产生式

  • 构造方法:

    • 对于文法 $G$ 的每个产生式 $A \rightarrow \alpha$:

      对于 $\text{First}(\alpha)$ 中的每个终结符号 $a$,将 $A \rightarrow \alpha$ 加入到 $M[A, a]$ 中;

      如果 $\varepsilon \in \text{First}(\alpha)$,那么对于 $\text{Follow}(A)$ 中的每个符号 $b$,将 $A \rightarrow \alpha$ 加入到 $M[A, b]$ 中。

    • 最后在所有的空白条目中填入 $\text{error}$

image-20241115024536309

LL (1) 文法解析例子

假设有以下文法:

$$ \begin{aligned} E &\rightarrow T E' \ E' &\rightarrow + T E' | \varepsilon \ T &\rightarrow F T' \ T' &\rightarrow * F T' | \varepsilon \ F &\rightarrow ( E ) | id \end{aligned} $$

假设输入字符串为 $id + id * id$。

那么,LL (1) 分析器的工作流程如下:

首先计算出 First 集合:

$$ \begin{aligned} \text{First}(E) &= {(, id)} \ \text{First}(E') &= {+, \varepsilon} \ \text{First}(T) &= {(, id)} \ \text{First}(T') &= {*, \varepsilon} \ \text{First}(F) &= {(, id)} \end{aligned} $$

  1. 初始状态:
    • 输入:$$id + id * id $ $$($$ $ $$ 是输入结束符)
    • 符号栈:$$E $ $$(注意,左边是栈顶
  2. 根据预测:
    • 当前栈顶 $E$ 和输入符号 $id$ 使我们选择产生式 $E \rightarrow T E'$
    • 更新符号栈为 $$T E' $ $$
  3. 继续向下:
    • 栈顶 $T$ 和输入符号 $id$ 选择 $T \rightarrow F T'$
    • 更新符号栈为 $$F T' E' $ $$
  4. 接着:
    • 栈顶 $F$ 和输入符号 $id$ 使用 $F \rightarrow id$,匹配后弹出 $id$
    • 更新符号栈为 $$T' E' $ $$
  5. 重复此过程,运用 First 集合和下一个输入符号进行预测,以此类推

错误处理

目的:继续完成整段程序的语法分析

思路:将预测分析表中的空白位置以某种方式填充

两种方法:恐慌模式 / 短语层次的恢复

语法错误的类型

  • 词法错误:标识符 / 关键字拼写错误等
  • 语法错误:分号位置错误,${ }$ 不匹配
  • 语义错误:运算符和变量类型不匹配
  • 逻辑错误:编译器看不出来,例如 = 和 == 写错导致的错误(但可以过编译)

恐慌模式

思路:忽略输入中的部分符号,直到出现特定的 “同步词法单元”,我们认为 “同步词法单元(sync)” 和之前的内容都属于当前符号(出错的这个符号),然后跳过该段,继续分析

省流:当分析器遇到错误时,它会忽略一些输入符号,直到遇到某个可以继续解析的符号。

方法:将 $\text{First}(A)$ 和 $\text{Follow}(A)$ 加入 $A$ 的 同步集合 中。在预测分析表中,标记为 sync

假设出错时,我们在试图识别一个非终结符号 $A$ (栈顶为 $A$),遇到了终结符号 $a$

  • 如果 $M[A, a]$ 为空,什么也不做,直接忽略 $a$ (将之视为多打了的字符)
  • 如果 $M[A, a]$ 为 sync,则跳过 $a$,弹出栈顶的 $A$ (认为从当前位置一直到 $a$ 都属于 $A$ 的范畴),然后尝试继续正常的语法分析过程

短语层次的恢复

  • 预测分析表 的空白位置,填入 指向错误处理例程的指针

  • 错误处理例程的可能行为:

    • 改变 / 插入 / 删除符号
    • 发送错误信息
    • 弹栈
  • 要避免死循环

💾

  •  

语法分析 I

概述

程序设计语言构造的语法可使用 上下文无关文法BNF 表示法 来描述

语法分析器的作用

graph LR
    A[源程序] --> B[词法分析器] -->|Token| C[语法分析器] --> D[分析树]
    C -->|取下一个Token| B
  • 功能:根据文法规则,从源程序单词符号串中识别出语法成分,并进行语法检查
  • 基本任务:识别符号串 $S$ 是否为某个合法的语法单元

语法分析器的种类

分类

  • 通用语法分析器
    • 可以对任意文法进行语法分析
    • 效率很低,不适合用于编译器
  • 自顶向下 的语法分析器
    • 从语法分析树的根部开始构造语法分析树
  • 自底向上 的语法分析器
    • 从语法分析树的叶子开始构造语法分析树

后两种方法

  • 通常从左到右逐个扫描词法单元
  • 为了保证效率,只针对特定类型的文法,但是这些文法足以用来描述常见的程序设计语言

文法(Grammar)

定义:文法 $G = (V_T, V_N, S, P)$,其中:

  • $V_T$ 是一个非空有穷的 终结符号(terminal) 集合

  • $V_N$ 是一个非空有穷的 非终结符号(nonterminal) 集合,且 $V_T \cap V_N = \varnothing$

  • $P = { \alpha \to \beta | \alpha \in (V_T \cup V_N)^\text{且至少包含一个非终结符号}, \beta \in (V_T \cup V_N)^}$,称为 产生式(production) 集合

    • BNF 范式:产生式可以写成 $A ::= \alpha$ 或 $A \rightarrow \alpha$
    • $A \rightarrow \alpha_1 \quad A \rightarrow \alpha_2$ 可以缩写为:$A \rightarrow \alpha_1 | \alpha_2$
  • $S \in V_N$,称为 开始符号(start symbol)

    $S$ 必须在某个产生式的左部至少出现一次

关于文法的一些约定

通常可以不用将文法 $G$ 的四元组显式地表示出来,而只需将产生式写出,一般约定:

  • 第一条产生式 $P_0$ 的左部是 开始符号
  • 尖括号 $<>$ 括起来的是 非终结符号,而 不用尖括号 的是 终结符号
  • 或者 大写字母 $ABC$ 表示 非终结符号小写字母 $abc$ 表示 终结符号
  • 小写的希腊字母 $\alpha \beta \gamma$ 表示 (可能为空的) 文法符号串

另外也可以把 $G$ 表示为 $G[S]$,其中 $S$ 为开始符号

上下文无关文法(Context-free grammar,CFG)

所有产生式的左边只有一个非终结符号,即

  • 产生式的形式为:$A \rightarrow \beta$
  • 因此不需要任何上下文(context)就可以对 $A$ 进行推导

上下文无关文法描述的语言称为上下文无关语言

推导 / 规约

直接推导 / 直接规约

直接推导(Immediate Derivation)/ 直接规约(Immediate Reduction):若某个串 $\alpha$ 可以根据某条文法一步化为串 $\beta$,则称:

  • $\alpha$ 可以直接推导出 $\beta$
  • $\beta$ 可以直接归约到 $\alpha$

标准定义:令语法 $G=(V_T, V_N, S, P)$,若 $\alpha \to \beta \in P$,且 $\gamma, \delta \in (V_T \cup V_N)^*$,则称 $\gamma \alpha \delta$ 可以直接推导出 $\gamma \beta \delta$,表示为:

$$ \gamma \alpha \delta \Rightarrow \gamma \beta \delta $$

如果 $\gamma \alpha \delta$ 直接推导(左到右) 出 $\gamma \beta \delta$,即 $\gamma \alpha \delta \Rightarrow \gamma \beta \delta$,则称 $\gamma \beta \delta$ 直接归约(右到左) 到 $\gamma \alpha \delta$。

规约是推导的逆过程。

推导(Derivation)

若一个直接推导序列为:

$$ \alpha_0 \Rightarrow \alpha_1 \Rightarrow \alpha_2 \Rightarrow \ldots \Rightarrow \alpha_n \quad (n > 0) $$

可以表示为:

$$ \alpha_0 \Rightarrow^+ \alpha_n $$

拓展定义 $\alpha_0 \Rightarrow^* \alpha_n$ 为:

  • 要么 $\alpha_0 = \alpha_n$ (直接就是)
  • 要么 $\alpha_0 \Rightarrow^+ \alpha_n$ (经过几次推导)

这里类似正则表达式,在正则表达式中:

  • + 代表一次或者多次匹配
  • * 代表零次或者多次匹配

最左推导和最右推导

对于文法 $G$ 和字符串 $w$,如果 $w \in L(G)$,即 $w$ 可以由 $G$ 生成,那么有如下构造推导 $S \Rightarrow^* w$ 的方法:

  • 最左推导:若 $\alpha A \beta \Rightarrow_{lm} \alpha \gamma \beta$, $\alpha \in V_T^*$,即 $\alpha$ 是一个由终结符组成的字符串
  • 最右推导:若 $\alpha A \beta \Rightarrow_{rm} \alpha \gamma \beta$, $\beta \in V_T^*$,即 $\beta$ 是一个由终结符组成的字符串

最左推导每次替换最左边的非终结符,而最右推导每次替换最右边的非终结符。

句型 / 句子 / 语言

句型(sentential form)

如果 $S \Rightarrow^* \alpha$,那么 $\alpha$ 是文法的句型

  • 句型 可能既包含非终结符号,又包含终结符号
  • 句型也 可以是空串

型 → 行,即可以达到的状态

句子(sentence)

文法的句子是 不包含非终结符号 的句型(即全是终结符号,最终状态)

子 → 子集(即最具体的句子)

语言

文法 $G$ 的语言是 $G$ 的所有 句子 的集合,记为 $L(G)$

$w$ 在 $L(G)$ 中当且仅当 $w$ 是 $G$ 的句子,即 $S \Rightarrow^* w$

证明文法生成的语言

基本步骤:

  1. 首先证明 $L(G) \subseteq L$(文法 $G$ 生成的任意句子都属于语言 $L$)
  2. 然后证明 $L \subseteq L(G)$(语言 $L$ 的任意句子都可以用文法 $G$ 生成)
  3. 一般可以使用 数学归纳法
    • $L(G)\subseteq L$:按推导序列的长度来归纳
    • $L\subseteq L(G)$:按符号串长度来构造推导序列

文法生成语言的例子

文法 $G$:$$ S \rightarrow (S)S \mid \varepsilon $$

语言 $L$:所有具有对称括号的串。

$L(G) \subseteq L$ 的证明:依据 推导序列的长度 来归纳

  • 归纳基础:推导长度为 $n=1$, $S \Rightarrow \varepsilon$,满足括号对称。

  • 归纳步骤:假设长度小于 $n$ 的推导都能得到括号对称的句子。考虑推导步骤为 $n$ 的最左推导:

    $$ S \Rightarrow_{lm} (S)S \Rightarrow_{lm}^{} (x)S \Rightarrow_{lm}^{} (x)y $$

    其中 $x$ 和 $y$ 的 推导步骤 都小于 $n$,因此 $x$ 和 $y$ 也是括号对称的句子

    即依据 推导路径长度 来进行归纳

$L \subseteq L(G)$ 的证明:依据 生成句子长度 来进行归纳

  • 注意:指括号对称的串的长度必然是偶数。

  • 归纳基础:如果指括号对称的串的长度为 $0$,那么它可以从 $S$ 推导得到。

  • 归纳步骤:假设长度小于 $2n$ 的指括号对称的串都能被 $S$ 推导得到,$w$ 是括号对称且长度为 $2n$ 的串。

    那么,$w$ 必然以左括号开头,且可以写成 $(x)y$ 的形式,其中 $x$ 也是括号对称的。因为 $x$、$y$ 的长度都小于 $2n$ ,根据归纳假设,$x$ 和 $y$ 都可以从 $S$ 推导得到,进而 $w$ 可以从 $S$ 推导得到: $$ S \Rightarrow_{lm} (S)S \Rightarrow_{lm}^{} (x)S \Rightarrow_{lm}^{} (x)y $$

语法解析树(Parse Tree)

语法解析树:推导的一种图形表示形式

  • 根节点:文法的开始符号 $S$
  • 叶子节点:非终结符号、终结符号或 $\varepsilon$
  • 内部节点(即非叶子节点):非终结符号
    • 每个内部节点往下推,表示某个产生式的一次应用
    • 内部节点的标签为产生式左部,该节点的子节点从左到右对应产生式的右部

image-20241109121121869

画的时候可以从顶向下推导,也可以从底向上规约。

几点说明:

  • 有时允许根不是开始符号(对应于某个短语)
  • 树的叶子组成的序列是根的文法符号的句型
  • 一棵解析树可对应多个推导序列,但是解析树和最左(右)推导序列之间具有一一对应关系

二义性 / 歧义性(Ambiguity)

定义

  • 如果一个文法中存在某个句子有两棵解析树,那么该句子是 二义性的
  • 如果一个文法产生二义性的句子,则称这个文法是 二义性的
  • 否则,该文法是 无二义性的

举例

考虑下面的表达式文法 $G2[E]$,其产生式如下:

$$ E \rightarrow E + E \mid E * E \mid (E) \mid a $$

对于句子 $a + a * a$,有如下两个最左推导:

$$ E \Rightarrow E + E \Rightarrow a + E \Rightarrow a + E * E \Rightarrow a + a * E \Rightarrow a + a * a $$

$$ E \Rightarrow E * E \Rightarrow E + E * E \Rightarrow a + E * E \Rightarrow a + a * E \Rightarrow a + a * a $$

image-20241109121121869

几点说明

  1. 一般来说,程序语言存在 无二义性文法

  2. 在能够驾驭的情况下,经常使用二义性文法

    条件语句通常使用二义性文法描述。

  3. 对于任意一个上下文无关文法,不存在一个算子,判定它是无二义性的;

    但能够给出一组充分条件,满足这组充分条件的文法是无二义性的。

  4. 存在 先天二义性语言,即语言本身就是二义性,无论采用何种文法描述。

    例如:${ a^i b^i c^j | i, j \geq 1 } \cup { a^i b^j c^j | i, j \geq 1 }$

    存在一个二义性的句子 $a^k b^k c^k$。

上下文无关文法和正则表达式

上下文无关文法比正则表达式的能力 更强

  • 所有的正则语言都可以使用上下文无关文法描述。
  • 但是一些用上下文无关文法描述的语言不能用正则文法描述。

用上下文无关文法描述的语言不都能用正则文法描述

  1. 首先证明:存在上下文无关文法 $S \rightarrow aSb \mid ab$ 描述了语言 ${ a^n b^n | n > 0 }$,但是它 无法用 DFA 识别

  2. 反证法:假设 DFA 识别该语言,设这个文法有 $k$ 个状态。

    那么在其尝试识别 $a^{k+1}$ (即输入串中有 $k+1$ 个 $a$)的输入串时,必然两次到达同一个状态($a$ 到 $a^k$ 最多用 $k$ 个状态,再来一个肯定重复,也即抽屉原理)。

    设自动机在第 $i$ 和第 $j$ 个输入 $a$ 时到达同一个状态(那么就形成了环路)。

    那么,因为 DFA 识别 $L$,$a^i b^i$ 必然到达接受状态。

    由于 $a^i$、$a^j$ 使得 DFA 到达同一个状态,所以 $a^j b^i$ 也必然到达接受状态。

    这与 $a^j b^i$ 不是语言的句子矛盾。

任何正则语言都可以表示为上下文无关文法的语言

首先,任何正则语言都必然有一个等价的 NFA。

而对于任意的 NFA 可以构造如下的上下文无关文法:

  1. 对 NFA 的每个状态 $i$,创建非终结符号 $A_i$
  2. 如果有 $i$ 在输入 $a$ 上到达 $j$ 的转换,增加产生式 $A_i \rightarrow a A_j$
  3. 如果 $i$ 在输入 $\varepsilon$ 上到达 $j$,那么增加产生式 $A_i \rightarrow A_j$
  4. 如果 $i$ 是一个接受状态,增加产生式 $A_i \rightarrow \varepsilon$
  5. 如果 $i$ 是初始状态,令 $A_i$ 为所得文法的开始符号

非上下文无关的语言结构

在程序语言中,某些语言结构 不能总能用上下文无关文法 描述。

  1. 例 1 $$ L_1 = { wcw \mid w \in {a,b}^+ } $$
    • 例如,aabcaab 是 $L_1$ 的一个句子

    • 该语言是检查程序中标识符的声明应先于引用的抽象(先声明 $w$,隔了 $c$,再引用 $w$)

      int a;
      // ...
      a++;
      
  2. 例 2 $$ L_2 = { a^n b^m c^n d^m \mid n,m \geq 0 } $$
    • 它是检查程序声明的形参个数和过程调用的实参个数一致的问题的抽象(先 $n$ 个 $a$,再 $m$ 个 $b$,再 $n$ 个 $c$,再 $m$ 个 $d$)

      int f(int a, int b){
        //...
      }
      f(1, 2)
      

文法分类(Chomsky)

0 型(任意文法)

$$ G = (V_T, V_N, S, P) $$

  • 规则形式:$\alpha \rightarrow \beta, {~}{~}\alpha, \beta \in (V_T \cup V_N)^*, {~}{~}\alpha \neq \varepsilon$
  • 翻译:任意非空串到任意串
  • 推导:$\gamma \alpha \delta \Rightarrow \gamma \beta \delta$

1 型(上下文有关,Context-Sensitive Grammar)

  • 规则形式:$\alpha A \beta \rightarrow \alpha \gamma \beta,{~}{~}A \in V_N, {~}{~}\alpha, \gamma, \beta \in (V_T \cup V_N)^*,{~}{~} \gamma \neq \varepsilon$
  • 翻译:需要一个上下文(式中 $\alpha,\beta$),然后发生一次非终止符号到任意串的推导
  • 注:可以包含 $S \to \varepsilon$,但此时不允许 $S$ 出现在产生式右边

2 型(上下文无关,Context-Free Grammar, CFG)

  • 规则形式:$A \rightarrow \beta, {~}{~}A \in V_N, {~}{~}\beta \in (V_T \cup V_N)^*$
  • 翻译:没有上下文,产生式左侧只能为一个非终止符号,右侧可以为任意串
  • 上下文无关语法是没有记忆的

3 型(正则文法,Regular Grammar)

  • 右线性:$A \rightarrow aB, {~}{~}A \rightarrow a$
  • 左线性:$A \rightarrow Ba, {~}{~}A \rightarrow a, {~}{~}a \in V_T \cup { \varepsilon }$
  • 两种只能选其一
  • 翻译:产生式左侧只能为一个非终止符号,右侧最多包含两个符号,且其中一个必须是非终结符

总结

  • 每一类逐渐对产生式施加限制,表示范围逐步缩小。
  • 任意文法 > 上下文有关**(可以有记忆)> 上下文无关(没有记忆)**> 正则文法

在程序语言中的实际应用

  • 与词法相关的规则属于 正则文法

  • 与局部语法相关的规则属于 上下文无关文法

  • 与全局语法和语义有关的部分主要用 上下文有关文法 来描述,实际上很少使用

  • 为简化分析过程,会把 描述词法的正则文法描述语法的上下文无关文法 中分离出来

    在分离出正则文法后的上下文无关文法中,这些单词符号属于终结符号 $V_T$ 中的符号

💾

  •  

词法分析 III

从 NFA 构造 DFA

闭包 $\varepsilon\text{_closure}(S)$

定义:从状态集合 $S$ 中 任一状态出发,仅沿 $\varepsilon$ 弧到达的状态集合(包括 $S$ 自身)称为 $S$ 的 $\varepsilon$ 闭包,记为 $\varepsilon\text{_closure}(S)$:

$$ T = S \cup (\bigcup \text{edge}(t, \varepsilon)), \quad t \in T $$

其中,$\text{edge}(t, a)$ 是 $M$ 中从状态 $t$ 出发,仅沿 $a$ 弧到达的状态集合。

DFA M' 中的状态

  • $M'$ 中的每个状态是 $M$ 的状态集合。
  • 令 $t_0$ 是 $M$ 的初始状态,$M'$ 的初始状态 $d_0 = \varepsilon\text{_closure}({t_0})$。
  • 包含 $M$ 的任意终止状态的状态集合都是 $M'$ 中的终止状态。

DFA M' 的转移函数

$$ \text{DFAedge}(d, a) = \varepsilon\text{_closure}(\bigcup_{t \in d} \text{edge}(t, a)) $$

其中:

  • $d$ 是 $M$ 的状态集合
  • $a \in \Sigma$
  • $\text{edge}(t, a)$ 是 NFA $M$ 中从状态 $t$ 出发,仅沿 $a$ 弧到达的状态集合。

DFA 的最小化

给定 DFA $M = (\Sigma, Q, q_0, F, \delta)$,寻找一个状态数更少的 DFA $M'$,使 $L(M') = L(M)$。

可以证明,存在一个最少状态的 DFA $M'$,使 $L(M) = L(M')$。

等价状态

  • 设 $p, q \in Q$,若对任意 $w \in \Sigma^*$,$\delta(p, w) \in F$ 当且仅当 $\delta(q, w) \in F$($F$ 是终态集合),则称 $p$ 和 $q$ 是等价状态
  • 否则,称 $p$ 和 $q$ 是可区别的

等价状态的意义:如果两个状态是等价的,则可以将它们合并成一个状态而不影响 DFA 接受的语言。

等价状态的判别条件

等价状态定义了状态集合上的等价关系。因此状态集合能被划分成等价类。

两个状态 $p$ 和 $q$ 等价应满足如下条件:

  • 一致性条件:$p$ 和 $q$ 必须同时为接受状态或为非接受状态。
  • 蔓延性条件
    • 对于 $\forall a \in \Sigma$,$\delta(p, a) = r$,$\delta(q, a) = s$,$r$ 和 $s$ 必须 等价
    • 反之若 $r$ 和 $s$ 不等价,则 $p$ 和 $q$ 不等价

等价类划分方法

  1. 把所有状态划分为两个组:接受状态组和非接受状态组。
  2. 任意选定一个输入符号 $a$,判断每个组中的各个状态对于 $a$ 的转换,如果落入不同的组中,就把该组中的状态按照转换之后的组进行分割,使分割之后的每个组对于 $a$ 的转换都落入同一个组。
  3. 重复第 2 步,直至每个组中的所有状态都等价。

感觉是一个不断二分的过程?

例子

dfa_minimize_1

dfa_minimize_2

从正则表达式构造 FA(有限自动机)

定理:设 $r$ 是 $\Sigma$ 上一个正则表达式,则存在 FA $M$ 接受 $L(r)$,并且 $M$ 的终态是唯一的且无有向边射出。

证明:对正则表达式 $r$ 的 运算符数目 作归纳。

设 $r$ 具有零个运算符,必有 $r=\varepsilon$ 或 $r=\varnothing$ 或 $r=a \in \Sigma$,则 FA 分别为:

reg_fa

设结论对少于 $i$($i\leq1$)个运算的正则表达式 $r$ 成立。

当 $r$ 有 $i$ 个运算时,有三种情况:

  • $r = r_1 \mid r_2$
  • $r = r_1 r_2$
  • $r = r_1^*$

有 $M_1=(\Sigma_1, Q_1, q_1, F_1, \delta_1)$,$M_2=(\Sigma_2, Q_2, q_2, F_2, \delta_2)$ 且 $L(M_1)=L(r_1)$,$L(M_2)=L(r_2)$。

由 $M_1$ 和 $M_2$ 构造 $M$,使得 $L(M)=L(r)$,构造方法如图示如下:

  • 情况 1:$r = r_1 \mid r_2$

    regex_1

  • 情况 2:$r = r_1 r_2$

    regex_2

  • 情况 3:$r = r_1^*$

    regex_3

由此可以证明:假定知道 $r$ 的计算顺序,对于任意正则表达式 $r$,可以构造一个 FA $M$,使得 $L(M)=L(r)$。

转换得到的 NFA 的特性

  • 状态数量最多为 $r$ 中的运算符和运算符分量总数的两倍
    • 因为每个步骤只引入两个状态
  • 有且只有一个开始状态和一个接受状态
  • 除接受状态之外,每个状态要么有一条标号不为 $\varepsilon$ 的出边,要么有两条标号为 $\varepsilon$ 的出边

NFA 合并的方法

  1. 引入新状态:引入新的开始状态 $s_0$,并引入从这个开始状态到各个原开始状态的 $\varepsilon$ 转换
  2. 语言并集:得到的 NFA 所接受的语言是原来各个 NFA 语言的 并集
  3. 不同接受状态:不同的接受状态可代表不同的模式
  4. 模式识别:不仅判断输入前缀是否 NFA 的语言,还需知道对应于哪个模式

nfa_merge

NFA 到 DFA 的转换

  1. 确定化:对得到的 NFA 进行确定化,得到 DFA。

    可进一步对得到的 DFA 的状态进行最小化。

  2. 状态集合:一个 DFA 的接受状态对应于 NFA 状态的集合,其中 至少包括一个 NFA 接受状态

    如果其中包括多个对应于不同模式的 NFA 接受状态,则表示当前的输入前缀对应于多个模式,存在冲突。

  3. 模式输出:找出第一个这样的模式,将这个模式作为这个 DFA 接受状态的输出。

例子

image-20241109074237696

image-20241109074244234

image-20241109074255086

运行的方式

  1. 模拟 DFA,不断读入字符串中的字符,直到某一时刻没有后继为止(不是达到某个接受状态)
  2. 回头查找最后的接受状态,执行相应的动作
    • 如果查不到,报告词法错误
    • 在回退时,需要同时回退读入的字符

💾

  •  

词法分析 II

状态转换图 (Transition Diagram)

状态 (State):在识别词素时可能出现的情况,即对表示已处理部分的总结。

  • 接受状态或最终状态:表示找到词素。
  • 加上 * 的接受状态:表示最后读入的符号不在词素中。
  • 开始状态(初始状态):用 “开始 / Start” 边表示。

边 (Edge):从一个状态指向另一个状态,边的标号是一个或多个符号。

  • 当前状态为 $s$,下一个输入符号为 $a$,则从 $s$ 沿着标号为 $a$ 的边到达下一个状态 $s \xrightarrow{a} s'$

词法单元的自动识别

基本目标:判断一个串 $s$ 是否属于一个正则表达式 $R$ 表示的语言:

$$ s \in L(R) $$

词法自动识别过程

  1. 分别为每一类词法单元写出正则表达式 $R_i$
  2. 构造一个正则表达式 $R$ 来匹配所有的词法单元: $$ R = R_1 | R_2 | \ldots | R_k $$
  3. 输入为 $x_1 x_2 \ldots x_n$,对于 $1 \leq i \leq n$,检查是否 $x_1 \ldots x_i \in L(R)$
  4. 如果匹配成功,则存在 $j$,使得 $x_1 \ldots x_i \in L(R_j)$
  5. 把 $x_1 \ldots x_i$ 从输入中移走,继续执行步骤(3)

匹配过程中需要解决的问题

  1. 确定匹配长度:可能有多种前缀,选择最长匹配。
  2. 选择正则表达式:可能有多个正则表达式匹配,优先匹配前面的。
  3. 无法匹配:构造一个 ERROR 正则表达式,放在表末尾,用于报错。

Lex

Lex:一种词法分析程序自动构造工具,通常与 Yacc 一起使用,生成编译器前端。

实现原理:根据正则表达式自动生成词法分析程序,利用正则表达式与 DFA 的等价性。

转换方式:正则表达式 $\Rightarrow$ NFA $\Rightarrow$ DFA $\Rightarrow$ min DFA

用 Lex 建立词法分析程序的过程

lex_process

词法分析器的工作方式

  • Lex 生成的词法分析器作为函数被调用
  • 每次调用过程中读取输入符号
  • 发现最长的匹配输入前缀时,执行相应动作
    • 动作处理并返回控制
    • 如果不返回,继续寻找词素

Lex 源程序

由三部分组成:声明、转换规则及动作、辅助子程序

各部分用 %% 隔开

声明

  • 包括变量、C 语言常量和正则定义式

转换规则及动作

  • 形式:p_i {动作 i}
  • 识别某类单词时,执行相应动作
  • 动作用 C 语言书写

辅助子程序

  • 执行动作所需的 C 语言程序,可单独编译

Lex 冲突解决方法:优先按规则顺序匹配,规则在前者优先。

Lex 程序示例

%{
/* 定义常量 */
LT, LE, EQ, NE, GT, GE, IF, THEN, ELSE, ID, NUMBER, RELOP
%}

/* 正则定义 */
delim       [\t\n]
ws          {delim}+
Letter      [A-Za-z]
digit       [0-9]
id          {Letter}({Letter}|{digit})*
Number      {digit}+(\.{digit}+)?(E[+-]?{digit}+)?

%%

{ws}        {/* 不返回 */}
if          {return(IF);}
then        {return(THEN);}
else        {return(ELSE);}
{id}        {yylval = (int) installID(); return(ID);}
{number}    {yylval = (int) installNum(); return(NUMBER);}
"<"         {yylval = LT; return(RELOP);}
"<="        {yylval = LE; return(RELOP);}
"=="        {yylval = EQ; return(RELOP);}
"!="        {yylval = NE; return(RELOP);}
">"         {yylval = GT; return(RELOP);}
">="        {yylval = GE; return(RELOP);}

%%

int installID() {/* 添加符号表指向 yytext */}
int installNum() {/* 添加数字常量到表格 */}

yylval 是 Lex 提供的变量,用于返回词法单元的值。

有限自动机 (Finite Automata)

有限自动机是词法分析器生成工具(Lex)的关键技术。

正则表达式 $\rightarrow$ 有限自动机 $\rightarrow$ 词法分析程序

识别功能:有限自动机与状态转换图类似,只能对每个可能的输入串简单地回答 “yes” 或 “no”。

分类

  • 确定的有限自动机(Deterministic Finite Automaton, DFA
  • 不确定的有限自动机(Nondeterministic Finite Automaton, NFA

确定的有限自动机 (DFA)

定义:一个确定的有限自动机 $M$(记作 DFA $M$)是一个五元组 $M = (\Sigma, Q, q_0, F, \delta)$,其中:

  1. $\Sigma$ 是一个有限字母表,称为输入符号。

  2. $Q$ 是一个有限状态集合。

  3. $q_0 \in Q$,称为初始状态。

  4. $F \subseteq Q$,称为终止状态(或接受状态)集合。

  5. $\delta$ 是一个从 $Q \times \Sigma \to Q$ 的单值映射(称为转换函数)

    即:$\delta(q, a) = q' \quad (q, q' \in Q, a \in \Sigma)$ 表示当前状态为 $q$,输入符号为 $a$ 时,自动机 $M$ 将转换到下一个状态 $q'$,$q'$ 称为 $q$ 的一个后继

DFA 接受的语言

如果 DFA 中存在一条 从初始状态到接受状态 的路径,路径上的符号序列构成的字符串是 $w$,那么该 DFA 可以接受字符串 $w$。

  • $\delta(q, \varepsilon) = q$
  • $\delta(q, wa) = \delta(\delta(q, w), a)$
  • $L(M) = {w \mid w \in \Sigma^*, \text{若存在} q \in F \text{(接受状态)}, \text{使} \delta(q_0, w) = q}$

表示形式

  • 转移矩阵
  • 状态转换图

expression

举例

识别 $\Sigma={0,1}$ 上能被能 $5$ 整除的二进制数

dfa_example

(0|1(10)*(0|11)(01*01|01*00(10)*(0|11))*1)*

先画出 DFA,然后从 0 开始,转换到 1,转换到 2,再转换到 0。中间有环路的描述。

这里每条转换的 $q \xrightarrow{a} q':q' = (2 \times q + a) % 5$ 。

不确定的有限自动机(NFA)

定义:NFA 是一个五元组 $M = (\Sigma, Q, q_0, F, \delta)$,其中:

  1. $\Sigma$ 是一个有限字母表,称为输入符号。
  2. $Q$ 是一个有限状态集合。
  3. $q_0 \in Q$,称为初始状态。
  4. $F \subseteq Q$,称为终止状态(或接受状态)集合。
  5. $\delta$ 是一个从 $Q \times (\Sigma \cup {\varepsilon}) \to 2^Q$ 的映射(称为转换函数,$2^Q$ 表示 $Q$ 的幂集)

NFA 接受的语言

如果 NFA 中存在一条 从初始状态到接受状态 的路径,路径上的符号序列构成的字符串是 $w$,那么该 NFA 可以接受字符串 $w$,记作 $w \in L(M)$。

关于 NFA 的说明

  1. 接受的字符串和语言
    • 字符串在 NFA 中可能对应不同的接受路径。
    • 接受的字符串可能存在其他不能接受的路径。
    • 如果某状态对输入字符 $a$ 不存在可用的转移动作,则不能通过该路径接受当前字符串。
  2. DFA 是 NFA 的一种特例:DFA 的表达能力与 NFA 等价。

💾

  •  

词法分析 I

词法分析器

  • 读入源程序字符流,输出 token 序列
  • 过滤空白 / 换行 / 制表符 / 注释
  • 将 token 信息添加到符号表
  • 逻辑上独立于语法分析,但是通常和语法分析器在同一 Pass

lexical-analyzer

基础概念

词法单元 token

结构:<词法单元名, 属性值(可选)>

  • 单元名:表明该词法单位的种类,是表示词法单位种类的抽象符号,词法分析器通过各 token 的单元名即可确定词法单元序列的结构
  • 属性值:可选,用于语义分析之后的阶段

模式 pattern

描述一类词法单元的词素可能具有的形式

词素 lexeme

  • 源程序中的字符序列
  • 如果一个词素和某个 token 的模式相匹配,它会被词法分析器识别为该 token 的实例

词法分析器的功能

  • 识别词法单元 token
  • 去除注释 / 空白 / 空行 / 制表符
  • 将编译器生成的错误信息关联到源文件
  • 可能要进行一些 预处理:识别宏 macro;宏的扩展

token 的类别

  • 关键字 Keyword:if, else, while, return,没有属性值
  • 标识符 Identifier:变量名等
  • 字面常数 Literal:12,true,1e+3
  • 运算符 Operator:+ - * /
  • 分界符 Delimiter:逗号 / 分号 / 冒号 /etc

词法分析器的输出

Token 的基本输出格式:<类别编码, 词法单元自身的属性值>

在词法分析过程中,有时候需要无限长的向前看

词法分析的设计

  • 可以实现为单独的一个扫描(pass)
  • 也可以作为语法分析 / 语义分析的子程序,即每调用一次 getToken() 函数即获得一个 token

语言和正则表达式

规约(Specification):用正则表达式来描述处理词法单元时用到的模式类型

字母表 Alphabet

字母表:符号的非空有穷集合

每一程序语言都有自己的字母表

  • 机器语言:符号 01
  • ASCII 字符集

符号串 String / 字 word

已知字母表 $\Sigma$

  1. $ε$ 是 $\Sigma$ 上的一个 符号串 (空串)
  2. 若 $\alpha$ 是 $\Sigma$ 上的符号串,而 $a$ 是 $\Sigma$ 的元素,则 $\alpha a$ 是 $\Sigma$ 上的符号串。
  3. $\beta$ 是 $\Sigma$ 上的符号串,当且仅当它由 1 和 / 或 2 导出(递归定义)。

定义:由字母表中的符号所组成的 任意有穷序列 被称为该字母表上的 符号串(String),也称作 字(Word)

通常约定

  • 靠前的小写字母表示 符号:$a, b, c$
  • 小写希腊字母或靠后的小写英文字母表示 符号串:$α, β, γ, x ,y ,z$
  • $ε$ 通常表示 空串
  • 大写字母表示 符号串集合:$A,B,C$

相关概念

设 $x$ 是一个符号串,定义如下概念:

  • 前缀(prefix):移走 $x$ 尾部的 零个 或多个连续的符号。
  • 后缀(suffix):移走 $x$ 头部的 零个 或多个连续的符号。
  • 子串(substring):从 $x$ 中删去一个前缀和一个后缀。
  • 真前缀 / 真后缀 / 真子串:首先要非空(和集图不同),而且不等,即 $y\neq x\mathrm{~}&\mathrm{~}y\neq\mathbf{\varepsilon}$
  • 子序列(subsequence):从 $x$ 中删去 零个或多个 符号(这些符号 不要求是连续的 )。
  • 逆转(reverse) :或称转置,用 $x^R$ 表示。将 $x$ 中的符号按相反次序写出而得到的符号串。
  • 长度(length) :符号串中的符号的数目。如 $|aab| = 3$,$|\varepsilon| = 0$

符号串的运算

  1. 连接 (concatenation)

    设 $x$ 和 $y$ 是符号串,它们的连接 $xy$ 是把 $y$ 的符号写在 $x$ 的符号之后得到的符号串。

    例如,$x = ba,{~}y = nana\Rightarrow{~}xy = banana$

  2. 方幂 (exponentiation)

    • $x^0 = \varepsilon$
    • $x^1 = x$
    • $x^2 = xx$
    • $x^n = x^{n-1}x$

语言(符号串集合)

语言(language):某个给定字母表上的一个任意的可数的符号串集合。

语言的例子

  • 空集 $\varnothing$
  • 只包含空串的集合 ${\varepsilon}$
  • 所有符合规范的 C 语言标识符的集合
  • 所有语法正确的 C 语言程序的集合
  • 所有语法正确的英语句子的集合

语言的运算

设 $L$ 和 $M$ 是两个符号串集合,则:

  1. 合并 (union) $$ L \cup M = {s | s \in L \text{ 或 } s \in M} $$

  2. 连接 (concatenation) $$ LM = {st | s \in L \text{ 且 } t \in M} $$

  3. 方幂 (exponentiation)

    • $L^0 = {\varepsilon}$
    • $L^1 = L$
    • $L^2 = LL$
    • $L^n = L^{n-1}L$
  4. 语言 $L$ 的 Kleene 闭包(closure)

    记作 $L^*$:

    $$ L^* = \bigcup_{i \geq 0} L^i = L^0 \cup L^1 \cup L^2 \cup L^3 \cup \ldots $$

  5. 语言 $L$ 的正闭包(positive closure)

    记作 $L^+$:

    $$ L^+ = L \cdot L^* $$

    $$ L^+ = \bigcup_{i \geq 1} L^i = L^1 \cup L^2 \cup L^3 \cup L^4 \cup \ldots $$

辨析

  1. 空集 $\varnothing$:空集是一个不包含任何元素的集合。
  2. 只包含空串的集合 ${\varepsilon}$:这个集合包含一个元素,即空串 $\varepsilon$。空串是长度为零的字符串。

运算性质:

  • 空集 $\varnothing$:没有元素。

    因此,对于任何集合 $M$,有:

    $$ \varnothing M = M \varnothing = \varnothing $$

    因为空集与任何集合的笛卡尔积仍然是空集

  • 集合 ${\varepsilon}$:只包含空串。

    这个集合包含一个元素 $\varepsilon$。对于任何集合 $M$,有:

    $$ {\varepsilon} M = M {\varepsilon} = M $$

    因为空串与任何字符串的连接操作不会改变字符串

正则表达式与正则语言 Regular Expression

定义:某个字母表 $\Sigma$ 上的正则表达式及其对应的正则集合(正则语言),满足以下条件:

  1. $\varepsilon$ 是一个正则表达式,表示的语言 $L(\varepsilon) = {\varepsilon}$。
  2. 若 $a \in \Sigma$,$a$ 是一个正则表达式,$L(a) = {a}$。
  3. 归纳步骤:设 $r$ 和 $s$ 是 $\Sigma$ 上的正则表达式:
    • $(r) | (s)$ 是一个正则表达式,表示语言 $L(r) \cup L(s)$,即或
    • $(r)(s)$ 是一个正则表达式,表示语言 $L(r) L(s)$,即连接在一起
    • $(r)^$ 是一个正则表达式,表示语言 $(L(r))^$,即重复
    • $(r)$ 是一个正则表达式,表示语言 $L(r)$

注意:去掉一个正则表达式中的冗余括号之后,它表示的正则语言不变(注意运算的优先级)。

正则表达式示例

例:$\Sigma = {a, b}$

  • $a | b$:${a, b}$
  • $(a | b)(a | b)$:${aa, ab, ba, bb}$
  • $a^*$:${\varepsilon, a, aa, aaa, aaaa, \dots}$
  • $(a | b)^$ 或 $(a^b^)^$:${\varepsilon, a, b, aa, ab, ba, bb, aaa, \dots}$
  • $a^*b$:${b, ab, aab, aaab, \dots}$

C 语言标识符可视化

(A|B|...|Z|a|b|...|z|_)((A|B|...|Z|a|b|...|z|_ |0|1|...|9))*
// [A-Z_][A-Za-z0-9_]*

有符号整数可视化

(+|-|ε)(0|1|...|9)(0|1|...|9)*
// [+-]?[0-9][0-9]*

正则表达式的性质

设 $e_1, e_2, e_3$ 均为某字母表上的正则表达式,则有:

  • 单位正则表达式 $\varepsilon$:$\varepsilon e = e \varepsilon = e$
  • 交换律:$e_1 | e_2 = e_2 | e_1$
  • 结合律:$e_1 | (e_2 | e_3) = (e_1 | e_2) | e_3$,$e_1(e_2 e_3) = (e_1 e_2)e_3$
  • 分配律:$e_1(e_2 | e_3) = e_1 e_2 | e_1 e_3$,$(e_1 | e_2)e_3 = e_1 e_3 | e_2 e_3$

此外:

  • $r^* = (r\varepsilon)^*$
  • $r^{**} = r^*$
  • $(r|s)^* = (r^* s^)^$

正则定义(Regular Definition)

正则定义是如下形式的定义序列:

$$ D_1 \rightarrow R_1 \ D_2 \rightarrow R_2 \ \vdots \ D_n \rightarrow R_n $$

其中:

  • $R_1, R_2, \ldots, R_n$ 为正则表达式。
  • $D_1, D_2, \ldots, D_n$ 为正则表达式名字。

限定:在 $R_i$ 中只能出现字母表 $\Sigma$ 中的字符,以及前面已定义的正则表达式名字,即 $D_1, D_2, \ldots, D_{i-1}$。

我们用这种辅助定义式(相当于规则)来定义程序语言的单词符号。

正则表达式的扩展形式

为了表达的方便,通常可以对正则表达式做如下的扩展:

  • 1 次或多次出现:$(r)+$ 用来表示 $L(r)+$

    $r^* = r+|\varepsilon \quad r+ = rr^* = r^* r$

  • 0 次或 1 次出现:$r?$ 用来表示 $r | \varepsilon$

    也就是 $L(r) \cup {\varepsilon}$

  • 字符类:$[abc]$ 表示 $a|b|c$;$[a-z]$ 表示 $a|b|c|\ldots|z$

建议看 RegexLearn

例题

写出语言 “所有相邻数字都不相同的非空数字串” 的正则定义。

解答:正则定义如下

$$ \begin{aligned} &\text{answer} & \rightarrow &\ (0 \mid \text{no_0}\ 0)(\text{no_0}\ 0)^(\text{no_0} \mid \varepsilon) \mid \text{no_0} \ &\text{no_0} & \rightarrow &\ (1 \mid \text{no_0-1}\ 1)(\text{no_0-1}\ 1)^(\text{no_0-1} \mid \varepsilon) \mid \text{no_0-1} \ &{~~~}\vdots & &\ \ &\text{no_0-8} & \rightarrow &\ 9 \ \end{aligned} $$

将这些正则定义逆序排列就是答案。

  1. 顶层规则 answer

    • answer 可以是以 0 开头的数字串,或者以 no_0 开头的数字串。
    • 对于以 0 开头的串,后面可以跟任意多个 (no_0 0),最后再跟一个 no_0 或者为空($\varepsilon$)。
    • 对于以 no_0 开头的串,直接匹配 no_0
  2. 子表达式 no_0

    • no_0 代表不能以 0 开头的数字串,其定义类似于 answer,但替换了数字。
    • no_0 可以是以 1 开头,后面可以跟任意多个 (no_0-1 1),最后再跟一个 no_0-1 或者为空($\varepsilon$)。
    • 对于以 no_0-1 开头的串,直接匹配 no_0-1
  3. 递归定义

    • 其他子表达式 no_0-1no_0-2,直到 no_0-8,都以类似的方式定义,保证生成的串中相邻数字始终不同。
    • 最终,no_0-8 只能匹配 9

💾

  •