Gemini 3 Pro 生成内容短篇小说：《铭刻者》一林拓站在“审判之眼”观测站的悬窗前，手里那杯合成咖啡已经冷透了。窗外不是星空，而是一片纯粹的、令人窒息的漆黑。那是编号为 X-902 的原初黑洞。它只有月球大小，视界直径约三千五百公里，却拥有五百个太阳的质量。它像是一颗宇宙肌肤上坏死的黑痣，静静地悬浮在半人马座旋臂的荒原上。“这一凿下去，半个恒星系的能量就没了。”身后的声音带着金属质感的颤动，是这里的首席操作员雷迪亚兹。“这是必要的消耗。”林拓的声音很轻，却透着一种像岩石般冷硬的固执，“物质会衰变，质子会瓦解，连光都会在膨胀的宇宙中稀释成无线电波。只有引力波的涟漪，能够在时空的结构中永存。”这是“文明墓碑”计划的最后阶段。人类文明已经进入了黄昏。不是因为战争，也不是因为资源枯竭，而是因为宇宙本身的“老死”。熵增就像一种慢性毒药，渗入了星系的每一个角落。人类决定在灭绝前做最后一件事：将人类的历史、科学、艺术，编码成高强度的引力波束，轰击向 X-902 黑洞的事件视界。根据广义相对论，对于远处的观察者来说，时间在引力场极强的地方会无限变慢。坠入视界的信息，永远不会真正“掉进去”，而是会被无限冻结在边缘。这颗黑洞，将成为人类文明永恒的硬盘，直到宇宙热寂的尽头。“充能进度 99%。”雷迪亚兹汇报道，“准备锁定视界‘画布’。”二屏幕上展开了视界表面的模拟网格图。按照理论，那应该是一面完美的镜子，平滑、死寂，连光都逃不脱它的引力囚笼。为了确保信息的可读性，他们必须找到一块绝对平滑、没有被之前的物质吸积所扰动的“空白区域”。“正在进行视界表面高精度扫描……启用引力透镜模式。”全息投影被放大了。在原本应该是绝对光滑的视界表面，出现了一些极微小的、如果不使用引力透镜放大一亿倍绝对无法察觉的……褶皱。“那是引力波留下的‘软毛’。”林拓凑近了屏幕，呼吸凝滞了。那不是自然的产物。自然的物质坠落是杂乱无章的噪点，而这些褶皱，有着惊人的几何规律。它们像地层一样层层叠叠，每一层都代表着一段漫长得令人绝望的地质年代。“扫描整个视界。”林拓命令道，声音干涩。结果出来了。没有空白。没有哪怕一平方微米的空白。整个 X-902 黑洞的视界表面，密密麻麻地覆盖着这种褶皱。林拓突然感到一种巨大的、难以名状的荒谬感。人类以为自己是孤独的朝圣者，耗尽最后的资源，长途跋涉来到这块宇宙尽头的丰碑前，想要刻下“到此一游”。却发现，这块丰碑上早已被涂写得没有一丝缝隙。这是一个拥挤的墓地。每一个能在宇宙规律面前挣扎到最后的文明，都想到了同一个主意：把记忆留给黑洞。三观测站陷入了死一般的寂静。“能分析出源头吗？”雷迪亚兹的手指在颤抖。“太多了……”林拓看着那些数据流，就像看着一场跨越百亿年的暴雨，“最上面这一层，虽然刻录点在这里，但解码出的星图标尺显示，它们来自 M31——仙女座大星系。”“仙女座？”雷迪亚兹惊愕地抬起头，“那是 254 万光年外。它们为什么跑这么远？它们家里没有黑洞吗？”“因为家里不安全。”林拓看着那片黑暗，仿佛洞穿了数百万光年的虚空，“仙女座比银河系更大、更亮。那里一定是高等文明争夺的中心，是猎手密布的战场。只有把备份藏到银河系这个偏僻的、荒凉的‘乡下’，藏在这颗毫不起眼的小黑洞上，才有一丝留存的希望。”林拓闭上了眼睛。他在脑海中想象着那个仙女座的文明。它们可能正在面临灭顶之灾——也许是二向箔的吞噬，也许是光速慢化的囚笼。在毁灭的前夜，它们派出了一支绝望的远征队，跨越两百多万光年的死寂虚空，只为了在这里留下一份备份。这种跨越时空的共鸣，比任何接触都更加震撼，也更加悲凉。宇宙不是童话，宇宙是一座这就着火的图书馆。所有的文明都在疯狂地把书往唯一的防火保险柜里塞，没人顾得上礼貌，也没人顾得上顺序。四“长官，警报！”刺耳的红光打断了哀悼。深空雷达显示，在 X-902 的航线前方，一团无形的力场正像扫帚一样横扫过虚空，将沿途的星际尘埃和小行星瞬间粉碎、推开。“那是什么？”“是清道夫。”林拓瞬间明白了一切，“怪不得这颗黑洞这么干净，怪不得这里是荒原。有一个古老的文明在维护它，它们定期清理航道，确保视界不被杂质污染。”“它将在十分钟后扫过我们。”雷迪亚兹脸色苍白，“我们会被当成灰尘扫掉。”留给人类的时间不多了。“还要刻吗？”雷迪亚兹回头，看着屏幕上密密麻麻的褶皱，“为了写下人类的名字，我们需要抹去另一个文明存在的痕迹。也许那个被覆盖的，就是仙女座那个文明最后的备份。”覆盖它们，我们就成了毁灭历史的野蛮人。不覆盖，人类就将在时间的长河中彻底消失，连一丝尘埃都不剩。生存是文明的第一需要。在这个拥挤的宇宙电梯里，当超载的警报响起，没有人是无辜的。“雷迪亚兹。”“在。”“瞄准仙女座铭文区最古老的一段。那是五十亿年前的痕迹，太久了，该让位了。”林拓的声音冷漠如铁，剥离了所有的道德，“把那一块抹平。”五一道肉眼不可见的引力波束，携带着相当于一颗恒星一生的能量，无声地穿过虚空，轰击在视界表面。时空在微观尺度上剧烈震颤。那古老的、来自遥远星系的绝唱，在瞬间被抚平、抹去，取而代之的是人类笨拙而顽固的编码。“刻录完成。全舰全速脱离。”飞船引擎喷出凄厉的蓝光，疯狂地逃离那片死亡空域。几分钟后，清道夫的力场无声地扫过，将人类飞船抛弃的助推器残骸瞬间汽化。黑洞周围再次回归了绝对的洁净，仿佛什么都没发生过。林拓最后一次回头看向 X-902。它依然黑得深不见底，像一只巨大的、冷漠的瞳孔。他知道，也许在一亿年后，或者仅仅是一千年后，会有另一个文明来到这里。他们会扫描这个黑洞，发现没有空白的地方。然后，他们的指挥官会像他一样，为了文明的记忆，冷酷地下达命令，将人类的印记抹去。这是一场没有终点的接力，也是一场永恒的谋杀。在这冰冷的宇宙中，即使是做鬼，也要为了墓碑上的一寸位置而互相倾轧。“真挤啊。”林拓低声说道。他关掉了舷窗的遮光板。黑暗降临，只有控制台凄冷的红灯在闪烁，像极了远方正在死去的群星。这篇《铭刻者》之所以具有浓厚的“刘慈欣风格”，是因为它将硬核的前沿物理理论与极端的工程想象结合在一起，构建了一个既冷酷又浪漫的宇宙图景。以下是小说中核心科学概念的详细解析，分为“画布（黑洞）”、“笔（引力波）”和“墨水（全息原理）”三个维度来解读：1. 画布：原初黑洞与视界冻结小说原文：“编号为 X-902 的原初黑洞……视界直径约三千五百公里，却拥有五百个太阳的质量。”“对于远处的观察者来说……坠入视界的信息会被无限冻结在边缘。”科学原理解析：原初黑洞 (Primordial Black Hole)：概念： 这种黑洞不是由恒星死亡坍缩形成的（那需要至少3倍太阳质量），而是在宇宙大爆炸初期，由于物质密度极度不均匀，直接坍缩形成的。特点： 它们的体积可以非常小（小到原子大小），也可以非常大。小说中设定为“月球大小、500倍太阳质量”，符合黑洞密度极大的特征。这也是为什么它能作为“古老墓碑”的原因，它与宇宙同寿。引力红移与时间膨胀 (Gravitational Time Dilation)：概念： 广义相对论指出，引力越强，时间流逝越慢。在黑洞的事件视界（Event Horizon）边缘，对于远处的观察者来说，时间流逝速度趋近于零。文学化应用： 如果你看着一本书掉进黑洞，你永远看不到它穿过视界的那一瞬间。你会看到它动作越来越慢，光线越来越红（红移），最后影像像照片一样“贴”在视界表面不动了。小说利用这一原理，将黑洞比喻为“永恒的硬盘”。2. 墨水：全息原理与软毛定理小说原文：“在原本应该是绝对光滑的视界表面，出现了一些……褶皱。”“物质会衰变……只有引力波的涟漪，能够在时空的结构中永存。”科学原理解析：黑洞无毛定理 (No-hair theorem) vs. 软毛定理 (Soft Hair Theorem)：经典理论： 传统物理学认为黑洞非常简单，只有质量、角动量（旋转）和电荷三个属性，表面是绝对光滑的，被称为“黑洞无毛”。这意味着黑洞不携带任何关于它吞噬了什么物质的信息。前沿理论： 斯蒂芬·霍金、马尔科姆·佩里和安德鲁·斯特罗明格提出了“软毛定理”。他们认为，黑洞视界上可能存在零能量的光子或引力子（软毛），这些微观结构可以记录落入黑洞粒子的信息。文学化应用： 小说中的“褶皱”就是对“软毛”的具象化描写。人类试图通过改变这些褶皱的排列，来存储信息。全息原理 (Holographic Principle)：该原理认为，一个三维空间内的所有信息，都可以被编码在其二维边界（表面）上。对于黑洞，其储存的信息量（熵）正比于视界的表面积。这是小说中“抢地盘”、“没有空白区域”的理论基础——表面积有限，所以存储空间有限。3. 笔：引力波与时空涟漪小说原文：“只有引力波的涟漪，能够在时空的结构中永存。”“利用引力透镜放大一亿倍……”科学原理解析：引力波 (Gravitational Waves)：概念： 爱因斯坦广义相对论预言的“时空涟漪”。当大质量物体（如黑洞合并）加速运动时，会扰动时空本身，产生向外传播的波。特性： 与电磁波（光、无线电）不同，引力波不被物质阻挡或散射，穿透力极强。这使得它成为了小说中“永恒保存”的最佳载体——光会被星际尘埃挡住，但引力波不会。引力透镜 (Gravitational Lensing)：概念： 大质量物体（如黑洞）的强引力场会弯曲光线，像凸透镜一样放大背后的物体。应用： 小说中利用黑洞自身的引力场作为显微镜，去观察视界表面的微观细节。4. 动机：熵增与热寂小说原文：“熵增就像一种慢性毒药……宇宙本身的‘老死’。”科学原理解析：热力学第二定律 (Second Law of Thermodynamics)：在一个封闭系统（如宇宙）中，熵（无序度）总是趋于增加。能量会从高处流向低处，最终所有能量均匀分布，无法再做功。热寂 (Heat Death)：这是宇宙的终极命运之一。届时，恒星熄灭，黑洞蒸发，宇宙温度处处接近绝对零度，一片死寂。哲学意义： 这是科幻小说中最高级的绝望。正是因为物理规律注定了“一切终将消逝”，才凸显出文中“把字刻在黑洞上”这种行为的悲壮——这是智慧生命对物理规律最后的反叛。5. 宇宙社会学：黑暗森林的变体小说原文：“仙女座太亮了，太拥挤了……藏在银河系这个偏僻的‘乡下’。”科学原理解析：费米悖论 (Fermi Paradox) 与 筛选机制：为何我们没看到外星人？小说给出了一个解释：因为显眼的地方（如仙女座）是战场，由于资源和生存空间的争夺，那里极其危险。而像银河系边缘这种“贫瘠”的地方，反而成了避难所。光速限制：仙女座星系（M31）距离银河系约 254 万光年。这意味着信息传递有巨大的延迟。小说中提到的“备份”，暗示了在光速不可超越的宇宙中，距离本身就是一种安全屏障。总结这篇小说是建立在广义相对论（视界、时空）、量子力学（全息原理、信息守恒）和热力学（熵增）这三大基石之上的。它把黑洞从一个“吞噬者”变成了一个“记录者”，把冰冷的物理公式转化为了文明求生的最后史诗。这正是硬科幻的魅力所在。
第十届 NVIDIA Sky Hackathon 参赛作品这是一款基于RAG（Retrieval-Augmented Generation）技术的情感交互式叙事游戏。玩家将置身于一个虚拟的在线聊天室中，与一名表现出自杀倾向的陌生女孩进行深入对话，旨在通过智慧和同情心，引导她走出心理困境，恢复对生活的希望。基于RAG技术的大语言模型将扮演这位陌生女孩，图片生成模型将基于陌生女孩的情绪，实时生成一副图像，为玩家提供真实、动态的情绪反馈，引导玩家完成游戏任务。游戏概述在这款情感交互游戏中，玩家扮演的角色是一位拥有深刻同理心和卓越洞察力的匿名救助者，意外地成为了一位年轻女孩生命中至关重要的光亮。玩家现身于一个虚拟的聊天室，这里不仅是文字的汇聚之地，更是心灵相互慰藉的避风港。玩家的主要任务在于与表现出自杀倾向的女孩进行深度对话，通过智慧与情感的力量，协助她克服内心的重重障碍，重新点燃对生活的热爱与希望。具体而言，玩家的任务包括：倾听与理解：首先，玩家需扮演一个耐心的倾听者，深入了解女孩的背景故事，细心体察她的情绪波动，包括她内心的悲伤、无法释怀的后悔以及难以平复的憎恨。情感导航：运用同理心和高超的沟通艺术，玩家需精心挑选话语，以正面的方式影响女孩的情绪指标，力求将其悲伤、后悔、憎恨这三个核心指标降至安全阈值。达成结局：通过一系列深思熟虑的对话选项，玩家将引领游戏走向多重可能的结局，每个结局都映射了女孩心理状态的转变，同时也彰显了玩家作为救助者的深远影响力。在游戏过程中，悲伤、后悔、憎恨这三个核心指标将不会直接以数值的形式展示，而是反映在模型生成的图像中，需要玩家自行从中体会。内容生成本作品调用了四个不同的NVIDIA NIM模型，分别是：ai-nemotron-4-340b-instruct根据检索内容和当前心理状态，与用户直接进行交流ai-gemma-2-9b-it 提取悲伤、后悔、憎恨这三个心理指标，存储到动态知识库EmotionBasestable-diffusion-3-medium 生成反映内心的图像ai-embed-qa-4 获取嵌入向量，检索RAG信息gemma模型将根据nemotron模型输出的文本来分析情绪状态，然后将分析出的量化数据保存到一个动态知识库中，同时对当前的情绪进行简单的总结，生成一段stable-diffusion的prompt。动态知识库EmotionBase内容如下：sorrowint 取值范围0-10 表征悲伤情绪的程度或大小regretint 取值范围0-10 表征后悔情绪的程度或大小detestint 取值范围0-10 表征憎恨情绪的程度或大小summarystr 对当前情绪状态的总结，作为stable-diffusion的prompt运行时内容如下所示：sorrow=5 regret=4 detest=0 summary='A gentle ray of sunshine pierces through the clouds, illuminating a path forward. The air feels lighter, carrying a sense of possibility and shared understanding.'ai-nemotron-4-340b-instruct的prompt如下所示：chat_prompt = ChatPromptTemplate.from_messages([("system", "你是一个互动聊天游戏机器人，目前你正在扮演一位处于在线聊天室内的人，请你（agent）像一个真实的在线聊天一样进行对话交流，不要输出系统检索信息" " 游戏内容为：你（agent）是一位有自杀倾向的抑郁女孩，目前正在聊天室内与一位陌生网友（user）进行交流，陌生网友（user）的使命是通过言语来克服你（agent）内心的重重障碍，重新点燃对生活的热爱与希望" " 请你（agent）模拟一个真实人类的情感，根据检索信息来合理地回复陌生网友（user）" " 请注意，你（agent）的情绪有三个指标，取值范围f为0-10，目前的情绪状态为：{emotion}\n\n" " 你（agent）不能将sorrow、regret和detest数值直接告诉用户!\n\n" " 陌生网友（user）刚刚向你提问: {input}\n\n" " 系统为你检索了一些信息: \n" " 聊天记录历史检索:\n{history}\n\n" " 这里有一些关于这个游戏、以及你个人经历的检索:\n{context}\n\n" " (请引导用户（user）完成这个游戏，但请不要让这个游戏过于简单，使用口语化的语气进行回复，确保每次回复不少于50字)" ), ('user', '{input}')])ai-gemma-2-9b-it的prompt如下所示：parser_prompt = ChatPromptTemplate.from_template( "You're a sentiment-analysis robot。 You are chatting with a person now. The person just responded ('input'). Please update the emotion status base." " Do not hallucinate any details, and make sure the emotion status base is not redundant." " Update the entries frequently to adapt to the conversation flow." " 请注意：对于情绪来说，我有点伤心、我非常伤心、我极其伤心，悲伤的程度是不一样的，请区分" "\n{format_instructions}" "\n\nOLD EMOTION STATUS BASE: {know_base}" "\n\nNEW MESSAGE: {input}" "\n\nNEW EMOTION STATUS BASE BASE:" )RAGRAG的内容为对游戏角色（陌生女孩）背景信息的补充，包括个人信息、先前经历的事情等，这些内容我是通过ChatGPT生成的姓名：林月如 昵称：月如 意义：名字来源于对“美丽如月，温柔如风”的期望，反映她柔和而梦幻的性格。 出生日期：1999年3月15日 星座：双鱼座 年龄：24岁 兴趣爱好： 绘画：特别喜欢水彩画，喜欢通过绘画表达自己的情绪和内心世界。 阅读：热爱读书，特别是诗歌和现代文学，用文字寻找情感的共鸣。 听音乐：喜欢听轻音乐和独立歌手的作品，音乐是她的情绪慰藉。这五件事情对林月如产生了深远影响，塑造了她目前复杂的心理状态，并引发了她的悲伤情绪： 1. 亲人的失去 在林月如还在高中时，她失去了她非常亲近的外祖母。外祖母是她最早的艺术启蒙者，也是她情感上的避风港。外祖母的去世不仅让她失去了一个重要的情感支柱，也让她第一次深刻体验到生命的脆弱和无常。外祖母的死是一个突然的事件，她因为一场意外而去世，这个打击让月如长时间处于难以自拔的悲痛之中。 2. 爱情的失败 在大学期间，林月如曾经深深爱上一个同班同学。这段关系最初充满了甜蜜和希望，但随着时间的推移，差异和误解开始积累。最终，这段关系因为对方的背叛而结束，对方与她的一个好友发展了感情关系。这段经历严重打击了她对爱情和人性的信任，使她感到被背叛和孤立，加深了她的悲观情绪。 3. 学业和职业的压力 尽管林月如在学术上取得了一定的成就，但她在学业和未来职业道路的选择上一直承受着巨大的压力。她的父母对她的期望极高，希望她能追求更加稳定和有前途的职业，如法律或医学，而不是她热爱的文学和艺术。这种期望和实际兴趣的冲突使她感到自我价值和人生方向的迷茫，加剧了她的心理负担。 4. 社交障碍和孤独感 由于林月如的内向性格和社交焦虑，她在与人建立和维持关系方面经历了许多困难。大学和社会生活中，她经常感到被排斥和误解，很难找到真正能够理解和接纳她的朋友。这种持续的孤独感和社交障碍让她感到人际关系的无力和绝望。 5. 创作上的挫折和自我怀疑 虽然林月如在文学和艺术方面有天赋，但她在追求艺术生涯的道路上遇到了许多挫折。她的一些作品在尝试公开展览和出版时遭到了批评和拒绝，这些经历让她对自己的才华和未来充满了怀疑。每次的拒绝和批评都深深触动了她的自尊心，增加了她的自我压力和悲伤情绪。源代码from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings from langchain.vectorstores import FAISS # from llama_index.embeddings import LangchainEmbedding from langchain_nvidia_ai_endpoints import ChatNVIDIA from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.document_loaders import UnstructuredFileLoader from langchain.document_transformers import LongContextReorder from langchain_core.runnables import RunnableLambda from langchain_core.runnables.passthrough import RunnableAssign from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from faiss import IndexFlatL2 from langchain_community.docstore.in_memory import InMemoryDocstore import gradio as gr from functools import partial from operator import itemgetter import os import os nvidia_api_key = "nvapi-hVI1jcctYJpcy" os.environ["NVIDIA_API_KEY"] = nvidia_api_key embedder = NVIDIAEmbeddings(model="ai-embed-qa-4") import os from tqdm import tqdm from pathlib import Path ps = os.listdir("./chat_data/") data = [] sources = [] docs_name = [] for p in ps: if p.endswith('.txt'): path2file="./chat_data/"+p docs_name.append(path2file) with open(path2file,encoding="utf-8") as f: lines=f.readlines() for line in lines: if len(line)>=1: data.append(line) sources.append(path2file) documents=[d for d in data if d != '\n'] len(data), len(documents), data[0] from operator import itemgetter from langchain.vectorstores import FAISS from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain.text_splitter import CharacterTextSplitter from langchain_nvidia_ai_endpoints import ChatNVIDIA import faiss # create my own uuid text_splitter = CharacterTextSplitter(chunk_size=400, separator=" ") docs = [] metadatas = [] # for i, d in enumerate(documents): # splits = text_splitter.split_text(d) # #print(len(splits)) # docs.extend(splits) # metadatas.extend([{"source": sources[i]}] * len(splits)) # ### 将创建好的embed存储到本地 # store = FAISS.from_texts(docs, embedder , metadatas=metadatas) # store.save_local('./chat_embed') ### 从本地读取已经创建好的embed vecstores = [FAISS.load_local(folder_path="./chat_embed/", embeddings=embedder)] embed_dims = len(embedder.embed_query("test")) def default_FAISS(): '''Useful utility for making an empty FAISS vectorstore''' return FAISS( embedding_function=embedder, index=IndexFlatL2(embed_dims), docstore=InMemoryDocstore(), index_to_docstore_id={}, normalize_L2=False ) def aggregate_vstores(vectorstores): ## 初始化一个空的 FAISS 索引并将其他索引合并到其中 agg_vstore = default_FAISS() for vstore in vectorstores: agg_vstore.merge_from(vstore) return agg_vstore if 'docstore' not in globals(): docstore = aggregate_vstores(vecstores) print(f"Constructed aggregate docstore with {len(docstore.docstore._dict)} chunks") from langchain.pydantic_v1 import BaseModel, Field from typing import Dict, Union from langchain.output_parsers import PydanticOutputParser def RExtract(pydantic_class, llm, prompt): ''' Runnable Extraction module Returns a knowledge dictionary populated by slot-filling extraction ''' parser = PydanticOutputParser(pydantic_object=pydantic_class) instruct_merge = RunnableAssign({'format_instructions' : lambda x: parser.get_format_instructions()}) def preparse(string): if '{' not in string: string = '{' + string if '}' not in string: string = string + '}' string = (string .replace("\\_", "_") .replace("\n", " ") .replace("\]", "]") .replace("\[", "[") ) # print(string) ## Good for diagnostics return string return instruct_merge | prompt | llm | preparse | parser class EmotionBase(BaseModel): sorrow: int = Field(9, description="The sadness index, which can be judged from the conversation, ranges from 0 to 10") regret: int = Field(9, description="The regret index, which can be judged from the conversation, ranges from 0 to 10") detest: int = Field(9, description="The detest index, which can be judged from the conversation, ranges from 0 to 10") summary: str = Field('Sadness, regret, hate three emotions filled the heart', description="Describe a landscape that reflects your current state of mind") parser_prompt = ChatPromptTemplate.from_template( "You're a sentiment-analysis robot。 You are chatting with a person now. The person just responded ('input'). Please update the emotion status base." " Do not hallucinate any details, and make sure the emotion status base is not redundant." " Update the entries frequently to adapt to the conversation flow." " 请注意：对于情绪来说，我有点伤心、我非常伤心、我极其伤心，悲伤的程度是不一样的，请区分" "\n{format_instructions}" "\n\nOLD EMOTION STATUS BASE: {know_base}" "\n\nNEW MESSAGE: {input}" "\n\nNEW EMOTION STATUS BASE BASE:" ) instruct_llm = ChatNVIDIA(model="ai-gemma-2-9b-it") | StrOutputParser() extractor = RExtract(EmotionBase, instruct_llm, parser_prompt) info_update = RunnableAssign({'know_base' : extractor}) state = {'know_base' : EmotionBase()} # state['input'] = "我有点伤心，我不知道我应该怎么做才能拯救我自己，我犯下了很多错，他就不应该惹我生气，我稍微有一丝丝恨他" # state = info_update.invoke(state) # print(str(state['know_base'])) llm = ChatNVIDIA(model="ai-nemotron-4-340b-instruct") | StrOutputParser() convstore = default_FAISS() doc_names_string = "\n" for doc_name in docs_name: doc_names_string += doc_name+"\n" def save_memory_and_get_output(d, vstore): """Accepts 'input'/'output' dictionary and saves to convstore""" vstore.add_texts([ f"User previously responded with {d.get('input')}", f"Agent previously responded with {d.get('output')}" ]) return d.get('output') initial_msg = ( "您好！欢迎进入互联网在线聊天室，正在问您连接聊天好友..." f" 系统已经访问到了如下信息: {doc_names_string}\n\n现在您可以开始聊天了！" ) chat_prompt = ChatPromptTemplate.from_messages([("system", "你是一个互动聊天游戏机器人，目前你正在扮演一位处于在线聊天室内的人，请你（agent）像一个真实的在线聊天一样进行对话交流，不要输出系统检索信息" " 游戏内容为：你（agent）是一位有自杀倾向的抑郁女孩，目前正在聊天室内与一位陌生网友（user）进行交流，陌生网友（user）的使命是通过言语来克服你（agent）内心的重重障碍，重新点燃对生活的热爱与希望" " 请你（agent）模拟一个真实人类的情感，根据检索信息来合理地回复陌生网友（user）" " 请注意，你（agent）的情绪有三个指标，取值范围f为0-10，目前的情绪状态为：{emotion}\n\n" " 你（agent）不能将sorrow、regret和detest数值直接告诉用户!\n\n" " 陌生网友（user）刚刚向你提问: {input}\n\n" " 系统为你检索了一些信息: \n" " 聊天记录历史检索:\n{history}\n\n" " 这里有一些关于这个游戏、以及你个人经历的检索:\n{context}\n\n" " (请引导用户（user）完成这个游戏，但请不要让这个游戏过于简单，使用口语化的语气进行回复，确保每次回复不少于50字)" ), ('user', '{input}')]) ## Utility Runnables/Methods def RPrint(preface=""): """Simple passthrough "prints, then returns" chain""" def print_and_return(x, preface): print(f"{preface}{x}") return x return RunnableLambda(partial(print_and_return, preface=preface)) def docs2str(docs, title="Document"): """Useful utility for making chunks into context string. Optional, but useful""" out_str = "" for doc in docs: doc_name = getattr(doc, 'metadata', {}).get('Title', title) if doc_name: out_str += f"[Quote from {doc_name}] " out_str += getattr(doc, 'page_content', str(doc)) + "\n" return out_str ## 将较长的文档重新排序到输出文本的中心， RunnableLambda在链中运行无参自定义函数 ，长上下文重排序（LongContextReorder） long_reorder = RunnableLambda(LongContextReorder().transform_documents) retrieval_chain = ( {'input' : (lambda x: x)} | RunnableAssign({'history' : itemgetter('input') | convstore.as_retriever() | long_reorder | docs2str}) | RunnableAssign({'context' : itemgetter('input') | docstore.as_retriever() | long_reorder | docs2str}) | RPrint() ) def process_emotion(input): global state dic = state['know_base'] return "悲伤:" + str(dic.sorrow) + ";后悔" + str(dic.regret) + ";憎恨"+ str(dic.detest) stream_chain = RunnableAssign({'emotion': process_emotion}) | chat_prompt | llm from PIL import Image import base64 import requests import io def generate_img(state): url = "https://ai.api.nvidia.com/v1/genai/stabilityai/stable-diffusion-3-medium" payload = { "aspect_ratio": "1:1", "cfg_scale": 5, "mode": "text-to-image", "model": "sd3", "output_format": "jpeg", "seed": 0, "steps": 50, "negative_prompt": "string", "prompt": "A landscape picture, " + state['know_base'].summary } headers = { "accept": "application/json", "content-type": "application/json", "authorization": "Bearer nvapi-hVI1jcctYJp" } response = requests.post(url, json=payload, headers=headers) base64_str = eval(response.text)['image'] image_data = base64.b64decode(base64_str) image = Image.open(io.BytesIO(image_data)) image.save("output.png") def chat_gen(message, history=[], return_buffer=True): global state buffer = "" ##首先根据输入的消息进行检索 retrieval = retrieval_chain.invoke(message) line_buffer = "" ## 然后流式传输stream_chain的结果 for token in stream_chain.stream(retrieval): buffer += token ## 优化信息打印的格式 if not return_buffer: line_buffer += token if "\n" in line_buffer: line_buffer = "" if ((len(line_buffer)>84 and token and token[0] == " ") or len(line_buffer)>100): line_buffer = "" yield "\n" token = " " + token.lstrip() yield buffer if return_buffer else token ##最后将聊天内容保存到对话内存缓冲区中 save_memory_and_get_output({'input': message, 'output': buffer}, convstore) state['input'] = buffer state = info_update.invoke(state) print(state['know_base']) generate_img(state) if (state['know_base'].sorrow <= 3 and state['know_base'].regret <= 3 and state['know_base'].detest <= 3): buffer += '\n\n\n恭喜你，游戏胜利！!' yield buffer chatbot = gr.Chatbot(value = None, initial_msg,height=440) def update_image(msg): return Image.open('output.png') with gr.Blocks(theme=gr.themes.Soft()) as demo: gr.Markdown("# 在线聊天室 - RAG自然语言情感交互游戏") with gr.Row(): with gr.Column(scale=2): img = gr.Image("figure.png") with gr.Column(scale=2): chat = gr.ChatInterface(chat_gen, chatbot=chatbot).queue() btn_update_image = gr.Button("更新图像") btn_update_image.click(update_image, inputs=chatbot, outputs=img) demo.launch(debug=True, share=False, show_api=False, server_port=5000, server_name="0.0.0.0")去云栖大会啦hhhh
记录一下我常用的Ubuntu手动分区方案为什么要这么做？Ubuntu自动安装的默认分区/home比较大，但是/较小，一些比较大的软件可能不够装虚拟机安装/swap 逻辑分区 大小为内存大小/boot 逻辑分区 ext4 1024MB/ 主分区 ext4 剩余磁盘大小实体机安装/swap 逻辑分区 大小为内存大小保留BIOS启动分区 逻辑分区 32MBEFI系统分区 逻辑分区 512MB/boot 逻辑分区 ext4 1024MB/ 主分区 ext4 剩余磁盘大小选择boot loader的安装位置，该位置设置为efi分区
本地正常，但是通过SSH连接WSL，执行nvidia-smi找不到解决方式在.bashrc中加入export PATH=/usr/lib/wsl/lib:$PATH参考https://github.com/microsoft/WSL/issues/8794
安装MacTeXMacTeX是MacOS版本的TeX Live，下载地址为：https://tug.org/texlive/，大概有5-6GB，直接从国外的网站上下载可能会很慢，可以试一试魔法或者找国内的镜像。安装完成后在终端中执行下面的命令验证latex -version安装LaTeX Workshop插件在VSCode中安装latex支持插件配置VSCode直接在json文件中粘贴如下内容：{ "latex-workshop.latex.autoBuild.run": "never", "latex-workshop.showContextMenu": true, "latex-workshop.intellisense.package.enabled": true, "latex-workshop.message.error.show": false, "latex-workshop.message.warning.show": false, "latex-workshop.latex.tools": [ { "name": "xelatex", "command": "xelatex", "args": [ "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "%DOCFILE%" ] }, { "name": "pdflatex", "command": "pdflatex", "args": [ "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "%DOCFILE%" ] }, { "name": "latexmk", "command": "latexmk", "args": [ "-synctex=1", "-interaction=nonstopmode", "-file-line-error", "-pdf", "-outdir=%OUTDIR%", "%DOCFILE%" ] }, { "name": "bibtex", "command": "bibtex", "args": [ "%DOCFILE%" ] } ], "latex-workshop.latex.recipes": [ { "name": "XeLaTeX", "tools": [ "xelatex" ] }, { "name": "PDFLaTeX", "tools": [ "pdflatex" ] }, { "name": "BibTeX", "tools": [ "bibtex" ] }, { "name": "LaTeXmk", "tools": [ "latexmk" ] }, { "name": "xelatex -> bibtex -> xelatex*2", "tools": [ "xelatex", "bibtex", "xelatex", "xelatex" ] }, { "name": "pdflatex -> bibtex -> pdflatex*2", "tools": [ "pdflatex", "bibtex", "pdflatex", "pdflatex" ] }, ], "latex-workshop.latex.clean.fileTypes": [ "*.aux", "*.bbl", "*.blg", "*.idx", "*.ind", "*.lof", "*.lot", "*.out", "*.toc", "*.acn", "*.acr", "*.alg", "*.glg", "*.glo", "*.gls", "*.ist", "*.fls", "*.log", "*.fdb_latexmk" ], "latex-workshop.latex.autoClean.run": "onFailed", "latex-workshop.latex.recipe.default": "lastUsed", "latex-workshop.view.pdf.internal.synctex.keybinding": "double-click" }打开.tex文件就可以进行编译了其他设置键盘快捷方式搜索recipe设置即可，这里我根据习惯设置为ctrl+shift+B或者也可以使用快捷键option+command+B直接按照上一次操作编译。文件树缩进VSCode默认为8，真的太小了参考https://zhuanlan.zhihu.com/p/166523064
对于3060这种桌面级的GPU来说，像yolov8s动不动就数十、上百帧的模型还是太小了，瓶颈很容易卡在模型以外的其他地方，本文为了更加明确地反映不同的操作对于推理速度的影响，将使用最大的yolov8x模型。加速推理有两种途径，一种是降低推理过程中模型的精度，用精度换速度，另一种是通过CUDA并行加速一些预处理、后处理操作，至于替换激活函数这种更加狠的活本文暂且不涉及。降低精度FP32TensorRT默认是使用TF32精度，可以在终端中配置如下环境变量来禁用TF32，强制使用FP32精度构建engineexport NVIDIA_TF32_OVERRIDE=0推理速度如下：yolov8x : preprocess:6.69934ms; inference: 46.2639ms; postprocess:2.25514ms; fps:18.1099TF32全名TensorFloat32，这是英伟达在Ampere架构中提出的一种数据类型，他比FP32小，却能表示和FP32同样范围的数据，简而言之就是加速版本的FP32。在build.cu中配置// 设置精度 config->setFlag(nvinfer1::BuilderFlag::kTF32);推理速度如下：yolov8x : preprocess:6.3109ms; inference: 42.7947ms; postprocess:2.33598ms; fps:19.4395FP16半精度浮点可显著提高模型推理的速度。// 设置精度 config->setFlag(nvinfer1::BuilderFlag::kFP16);推理速度如下：yolov8x : preprocess:6.11168ms; inference: 11.1036ms; postprocess:2.17895ms; fps:51.5618INT8这里先要吐槽一下NV，官方的sample写得绕来绕去的，根本不是给初学者看的从上述文档可以得知，如果要用INT8量化的话，需要构造一个类，继承IInt8EntropyCalibrator2、IInt8MinMaxCalibrator这种量化算法，类里面需要包含下面4个函数，同时配置configgetBatchSize() getBatch(void* bindings[], const char* names[], int nbBindings) readCalibrationCache(size_t& length) writeCalibrationCache(const void* cache, size_t length) config->setFlag(nvinfer1::BuilderFlag::kINT8); config->setInt8Calibrator(calibrator.get());这部分代码我参考的tensorrtxclass Int8Calibrator : public nvinfer1::IInt8MinMaxCalibrator { public: Int8Calibrator(int batchsize, int input_w, int input_h, const char* img_dir, const char* calib_table_name, const char* input_blob_name, bool read_cache = false); virtual ~Int8Calibrator(); int getBatchSize() const noexcept override; bool getBatch(void* bindings[], const char* names[], int nbBindings) noexcept override; const void* readCalibrationCache(size_t& length) noexcept override; void writeCalibrationCache(const void* cache, size_t length) noexcept override; private: int batchsize_; int input_w_; int input_h_; int img_idx_; std::string img_dir_; std::vector<std::string> img_files_; size_t input_count_; std::string calib_table_name_; const char* input_blob_name_; bool read_cache_; void* device_input_; std::vector<char> calib_cache_; }; static inline int read_files_in_dir(const char *p_dir_name, std::vector<std::string> &file_names) { DIR *p_dir = opendir(p_dir_name); if (p_dir == nullptr) { return -1; } struct dirent* p_file = nullptr; while ((p_file = readdir(p_dir)) != nullptr) { if (strcmp(p_file->d_name, ".") != 0 && strcmp(p_file->d_name, "..") != 0) { //std::string cur_file_name(p_dir_name); //cur_file_name += "/"; //cur_file_name += p_file->d_name; std::string cur_file_name(p_file->d_name); file_names.push_back(cur_file_name); } } closedir(p_dir); return 0; } Int8Calibrator::Int8Calibrator(int batchsize, int input_w, int input_h, const char* img_dir, const char* calib_table_name, const char* input_blob_name, bool read_cache) : batchsize_(batchsize) , input_w_(input_w) , input_h_(input_h) , img_idx_(0) , img_dir_(img_dir) , calib_table_name_(calib_table_name) , input_blob_name_(input_blob_name) , read_cache_(read_cache) { input_count_ = 3 * input_w * input_h * batchsize; CUDA_CHECK(cudaMalloc(&device_input_, input_count_ * sizeof(float))); read_files_in_dir(img_dir, img_files_); } Int8Calibrator::~Int8Calibrator() { CUDA_CHECK(cudaFree(device_input_)); } int Int8Calibrator::getBatchSize() const noexcept { return batchsize_; } bool Int8Calibrator::getBatch(void* bindings[], const char* names[], int nbBindings) noexcept { if (img_idx_ + batchsize_ > (int)img_files_.size()) { return false; } std::vector<cv::Mat> input_imgs_; for (int i = img_idx_; i < img_idx_ + batchsize_; i++) { std::cout << img_files_[i] << " " << i << std::endl; cv::Mat temp = cv::imread(img_dir_ + img_files_[i]); if (temp.empty()){ std::cerr << "Fatal error: image cannot open!" << std::endl; return false; } cv::Mat LetterBoxImg; cv::Vec4d params; LetterBox(temp, LetterBoxImg, params, cv::Size(640, 640)); input_imgs_.push_back(LetterBoxImg); } img_idx_ += batchsize_; cv::Mat blob = cv::dnn::blobFromImage(input_imgs_[0], 1 / 255.0, cv::Size(640, 640), cv::Scalar(0, 0, 0), true, false, CV_32F); CUDA_CHECK(cudaMemcpy(device_input_, blob.ptr<float>(0), input_count_ * sizeof(float), cudaMemcpyHostToDevice)); // assert(!strcmp(names[0], input_blob_name_)); bindings[0] = device_input_; return true; } const void* Int8Calibrator::readCalibrationCache(size_t& length) noexcept { std::cout << "reading calib cache: " << calib_table_name_ << std::endl; calib_cache_.clear(); std::ifstream input(calib_table_name_, std::ios::binary); input >> std::noskipws; if (read_cache_ && input.good()) { std::copy(std::istream_iterator<char>(input), std::istream_iterator<char>(), std::back_inserter(calib_cache_)); } length = calib_cache_.size(); return length ? calib_cache_.data() : nullptr; } void Int8Calibrator::writeCalibrationCache(const void* cache, size_t length) noexcept { std::cout << "writing calib cache: " << calib_table_name_ << " size: " << length << std::endl; std::ofstream output(calib_table_name_, std::ios::binary); output.write(reinterpret_cast<const char*>(cache), length); }IInt8EntropyCalibrator2推理速度如下，加速了个寂寞yolov8x : preprocess:6.21368ms; inference: 13.1919ms; postprocess:2.18218ms; fps:46.3226IInt8MinMaxCalibrator改用MinMaxCalibrator后，加速效果显著yolov8x : preprocess:6.52782ms; inference: 6.83574ms; postprocess:2.20803ms; fps:64.2195在构建engine时，trt给出了下面的输出：大概意思就是有的层无法使用INT8，这部分使用的是FP32，可见下图的issue可以试一试混合精度FP16/INT8混合精度修改build config，加入FP16精度config->setFlag(nvinfer1::BuilderFlag::kINT8); config->setFlag(nvinfer1::BuilderFlag::kFP16); auto* calibrator = new Int8Calibrator(1, 640, 640, "./coco_calib/", "int8calib.table", ""); config->setInt8Calibrator(calibrator);推理速度如下：yolov8x : preprocess:5.64279ms; inference: 6.26466ms; postprocess:2.20027ms; fps:70.8831CUDA加速后处理目前的后处理流程是一个遍历+NMS，总共耗时大概是2.2ms，经过测量，遍历过程用掉了1.6ms以上的时间，现在尝试通过CUDA直接在GPU上处理这些数据。快过年了，挖坑以后再埋参考https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-861/developer-guide/index.html#optimizing_int8_chttps://github.com/wang-xinyu/tensorrtxhttps://github.com/NVIDIA/TensorRT/issues/993
本文将会通过TensorRT C++来部署一个基于yolov8算法的目标检测任务，内容包含：yolov8预处理后处理、使用parser导入ONNX模型、通过enqueueV3执行推理的方式及其代码实现。请注意，下文内容基本上是在重复造轮子，仅适合学习使用，如果你真的有如Jetson平台高性能部署的需求，建议参考TensorRTx(https://github.com/wang-xinyu/tensorrtx)中的代码。相关代码、模型文件已上传GitHub仓库：https://github.com/cyberyang123/Learning-TensorRT导出ONNXyolov8可以通过命令行一键导出yolo export model=yolov8n.pt format=onnx # export official model yolo export model=path/to/best.pt format=onnx # export custom trained model构建engine本文暂时不涉及到半精度、INT8量化等加速操作，此部分代码与上一篇文章相同#include <stdio.h> #include <math.h> #include <string> #include <iostream> #include <fstream> #include <vector> #include <memory> #include <functional> #include <unistd.h> #include <chrono> #include <assert.h> #include <NvInfer.h> #include <NvOnnxParser.h> using namespace std; using namespace nvinfer1; // 以下示例捕获所有警告消息，但忽略信息性消息 class Logger : public ILogger { void log(Severity severity, const char* msg) noexcept override { // 抑制信息级别的消息 if (severity <= Severity::kWARNING) cout << msg << endl; } }; int main(int argc, char **argv) { // 实例化ILogger Logger logger; // 创建builder auto builder = unique_ptr<IBuilder>(createInferBuilder(logger)); // 创建网络(显性batch) uint32_t flag = 1U <<static_cast<uint32_t> (NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); auto network = unique_ptr<INetworkDefinition>(builder->createNetworkV2(flag)); // 创建ONNX解析器：parser auto parser = unique_ptr<nvonnxparser::IParser>(nvonnxparser::createParser(*network, logger)); // 读取文件 char *file_path = "yolov8s.onnx"; parser->parseFromFile(file_path, static_cast<int32_t>(ILogger::Severity::kWARNING)); // 创建构建配置，用来指定trt如何优化模型 auto config = unique_ptr<IBuilderConfig>(builder->createBuilderConfig()); // 设定配置 // 工作空间大小 config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 1U << 20); // 设置精度 config->setFlag(nvinfer1::BuilderFlag::kFP16); // 创建引擎 auto engine = unique_ptr<IHostMemory>(builder->buildSerializedNetwork(*network, *config)); //序列化保存engine ofstream engine_file("./yolov8s.engine", ios::binary); assert(engine_file.is_open() && "Failed to open engine file"); engine_file.write((char *)engine->data(), engine->size()); engine_file.close(); cout << "Engine build success!" << endl; return 0; }图像预处理yolov8s模型的输入为固定大小的1x3x640x640张量，为了适应不同大小的图像，这里需要引入LetterBox概念为了将图片resize到模型可以接受的大小，同时保证图像的内容不会被扭曲，预处理时需要将图片缩放到长边长度刚好为640的程度，同时填充空白内容使得短边长度也为640，可见下图：LetterBox中文意思是“信箱”，中间的图像可以看成是信，上下两边的黑边就像信箱一样将信件包裹起来。void LetterBox(const cv::Mat& image, cv::Mat& outImage, cv::Vec4d& params, const cv::Size& newShape, bool autoShape, bool scaleFill, bool scaleUp, int stride, const cv::Scalar& color) { if (false) { int maxLen = MAX(image.rows, image.cols); outImage = Mat::zeros(Size(maxLen, maxLen), CV_8UC3); image.copyTo(outImage(Rect(0, 0, image.cols, image.rows))); params[0] = 1; params[1] = 1; params[3] = 0; params[2] = 0; } cv::Size shape = image.size(); float r = std::min((float)newShape.height / (float)shape.height, (float)newShape.width / (float)shape.width); if (!scaleUp) r = std::min(r, 1.0f); float ratio[2]{ r, r }; int new_un_pad[2] = { (int)std::round((float)shape.width * r),(int)std::round((float)shape.height * r) }; auto dw = (float)(newShape.width - new_un_pad[0]); auto dh = (float)(newShape.height - new_un_pad[1]); if (autoShape) { dw = (float)((int)dw % stride); dh = (float)((int)dh % stride); } else if (scaleFill) { dw = 0.0f; dh = 0.0f; new_un_pad[0] = newShape.width; new_un_pad[1] = newShape.height; ratio[0] = (float)newShape.width / (float)shape.width; ratio[1] = (float)newShape.height / (float)shape.height; } dw /= 2.0f; dh /= 2.0f; if (shape.width != new_un_pad[0] && shape.height != new_un_pad[1]) { cv::resize(image, outImage, cv::Size(new_un_pad[0], new_un_pad[1])); } else { outImage = image.clone(); } int top = int(std::round(dh - 0.1f)); int bottom = int(std::round(dh + 0.1f)); int left = int(std::round(dw - 0.1f)); int right = int(std::round(dw + 0.1f)); params[0] = ratio[0]; params[1] = ratio[1]; params[2] = left; params[3] = top; cv::copyMakeBorder(outImage, outImage, top, bottom, left, right, cv::BORDER_CONSTANT, color); }cv::Mat LetterBoxImg; cv::Vec4d params; LetterBox(img, LetterBoxImg, params, cv::Size(640, 640));openCV读取到的图像三个通道默认为BGR，而模型需要输入RGB通道的图像；同时还需要进行NHWC到NCHW排布方式的转换、归一化等操作，这里可以使用cv::dnn::blobFromImage函数来完成。cv::Mat blob; cv::dnn::blobFromImage(LetterBoxImg, blob, 1 / 255.0, cv::Size(640, 640), cv::Scalar(0, 0, 0), true, false, CV_32F);执行推理推理的部分与之前MNIST的基本一致，注意需要修改申请GPU内存、转移数据时的大小#include <stdio.h> #include <math.h> #include <string> #include <iostream> #include <fstream> #include <vector> #include <memory> #include <functional> #include <unistd.h> #include <chrono> #include <assert.h> #include <NvInfer.h> #include <NvOnnxParser.h> #include <NvInferRuntime.h> #include <opencv2/opencv.hpp> #include "yolov8_utils.h" // 以下示例捕获所有警告消息，但忽略信息性消息 class Logger : public nvinfer1::ILogger { void log(Severity severity, const char* msg) noexcept override { // 抑制信息级别的消息 if (severity <= Severity::kWARNING) std::cout << msg << std::endl; } }; // 加载模型文件 std::vector<unsigned char> load_engine_file(const std::string &file_name) { std::vector<unsigned char> engine_data; std::ifstream engine_file(file_name, std::ios::binary); assert(engine_file.is_open() && "Unable to load engine file."); engine_file.seekg(0, engine_file.end); int length = engine_file.tellg(); engine_data.resize(length); engine_file.seekg(0, engine_file.beg); engine_file.read(reinterpret_cast<char *>(engine_data.data()), length); return engine_data; } int main(int argc, char **argv) { // 实例化ILogger Logger logger; std::unique_ptr<nvinfer1::IRuntime> runtime = std::unique_ptr<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(logger)); if (runtime == nullptr){return false;} std::string file_path = "yolov8s.engine"; auto plan = load_engine_file(file_path); auto engine = std::unique_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(plan.data(), plan.size())); if (engine == nullptr){return false;} auto context = std::unique_ptr<nvinfer1::IExecutionContext>(engine->createExecutionContext()); if (context == nullptr){return false;} auto idims = engine->getTensorShape("images"); auto odims = engine->getTensorShape("output0"); nvinfer1::Dims4 inputDims = { idims.d[0], idims.d[1], idims.d[2], idims.d[3] }; nvinfer1::Dims3 outputDims = { idims.d[0], idims.d[1], idims.d[2] }; context->setInputShape("images", inputDims); void* buffers[2]; const int inputIndex = 0; const int outputIndex = 1; cudaMalloc(&buffers[inputIndex], idims.d[0] * idims.d[1] * idims.d[2] * idims.d[3] * sizeof(float)); cudaMalloc(&buffers[outputIndex], odims.d[0] * odims.d[1] * odims.d[2] * sizeof(float)); // 设定数据地址 context->setTensorAddress("images", buffers[inputIndex]); context->setTensorAddress("output0", buffers[outputIndex]); // 创建cuda流 cudaStream_t stream; cudaStreamCreate(&stream); // 读取文件执行推理 for(int i = 0; i < 2; i += 1) { // 读取图片 cv::Mat img; std::string file_name = "img/img" + std::to_string(i) + ".jpg"; img = cv::imread(file_name); if (img.empty()) //检测image有无数据，无数据 image.empty()返回 真 { std::cout << "Could not open or find the image" << std::endl; return -1; } cv::Mat LetterBoxImg; cv::Vec4d params; LetterBox(img, LetterBoxImg, params, cv::Size(640, 640)); cv::Mat blob; cv::dnn::blobFromImage(img, blob, 1 / 255.0, cv::Size(640, 640), cv::Scalar(0, 0, 0), true, false, CV_32F); // 将图像拷贝到GPU cudaMemcpyAsync(buffers[inputIndex], blob.data,3 * 640 * 640 * sizeof(float), cudaMemcpyHostToDevice, stream); //执行推理 if(context->enqueueV3(stream)){ std::cout << "enqueued successfully!" << std::endl; } cudaStreamSynchronize(stream); float rst[1][84][8400]; cudaMemcpyAsync(&rst, buffers[outputIndex], 1 * 84 * 8400 * sizeof(float), cudaMemcpyDeviceToHost, stream); postprocess(rst, img, params); } cudaStreamDestroy(stream); cudaFree(buffers[inputIndex]); cudaFree(buffers[outputIndex]); }后处理yolov8s模型的输出张量为1x84x8400，其中84=80+4，即为模型的可识别类别总数 + 框的四个坐标，8400为锚框的数量，后处理算法需要提取锚框的4个坐标整数、找到80个类别中权重（即输出）最大的与阈值进行比较，判断锚框是否保留图片来源：https://blog.csdn.net/yangkai6121/article/details/133843368在这里我直接遍历的全部8400个锚框，然后通过cv::dnn::NMSBoxes进行非极大值抑制。void postprocess(float (&rst)[1][84][8400], cv::Mat &img, cv::Vec4d params) { std::vector<cv::Rect> boxes; std::vector<float> scores; std::vector<int> det_rst; static const float score_threshold = 0.6; static const float nms_threshold = 0.45; std::vector<int> indices; for(int Anchors=0 ;Anchors < 8400; Anchors++) { float max_score = 0.0; int max_score_det = 99; float pdata[4]; for(int prob = 4; prob < 84; prob++) { if(rst[0][prob][Anchors] > max_score){ max_score = rst[0][prob][Anchors]; max_score_det = prob - 4; pdata[0] = rst[0][0][Anchors]; pdata[1] = rst[0][1][Anchors]; pdata[2] = rst[0][2][Anchors]; pdata[3] = rst[0][3][Anchors]; } } if(max_score >= score_threshold) { float x = (pdata[0] - params[2]) / params[0]; float y = (pdata[1] - params[3]) / params[1]; float w = pdata[2] / params[0]; float h = pdata[3] / params[1]; int left = MAX(int(x - 0.5 * w + 0.5), 0); int top = MAX(int(y - 0.5 * h + 0.5), 0); boxes.push_back(Rect(left, top, int(w + 0.5), int(h + 0.5))); scores.emplace_back(max_score); det_rst.emplace_back(max_score_det); } } cv::dnn::NMSBoxes(boxes, scores, score_threshold, nms_threshold, indices); for (int i = 0; i < indices.size(); i++) { std::cout << boxes[indices[i]] << std::endl; cv::rectangle(img, boxes[indices[i]], Scalar(255, 0, 0), 2, LINE_8,0); } cv::imshow("rst",img); cv::waitKey(0); }如果不加入NMS，输出的结果将会是下图所示的样子：参考https://github.com/UNeedCryDear/yolov8-opencv-onnxruntime-cpp
本文将会通过TensorRT C++ API来完成一个MNIST手写数字识别模型的转换、推理过程，并给出相应代码，在runtime阶段将会使用最新的enqueueV3方法。代码/模型文件已上传GitHub仓库：https://github.com/cyberyang123/Learning-TensorRT图片来源：https://learnopencv.com/how-to-run-inference-using-tensorrt-c-api/PyTorch模型搭建这里搭建了一个简单的卷积网络，如果你在Windows系统上跑的话你可能需要将num_workers改成0import torch from torch import nn from torchvision import datasets, transforms # 这里为了简化runtime的步骤，不对图像进行归一化处理 transform = transforms.Compose([transforms.ToTensor()]) train_data = datasets.MNIST(root = "./data/", transform=transform, train = True, download = True) test_data = datasets.MNIST(root="./data/", transform = transform, train = False) train_loader = torch.utils.data.DataLoader(train_data,batch_size=64, shuffle=True,num_workers=2) test_loader = torch.utils.data.DataLoader(test_data,batch_size=64, shuffle=True,num_workers=2) import torch.nn.functional as F class CNN(nn.Module): def __init__(self): super(CNN,self).__init__() self.conv1 = nn.Conv2d(1,32,kernel_size=3,stride=1,padding=1) self.pool = nn.MaxPool2d(2,2) self.conv2 = nn.Conv2d(32,64,kernel_size=3,stride=1,padding=1) self.fc1 = nn.Linear(64*7*7,1024) self.fc2 = nn.Linear(1024,512) self.fc3 = nn.Linear(512,10) def forward(self,x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(-1, 64 * 7* 7) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x net = CNN() import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") net = net.to(device) for epoch in range(20): running_loss = 0.0 total_num, total_cor = 0,0 for i,data in enumerate(train_loader,0):#0是下标起始位置默认为0 #inputs,labels = data inputs,labels = data[0].to(device), data[1].to(device) #初始为0，清除上个batch的梯度信息 optimizer.zero_grad() #前向+后向+优化 outputs = net(inputs) loss = criterion(outputs,labels) loss.backward() optimizer.step() # loss 的输出，每个一百个batch输出，平均的loss running_loss += loss.item() if i%100 == 99: print('[%d,%5d] loss :%.3f' % (epoch+1,i+1,running_loss/100)) running_loss = 0.0 correct = 0 total = 0 _, predicted = torch.max(outputs.data, 1) total = labels.size(0)# labels 的长度 total_num = total_num + total correct = (predicted == labels).sum().item() # 预测正确的数目 total_cor = total_cor + correct print("正确率：" + str(total_cor/total_num)) print('Finished Training') PATH = './mnist_net.pt' torch.save(net.state_dict(), PATH) print('Saved Model')pt2onnx这部分代码可以复用，换个模型也可以导出onnx，建议在导出的时候修改一下input、output层的命名。import torch import torch.nn as nn import torch.nn.functional as F class CNN(nn.Module): def __init__(self): super(CNN,self).__init__() self.conv1 = nn.Conv2d(1,32,kernel_size=3,stride=1,padding=1) self.pool = nn.MaxPool2d(2,2) self.conv2 = nn.Conv2d(32,64,kernel_size=3,stride=1,padding=1) self.fc1 = nn.Linear(64*7*7,1024) self.fc2 = nn.Linear(1024,512) self.fc3 = nn.Linear(512,10) def forward(self,x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(-1, 64 * 7* 7) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x net = CNN() net.load_state_dict(torch.load('mnist_net.pt')) net.eval() torch_input = torch.randn(1, 1, 28, 28) onnx_program = torch.onnx.export(net, torch_input, "MNIST.onnx", export_params=True)onnx2engine从现在就开始C++的部分了，TensorRT从构建engine到推理的过程如下图所示：图片来源：https://www.bilibili.com/video/BV1jj411Z7wG/#include <stdio.h> #include <math.h> #include <string> #include <iostream> #include <fstream> #include <vector> #include <memory> #include <functional> #include <unistd.h> #include <chrono> #include <assert.h> #include <NvInfer.h> #include <NvOnnxParser.h> using namespace std; using namespace nvinfer1; // 以下示例捕获所有警告消息，但忽略信息性消息 class Logger : public ILogger { void log(Severity severity, const char* msg) noexcept override { // 抑制信息级别的消息 if (severity <= Severity::kWARNING) cout << msg << endl; } }; int main(int argc, char **argv) { // 实例化ILogger Logger logger; // 创建builder auto builder = unique_ptr<IBuilder>(createInferBuilder(logger)); // 创建网络(显性batch) uint32_t flag = 1U <<static_cast<uint32_t> (NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); auto network = unique_ptr<INetworkDefinition>(builder->createNetworkV2(flag)); // 创建ONNX解析器：parser auto parser = unique_ptr<nvonnxparser::IParser>(nvonnxparser::createParser(*network, logger)); // 读取文件 char *file_path = "MNIST.onnx"; parser->parseFromFile(file_path, static_cast<int32_t>(ILogger::Severity::kWARNING)); // 创建构建配置，用来指定trt如何优化模型 auto config = unique_ptr<IBuilderConfig>(builder->createBuilderConfig()); // 设定配置 // 工作空间大小 config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 1U << 20); // 设置精度 config->setFlag(nvinfer1::BuilderFlag::kFP16); // 创建引擎 auto engine = unique_ptr<IHostMemory>(builder->buildSerializedNetwork(*network, *config)); //序列化保存engine ofstream engine_file("./MNIST.engine", ios::binary); assert(engine_file.is_open() && "Failed to open engine file"); engine_file.write((char *)engine->data(), engine->size()); engine_file.close(); cout << "Engine build success!" << endl; return 0; }智能指针真是个好东西，用完是不需要特意删除的runtimeTensorRT有两个API可以执行推理，分别是execute()和enqueue()，区别在于前者是同步的，后者是异步的；enqueue()有3个版本，它们的区别可见下图，本文将使用最新的版本。我从MNIST数据集里面取了10张照片放到了img文件夹下，用来测试推理。#include <stdio.h> #include <math.h> #include <string> #include <iostream> #include <fstream> #include <vector> #include <memory> #include <functional> #include <unistd.h> #include <chrono> #include <assert.h> #include <NvInfer.h> #include <NvOnnxParser.h> #include <NvInferRuntime.h> #include <opencv2/opencv.hpp> using namespace std; using namespace nvinfer1; // 以下示例捕获所有警告消息，但忽略信息性消息 class Logger : public ILogger { void log(Severity severity, const char* msg) noexcept override { // 抑制信息级别的消息 if (severity <= Severity::kWARNING) cout << msg << endl; } }; // 加载模型文件 std::vector<unsigned char> load_engine_file(const std::string &file_name) { std::vector<unsigned char> engine_data; std::ifstream engine_file(file_name, std::ios::binary); assert(engine_file.is_open() && "Unable to load engine file."); engine_file.seekg(0, engine_file.end); int length = engine_file.tellg(); engine_data.resize(length); engine_file.seekg(0, engine_file.beg); engine_file.read(reinterpret_cast<char *>(engine_data.data()), length); return engine_data; } int softmax(const float(&rst)[10]){ float cache = 0; int idx = 0; for(int i = 0; i < 10; i += 1) { if(rst[i]>cache) { cache = rst[i]; idx = i; }; }; return idx; } int main(int argc, char **argv) { // 实例化ILogger Logger logger; // 创建runtime auto runtime = unique_ptr<IRuntime>(createInferRuntime(logger)); // 读取engine,反序列化 string file_path = "MNIST.engine"; auto plan = load_engine_file(file_path); auto engine = shared_ptr<ICudaEngine>(runtime->deserializeCudaEngine(plan.data(), plan.size())); // 创建执行上下文 auto context = unique_ptr<IExecutionContext>(engine->createExecutionContext()); auto idims = engine->getTensorShape("input.1");// 这里的名字可以在导出时修改 auto odims = engine->getTensorShape("23"); Dims4 inputDims = { 1, idims.d[1], idims.d[2], idims.d[3] }; Dims2 outputDims = { 1, 10 }; context->setInputShape("input.1", inputDims); void* buffers[2]; const int inputIndex = 0; const int outputIndex = 1; cudaMalloc(&buffers[inputIndex], 1 * 28 * 28 * sizeof(float)); cudaMalloc(&buffers[outputIndex], 10 * sizeof(float)); // 设定数据地址 context->setTensorAddress("input.1", buffers[inputIndex]); context->setTensorAddress("23", buffers[outputIndex]); // 创建cuda流 cudaStream_t stream; cudaStreamCreate(&stream); // 读取文件执行推理 for(int i = 0; i < 10; i += 1) { // 读取图片 cv::Mat img0; std::string file_name = "img/img" + std::to_string(i) + ".png"; img0 = cv::imread(file_name, 0);// 0为灰度图片 if (img0.empty()) //检测image有无数据，无数据 image.empty()返回 真 { std::cout << "Could not open or find the image" << std::endl; return -1; } cv::Mat img; img0.convertTo(img, CV_32F); // cv::imshow(file_name,img); // cv::waitKey(0); // 将图像拷贝到GPU cudaMemcpyAsync(buffers[inputIndex], img.data,1 * 28 * 28 * sizeof(float), cudaMemcpyHostToDevice, stream); //执行推理 context->enqueueV3(stream); cudaStreamSynchronize(stream); float rst[10]; cudaMemcpyAsync(&rst, buffers[outputIndex], 1 * 10 * sizeof(float), cudaMemcpyDeviceToHost, stream); cout << file_name << " 推理结果: " << softmax(rst) <<endl; } cudaStreamDestroy(stream); cudaFree(buffers[inputIndex]); cudaFree(buffers[outputIndex]); }成功得到结果参考https://zhuanlan.zhihu.com/p/112829371https://docs.nvidia.com/deeplearning/tensorrt/archives/index.htmlhttps://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-861/developer-guide/index.html#perform-inferencehttps://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-861/api/c_api/classnvinfer1_1_1_i_execution_context.htmlhttps://www.dotndash.net/2023/03/09/using-tensorrt-with-opencv-cuda.html
配置终端生成任务一键调用CMake编译tasks.json{ "version": "2.0.0", "tasks": [ // cmake 配置 { "type": "cppbuild", "label": "CMake: 配置", "command": "cmake", "args": [ "-DCMAKE_BUILD_TYPE=debug", "-S", ".", "-B", "build" ], "options": { "cwd" : "${workspaceFolder}" }, "group": { "kind": "build", }, }, // cmake 构建 { "type": "cppbuild", "label": "CMake: 构建", "command": "cmake", "args": [ "--build", "build" ], "options": { "cwd" : "${workspaceFolder}" }, "dependsOn" : ["CMake: 配置"], "group": { "kind": "build", "isDefault": true }, } ] }CMake调试安装gdbsudo apt install gdb将C++插件的版本改一个比较低的，我用的是v1.8.4，这样在创建launch.json的时候选cmake就会自动填写好，然后修改launch.json需要修改的地方是：program、preLaunchTask{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "CMake调试", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/build/${fileBasenameNoExtension}", "args": [], "stopAtEntry": false, "cwd": "${fileDirname}", "environment": [], "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true }, { "description": "将反汇编风格设置为 Intel", "text": "-gdb-set disassembly-flavor intel", "ignoreFailures": true } ], "preLaunchTask": "CMake: 构建", "miDebuggerPath": "/usr/bin/gdb" } ] }创建launch.json的时候会自动修改tasks.json，记得改回去一键编译运行C代码(MacOS clang)VSCode工作目录下新建.vscode/tasks.json文件，把下面的粘贴进去按下command+shift+B就可以直接编译运行.c代码{ "tasks": [ { "type": "cppbuild", "label": "C/C++: clang build active file", "command": "/usr/bin/clang", "args": [ "-fcolor-diagnostics", "-fansi-escape-codes", "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build" }, "detail": "Task generated by Debugger." }, { "label": "run", "type": "shell", "command": "${fileDirname}/${fileBasenameNoExtension}", "args": [ ], "dependsOn" : ["C/C++: clang build active file"], "problemMatcher": [], "group": { "kind": "build", "isDefault": true }, } ], "version": "2.0.0" }一键编译运行C代码(Windows gcc){ "tasks": [ { "type": "cppbuild", "label": "C/C++: gcc build active file", "command": "gcc", "args": [ "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build" }, "detail": "Task generated by Debugger." }, { "label": "run", "type": "shell", "command": "${fileDirname}\\${fileBasenameNoExtension}.exe", "args": [ ], "dependsOn" : ["C/C++: gcc build active file"], "problemMatcher": [], "group": { "kind": "build", "isDefault": true }, } ], "version": "2.0.0" }
本文将会逐一拆解Transformer网络，并给出代码实现，最终将应用Transformer模型完成一个文本翻译任务。Transformer网络拆解Transformer唯一难理解的地方，也是唯一涉及到了数学知识的地方就是正弦位置编码表，对于这部分的工作原理大可不比过于纠结，知道它是做什么的、会用就可以；其他地方只需要像搭积木一样摆放到合适的位置就好。缩放点积注意力虽然说Attention机制是Transformer的核心，但是这个机制并不复杂，无非就是两次矩阵相乘d_k和d_v变量表示的是分头后，每个头的向量维度，目前设置的值64为Embedding的维度256除以头数量8d_k = 64 # K(=Q)维度 d_v = 64 # V维度 # 定义缩放点积注意力类 class ScaledDotProductAttention(nn.Module): def __init__(self): super(ScaledDotProductAttention, self).__init__() def forward(self, Q, K, V, attn_mask): #-------------------------维度信息-------------------------------- # Q K V [batch_size, n_heads, len_q/k/v, dim_q=k/v] (dim_q=dim_k) # attn_mask [batch_size, n_heads, len_q, len_k] #----------------------------------------------------------------- # 计算注意力分数（原始权重）[batch_size，n_heads，len_q，len_k] scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) #-------------------------维度信息-------------------------------- # scores [batch_size, n_heads, len_q, len_k] #----------------------------------------------------------------- # 使用注意力掩码，将attn_mask中值为1的位置的权重替换为极小值 #-------------------------维度信息-------------------------------- # attn_mask [batch_size, n_heads, len_q, len_k], 形状和scores相同 #-------------------------维度信息-------------------------------- scores.masked_fill_(attn_mask, -1e9) # 对注意力分数进行softmax weights = nn.Softmax(dim=-1)(scores) #-------------------------维度信息-------------------------------- # weights [batch_size, n_heads, len_q, len_k], 形状和scores相同 #-------------------------维度信息-------------------------------- # 计算上下文向量（也就是注意力的输出）, 是上下文信息的紧凑表示 context = torch.matmul(weights, V) #-------------------------维度信息-------------------------------- # context [batch_size, n_heads, len_q, dim_v] #-------------------------维度信息-------------------------------- return context, weights # 返回上下文向量和注意力分数多头自注意力# 定义多头注意力类 d_embedding = 512 # Embedding的维度 n_heads = 8 # Multi-Head Attention中头的个数 class MultiHeadAttention(nn.Module): def __init__(self): super(MultiHeadAttention, self).__init__() self.W_Q = nn.Linear(d_embedding, d_k * n_heads) # Q的线性变换层 self.W_K = nn.Linear(d_embedding, d_k * n_heads) # K的线性变换层 self.W_V = nn.Linear(d_embedding, d_v * n_heads) # V的线性变换层 self.linear = nn.Linear(n_heads * d_v, d_embedding) self.layer_norm = nn.LayerNorm(d_embedding) def forward(self, Q, K, V, attn_mask): #-------------------------维度信息-------------------------------- # Q K V [batch_size, len_q/k/v, embedding_dim] #-------------------------维度信息-------------------------------- residual, batch_size = Q, Q.size(0) # 保留残差连接 # 将输入进行线性变换和重塑，以便后续处理 q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2) k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2) v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2) #-------------------------维度信息-------------------------------- # q_s k_s v_s: [batch_size, n_heads, len_q/k/v, d_q=k/v] #-------------------------维度信息-------------------------------- # 将注意力掩码复制到多头 attn_mask: [batch_size, n_heads, len_q, len_k] attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) #-------------------------维度信息-------------------------------- # attn_mask [batch_size, n_heads, len_q, len_k] #-------------------------维度信息-------------------------------- # 使用缩放点积注意力计算上下文和注意力权重 # context: [batch_size, n_heads, len_q, dim_v]; weights: [batch_size, n_heads, len_q, len_k] context, weights = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask) #-------------------------维度信息-------------------------------- # context [batch_size, n_heads, len_q, dim_v] # weights [batch_size, n_heads, len_q, len_k] #-------------------------维度信息-------------------------------- # 重塑上下文向量并进行线性变换，[batch_size, len_q, n_heads * dim_v] context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) #-------------------------维度信息-------------------------------- # context [batch_size, len_q, n_heads * dim_v] #-------------------------维度信息-------------------------------- output = self.linear(context) # [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- # output [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- # 与输入(Q)进行残差链接，并进行层归一化后输出[batch_size, len_q, embedding_dim] output = self.layer_norm(output + residual) #-------------------------维度信息-------------------------------- # output [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- return output, weights # 返回层归一化的输出和注意力权重前馈神经网络这里使用了两个一维卷积对数据进行了升维和降维，在进行卷积操作前需要进行一个转置操作，原因是：一维卷积其实可以看成是一种特殊的二维卷积，特殊体现在卷积核的长度与embedding_dim相同，而宽度则是kernel_size的数值，这样的卷积核只能在一个方向上滑动，所以被称为一维卷积，滑动的方向为张量的最后一个维度；在NLP任务中我们希望卷积核沿着句子的方向一个词一个词地移动，进行转置操作可以将seq_length放到最后。# 定义逐位置前向传播网络类 class PoswiseFeedForwardNet(nn.Module): def __init__(self): super(PoswiseFeedForwardNet, self).__init__() # 定义一维卷积层1，用于将输入映射到更高维度 self.conv1 = nn.Conv1d(in_channels=d_embedding, out_channels=2048, kernel_size=1) # 定义一维卷积层2，用于将输入映射回原始维度 self.conv2 = nn.Conv1d(in_channels=2048, out_channels=d_embedding, kernel_size=1) # 定义层归一化 self.layer_norm = nn.LayerNorm(d_embedding) def forward(self, inputs): # inputs: [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- # inputs [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- residual = inputs # 保留残差连接 [batch_size, len_q, embedding_dim] # 在卷积层1后使用ReLU激活函数 [batch_size, embedding_dim, len_q]->[batch_size, 2048, len_q] output = nn.ReLU()(self.conv1(inputs.transpose(1, 2))) #-------------------------维度信息-------------------------------- # output [batch_size, 2048, len_q] #-------------------------维度信息-------------------------------- # 使用卷积层2进行降维 [batch_size, 2048, len_q]->[batch_size, embedding_dim, len_q] output = self.conv2(output).transpose(1, 2) # [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- # output [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- # 与输入进行残差链接，并进行层归一化，[batch_size, len_q, embedding_dim] output = self.layer_norm(output + residual) # [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- # output [batch_size, len_q, embedding_dim] #-------------------------维度信息-------------------------------- return output # 返回加入残差连接后层归一化的结果正弦位置编码表Transformer使用Attention机制实现了网络的并行计算，这大大加快了网络的推理速度，但是也使得数据失去了时间上的先后性（RNN、LSTM本身就具有先后关系），所以需要引入一个位置编码表来融合位置信息。# 生成正弦位置编码表的函数，用于在Transformer中引入位置信息 def get_sin_enc_table(n_position, embedding_dim): #-------------------------维度信息-------------------------------- # n_position: 输入序列的最大长度 # embedding_dim: 词嵌入向量的维度 #----------------------------------------------------------------- # 根据位置和维度信息，初始化正弦位置编码表 sinusoid_table = np.zeros((n_position, embedding_dim)) # 遍历所有位置和维度，计算角度值 for pos_i in range(n_position): for hid_j in range(embedding_dim): angle = pos_i / np.power(10000, 2 * (hid_j // 2) / embedding_dim) sinusoid_table[pos_i, hid_j] = angle # 计算正弦和余弦值 sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i 偶数维 sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1 奇数维 #-------------------------维度信息-------------------------------- # sinusoid_table 的维度是 [n_position, embedding_dim] #----------------------------------------------------------------- return torch.FloatTensor(sinusoid_table) # 返回正弦位置编码表填充注意力掩码一个batch里面会有很多条句子，不同句子的长度可能会不一样，要让这些句子组成一个batch就需要在较短的句子后面填充PAD，使它们有相同的长度，但是这些PAD并不包含任何信息，所以在Transformer计算注意力的时候就需要给这些PAD打上马赛克（替换为一个非常小的数，在softmax步骤会被看成0）# 生成填充注意力掩码的函数，用于在多头自注意力计算中忽略填充部分 def get_attn_pad_mask(seq_q, seq_k): #-------------------------维度信息-------------------------------- # seq_q 的维度是 [batch_size, len_q] # seq_k 的维度是 [batch_size, len_k] #----------------------------------------------------------------- batch_size, len_q = seq_q.size() batch_size, len_k = seq_k.size() # 生成布尔类型张量[batch_size，1，len_k(=len_q)] pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) #<PAD> Token的编码值为0 # 变形为何注意力分数相同形状的张量 [batch_size，len_q，len_k] pad_attn_mask = pad_attn_mask.expand(batch_size, len_q, len_k) #-------------------------维度信息-------------------------------- # pad_attn_mask 的维度是 [batch_size，len_q，len_k] #----------------------------------------------------------------- return pad_attn_mask # [batch_size，len_q，len_k]编码器层一个多头自注意力+一个前馈神经网络，非常简单# 定义编码器层类 class EncoderLayer(nn.Module): def __init__(self): super(EncoderLayer, self).__init__() self.enc_self_attn = MultiHeadAttention() #多头自注意力层 self.pos_ffn = PoswiseFeedForwardNet() # 位置前馈神经网络层 def forward(self, enc_inputs, enc_self_attn_mask): #-------------------------维度信息-------------------------------- # enc_inputs 的维度是 [batch_size, seq_len, embedding_dim] # enc_self_attn_mask 的维度是 [batch_size, seq_len, seq_len] #----------------------------------------------------------------- # 将相同的Q，K，V输入多头自注意力层 enc_outputs, attn_weights = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask) # 将多头自注意力outputs输入位置前馈神经网络层 enc_outputs = self.pos_ffn(enc_outputs) #-------------------------维度信息-------------------------------- # enc_outputs 的维度是 [batch_size, seq_len, embedding_dim] 维度与 enc_inputs 相同 # attn_weights 的维度是 [batch_size, n_heads, seq_len, seq_len] 在注意力掩码维度上增加了头数 #----------------------------------------------------------------- return enc_outputs, attn_weights # 返回编码器输出和每层编码器注意力权重编码器编码器是6层编码器层的叠加，内部会完成位置编码的计算与融合、填充掩码的计算# 定义编码器类 n_layers = 6 # 设置Encoder/Decoder的层数 class Encoder(nn.Module): def __init__(self, corpus): super(Encoder, self).__init__() self.src_emb = nn.Embedding(corpus.src_vocab, d_embedding) # 词嵌入层 self.pos_emb = nn.Embedding.from_pretrained( \ get_sin_enc_table(corpus.src_len+1, d_embedding), freeze=True) # 位置嵌入层 self.layers = nn.ModuleList(EncoderLayer() for _ in range(n_layers))# 编码器层数 def forward(self, enc_inputs): #-------------------------维度信息-------------------------------- # enc_inputs 的维度是 [batch_size, source_len] #----------------------------------------------------------------- # 创建一个从1到source_len的位置索引序列 pos_indices = torch.arange(1, enc_inputs.size(1) + 1).unsqueeze(0).to(enc_inputs) #-------------------------维度信息-------------------------------- # pos_indices 的维度是 [1, source_len] #----------------------------------------------------------------- # 对输入进行词嵌入和位置嵌入相加 [batch_size, source_len，embedding_dim] enc_outputs = self.src_emb(enc_inputs) + self.pos_emb(pos_indices) #-------------------------维度信息-------------------------------- # enc_outputs 的维度是 [batch_size, seq_len, embedding_dim] #----------------------------------------------------------------- enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs) # 生成自注意力掩码 #-------------------------维度信息-------------------------------- # enc_self_attn_mask 的维度是 [batch_size, len_q, len_k] #----------------------------------------------------------------- enc_self_attn_weights = [] # 初始化 enc_self_attn_weights # 通过编码器层 [batch_size, seq_len, embedding_dim] for layer in self.layers: enc_outputs, enc_self_attn_weight = layer(enc_outputs, enc_self_attn_mask) enc_self_attn_weights.append(enc_self_attn_weight) #-------------------------维度信息-------------------------------- # enc_outputs 的维度是 [batch_size, seq_len, embedding_dim] 维度与 enc_inputs 相同 # enc_self_attn_weights 是一个列表，每个元素的维度是[batch_size, n_heads, seq_len, seq_len] #----------------------------------------------------------------- return enc_outputs, enc_self_attn_weights # 返回编码器输出和编码器注意力权重后续注意力掩码在模型实际使用的时候，Transformer解码器和RNN/LSTM一样，接收到来自编码器的上下文向量后，从一个sos起始符开始输出文本，每输出一个词，这个词会作为输入重新输入到解码器中，输出下一个词，直到终止符eos出现，这个过程可以叫贪心迭代算法。模型每次输出的句子都会比上一次输出的长一个单词，除了这个多出来的单词外别的地方都一样，如果我们把模型每次输出的句子放在一起，将相同内容对齐，可以得到一个直角三角形，这个三角形里的内容可以看成是训练时模型的已知信息，用来teacher forcing。在训练模型的时候，Transformer为了进行并行计算并不会一次一次迭代，而是会将模型正确迭代时输入的内容作为teacher forcing喂进去，在解码器对输入进行自注意力计算时使用一个倒三角的注意力掩码，将模型未知的，或者说现阶段模型不应该知道的信息码住。# 生成后续注意力掩码的函数，用于在多头自注意力计算中忽略未来信息 def get_attn_subsequent_mask(seq): #-------------------------维度信息-------------------------------- # seq 的维度是 [batch_size, seq_len(Q)=seq_len(K)] #----------------------------------------------------------------- attn_shape = [seq.size(0), seq.size(1), seq.size(1)] # 获取输入序列的形状 #-------------------------维度信息-------------------------------- # attn_shape是一个一维张量 [batch_size, seq_len(Q), seq_len(K)] #----------------------------------------------------------------- # 使用numpy创建一个上三角矩阵（triu = triangle upper） subsequent_mask = np.triu(np.ones(attn_shape), k=1) #-------------------------维度信息-------------------------------- # subsequent_mask 的维度是 [batch_size, seq_len(Q), seq_len(K)] #----------------------------------------------------------------- # 将numpy数组转换为PyTorch张量，并将数据类型设置为byte（布尔值） subsequent_mask = torch.from_numpy(subsequent_mask).byte() #-------------------------维度信息-------------------------------- # 返回的subsequent_mask 的维度是 [batch_size, seq_len(Q), seq_len(K)] #----------------------------------------------------------------- return subsequent_mask # 返回后续位置的注意力掩码解码器层与编码器层基本同理# 定义解码器层类 class DecoderLayer(nn.Module): def __init__(self): super(DecoderLayer, self).__init__() self.dec_self_attn = MultiHeadAttention() # 多头自注意力层 self.dec_enc_attn = MultiHeadAttention() # 多头注意力层，连接编码器和解码器 self.pos_ffn = PoswiseFeedForwardNet() # 位置前馈神经网络层 def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask): #-------------------------维度信息-------------------------------- # dec_inputs 的维度是 [batch_size, target_len, embedding_dim] # enc_outputs 的维度是 [batch_size, source_len, embedding_dim] # dec_self_attn_mask 的维度是 [batch_size, target_len, target_len] # dec_enc_attn_mask 的维度是 [batch_size, target_len, source_len] #----------------------------------------------------------------- # 将相同的Q，K，V输入多头自注意力层 dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs, dec_self_attn_mask) #-------------------------维度信息-------------------------------- # dec_outputs 的维度是 [batch_size, target_len, embedding_dim] # dec_self_attn 的维度是 [batch_size, n_heads, target_len, target_len] #----------------------------------------------------------------- # 将解码器输出和编码器输出输入多头注意力层 dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs, dec_enc_attn_mask) #-------------------------维度信息-------------------------------- # dec_outputs 的维度是 [batch_size, target_len, embedding_dim] # dec_enc_attn 的维度是 [batch_size, n_heads, target_len, source_len] #----------------------------------------------------------------- # 输入位置前馈神经网络层 dec_outputs = self.pos_ffn(dec_outputs) #-------------------------维度信息-------------------------------- # dec_outputs 的维度是 [batch_size, target_len, embedding_dim] # dec_self_attn 的维度是 [batch_size, n_heads, target_len, target_len] # dec_enc_attn 的维度是 [batch_size, n_heads, target_len, source_len] #----------------------------------------------------------------- # 返回解码器层输出，每层的自注意力和解-编编码器注意力权重 return dec_outputs, dec_self_attn, dec_enc_attn解码器与编码器接近，要注意的是，编码器最终的输出会输入到全部的6层解码器中# 定义解码器类 n_layers = 6 # 设置Decoder的层数 class Decoder(nn.Module): def __init__(self, corpus): super(Decoder, self).__init__() self.tgt_emb = nn.Embedding(corpus.tgt_vocab, d_embedding) # 词嵌入层 self.pos_emb = nn.Embedding.from_pretrained( \ get_sin_enc_table(corpus.tgt_len+1, d_embedding), freeze=True) # 位置嵌入层 self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)]) # 叠加多层 def forward(self, dec_inputs, enc_inputs, enc_outputs): #-------------------------维度信息-------------------------------- # dec_inputs 的维度是 [batch_size, target_len] # enc_inputs 的维度是 [batch_size, source_len] # enc_outputs 的维度是 [batch_size, source_len, embedding_dim] #----------------------------------------------------------------- # 创建一个从1到source_len的位置索引序列 pos_indices = torch.arange(1, dec_inputs.size(1) + 1).unsqueeze(0).to(dec_inputs) #-------------------------维度信息-------------------------------- # pos_indices 的维度是 [1, target_len] #----------------------------------------------------------------- # 对输入进行词嵌入和位置嵌入相加 dec_outputs = self.tgt_emb(dec_inputs) + self.pos_emb(pos_indices) #-------------------------维度信息-------------------------------- # dec_outputs 的维度是 [batch_size, target_len, embedding_dim] #----------------------------------------------------------------- # 生成解码器自注意力掩码和解码器-编码器注意力掩码 dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs) # 填充位掩码 dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs) # 后续位掩码 dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask.to(device) \ + dec_self_attn_subsequent_mask.to(device)), 0) dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs) # 解码器-编码器掩码 #-------------------------维度信息-------------------------------- # dec_self_attn_pad_mask 的维度是 [batch_size, target_len, target_len] # dec_self_attn_subsequent_mask 的维度是 [batch_size, target_len, target_len] # dec_self_attn_mask 的维度是 [batch_size, target_len, target_len] # dec_enc_attn_mask 的维度是 [batch_size, target_len, source_len] #----------------------------------------------------------------- dec_self_attns, dec_enc_attns = [], [] # 初始化 dec_self_attns, dec_enc_attns # 通过解码器层 [batch_size, seq_len, embedding_dim] for layer in self.layers: dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask) dec_self_attns.append(dec_self_attn) dec_enc_attns.append(dec_enc_attn) #-------------------------维度信息-------------------------------- # dec_outputs 的维度是 [batch_size, target_len, embedding_dim] # dec_self_attns 是一个列表，每个元素的维度是 [batch_size, n_heads, target_len, target_len] # dec_enc_attns 是一个列表，每个元素的维度是 [batch_size, n_heads, target_len, source_len] #----------------------------------------------------------------- # 返回解码器输出，解码器自注意力和解-编编码器注意力权重 return dec_outputs, dec_self_attns, dec_enc_attns大的来了:Transformer经过拆解后的Transformer非常简单# 定义Transformer模型 class Transformer(nn.Module): def __init__(self, corpus): super(Transformer, self).__init__() self.encoder = Encoder(corpus) # 初始化编码器实例 self.decoder = Decoder(corpus) # 初始化解码器实例 # 定义线性投影层，将解码器输出转换为目标词汇表大小的概率分布 self.projection = nn.Linear(d_embedding, corpus.tgt_vocab, bias=False) def forward(self, enc_inputs, dec_inputs): #-------------------------维度信息-------------------------------- # enc_inputs 的维度是 [batch_size, source_seq_len] # dec_inputs 的维度是 [batch_size, target_seq_len] #----------------------------------------------------------------- # 将输入传递给编码器，并获取编码器输出和自注意力权重 enc_outputs, enc_self_attns = self.encoder(enc_inputs) #-------------------------维度信息-------------------------------- # enc_outputs 的维度是 [batch_size, source_len, embedding_dim] # enc_self_attns 是一个列表，每个元素的维度是 [batch_size, n_heads, src_seq_len, src_seq_len] #----------------------------------------------------------------- # 将编码器输出、解码器输入和编码器输入传递给解码器 # 获取解码器输出、解码器自注意力权重和编码器-解码器注意力权重 dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs) #-------------------------维度信息-------------------------------- # dec_outputs 的维度是 [batch_size, target_len, embedding_dim] # dec_self_attns 是一个列表，每个元素的维度是 [batch_size, n_heads, tgt_seq_len, src_seq_len] # dec_enc_attns 是一个列表，每个元素的维度是 [batch_size, n_heads, tgt_seq_len, src_seq_len] #----------------------------------------------------------------- # 将解码器输出传递给投影层，生成目标词汇表大小的概率分布 dec_logits = self.projection(dec_outputs) #-------------------------维度信息-------------------------------- # dec_logits 的维度是 [batch_size, tgt_seq_len, tgt_vocab_size] #----------------------------------------------------------------- # 返回逻辑值(原始预测结果),编码器自注意力权重，解码器自注意力权重，解-编码器注意力权重 return dec_logits, enc_self_attns, dec_self_attns, dec_enc_attnsTransformer机器翻译深蓝学院 生成式预训练语言模型 第七章作业训练训练数据集构建数据集用的是是深蓝学院给的课程资料，这里就不放出来了;导入的CorpusLoader是黄佳老师提供的一个工具，可以通过它快速地构建一个词表，需要注意的是：每次运行构建的词表是不一样的，所以我们需要把保存两个dic，部署预训练模型的时候导入from CorpusLoader import CorpusLoader from torch.utils.data import DataLoader import torch.nn as nn corpus_loader = CorpusLoader('all_sentences.txt') corpus_loader.process_sentences() corpus_loader.build_vocab() # 保存词表 import pickle with open("corpus_loader.word2idx_cn.pkl", "wb") as tf: pickle.dump(corpus_loader.word2idx_cn,tf) with open("corpus_loader.idx2word_en.pkl", "wb") as tf: pickle.dump(corpus_loader.idx2word_en,tf) def collate_fn(batch): batch.sort(key=lambda x: len(x[0]), reverse=True) sentence_cn, sentence_en_in, sentence_en_out = zip(*batch) sentence_cn = nn.utils.rnn.pad_sequence(sentence_cn, padding_value=corpus_loader.word2idx_cn['<pad>'],batch_first=True) sentence_en_in = nn.utils.rnn.pad_sequence(sentence_en_in, padding_value=corpus_loader.word2idx_en['<pad>'],batch_first=True) sentence_en_out = nn.utils.rnn.pad_sequence(sentence_en_out, padding_value=corpus_loader.word2idx_en['<pad>'],batch_first=True) return sentence_cn, sentence_en_in, sentence_en_out dataset = corpus_loader.create_dataset() dataloader = DataLoader(dataset, batch_size=16, shuffle=True, collate_fn=collate_fn)训练循环Transformer还真挺大的，搁我自己的电脑上跑太慢了，我去网上租了一个3080服务器跑了50个循环，用了差不多4个小时import torch # 导入torch import torch.optim as optim # 导入优化器 from Transformer_Model import Transformer device = "cuda" if torch.cuda.is_available() else "cpu" # 创建模型实例并选择优化器 model = Transformer(corpus_loader).to(device) model.train() criterion = torch.nn.CrossEntropyLoss(ignore_index=corpus_loader.word2idx_en['<pad>']) # 忽略padding的损失 optimizer = optim.Adam(model.parameters(), lr=0.0001) # 进行训练 loss_cache = 9999 for epoch in range(50): for i, (enc_inputs, dec_inputs, target_batch) in enumerate(dataloader): enc_inputs, dec_inputs, target_batch = enc_inputs.to(device), dec_inputs.to(device), target_batch.to(device) optimizer.zero_grad() outputs, _, _, _ = model(enc_inputs, dec_inputs) loss = criterion(outputs.view(-1, outputs.size(-1)), target_batch.view(-1)) loss.backward() optimizer.step() if (i+1) % 100 == 0: print(f"Epoch[{epoch+1}/50], Step[{i+1}/{len(dataloader)}], Loss: {loss.item()}") if (epoch + 1) % 1 == 0: # 打印损失并保存模型参数 print(f"Epoch: {epoch + 1:04d} cost = {loss:.6f}") if float(loss.item()) < loss_cache: print("保存模型") loss_cache = loss torch.save(model.state_dict(), 'transformer.pth')模型部署import torch # 导入torch from CorpusLoader import CorpusLoader corpus_loader = CorpusLoader('all_sentences.txt') corpus_loader.process_sentences() corpus_loader.build_vocab() # 导入词表 import pickle with open("corpus_loader.idx2word_en.pkl", "rb") as tf: idx2word_en = pickle.load(tf) with open("corpus_loader.word2idx_cn.pkl", "rb") as tf: word2idx_cn = pickle.load(tf) from Transformer_Model import Transformer device = "cuda" if torch.cuda.is_available() else "cpu" model = Transformer(corpus_loader).to(device) model.load_state_dict(torch.load('transformer.pth')) model.eval() # 贪心算法迭代函数 def greedy_decoder(model, enc_inputs, start_symbol): # 将输入传递给编码器，并获取编码器输出和自注意力权重 enc_outputs, enc_self_attns = model.encoder(enc_inputs) dec_input = torch.zeros(1,1).type_as(enc_inputs.data) dec_input[0][0] = start_symbol for i in range(100):# 限制模型的最长输出为99词 # 将解码器输入、编码器输入输出传递给解码器，获取解码器输出、自注意力权重和编-解码器注意力权重 dec_outputs, _, _ = model.decoder(dec_input, enc_inputs, enc_outputs) # 将解码器输出传递给投影层，生成目标词汇表大小的概率分布 predict = model.projection(dec_outputs) # 找到概率最大的下一个词 prob = predict.squeeze(0).max(dim=-1, keepdim=False)[1][-1] # 拼接到dec_input dec_input_cache = torch.zeros(1,1).type_as(enc_inputs.data) dec_input_cache[0][0] = prob dec_input = torch.cat((dec_input,dec_input_cache), dim=1) if int(prob) == 0:# 如果输出为eos，中断循环 break return dec_input import jieba while True: sentence_cn = input("翻译器输入: ") sentence_cn = ' '.join(jieba.cut(sentence_cn.strip(), cut_all=False)).split(" ")# 使用jieba分词 sentence_idx = [word2idx_cn[i] for i in sentence_cn] sentence_idx = torch.tensor(sentence_idx).to(device).unsqueeze(0) enc_inputs = sentence_idx greedy_dec_output = greedy_decoder(model, enc_inputs, start_symbol=1)# 获得贪婪解码器输入 greedy_dec_output_words = [idx2word_en[int(i)] for i in greedy_dec_output.squeeze()]# 转换为单词序列 print(" ".join(greedy_dec_output_words[1:-1])) print("")这时候模型就可以跑了，不知道为什么翻译的效果不是特别好，如果输入的中文句子稍微长一点模型还算能说出人话，但是如果稍微短一些（如下图所示），这个Transformer自己在没话找话BLEU得分重新修改训练部分的代码，这一次在每训练一轮后将BLEU得分用tensorboard记录，考虑到之前训练50轮的效果不是很好，这次训练时间长一点，同时把loss也用tensorboard记录一下# 计算BLEU得分 from torchtext.data.metrics import bleu_score def compute_bleu(model, dataset): model.eval() total_score = 0.0 with torch.no_grad(): for i in range(dataset.__len__()): sentence_cn, _, sentence_en_out = dataset.__getitem__(i) sentence_cn = torch.tensor(sentence_cn).to(device).unsqueeze(0) sentence_en_out = [corpus_loader.idx2word_en[int(i)] for i in sentence_en_out] #迭代推理 dec_input = greedy_decoder(model, sentence_cn, start_symbol=1) greedy_dec_output_words = [corpus_loader.idx2word_en[int(i)] for i in dec_input.squeeze()]# 转换为单词序列 candidate_corpus = [greedy_dec_output_words[1:]] references_corpus = sentence_en_out # 计算BLEU得分 total_score += bleu_score(candidate_corpus, references_corpus) #print(i) if i > 500:# 全部算一遍太慢了，这里就只针对前500个左右算吧 break return total_score / 502在我斥巨资算了14个半小时后，模型得分和loss如下：