[{"content":"现在是 2025 年 10 月 27 日，我还困在杭州，接受为期两个月的出差折磨。\n每到这种生活最艰难、情绪最糟的时候，我都会想起两年前，那个在计划第一次出国旅行的自己。 说出来可能很俗，但我一直觉得，一次旅行里最开心的时刻，往往发生在出发之前的那段筹划期。\n时间拨回到 2023 年 11 月。 刚刚经历完残酷的保研，那种“总算活下来了”的虚脱感很快被另一种冲动取代：我突然很想在寒假出一次国。\n这个想法来得非常突然，也可能并不突然。也许是从 9 月开始一路积压下来的压力，让我意识到，我的大学四年好像一直都在为“保研”服务。既然阶段性目标终于落地，是不是可以允许自己，真正地，休息一下？\n本科那几年，说实话，疫情把我们困得很死，基本没什么旅游的机会。 我和朋友有一次在暑假飞成都，计划玩一周，结果刚落地成都就发现当地出现病例。我们第一天的第一件事不是吃火锅，而是去社区排队做核酸。那种体验，说真的，到现在都不太想回忆。\n所以当疫情终于结束、保研也尘埃落定的时候，我感觉像是被突然放生了：我想出去走走，去一个完全不一样的地方，感受一种完全不属于我日常生活的节奏。\n我立刻联系了朋友，开始准备护照，开始讨论去哪儿。\n我其实一直有几个“如果有机会我一定想去”的目的地。 第一是美国西雅图。因为我初中的时候，DOTA2的TI就在西雅图举办。 第二个，就是日本。\n我高中时被同学一把拉进了二次元的坑。日本对动漫爱好者来说不仅仅是“一个国家”，它是现实世界可以去的“圣地巡礼地图”。 所谓“圣地巡礼”，就是去动画里的取景地，站在和角色同一块楼梯、同一段铁轨边，去体验“我和角色真的在同一个时空里”的那种错觉。\n《你的名字》里的楼梯，《吹响！上低音号》里的宇治市街景……这些地方在日本都是真实存在、真能走到眼前的。对我来说，这个诱惑实在是太大了。\n更现实一点，日本的性价比也很高：从国内过去，机票价格在所有出国游里算便宜的。不是那种去欧美一张票几千上万，还得熬十几个小时航班的硬仗。淡季往返日本甚至不用 2000，飞行时间也就三小时左右。相比东南亚，日本整体治安更让人放心；相比新加坡、马来西亚，对我个人来说，日本的文化密度又不会显得无聊。\n所以几乎没有太多犹豫，我就直接把板子拍死了：去日本。\n一系列准备之后，2024 年 1 月 19 日，我们终于登上了飞往日本的航班。\n整趟旅行，我们的路线是“阪进东出”，也就是：大阪入境，坐新干线一路玩到东京，再从东京回国。\n这条路线是日本自由行非常经典的路线，基本可以一次性扫过日本大部分大家耳熟能详的城市和景点。 确实会有点走马观花，但对我们这些第一次踏上日本国土的人来说，这个行程几乎完美。 就算是走马观花，那也是骑着赤兔马去看上野的樱花。\n当我们真正从关西机场走出来的时候，已经是下午四点多了。 折腾好了交通卡，我们直接上了去市中心的电车。\n在那之前，我坐过的轨道交通基本只有国内的地铁——比如北京地铁。 所以第一次上日本的电车，我还是有点被惊到了。\n日本的大部分轨道交通其实都跑在地上。一方面应该是因为地上修更省钱，另一方面也可能是很多地方地下空间并不宽裕。当然，在市中心区域还是有纯地下的地铁线路，只是相比国内那种“全城全是地下线”的印象，日本更多是“地上+地下”并行。\n这次一共有四个人一起旅行。我们在住酒店还是住民宿这件事上犹豫了很久，最终还是选了民宿。\n（后来回国后和别的朋友聊到这件事才发现，好像不是所有人都喜欢住民宿……）\n我们当时的理由其实很朴素：一是便宜。同等地段，民宿人均价格确实能比酒店低不少。二是大家可以住在一起。晚上还能聊天吹牛，而不是像住酒店那样各回各屋，一个房间塞两个人，空间又小。\n如果让我现在再选，我还是会倾向于民宿。原因甚至已经不是便宜，而是它更像是在真实地住进日本的日常。\n从机场到我们住的地方，沿途可以很明显地感受到环境在变化：从郊外，到相对繁华的市区，再到最后那种有点像民居密集区的街区，甚至可以说带点平民窟味道的那种压迫感。\n日本的轨道交通真的很夸张，夸张到像任意门。 你走出同一个地铁站的出口，眼前可能是最繁华的商圈，也可能是四下无人、安静到诡异的一片独栋住宅区。\n街道普遍很窄，大多是单行道，四个人并排行走都会觉得别扭。 很多地方甚至没有严格意义上的人行道——行人和车就这么共用一条巷子。 房子和街道之间几乎没有退距，民宅一栋挨着一栋，像积木拼出来一样，没有我们习惯意义上的采光权、隐私距离这种浪漫说法。 头顶的高压线就像喝醉了的上班族，晃晃悠悠地在空中乱拧成一团。\n我来之前其实刷过很多日本看房视频，但是真正站在巷子里，还是会被这种逼仄和密度震到。\n等我们终于摸到民宿的时候，天已经黑了。 但所谓特种兵式旅游是没有休息的，于是我们放下行李就直奔心斋桥，顺便解决晚饭。\n日本地铁的复杂程度，足够让第一次来的游客当场晕头转向。\n我今年又再去了一次大阪，结果在难波附近换乘的时候还是一脸懵。 不同线路交织在一起，站厅分层、站中站、同名不同站台……你真的会怀疑自己是不是走进了什么迷宫副本。\n那天我们实在搞不清楚了，决定逮个人问路。\n正好迎面走来一位中年大叔。我们把他拦下来，刚准备用我们那点并不好用的日式寒暄开口，他直接打断：“别跟我在这儿私密马赛了，你说你要去哪？”\n这一句把我们全笑疯了，场面一下子松了下来。后来问路也就特别顺。\n日本的城市规划和国内是有明显差异的。\n我有个学城乡规划的同学给我科普过：因为二战后日本很多城市几乎是从废墟里重建出来的，所以他们在战后重建时，得以把很多现代城市规划理念直接用上。最典型的，就是“以公共交通为核心来组织城市”的 TOD 模式。\nTOD，全称是“以公共交通为导向的开发”（Transit-Oriented Development）。 它和美国、也和我们很多城市那种“以汽车为中心”的城市生长逻辑完全不同。TOD 的目标就是：让人尽量不需要开车。\n简单说，一个住在东京的人，基本可以靠电车/地铁系统去到他日常想去的任何地方。\n这种模式对第三产业特别友好。轨道交通本身就会吸引大量人流，在大站周围，商业被自然堆积和放大，于是“地铁站出口 = 直接进入商业区”就变成了常态。\n对于普通乘客来说，进站刷卡之后你并不是直接进月台，而是会被带进一整套地下商业空间。吃饭、药妆店、日用品便利店、咖啡、甜品，甚至小型诊所，几乎全都在那一层里就可以搞定。\n大型枢纽站之间甚至会用地下商业街彼此连成地下走廊，逛着逛着你已经从 A 商场走到了 B 百货，但全程还在室内。 而在居民区的小站，那个站本身几乎就兼任了“社区入口+小型购物中心”的角色——社畜下班从市中心坐车回来，出闸口前就已经把晚餐、日用品和第二天的便当都买好了，再抬脚回家，整个生活半径极小。\n听上去真的很理想，对吧？那为什么我们国内并不常见？\n其实是我们也在往这个方向走，但很多城市已经积重难返了。 我国的大城市早期更偏向学习美国式的摊大饼式扩张——地太大、路太宽、功能区分离明显，大家默认开车才是主流通勤方式。 这种模式一旦铺开，就很难再把商业、住宅、交通枢纽重新压到同一个点上，除非大拆大建。\n比如北京，出门靠地铁当然行，但你很难将北京和步行者天堂联想到一起。\n扯远了扯远了。反正，绕了一大圈，我们最后还是成功找到了心斋桥。\n坦白说，心斋桥并没有我想象中那么繁华。 大阪整体给人的感觉也确实和东京不太一样：商圈是热闹的，但热闹有边界，往外一走，街景一下子就冷了。\n如果要用廊坊的比喻，我会说心斋桥、难波、道顿堀这一带，就像是围绕万达广场的那个最核心的十字路口，灯牌密集、广告牌闪个不停，霓虹灯是那种上世纪九十年代录像带滤镜式的霓虹灯。离开这几条街的范围以后，气氛就明显沉下去了。\n但对当时的我们来说，那一切都带着滤镜，就算它略显陈旧，也依然浪漫。\n逛着逛着，我们饿到不行，完全没做攻略的四个人，直接在路边挑了一家大阪烧的小店推门进去了。现在回想，真的挺敢的。更幸运的是：我们竟然没被宰。\n店里一个客人都没有，只有一个头发半白的老板在吧台后面收拾东西。 他先用日语打招呼，发现我们都是听不懂的表情后，就开始用手势和我们交流。这个时候你会发现：所谓“肢体语言才是世界语言”不是虚的。中国人说英语、日本人说英语，双方都可能听不懂，但是手势+表情+菜单指指点点，永远能搞定。\n我们最后点了大阪烧和生啤的套餐，人均大概 1500 日元左右。现在回头看，这个价格在景区边上真的算不上狠宰了，而且还有酒喝，算良心。\n唯一的槽点是：分量真的很少。 日本的定食分量偏小我们是早有耳闻，但第一次实际吃到的时候，还是会有一种“就这”的错愕。对我们来说，这一餐也就吃了个半饱，距离满足还有一段路。\n不过这顿八嘎菜，算是正式宣布我们在日本的第一天结束了。\n吃完出来的时候我们才发现，街上的很多店已经开始拉门打烊了。\n这点和国内的商业街差异很大：无论是大商场还是小店，日本很多地方晚上八九点就基本结束营业。日本人的夜生活主要集中在居酒屋——下班喝酒，再喝酒，继续喝酒。 这让我们当时非常不适应：我们还想逛逛夜市的，结果街上已经冷冷清清了。只能扫兴撤退，准备回民宿。\n我们的民宿在岸里玉出站附近。这个车站非常朴素，甚至可以说有点寒酸。\n像岸里玉出这样的车站，在日本是主流形态：地面站台，一条轨道，简单的棚子挡雨，站台和铁轨之间几乎没有防护，站在边缘会本能地觉得我是不是离死亡只差半步。\n回民宿之前，我们顺路进了附近的超市。本来只是想买点水，结果直接震撼。\n我个人觉得：去日本旅游，必须安排的一个环节，是便利店/超市夜间采购环节。而且这个采购的重点不是奢侈品，是零食和酒。\n国内便利店的零食和饮料，说实话，现在同质化挺严重的。 可在日本，你会第一次意识到：\n原来货架上的饮料口味可以丰富成这样。\n原来低度酒可以细分成这么多品类。\n原来便当这种东西，在晚上八点半之后，会统一被贴上打折贴纸，然后疯狂降价清仓。\n我们第一天晚上就发现了这件事，当场疯狂扫货，买了一堆酒和零食带回民宿。从那以后我们彻底上瘾：每天白天观光打卡，晚上回去就开局小聚会，喝到开心为止。几乎变成了我们这趟旅行的固定节目。\n在这种节奏里，我们的日本之旅正式开始了。\n第二天的行程安排得很轻松：通天阁和大阪城。\n这两个地方可以说是大阪的经典打卡点。 通天阁号称“大阪之塔”，据说是仿照东京晴空塔那一类的现代观景塔建的（虽然高度完全不是一个量级，后面也听说已经被梅田那边的新地标超过去了）。 大阪城则是市中心最具代表性的历史景点之一，虽然现在的天守本体其实是重建过的。\n通天阁是可以购票上塔参观的。我们第一次来当然老老实实买了票，一路坐电梯到顶层观景区。 塔顶可以 360 度环视大阪城区，如果再加钱，可以上到最顶层的露天平台。\n说实话，整体观景体验还是挺新鲜的，但如果你以后还打算去东京登晴空塔，那我会建议：通天阁远观留照就好，钱包可以先收着（笑）。\n大阪城就不一样了，我个人会更推荐。\n大阪城整块区域被护城河包围，形成了一个大阪城公园。公园里除了大阪城本体，还有一些现代建筑，比如剑道馆、酒店、游客中心之类的设施。沿路能看到一拨拨中学生在集体活动，那个画面充满了日常的感觉。\n如果你是《名侦探柯南》党，尤其是服部平次粉丝，那你应该立刻能认出来某几个视角。现实里的取景点和动画里的分镜几乎一模一样，圣地巡礼爱好者真的会爽到。\n大阪城的内部，其实就是一座历史博物馆。 买票进城之后，城体的各个楼层会依次展示幕府相关的历史、丰臣秀吉时代的背景、战事复原，还有各类文物复制/保存展陈。我记得其中某一层还能穿上盔甲拍照，现场有几个欧洲佬玩得特别投入。\n整个参观体验对游客是比较友好的：展板有英文，也有中文说明（至少当时我记得是有的），所以不太会出现那种只能看热闹的尴尬。\n总的来说，我们第二天行程其实挺松弛的，主要也就逛了通天阁和大阪城。 现在回想起来还有点遗憾：第一次来竟然没有顺路去日本桥逛一逛二次元圣地，实在可惜。按理说那可是我们的主战场。\n而且如果让我给第一次“阪进东出”的人一点建议，我会说：落地大阪之后，最关键的其实不是在大阪玩，而是一定要去京都。\n京都作为历史古城，景点密度和文化厚度都远高于大阪，有一种穿越回唐朝的奇幻感。奈良也类似，它不是“到此一游”的城市，它更像是“你得慢慢走路、慢慢听故事”的城市。\n但这就引出了我们当时最大的焦虑： 对于这些带有厚重历史感的地方来说，光是你自己去看看建筑、拍拍照，其实是远远不够的。\n我当时就特别想找一个本地导游，最好是能边走边讲那种，把建筑、历史、典故串起来。 在国内景点我们很习惯有讲解员，甚至还能蹭一下旅行团导游。但我当时完全不确定日本是不是也有这种东西。\n结果还真有。\n在出发前，我在日本旅游的官方网站上发现了一个“志愿者导游”栏目。这个项目的形式大概是这样：各地的自治团体可以自发组织自己的志愿导游小组，把信息报上去，由官方统一挂在页面上，方便游客联系。\n因为我们的行程当中包含了京都、奈良和后面的镰仓这几个历史城市，所以我定下的策略是：大阪段找一个导游，东京段再找一个导游。 我当时基本是“海投邮件”，给好几个志愿团体都发了需求邮件。\n最后，大阪这边我们联系上了一个叫中岛的导游，他说可以带我们逛京都和奈良。 东京那边我们也约到了另一个导游（很不好意思，我现在真的想不起来他的名字了），准备带我们逛镰仓。\n大阪这边的行程是我们和中岛一起讨论敲定的。 因为他们是志愿者性质的导览，所以我们不需要额外付导游费，只需要承担他们当天的交通费和餐费。 而且中岛会中文和英文，按理说我们整个行程可以用中文交流，必要的时候用一点英文补充。\n说实话，前一晚我真的挺激动，也有点紧张。 这是我第一次真正意义上和一个日本人接触。\n第三天一早，我们按约定的时间到了地铁站门口。 远远地就看到一个收拾得很利落的老头，戴着针织帽，站在出口附近，一边环顾一边等人。\n我们走上去，他主动迎了过来：\n“啊，是安先生吗？” “是的。” “我是中岛，很高兴见到你！”\n他用中文做了自我介绍，还和我们一一握手。\n说真的，我第一眼是有点惊到的，因为他看起来年纪真的不小了。 后来路上聊天才知道，他已经七十多岁了。退休之后来做志愿者导游，是想让自己的生活更有趣一点。年轻的时候他是大阪大学的学生，读政治学，还是 70 年代的大学生；改革开放初期他去过中国，也去过北京王府井。现在他主要靠广播节目和书本慢慢学中文。\n他的中文并不算特别流利，有些词要想一下，但我们四个都特别敬佩他：七十多岁了，还在认真学一种新的语言，然后用它带陌生人看他的城市。\n那一刻开始，我对“旅行”这两个字的理解，发生了第一次真实的位移。\nTo be continued\u0026hellip;\n","date":"2025-10-27T00:00:00Z","image":"https://anyifan117.github.io/post/%E8%AE%B0%E6%97%A5%E6%9C%AC%E6%B8%B8-%E5%85%B6%E4%B8%80/fengmian_hu_402a194bf20bf175.jpg","permalink":"https://anyifan117.github.io/post/%E8%AE%B0%E6%97%A5%E6%9C%AC%E6%B8%B8-%E5%85%B6%E4%B8%80/","title":"记日本游-其一"},{"content":"Skysense Skysense简单来说，是一个能够处理多模态，在遥感下游任务上表现良好的基模。\n他的核心架构如下： 整体结构较为简单，主要分为以下几个部分：\n因子化多模态时空编码器 说的很玄乎，其实就是编码前对向量的处理\n主要做法是，分别利用3个模态的encoder对可见光，多光谱和SAR图像进行编码，然后concat起来，加上时间信息向量（和传统的位置向量有点相似），在头部concat一个cls向量，然后送入encoder得到最终包含时空信息的编码 $F^{mm}_{fus}$。\n得到这个 $F^{mm}_{fus}$之后，结合在预训练中得到的一个全球地理特征表（图右侧），进行一个QKV计算，Q是 $F^{mm}_{fus}$，KV是特征表，这样进行与地理特征表中的特征有关的向量生成，再和 $F^{mm}_{fus}$ concat得到最后的 $F_{fus}$。\n预训练时的设计 首先他们采用的是student-teacher的架构，来最大程度避免RSI的负样本问题\n多粒度对比学习\n这个多粒度就是像素、目标、图像这三个粒度\n分别计算Loss之后加起来\n多模态对齐\n用了一个multi-modal contrastive loss，来把同一位置的不同模态图片信息对齐\n非监督地理信息原型学习\n这个貌似是创新点。\n就是对地球进行了建模，以经度纬度为单位，在小格子里建立多层信息，然后利用$F^{mm}_{fus}$这个语义丰富的向量来训练这个格子，最终让格子里的每一层学习到对应的地理信息，用于辅助下游任务。\n实验设置 他们的主要对比对象有如下模型：\n但是在文中没有写清楚选择标准，同时有一些数据是空缺的，怀疑直接从别人的论文里抄的数据。\n这里我就想吐槽一下这些AI的文章。他们的实验设置讲的都是稀烂。没有工具的选择标准，没有一个对比的基线。\n基本上，每一个任务里的对比模型都不尽相同，而文章中也没有解释，为什么在前一节能够对比的模型，到了这一节却突然消失了。我大发慈悲地猜测，可能是因为每个模型能做到的任务不同，所以可能对于分类任务能够对比，但是分割任务不能对比。而这种信息不应该让读者去猜，而是应该直接写出来。\n这要是高教授来审稿，这里的实验部分肯定是要重写的。\n","date":"2025-10-27T00:00:00Z","image":"https://anyifan117.github.io/post/skysense%E7%AC%94%E8%AE%B0/skysense_hu_fddd7697cc316886.png","permalink":"https://anyifan117.github.io/post/skysense%E7%AC%94%E8%AE%B0/","title":"Skysense笔记"},{"content":"Transformer学习笔记 本篇笔记记录了笔者在学习transformer架构时的一些心得体会。笔者的学习资料来自科技猛兽的博客。也请大家去阅读原文，原文的介绍更加详细。本文在原文的理解基础上，加入了更详细的代码解读。\ntransformer整体结构的核心点有以下几点：\nSelf-Attention机制 多头注意力 位置编码 Self-Attention 注意力是transformer的核心，他的目标是对输入序列进行全局关注，并最终能够并行计算。\nRNN只顺序计算，并且只能考虑之前序列的输入，不能考虑之后的。 CNN只能考虑一个范围内的输入，内容更加有限\nAttention主要由三个部分计算得到：Q(query), K(key), V(value)\n就像从字典里查询单词的过程一样，对于一个Q，我们取寻找和他相似的K，然后获取K对应的结果V。\n整体的流程如下图：\n对于一个输入序列$I = (a^1, a^2, ..., a^n) = W(x^1, x^2, ..., x^n)$, 其中$x^i$是输入的词向量，$W$是词向量编码矩阵，$a^i$是输入序列的权重向量。\nW的代码表示：\n1 2 # encoder self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx) 其中n_src_vocab是源语言的词表大小，d_word_vec是词向量的维度，pad_idx是填充标签\u0026lt;pad\u0026gt;的索引。对于decoder来说就是n_trg_vocab和d_word_vec。如果源语言和目标语言的编码相似，那么就可以共享参数。\n$I$经过三个对应的转化矩阵，将$a^i$转化为$q^i, k^i, v^i$： $$ q^i = W^qa^i\\\\ k^i = W^ka^i\\\\ v^i = W^va^i $$ 现在我们可以做$q$和$k$的attention，具体来说就是计算点积： $$ Scaled\\ Dot{-}Product\\ Attetion: \\alpha_{i,j} = \\frac{q^i \\cdot k^j}{\\sqrt{d_k}} $$其中$d_k$是$k$和$q$的维度，他们是相等的。这么做有一个归一化的效果。\n接着我们将所有的$\\alpha$进行softmax：\n$$ \\hat{\\alpha_{i,j}} = \\frac{\\exp(\\alpha_{i,j})}{\\sum_j \\exp(\\alpha_{i,j})} $$紧接着，我们使用$\\hat{\\alpha}$和$v$对应相乘来得到$b$,作为最终的输出：\n$$ b^i = \\sum_j \\hat{\\alpha_{i,j}}v^j $$代码实现 在代码中，计算注意力和输出$B$是使用单独模块实现的：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ScaledDotProductAttention(nn.Module): def __init__(self, temperature, attn_dropout=0.1): super().__init__() self.temperature = temperature # 这里就是d_k self.dropout = nn.Dropout(attn_dropout) def forward(self, q, k, v, mask=None): attn = torch.matmul(q / self.temperature, k.transpose(2, 3)) # 此处为Q·K^T/sqrt(d_k) if mask is not None: attn = attn.masked_fill(mask == 0, -1e9) # 如果有，加入超大负数作为掩码 attn = self.dropout(F.softmax(attn, dim=-1)) # softmax操作 output = torch.matmul(attn, v) # 和所有的v相乘计算得到b return output, attn 对QKV的计算在多头自注意力模块实现。\n多头自注意力 所谓多头自注意力，实际上就是将原本的$Q$,$K$,$V$分别拆分成$n\\_head$个子矩阵，然后进行$n\\_head$次注意力计算，最后将所有的结果进行拼接，得到最终的输出。\n对于一个完整的转化矩阵$W^q$，通过变化我们将它拆分为$W^{q,1}, W^{q,2}, ..., W^{q,n\\_head}$,在得到$q^i$之后拆分为对应的$q^{i,1}, q^{i,2}, ..., q^{i,n\\_head}$，与对应的$k$和$v$重新完成attention计算得到对应的$b^{i,1}, b^{i,2}, ..., b^{i,n\\_head}$，最后将他们concat起来，通过一个矩阵调整维度变为最后的$b^i$\n代码实现 在代码实现里，这部分比较巧妙。 为了便于计算，代码中只有一个大的矩阵（以计算$Q$为例）$W^q$，他被定义为(d_model, n_head*d_k)。而对于输入的$A$，在代码中定义为(batch_size, seq_len, d_model)，通过矩阵计算之后，会得到一个$Q$矩阵(batch_size, seq_len, n_head*d_k)。这个$Q$矩阵包含了所有头的$Q$向量，这样通过一次计算我们便得到了所有的头的结果。\n实际上，这个大的矩阵$W^q$我们可以看作是许多头的矩阵的concat: $$W^q = Concat([W^{q,1},W^{q,2},...,W^{q,n\\_head}])$$而我们仅通过简单的reshape操作，就可以将每一个头的$q^i$矩阵从$Q$里拆分出来，送入后续的计算注意力环节。对于$K$和$V$向量同理。\n具体代码实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 class MultiHeadAttention(nn.Module): def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1): super().__init__() self.n_head = n_head self.d_k = d_k self.d_v = d_v # 因为是多头，所以对于W^q,W^k,W^v，需要将维度扩展到n_head倍 self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False) self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False) self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False) self.fc = nn.Linear(n_head * d_v, d_model, bias=False) self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5) self.dropout = nn.Dropout(dropout) self.layer_norm = nn.LayerNorm(d_model, eps=1e-6) def forward(self, q, k, v, mask=None): # 此处的q,k,v的输入是enc_input, 也就是文章中提到的a,并未真正的q,k,v,此处的qkv大小是(batch_size, seq_len, d_model) d_k, d_v, n_head = self.d_k, self.d_v, self.n_head sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1) residual = q # Pass through the pre-attention projection: b x lq x (n*dv) # Separate different heads: b x lq x n x dv # 核心部分，一次计算得到所有头的QKV,然后分离为多个头 q = self.w_qs(q).view(sz_b, len_q, n_head, d_k) k = self.w_ks(k).view(sz_b, len_k, n_head, d_k) v = self.w_vs(v).view(sz_b, len_v, n_head, d_v) # Transpose for attention dot product: b x n x lq x dv q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) if mask is not None: mask = mask.unsqueeze(1) q, attn = self.attention(q, k, v, mask=mask) # Transpose to move the head dimension back: b x lq x n x dv # Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv) # 因为计算attention需要变一下形状，现在计算完了原样变回来 q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1) #q (sz_b,len_q,n_head,N * d_k) q = self.dropout(self.fc(q)) q += residual q = self.layer_norm(q) return q, attn 至此，通过多头注意力机制和点积注意力计算，我们已经完成了transformer里核心的注意力模块的实现。\n位置编码 因为是注意力机制，对于同样序列的排列组合，如果没有注意力，模型无法区分。所以需要加上位置编码能够让模型对“我爱你”和“你爱我”进行区分。\n在transformer里，位置编码是认为给定的一个向量$e^i$，这个向量在每一个位置都是唯一且不同的。他会与$a^i$进行相加，从而实现位置编码。\n这里的相加不会损失位置信息。可以按照科技猛兽博主里的思路来理解：\n按照我们正常的理解，应该给单词$x^i$在后面concat一个位置编码$p^i$来表示他的位置。这样的话，当我们在计算单词的词向量的时候，就会有：\n$$ W \\cdot x^i_p = [W^I, W^P] \\cdot \\begin{bmatrix} x^i \\\\ p^i \\end{bmatrix} = W^I \\cdot x^i + W^P \\cdot p^i = a^i + e^i $$所以我们的相加操作在宏观上就是对单词进行了一个位置编码的concat。\n在实际设计中，transformer利用了三角函数来计算位置编码，用$PE$表示。\n$$ PE_{(pos, 2i)} = \\sin(pos / 10000^{2i/d_{model}}) \\\\ PE_{(pos, 2i+1)} = \\cos(pos / 10000^{2i/d_{model}}) $$这里的这个$i$有点抽象，他的取值范围是$[0, d_{model} / 2)$。个人理解，这个$2i$和$2i+1$是为了区分sin和cos的。即对于偶数的隐藏层都是sin,对于奇数的隐藏层都是cos。我在下面换一个写法可能就比较能看清楚了。$pos$则表示单词的位置，对于特定的位置$pos$，都可以计算出包含全部$i$的一个位置编码。即：\n$$ PE(pos) = \\sum_{i=0}^{d_{model}/2} PE_{(pos, 2i)}+PE_{(pos, 2i+1)} $$这个函数有一个很重要的性质就是他的可以线性变化的。即对于一个$PE(pos+k)$，是可以被$PE(pos)$所线性表示的。，这样对于模型来说，它可以学习到相对的位置信息。\n代码实现 在transformer的源码中，对于位置编码的计算，是通过先计算sin和cos函数内部的那个$pos / 10000^{2i/d_{model}}$，然后奇数位置取sin，偶数位置取cos。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class PositionalEncoding(nn.Module): def __init__(self, d_hid, n_position=200): super().__init__() # Not a parameter self.register_buffer(\u0026#39;pos_table\u0026#39;, self._get_sinusoid_encoding_table(n_position, d_hid)) def _get_sinusoid_encoding_table(self, n_position, d_hid): def get_position_angle_vec(position): # 对所有隐藏层两两编码，使2i和2i+1相等。 return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)] sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)]) # 对2i使用sin，对2i+1使用cos sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) return torch.FloatTensor(sinusoid_table).unsqueeze(0) def forward(self, x): return x + self.pos_table[:, :x.size(1)].clone().detach() Transformer整体拼装 我们完成了对于transformer里的核心部分的梳理，接下来就是根据论文结果，对应实现encoder和decoder，将所有模块拼装到一起。\nencoder层 整体encoder层的结构如下：\n让我们从下往上一点一点开始。\nInputEmbedding 首先是InputEmbedding，这块使用的是一个nn.Embedding层实现的：\n1 self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx) 这里的n_src_vocab是源语言的词表大小，d_word_vec是词向量的维度，pad_idx是填充的索引。\nPositionalEncoding 然后是我们介绍过的PositionalEncoding层，实现已经在前面介绍过了。\nEncoderLayer 接下来是就是多个EncoderLayer层组成的模块。 每一个EncoderLayer层里都有一个MultiHeadAttention层和一个PositionwiseFeedForward层，并且使用残差和层归一化连接。\nMultiHeadAttention层在前面介绍过了，这里不再赘述。\nPositionwiseFeedForward层实际上就是一个两层全连接层，并且使用ReLU激活函数。他的实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class PositionwiseFeedForward(nn.Module): def __init__(self, d_in, d_hid, dropout=0.1): super().__init__() self.w_1 = nn.Linear(d_in, d_hid) self.w_2 = nn.Linear(d_hid, d_in) self.layer_norm = nn.LayerNorm(d_in, eps=1e-6) self.dropout = nn.Dropout(dropout) def forward(self, x): residual = x # Linear -\u0026gt; ReLU -\u0026gt; Linear x = self.w_2(F.relu(self.w_1(x))) x = self.dropout(x) x += residual x = self.layer_norm(x) return x encoder整体回顾 综上所述，整体的encoder层代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Encoder(nn.Module): def __init__( self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v, d_model, d_inner, pad_idx, dropout=0.1, n_position=200): super().__init__() self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx) self.position_env = PositionalEncoding(d_word_vec, n_position) self.dropout = nn.Dropout(dropout) self.layer_stack = nn.ModuleList([ EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout) for _ in range(n_layers) ]) self.layer_norm = nn.LayerNorm(d_model, eps=1e-6) def forward(self, src_seq, src_mask, return_attns=False): enc_slf_attn_list = [] # encoderLayer前的预处理 enc_output = self.dropout(self.position_env(self.src_word_emb(src_seq))) enc_output = self.layer_norm(enc_output) # encoderLayer for enc_layer in self.layer_stack: enc_output, enc_slf_attn = enc_layer( enc_output, slf_attn_mask=src_mask) enc_slf_attn_list += [enc_slf_attn] if return_attns else [] if return_attns: return enc_output, enc_slf_attn_list return enc_output 可以看到整体encoder的结构还是非常清晰的。最终获得了所有encoderLayer的输出，并返回，用于输入给decoder。\ndecoder层 整个decoder层的结构如下：\n与encoder类似，decoder的下端输入也是一个embedding向量trg_word_emb加上位置编码。只不过此时的embedding是目标语言的，而encoder的是源语言的。\n我们重点来看一下整体的DecoderLayer类。\nDecoderLayer 与EncoderLayer类不同，DecoderLayer面对不同的层，他的输入是不同的。\n在第一层多头掩码注意力层中，输入是trg_word_emb加上位置编码的dec_input。\n1 dec_output, dec_slf_attn = self.slf_attn(dec_input, dec_input, dec_input, mask=slf_attn_mask) 接下来，第二个多头注意力层的K和V是encoder层的最终输出，而Q是刚才得到的dec_output。\n1 2 dec_output, dec_enc_attn = self.enc_attn( dec_output, enc_output, enc_output, mask=dec_enc_attn_mask) 这一步其实就是在匹配目标语言和输入语言。\n最后接一个前文提到的全连接网络PositionwiseFeedForward，便完成了一层decoder的全过程。\ndecoder整体结构 同理，给出整体的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Decoder(nn.Module): def __init__( self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v, d_model, d_inner, pad_idx, n_position=200, dropout=0.1, scale_emb=False): super().__init__() self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx) self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position) self.dropout = nn.Dropout(dropout) self.layer_stack = nn.ModuleList([ DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout) for _ in range(n_layers) ]) self.layer_norm = nn.LayerNorm(d_model, eps=1e-6) self.scale_emb = scale_emb self.d_model = d_model def forward(self, trg_seq, trg_mask, enc_output, enc_mask, return_attns=False): def_slf_attn_list, dec_enc_attn_list = [], [] dec_output = self.trg_word_emb(trg_seq) if self.scale_emb: dec_output *= self.d_model ** 0.5 dec_output = self.dropout(self.position_enc(dec_output)) dec_output = self.layer_norm(dec_output) for dec_layer in self.layer_stack: dec_output, dec_slf_attn, dec_enc_attn = dec_layer( dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=enc_mask) def_slf_attn_list += [dec_slf_attn] if return_attns else [] dec_enc_attn_list += [dec_enc_attn] if return_attns else [] if return_attns: return dec_output, def_slf_attn_list, dec_enc_attn_list return dec_output 整体和encoder非常相似，要注意需要输入enc_output和对应的掩码信息。\n","date":"2025-10-27T00:00:00Z","image":"https://anyifan117.github.io/post/transformer%E7%AC%94%E8%AE%B0/transformer_hu_4a35ee63c848dd8.png","permalink":"https://anyifan117.github.io/post/transformer%E7%AC%94%E8%AE%B0/","title":"Transformer笔记"},{"content":"《秽翼的尤斯蒂娅》简评 原链接：知乎回答\n《秽翼的尤斯蒂娅》是本人受到舍友推荐游玩的第一部八月社的作品。关于游戏的介绍，我再此就不赘述了，我简单谈一下个人游玩的一些体验和对他人评价的感想。\n请注意，以下内容含有剧透\n以下观点仅代表个人看法，如果和你的价值观出现冲突，那就是你对！\n各人物支线剧情的问题 在游玩了真结局之后，回味各人物结局，会给我带来一种“苟延残喘”的感觉。小动物在真结局路线，面对天使寥寥几语的诱劝就能对凯伊姆产生如此强烈的占有欲，那么当他得知在其他路线上，凯伊姆（玩家）的选择之后，肯定会毅然决然选择让都市坠落吧（笑）。这样回过头去品味得到的感觉，让真结局显得弥足珍贵呢。\n而最直观的游玩体验，便是我们的主人公凯伊姆桑前后不一致的割裂感。在我看来，凯伊姆在真结局之前，是对自己存在的意义这个问题，没有一个明确的答案的。他可能是对女人温柔的，但他绝对不会是沉浸在温柔乡里的人，他是自由的、同时也是空虚的和无目标的。如同在天空中漂浮的蒲公英，自由且无目的的随风飘拂。\n总体来说，“脏翅膀”的支线和主线的关系，就好比树木的主干和枝桠。但我相信游玩到后期，枝桠的剧情并不能唤起玩家的共鸣。\n有人因为支线剧情的敷衍批评作品，但我认为这不是八月社的本意，而是“脏翅膀”作为一个galgame美少女恋爱游戏来说，不得不有的一部分。作为一个galgame，他应该为玩家提供多个可以攻略的对象，并且提供满足不同XP的玩家的结局。而八月社，很明显是把重点放在了树木的主干之上，想要用更大的篇幅去完善整个故事。\n主人公的心理历程 和广大玩家一样，我在游玩过程中，也有着 “这样就说服了吗” 的违和感，在菲奥奈和艾丽丝篇尤为严重。一方面原因可能因为玩的时候没有一字一句去分析，看的太快；另一方面可能就是剧本和人物的变化没有连接的很好。\n但是就主人公凯伊姆的心理变化，我认为是描写的非常清晰的。随着凯伊姆的“阅历”增加，他的心理也在发生变化。有的人认为这是人物塑造得失败，尤其在高潮部分主人公的反复转变让许多玩家觉得无法理解他在想什么。其实在结尾，制作组给出了解释，那就是从故事开始就一直困扰凯伊姆的那句 “存在的意义” 。\n凯伊姆因为大崩坏落入牢狱，受到牢狱的环境影响，认为自己就是牢狱的人。后来随着故事发展，他的活动范围在不断上升，而他因为 “逃避选择” ，所以自然而然把身边的环境和自己进行了绑定。这就是玩家在游玩中体会到的，“这个人怎么说话越来越像贵族一样” 的感觉，也就是主人公前后立场不一样的体现。\n凯伊姆缺乏对人生的明确目标和自身存在意义的思考，所以受到身边的环境影响，而改变了自己的思考方式。理解了这一点，就很好理解他内心的纠结和最终做出的选择了。虽然说他最终最初的选择在我看来有一点儿女情长，但是你就说选没选嘛）\n反观鲁基乌斯，在我游玩的时候，我感觉这个人就是卫宫切嗣的写照。在这种带有明显的功利主义的价值观的影响下，他做出了最符合人物设定的抉择。虽然这可能让玩家对他产生极大的厌恶，但是我却很难对他产生讨厌，因为他确实是向着自己坚信的存在意义不断前进的。\n关于小动物 越是玩到后期，我对小动物越是不能直视。我可以明确的说，制作组绝对受到了基督教的影响，而小动物就是那个以耶稣基督为原型创造出来的救世主一样的人物。\n因为家庭原因，我对基督教相关也有些许了解。所以当我看到小动物的使命 “就是去受尽苦难” ，当我看到原天使被囚禁在塔中的形态，很难不去联想到耶稣基督为了拯救世人而钉十字架，而 “耶稣受难” 也正是基督教的教义之一。\n有人说结局是俗套的，小动物的这种 “讨好型人格” 很出戏之类，如果带入基督教的色彩去思考的话，那么整个故事的结局可以说是作者对自己想要表达的宗教观点的一个很好的说明。亦或者说，这个结局本该如此，因为在圣经里，耶稣也是这么做的。\n作为圣子出生在马棚，代替罪孽深重的人类，去受十字架之苦，只为拯救世人，是不是和小动物的故事很像？你可能在之前觉得无法理解小动物的想法和行为，觉得她就是傻白甜天然善良，但我想告诉你，这不是制作组按照现在的天然女主作为模板设定出来的，而是按照耶稣的原型设计的角色，你会不会觉得对小动物的性格有了新的看法呢?\n总结 《秽翼的尤斯蒂娅》可以说是制作组对基督教的一个很好的宣传或者说致敬。对于各位玩家来说，如果大家能够在游玩完游戏之后，愿意去了解基督教相关故事（了解即可，没有别的意思），它是一部能为你带来很多遐想的作品。而我本人游玩之后，也是希望其他玩家能够带着基督教的背景设定去重新思考故事，可能会给你带来不一样的体验。\n","date":"2025-10-26T00:00:00Z","image":"https://anyifan117.github.io/post/%E7%A7%BD%E7%BF%BC%E7%9A%84%E5%B0%A4%E6%96%AF%E8%92%82%E5%A8%85%E7%AE%80%E8%AF%84/home_hu_ad305993fec513e5.png","permalink":"https://anyifan117.github.io/post/%E7%A7%BD%E7%BF%BC%E7%9A%84%E5%B0%A4%E6%96%AF%E8%92%82%E5%A8%85%E7%AE%80%E8%AF%84/","title":"《秽翼的尤斯蒂娅》简评"},{"content":"终于配好了 终于把这个傻逼stack主题配好了，真是傻逼，真是傻逼。\n简介 如果在网络上去搜索某些技术文档或者教程，你大概率会找到各种各样开发者自建的博客。这些博客五花八门，多数有着绚丽的鼠标追踪背景，live2d小人，你可以在里面自定义自己的小世界。\n笔者近期闲来无事，也就有了想要自己搭建一个博客站的想法。经过简单的了解，最终决定使用 Hugo 来搭建。\nHugo是一个开源的静态网站生成器。能够让用户在无代码的环境下快速部署静态网站(当然还是需要有一些命令行和git的基础)。同时Hugo有大量的第三方主题(theme)可供选择，整个社区的十分活跃，教程十分丰富，非常适合作为博客开发的入门框架使用。\n当然，我是看中了Hugo的Stack主题，我觉得这个主题还挺好看的，符合了我对于博客站的一切幻想。\n接下来就简单介绍一下，我用Hugo和Stack主题来搭建这个博客站的一些踩坑经历。\n⚠️声明： 本人在初期开发过程中参照了bilibili的up主Letere-莱特雷的教程。在其基础上，本人总结了一些在linux环境下搭建的踩坑经验，供各位参考。本人也是正在学习Hugo的相关配置，如果有可以优化的地方，可以在评论里指出。\n叠甲完毕，让我们开始吧！\n安装Hugo Hugo整体需要安装Hugo, Go和Dart SCSS，我们一步一步来。\nHugo 首先的首先，我们需要安装Hugo，这也是整个配置中最关键的一环。\nHugo的版本分为很多种，我们去他的github仓库的release部分就可以看到所有的版本和每一个版本里的所有的类型。\n彼时时间为2025年11月2日，以当前最新的0.152.2版本为例，在github上可以安装的发布类型如图所示。\n一般来说，我们肯定是下载最新的版本是最好的。但是！我们使用的Stack主题却对版本有硬性要求！\n以下安装教程仅针对需要使用Stack主题的读者参考\n让我们来看一下Stack的文档中给出的安装提示。\n可以看到，因为Stack主体使用了SCSS和Typescript来开发样式，所以他们需要安装Hugo extend环境。而Hugo的最新版是没有extend版本的！\n所以不管你是想要使用apt-get安装，还是使用github release安装，还是用官方文档里推荐的snap安装，必须要确保安装的版本和你想使用的theme的要求版本是一致的。\n当然，如果要使用stack主题的话，用snap安装是会和scss冲突的，我这里就踩坑了。\n所以推荐大家，使用稍旧一点的，带有extend的Hugo安装。\n在release界面下载0.152.1的含有extend前缀的版本。根据你自己的操作系统来选择安装。安装过程就不详细说明了，因系统而异。\n安装完成，运行命令\n1 hugo version 得到结果\n1 hugo v0.152.1-5869cbddd88590563c2b7b400e804ccc7d2cb697+extended linux/amd64 BuildDate=2025-10-22T19:10:44Z VendorInfo=gohugoio 带有extend字样，那么就OK了。\nGo 接下来是安装Go，这里没有什么坑，直接安装即可。\nDart SCSS 安装Dart SCSS也相对简单，按照官方文档一步一步来就可以了。\n这里要注意，在Hugo的文档里，他说了一句话：\nIf you install Hugo as a Snap package there is no need to install Dart Sass. The Hugo Snap package includes Dart Sass.\n他说的很有道理，如果你使用snap安装Hugo，就可以省略这一步。\n但是，如果你想用Stack主题，则不能用snap安装Hugo，因为snap安装的Hugo没有extend版本。\nBest practice 在github的release上找到extend版本Hugo安装 安装Go 安装Dart SCSS 好的，至此我们的环境搭建完毕了。接下来就是可以搭建博客和配置主题了！\n配置Stack 接下来的内容，和莱特雷的教程就基本一致了，我也是参考他的配置来使用的Stack主题。这里放上他的视频，读者们可以进行一些参考。\n同时关于后期使用github action进行部署，也可以参考他的博客，写的很详细，笔者基本沿用了他的方法。\n笔者的配置和莱特雷不同的点是，我直接使用了Stack文档中推荐的git submodule的方式进行主题安装。\n首先，在我们新建的项目在，初始化git仓库，因为我们要进行版本管理和后期在github上使用action进行部署。\n紧接着就可以运行\n1 git submodule add https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack 将Stack主体添加到项目根目录的themes目录下。\n这就不用像视频里一样去下载github的zip压缩包了，更加便利。\n当然，因为我们使用了git submodule的方式，所以当我们修改了Stack主体的一些内容之后(这是可能的，因为我们可以修改里面的文件自定义样式和功能)，需要fork一份Stack的仓库进行提交，比较麻烦\n笔者这里直接把这个仓库的.git配置给删除了，让他变成普通目录，力大砖飞，并不推荐。\n根据笔者的观察和理解，当创建了Hugo的项目之后，Hugo会使用配置文件来管理项目，这个配置文件是可以有三种格式的：toml,yaml,json。\n我们使用的0.152.1版本给出的默认配置文件是toml格式的，他的名字叫hugo.toml，就在项目的根目录下。\n但是Stack主题使用的配置文件叫hugo.yaml。这里就需要你把Stack主题的配置文件复制过来，然后删除原本的toml文件。以后只需要在hugo.yaml文件里进行修改。\n尾声 至此，整体使用Hugo+Stack的配置踩坑记录就结束了，希望大伙玩得愉快:)\n","date":"2025-10-26T00:00:00Z","permalink":"https://anyifan117.github.io/post/2025-%E6%9C%80%E6%96%B0hugo-stack%E4%B8%BB%E9%A2%98%E9%85%8D%E7%BD%AE%E8%B8%A9%E5%9D%91/","title":"2025 最新Hugo+Stack主题配置踩坑"}]