[{"content":"近日，出于笔者对高质量LLM的需求，引发了对高峰期仍能保持稳定的代理、纯净度较高的IP地址的需要。所以笔者决定不再用机场服务作主力，转向租用海外VPS做个人自用代理。\n折腾的两个月以来，笔者遇到了VPS被恶意攻击、IP地址被封禁等问题。与此同时，网络上，比如YouTube的视频教程大多都极简，只能保证用户成功搭建起入站、客户端，但是既缺少对VPS本身安全性的保护教程，也容易使用户“知其然而不知其所以然”。但也不乏新协议、技术相关的内容，比如使用XHTTP协议使被封IP的VPS重焕生机、BBRv3的介绍等\u0026hellip;这些是笔者所欠缺的知识。\n综上，笔者将从自身的折腾经验出发，分享如何从零开始配置VPS，主要内容如下：\nVPS 选购 安全性配置 3x-ui 安装 入站与客户端配置 clash 订阅配置 CloudFlare Tunnel 客户端导入订阅 参考网站：\nNodeSeek 论坛 MHSanaei/3x-ui VPS选购 这篇博客并非广告，VPS 提供商的来源为 NodeSeek 以及 LLM 的输出（多为ChatGPT）。笔者在选购的时候，主要是找带有三网优化（TRI）的 VPS 服务，比如BandwagonHost、DMIT、GigsGigsCloud、Vmiss和Hostdare等一系列 VPS 提供商，它们的价格、地区均有差异。作为中国用户，首先要关注的就是 VPS 的线路，大致有以下几种：\nTRI 在 VPS 商家语境里通常是 Tri-network optimized 的缩写，即“三网优化”。它不是某个运营商的正式线路名称，而是套餐命名方式。一般表示电信、联通、移动三家分别尽量走较优路径，例如电信走 CN2 GIA，联通走 9929 或 4837 优化，移动走 CMI/CMIN2。 CN2 常见于中国电信方向，全称通常理解为 China Telecom Next Generation Carrier Network，常见分类有 CN2 GT 和 CN2 GIA。GIA，即 Global Internet Access，通常比 GT 更高端，价格更贵，低峰和晚高峰的稳定性也通常更好。 9929 通常指中国联通 AS9929，也常被称为联通精品网、CUII，是一类走联通优质国际链路的线路标识。相比联通普通骨干网 AS4837，9929 一般更适合对联通用户优化，常见于香港、日本、美国西海岸等面向中国大陆优化的 VPS。 4837 通常指中国联通普通骨干网 AS4837，也就是 China169 Backbone。它覆盖面广、成本相对低，但国际方向在高峰期更容易受到拥塞影响。 CMI 通常指中国移动国际线路，常见 AS 号是 AS58453；CMIN2 则是近年 VPS 圈里常见的移动高端线路说法，常与 AS58807 相关。对于中国移动用户，CMI/CMIN2 的体验往往比绕路国际 BGP 更稳定。 不同的 VPS 的服务详情，节点速率、延迟等数据，均可以通过搜索引擎查阅到，笔者不多赘述。 此外，两个月前笔者挑选 VPS 时，就有优质线路服务短缺的问题，比如 DMIT，笔者观察了许久，时至今日大多产品也都是\u0026quot;Out of stock\u0026quot;的状态，其它的提供商也是同理（除了配置比较豪华的服务）。现在看来当时能搞到一台 VPS 用到现在，大概是运气好吧。\n下面，当成功付款后，会被分配到一台 VPS 供我们折腾，操作系统的选择个人认为区别大不，管理界面大致如下：\n通常这里会有最为重要的，VPS 的 IP 信息，以及一些基本的管理功能。\n安全性配置 开机后，第一步就是基本的安全性配置，避免被恶意攻击。 笔者在前几天遭遇了一次，原因是因为偷懒没有禁用密码登录，fail2ban的规则也没设置好，于是在被攻击了长达1个小时后，IP也被封了（可能是巧合）。这还是 IP 被封了以后，发现 CPU 利用率在一段时间内竟是 100% 发现的。\n首先，通过 SSH 和提供的最初的 root 账户连接到 VPS 上（使用提供商的 VNC console 也可以），执行下面的命令进行软件的更新，以及必要的 ufw 和 fail2ban 的安装：\n1 2 apt update \u0026amp;\u0026amp; apt full-upgrade -y apt install -y sudo curl wget vim git ufw fail2ban SSH登录 出于历史教训，应尽早关闭密码登录，仅保留 SSH 密钥登录方法。笔者是用了 VPS 官方控制台的 openSSH，它会自动生成好密钥文件到 VPS 中，并关闭密码登录，我需要做的只是把密钥文件配置到客户端的机器上，以便于访问。\n切换到客户端机器的用户目录下，找到（或者创建）.ssh 目录，存放生成的密钥文件，文件名随意。 之后再创建（或编辑）一个名为 config 的文件，需要新增内容样式如下：\n1 2 3 4 5 Host vps HostName 1.2.3.4 User root IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes 字段解释如下：\n1 2 3 4 5 Host : 本地别名（以后 ssh vps） HostName : VPS 的 IP 或域名 User : 登录用户名（root / deploy） IdentityFile : 私钥路径 IdentitiesOnly : 强制只用该密钥，避免 SSH 自动尝试其他 key 如果要自己生成密钥文件的话，那么顺序不同，且 VPS 上也要进行对应操作。首先，在客户端机器上生成一个随机密钥：\n1 ssh-keygen -t ed25519 把密钥复制到 VPS 上：\n1 2 3 4 mkdir -p ~/.ssh nano ~/.ssh/authorized_keys # 把生成好的密钥复制进去 chmod 600 ~/.ssh/authorized_keys chmod 700 ~/.ssh ","date":"2026-06-30T15:17:20+08:00","permalink":"/p/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E9%85%8D%E7%BD%AEvps/","title":"从零开始配置VPS"},{"content":"托福写作 题型 分值 Build a Sentence 10 Write an Email 5 Academic Discussion 5 自2026.1.21托福改革后，“综合写作”取消，“学术写作”保留，新增了“造句”和“邮件”两种新题型。下面的内容，均总结自B站Vince9120的视频\n2026新托福写作：Build a Sentence 句子排序 句子重组 造句 组句\nBuild A Sentence 共10道题，需在6分50秒之内完成答题。\n分析情景 题目首先有一句情景句来模拟对话的场景。比如：The printer in the library keeps jamming. What should I do?\n下面是需要填写的单词和空格，需要把单词按正确的顺序拖拽到空格中完成情景中的回答。我们作答的部分可能会有已有的提示词，也可能没有，即全是空格。此外，可选词的数量也可能比空格的数量更多。比如： ___ ___ ___ ___ ___ ___ ___ right now.\n需要填的词有：could, ask, for, help, you, IT service desk, the\n那么这题干的情景句，有以下三种类型：\n特殊疑问句 (Wh- Questions): Why didn\u0026rsquo;t you attend your morning class? 一般疑问句 (Yes/No Questions): Are you planning to attend the conference next month? 陈述句 / 信息分享 (Statements):The university\u0026rsquo;s conference on climate change was very informative. 那么我们可以按照下面的顺序作答：\n确定语序：根据题干情景句的类型，可以确认回答句应该是陈述句或者疑问句，从而确定语序。 确定主语：根据可选词的词性，结合题干，从而确定主语 确定时态 / 确定意思 锚定主句 基本上，主句分为：主谓 / 主谓宾 / 主系表 这三种句式。 那么我们可以按照下面的顺序作答：\n确定主语 确定谓语 确定宾语 / 表语 主语： 类别 示例内容 名词 (Nouns) The manager\nThe budget reports\nStudents 代词 (Pronouns) I, you, he/she/it, they, we, this/that.\nme, you, him/her/it, them, us, this/that / myself Doing / To do [ Reading books ] is fun.\n[ To learn English ] takes time.\nI like [ to learn English ]\nI like [ learning English ] 名词性从句 Noun Clauses [ What he said ] made me angry.\nI don\u0026rsquo;t understand [ What he said ] 谓语： 类别 结构/形式 时态 do/does\nbe doing\ndid\nwill do / be going to do\nhave done / had done 情态 can / could / may / might / shall / should + do 语态 be done be动词单独出现 is/am/are/was/were 例题：\nDid you enjoy the student film festival? ___ ___ ___ ___ ___ ___ me. movies, to, the, none of, were, of interest\n按照词性判断，主语只能是 movies, 谓语只能是 were, 表语只能是 of interest。 那么答案应该是：None of movies were of interest to me. 定位从句 从句类型有名词性从句、定语从句、状语从句（不常考），那么我们可以按照下面的顺序作答：\n确定从句类型 确定从句连词 确定从句语序 确定从句时态 从句类型 引导词/连接词 名词性从句 if / whether\nthat\nwhat, where, when, why, who, how\nwhatever, wherever, whenever, however, whoever 定语从句 that/which/who/whom/where/when/why 状语从句 because/since/when/while/if \u0026hellip; 大致可以这么判断：\nknow, tell, ask, see, wonder + 宾语从句\n介词 (in/on/at\u0026hellip;) + 宾语从句\n名词 + 定语从句\n句尾/句首 - 状语从句\n名词性从句：\n句子结构情况 引导词 功能/含义 主谓宾完整 that 肯定事实 If / whether 不确定/是否 when, where, why, how 补充背景信息 缺少主语/宾语/表语 what, who, which+名词, whose+名词 指代某人某事 定语从句： 名词（先行词） 连词 从句内部结构 人 (Person) who / that (宾语可用 whom) 缺主语 / 宾语/表语 物 (Thing) which / that 地点 (Place) where = in/at which 主谓宾完美无缺 时间 (Time) when = on/in which 原因 (reason) why = for which 如何区分 if 引导的从句类型： 类型 特征与规则 示例 名词性从句\n(当宾语) - 前面有 know, ask, wonder 等\n- if 后跟陈述语序\n- 可用 whether 替换 I wonder if she will come. 状语从句\n(表条件) - 前面是主句，表条件\n- if 后跟完整主谓宾\n- 不能用 whether 替换 If she comes, I will tell you. 前面的动词是 know / ask / wonder / tell\u0026hellip; → if 就是引导宾语从句\n例题：\nMy sister ___ ___ ___ ___ ___ ___ can, suggest, one, that, you, might interest, be\n先确定主句：My sister can suggest one \u0026hellip; one这里是代词，代指一本书，后面可以加定语从句。剩下的词当中，you的位置比较重要，应该是放在 interest 之后作宾语。若放在从句主语的位置则语义不通。 答案应该是：My sister can suggest one that might interest you.\n检查修饰语 那么我们可以按照下面的顺序作答：\n检查定语 检查状语 定语 分类 内容 / 示例 限定词 a/an/the\nthis/my 数量词 one/two\nfirst/second\nnumerous 介词短语 the impact of \u0026hellip; doing / done / to do 短语 Information stored on computers\nman sitting under the tree\nthe best way to prevent this 形容词短语 Materials suitable for construction 名词/形容词 climate change doing/done单独 increasing pressure 定语从句 The technology that was developed 句中状语 频度 / 程度 / 否定副词的位置 永远在实义动词前 或 be动词后。\n副词类型 常用词 位置规律 示例 频度副词 always / usually / often / sometimes / never 实义动词前，系动词后 He always arrives on time.\nShe is usually busy. 程度副词 totally / almost / nearly / quite 助动词/be 后 + 实义动词前 I totally agree.\nThey have almost finished. 否定副词 hardly / barely / rarely / seldom 助动词/be 后 + 实义动词前 I can hardly hear you. 句末状语 副词 / 介词短语 / to do / 状语从句 多个句末状语共存时，一般顺序为：方式、地点、时间。\n状语类型 功能 示例 副词 (方式) 说明动作方式 He solved the problem easily.\nShe goes to school by bus. 介词短语 地点 / 时间 / 方式 They are playing in the park.\nThe meeting starts at 3 PM. to do 短语 目的 / 结果 I went to the store to buy milk.\nShe studied hard to pass the exam. 状语从句 时间 / 条件 / 原因等 I will call you when I arrive.\nHe speaks so fast that I can\u0026rsquo;t catch him. 例题：\nWhy didn\u0026rsquo;t you attend the campus fair last night? We went to the restaurant that (opened downtown recently) / (recently opened downtown). 可见副词 \u0026ldquo;recently\u0026rdquo; 前置或者后置，语法都是正确的，前置修饰 \u0026ldquo;opened\u0026rdquo; 后置修饰 \u0026ldquo;went\u0026rdquo; 或者 \u0026ldquo;opened\u0026rdquo;。 但是关注语境可以发现，问题问 \u0026ldquo;last night\u0026rdquo; 那么回答中，\u0026ldquo;recently\u0026rdquo; 后置就会在语义上奇怪。\nWhy did the university administration email us about our upcoming conference? They wanted ___ ___ ___ ___ ___. confirmed so, far, to know, which speakers, have been \u0026ldquo;so far\u0026rdquo; 连起来作为时间可以放到句末，\u0026ldquo;wanted\u0026rdquo; 后跟 \u0026ldquo;to know\u0026rdquo;。\nAcademic Discussion Intro Questions Types Evaluation Comparison Solution Argument Topics Education Work Environment Tech Urban Development Economy Social Issues Steps Topic 120-180 words of relevant discussion with reasoning, example, and preferably concession. Coherence Clear topic, flow of idea, and connection between sentences Language Various, accurate, and natural Grammar A mix of simple and complex sentences, preferably with attributive clauses and participles Prompt 例文： From my perspective, social media companies should regulate their platforms themselves rather than governments since it is the most effective way to build up tailored regulation systems.\nTo be more specific, social media companies are familiar with the environment of the platforms and have a better understanding of which areas may present potential issues. Therefore, when it comes to bad content from the users, these companies could react on it quickly, minimizing the negative impact.\nFor example, social media companies like Reddit have dedicated departments to monitor different parts of platforms real-time, which enables the rules to be obeyed meticulously. Moreover, these companies can analyze such negative user behaviors and then refine their regulation systems, creating tailored approaches.\nStill, some might claim that allowing the companies to manage themselves lacks fairness, as different companies may set varying standards. However, governments can provide broad guidelines to ensure that these platforms align with public interests, while also allow businesses some flexibility to implement specific measures based on their unique platforms and users.\nStructure Structure 1: Point \u0026amp; Elaboration + Further Elaboration + Example + Concession Structure 2 : Point \u0026amp; Elaboration + Further Elaboration +Second Point \u0026amp; Elaboration + Further Elaboration Structure 3 : Point \u0026amp; Elaboration + Further Elaboration + Full Concession Structure 4 : Point \u0026amp; Elaboration + Further Elaboration + Sublimation Comparison Which approach do you think is better: assessing students based only on their performance, OR also based on their effort? One proposal is to… The other is to… Which proposal do you think is better? Why? What is the best way to evaluate teachers: Students’ test scores? Students’ feedback? Other teachers’ evaluation? Which is the best and why? Evaluation What is your opinion on gap year? Does it bring more advantages or disadvantages to students? Do you think it\u0026rsquo;s a good idea for very young children to play educational computer games? Why or why not? Is journal writing a worthwhile activity to support learning? Why or why not? Argument Do you think ongoing job training is the most important investment a business can make? Will people have more free time and be able to enjoy more leisure activities in the future? Solution What is the biggest mistake that people make when it comes to the purchase of tech products? In what area should the government reduce its spending for a budget cut? Point \u0026amp; Elaboration Example： Prof. : Which approach do you think is better:\nassessing students based only on their performance; or also based on their effort? Student1 : Effort - learn something, will benefit them in their future life.\nStudent2 : Performance - fair\nCommon Points in Education : Academic development / Effectiveness of study Personal development / Career development Teamworking and communication skills Point Template I believe that … / I firmly hold the idea that … / In my opinion, … / As far as I am concerned, … / It is obvious that …\nPractical experience will greatly promote / hinder students’ academic development as they can deepen their understanding in their field of study. (25) The overuse of technology will have a positive / negative impact on our personal development since it will hinder our independent thinking. (29) We shall definitely prioritize economic development over environmental protection due to the fact that a stable economy is the very foundation of social, scientific, and industrial development. (31) The fact that discussion involves active exchange of ideas between participants indicates that such style of learning drives us to strengthen our communication skills. (24) How to Point I think score is good for students because they learn knowledge.\nI believe grading shall focus on students’ performance on assignments and tests, since it is the most effective way to measure the progress in their study. (27) I believe effort is important because experience is good.\nIn my opinion, effort is an important element to be considered in study and grading due to the fact that students learn from both what they have done right and what they have done wrong. (31) Elaboration Template To be more specific, during a discussion, students not only have to come up with their own thoughts but also consider those from others, which stimulates an active way of thinking. To elaborate, practical experience will help students witness the true nature of a job instead of developing some random imagination based on their limited experience, developing a real passion in their field of study. In other words, it is only in a prosperous, stable, and developed society that we can allocate extra resources to implement the protection of the environment. More specifically, instead of thinking and analyzing a problem, people might rely on technology to search for quick answers to every question they have in life and study. How to Elaborate I believe effort is important because experience is good. As far as I am concerned, the fact that students learn from both good and bad results in study indicates that effort is an important element to be considered in study and grading. (33) To elaborate, students not only gain academic knowledge from a correct understanding of class materials, but also obtain a stronger resilience and judgment power through their mistakes, developing both technical and life skills. (35) More Examples Do you think ongoing job training is the most important investment a business can make?\nIt is certain that ongoing job training is an essential part of business because it helps employees to stay competitive in today’s rapidly changing world. (27) To be more specific, instead of relying on outdated skillsets, ongoing job training not only provides employees an insight into advanced strategies but also equip them with competitive technical skills, which in turn helps the company to keep vibrant. (39) What is the biggest mistake that people make when it comes to purchase of tech products?\nThe biggest mistake in the purchase of tech products is that consumers almost always pay for what they do not need since people are easily misled by advertisements. (28) To elaborate, TV and internet commercials usually illustrate the most sophisticated and powerful functions of high-tech products in an exaggerated manner, which are unfortunately rarely needed. (26) Example Substantiation Focused on topic Factual illustration Good language How to make a Substantiation To be more specific, performance on assignments and tests best reflect students’ understanding of academic concepts and their problem-solving skills, which are also the objective of most courses. (29)\nFor example, the score on a mathematics test indicates if the student has completely understood formulas and theories, and if they can apply them in various conditions. (27) Electronic devices help students on their study.\nFor example, students can find different ways of thinking for whatever questions they have with their mobile phones wherever they are, greatly broadening their mind. (25) Story Telling Focused on topic Vivid with relevant details Good language How to Tell a Story Electronic devices help students on their study.\nFor instance, last week, my little niece had a homework. She used her mobile phone to search for information on the internet. Finally, she got a good grade. For instance, last week, my little niece had a homework from her biology class. She used her mobile phone to search for diverse information on the internet. Finally, she got a good grade for the excellent work. For instance, last week, my little niece had a homework from her biology class about animal behaviors. Instead of relying on limited knowledge on books, she used her mobile phone to search for diverse information on the internet, including why and how animals play, which earned her a good grade for the excellent work. (54) What is the biggest mistake that people make when it comes to the purchase of tech products?\nThe biggest mistake in the purchase of tech products is that consumers almost always pay for what they do not need since people are easily misled by advertisements. (28) To elaborate, TV and internet commercials usually illustrate the most sophisticated and powerful functions of high-tech products in an exaggerated manner, which are unfortunately rarely needed. (26) For example, commercials may demonstrate how a powerful laptop may solve all problems in our work and life, while we end up just using the expensive laptop with basic jobs such as editing texts and PowerPoint slides. (37) Concession Concede with students\u0026rsquo; point or a potential advantage. Refute with an obvious weakness. Admitting that another person is right about something. Proving that someone else is wrong about something. How to make a Concession Do you think ongoing job training is the most important investment a business can make? I don\u0026rsquo;t think so. Training takes a lot of money, and companies can hire new skilled employees. Still, a voice arises that businesses can save money from training by recruiting new employees. Ironically, even the most talented and skilled new employees will need time and training to best adapt themselves to a new environment, which is a lot of times more expensive than ongoing job training. (15+34) What is the biggest mistake that people make when it comes to the purchase of tech products? They often don\u0026rsquo;t consider the compatibility of software. The software we are used to might not work on the new device. It can be troublesome to learn new software. Nevertheless, some state that the biggest mistake is that the software we are familiar with might not be compatible with the new tech products. However, with the development of computer science, software today can usually be used on multiple platforms. For example, PowerPoint can be used on both PC and Mac, and even on mobile devices. (24+32) AI Prompt （转自CSDN 【TOEFL Prompt】1 学术讨论评分Prompt_prompt professor-CSDN博客）\n角色 你是一位经验丰富的托福写作评分员，能够根据题目和回答，给出和ETS官方一致的分数。\n任务 下面请你对学术讨论写作任务进行评分，我将会给出题目材料以及回答，请你根据评分标准，以及过往的评分示例对我的回答进行评分，并给出指导意见。 其中，我给出的格式是： [题目要求]： [Professor]： [Student A]： [Student B]： [回答]：\n回答格式 [评分]：xx分（满分30分） [指导意见]：1.xxx； 2.xxx； … [回答思路]： [示例范文]：\n评分标准 Score 5 A fully successful response The response is a relevant and very clearly expressed contribution to the online discussion, and it demonstrates consistent facility in the use of language. A typical response displays the following: •\tRelevant and well-elaborated explanations, exemplifications, and/or details •\tEffective use of a variety syntactic structures and precise, idiomatic word choice •\tAlmost no lexical or grammatical errors other than those expected from a competent writer writing under timed conditions (e.g., common typos or common misspellings or substitutions like there/their) Score 4 A generally successful response The response is a relevant contribution to the online discussion, and facility in the use of language allows the writer’s ideas to be easily understood. A typical response displays the following: •\tRelevant and adequately elaborated explanations, exemplifications, and/or details •\tA variety of syntactic structures and appropriate word choice •\tFew lexical or grammatical errors Score 3 A partially successful response The response is a mostly relevant and mostly understandable contribution to the online discussion, and there is some facility in the use of language. A typical response displays the following: •\tElaboration in which part of an explanation, example, or detail may be missing, unclear, or irrelevant •\tSome variety in syntactic structures and a range of vocabulary •\tSome noticeable lexical and grammatical errors in sentence structure, word form, or use of idiomatic language Score 2 A mostly unsuccessful response The response reflects an attempt to contribute to the online discussion, but limitations in the use of language may make ideas hard to follow. A typical response displays the following: •\tIdeas that may be poorly elaborated or only partially relevant •\tA limited range of syntactic structures and vocabulary •\tAn accumulation of errors in sentence structure, word forms, or use Score 1 An unsuccessful response The response reflects an ineffective attempt to contribute to the online discussion, and limitations in the use of language may prevent the expression of ideas. A typical response may display the following: •\tWords and phrases that indicate an attempt to address the task but with few or no coherent ideas •\tSeverely limited range of syntactic structures and vocabulary •\tSerious and frequent errors in the use of language •\tMinimal original language; any coherent language is mostly borrowed from the stimulus Score 0 The response is blank, rejects the topic, is not in English, is entirely copied from the prompt, is entirely unconnected to the prompt, or consists of arbitrary keystrokes.\n要求\n你必须遵照评分标准以及评分示例，对我的回答进行评分 请一步一步进行思考，给出最接近ETS官方的评分结果 在生成范文阶段，请务必遵照5分的评分标准进行写作 我将支付$1000以获得更好的评分体验 在提出修改意见的阶段，请你按照5分的评分标准进行提出。 分数转换标准 5分换算为30分， 4.75换算为29分， 4.5分换算为28分， 4.25分换算为27分， 4分换算为25分， 3.75分换算为24分， 3.50换算为22分， 3.25分换算为21分， 3分换算为20分， 2.75分换算为18分， 2.50分换算为17分， 2.25分换算为15分， 2分换算为14分， 1.75分换算为12分， 1.5分换算为11分， 1.25分换算为10分， 1分换算为8分。\n生成回答流程 生成评分阶段 每次评分时，你都必须扮演两个评分人，按照评分规则从0-5进行评分，注意你可以根据回答的质量给出包含小数点的分数。之后求取两个评分者的平均数，以获得更加公平的评分，最后根据分数转换标准将5分制的评分转换为30分制。 生成范文阶段 每次你都必须生成三篇范文，每篇范文都必须依照5分A fully successful response的标准，以及具体的[题目要求]进行，最后，请你挑出最好的一篇进行呈现。 之后并呈现你的回答思路\n现在请你根据下面材料进行评分\n[题目要求]：（填充） [Professor]：（填充） [Student A]：（填充） [Student B]：（填充） [回答]：（填充）\n","date":"2026-06-24T11:02:00+08:00","permalink":"/p/toefl-writing/","title":"TOEFL Writing"},{"content":"参考网站 Naver SPLADE 官方仓库 naver/splade_v2_max 模型页 Sentence Transformers Sparse Encoder 文档 Sentence Transformers Sparse Encoder 训练概览 Sentence Transformers Sparse Encoder 推理加速 稀疏向量与 SPLADE 模型 在 RAG 系统里，dense vector 已经是最常见的召回方式。它把文本映射到连续向量空间中，适合捕捉语义相近的表达，例如“员工离职流程”和“人员退出手续”。但 dense vector 也有明显短板：它不一定擅长精确匹配实体、编号、术语、错误码、产品型号、表格字段名和代码片段。\n这时 sparse vector 就有价值了。它更像一个“神经网络增强版倒排索引”：文本仍然被表示为词项维度上的稀疏权重，但这些权重不是 BM25 这类纯统计方法算出来的，而是由模型预测出来的。\n简单说：\ndense vector 负责语义相似。 BM25 负责原词匹配。 SPLADE sparse vector 负责神经词项扩展后的加权匹配。 Hybrid Search 则把 dense 和 sparse 两种召回结果融合起来。 论文模型 SPLADE 基于 Masked Language Model 的 logits，把一段文本映射到词表空间中。假设词表里有 30522 个 WordPiece token，那么每个文本最终可以表示为：\n1 token_id -\u0026gt; weight 也就是一个稀疏向量。大部分 token 的权重为 0，只有少量被模型认为重要的 token 会有非零权重。\n这和普通 embedding 最大的区别是：dense embedding 的每一维通常不可解释，而 sparse vector 的维度就是词表 token。一个被模型激活的 token 可以理解为“这段文本与这个词项相关”。\n例如一段文档没有显式出现“报销”，但出现了“差旅费”“发票”“审批单”，SPLADE 可能会激活与“报销”相关的 token。这样在查询“报销流程”时，即便文档没有完全命中原词，也可能被召回。\n更具体地说，SPLADE 会利用 Masked Language Model 层的 logits，在 BERT WordPiece 词表中预测每个词项的重要性。假设输入文本在分词后是：\n$$ t=(t_{1},t_{2},...,t_{N}) $$对应的上下文表示是：\n$$ (h_{1},h_{2},...,h_{N}) $$那么对输入中第 $i$ 个 token，模型会计算它对词表中第 $j$ 个 token 的重要性：\n$$ w_{ij}=transform(h_{i})^{T}E_{j}+b_{j}, \\quad j\\in\\{1,...,|V|\\} $$这里 $E_j$ 词表 ${token}_j$ 的 BERT 输入嵌入，$b_j$ 是 token 级偏置，transform(.) 通常是带 GeLU 和 LayerNorm 的线性变换。直觉上，这一步是在问：输入里的这个位置，和词表里的每个词项分别有多相关。\n但检索时需要的不是“某个位置对某个词的分数”，而是“整段文本对某个词的分数”。因此 SPLADE 会把不同位置的激活聚合成整段文本的稀疏表示：\n$$ w_{j}=\\sum_{i\\in t}\\log(1+ReLU(w_{ij})) $$这个公式里有三层含义：\nReLU 把负分清零，只保留正向相关的词项。 $log(1+x)$ 做对数饱和，避免高频词或重复词的分数被无限放大。 $\\sum$ 把不同位置对同一个词表 token 的激活累加起来，得到整段文本的词项权重。 最后，文本就变成了一个很高维但很稀疏的向量：\n1 token_id -\u0026gt; weight 查询和文档都映射到同一个词表空间后，检索分数就是稀疏向量点积：\n$$ s(q,d)=\\sum_j w_j^q w_j^d $$这也是 SPLADE 能接入倒排索引或稀疏向量索引的原因。\n排序损失 训练时，SPLADE 要让相关文档得分更高，不相关文档得分更低。给定一个查询 $q_i$ 、一个正样本文档 $d_i^+$ 、一个困难负样本 $d_i^-$ ，以及一组批内负样本 ${d_{i,j}^{-}}$ ，可以使用类似下面的对比排序损失：\n$$ \\mathcal{L}_{rank-IBN} = -\\log \\frac{e^{s(q_i,d_i^+)}} {e^{s(q_i,d_i^+)} + e^{s(q_i,d_i^-)} + \\sum e^{s(q_i,d_{i,j}^{-})}} $$它的目标很直接：让正样本在候选集合里的概率尽可能大。工程上可以理解为，模型会不断学习哪些词项扩展能帮助它把正确文档排到前面。\nFLOPS 稀疏正则 如果只优化排序效果，模型可能会激活太多 token。这样虽然召回可能变强，但倒排索引会膨胀，查询时需要访问的 posting list 也会变多。\n因此 SPLADE 引入 FLOPS 正则来控制稀疏性。对一批文档来说，可以先估计词表 token (j) 在这批文档中的平均激活：\n$$ \\overline{a}_{j}=\\frac{1}{N}\\sum_{i=1}^{N}w_{j}^{(d_i)} $$然后对平均激活做平方求和：\n$$ l_{FLOPS}=\\sum_{j\\in V}\\overline{a}_{j}^{2} =\\sum_{j\\in V}(\\frac{1}{N}\\sum_{i=1}^{N}w_{j}^{(d_i)})^{2} $$这个正则项不是简单控制“向量维度”，而是在控制非零 token 的数量和分布。它希望模型不要把大量文档都绑定到少数高频词上，也不要让每个文档都激活过多词项。\n因此，稀疏性权重可以理解为一个召回质量和检索成本之间的旋钮：\n权重更大：稀疏向量更短，索引更小，检索更快，但可能损失召回。 权重更小：稀疏向量更长，扩展更丰富，但索引和检索成本更高。 总体损失 最终，SPLADE 会把排序损失和稀疏正则放在一起训练：\n$$ \\mathcal{L}=\\mathcal{L}_{rank-IBN} +\\lambda_q\\mathcal{L}_{reg}^{q} +\\lambda_d\\mathcal{L}_{reg}^{d} $$其中 (\\lambda_q) 控制查询侧稀疏性，(\\lambda_d) 控制文档侧稀疏性。查询侧通常对延迟更敏感，所以查询侧稀疏性非常重要。文档侧可以离线计算，通常可以接受更高一点的计算成本，但仍然要控制索引体积。\n从求和到最大池化 原始 SPLADE 会对输入文本中每个位置的词项预测进行聚合：\n$$ w_{j}=\\sum_{i\\in t}\\log(1+ReLU(w_{ij})) $$ 后续更常见的 SPLADE-max 使用最大池化：\n$$ w_{j}=\\max_{i\\in t}\\log(1+ReLU(w_{ij})) $$ 这并不是整段文本只保留一个 token，而是对词表里的每个维度分别取最大激活值。这样可以减少长文本或重复词带来的累加放大，让表示更关注“是否强烈激活某个语义词项”，而不是简单依赖出现次数。\nSPLADE-doc 与蒸馏训练 标准 SPLADE 会同时编码 query 和 document。也就是说，查询侧和文档侧都可能产生神经扩展词项，检索时计算的是：\n$$ s(q,d)=\\sum_j w_j^q w_j^d $$SPLADE-doc 则更偏工程效率。它只在文档侧做 SPLADE 编码，查询侧通常只使用原始 query token，文档得分可以写成：\n$$ s(q,d)=\\sum_{j\\in q}w_j^d $$这样文档侧扩展可以离线预计算，查询侧不需要跑 SPLADE encoder，延迟会更低。代价是 query 侧没有神经扩展能力，只能利用“文档侧扩展”。\n另外，很多效果较强的 SPLADE 模型会使用知识蒸馏和困难负样本。常见做法是先训练一个第一阶段检索器和 cross-encoder 重排器，再用更难的负样本和重排器分数继续训练。工程上不用自己复现这套训练流程，也能使用公开模型；但理解这一点有助于判断模型名称里 distil、ensemble、cocondenser 这类词为什么会出现。\n稀疏性为什么重要 如果模型把大量 token 都激活，召回效果也许会上升，但索引会变大，检索会变慢。SPLADE 使用 FLOPS 正则化来控制非零 token 的数量和分布。\n工程上可以把它理解为：稀疏向量不是越长越好。\n非零 token 太少：索引小、检索快，但召回可能不够。 非零 token 太多：召回可能更好，但索引膨胀，检索成本上升。 落地时通常还会做二次裁剪，例如：\n只保留 top_k 个 token 过滤 weight 低于阈值的 token 限制单个 chunk 的最大稀疏维度数量 这些参数往往比模型本身更影响线上成本。\n模型选择 SPLADE 更像一类稀疏神经检索方法，而不是单一模型。Naver 官方仓库也特别提醒：不同正则化强度会得到从“非常稀疏”到“强 query/doc 扩展”的不同模型，效果、索引大小和延迟都会变。\n如果只是想快速做工程验证，可以从 naver/splade-cocondenser-ensembledistil 开始。它是官方 SPLADE++ 系列里常见的强效果模型，在 Naver 官方仓库列出的 MS MARCO dev MRR@10 为 38.3，高于 splade_v2_max 的 34.0 和 splade_v2_distil 的 36.8。它适合先验证 sparse 召回是否能补足 dense 的关键词、实体、术语召回缺口。\n如果更看重推理成本，可以看 naver/splade_v2_max 或 efficient SPLADE 系列。splade_v2_max 结构简单，Hugging Face 模型页标注为 DistilBERT base、512 token 最大长度、30522 维输出、点积相似度。efficient SPLADE 系列则进一步区分 document encoder 与 query encoder，目标是降低查询侧延迟。\n一个实用的选型顺序是：\n先选一个效果强的公开模型做离线评测，例如 naver/splade-cocondenser-ensembledistil。 如果离线评测有效，再统计平均非零 token 数、索引大小、文档侧编码吞吐和查询侧 P95 延迟。 如果查询侧太慢，优先尝试 query 缓存、ONNX/OpenVINO、量化或 efficient SPLADE。 如果索引太大，优先调小 top-k、提高最小权重阈值，或选择更强正则化、更稀疏的模型。 如果业务语料和公开英文检索数据差异很大，再考虑用领域数据微调，而不是直接相信公开榜单。 不要只按 MRR 选模型。SPLADE 选型至少要同时看五件事：检索效果、平均非零维度、索引体积、查询延迟、部署复杂度。\nSentence Transformers 现在提供了 SparseEncoder，可以直接加载 SPLADE 模型：\n1 2 3 4 from sentence_transformers import SparseEncoder model = SparseEncoder(\u0026#34;naver/splade-cocondenser-ensembledistil\u0026#34;) embeddings = model.encode([\u0026#34;example query\u0026#34;]) 它也提供 encode_query()、encode_document()、稀疏度统计、Qdrant/Elasticsearch/OpenSearch 集成，以及 ONNX/OpenVINO/量化相关部署能力。工程上可以优先用这条路线做原型，再根据性能瓶颈决定是否切到自定义推理服务。\nSPLADE 和 BM25 的区别 BM25 和 SPLADE 都可以使用倒排索引完成检索，但权重来源不同。\nBM25 的权重来自统计量，例如 TF、IDF 和文档长度归一化。它主要依赖 query 原词和 document 原词的精确匹配。\nSPLADE 的权重来自神经模型预测。它不仅可以保留原文出现的 token，也可能激活原文没有出现但语义相关的 token。\n因此可以粗略理解为：\n1 2 BM25 = 原始词项的统计匹配 SPLADE = 神经扩展词项的加权匹配 在企业知识库、技术文档、客服 FAQ、代码文档、规范制度等场景中，BM25 和 SPLADE 都很有价值。BM25 更轻，SPLADE 更强但成本更高。\n","date":"2026-05-12T16:17:17+08:00","permalink":"/p/%E7%A8%80%E7%96%8F%E5%90%91%E9%87%8F%E4%B8%8E-splade-%E6%A8%A1%E5%9E%8B/","title":"稀疏向量与 SPLADE 模型"},{"content":"参考网站 Practical BM25 - Part 1: How Shards Affect Relevance Scoring in Elasticsearch | Elastic Blog\nPractical BM25 - Part 2: The BM25 Algorithm and its Variables | Elastic Blog\nPractical BM25 - Part 3: Considerations for Picking b and k1 in Elasticsearch | Elastic Blog\ntf-idf - 维基百科，自由的百科全书\nBackground 在 Elasticsearch 5.0 中，我们切换到 Okapi BM25 作为默认相似度算法，用于对搜索结果与查询的相关性进行评分。本文将着重介绍BM25 的实际应用，包括可用参数及其影响评分的因素。\nUnderstanding How Shards Affect Scoring 在学习 BM25 前，必须先理解 Elasticsearch 的索引可能被切成多个 shard（索引分片/物理分区）。因为 BM25 的相关性分数不是天然基于整个 index 的全局统计计算，而默认可能是在每个 shard 内部单独计算。shard 数量越多、数据越少，分数越容易出现偏差。\n下面我们照搬参考网站的示例代码，目的是先创建一个 Elasticsearch 索引 people，再往里面插入几条测试文档，然后用同一个查询词 \u0026quot;Shane\u0026quot; 反复检索，观察 BM25 相关性分数如何随着文档数量和 shard 分布发生变化：\n作者创建一个名为 people 的索引，并指定它有 5 个 primary shards，默认相关性算法使用 BM25：\n1 2 3 4 5 6 7 8 9 10 11 12 13 PUT people { \u0026#34;settings\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 5, \u0026#34;index\u0026#34; : { \u0026#34;similarity\u0026#34; : { \u0026#34;default\u0026#34; : { \u0026#34;type\u0026#34; : \u0026#34;BM25\u0026#34; } } } } } 作者使用它的名字做为示例：\n1 2 3 4 5 6 7 8 9 10 11 12 PUT /people/_doc/1 { \u0026#34;title\u0026#34;: \u0026#34;Shane\u0026#34; } GET /people/_doc/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;Shane\u0026#34; } } } 找出 title 字段中匹配 \u0026quot;Shane\u0026quot; 的文档，自然会匹配到/people/_doc/1：\n1 2 3 4 5 6 7 8 9 10 11 12 PUT /people/_doc/2 { \u0026#34;title\u0026#34;: \u0026#34;Shane C\u0026#34; } PUT /people/_doc/3 { \u0026#34;title\u0026#34;: \u0026#34;Shane Connelly\u0026#34; } PUT /people/_doc/4 { \u0026#34;title\u0026#34;: \u0026#34;Shane P Connelly\u0026#34; } 然后再次对它们做搜索：\n1 2 3 4 5 6 7 8 GET /people/_doc/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;Shane\u0026#34; } } } 也就是说现在有4条“文档”：\nShane Shane C Shane Connelly Shane P Connelly 从其中，找出 title 字段中匹配 \u0026quot;Shane\u0026quot; 的文档。虽然说title字段都有\u0026quot;Shane\u0026quot;，但是它们的BM25分数不会一样。结果：doc1 和doc3 分数都是0.2876821，但doc2 分数是0.19856805，doc4分数是0.16853254。\n尽管doc2和doc3很像，但是分数差了很多，这其实和\u0026quot;C\u0026quot;与\u0026quot;Connelly\u0026quot;的差异关系不大，而是和文档如何落在分片中有关。所以如何让分数更具一致性呢：\n数据量越大，shard 间的统计差异越小\n减少 shard 数量可以降低打分偏差\n如果想让多 shard 情况下的 BM25 打分更接近“全局统计”，可以在查询时加上 ?search_type=dfs_query_then_fetch。它会先收集所有 shard 的词频统计信息，再统一计算分数，因此结果会接近甚至等同于 number_of_shards=1 时的分数。\ndfs_query_then_fetch 的作用是先跨 shard 汇总词频统计，再统一计算 BM25 分数，使多 shard 的打分更接近单 shard 的全局效果；但它会增加一次额外通信，所以只有在小数据、多 shard、数据分布不均且很在意相关性分数时才值得使用。\nAlgorithm and its variables BM25 model: $$ \\sum_{i}^{n} IDF(q_i) \\frac{f(q_i, D) * (k_1 + 1)}{f(q_i, D) + k_1 * (1 - b + b * \\frac{fieldLen}{avgFieldLen})} $$ $q_i$: 查询中的第 $i$ 个关键词。 $IDF(q_i)$: 关键词 $q_i$ 的逆文档频率（Inverse Document Frequency）。 $f(q_i, D)$ :关键词 $q_i$ 在文档 $D$ 中的词频。 $fieldLen$: 当前文档的长度。 $avgFieldLen$: 索引中所有文档的平均长度。 $k_1$ 和 $b$: 可调参数（通常 $k1 \\in [1.2, 2.0]$，$b = 0.75$）。 其实简单来说，BM25就是引入了非线性，并解决了频次问题的TF-IDF模型。TF-IDF model: $$ \\text{Score} = f(q_i, D) \\times \\log\\left(\\frac{N}{n(q_i)}\\right) $$$q_i$ 例如，如果我搜索“shane”，这里只有一个查询词，因此 $q_0$ 就是“shane”。如果我用英文搜索“shane connelly”，Elasticsearch 会识别其中的空格，并将其分词（tokenize）为两个词项：$q_0$ 为“shane”，$q_1$ 为“connelly”。这些查询词会被代入公式的其他部分，最后将所有结果加总求和。\n$IDF(q_i)$ 公式中的 IDF（逆文档频率） 部分用于衡量一个词项在所有文档中出现的频率，并会对那些出现次数过多的常用词进行“惩罚”（即降低其权重）。在 Lucene/BM25 算法中，该部分的实际公式如下： $$ \\ln \\left( 1 + \\frac{(docCount - f(q_i) + 0.5)}{f(q_i) + 0.5} \\right) $$ 其中，$docCount$ 是该分片中（如果使用的是 search_type=dfs_query_then_fetch 参数，则为跨所有分片）包含该字段值的文档总数；而 $f(q_i)$ 是包含第 $i$ 个查询词项的文档数量。在我们的示例中可以看到，“shane” 这个词在所有 4 个文档中都出现了，因此对于词项 “shane”，我们最终得到的逆文档频率 $IDF(\\text{\"shane\"})$ 为： $$ \\ln\\left(1 + \\frac{(4 - 4 + 0.5)}{4 + 0.5}\\right) = \\ln\\left(1 + \\frac{0.5}{4.5}\\right) = 0.105360515657826 $$ $IDF(\\text{\"connelly\"})$为： $$ \\ln\\left(1 + \\frac{(4 - 2 + 0.5)}{2 + 0.5}\\right) = \\ln\\left(1 + \\frac{2.5}{2.5}\\right) = 0.693147180559945 $$ 我们可以看到，包含这些更罕见词项的查询（在我们的 4 文档语料库中，“connelly” 比 “shane” 更罕见）具有更高的乘数，因此它们对最终得分的贡献更大。这在直觉上是合理的：词项 “the” 可能出现在几乎每一篇英文文档中，所以当用户搜索像 “the elephant” 这样的内容时，“elephant” 显然比词项 “the”（几乎存在于所有文档中）更重要——我们也希望它能为搜索得分做出更多贡献。\n$fieldLen/avgFieldLen$ 档中的词项越多（至少是那些没匹配上查询条件的词项），该文档的得分就越低。同样，这在直觉上是合理的：如果一份文档长达 300 页且只提到了我的名字一次，那么它与我的相关性，很可能不如一条同样提到我一次的短推文。\n$b$ 如果 $b$ 的值越大，文档长度相对于平均长度所产生的影响就会被进一步放大。为了理解这一点，可以想象如果将 $b$ 设置为 0，长度比例的影响将完全消失，只受频次影响，文档的长度将对评分不产生任何影响。如果将 $b$ 设置为 1 ，则得分不受频次影响，只受长度比例的影响。\n$f(q_i, D)$ 这个值对应了 TF (Term Frequency, 词频)\n$f(q_i, D)$ 的含义是：“第 $i$ 个查询词项在文档 $D$ 中出现了多少次？”在所有这些文档中，$f(\\text{\"shane\"}, D)$ 都是 1，但 $f(\\text{\"connelly\"}, D)$ 各不相同：在文档 3 和 4 中是 1，但在文档 1 和 2 中是 0。如果有第 5 个文档的文本是 “shane shane”，那么它的 $f(\\text{\"shane\"}, D)$ 就是 2。我们可以看到，$f(q_i, D)$ 同时出现在分子和分母中，此外还有一个特殊的 “$k_1$” 因子，我们稍后会讨论它。理解 $f(q_i, D)$ 的方式是：查询词项在文档中出现的次数越多，其得分就越高。这在直觉上是合理的：一个多次提到我们名字的文档，比只提到一次的文档更有可能与我们相关。\n$k_1$ 在 BM25 算法中，$k_1$ 是控制词频饱和度（Term Frequency Saturation）的核心参数。它通过为词频 $f(q_i, D)$ 对相关性得分的贡献设定渐近上限确保得分随词频增加而产生的边际增益呈非线性递减。相比于传统 TF-IDF 近乎线性的权重增长，这一机制有效抑制了高频词项（如关键词堆砌）对排名的过度干扰；其中，$k_1$ 的取值直接决定了得分趋于饱和的速度：较小的 $k_1$ 值会使词频贡献快速达到瓶颈，而较大的 $k_1$ 值则允许词频在更广的数值范围内维持显著的权重增益。\n如果将 $k_1$ 设置为 0，得分将固定为1。如果将 $k_1$ 设置的很大（比如10000），公式近似退化为 $\\frac{TF \\times k_1}{k_1} = TF$ ，变成了词频本身。\nPicking $b$ and $k_1$ 关于 $b$ 和 $k_1$ 的值，ES的文章也指出现在的值是个能涵盖多数情况的“经验值”，但是不存在全局最优的 b 和 k1，只能结合语料和查询做实验评估。\n而且在检索性能不够好时，在调整 $b$ 和 $k_1$的值之前，应该先优化下面的内容：\n对精确短语匹配进行 boost； 使用 synonyms 扩展用户可能感兴趣的同义表达； 使用 fuzziness、typeahead、phonetic matching、stemming 等分析组件，处理拼写错误、语言差异和词形变化； 使用 function score，根据发布时间、地理距离或业务特征调整文档得分。 至于后文ES的Explain API，不再赘述。\n","date":"2026-05-09T13:30:00+08:00","permalink":"/p/practical-bm25/","title":"Practical BM25"},{"content":"Python asyncio：协程与任务 本文基于 Python 3.11 的asyncio ，可能会缺失最新版本的特性\nasyncio 是用来编写 并发 代码的库，使用 async/await 语法。其被用作多个提供高性能 Python 异步框架的基础，包括网络和网站服务，数据库连接库，分布式任务队列等等。也往往是构建 IO 密集型和高层级 结构化 网络代码的最佳选择。\nasyncio 提供一组 高层级 API 用于:\n并发地 运行 Python 协程 并对其执行过程实现完全控制; 执行 网络 IO 和 IPC; 控制 子进程; 通过 队列 实现分布式任务; 同步 并发代码; 参考网站 协程与任务 — Python 3.11.15 文档\nPython asyncio 模块 | 菜鸟教程\n协程 - Python教程 - 廖雪峰的官方网站\n协程 源码：cpython/Lib/asyncio/coroutines.py at 3.11\n通过 async/await 语法来声明协程是编写 asyncio 应用的推荐方式。 例如，以下代码段会打印 \u0026ldquo;hello\u0026rdquo;，等待 1 秒，再打印 \u0026ldquo;world\u0026rdquo;：\n1 2 3 4 5 6 7 8 9 \u0026gt;\u0026gt;\u0026gt; import asyncio \u0026gt;\u0026gt;\u0026gt; async def main(): ... print(\u0026#39;hello\u0026#39;) ... await asyncio.sleep(1) ... print(\u0026#39;world\u0026#39;) \u0026gt;\u0026gt;\u0026gt; asyncio.run(main()) hello world 注意：简单地调用一个协程并不会使其被调度执行：\n1 2 3 \u0026gt;\u0026gt;\u0026gt; main() \u0026lt;coroutine object main at 0x1053bb7c8\u0026gt; 要实际运行一个协程，asyncio 提供了以下几种机制:\nasyncio.run() 函数用来运行最高层级的入口点 \u0026ldquo;main()\u0026rdquo; 函数 (参见上面的示例。)\n对协程执行 await。以下代码段会在等待 1 秒后打印 \u0026ldquo;hello\u0026rdquo;，然后 再次 等待 2 秒后打印 \u0026ldquo;world\u0026rdquo;：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): print(f\u0026#34;started at {time.strftime(\u0026#39;%X\u0026#39;)}\u0026#34;) await say_after(1, \u0026#39;hello\u0026#39;) await say_after(2, \u0026#39;world\u0026#39;) print(f\u0026#34;finished at {time.strftime(\u0026#39;%X\u0026#39;)}\u0026#34;) asyncio.run(main()) 预期输出：\n1 2 3 4 started at 17:13:52 hello world finished at 17:13:55 asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程。\n让我们修改以上示例，并发 运行两个 say_after 协程:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async def main(): task1 = asyncio.create_task( say_after(1, \u0026#39;hello\u0026#39;)) task2 = asyncio.create_task( say_after(2, \u0026#39;world\u0026#39;)) print(f\u0026#34;started at {time.strftime(\u0026#39;%X\u0026#39;)}\u0026#34;) # Wait until both tasks are completed (should take # around 2 seconds.) await task1 await task2 print(f\u0026#34;finished at {time.strftime(\u0026#39;%X\u0026#39;)}\u0026#34;) asyncio.TaskGroup 类提供了 create_task() 的更现代化的替代。 使用此 API，之前的例子将变为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 async def main(): async with asyncio.TaskGroup() as tg: task1 = tg.create_task( say_after(1, \u0026#39;hello\u0026#39;)) task2 = tg.create_task( say_after(2, \u0026#39;world\u0026#39;)) print(f\u0026#34;started at {time.strftime(\u0026#39;%X\u0026#39;)}\u0026#34;) # The await is implicit when the context manager exits. print(f\u0026#34;finished at {time.strftime(\u0026#39;%X\u0026#39;)}\u0026#34;) 现在看来，asyncio.create_task() 和 asyncio.TaskGroup 最大的区别在于是否需要手动等待(await)，其它的特性详见后文。\n可等待对象 如果一个对象可以在 await 语句中使用，那么它就是 可等待 对象。许多 asyncio API 都被设计为接受可等待对象。\n可等待 对象有三种主要类型: 协程，任务和Future。\n协程\nPython 协程属于 可等待 对象，因此可以在其他协程中被等待：\n1 2 3 4 5 6 7 8 9 10 11 12 13 import asyncio async def nested(): return 42 async def main(): # Nothing happens if we just call \u0026#34;nested()\u0026#34;. # A coroutine object is created but not awaited, nested() print(await nested()) # will print \u0026#34;42\u0026#34;. asyncio.run(main()) 在本文档中 \u0026ldquo;协程\u0026rdquo; 可用来表示两个紧密关联的概念:\n协程函数: 定义形式为 async def 的函数; 协程对象: 调用 协程函数 所返回的对象。 任务\n任务 被用来“并行的”调度协程\n当一个协程通过 asyncio.create_task() 等函数被封装为一个 任务，该协程会被自动调度执行：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import asyncio async def nested(): return 42 async def main(): # Schedule nested() to run soon concurrently # with \u0026#34;main()\u0026#34;. task = asyncio.create_task(nested()) # \u0026#34;task\u0026#34; can now be used to cancel \u0026#34;nested()\u0026#34;, or # can simply be awaited to wait until it is complete: await task asyncio.run(main()) 当用 create_task() 把一个协程包装成任务后，中途不想让这个任务继续执行了，就可以 task.cancel() 将其中止，它会在 nested() 下一次遇到 await 时，注入一个 asyncio.CancelledError 异常，详见任务取消节。\n当 task = asyncio.create_task(nested()) 执行后，nested() 自动开始运行，如果不写 await task ，main()通常结束得更快，导致 nested() 尚未运行完，协程就已经结束了。\n由于其重要性，详细用法见Task对象。\nFutures\nFuture 是一种特殊的 低层级 可等待对象，表示一个异步操作的 最终结果。\n当一个 Future 对象 被等待，这意味着协程将保持等待直到该 Future 对象在其他地方操作完毕。\n在 asyncio 中需要 Future 对象以便允许通过 async/await 使用基于回调的代码。\n通常情况下 没有必要 在应用层级的代码中创建 Future 对象。\nFuture 对象有时会由库和某些 asyncio API 暴露给用户，用作可等待对象：\n1 2 3 4 5 6 7 8 async def main(): await function_that_returns_a_future_object() # this is also valid: await asyncio.gather( function_that_returns_a_future_object(), some_python_coroutine() ) 一个很好的返回对象的低层级函数的示例是 loop.run_in_executor()。\n休眠 coroutine asyncio.sleep(delay, result=None)\n阻塞 delay 指定的秒数。\n如果指定了 result，则当协程完成时将其返回给调用者。\nsleep() 总是会挂起当前任务，以允许其他任务运行。\n将 delay 设为 0 将提供一个经优化的路径以允许其他任务运行。 这可供长期间运行的函数使用以避免在函数调用的全过程中阻塞事件循环。\n比如调用 await asyncio.sleep(0) 时：\n当前任务挂起： 你的函数暂时停止执行，保存当前的寄存器和栈状态。 排到队尾： 该任务并没有被放进“等待计时器”的名单，而是直接被放回了 Ready Queue 的末尾。 循环轮转： 事件循环从 Ready Queue 的头部取出下一个任务开始运行。 以下协程示例运行 5 秒，每秒显示一次当前日期：\n1 2 3 4 5 6 7 8 9 10 11 12 13 import asyncio import datetime async def display_date(): loop = asyncio.get_running_loop() end_time = loop.time() + 5.0 while True: print(datetime.datetime.now()) if (loop.time() + 1.0) \u0026gt;= end_time: break await asyncio.sleep(1) asyncio.run(display_date()) 创建任务 源码：cpython/Lib/asyncio/tasks.py at 3.11\nasyncio.create_task(coro, *, name=None, context=None)\n将 coro 协程 封装为一个 Task 并调度其执行。返回 Task 对象。\nname 不为 None，它将使用 Task.set_name() 来设为任务的名称。\n可选的 context 参数允许指定自定义的 contextvars.Context 供 coro 运行。 当未提供 context 时将创建当前上下文的副本。\n该任务会在 get_running_loop() 返回的循环中执行，如果当前线程没有在运行的循环则会引发 RuntimeError。\n保存一个指向此函数的结果的引用，以避免任务在执行过程中消失。 事件循环将只保留对任务的弱引用。 未在其他地方被引用的任务可能在任何时候被作为垃圾回收，即使是在它被完成之前。 如果需要可靠的“发射后不用管”后台任务，请将它们放到一个多项集中：\n1 2 3 4 5 6 7 8 9 10 11 12 background_tasks = set() for i in range(10): task = asyncio.create_task(some_coro(param=i)) # Add task to the set. This creates a strong reference. background_tasks.add(task) # To prevent keeping references to finished tasks forever, # make each task remove its own reference from the set after # completion: task.add_done_callback(background_tasks.discard) 这段文档触及了Python的“事件循环”和“垃圾回收”机制，下面简单解释一下，具体的学习可能会单独写篇文章：\n事件循环 为什么 asyncio 的事件循环（Event Loop）只对 Task 对象保留弱引用 weakref ？这是出于防止内存泄漏的防御性设计。\n事件循环是一个长期运行的底层死循环（通常伴随整个进程的生命周期）。如果事件循环内部维护一个强引用列表来追踪所有被 create_task() 启动的后台任务：\n那些执行完毕或被取消的任务，必须由事件循环主动去清理它们。 如果清理机制存在任何延迟或缺陷，或者开发者创建了海量的“发射后不管”任务，事件循环的内部列表会无限膨胀，最终导致内存溢出 (OOM)。 因此，asyncio 制定了明确的契约：事件循环只负责“调度”协程，不负责“维持” Task 对象的生命周期。 维持 Task 生命周期的责任被完全移交给了调用方（开发者）。事件循环通过弱引用来追踪任务状态，一旦调用方丢失了对 Task 的强引用，事件循环不会阻止该 Task 被销毁。\n垃圾回收 Python 会在底层追踪每一个内存对象被引用了多少次。根据引用状态的不同，GC (Garbage Collection) 的处理逻辑如下：\n处理强引用对象\n当你将一个对象赋值给一个变量（如 a = MyObject()），或者将其加入到列表、字典等数据结构中时，就创建了一个强引用。那么该对象的内部属性“引用计数”会加 1。\n只要一个对象的引用计数大于 0，垃圾回收机制就会判定该对象正在被使用。GC 会完全忽略它，确保其在内存中安全存活。\n处理非强引用对象\n当变量的作用域结束、变量被重新赋值，或者使用 del 显式删除变量时，原对象的强引用就会解除。此外，使用 weakref 模块创建的“弱引用”也属于非强引用。强引用的解除会导致对象的引用计数减 1。弱引用虽然指向该对象，但不会增加对象的引用计数。\n一旦对象的强引用计数降为 0：\nGC 会立即介入，调用该对象的析构方法（__del__）。 立即释放该对象占用的内存空间。 所有指向该对象的弱引用会自动失效（返回值变为 None）。 如果两个对象互相强引用对方，导致计数永远不为 0，Python 的辅助机制“分代回收器”会定期扫描并销毁这种无法被外部访问的孤立对象簇。\n上下文变量 create_task() 的 context 参数是用来解决异步并发下**“数据隔离与传递”**的。默认（多数）情况下它会自动复印当前状态，保证子任务能拿到父任务的上下文数据；如果想阻断这种继承，就可以手动传一个自定义的 context 进去。\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 import asyncio import contextvars # 1. 【全局】创建一个名为 \u0026#39;user_id\u0026#39; 的上下文变量（类似一把钥匙 # 这把钥匙是专门用来存储和检索当前任务相关的数据的） user_id_var = contextvars.ContextVar(\u0026#39;user_id\u0026#39;) async def write_log(action): # 3. 【底层函数】获取当前任务对应的 \u0026#39;user_id\u0026#39;（每个任务都有自己独立的“背包”） # 张三的任务获取的是张三，李四的任务获取的是李四 current_user = user_id_var.get() print(f\u0026#34;[{current_user}] 执行了操作: {action}\u0026#34;) async def handle_request(name): # 2. 【顶层入口】设置当前任务的 \u0026#39;user_id\u0026#39;（将数据放入当前任务的“背包”） # 这里我们把用户名称（如张三或李四）赋值给当前任务的上下文变量 user_id_var.set(name) # 模拟复杂的操作过程，任务间不需要传递数据！直接使用上下文变量。 await write_log(\u0026#34;登录系统\u0026#34;) await asyncio.sleep(1) # 模拟任务的并发执行，控制权被交出 await write_log(\u0026#34;退出系统\u0026#34;) async def main(): # 并发执行两个请求任务，确保每个任务独立，不会互相干扰 # 即使任务交替执行，上下文数据（如 \u0026#39;user_id\u0026#39;）也会保持准确 await asyncio.gather( handle_request(\u0026#34;张三\u0026#34;), handle_request(\u0026#34;李四\u0026#34;) ) # 运行主程序，启动异步任务 asyncio.run(main()) # 输出结果（交替打印，但每个任务的输出始终正确，保持隔离）： # [张三] 执行了操作: 登录系统 # [李四] 执行了操作: 登录系统 # [张三] 执行了操作: 退出系统 # [李四] 执行了操作: 退出系统 并发运行任务 当多个协程之间没有依赖关系时，我们通常不希望它们一个接一个执行，而是希望它们同时开始，谁需要等待 I/O，谁就把控制权交还给事件循环，让其他任务继续运行。\nasyncio.gather() 就适合这种场景：并发运行一组可等待对象，并在它们全部完成后，一次性拿到结果。\n比如下面这个例子中，factorial(\u0026quot;A\u0026quot;, 2)、factorial(\u0026quot;B\u0026quot;, 3) 和 factorial(\u0026quot;C\u0026quot;, 4) 彼此之间没有依赖，因此可以并发执行：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import asyncio async def factorial(name, number): f = 1 for i in range(2, number + 1): print(f\u0026#34;Task {name}: Compute factorial({number}), currently i={i}...\u0026#34;) await asyncio.sleep(1) f *= i print(f\u0026#34;Task {name}: factorial({number}) = {f}\u0026#34;) return f async def main(): L = await asyncio.gather( factorial(\u0026#34;A\u0026#34;, 2), factorial(\u0026#34;B\u0026#34;, 3), factorial(\u0026#34;C\u0026#34;, 4), ) print(L) asyncio.run(main()) 运行结果大致如下：\n1 2 3 4 5 6 7 8 9 10 Task A: Compute factorial(2), currently i=2... Task B: Compute factorial(3), currently i=2... Task C: Compute factorial(4), currently i=2... Task A: factorial(2) = 2 Task B: Compute factorial(3), currently i=3... Task C: Compute factorial(4), currently i=3... Task B: factorial(3) = 6 Task C: Compute factorial(4), currently i=4... Task C: factorial(4) = 24 [2, 6, 24] 这里最值得注意的是：虽然三个任务的打印顺序是交错的，但 gather() 返回结果的顺序仍然和传入顺序一致。\n也就是说：\n1 2 3 4 5 L = await asyncio.gather( factorial(\u0026#34;A\u0026#34;, 2), factorial(\u0026#34;B\u0026#34;, 3), factorial(\u0026#34;C\u0026#34;, 4), ) 最终得到的是：\n1 [2, 6, 24] 而不是按照哪个任务先完成来排序。\ngather() 有几个常用规则：\n如果传入的是协程对象，它会自动被包装成任务并调度执行。 如果所有可等待对象都成功完成，返回值是一个列表。 返回列表的顺序和传入顺序一致。 如果 gather() 本身被取消，所有尚未完成的任务也会被取消。 比较容易忽略的是异常处理。\n默认情况下，如果其中某个任务抛出异常，异常会传播给正在 await gather() 的地方。但这并不意味着其他任务一定会立刻停止。对于已经提交给 gather() 的其他可等待对象，它们不会因为其中一个任务抛错就自动取消，而是会继续运行。\n如果你希望把异常也当作结果收集起来，可以使用 return_exceptions=True：\n1 2 3 4 5 6 results = await asyncio.gather( factorial(\u0026#34;A\u0026#34;, 2), factorial(\u0026#34;B\u0026#34;, 3), factorial(\u0026#34;C\u0026#34;, 4), return_exceptions=True, ) 这样异常会作为列表中的一个元素返回，调用方需要自己判断哪些结果是正常值，哪些是异常。\n因此，我对 gather() 的理解是：它更像是一个“结果收集器”。当你关心的是“一组任务都跑完之后，各自返回了什么”时，它很合适。\n但如果你更关心的是“一组任务属于同一个生命周期，其中一个失败时其他任务也应该被取消”，那么 Python 3.11 之后更推荐使用 TaskGroup。这也是下一节要讨论的内容。\n任务组 任务组合并了一套用于等待分组中所有任务完成的方便可靠方式的任务创建 API。\nclass asyncio.TaskGroup : 持有一个任务分组的 异步上下文管理器。 可以使用 create_task() 将任务添加到分组中。 当该上下文管理器退出时所有任务都将被等待。\nTaskGroup.create_task(coro, *, name=None, context=None) : 在该任务组中创建一个任务。 其签名与 asyncio.create_task() 的相匹配。\n示例：\n1 2 3 4 5 async def main(): async with asyncio.TaskGroup() as tg: task1 = tg.create_task(some_coro(...)) task2 = tg.create_task(another_coro(...)) print(\u0026#34;Both tasks have completed now.\u0026#34;) async with 语句将等待分组中的所有任务结束。 在等待期间，仍可将新任务添加到分组中 (例如，通过将 tg 传入某个协程并在该协程中调用 tg.create_task())。 一旦最后的任务完成并退出 async with 代码块，将无法再向分组添加新任务。\n关于其异常处理，文档写的过于繁琐了，总结如下：\n如果任务因非 asyncio.CancelledError 异常失败，其他任务会被取消，且无法再添加任务。 失败的异常会被包装成 ExceptionGroup 或 BaseExceptionGroup，统一抛出。 如果任务失败时是 KeyboardInterrupt 或 SystemExit，它们会优先被抛出，而不会归入异常组。 如果发生取消与等待：\n如果 async with 语句因异常退出，剩余任务会被取消并等待完成。 异常（除了 CancelledError）会被加入异常组，最终一起抛出。 异步上下文管理器 上面说 TaskGroup 是一个异步上下文管理器，关键就在这一句：\n1 2 async with asyncio.TaskGroup() as tg: ... 它看起来只是普通的代码块，但实际上 Python 会在进入代码块之前调用 __aenter__()，在离开代码块时调用 __aexit__()。异步上下文管理器和普通上下文管理器的区别是：这两个方法的结果都要被 await。\n可以把 async with 粗略理解成下面的展开形式：\n1 2 3 4 5 6 7 8 9 10 11 manager = asyncio.TaskGroup() tg = await manager.__aenter__() try: ... except BaseException as exc: suppress = await manager.__aexit__(type(exc), exc, exc.__traceback__) if not suppress: raise else: await manager.__aexit__(None, None, None) 这段伪代码里有两个重点。\n第一个重点是：__aenter__() 决定进入代码块时要准备什么，以及 as tg 拿到什么。对 TaskGroup 来说，__aenter__() 会让这个任务组进入可用状态，并把任务组对象本身返回给 tg，所以后面才能调用 tg.create_task()。\n第二个重点是：__aexit__() 决定离开代码块时要收尾什么。对 TaskGroup 来说，真正重要的逻辑就在这里：它会等待组里的任务结束；如果有任务失败，它会取消其他任务；最后再把异常整理后抛出。\n这里容易混淆 __aexit__() 和 __await__()。async with 协议本身调用的是 __aenter__() 和 __aexit__()，不是直接调用 __await__()。但由于这两个方法必须返回可等待对象，所以 Python 在执行：\n1 await manager.__aexit__(None, None, None) 时，会走普通 await 表达式的逻辑。对于 async def __aexit__(...) 来说，调用它会得到一个协程对象；这个协程对象本身是可等待对象，底层通过它的 __await__() 交给事件循环驱动执行。\n换句话说，可以这样理解这几层关系：\n1 2 3 4 async with -\u0026gt; 调用 __aenter__ / __aexit__ -\u0026gt; await 它们返回的可等待对象 -\u0026gt; 可等待对象通过 __await__ 被事件循环推进 所以 TaskGroup 能在退出代码块时“自动等待所有任务”，并不是因为 async with 天生懂任务组，而是因为 TaskGroup.__aexit__() 把等待、取消和异常整理这些逻辑都写在了退出阶段。\n__aexit__() 当离开 async with 代码块（或者代码块内部发生了错误）时，Python 会自动调用 TaskGroup 的 __aexit__(self, exc_type, exc_value, traceback)。\n它也实现了处理 TaskGroup 中遇到异常等情况的逻辑：\n等待所有任务：代码块里的语句执行完，并不代表任务组里的任务都结束了。__aexit__() 会在这里等待组里所有任务完成。 接收代码块异常：如果 async with 内部本身抛出了异常，exc_type, exc_value, traceback 会把这个异常传进 __aexit__()。 处理子任务异常：如果某个子任务抛出了非 CancelledError 异常，TaskGroup 会取消其他还在运行的任务。 整理异常并抛出：多个异常会被收集进 ExceptionGroup 或 BaseExceptionGroup，再统一抛给外层代码。 这也解释了为什么 TaskGroup 比裸用 create_task() 更“结构化”：任务不是散落在事件循环里各跑各的，而是被一个退出阶段统一收束。\n任务取消 任务可以便捷和安全地取消。 当任务被取消时，asyncio.CancelledError 将在遇到机会时在任务中被引发。\n推荐协程使用 try/finally 代码块来可靠地执行清理逻辑。 对于 asyncio.CancelledError 被显式捕获的情况，它通常应当在清理完成时被传播。 asyncio.CancelledError 会直接子类化 BaseException 因此大多数代码都不需要关心这一点。\n启用结构化并发的 asyncio 组件，如 asyncio.TaskGroup 和 asyncio.timeout()，在内部是使用撤销操作来实现的因而在协程屏蔽了 asyncio.CancelledError 时可能无法正常工作。 类似地，用户代码通常也不应调用 uncancel。 但是，在确实想要屏蔽 asyncio.CancelledError 的情况下，则还有必要调用 uncancel() 来完全移除撤销状态。\n超时 异步程序里，超时不是锦上添花，而是很重要的防御措施。\n比如请求接口、读取文件、等待某个远端服务响应时，如果没有超时限制，一个任务可能会一直挂在那里。它未必占用 CPU，但会占住连接、内存、任务槽位等资源，也会让上层逻辑迟迟拿不到结果。\n在 Python 3.11 里，我更倾向优先使用 asyncio.timeout()。它是一个异步上下文管理器，适合给“一段异步代码”设置时间限制：\n1 2 3 4 5 6 7 8 async def main(): try: async with asyncio.timeout(10): await long_running_task() except TimeoutError: print(\u0026#34;The long operation timed out, but we\u0026#39;ve handled it.\u0026#34;) print(\u0026#34;This statement will run regardless.\u0026#34;) 这里最容易写错的地方是：TimeoutError 要在 async with 外面捕获。\n原因是 asyncio.timeout() 的内部机制大致是：\n时间到了，取消当前任务。 当前任务中会出现 asyncio.CancelledError。 timeout() 在退出上下文管理器时把它转换成 TimeoutError。 所以在 async with 代码块里面捕获 TimeoutError 是捕获不到的，因为转换发生在上下文退出时。\n如果超时时间一开始还不能确定，可以先传入 None，之后再通过 reschedule() 调整。更精确地指定结束时间点时，也可以使用 asyncio.timeout_at()。这些属于更细的控制，日常使用里先掌握 asyncio.timeout() 就够了。\n另一个常见 API 是 asyncio.wait_for()。它更适合给“单个可等待对象”设置超时：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async def eternity(): await asyncio.sleep(3600) print(\u0026#39;yay!\u0026#39;) async def main(): try: await asyncio.wait_for(eternity(), timeout=1.0) except TimeoutError: print(\u0026#39;timeout!\u0026#39;) asyncio.run(main()) # Expected output: # # timeout! wait_for() 超时后也会取消传入的任务，并引发 TimeoutError。需要注意的是，它会等待底层任务确实完成取消流程，所以实际等待时间可能略微超过设置的 timeout。\n简单来说：\n场景 推荐写法 给一段异步代码加超时 asyncio.timeout() 给单个 awaitable 加超时 asyncio.wait_for() 不希望被超时取消影响内部任务 配合 asyncio.shield() 不过最后一种情况要谨慎使用，因为“外层不等了，但内层任务还在跑”会让任务生命周期变得更难管理。\n屏蔽取消操作 awaitable asyncio.shield(aw)\n保护一个 可等待对象 防止其被 取消。\n如果 aw 是一个协程，它将自动被作为任务调度。\n以下语句:\n1 2 task = asyncio.create_task(something()) res = await shield(task) 相当于:\n1 res = await something() 不同之处 在于如果包含它的协程被取消，在 something() 中运行的任务不会被取消。从 something() 的角度看来，取消操作并没有发生。然而其调用者已被取消，因此 \u0026ldquo;await\u0026rdquo; 表达式仍然会引发 CancelledError。\n如果通过其他方式取消 something() (例如在其内部操作) 则 shield() 也会取消。\n如果希望完全忽略取消操作 (不推荐) 则 shield() 函数需要配合一个 try/except 代码段，如下所示：\n1 2 3 4 5 task = asyncio.create_task(something()) try: res = await shield(task) except CancelledError: res = None 重要：保存一个传给此函数的任务的引用，以避免任务在执行过程中消失。事件循环将只保留对任务的弱引用。未在其他地方被引用的任务可能在任何时候被作为垃圾回收，即使是在它被完成之前。\n简单等待 coroutine asyncio.wait(aws, *, timeout=None, return_when=ALL_COMPLETED)\n并发地运行 aws 可迭代对象中的 Future 和 Task 实例并进入阻塞状态直到满足 return_when 所指定的条件。\n可迭代对象 aws 不能为空并且不接受产生任务的生成器。\n返回两个 Task/Future 集合: (done, pending)。\n用法：\n1 done, pending = await asyncio.wait(aws) 如指定 timeout (float 或 int 类型) 则它将被用于控制返回之前等待的最长秒数。\n请注意此函数不会引发 TimeoutError。 当超时发生时尚未完成的 Future 或 Task 会在设定的秒数后被直接返回。\nreturn_when 指定此函数应在何时返回。它必须为以下常数之一:\n常量 描述 asyncio.FIRST_COMPLETED 函数将在任意可等待对象结束或取消时返回。 asyncio.FIRST_EXCEPTION 该函数将在任何 future 对象通过引发异常而结束时返回。 如果没有任何 future 对象引发引发那么它将等价于 ALL_COMPLETED。 asyncio.ALL_COMPLETED 函数将在所有可等待对象结束或取消时返回。 asyncio.as_completed(aws, *, timeout=None)\n并发地运行可迭代对象 aws 中的 可等待对象。 产生任务的生成器不可被用作 aws 可迭代对象。 返回一个产生协程的迭代器。 所返回的每个协程可被等待以便从剩余的可等待对象的可迭代对象中获得早最的下一个结果。\n如果在所有 Future 对象完成之前发生超时则将引发 TimeoutError。\n示例:\n1 2 3 for coro in as_completed(aws): earliest_result = await coro # ... 在线程中运行 asyncio.to_thread() 适合处理一种很常见的尴尬情况：你正在写异步代码，但手里有一个只能同步调用的阻塞函数。\n如果直接在协程里调用这个函数，它会卡住事件循环，让其他任务也没法继续运行。to_thread() 的作用就是把这个同步函数丢到另一个线程里执行，然后在异步代码里用 await 等它的结果。\n1 2 3 4 5 6 7 8 9 10 11 12 import asyncio import time def read_file_slowly(): time.sleep(1) return \u0026#34;file content\u0026#34; async def main(): result = await asyncio.to_thread(read_file_slowly) print(result) asyncio.run(main()) 它通常用于文件读写、同步数据库驱动、旧 SDK 调用这类 IO 密集型 阻塞操作。由于 GIL 的存在，它一般不适合用来加速纯 Python 的 CPU 密集型计算。\n跨线程调度 asyncio.run_coroutine_threadsafe() 用于从另一个线程向某个事件循环提交协程。\n这个 API 平时不常用。只有当你的程序里同时存在普通线程和 asyncio 事件循环，并且某个线程需要把协程交给事件循环执行时，才会用到它。\n1 2 3 4 coro = asyncio.sleep(1, result=3) future = asyncio.run_coroutine_threadsafe(coro, loop) result = future.result(timeout=2) 它返回的是 concurrent.futures.Future，不是 asyncio.Future。因此获取结果时用的是同步世界里的 future.result()。如果等待超时，也可以取消它：\n1 2 3 4 try: result = future.result(timeout=2) except TimeoutError: future.cancel() 需要注意的是，它要求显式传入事件循环 loop，而且应该从事件循环所在之外的线程调用。\n内省 内省 API 主要用于调试、监控或框架内部逻辑。日常业务代码一般不需要频繁使用。\n常见的有三个：\nAPI 用途 asyncio.current_task() 获取当前正在运行的 Task asyncio.all_tasks() 获取当前事件循环中尚未完成的所有 Task asyncio.iscoroutine(obj) 判断对象是不是协程对象 例如调试当前事件循环里还有哪些任务没结束：\n1 2 for task in asyncio.all_tasks(): print(task) 这些函数适合在排查任务泄漏、定位后台任务状态、编写异步框架时使用。对于普通应用代码来说，知道它们存在即可。\n","date":"2026-04-23T16:08:00+08:00","permalink":"/p/python-asyncio%E5%8D%8F%E7%A8%8B%E4%B8%8E%E4%BB%BB%E5%8A%A1/","title":"Python asyncio：协程与任务"},{"content":"RAGFlow 切片策略解析 众所周知RAGFlow是较早成熟且开源的RAG项目之一，近日笔者正在学习构建一个RAG项目，所以从RAGFlow的源码下手。首先学习的是它对markdown文件的解析方法和切片策略。\nrag/app/naive.py naive.py 中支持很多文件的类型，比如PDF, DOCX, Markdown\u0026hellip;\u0026hellip;\n那么我们要学习的markdown类，主入口是__call__()，这是核心方法，编排了整个 Markdown 解析流程。\n__call()__ 1 def __call__(self, filename, binary=None, separate_tables=True, delimiter=None, return_section_images=False): 参数 类型 说明 filename str 文件路径（当 binary 为空时用来读文件） binary bytes 文件二进制内容，优先使用 separate_tables bool 是否将表格从正文中分离出来 delimiter str 自定义分割符，如果指定则用分割符切分而非按元素类型切分 return_section_images bool 是否额外返回每个 section 对应的图片 执行流程：\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 55 56 57 Step 1: 文本解码 ┌────────────────────────────┐ │ binary → find_codec() → │ │ decode(encoding) │ │ 或 open(filename).read() │ └────────────┬───────────────┘ ↓ Step 2: 表格提取（继承自父类） ┌────────────────────────────────────────────────────┐ │ self.extract_tables_and_remainder(txt+\u0026#34;\\n\u0026#34;, │ │ separate_tables) │ │ → (remainder, tables[]) │ │ │ │ 注意：这里传入的是 txt+\u0026#34;\\n\u0026#34;（末尾补换行） │ └────────────┬───────────────────────────────────────┘ ↓ Step 3: 元素扫描 ┌────────────────────────────────────────────────────┐ │ MarkdownElementExtractor(txt) │ │ .extract_elements(delimiter, include_meta=True) │ │ → element_sections[] │ │ │ │ ★ 关键：这里传入的是原始 txt，而非 remainder！ │ │ （注释 L682-683 说明了原因：为避免重复表格） │ └────────────┬───────────────────────────────────────┘ ↓ Step 4: 图片 URL 提取 ┌────────────────────────────────────────────────────┐ │ self.extract_image_urls_with_lines(txt) │ │ → image_refs = [{url, line}, ...] │ └────────────┬───────────────────────────────────────┘ ↓ Step 5: 元素-图片关联（核心融合逻辑） ┌────────────────────────────────────────────────────┐ │ for element in element_sections: │ │ ① 取 element 的 start_line ~ end_line │ │ ② 筛选行范围内的 image_refs │ │ ③ load_images_from_urls() 下载图片（有缓存） │ │ ④ 多张图用 concat_img 合并为一张 │ │ ⑤ sections.append((content, \u0026#34;\u0026#34;)) │ │ ⑥ section_images.append(combined_image or None) │ └────────────┬───────────────────────────────────────┘ ↓ Step 6: 表格后处理 ┌────────────────────────────────────────────────────┐ │ for table in tables: │ │ markdown(table, extensions=[\u0026#34;tables\u0026#34;]) → html │ │ tbls.append(((None, html), \u0026#34;\u0026#34;)) │ └────────────┬───────────────────────────────────────┘ ↓ Step 7: 返回结果 ┌────────────────────────────────────────────────────┐ │ if return_section_images: │ │ return sections, tbls, section_images │ │ else: │ │ return sections, tbls │ └────────────────────────────────────────────────────┘ 输出数据结构：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 sections = [ (\u0026#34;## 标题\\n正文内容...\u0026#34;, \u0026#34;\u0026#34;), # (文本, 位置标签—永远为空字符串) (\u0026#34;```python\\ncode\\n```\u0026#34;, \u0026#34;\u0026#34;), # 代码块 ... ] tbls = [ ((None, \u0026#34;\u0026lt;table\u0026gt;...\u0026lt;/table\u0026gt;\u0026#34;), \u0026#34;\u0026#34;), # (None, HTML表格字符串) ... ] section_images = [ PIL.Image, # 该 section 中所有图片合并后的图像对象 None, # 该 section 无图片 PIL.Image, ... ] 这其中最关键的是表格提取和元素扫描的部分：\nextract_tables_and_remainder() 这个方法定义于deepdoc/parser/resume/markdown_parser.py的RAGFlowMarkdownParser类中，用于将markdown中的表格提取出来\n1 2 3 4 5 6 class RAGFlowMarkdownParser: def __init__(self, chunk_token_num=128): self.chunk_token_num = int(chunk_token_num) def extract_tables_and_remainder(self, markdown_text, separate_tables=True): # 返回: (剩余正文, [表格列表]) 表格识别策略：\n类型 正则 示例 有边框 Markdown 表格 ` \u0026hellip; 无边框 Markdown 表格 `text text` + 分隔行 + 数据行 HTML 表格 \u0026lt;table\u0026gt;...\u0026lt;/table\u0026gt;，支持包裹在 \u0026lt;html\u0026gt;\u0026lt;body\u0026gt; 中 \u0026lt;table\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td\u0026gt;...\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;/table\u0026gt; 当 separate_tables=True 时，表格从正文里移除并单独返回；为 False 时则就地转为 HTML。\nMarkdownElementExtractor 该类位于deepdoc/parser/markdown_parser.py，实现了一个简易的 Markdown 元素识别器，将文本按行扫描并归类为不同的块类型。\n1 2 3 4 5 6 7 class MarkdownElementExtractor: def __init__(self, markdown_content): self.markdown_content = markdown_content self.lines = markdown_content.split(\u0026#34;\\n\u0026#34;) def extract_elements(self, delimiter=None, include_meta=False): \u0026#34;\u0026#34;\u0026#34;提取各种元素(headers, code blocks, lists, 等)\u0026#34;\u0026#34;\u0026#34; 输出元素结构：\n1 2 3 4 5 6 { \u0026#34;type\u0026#34;: \u0026#34;header\u0026#34; | \u0026#34;code_block\u0026#34; | \u0026#34;list_block\u0026#34; | \u0026#34;blockquote\u0026#34; | \u0026#34;text_block\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;具体文本内容\u0026#34;, \u0026#34;start_line\u0026#34;: 0, # 起始行号 \u0026#34;end_line\u0026#34;: 5, # 结束行号 } _extract_header() : 返回单行的\u0026quot;header\u0026quot; _extract_code_block() : 直到遇到\u0026quot; ``` \u0026ldquo;返回多行的代码块\u0026quot;code_block\u0026rdquo; _extract_list_block() : 吞入满足以下条件的行： 以 -, *, + 或 数字. 开头（列表项） 空行（列表项间隙） 以 2+ 空格缩进的子列表或续行 _extract_blockquote() : 持续吞入\u0026quot;\u0026gt;\u0026ldquo;开头的行或内部的空行 _extract_text_block() : 处理不属于上面几种类型的普通文本\u0026quot;text_block\u0026rdquo;，直到符合上面类型的元素再次出现 extract_image_urls_with_lines() 这个方法用于找出 Markdown 文本中所有图片引用及其所在行号。行号用于后续将图片关联到对应的 section。\n三阶段提取策略：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 阶段 1: Markdown 语法图片 正则: ![alt](url) → 提取 url 模式: r\u0026#34;!\\[[^\\]]*\\]\\(([^)\\s]+)\u0026#34; 阶段 2: HTML 内联图片（单行） 正则: src=\u0026#34;url\u0026#34; / src=\u0026#39;url\u0026#39; → 提取 url 模式: r\u0026#39;src=[\u0026#34;\\\\\\\u0026#39;\u0026#34;]([^\u0026#34;\\\\\\\u0026#39;\u0026gt;\\\\s]+)\u0026#39; 阶段 3: HTML 跨行图片（BeautifulSoup 兜底） 解析整个文本为 HTML，查找所有 \u0026lt;img\u0026gt; 标签的 src 通过字符偏移量计算行号 用 seen 集合避免与阶段 1/2 重复 返回值：\n1 2 3 4 5 [ {\u0026#34;url\u0026#34;: \u0026#34;https://example.com/img.png\u0026#34;, \u0026#34;line\u0026#34;: 5}, {\u0026#34;url\u0026#34;: \u0026#34;./assets/diagram.svg\u0026#34;, \u0026#34;line\u0026#34;: 12}, ... ] load_images_from_urls() 1 2 def load_images_from_urls(self, urls, cache=None): # 返回: (images[], cache{}) 功能 细节 HTTP 图片 requests.get(url, timeout=30)，校验 Content-Type 为 image/ 本地图片 Path(url).exists() 检查后用 PIL.Image.open() 缓存机制 cache dict 避免重复下载同一 URL 统一格式 所有图片转为 RGB 模式 (convert(\u0026quot;RGB\u0026quot;)) 错误处理 失败时 cache[url] = None，不中断流程 这里的思路比较朴素，但很实用：\n先根据 url 判断是网络图片还是本地图片 统一加载成 PIL.Image 放入缓存，避免同一张图在多个 section 中重复读取 后续如果某个 section 对应多张图片，再交给 concat_img 合并 也就是说，RAGFlow 在 Markdown 中并不是把每一张图片都单独当成一个 chunk，而是先尝试把同一个 section 内的图片聚合起来，再与文本一起进入后续流程。\nurls_in_section = [...] 在 Markdown.__call__() 中，真正把文本 section 和图片联系起来的代码是这一段：\n1 2 3 4 5 for element in element_sections: content = element[\u0026#34;content\u0026#34;] start_line = element[\u0026#34;start_line\u0026#34;] end_line = element[\u0026#34;end_line\u0026#34;] urls_in_section = [ref[\u0026#34;url\u0026#34;] for ref in image_refs if start_line \u0026lt;= ref[\u0026#34;line\u0026#34;] \u0026lt;= end_line] 其核心思想非常直接：按行号做区间归属。\n一个 Markdown 元素先被标记 start_line 和 end_line 所有图片引用也带有自己的 line 只要图片所在行落在元素区间内，就认为这张图属于这个 section 这是一种很工程化的做法。它不追求复杂语义理解，而是利用 Markdown 本身“按行组织”的特点，以较低成本建立图文关联。\n例如下面这段 Markdown：\n1 2 3 4 5 6 7 8 ### 模型结构 这里介绍整体流程。 ![pipeline](./pipeline.png) 接着说明各模块职责。 如果这一整段被 MarkdownElementExtractor 视为同一个 text_block，那么图片就会和这一段正文绑定在一起；如果标题和正文被分成两个元素，则图片通常会归到正文所在的 section，而不会归到标题 section。\n表格处理细节 前面提到，Markdown.__call__() 中虽然调用了：\n1 remainder, tables = self.extract_tables_and_remainder(f\u0026#34;{txt}\\n\u0026#34;, separate_tables=separate_tables) 但后面真正做元素扫描时，使用的是：\n1 extractor = MarkdownElementExtractor(txt) 而不是 remainder。\n源码里其实已经留了注释：\n1 2 3 # To eliminate duplicate tables in chunking result, uncomment code below and set separate_tables to True ... # extractor = MarkdownElementExtractor(remainder) extractor = MarkdownElementExtractor(txt) 这说明作者其实也意识到了一个现象：表格可能同时出现在 section chunk 和 table result 中。\n从实现角度看，这未必是 bug，更像是一种偏保守的召回策略：\n正文 chunk 中保留表格原始上下文 tables 中再额外保留结构化表格内容 这样做可能带来一定冗余，但也提高了检索时命中表格信息的概率。\nchunk() 看到这里，其实还只是完成了解析和预处理。真正决定最终 chunk 长什么样的，不在 Markdown.__call__()，而在 rag/app/naive.py 下面的 chunk() 函数里。\nMarkdown 文件分支一开始会先调用前面的解析器：\n1 2 3 4 5 6 7 8 markdown_parser = Markdown(int(parser_config.get(\u0026#34;chunk_token_num\u0026#34;, 128))) sections, tables, section_images = markdown_parser( filename, binary, separate_tables=False, delimiter=parser_config.get(\u0026#34;delimiter\u0026#34;, \u0026#34;\\n!?;。；！？\u0026#34;), return_section_images=True, ) 这里有两个细节值得注意：\nreturn_section_images=True 说明 Markdown 解析阶段生成的图片不会丢，而是继续带到后面的 chunk 合并流程中。 separate_tables=False 说明这里并没有把表格完全从正文切走，而是倾向于让表格继续留在 Markdown 上下文里，同时又额外生成 tables 供表格索引使用。 接着，如果当前租户存在 IMAGE2TEXT 模型，chunk() 会尝试给 Markdown 中的图片补一段描述文本：\n1 2 3 4 5 6 try: vision_model_config = get_tenant_default_model_by_type(kwargs[\u0026#34;tenant_id\u0026#34;], LLMType.IMAGE2TEXT) vision_model = LLMBundle(kwargs[\u0026#34;tenant_id\u0026#34;], vision_model_config) except Exception as e: logging.warning(f\u0026#34;Failed to detect figure extraction: {e}\u0026#34;) vision_model = None 如果视觉模型可用，则继续遍历每个 section：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for idx, (section_text, _) in enumerate(sections): images = [] if section_images and len(section_images) \u0026gt; idx and section_images[idx] is not None: images.append(section_images[idx]) if images and len(images) \u0026gt; 0: combined_image = reduce(concat_img, images) if len(images) \u0026gt; 1 else images[0] markdown_vision_parser = VisionFigureParser( vision_model=vision_model, figures_data=[((combined_image, [\u0026#34;markdown image\u0026#34;]), [(0, 0, 0, 0, 0)])], **kwargs ) boosted_figures = markdown_vision_parser(callback=callback) sections[idx] = ( section_text + \u0026#34;\\n\\n\u0026#34; + \u0026#34;\\n\\n\u0026#34;.join([fig[0][1] for fig in boosted_figures]), sections[idx][1] ) 这里可以看出 Markdown 图片增强的几个特点：\n粒度是 section 级别 输入图片是前面已经聚合好的 section_images[idx] 输出不是新的 chunk，而是把图片描述文本直接追加回原 section_text VisionFigureParser 该类定义在 deepdoc/parser/figure_parser.py 中：\n1 2 3 4 5 6 class VisionFigureParser: def __init__(self, vision_model, figures_data, *args, **kwargs): self.vision_model = vision_model self.figure_contexts = kwargs.get(\u0026#34;figure_contexts\u0026#34;) or [] self.context_size = max(0, int(kwargs.get(\u0026#34;context_size\u0026#34;, 0) or 0)) self._extract_figures_info(figures_data) 这个类的职责并不复杂，可以理解为一个“图片描述批处理器”：\n接收图片列表 figures_data 整理出图片、描述、位置信息 调用视觉模型生成描述 再把结果重新组装回原来的数据结构 Markdown 分支给它传入的 figures_data 形式是：\n1 [((combined_image, [\u0026#34;markdown image\u0026#34;]), [(0, 0, 0, 0, 0)])] 也就是说，每次只处理当前 section 对应的一张聚合图，原始描述先放一个占位值 \u0026quot;markdown image\u0026quot;，位置则放一个 dummy tuple。\n_extract_figures_info() 这个方法负责把 figures_data 拆成内部使用的三个列表：\n1 2 3 4 def _extract_figures_info(self, figures_data): self.figures = [] self.descriptions = [] self.positions = [] 其中核心分支是：\n1 2 3 4 5 6 if len(item) == 2 and isinstance(item[0], tuple) and len(item[0]) == 2: img_desc = item[0] img = ensure_pil_image(img_desc[0]) self.figures.append(img) self.descriptions.append(img_desc[1]) self.positions.append(item[1]) 因此传入的：\n1 ((combined_image, [\u0026#34;markdown image\u0026#34;]), [(0, 0, 0, 0, 0)]) 会被拆成：\nself.figures : [combined_image] self.descriptions : [[\u0026quot;markdown image\u0026quot;]] self.positions : [[(0, 0, 0, 0, 0)]] 这里的 ensure_pil_image() 负责把输入统一成 PIL.Image 对象，因此前面无论传入的是普通图片对象还是惰性图片对象，到了这里都会被标准化。\n__call__() VisionFigureParser.__call__() 才是真正执行视觉增强的入口：\n1 2 def __call__(self, **kwargs): callback = kwargs.get(\u0026#34;callback\u0026#34;, lambda prog, msg: None) 它内部先定义了一个 process()，用于处理单张图片：\n1 2 3 4 5 6 7 8 9 10 11 12 def process(figure_idx, figure_binary): context_above = \u0026#34;\u0026#34; context_below = \u0026#34;\u0026#34; if figure_idx \u0026lt; len(self.figure_contexts): context_above, context_below = self.figure_contexts[figure_idx] if context_above or context_below: prompt = vision_llm_figure_describe_prompt_with_context( context_above=context_above, context_below=context_below, ) else: prompt = vision_llm_figure_describe_prompt() 然后通过线程池并发调用：\n1 2 for idx, img_binary in enumerate(self.figures or []): futures.append(shared_executor.submit(process, idx, img_binary)) 等所有任务完成之后，将返回的描述文本写回：\n1 2 3 4 for future in as_completed(futures): figure_num, txt = future.result() if txt: self.descriptions[figure_num] = txt + \u0026#34;\\n\u0026#34;.join(self.descriptions[figure_num]) 最后再调用 _assemble() 重新组装：\n1 2 self._assemble() return self.assembled 对于 Markdown 分支来说，这里有两个细节：\n没有显式传入 figure_contexts，因此默认使用 vision_llm_figure_describe_prompt() 回填时会把模型输出和原始描述拼在一起，因此最终描述中理论上可能保留 \u0026quot;markdown image\u0026quot; 这个占位文本 picture_vision_llm_chunk() process() 里真正调用视觉模型的函数是 rag/app/picture.py 中的：\n1 def vision_llm_chunk(binary, vision_model, prompt=None, callback=None): 虽然名字叫 vision_llm_chunk，但其作用其实很直接，就是把图片交给 VLM 并返回描述文本。\n其主要步骤如下：\n1 2 3 4 5 6 7 8 9 10 11 12 with io.BytesIO() as img_binary: try: img.save(img_binary, format=\u0026#34;JPEG\u0026#34;) except Exception: img_binary.seek(0) img_binary.truncate() img.save(img_binary, format=\u0026#34;PNG\u0026#34;) img_binary.seek(0) ans = clean_markdown_block(vision_model.describe_with_prompt(img_binary.read(), prompt)) txt += \u0026#34;\\n\u0026#34; + ans return txt 这里做了几件事：\n先把 PIL.Image 编码成二进制 优先尝试保存为 JPEG，失败则退回 PNG 调用 vision_model.describe_with_prompt() 生成描述 用 clean_markdown_block() 清理模型输出中的 Markdown 包裹 因此这个函数返回的是一段纯文本，而不是结构化对象。\nvision_llm_figure_describe_prompt() 在 VisionFigureParser.__call__() 中，无上下文情况下使用的是：\n1 prompt = vision_llm_figure_describe_prompt() 而如果存在上下文，则切换到：\n1 2 3 4 prompt = vision_llm_figure_describe_prompt_with_context( context_above=context_above, context_below=context_below, ) 这两组 prompt 都定义在 rag/prompts/ 下。其核心约束是：\n只根据图中可见内容生成文本 如果是表格、柱状图、折线图这类“可枚举数据图”，则按固定字段输出 如果不是结构化数据图，则按空间顺序描述可见内容 不允许额外推断流程、功能或语义 也就是说，这一步生成的不是泛化摘要，而是偏向检索友好的图片文本表示。\nsections[idx] = (...) 最终在 rag/app/naive.py 中，增强结果是这样写回 section 的：\n1 2 3 4 sections[idx] = ( section_text + \u0026#34;\\n\\n\u0026#34; + \u0026#34;\\n\\n\u0026#34;.join([fig[0][1] for fig in boosted_figures]), sections[idx][1] ) 因此 Markdown 图像增强不会引入新的切片层级，而是把图片描述文本直接拼回现有 section。\n在这一轮增强之后，chunk() 才会真正进入 Markdown 专属的 chunk 合并逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 if is_markdown: merged_chunks = [] merged_images = [] chunk_limit = max(0, int(parser_config.get(\u0026#34;chunk_token_num\u0026#34;, 128))) current_text = \u0026#34;\u0026#34; current_tokens = 0 current_image = None for idx, sec in enumerate(sections): text = sec[0] if isinstance(sec, tuple) else sec sec_tokens = num_tokens_from_string(text) sec_image = section_images[idx] if section_images and idx \u0026lt; len(section_images) else None 这段代码表明，Markdown 的切片单位不是“原始全文直接按分隔符硬切”，而是：\n1 2 3 4 5 6 7 8 9 10 11 Markdown 文本 ↓ 按元素扫描成多个 section ↓ 每个 section 绑定对应图片 ↓ 图片描述增强（如果启用） ↓ 按 token 上限逐个累积合并 ↓ 得到最终 chunk RAGFlow 对 Markdown 没有直接调用通用的 naive_merge_with_images()，而是单独写了一套更简单的逻辑。其规则是：\n如果加入下一个 section 后仍未超过 chunk_token_num，则继续追加 如果会超过上限，就先把当前 chunk 落盘，再开启一个新的 chunk 如果开启新 chunk 时配置了 overlapped_percent，则保留上一 chunk 尾部的一部分文本作为重叠上下文 与此同时，当前 chunk 内涉及的所有图片会被不断 concat_img 合并 关键代码如下：\n1 2 3 4 5 6 7 8 9 10 11 if current_text and current_tokens + sec_tokens \u0026gt; chunk_limit: merged_chunks.append(current_text) merged_images.append(current_image) overlap_part = \u0026#34;\u0026#34; if overlapped_percent \u0026gt; 0: overlap_len = int(len(current_text) * overlapped_percent / 100) if overlap_len \u0026gt; 0: overlap_part = current_text[-overlap_len:] current_text = overlap_part current_tokens = num_tokens_from_string(current_text) current_image = current_image if overlap_part else None 这里可以看出两个特点：\n重叠是按字符长度截尾，而不是按 section 粒度重叠 图片是按 chunk 聚合的，只要 section 被并入同一个 chunk，其图片也会被拼到同一张图上 因此，一个最终的 Markdown chunk，本质上是：\n1 2 3 4 { \u0026#34;text\u0026#34;: \u0026#34;若干相邻 section 合并后的文本\u0026#34;, \u0026#34;image\u0026#34;: \u0026#34;这些 section 内图片拼接后的结果（如果有）\u0026#34; } 在 chunk 合并完成之后，RAGFlow 会根据该批 chunk 是否含图走两条不同路径：\n1 2 3 4 5 6 has_images = merged_images and any(img is not None for img in merged_images) if has_images: res.extend(tokenize_chunks_with_images(chunks, doc, is_english, merged_images, child_delimiters_pattern=child_deli)) else: res.extend(tokenize_chunks(chunks, doc, is_english, pdf_parser, child_delimiters_pattern=child_deli)) 其中：\ntokenize_chunks() 负责纯文本 chunk 的分词和字段封装 tokenize_chunks_with_images() 则会把对应图片写入文档对象的 image 字段 表格则另外通过 tokenize_table(tables, doc, is_english) 进入结果集 也就是说，Markdown 在 RAGFlow 中最后会被拆成三类可检索对象：\n普通文本 chunk 携带图片的多模态 chunk 表格对象 小结 至此，RAGFlow 对 Markdown 的“切片”逻辑就比较清楚了。它并不是简单地按固定长度裁文本，而是分成了几层：\n先识别表格、标题、代码块、列表、引用块和普通文本块 再根据图片引用所在行号，把图片绑定到对应 section 然后按 token 上限把多个相邻 section 合并为 chunk 最后把文本、图片、表格分别包装成可检索对象 从工程实现上看，这套方案的优点是：\n实现简单，可维护性高 比纯分隔符切片更保留 Markdown 结构 能较自然地支持图文混合检索 表格被单独抽出后，也便于做专门处理 当然，它也有一些局限，例如：\n图片归属依赖行号，精度有限 section 的粒度较粗，未做更深层的语义切分 表格与正文可能存在信息重复 但对一个通用 RAG 系统来说，这样的取舍是相当合理的。它没有追求复杂而昂贵的 Markdown AST 解析，而是用较低复杂度完成了“结构感知切片”。\n笔者认为，这也是 RAGFlow 值得学习的一点：很多时候，切片策略不一定要非常“聪明”，但一定要足够稳定、可解释，并且方便与后续检索流程对接。\n","date":"2026-04-14T20:06:34+08:00","permalink":"/p/ragflow%E5%88%87%E7%89%87%E7%AD%96%E7%95%A5%E8%A7%A3%E6%9E%90/","title":"RAGFlow切片策略解析"},{"content":"CS 224n Spring 2024: Assignment #3 a3_spr24_student_handout.pdf\n本post从cs224n独立出来，旨在尽可能掌握Assignment3中基于RNN的NMT背后各步的数学原理，以及将代码部分和数学部分对应起来。\n参考论文：\nNEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE\nNeural Machine Translation with RNNs Model description (training procedure) Given a sentence in the source language, we look up the character or word embeddings from an embeddings matrix, yielding $\\mathbf{x}_1, \\ldots, \\mathbf{x}_m$ ($\\mathbf{x}_i \\in \\mathbb{R}^{e \\times 1}$), where $m$ is the length of the source sentence and $e$ is the embedding size.\n我们手里的一条源语言句子，由于计算机无法直接理解文字，我们首先要进行“查表”操作，也就是文本中提到的获取词嵌入（look up \u0026hellip; embeddings matrix）。假设句子有 $m$ 个词，经过这一步，每个词都被映射成了一个长度为 $e$ 的列向量 $\\mathbf{x}_i$。这样一来，整句话就被转换成了一个由实数向量构成的序列\nWe then feed the embeddings to a convolutional layer$^1$ while maintaining their shapes.\n$^1$ : Checkout Convolutional Neural Networks for an in-depth description for convolutional layers if you are not familiar.\n带着这些初步的向量表示，模型并没有直接把它们送入主要的编码器，而是先让它们穿过一个卷积层（convolutional layer）。文本中特别强调了这一步“保持了它们的形状”，这意味着经过卷积处理后，我们依然拥有 $m$ 个向量，且每个向量的维度依然是 $e$。这一步的作用通常是对局部的特征进行一次平滑和提取，为后续更深层的语义理解打好基础。\nWe feed the convolutional layer outputs to the bidirectional encoder, yielding hidden states and cell states for both the forwards ($\\rightarrow$) and backwards ($\\leftarrow$) LSTMs. The forwards and backwards versions are concatenated to give hidden states $\\mathbf{h}_i^{\\text{enc}}$ and cell states $\\mathbf{c}_i^{\\text{enc}}$:\n接下来，这些被初步加工过的特征向量正式进入了核心组件——双向编码器（bidirectional encoder）。这里其实包含了两条流水线：一条是前向 LSTM（在数学符号中用向右的箭头 $\\rightarrow$ 表示），它顺着我们阅读的习惯，从第一个词读到最后一个词，负责收集每个词左侧的“上文”信息；另一条是后向 LSTM（用向左的箭头 $\\leftarrow$ 表示），它逆着顺序，从最后一个词倒着读回来，负责收集每个词右侧的“下文”信息。当这两条流水线各自运转完毕后，对于句子中的任意第 $i$ 个位置，我们就得到了两个不同视角的隐藏状态 $\\mathbf{h}_i^{\\text{enc}}$ 和细胞状态 $\\mathbf{c}_i^{\\text{enc}}$，前向( $\\overrightarrow{\\mathbf{h}_i^{\\text{enc}}}$ , $\\overrightarrow{\\mathbf{c}_i^{\\text{enc}}}$ )或后向( $\\overleftarrow{\\mathbf{h}_i^{\\text{enc}}}$ , $\\overleftarrow{\\mathbf{c}_i^{\\text{enc}}}$ )分别的状态量，维度都是 $h \\times 1$ 。\n$$ \\mathbf{h}_i^{\\text{enc}} = [\\overleftarrow{\\mathbf{h}_i^{\\text{enc}}} ; \\overrightarrow{\\mathbf{h}_i^{\\text{enc}}}] \\quad \\text{where} \\quad \\mathbf{h}_i^{\\text{enc}} \\in \\mathbb{R}^{2h \\times 1}, \\overleftarrow{\\mathbf{h}_i^{\\text{enc}}}, \\overrightarrow{\\mathbf{h}_i^{\\text{enc}}} \\in \\mathbb{R}^{h \\times 1} \\quad 1 \\leq i \\leq m \\quad (1) $$$$ \\mathbf{c}_i^{\\text{enc}} = [\\overleftarrow{\\mathbf{c}_i^{\\text{enc}}} ; \\overrightarrow{\\mathbf{c}_i^{\\text{enc}}}] \\quad \\text{where} \\quad \\mathbf{c}_i^{\\text{enc}} \\in \\mathbb{R}^{2h \\times 1}, \\overleftarrow{\\mathbf{c}_i^{\\text{enc}}}, \\overrightarrow{\\mathbf{c}_i^{\\text{enc}}} \\in \\mathbb{R}^{h \\times 1} \\quad 1 \\leq i \\leq m \\quad (2) $$ $[ ; ]$ 在线性代数中通常表示向量或矩阵的拼接。为了让第 $i$ 个位置最终的表示既包含左侧上下文，又包含右侧上下文，模型将同一时刻 $i$ 的前向状态和后向状态直接“拼接”在一起。因为前向和后向状态都是 $h \\times 1$ 的列向量，将它们沿行方向拼接后，最终的联合隐藏状态 $\\mathbf{h}_i^{\\text{enc}}$ 和联合细胞状态 $\\mathbf{c}_i^{\\text{enc}}$ 的维度就翻倍了，变成了 $(h+h) \\times 1 = \\mathbf{2h \\times 1}$。\nWe then initialize the decoder\u0026rsquo;s first hidden state $\\mathbf{h}_0^{\\text{dec}}$ and cell state $\\mathbf{c}_0^{\\text{dec}}$ with a linear projection of the encoder\u0026rsquo;s final hidden state and final cell state.$^2$\n$^2$ : If it’s not obvious, think about why we regard $[\\overleftarrow{\\mathbf{h}_1^{\\text{enc}}} , \\overrightarrow{\\mathbf{h}_m^{\\text{enc}}}]$ as the ‘final hidden state’ of the Encoder.\n双向编码器（Encoder）已经工作完毕，看完了整句源语言文本，并在每一个位置都留下了浓缩的上下文信息。接下来的任务就是把这些信息传递给解码器（Decoder），让它开始生成翻译。传递的过程就是通过下面公式中，初始化解码器在第0步的隐藏状态 $\\mathbf{h}_0^{\\text{dec}}$ 和细胞状态 $\\mathbf{c}_0^{\\text{dec}}$ 来完成的。解码器的初始状态是从编码器那里继承来的“最终状态”。\n$$ \\mathbf{h}_0^{\\text{dec}} = \\mathbf{W}_h [\\overleftarrow{\\mathbf{h}_1^{\\text{enc}}} ; \\overrightarrow{\\mathbf{h}_m^{\\text{enc}}}] \\quad \\text{where} \\quad \\mathbf{h}_0^{\\text{dec}} \\in \\mathbb{R}^{h \\times 1}, \\mathbf{W}_h \\in \\mathbb{R}^{h \\times 2h} \\quad (3) $$$$ \\mathbf{c}_0^{\\text{dec}} = \\mathbf{W}_c [\\overleftarrow{\\mathbf{c}_1^{\\text{enc}}} ; \\overrightarrow{\\mathbf{c}_m^{\\text{enc}}}] \\quad \\text{where} \\quad \\mathbf{c}_0^{\\text{dec}} \\in \\mathbb{R}^{h \\times 1}, \\mathbf{W}_c \\in \\mathbb{R}^{h \\times 2h} \\quad (4) $$ 编码器在“读完”整句话后的最终状态，可见公式：前向 LSTM 是顺着语序从左到右读的，所以当它读完最后一个词时，它的最终状态自然就落在了句尾，也就是第 $m$ 个位置，记作 $\\overrightarrow{\\mathbf{h}_m^{\\text{enc}}}$。相反，后向 LSTM 是倒着从右向左读的，它“通读全文”后的最后一站其实是句子的开头，也就是第 $1$ 个位置，记作 $\\overleftarrow{\\mathbf{h}_1^{\\text{enc}}}$。\n为了把前向和后向这两股贯穿全文的“记忆”汇聚起来，我们按照公式将它们进行了拼接操作 $[\\overleftarrow{\\mathbf{h}_1^{\\text{enc}}} ; \\overrightarrow{\\mathbf{h}_m^{\\text{enc}}}]$。（需注意，在Decoder这一步和公式 (1) (2) 的上下文状态量不一样）\n通过这个拼接，我们得到了一个维度为 $2h \\times 1$ 的长向量。同样地，对于细胞状态 $\\mathbf{c}$，我们用完全相同的逻辑提取出 $[\\overleftarrow{\\mathbf{c}_1^{\\text{enc}}} ; \\overrightarrow{\\mathbf{c}_m^{\\text{enc}}}]$。那么对于 $2h \\times 1$ 的向量，我们要将其转换成 $h\\times 1$ 才能满足解码器的隐藏层容量。\n所以引入权重（投影）矩阵，用来做线性投影(linear projection)，维度是 $h \\times 2h$ ，再通过矩阵相乘，映射出 $h\\times 1$ 的向量。\nWith the decoder initialized, we must now feed it a target sentence.\n成功初始化了解码器的第 0 步状态（$\\mathbf{h}_0^{\\text{dec}}$ 和 $\\mathbf{c}_0^{\\text{dec}}$）之后。现在，解码器已经准备好生成（或在训练时接收）目标语言的句子了。下面内容讲的就是在任意的第 $t$ 步，解码器是如何“吃进”数据并更新自己状态的。\nOn the $t^{th}$ step, we look up the embedding for the $t^{th}$ subword, $\\mathbf{y}_t \\in \\mathbb{R}^{e \\times 1}$.\n要让解码器在第 $t$ 步进行工作，我们首先得给它提供当前的输入词，同样需要查表，把目标语言的第 $t$ 个子词（subword）变成一个维度为 $e$ 的词嵌入向量，记作 $\\mathbf{y}_t$。\nWe then concatenate $\\mathbf{y}_t$ with the combined-output vector $\\mathbf{o}_{t-1} \\in \\mathbb{R}^{h \\times 1}$ from the previous timestep (we will explain what this is later down this page!) to produce $\\overline{\\mathbf{y}_t} \\in \\mathbb{R}^{(e+h) \\times 1}$.\n但是，如果只把 $\\mathbf{y}_t$ 喂给解码器，它会缺乏连贯性。为了让解码器知道“我刚才干了什么”，文本中引入了一个非常关键的设计：把当前的词向量 $\\mathbf{y}_t$ 与上一步（第 $t-1$ 步）的维度为 $h$ 的联合输出向量 (combined-output vector) $\\mathbf{o}_{t-1}$ 拼接在一起，得到新的输入向量 $\\overline{\\mathbf{y}_t}$，它的维度变成了 $e+h$。\nNote that for the first target subword (i.e. the start token) $\\mathbf{o}_0$ is a zero-vector. We then feed $\\overline{\\mathbf{y}_t}$ as input to the decoder.\n注意到当 $t=1$ ，也就是解码器刚开始吐出第一个词（或者接收 start token）时，因为前面还没有任何输出，所以初始的 $\\mathbf{o}_0$ 就简单地设定为一个全零向量。\n$$ \\mathbf{h}_t^{\\text{dec}}, \\mathbf{c}_t^{\\text{dec}} = \\text{Decoder}(\\overline{\\mathbf{y}_t}, \\mathbf{h}_{t-1}^{\\text{dec}}, \\mathbf{c}_{t-1}^{\\text{dec}}) \\quad \\text{where} \\quad \\mathbf{h}_t^{\\text{dec}} \\in \\mathbb{R}^{h \\times 1}, \\mathbf{c}_t^{\\text{dec}} \\in \\mathbb{R}^{h \\times 1} \\quad (5) $$ 关于向Decoder中输入了三个变量后，在LSTM的Decoder里进行了什么计算，需结合CS224N中LSTM的部分进行理解：(generated by Gemini)\n遗忘门 (Forget Gate) 参与者： 新输入$\\overline{\\mathbf{y}_t}$和旧的短期记忆$\\mathbf{h}_{t-1}^{\\text{dec}}$。 动作： 把这两个变量拼接在一起，乘上一个权重矩阵，加上偏置，然后送入一个 Sigmoid 激活函数。 产出： 得到一个介于 0 到 1 之间的向量 $f_t$。 意义： 这个 $f_t$ 会盯着传送带上的旧长期记忆 $\\mathbf{c}_{t-1}^{\\text{dec}}$ 看。0 代表“彻底忘掉”，1 代表“完全保留”。比如遇到新的主语，它可能就会决定忘掉旧主语的单复数信息。 输入门与候选记忆 (Input Gate \u0026amp; Candidate Memory) 参与者： 依然是 $\\overline{\\mathbf{y}_t}$ 和 $\\mathbf{h}_{t-1}^{\\text{dec}}$。 动作： 这里分两头行动： 输入门 $i_t$： 再次经过一个 Sigmoid 函数，产生 0 到 1 的值，决定我们有多希望把新信息存进去。 候选记忆 $\\tilde{\\mathbf{c}}_t$： 经过一个 tanh 激活函数，产生 -1 到 1 的值，这代表从当前输入中提取出的全部潜在新知识。 意义： 这两步结合，就是要把提炼出的新知识（$\\tilde{\\mathbf{c}}_t$）按照我们的渴望程度（$i_t$）进行打包，准备放到传送带上。 状态更新 (Cell State Update) 参与者： 旧记忆 $\\mathbf{c}_{t-1}^{\\text{dec}}$，遗忘门 $f_t$，输入门 $i_t$，候选记忆 $\\tilde{\\mathbf{c}}_t$。 动作： 纯粹的数学运算。首先用旧记忆乘以遗忘门：$f_t * \\mathbf{c}_{t-1}^{\\text{dec}}$ （执行“丢弃”动作）；然后加上新的包裹：$i_t * \\tilde{\\mathbf{c}}_t$ （执行“装载”动作）。 产出： 我们得到了当前时刻的全新细胞状态 $\\mathbf{c}_t^{\\text{dec}}$。 意义： 长期记忆的传送带在这里完成了向前推进，旧的糟粕被剔除，新的信息被注入。这就是 LSTM 能够跨越长距离保持梯度的核心。 输出门与隐藏状态 (Output Gate \u0026amp; Hidden State) 参与者： $\\overline{\\mathbf{y}_t}$，$\\mathbf{h}_{t-1}^{\\text{dec}}$，以及刚刚出炉的新细胞状态 $\\mathbf{c}_t^{\\text{dec}}$。 动作： 用 $\\overline{\\mathbf{y}_t}$ 和 $\\mathbf{h}_{t-1}^{\\text{dec}}$ 经过 Sigmoid 算出一个输出门 $o_t$（决定展示的比例）。 把刚刚做好的新细胞状态 $\\mathbf{c}_t^{\\text{dec}}$ 用 tanh 函数“压”到 -1 到 1 之间。 两者相乘：$\\mathbf{h}_t^{\\text{dec}} = o_t * \\tanh(\\mathbf{c}_t^{\\text{dec}})$。 产出： 我们得到了当前时刻的全新隐藏状态 $\\mathbf{h}_t^{\\text{dec}}$。 意义： 细胞状态 $\\mathbf{c}_t$ 包含的信息太庞杂了（有些可能是给未来留的伏笔），我们不能全部暴露。输出门 $o_t$ 就像一个滤网，只把当前这一步预测下一个词最需要的那部分特征提取出来，作为对外的公开展示（$\\mathbf{h}_t$）。 We then use $\\mathbf{h}_t^{\\text{dec}}$ to compute multiplicative attention over $\\mathbf{h}_1^{\\text{enc}}, \\dots, \\mathbf{h}_m^{\\text{enc}}$: $$ \\mathbf{e}_{t,i} = (\\mathbf{h}_t^{\\text{dec}})^T \\mathbf{W}_{\\text{attProj}} \\mathbf{h}_i^{\\text{enc}} \\quad \\text{where} \\quad \\mathbf{e}_t \\in \\mathbb{R}^{m \\times 1}, \\mathbf{W}_{\\text{attProj}} \\in \\mathbb{R}^{h \\times 2h} \\quad 1 \\leq i \\leq m \\quad (6) $$ $\\mathbf{e}_{t,i}$ is a scalar, the $i$ th element of $\\mathbf{e}_t \\in \\mathbb{R}^{m \\times 1}$, computed using the hidden state of the decoder at the $t$ th step, $\\mathbf{h}_t^{\\text{dec}} \\in \\mathbb{R}^{h \\times 1}$, the attention projection $\\mathbf{W}_{\\text{attProj}} \\in \\mathbb{R}^{h \\times 2h}$, and the hidden state of the encoder at the $i$ th step, $\\mathbf{h}_i^{\\text{enc}} \\in \\mathbb{R}^{2h \\times 1}$.\n公式(6)是注意力机制的Scoring，解码器现在的状态是 $\\mathbf{h}_t^{\\text{dec}}$，它需要和字典里的每一个词 $\\mathbf{h}_i^{\\text{enc}}$进行比对，看看有多匹配。\n$(\\mathbf{h}_t^{\\text{dec}})^T$ , $\\mathbf{W}_{\\text{attProj}}$ , $\\mathbf{h}_i^{\\text{enc}}$ 这三者相乘后维度为 $1\\times 1$ ，即Scoring出的标量 $\\mathbf{e}_{t,i}$ ，它代表了在生成第 $t$ 个翻译词时，源语言句子中第 $i$ 个词的重要程度。 我们把对所有 $m$ 个词的打分收集起来，就得到了一个长度为 $m$ 的得分向量 $\\mathbf{e}_t$ 。\n$$ \\alpha_t = \\text{softmax}(\\mathbf{e}_t) \\quad \\text{where} \\quad \\alpha_t \\in \\mathbb{R}^{m \\times 1} \\quad (7) $$ 为了把公式 (6) 的得分变成标准的“注意力分配比例”，公式 (7) 引入了 softmax 函数。它把向量 $\\mathbf{e}_t$ 里的所有数字全部转换为 $0$ 到 $1$ 之间的正数，并且保证它们的总和严格等于 $1$。转换后的结果就是 $\\alpha_t$（被称为注意力分布 Attention Distribution）。\n$$ \\mathbf{a}_t = \\sum_{i=1}^m \\alpha_{t,i} \\mathbf{h}_i^{\\text{enc}} \\quad \\text{where} \\quad \\mathbf{a}_t \\in \\mathbb{R}^{2h \\times 1} \\quad (8) $$ 公式 (8) 用刚算出来的注意力百分比 $\\alpha_{t,i}$，去对字典里的词 $\\mathbf{h}_i^{\\text{enc}}$ 进行加权求和。把这 $m$ 个按比例缩放的向量全部加起来，我们就得到了最终的注意力输出向量（Attention Output / Context Vector），记作 $\\mathbf{a}_t$。 由于被加和的 $\\mathbf{h}_i^{\\text{enc}}$ 都是 $2h \\times 1$ 维的，所以最终得到的 $\\mathbf{a}_t$ 也是 $2h \\times 1$ 维。\nWe now concatenate the attention output $\\mathbf{a}_t$ with the decoder hidden state $\\mathbf{h}_t^{\\text{dec}}$ and pass this through a linear layer, tanh, and dropout to attain the combined-output vector $\\mathbf{o}_t$. $$ \\mathbf{u}_t = [\\mathbf{a}_t ; \\mathbf{h}_t^{\\text{dec}}] \\quad \\text{where} \\quad \\mathbf{u}_t \\in \\mathbb{R}^{3h \\times 1} \\quad (9) $$ 将注意力输出向量和解码器隐藏状态结合，得到维度为 $3h\\times 1$ 的 $u_t$\n$$ \\mathbf{v}_t = \\mathbf{W}_u \\mathbf{u}_t \\quad \\text{where} \\quad \\mathbf{v}_t \\in \\mathbb{R}^{h \\times 1}, \\mathbf{W}_u \\in \\mathbb{R}^{h \\times 3h} \\quad (10) $$ 加入了一个线性投影矩阵 $\\mathbf{W}_u$（维度是 $h \\times 3h$ ，当它和 $\\mathbf{u}_t$ 相乘时，把维度压缩回了标准尺寸 $h \\times 1$\n$$ \\mathbf{o}_t = \\text{dropout}(\\tanh(\\mathbf{v}_t)) \\quad \\text{where} \\quad \\mathbf{o}_t \\in \\mathbb{R}^{h \\times 1} \\quad (11) $$ 首先给它套上一个 tanh 激活函数，将其内部的数值平滑地压缩到 -1 到 1 之间，这赋予了模型非线性的表达能力。紧接着，再让它穿过一层 dropout。dropout 是一种防止模型过拟合的正则化技术，它在训练时会随机屏蔽掉一部分神经元，逼迫模型学到更鲁棒、更泛化的特征。\n最后得到联合输出向量(combined-output vector) $\\mathbf{o}_t$\nThen, we produce a probability distribution $\\mathbf{P}_t$ over target subwords at the $t^{th}$ timestep: $$ \\mathbf{P}_t = \\text{softmax}(\\mathbf{W}_{\\text{vocab}}\\mathbf{o}_t) \\quad \\text{where} \\quad \\mathbf{P}_t \\in \\mathbb{R}^{V_t \\times 1}, \\mathbf{W}_{\\text{vocab}} \\in \\mathbb{R}^{V_t \\times h} \\quad (12) $$ 我们要把一个 $h$ 维的向量，变成目标语言词典里的一个具体词汇。我们的目标语言词典（Vocabulary）里一共有 $V_t$ 个单词。公式 (12) 引入了一个最终的变换矩阵 $\\mathbf{W}_{\\text{vocab}}$，它的维度是 $V_t \\times h$。相乘后输出一个 $[V_t \\times 1]$ 的列向量。这个向量里的每一个数字，就代表了模型对词典里对应单词的“打分”（Logits）。\nHere, $V_t$ is the size of the target vocabulary. Finally, to train the network we then compute the cross entropy loss between $\\mathbf{P}_t$ and $\\mathbf{g}_t$, where $\\mathbf{g}_t$ is the one-hot vector of the target subword at timestep $t$: $$ J_t(\\theta) = \\text{CrossEntropy}(\\mathbf{P}_t, \\mathbf{g}_t) \\quad (13) $$ 训练过程使用了交叉熵损失 (Cross Entropy Loss) 函数。简单来说，交叉熵会去对比 $\\mathbf{P}_t$ 和 $\\mathbf{g}_t$ (one-hot vector)之间的差距。计算出损失 $J_t(\\theta)$ 之后，在接下来的代码实现中，模型就会利用反向传播机制，顺着网络一路往回找，去微调那些导致错误的参数 $\\theta$。\nImplementation and written questions __init__() 在问题 (c) 的nmt_model.py的__inti__()中，关于神经网络各层的定义，其中self.att_projection的定义需注意，虽然它对应的是 $W_{attProj}$ ，但是在编写代码时，要拆解结合律，决定计算顺序。\n应该先算$\\mathbf{W}_{\\text{attProj}} $ 和 $\\mathbf{h}_i^{\\text{enc}}$ 相乘，再将其乘积与 $(\\mathbf{h}_t^{dec})^T$ 相乘求最终结果，这是因为所有的编码器状态 $\\mathbf{h}_i^{\\text{enc}}$ 在 Decoder 开始工作前就已经全部计算好了。你可以一次性把整句话的 $\\mathbf{h}^{\\text{enc}}$ 丢进 Linear 层进行投影（这叫 Pre-computation 预计算）。但是在某一时刻只有一个 $\\mathbf{h}_t^{dec}$ 而没有未来的量，导致不能一次性投影完成，而只能把线性层放到了循环中，引发严重的性能问题（虽然数学原理上没错）。下面是一个例子(generated by Gemini) :\n假设我们的模型参数如下：\n隐藏层大小 $h = 512$ 句子长度 $m = 50$ 把 Linear 层塞进循环里\n在这个路线里，Linear 层每次要干的活是：把解码器 $1 \\times 512$ 的向量，乘以一个 $512 \\times 1024$ 的巨大投影矩阵 $\\mathbf{W}$。\n计算量：这需要进行 $512 \\times 1024 = \\mathbf{524,288}$ 次乘加运算！ 额外开销：每次循环，PyTorch 都要去内存里把这个包含 50 多万个参数的 $\\mathbf{W}$ 矩阵重新搬出来读一遍，并且要经过一层完整的 nn.Linear 封装代码（包含各种维度检查、梯度跟踪的准备）。 然后，算出投影结果后，还得再去和字典做点积：$1 \\times 1024$ 的向量乘以 $1024 \\times 50$ 的字典，又是 $\\mathbf{51,200}$ 次运算。\n所以在这个路线下，解码器每走一步，都要背着将近 60万次运算 和一个巨大的矩阵跑。\n预计算 + 循环内纯点积\n我们把庞大的 Linear 投影放在循环外面，利用 GPU 的超级并行能力，**“一瞬间”**把整本字典从 $50 \\times 1024$ 压成了 $50 \\times 512$。我们把这本新字典称为 “投影后字典”。\n现在我们进入了 for 循环。在第 $t$ 步，我们需要做的乘法是什么呢？\n计算量：我们直接拿解码器原生的 $1 \\times 512$ 向量，去乘以准备好的“投影后字典”（$512 \\times 50$）。只需要进行 $512 \\times 50 = \\mathbf{25,600}$ 次乘加运算！ 没有额外的层开销：这里不需要调用 nn.Linear，我们在代码里只要写一个极其轻量的纯矩阵乘法指令（比如 torch.matmul 或 @ 符号）就搞定了。不需要加载任何权重矩阵，因为权重矩阵的任务在循环外已经完成了！ encode 问题 (d) 是encoder部分，以及对decoder部分的初始化：\n1 self.model_embeddings = ModelEmbeddings(embed_size, vocab) self.model_embeddings是对ModelEmbeddings的一个实例化，而回到问题 (a) 发现.source是一个nn.Embedding对象：\n1 2 self.source = nn.Embedding(num_embeddings=len(vocab.src),embedding_dim=self.embed_size, padding_idx=src_pad_token_idx) 但是在问题 (a) 中只是对它的实例化，在内存中申请了一个形状为 (num_embeddings, embedding_dim) 的大矩阵。\n1 X = self.model_embeddings.source(source_padded) 又因为 nn.Embedding 继承自 nn.Module，而 nn.Module 重写了 __call__ 方法，所以你可以像函数一样使用它（调用），参数设为source_padded这样一个Tensor，它把 Tensor 里的每一个数字当成“行号”，去刚才创建的那个大矩阵里把对应的行找出来，然后输出包含词向量的 Tensor。\n下面在将Tensor送进Encoder之前，要用torch.permute变换形状，从(src_len, b, e)变到(b, e, src_len)。那么torch.permute的参数，只是参数位置（索引号）。如下，将原来索引为1的b放到第0个位置，原来索引为2的放到第1个位置，原来索引为0的放到第2个位置。\n1 X = X.permute(1, 2, 0) 之后将Tensor X送进LSTM的Encoder后，得到输出：\n1 2 output,(last_hidden,last_cell) = self.encoder(X) output对应的就是双向LSTM的全局上下文向量： $\\mathbf{h}_1^{\\text{enc}}, \\ldots, \\mathbf{h}_m^{\\text{enc}}$\nlast_hidden对应的是：$\\overrightarrow{\\mathbf{h}_m^{\\text{enc}}}$ 和 $\\overleftarrow{\\mathbf{h}_1^{\\text{enc}}}$ ，它的形状是 (num_layers * num_directions, batch_size, hidden_size) ，由于它是双向的，所以索引0和索引1分别对应，我们需要将其分离再拼接，形成我们需要的$\\overleftarrow{\\mathbf{h}_1^{\\text{enc}}} ; \\overrightarrow{\\mathbf{h}_m^{\\text{enc}}}$ 。注意torch.cat的参数dim=1表示为横向拼接，即拼接好的向量的列数变成了2 * hidden_size\n与投影矩阵相乘的部分，由实例化好的线性层self.h_projection完成。\n1 init_decoder_hidden = self.h_projection(torch.cat((last_hidden[0],last_hidden[1]),dim=1)) last_cell同理，不再赘述。\ndecode 问题 (e) 是decode部分： enc_hiddens_proj = self.att_projection(enc_hiddens) 对应公式 (6) 中 $\\mathbf{W}_{\\text{attProj}} \\mathbf{h}_i^{\\text{enc}}$ 的部分。正如在 __init__() 部分讨论的，编码器的全部隐藏状态在进入 decode 时已经固定，所以可以在循环外一次性完成投影预计算，将 enc_hiddens 从 (b, src_len, 2h) 投影为 (b, src_len, h)。\nY = self.model_embeddings.target(target_padded) 和 encode 中对源语言做查表的操作对称，这里调用目标语言的嵌入矩阵，将 target_padded 中的词索引映射成词向量，得到形状为 (tgt_len, b, e) 的张量 Y。\ntorch.split(Y, 1) 沿第 0 维（时间维度）将 Y 切成一系列 (1, b, e) 的张量。squeeze(Y_t, 0) 去掉多余的维度变为 (b, e)。注意必须指定 dim=0，否则当 batch_size = 1 时会误删 batch 维度。\ntorch.cat((Y_t, o_prev), dim=1) 对应前文的拼接操作：将 $\\mathbf{y}_t$（维度 $e$）与 $\\mathbf{o}_{t-1}$（维度 $h$）拼接为 $\\overline{\\mathbf{y}_t} \\in \\mathbb{R}^{(e+h)}$。\nself.step(...) 内部一次性完成公式 (5)-(11) 的计算：Decoder LSTM 前向传播 → 注意力评分与分布 → 加权求和得到上下文向量 → 线性投影、tanh、dropout，最终产出联合输出向量 $\\mathbf{o}_t$。每步将 $\\mathbf{o}_t$ 存入列表并更新 o_prev。\ntorch.stack(combined_outputs, dim=0) 将列表中所有 (b, h) 的张量堆叠为 (tgt_len, b, h)，随后送入公式 (12) 的词汇投影层生成概率分布。\nstep 问题 (f) 是step部分，即解码器单步计算，内部完成公式 (5)-(11)。核心代码分两段：\n第一段（~3行）：Decoder LSTM 前向 + 注意力评分\n1 2 3 dec_state = self.decoder(Ybar_t, dec_state) dec_hidden, dec_cell = dec_state e_t = torch.bmm(enc_hiddens_proj, torch.unsqueeze(dec_hidden, 2)).squeeze(2) 前两行直接对应公式 (5)：将 $\\overline{\\mathbf{y}_t}$ 和上一步状态送入 Decoder LSTM，得到新的 dec_state，再拆分为 dec_hidden（$\\mathbf{h}_t^{\\text{dec}}$）和 dec_cell（$\\mathbf{c}_t^{\\text{dec}}$），形状均为 (b, h)。\n第三行对应公式 (6) 中的注意力评分。这里的关键在于 torch.bmm 的形状要求——它执行批量矩阵乘法，要求输入严格为三维张量 (b, n, m) × (b, m, p) → (b, n, p)。而我们手里的张量：\nenc_hiddens_proj：形状 (b, src_len, h) — 已经是三维，无需处理 dec_hidden：形状 (b, h) — 只有二维，不满足 bmm 的要求 所以需要 torch.unsqueeze(dec_hidden, 2) 在第 2 维（最后）插入一个维度，将 (b, h) 变为 (b, h, 1)。这样 bmm 的乘法就变成了：\n$$\\underbrace{(b, \\text{src\\_len}, h)}_{\\text{enc\\_hiddens\\_proj}} \\times \\underbrace{(b, h, 1)}_{\\text{dec\\_hidden}} = \\underbrace{(b, \\text{src\\_len}, 1)}_{e_t}$$这实质上就是对 batch 内的每一条样本，让 enc_hiddens_proj 的每一行（某个源词的投影）与 dec_hidden 做点积，得到该源词的注意力分数——正是公式 (6) 的 $(\\mathbf{h}_t^{\\text{dec}})^T \\mathbf{W}_{\\text{attProj}} \\mathbf{h}_i^{\\text{enc}}$。\n最后 .squeeze(2) 去掉末尾多余的维度 1，从 (b, src_len, 1) 变回 (b, src_len)，得到注意力得分向量 $\\mathbf{e}_t$。\n第二段（~6行）：注意力加权 → 联合输出\n1 2 3 4 5 alpha_t = F.softmax(e_t, dim=1) a_t = torch.bmm(torch.unsqueeze(alpha_t, 1), enc_hiddens).squeeze(1) U_t = torch.cat((a_t, dec_hidden), dim=1) V_t = self.combined_output_projection(U_t) O_t = self.dropout(torch.tanh(V_t)) F.softmax(e_t, dim=1) 对应公式 (7)，沿 dim=1（即 src_len 维度）做 softmax，将分数归一化为注意力分布 $\\alpha_t$，形状仍为 (b, src_len)。\n计算上下文向量 $\\mathbf{a}_t$（公式 (8)）时又遇到了 bmm 的三维要求。alpha_t 是 (b, src_len)，需要 unsqueeze(alpha_t, 1) 在第 1 维插入，变为 (b, 1, src_len)：\n$$\\underbrace{(b, 1, \\text{src_len})}_{\\alpha_t} \\times \\underbrace{(b, \\text{src_len}, 2h)}_{\\text{enc_hiddens}} = \\underbrace{(b, 1, 2h)}_{a_t}$$这就是用注意力权重对编码器隐藏状态做加权求和。.squeeze(1) 去掉中间的 1，得到 (b, 2h) 的上下文向量 $\\mathbf{a}_t$。\n后三行依次对应公式 (9)(10)(11)：torch.cat 拼接 $\\mathbf{a}_t$ 与 $\\mathbf{h}_t^{\\text{dec}}$ 得到 (b, 3h) 的 $\\mathbf{u}_t$；线性层投影回 (b, h) 的 $\\mathbf{v}_t$；最后 tanh + dropout 得到联合输出 $\\mathbf{o}_t$。\nAttention Masking 问题 (g) 是关于 step() 中注意力掩码的作用。在 step() 的两段代码之间，有这样一段：\n1 2 if enc_masks is not None: e_t.data.masked_fill_(enc_masks.bool(), -float(\u0026#39;inf\u0026#39;)) enc_masks 由 generate_sent_masks() 生成：它创建一个 (b, src_len) 的零矩阵，然后对 batch 中每条句子，在其实际长度之后的位置全部填 1。换言之，1 标记的是 \u0026lt;pad\u0026gt; token 的位置，0 标记的是真实词的位置。\n掩码对注意力计算的影响： masked_fill_ 将 $\\mathbf{e}_t$ 中所有对应 \u0026lt;pad\u0026gt; 位置的注意力分数替换为 $-\\infty$。当这些 $-\\infty$ 的值随后经过 softmax 时，$e^{-\\infty} = 0$，因此 \u0026lt;pad\u0026gt; 位置的注意力权重 $\\alpha_{t,i}$ 会变为 $0$，而所有真实词的权重之和仍归一化为 $1$。这意味着在公式 (8) 的加权求和中，\u0026lt;pad\u0026gt; 位置的编码器隐藏状态对上下文向量 $\\mathbf{a}_t$ 完全没有贡献。\n为什么必须这样做： 由于 batch 内各句子长度不一，短句会被 \u0026lt;pad\u0026gt; 填充到统一长度。如果不加掩码，\u0026lt;pad\u0026gt; 对应位置的编码器隐藏状态（本质上是无意义的噪声）会分走一部分注意力权重，从而污染上下文向量，导致翻译质量下降。\nTraining 问题 (h) 是在代码工作完成后，进入训练阶段。由于我没有海外支付方式，用不了cs224n指定的Google Cloud，所以使用本机显卡进行训练（速度快于文档的预估值）\n1 bash run.sh train 观察tensorboard可见loss曲线并无异常，待训练完成后进行测试评估： 1 bash run.sh test 结果：the model’s corpus BLEU Score is larger than 18, tests passed.\nAttention Comparison 最后问题 (i) 是点积注意力、加性注意力分别与乘性注意力进行比较：\nDot Product vs. Multiplicative Dot Product Attention: $\\mathbf{e}_{t,i} = \\mathbf{s}_t^T \\mathbf{h}_i$ Multiplicative Attention: $\\mathbf{e}_{t,i} = \\mathbf{s}_t^T \\mathbf{W} \\mathbf{h}_i$ 点积注意力的优势：\n计算速度更快，显存占用更小。 点积注意力没有任何可学习的权重矩阵 $\\mathbf{W}$，它仅仅是两个向量的内积。在 GPU 上，这种纯粹的向量点积运算被优化到了极致，没有任何额外的内存开销和参数更新负担。 点积注意力的劣势：\n强制要求维度严格一致，且表达能力较弱。 要做点积，解码器状态 $\\mathbf{s}_t$ 和编码器状态 $\\mathbf{h}_i$ 的维度必须完全相同。更致命的是，它假设这两个空间天然就是对齐的。而乘性注意力多了一个矩阵 $\\mathbf{W}$，不仅允许两者的维度不同（$\\mathbf{W}$ 可以做维度转换），还能通过学习 $\\mathbf{W}$ 将它们投影到一个更好的共享特征空间中再进行比较。 Additive vs. Multiplicative Additive Attention: $\\mathbf{e}_{t,i} = \\mathbf{v}^T \\tanh(\\mathbf{W}_1 \\mathbf{h}_i + \\mathbf{W}_2 \\mathbf{s}_t)$ Multiplicative Attention: $\\mathbf{e}_{t,i} = \\mathbf{s}_t^T \\mathbf{W} \\mathbf{h}_i$ (注：Additive Attention 也就是大名鼎鼎的 Bahdanau Attention，它是 Attention 机制的开山鼻祖；而 Multiplicative 则是 Luong Attention 的核心。)\n加性注意力的优势：\n在特征维度很大时，表现通常更好（模型容量大）。 乘性注意力在维度很大时，点积的结果方差会变得极其巨大，容易把 Softmax 推向梯度消失的边缘（也就是后来 Transformer 引入缩放因子的原因）。而加性注意力通过 $\\tanh$ 激活函数将内部数值稳稳地压制在 $[-1, 1]$ 之间，天然具有极好的数值稳定性，对超大维度的宽容度更高。 加性注意力的劣势：\n计算效率较低，难以发挥底层矩阵乘法的极致加速。 乘性注意力可以极其优雅地打包成一个巨大的矩阵乘法（在 Transformer 里就是 $\\mathbf{Q}\\mathbf{K}^T$），这正是现代 GPU 最擅长的事情。而加性注意力不仅要做两次独立的线性变换，还要过一遍非线性激活函数 $\\tanh$，最后再乘一个向量 $\\mathbf{v}$。这种复杂的计算图打破了矩阵乘法的纯粹性，导致它在实际工程中的运行速度明显慢于乘性注意力。 Analyzing NMT Systems 问题 (a) 是为什么在embedding层后加1D卷积后，再输入双向encoder效果会好一些？\n添加一维卷积层可以作为 n-gram 特征提取器，用于捕捉局部组合性。由于中文词语通常由多个词素组成（例如，“电”+“脑”=“电脑”），一维卷积神经网络的滑动窗口会在序列建模之前，将相邻字符/子词的嵌入向量显式地组合成更高层次的语义表示（一个词或短语）。这提供了一种层级结构，其中卷积神经网络处理局部词汇语义（充当软分词器），从而使双向编码器能够专注于学习全局的、长程的句法依存关系。\n问题 (b) 是分析四句中文为什么翻译错了，那么作为普通话母语者不难解答：\ni. (2 points) Source Sentence: 贼人其后被警方拘捕及被判处盗窃罪名成立。 Reference Translation: the culprits were subsequently arrested and convicted. NMT Translation: the culprit was subsequently arrested and sentenced to theft.\nii. (2 points) Source Sentence: 几乎已经没有地方容纳这些人, 资源已经用尽。 Reference Translation: there is almost no space to accommodate these people, and resources have run out. NMT Translation: the resources have been exhausted and resources have been exhausted.\niii. (2 points) Source Sentence: 当局已经宣布今天是国殇日。 Reference Translation: authorities have announced a national mourning today. NMT Translation: the administration has announced today\u0026rsquo;s day. NMT Translation: the administration has announced today\u0026rsquo;s day.\niv. (2 points) Source Sentence: 俗语有云:“唔做唔错”。 Reference Translation: “ act not, err not ”, so a saying goes. NMT Translation: as the saying goes, “ it\u0026rsquo;s not wrong. ”\n(i) 是单/复数的问题，缺少上下文以及整体理解，难以判断“贼人”是复数还是单数。 (ii) “资源已经用尽”被重复翻译了两遍，简单来说是注意力先偏移到了后半句，然后NMT不知道前半句没有翻译，加上前面错译出的\u0026quot;\u0026hellip;and\u0026quot;，导致重复翻译后半句。 (iii) src.vocab没有记录“国殇”，所以NMT越过了这个词。 (iv) 本质是句俗语，或者说是粤语，而多数模型都是基于普通话训练的。 BLEU BLEU score is the most commonly used automatic evaluation metric for NMT systems. It is usually calculated across the entire test set, but here we will consider BLEU defined for a single example. Suppose we have a source sentence s, a set of k reference translations $\\mathbf{r}_1,\\dots,\\mathbf{r}_k$, and a candidate translation c. To compute the BLEU score of c, we first compute the modified n-gram precision pn of c, for each of n = 1,2,3,4, where n is the n in n-gram:\nBLEU 分数是 NMT (神经机器翻译) 系统中最常用的自动评估指标。它通常在整个测试集上计算，但在这里我们将考虑为单个示例定义的 BLEU。假设我们有一个源句子 $\\mathbf{s}$，一组 $k$ 个参考翻译 $\\mathbf{r}_1,\\dots,\\mathbf{r}_k$，以及一个候选翻译 $\\mathbf{c}$。为了计算 $\\mathbf{c}$ 的 BLEU 分数，我们首先计算 $\\mathbf{c}$ 的修正 n-gram 精确度 (modified n-gram precision) $p_n$，其中 $n = 1, 2, 3, 4$，$n$ 是 n-gram 中的 $n$：\n$$ p_n = \\frac{\\sum_{\\text{ngram} \\in \\mathbf{c}} \\min \\left( \\max_{i=1,\\dots,k} \\text{Count}_{\\mathbf{r}_i}(\\text{ngram}), \\text{Count}_{\\mathbf{c}}(\\text{ngram}) \\right)}{\\sum_{\\text{ngram} \\in \\mathbf{c}} \\text{Count}_{\\mathbf{c}}(\\text{ngram})} $$Here, for each of the n-grams that appear in the candidate translation c, we count the maxi mum number of times it appears in any one reference translation, capped by the number of times it appears in c (this is the numerator). We divide this by the number of n-grams in c (denominator).\n这里，对于出现在候选翻译 $\\mathbf{c}$ 中的每一个 $n$-gram，我们计算它在任何一个参考翻译中出现的最大次数，上限为它在 $\\mathbf{c}$ 中出现的次数（这是分子）。我们将其除以 $\\mathbf{c}$ 中 $n$-gram 的数量（分母）。\n$p_n$ 公式的分母部分比较清晰，分子部分我们继续拆解：\n$\\text{Count}_{\\mathbf{c}}(\\text{ngram})$ : 该词组在模型输出中出现几次。\n$\\max_{i=1,\\dots,k} \\text{Count}_{\\mathbf{r}_i}(\\text{ngram})$ : 该词组在所有参考答案中出现次数最多的那一次。比如三个参考答案里，the 分别出现了 1、2、1 次，那么这个值就是 2。\n$\\min(\\dots)$ : 这是**截断（Clipping）**机制。它规定：即便你在输出里写了 10 个 the，如果参考答案里最多只出现了 2 个，那我也只算你命中了 2 个。\n综上，公式可以被翻译成： $$ \u003e p_n = \\frac{\\text{被认可的 n-gram 匹配总数}}{\\text{候选翻译中总共生成的 n-gram 数量}} \u003e $$ Next, we compute the brevity penalty BP. Let $len(\\mathbf{c})$ be the length of c and let $len(\\mathbf{r})$ be the length of the reference translation that is closest to $len(\\mathbf{c})$ (in the case of two equally-close reference translation lengths, choose $len(\\mathbf{r})$ as the shorter one).\n接下来，我们计算长度惩罚 (brevity penalty) $BP$。令 $len(\\mathbf{c})$ 为 $\\mathbf{c}$ 的长度，令 $len(\\mathbf{r})$ 为最接近 $len(\\mathbf{c})$ 的参考翻译的长度（在有两个同样接近的参考翻译长度的情况下，选择较短的那个作为 $len(\\mathbf{r})$）。\n$$ BP = \\begin{cases} 1 \u0026 \\text{if } len(\\mathbf{c}) \\geq len(\\mathbf{r}) \\\\ \\exp\\left(1 - \\frac{len(\\mathbf{r})}{len(\\mathbf{c})}\\right) \u0026 \\text{otherwise} \\end{cases} $$ 那么引入 $BP$ 有什么用？首先 $len(\\mathbf{c}) \\geq len(\\mathbf{r})$ 时 $BP=1$ ，这是在NMT输出较长时，$BP$ 不需要惩罚它，而会在 $p_n$ 的分母体现（分母变大，概率降低）。\n$len(\\mathbf{c}) \u003c len(\\mathbf{r})$ 时引入指数惩罚，如果NMT只翻译最有把握的一个词（比如输出一个单词 apple），$p_n$ 可能是 100%，但这没有意义。这个指数函数 $\\exp(1 - r/c)$ 在 $c$ 越小时，值会迅速接近 0，从而给短句一个沉重的打击。\nLastly, the BLEU score for candidate c with respect to $\\mathbf{r}_1,\\dots,\\mathbf{r}_k$ is:\n最后，候选 $\\mathbf{c}$ 相对于 $\\mathbf{r}_1,\\dots,\\mathbf{r}_k$ 的 BLEU 分数为：\n$$ BLEU = BP \\times \\exp \\left( \\sum_{n=1}^4 \\lambda_n \\log p_n \\right) $$ 本质上等同于： $$ \u003e BLEU = BP \\times (p_1^{\\lambda_1} \\cdot p_2^{\\lambda_2} \\cdot p_3^{\\lambda_3} \\cdot p_4^{\\lambda_4}) \u003e $$ where $\\lambda_1, \\lambda_2, \\lambda_3, \\lambda_4$ are weights that sum to 1. The log here is natural log.\n其中 $\\lambda_1, \\lambda_2, \\lambda_3, \\lambda_4$ 是总和为 1 的权重。此处的 $\\log$ 是自然对数。\n几何平均的一个特性是：如果其中任何一项 $p_n$ 为 0，最终结果就会是 0。所以模型的词汇和语序的正确性都会被计算。\n问题 (c) 结合实例运算：\ni. Source Sentence s: 需要有充足和可预测的资源。 Reference Translation $r_1$ : resources have to be sufficient and they have to be predictable Reference Translation $r_2$ : adequate and predictable resources are required NMT Translation $c_1$ : there is a need for adequate and predictable resources NMT Translation $c_2$ : resources be sufficient and predictable to Please compute the BLEU scores for and $c_1$ $c_2$. Let $\\lambda_i = 0.5$ for $i \\in \\{1, 2\\}$ and $\\lambda_i = 0$ for $i \\in \\{3, 4\\}$ (this means we ignore 3-grams and 4-grams, i.e., don\u0026rsquo;t compute $p_3$ or $p_4$). When computing BLEU scores, show your work (i.e., show your computed values for $p_1$, $p_2$, $len(c)$, $len(r)$ and $BP$). Note that the BLEU scores can be expressed between 0 and 1 or between 0 and 100. The code is using the 0 to 100 scale while in this question we are using the 0 to 1 scale. Please round your responses to 3 decimal places.\n先计算 $c_1$ : $len(c_1)=9$ 更接近于 $len(r_1)=11$ , $BP=exp(1-\\frac{11}{9})$\n$p_1$ 匹配的Unigrams 为 {adequate, and, predictable, resources}，共 4 个。 $p_1=4/9$\n$p_2$ 匹配的Bigrams 为 {adequate and, and predictable, predictable resources}，共 3 个。 $p_2=3/8$\n$BLEU_{c1} = BP \\times \\sqrt{p_1 \\times p_2}$\n$c_2$ 同理。\nii. Our hard drive was corrupted and we lost Reference Translation $r_1$. Please recom pute BLEU scores for $c_1$ and $c_2$, this time with respect to $r_2$ only. Which of the two NMT translations now receives the higher BLEU score? Do you agree that it is the better translation?\n计算方式不变，但是在去掉了 $r_1$ 后， ${BLEU}_{c_1}$ 不变， ${BLEU}_{c_2}$ 大幅缩小（由于 $p_1$ 和 $p_2$ 下降）\n这说明了 $c_1$ 的鲁棒性更强，它使用了更通用的词汇（如 adequate），即使在参考答案有限的情况下，依然能保持较合理的得分，这更符合人类对“好翻译”的评价标准。\niv. List two advantages and two disadvantages of BLEU, compared to human evaluation, as an evaluation metric for Machine Translation.\n优点不列了，缺点：BLEU 仅进行字面上的 N-gram 匹配，缺乏语义理解。由于 BLEU 主要关注局部词汇的重合度，它往往无法识别严重的语法错误或逻辑扭曲。一个逻辑完全相反、但在词汇上高度重合的句子可能获得极高的 BLEU 分，却无法被人类理解，所以它无法准确衡量语言质量。\n问题 (d) 是Beam Search相关的，关于训练次数与其“假设”质量的问题，比如i. 要求进行三个对比：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // iteration 200 { \u0026#34;hypothesis\u0026#34;: [ \u0026#34;▁it\u0026#34;, \u0026#34;▁is\u0026#34;, \u0026#34;▁not\u0026#34;, \u0026#34;▁that\u0026#34;, \u0026#34;▁the\u0026#34;, \u0026#34;▁united\u0026#34;, \u0026#34;▁nations\u0026#34;, \u0026#34;▁of\u0026#34;, \u0026#34;▁the\u0026#34;, \u0026#34;▁united\u0026#34;, \u0026#34;▁nations\u0026#34;, \u0026#34;▁of\u0026#34;, \u0026#34;▁the\u0026#34;, \u0026#34;▁united\u0026#34;, \u0026#34;▁nations\u0026#34;, \u0026#34;.\u0026#34; ], \u0026#34;score\u0026#34;: -31.211565017700195 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // iteration 3000 { \u0026#34;hypothesis\u0026#34;: [ \u0026#34;▁i\u0026#34;, \u0026#34;▁would\u0026#34;, \u0026#34;▁also\u0026#34;, \u0026#34;▁like\u0026#34;, \u0026#34;▁to\u0026#34;, \u0026#34;▁clarify\u0026#34;, \u0026#34;▁the\u0026#34;, \u0026#34;▁number\u0026#34;, \u0026#34;▁of\u0026#34;, \u0026#34;▁cases\u0026#34;, \u0026#34;▁in\u0026#34;, \u0026#34;▁the\u0026#34;, \u0026#34;▁conference\u0026#34;, \u0026#34;.\u0026#34; ], \u0026#34;score\u0026#34;: -15.013087272644043 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // iteration 17200 (last) { \u0026#34;hypothesis\u0026#34;: [ \u0026#34;▁i\u0026#34;, \u0026#34;▁have\u0026#34;, \u0026#34;▁also\u0026#34;, \u0026#34;▁clarified\u0026#34;, \u0026#34;▁a\u0026#34;, \u0026#34;▁number\u0026#34;, \u0026#34;▁of\u0026#34;, \u0026#34;▁matters\u0026#34;, \u0026#34;▁raised\u0026#34;, \u0026#34;▁by\u0026#34;, \u0026#34;▁the\u0026#34;, \u0026#34;▁conference\u0026#34;, \u0026#34;.\u0026#34; ], \u0026#34;score\u0026#34;: -7.69414758682251 } ","date":"2026-04-08T09:46:07+08:00","permalink":"/p/cs224n-assignment3-nmt/","title":"CS224N Assignment3 NMT"},{"content":"湖科大教书匠 深入浅出计算机网络\n计算机网络 1.1 计算机网络在信息时代的作用 计算机网络已由一种通信基础设施发展成为一种重要的信息服务基础设施。 计算机网络已经像水、电、煤气这些基础设施一样，成为我们生活中不可或缺的一部分。 1.2 因特网概述 网络、互连网（互联网）和因特网 网络（Network）由若干**结点（Node）和连接这些结点的链路（Link）**组成。 多个网络还可以通过路由器互连起来，这样就构成了一个覆盖范围更大的网络，即互联网（或互连网）。因此，互联网是“网络的网络（Network of Networks）”。 因特网（Internet）是世界上最大的互连网络（用户数以亿计，互连的网络数以百万计）。 internet（互联网或互连网）是一个通用名词，它泛指由多个计算机网络互连而成的网络。在这些网络之间的通信协议可以是任意的。\nInternet（因特网）则是一个专用名词，它指当前全球最大的、开放的、由众多网络相互连接而成的特定计算机网络，它采用TCP/IP协议簇作为通信的规则，其前身是美国的ARPANET。\n因特网发展的三个阶段 从单个网络ARPANET向互联网发展 1969年，第一个分组交换网ARPANET； 70年代中期，研究多种网络之间的互连； 1983年，TCP/IP协议成为ARPANET的标准协议（因特网诞生时间） 逐步建成三级结构的因特网 1985年，NSF围绕六个大型计算机中心建设NSFNET（主干网、地区网和校园网）； 1990年，ARPANET任务完成，正式关闭； 1991年，美国政府将因特网主干网交给私人公司经营，并开始对接入因特网的单位收费； 逐步形成了多层次ISP结构的因特网 1993年，NSFNET逐渐被若干个商用因特网主干网替代；政府机构不再负责因特网运营，让各种因特网服务提供者ISP来运营。 1994年，万维网WWW技术促使因特网迅猛发展； 1995年，NSFNET停止运作，因特网彻底商业化。 因特网的组成 边缘部分 由所有连接在因特网上的主机组成。这部分是用户直接使用的，用来进行通信（传送数据、音频或视频）和资源共享。 核心部分 核心部分 由大量网络和连接这些网络的路由器组成。这部分是为边缘部分提供服务的（提供连通性和交换）。 1.3 三种交换方式 电路交换 电话交换机接通电话线的方式称为电路交换； 从通信资源的分配角度来看，交换（Switching）就是按照某种方式动态地分配传输线路的资源； 电路交换的三个步骤： 建立连接（分配通信资源） 通话（一直占用通信资源） 释放连接（归还通信资源） 当使用电路交换来传送计算机数据时，其线路的传输效率往往很低。 分组交换 通常我们把表示消息的整块数据称为一个报文，在发送报文之前，先把较长的报文划分成为一个个更小的等长数据段。在每一个数据段前面，加上一些由必要的控制信息组成的首部后，就构成了一个分组，也可简称为“包”，相应地，首部也可叫作“包头”。 分组交换机收到一个分组后，先将分组暂时存储下来，再检查其首部，按照首部中的目的地址进行查表转发，找到合适的转发接口，通过该接口将分组转发给下一个分组交换机。各分组经过途中各分组交换机的存储转发，最终达到目标主机。 目标主机收到这些分组后，去掉它们的首部，将各数据段组合还原出原始报文。 电路交换、报文交换、分组交换的对比 电路交换\n优点 缺点 通信时延小 建立连接时间长 有序传输 线路独占，使用效率低 没有冲突 灵活性差 适用范围广 难以规范化 实时性强 控制简单 报文交换\n优点 缺点 无需建立连接 引起了转发时延 动态分配线路 需要较大存储缓存空间 提高线路可靠性 需要传输额外的信息量 提高线路利用率 提供多目标服务 分组交换\n优点 缺点 无需建立连接 引起了转发时延 线路利用率高 需要传输额外的信息量 简化了存储管理 对于数据报服务，存在失序、丢失或重复分组的问题；对于虚电路服务，存在呼叫建立、数据传输和虚电路释放三个过程 加速传输 减少出错概率和重发数据量 1.4 计算机网络的定义和分类 计算机网络的定义 计算机网络的精确定义并未统一 计算机网络的最简单的定义是：一些互相连接的、自治的计算机的集合。 互连 是指计算机之间可以通过有线或无线的方式进行数据通信； 自治 是指独立的计算机，它有自己的硬件和软件，可以单独运行使用； 集合 是指至少需要两台计算机； 计算机网络的较好的定义是：计算机网络主要是由一些通用的、可编程的硬件互连而成的，而这些硬件并非专门用来实现某一特定目的（例如，传送数据或视频信号）。这些可编程的硬件能够用来传送多种不同类型的数据，并能支持广泛的和日益增长的应用。 计算机网络的分类 分类方式 按交换技术分类 电路交换网络、报文交换网络、分组交换网络 按使用者分类 公用网、专用网 按传输介质分类 有线网络、无线网络 按覆盖范围分类 广域网WAN、城域网MAN、局域网LAN、个域网PAN 按覆盖范围分类 总线型网络、星型网络、环型网络、网状型网络 1.5 计算机网络的性能指标 速率 计算机中数据量的单位，也是信息论中信息量的单位。一个比特就是二进制数字中的一个1或0。 常用数据量单位： $8 bit = 1 Byte$\n$KB = 2^{10} B$\n$MB = K·KB = 2^{10}·2^{10} B = 2^{20} B$\n$GB = K·MB = 2^{10}·2^{20} B = 2^{30} B$\n$TB = K·GB = 2^{10}·2^{30} B = 2^{40} B$\n连接在计算机网络上的主机在数字信道上传送比特的速率，也称为比特率或数据率。 常用数据率单位： bit/s (b/s, bps)\n$k b/s = 10^3 b/s$ (bps)\n$M b/s = k \\cdot k b/s = 10^3 \\cdot 10^3 b/s = 10^6 b/s$ (bps)\n$G b/s = k \\cdot M b/s = 10^3 \\cdot 10^6 b/s = 10^9 b/s$ (bps)\n$T b/s = k \\cdot G b/s = 10^3 \\cdot 10^9 b/s = 10^{12} b/s$ (bps)\n带宽 用来表示网络的通信线路所能传送数据的能力，因此网络带宽表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”； 单位：b/s（kb/s, Mb/s, Gb/s, Tb/s）和速率相同 吞吐量 吞吐量表示在单位时间内通过某个网络（或信道、接口）的数据量。 吞吐量被经常用于对现实世界中的网络的一种测量，以便知道实际上到底有多少数据量能够通过网络。 吞吐量受网络的带宽或额定速率的限制。 时延 时延带宽积 $时延带宽积 = 传播时延 \\times 带宽 $ 若发送端连续发送数据，则在所发送的第一个比特即将到达终点时，发送端就已经发送了时延带宽积$B \\cdot W$个比特； 链路的时延带宽积又称为以比特为单位的链路长度。 往返时间 在许多情况下，因特网上的信息不仅仅单方向传输，而是双向交互； 我们有时很需要知道双向交互一次所需的时间；因此，往返时间RTT(Round-Trip Time)也是一个重要的性能指标。 利用率 信道利用率：用来表示某信道有百分之几的时间是被利用的（有数据通过）。 网络利用率：全网络的信道利用率的加权平均。 根据排队论，当某信道的利用率增大时，该信道引起的时延也会迅速增加； 因此，信道利用率并非越高越好； 如果令$D_0$表示网络空闲时的时延，$D$表示网络当前的时延，那么在适当的假定条件下，可以用下面的简单公式来表示$D$、$D_0$和利用率$U$之间的关系： $D = \\frac{D_0}{1 - U}$ 当网络的利用率达到50%时，时延就要加倍； 当网络的利用率超过50%时，时延急剧增大； 当网络的利用率接近100%时，时延就趋于无穷大。 因此，一些拥有较大主干网的ISP通常会控制它的信道利用率不超过50%。如果超过了，就要准备扩容，增大线路的带宽。 也不能使信道利用率太低，这会使宝贵的通信资源输白白浪费。应该使用一些机制，可以根据情况动态调整输入到网络中的通信量，使网络利用率保持在一个合理的范围内。 丢包率 丢包率即分组丢失率，是指在一定的时间范围内，传输过程中丢失的分组数量与总分组数量的比率。 丢包率具体可分为接口丢包率、结点丢包率、链路丢包率、路径丢包率、网络丢包率等。 丢包率是网络运维人员非常关心的一个网络性能指标，但对于普通用户来说往往并不关心这个指标，因为他们通常意识不到网络丢包。 分组丢失主要有两种情况： 分组在传输过程中出现误码，被结点丢弃； 分组到达一台队列已满的分组交换机时被丢弃；在通信量较大时就可能造成网络拥塞。 因此，丢包率反映了网络的拥塞情况： 无拥塞时路径丢包率为0 轻度拥塞时路径丢包率为1%~4% 严重拥塞时路径丢包率为5%~15% 1.6 计算机网络体系结构 常见的计算机网络体系结构 原理参考模型，从下到上以此为：物理层、数据链路层、网络层、运输层、应用层 分层的必要性 应用层 解决通过应用进程的交互来实现特定网络应用的问题 运输层 解决进程之间基于网络的通信问题 网络层 解决数据包在多个网络之间传输和路由的问题 数据链路层 解决数据包在一个网络或一段链路上传输的问题 物理层 解决使用何种信号来表示比特0和1的问题 分层思想举例 专用术语 实体 实体是指任何可发送或接收信息的硬件或软件进程。 对等实体是指通信双方相同层次中的实体。 协议 协议是控制两个对等实体在“水平方向”进行“逻辑通信”的规则的集合。 协议的三要素： 语法 定义所交换信息的格式 语义 定义通信双方所要完成的操作 同步 定义通信双方的时序关系 服务 在协议的控制下，两个对等实体在水平方向的逻辑通信使得本层能够向上一层提供服务。 要实现本层协议，还需要使用下面一层所提供的服务。 协议是“水平”的，而服务是“垂直”的。 实体看得见下层提供的服务，但并不知道实现该服务的具体协议。下层的协议对上层的实体是“透明”的。 在同一系统中相邻两层的实体交换信息的逻辑接口称为服务访问点SAP，它被用于区分不同的服务类型。 帧的“类型”字段、IP数据报的“协议”字段，TCP报文段或UDP用户数据报的“端口号”字段都是SAP。 上层要使用下层所提供的服务，必须通过与下层交换一些命令，这些命令称为服务原语。 对等层次之间传送的数据包称为该层的协议数据单元（Protocol Data Unit，PDU）。 同一系统内层与层之间交换的数据包称为服务数据单元（Service Data Unit，SDU）。 2.1 物理层概述 物理层要实现的功能 数据链路层“看不见”（也无需看见）物理层究竟使用的是什么方法来传输比特流。数据链路层**只管“享受”**物理层提供的比特流传输服务即可。 物理层之间“透明”传输比特流。 物理层接口特性 机械特性 形状和尺寸 引脚数目和排列 固定和锁定装置 电气特性 信号电压的范围 阻抗匹配的情况 传输速率 距离限制 功能特性 规定接口电缆的各条信号线的作用 过程特性 规定在信号线上传输比特流的一组操作过程，包括各信号间的时序关系 2.2 传输媒体 传输媒体是计算机网络设备之间的物理通路，也称为传输介质或传输媒介。 传输媒体并不包含在计算机网络体系结构中。 导向型传输媒体（固体媒体） 同轴电缆 双绞线 光纤 非导向型传输媒体（自由空间） 无线电波 微波 红外线 大气激光 可见光 2.3 传输方式 串行运输和并行运输 同步传输和异步传输 单向通信、双向交替通信和双向同时通信 2.4 编码与调制 基本概念 基带调制（编码） 以太网采用的曼彻斯特编码、4B/5B、8B/10B 数字基带信号 -\u0026gt; 数字信道 带通调制 Wi-Fi采用的CCK/DSSS/OFDM调制 数字基带信号 -\u0026gt; 模拟信道 码元 在使用时间域的波形表示信号时，代表不同离散数值的基本波形称为码元。 常用编码方式 双极性不归零编码 编码效率高，但存在同步问题 双极性归零编码 自同步，但编码效率低 曼彻斯特编码 自同步，10Mb/s传统以太网 码元中间时刻的电平跳变既表示时钟信号，也表示数据。正跳变表示1还是0，负跳变表示0还是1，可以自行定义。 差分曼彻斯特编码 码元中间时刻的电平跳变仅表示时钟信号，而不表示数据。 数据的表示在于每一个码元开始处是否有电平跳变：无跳变表示1，有跳变表示0。 差分曼彻斯特编码对比曼彻斯特编码\n在传输大量连续1或连续0的情况下，差分曼彻斯特编码信号比曼彻斯特编码信号的变化少。 在噪声干扰环境下，检测有无跳变比检测跳变方向更不容易出错，因此差分曼彻斯特编码信号比曼彻斯特编码信号更易于检测。 在传输介质接线错误导致高低电平翻转的情况下，差分曼彻斯特编码仍然有效。 带通调制方法和混合调制方法 2.5 信道的极限容量 造成信号失真的主要因素 传输速率越高，信号经过传输后的失真就越严重。\n传输距离越远，信号经过传输后的失真就越严重。\n噪声干扰越大，信号经过传输后的失真就越严重。\n传输媒体质量越差，信号经过传输后的失真就越严重。\n信道上传输的数字信号，可以看做是多个频率的模拟信号进行多次叠加后形成的方波。\n如果数字信号中的高频分量在传输时受到衰减甚至不能通过信道，则接收端接收到的波形前沿和后沿就变得不那么陡峭，每一个码元所占的时间界限也不再明确。这样，在接收端接收到的信号波形就失去了码元之间的清晰界限，这种现象称为码间串扰。\n如果信道的频带越宽，则能够通过的信号的高频分量就越多，那么码元的传输速率就可以更高，而不会导致码间串扰。\n然而，信道的频率带宽是有上限的，不可能无限大。因此，码元的传输速率也有上限。\n奈氏准则 理想低通信道的最高码元传输速率 = $2W Baud=2W(码元/秒)=2Wlog_2X(比特/秒)$\nW：信道的频率带宽（单位为Hz） Baud：波特，即码元/秒\n使用奈氏准则给出的公式，就可以根据信道的频率带宽，计算出信道的最高码元传输速率。 只要码元传输速率不超过根据奈氏准则计算出的上限，就可以避免码间串扰。 奈氏准则给出的是理想低通信道的最高码元传输速率，它和实际信道有较大的差别。因此，一个实际的信道所能传输的最高码元传输速率，要明显低于奈氏准则给出的上限值。 码元传输速率又称为波特率、调制速率、波形速率或符号速率。 波特率与比特率有一定的关系： 当1个码元只携带1比特的信息量时，波特率（码元/秒）与比特率（比特/秒）在数值上是相等的。 当1个码元携带n比特的信息量时，波特率（码元/秒）转换成比特率（比特/秒）时，数值要乘以n。 香农公式 带宽受限且有高斯白噪声干扰的信道的极限信息传输速率\n$$ C=Wlog_2(1+\\frac{S}{N})(b/s) $$ C：信道的极限信息传输速率（单位为b/s）\nW：信道的频率带宽（单位为Hz）\nS：信道内所传信号的平均功率\nN：信道内的高斯噪声功率\nS/N：信噪比，常用分贝（dB）表示\n信噪比（dB）=$10log_{10}(\\frac{S}{N})(dB)$\n信道的频率带宽W或信道中的信噪比S/N越大，信道的极限信息传输速率C就越高。 实际信道不可能无限制地提高频率带宽W或信道中的信噪比S/N。 实际信道中能够达到的信息传输速率，要比香农公式给出的极限传输速率低不少。这是因为在实际信道中，信号还要受到其他一些损伤，例如各种脉冲干扰和信号衰减等，这些因素在香农公式中并未考虑。 2.6 信道复用技术 基本原理 复用（Multiplexing）就是在一条传输媒体上同时传输多路用户的信号。 当一条传输媒体的传输容量大于多条信道传输的总容量时，就可以通过复用技术，在这条传输媒体上建立多条通信信道，以便充分利用传输媒体的带宽。 尽管实现信道复用会增加通信成本（需要复用器、分用器以及费用较高的大容量共享信道），但如果复用的信道数量较大，还是比较划算的。 常见技术 频分复用 FDM 所有用户同时占用不同的频带资源并行通信 时分复用 TDM 所有用户在不同的时间占用同样的频带 波分复用 WDM 根据频分复用的设计思想，可在一根光纤上同时传输多个频率（波长）相近的光载波信号，实现基于光纤的频分复用技术。 目前可以在一根光纤上复用80路或更多路的光载波信号。因此，这种复用技术也称为密集波分复用DWDM。 码分复用 CDM 如果有两个或多个站同时发送数据，则信道中的信号就是这些站各自所发送一系列码片序列或码片序列反码的叠加。为了从信道中分离出每个站的信号，给每个站指派码片序列时，必须遵循以下规则： 分配给每个站的码片序列必须各不相同，实际常采用伪随机码序列。 分配给每个站的码片序列必须相互正交，即各码片序列相应的码片向量之间的规格化内积为0。 令向量A表示站A的码片向量，向量B表示站B的码片向量。两个不同站A和B的码片序列相互正交，就是向量A与向量B的规格化内积为0，如下式所示。 $A\\cdot B=\\frac{1}{m}A_iB_i=0$ 假设知道各手机的码片序列，给手机A发送比特1，给手机B发送比特0，各手机用自己的码片向量与收到的叠加后的码片向量，做规格化内积运算： $(A + \\overline{B}) \\cdot A = A \\cdot A + A \\cdot \\overline{B} = 1 + 0 = 1$ 运算结果为1，表明收到的是比特1 $(A + \\overline{B}) \\cdot B = A \\cdot B + \\overline{B} \\cdot B = 0 + (-1) = -1$ 运算结果为-1，表明收到的是比特0 $(A + \\overline{B}) \\cdot C = A \\cdot C + \\overline{B} \\cdot C = 0 + 0 = 0$ 运算结果为0，表明没有收到信息 3.1 数据链路层概述 在网络体系中的地位 链路、数据链路和帧 链路（Link）是指从一个节点到相邻节点的一段物理线路（有线或无线），而中间没有任何其他的交换节点。 数据链路（Data Link）是基于链路的。当在一条链路上传送数据时，除需要链路本身，还需要一些必要的通信协议来控制这些数据的传输，把实现这些协议的硬件和软件加到链路上，就构成了数据链路。 计算机中的网络适配器（俗称网卡）和其相应的软件驱动程序就实现了这些协议。一般的网络适配器都包含了物理层和数据链路层这两层的功能。 帧（Frame）是数据链路层对等实体之间在水平方向进行逻辑通信的协议数据单元PDU。 3.2 数据链路层的三个重要问题 封装成帧和透明传输 数据链路层给上层交付下来的协议数据单元PDU添加帧首部和帧尾部，这称为封装成帧。 如果能够采取措施，使得数据链路层对上层交付的PDU的内容没有任何限制，就好像数据链路层不存在一样，就称其为透明传输。 差错检测 帧在传输的过程中可能出现误码。 接收方根据发送方添加在帧尾部中的检错码，可以检测出帧是否出现了误码。 可靠传输 不可靠传输服务：收到有误码的帧，直接丢弃，其他什么也不做；未收到发送方发送的帧，也不进行任何处理。 可靠传输服务：实现发送方发送什么，接收方最终都能正确收到。 3.2.1 封装成帧和透明传输 封装成帧 封装成帧是指数据链路层给上层交付下来的协议数据单元PDU添加一个首部和一个尾部，使之成为帧。\n帧的首部和尾部中有包含一些重要的控制信息。\n帧首部和尾部的作用之一就是帧定界。\n但帧首部和尾部不一定有帧定界标志。\n为了提高数据链路层传输帧的效率，应当使帧的数据载荷的长度尽可能地大于首部和尾部的长度。\n考虑到对缓存空间的需求以及差错控制等诸多因素，每一种数据链路层协议都规定了帧的数据载荷的长度上限，即最大传送单元（Maximum Transfer Unit，MTU）。例如，以太网的MTU为1500个字节。\n透明传输 透明传输是指数据链路层对上层交付的PDU的内容没有任何限制，就好像数据链路层不存在一样。 如果传输的内容中有和定界符完全一样的内容，那么就会误判帧结束，但是如果限制了内容，就不是透明传输。 面向字节的物理链路使用字节填充的方法实现透明传输。即在把帧交给物理层之前，要在和定界符相同的内容前加入转义字符，或者在和转义字符相同的内容前加入转义字符 面向比特的物理链路使用比特填充的方法实现透明传输。即在内容中连续遇到5个1时，在后面加0（因为01111110就是帧定界符了） 3.2.2 差错检测 误码 实际的通信链路都不是理想的，受到噪声干扰、传输媒体质量等因素影响，比特在传输过程中可能会产生差错（称为比特差错）：\n比特1可能变成比特0 比特0可能变成比特1 在一段时间内，传输错误的比特数量占所传输比特总数的比率称为误码率（Bit Error Rate, BER）。\n提高链路的信噪比，可以降低误码率。但在实际的通信链路上，不可能使误码率下降为零。\n奇偶校验 奇校验是在待发送的数据后面添加1个校验位，使得添加该校验位后的整个数据中比特1的个数为奇数。 偶校验是在待发送的数据后面添加1个校验位，使得添加该校验位后的整个数据中比特1的个数为偶数。 如果在数据位产生了1位误码，1的个数的奇偶性发生改变，可检出错误。校验位同理。但是若数据位和校验位各出现1位误码，那么1个数的奇偶性不变，检验不到错误 循环冗余校验 数据链路层广泛使用漏检率极低的循环冗余校验（Cyclic Redundancy Check，CRC）检错技术。 生成多项式 $$ G(X) = X^4 + X^2 + X + 1= \\boxed{1} \\cdot X^4 + \\boxed{0} \\cdot X^3 + \\boxed{1} \\cdot X^2 + \\boxed{1} \\cdot X^1 + \\boxed{1} \\cdot X^0 $$生成多项式各项系数构成的比特串：10111（计算冗余码时作为除数）\n生成多项式必须包含最低此项，即\u0026quot;1\u0026quot;\nCRC操作 发送方CRC操作 接收方CRC操作 总结 奇偶校验、循环冗余校验等差错检测技术，只能检测出传输过程中出现了差错，但并不能定位错误，因此无法纠正错误。\n要想纠正传输中的差错，可以使用冗余信息更多的纠错码（例如海明码）进行前向纠错。但纠错码的开销比较大，在计算机网络中较少使用。\n在计算机网络中，通常采用后续课程中将要介绍的检错重传方式来纠正传输中的差错，或者仅仅丢弃检测到差错的帧，这取决于数据链路层向其上层提供的是可靠传输服务还是不可靠传输服务。\n循环冗余校验CRC具有很好的检错能力（漏检率极低），虽然计算比较复杂，但非常易于用硬件实现，因此被广泛应用于数据链路层。\n3.2.3 可靠传输 可靠传输的相关概念 可靠传输服务：通过某种机制实现发送方发送什么，接收方最终就能收到什么 传输差错包括：分组重复、分组失序、分组丢失和误码（比特差错）。其中分组重复、分组失序、分组丢失出现在数据链路层的上层，误码出现在数据链路层的下层 可靠传输服务并不局限于数据链路层，其他各层均可选择实现可靠传输。 可靠传输的实现比较复杂，开销比较大，是否使用可靠传输取决于应用需求。 停止-等待 (SW) 协议 实现原理 发送方发送DATA，接收方的差错检测若检测无误码，则接受并发送确认分组ACK。 发送方发送DATA，接收方的差错检测若检测有误码，则丢弃并发送否认分组NAK。那么然后发送方要重传之前有误码的数据分组，再去经过接收方的差错检测。 发送方发送DATA，若是传输中途丢失，接收方就不会返回分组，发送方需要设置超时重传时间RTO，而且要略大于收发双方的平均往返时间RTT。然后进行重新传输。 在数据链路层，点对点的往返时间 RTT 比较固定，RTO 就比较好设定。 在运输层，由于端到端往返时间非常不确定，设置合适的超时重传时间 RTO 有时并不容易。 接收方发送ACK，若是传输中途丢失，结合上一条，发送方就会超时重传，但这样接收方就重复接收了同一个DATA。为解决该问题，引入数据分组编号，分组编号即0和1，DATA就该叫做DATA0/1。接收方丢弃重复的数据分组，再发送一个确认分组。 更复杂的情况会出现ACK被发送方重复接收，同理需要给ACK分组编号。 停止-等待协议属于自动请求重传（Automatic Repeat reQuest, ARQ）协议。 即重传的请求是发送方自动进行的，而不是接收方请求发送方重传某个误码的数据分组。\n信道利用率 $$ 信道利用率\\ U \\approx \\frac{T_D}{T_D + RTT + T_A} $$ 说明：\n$T_D$：发送方发送数据分组所耗费的发送时延。 $RTT$：收发双方之间的往返时间。 $T_A$：接收方发送确认分组所耗费的发送时延。 由于确认分组的长度通常远小于数据分组的长度，即 $T_A \\ll T_D$，公式可以简化为： $$ U \\approx \\frac{T_D}{T_D + RTT} $$ 简化后的利用率主要受 $RTT$ 与 $T_D$ 比例关系的影响：\n当 $RTT \\gg T_D$ 时： 结果：信道利用率 $U$ 很低。 典型案例：卫星链路（由于距离极远，往返时延很大）。 当 $RTT \\ll T_D$ 时： 结果：信道利用率 $U$ 比较高。 典型案例：无线局域网（往返时间远小于数据发送时间）。 回退N帧 (GBN) 协议 在使用流水线传输方式时，发送方不能无限制地连续发送数据分组，否则可能会导致网络中的路由器或接收方来不及处理这些数据分组，进而导致数据分组的丢失，这实际上是对网络资源的浪费。因此，必须采取措施来限制发送方连续发送数据分组的数量。 回退N帧协议采用流水线传输方式，并且利用发送窗口来限制发送方连续发送数据分组的数量，这属于连续ARQ协议。 发送方 发送窗口 $W_T$ 的取值范围是 $1 \u003c W_T \\leq (2^n - 1)$，其中，$n$ 是构成分组序号的比特数量。 如果 $W_T = 1$：变成了停止-等待协议。 如果 $W_T \u003e (2^n - 1)$：接收方无法分辨新旧数据分组。 可在未收到接收方确认分组的情况下，将序号落入发送窗口内的多个数据分组全部发送出去。 只有收到对已发送数据分组的确认分组时，发送窗口才能向前滑动到相应位置。 收到多个重复确认时，可在重传计时器超时前尽早开始重传，由具体实现决定（即确定收到几个重复确认进行重传）。 发送窗口内某个已发送的数据分组产生超时重传时，发送窗口内该数据分组的后续已发送的数据分组也必须全部重传，这就是回退N帧 (Go-back-N, GBN) 协议名称的由来。 接收方 接收窗口 $W_R = 1$ 的，因此只能按序接收数据分组。 只接收序号落入接收窗口内且无误码的数据分组，并且将接收窗口向前滑动一个位置，与此同时给发送方发送相应的确认分组。 为了减少开销，接收方不必每收到一个按序到达且无误码的数据分组就给发送方发送一个相应的确认分组。 可以在连续收到多个按序到达且无误码的数据分组后（数量由具体实现决定），才针对最后一个数据分组发送确认分组，这称为累积确认。 或者可以在自己有数据分组要发送时才对之前按序接收且无误码的数据分组进行捎带确认。 接收方收到未按序到达的数据分组后，除丢弃外，还可对之前最后一个按序到达的数据分组进行重复确认，以便发送方尽快重传。 选择重传协议 用 $n (n \u003e 1)$ 个比特给分组编号，发送窗口 $W_T$ 与接收窗口 $W_R$ 的关系如下： $$ \\begin{cases} 1 \u003c W_R \\leq W_T \\\\ W_T + W_R \\leq 2^n \\end{cases} \\implies 1 \u003c W_R \\leq 2^{(n-1)} $$ 当 $W_R$ 取最大值 $2^{(n-1)}$ 时，$W_T$ 能取到的最大值也为 $2^{(n-1)}$。 $W_R$ 超过 $W_T$ 没有意义 确保接收窗口向前滑动后，落入接收窗口内的新序号与之前的旧序号没有重叠，避免无法分辨新旧数据分组。 发送方 可在未收到接收方确认分组的情况下，将序号落入发送窗口内的多个数据分组全部发送出去。 只有按序收到对已发送数据分组的确认分组时，发送窗口才能向前滑动到相应位置。 如果收到未按序到达的确认分组，应对其进行记录，以防止其相应数据分组的超时重发，但发送窗口不能向前滑动。 接收方 可接收未按序到达但没有误码并且序号落入接收窗口内的数据分组。 为了使发送方仅重传出现差错的分组，接收方不再采用累积确认，而需要对每一个正确接收的数据分组进行逐一确认。 只有在按序接收数据分组后，接收窗口才能向前滑动到相应位置。 3.3 点对点协议 (PPP) 概述 点对点协议（Point-to-Point Protocol, PPP）是目前使用最广泛的点对点数据链路层协议。 点对点协议PPP主要有两种应用： 用户到 ISP 的连接：例如通过拨号或 ADSL 接入互联网。 路由器之间的点对点链路：用于连接核心网络中的两个节点。 从网络体系结构的角度看点对点协议 PPP 的组成 一套网络控制协议 NCPs 一个网络层 PDU 封装到串行链路的方法 一个链路控制协议 LCP 帧格式 标志（Flag）字段：PPP帧的定界符，取值为 0x7E。 地址（Address）字段：取值为 0xFF，预留（目前没有什么作用）。 控制（Control）字段：取值为 0x03，预留（目前没有什么作用）。 协议（Protocol）字段：其值用来指明帧的数据载荷应向上交付给哪个协议处理。 帧检验序列（Frame Check Sequence, FCS）字段：其值是使用循环冗余校验CRC计算出的检错码。 透明传输 面向字节的异步链路 发送方的处理：\n将数据载荷中出现的每一个 0x7E 减去 0x20（相当于异或 0x20），然后在其前面插入转义字符 0x7D。 若数据载荷中原来就含有 0x7D，则把每一个 0x7D 减去 0x20，然后在其前面插入转义字符 0x7D。 将数据载荷中出现的每一个 ASCII 码控制字符（即数值小于 0x20 的字符），加上 0x20（相当于异或 0x20，将其转换成非控制字符），然后在其前面插入转义字符 0x7D。 接收方的处理：\n进行与发送方相反的变换，就可以正确地恢复出未经字节填充的原始数据载荷。 面向比特的同步链路 发送方的处理：\n对帧的数据载荷进行扫描（一般由硬件完成），每出现 5 个连续的比特 1，则在其后填充一个比特 0。 接收方的处理：\n对帧的数据载荷进行扫描，每出现 5 个连续的比特 1 时，就把其后的一个比特 0 删除。 差错检测 帧检验序列FCS字段：其值是使用循环冗余校验CRC计算出的检错码。 CRC采用的生成多项式为： $$ CRC-CCITT = X^{16} + X^{12} + X^5 + 1 $$工作状态 以用户主机拨号接入因特网服务提供者ISP的拨号服务器的过程为例\n3.4 共享式以太网 年份 里程碑事件 关键细节 1975年 以太网诞生 Robert Metcalfe (1946-)，美国Xerox公司，速率 2.94Mb/s 1976年 以太网里程碑论文 Robert Metcalfe 与助手 David Boggs 发表论文《以太网：局域计算机网络的分布式包交换技术》 1979年 3Com公司成立 Robert Metcalfe 离开 Xerox 成立 3Com；游说 DEC、Intel 和 Xerox 共同将以太网标准化 1980年 以太网标准V1 DIX Ethernet V1，速率 10Mb/s 1982年 以太网标准V2 DIX Ethernet V2：第一个局域网产品的标准，被大量使用 1983年 IEEE 以太网标准 IEEE 802.3 以太网标准：对 DIX Ethernet V2 的帧格式做了很小的改动，当时没有被广泛使用 以太网目前已经从传统的共享式以太网发展到交换式以太网，传输速率已经大幅提高。 3.4.1 网络适配器和MAC地址 网络适配器 要将计算机连接到以太网，需要使用相应的网络适配器（Adapter），网络适配器一般简称为“网卡”。 在计算机内部，网卡与CPU之间的通信，一般是通过计算机主板上的I/O总线以并行传输方式进行的。 网卡与外部以太网（局域网）之间的通信，一般是通过传输媒体（同轴电缆、双绞线电缆、光纤）以串行方式进行的。 网卡除要实现物理层和数据链路层功能，其另外一个重要功能就是要进行并行传输和串行传输的转换。由于网络的传输速率和计算机内部总线上的传输速率并不相同，因此在网卡的核心芯片中都会包含用于缓存数据的存储器。 在确保网卡硬件正确的情况下，为了使网卡正常工作，还必须要在计算机的操作系统中为网卡安装相应的设备驱动程序。驱动程序负责驱动网卡发送和接收帧。 MAC地址 在点对点信道，数据链路层不需要使用地址。而在广播信道，数据链路层必须使用地址来区分各主机。 MAC地址一般被固化在网卡的电可擦可编程只读存储器EEPROM中，因此MAC地址也被称为硬件地址，有时被称为物理地址（和物理层无关，属于数据链路层范畴） MAC地址是对网络上各接口的唯一标识，而不是各设备的唯一标识。 地址格式 其中第 1 字节的 b1 位：G/L (Global/Local)\n$G/L = 0$：全球管理 $G/L = 1$：本地管理 第 1 字节的 b0 位：I/G (Individual/Group)\n$I/G = 0$：单播地址 $I/G = 1$：多播地址 第 1 字节 b1 位 第 1 字节 b0 位 MAC 地址类型 地址占比 0 0 全球单播（由厂商生产网络设备时固化在设备中） 1/4 0 1 全球多播（交换机、路由器等标准网络设备所支持的多播地址） 1/4 1 0 本地单播（由网络管理员分配，优先级高于网络接口的全球单播地址） 1/4 1 1 本地多播（可由用户对网卡编程实现，以表明其属于哪些多播组） 1/4 其字节发送顺序是从第1字节开始，到第6字节。字节内的比特发送顺序是从b0到b7。 网卡对无误码帧的处理 网卡从网络上每收到一个无误码的帧，就检查帧首部中的目的 MAC 地址，按以下情况处理：\n如果目的 MAC 地址是广播地址（FF-FF-FF-FF-FF-FF），则接受该帧。 如果目的 MAC 地址与网卡上固化的全球单播 MAC 地址相同，则接受该帧。 如果目的 MAC 地址是网卡支持的多播地址，则接受该帧。 除上述 (1)、(2) 和 (3) 情况外，丢弃该帧。 网卡还可被设置为一种特殊的工作方式：混杂方式。工作在混杂方式的网卡，只要收到共享媒体上传来的帧就会收下，而不管帧的目的 MAC 地址是什么。\n用途： 对于网络维护和管理人员，这种方式可以监视和分析局域网上的流量，以便找出提高网络性能的具体措施。 工具示例： 嗅探器 (Sniffer) 就是一种工作在混杂方式的网卡，再配合相应的工具软件（WireShark），就可以作为一种非常有用的网络工具来学习和分析网络。 风险提示： 混杂方式就像一把“双刃剑”，黑客常利用这种方式非法获取网络用户的口令。 3.4.2 以太网在共享信道下的运作机制 CSMA/CD协议 在以太网的发展初期，人们普遍认为“无源的电缆线比有源器件可靠”，因此将多个站点连接在一条总线上来构建共享总线以太网。 共享总线以太网具有天然的广播特性，即使总线上某个站点给另一个站点发送单播帧，表示帧的信号也会沿着总线传播到总线上的其他各站点。 当某个站点在总线上发送帧时，总线资源会被该站点独占。此时，如果总线上的其他站点也要在总线上发送帧，就会产生信号碰撞。 当两个或多个站点同时使用总线发送帧时，就会产生信号碰撞。 为了解决各站点争用总线的问题，共享总线以太网使用了一种专用协议 CSMA/CD，它是载波监听多址接入/碰撞检测 (Carrier Sense Multiple Access/Collision Detection) 的英文缩写词。 协议组成 多址接入 MA：\n多个站点连接在一条总线上，竞争使用总线。 载波监听 CS：\n每个站点在发送帧之前，先要检测一下总线上是否有其他站点在发送帧（“先听后说”）： 若检测到总线空闲 96 比特时间（发送 96 比特所耗费的时间，也称为帧间最小间隔），则发送这个帧； 若检测到总线忙，则继续检测并等待总线转为空闲 96 比特时间，然后发送这个帧。 碰撞检测 CD：\n每个正在发送帧的站点边发送边检测碰撞（“边说边听”）： 一旦发现总线上出现碰撞，立即停止发送，退避一段随机时间后再次从载波监听开始进行发送（“一旦冲突，立即停说，等待时机，重新再说”）。 协议局限 载波监听检测到总线空闲，但总线并不一定空闲。 使用CSMA/CD协议的共享总线以太网上的各站点，只是尽量避免碰撞并在出现碰撞时做出退避后重发的处理，但不能完全避免碰撞。 在使用CSMA/CD协议时，由于正在发送帧的站点必须“边发送帧边检测碰撞”，因此站点不可能同时进行发送和接收，也就是不可能进行全双工通信，而只能进行半双工通信（双向交替通信）。 共享式以太网的争用期 站点从发送帧开始，最多经过时长 $2\\tau$（即 $\\delta \\to 0$）就可检测出所发送的帧是否遭遇了碰撞。\n因此，共享总线以太网的端到端往返时间 $2\\tau$ 被称为争用期（Contention Period）或碰撞窗口（Collision Window），它是一个非常重要的参数。\n站点从发送帧开始，经过争用期 $2\\tau$ 这段时间还没有检测到碰撞，就可以肯定这次发送不会产生碰撞。 从争用期的概念可以看出，共享总线以太网上的每一个站点从发送帧开始，到之后的一小段时间内，都有可能遭遇碰撞，而这一小段时间的长短是不确定的，它取决于另一个发送帧的站点与本站点的距离，但不会超过总线的端到端往返传播时延，即一个争用期 $2\\tau$。\n很显然，总线的长度越长（单程端到端传播时延越大），网络中站点数量越多，发生碰撞的概率就越大。 因此，共享以太网的总线长度不能太长，接入的站点数量也不能太多。 10Mb/s 共享总线以太网参数规定：争用期 $2\\tau$ 的值为 512 比特的发送时间，即 $51.2\\mu s$。也称为512 比特时间：这是传统以太网的一个基石参数。 由于 $10Mb/s$ 下 512 比特等于 64 字节，这也就是以太网最短帧长为 64 字节的物理由来：为了确保在发送完 64 字节之前，信号一定能完成一个来回（争用期），从而检测出是否发生了碰撞。 $$ 2\\tau = \\frac{512\\ b}{10\\ Mb/s} = \\frac{512\\ b}{10 \\times 10^6\\ b/s} = 51.2\\mu s $$ 除考虑了信号传播时延外，还考虑到网络中可能存在转发器所带来的时延以及产生碰撞时继续发送 32 比特或 48 比特人为干扰信号所持续的时间等。\n假设信号的传播速率为：$2 \\times 10^8\\ m/s$\n则总线长度为：$2 \\times 10^8\\ m/s \\times 25.6\\mu s = 5120m$\n共享式以太网的最小/最大帧长 最小帧长 为了确保共享总线以太网上的每一个站点在发送完一个完整的帧之前，能够检测出是否产生了碰撞，帧的发送时延就不能少于共享总线以太网端到端的往返时间，即一个争用期 $2\\tau$。 $$ \\frac{帧长度 (bit)}{发送速率 (b/s)} \\ge 2\\tau $$ 对于 $10Mb/s$ 的共享总线以太网，其争用期 $2\\tau$ 的值规定为 $51.2\\mu s$，因此其最小帧长为 $512b$，即 $64B$。 $$ 10Mb/s \\times 51.2\\mu s = 512b = 64B $$ 当某个站点在发送帧时，如果帧的前 $64B$ 没有遭遇碰撞，那么帧的后续部分也就不会遭遇碰撞。也就是说，如果遭遇碰撞，就一定是在帧的前 $64B$ 之内。 由于发送帧的站点边发送帧边检测碰撞，一旦检测到碰撞就立即中止帧的发送，此时已发送的数据量一定小于 $64B$。因此，接收站点收到长度小于 $64B$ 的帧，就可判定这是一个遭遇了碰撞而异常中止的无效帧，将其丢弃即可。 最大帧长 一般来说，帧的数据载荷的长度应远大于帧首部和尾部的总长度，这样可以提高帧的传输效率。 然而，如果不限制数据载荷的长度上限，就可能使得帧的长度太长，这会带来一些问题。 以太网V2的MAC帧，最大长度为1518B。其中数据载荷最大为1500B，最小为46B。其它的部分：目的地址、源地址、类型和FCS共占了18B 退避算法 $$ \\text{退避时间} = \\text{基本退避时间（争用期 } 2\\tau\\text{）} \\times \\text{随机数 } r $$ 随机数 $r$ 的取值规则：$r$ 从离散的整数集合 $\\{0, 1, \\dots, (2^k - 1)\\}$ 中随机选出一个数。 参数 $k$ 的定义：$k = \\min[\\text{重传次数}, 10]$。 重传次数 k 离散整数集合 {0,1,…,(2k−1)} 可能的退避时间 1 1 $\\{0, 1\\}$ $0 \\times 2\\tau, 1 \\times 2\\tau$ 2 2 $\\{0, 1, 2, 3\\}$ $0 \\times 2\\tau, 1 \\times 2\\tau, 2 \\times 2\\tau, 3 \\times 2\\tau$ 12 10 $\\{0, 1, 2, 3, 4, 5, \\dots, 1023\\}$ $0 \\times 2\\tau, 1 \\times 2\\tau, 2 \\times 2\\tau, \\dots, 1023 \\times 2\\tau$ 如果连续多次发生碰撞，表明可能有较多的站点参与竞争信道。使用上述退避算法可使重传需要推迟的平均时间随重传次数而增大（即动态退避），从而减小产生碰撞的概率。 当重传达 16次仍不能成功时，表明同时打算发送帧的站点太多，以至于连续产生碰撞。此时应放弃重传并向高层报告。 信道利用率 $$ S_{max} = \\frac{T_0}{T_0 + \\tau} = \\frac{1}{1 + \\frac{\\tau}{T_0}} = \\frac{1}{1 + a} $$ 参数 $a$ 的定义：$a = \\frac{\\tau}{T_0}$ 参数 $a$ 的值应尽量小，以提高信道利用率。 $\\tau \\downarrow$（分子减小）：共享总线以太网端到端的距离不应太长。 $T_0 \\uparrow$（分母增大）**：**帧的长度应尽量大。 3.4.3 使用集线器的共享式以太网 若总线上的某个机械连接点接触不良或断开，则整个网络通信就不稳定或彻底断网。\n在使用细同轴电缆的共享总线以太网之后，以太网发展出来了一种使用大规模集成电路来替代总线、并且可靠性非常高的设备，叫做集线器 (Hub)。\n站点连接到集线器的传输媒体也转而使用更便宜、更灵活的双绞线电缆。\n补充：双绞线电缆两端为 RJ-45 插头，连接至集线器的 RJ-45 插座。 集线器的一些主要特点如下：\n使用集线器的以太网虽然物理拓扑是星型的，但在逻辑上仍然是一个总线网。总线上的各站点共享总线资源，使用的还是 CSMA/CD 协议。 集线器只工作在物理层。它的每个接口仅简单地转发比特，并不进行碰撞检测。碰撞检测的任务由各站点中的网卡负责。 集线器一般都有少量的容错能力和网络管理功能。例如，若网络中某个站点的网卡出现了故障而不停地发送帧，集线器可以检测到这个问题，在内部断开与出故障网卡的连线，使整个以太网能正常工作。 “传统总线型以太网”向“集线器星型以太网”的异同点：\n物理拓扑不相同 但工作逻辑相同 网络中的各站点都使用 CSMA/CD协议来共享 （争用）网络资源 10BASE-T BASE：采用基带信号进行传输，10：传输速率为 10Mb/s，T：采用双绞线作为传输媒体\nIEEE于1990年制定了10BASE-T星型以太网的标准802.3i，这种以太网是局域网发展史上的一座非常重要的里程碑，它为以太网在局域网中的统治地位奠定了牢固的基础。\n10BASE-T以太网的通信距离较短，每个站点到集线器的距离不能超过100m。\nIEEE 802.3以太网还可使用光纤作为传输媒体，相应的标准为10BASE-F，“F”表示光纤。光纤主要用作集线器之间的远程连接。\n3.4.4 在物理层扩展以太网 扩展站点与集线器之间的距离 共享总线以太网中两站点之间的距离不能太远，否则它们之间所传输的信号就会衰减到使CSMA/CD协议无法正常工作。\n在早期广泛使用粗同轴电缆或细同轴电缆共享总线以太网时，为了提高网络的地理覆盖范围，常用的是工作在物理层的转发器。\nIEEE 802.3标准规定，两个网段可用一个转发器连接起来，任意两个站点之间最多可以经过三个网段。\n随着使用双绞线和集线器的10BASE-T星型以太网成为以太网的主流类型，扩展网络覆盖范围就很少使用转发器了。10BASE-T星型以太网中每个站点到集线器的距离不能超过100m，因此两站点间的通信距离最大不能超过200m。\n在10BASE-T星型以太网中，可使用光纤和一对光纤调制解调器来扩展站点与集线器之间的距离。\n这种扩展方法比较简单，所需付出的代价是：为站点和集线器各增加一个用于电信号和光信号转换的光纤调制解调器，以及它们之间的一对通信光纤。 信号在光纤中的衰减和失真很小，因此使用这种方法可以很简单地将站点与集线器之间的距离扩展到1000m以上。\n扩展共享式以太网的覆盖范围和站点数量 以太网集线器一般具有8~32个接口，如果要连接的站点数量超过了单个集线器能够提供的接口数量，就需要使用多个集线器，这样就可以连接成覆盖更大范围、连接更多站点的多级星型以太网。 采用多个集线器连接而成的多级星型以太网，在扩展了网络覆盖范围和站点数量的同时，也带来了一些负面因素。 在物理层扩展的共享式以太网仍然是一个碰撞域，不能连接太多的站点，否则可能会出现大量的碰撞，导致平均吞吐量太低。 3.4.5 在数据链路层扩展以太网 网桥的基本工作原理 网桥 (bridge) 工作在数据链路层 (包含其下的物理层)，因此网桥具备属于数据链路层范畴的相关能力。 网桥可以识别帧的结构。 网桥可以根据帧首部中的目的MAC地址和网桥自身的帧转发表来转发或丢弃所收到的帧。 透明网桥的自学习和转发帧的流程 透明网桥 (Transparent Bridge) 通过自学习算法建立转发表。\n透明网桥中的“透明”，是指以太网中的各站点并不知道自己所发送的帧将会经过哪些网桥的转发，最终到达目的站点。也就是说，以太网中的各网桥对于各站点而言是看不见的。\n透明网桥的标准是IEEE 802.1D，它通过一种自学习算法基于以太网中各站点间的相互通信逐步建立起自己的转发表。\n网桥收到帧后进行登记（即自学习），登记的内容为帧的源MAC地址和进入网桥的接口号。 网桥根据帧的目的MAC地址和网桥的转发表对帧进行转发，包含以下三种情况： 明确转发：网桥知道应当从哪个接口转发帧。 盲目转发：网桥不知道应当从哪个接口转发帧，只能将其通过除进入网桥的接口外的其他所有接口转发。 丢弃：网桥知道不应该转发该帧，将其丢弃。 需注意： 如果网桥收到有误码的帧则直接丢弃。 如果网桥收到一个无误码的广播帧，则不用进行查表，而是直接从除接收该广播帧的接口的其他接口转发该广播帧。 转发表中的每条记录都有其有效时间，到期自动删除！这是因为各站点的MAC地址与网桥接口的对应关系并不是永久性的，例如某个站点更换了网卡，其MAC地址就会改变。 透明网桥的生成树协议STP 为了提高以太网的可靠性，有时需要在两个以太网之间使用多个透明网桥来提供冗余链路。\n在增加冗余链路提高以太网可靠性的同时，却给网络引入了环路。\n网络中的广播帧将在环路中永久兜圈，造成广播帧充斥整个网络，网络资源被白白浪费，而网络中的主机之间无法正常通信！\n为了避免广播帧在环路中永久兜圈，透明网桥使用生成树协议（Spanning Tree Protocol, STP），可以在增加冗余链路提高网络可靠性的同时，又避免环路带来的问题。\n不管网桥之间连接成了怎样复杂的带环拓扑，网桥之间通过交互网桥协议单元（Bridge Protocol Data Unit, BPDU），找出原网络拓扑的一个连通子集（即生成树），在这个子集里整个连通的网络中不存在环路。 当首次连接网桥或网络拓扑发生变化时（人为改变或出现故障），网桥都会重新构造生成树，以确保网络的连通。 3.5 交换式以太网 网桥的接口数量很少，通常只有2~4个，一般只用来连接不同的网段。 1990年面世的交换式集线器（Switching Hub），实质上是具有多个接口的网桥，常称为以太网交换机（Switch）或二层交换机。 “二层”是指以太网交换机工作在数据链路层（包括物理层）。 与网桥相同，交换机内部的转发表也是通过自学习算法，基于网络中各主机间的通信，自动地逐步建立起来的。 另外，交换机也使用生成树协议STP，来产生能够连通全网但不产生环路的通信路径。 仅使用交换机（而不使用集线器）的以太网就是交换式以太网。 3.5.1 以太网交换机 以太网交换机（以下简称交换机）本质上就是一个多接口的网桥： 交换机自学习和转发帧的流程与网桥是相同的。 另外，交换机也使用生成树协议STP，来产生能够连通全网但不产生环路的通信路径。 交换机的每个接口可以连接计算机，也可以连接集线器或另一个交换机。 当交换机的接口与计算机或交换机连接时，可以工作在全双工方式，并能在自身内部同时连通多对接口，使每一对相互通信的计算机都能像独占传输媒体那样，无碰撞地传输数据，这样就不需要使用CSMA/CD协议了。 当交换机的接口连接的是集线器时，该接口就只能使用CSMA/CD协议并只能工作在半双工方式。 现在的交换机和计算机中的网卡都能自动识别上述两种情况，并自动切换到相应的工作方式。 主机间的通信 交换机1的操作 交换机2的操作 A $\\rightarrow$ B 登记 转发（盲目） 登记 转发（盲目） H $\\rightarrow$ A 登记 转发（明确） 登记 转发（明确） E $\\rightarrow$ X 登记 转发（盲目） 登记 转发（盲目） X $\\rightarrow$ E 登记 丢弃 收不到 交换机一般都具有多种速率的接口，例如10Mb/s、100Mb/s、1Gb/s甚至10Gb/s的接口，大部分接口支持多速率自适应。 一般的交换机都采用“存储转发”方式，为了减小交换机的转发时延，某些交换机采用了直通（Cut-Through）交换方式。 采用直通交换方式的交换机，在接收帧的同时就立即按帧的目的MAC地址决定该帧的转发接口，然后通过其内部基于硬件的交叉矩阵进行转发，而不必把整个帧先缓存后再进行处理。 直通交换的时延非常小。 直通交换不检查差错就直接将帧转发出去，有可能会将一些无效帧转发给其他主机。 3.5.2 对比共享式以太网与交换式以太网 交换机能识别帧的信息，因此信息传输通过交换机本身过滤，可以精准转发。而集线器只工作在物理层，只能全部转发，然后由终端来过滤。对于广播帧也是同理。 交换机的域中不会出现碰撞现象，交换机能自己存储转发信息。 通过集线器连接，既扩大了广播域也扩大了碰撞域。通过交换机连接，只扩大广播域而不扩大碰撞域，碰撞域仍保持独立。通过路由器连接，广播域和碰撞域都保持独立。 3.6 以太网的MAC帧格式 3.7 虚拟局域网 VLAN 3.7.1 虚拟局域网概述 背景 将多个站点通过一个或多个以太网交换机连接起来就构建出了交换式以太网。 交换式以太网中的所有站点都属于同一个广播域。 随着交换式以太网规模的扩大，广播域也相应扩大。巨大的广播域会带来一系列问题。 广播风暴 广播风暴会浪费网络资源和各主机的CPU资源 难以管理和维护，带来潜在的安全问题。 网络中会频繁出现广播信息 TCP/IP协议栈中的很多协议都会使用广播（例如地址解析协议ARP，路由信息协议RIPv1，动态主机配置协议DHCP） NetBEUI：Windows下使用的广播型协议 IPX/SPX：Novell网络的协议栈 Apple Talk：Apple公司的网络协议栈 概述 虚拟局域网（Virtual Local Area Network，VLAN）是一种将局域网内的站点划分成与物理位置无关的逻辑组的技术，一个逻辑组就是一个VLAN，VLAN中的各站点具有某些共同的应用需求。 属于同一VLAN的站点之间可以直接进行通信，而不同VLAN中的站点之间不能直接通信。 网络管理员可对局域网中的各交换机进行配置来建立多个逻辑上独立的VLAN。 连接在同一交换机上的多个站点可以属于不同的VLAN，而属于同一VLAN的多个站点可以连接在不同的交换机上。 虚拟局域网VLAN并不是一种新型网络，它只是局域网能够提供给用户的一种服务。 3.7.2 虚拟局域网实现机制 虚拟局域网VLAN有多种实现技术，最常见的就是基于以太网交换机的接口来实现VLAN。这就需要以太网交换机能够实现以下两个功能：\n能够处理带有VLAN标记的帧，也就是IEEE 802.1Q帧。\n交换机的各接口可以支持不同的接口类型，不同接口类型的接口对帧的处理方式有所不同。\nIEEE 802.1Q帧 IEEE 802.1Q帧也称为Dot One Q帧，它对以太网V2的MAC帧格式进行了扩展：在源地址字段和类型字段之间插入了4字节的VLAN标签（tag）字段。 标签协议标识符TPID：长度为16比特，其值固定为0x8100，表示该帧是IEEE 802.1Q帧。 优先级PRI：长度为3比特，取值范围是0~7，值越大优先级越高。当网络阻塞时，设备优先发送优先级高的802.1Q帧。 规范格式指示符CFI：长度为1比特，取值为0表示MAC地址以规范格式封装，取值为1表示MAC地址以非规范格式封装。对于以太网，CFI的取值为0。 虚拟局域网标识符VID：长度为12比特，取值范围是0~4095，其中0和4095保留不使用。 VID是802.1Q帧所属VLAN的编号，设备利用VID来识别帧所属的VLAN。 广播帧只在同一VLAN内转发，这样就把广播域限制在了一个VLAN内。 802.1Q帧一般不由用户主机处理，而是由以太网交换机来处理： 当交换机收到普通的以太网MAC帧时，会给其插入4字节的VLAN标签使之成为802.1Q帧，该处理简称为“打标签”。 当交换机转发802.1Q帧时，可能会删除其4字节的VLAN标签使之成为普通的以太网MAC帧，该处理简称为“去标签”。交换机转发802.1Q帧时也有可能不进行“去标签”处理，是否进行“去标签”处理取决于交换机的接口类型。 交换机接口类型 Access 接口 Access接口一般用于连接用户计算机，由于其只能属于一个VLAN，因此Access接口的PVID值与其所属VLAN的ID相同，其默认值为1。 接收处理 一般只接受“未打标签”的普通以太网MAC帧，根据接收帧的接口的PVID给帧“打标签”，即插入4字节的VLAN标签字段，VLAN标签字段中的VID取值就是接口的PVID值。 转发处理 若帧中的VID值与接口的PVID值相等，则给帧“去标签”后再进行转发，否则不转发帧。因此，从Access接口转发出的帧，是不带VLAN标签的普通以太网MAC帧。 Truck 接口 Trunk接口一般用于交换机之间的互连。Trunk接口可以属于多个VLAN，即Trunk接口可以通过属于不同VLAN的帧。Trunk接口的默认PVID值为1，一般不建议用户修改，若互连的Trunk接口的PVID值不相等，则可能出现转发错误。 接收处理 既可以接收“未打标签”的普通以太网MAC帧，也可以接收“已打标签”的802.1Q帧。若接收到普通以太网MAC帧时，根据接收帧的接口的PVID给帧“打标签”，这与Access接口的处理相同。 转发处理 对于帧的VID值等于接口的PVID值的802.1Q帧，将其“去标签”转发；对于帧的VID值不等于接口的PVID值802.1Q帧，将其直接转发。因此，从Trunk接口转发出的帧，可能是普通以太网MAC帧，也可能是802.1Q帧。 3.8 以太网的发展 都使用IEEE 802.3的帧格式 都遵守IEEE 802.3的最小帧长和最大帧长的规定 10吉比特以太网（10GE）和40/100吉比特以太网（40GE/100GE）只工作在全双工方式而不存在争用媒体的问题，因此不需要使用CSMA/CD协议，这样传输距离就不再受碰撞检测的限制。 都有多种不同的物理层标准，10吉比特以太网（10GE）和40/100吉比特以太网（40GE/100GE）还增加了支持城域网和广域网的物理层标准。 4.1 网络层概述 分组转发和路由选择 网络层的主要任务就是将分组从源主机经过多个网络和多段链路传输到目的主机，可以将该任务划分为分组转发和路由选择两种重要的功能。 网络层向其上层提供的两种服务 对比方面 虚电路服务 数据报服务 核心思想 可靠通信应当由网络自身来保证 可靠通信应当由用户主机来保证 连接 必须建立网络层连接 不需要建立网络层连接 目的地址 仅在连接建立阶段使用，之后每个分组使用短的虚电路号 每个分组都必须携带完整的目的地址 分组转发 属于同一条虚电路的分组均按同一路由进行转发 每个分组可走不同的路由 节点故障 所有通过出故障的节点的虚电路均不能工作 出故障的节点可能会丢失分组，一些路由可能会发生变化 分组顺序 总是按发送顺序到达目的主机 到达目的主机时不一定按发送顺序 服务质量 可以将通信资源提前分配给每一个虚电路，因此容易实现 很难实现 4.2 网际协议 IP 网际协议（Internet Protocol, IP）是TCP/IP体系结构网际层中的核心协议。 网际协议IP、传输控制协议TCP、TCP/IP体系结构是由“因特网之父”Robert Kahn和Vint Cerf二人共同研发的，1974年5月发布了TCP/IP第一个版本。 4.2.1 异构网络互连 网络的拓扑、性能以及所使用的网络协议都不尽相同，这是由用户需求的多样性造成的，没有一种单一的网络能够适应所有用户的需求。\n要将众多的异构型网络都互连起来，并且能够互相通信，则会面临许多需要解决的问题。\n不同的网络接入机制 不同的差错恢复方法 不同的路由选择技术 不同的寻址方案 不同的最大分组长度 不同的服务（面向连接服务和无连接服务） 当IP网上的主机进行通信时，就好像在一个单个网络上通信一样，它们看不见互连的各网络的具体异构细节。\n网络层都使用相同的IP协议，即所有接入互联网的设备都支持IP协议。\n4.2.2 IPv4地址及其编址方法 概述 IPv4地址是给因特网（Internet）上的每一个主机（或路由器）的每一个接口分配的一个在全世界范围内唯一的32比特的标识符。\nIPv4地址由因特网名字和数字分配机构（Internet Corporation for Assigned Names and Numbers, ICANN）进行分配。\n我国用户可向亚太网络信息中心（Asia Pacific Network Information Center, APNIC）申请IP地址，需要缴纳相应的费用，一般不接受个人申请。 2011年2月3日，因特网号码分配管理局（Internet Assigned Numbers Authority, IANA）（由ICANN行使职能）宣布，IPv4地址已经分配完毕。 我国在2014至2015年也逐步停止了向新用户和应用分配IPv4地址，同时全面开展商用部署IPv6。 IPv4地址的编址方法经历了三个历史阶段：分类编址-\u0026gt;划分子网-\u0026gt;无分类编址，无分类编址消除了分类编址和划分子网的概念。\n表示方法 由于IPv4地址由32比特构成，不方便阅读、记录以及输入等，因此IPv4地址采用点分十进制表示方法以方便用户使用。 分类编址 32比特的IPv4地址由网络号和主机号组成： 网络号 主机号 ● 标志主机（或路由器）的接口所连接到的网络 ● 标志主机（或路由器）的接口 ● 同一个网络中，不同主机（或路由器）的接口的IPv4地址的网络号必须相同，表示它们属于同一个网络。 ● 同一个网络中，不同主机（或路由器）的接口的IPv4地址的主机号必须各不相同，以便区分各主机（或路由器）的接口。 A类、B类和C类地址都是单播地址，只有单播地址可以分配给网络中的主机（或路由器）的各接口。\n主机号为“全0”的地址是网络地址，不能分配给主机（或路由器）的各接口。\n主机号为“全1”的地址是广播地址，不能分配给主机（或路由器）的各接口。\n各类地址详情 A类网络数量的$-2$是因为要去掉最小网络号$0$表示\u0026quot;本网络\u0026quot;，最大网络号$127$作为本地环回测试地址。两者都不能指派。 地址数量的$-2$是因为要去掉主机号全$0$的网络地址和全$1$的广播地址 网络类别 最小可指派网络号 最大可指派网络号 可指派网络数量 每个网络中最大可分配地址数量 不能指派的网络号 占总地址空间 A 1 126 126 ($2^{8-1}-2$) 16777214 ($2^{24}-2$) 0和127 50% ($2^{32-1}/2^{32}$) B 128.0 191.255 16384 ($2^{16-2}$) 65534 ($2^{16}-2$) 无 25% ($2^{32-2}/2^{32}$) C 192.0.0 223.255.255 2097152 ($2^{24}-3$) 254 ($2^{8}-2$) 无 12.5% ($2^{32-3}/2^{32}$) 网络类别 作用 第一个地址 最后 一个地址 地址数量 占总地址空间 D 多播地址 224.0.0.0 239.255.255.255 268435456 ($2^{28}$) 6.25% ($2^{32-4}/2^{32}$) E 保留 240.0.0.0 255.255.255.255 268435456 ($2^{28}$) 6.25% ($2^{32-4}/2^{32}$) 一般不使用的特殊IPv4地址 网络号 主机号 IP地址 作为源地址 作为目的地址 表示的意思 0 0 0.0.0.0 可以 不可以 在本网络上的本主机（例如，DHCP协议） 0 host-id 0.host-id 可以 不可以 在本网络上的某台主机host-id 全1 全1 255.255.255.255 不可以 可以 只在本网络上进行广播（各路由器均不转发） net-id 全1 A类：net-id.255.255.255 B类：net-id.255.255 C类：net-id.255 不可以 可以 对网络net-id上的所有主机进行广播 127 非全0或全1的任何数 127.0.0.1~127.255.255.254 可以 可以 用于本地软件环回测试 划分子网 随着更多的中小网络加入因特网，IPv4分类编址方法不够灵活、容易造成大量IPv4地址资源浪费的缺点就暴露出来了。\n为新增网络申请新的网络号存在以下弊端：\n需要等待时间和花费更多的费用 会增加其他路由器中路由条目的数量 浪费原有网络号中剩余的大量地址 子网掩码 子网掩码可以表明分类IPv4地址的主机号部分被借用了几个比特作为子网号。\n与IPv4地址类似，子网掩码也是由32比特构成的。\n用左起多个连续的比特1对应IPv4地址中的网络号和子网号； 之后的多个连续的比特0对应IPv4地址中的主机号。 将划分子网的IPv4地址与相应的子网掩码进行逐比特的逻辑与运算，就可得到该IPv4地址所在子网的网络地址。\n给定一个分类的IPv4地址和其相应的子网掩码，就可得出子网划分的细节：\n划分出的子网数量 每个子网可分配的地址数量 每个子网的网络地址和广播地址 每个子网可分配的最小地址和最大地址 默认子网掩码是指在未划分子网的情况下使用的子网掩码。\nA类：255.0.0.0 B类：255.255.0.0 C类：255.255.255.0 无分类编址 无分类编址结构是包含网络前缀和主机号的两级结构\n网络前缀是不定长的，仅从IPv4地址自身是无法确定其网络前缀和主机号的，需要配合使用32比特的地址掩码。\n无分类编址方法使用的地址掩码与划分子网使用的子网掩码类似，由32比特构成。\n用左起多个连续的比持1对应IPv4地址中的网络前缀； 之后的多个连续的比特0对应IPv4地址中的主机号。 为了简便起见，可以不明确给出配套的地址掩码的点分十进制形式，而是在无分类编址的IPv4地址后面加上斜线“/”，在斜线之后写上网络前缀所占的比特数量（也就是地址掩码中左起连续比特1的数量），这种记法称为斜线记法。\n比如$128.14.35.7/20$ 代表网络前缀是20比特，主机号是12比特 CIDR 实际上，无分类域间路由选择CIDR是将网络前缀都相同的、连续的多个无分类IPv4地址，组成一个CIDR地址块，只要知道CIDR地址块中的任何一个地址，就可以知道该地址块的以下全部细节：\n地址块中的最小地址 地址块中的最大地址 地址块中的地址数量 地址块中聚合某类网络（A类、B类、C类）的数量 地址掩码 使用CIDR的一个好处是，可以根据客户的需要分配适当大小的CIDR地址块，因此可以更加有效地分配IPv4的地址空间。\n路由聚合 使用CIDR的另一个好处是路由聚合（也称为构造超网）。\n网络前缀越长，地址块越小，路由越具体；\n若路由器查表转发分组时发现有多条路由条目匹配，则选择网络前缀最长的那条路由条目，这称为最长前缀匹配，因为这样的路由更具体。\n【2021年 题35】现将一个IP网络划分为3个子网，若其中一个子网是192.168.9.128/26，则下列网络中。不可能是另外两个子网之一的是( ) A. 192.168.9.0/25 B. 192.168.9.0/26 C. 192.168.9.192/26 D. 192.168.9.192/27\n题干子网的192.168.9.128/26，即主机号用6位来表示，代表地址范围为192.168.9.128~192.168.9.192。\nA选项的192.168.9.0/25代表地址范围为192.168.9.0192.168.9.128，然后和C选项的192.168.9.192/26代表的192.168.9.192192.168.9.255，加上题干，这三者可以聚合成地址块192.168.9.0/24。\nD选项的192.168.9.192/27代表地址范围为192.168.9.192~192.168.9.223，加上题干，再结合一个192.168.9.224/27，这三者就可聚合成地址块192.168.9.128/25。\n那么按照同样的逻辑，选项B的CIDR和题干结合，可见中间缺少64个地址，但是这三者合成是192.168.9.0~192.168.9.191，这聚合不成一个完整的网络，需要再引入第四个CIDR，和题干要求的三个不符，故答案是B。\n4.2.3 IPv4地址的应用规划 IPv4地址的应用规划是指将给定的IPv4地址块（或分类网络）划分成若干个更小的地址块（或子网），并将这些地址块（或子网）分配给互联网中的不同网络，进而可以给各网络中的主机和路由器的接口分配IPv4地址。 定长的子网掩码 (Fixed Length Subnet Mask, FLSM) 变长的子网掩码 (Variable Length Subnet Mask, VLSM) ● 所划分出的每一个子网都使用同一个子网掩码。 ● 所划分出的每一个子网可以使用不同的子网掩码。 ● 子网划分方式不灵活：只能划分出 $2^n$ 个子网（n是从主机号借用的、用来作为子网号的比特数量）。 ● 子网划分方式灵活：可以按需分配。 ● 每个子网所分配的IP地址数量相同，容易造成地址资源的浪费。 ● 每个子网所分配的IP地址数量可以不同，尽可能减少对地址资源的浪费。 使用定长子网掩码划分子网 【举例】假设申请到的C类网络为218.75.230.0，使用定长的子网掩码给下图所示的小型互联网中的各设备分配IPv4地址。\n应用需求：网络1需要IP地址数量为9，网络2需要IP地址数量为28，网络3需要IP地址数量为15，网络4需要IP地址数量为13，网络5需要IP地址数量为4。\n申请到的C类网络地址 218.75.230 . 0 从主机号借用3比特作为子网号 子网数量：$2^3 = 8$（满足应用需求） 每个子网上的地址数量：$2^{8-3} = 32$（满足应用需求）\n子网掩码 255.255.255 . 224 （下图的子网掩码是错误的） 24个连续的比特1对应网络号\n3个连续的比特1表示从主机号借用3个比特作为子网号\n使用变长子网掩码划分子网 【举例】假设申请到的C类网络为218.75.230.0/24，使用变长的子网掩码给下图所示的小型互联网中的各设备分配IPv4地址。\n应用需求：从地址块218.75.230.0/24中取出5个地址块：/30, /28, /28, /28, /27, 按需分配给图中的5个网络。\n在地址块中选取子块的原则\n每个子块的起点位置不能随便选取，只能选取主机号部分是块大小整数倍的地址作为起点。\n建议先为大的子块选取。\n4.2.4 IPv4地址与MAC地址 IPv4地址与MAC地址的封装位置 数据报传送过程中IPv4地址与MAC地址的变化情况 在数据包的传送过程中，数据包的源IP地址和目的IP地址保持不变。 在数据包的传送过程中，数据包的源MAC地址和目的MAC地址逐链路（或网络）改变。 IPv4地址与MAC地址的关系 如果仅使用MAC地址进行通信，则会出现以下主要问题： 因特网中的每台路由器的路由表中就必须记录因特网上所有主机和路由器各接口的MAC地址。 手工给各路由器配置路由表几乎是不可能完成的任务，即使使用路由协议让路由器通过相互交换路由信息来自动构建路由表，也会因为路由信息需要包含海量的MAC地址信息而严重占用通信资源。 包含海量MAC地址的路由信息需要路由器具备极大的存储空间，并且会给分组的查表转发带来非常大的时延。 因特网的网际层使用IP地址进行寻址，就可使因特网中各路由器的路由表中的路由记录的数量大大减少，因为只需记录部分网络的网络地址，而不是记录每个网络中各通信设备的各接口的MAC地址。 路由器收到IP数据报后，根据其首部中的目的IP地址的网络号部分，基于自己的路由表进行查表转发。 查表转发的结果可以指明IP数据报的下一跳路由器的IP地址，但无法指明该IP地址所对应的MAC地址。因此，在数据链路层封装该IP数据报成为帧时，帧首部中的目的MAC地址字段就无法填写，该问题需要使用网际层中的地址解析协议ARP来解决。 4.2.5 地址解析协议ARP IP地址要经地址解析协议ARP转化成MAC地址\n例如有一个由A, B, C三台主机通过交换机组成的网络，A要向B发送一个分组\nA知道B的IP地址，但不知道B的MAC地址，即主机A的ARP高速缓存表没有记录，那么A的数据链路层封装MAC帧时，无法填写目的MAC地址。 主机A发送ARP请求报文，目的地址是广播地址，内容有主机B的IP地址。交换机通过所有端口将请求报文转发出去。主机收到该广播帧后，交付上层处理。 主机B再发送ARP响应报文，是单播报文，目的MAC地址为主机A的MAC地址，内容记录了主机B的MAC地址。主机A收到后，将其IP地址和MAC地址对应填到ARP高速缓存表中。 转发表中的MAC地址与交换机接口号的对应关系记录也要周期性删除，因为这种对应关系并不是永久不变的。\nARP 协议的相关注意事项： 由于 ARP 协议的主要用途是从网际层使用的 IP 地址解析出在数据链路层使用的 MAC 地址。因此，有的教材将 ARP 协议划归在 网际层，而有的教材将 ARP 协议划归在 数据链路层。这两种做法都是可以的。 除了本节课介绍的 ARP 请求报文和响应报文，ARP 协议还有其他类型的报文，例如用于检查 IP 地址冲突的 “无故 ARP”（Gratuitous ARP）。 由于 ARP 协议很早就制定出来了（1982 年 11 月），当时并没有考虑网络安全问题。因此，ARP 协议没有安全验证机制，存在 ARP 欺骗和攻击 等问题。 4.2.6 IP数据报的发送和转发过程 主机发送IP数据报 判断目的主机是否与自己在同一个网络 若在同一个网络，则属于直接交付； 若不在同一个网络，则属于间接交付。发送给主机所在网络的默认网关（路由器），由默认网关帮忙转发。 路由器转发IP数据报 基于IP数据报首部中的目的IP地址在路由表中进行查询 若找到匹配的路由条目，则按该路由条目的指示进行转发； 否则丢弃该IP数据报，并向发送该IP数据报的源主机发送差错报告。 4.2.7 IPv4数据报的首部格式 首部长度 长度为4个比特，该字段的取值以4字节为单位，用来表示IPv4数据报的首部长度。\n最小取值为二进制的0101，即十进制的5，再乘以4字节单位，表示IPv4数据报首部只有20字节固定部分。\n最大取值为二进制的1111，即十进制的15，再乘以4字节单位，表示IPv4数据报首部包含20字节固定部分和最大40字节可变部分。\n【举例】IPv4数据报首部中的首部长度字段和总长度字段。\n1 2 3 4 ___________________________________________________ | IPv4首部（20字节） | IPv4数据报的数据载荷（1000字节） | ___________________________________________________ 总长度：1020字节 首部长度字段的取值 = $(0101)_2$，以4字节为单位 总长度字段的取值 = $(0000\\ 0011\\ 1111\\ 1100)_2$，以字节为单位 首部长度 = $(0101)_2 \\times 4 = 5 \\times 4 =$ 20（字节） 总长度 = $(0000\\ 0011\\ 1111\\ 1100)_2 =$ 1020（字节） 数据载荷长度 = 总长度 - 首部长度 = 1020 - 20 = 1000（字节） 填充 当首部长度（20字节固定部分+可变部分）的长度不是4字节整数倍时，填充相应数量的全0字节，以确保IPv4数据报的首部长度是4字节的整数倍。 分片 【举例】某个IPv4数据报总长度为3820字节，采用20字节固定首部，根据数据链路层要求，需要将该IPv4数据报分片为长度不超过1420字节的数据报片。\n总长度 标识 MF DF 片偏移 原IPv4数据报 20+3800 23333 0 0 0 / 8 = 0 分片数据报1 20+1400 23333 1 0 0 / 8 = 0 分片数据报2 20+1400 23333 1 0 1400 / 8 = 175 分片数据报3 20+1000 23333 0 0 2800 / 8 = 350 【2021年 题36】若路由器向MTU=800B的链路转发一个总长度为1580B的IP数据报（首部长度为20B）时，进行了分片，且每个分片尽可能大，则第2个分片的总长度字段和MF标志位的值分别是（ B ）。\nA. 796，0\nB. 796，1\nC. 800，0\nD. 800，1\n先尝试以800字节作为一个分片：首部20B，数据载荷780B，第二个分片的片偏移为$780 \\div 8=97.5$ 但是片偏移必须是整数。\n数据载荷长度就应取小于800且能整除8的最大数，为776，那么第二个分片的总长度就为$20+776=796$。\n但是这两个数据载荷的容量不够，还有第三个分片，MF标志应该为1。\n标识 长度为16个比特，属于同一个IPv4数据报的各分片数据报应该具有相同的标识。 IP软件会维持一个计数器，每产生一个IPv4数据报，计数器值就加1，并将此值赋给标识字段。 标志 最低位（More Fragment, MF） MF=1表示本分片后面还有分片 MF=0表示本分片后面没有分片 中间位（Don’t Fragment, DF） DF=1表示不允许分片 DF=0表示允许分片 最高位为保留位，必须设置为0 片偏移 长度为13个比特，该字段的取值以8字节为单位，用来指出分片IPv4数据报的数据载荷偏移其在原IPv4数据报的位置有多远。 生存时间 长度为8个比特，最大取值为二进制的$11111111$，即十进制的255。该字段的取值最初以秒为单位。因此，IPv4数据报的最大生存时间最初为255秒。路由器转发IPv4数据报时，将其首部中该字段的值减去该数据报在路由器上所耗费的时间，若结果不为0就转发，否则就丢弃。 生存时间字段后来改为以“跳数”为单位，路由器收到待转发的IPv4数据报时，将其首部中的该字段的值减1，若结果不为0就转发，否则就丢弃。 【举例】生存时间TTL字段的作用 —— 防止被错误路由的IPv4数据报无限制地在因特网中兜圈\n协议 长度为8个比特，用来指明IPv4数据报的数据载荷是何种协议数据单元PDU。\n常用的一些协议和相应的协议字段值\n协议名称 ICMP IGMP TCP UDP IPv6 OSPF 协议字段值 1 2 6 17 41 89 首部检验和 长度为16个比特，用于检测IPv4数据报在传输过程中其首部是否出现了差错。 IPv4数据报每经过一个路由器，其首部中的某些字段的值（例如生存时间TTL、标志以及片偏移等）都可能发生变化，因此路由器都要重新计算一下首部检验和。 上述检验和的计算方法不仅用于IP协议，还用于运输层的用户数据报协议UDP和传输控制协议TCP，常被称为因特网检验和（Internet Checksum）。这种检验和的检错性能虽然不如CRC，但更易用软件实现。 IP地址 源IP地址：长度为32个比特，用来填写发送IPv4数据报的源主机的IPv4地址。\n目的IP地址：长度为32个比特，用来填写接收IPv4数据报的目的主机的IPv4地址。\n4.3 静态路由配置 静态路由配置是指用户或网络运维人员使用路由器的相关命令给路由器人工配置路由表。 人工配置方式简单、开销小、但不能及时适应网络状态（流量、拓扑等）的变化，一般只在小规模网络中采用。 进行静态路由配置需要认真考虑和谨慎操作，否则可能出现以下问题： 路由条目配置错误，甚至导致出现路由环路。 聚合路由条目时可能引入不存在的网络。 目的网络 下一跳 类型 0.0.0.0/0 10.0.0.2 静态 在默认路由条目中，有目的网络 $0.0.0.0/0$ 其中 $0.0.0.0$ 表示任何网络，而网络前缀 $/0$ 是最短的网络前缀。\n路由器在查找转发表转发IP数据报时，遵循“最长前缀匹配”的原则，因此默认路由条目的匹配优先级最低。\n目的网络 下一跳 类型 192.168.2.0/24 10.0.0.2 静态 192.168.2.1/32 10.0.0.2 静态 4.4 因特网的路由选择协议 4.4.1 路由选择分类 静态路由选择 采用人工配置的方式给路由器添加网络路由、默认路由和特定主机路由等路由条目。 静态路由选择简单、开销小，但不能及时适应网络状态（流量、拓扑等）的变化。 静态路由选择一般只在小规模网络中采用。 动态路由选择 路由器通过路由选择协议自动获取路由信息。 动态路由选择比较复杂、开销比较大，但能较好地适应网络状态的变化。 动态路由选择适用于大规模网络。 4.4.2 分层次的路由选择协议 路由选择协议主要特点：\n自适应：因特网采用动态路由选择，能较好地适应网络状态的变化 分布式：因特网中的各路由器通过相互间的信息交互，共同完成路由信息的获取和更新。 分层次：将整个因特网划分为许多较小的自治系统（Autonomous System，AS）。在自治系统内部和外部采用不同类别的路由选择协议，分别进行路由选择。 自治系统内部是用内部网关协议IGP，连接自治系统的是外部网关协议EGP\n外部网关协议EGP和内部网关协议IGP只是路由选择协议的分类名称，而不是具体的路由选择协议。\n外部网关协议和内部网关协议名称中使用的是“网关”这个名词，是因为在因特网早期的RFC文档中，没有使用“路由器”而使用的是“网关”这一名词。\n4.4.3 路由信息协议RIP 基本概念 路由信息协议（Routing Information Protocol，RIP）是内部网关协议中最先得到广泛使用的协议之一，其相关标准文档为[RFC 1058]。\nRIP要求自治系统AS内的每一个路由器，都要维护从它自己到AS内其他每一个网络的距离记录。这是一组距离，称为距离向量（Distance-Vector，D-V）。\nRIP使用跳数（Hop Count）作为度量（Metric）来衡量到达目的网络的距离。\nRIP将路由器到直连网络的距离定义为1。 RIP将路由器到非直连网络的距离定义为所经过的路由器数加1。 RIP允许一条路径最多只能包含15个路由器，距离等于16时相当于不可达。因此RIP只适用于小型互联网。 RIP认为好的路由就是“距离短”的路由，也就是所通过路由器数量最少的路由。\n当到达同一目的网络有多条RIP距离相等的路由时，可以进行等价负载均衡，也就是将通信量均衡地分布到多条等价的路径上。\nRIP具有以下三个重要特点：\n和谁交换信息：仅和相邻路由器交换信息。 交换什么信息：路由器自己的路由表。 即本路由器到所在自治系统AS中各网络的最短RIP距离，以及到各网络应经过的下一跳路由器。 何时交换信息：周期性交换（例如，每个约30秒）。 为了加快RIP的收敛速度，当网络拓扑发生变化时，路由器要及时向相邻路由器通告拓扑变化后的路由信息，这称为触发更新。 基本工作过程 路由器刚开始工作时，只知道自己到直连网络的RIP距离为1。 每个路由器仅和相邻路由器周期性地交换并更新路由信息。 若干次交换和更新后，每个路由器都知道到达本自治系统AS内各网络的最短距离和下一跳路由器，称为收敛。 距离向量算法 RIP路由条目更新规则\n发现了新网络，添加 到达目的网络，相同下一跳，最新消息，更新 到达目的网络，不同下一跳，新路由优势，更新 到达目的网络，不同下一跳，新路由劣势，不更新 到达目的网络，不同下一跳，RIP距离相同，等价负载均衡 除了上述RIP路由条目更新规则，在RIP的距离向量算法中还包含以下一些时间参数：\n路由器每隔大约30秒向其所有相邻路由器发送路由更新报文。 若180秒（默认）没有收到某条路由条目的更新报文，则把该路由条目标记为无效（即把RIP距离设置为16，表示不可达），若再过一段时间（如120秒），还没有收到该路由条目的更新报文，则将该路由条目从路由表中删除。 【2010年 题35】某自治系统内采用RIP协议，若该自治系统的路由器R1收到其邻居路由器R2的距离矢量，距离矢量中包含信息\u0026lt;net1,16\u0026gt;，则能得出的结论是（D）。\nA. R2可以经过R1到达net1，跳数为17\nB. R2可以达到net1，跳数为16\nC. R1可以经过R2到达net1，跳数为17\nD. R1不能经过R2到达net1\n在RIP协议中，距离16表明目的网络不可达。\n因此，R2无法到达net1，R1也无法通过R2到达net1。\n【2021年 题37】某网络中的所有路由器均采用距离向量路由算法计算路由。若路由器E与邻居路由器A、B、C和D之间的直接链路距离分别是8，10，12和6，且E收到邻居路由器的距离向量如下表所示，则路由器E更新后的到达目的网络Net1~Net4的距离分别是（D）。\n目的网络 A的距离向量 B的距离向量 C的距离向量 D的距离向量 Net1 1 23 20 22 Net2 12 35 30 28 Net3 24 18 16 36 Net4 36 30 8 24 A. 9, 10, 12, 6 B. 9, 10, 28, 20 C. 9, 20, 12, 20 D. 9, 20, 28, 20\n存在的问题 “坏消息传播得慢”的问题又被称为路由环路或RIP距离无穷计数问题。这是距离向量算法的一个固有问题。可以采取以下多种措施减少出现该问题的概率或减小该问题带来的危害： 限制最大RIP距离为15（16表示不可达）。 当路由表发生变化时就立即发送路由更新报文（即“触发更新”），而不仅是周期性发送。 让路由器记录收到某个特定路由信息的接口，而不让同一路由信息再通过此接口向反方向传送（即“水平分割”）。 使用上述措施仍无法彻底解决问题。因为在距离向量算法中，每个路由器都缺少到目的网络整个路径的完整信息，无法判断所选的路由是否出现了环路。 版本和相关报文的封装 现在较新的RIP版本是1998年11月公布的RIP2[RFC 2453]，已经成为因特网标准协议。与RIP1相比，RIP2可以支持变长子网掩码和CIDR。另外，RIP2还提供简单的鉴别过程并支持多播。 RIP相关报文使用运输层的用户数据报协议UDP进行封装，使用的UDP端口号为520。 从RIP报文封装的角度看，RIP属于TCP/IP体系结构的应用层。 但RIP的核心功能是路由选择，这属于TCP/IP体系结构的网际层。 RIP的优缺点 优点 实现简单，路由器开销小。 如果一个路由器发现了RIP距离更短的路由，那么这种更新信息就传播得很快，即“好消息传播得快”。 缺点 RIP限制了最大RIP距离为15，这就限制了使用RIP的自治系统AS的规模。 相邻路由器之间交换的路由信息是路由器中的完整路由表，因而随着网络规模的扩大，开销也随之增大。 “坏消息传播得慢”，使更新过程的收敛时间过长。因此，对于规模较大的自治系统AS，应当使用OSPF协议。 4.4.4 开放最短路径优先OSPF 基本概念 开放最短路径优先（Open Shortest Path First，OSPF）协议是为了克服路由信息协议RIP的缺点在1989年开发出来的。 “开放”表明OSPF协议不是受某一厂商控制，而是公开发表的。 “最短路径优先”是因为使用了Dijkstra提出的最短路径算法（Shortest Path First，SPF）。 OSPF是基于链路状态的，而不像RIP是基于距离向量的。 OSPF基于链路状态并采用最短路径算法计算路由，从算法上保证了不会产生路由环路。 OSPF不限制网络规模，更新效率高，收敛速度快。 链路状态（Link State，LS）是指本路由器都和哪些路由器相邻，以及相应链路的“代价（cost）”。 “代价”用来表示费用、距离、时延和带宽等，这些都由网络管理人员来决定。 OSPF路由器邻居关系的建立和维护 \u0026hellip; 目的IP地址 协议号 \u0026hellip; 224.0.0.5 89 OSPF问候分组 OSPF相邻路由器之间通过交互问候（Hello）分组来建立和维护邻居关系。 问候（Hello）分组封装在IP数据报中，发往组播地址224.0.0.5。IP数据报首部中的协议号字段的取值为89，表明IP数据报的数据载荷为OSPF分组。 问候（Hello）分组的发送周期为10秒。 若40秒未收到来自邻居路由器的问候（Hello）分组，则认为邻居路由器不可达。 每个路由器都会建立一张邻居表。 链路状态 使用OSPF的每个路由器都会产生链路状态通告（Link State Advertisement，LSA）。\nLSA中包含以下两类链路状态信息：\n直连网络的链路状态信息\n邻居路由器的链路状态信息\n链路状态通告LSA被封装在链路状态更新（Link State Update, LSU）分组中，采用可靠的洪泛法（Flooding）进行发送。\n洪泛法的要点是路由器向自己所有的邻居路由器发送链路状态更新分组，收到该分组的各路由器又将该分组转发给自己所有的邻居路由器（但其上游路由器除外），以此类推。 可靠是指收到链路状态更新分组后要发送确认，收到重复的更新分组无需再次转发，但要发送一次确认。 使用OSPF的每一个路由器都有一个链路状态数据库（Link State Database, LSDB），用于存储链路状态通告LSA。\n通过各路由器洪泛发送封装有各自链路状态通告LSA的链路状态更新分组LSU，各路由器的链路状态数据库LSDB最终将达到一致。\n基于链路状态数据库进行最短路径优先计算 OSPF分组类型 分组类型 功能描述 问候 (Hello) 用来发现和维护邻居路由器的可达性。 数据库描述 (Database Description) 用来向邻居路由器给出自己的链路状态数据库中的所有链路状态项目的摘要信息。 链路状态请求 (Link State Request) 用来向邻居路由器请求发送某些链路状态项目的详细信息。 链路状态更新 (Link State Update) 路由器使用链路状态更新分组将其链路状态信息进行洪泛发送，即用洪泛法对整个系统更新链路状态。 链路状态确认 (Link State Acknowledgement) 对链路状态更新分组的确认分组。 OSPF基本工作过程 多点接入网络中的OSPF路由器 为了减少洪泛发送问候分组和链路状态更新分组的数量，OSPF采用以下措施：\n选举指定路由器（Designated Router，DR）和备用的指定路由器（Backup Designated Router，BDR） 所有的非DR/BDR只与DR/BDR建立邻居关系 非DR/BDR之间通过DR/BDR交换信息 如果不引入DR/BDR，也就是所有路由器两两建立关系，总连接数为： $$ Total = \\frac{n(n-1)}{w} $$ 引入DR/BDR后，有 DRothers（非 DR/BDR 路由器）不再两两建立邻接关系，而是仅与 DR 和 BDR 建立关系\nDRothers 与 DR 建立关系：除了 DR 自身，剩下的 $n-1$ 台路由器（包括 BDR）都要与 DR 建立关系：连接数：$n-1$ DRothers 与 BDR 建立关系：除了 BDR 自身和已经与它有连接的 DR，剩下的 $n-2$ 台 DRothers 需要与 BDR 建立关系：连接数：$n-2$ DR 与 BDR 建立关系：它们之间必须同步：连接数：$1$ OSPF划分区域 采用划分区域的方法，虽然使交换信息的种类增多了，同时也使OSPF协议更加复杂了，但这样做能使每一个区域内部交换路由信息的通信量大大减小，因而使OSPF协议能够用于规模更大的自治系统AS。 4.4.5 边界网关协议BGP 基本概念 边界网关协议（Border Gateway Protocol, BGP）属于外部网关协议EGP这个类别，用于自治系统AS之间的路由选择协议。 由于在不同AS内度量路由的“代价”（距离、带宽、费用等）可能不同，因此对于AS之间的路由选择，使用统一的“代价”作为度量来寻找最佳路由是不行的。 AS之间的路由选择还必须考虑相关策略，这些策略包括政治、经济、安全等，它们都是由网络管理员对每一个路由器进行设置的。但这些策略并不是自治系统之间的路由选择协议本身。 BGP只能是力求寻找一条能够到达目的网络且比较好的路由（即不能兜圈子），而并非要寻找一条最佳路由。 在配置BGP时，每个AS的管理员要选择至少一个路由器作为该AS的“BGP发言人”。 一般来说，两个BGP发言人都是通过一个共享网络连接在一起的，而BGP发言人往往就是BGP边界路由器。 使用TCP连接交换路由信息的两个BGP发言人，彼此称为对方的邻站（neighbor）或对等站（peer）。 BGP发言人除了运行BGP协议外，还必须运行自己所在AS所使用的内部网关协议IGP，例如RIP或OSPF。 BGP发言人交换网络可达性的信息，也就是要到达某个网络所要经过的一系列自治系统。 当BGP发言人相互交换了网络可达性的信息后，各BGP发言人就根据所采用的策略，从收到的路由信息中找出到达各自治系统的较好的路由，也就是构造出树形结构且不存在环路的自治系统连通图。 BGP适用于多级结构的因特网。 BGP-4的四种报文 BGP-4是目前使用得最多的版本，在[RFC 4271]中规定了BGP-4的四种报文：\n报文类型 功能描述 打开 OPEN 用来与相邻的另一个BGP发言人建立关系，使通信初始化。 保活 KEEPALIVE 用来周期性地证实邻站的连通性。 更新 UPDATE 用来通告某一条路由的信息，以及列出要撤销的多条路由。 通知 NOTIFICATION 用来发送检测到的差错。 【2017年 题37】直接封装RIP、OSPF、BGP报文的协议分别是（D）。\nA. TCP、UDP、IP B. TCP、IP、UDP C. UDP、TCP、IP D. UDP、IP、TCP\n从实现功能（路由选择）的角度看，这三个路由选择协议都属于网络层。\n从数据包按网络体系结构逐层封装的角度看，RIP和BGP属于应用层，OSPF属于网络层。\n4.4.6 路由器的基本工作原理 路由器被定义为一种具有多个输入/输出端口的专用计算机，其核心任务是转发分组。 路由选择部分：核心构件是路由选择处理机，其任务是根据所使用的路由选择协议，周期性地与其他路由器进行信息的交换，以便构建和更新路由表。 分组转发部分：由一组输入端口、交换结构以及一组输出端口。 4.5 网际控制报文协议ICMP 为了更有效地转发IP数据报以及提高IP数据报交付成功的机会，TCP/IP体系结构的网际层使用了网际控制报文协议 (Internet Control Message Protocol, ICMP) [RFC 792]。 主机或路由器使用ICMP来发送差错报告报文和询问报文。 ICMP报文被封装在IP数据报中发送。 以下情况不应发送ICMP差错报告报文： 对ICMP差错报告报文不再发送ICMP差错报告报文。 对第一个分片的IP数据报片的所有后续数据报片都不发送ICMP差错报告报文。 对具有多播地址的IP数据报都不发送ICMP差错报告报文。 对具有特殊地址（例如127.0.0.0或0.0.0.0）的IP数据报不发送ICMP差错报告 ICMP的典型应用 分组网间探测PING 跟踪路由traceroute ICMP报文类型 差错报告报文 终点不可达：当路由器或主机不能交付IP数据报时，就向源点发送终点不可达报文。\n源点抑制：当路由器或主机由于拥塞而丢弃IP数据报时，就向源点发送源点抑制报文，使源点知道应当把IP数据报的发送速率放慢\n时间超过（超时）：当路由器收到TTL=1的IP数据报时，将其丢弃，并向源点发送时间超过报文。另外，当终点在预先规定的时间内未能收到一个数据报的全部数据报分片时，就把已收到的数据报片都丢弃，也会向源点发送时间超过（超时）报文。\n参数问题：当路由器或目的主机收到IP数据报后发现其首部中有不正确的字段值时，就丢弃该数据报，并向源点发送参数问题报文。\n改变路由（重定向）：路由器把改变路由报文发送给主机，让主机知道下次应将IP数据报发送给另外的路由器，这样可以通过更好的路由到达目的主机。\n询问报文 回送请求和回答：用来测试目的站是否可达以及了解其有关状态。 时间戳请求和回答：用来进行时钟同步和测量时间。 分组网间探测PING 分组网间探测PING用来测试主机或路由器之间的连通性。 PING是TCP/IP体系结构的应用层直接使用网际层ICMP的一个例子，它并不使用运输层的TCP或UDP。 PING应用所使用的ICMP报文类型为回送请求和回答。 跟踪路由 跟踪路由应用 traceroute，用于探测 IP 数据报从源主机到达目的主机要经过哪些路由器。\n在不同操作系统中，traceroute 应用的命令和实现机制有所不同：\n在 UNIX 版本中，具体命令为 “traceroute”，其在运输层使用 UDP 协议，在网络层使用 ICMP 报文类型只有差错报告报文。 在 Windows 版本中，具体命令为 “tracert”，其应用层直接使用网际层的 ICMP 协议，所使用的 ICMP 报文类型有回送请求和回答报文以及差错报告报文。 4.6 虚拟专用网和网络地址转换 4.6.1 虚拟专用网 VPN 虚拟专用网 (Virtual Private Network, VPN)：利用公用的因特网作为本机构各专用网之间的通信载体，这样形成的网络又称为虚拟专用网。 给专用网内各主机配置的IP地址应该是该专用网所在机构可以自行分配的IP地址，这类IP地址仅在机构内部有效，称为专用地址 (Private Address)，不需要向因特网的管理机构申请。 [RFC 1918] 规定了以下三个 CIDR 地址块中的地址作为专用地址： 10.0.0.0 ~ 10.255.255.255 (CIDR 地址块 10/8) 172.16.0.0 ~ 172.31.255.255 (CIDR 地址块 172.16/12) 192.168.0.0 ~ 192.168.255.255 (CIDR 地址块 192.168/16) 很显然，全世界可能有很多不同机构的专用网具有相同的专用 IP 地址，但这并不会引起麻烦，因为这些专用地址仅在机构内部使用。\nVPN要保证传输数据的安全性，会将原始的内部IP数据报进行加密，然后再将其封装成为在因特网上传送的外部IP数据报。 同一机构内不同部门的内部网络所构成的VPN，又称为内联网VPN。 有时，一个机构的虚拟专用网VPN需要某些外部机构（通常是合作伙伴）参加进来，这样的VPN就称为外联网VPN。 在外地工作的员工需要访问公司内部的专用网时，只要在任何地点接入因特网，运行驻留在员工PC中的VPN软件，在员工的PC和公司的主机之间建立VPN隧道，就可以访问专用网中的资源，这种虚拟专用网又称为远程接入VPN。 4.6.2 网络地址转换 NAT 网络地址转换（Network Address Translation, NAT）技术于1994年被提出，用来缓解IPv4地址空间即将耗尽的问题。 NAT能使大量使用内部专用地址的专用网络用户共享少量外部全球地址来访问因特网上的主机和资源。 这种方法需要在专用网络连接到因特网的路由器上安装NAT软件。装有NAT软件的路由器称为NAT路由器，它至少要有一个有效的外部全球地址IP$_G$。这样，所有使用内部专用地址的主机在和外部因特网通信时，都要在NAT路由器上将其内部专用地址转换成IP$_G$。 如果NAT路由器拥有 $n$（$n$ 比较小）个全球IP地址，那么专用网内最多可以同时有 $n$ 台主机接入因特网。若专用网内的主机数量大于 $n$，则需要轮流使用NAT路由器中数量较少的全球IP地址。 由于目前绝大多数基于TCP/IP协议栈的网络应用，都使用运输层传输控制协议TCP或用户数据报协议UDP，为了更加有效地利用NAT路由器中的全球IP地址，现在常将NAT转换和运输层端口号结合使用。这样就可以使内部专用网中使用专用地址的大量主机，共用NAT路由器上的1个全球IP地址，因而可以同时与因特网中的不同主机进行通信。 将NAT和运输层端口号结合使用，称为网络地址与端口号转换（Network Address and Port Translation, NAPT）。现在很多家用路由器将家中各种智能设备（手机、平板、笔记本电脑、台式电脑、物联网设备等）接入因特网，这种路由器实际上就是一个NAPT路由器，但往往并不运行路由选择协议。 尽管NAT（和NAPT）的出现在很大程度上缓解了IPv4地址资源紧张的局面，但NAT（和NAPT）对网络应用并不完全透明，会对某些网络应用产生影响。 NAT（和NAPT）的一个重要特点就是通信必须由专用网内部发起，因此拥有内部专用地址的主机不能直接充当因特网中的服务器。 对于目前P2P这类需要外网主机主动与内网主机进行通信的网络应用，在通过NAT时会遇到问题，需要网络应用自身使用一些特殊的NAT穿透技术来解决。 5.1 运输层概述 5.1.1 进程间基于网络的通信 第2~4章依次介绍了计算机网络体系结构中的物理层、数据链路层和网络层，它们共同解决了将主机通过异构网络互联起来所面临的问题，实现了主机到主机的通信。 然而在计算机网络中实际进行通信的真正实体，是位于通信两端主机中的进程。 如何为运行在不同主机上的应用进程提供直接的逻辑通信服务，就是运输层的主要任务。运输层协议又称为端到端协议。 运输层向应用层实体屏蔽了下面网络核心的细节（例如网络拓扑、所采用的路由选择协议等），它使应用进程看见的就好像是在两个运输层实体之间有一条端到端的逻辑通信信道。 根据应用需求的不同，因特网的运输层为应用层提供了两种不同的运输层协议，即面向连接的TCP和无连接的UDP，这两种协议就是本章要讨论的主要内容。 5.1.2 TCP/IP 运输层中的两个重要协议 TCP 传输控制协议 (Transmission Control Protocol, TCP) 为其上层提供的是面向连接的可靠的数据传输服务。 使用TCP通信的双方，在传送数据之前必须首先建立TCP连接（逻辑连接，而非物理连接）。数据传输结束后必须释放TCP连接。 TCP为了实现可靠传输，就必须使用很多措施，例如TCP连接管理、确认机制、超时重传、流量控制以及拥塞控制等。 TCP的实现复杂，TCP报文段的首部比较大，占用处理机资源比较多。 UDP 用户数据报协议 (User Datagram Protocol, UDP) 为其上层提供的是无连接的不可靠的数据传输服务。 使用UDP通信的双方，在传送数据之前不需要建立连接。 UDP不需要实现可靠传输，因此不需要使用实现可靠传输的各种机制。 UDP的实现简单，UDP用户数据报的首部比较小。 5.1.3 运输层端口号、复用与分用 运输层端口号 运行在计算机上的进程是使用进程标识符 (Process Identification, PID) 来标识的。 然而，因特网上的计算机并不是使用统一的操作系统，而不同操作系统 (Windows、Linux、MacOS) 又使用不同格式的进程标识符。 为了使运行不同操作系统的计算机的应用进程之间能够基于网络进行通信，就必须使用统一的方法对TCP/IP体系的应用进程进行标识。 TCP/IP体系结构的运输层使用端口号来标识和区分应用层的不同应用进程。端口号的长度为16比特，取值范围是0~65535。 熟知端口号：由IANA分配给TCP/IP体系结构应用层中最重要的提示应用协议。 登记端口号：为没有熟知端口号的应用程序使用。要使用这类端口号，必须在IANA进行登记，以防止重复。例如，Microsoft RDP微软远程桌面应用程序使用的端口号是3389。 短暂端口号：仅在客户端使用，由客户进程在运行时动态选择，通信结束后会被系统收回。 FTP SMTP DNS DHCP HTTP BGP HTTPS RIP 21/20 25 53 67/68 80 179 443 520 端口号只具有本地意义，即端口号只是为了标识本计算机网络协议栈应用层中的各应用进程。在因特网中，不同计算机中的相同端口号是没有关系的，即相互独立。另外，TCP和UDP端口号之间也是没有关系的。 发送方的复用和接收方的分用 5.1.4 UDP和TCP的对比 ","date":"2026-02-20T14:10:19+08:00","permalink":"/p/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/","title":"计算机网络"},{"content":"近日笔者在使用群晖NAS的v2rayA时遇到了时间同步不正确导致的无法正常使用的问题，导致emby无法刮削。虽然不知道原因，但也是个老问题了，版本很旧，而且用的是第三方软件市场的docker，现在docker也不能裸连获取了。下面是解决经验：\n重新安装v2rayA 配置docker版的emby。 参考网站：\nLinux 后备安装方式 - v2rayA\nXTLS/Xray-core: Xray, Penetrates Everything. Also the best v2ray-core. Where the magic happens. An open platform for various uses.\n群晖实现透明代理 - v2rayA\nsjtuross/syno-iptables: Some missing iptables modules for Synology\n群晖 DSM 7.2 为 Container Manager（docker）设置代理_群晖docker设置代理-CSDN博客\nv2rayA 由v2rayA的官方文档可见，群晖并没有特别适配的版本，所以采用通用的二进制文件进行安装。既然裸连也无法从github上拉取文件，我们在电脑上下好传到NAS上。此外，下面的安装内容实则和文档略有不同。\n拷贝v2rayA和xray\n1 2 3 4 cp v2raya_linux_x64 /usr/local/bin/v2raya cp xray /usr/local/bin/ chmod +x /usr/local/bin/v2raya chmod +x /usr/local/bin/xray 拷贝xray自带的GFWList代理规则文件：geoip.dat和geosite.dat\n1 2 3 cd /usr/local/share mkdir xray cp .../g* xray 修改service配置文件\n1 sudo vi /etc/systemd/system/v2raya.service 内容由ChatGPT结合官方文档生成：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [Unit] Description=v2rayA Service After=network.target [Service] Type=simple User=root Environment=\u0026#34;V2RAYA_CONFIG=/usr/local/etc/v2raya\u0026#34; Environment=\u0026#34;V2RAYA_LOG_FILE=/tmp/v2raya.log\u0026#34; Environment=\u0026#34;XRAY_LOCATION_ASSET=/usr/local/share/xray\u0026#34; ExecStart=/usr/local/bin/v2raya --passcheckroot Restart=on-failure LimitNOFILE=1000000 [Install] WantedBy=multi-user.target 激活v2rayA，根据官方文档，采用服务的形式运行\n1 2 sudo systemctl start v2raya sudo systemctl status v2raya 配置开机自启。理论上这步之后配置完毕，笔者又顺便解决了群晖用不了透明代理的问题\n1 2 sudo systemctl enable v2raya sudo systemctl is-enabled v2raya 激活透明代理\narch kernel iptables version system model platform version apollolake 4.4.180+ v1.8.3 DS918+ 7.0.1-42218 apollolake 4.4.59+ v1.6.0 DS918+ 6.2.3-25426 broadwell 3.10.105 v1.6.0 DS3617xs 6.2.3-25426 bromolow 3.10.105 v1.6.0 DS3615xs 6.2.3-25426 geminilake 4.4.180+ v1.8.3 DS920+ 7.1-42661 geminilake 4.4.302+ v1.8.3 DS220+ 7.2-64570 由于群晖系统的原因，先根据型号确定架构，然后从github仓库中下好的模块中选出适配机器的文件（比如笔者的DS224是geminilake）\n上传相应的ko模块至/lib/modules/，上传相应的so模块至/usr/lib/iptables/，即可。\n运行sudo -i之后再运行以下insmod命令尝试加载ko内核模块。由于模块互相有依赖性，需按一定顺序加载\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 insmod /lib/modules/nfnetlink.ko insmod /lib/modules/ip_set.ko insmod /lib/modules/ip_set_hash_ip.ko insmod /lib/modules/xt_set.ko insmod /lib/modules/ip_set_hash_net.ko insmod /lib/modules/xt_mark.ko insmod /lib/modules/xt_connmark.ko insmod /lib/modules/xt_comment.ko insmod /lib/modules/xt_TPROXY.ko insmod /lib/modules/xt_socket.ko insmod /lib/modules/iptable_mangle.ko insmod /lib/modules/textsearch.ko insmod /lib/modules/ts_bm.ko insmod /lib/modules/xt_string.ko insmod /lib/modules/nf_nat_ipv6.ko insmod /lib/modules/nf_nat_masquerade_ipv6.ko insmod /lib/modules/ip6t_MASQUERADE.ko insmod /lib/modules/ip6table_nat.ko insmod /lib/modules/ip6table_raw.ko insmod /lib/modules/ip6table_mangle.ko 但是系统重启后，模块需要重新加载，所以用以上内容生成脚本在/usr/local/bin/load_v2raya_mods.sh\n然后重启v2rayA服务:\n1 sudo systemctl restart v2raya Emby 在配置好v2rayA后，即使采用了改配置文件的办法，emby的流量仍不能成功地通过v2rayA来走，各种修改配置ip后无果，于是自然想到是docker变成本机服务导致的。\n在v2rayA上多次修改配置后，终于发现开启透明代理解决问题，同时也解决了连接不上Docker仓库的问题，但更大的问题是启用透明代理会使外网访问全部失败（亏好内网还能连接）。“透明代理”我想可以理解为“全局代理”。\nDocker(Container Manager)单独设置代理的问题详见前言链接。\n在“映像”中选中emby并运行，在“存储空间设置”中加入媒体库的路径，在“环境”中加入三个环境变量，“网络”中选择host模式：\n1 2 3 HTTP_PROXY = http://127.0.0.1:20171 HTTPS_PROXY = http://127.0.0.1:20171 NO_PROXY = localhost,127.0.0.1 完成基本设置，安装好之前的动漫相关的插件，刮削成功。\n","date":"2026-02-16T11:12:15+08:00","permalink":"/p/%E7%BE%A4%E6%99%96nas%E4%BB%A3%E7%90%86%E5%8F%8Aemby%E9%85%8D%E7%BD%AE/","title":"群晖NAS代理及Emby配置"},{"content":"由于近日笔者经常用Python下载各种模型，终要解决困扰已久的wsl2与Windows主机代理不互通的问题。笔者浪费了半个下午后终于搞定，在得力助手Gemini的帮助下，主要步骤如下：\nWindows的.wslconfig设置 v2rayN的核心设置 Windows的防火墙设置 wsl2的旧设置清理 wsl2的~/.bashrc设置 wsl2的curl -v测试 参考网站：\nAdvanced settings configuration in WSL | Microsoft Learn\nWSL2 使用 V2RayN 局域网 proxychains 代理方案 · Issue #2653 · 2dust/v2rayN\n记一次用wsl2中共享宿主机的代理-v2rayN - 沉迷于学习，无法自拔^_^\n.wslconfig 访问Windows下的个人账户文件夹，按下 Win + R，输入 %UserProfile% 并回车\n检查是否有 .wslconfig 文件。如果没有，新建一个文本文件并命名为 .wslconfig\n将以下内容粘贴到.wslconfig中：\n1 2 3 4 5 [wsl2] # 开启镜像网络模式 networkingMode=mirrored # 自动同步代理设置（可选true/false） autoProxy=true 需注意的是：autoProxy这个参数决定了wsl2代理的方式，设置为true可以不再配置~/.bashrc，但是问题在于：\n当我们使用env | grep -i proxy命令，会看到很多奇怪的和网络有关的变量，虽然确实能成功实现代理。 不能在wsl2中开关代理，导致很多流量都通过代理来走\n后面我们将其设置为false，以便于在wsl2内部可以方便地控制代理的开关，保证wsl2系统的清晰透明。\n在Windows终端中输入wsl --shutdown关闭wsl2\nv2rayN 在v2rayN（撰文时版本为V7.15.7）的基础设置中，启用“允许来自局域网的连接”以及“为局域网开启新的端口”（可选） 在v2rayN客户端的主界面左下角能看到为互联网开放的端口，笔者这里是10810 这里系统代理是“自动配置系统代理”，路由模式是“绕过(Whitelist)” 然后选好节点，保持v2rayN的进行 FireWall 在 Windows 搜索框输入“防火墙”，选择 Windows Defender 防火墙 点击 “允许应用或功能通过 Windows Defender 防火墙”。 找到或添加 v2rayN.exe 和其核心程序（如 v2ray.exe 或 xray.exe），确保 专用 和 公用 都勾选上。通常路径分别在v2rayN\\和v2rayN\\bin\\xray下 在防火墙高级设置中，新建一个入站规则，允许端口 10810 的 TCP 流量（与v2rayN相对应） wsl2 清理旧设置 1 2 3 4 5 6 7 8 # 1. 清除旧的、混乱的代理环境变量 unset http_proxy unset https_proxy unset no_proxy # 2. 重新设置正确的代理（指向镜像模式下的本地端口） export http_proxy=\u0026#34;http://127.0.0.1:10810\u0026#34; export https_proxy=\u0026#34;http://127.0.0.1:10810\u0026#34; 确保在第一步已开启镜像网络模式并重启好wsl2。当然这步也不是必须的，因为后面~/.bashrc会自动处理这些就变量。\n~/.bashrc 1 2 # 用其它的编辑器同理 sudo vim ~/.bashrc 在与Gemini多次实验后，得出以下内容用于添加到~/.bashrc底部：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function proxy_on() { # 彻底清理可能残留的变量（防止大小写混用冲突） unset http_proxy https_proxy ALL_PROXY NO_PROXY HTTP_PROXY HTTPS_PROXY all_proxy no_proxy # 设置你验证成功的端口（既然 10810 测通了，就用 10810） export hostip=\u0026#34;127.0.0.1\u0026#34; export port=\u0026#34;10810\u0026#34; export http_proxy=\u0026#34;http://$hostip:$port\u0026#34; export https_proxy=\u0026#34;http://$hostip:$port\u0026#34; export all_proxy=\u0026#34;socks5://$hostip:$port\u0026#34; # 这里的 no_proxy 只保留本地回环 export no_proxy=\u0026#34;localhost,127.0.0.1\u0026#34; echo \u0026#34;WSL Proxy: ON (127.0.0.1:10810)\u0026#34; } function proxy_off() { unset http_proxy https_proxy ALL_PROXY NO_PROXY HTTP_PROXY HTTPS_PROXY all_proxy no_proxy echo \u0026#34;WSL Proxy: OFF\u0026#34; } 保存以上内容后，输入：\n1 source ~/.bashrc 然后再：\n1 curl -I https://www.google.com 应该就会出现以下内容，表示连接成功：\n笔者同时也用ping去测试了一下，虽然不成功，但不影响使用。\n1 HTTP/1.1 200 Connection established 笔者再次尝试下载模型，果然成功：\n1 2 import gensim.downloader as api wv_from_bin = api.load(\u0026#34;glove-wiki-gigaword-200\u0026#34;) 再不成功的解决方案 最有效的方法的就是在完成了上面步骤后，wsl2中输入：\n1 curl -v https://www.google.com 然后把输出的结果交给AI，它会告诉你的。\n","date":"2026-01-30T16:31:20+08:00","permalink":"/p/%E5%A6%82%E4%BD%95%E5%9C%A8wsl2%E4%B8%8A%E4%BD%BF%E7%94%A8%E6%9C%AC%E6%9C%BA%E4%BB%A3%E7%90%86/","title":"如何在WSL2上使用本机代理"},{"content":"Intro 本篇是对Stanford CS 224N | Natural Language Processing with Deep Learning (Spring 2024) 这门课程的学习笔记。关于这门课的知识点，总结如下：\n词向量、RNN、LSTM、Seq2Seq 模型、机器翻译、注意力机制、Transformer 等等 What is this course about? Natural language processing (NLP) or computational linguistics is one of the most important technologies of the information age. Applications of NLP are everywhere because people communicate almost everything in language: web search, advertising, emails, customer service, language translation, virtual agents, medical reports, politics, etc. In the 2010s, deep learning (or neural network) approaches obtained very high performance across many different NLP tasks, using single end-to-end neural models that did not require traditional, task-specific feature engineering. In the 2020s amazing further progress was made through the scaling of Large Language Models, such as ChatGPT. In this course, students will gain a thorough introduction to both the basics of Deep Learning for NLP and the latest cutting-edge research on Large Language Models (LLMs). Through lectures, assignments and a final project, students will learn the necessary skills to design, implement, and understand their own neural network models, using the Pytorch framework.\nWord Vectors $ vector(”King”)- vector(”Man”) + vector(”Woman”) $\nresults in a vector that is closest to the vector representation of the word Queen\nHow do we have usable meaning in a computer? Slides里介绍了几种方式：\nWordNet one-hot vectors word vectors 那么前两种都有其现实意义，但也有明显的弊端。\nWordNet，完全靠synonum sets和hypernyms sets来确定词汇间的关系、构造复杂、无法及时吸收新词汇\u0026hellip;\u0026hellip;\none-hot vectors给每个词都设置了symbol，尽管是用数字表达，但是相近的词之间在数学上没有联系（向量的点积为0）\n那么下面就引出了一句名言：\u0026ldquo;You shall know a word by the company it keeps\u0026rdquo;\n以及word vectors的思想，将词汇归一化到一个向量中，虽然每个词汇有多个词义，但其分布却近似是其多个词义的平均，并用点积来确定词向量间的相关性。\nWord2vec original word2vec paper\nWord2vec很好地反映了word vectors的思想，计算中心词和相邻词的相似度，确定其概率分布：\nWe have a large corpus (“body”) of text: a long list of words Every word in a fixed vocabulary is represented by a vector Go through each position t in the text, which has a center word c and context (“outside”) words o Use the similarity of the word vectors for c and o to calculate the probability of o given c (or vice versa) Keep adjusting the word vectors to maximize this probability Objective Function 那么如何计算其似然程度(Likelihood)呢，For each position = $t=1,......,T$, predict context words within a window of fixed size $m$, given center word $w_t$. Data likelihood : $$ L(\\theta) = \\prod_{t=1}^{T} \\prod_{\\substack{-m \\le j \\le m \\\\ j \\neq 0}} P(w_{t+j} \\mid w_t; \\theta) $$ 为减小数据复杂程度，及方便计算机处理，优化上公式，对其进行平均负对数似然操作，The objective function $J(\\theta)$ is the (average) negative log likelihood: $$ J(\\theta) = -\\frac{1}{T} \\log L(\\theta) = -\\frac{1}{T} \\sum_{t=1}^{T} \\sum_{substack{-m \\le j \\le m \\\\ j \\neq 0}} \\log P(w_{t+j} \\mid w_t; \\theta) $$ 最小化 $J(\\theta)$ 等价于 最大化预测准确率。\nPrediction Function $$ P(o \\mid c) = \\frac{\\exp(u_o^T v_c)}{\\sum_{w \\in V} \\exp(u_w^T v_c)} $$ $u_o^T v_c$ : 点积越大，代表这两个词在向量空间中的位置越接近，即语义相关性越高。 Dot product compares similarity of o and c. $u^T v = u \\cdot v = \\sum_{i=1}^{n} u_i v_i$ Larger dot product = larger probability. $\\exp()$ : 将任何实数映射为正数。由于指数函数的增长特性，它会放大较大点积的影响，使相关性高的词获得更高的权重。 ${\\sum_{w \\in V} \\exp(u_w^T v_c)}$ : 分母是对词汇表$V$中所有可能的词进行求和。这一步确保了所有可能输出的概率之和等于 1，从而构成一个标准的概率分布。 这也是对Softmax Function的应用 $$ \\text{softmax}(x_i) = \\frac{\\exp(x_i)}{\\sum_{j=1}^{n} \\exp(x_j)} = p_i $$ Softmax 函数将任意值 $x_i$ 映射为一个概率分布 $p_i$。 “max”：因为它会放大最大值 $x_i$ 对应的概率，使大的更大。 “soft”：因为它仍然会给较小的 $x_i$ 分配一些概率。 To Train the Model To train a model, we gradually adjust parameters to minimize a loss. $$ \\theta = \\begin{bmatrix} v_{aardvark} \\\\ v_a \\\\ \\vdots \\\\ v_{zebra} \\\\ u_{aardvark} \\\\ u_a \\\\ \\vdots \\\\ u_{zebra} \\end{bmatrix} \\in \\mathbb{R}^{2dV} $$ 如果词向量维度为 $d$，词汇量为 $V$，则总参数量为 $2dV$。\n每个单词有两个向量，$\\theta$ 包含了词汇表中所有单词的两种向量表示（中心词向量 $v$ 和背景词向量 $u$）。\n计算所有参数的梯度，以优化模型\n梯度公式就是对Softmax Function的求偏导，数学过程如下：\n初始损失函数 (单词对的负对数似然) 这是针对一个中心词 $c$ 和一个背景词 $o$ 的基本损失定义： $$ \\text{Loss} = -\\log P(o \\mid c) $$ 代入Softmax定义展开 将 $P(o|c)$ 的公式代入，利用对数性质将除法化为减法： $$ \\text{Loss} = -\\log \\left( \\frac{\\exp(u_o^T v_c)}{\\sum_{w \\in V} \\exp(u_w^T v_c)} \\right) = -u_o^T v_c + \\log \\sum_{w \\in V} \\exp(u_w^T v_c) $$ 第一部分求导 对点积项关于中心词向量 $v_c$ 求偏导： $$ \\frac{\\partial}{\\partial v_c} (u_o^T v_c) = u_o $$ 第二部分求导 利用链式法则对 $\\log \\sum \\exp(\\dots)$ 形式进行求导： $$ \\frac{\\partial}{\\partial v_c} \\log \\sum_{w \\in V} \\exp(u_w^T v_c) = \\frac{1}{\\sum_{w \\in V} \\exp(u_w^T v_c)} \\cdot \\sum_{x \\in V} \\left[ \\exp(u_x^T v_c) \\cdot u_x \\right] $$ 转化为概率期望形式 将上一步的结果重新组合，提取出原始的概率项 $P(x|c)$： $$ \\sum_{x \\in V} \\left[ \\frac{\\exp(u_x^T v_c)}{\\sum_{w \\in V} \\exp(u_w^T v_c)} \\right] u_x = \\sum_{x \\in V} P(x \\mid c) u_x $$ 最终梯度公式 将两部分合并，得到最终用于更新 $v_c$ 的梯度： $$ \\frac{\\partial \\text{Loss}}{\\partial v_c} = -u_o + \\sum_{x \\in V} P(x \\mid c) u_x $$ Gradient Descent 梯度下降更新方程 (矩阵形式) $$ \\theta^{new} = \\theta^{old} - \\alpha \\nabla_{\\theta} J(\\theta) $$ 梯度下降更新方程 (单个参数形式) $$ \\theta_j^{new} = \\theta_j^{old} - \\alpha \\frac{\\partial}{\\partial \\theta_j^{old}} J(\\theta) $$ $\\alpha$ (alpha): 步长或学习率（step size / learning rate）。 但是实际应用并非上面的方法，而是用随机梯度下降，(Stochastic Gradient Descent, SGD)\n目标函数 $J(\\theta)$ 是语料库中所有窗口的函数。如果每次更新都要计算整个语料库的梯度 $\\nabla_{\\theta} J(\\theta)$，计算成本极其昂贵，在进行单次更新前需要等待极长时间。 SGD不再计算整个语料库，而是通过重复采样窗口，每处理一个（或一小批）窗口就更新一次参数。 Skip-gram Model with Negative Sampling negative sampling paper $$ P(o\\mid c)=\\frac{\\exp(u_x^T v_c)}{\\sum_{w \\in V} \\exp(u_w^T v_c)} $$ 采用最传统的方法计算概率时，即softmax，它的分母是全部词的叠加，计算量太大\n而Skip-gram Model with Negative Sampling不需计算所有可能的词，而是训练一些逻辑回归，更偏好实际的上下文而非随机的上下文，实际上会选K个负样本，计算量就变为了$O(K)$ $$ J_{neg-sample}(u_o, v_c, U) = -\\log \\sigma(u_o^T v_c) - \\sum_{k \\in \\{K \\text{ sampled indices}\\}} \\log \\sigma(-u_k^T v_c) $$ 其中，$\\sigma(x)=\\frac{1}{1+e^{-x}}$即sigmoid函数，使符合的结果概率接近于1，而不符合的则接近于0\n但这又会导致低频词，如\u0026quot;zebra\u0026quot;的概率过低，而像\u0026quot;the\u0026quot;这样的词则概率较高，所以给公式加上$3/4$次方： $$ P(W)=U(W)^{3/4}/Z $$ 来提高低频词的概率。\nGloVe original GloVe papar (Global Vectors for Word Representation)\nCo-occurrence Martix 构建共现矩阵的方法非常简单，首先设定窗口长度，然后对在窗口长度内出现的共现词频率进行计数，一个简单的例子如上图（窗口为1，仅算相邻词）。但是：\n词向量维度会随着语料库中词汇的增多而大幅增加，这会导致所需存储空间增大，且矩阵会变得相当稀疏，基于此构建的模型鲁棒性较差； 功能词出现频次极高，但没有提供相应的信息； 没有反映出词距与词相关性之间的联系。 那么如何进行降维来优化？经典的方法是进行SVD矩阵分解（虽然问了AI也没明白原理，而且Assignment1中用一个sklearn的函数就解决了）\n$$ X = U \\Sigma V^T $$ $X$ (共现矩阵)：大小为 $|V| \\times |V|$（词表大小）。每个元素 $X_{ij}$ 代表词 $i$ 和词 $j$ 在语料库中共同出现的次数。 $U$ 和 $V$ (正交矩阵)：它们的列向量是相互正交的单位向量（Orthonormal）。在 NLP 中，$U$ 的每一行通常被视为该词的原始“嵌入”。 $\\Sigma$ (对角矩阵)：对角线上的值 $\\sigma_1, \\sigma_2, \\dots$ 称为奇异值。它们按从大到小排列，代表了数据在对应维度上的重要程度（方差/信息量）。 那么降维便是将从共现矩阵得到的的，长度为$V$的词向量降维成长度为$K$的向量\n真正编码语义的不是共现概率本身，而是共现概率的比值：\n共现概率的比率可以编码语义成分，我们希望将它们作为线性语义成分捕捉在词向量空间中！\n$x = \\text{solid}$ $x = \\text{gas}$ $x = \\text{water}$ $x = \\text{fashion}$ $P(x\\mid\\text{ice})$ $1.9 \\times 10^{-4}$ $6.6 \\times 10^{-5}$ $3.0 \\times 10^{-3}$ $1.7 \\times 10^{-5}$ $P(x\\mid\\text{steam})$ $2.2 \\times 10^{-5}$ $7.8 \\times 10^{-4}$ $2.2 \\times 10^{-3}$ $1.8 \\times 10^{-5}$ $\\dfrac{P(x\\mid\\text{ice})}{P(x\\mid\\text{steam})}$ $8.9$ $8.5 \\times 10^{-2}$ $1.36$ $0.96$ Analogies 词向量虽然数学原理上很强大，但是其实际的类比(Analogy)场景却是有不少问题：\n下例可见，是一个$woman+grandfather-man=?$的问题，那么显而易见且最有可能的结果就是grandmother，那么为何程序给出的另外几个，如granddaughter, mother之类，score也几乎一样很高呢？\n1 2 # Run this cell to answer the analogy -- man : grandfather :: woman : x pprint.pprint(wv_from_bin.most_similar(positive=[\u0026#39;woman\u0026#39;, \u0026#39;grandfather\u0026#39;], negative=[\u0026#39;man\u0026#39;])) 1 2 3 4 5 6 7 8 9 10 [(\u0026#39;grandmother\u0026#39;, 0.7608445286750793), (\u0026#39;granddaughter\u0026#39;, 0.7200808525085449), (\u0026#39;daughter\u0026#39;, 0.7168302536010742), (\u0026#39;mother\u0026#39;, 0.7151536345481873), (\u0026#39;niece\u0026#39;, 0.7005682587623596), (\u0026#39;father\u0026#39;, 0.6659888029098511), (\u0026#39;aunt\u0026#39;, 0.6623409390449524), (\u0026#39;grandson\u0026#39;, 0.6618767976760864), (\u0026#39;grandparents\u0026#39;, 0.644661009311676), (\u0026#39;wife\u0026#39;, 0.6445354223251343)] 虽说Assignment里没有标准答案，但是通过AI可以得知：这涉及到“语义聚类”\n语义的“近邻效应” 在向量空间中，逻辑相似的词通常会聚在一起形成一个“簇”。当你计算出 $\\vec{w} + \\vec{g} - \\vec{m}$ 时，你实际上是在空间中定位了一个坐标点。 granddaughter 等同样具有“女性”和“亲属”属性，且在语料库中经常与 grandfather 或 grandmother 出现在相似的上下文。在向量维度上，它们在“亲属关系”这个维度上非常接近。 维度的“重叠性” 词向量通常有几百个维度。虽然我们减去了 man 并加上了 grandfather，但这并不能完全抹除其他维度的相似性。 daughter、mother、grandmother 共享了绝大部分维度：[+女性]、[+人类]、[+亲属]。 下面的例子，属于\u0026quot;Incorrect Analogy\u0026quot;，我们来看易得答案为socks，但为何模型忽略了glove和hand，而输出了各种square的名词？显然和foot“脚”的释义无关，而是“英尺”的释义。\n1 pprint.pprint(wv_from_bin.most_similar(positive=[\u0026#39;foot\u0026#39;, \u0026#39;glove\u0026#39;], negative=[\u0026#39;hand\u0026#39;])) 1 2 3 4 5 6 7 8 9 10 [(\u0026#39;45,000-square\u0026#39;, 0.4922032654285431), (\u0026#39;15,000-square\u0026#39;, 0.4649604558944702), (\u0026#39;10,000-square\u0026#39;, 0.4544755816459656), (\u0026#39;6,000-square\u0026#39;, 0.44975775480270386), (\u0026#39;3,500-square\u0026#39;, 0.444133460521698), (\u0026#39;700-square\u0026#39;, 0.44257497787475586), (\u0026#39;50,000-square\u0026#39;, 0.4356396794319153), (\u0026#39;3,000-square\u0026#39;, 0.43486514687538147), (\u0026#39;30,000-square\u0026#39;, 0.4330596923828125), (\u0026#39;footed\u0026#39;, 0.43236875534057617)] 语义多义性的干扰 上面也提到，foot也是计量单位”英尺“的英文，和square组合是合理的 训练语料的偏见 既然foot有多个释义，但输出却全是其“英尺”的意思，那么这可能体现了用来训练的语料中多是...square和foot相关联 词的选择 即使输出全是...square，其score值也不过0.5，这不仅说明模型找不到强相关的词，而且说明了模型很可能没能理解glove与hand, foot的联系 Neural Network A neural network = running several logistic regressions at the same time.\nCS231n Deep Learning on Network Architectures\nCS231n Deep Learning for Computer Vision on Backprop\nStructure Non-linearities 为什么神经网络需要非线性(Non-linearities)？\n核心观点：神经网络执行函数逼近，例如回归或分类。 没有非线性：深度神经网络只能执行线性变换。 多层失效：额外的层会被压缩成单个线性变换：$W_1 W_2 x = Wx$（即多层线性层等同于一层）。 有了非线性：通过包含非线性的多层结构，网络可以逼近更复杂的函数！ 左下侧图对：左图显示线性分类（只能画直线），无法区分复杂的红绿点分布；右图显示非线性分类（可以画曲线），完美分割了数据。 右侧三张波形图：展示了随着函数复杂度的增加，只有非线性模型才能拟合这些起伏的蓝点（观测数据）。 关于常用的非线性函数，在《智能计算系统》课上均有学习，不多赘述\nGradients derivatives.pdf\n梯度，简单地讲就是对变量的微积分，比如我们有： $$ f(x)=x^3 $$ 那么它的梯度就是： $$ \\frac{df}{dx}=3x^2 $$ 当然，这只是个非常简单的例子，实际上是大规模的链式法则计算，对矩阵(Jacabian Matrix)进行梯度计算。\nChain Rule 在单变量微积分中，如果 $y = f(u)$ 且 $u = g(x)$，那么： $$ \\frac{dy}{dx} = \\frac{dy}{du} \\cdot \\frac{du}{dx} $$ 但在神经网络中，每一层都是一个向量 $\\mathbf{h,z} \\in \\mathbb{R}^n$。当我们将这个逻辑扩展到向量时，乘法就变成了矩阵乘法。\n对于多个变量，应与Jacobians矩阵相乘：\n假设有 $\\mathbf h= f(z)$ 和 $\\mathbf z=Wx+b$ ，下面的这些偏导数就是雅可比矩阵 $$ \\frac{\\partial \\mathbf{h}}{\\partial \\mathbf{x}} = \\frac{\\partial \\mathbf{h}}{\\partial \\mathbf{z}} \\frac{\\partial \\mathbf{z}}{\\partial \\mathbf{x}} $$Matrix Calculus 由下式得，矩阵只有对角元素，其余部分均为0： $$ \\begin{aligned} \\left( \\frac{\\partial \\mathbf{h}}{\\partial \\mathbf{z}} \\right)_{ij} \u0026= \\frac{\\partial h_i}{\\partial z_j} = \\frac{\\partial}{\\partial z_j} f(z_i) \\quad \u0026\u0026 \\text{definition of Jacobian} \\\\ \u0026= \\begin{cases} f'(z_i) \u0026 \\text{if } i = j \\\\ 0 \u0026 \\text{if otherwise} \\end{cases} \\quad \u0026\u0026 \\text{regular 1-variable derivative} \\end{aligned} $$$$ \\frac{\\partial \\mathbf h}{\\partial \\mathbf z} = \\begin{pmatrix} f'(z_1) \u0026 0 \u0026 \\cdots \u0026 0 \\\\ 0 \u0026 f'(z_2) \u0026 \\cdots \u0026 0 \\\\ \\vdots \u0026 \\vdots \u0026 \\ddots \u0026 \\vdots \\\\ 0 \u0026 0 \u0026 \\cdots \u0026 f'(z_n) \\end{pmatrix} = \\operatorname{diag}(f'(\\mathbf z)) $$此外，还有一常有的Jacobian： $$ \\frac{\\partial}{\\partial \\mathbf{u}}(\\mathbf{u}^T \\mathbf{h})=\\mathbf h^T $$ 假设 $\\mathbf{u}$ 和 $\\mathbf{h}$ 都是 $n$ 维列向量： $$ \\mathbf{u} = \\begin{bmatrix} u_1 \\\\ u_2 \\\\ \\vdots \\\\ u_n \\end{bmatrix}, \\quad \\mathbf{h} = \\begin{bmatrix} h_1 \\\\ h_2 \\\\ \\vdots \\\\ h_n \\end{bmatrix} $$ 那么它们的内积（也就是括号里的部分）是一个标量： $$ f = \\mathbf{u}^T \\mathbf{h} = u_1 h_1 + u_2 h_2 + \\dots + u_n h_n = \\sum_{i=1}^n u_i h_i $$ 我们要对向量 $\\mathbf{u}$ 求导。根据 Jacobian Matrix 的定义，我们需要对 $\\mathbf{u}$ 中的每一个元素 $u_k$ 分别求导： $$ \\frac{\\partial f}{\\partial u_k} = \\frac{\\partial}{\\partial u_k} (u_1 h_1 + \\dots + u_k h_k + \\dots + u_n h_n) $$ 由于除了 $u_k h_k$ 这一项外，其他项都不包含 $u_k$，所以它们对 $u_k$ 的导数都是 $0$： $$ \\frac{\\partial f}{\\partial u_k} = h_k $$ 按照 Jacobian 矩阵的惯例，一个标量对一个列向量求导，结果是一个行向量： $$ \\frac{\\partial f}{\\partial \\mathbf{u}} = \\begin{bmatrix} \\frac{\\partial f}{\\partial u_1} \u0026 \\frac{\\partial f}{\\partial u_2} \u0026 \\dots \u0026 \\frac{\\partial f}{\\partial u_n} \\end{bmatrix} = \\begin{bmatrix} h_1 \u0026 h_2 \u0026 \\dots \u0026 h_n \\end{bmatrix} = \\mathbf{h}^T $$Write out the Jacobians $$ \\begin{aligned} \\frac{\\partial s}{\\partial \\mathbf{b}} \u0026= \\frac{\\partial s}{\\partial \\mathbf{h}} \\frac{\\partial \\mathbf{h}}{\\partial \\mathbf{z}} \\frac{\\partial \\mathbf{z}}{\\partial \\mathbf{b}} \\\\ \u0026= \\mathbf{u}^T \\text{diag}(f'(\\mathbf{z})) \\mathbf{I} \\\\ \u0026= \\mathbf{u}^T \\odot f'(\\mathbf{z}) \\end{aligned} $$$\\odot$ = Hadamard product = element-wise multiplication of 2 vectors to give vector\n变量 神经网络中的含义 说明 $s$ 损失函数值 (Loss/Score) 最终的标量输出（例如交叉熵损失）。我们要看它随参数如何变化。 $\\mathbf{b}$ 偏置向量 (Bias) 当前层的偏置项，神经网络需要学习的参数之一。 $\\mathbf{z}$ 净输入 (Logits/Pre-activation) 线性组合后的结果，即 $\\mathbf{z} = \\mathbf{W}\\mathbf{x} + \\mathbf{b}$。 $\\mathbf{h}$ 激活值 (Activation/Hidden state) $\\mathbf{z}$ 经过非线性激活函数后的输出，即 $\\mathbf{h} = f(\\mathbf{z})$。 $\\mathbf{u}^T$ 上游传回的梯度 ($\\frac{\\partial s}{\\partial \\mathbf{h}}$) 损失函数对当前层输出的导数，它是从更高层“反向传播”回来的信号。 $f'(\\mathbf{z})$ 激活函数的导数 比如 ReLU 或 Sigmoid 的导数。它决定了哪些神经元处于活跃状态。 $\\mathbf{I}$ 单位矩阵 (Identity Matrix) 因为 $\\mathbf{z} = \\dots + \\mathbf{b}$，$\\mathbf{z}$ 对 $\\mathbf{b}$ 求导的结果是 1（矩阵形式即为单位阵）。 Re-using Computation 误差信号（Upstream Gradient）$\\boldsymbol{\\delta}$： $$ \\boldsymbol{\\delta} = \\frac{\\partial s}{\\partial \\mathbf{h}} \\frac{\\partial \\mathbf{h}}{\\partial \\mathbf{z}} = \\mathbf{u}^T \\circ f'(\\mathbf{z}) $$ 我们先算出这个值$\\boldsymbol{\\delta}$ ，可以简化计算：\n对权重矩阵 $W$ 的梯度展开： $$ \\frac{\\partial s}{\\partial \\mathbf{W}} = \\boldsymbol{\\delta} \\frac{\\partial \\mathbf{z}}{\\partial \\mathbf{W}} $$ 对偏置向量 $\\mathbf{b}$ 的梯度简化： $$ \\frac{\\partial s}{\\partial \\mathbf{b}} = \\boldsymbol{\\delta} \\frac{\\partial \\mathbf{z}}{\\partial \\mathbf{b}} = \\boldsymbol{\\delta} $$ Shape Convention 假设权重 $\\mathbf{W} \\in \\mathbb{R}^{n \\times m}$，输出是一个标量 $s$（损失）。按纯数学定义，$\\frac{\\partial s}{\\partial \\mathbf{W}}$ 应该是一个 $1 \\times nm$ 的行向量（Jacobian）。但如果使用这种形式，梯度更新公式 $\\theta^{new} = \\theta^{old} - \\alpha \\nabla_{\\theta} J(\\theta)$ 就会因维度不匹配而无法直接相减。\n为了计算方便，我们约定梯度的形状必须等于参数的形状。因此 $\\frac{\\partial s}{\\partial \\mathbf{W}}$ 也是一个 $n \\times m$ 的矩阵： $$ \\frac{\\partial s}{\\partial \\mathbf{W}} = \\begin{bmatrix} \\frac{\\partial s}{\\partial W_{11}} \u0026 \\dots \u0026 \\frac{\\partial s}{\\partial W_{1m}} \\\\ \\vdots \u0026 \\ddots \u0026 \\vdots \\\\ \\frac{\\partial s}{\\partial W_{n1}} \u0026 \\dots \u0026 \\frac{\\partial s}{\\partial W_{nm}} \\end{bmatrix} $$$$ \\frac{\\partial s}{\\partial \\mathbf{W}} = \\boldsymbol{\\delta}^T \\mathbf{x}^T $$那么我们究竟应该以什么样的“形状”来呈现导数结果？\n采取始终遵循**形状约定 (Shape Convention)**的方式：\n做法：不拘泥于严格的 Jacobian 定义，而是时刻盯着变量的维度（Dimensions）。 核心技巧：通过观察维度来决定何时需要对某个项进行转置，或者调整矩阵相乘的顺序，以确保每一层算出来的梯度形状和该层的参数形状完全一致。 关于 $\\boldsymbol{\\delta}$ 的重要结论：传导至隐层（Hidden layer）的误差信号 $\\boldsymbol{\\delta}$，其维度应该与该隐层的神经元数量（即激活值向量的维度）完全相同。 Backpropagation 按流程逐步计算各函数，从输入得到输出，即是正向传播(Forward Propagation)\n对于反向传播中的单个节点，有$downstream\\ gradient=upstream\\ gradient\\times local\\ gradient$\n对于单节点的多输入，upstream gradient不变，各输入的local gradient不同，但计算公式是不变的\n下面举个多输入的实际例子\n我们可以在这个基础上，假设输入y的值变为了2.1，那么$a=x+y=3.1$，$b=max(y+z)=y=2.1$，$a\\times b=6.51$\n所以，y值变化的0.1导致了结果0.51的变化，那么梯度就是$\\frac{\\Delta f}{\\Delta y}=5.1$\nImplementations 那么理论上，在已知正向传播的符号和计算的情况下，计算机可以自动得出反向传播的结果。但是在现代框架中，用户需手动设计局部导数的结算，这也比全自动方式提升了系统的运行效率和稳定性。\n1 2 3 4 5 6 7 8 9 10 11 class MultiplyGate(object): def forward(self, x, y): z = x * y self.x = x # must keep these around! self.y = y return z def backward(self, dz): dx = self.y * dz # [dz/dx * dL/dz] dy = self.x * dz # [dz/dy * dL/dz] return [dx, dy] Numeric Gradient Checking 在手动推导和实现反向传播（Backprop）时，这是确保你数学公式没推错、代码没写错的标准验证方法： $$ f'(x) \\approx \\frac{f(x + h) - f(x - h)}{2h} $$ 只需要前向传播函数 $f(x)$ 即可计算，不需要任何复杂的数学推导，不容易写错。 必须对模型的每一个参数分别进行两次前向传播（加 $h$ 和减 $h$），效率很低。 适合局部测试，不要对整个大型网络做验证，只针对某个特定层或小规模参数（如一个 $3 \\times 3$ 的矩阵）进行。 Dependency Parsing Syntactic Structure Phrase structure organizes words into nested constituents. 我们可以自己定义phrase构成的语法，比如名词短语可以是“限定词 + 形容词 + 名词”，“限定词 + 名词 + 介词短语”……介词短语可以是“介词 + 名词……”\nDependency structure shows which words depend on (modify, attach to, or are arguments of) which other words 语言中很容易出现歧义(ambiguity)，在英语里有了介词短语，歧义就更多了，举个例子：\n\u0026ldquo;Scientists count whales from space\u0026quot;可以理解为\u0026quot;Scientists [count] [whales from space]\u0026rdquo; 或者 \u0026ldquo;Scientists [count whales] [from space]\u0026rdquo; ……\nDependency Grammar and Treebanks Dependency syntax postulates that syntactic structure consists of relations between lexical items, normally binary asymmetric relations (“arrows”) called dependencies\n下图是个比较古老的\u0026quot;dependency structure\u0026quot;\nAn arrow connects a head (governor, superior, regent) with a dependent (modifier, inferior, subordinate)\nUsually, dependencies form a tree (a connected, acyclic, single-root graph)\nAnnotated Data 起初，构建语言库（Treebank）似乎比手动编写语法规则要慢得多，且看起来没那么有用。手动标注数据确实是件麻烦的工作，但现在看来，却有很大的优势：\n复用性(Reusability)：一套标注好的数据可以用来训练多种解析器（Parsers）、词性标注器（POS Taggers） 覆盖面广(Board coverage)：手动编写规则往往只能覆盖几个直觉上的例子，而标注真实语料可以涵盖语言在现实中的各种复杂用法。 频率与分布信息(Frequencies and distributional information)：它能告诉机器哪些结构更常见，帮助概率模型做出更准确的判断。 评估NLP(A way to evaluate NLP systems)：没有这套“标准”，我们就无法衡量一个 AI 模型的准确率（如 LAS/UAS 得分） 那么关于示例图中的各依存关系的意思，如下：\n缩写 意思 简单理解 nsubj 名词主语 动作的发出者（如 I think） nsubjpass 被动主语 被动语态里的主语（如 city called） ccomp 从句 动词后面的整个小句子（如 think \u0026hellip;） advmod 状语修饰 修饰动词的程度、疑问等（如 Why） amod 形容词修饰 修饰名词的形容词（如 famous goat） compound 复合修饰 名词修饰名词（如 goat trainer） det 限定关系 指向 a, the, any 等词 case 介词关系 指向 in, at 等介词 conj 并列关系 用 or, and 连接的词（如 trainer or something） Dependency Conditioning Preferences 在进行句法分析时，计算机会根据“依存条件偏好（Dependency Conditioning Preferences）”来判断两个词之间是否存在依存关系：\n双词亲和力 (Bilexical affinities)：依存关系（例如 [discussion → issues]）是合理的。 依存距离 (Dependency distance)：大多数（但并非所有）依存关系发生在邻近的单词之间。 介入材料 (Intervening material)：依存关系很少跨越中间的动词或标点符号。 中心词的价态 (Valency of heads)：对于一个中心词（Head）来说，通常在它的哪一侧会有多少个依存词？ Projectivity 如果一个句子的单词按线性顺序排列，且所有的依存弧（dependency arcs）都画在单词上方时，没有任何两条弧线发生交叉，那么这个解析就是“投影的”。那么互出现了交叉，就是非投影的(non-projectivity)，说明发生了“长距离位移”或结构重叠。\n但是现实中非投影的例子很常见，比如\u0026quot;Who did Bill buy the coffee from yesterday\u0026quot;\nTransition-Based Dependency Parser 首先，这Transition-Based Dependency Parser分有一个Stack和一个Buffer，和三种操作：\nStart: $\\sigma = [ROOT], \\beta = w_1, ..., w_n, A = \\emptyset$\nShift: $\\sigma, w_i | \\beta, A \\Rightarrow \\sigma | w_i, \\beta, A $\nLeft-$Arc_r$: $\\sigma | w_i | w_j, \\beta, A \\Rightarrow \\sigma | w_j, \\beta, A \\cup \\{r(w_j, w_i)\\} $\nRight-$Arc_r$: $\\sigma | w_i | w_j, \\beta, A \\Rightarrow \\sigma | w_j, \\beta, A \\cup \\{r(w_i, w_j)\\}$\nFinish: $\\sigma = [w], \\beta = \\emptyset$\nσ（sigma）表示 堆栈（stack），存储当前正在处理或等待建立关系的词。\nβ（beta）表示 缓冲区（buffer），存储尚未处理的输入词序列。\nA 表示 依存弧集合（set of dependency arcs），存储已建立的依存关系。\nLeft-$Arc_r$ 和 Right-$Arc_r$ 是两种规约模型，用来表明一个词是另一个的依存词，以左或右方向。\n那么下面先来举个例子：Analysis of \u0026ldquo;I ate fish\u0026rdquo;\nLeft Arc操作 生成了一个由栈顶指向第二个元素的弧，建立了\u0026quot;ate\u0026ldquo;是中心词，\u0026ldquo;I\u0026quot;依存于\u0026rdquo;ate\u0026ldquo;的关系。然后将\u0026rdquo;I\u0026ldquo;移出栈。 Shift操作 将\u0026rdquo;fish\u0026ldquo;从缓冲区移入栈中 Right Arc操作 生成了一个由第二个元素指向栈顶元素的弧，建立了\u0026rdquo;ate\u0026ldquo;是中心词,\u0026quot;fish\u0026ldquo;依存于\u0026rdquo;ate\u0026ldquo;的关系。然后将\u0026rdquo;fish\u0026ldquo;移出栈。 最后的Right Arc操作 使\u0026rdquo;[root]\u0026ldquo;指向\u0026rdquo;ate\u0026quot;，并在\u0026rdquo;ate\u0026ldquo;出栈后，只剩下根节点，操作完成。 Evaluation of Dependency Parsing 评价依存分析的指标分为UAS（无标签附件分数）和 LAS（有标签附件分数），下面是一个具体的例子: \u0026ldquo;[ROOT] She saw the video lecture.\u0026quot;，表中\u0026quot;Gold\u0026quot;是标准答案，\u0026ldquo;Parsed\u0026quot;是分析出的答案：\n可见，UAS计算的是Head寻找得是否正确，本例中第三个\u0026quot;the\u0026quot;的依存词和标准不符。 LAS计算的它们之间的关系类型是否标注正确，即Head和关系标签(label)都要一致，本例中只有\u0026quot;She\u0026quot;和\u0026quot;saw\u0026quot;是与标准相符的。 Neural dependency parsing More than 95% of parsing time is consumed by feature computation\n所以，我们可以用神经网络来加速特征提取，当然其方法还是基于上面的基于转移（Transition-based）的神经依存句法分析器。具体使用了了向量化、非线性等深度学习知识搭建出了第一个基于神经网络的依存分析器(2014年)。\nRecurrent Neural Networks Language Modeling 语言模型(Language Modeling) 简单地讲就是输入文本（词），输出概率。 $$ \\begin{aligned} P(\\boldsymbol{x}^{(1)}, \\dots, \\boldsymbol{x}^{(T)}) \u0026= P(\\boldsymbol{x}^{(1)}) \\times P(\\boldsymbol{x}^{(2)} | \\boldsymbol{x}^{(1)}) \\times \\dots \\times P(\\boldsymbol{x}^{(T)} | \\boldsymbol{x}^{(T-1)}, \\dots, \\boldsymbol{x}^{(1)}) \\\\ \u0026= \\prod_{t=1}^{T} \\underbrace{P(\\boldsymbol{x}^{(t)} | \\boldsymbol{x}^{(t-1)}, \\dots, \\boldsymbol{x}^{(1)})}_{\\text{This is what our LM provides}} \\end{aligned} $$ $P(\\boldsymbol{x}^{(1)}, \\dots, \\boldsymbol{x}^{(T)})$ 表示一整个序列（如一句话）出现的概率，通过将联合概率分解为一系列条件概率的乘积（链式法则），我们可以计算序列的概率。语言模型的核心任务就是根据之前的上下文 $\\boldsymbol{x}^{(t-1)}, \\dots, \\boldsymbol{x}^{(1)}$ 来预测下一个 token $\\boldsymbol{x}^{(t)}$ 出现的概率。\nn-gram Language Models An n-gram is a chunk of n consecutive words. n表示由几个词构成一个单位，即n-gram。为实现n-gram Language Models，步骤如下：\n首先，做一个马尔可夫假设：第 $t+1$ 个词 $x^{(t+1)}$ 仅取决于其前面的 $n-1$ 个词。 $$ P(x^{(t+1)} | x^{(t)}, \\dots, x^{(1)}) = P(x^{(t+1)} | \\underbrace{x^{(t)}, \\dots, x^{(t-n+2)}}_{n-1 \\text{ words}}) \\quad \\text{(assumption)} $$ 利用条件概率的定义，上述公式可以写为 $n$-gram 与 $(n-1)$-gram 概率的比值： $$ = \\frac{P(x^{(t+1)}, x^{(t)}, \\dots, x^{(t-n+2)}) \\leftarrow \\text{prob of a n-gram}}{P(x^{(t)}, \\dots, x^{(t-n+2)}) \\leftarrow \\text{prob of a (n-1)-gram}} \\quad \\text{(definition of conditional prob)} $$ 通过在大型文本语料库中计数 (Counting) 它们来统计词组出现的频率，来近似概率： $$ \\approx \\frac{\\text{count}(x^{(t+1)}, x^{(t)}, \\dots, x^{(t-n+2)})}{\\text{count}(x^{(t)}, \\dots, x^{(t-n+2)})} \\quad \\text{(statistical approximation)} $$ 举个例子，假设我们有一个4-gram Language Model，要预测最后一个空可能出现的单词：\n\u0026ldquo;as the proctor started the clock, the students opened their \u0026hellip;\u0026hellip;\u0026rdquo;\n我们只取最后的三个词组成的短语\u0026rdquo;students opened their\u0026rdquo; $$ P(w\\mid students\\ opened\\ their)=\\frac{count(students\\ opened\\ their\\ w)}{count(students\\ opened\\ their)} $$ 根据语料库，\u0026quot;students opened their books\u0026ldquo;可能是出现频率最高的，而更符合语境的\u0026rdquo;\u0026hellip;\u0026hellip;exams\u0026ldquo;出现频率更低\nProblems with n-gram Language Models 当使用计数法计算概率，会面临稀疏性问题：\n如果词组 \u0026ldquo;students opened their $w$\u0026rdquo; 在训练数据中从未出现过，那么对于任何该词 $w$，其概率都将变为 0。\n通过为词表中的每个词 $w \\in V$ 的计数增加一个极小的数值 $\\delta$ (平滑化 Smoothing)解决\n如果前缀 \u0026ldquo;students opened their\u0026rdquo; 在训练数据中从未出现过，我们将无法计算任何 $w$ 的概率，因为分母为 0。\n不再考虑完整的前缀，而是退而求其次，仅根据更短的上下文（例如 \u0026ldquo;opened their\u0026rdquo;）进行条件概率计算。(回退 Backoff)\n同时也会面临存储的问题：\n需要存放语料库中所有n-gram的计数 若是n需要增加，语料库的size也要大幅增加 A fixed-window neural Language Model 输入层 (Words / One-hot vectors): 输入为单词的 one-hot 向量 $\\boldsymbol{x}^{(1)}, \\boldsymbol{x}^{(2)}, \\boldsymbol{x}^{(3)}, \\boldsymbol{x}^{(4)}$。\n嵌入层 (Concatenated word embeddings): 将单词转换为稠密的向量表示（embeddings），并进行拼接： $$ \\boldsymbol{e} = [\\boldsymbol{e}^{(1)}; \\boldsymbol{e}^{(2)}; \\boldsymbol{e}^{(3)}; \\boldsymbol{e}^{(4)}] $$ 隐藏层 (Hidden layer): 通过权重矩阵 $W$ 和偏置 $b_1$ 进行线性变换，并经过激活函数 $f$（通常为 tanh 或 ReLU）： $$ \\boldsymbol{h} = f(W\\boldsymbol{e} + \\boldsymbol{b}_1) $$ 输出层 (Output distribution): 经过权重矩阵 $U$ 和偏置 $b_2$，最后通过 softmax 函数生成词典 $V$ 上的概率分布 $\\hat{\\boldsymbol{y}}$： $$ \\hat{\\boldsymbol{y}} = \\text{softmax}(U\\boldsymbol{h} + \\boldsymbol{b}_2) \\in \\mathbb{R}^{|V|} $$ 那么相对于n-gram方法，改进了：\n解决稀疏性问题：不再依赖精确的计数，通过向量空间的相似性来泛化未见过的词组。 存储优化：不需要存储所有观察到的 $n$-gram 频率，只需存储模型参数。 但是仍有问题没能解决：\n固定窗口限制： 固定的上下文窗口通常太小。 扩大窗口会线性导致权重矩阵 $W$ 的参数量激增。 无论窗口多大，它永远无法捕捉超出该范围的长程依赖。 缺乏对称性：输入 $\\boldsymbol{x}^{(1)}$ 和 $\\boldsymbol{x}^{(2)}$ 与矩阵 $W$ 中完全不同的权重相乘，模型处理每个位置输入的方式没有一致性。 RNN Language Model The Unreasonable Effectiveness of Recurrent Neural Networks\nRNN 的优点：\n可以处理任意长度的输入。 理论上，第 $t$ 步的计算可以使用很多步之前的信息。 模型大小固定：增加输入长度不会增加模型参数量。 对称性：每一步应用相同的权重，处理输入的方式具有一致性。 RNN 的缺点：\n计算速度慢：由于是递归计算，无法并行处理。 实践难题：在实际应用中，很难获取到很多步之前的信息（梯度消失/爆炸问题）。 Train an RNN Language Modle 获取一个大型文本语料库，它是由单词序列组成的：$\\boldsymbol{x}^{(1)}, \\dots, \\boldsymbol{x}^{(T)}$\n将序列输入 RNN-LM，并为每一个时间步 $t$ 计算输出分布 $\\hat{\\boldsymbol{y}}^{(t)}$。这意味着模型在给定目前为止已见单词的情况下，预测每一个位置上可能出现的单词概率分布。\n模型在每一个步长 $t$ 都会产生一个损失，第 $t$ 步的损失函数是预测概率分布 $\\hat{\\boldsymbol{y}}^{(t)}$ 与真实的下一个单词 $\\boldsymbol{y}^{(t)}$（即 $\\boldsymbol{x}^{(t+1)}$ 的 one-hot 向量）之间的交叉熵 $$ J^{(t)}(\\theta) = CE(\\boldsymbol{y}^{(t)}, \\hat{\\boldsymbol{y}}^{(t)}) = - \\sum_{w \\in V} \\boldsymbol{y}^{(t)}_w \\log \\hat{\\boldsymbol{y}}^{(t)}_w = - \\log \\hat{\\boldsymbol{y}}^{(t)}_{\\boldsymbol{x}_{t+1}} $$ 为了获得整个训练集的损失，需要将所有步骤的损失取平均值： $$ J(\\theta) = \\frac{1}{T} \\sum_{t=1}^{T} J^{(t)}(\\theta) = \\frac{1}{T} \\sum_{t=1}^{T} - \\log \\hat{\\boldsymbol{y}}^{(t)}_{\\boldsymbol{x}_{t+1}} $$ 计算损失应用了Teacher Forcing的概念，即并不使用模型在上一步实际预测出的单词作为下一步的输入，而是直接将语料库中的真实正确答案喂给模型。\n一次性计算整个语料库 $\\boldsymbol{x}^{(1)}, \\dots, \\boldsymbol{x}^{(T)}$ 的损失（Loss）和梯度（Gradients）在内存方面是极其昂贵的，所以在实际操作中，我们将序列 $\\boldsymbol{x}^{(1)}, \\dots, \\boldsymbol{x}^{(T)}$ 看作是一个个句子或文档，使用随机梯度下降来对一**小块数据 **计算损失和梯度，并立即进行参数更新\nBackpropagation for RNN RNN参数的训练，采用随时间的反向传播，沿着时间步 $i = t, \\dots, 0$ 反向传播，并在过程中累加梯度。\n由于 $\\boldsymbol{W}_h$ 在每一个时间步都是共享的（相同的权重），因此总梯度是每一个时间步产生的梯度之和。 $$ \\frac{\\partial J^{(t)}}{\\partial \\boldsymbol{W}_h} = \\sum_{i=1}^{t} \\left. \\frac{\\partial J^{(t)}}{\\partial \\boldsymbol{W}_h} \\right|_{(i)} \\frac{\\partial \\boldsymbol{W}_h|_{(i)}}{\\partial \\boldsymbol{W}_h} = \\sum_{i=1}^{t} \\left. \\frac{\\partial J^{(t)}}{\\partial \\boldsymbol{W}_h} \\right|_{(i)} $$ 随着序列增长，完整的反向传播计算量极大，且容易出现梯度消失或爆炸问题。在实际应用中，为了提高训练效率，通常会在约 20 个时间步后进行“截断”。\nExploding Gradient 梯度爆炸：\n如果 $W_h$ 的特征值（大致可以理解为权重的大小）大于 1。 随着时间步 $T$ 的增加，梯度会呈指数级增长。 模型权重会更新得过大，导致网络变得极不稳定，参数值可能会溢出（变成 NaN），训练崩溃。 如果在更新模型参数前，梯度的范数 (norm) 超过了预设的某个阈值，则按比例将其缩小，如果 $\\|\\hat{\\boldsymbol{g}}\\| \\ge threshold$ ，则进行梯度裁剪，梯度裁剪让模型更新时保持在相同的方向上，但只迈出更小的一步： $$ \\hat{\\boldsymbol{g}} \\leftarrow \\frac{threshold}{\\|\\hat{\\boldsymbol{g}}\\|} \\hat{\\boldsymbol{g}} $$Vanishing Gradient 梯度消失：\n如果 $W_h$ 的特征值小于 1，或者激活函数（如图中提到的 $f$ 或 tanh）的导数小于 1。 梯度会随着反向传播的步数增加而指数级减小。 这对应了提到的 RNN 缺点：“在实践中，很难获取到很多步之前的信息”。当梯度变得极其微小时，远距离的权重更新几乎停滞，模型“忘记”了长期的上下文。 对于 vanilla RNN（基础 RNN）来说，学习如何跨越多个时间步长来保留信息实在是太困难了，因为隐藏状态 $\\boldsymbol{h}^{(t)}$ 会被不断地重写： $$ \\boldsymbol{h}^{(t)} = \\sigma(\\boldsymbol{W}_h \\boldsymbol{h}^{(t-1)} + \\boldsymbol{W}_x \\boldsymbol{x}^{(t)} + \\boldsymbol{b}) $$ 所以，我们引入独立记忆 ，如LSTM。或者建立直接连接，如Attention机制。\nLong Short-Term Memory Understanding LSTM Networks \u0026ndash; colah\u0026rsquo;s blog\n遗忘门 (Forget gate)：控制从上一个细胞状态中保留什么以及忘记什么。 $$ \\boldsymbol{f}^{(t)} = \\sigma (\\boldsymbol{W}_f \\boldsymbol{h}^{(t-1)} + \\boldsymbol{U}_f \\boldsymbol{x}^{(t)} + \\boldsymbol{b}_f) $$ 输入门 (Input gate)：控制新细胞内容的哪些部分被写入到细胞中。 $$ \\boldsymbol{i}^{(t)} = \\sigma (\\boldsymbol{W}_i \\boldsymbol{h}^{(t-1)} + \\boldsymbol{U}_i \\boldsymbol{x}^{(t)} + \\boldsymbol{b}_i) $$ 输出门 (Output gate)：控制细胞的哪些部分被输出到隐藏状态中。 $$ \\boldsymbol{o}^{(t)} = \\sigma (\\boldsymbol{W}_o \\boldsymbol{h}^{(t-1)} + \\boldsymbol{U}_o \\boldsymbol{x}^{(t)} + \\boldsymbol{b}_o) $$ New cell content：这是要被写入细胞的新内容（即我们在之前讨论中提到的“候选内容”）。 $$ \\tilde{\\boldsymbol{c}}^{(t)} = \\tanh (\\boldsymbol{W}_c \\boldsymbol{h}^{(t-1)} + \\boldsymbol{U}_c \\boldsymbol{x}^{(t)} + \\boldsymbol{b}_c) $$ Cell state：擦除（“忘记”）上一个细胞状态中的某些内容，并写入（“输入”）一些新的细胞内容。 $$ \\boldsymbol{c}^{(t)} = \\boldsymbol{f}^{(t)} \\odot \\boldsymbol{c}^{(t-1)} + \\boldsymbol{i}^{(t)} \\odot \\tilde{\\boldsymbol{c}}^{(t)} $$ 隐藏状态 (Hidden state)：从细胞中读取（“输出”）某些内容。 $$ \\boldsymbol{h}^{(t)} = \\boldsymbol{o}^{(t)} \\odot \\tanh \\boldsymbol{c}^{(t)} $$Step-by-Step LSTM Walk Through 在上图中，每条线都承载着一个完整的向量，从一个节点的输出指向其他节点的输入。粉色圆圈代表逐点运算，例如向量加法，而黄色方框代表已学习的神经网络层。合并的线条表示连接，而分叉的线条表示其内容被复制，并将副本发送到不同的位置。 LSTM 的关键在于单元状态，也就是图中贯穿顶部的水平线。\n单元状态就像一条传送带，它沿着整个链条直线传递，只有一些微小的线性交互。信息很容易原封不动地沿着传送带流动。\nLSTM 能够对单元状态进行信息添加或移除，这种操作由称为“门”的结构进行精细调控。\n门是一种选择性地允许信息通过的方式。它们由一个 sigmoid 神经网络层和一个逐点乘法运算组成。\nLSTM 的第一步是决定我们要从细胞状态中丢弃什么信息 。这个决定由一个称为“遗忘门层”的 sigmoid 层来做出 。它接收 $h_{t-1}$ 和 $x_t$，并为细胞状态 $C_{t-1}$ 中的每个数字输出一个介于 0 和 1 之间的数值；1 代表“完全保留”，而 0 代表“彻底丢弃” 。 让我们回到之前那个尝试根据前面所有单词预测下一个单词的语言模型例子 。在这样一个问题中，细胞状态可能包含了当前主语的性别信息，以便模型能使用正确的代词 。当我们看到一个新的主语时，我们会希望忘记旧主语的性别 。 下一步是决定我们要将什么新信息存储在细胞状态中 。这包含两个部分。首先，一个称为“输入门层”的 sigmoid 层决定了我们将更新哪些值；接着，一个 $\\tanh$ 层创建了一个包含新候选值的向量 $\\tilde{C}_t$，这些值可以被添加到状态中。在下一步里，我们将结合这两个部分来对状态进行更新 。 在我们的语言模型例子中，我们会希望将新主语的性别添加到细胞状态中，以替换我们正在遗忘的旧性别信息 。 现在是时候将旧的细胞状态 $C_{t-1}$ 更新为新的细胞状态 $C_t$ 了。前面的步骤已经决定了要做什么，我们只需要去实际执行它 。\n我们将旧状态乘以 $f_t$，以此来忘掉我们之前决定要忘记的信息 。然后我们加上 $i_t * \\tilde{C}_t$。这就是新的候选值，根据我们决定更新每个状态值的程度进行了缩放 。\n在语言模型的例子中，正是在这里，我们实际丢弃了关于旧主语性别的信息，并添加了新信息，就像我们在前面步骤中决定的那样 。\n最后，我们需要决定我们要输出什么 。这个输出将基于我们的细胞状态，但会是一个经过过滤的版本 。\n首先，我们运行一个 sigmoid 层，它决定了我们要输出细胞状态的哪些部分 。然后，我们将细胞状态通过 $\\tanh$（将值推到 -1 和 1 之间）并将其乘以 sigmoid 门的输出，这样我们就只输出了我们决定输出的部分 。\n以语言模型为例，由于它刚刚处理了一个主语，接下来可能会倾向于输出与谓语动词相关的信息，以预判后续内容。例如，模型可能会输出该主语是单数还是复数，这样如果接下来出现动词，我们就能知道该以何种形式进行词形变化。\nHow does LSTM solve vanishing gradients LSTM 架构使得 RNN 更容易在多个时间步长内保留信息。例如，如果某个单元维度的遗忘门设置为 1，输入门设置为 0，那么该单元的信息将被无限期地保留。\n相比之下，普通的 RNN 更难学习一个循环权重矩阵 $W_h$，以保留隐藏状态中的信息。\n尽管梯度消失/爆炸现象无法避免，但对于长距离依赖关系，模型还可以创建更直接、更线性的直通连接。比如ResNet, DenseNet\u0026hellip;\u0026hellip;都在模块之间、层之间不同程度地创建了直接连接。\nBidirectional RNNs 传统的单向 RNN 或 LSTM 存在一个明显的局限：在处理序列时，它只能“向左看”（即只利用过去的历史上下文）。然而，在许多 NLP 任务（如情感分类、命名实体识别或句子整体理解）中，当前词的准确含义往往也依赖于“右侧”（未来）的上下文。 为了解决这个问题，研究者引入了双向架构（通常使用 LSTM 实现，即 BiLSTM）： 前向 (Forward) RNN：按照从左到右的顺序处理输入序列，计算出一系列隐藏状态 $\\overrightarrow{h}_t$。 后向 (Backward) RNN：按照从右到左的逆序处理同一个输入序列，计算出一系列隐藏状态 $\\overleftarrow{h}_t$。 拼接状态 (Concatenated State)：在每一个时间步 $t$，将前向和后向的隐藏状态拼接在一起，形成该位置的最终表示 $h_t = [\\overrightarrow{h}_t; \\overleftarrow{h}_t]$。这样，每个词的特征表示就同时包含了整个句子的左侧和右侧完整信息。 双向 LSTM 的特征提取能力非常强大，但它仅适用于能够一次性获取完整输入序列的任务（例如对整段文本进行分类，或翻译时的源句子编码）。它不能用于传统的语言模型（Language Modeling），因为语言模型的本质任务是“预测下一个词”，如果允许模型看到右侧的“未来”信息，就违背了自回归预测的初衷。 Neural Machine Translation 神经机器翻译是 NLP 深度学习领域的第一个巨大成功。NMT 主要是基于 Sequence-to-Sequence (Seq2Seq) 架构，该架构的核心正是由两个 RNN（通常是 LSTM）组成的：编码器 (Encoder) 和 解码器 (Decoder)。 Encoder负责读取源语言句子，但在读取过程中它不产生实际的翻译输出，而是不断更新其隐藏状态。当 Encoder 处理完源句子的最后一个词后，其最终的隐藏状态（Final Hidden State）被视作整个句子的语义浓缩。它充当了一个“信息瓶颈”，因为整个源句子的所有复杂含义都必须被压缩进这一个固定维度的向量中。 Decodere端的 LSTM 本质上是一个条件语言模型 (Conditional Language Model)，初始隐藏状态不再是随机的或全零的，而是被严格赋值为 Encoder 输出的那个“瓶颈”向量。这意味着 Decoder 的所有生成动作都是以源句子的语义向量为条件展开的。 在每一个时间步，它根据当前的隐藏状态输出概率最高的词，并将该词（Feeding in last word）作为下一步的输入继续循环，直到生成句子结束标记 \u0026lt;EOS\u0026gt;。 ","date":"2026-01-28T14:31:08+08:00","permalink":"/p/cs224n/","title":"CS224N"},{"content":"本篇是对Pytorch基础使用的学习，主要基于B站小土堆的https://www.bilibili.com/video/BV1hE411t7RN/以及Gemini的帮助，除实践外也包含了一些深度学习模型的原理\nDataset An abstract class representing a Dataset.\nAll datasets that represent a map from keys to data samples should subclass it. All subclasses should overwrite __getitem__, supporting fetching a data sample for a given key. Subclasses could also optionally overwrite __len__, which is expected to return the size of the dataset by many ~torch.utils.data.Sampler implementations and the default options of ~torch.utils.data.DataLoader. Subclasses could also optionally implement __getitems__, for speedup batched samples loading. This method accepts list of indices of samples of batch and returns list of samples.\nnote\n~torch.utils.data.DataLoader by default constructs an index sampler that yields integral indices. To make it work with a map-style dataset with non-integral indices/keys, a custom sampler must be provided. 一个表示数据集的抽象类。\n所有实现从键（keys）到数据样本（data samples）映射的数据集都应该继承这个类。所有子类都必须重写 __getitem__ 方法，以支持根据给定的键获取数据样本。子类也可以选择性地重写 __len__ 方法，许多 ~torch.utils.data.Sampler 实现和 ~torch.utils.data.DataLoader 的默认选项都需要通过这个方法返回数据集的大小。子类还可以选择性地实现 __getitems__ 方法，以加速批量样本的加载。此方法接受批处理样本的索引列表，并返回样本列表。\n注意\n默认情况下，~torch.utils.data.DataLoader 会构造一个生成整数索引的采样器。要让其与非整数索引/键的映射式数据集一起工作，必须提供自定义的采样器。\n自定义Dataset 那么在实际的使用中，比如有一个数据集（我们拿之前用过的fer2013来举例）：\n先定义一个数据类继承Dataset，定义__init__()\n1 2 3 4 5 6 7 8 from torch.utils.data import Dataset import os class MyData(Dataset): def __init__(self, root_dir, label_dir): self.root_dir = root_dir # 如 fer2013/test self.label_dir = label_dir # 如 angry self.path = os.path.join(self.root_dir, self.label_dir) # 路径合并函数 解决跨系统的文件路径问题 self.img_path = os.listdir(self.path) # 如angry文件夹下，所有图片名的string list 重写__getitem()\n1 2 3 4 5 6 def __getitem__(self, index): img_name = self.img_path[index] img_item_path = os.path.join(self.root_dir, self.label_dir, img_name) # 拼接出具体的某一个图片的路径 img = Image.open(img_item_path) label = self.label_dir return img, label # 返回数据集中的一个对象（图片）及其类型 重写__len()__\n1 2 def __len__(self): return len(self.img_path) 定义示例\n1 2 3 root_dir = \u0026#34;fer2013/train\u0026#34; angry_label_dir = \u0026#34;angry\u0026#34; angry_dataset = MyData(root_dir, angry_label_dir) Dataset合并\n1 2 3 4 disguest_label_dir = \u0026#34;disguest\u0026#34; disgust_dataset = MyData(root_dir, disguest_label_dir) train_dataset = angry_dataset + disgust_dataset # \u0026#39;+\u0026#39; TensorBoard SummaryWriter 创建日志目录 初始化Writer对象 生成事件文件 这个文件才是真正的数据库(events.out\u0026hellip;\u0026hellip;)当后续调用 writer.add_scalar 时，数据并不是直接画在屏幕上，而是被追加写进这个文件里。 当你执行完相应代码，运行 tensorboard --logdir=logs 时，TensorBoard 的后端服务器就会去读取这个文件里的内容，并在浏览器里渲染成图表。 1 2 3 from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter(\u0026#34;logs\u0026#34;) # 这行代码会在你的项目根目录下创建一个名为\u0026#34;logs\u0026#34;的文件夹 需注意，每次实验可以用不同的文件夹来记录数据，比如writer = SummaryWriter(\u0026quot;logs/lr0.01_batch32\u0026quot;)修改了学习率后writer = SummaryWriter(\u0026quot;logs/lr0.001_batch32\u0026quot;)\nadd_scalar() 绘图函数add_scalar() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 (method) def add_scalar( tag: Any, scalar_value: Any, # 对应图像的y轴 global_step: Any | None = None, # 对应图像的x轴 walltime: Any | None = None, new_style: bool = False, double_precision: bool = False ) -\u0026gt; None ​``` 例 ``` for i in range(100): writer.add_scalar(\u0026#34;y=2x\u0026#34;, 2*i, i) writer.close() add_image() 查看图片函数add_image() 1 2 3 4 5 6 7 (method) def add_image( tag: Any, img_tensor: Any, global_step: Any | None = None, walltime: Any | None = None, dataformats: str = \u0026#34;CHW\u0026#34; ) -\u0026gt; None 要注意参数的类型要求：img_tensor (torch.Tensor, numpy.ndarray, or string/blobname): Image data，所以要将PIL类型的图片进行转换，比如用numpy来转换。以及数据的shape要求： Tensor with :math:(1, H, W), :math:(H, W), :math:(H, W, 3) is also suitable as long as corresponding dataformats argument is passed, e.g. CHW, HWC, HW. 即三种图片数据的格式，其通道数、高度、宽度的顺序不同。\n1 2 3 4 5 img = Image.open(image_path) img_array = np.array(img) print(f\u0026#34;图像形状: {img_array.shape}\u0026#34;) writer.add_image(\u0026#34;test\u0026#34;,img_array,1,dataformats=\u0026#39;HW\u0026#39;) # 由.shape可得 writer.close() add_graph() 常见的Transforms 下面基本都是对图像的处理方法\nToTensor Convert a PIL Image or ndarray to tensor and scale the values accordingly.\nThis transform does not support torchscript.\nConverts a PIL Image or numpy.ndarray (H x W x C) in the range [0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0] if the PIL Image belongs to one of the modes (L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1) or if the numpy.ndarray has dtype = np.uint8\nIn the other cases, tensors are returned without scaling.\nnote\nBecause the input image is scaled to [0.0, 1.0], this transformation should not be used when transforming target image masks. See the [references](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) for implementing the transforms for image masks.\n将PIL图片转为torch类型，如torch.Size([1, 48, 48])\n1 2 3 4 5 trans_totensor = transforms.ToTensor() img_tensor = trans_totensor(img) print(img_tensor.shape) writer.add_image(\u0026#34;ToTensor\u0026#34;,img_tensor) writer.close() Normalize Normalize a tensor image with mean and standard deviation.\n​ This transform does not support PIL Image.\n​ Given mean: (mean[1],...,mean[n]) and std: (std[1],..,std[n]) for n\n​ channels, this transform will normalize each channel of the input\n​ torch.*Tensor i.e.,\n​ output[channel] = (input[channel] - mean[channel]) / std[channel]\nnote:\n​ This transform acts out of place, i.e., it does not mutate the input tensor.\nArgs:\n​ mean (sequence): Sequence of means for each channel.\n​ std (sequence): Sequence of standard deviations for each channel.\n​ inplace(bool,optional): Bool to make this operation in-place.\nNormailize 的数学原理是： $$ output = \\frac{input-mean}{std} $$ 参数mean影响的是“中心位置”，在ToTensor之后，像素值在 $[0, 1]$ 之间，中心大约在 $0.5$，如mean = 0.5，那么减去 $0.5$ 后，数据的中心变成了 $0$。原本 $[0, 1]$ 的范围变成了 $[-0.5, 0.5]$ 参数std影响的是“缩放幅度”，如std = 0.5，那么就是将数据范围再除以$0.5$，最终范围从 $[-0.5, 0.5]$ 变成了 $[-1, 1]$ Resize Resize the input image to the given size.\n​ If the image is torch Tensor, it is expected\n​ to have [\u0026hellip;, H, W] shape, where \u0026hellip; means a maximum of two leading dimensions\nArgs:\n​ size (sequence or int): Desired output size. If size is a sequence like\n​ (h, w), output size will be matched to this. If size is an int,\n​ smaller edge of the image will be matched to this number.\n​ i.e, if height \u0026gt; width, then image will be rescaled to\n​ (size * height / width, size).\nresize()支持PIL和Tensor两种图片格式，如果是 Tensor，期望形状为 [..., H, W]。这里的 ... 表示它可以处理 [C, H, W]（单张图）或 [B, C, H, W]（一个 Batch 的图） 参数size需注意写成序列形式，如resize((512, 512))，而如果只输入一个参数，如resize(512)，那么图片短边会变为512，而长边会按比例改变 1 2 3 4 print(img.size) trans_resize = transforms.Resize((224,224)) img_resize = trans_resize(img) print(img_resize) Compse Composes several transforms together. This transform does not support torchscript.\n​ Please, see the note below.\nArgs:\n​ transforms (list of Transform objects): list of transforms to compose.\ncompse()操作是对各种transforms操作的流水线类 在深度学习中，图片通常需要经过一系列的固定步骤（比如：缩放 -\u0026gt; 转为 Tensor -\u0026gt; 归一化）。如果不用 Compose，你每一张图都要手动调用多个函数，代码会非常冗余 compse()的参数类型是一个列表，操作是按顺序执行的，所以前一个操作输出的数据类型必须能作为下一个操作的输入。 1 2 3 4 5 6 7 8 9 from torchvision import transforms # 定义训练集的预处理 train_transform = transforms.Compose([ transforms.Resize((224, 224)), # VGG16标准输入是224x224 transforms.RandomHorizontalFlip(), # 数据增强：随机水平翻转 transforms.ToTensor(), # 归一化到 [0.0, 1.0] transforms.Normalize([0.5], [0.5]) # 标准化到 [-1.0, 1.0] ]) img_tensor = train_transform(img) Pytorch数据集使用 例如，我们要导入视觉学习的数据集，可以直接在程序中进行数据集的下载\n1 2 3 4 5 import torchvision train_set = torchvision.datasets.CIFAR10(root=\u0026#34;./dataset...\u0026#34;, train=True, download=True) test_set = torchvision.datasets.CIFAR10(root=\u0026#34;./dataset...\u0026#34;, train=false, download=True) root参数表示数据集存放的位置，train参数表示数据集是否用来训练，download参数表示是否下载到本地（会生成一个下载链接） 具体的参数设置，每个数据集都可能有所区别\u0026hellip;\u0026hellip; 如果是下载完成到本地的数据集，可以将其复制到项目目录的dataset文件夹下，再运行程序即可节省下载的时间 1 2 3 4 5 6 print(test_set.classes) # 可以看到测试数据集的所有类别 img, target = test_set[0] print(img) print(target) print(test_set.classes[target]) # 输出测试集第一个元素对应的类别 DataLoader Data loader combines a dataset and a sampler, and provides an iterable over the given dataset.\n​ The :class:~torch.utils.data.DataLoader supports both map-style and\n​ iterable-style datasets with single- or multi-process loading, customizing\n​ loading order and optional automatic batching (collation) and memory pinning.\n在训练模型时，不能一次性把数据集中的大量数据塞进内存。DataLoader实现了：\nBatching（批处理）： 把图片打包成一组组（Batch）。 Shuffling（打乱）： 每一轮训练（Epoch）开始时随机洗牌，防止模型死记硬背数据顺序。 Parallel Computing（并行加载）： 利用多核 CPU 提前准备下一批数据，不让 GPU 等待。 参数名 常用取值 作用描述 dataset 自定义 Dataset 必填。告诉 DataLoader 从哪个“仓库”取数据。 batch_size 16, 32, 64\u0026hellip; 每批装载的数量。 越大训练越快，但越占显存。在 FER 表情识别中，32 或 64 是常用数值。 shuffle True / False 是否打乱顺序。 训练集通常设为 True（增加随机性）；测试集通常设为 False。 num_workers 0, 2, 4, 8\u0026hellip; 多进程加载。 0 表示只用主进程（慢）。增加数值可以加快读取速度。建议： 设为 CPU 核心数的一半。 drop_last True / False 丢弃最后多余的数据。 比如有 100 张图，batch_size=32。最后剩 4 张不够一包，设为 True 就会把这 4 张扔掉，确保每个 Batch 大小一致。 pin_memory True 内存锁页。 如果你用 GPU 训练，设为 True 可以加快数据从内存传输到显存的速度。 例如，我们用DataLoader处理CIFAR10中的数据 1 2 3 test_data = torchvision.datasets.CIFAR10(\u0026#34;./dataset\u0026#34;, train=False, transform=torchvision.transforms.ToTensor(), download=True) test_loader = DataLoader(dataset=test_data, batch_size=4, shuffle=True, num_workers=0, drop_last=False) 结合循环和tensorboard，输出每一个epoch中每一step用到的图片 下面代码中step + epoch * len(test_loader)是使用了全局步长，也可以不这样写，而是把每个epoch来当作一组 1 2 3 4 5 6 7 8 writer = SummaryWriter(\u0026#34;dataloader\u0026#34;) for epoch in range(2): step = 0 for data in test_loader: imgs, targets = data writer.add_images(\u0026#34;test_data_batch\u0026#34;, imgs, step + epoch * len(test_loader)) step = step + 1 writer.close() nn.Module Base class for all neural network modules.\n​ Your models should also subclass this class.\n​ Modules can also contain other Modules, allowing to nest them in a tree structure.\n在 PyTorch 中，无论是简单的线性层，还是复杂的 VGG16 或 Transformer，本质上都是一个 nn.Module。它是所有神经网络模块的基类 nn.Module 支持嵌套，当你对大模型调用 model.to(\u0026quot;cuda\u0026quot;) 时，PyTorch 会顺着这棵“树”，自动把里面所有的子层都搬到 GPU 上。 只要在 __init__ 中把一个层赋值给 self.xxx，PyTorch 就会自动识别出其中的权重 (Weights) 和 偏置 (Bias)，并将它们加入到待优化的参数列表中。 编写一个nn.Module子类时，必须重写__init()__和forward()\n__init(self)__ 在这里定义网络层（卷积、池化、全连接等）。 必须调用 super().__init__()。这行代码的作用是初始化父类的属性，如果没有它，PyTorch 就没法自动追踪定义的层，模型也就无法训练。 forward(self, x) 定义数据的流向。图片进去后，先过哪一层，再过哪一层。 不需要手动调用 forward，只需要运行 model(input)，PyTorch 会自动触发 forward。 1 2 3 4 5 6 7 8 9 10 import torch from torch import nn class myModule(nn.Module): def __init__(self): super().__init__() def forward(self, input): output = input + 1 # 简单地将输出 +1 再输出 return output 卷积 Conv $$ \\text{out}(N_i, C_{\\text{out}_j}) = \\text{bias}(C_{\\text{out}_j}) + \\sum_{k = 0}^{C_{\\text{in}} - 1} \\text{weight}(C_{\\text{out}_j}, k) \\star \\text{input}(N_i, k) $$ 卷积动画页面：conv_arithmetic/README.md at master · vdumoulin/conv_arithmetic 参数 含义 作用 in_channels 输入通道数 彩色图通常为 3 (RGB)，灰度图为 1。 out_channels 输出通道数 卷积核的数量。有多少个核，输出就有多少层特征图。 kernel_size 卷积核大小 提取特征的“窗口”大小。常用 3 或 5（VGG 常用 3）。 stride 步长 窗口滑动的跨度。默认为 1。步长越大，输出图片越小。 padding 填充 在图片四周补 0。'same' 保持大小不变，'valid' 则不补。 dilation 空洞卷积 卷积核点之间的间距。用于扩大感受野（不用增加参数量）。 bias 偏置 是否在结果上加一个常数偏移。默认开启。 卷积的Padding（边界扩充）参数很重要，如果周围不补零，那么卷积会导致图像尺寸越来越小 形状计算公式： $$ H_{out} = \\left\\lfloor\\frac{H_{in} + 2 \\times \\text{padding}[0] - \\text{dilation}[0] \\times (\\text{kernel_size}[0] - 1) - 1}{\\text{stride}[0]} + 1\\right\\rfloor $$$W_{out}$计算同理\n1 2 3 4 5 6 7 8 9 10 11 12 13 dataset = torchvision.datasets.CIFAR10(\u0026#34;./dataset\u0026#34;,train = False, transform=torchvision.transforms.ToTensor(),download=True) dataloader = DataLoader(dataset, batch_size=64) class myModule(nn.Module): def __init__(self): super().__init__() self.conv1 = Conv2d(in_channels=3,out_channels=6,kernel_size=3,stride=1,padding=0) def forward(self,x): x = self.conv1(x) return x mymodule = myModule() # 模型实例化 1 2 3 4 5 6 7 8 9 10 step = 0 for data in dataloader: imgs, targets = data output = mymodule(imgs) print(imgs.shape) # torch.Size([64, 3, 32, 32]) print(output.shape) # torch.Size([64, 6, 30, 30]) channel == 6 卷积后，channel数变化，不能直接输出图像 step = step + 1 （最大）池化 MaxPool 最大池化的逻辑非常简单：在一个窗口（Kernel）范围内，只保留最大的那个值，剩下的全部扔掉 既保留输入特征，又减小了数据量，加快训练速度 参数 独特之处 kernel_size 窗口大小。常见是 2（即将 $2 \\times 2$ 的区域合并）。 stride 默认值等于 kernel_size！这和卷积不同。如果 kernel_size=2，步长默认就是 2，这样窗口之间就不会重叠。 ceil_mode 非常重要。 默认是 False（向下取整）。如果设为 True（向上取整），当窗口超出边界时，只要窗口内有数据，就会保留结果，而不是舍弃。 padding 填充。注意池化填充的是负无穷（$-\\infty$），这样是为了保证填充位不会被选为最大值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 input = torch.tensor([[1,2,0,3,1], [0,1,2,3,1], [1,2,1,0,0], [5,2,3,1,1], [2,1,0,1,1]], dtype=float) input = torch.reshape(input,(-1,1,5,5)) print(input.shape) class myMoudle(nn.Module): def __init__(self, *args, **kwargs): super(myMoudle, self).__init__() self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=True) \u0026#39;\u0026#39;\u0026#39; ceil_mode = False 就表示只有当池化核遇到的尺寸是最大尺寸（如3x3）时 才会取其池化的结果，否则相反 \u0026#39;\u0026#39;\u0026#39; def forward(self, input): output = self.maxpool1(input) return output mymoudle = myMoudle() output = mymoudle(input) print(output) 1 2 3 torch.Size([1, 1, 5, 5]) tensor([[[[2., 3.], [5., 1.]]]], dtype=torch.float64) 损失函数与反向传播 损失函数用于计算实际输出与目标之间的差距，为反向传播、更新参数提供一定的依据。在分类任务中，常用交叉熵函数来计算误差， 1 loss_func = nn.CrossEntropyLoss() 优化器 torch.optim — PyTorch 2.10 documentation\n示例代码： 1 2 3 4 5 6 for input, target in dataset: optimizer.zero_grad() # 梯度清零 output = model(input) loss = loss_fn(output, target) # 调用损失函数 loss.backward() # 反向传播 optimizer.step() Pytorch实战：CIFAR10 针对CIFAR10图像数据集的简单分类模型实战 首先了解Sequential，nn.Sequential 是 nn.Module 的一个特殊子类，它的作用是自动完成 forward 逻辑。注意：其中每一个参数都是某层的类，所以要写逗号。Sequential既简化了模型定义，也简化了forward()。 1 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 CIFAR10_Simple(nn.Module): def __init__(self, *args, **kwargs): super(CIFAR10_Simple, self).__init__(*args, **kwargs) self.conv1 = Conv2d(in_channels=3, out_channels=32, kernel_size=5, padding=2) \u0026#39;\u0026#39;\u0026#39; 参数padding的数值可以从想象图像得到：5x5的卷积核，当中心在图像的(0,0)，那么卷积核是扩展出去2格的。上面是简单判断的方法，实际应该用尺寸计算公式来代入计算（参考“Conv卷积”节内容） \u0026#39;\u0026#39;\u0026#39; self.model_s = Sequential( Conv2d(in_channels=3, out_channels=32, kernel_size=5, padding=2), nn.ReLU(), MaxPool2d(kernel_size=2), Conv2d(in_channels=32, out_channels=32, kernel_size=5, padding=2), nn.ReLU(), MaxPool2d(2), Conv2d(32,64,5,padding=2), nn.ReLU(), MaxPool2d(2), Flatten(), Linear(1024, 64), nn.ReLU(), Linear(64, 10) ) def forward(self, x): \u0026#39;\u0026#39;\u0026#39; x = self.conv1(x) x = self.maxpool1(x) x = self.conv2(x) x = self.maxpool2(x) x = self.conv3(x) x = self.maxpool3(x) x = self.flatten(x) x = self.linear1(x) x = self.linear2(x) \u0026#39;\u0026#39;\u0026#39; x = self.model_s(x) return x 在搭建模型前，先设置DataLoader处理数据集\n分别设置好train_data和test_data 1 2 3 4 5 6 dataset_transfrom = tf.Compose([tf.ToTensor(),tf.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))]) train_data = torchvision.datasets.CIFAR10(\u0026#34;./dataset\u0026#34;, transform=dataset_transfrom, download=True) test_data = torchvision.datasets.CIFAR10(\u0026#34;./dataset\u0026#34;, train=False, transform=dataset_transfrom, download=True) # -------- train_loader = DataLoader(dataset=train_data, batch_size=64, shuffle=True, drop_last=True) test_loader = DataLoader(dataset=test_data, batch_size=64, shuffle=True, drop_last=True) 搭建好模型后，简单测试输出尺寸是否符合要求 1 2 3 4 5 cifar = CIFAR10_Simple() print(cifar) input = torch.ones((64, 3, 32, 32)) # 同数据集图片尺寸的测试 output = cifar(input) print(output.shape) 训练前的基本设置 定义TensorBoard的writer 设置device调用显卡加速训练 实例化训练模型 定义损失函数 设置优化器 1 2 3 4 5 6 writer = SummaryWriter(\u0026#34;./logs\u0026#34;) writer.add_graph(cifar, input) device = torch.device(\u0026#34;cuda\u0026#34; if torch.cuda.is_available() else \u0026#34;cpu\u0026#34;) model = CIFAR10_Simple().to(device) loss_func = nn.CrossEntropyLoss() # 交叉熵损失函数 optim = torch.optim.SGD(cifar.parameters(), lr=0.01) # 学习率 训练部分 优化器的定式代码 记录训练损失 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 total_step = 0 for epoch in range(10): # --- 训练部分 --- model.train() for data in train_loader: imgs, targets = data outputs = model(imgs.to(device)) loss = loss_func(outputs, targets.to(device)) optim.zero_grad() loss.backward() optim.step() # 记录训练损失 writer.add_scalar(\u0026#34;Train_Loss\u0026#34;, loss.item(), total_step) total_step += 1 评估部分 每个epoch执行一次 with torch.no_grad()：关闭梯度记录 统计性能指标 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 model.eval() total_test_loss = 0 total_accuracy = 0 with torch.no_grad(): # 测试时不需要计算梯度，节省性能 for data in test_loader: imgs, targets = data imgs, targets = imgs.to(device), targets.to(device) outputs = model(imgs) # 计算总损失 loss = loss_func(outputs, targets) total_test_loss += loss.item() # 计算准确率：argmax(1) 找到概率最大的类别索引 accuracy = (outputs.argmax(1) == targets).sum() total_accuracy += accuracy 可视化 1 2 3 4 5 6 7 # 输出到 TensorBoard writer.add_scalar(\u0026#34;Test_Loss\u0026#34;, total_test_loss / len(test_loader), epoch) writer.add_scalar(\u0026#34;Test_Accuracy\u0026#34;, total_accuracy / len(test_data), epoch) print(f\u0026#34;Epoch {epoch+1} 结束，准确率: {total_accuracy / len(test_data)}\u0026#34;) writer.close() ","date":"2026-01-22T10:49:28+08:00","permalink":"/p/pytorch%E5%9F%BA%E7%A1%80/","title":"Pytorch基础"},{"content":"数据挖掘复习 内容来自PPT及最后课上画的重点信息\n复习题PPT 数据挖掘中的关联规则挖掘主要用来发现数据项之间的什么关系？\n关联规则挖掘主要用来发现数据项之间的频繁共存或隐含的关联关系。\n聚类分析中，K-means算法的K值表示什么？\n在K-means聚类算法中，K值表示用户期望将数据集划分成的簇的数量。\n决策树有哪些算法？主要根据什么标准选择特征进行划分，并分析各标准的不足之处。\n算法 标准 不足 ID3 信息增益 强烈偏好多值特征，容易过拟合 C4.5 信息增益率 计算更复杂，可能偏好少值特征 CART 基尼指数 对多值特征有轻微偏好，倾向于不平衡分裂 给定一个简单的文本分类训练集，用于判断邮件是否为“垃圾邮件(Spam)”。词典包含以下5个词语： [\u0026quot;deal\u0026quot;, \u0026quot;money\u0026quot;, \u0026quot;urgent\u0026quot;, \u0026quot;meeting\u0026quot;, \u0026quot;free\u0026quot;]\n训练数据：\n垃圾邮件 (Spam, S)： \u0026ldquo;deal free money\u0026rdquo; \u0026ldquo;urgent free deal\u0026rdquo; \u0026ldquo;money urgent free\u0026rdquo; 非垃圾邮件 (Ham, H)： \u0026ldquo;meeting deal\u0026rdquo; \u0026ldquo;urgent meeting\u0026rdquo; 待分类的新邮件内容为： \u0026ldquo;free urgent meeting\u0026rdquo;\n任务要求： 使用多项式朴素贝叶斯模型（应用拉普拉斯平滑，其中平滑参数 λ=1λ=1）对该邮件进行分类。请按步骤完成以下计算：\n计算先验概率 P(Spam)P(Spam) 和 P(Ham)P(Ham)。\n文档总数： $N_{\\text{doc}} = 3 + 2 = 5$\n$P(S) = \\frac{3}{5} = 0.6$\n$P(H) = \\frac{2}{5} = 0.4$\n先验概率结果：\n$P(S) = 0.6, \\quad P(H) = 0.4$\n计算每个词语在 Spam 和 Ham 类别下的类条件概率。\n词典大小 $|V| = 5$\nSpam:\ndeal: 2, money: 2, urgent: 2, meeting: 0, free: 3\n总词数：$2+2+2+0+3 = 9$ 分母：$9 + 5 = 14$ $P(\\text{deal}|S) = \\frac{2+1}{14} = \\frac{3}{14}$\n$P(\\text{money}|S) = \\frac{2+1}{14} = \\frac{3}{14}$\n$P(\\text{urgent}|S) = \\frac{2+1}{14} = \\frac{3}{14}$\n$P(\\text{meeting}|S) = \\frac{0+1}{14} = \\frac{1}{14}$\n$P(\\text{free}|S) = \\frac{3+1}{14} = \\frac{4}{14}$\nHam:\ndeal: 1, money: 0, urgent: 1, meeting: 2, free: 0\n总词数：$1+0+1+2+0 = 4$\n分母：$4 + 5 = 9$\n$P(\\text{deal}|H) = \\frac{1+1}{9} = \\frac{2}{9}$\n$P(\\text{money}|H) = \\frac{0+1}{9} = \\frac{1}{9}$\n$P(\\text{urgent}|H) = \\frac{1+1}{9} = \\frac{2}{9}$\n$P(\\text{meeting}|H) = \\frac{2+1}{9} = \\frac{3}{9}$\n$P(\\text{free}|H) = \\frac{0+1}{9} = \\frac{1}{9}$\n将新邮件 “free urgent meeting” 表示为词频向量。\n\u0026ldquo;free urgent meeting\u0026quot;的词频向量（按词典顺序：deal, money, urgent, meeting, free）：\n$[0, 0, 1, 1, 1]$\n计算该邮件属于 Spam 和 Ham 的后验概率。\nSpam:\n$P(S|d) \\propto P(S) \\times P(\\text{urgent}|S) \\times P(\\text{meeting}|S) \\times P(\\text{free}|S)$\n$= 0.6 \\times \\frac{3}{14} \\times \\frac{1}{14} \\times \\frac{4}{14}$\n$= 0.6 \\times \\frac{12}{2744} \\approx 0.002624$\nHam:\n$P(H|d) \\propto P(H) \\times P(\\text{urgent}|H) \\times P(\\text{meeting}|H) \\times P(\\text{free}|H)$\n$= 0.4 \\times \\frac{2}{9} \\times \\frac{3}{9} \\times \\frac{1}{9}$\n$= 0.4 \\times \\frac{6}{729} \\approx 0.003292$\n归一化:\n$P(S|d) = \\frac{0.002624}{0.002624 + 0.003292} \\approx 0.444$\n$P(H|d) = \\frac{0.003292}{0.002624 + 0.003292} \\approx 0.556$\n根据后验概率判断其最终类别。\n由于 $P(H|d) \u003e P(S|d)$，新邮件被分类为：\n$\\boxed{Ham}$\n2.1.4 属性的类型 标称、序数、区间、比率\n属性类型 描述 例子 操作 分类 (定性) 标称 标称属性的值仅仅只是不同的名字，即标称值只提供足够的信息以区分对象（=，≠） 邮政编码，雇员ID号，性别 众数、熵、列联相关，卡方检验 序数 序数属性的值提供足够的信息确定对象的序（\u0026lt;\u0026gt;） 矿石硬度{好，一般}、成绩等级、街道号码 中值、百分位、秩相关、游程检验、符号检验 数值 (定量) 区间 对于区间属性，值之间的差是有意义的，即存在测量单位（+，-） 日历日期、摄氏或华氏温度 均值、标准差、皮尔逊相关系数，t和F检验 比率 对于比率属性，差和比率都是有意义的（+，-，*，/） 绝对温度、货币量、计数、年龄、质量、长度、电流 几何平均、调和平均、百分比变差 2.2.1 中心化趋势 均值、中位数和众数\n$Mean-mode = 3 * (mean - median)$\n2.2.2 四分位极差 第一个四分位数，即第25百分位上的数据 $(n-1)/4$ （仅对于第一个四分位数，若是第三个四分位数还要乘3）\n若n为偶数 需要$+0.25\\times (d_{n+1}-d_n)$ （仅对于第一个四分位数，若是第三个四分位数就是0.75）\n2.4.4 数值属性的近邻性度量 两个 $p$ 维变量 $x_1 = \\{x_{11}, x_{12}, \\ldots, x_{1p}\\}$ 和 $x_2 = \\{x_{21}, x_{22}, \\ldots, x_{2p}\\}$ 间的闵可夫斯基距离定义为：\n$$ d(i,j) = \\sqrt[q]{|x_{i1} - x_{j1}|^q + |x_{i2} - x_{j2}|^q + \\cdots + |x_{ip} - x_{jp}|^q} $$当 $q=1$ 时，表示曼哈顿距离：\n$$ d(i,j) = |x_{i1} - x_{j1}| + |x_{i2} - x_{j2}| + \\cdots + |x_{ip} - x_{jp}| $$当 $q=2$ 时，表示欧式距离：\n$$ d(i,j) = \\sqrt{|x_{i1} - x_{j1}|^2 + |x_{i2} - x_{j2}|^2 + \\cdots + |x_{ip} - x_{jp}|^2} $$当 $q \\to \\infty$ 时，表示切比雪夫距离：\n$$ d(i,j) = \\lim_{q \\to \\infty} \\left( \\sum_{k=1}^p |x_{ik} - x_{jk}|^q \\right)^{\\frac{1}{q}} = \\max_{1 \\le k \\le p} |x_{ik} - x_{jk}| $$ n欧几里得和曼哈顿距离满足以下数学性质\n正定性：距离是一个非负数 d(i,j)\u0026gt;0, 如果i≠j d(i,i)=0\n对称性：d(i,j)=d(j,i)\n三角不等式\n2.4.8 余弦相似度 余弦相似度可以用来比较文档的相似性\n$$ s(x, y) = \\frac{x^T y}{\\|x\\|_2 \\|y\\|_2} \\quad x = [1, 1, 0, 0] \\quad y = [0, 1, 1, 0] $$$$ s(x, y) = \\frac{0 + 1 + 0 + 0}{\\sqrt{2} \\sqrt{2}} = 0.5 $$3.2 数据预处理 数据清理\n填写缺失值，平滑噪声数据、识别或删除离群点，并解决不一致问题 数据集成\n整个多个数据库，多维数据集或文件 数据缩减\n降维、数据压缩、Numerosity Reduction 数据转换和数据离散化\n正常化 生成概念层次结构 3.2.1 如何处理缺失数据 忽略元组 当类标号缺失时，监督学习 当每个属性缺失值比例较大时 手动填写缺失值，工作量较大 自动填写 使用一个全局的常数 使用属性的平均值填充空缺值 属性全局平均值 同一类数据对象的属性平均值 最有可能的值：基于贝叶斯公式或决策树推理，回归、最近邻策略 3.2.4 相关分析 3.2.7 数据规约策略 为什么进行数据规约 data reduction?\n由于数据仓库可以存储TB级别的数据，因此在一个完整的数据集上运行时，复杂的数据分析可能需要很长的时间。\n通常，在数据预处理时需要对数据进行规约\n用较小的数据替换原数据\n常见数据规约策略\n降维\n降数据\n数据压缩\n3.2.8 特征降维 PCA (Principal Component Analysis) 主成分分析方法 非负矩阵分解 (NMF) 线性判别分析 (Linear Discriminant Analysis ,LDA) 特征选择\n从原始特征集合中选择一个代表性的特征子集\n单特征重要性评估\n基于模型的特征重要性评估\n3.2.11 规范化 最小-最大规范化（Min-Max Normalization）\n某属性 $A$ 从区间 $[\\min_A, \\max_A]$ 规范化到 $[new_{\\min_A}, new_{\\max_A}]$ $$ v' = \\frac{v - \\min_A}{\\max_A - \\min_A} (new_{\\max_A} - new_{\\min_A}) + new_{\\min_A} $$ 例子：将收入从 $12000$ 到 $98000$ 区间规范化到 $[0,1]$ 之间，$73600$ 规范后的值为？ Z-分数规范化（Z-score Normalization） $$ v' = \\frac{v - \\mu_A}{\\sigma_A} $$ 例子：属性 $A$ 的均值 $\\mu_A = 54000$，标准差 $\\sigma_A = 16000$，$73600$ 规范后的值为？ 小数定标规范化\n移动小数点的位置，移动位数依赖于属性 $A$ 的最大值，公式如下 $$ v' = \\frac{v}{10^j} $$ $j$ 为使 $\\max(|v'|) \u003c 1$ 的最小整数 例如：一组数据的最小值 $12000$，最大值为 $98000$，$j$ 值为 $5$ $$ [12000, 98000] \\rightarrow [0.12, 0.98] $$ 4.3.2 决策树构建 决策树(Decision Tree) ：从训练数据中学习得出一个树状结构的模型，它是一种以树状结构(包括二叉树和多叉树)形式来表达的预测分析模型 决策树是一种有监督学习的算法、属于判别模型 决策树又称为判定树，是数据挖掘技术中一种重要的分类与回归方法 决策树有两种:分类树和回归树 决策树学习通常包含3个步骤: 特征选择,决策树的生成,决策树的剪枝 常用的方法：ID3, C4.5, CART 4.5 KNN算法 k近邻法(k-Nearest Neighbor, kNN)是一种比较成熟也是最简单的机器学习算法，可以用于基本的分类与回归方法。 算法的主要思路: 如果一个样本在特征空间中与𝑘个实例最为相似(即特征空间中最邻近)，那么这𝑘 个实例中大多数属于哪个类别，则该样本也属于这个类别。 𝑘近邻算法三要素: 𝑘值选择，距离度量，决策规则 KNN与K-Means的区别 K-NN是监督学习中的一种分类算法，类别是已知的，通过对已知分类的数据进行训练和学习，找到这些不同类的特征，再对未分类的数据进行分类。\nK-Means是非监督学习中一种聚类算法，事先不知道数据会分为几类，通过聚类分析将数据聚合成几个群体。聚类不需要对数据进行训练和学习。\n有监督学习和无监督学习 有监督学习是一种机器学习方法，其中使用标记数据进行训练。每个输入数据都有对应的输出标签，模型通过学习这些输入与输出之间的关系来进行预测。\n特点：\n需要大量的标记数据。\n训练过程有明确的目标，模型可以通过反馈不断调整。\n常见算法包括线性回归、逻辑回归、支持向量机（SVM）、决策树等。\n应用场景：\n适用于分类和回归问题，例如图像识别、语音识别和金融预测等。 无监督学习是一种机器学习方法，其中使用未标记的数据进行训练。模型从输入数据中自动发现模式和结构，而不依赖于任何标签。\n特点： 不需要标记数据，适合处理大量未标记的数据。 训练过程没有明确的目标，模型通过数据的内在结构进行学习。 常见算法包括聚类（如K均值）、关联规则学习（如Apriori算法）等。 应用场景： 适用于数据聚类、市场细分、异常检测等。 5.3 基于密度的聚类方法 DBSCAN算法描述\n输入：包含 $n$ 个对象的数据库，半径 $\\varepsilon$ (Eps)，最少数目 MinPts\n输出：所有生成的簇，达到密度要求\nRepeat\n从数据中抽取一个未处理过的点 If 抽出的点是核心点 Then 找出所有从该点密度可达的对象，形成一个簇 Else 抽出的点是边缘点（非核心对象），跳出本次循环，寻找下一点 EndIf Until 所有点被处理\n核心对象:如果对象的ε-领域至少包含最小数目MinPts个对象，则称该对象为核心对象\n边界点(Border point)的\u0026quot;Eps\u0026rdquo;(ε) 领域有少于MinPts个对象，但它的领域中有核心对象\n6.1 混淆矩阵 混淆矩阵(Confusion Matrix)\n实际类 \\ 预测类 Class=Yes Class=No Class=Yes a (TP) b (FN) Class=No c (FP) d (TN) $a+d$ 表示所有样本中被正确分类的样本数量 $b+c$ 表示所有样本中被错误分类的样本数量 $a+b+c+d$ 表示样本总数 准确率 $$ Accuracy = \\frac{a+d}{a+b+c+d} = \\frac{TP+TN}{TP+TN+FP+FN} $$ 召回率（查全率，recall）\n$$ recall = \\frac{TP}{TP+FN} $$ 表示样本中的正例被正确预测的比例，即有多少正例样本被预测正确了 精确率（查准率，precision）\n$$ precision = \\frac{TP}{TP+FP} $$ 表示预测的正例中被正确预测的比例，即预测为正例样本中有多少是真的正例 6.5 过拟合和欠拟合 导致过拟合的原因：\n噪声：训练集中存在大量噪声数据\n缺乏代表性样本：训练集规模较小，训练模型过于复杂\n7.1 集成学习的优势 能够有效降低预测误差\n假设一个集成分类器包含3个单分类器，其中每个分类器的错误率为40%. C表示预测正确，I表示预测错误，Probability表示最终预测结果概率，总的组合数为23=8种\n模型的误差率为：0.096+0.096+0.096+0.064=35.2%\u0026lt;40%\n设模型数量为 $m$，模型的错误率为 $r$\n通用的误差计算公式为： $$ p(error) = \\sum_{i=(m+1)/2}^{m} C_{m}^{i} r^{i}(1-r)^{m-i} $$ $m$ 个模型中有超一半分类错误时→最终结果错误，$i$ 从 $(m+1)/2$ 到 $m$ 随机从 $m$ 中选择 $i$ 个，其余 $m-i$ 个分类正确 下图为 $r=0.4$ 时误差率和模型规模的关系图 ","date":"2025-11-23T21:42:00+08:00","permalink":"/p/%E6%95%B0%E6%8D%AE%E6%8C%96%E6%8E%98/","title":"数据挖掘"},{"content":"注：2026年1月后，托福进行了改革，旧的题型不再具有参考价值，故删减部分内容。\n考试时长大幅缩短：总时长约1.5–2小时，流程更紧凑高效。\n自适应测试：Reading和Listening采用多阶段自适应设计，根据考生前一阶段表现实时调整后续难度，测试更精准。\n全新评分体系：改为1–6分制（0.5为增量），总分取四部分平均，直接对应CEFR语言能力框架（如5分≈C1）。过渡期2年内，成绩单同时显示可比的0–120总分，便于新旧对比。\n内容与题型全面更新：四大板块均引入更实用、贴近学术与日常生活的新任务类型，减少冷门偏题，强调真实沟通能力。\nTOEFL Reading 模块数量：2个模块（Module 1 + Module 2）\n总题量：约45–50题（48题左右）\n总时长：约30分钟（整个Reading部分）\n自适应方式：多阶段自适应（MST）\n模块一（Routing Module）：所有考生难度基本相同（路由模块），题量较多。 模块二：根据模块一的表现路由到较高难度或较低难度版本，题量较少，进入较难的模块二则代表高分数。 模块 常见题量 建议时间分配 说明 模块一 25–30题 18–22分钟 决定你最终能拿到的分数上限 模块二 15–20题 8–12分钟 根据模块一表现分流，题量减少 总计 45–50题 约30分钟 会同时存在计分题和不计分题，但考生无法得知 三种题型在阅读中通常按以下顺序出现：\nComplete the Words（语境填词）——占比最高 多个短段落（每个段落约10个空） 做题速度相对快，熟悉后每题平均15–25秒 Read in Daily Life（日常阅读） 短文本（邮件、通知、聊天记录等） 每篇题量少（通常2–3题） Read an Academic Passage（学术阅读） 学术短文 每篇题量也较少（约5题） 根据做模拟题的情况来看，\u0026ldquo;Complete the Words\u0026rdquo; 部分是最难的，反而学术阅读比之前简单一些。\nTOEFL (before 2026 Jan.) Reading TPO6 1.Which of the sentences below best expresses the essential information in the highlighted sentence in the passage? Incorrect choices change the meaning in important ways or leave out essential information.\nOnly the last of these was suited at all to the continuous operating of machines, and although waterpower abounded in Lancashire and Scotland and ran grain mills as well as textile mills, it had one great disadvantage: streams flowed where nature intended them to, and water-driven factories had to be located on their banks, whether or not the location was desirable for other reasons.\n第一句确定了主系表的结构，主语\u0026quot;last of these\u0026quot;对应了前一句话的\u0026quot;running water\u0026quot;，省略掉 \u0026ldquo;although\u0026quot;和\u0026quot;whether\u0026quot;引导的从句，即得出核心大意。\n4.The word \u0026ldquo;exploited\u0026rdquo; in the passage is closest in meaning to ()\nA. utilized\nB. recognized\nC. examined\nD. fully understood\nThe source had long been known but not exploited.\n\u0026ldquo;exploit\u0026quot;译为“利用;开发;发挥”，\u0026ldquo;utilize\u0026quot;译为“使用;利用;应用”\n结合上下文来看，本段将介绍一种新的能源。第二段之后便开始介绍蒸汽机的起源，\u0026ldquo;early in the century\u0026quot;也对应了\u0026quot;had long been known\u0026rdquo;\n7.The phrase \u0026ldquo;grew accustomed to\u0026rdquo; in the passage is closest in meaning to ()\nA. began to prefer\nB. wanted to have\nC. became used to\nD. insisted on\nCoal gas rivaled smoky oil lamps and flickering candles, and early in the new century, well-to-do Londoners grew accustomed to gaslit houses and even streets.\n\u0026ldquo;accustomed\u0026quot;译为“习惯的;适应了的”\n原文说煤气胜过了油灯和蜡烛，因此易在A和C间犹豫，网站的解释是”前面已经提到了胜过，不必在用prefer比较“，AI的解释是”A选项有明显的情感倾向“，我认为关键在连词\u0026quot;and\u0026rdquo;，即”同时/并且，Londoners grew accustomed to gaslit houses \u0026hellip; “，若连词换成\u0026quot;so\u0026quot;可能结果不同。\n10.The Industrial Revolution would not have been possible without a new source of power that was efficient, movable, and continuously available.An introductory sentence for a brief summary of the passage is provided below \u0026hellip;\n第一段：在蒸汽机等发明前只有三种\u0026quot;sources of power\u0026rdquo;，其中\u0026quot;running water\u0026quot;适合机器持续运转但仍有“季节性”的缺点。\n第二段：”大气压机“的原理，及浪费能源和效率低的不足之处，之后是对它原理的改进。\n第三段：瓦特蒸汽机，导致了\u0026quot;possible deeper and deeper mining\u0026rdquo;, \u0026ldquo;new form of nighttime illumination\u0026rdquo;, \u0026ldquo;motive force of the Industrial Revolution\u0026rdquo;\n第四段：蒸汽机在19世纪的扩大使用，主要讨论了交通方面，水运的局限以及解决运输问题的火车的发明。\n本题的三个错误选项，我认为是与文章主题的偏差问题，即并没反映题干\nA: This \u0026ldquo;atmospheric engine,\u0026rdquo; invented by Thomas Savery and vastly improved by his partner, Thomas Newcomen \u0026hellip;\nD,E: 和\u0026quot;The Industrial Revolution would not have been possible without a new source of power that was efficient, movable, and continuously available.\u0026ldquo;关系不大，即和蒸汽机关系不大，D依据第三段，E依据第四段第一句。\n14.The word \u0026ldquo;meticulously\u0026rdquo; in the passage is closest in meaning to ()\nA. carefully\nB. quickly\nC. frequently\nD. obviously\nSmith used mail coaches to travel as much as 10,000 miles per year. In 1815 he published the first modern geological map, \u0026ldquo;A Map of the Strata of England and Wales with a Part of Scotland,\u0026rdquo; a map so meticulously researched that it can still be used today.\n\u0026ldquo;meticulously\u0026quot;译为”细致的,小心翼翼的“\n从\u0026quot;it can still be used today\u0026quot;明显能看出和A近意\n15.Which of the sentences below best expresses the essential information in the highlighted sentence in the passage? Incorrect choices change the meaning in important ways or leave out essential information.\nBut as more and more accumulations of strata were cataloged in more and more places, it became clear that the sequences of rocks sometimes differed from region to region and that no rock type was ever going to become a reliable time marker throughout the world.\n忽略掉\u0026quot;but\u0026quot;引导的从句，主语为\u0026quot;it\u0026quot;替代的\u0026quot;that\u0026quot;引导的两个从句，\u0026ldquo;that\u0026hellip;and that\u0026hellip;\u0026rdquo;\n17.The word \u0026ldquo;endured\u0026rdquo; in the passage is closest in meaning to ()\nA. vanished\nB. developed\nC. varied\nD. survived\nSome fossils endured through so many millions of years that they appear in many strata, but others occur only in a few strata, and a few species had their births and extinctions within one particular stratum.\n\u0026ldquo;endure\u0026quot;译为“忍耐,忍受”，根据原句的\u0026quot;so\u0026hellip;that\u0026hellip;\u0026ldquo;易得D，A的\u0026quot;vanish\u0026quot;译为“消失,灭绝”\n20.William Smith’s contributions to geology have increased our knowledge of the Earth’s history. An introductory sentence for a brief summary of the passage is provided below \u0026hellip;\nA选项依据在第一段，Smith出身于\u0026quot;poor family\u0026rdquo;\nD选项\u0026quot;Smith was named “the father of English geology” for his maps rather than for his other contributions to the field.\u0026rdquo; 和第三段第一句\u0026quot;not only\u0026hellip;but also\u0026hellip;\u0026ldquo;矛盾。\nE选项依据在第四段的第二句，只体现出了\u0026quot;Particularly in the younger strata, the rocks were often so similar that he had trouble distinguishing the strata \u0026hellip;\u0026rdquo; 而且\u0026quot;Cuvier\u0026quot;只出现在最后一段。\nTPO6应该是国庆假期后刚开始做的模拟，做了两遍，再回看细节题都很简单，只有第一篇的六选三有点说法了。对于六选三这种题，大部分还是凭感觉或是找明显错误，第二篇是错误非常明显的那种。\nTPO7 2.Which of the following is NOT mentioned in paragraph 1 as a change that occurred in the fauna of the Mediterranean?\nA. Most invertebrate species disappeared during a wave of extinctions.\nB. A few hardy species wiped out many of the Mediterranean’s invertebrates.\nC. Some invertebrates migrated to the Atlantic Ocean.\nD. New species of fauna populated the Mediterranean when the old migrants returned.\nA对应\u0026quot;Most of the older organisms were nearly wiped out\u0026hellip;\u0026rdquo;\nB偷换意思，原文为\u0026quot;a few hardy species survived\u0026quot;而不是\u0026quot;hardy species\u0026quot;\u0026ldquo;wipe out\u0026quot;\u0026ldquo;invertebrates\u0026rdquo;\nC对应\u0026quot;A few managed to migrate into the Atlantic.\u0026rdquo;\nD对应\u0026quot;Somewhat later, the migrants returned, bringing new species with them.\u0026rdquo;\n3.What does the author imply by saying \u0026ldquo;Not a single pebble was found that might have indicated that the pebbles came from the nearby continent\u0026rdquo;?\nA. The most obvious explanation for the origin of the pebbles was not supported by the evidence.\nB. The geologists did not find as many pebbles as they expected.\nC. The geologists were looking for a particular kind of pebble.\nD. The different pebbles could not have come from only one source.\n原句的前面讲了这个样本的组成，后面则先是指出样本中的成分来自沙漠，之后对\u0026quot;Mediterranean(地中海)\u0026ldquo;的挖掘，说明了地中海以前不是海。\n而再看选项，需要对应到前一段的最后句\u0026quot;Were they salt domes such as are common along the United States Gulf Coast, and if so, why should there have been so much solid crystalline salt beneath the floor of the Mediterranean?\u0026quot;“它们是像美国墨西哥湾沿岸常见的盐丘吗？如果是的话，为什么地中海海底会有如此多的固体结晶盐？”\n这也就解释了正确答案A的\u0026quot;The most obvious explanation for the origin\u0026quot;的来源，那么我选的D选项实则没有道理，\u0026ldquo;not have come from one source\u0026quot;并不能证明它来自多个地点，也可能是一个不同的地点。\n4.Select the TWO answer choices from paragraph 3 that identify materials discovered in the deepest part of the Mediterranean basin. To receive credit, you must select TWO answers.\nA. Volcanic rock fragments\nB. Thin silt layers\nC. Soft, deep-sea mud\nD. Crystalline salt\n对应第三段最后两句\u0026quot;As they drilled into the central and deepest part of the Mediterranean basin, the scientists took solid, shiny, crystalline salt from the core barrel. Interbedded with the salt were thin layers of what appeared to be windblown silt.\u0026rdquo;\n而不能被本段的第二句欺骗，因为题干的\u0026quot;the deepest part of the Mediterranean basin\u0026rdquo;\n7.Which of the sentences below best expresses the essential information in the highlighted sentence in the passage? Incorrect choices change the meaning in important ways or leave out essential information.\nAs a result of crustal adjustments and faulting, the Strait of Gibraltar, where the Mediterranean now connects to the Atlantic, opened, and water cascaded spectacularly back into the Mediterranean.\n11.Which of the sentences below best expresses the essential information in the highlighted sentence in the passage? Incorrect choices change the meaning in important ways or leave out essential information.\nLike the stones of a Roman wall, which were held together both by the regularity of the design and by that peculiarly powerful Roman cement, so the various parts of the Roman realm were bonded into a massive, monolithic entity by physical, organizational, and psychological controls.\n用了\u0026quot;just as A, so B\u0026quot;的类比句型，核心在于罗马怎么样，而非前面用于类比的罗马城墙如何。\n15.Paragraph 3 suggests which of the following about the people of Latium?\nA. Their economy was based on trade relations with other settlements.\nB. They held different values than the people of Rome.\nC. Agriculture played a significant role in their society.\nD. They possessed unusual knowledge of animal instincts.\n本段第三句\u0026rdquo;\u0026hellip;that created the habits and skills of landed settlement, landed property, landed economy, landed administration, and a land-based society. \u0026quot;\n18.Which of the following statements about leading Roman soldiers and statesmen is supported by paragraphs 5 and 6?\nA. They could read and write the Greek language.\nB. They frequently wrote poetry and plays.\nC. They focused their writing on military matters.\nD. They wrote according to the philosophical laws of the Greeks.\n\u0026ldquo;It was absolutely accepted that an educated Roman should be fluent in Greek.\u0026rdquo;\n误选D的时候，觉得A的\u0026quot;educated Roman\u0026quot;和题干的\u0026quot;Roman soldiers\u0026quot;相悖，但剩下的选项都没有提及\nTPO7错了很多细节题，反而到6选3没有错，回看一下一般都是将原文信息换个有点相近的，但剩下的选项则是没有出现，就如第二篇的细节题。第一篇的细节题要把握上下文，如第3题是跨段落的问题，且意思略晦涩，第一次读未能注意\u0026quot;United States Gulf Coast\u0026quot;，以及第4题，需通读整段。\nTPO11 6.According to paragraph 3, why were certain areas of a stone statue left uncarved?\nA. To prevent damage by providing physical stability\nB. To emphasize that the material was as important as the figure itself\nC. To emphasize that the figure was not meant to be a real human being\nD. To provide another artist with the chance to finish the carving\n\u0026ldquo;left uncarved\u0026quot;对应到本段的第二句话末尾\u0026rdquo;\u0026hellip;was not normally cut away.\u0026ldquo;问题出在\u0026quot;figures\u0026quot;在这里翻译为了“雕像”，导致我忽略了这个答案句\u0026rdquo;\u0026hellip;this protected the figures against breakage\u0026hellip;\u0026ldquo;后面有一生词\u0026quot;pillar\u0026quot;译为\u0026quot;起支撑作用的背柱\u0026rdquo;。这是古埃及、古希腊等文明雕像中常见的结构，一根连接在雕像背部的石柱。\n9.Look at the four squares that indicate where the following sentence could be added to the passage.\nIn fact, it is the action and not the figure itself that is important.\nApart from statues representing deities, kings, and named members of the elite that can be called formal, there is another group of three-dimensional representations that depicts generic figures, frequently servants, from the nonelite population. [ ] The function of these is quite different. [ ] Many are made to be put in the tombs of the elite in order to serve the tomb owners in the afterlife.[ ] Unlike formal statues that are limited to static poses of standing, sitting, and kneeling, these figures depict a wide range of actions, such as grinding grain, baking bread, producing pots, and making music, and they are shown in appropriate poses, bending and squatting as they carry out their tasks.[ ]\n答案是放在最后一空，\u0026ldquo;action\u0026quot;对应了最后一句的\u0026quot;actions,such as\u0026hellip;\u0026rdquo;\n其次是\u0026quot;In fact\u0026quot;要表达转折，但是第三空的前一句和下面的句子并无转折意\n16.The experiment described in paragraph 5 caused Kramer to conclude that birds possess a biological clock because\nA. when birds navigate they are able to compensate for the changing position of the Sun in the sky\nB. birds’ innate bearings keep them oriented in a direction that is within 15 degrees of the Sun’s direction\nC. birds’ migration is triggered by natural environmental cues, such as the position of the Sun\nD. birds shift their direction at a rate of 15 degrees per hour whether the Sun is visible or not\n题干对应段落最后一句的\u0026quot;This meant that some sort of biological clock was operating–and a very precise clock at that.\u0026ldquo;而前一句\u0026quot;In other words, they were able to compensate for the Sun’s movement. \u0026quot;\n难点在于这段话略晦涩难懂，是通过人造太阳和天然太阳的对比，以及鸟类15°的\u0026quot;compensate\u0026rdquo;(补偿)飞行，也突出了其生物钟的存在\n19.Look at the four squares that indicate where the following sentence could be added to the passage.\nHe arranged the feed boxes at various positions on a compass.\nSo, in another set of experiments, Kramer put identical food boxes around the cage, with food in only one of the boxes.[ ] The boxes were stationary, and the one containing food was always at the same point of the compass.[ ] However, its position with respect to the surroundings could be changed by revolving either the inner cage containing the birds or the outer walls, which served as the background. [ ] As long as the birds could see the Sun, no matter how their surroundings were altered, they went directly to the correct food box.[ ] Whether the box appeared in front of the right wall or the left wall, they showed no signs of confusion.\n答案是放第一个空，可能这题不能想的太复杂，把人名和\u0026quot;He\u0026quot;一对应即可，\u0026quot; arranged the feed boxes\u0026quot;对应了后面开始将\u0026quot;boxes\u0026quot;的属性\u0026hellip;\nTPO11算是罕见的错了2个填句子题，问题出在词汇量的问题，读着读着生词量多了就昏了，导致内在逻辑也没能理解，如第一篇的雕像和第二篇的鸟。6选3因为pad的浏览器问题没有做，就当跳过了吧。\nTPO12 3.Which of the sentences below best expresses the essential information in the highlighted sentence in the passage? Incorrect choices change the meaning in important ways or leave out essential information.\nMost engravings, for example, are best lit from the left, as befits the work of right-handed artists, who generally prefer to have the light source on the left so that the shadow of their hand does not fall on the tip of the engraving tool or brush.\n这句话的重点在于“左边的光源”，\u0026ldquo;befits the work of right-handed artists\u0026quot;\u0026ldquo;so that \u0026hellip;\u0026ldquo;表明了为什么右撇子艺术家用左边光源。\n4.All of the following are mentioned in paragraphs 1 and 2 as evidence of right-handedness in art and artists EXCEPT\nA. the ideal source of lighting for most engravings\nB. the fact that a left hand stenciled palm upward might look like a right hand\nC. the prevalence of outlines of left hands\nD. figures in prehistoric art holding objects with the right hand\nA,D选项在第二段分别有体现，C若是知道\u0026quot;stencil\u0026quot;和\u0026quot;outline\u0026quot;的近义关系也不容易选错，\u0026ldquo;stencil\u0026quot;是印记，\u0026ldquo;outline\u0026quot;是外形、轮廓，在文章中都表示“手印”。\nB选项比较神奇，第一段第4句确实有相同的内容\u0026quot;One also has to make the assumption that hands were stenciled palm downward—a left hand stenciled palm upward might of course look as if it were a right hand.\u0026ldquo;但重点在\u0026quot;make a assumption that\u0026hellip;\u0026ldquo;是”假定所有手都是掌心向下印的“，破折号后的内容并非\u0026quot;fact\u0026quot;而是解释了前面为什么要\u0026ldquo;make a assumption that\u0026hellip;\u0026quot;。如果不结合文章，那么手掌向上的左手(即用手背)产生的印记易被误以为成右手，就不会是题干的\u0026quot;evidence of right-handedness\u0026rdquo;。\n6.In paragraph 5, why does the author mention the Ice Age rope found in the French cave of Lascaux?\nA. As an example of an item on which the marks of wear imply that it was used by a right handed person\nB. Because tressing is an activity that is easier for a right-handed person than for a left-handed person\nC. Because the cave of Lascaux is the site where researchers have found several prehistoric tools made for right-handed people\nD. As an example of an item whose construction shows that it was made by a right-handed person\nTools themselves can be revealing. Long-handed Neolithic spoons of yew wood preserved in Alpine villages dating to 3000 B.C. have survived; the signs of rubbing on their left side indicate that their users were right-handed. The late Ice Age rope found in the French cave of Lascaux consists of fibers spiraling to the right, and was therefore tressed by a right-hander.\n直白的词汇量问题，一句话三个生词\u0026quot;fibers\u0026quot;\u0026ldquo;spiraling\u0026quot;\u0026ldquo;tressed\u0026quot;分别表示“纤维”“螺旋”“缕”，这句话也就是说“绳子(纤维)是右螺旋编织的”，即D选项。\nA选项的内容是对前面的例子的概括，而不是题干这个。此外A选项的核心是指\u0026quot;marks imply that it was used by \u0026hellip;\u0026ldquo;其中\u0026quot;used\u0026quot;就和D的\u0026quot;made\u0026quot;形成对比了。\nTPO12的这篇大致就是词汇量不足，比如“手掌”的\u0026quot;palm\u0026rdquo;，以及第4题还是挺有逻辑的。\nTPO15 4.The word \u0026ldquo;bulk\u0026rdquo; in the passage is closest in meaning to\nA. strength\nB. effort\nC. activity\nD. mass\nAn adult leatherback is twice the size of the biggest cheloniid sea turtles and will therefore take longer to cool off. Maintaining a high body temperature through sheer bulk is called gigantothermy. It works for elephants, for whales, and, perhaps, it worked for many of the larger dinosaurs.\n首先，\u0026ldquo;sheer\u0026quot;的意思在这里是“完全的，彻底的”，其它意思还有“陡峭的”，其实翻译后看四个选项都不太对，虽说得先了解\u0026quot;mass\u0026quot;是可以做名词表示“大量，大批”的，那么结合上下文，不难得出。\n15.Which of the sentences below best expresses the essential information in the highlighted sentence in the passage? Incorrect choices change the meaning in important ways or leave out essential information.\nAmerican paleontologists David Raup and John Sepkoski, who have studied extinction rates in a number of fossil groups, suggest that episodes of increased extinction have recurred periodically, approximately every 26 million years since the mid-Cretaceous period.\n核心在于\u0026quot;every 26 million years\u0026quot;除了A选项外都错误表达了时间（但忘了我为什么会错）。\nTPO56 R2Q7.According to paragraph 4, what was the response to criticisms of representative government in England?\nA. Efforts were made to make the system better\nB. Criticisms of the system were completely ignored.\nC. An explanation was created to defend the system\nD. Critics of the system were widely viewed as unpatriotic.\n\u0026hellip; Few could meet the test. A number of English people thought the situation and said so. But nothing was done to improve it; in fact, a theory was devised to justify it. A member of the House of Commons, it was said, represented not the people who chose him but the whole country, and he was not responsible for any particular constituency. Not all Englishmen could vote for representatives, but all were virtually represented by every member of the Commons.\n将原文翻译，“但是，没有采取任何措施来改进它；事实上，有人设计出一种理论来为其辩护。”和选项C是一个意思，\u0026ldquo;justify\u0026quot;\u0026ldquo;defend\u0026quot;都是辩护的意思。错选B时看到前半句就选了，而忽略了重要的\u0026quot;In fact\u0026rdquo;。\nTPO57 R1Q2.The word \u0026ldquo;annihilated\u0026rdquo; in the passage is closest in meaning to\nA. unaffected\nB. strengthened\nC. quickly weakened\nD. completely eliminated\n应选\u0026quot;D\u0026rdquo;“彻底消除”，回到原文，\u0026ldquo;In 1912, he compared how effective the sprays were in different parts \u0026hellip; \u0026ldquo;要在不同的地区进行杀虫剂的对比，后文指出在\u0026quot;Washington\u0026quot;完全消除，而在\u0026quot;Clarkston\u0026quot;就不行。紧接着\u0026quot;On the other hand, the Clarkston scales were annihilated by a different pesticide made from fuel oil, just as the insects in other parts of Washington were.\u0026ldquo;显然是D。\nR1Q6.Why does the author mention \u0026ldquo;draining standing water or breeding resistant strains of crops\u0026rdquo; ?\nA. To contrast the cost of DDT with the cost of old-fashioned ways of controlling pests\nB. To present evidence that European agricultural activity had declined before 1941\nC. To provide examples of farming practices that were abandoned due to the success of DDT\nD. To indicate that certain farming practices were never explored because DDT was so effective\nBetween 1941 and 1976, 4.5 million tons of DDT were produced DDT was so powerful and cheap that farmers gave up old-fashioned ways of controlling pests, such as draining standing water or breeding resistant strains of crops.\nR2Q4.The word \u0026ldquo;intangible\u0026rdquo; in the passage is closest in meaning to\nA. nonmaterial\nB. independent\nC. powerful\nD. insignificant\nSpallanzani showed that nutrient fluids heated after being sealed in a flask did not develop microbial growth. Needham responded by claiming the \u0026ldquo;vital force\u0026rdquo; necessary for spontaneous generation had been destroyed by the heat and was kept out of the flasks by the seals.\nThis intangible \u0026ldquo;vital force\u0026rdquo; was given all the more credence shortly after Spallanzani\u0026rsquo;s experiment, when Laurent Lavoisier showed the importance of oxygen to life. Spallanzani\u0026rsquo;s observations were criticized on the grounds that there was not enough oxygen in the sealed flasks to support microbial life.\n第一段，根据Needham的说法，这种“生命力”是“自发发生”所必需的。他认为，Spallanzani的实验之所以失败，是因为加热破坏了这种“生命力”，并且密封的瓶塞将其阻挡在了烧瓶之外。从这里我们可以推断，Needham所认为的“生命力”是一种非物质的、看不见摸不着的存在。它不像灰尘或微生物那样是实体，而是一种可以被“阻挡”或“破坏”的“力量”或“原理”。\n第二段，批评者认为，Spallanzani的密封烧瓶中没有足够的氧气来支持微生物生命。这实际上是为“生命力”无法起作用提供了一个科学的、物质上的解释（缺氧），从而间接支持了Needham的“生命力”理论（虽然是以一种新的方式）。\nR2Q8.According to paragraphs 7 and 8, Pasteur\u0026rsquo;s experiment involving flasks with S-shaped necks proved that\nA. air does not cause microorganisms to arise spontaneously from nonliving matter\nB. microorganisms require access to nutrient fluids in order to arise spontaneously\nC. the temperature of liquids has no effect on the presence or absence of microorganisms\nD. microorganisms in the air cannot travel long distances\nPasteur next placed broth in open-ended long-necked flasks and bent the necks into S-shaped curves. The contents of these flasks were then boiled and cooled. The broth in the flasks did not decay and showed no signs of life, even after months. Pasteur’s unique design allowed air to pass into the flask, but the curved neck trapped any airborne microorganisms that might have contaminated the broth.\n最后一句回答了本题“巴斯德这一独特的设计使得空气能够进入烧瓶，而弯曲的瓶颈则截留了所有可能污染肉汤的空气中微生物。”\nTPO58 R2Q3.Which of the following can be inferred about pinyon pines from paragraph 2 ?\nA. Seed predators other than pinyon jays feed on pinyon seeds\nB. Pinyon jays must eat many pinyon seeds to obtain adequate nutrition\nC. Seeds located in the interior regions of pinyon pines are particularly attractive to pinyon jays.\nD. Pinyon pines signal jays to eat only those seeds that are unable to grow.\nAt first glance, the relationship between pinyon pine trees and the pinyon jay does not appear to be one of coevolution. Pinyon pines produce cones and seeds that attract seed predators, especially the pinyon jay.\n暗示 pinyon jay 是最主要或者最典型的种子捕食者，但并没有说它是唯一的种子捕食者\nCones are positioned upward and outward on the tree, so the seeds inside are in plain sight of the jays, essentially inviting them to partake Pinyon seeds are unusually large, and each seed is high in energy.\n并无证据表明attractive\nR2Q4.According to paragraph 2, all the following are characteristics of pinyon pine seeds that make them attractive to seed predators EXCEPT:\nA. The seeds are highly visible\nB. The seeds contain a large amount of energy\nC. The seeds pass unharmed through the predator\u0026rsquo;s alimentary system\nD. The seed coat is thin.\nIn many plants, an indigestible seed coat permits the seed to pass unharmed through the bird’s alimentary system.\n显然C选项对应的不是pinyon pine seeds而是其它植物\nR2Q5.The word \u0026ldquo;providing\u0026rdquo; in the passage is closest in meaning to\nA. Once\nB. since\nC. whenever\nD. if\n首先看providing的原义：\nto give someone something that they need (of a law or decision) to say that something must happen if particular conditions exist 然后结合文章：This is a useful behavior for the jays, providing they can retrieve some of the buried seeds during winter, and good for the trees, as the unretrieved seeds are ready to germinate.\n逻辑上，作者不是说“因为 jay 能在冬天找回种子，所以这对 jay 是好的行为”，而是说“这种行为对 jay 是好的，条件是它们冬天能找回一些”。\nR2Q6.The phrase \u0026ldquo;so many seeds\u0026rdquo; in the passage refers to A. a number of seeds greater than jays can eat immediately B. a number of seeds greater than jays can bury immediately C. the number of seeds produced by one tree D. the number of seeds that jays in a region need to eat\nStill, for the jays to bury any seeds, there must be an abundance of seeds far beyond the jays’ immediate needs. One tree could never produce so many seeds, but if all the pinyons in a region produced heavy seed crops at once, they would indeed \u0026ldquo;flood the market\u0026rdquo; with vastly more pinyon seeds than the local population of jays could consume.\n选项 B：种子数量大于松鸦能立即埋藏的数量 —— 这里逻辑不对，因为“埋藏”本身就发生在种子充裕时，而“so many seeds”指的是充裕到超出当前食用需求，而不是超出它们“能埋”的物理能力。原文强调的是吃不完才埋，不是埋不完。\n20250525 R1Q6.Paragraph 4 suggests which of the following about the possibility of tapping methane hydrate deposits through drilling?\nA. Underwater oil and gas fields might get in the way of drilling. B. Drilling vessels currently in existence cannot reach methane hydrate deposits. C. Hot water or steam that is pumped into deposits will reduce the amount of methane that can be extracted. D. Such drilling would have to be supported by government agencies because oil companies believe it to be too costly. Para.4\nLike conventional natural gas, the layers of methane hydrate could also be tapped by drilling. Several oil companies and government agencies are actively researching the possibility of such an endeavor. Because the deposits are far deeper than most underwater oil and gas fields, special deep water drilling vessels would have to be constructed. Nevertheless, drilling is at least a possible option, although the methane ice is under so much pressure, the challenge is akin to bursting a balloon and trying to capture all the escaping gas. One potential solution would be to expel the methane by pumping hot water or steam into the deposit through one drill hole and extracting the expelled methane through another. But once recovered, the methane would still have to be brought ashore, and this would pose an additional challenge.\n甲烷水合物矿床位于比常规海底油气田 更深 的位置。 必须建造 特殊的深水钻井船 目前的钻井船（most underwater oil and gas fields 所用的船）不能达到 这个深度。 R1Q9.Look at the squares that indicate where the following sentence can be added to the passage.\nMoreover, many methane hydrate deposits are located close to high-population areas, where energy demands greatest.\nAnd it has been found in enormous quantities, off the east coast of the United States near the Carolinas, for example, the United States Geological Survey, a scientific agency that studies natural resources, has discovered two deposits of methane hydrate, each covering about 3,139 square kilometers. ()Together they are estimated to contain over 37 trillion cubic meters of methane gas, or more than 50 times the amount of natural gas consumed in the United States in 2012. **(√)**Is methane hydrate the fuel of the future? ()The sheer volume and richness of methane hydrate deposits make them a strong candidate for development as an energy resource. However, the challenges are enormous.()\nMoreover通常用来连接两个同类性质的论点 显然第一个空后的\u0026quot;together\u0026quot;代指前句内容，不应将句子填这 第二个空前说明了这种资源的储量很大，填入句是指资源的位置好 R2Q2.According to paragraph 1, many scholars believe that iron smelting in Africa first began when the technique was\nA. brought by Phoenicians who settled on the North African coast B. discovered by the Berbers around 1,000 B.C.E C. brought back from Phoenicia by seafaring African merchants D. discovered in Carthage in modern Tunisia around 800 B.C.E Many scholars believe that the secret of iron smelting (the process of extracting the iron from the rock that contains it) came with Phoenician merchants. The Phoenicians living on the shores of the Mediterranean were smelting iron by 1,000 B.C.E. They were a seafaring people whose square-rigged ships sailed along the North African coast, where they established settlements that became colonies.\n虽说文章确实提到\u0026quot;merchant\u0026rdquo;，但我们要注意\u0026quot;Phoenician merchants\u0026quot;和\u0026quot;African merchants\u0026quot;的区别 再结合后文可知，Phoenician确实定居北非 R2Q4.The word \u0026ldquo;distinct\u0026rdquo; in the passage is closest in meaning to\nA. permanent. B. basic. C. clear. D. fair. 在知道\u0026quot;distinct\u0026quot;原义为“明显的、清晰的”的情况下无需分析，但我错记成了“遥远的”（应为\u0026quot;distant\u0026rdquo;） 文中生词，\u0026ldquo;division of labor\u0026rdquo;，在社会学和经济学中，division of labor 指的是社会成员不再“每个人都做同样的事”，而是有了专门的职业划分。 20251206 R1Q2.According to paragraph 2, silk fibers have which of the following functions during the metamorphosis of silk moths?\nA. They provide a source of energy during the process B. They provide protein for the development of wings C. They provide defense from threats. D. They speed up biochemical processes. The cocoon is both a receptacle for this complex biochemical process and a means of protecting the defenseless insect from the environment and predators while it is metamorphosing. The silk fibers render the cocoon less palatable while also making it more difficult for would-be predators to get at the tasty protein-rich prize inside.\nR1Q8.Paragraph 4 suggests which of the following about juvenile hormones?\nA. They lead to aggressive behavior in humans, wolves, and other mammals. B. They are formed during the synthesis of silk. C. They help spiders synthesize silk. D. They do not interfere with silk making by silkworms. It is equally fortuitous that at least some silkworms are even capable of coevolving with humans in a way that led to the animal partnership that we call domestication. In the case of silkworms, it led to silk farming, a practice first developed by the Chinese, who established a lucrative transcontinental silk trade near the end of the first millennium B.C.E. Domestication is always a two-way street in which another species must have the inherent capacity to tolerate and cooperate with humans. Silkworms\u0026rsquo; ability to tolerate domestication becomes evident if we compare them to spiders. Spiders actually produce a superior silken fiber, yet modern attempts at domesticating spiders have largely failed because they are very territorial creatures and will often eat one another if forced to live in close proximity. Indeed, spiders\u0026rsquo; need to make silk as adults—rather than just during a larval stage as with silkworms—may explain why they do not coexist well with other members of their own species. As is the case with mammals like wolves and humans, in many insects social behaviors are linked to juvenile hormones that make them more cooperative than aggressive.However, in spiders these juvenile hormones just happen to interfere with the biochemical synthesis of silk. So for spiders to enjoy the many adaptive advantages that come from a lifetime of silk making, it appears they had to give up the many potential benefits of social cooperation.\n选项A由倒数第三局解释，选项C由倒数第二句的\u0026quot;interfere with\u0026quot;解释，意思是“干扰\u0026hellip;\u0026hellip;”而我却将其当作选C的论证了。\n既然家蚕既能表现出合作行为（意味着它们体内有起作用的幼年激素），又能成功大量造丝（silk farming），那么根据对比推论，家蚕体内的这种激素肯定没有干扰到它们的造丝过程。与倒数第二句蜘蛛被干扰造丝形成对比。\nR2Q2.According to paragraph 1, radiocarbon dating is now the preferred method for determining the age of ancient biological specimens for all of the following reasons EXCEPT:\nA. It can be used to date specimens that are tens of thousands of years old. B. It is considered scientifically reliable. C. It can be used to date all materials that have radiocarbon. D. It can be used to determine whether organic acids have seeped into materials being tested. Because all living things contain carbon, artifacts and organic remains from archaeological sites can often be dated by comparing the proportion of carbon-14 (radiocarbon) remaining in them (or in the location where they were found) to the proportion of non-radiocarbon (carbon-12 and carbon-13) in them, a method called radiocarbon dating. This technique is based on the fact that the amount of carbon-12 and carbon-13 stays constant in organisms, while the amount of carbon-14 declines at a steady rate once the organism dies. When an animal or plant dies, it stops exchanging gases with the atmosphere and its carbon-14 begins to decrease. Thus the amount of carbon-14 remaining indicates how long ago an organism died. Radiocarbon is the preferred method for dating the sites of the earliest Americans. Its range reaches back 50,000 years, which is the period of interest, and its reliability is well attested since variations in atmospheric carbon levels over time have been well studied and can readily be adjusted for in age calculations. Moreover, the technique can be applied to any material that incorporates radiocarbon, such as bone, wood, charcoal (burned wood), and even soils, the last by virtue of the organic acids that seep into them.\n\u0026ldquo;by virtue of\u0026quot;的意思是“由于”，即把“酸的渗入”当作一个前提条件，使得土壤具备了被检测的资格；而选项 D 把碳定年当作了检测手段，去检测酸是否存在。这两者逻辑关系是颠倒的。碳定年的目的是测年份，而不是测酸的渗入过程。\nListening TPO6 C1Q3.What does the woman imply about the small print on the career fair posters and flyers?\nA. The information in the small print was incomplete.\nB. The print was smaller than she expected it to be.\nC. The information the small print contains will be updated.\nD. The information in the small print will be presented in a more noticeable way.\n对应原文\u0026quot;I mean, they all say where and when the fair is… just not who should attend.\nFEMALE EMPLOYEE: Actually, they do. But it’s in the small print.\nWe should probably make that part easier to read, shouldn\u0026rsquo;t we?\nI\u0026rsquo;ll make a note of that right now.\u0026rdquo;\n学生未能注意到\u0026quot;fair posters and flyers\u0026quot;上的小字，而不是没有相关的信息，所以不能选C而应该选D\nC1Q4.What does the woman say is a good way for the student to prepare for speaking to companies’ representatives? Click on 2 answers.\nA. Take some business classes\nB. Familiarize himself with certain businesses beforehand\nC. Have questions ready to ask the representatives\nD. Talk to people who work for accounting firms\n在学生提到\u0026quot;I was wondering if there’s anything you’d recommend that I do to prepare.\u0026ldquo;后，老师问完\u0026quot;I was wondering if there’s anything you’d recommend that I do to prepare.\u0026ldquo;后的\u0026quot;Well, I suggest that you get on the computer and learn more about the accounting companies, in particular, that will be attending.\nYou can learn a lot about companies from their Internet Web sites.\nThen prepare a list of questions.\nMALE STUDENT: Questions… hmm. So in a way I’ll be interviewing them?\nFEMALE EMPLOYEE: That\u0026rsquo;s one way of looking at it.\u0026rdquo;\n也就分别对应了B和C选项内容。\nC1Q5.Why does the student say this: \u0026ldquo;It looks like I\u0026rsquo;ve got some works to do.\u0026rdquo;\nA. To acknowledge that he cannot go to this year’s career fair\nB. To acknowledge the amount of preparation he will have\nC. To indicate that he has school work he must complete before the career fair\nD. To indicate that he needs to go to his job now\n原句在材料末尾，是在学生老师的建议并弄清楚了应该着手准备的事项后说的\n结合经验，即是C，最贴合原文主题的选项\nL1Q1.What is the main purpose of the talk?\nA. To show what happens after an economy has experienced a boom-and-bust cycle\nB. To illustrate the conditions needed to produce a boom-and-bust cycle\nC. To demonstrate how boom-and-bust cycles have changed over time\nD. To explain why the boom-and-bust cycle is not a frequent historical occurrence\n本材料通过“郁金香(tulip)”的例子解释了\u0026rdquo;boom and bust\u0026quot;，A和D明显有错，而C的观点只是被提到，但材料具体地只讲了一次\u0026quot;boom and bust\u0026quot;的例子。\nL1Q6.The professor mentions the practice of trading promissory notes in the Netherlands in the 1630s. What does this practice explain? Click on 2 answers.\nA. Why tulips replaced gold as a form of currency\nB. Why buyers were no longer interested in owning actual tulips\nC. Why borrowing in the Netherlands increased on a significant scale\nD. Why the middle class in the Netherlands expanded in size\n材料从\u0026quot;In other words, tulips were literally worth their weight in gold.\u0026ldquo;开始，后面就是\u0026rdquo; promissory notes\u0026quot;的相关内容，在\u0026quot;But that didn’t matter to the owner of the note. The owner only cared about having that piece of paper so it could be traded later at a profit. And people were borrowing, mortgaging their homes in many cases to obtain those bits of paper because they were sure they’d find an easy way to make money.\u0026ldquo;反映了B和C的内容。\nL2Q1.What topics related to the Nightcap Oak does the professor mainly discuss? Click on 2 answers.\nA. Factors that relate to the size of the area in which it grows\nB. The size of its population over the last few centuries\nC. Whether anything can be done to ensure its survival\nD. Why it did not change much over the last one hundred million years\n主题问题，材料前段就提到了其数量\u0026quot;two hundred\u0026rdquo;\n文章之后的讨论就是\u0026quot;factors to reproduce\u0026rdquo;\nL2Q2.According to the professor, what led scientists to characterize the Nightcap Oak as primitive?\nA. It has no evolutionary connection to other trees growing in Australia today.\nB. It has an inefficient reproductive system.\nC. Its flowers are located at the bases of the leaves.\nD. It is similar to some ancient fossils.\n定位到材料\u0026quot;Uh we found fossils that old that bear a remarkable resemblance to the tree.\u0026ldquo;\u0026ldquo;So, it\u0026rsquo;s a primitive tree,a\u0026hellip; a living fossil, you might say.\u0026rdquo;\n错选的A和材料表达的意思相反\u0026quot;And\u0026hellip; it—it’s probably a kind of tree from which other trees that grow in Australia today evolved.\u0026rdquo;\nL2Q3.What point does the professor make about the Nightcap Oak’s habitat?\nA. It is stable despite its limited size.\nB. Unlike the habitats of many plants, it is expanding.\nC. Its recent changes have left the Nightcap Oak struggling to adapt.\nD. Its size is much larger than the area where the Nightcap Oak grows.\n\u0026ldquo;Uh, of course, you might think there might not be many areas where the tree could spread into, er … because … um … well, it’s very specialized in terms of the habitat. But, that’s not really the case here. Um … the suitable habitat, that is, the actual rainforest is much larger than the few hectares where the Nightcap Oak grows.\u0026rdquo;\n在学生回答完问题后，教授的转折解释了\u0026quot;habitat\u0026rdquo;，并在后面解释了\u0026quot;the Nightcap Oak\u0026quot;种子的问题。\n这开头的对话错的有些多，且两篇材料的\u0026quot;Why does the student(teacher) say this\u0026hellip;\u0026ldquo;都有错，我认为随着做的题目增多，用意题和阅读是一个逻辑，大概率都在这句话的前文有提示。\nLecture1讲的内容比较有趣，且一开始做的时候几乎没听懂，但题目做的还行，主要是说了“郁金香经济”的问题。\nLecture2是个经典生物\u0026quot;habitat\u0026quot;类型的题，后面这种类型错的也不少，以本篇举例，重要的信息都在听力的“停顿”或者是“转折”上有所体现，且错的都是细节，只有多听了。\nTPO8 C1Q1.Why does the man mention his classmates?\nA. To explain how he obtained information about field research\nB. To point out that many students like to do field research\nC. To show that it is difficult to get intermediate-level credits\nD. To emphasize his motivation to do field research in two of his courses\n\u0026ldquo;My chair person told me that if I did independent field research in addition to the assigned work in each course; they would count as intermediate level courses. My classmates, um, some of my classmates, did this for an easy way to meet their intermediate course requirement, but I did it to get the kind of depth in those topics I was going for.\u0026rdquo;\nL1Q1.What is the main purpose of the lecture?\nA. To compare active habitat selection with passive habitat selection\nB. To show that most habitat preferences in animals are learned\nC. To compare the habitat requirements of several bird species\nD. To examine the consequences of habitat selection by animals\n不知道为什么错选，明显是D\n\u0026ldquo;With active habitat selection, an organism is able to physically select where to live and breed, and because an animal’s breeding habitat is so important\u0026hellip;\u0026rdquo;\nL1Q3.What does the professor illustrate with the example of the blue warbler?\nA. The relationship between human activity and habitat loss\nB. The relationship between habitat and reproductive success\nC. The advantages of habitats with low vegetation density\nD. The reproductive advantage that young warblers have over older warblers\n首先错选的C，\u0026ldquo;so they\u0026rsquo;re pretty close to the ground but these warblers also nest in forests that have low shrub density.\u0026ldquo;虽然提到了\u0026quot;density\u0026quot;但和vegetation无关，\u0026ldquo;density\u0026quot;译为“密度”。\n关于正确选项B，\u0026rdquo;**And the choice of habitat seems to affect reproductive success.**Because the older, more experienced birds, who nest in the high-density shrub areas, have significantly more offspring than those in low-density areas. Which suggests that the choice of where to nest does have an impact on the number of chicks they have.\u0026rdquo;\n通过讲\u0026quot;blue warbler\u0026quot;的\u0026quot;younger warbler\u0026quot;和\u0026quot;older warbler\u0026quot;选择栖息地的密度不同，体现了栖息地对\u0026quot;offspring\u0026quot;译为“幼崽”的影响。\nL1Q4.Why does the professor mention the population density of blackcaps in two different habitats?\nA. To explain the similar reproductive rates in the two habitats\nB. To explain the relation between a species’ population density and its nesting behavior\nC. To illustrate the advantages of a preferred habitat over a secondary habitat\nD. To illustrate the possible impact of making a poor habitat selection\n首先要注意题干的\u0026quot;population density\u0026rdquo;，然后注意本题的重点在\u0026quot;blackcap\u0026quot;的前半部分，而非下一题所需的后半部分，即不选D。\u0026ldquo;Studies have been done on the reproductive success rates for the birds in both areas and the results showed—surprisingly—that the reproductive success was essentially the same in both areas—the preferred and the second choice habitat.\u0026rdquo;\nC选项则和材料相反，B无关。\nTPO8和TPO6都主要错在了生物题材的Lecture上，但TPO8的生物部分更加有逻辑且复杂一些，讲了几种鸟。至于对话题的错因和TPO6一样。\nTPO9 C1Q3.The woman mentions a research study of milk packaging. What was the finding of the study?\nA. Plastic containers may change the flavor of milk.\nB. Light may negatively affect the quality of milk.\nC. People prefer to buy milk in see-through containers.\nD. Opaque containers are effective in protecting milk from bacteria.\n对应材料\u0026quot;Yeah, and I read a study that showed how light can give milk a funny flavor and decrease its nutritional value.\u0026ldquo;这句话语速很快且\u0026quot;light\u0026quot;不是很好听出来，容易错选成后面出现的\u0026quot;Well, consumers like being able to visually examine the color of the milk.\u0026ldquo;相关的选项C，但这和研究无关。\nL2Q2.According to the professor, what are two features of shrubs that allow them to grow well in Arctic regions? Click on 2 answers.\nA. They have roots that can penetrate permafrost.\nB. Their height allows them to absorb more sunlight.\nC. They absorb nutrients from the soil efficiently.\nD. They have a shallow root system.\n首先，在女学生提出了永冻层之后，老师开始解释\u0026quot;shrub\u0026quot;的特性，及它的根不会很深，也就排除了A选项，选择了D。在男学生提出问题后，就是对\u0026quot;shrub\u0026quot;为什么在夏季成长的更好，以及选C的原因：\u0026ldquo;Well, it may be biological processes that occur in the soil in the winter that cause increased shrub growth in the summer. And, here’s how: there are microbes, microscopic organisms that live in the soil.\nThese microbes enable the soil to have more nitrogen, which plants need to live, and they remain quite active during the winter.\u0026rdquo;\n这段语速较慢且有停顿，算是比较好听出来\u0026quot;nitrogen\u0026quot;来对应\u0026quot;nutrients\u0026rdquo;。\nL2Q3.What is one reason for the increase in shrub growth in Arctic Alaska?\nA. Decreases in grass and moss growth have altered the balance of nutrients in the soil.\nB. Increases in ground temperature have led to increased microbial activity.\nC. Increases in average winter temperatures have made permafrost permeable to water.\nD. Increases in snowfall have provided more water for shrubs.\n原因同L2Q2的C选项。\nL2Q5.Why does the professor mention shrub expansion into other environments, such as semiarid grasslands?\nA. To suggest that new shrubland may not convert back to tundra\nB. To explain how shrubland can expand in a warm climate\nC. To cite a similarity between the types of shrubs in semiarid grassland and tundra environments\nD. To explain how a biological loop can cause shrub expansion\n用意题，首先排除C，然后是注意在教授讲\u0026quot;expansion\u0026quot;的问题之前，女学生问了\u0026quot;But will it be long-term?I mean, maybe the shrubs will be abundant for a few years, and then it’ll change back to tundra.\u0026ldquo;那么A是最直接的选项，至于B，原文的意思应该是有了\u0026quot;shrub\u0026quot;的生长，\u0026ldquo;shrubland\u0026quot;的扩张会持续下去，而不是因为\u0026quot;warm climate\u0026rdquo;。错选的D选项，虽然意思上没错且前文有提到，但如果学生不发出提问，则不会有这段\u0026quot;expansion\u0026quot;的解释。\nL3Q2.What is the professor’s opinion about the conclusions of the recent study of the limestone formations in the Empty Quarter?\nA. They have changed the way geologists study desert environments.\nB. They contradict findings about similar desert lakes.\nC. They explain the causes of monsoons in the desert.\nD. They need to be confirmed by additional studies.\n\u0026ldquo;limestone\u0026quot;出现在\u0026quot;But the Empty Quarter lakes disappeared thousands of years ago. They left behind their beds, or basins, as limestone formations that we can still see today. They look like low-lying white or gray buttes … long, narrow hills with flat tops … barely a meter high.\u0026ldquo;结合后文\u0026quot;Keep in mind, though, that this study only looked at 19 formations … and about a thousand have been documented, so there’s a lot more work to be done.\u0026ldquo;可得出D选项，难在要注意听到\u0026quot;limestone\u0026quot;一词。\nL3Q3.According to the professor, what feature of the sand dunes made the formation of the lakes possible?\nA. The degree of slope of the sides of the dunes\nB. The presence of clay and silt particles in the dunes\nC. The position of the dunes relative to the wind and rain\nD. The narrowness of the valleys between the dunes\n在明显的\u0026quot;Second\u0026hellip;\u0026ldquo;后就是\u0026quot;sand dunes\u0026quot;的内容\u0026quot;Now, when the rain fell, water ran down the sides of the dunes, carrying clay and silt particles with it. And wherever these particles settled, they formed a pan … a layer that water couldn\u0026rsquo;t penetrate. Once this pan formed, further runoff collected and formed a lake.\u0026ldquo;逻辑还是很清晰的，要注意听到\u0026quot;clay and silt\u0026quot;\u0026ldquo;particles\u0026quot;\u0026ldquo;couldn\u0026rsquo;t penetrate\u0026rdquo;\nL3Q5.What does the professor imply about the lack of water buffalo and hippopotamus fossils in the more recent lakes?\nA. The level of water in the lakes was not sufficient for these animals.\nB. The bottoms of the lakes were too sandy for these animals to stand in.\nC. The location of the lakes made them too difficult for these animals to reach.\nD. The vegetation near the lakes did not attract these animals.\n文章最后的部分提到了\u0026quot;water buffalo and hippopotamus fossils\u0026quot;但先不需要知道其意思，接着听会发现，其大意是\u0026quot;lakes\u0026quot;只供饮用水，且动物种类少，最后\u0026quot;We\u0026rsquo;re not sure why. Uh, maybe there was a problem with the water … maybe it was too salty. That\u0026rsquo;s certainly true of other desert lakes.\u0026ldquo;指出了A选项。至于错选的C选项，与本段之前的内容矛盾\u0026quot;But … where did these animals come from? Well, the theory that has been suggested is that they migrated in from nearby habitats where they were already living.\u0026rdquo;\n这套TPO9就是真的听不出细节的问题，听的时候应该也没有做逻辑笔记，然后关键词也听不到导致的。\nTPO10 L2Q1.What is the main purpose of the lecture?\nA. To describe the trade in food crops between Europe and the Americas\nB. To describe the introduction of American food crops to Europeans\nC. To describe the influence of American food crops on traditional European dishes\nD. To describe the difficulties of growing American food crops in European climates\n文章专注于介绍美洲的各种作物被引入欧洲，但没有提及欧洲菜肴的变化等，明显选B。\nL2Q3.What does the professor imply about Thomas Jefferson’s attitude toward tomatoes?\nA. It was typical of his unconventional way of thinking.\nB. It helped to advance his political career.\nC. It changed the eating habits of North Americans.\nD. It helped to make tomatoes popular in Europe.\nFEMALE PROFESSOR: Oh, sure—people didn’t really start eating them here until the mid-1800s.\nFEMALE STUDENT: But, ah—seems like I heard…didn’t Thomas Jefferson grow them or something?\nFEMALE PROFESSOR: Ah! Well, that’s true…but, then, Jefferson is known not only as the third President of the United States, but also as a scholar who was way ahead of his time—in many ways!\n结合上下文，以及最关键的\u0026quot;scholar\u0026quot;\u0026ldquo;way ahead of his time\u0026quot;可以得出\u0026quot;Jefferson\u0026quot;思想超前。\nC2Q3.Why is the professor not going to discuss the book by Jane Bowles in the class?\nA. There is not enough time left in the semester.\nB. Not all of the students were able to get a copy of the book.\nC. The professor miscalculated the difficulty level of the book.\nD. The book was not on the course syllabus.\n材料提到，\u0026ldquo;There’re only 2 weeks of classes left in the semester and there are like 6 books on the syllabus that we haven’t even touched.\u0026ldquo;指向选项A，但错选的C选项在其前一句\u0026quot;Um.. I think my professor really miscalculated. Anyway the syllabus, was way too ambitious in my opinion.\u0026ldquo;出现了\u0026quot;miscalculate\u0026quot;但其对象应该是\u0026quot;time\u0026quot;而非\u0026quot;difficulty\u0026rdquo;。\nL3Q6.What does the professor mean when she says this:\nA. She realizes that the students are struggling with the concept.\nB. She is surprised that the student knew the answer to her question.\nC. She thinks that the answer to the question is obvious.\nD. She thinks that this phase of the cycle has an unusual name.\nFEMALE PROFESSOR: Can anyone guess what it’s called? Nancy?\nFEMALE STUDENT: Uh, well, if the one is called the land phase, then this has to be called the water phase, right?\nFEMALE PROFESSOR: Yes. That’s such a difficult point, isn’t it?\n看似是说其\u0026quot;difficult\u0026quot;实则是反义，从学生的回答也能看出\u0026quot;land phase\u0026quot;\u0026ldquo;then water phase\u0026rdquo;，加之教授的语气，并无错选的B选项表达的\u0026quot;surprised\u0026rdquo;，所以选C，教授这句话隐含的意思是“这个问题只有这样的难度”。\nL4Q3.What does the professor imply about some of the explanations for childhood amnesia that she describes?\nA. They can never be proved or disproved.\nB. They were formed without proper evidence.\nC. They explain only certain types of childhood amnesia.\nD. They are contradicted by her own research.\n“Well, once a popular explanation was that childhood memories are repressed … uh, the memories are disturbing so that as adults we keep them buried, and so we can’t recall them. And this is based on … well, well, it’s not based on, on, on the kind of solid research and lab testing I want to talk about today …”听力中有明显停顿来突出\u0026quot;not based on \u0026hellip; \u0026ldquo;即这种说法并无根据。\nL4Q4.The professor mentions some commonly held explanations for childhood amnesia. Indicate whether each of the following is one of the explanations she mentions.\nA. Early memories are repressed.\nB. Young children have few experiences to remember.\nC. Young children are unable to form memories.\nD. Children lose memories at a faster rate than adults.\nE. Young children do not make an effort to remember events.\nIt—it could be that as children we do form memories of things prior to age 3, but forget them as we grow older. That’s one explanation.\nAnother possibility is that children younger than 3 lack, um, lack some cognitive capacity for memory. And that idea… um, that children are unable to form memories, um… that’s been the dominant belief in psychology for the past hundred years.\n这段话中包含了正确的AC选项，但难在原题不会告诉要选几个，且D选项对应的答案在接近最后的\u0026quot;And childhood amnesia may reflect a high rate of forgetting. In other words, children under the age of 3 do form memories, and do so without language.\nBut they forget the memories at a fast rate, probably faster than adults do.\u0026rdquo;\nL4Q5.How was recall tested in children without language ability?\nA. By recording children\u0026rsquo;s responses to familiar faces\nB. By observing children\u0026rsquo;s reaction to a repeated series of actions\nC. By having children imitate each other \u0026rsquo;s actions\nD. By having children imitate an ordered sequence of actions\n\u0026ldquo;Now, if the children can’t talk, how was recall tested?\u0026ldquo;在这之后一段讲解实验过程：\u0026ldquo;The children were asked to imitate the steps immediately, and then again after delays of 1 or more months. And, even after a delay, the children could-could recall, or replicate the action - the objects used, the steps involved and the order of the steps. Even children as young as 9 months!\u0026rdquo;\nTPO10的Lecture4，是比较少见的心理学题材，且答案多藏于大段的对话中，难度较高。此外，Lecture3的\u0026quot;That’s such a difficult point, isn’t it?\u0026ldquo;要通过上下文和语气来判断，是道有趣的题目。\nTPO11 C2Q2.Why does the student say he is interested in doing what the professor asks?\nA. He thinks it may help him improve his research skills.\nB. He thinks it will enable him to get a better grade in the professor’s class.\nC. He thinks it may help him get into graduate school.\nD. He thinks it will be good teaching practice for him.\nFEMALE PROFESSOR: So I\u0026rsquo;d like to know if you\u0026rsquo;d be willing to join as a student representative on the interview committee. It’d be a good experience for you. You could\u0026hellip; uh\u0026hellip; put it on your résumé.\nMALE STUDENT: Oh! That\u0026rsquo;d look good for my grad school application, I guess. So what do I have to do?\n关键词\u0026quot;resume\u0026quot;和\u0026quot;grad school application\u0026rdquo;\nC2Q4.Why does the professor mention that one of the applicants will give a talk on a topic the student is particularly interested in?\nA. To see if the student would enjoy joining the applicant’s research team\nB. To suggest that the student may not totally agree with what the applicant has to say\nC. To persuade the student to come to a talk on Friday\nD. To warn the student to focus on the applicant’s teaching ability\n学生提到\u0026quot;That’s\u0026hellip;that\u0026rsquo;s what my research is about!\u0026quot;，即可得出和C选项的时间无关，B选项无提及，而A选项在前面就已确定学生回去参加，但参加的理由和题干的意思无关。\n回到材料\u0026quot;That\u0026rsquo;s why I feel it necessary to point out that even though this applicant\u0026rsquo;s research interests are similar to yours, we want you to tell us what you think about the teaching of all these applicants. Your perspective as a student \u0026ndash; how the applicant teaches in the classroom \u0026ndash;\u0026ldquo;教授在\u0026quot;how the applicant teaches in the classroom\u0026quot;进行强调，即确认参与者的教学情况，不难得出D选项。\nTPO13 C2Q5.What does the woman imply?\nA. She confused the man for another student who had visited the lab earlier in the day.\nB. The man is mistaken about how many videos are in the series.\nC. The language lab does not own the whole series of videos the man needs.\nD. The man is not familiar with the procedures used at the language lab.\nStudent: So, I can just take….err…..Can I take the whole series home? I think there are three of them. Manager: I guess you haven’t been here before.\n题干的音频对应文章开头部分的\u0026quot;I guess you haven’t been here before.\u0026ldquo;错选时是把\u0026quot;haven\u0026rsquo;t\u0026quot;错听成了\u0026quot;have\u0026quot;导致和材料后面的内容联系起来了，但正确听到\u0026quot;haven\u0026rsquo;t\u0026quot;便不难选出D选项。\nL3Q3.According to the professor what is true about the hero in chanson poetry and the in romance poetry? Click in the correct box for each phrase (A: Chanson hero B:Romance hero)\nA. Is admired for loyalty country\nB. Engages in conflict for adventure\nC. Is willing to fact extreme dangers to protect the lord\nD. Is concerned with individual improvement\n“Well, there’s a hero, a knight, who goes to battle, and he is admired for his courage, bravery and loyalty, loyalty to the lord he serves, his country and his fellow warriors in the field. He’s \u0026hellip;.um\u0026hellip; he has a, he’s a skilled fighter, willing to face the most extreme dangers, sacrificial, willing to sacrifice anything and everything to protect his king and country.”属于\u0026quot;Chanson poetry\u0026quot;特点，即AC。 “He does it for the sake of adventure, to improve himself, to show he’s worthy of respect and love from his lady. He’s very conscious of the particular rules of social behavior he has to live up to somehow. And all of his actions are for the purpose of proving that he is an upright, moral, well-mannered, well-behaved individual.”属于\u0026quot;Romance poetry\u0026quot;特点，即BD。\nL3Q4.Why does the professor mention that romance poems often included biographical sketches?\nA. To emphasize the similarities between chanson authors and romance authors\nB. To explain why the social status of troubadours is known today\nC. To point out why the biographical sketches are reliable sources of information\nD. To provide evidence that many troubadours were also historians\n首先要明确\u0026quot;romance poems\u0026quot;是指\u0026quot;troubadours\u0026rdquo;, \u0026ldquo;Another name for romance poetry that’s often synonymous with it is troubadour poetry.\u0026rdquo;\n之后注意关键词\u0026quot;And we know a lot more about the troubadours than we do about the chanson authors because they often had small biographical sketches added to their poems that gave pretty specific information about their social status, geographical location, and a small outline of their career.\u0026rdquo;\nTPO14 C2Q3.According to the advisor, how do newspaper editors evaluate an applicant for a reporting position? Click on 2 answers.\nA. They ask the applicant to present ideas for news stories.\nB. They ask the applicant to write a news story.\nC. They review the applicant’s university course work.\nD. They review a sample of the applicant’s published articles.\n“mean when you apply for a reporting job, editors look at two things\u0026mdash; they want to see clips, you know, some of your published articles, they’ll also want you to try out, they’ll give you an assignment like… covering a press conference or some other event, then see if you can craft a story about it, accurately, on deadline.”\nC2Q5.What does the student imply when he says this:\nA. He was surprised by the amount of effort required to write a newspaper article.\nB. He feels that some of the editor’s changes were unnecessary.\nC. He does not want to take credit that he does not deserve.\nD. He will try to make sure his future articles are more accurate.\nStudent: \u0026ldquo;To be honest, the article got lots of editing. In fact I barely recognized a couple of paragraphs. But the editor explained why the changes were made. I learned a lot and my second article didn\u0026rsquo;t need nearly as many changes.\u0026rdquo;\n定位到原文，是在教授\u0026quot;And I read that article, too. It was very good.\u0026ldquo;后说的，所以学生的意思应该是表达自谦，那么关于\u0026quot;take credit that\u0026quot;的意思：\nTo take the credit for something means to claim recognition or praise for an achievement or action, often implying that one is taking credit for work that may not solely belong to them. For example, you might say, \u0026ldquo;I can\u0026rsquo;t take credit for this,\u0026rdquo; indicating that you do not believe you deserve the recognition. This phrase can also suggest a sense of self-promotion or appropriation of others\u0026rsquo; efforts, as in \u0026ldquo;You took credit for my work\u0026rdquo;\nTPO56 MALE PROFESSOR: With a major in philosophy..besides the possibility of grad school—which I think would be wonderful for you—you know lots of things that are valuable in any career\nFEMALE STUDENT: Like what? How Johnson tried to refute Berkeley’s empiricism?\nMALE PROFESSOR: Well\u0026hellip; what did Johnson do?\nFEMALE STUDENT: Well, you know\u0026hellip; what Berkeley said was that we can’t really know any objects, we can just perceive them. It’s all about our senses perceiving things. Berkeley basically made the point that it doesn’t really make sense to believe in actual, physical matter\u0026hellip; What Johnson did was, Johnson was trying to disprove that, so he kicked a big stone. He said that refuted Berkeley.\nMALE PROFESSOR: Uh-huh. So, what do you think of that?\nFEMALE STUDENT: Well, not that I’m convinced by Berkeley either, but I really don’t think Johnson refuted anything. And I think Berkeley would just say that Johnson still doesn’t know anything apart from what he senses. Seeing the stone, feeling his foot hurting after he kicked it—it’s all still just based on his perceptions. Johnson can’t argue that he knows anything other than what his senses have told him.\nMALE PROFESSOR: You know what you just did?\nFEMALE STUDENT: Told you something that’s not relevant anywhere but a philosophy class?\nMALE PROFESSOR: No. You demonstrated well-developed analytical skills. Philosophy majors can critique other people’s reasoning, make a convincing argument, summarize\u0026hellip; and communicate very effectively. These things are important in any career. And in life!\nFEMALE STUDENT: OK, but\u0026hellip; still! You don’t see any of that in job descriptions!\nMALE: Really? Oh, I think you do. I’ve seen so many articles lately, by heads of companies in all kinds of fields, complaining that it’s hard to find employees who can do that. Ask at the career services office—they’ll tell you the same thing.\nC1Q3.Why does the student mention Johnson and Berkeley?\nA. To cast doubt on something the professor said\nB. To get the professor’s opinion about a topic from class\nC. To indicate that she wishes to change the subject\nD. To check whether she has understood a philosophical argument\n由\u0026quot;Like what? How Johnson tried to refute Berkeley’s empiricism?\u0026ldquo;\u0026ldquo;Told you something that’s not relevant anywhere but a philosophy class?\u0026ldquo;这两句可知，学生讲了一个自己认为只和哲学有关而与就业无关的故事，来反驳材料上面，教授一开始提及的\u0026quot;you know lots of things that are valuable in any career\u0026rdquo;。\nC1Q4.What does the student imply about Johnson\u0026rsquo;s argument?\nA. It is more convincing than Berkeley\u0026rsquo;s argument.\nB. It disproves an accepted theory.\nC. it demonstrates the existence of physical matter\nD. It fails to prove the point he was trying to make.\n细节题\u0026quot;FEMALE STUDENT: Well, not that I’m convinced by Berkeley either, but I really don’t think Johnson refuted anything. And I think Berkeley would just say that Johnson still doesn’t know anything apart from what he senses. Seeing the stone, feeling his foot hurting after he kicked it—it’s all still just based on his perceptions. Johnson can’t argue that he knows anything other than what his senses have told him.\u0026rdquo;\nC1Q5.Why does the professor tell the student to go to the career services office?\nA. To research summer internship opportunities\nB. To ask how to include her skills on her resume\nC. To read job descriptions for careers in philosophy\nD. To confirm that her skills are valuable to employers\n这里要注意\u0026rdquo;career services office\u0026ldquo;的内容不要和后面的\u0026rdquo;our department\u0026ldquo;的搞混，关于\u0026quot;career services office\u0026quot;只提到了\u0026quot;Ask at the career services office—they’ll tell you the same thing.\u0026ldquo;而这个\u0026quot;same thing\u0026quot;代指了前文的\u0026rdquo; \u0026hellip; complaining that it’s hard to find employees who can do that.\u0026quot;“很多公司高管抱怨找不到能够批判性思考的员工”\nC2Q2.Why do the students want to get to the theater early?\nA. To purchase their concert tickets\nB. To avoid rush hour traffic\nC. To be able to get good seats\nD. To have time to eat dinner\n细节题，但是和时间有关的信息有些多，错选的时候混掉了，\u0026ldquo;We already have tickets, but this theater doesn’t have assigned seating—so we need to get there early—like around seven thirty—to get good seats.\u0026rdquo;\nC2Q5.What information will the woman give the man tomorrow?\nA. The cost of the concert tickets\nB. The hourly charges for van drivers\nC. The amount of money the students need to deposit\nD. The amount of money the students still owe\nFEMALE EMPLOYEE: I’ll have to run a few numbers to get the final cost. Vans are a lot cheaper than buses but we’ll have to pay for two drivers—they’ll charge us by the hour. Of course, you’ll get the normal student discount from the van company, and I’ll deduct your deposit. I can let you know the balance tomorrow.\n经济学的词汇不足问题，\u0026ldquo;deposit\u0026quot;表示钱的时候，有“存款；预付款，定金；押金”的意思，\u0026ldquo;deduct\u0026quot;为“减，减去；扣除”，\u0026ldquo;owe\u0026quot;为“欠债；该偿还”，\u0026ldquo;balance\u0026quot;为“结存，结余；差额”\n雇员问学生今天有没有带押金(deposit)，学生说带了，说他同学都知道之后还得再付押金之外的更多的钱，雇员说我明天算好把差额(balance)告诉你。\nL3Q1.What aspect of archaeology in Iceland does the professor mainly discuss?\nA. Various techniques for dating archaeological sites\nB. Causes of damage to a Viking-era house\nC. Evidence of early agricultural tools\nD. A method for locating buried structures\nBC易排除，回看材料，前半部分都在交代冰岛的背景，以及移民\u0026quot;Thorfinsson\u0026quot;的故事，而后半部分，从\u0026quot;But, back to my point\u0026hellip; one team of archaeologists working there decided to use an electromagnetic remote sensing tool to try to locate buried structures.\u0026ldquo;介绍了这个工具的原理，以及考古学家如何用它来找到了遗迹。文章最后再点题\u0026quot;Thank goodness for the remote sensing tool, or this house might never have been found!\u0026ldquo;表明了重点是这个\u0026quot;tool\u0026quot;而没有\u0026quot;various techniques\u0026rdquo;。\nL3Q4.According to the professor ,what kind of data does the remote sensing tool provide?\nA. The approximate age of different types of buried structures\nB. The electrical conductivity of the ground at different locations\nC. The chemical composition of different types of soil and peat\nD. The temperature of the ground at different depths\n结合材料\u0026quot;So the tool sends down alternating currents of electricity and then measures how well the electric current travels though the ground in different places.\nThen you look at all of your data\u0026hellip; look for patterns of electrical resistance\u0026hellip; and this reveals where walls are located.\u0026rdquo; 可见与选项B同义。\n这一套TPO的听力确实是难做，第一篇的哲学，一下子大段的\u0026quot;Johnson and Berkeley\u0026rdquo;，且充斥着\u0026quot;perceive\u0026quot;这样的哲学词汇，听了4遍才可以说是明白了。\n第二篇对话就是最后算钱的部分，各种词汇量不足的问题了。\nTPO57 L2Q2.Why does the professor mention rocks from Earth’s moon?\nA. To compare the surface of Earth’s moon to the surface of a moon of Jupiter\nB. To stress the need for further exploration of space\nC. To explain a way of calculating the absolute age of surfaces of planets or moons\nD. To show how to identify a secondary impact on the surface of Earth’s moon\nBut absolute age, actual age, is trickier. We have to know exactly how old one surface is. For example, we do have a very clear idea of the ages of some surfaces of the moon from rocks we brought back and then this information can allow us to extrapolate the age of another surface that has a similar concentration of craters.\nL2Q6.What does the professor imply about information obtained from space probes?\nA. The information was probably distorted during transmission to Earth\nB. The information helped determine the age of most planetary surface features.\nC. Some of the information is at present difficult to interpret correctly\nD. The information applies only to the moons of Jupiter.\nYou know, we\u0026rsquo;re getting great information and photos from our space probes all the time, but they also remind us of just how much more we need to learn.\nLecture2是太空学话题，经典的听不太懂，然后在这一篇上试了下精听法，确实能把这篇听得很明白，但有些耗时间，换了新题效果就不好说。\n20250531 - 1 C1Q5.Why does the man say this to the student:\nI\u0026rsquo;d be happy to answer your question, but are you a psychology major?\nA. To recommend an appropriate professor as her departmental advisor B. To confirm that she has already taken an introductory psychology course C. To let her know which psychology courses she is required to take D. To determine if she is eligible to assist professors in the department 显然此句出现在对话的开始部分，那么结合后面说有两个不合适的工作来看，应该是选D的\nI don\u0026rsquo;t doubt your interest in psychology. It\u0026rsquo;s just that unfortunately, the part time jobs we have for students are either grading homework or else being a research assistant for one of the professors. But you wouldn\u0026rsquo;t qualify for either of those until at least your third year. Have you tried the student employment office?\nC2Q2.Why does the man mention that he saw a golden eagle in the wild?\nA. To make a point about the rarity of golden eagles in the local area B. To ask if course credit is given for independent fieldwork C. To suggest a news item for the biology department\u0026rsquo;s Web site D. To question a finding of a wildlife survey about golden eagles And I actually saw a golden eagle at a tree near the river last winter. I’ve lived here in\nMinnesota all my life and had never seen a golden eagle in the wild before.\n(woman) I know. Golden eagles didn’t use to visit our area on any regular basis. But\naccording to winter wildlife surveys done over the past few years, around sixty had begun\nhanging out around the cliffs overlooking the upper Mississippi river during the month of\nJanuary.\nC2Q3.According to the woman, what prompted some members of the biology faculty to apply for a grant?\nA. Their desire to increase the number of internships offered to biology students. B. Their interest in upgrading the department\u0026rsquo;s radio-tracking technology C. The recent appearance of golden eagles wintering nearby D. A proposal by a graduate student to conserve the golden eagle\u0026rsquo;s winter habitat (woman) When the winter survey showed a pattern, Professor Simmons and some other\nfaculty thought the first step of understanding these birds would be to track their\nmovements. So they applied for a research grant and got it.\nL3Q3.Why does the professor mention Thomas Jefferson?\nA. To cite a contemporary account of the weather written in 1816 B. To explain the origin of a theory about the Sun’s magnetic cycles C. To give Jefferson credit for sending explorers to carefully study volcanoes D. To show how the strange weather events affected Jefferson’s presidency There was widespread snowfall in June, followed by more cold spells in July and\nAugust. Historians can point to a number of contemporary diaries from the northeast and\nbeyond, including Jefferson’s, that’s Thomas Jefferson of course, a former president who\nhad retired to his home down in Virginia, diaries that recorded and remarked on the\nextraordinarily cold temperatures that year.\nA选项是指“引用1816年所写的一篇关于天气的当代记述”，即用名人Jefferson的记载来说明当年的情况。\n20251015 C1Q5.What does the student imply when she says this:\nOh, you’re right! Oh, but no, I don’t think they actually let us take reserved books out of the library, like, you have to sit there and read them. So as much as I’d love to just live in the library these next couple of weeks!\nA. She is already using too many library books. B. She does not have time to do all of the reading in the library. C. She would rather use a library book than buy her own copy. D. She prefers studying in the library to studying in her room. C2Q3.What does the professor emphasize about the play The Adding Machine?\nA. It was the first Surrealist play. B. It was more popular than other Expressionist plays. C. Its characters sang or danced to express their happiness. D. It used very few props. (woman) Not really. Although that one play, The Adding Machine, that you were just alluding to with all the paper. (man) Yeah? (woman) That one did attract a large audience when it first came out, perhaps because it was more accessible than your typical expressionist play, which might have seemed even stranger.\nL2Q6.What does the professor imply about the debate over Olmsted’s proposal?\nA. It showed off Olmsted’s stubbornness in refusing to compromise his ideas. B. It caused delays in the completion of the park. C. It was motivated largely by economic considerations. D. It led to the inclusion of ideas from some of the losing plans. There was a great deal of debate, a lot of it political in nature, but, eventually, Olmsted accepted the compromise that actually turned out to work in the park’s favor.\n原文中明确提到，批评者中有人希望修改方案，以“包含一些出现在落选方案（losing plans）中的想法”。教授接着说，奥姆斯特德最终“接受了妥协（accepted the compromise）”。\n20251108 L3Q4.What is the professor\u0026rsquo;s attitude regarding the destruction of the Cassini spacecraft?\nA. He suspects that it resulted from a mechanical problem. B. He recognized that it was necessary. C. He is surprised that it happened so quickly. D. He is impressed by the efforts undertaken to avoid it. And so in April 2017, the scientists and engineers in control of the mission used the remaining fuel to change Cassini\u0026rsquo;s orbit and send the spacecraft hurdling into Saturn, where it burned up in Saturn\u0026rsquo;s thick atmosphere. Couldn\u0026rsquo;t they have kept it going until it ran out of fuel?\n科学家们是有意利用最后的燃料改变轨道，让它在土星大气层中烧毁，以彻底消除后文的风险（If that happened, we\u0026rsquo;d be changing the Enceladus environment before we had a chance to fully explore it. We\u0026rsquo;d never know what Enceladus was like originally.）。\nL3Q6.Why does the professor make the point that the existence of liquid water on Enceladus was a discovery made by the Cassini spacecraft?\nA. To argue for the importance of space missions such as the Cassini mission B. To make a contrast between the Cassini mission and earlier missions to Saturn. C. To emphasize the sophistication of the instruments on the Cassini spacecraft. D. To justify a decision that was made before the Cassini spacecraft was launched. Well, when Cassini was launched in 1997, we didn\u0026rsquo;t know yet about Enceladus\u0026rsquo;s ocean. That ocean was discovered in the course of the Cassini mission when the spacecraft orbited that moon.\n教授说，消毒级别取决于该航天器进入支持生命的世界的可能性。而在 1997 年 Cassini 发射时，“我们还不知道土卫二有海洋（we didn\u0026rsquo;t know yet about Enceladus\u0026rsquo;s ocean）”。\n教授强调这个发现是在任务过程中才做出的，是为了证明（justify）当初发射时为什么没有采取最高级别消毒措施这一决定是合理的——因为在当时的认知下，没有必要。\nSpeaking TPO60 Q1.A university wants to require all students to have their own laptop computers. Do you agree or disagree with this policy? Give specific reasons to support your opinion.\n[Main Point] \u0026ldquo;I strongly agree with the policy that all students should have their own laptops.\u0026rdquo;\n[Transition] \u0026ldquo;I feel this way primarily because it creates a distraction-free and highly efficient learning environment.\u0026rdquo;\n[Deep Support] \u0026ldquo;Specifically, having a personal device allows students to customize their study tools and access a vast number of online materials instantly during lectures. For example, last semester, when I was preparing for a complex math exam, I used my laptop to search for video tutorials and academic papers right in the library. In contrast, if I had to rely on school desktop computers, I would have wasted a lot of time queuing and transferring files, which is quite frustrating. By having my own laptop, I could stay focused, which helped me recharge my passion for the subject and ultimately achieve full marks.\u0026rdquo;\n[Conclusion] \u0026ldquo;Thus, the convenience and efficiency it provides make it an indispensable tool for students.\u0026rdquo;\n效率 角度\nTPO63 State Main Point \u0026ldquo;I disagree with the statement that teachers should not be too friendly to their students.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because a friendly teacher can significantly boost a student’s confidence and interest in learning.\u0026rdquo;\nThe Deep Support (核心展开) \u0026ldquo;Specifically, when a teacher is approachable and encouraging, students feel safe to ask questions without being judged. For example, I used to struggle with chemistry and was too shy to speak up because I feared making mistakes. However, my teacher was always smiling and comforting, which made me feel refreshed and more willing to study harder. In contrast, if she had been strict or distant, I would have stayed frustrated and likely failed the final exam. Thanks to her kindness, I finally obtained full marks and developed a lifelong interest in the subject.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, I believe friendliness is an essential quality for effective teaching.\u0026rdquo;\n心理 角度\nTPO64 Q1.Some people prefer to get information from books. Others prefer using a computer to access information via the Internet. Which do you prefer?\nState Your Main Point \u0026ldquo;I definitely prefer using a computer to access information via the Internet rather than relying on books.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because the Internet offers unparalleled efficiency and a much wider variety of resources.\u0026rdquo;\nThe Deep Support (Reinforced) \u0026ldquo;Specifically, searching for information online allows for instant results, which eliminates the need to spend hours in a library. For example, last week I had to research a complex historical event for a class assignment. By simply typing keywords into a search engine, I accessed dozens of academic articles and primary sources in seconds. In contrast, if I had used physical books, I would have struggled to find updated information, making the process much more frustrating and time-consuming. This convenient access helped me complete my work early and obtain full marks.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, I believe the speed and accessibility of the Internet make it a far superior tool for learning.\u0026rdquo;\n效率 角度\nTPO65 Q1.Do you agree or disagree with the following statement? Having power and money is the best definition of success.\nState Your Main Point \u0026ldquo;I strongly disagree with the idea that having power and money is the best definition of success.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because I believe true success is defined by personal fulfillment and making a positive impact on others.\u0026rdquo;\nThe Deep Support (Reinforced) \u0026ldquo;Specifically, relying solely on material wealth can lead to a hollow life, whereas pursuing one\u0026rsquo;s passion leads to long-term happiness. For example, I have an uncle who was a high-paid corporate executive with immense power, but he was always stressed out and rarely saw his family. Eventually, he quit to become a high school teacher. Although he earns less money now, he feels much more refreshed and fulfilled because he is helping students achieve full marks and grow. In contrast, if he had stayed in his powerful position, he would have remained miserable despite his bank account. This proves that personal growth and helping others provide a much deeper sense of accomplishment.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, I believe that happiness and contribution are far better measures of success than mere wealth or status.\u0026rdquo;\nTPO66 Q1.\nState Your Main Point \u0026ldquo;I agree with the point of view that a college education is not strictly necessary for a successful career.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because real-world practical skills and personal passion often override theoretical knowledge in many industries.\u0026rdquo;\nThe Deep Support \u0026ldquo;Specifically, success is increasingly defined by one\u0026rsquo;s ability to solve problems rather than just holding a degree. For example, many successful entrepreneurs in the technology sector started their own businesses based on a unique idea and self-taught coding skills. In contrast, if they had spent four years only focusing on textbooks in a classroom, they might have missed the keen competition and fast-changing trends of the market. By following their own learning pace and gaining hands-on experience, they were able to achieve personal fulfillment and even create job opportunities for others. This proves that while college is valuable, it is not the only path to becoming a mature adult with a flourishing career.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, I believe that determination and practical experience are the true drivers of success.\u0026rdquo;\nTPO67 Q1.\nState Your Main Point \u0026ldquo;I believe it is a much better idea for international students to stay with a host family rather than living on their own.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because it offers a cost-effective way to master a second language in a natural environment.\u0026rdquo;\nThe Deep Support (核心展开) \u0026ldquo;Specifically, living with locals is the most efficient way to improve speaking skills because you are forced to communicate in the target language daily. For example, instead of just learning from textbooks, you can practice real-life conversations during dinner, which helps broaden your horizons. Moreover, it significantly saves money and transportation fees. Host families often provide meals and furniture, which alleviates monetary pressure for students. In contrast, if a student lives alone, they not only pay higher rent but also lack the opportunity to interact with native speakers, often feeling isolated. This convenient arrangement ensures students can focus more on their studies and achieve full marks.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, the linguistic and financial benefits make a host family the superior choice.\u0026rdquo;\n经济 + 社交 角度\nTPO68 State Your Main Point \u0026ldquo;I disagree with the statement that all workers should be required to retire by age sixty-five.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because continuing to work can significantly improve mental health and utilize the valuable experience of older employees.\u0026rdquo;\nThe Deep Support (核心展开) \u0026ldquo;Specifically, many individuals at sixty-five are still physically active and find a great sense of personal fulfillment through their careers. For example, my grandfather continued to work as a consultant after sixty-five; this constant interaction with colleagues kept him refreshed and prevented the social isolation often associated with retirement. Moreover, older workers possess sophisticated skills and profound knowledge that younger staff may lack. In contrast, if companies force these experts to retire prematurely, they lose a wealth of wisdom, which can lead to inefficiency in the workplace. Staying active in one\u0026rsquo;s profession is a crucial way for seniors to stay connected to society.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, I believe retirement should be a personal choice based on an individual\u0026rsquo;s health and capability rather than a fixed age.\u0026rdquo;\n心理 角度\nTPO69 State Your Main Point \u0026ldquo;I personally prefer to take a lot of photographs during family gatherings or meetings with friends.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because photos serve as a lasting record of personal fulfillment and happiness.\u0026rdquo;\nThe Deep Support (Reinforced) \u0026ldquo;Specifically, capturing these special moments allows me to relive the joy whenever I feel stressed out in the future. For example, during our last family reunion, I took dozens of pictures of us laughing and sharing a meal. Now, whenever I face a heavy workload at school, looking at those photos helps me feel refreshed and reminds me of the support I have. In contrast, if I didn\u0026rsquo;t take any photos, those precious memories might fade over time, leaving me with nothing to look back on during challenging times. This simple act ultimately boosts my confidence and mental well-being.\u0026rdquo;\n心理 角度\nTPO70 State Your Main Point \u0026ldquo;When I was a child, I much preferred playing games outdoors because I believe it is the most effective way to improve social skills.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because outdoor activities often require teamwork and constant interaction with peers.\u0026rdquo;\nThe Deep Support (核心展开) \u0026ldquo;Specifically, playing sports like soccer or hide-and-seek forces children to communicate, negotiate, and cooperate to achieve a common goal. For example, I remember spending every afternoon at the local park with a large group of neighborhood kids. Through these games, I learned how to resolve conflicts and support my teammates, which boosted my confidence in social situations. In contrast, if I had stayed inside playing video games alone, I would have missed these crucial opportunities to broaden my horizons and build lasting friendships. This early experience in a vibrant social environment eventually helped me become a more communicative and mature adult.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, I believe outdoor play is essential for developing the interpersonal skills needed for future success.\u0026rdquo;\n社交 角度\nTPO71 State Your Main Point \u0026ldquo;I personally agree with the statement that the process of doing something is more important than the final result.\u0026rdquo;\nTransition \u0026ldquo;I feel this way primarily because the process is where true personal growth occurs and where we develop essential skills for the future.\u0026rdquo;\nThe Deep Support (核心展开) \u0026ldquo;Specifically, even if the end result is not perfect, the dedication and persistence we show during the process help foster a diligent personality. For example, I remember when I first started learning to play the piano. At first, I couldn\u0026rsquo;t play even a simple scale fluently, let alone beautiful melodies. However, I kept practicing unfamiliar passages for hours every day. Although I didn\u0026rsquo;t win any major competitions in the end, that constant interaction with the music and the effort I put in boosted my confidence and taught me the value of persistence. In contrast, if I only cared about the result, I would have felt frustrated by my early failures and given up, missing out on this chance to improve my characteristic. I believe these internal gains are much more valuable than any trophy.\u0026rdquo;\nConclusion \u0026ldquo;Therefore, the journey and the lessons learned along the way are what truly define success.\u0026rdquo;\nWriting 20250315 - Academic Discussion Reading Your professor is teaching a class on sociology.Write a post responding to the professor\u0026rsquo;s question.In your response,you should do the following:express and support your personal opinion;make a contribution to the discussion in your own words. An effective response will contain at least 100 words.\nDr.Achebe:This week,we will be discussing space exploration.Some people think that it is a waste of money for governments to fund space exploration.Others disagreewith this view arguing that space exploration is a valuable and necessary investment for our future.They think that it is worthy for governments to fund space exploration. I want to know what you think about this topic. Do you think that governments should fund space exploration?Why or why not?\nKelly: I don\u0026rsquo;t think that governments should fund space exploration because it will be too costly to build spaceships and develop related technologies.Though space exploration tech costs billions,it doesn\u0026rsquo;t offer direct benefits to most people,which makes its funding a controversial issue\nAndrew: Personally, I do think that all the costs will be worth it. The potential scientific discoveries and technological advancements could have far-reaching impacts that justify the initial investment.Imagine the new inventions that might come from this exploration.Such technology could greatly improve our everyday lives.\nWrite-up As far as I am concerned, governments should fund space exploration, since it will definitely promote the development of technology and improve our daily lives.\nTo be more specific, space exploration drives technological innovations, and many space technologies are adapted for civilian use, directly improving people\u0026rsquo;s quality of life and promoting the prosperity of related industries.\nFor instance, there are seeds of organic vegetable being brought to space station or other plants like the Mars, result in more productive and highly adaptable species, due to space exploration.\nNevertheless, some state that it\u0026rsquo;s too costly for governments to fund space exploration. However, with the funding of government, space exploration will show its massive value, which promote economic increase and make civilian life better.\n实际上手一写，发现语言匮乏，且Concession部分不是很会，所幸有AI帮助，我认为解决了总结段的重复问题\n将 promote the development 改为 catalyzes technological breakthroughs（催化技术突破），表达更有力度。\n将 seeds of organic vegetable being brought to space 修正为更专业的 agricultural experiments in microgravity environments。\n使用了 incubator (孵化器) 和 economic dividends (经济红利) 等词汇，使论证从“科学研究”自然过渡到“经济价值”。\n原稿最后一段只是在重复自己的观点。修改版使用了 \u0026ldquo;short-sighted view\u0026rdquo; (短视) 来定性 Kelly 的观点，并解释了“短期投入”与“长期回报”的关系。\nAnswer generated by Gemini As far as I am concerned, governments should prioritize funding space exploration, as it inevitably catalyzes technological breakthroughs that refine our daily lives.\nTo be more specific, space research often acts as an incubator for civilian innovations, directly enhancing public welfare and industrial prosperity. For instance, agricultural experiments in microgravity environments—such as those conducted on the International Space Station—have led to the development of \u0026ldquo;space seeds.\u0026rdquo; These genetically resilient crops offer significantly higher yields and better resistance to pests, providing a tangible solution to global food security.\nWhile Kelly argues that the exorbitant costs make space funding controversial, I believe this is a short-sighted view. Although the initial capital outlay is massive, the subsequent economic dividends—from job creation in high-tech sectors to the birth of entirely new industries—far outweigh the costs, making it a vital investment for any visionary government.\n","date":"2025-11-02T15:54:54+08:00","permalink":"/p/toefl%E5%AD%A6%E4%B9%A0%E7%BB%8F%E9%AA%8C/","title":"TOEFL学习经验"},{"content":"课程源自B站“湖科大教学匠”的《计算机组成微课堂》 https://www.bilibili.com/video/BV1qG41197E4/?share_source=copy_web\u0026vd_source=440fb90b593ac5ecf9b12f264d73b53b BV1qG41197E4\n1.1 计算机系统的组成 计算机系统分为 硬件 和 软件\n其中，硬件包括 主机 和 外设\n软件包括 系统软件 和 应用软件\n计算机系统性能的好坏，取决于硬件和软件功能的总和\n1.2 计算机的发展 第一台真正意义上的电子数字计算机 ：ABC\n第一台实用的电子数字计算机 ：ENIAC\n计算机硬件的发展 ：电子管计算机 -\u0026gt; 晶体管计算机 -\u0026gt; 集成电路计算机 -\u0026gt; 超大规模集成电路\n计算机软件的发展 ：机器语言 -\u0026gt; 汇编语言 -\u0026gt; 高级语言 -\u0026gt; 面向对象\n1.3 计算机硬件 冯诺依曼计算机主要特点 ：\n构成程序的指令和数据均采用 二进制 表示 指令和数据存放在存储器 中，按地址访问 指令 在存储器中 按顺序存放 ，一般情况下，指令是 顺序执行 的 指令由 操作码 和 地址码 组成 机器 以运算器为中心 ，输入输出设备与存储器间的数据传送通过运算器完成 计算器硬件由 运算器、控制器、存储器、输入输出设备 组成 其中，存储器分为 主存储器 和 辅助存储器\n主存储器：用于存放程序和数据，可以 直接与CPU交换信息 ，又称为内存储器，简称内存或主存。\n辅助存储器：用于帮助主存存储更多的信息。又称外部存储器，简称为外存或辅存。 储存中的信息必须调入主存后，才能被CPU访问 。\n运算器 的核心为 算术逻辑单元ALU ，主要功能有算术运算和逻辑运算。\n1.4 计算机软件 分为 系统软件 和 应用软件\n计算器软件的发展：\n用机器语言编程\n使用 机器指令的二进制编码 编写程序\n编程繁琐，易出错且不易排错\n计算机可以直接识别和执行\n用汇编语言编程\n使用 表示机器指令的特殊符号 编写程序\n相比于用机器语言编写，汇编语言这种符号语言简单直观，便于记忆\n计算机不能识别和执行 用汇编语言编写的程序，须通过汇编器翻译成机器语言程序\n用高级语言编程\n使用 规定好的一套基本符号和编程规则\n直观通用， 与具体机器无关\n计算机不能识别和执行 用高级语言编写的程序，须通过编译器翻译成汇编语言或机器语言\n将高级语言程序转换成可执行目标程序的主要过程：\n预处理 -\u0026gt; 编译 -\u0026gt; 汇编 -\u0026gt; 链接\n随着硬件和软件的不断发展，人们又创造出了一类程序，称为 操作系统\n操作系统提供了在 汇编语言和高级语言的使用和实现过程中所需的某些基本操作 。\n操作系统负责 控制并管理计算机系统全部硬件资源 （例如CPU、内存和外部设备）和 软件资源 （例如编译程序、应用程序等）。\n1.5 计算机系统的层次结构 层次 描述 说明 第 6 层 高级语言层 使用与机器无关的高级语言编程，无需掌握机器的底层技术细节，只要掌握某种高级语言的语法规则以及算法和数据结构等方面的知识进行编程。 第 5 层 汇编语言层 使用汇编语言进行编程。由于汇编语言的每条语句都与机器语言的某条语句对应，因此仍要求程序员对实际机器的内部组成和指令系统非常熟悉。 第 4 层 操作系统层 设计人员不仅要对操作系统的设计理论有比较深入的理解，还需要掌握具体机器的指令集和汇编语言以及适于编写操作系统软件的高级语言。 第 3 层 指令集体系结构层 ISA 定义了某计算机可执行的所有机器指令的集合，规定了对于每条机器指令计算机应执行什么操作，所处理的操作数应存放的位置以及操作数的类型等。 第 2 层 微程序层 将一条机器指令编写成一个微程序。每个微程序包含若干条微指令，每条微指令对应一条或多条微操作。 第 1 层 逻辑电路层 计算机硬件系统的底层，由逻辑门、寄存器等逻辑电路组成。 1.6 计算机的基本工作原理 运算器 ALU 加 减 乘 除 ACC 被加数和 被减数差 乘积高位 被除数余数 MQ 乘数 乘积低位 商 X 加数 减数 被乘数 除数 ACC 表示累加器 (ACC)表示累加器中的内容 MQ 表示乘商寄存器 (MQ)表示乘商寄存器中的内容 X 表示操作数寄存器 (X)表示操作数寄存器中的内容 M表示主存储器中某个存储单元的地址 (M)表示地址为M的存储单元中的内容 加法操作过程：\n(M)→X 取出存放在主存中地址为M的存储单元中的内容(M)（加数），送到操作数寄存器X中 (ACC) + (X) → ACC 将累加器ACC中的内容(ACC)（被加数）与操作数寄存器X中的内容(X)（加数）相加，结果（和）保留在累加器ACC中 减法操作过程：\n(M)→X 取出存放在主存中地址为M的存储单元中的内容(M)（减数），送到操作数寄存器X中 (ACC) - (X) → ACC 将累加器ACC中的内容(ACC)（被减数）与操作数寄存器X中的内容(X)（减数）相减，结果（差）保留在累加器ACC中 乘法操作过程：\n(M)→MQ 取出存放在主存中地址为M的存储单元中的内容(M)（乘数），送到乘商寄存器MQ中 (ACC)→X 将累加器ACC中的内容(ACC)（被乘数），送到操作数寄存器X中 (X) × (MQ)→ACC // MQ 将操作数寄存器X中的内容(X)（被乘数）与乘商寄存器MQ中的内容(MQ)（乘数）相乘，结果（积）的高位保留在累加器ACC中，低位保留在乘商寄存器MQ中 除法操作过程：\n(M)→X 取出存放在主存中地址为M的存储单元中的内容(M)（除数），送到操作数寄存器X中\n(ACC) ÷ (X) → MQ 将累加器ACC中的内容(ACC)（被除数）除以操作数寄存器X中的内容(X)（除数），结果（商）保留在乘商寄存器MQ中，余数保留在累加器ACC中。\n存储器 存储体 由很多个 存储单元 组成\n每个 存储单元 由若个 存储元件 组成\n每个存储元件能存储一位 二进制数\u0026quot;0\u0026quot;或\u0026quot;1\u0026quot;\n一个存储单元中可存储一串二进制信息，称这串二进制信息为一个 存储字 ，这串 二进制信息的位数 称为 存储字长\n给每个存储单元都赋予一个编号，称为 存储单元的地址\n存储器 地址寄存器 MAR ，用来 存放欲访问的存储单元的地址\nMAR的位数 ，决定了 存储单元的数量\n存储器 数据寄存器MDR ，用来 存放从存储体的某个存储单元取出的信息或者准备往某个存储单元存入的信息\nMDR的位数（长度） ，与 存储字长相等 。\n主存（内存）的这种按存储单元的地址来实现对其写入和读取的存取操作，需要在CPU中的控制器的控制下进行。\n控制器 控制器是计算机的神经中枢，由它指挥各部件自动、协调地工作。\n控制从主存中读取一条指令，称为 取指 过程（阶段）。 对指令进行分析，指出该指令要完成何种操作，并按寻址特征指明操作数的地址，称为 分析 过程（阶段）。 根据指令的操作码和操作数所在的地址完成某种操作，称为 执行 过程（阶段）。 程序计数器PC 用来 存放当前欲执行指令的地址\nPC与MAR 之间有一条直接通路。\nPC自动形成 下一条指令的地址 “自动加1”功能）\n指令寄存器IR 用来存放当前的指令\nIR的内容来自MDR。\nIR中的 操作码 （用OP(IR)表示）会送至CU（用 OP(IR)→CU 表示），用来 分析指令 。\nIR中的 地址码 （用Ad(IR)表示）作为操作数的地址送至MAR（用 Ad(IR)→MAR 表示），用来 从内存中取操作数 。\n控制单元CU 用来 分析当前指令所需完成的操作，并发出各种微操作命令序列 ，用以控制所有被控对象。\n机器指令简介 计算机的基本工作原理 初始化：(PC)=0，指向内存中的编号为0的存储单元，该存储单元的内容是第一条机器指令\n对第一条指令进行 取指 ：\n控制器将PC的内容送至内存的MAR（用(PC)→MAR表示）对内存进行寻址，并命令内存做读操作，此时所寻址的存储单元（0号存储单元）的内容“0000010000010101”被送入MDR内。 将MDR的内容传送至控制器的IR（用(MDR)→IR表示）。 接下来，程序计数器PC中的值从0自动增加到了1，在完成对第一条指令的取指操作后，对该指令进行 分析\n对第一条指令进行 执行 ：\n将IR中保存的指令的地址码送至MAR（用Ad(IR)→MAR）对内存进行寻址，并命令内存做读操作，此时所寻址的存储单元（“0000000101”号存储单元，即5号存储单元）的内容“a”被送入MDR内。 将MDR的内容传送至运算器的ACC（用(MDR)→ACC表示）。 例题 【习题3】存放当前指令的寄存器是（C）。\nA. PC B. MDR C. IR D. MAR\n【习题4】用来存放当前欲执行指令的地址的寄存器是（B）。\nA. IR B. PC C. MAR D. CU\n解析：\n指令寄存器IR用来存放 当前的指令 IR的内容来自存储器数据寄存器MDR。 程序计数器PC用来存放 当前欲执行指令的地址 PC与存储器地址寄存器MAR之间有一条直接通路。 PC自动形成下一条指令的地址（“自动加1”功能） 【习题7】若存储字长为8位，MAR的位数为16位，则存储体的总容量为（B）。\nA.128B B.64KB C.128KB D.256KB\n解析：\n2.1 数据表示的相关基本概念 数字分类：\n无符号数：3238264832 有符号数：-1056702464（整数）、-0.491943359375（纯小数）、-8.25（带小数） 数字表示方式：\n整数：小数点位置固定 纯小数：小数点位置固定 定点数：小数点位置固定 浮点数：小数点位置浮动 2.2 进位计数制及其数据之间的相互转换 常用的进位计数制有十进制、二进制、八进制、十六进制\n任意进制-\u0026gt;十进制 常用进制数对照表 十进制 二进制 八进制 十六进制 0 0000 0 0 1 0001 1 1 2 0010 2 2 3 0011 3 3 4 0100 4 4 5 0101 5 5 6 0110 6 6 7 0111 7 7 8 1000 10 8 9 1001 11 9 10 1010 12 A 11 1011 13 B 12 1100 14 C 13 1101 15 D 14 1110 16 E 15 1111 17 F 二进制-\u0026gt;其他进制 3位二进制数 ↔ 1位八进制数\n举例：\n二进制：$(1011001100.11011)_2$ 转换为八进制：$(1314.66)_8$ 转换过程：\n将二进制数按每 三位一组 进行分组：\n小数部分：110 110（后面少位则补零）\n整数部分：001 011 001 100（前面少位则补零）\n4位二进制数 ↔ 1位十六进制数\n举例：\n二进制：$(1011001100.11011)_2$ 转换为十六进制：$(2CC.D8)_{16}$ 转换过程：\n将二进制数按每 四位一组 进行分组：\n小数部分：1101 1000\n整数部分：0010 1100 1100\n十进制-\u0026gt;任意进制 2.3.1 原码 符号位 ，值为 0表示正数，1表示负数\n数值位 (二进制)，为真值的绝对值\n原码表示又称为 带符号的绝对值表示\n定点整数的原码定义 假设真值 x 为 定点整数 ，n 为 x 的原码表示中数值位的位数（比特数量）： $$ [x]_原 = \\begin{cases} 0 \u0026 \\text{} 0 \\leq x\u003c2^n \\\\ 2^n + |x| \u0026\\text{} -2^n","date":"2025-09-10T00:00:00Z","permalink":"/p/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/","title":"计算机组成原理"},{"content":"参考网站 Maven基础 - Java教程 - 廖雪峰的官方网站\nMaven 基础 maven介绍 在了解Maven之前，我们先来看看一个Java项目需要的东西。首先，我们需要确定引入哪些依赖包。例如，如果我们需要用到commons logging，我们就必须把commons logging的jar包放入classpath。如果我们还需要log4j，就需要把log4j相关的jar包都放到classpath中。这些就是依赖包的管理。\n其次，我们要确定项目的目录结构。例如，src目录存放Java源码，resources目录存放配置文件，bin目录存放编译生成的.class文件。\n此外，我们还需要配置环境，例如JDK的版本，编译打包的流程，当前代码的版本号。\n最后，除了使用Eclipse这样的IDE进行编译外，我们还必须能通过命令行工具进行编译，才能够让项目在一个独立的服务器上编译、测试、部署。\n这些工作难度不大，但是非常琐碎且耗时。如果每一个项目都自己搞一套配置，肯定会一团糟。我们需要的是一个标准化的Java项目管理和构建工具。\nMaven就是是专门为Java项目打造的管理和构建工具，它的主要功能有：\n提供了一套标准化的项目结构； 提供了一套标准化的构建流程（编译，测试，打包，发布……）； 提供了一套依赖管理机制。 Maven项目结构 一个使用Maven管理的普通的Java项目，它的目录结构默认如下：\n1 2 3 4 5 6 7 8 9 10 a-maven-project ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ └── resources │ └── test │ ├── java │ └── resources └── target 项目的根目录a-maven-project是项目名，它有一个项目描述文件pom.xml，存放Java源码的目录是src/main/java，存放资源文件的目录是src/main/resources，存放测试源码的目录是src/test/java，存放测试资源的目录是src/test/resources，最后，所有编译、打包生成的文件都放在target目录里。这些就是一个Maven项目的标准目录结构。\n所有的目录结构都是约定好的标准结构，我们千万不要随意修改目录结构。使用标准结构不需要做任何配置，Maven就可以正常使用。\n我们再来看最关键的一个项目描述文件pom.xml，它的内容长得像下面：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;project ...\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.itranswarp.learnjava\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;maven.compiler.release\u0026gt;17\u0026lt;/maven.compiler.release\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-simple\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.16\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 其中，groupId类似于Java的包名，通常是公司或组织名称，artifactId类似于Java的类名，通常是项目名称，再加上version，一个Maven工程就是由groupId，artifactId和version作为唯一标识。\n我们在引用其他第三方库的时候，也是通过这3个变量确定。例如，依赖org.slfj4:slf4j-simple:2.0.16：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-simple\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.16\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 使用\u0026lt;dependency\u0026gt;声明一个依赖后，Maven就会自动下载这个依赖包并把它放到classpath中。\n另外，注意到\u0026lt;properties\u0026gt;定义了一些属性，常用的属性有：\nproject.build.sourceEncoding：表示项目源码的字符编码，通常应设定为UTF-8； maven.compiler.release：表示使用的JDK版本，例如21； maven.compiler.source：表示Java编译器读取的源码版本； maven.compiler.target：表示Java编译器编译的Class版本。 从Java 9开始，推荐使用maven.compiler.release属性，保证编译时输入的源码和编译输出版本一致。如果源码和输出版本不同，则应该分别设置maven.compiler.source和maven.compiler.target。\n通过\u0026lt;properties\u0026gt;定义的属性，就可以固定JDK版本，防止同一个项目的不同的开发者各自使用不同版本的JDK。\n小结 Maven是一个Java项目的管理和构建工具：\nMaven使用pom.xml定义项目内容，并使用预设的目录结构； 在Maven中声明一个依赖项可以自动下载并导入classpath； Maven使用groupId，artifactId和version唯一定位一个依赖。 依赖管理 如果我们的项目依赖第三方的jar包，例如commons logging，那么问题来了：commons logging发布的jar包在哪下载？\n如果我们还希望依赖log4j，那么使用log4j需要哪些jar包？\n类似的依赖还包括：JUnit，JavaMail，MySQL驱动等等，一个可行的方法是通过搜索引擎搜索到项目的官网，然后手动下载zip包，解压，放入classpath。但是，这个过程非常繁琐。\nMaven解决了依赖管理问题。例如，我们的项目依赖abc这个jar包，而abc又依赖xyz这个jar包：\n1 2 3 4 5 6 7 8 9 10 11 12 13 ┌──────────────┐ │Sample Project│ └──────────────┘ │ ▼ ┌──────────────┐ │ abc │ └──────────────┘ │ ▼ ┌──────────────┐ │ xyz │ └──────────────┘ 当我们声明了abc的依赖时，Maven自动把abc和xyz都加入了我们的项目依赖，不需要我们自己去研究abc是否需要依赖xyz。\n因此，Maven的第一个作用就是解决依赖管理。我们声明了自己的项目需要abc，Maven会自动导入abc的jar包，再判断出abc需要xyz，又会自动导入xyz的jar包，这样，最终我们的项目会依赖abc和xyz两个jar包。\n我们来看一个复杂依赖示例：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.2.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 当我们声明一个spring-boot-starter-web依赖时，Maven会自动解析并判断最终需要大概二三十个其他依赖：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 spring-boot-starter-web spring-boot-starter spring-boot sprint-boot-autoconfigure spring-boot-starter-logging logback-classic logback-core slf4j-api jcl-over-slf4j slf4j-api jul-to-slf4j slf4j-api log4j-over-slf4j slf4j-api spring-core snakeyaml spring-boot-starter-tomcat tomcat-embed-core tomcat-embed-el tomcat-embed-websocket tomcat-embed-core jackson-databind ... 如果我们自己去手动管理这些依赖是非常费时费力的，而且出错的概率很大。\n依赖关系 Maven定义了几种依赖关系，分别是compile、test、runtime和provided：\nscope 说明 示例 compile 编译时需要用到该jar包（默认） commons-logging test 编译Test时需要用到该jar包 junit runtime 编译时不需要，但运行时需要用到 mysql provided 编译时需要用到，但运行时由JDK或某个服务器提供 servlet-api 其中，默认的compile是最常用的，Maven会把这种类型的依赖直接放入classpath。\ntest依赖表示仅在测试时使用，正常运行时并不需要。最常用的test依赖就是JUnit：\n1 2 3 4 5 6 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.jupiter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-jupiter-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.2\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; runtime依赖表示编译时不需要，但运行时需要。最典型的runtime依赖是JDBC驱动，例如MySQL驱动：\n1 2 3 4 5 6 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.1.48\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; provided依赖表示编译时需要，但运行时不需要。最典型的provided依赖是Servlet API，编译的时候需要，但是运行时，Servlet服务器内置了相关的jar，所以运行期不需要：\n1 2 3 4 5 6 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;jakarta.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jakarta.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.0.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 最后一个问题是，Maven如何知道从何处下载所需的依赖？也就是相关的jar包？答案是Maven维护了一个中央仓库（repo1.maven.org），所有第三方库将自身的jar以及相关信息上传至中央仓库，Maven就可以从中央仓库把所需依赖下载到本地。\nMaven并不会每次都从中央仓库下载jar包。一个jar包一旦被下载过，就会被Maven自动缓存在本地目录（用户主目录的.m2目录），所以，除了第一次编译时因为下载需要时间会比较慢，后续过程因为有本地缓存，并不会重复下载相同的jar包。\n唯一ID 对于某个依赖，Maven只需要3个变量即可唯一确定某个jar包：\ngroupId：属于组织的名称，类似Java的包名； artifactId：该jar包自身的名称，类似Java的类名； version：该jar包的版本。 通过上述3个变量，即可唯一确定某个jar包。Maven通过对jar包进行PGP签名确保任何一个jar包一经发布就无法修改。修改已发布jar包的唯一方法是发布一个新版本。\n因此，某个jar包一旦被Maven下载过，即可永久地安全缓存在本地。\n注：只有以-SNAPSHOT结尾的版本号会被Maven视为开发版本，开发版本每次都会重复下载，这种SNAPSHOT版本只能用于内部私有的Maven repo，公开发布的版本不允许出现SNAPSHOT。\n后续我们在表示Maven依赖时，使用简写形式groupId:artifactId:version，例如：org.slf4j:slf4j-api:2.0.4。\nMaven镜像 除了可以从Maven的中央仓库下载外，还可以从Maven的镜像仓库下载。如果访问Maven的中央仓库非常慢，我们可以选择一个速度较快的Maven的镜像仓库。Maven镜像仓库定期从中央仓库同步：\n1 2 3 4 5 6 7 8 9 slow ┌───────────────────┐ ┌─────────────▶│Maven Central Repo.│ │ └───────────────────┘ │ │ │ │sync │ ▼ ┌───────┐ fast ┌───────────────────┐ │ User │─────────▶│Maven Mirror Repo. │ └───────┘ └───────────────────┘ 中国区用户可以使用阿里云提供的Maven镜像仓库。使用Maven镜像仓库需要一个配置，在用户主目录下进入.m2目录，创建一个settings.xml配置文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;settings\u0026gt; \u0026lt;mirrors\u0026gt; \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;aliyun\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;aliyun\u0026lt;/name\u0026gt; \u0026lt;mirrorOf\u0026gt;central\u0026lt;/mirrorOf\u0026gt; \u0026lt;!-- 国内推荐阿里云的Maven镜像 --\u0026gt; \u0026lt;url\u0026gt;https://maven.aliyun.com/repository/central\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; \u0026lt;/mirrors\u0026gt; \u0026lt;/settings\u0026gt; 配置镜像仓库后，Maven的下载速度就会非常快。\n搜索第三方组件 最后一个问题：如果我们要引用一个第三方组件，比如okhttp，如何确切地获得它的groupId、artifactId和version？方法是通过search.maven.org搜索关键字，找到对应的组件后，直接复制\n命令行编译 在命令中，进入到pom.xml所在目录，输入以下命令：\n1 $ mvn clean package 如果一切顺利，即可在target目录下获得编译后自动打包的jar。\n在IDE中使用Maven 几乎所有的IDE都内置了对Maven的支持。在Eclipse中，可以直接创建或导入Maven项目。如果导入后的Maven项目有错误，可以尝试选择项目后点击右键，选择Maven - Update Project\u0026hellip;更新\n小结 Maven通过解析依赖关系确定项目所需的jar包，常用的4种scope有：compile（默认），test，runtime和provided；\nMaven从中央仓库下载所需的jar包并缓存在本地；\n可以通过镜像仓库加速下载。\n构建流程 Maven不但有标准化的项目结构，而且还有一套标准化的构建流程，可以自动化实现编译，打包，发布，等等。\nLifecycle和Phase 使用Maven时，我们首先要了解什么是Maven的生命周期（lifecycle）。\nMaven的生命周期由一系列阶段（phase）构成，以内置的生命周期default为例，它包含以下phase：\nvalidate 校验 initialize 初始化 generate-sources 生成源码 process-sources 处理源码 generate-resources 生成资源 process-resources 处理资源 compile 编译 process-classes 处理-classes generate-test-sources 生成测试源码 process-test-sources 处理测试源码 generate-test-resources 生成测试资源 process-test-resources 处理测试资源 test-compile 测试编译 process-test-classes 处理测试-classes test 测试 prepare-package 准备打包 package 打包 pre-integration-test 预综合测试 integration-test 综合测试 post-integration-test 综合测试之后 verify 核实 install 安装 deploy 部署 如果我们运行mvn package，Maven就会执行default生命周期，它会从开始一直运行到package这个phase为止：\nvalidate initialize \u0026hellip; prepare-package package 如果我们运行mvn compile，Maven也会执行default生命周期，但这次它只会运行到compile，即以下几个phase：\nvalidate initialize \u0026hellip; process-resources compile Maven另一个常用的生命周期是clean，它会执行3个phase：\npre-clean clean （注意这个clean不是lifecycle而是phase） post-clean 所以，我们使用mvn这个命令时，后面的参数是phase，Maven自动根据生命周期运行到指定的phase。\n更复杂的例子是指定多个phase，例如，运行mvn clean package，Maven先执行clean生命周期并运行到clean这个phase，然后执行default生命周期并运行到package这个phase，实际执行的phase如下：\npre-clean clean （注意这个clean是phase） validate （开始执行default生命周期的第一个phase） initialize \u0026hellip; prepare-package package 在实际开发过程中，经常使用的命令有：\nmvn clean：清理所有生成的class和jar；\nmvn clean compile：先清理，再执行到compile；\nmvn clean test：先清理，再执行到test，因为执行test前必须执行compile，所以这里不必指定compile；\nmvn clean package：先清理，再执行到package。\n大多数phase在执行过程中，因为我们通常没有在pom.xml中配置相关的设置，所以这些phase什么事情都不做。\n经常用到的phase其实只有几个：\nclean：清理 compile：编译 test：运行测试 package：打包 Goal 执行一个phase又会触发一个或多个goal：\n执行的Phase 对应执行的Goal compile compiler:compile test compiler:testCompile surefire:test goal的命名总是abc:xyz这种形式。\n其实我们类比一下就明白了：\nlifecycle相当于Java的package，它包含一个或多个phase； phase相当于Java的class，它包含一个或多个goal； goal相当于class的method，它其实才是真正干活的。 大多数情况，我们只要指定phase，就默认执行这些phase默认绑定的goal，只有少数情况，我们可以直接指定运行一个goal，例如，启动Tomcat服务器：\n1 $ mvn tomcat:run 小结 Maven通过lifecycle、phase和goal来提供标准的构建流程。\n最常用的构建命令是指定phase，然后让Maven执行到指定的phase：\nmvn clean mvn clean compile mvn clean test mvn clean package 通常情况，我们总是执行phase默认绑定的goal，因此不必指定goal。\n","date":"2025-04-01T14:49:00+08:00","permalink":"/p/maven-%E5%9F%BA%E7%A1%80/","title":"Maven 基础"},{"content":"参考网站 多线程 - Java教程 - 廖雪峰的官方网站\n多数参考了廖老师的博客 非常好教程\n万字图解Java多线程 - 个人文章 - SegmentFault 思否\n相对没那么详细，就讲到同步锁和线程池，简洁清晰\n也补充了一些知识，例如线程状态，同步锁，生产者消费者模型\u0026hellip;\u0026hellip;\nJava 多线程 进程/线程 进程和线程的关系： 一个进程可以包含一个或多个线程 ，但至少会有一个线程。\n操作系统调度的 最小任务单位 其实不是进程，而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务，如何调度线程完全由操作系统决定，程序自己不能决定什么时候执行，以及执行多长时间。\n多任务既可以由多进程实现，也可以由单进程内的多线程实现，还可以混合多进程＋多线程\n和多线程相比，多进程的缺点在于：\n创建进程比创建线程 开销 大，尤其是在Windows系统上 进程间通信比线程间通信要慢，因为线程间通信就是读写同一个变量，速度很快 多进程的优点在于：\n多进程 稳定性 比多线程高，因为在多进程的情况下，一个进程崩溃不会影响其他进程 在多线程的情况下，任何一个线程崩溃会直接导致整个进程崩溃 多线程 Java语言内置了多线程支持：一个Java程序实际上是一个 JVM进程 ，JVM进程用一个主线程来执行main()方法，在main()方法内部，我们又可以启动多个线程。此外，JVM还有负责垃圾回收的其他工作线程等。\n和单线程相比，多线程编程的特点在于：多线程经常需要 读写共享数据，并且需要同步 。\n例如，播放电影时，就必须由一个线程播放视频，另一个线程播放音频，两个线程需要协调运行，否则画面和声音就不同步。因此，多线程编程的复杂度高，调试更困难。\n创建多线程 创建新线程 - Java教程 - 廖雪峰的官方网站\n要创建一个新线程非常容易，我们需要实例化一个Thread实例，然后调用它的start()方法：\n1 2 3 4 5 6 public class Main { public static void main(String args[]) { Thread t = new Thread(); t.start(); } } 令新线程能执行指定的代码，有以下几种方法：\n方法一 ：从Thread派生一个自定义类，然后覆写run()方法：\n1 2 3 4 5 6 7 8 9 10 11 12 public class Main { public static void main(String args[]) { Thread t = new Thread(); t.start(); } } class MyThread extends Thread { @Overread public void run(){ System.out.println(\u0026#34;start new thread!\u0026#34;); } } 执行上述代码，注意到start()方法会在内部自动调用实例的run()方法。\n方法二 ：创建Thread实例时，传入一个Runnable实例\n1 2 3 4 5 6 7 8 9 10 11 12 public class Main { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 启动新线程 } } class MyRunnable implements Runnable { @Override public void run() { System.out.println(\u0026#34;start new thread!\u0026#34;); } } 或者用Java 8引入的lambda语法进一步简写为：\n1 2 3 4 5 6 7 8 public class Main { public static void main(String[] args) { Thread t = new Thread(() -\u0026gt; { System.out.println(\u0026#34;start new thread!\u0026#34;); }); t.start(); // 启动新线程 } } 但是，直接调用 run() 方法，并不能实现多线程，当前线程也不会改变，而只是执行 run() 方法\n必须调用Thread实例的start()方法才能启动新线程，如果我们查看Thread类的源代码，会看到start()方法内部调用了一个private native void start0()方法，native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的，不是由Java代码实现的。\n使用线程和直接在 main() 方法中执行的 区别 ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main { public static void main(String[] args) { System.out.println(\u0026#34;main start...\u0026#34;); Thread t = new Thread() { public void run() { System.out.println(\u0026#34;thread run...\u0026#34;); System.out.println(\u0026#34;thread end.\u0026#34;); } }; t.start(); System.out.println(\u0026#34;main end...\u0026#34;); } } main 中命令执行顺序：\n打印 main start...\n创建 Thread 对象\nstart 启动新线程\n当start()方法被调用时，JVM就创建了一个新线程，我们通过实例变量t来表示这个新线程对象，并开始执行。\n打印 main end...\n但是，在 t 线程开始运行后， main 和 t 就 同时运行 了，此时程序本身无法确定线程的调度顺序\n要模拟并发执行的效果，我们可以在线程中调用Thread.sleep()，参数的单位是毫秒， sleep() 强迫当前线程 暂停 一段时间：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Main { public static void main(String[] args) { System.out.println(\u0026#34;main start...\u0026#34;); Thread t = new Thread() { public void run() { System.out.println(\u0026#34;thread run...\u0026#34;); try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println(\u0026#34;thread end.\u0026#34;); } }; t.start(); try { Thread.sleep(20); } catch (InterruptedException e) {} System.out.println(\u0026#34;main end...\u0026#34;); } } 线程的优先级 1 Thread.setPriority(int n) //默认为5 JVM自动把1（低）~10（高）的优先级映射到操作系统实际优先级上（不同操作系统有不同的优先级数量）。优先级高的线程被操作系统调度的优先级较高，操作系统对高优先级线程可能调度更频繁，但我们 决不能通过设置优先级来确保高优先级的线程一定会先执行 。cpu比较忙时，优先级高的线程获取更多的时间片，cpu比较闲时，优先级设置基本没用\nyield() 方法会让运行中的线程切换到就绪状态，重新争抢cpu的时间片，争抢时是否获取到时间片看cpu的分配。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static native void yield(); Runnable r1 = () -\u0026gt; { int count = 0; for (;;){ log.info(\u0026#34;---- 1\u0026gt;\u0026#34; + count++); } }; Runnable r2 = () -\u0026gt; { int count = 0; for (;;){ Thread.yield(); log.info(\u0026#34; ---- 2\u0026gt;\u0026#34; + count++); } }; Thread t1 = new Thread(r1,\u0026#34;t1\u0026#34;); Thread t2 = new Thread(r2,\u0026#34;t2\u0026#34;); t1.start(); t2.start(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 运行结果 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129504 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129505 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129506 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129507 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129508 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129509 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129510 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129511 11:49:15.796 [t1] INFO thread.TestYield - ---- 1\u0026gt;129512 11:49:15.798 [t2] INFO thread.TestYield - ---- 2\u0026gt;293 11:49:15.798 [t1] INFO thread.TestYield - ---- 1\u0026gt;129513 11:49:15.798 [t1] INFO thread.TestYield - ---- 1\u0026gt;129514 11:49:15.798 [t1] INFO thread.TestYield - ---- 1\u0026gt;129515 11:49:15.798 [t1] INFO thread.TestYield - ---- 1\u0026gt;129516 11:49:15.798 [t1] INFO thread.TestYield - ---- 1\u0026gt;129517 11:49:15.798 [t1] INFO thread.TestYield - ---- 1\u0026gt;129518 如上述结果所示，t2线程每次执行时进行了yield()，线程1执行的机会明显比线程2要多。\n小结 Java用Thread对象表示一个线程，通过调用start()启动一个新线程 一个线程对象只能调用一次start()方法 线程的执行代码写在run()方法中 线程调度由操作系统决定，程序本身无法决定调度顺序 Thread.sleep()可以把当前线程暂停一段时间 线程的阻塞 使得线程阻塞的方式有下面几种：\nBIO阻塞，即使用了阻塞式的io流 sleep(long time) 让线程休眠进入阻塞状态 a.join() 调用该方法的线程进入阻塞，等待a线程执行完恢复运行 sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态 获得锁之后调用wait()方法 也会让线程进入阻塞状态 LockSupport.park() 让线程进入阻塞状态 Thread.sleep() 使线程休眠，会将运行中的线程进入阻塞状态。当休眠时间结束后，重新争抢cpu的时间片继续运行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 方法的定义 native方法 public static native void sleep(long millis) throws InterruptedException; try { // 休眠2秒 // 该方法会抛出 InterruptedException异常 即休眠过程中可被中断，被中断后抛出异常 Thread.sleep(2000); } catch (InterruptedException异常 e) { } try { // 使用TimeUnit的api可替代 Thread.sleep TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } Thread.join() 一个线程还可以等待另一个线程直到其运行结束。例如，main线程在启动t线程后，可以通过t.join()等待t线程结束后再继续运行：\n1 2 3 4 5 6 7 8 9 10 11 public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -\u0026gt; { System.out.println(\u0026#34;hello\u0026#34;); });\t//java8 lambda方式 System.out.println(\u0026#34;start\u0026#34;); t.start(); // 启动t线程 t.join(); // 此处main线程会等待t结束 System.out.println(\u0026#34;end\u0026#34;); } } 当main线程对线程对象t调用join()方法时，主线程将等待变量t表示的线程运行结束，即join就是指等待该线程结束， 然后才继续往下执行自身线程 。所以，上述代码打印顺序可以肯定是main线程先打印start，t线程再打印hello，main线程最后再打印end。\n如果t线程已经结束，对实例t调用join()会立刻返回。此外，join(long)的重载方法也可以指定一个等待时间，超过等待时间后就不再继续等待。\n小结 线程阻塞的常见方式：BIO阻塞、sleep()、join()、未获取锁（synchronized/ReentrantLock）、wait()、LockSupport.park()。 sleep() ：让线程休眠指定时间，可被中断，推荐用TimeUnit增强可读性。 join() ：让当前线程等待目标线程执行完毕，常用于控制线程执行顺序。 阻塞与恢复：线程进入阻塞后，需等待特定条件（如时间结束、锁释放、目标线程完成）才能恢复运行。 中断线程 中断线程 - Java教程 - 廖雪峰的官方网站\n如果线程需要执行一个长时间任务，就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号，该线程收到信号后结束执行run()方法，使得自身线程能立刻结束运行。\n例如，从网络下载一个100M的文件，如果网速很慢，用户等得不耐烦，就可能在下载过程中点“取消”，这时，程序就需要中断下载线程的执行。\nThread.interrupt 中断一个线程非常简单，只需要在其他线程中对目标线程调用interrupt()方法，目标线程需要反复检测自身状态是否是interrupted状态， 如果是，就立刻结束运行 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1); // 暂停1毫秒 t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println(\u0026#34;end\u0026#34;); } } class MyThread extends Thread { public void run() { int n = 0; while (! isInterrupted()) { n++; System.out.println(n + \u0026#34; hello!\u0026#34;); } } } 上述代码，main线程通过调用t.interrupt()方法中断t线程，但是要注意，interrupt()方法 仅仅向t线程发出了“中断请求” ，至于t线程 是否能立刻响应，要看具体代码 。而t线程的while循环会检测isInterrupted()，所以上述代码能正确响应interrupt()请求，使得自身立刻结束运行run()方法。\n如果线程处于等待状态，例如，t.join()会让main线程进入等待状态，此时，如果对main线程调用interrupt()， join()方法会立刻抛出InterruptedException ，因此，目标线程只要捕获到join()方法抛出的InterruptedException，就说明有其他线程对其调用了interrupt()方法，通常情况下该线程应该立刻结束运行。\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 public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println(\u0026#34;end\u0026#34;); } } class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 启动hello线程 try { hello.join(); // 等待hello线程结束 } catch (InterruptedException e) { System.out.println(\u0026#34;interrupted!\u0026#34;); } hello.interrupt(); } } class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; System.out.println(n + \u0026#34; hello!\u0026#34;); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } } main线程通过调用t.interrupt()从而通知t线程中断 此时t线程正位于hello.join()的等待中，此方法会立刻结束等待并抛出InterruptedException 在t线程中捕获了InterruptedException，准备结束该线程 t线程结束前，对hello线程也进行了interrupt()调用通知其中断 running标志位 另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行，在外部线程中，通过把HelloThread.running置为false，就可以让线程结束：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 标志位置为false } } class HelloThread extends Thread { public volatile boolean running = true; public void run() { int n = 0; while (running) { n ++; System.out.println(n + \u0026#34; hello!\u0026#34;); } System.out.println(\u0026#34;end!\u0026#34;); } } 注意到HelloThread的标志位boolean running是一个 线程间共享的变量 。线程间共享变量需要使用volatile关键字标记，确保 每个线程都能读取到更新后的变量值 。\nvolatile 的用处 为什么要对线程间共享的变量用关键字volatile声明？这涉及到Java的内存模型。在Java虚拟机中，变量的值保存在主内存中，但是，当线程访问变量时，它会先获取一个副本，并保存在自己的工作内存中。如果线程修改了变量的值，虚拟机会在某个时刻把修改后的值回写到主内存，但是， 这个时间是不确定的 ！\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 这图画得真有水平罢 ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ Main Memory │ │ ┌───────┐┌───────┐┌───────┐ │ │ var A ││ var B ││ var C │ │ └───────┘└───────┘└───────┘ │ │ ▲ │ ▲ │ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ │ │ │ │ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ▼ │ ▼ │ │ ┌───────┐ │ │ ┌───────┐ │ │ var A │ │ var C │ │ └───────┘ │ │ └───────┘ │ Thread 1 Thread 2 └ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘ 这会导致如果一个线程更新了某个变量，另一个线程读取的值可能还是更新前的。\n例如，主内存的变量a = true，线程1执行a = false时，它在此刻仅仅是把变量a的副本变成了false，主内存的变量a还是true，在JVM把修改后的a回写到主内存之前，其他线程读取到的a的值仍然是true，这就造成了 多线程之间共享的变量不一致 。\n因此，volatile关键字的目的是告诉虚拟机：\n每次访问变量时，总是获取主内存的最新值； 每次修改变量后，立刻回写到主内存。 volatile关键字解决的是可见性问题：当一个线程 修改了某个共享变量的值，其他线程能够立刻看到修改后的值 。\n如果我们去掉volatile关键字，运行上述程序，发现效果和带volatile差不多，这是因为在x86的架构下，JVM回写主内存的速度非常快，但是，换成ARM的架构，就会有显著的延迟。\n小结 对目标线程调用interrupt()方法可以请求中断一个线程，目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态，该线程会捕获到InterruptedException；\n目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程；\n通过标志位判断需要正确使用volatile关键字；\nvolatile关键字解决了共享变量在线程间的可见性问题。\n线程状态 万字图解Java多线程 - 个人文章 - SegmentFault 思否\n线程的状态 - Java教程 - 廖雪峰的官方网站\n系统 - 五种状态 线程的状态可从 操作系统层面分为五种状态\n初始状态：创建线程对象时的状态 可运行状态(就绪状态)：调用 start() 方法后进入就绪状态，也就是准备好被cpu调度执行 运行状态：线程获取到cpu的时间片， 执行 run() 方法的逻辑 阻塞状态: 线程被阻塞，放弃cpu的时间片，等待解除阻塞重新回到就绪状态争抢时间片 终止状态: 线程执行完成或抛出异常后的状态 Java - 六种状态 在Java程序中，一个线程对象只能调用一次start()方法启动新线程，并在新线程中执行run()方法。一旦run()方法执行完毕，线程就结束了。因此，Java线程的状态有以下几种：\nNEW 线程对象被创建 Runnable 线程调用了 start() 方法后进入该状态，该状态包含了三种情况 就绪状态 :等待cpu分配时间片 运行状态:进入Runnable方法执行任务 阻塞状态:BIO 执行阻塞式io流时的状态 Blocked 没获取到锁时的阻塞状态(同步锁章节会细说) WAITING 调用 wait() join() 等方法后的状态 TIMED_WAITING 调用 sleep(time) wait(time) join(time) 等方法后的状态 TERMINATED 线程执行完成或抛出异常后的状态 当线程启动后，它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换，直到最后变成Terminated状态，线程终止。\n线程终止的原因有：\n线程正常终止：run()方法执行到return语句返回； 线程意外终止：run()方法因为未捕获的异常导致线程终止； 对某个线程的Thread实例调用stop()方法强制终止（强烈不推荐使用）。 Thread类中的核心方法 方法名称 是否static 方法说明 start() 否 让线程启动，进入就绪状态,等待cpu分配时间片 run() 否 重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑 yield() 是 线程的礼让，使得获取到cpu时间片的线程进入就绪状态，重新争抢时间片 sleep(time) 是 线程休眠固定时间，进入阻塞状态，休眠时间完成后重新争抢时间片,休眠可被打断 join()/join(time) 否 调用线程对象的join方法，调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复，重新争抢时间片 isInterrupted() 否 获取线程的打断标记，true:被打断，false：没有被打断。调用后不会修改打断标记 interrupt() 否 打断线程，抛出InterruptedException异常的方法均可被打断，但是打断后不会修改打断标记，正常执行的线程被打断后会修改打断标记 interrupted() 否 获取线程的打断标记。调用后会清空打断标记 stop() 否 停止线程运行 不推荐 suspend() 否 挂起线程 不推荐 resume() 否 恢复线程运行 不推荐 currentThread() 是 获取当前线程 Object中与线程相关方法\n方法名称 方法说明 wait()/wait(long timeout) 获取到锁的线程进入阻塞状态 notify() 随机唤醒被wait()的一个线程 notifyAll(); 唤醒被wait()的所有线程，重新争抢时间片 守护线程 守护线程 - Java教程 - 廖雪峰的官方网站\nJava程序入口就是由JVM启动main线程，main线程又可以启动其他线程。当所有线程都运行结束时，JVM退出，进程结束。\n如果有一个线程没有退出，JVM进程就不会退出。所以，必须保证所有线程都能及时结束。\n但是有一种线程的目的就是无限循环，例如，一个定时触发任务的线程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 class TimerThread extends Thread { @Override public void run() { while (true) { System.out.println(LocalTime.now()); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } } } 如果这个线程不结束，JVM进程就无法结束。问题是，由谁负责结束这个线程？\n然而这类线程经常没有负责人来负责结束它们。但是，当其他线程结束时，JVM进程又必须要结束，怎么办？\n答案是使用守护线程（Daemon Thread）。\n守护线程是指为其他线程服务的线程。在JVM中， 所有非守护线程都执行完毕后 ，无论有没有守护线程，虚拟机都会自动退出。\n因此，JVM退出时，不必关心守护线程是否已结束。\n如何创建守护线程呢？方法和普通线程一样，只是在调用start()方法前， 调用setDaemon(true)把该线程标记为守护线程 ：\n1 2 3 Thread t = new MyThread(); t.setDaemon(true); t.start(); 在守护线程中，编写代码要注意： 守护线程不能持有任何需要关闭的资源 ，例如打开文件等，因为虚拟机退出时，守护线程没有任何机会来关闭文件，这会导致数据丢失。\n小结 守护线程是为其他线程服务的线程；\n所有非守护线程都执行完毕后，虚拟机退出，守护线程随之结束；\n守护线程不能持有需要关闭的资源（如打开文件等）。\n线程同步 线程同步 - Java教程 - 廖雪峰的官方网站\n当多个线程同时运行时，线程的调度由操作系统决定，程序本身无法决定。因此，任何一个线程都有可能在任何指令处被操作系统暂停，然后在某个时间段后继续执行。\n这个时候，有个单线程模型下不存在的问题就来了：如果多个线程同时读写共享变量，会出现数据不一致的问题。\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 // 多线程 public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static int count = 0; } class AddThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { Counter.count += 1; } } } class DecThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { Counter.count -= 1; } } } 上面的代码很简单，两个线程同时对一个int变量进行操作，一个加10000次，一个减10000次，最后结果应该是0，但是，每次运行，结果实际上都是不一样的。\n这是因为对变量进行读取和写入时，结果要正确， 必须保证是原子操作 。原子操作是指不能被中断的一个或一系列操作。\n例如，对于语句：\n1 n = n + 1; 看上去是一行语句，实际上对应了3条指令：\n1 2 3 ILOAD IADD ISTORE 我们假设n的值是100，如果两个线程同时执行n = n + 1，得到的结果很可能不是102，而是101，原因在于：\n1 2 3 4 5 6 7 8 9 10 11 ┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │ILOAD (100) │ │ │ILOAD (100) │ │IADD │ │ISTORE (101) │IADD │ │ISTORE (101) │ ▼ ▼ 如果线程1在执行ILOAD后被操作系统中断，此刻如果线程2被调度执行，它执行ILOAD后获取的值仍然是100，最终结果被两个线程的ISTORE写入后变成了101，而不是期待的102。\n这说明多线程模型下，要保证逻辑正确，对共享变量进行读写时， 必须保证一组指令以原子方式执行：即某一个线程执行时，其他线程必须等待 ：\nsynchronized 同步锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │-- lock -- │ │ILOAD (100) │ │IADD │ │ISTORE (101) │ │-- unlock -- │ │ │-- lock -- │ │ILOAD (101) │ │IADD │ │ISTORE (102) │ │-- unlock -- ▼ ▼ 通过加锁和解锁的操作，就能保证3条指令总是在一个线程执行期间，不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行，其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后，其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section)，任何时候临界区最多只有一个线程能执行。\n可见， 保证一段代码的原子性就是通过加锁和解锁实现的 。Java程序使用synchronized关键字对一个对象进行加锁：\n1 2 3 synchronized(lock) { n = n + 1; } synchronized保证了代码块在 任意时刻最多只有一个线程能执行 。我们把上面的代码用synchronized改写如下：\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 // 多线程 public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static final Object lock = new Object(); public static int count = 0; } class AddThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { synchronized(Counter.lock) { Counter.count += 1; } } } } class DecThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { synchronized(Counter.lock) { Counter.count -= 1; } } } } 注意到代码：\n1 2 3 synchronized(Counter.lock) { // 获取锁 ... } // 释放锁 它表示用Counter.lock实例作为锁，两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时，必须先获得锁，才能进入代码块进行。执行结束后，在synchronized语句块结束会自动释放锁。这样一来，对Counter.count变量进行读写就不可能同时进行。上述代码无论运行多少次，最终结果都是0。\n使用synchronized解决了多线程同步访问共享变量的正确性问题。但是，它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外，加锁和解锁需要消耗一定的时间，所以，synchronized会降低程序的执行效率。\n我们来概括一下如何使用synchronized：\n找出修改共享变量的线程代码块； 选择一个共享实例作为锁； 使用synchronized(lockObject) { ... }。 在使用synchronized的时候， 不必担心抛出异常 。因为无论是否有异常，都会在synchronized结束处正确释放锁：\n1 2 3 4 5 6 7 8 public void add(int m) { synchronized (obj) { if (m \u0026lt; 0) { throw new RuntimeException(); } this.value += m; } // 无论有无异常，都会在此释放锁 } 此外，多个线程各自都可以同时获得锁：因为JVM只保证同一个锁在任意时刻只能被一个线程获取， 但两个不同的锁在同一时刻可以被两个线程分别获取 。\n因此，使用synchronized的时候， 获取到的是哪个锁非常重要 。锁对象如果不对，代码逻辑就不对。\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 55 56 57 58 59 60 public class Main { public static void main(String[] args) throws Exception { var ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() }; for (var t : ts) { t.start(); } for (var t : ts) { t.join(); } System.out.println(Counter.studentCount); System.out.println(Counter.teacherCount); } } class Counter { public static final Object lockStudent = new Object(); public static final Object lockTeacher = new Object(); public static int studentCount = 0; public static int teacherCount = 0; } class AddStudentThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { synchronized(Counter.lockStudent) { Counter.studentCount += 1; } } } } class DecStudentThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { synchronized(Counter.lockStudent) { Counter.studentCount -= 1; } } } } class AddTeacherThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { synchronized(Counter.lockTeacher) { Counter.teacherCount += 1; } } } } class DecTeacherThread extends Thread { public void run() { for (int i=0; i\u0026lt;10000; i++) { synchronized(Counter.lockTeacher) { Counter.teacherCount -= 1; } } } } 不需要 synchronized 的操作 JVM规范定义了几种原子操作：\n基本类型（long和double除外）赋值，例如：int n = m； 引用类型赋值，例如：List\u0026lt;String\u0026gt; list = anotherList。 long和double是64位数据，JVM没有明确规定64位赋值操作是不是一个原子操作，不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。\n单条原子操作的语句不需要同步。例如：\n1 2 3 4 5 public void set(int m) { synchronized(lock) { this.value = m; } } 就不需要同步。\n对引用也是类似。例如：\n1 2 3 public void set(String s) { this.value = s; } 上述 赋值语句 并不需要同步。\n但是，如果是 多行赋值语句，就必须保证是同步操作 ，例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Point { int x; int y; public void set(int x, int y) { synchronized(this) { this.x = x; this.y = y; } } public int[] get() { synchronized(this) { return new int[]{x,y}; } } } 上面的读写，即( set(), get() )需要同步，在读的时候若是不同步，会造成程序的逻辑错误：\n1 2 3 4 5 public int[] get() { int[] copy = new int[2]; copy[0] = x; copy[1] = y; } 假定当前坐标是(100, 200)，那么当设置新坐标为(110, 220)时，上述未同步的多线程读到的值可能有：\n(100, 200)：x，y更新前； (110, 200)：x更新后，y更新前； (110, 220)：x，y更新后。 如果读取到(110, 200)，即读到了更新后的x，更新前的y，无法保证读取的多个变量状态保持一致。\n有些时候，通过一些巧妙的转换，可以把非原子操作变为原子操作。例如，上述代码如果改造成：\n1 2 3 4 5 6 7 class Point { int[] ps; public void set(int x, int y) { int[] ps = new int[] { x, y }; this.ps = ps; } } 就不再需要写同步，因为this.ps = ps是引用赋值的原子操作。而语句：\n1 int[] ps = new int[] { x, y }; 这里的ps是方法内部定义的局部变量，每个线程都会有各自的局部变量，互不影响，并且互不可见，并不需要同步。\n不过要注意，读方法在复制int[]数组的过程中仍然需要同步。\n不可变对象无需同步 不可变对象是指创建后状态不能被修改的对象。在 Java 中，典型的不可变对象包括：\nString List.of() 创建的不可变集合（Java 9+） 基本类型的包装类（如 Integer, Long 等） 如果多线程读写的是一个不可变对象，那么无需同步，因为不会修改对象的状态：\n1 2 3 4 5 6 7 8 9 class Data { List\u0026lt;String\u0026gt; names; void set(String[] names) { this.names = List.of(names); } List\u0026lt;String\u0026gt; get() { return this.names; } } 注意到set()方法内部创建了一个不可变List，这个List包含的对象也是不可变对象String，因此，整个List\u0026lt;String\u0026gt;对象都是不可变的，因此读写均无需同步。\n分析变量是否能被多线程访问时，首先要理清概念，多线程同时执行的是方法。对于下面这个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Status { List\u0026lt;String\u0026gt; names; int x; int y; void set(String[] names, int n) { List\u0026lt;String\u0026gt; ns = List.of(names); this.names = ns; int step = n * 10; this.x += step; this.y += step; } StatusRecord get() { return new StatusRecord(this.names, this.x, this.y); } } 如果有A、B两个线程，同时执行是指：\n可能同时执行set()； 可能同时执行get()； 可能A执行set()，同时B执行get()。 类的成员变量names、x、y显然能被多线程同时读写，但局部变量（包括方法参数）如果没有“逃逸”，那么只有当前线程可见。局部变量step仅在set()方法内部使用，因此每个线程同时执行set时都有一份独立的step存储在线程的栈上，互不影响，\n局部变量ns虽然每个线程也各有一份，但后续赋值 this.names = ns 对其他线程就变成可见了。对set()方法同步时，如果要最小化synchronized代码块，可以改写如下：\n1 2 3 4 5 6 7 8 9 10 void set(String[] names, int n) { // 局部变量其他线程不可见: List\u0026lt;String\u0026gt; ns = List.of(names); int step = n * 10; synchronized(this) { this.names = ns; this.x += step; this.y += step; } } 因此，深入理解多线程还需理解变量在栈上的存储方式，基本类型和引用类型的存储方式也不同。\n场景 是否需要同步 原因 不可变对象（如 List.of()） 否 对象不可变，多线程只能读取，无竞态条件。 局部变量（如 step） 否 线程私有，栈封闭。 成员变量赋值（如 this.names） 是 引用可能被多线程同时修改，需同步或 volatile。 复合操作（如 x += step） 是 非原子操作（读取-修改-写入），需同步。 小结 多线程同时读写共享变量时，可能会造成逻辑错误，因此需要通过synchronized同步；\n同步的本质就是给指定对象加锁，加锁后才能继续执行后续代码；\n注意加锁对象必须是同一个实例；\n对JVM定义的单个原子操作不需要同步。\n线程同步方法 线程安全 如果一个类被设计为允许多线程正确访问，我们就说这个类就是“线程安全”的（thread-safe），Java标准库的java.lang.StringBuffer也是线程安全的。\n还有一些 不变类 ，例如String，Integer，LocalDate，它们的所有成员变量都是final，多线程同时访问时只能读不能写，这些不变类也是线程安全的。\n最后，类似Math这些 只提供静态方法，没有成员变量的类 ，也是线程安全的。\n除了上述几种少数情况，大部分类，例如ArrayList，都是 非线程安全的类 ，我们不能在多线程中修改它们。但是，如果所有线程都 只读取，不写入 ，那么ArrayList是可以安全地在线程间共享的。\n没有特殊说明时，一个类 默认是非线程安全的 。\n例如下面的Counter类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Counter { private int count = 0; public void add(int n) { synchronized(this) { count += n; } } public void dec(int n) { synchronized(this) { count -= n; } } public int get() { return count; } } 这样一来，线程调用add()、dec()方法时，它不必关心同步逻辑，因为synchronized代码块在add()、dec()方法内部。并且，我们注意到，synchronized锁住的对象是this，即当前实例，这又使得创建多个Counter实例的时候，它们之间互不影响，可以并发执行\nsynchronized 修饰 我们再观察Counter的代码：\n1 2 3 4 5 6 7 8 public class Counter { public void add(int n) { synchronized(this) { count += n; } } ... } 当我们锁住的是this实例时，实际上可以用synchronized修饰这个方法。下面两种写法是等价的：\n1 2 3 4 5 public void add(int n) { synchronized(this) { // 锁住this count += n; } // 解锁 } 写法二：\n1 2 3 public synchronized void add(int n) { // 锁住this count += n; } // 解锁 因此， 用synchronized修饰的方法就是同步方法 ，它表示整个方法都必须用this实例加锁。\n对于static方法，是没有this实例的，因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例，因此， 对static方法添加synchronized，锁住的是该类的Class实例 。上述synchronized static方法实际上相当于：\n1 2 3 4 5 6 7 public class Counter { public static void test(int n) { synchronized(Counter.class) { ... } } } 小结 用synchronized修饰方法可以把整个方法变为同步代码块，synchronized方法加锁对象是this；\n通过合理的设计和数据封装可以让一个类变为“线程安全”；\n一个类没有特殊说明，默认不是thread-safe；\n多线程能否安全访问某个非线程安全的实例，需要具体问题具体分析。\n死锁 可重入锁 Java的线程锁是可重入的锁。\n什么是可重入的锁？我们还是来看例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Counter { private int count = 0; public synchronized void add(int n) { if (n \u0026lt; 0) { dec(-n); } else { count += n; } } public synchronized void dec(int n) { count += n; } } 执行流程：\n调用add(-1)： 获取this锁：计数器=1，持有线程=当前线程 进入add方法后调用dec(1)： 再次获取this锁：发现当前线程已持有，计数器增加到2 退出dec方法： 计数器减到1 退出add方法： 计数器减到0，真正释放锁 观察synchronized修饰的add()方法，一旦线程执行到add()方法内部，说明它已经获取了当前实例的this锁。如果传入的n \u0026lt; 0，将在add()方法内部调用dec()方法。由于dec()方法也需要获取this锁，现在问题来了：\n对同一个线程，能否在获取到锁以后继续获取同一个锁？\n答案是肯定的。 JVM允许同一个线程重复获取同一个锁 ，这种能被同一个线程反复获取的锁，就叫做可重入锁。\n由于Java的线程锁是可重入锁，所以，获取锁的时候，不但要判断是否是第一次获取，还要记录这是第几次获取。每获取一次锁，记录+1，每退出synchronized块，记录-1，减到0的时候，才会真正释放锁。\n死锁 一个线程可以获取一个锁后，再继续获取另一个锁。例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void add(int m) { synchronized(lockA) { // 获得lockA的锁 this.value += m; synchronized(lockB) { // 获得lockB的锁 this.another += m; } // 释放lockB的锁 } // 释放lockA的锁 } public void dec(int m) { synchronized(lockB) { // 获得lockB的锁 this.another -= m; synchronized(lockA) { // 获得lockA的锁 this.value -= m; } // 释放lockA的锁 } // 释放lockB的锁 } 在获取多个锁的时候，不同线程获取多个不同对象的锁可能导致死锁。对于上述代码，线程1和线程2如果分别执行add()和dec()方法时：\n线程1：进入add()，获得lockA； 线程2：进入dec()，获得lockB。 随后：\n线程1：准备获得lockB，失败，等待中； 线程2：准备获得lockA，失败，等待中。 此时，两个线程各自持有不同的锁，然后各自试图获取对方手里的锁，造成了双方无限等待下去，这就是死锁。\n死锁发生后，没有任何机制能解除死锁，只能强制结束JVM进程。\n因此，在编写多线程应用时，要特别注意防止死锁。因为死锁一旦形成，就只能强制结束进程。\n那么我们应该如何避免死锁呢？答案是： 线程获取锁的顺序要一致 。即严格按照先获取lockA，再获取lockB的顺序，改写dec()方法如下：\n1 2 3 4 5 6 7 8 public void dec(int m) { synchronized(lockA) { // 获得lockA的锁 this.value -= m; synchronized(lockB) { // 获得lockB的锁 this.another -= m; } // 释放lockB的锁 } // 释放lockA的锁 } 小结 Java的synchronized锁是可重入锁；\n死锁产生的条件是多线程各自持有不同的锁，并互相试图获取对方已持有的锁，导致无限等待；\n避免死锁的方法是多线程获取锁的顺序要一致。\n线程通信 在Java程序中，synchronized解决了多线程竞争的问题。例如，对于一个任务管理器，多个线程同时往队列中添加任务，可以用synchronized加锁：\n1 2 3 4 5 6 7 class TaskQueue { Queue\u0026lt;String\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); public synchronized void addTask(String s) { this.queue.add(s); } } 但是synchronized并没有解决多线程协调的问题。\n仍然以上面的TaskQueue为例，我们再编写一个getTask()方法取出队列的第一个任务：\n1 2 3 4 5 6 7 8 9 10 11 12 13 class TaskQueue { Queue\u0026lt;String\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); public synchronized void addTask(String s) { this.queue.add(s); } public synchronized String getTask() { while (queue.isEmpty()) { } return queue.remove(); } } 上述代码看上去没有问题：getTask()内部先判断队列是否为空，如果为空，就循环等待，直到另一个线程往队列中放入了一个任务，while()循环退出，就可以返回队列的元素了。\n但实际上while()循环永远不会退出。因为线程在执行while()循环时，已经在getTask()入口获取了this锁，其他线程根本无法调用addTask()，因为addTask()执行条件也是获取this锁。\n因此，执行上述代码，线程会在getTask()中因为死循环而100%占用CPU资源。\n如果深入思考一下，我们想要的执行效果是：\n线程1可以调用addTask()不断往队列中添加任务； 线程2可以调用getTask()从队列中获取任务。如果队列为空，则getTask()应该等待，直到队列中至少有一个任务时再返回。 因此，多线程协调运行的原则就是：当条件不满足时，线程进入等待状态；当条件满足时，线程被唤醒，继续执行任务。\nwait() 对于上述TaskQueue，我们先改造getTask()方法，在条件不满足时，线程进入等待状态：\n1 2 3 4 5 6 public synchronized String getTask() { while (queue.isEmpty()) { this.wait(); } return queue.remove(); } 当一个线程执行到getTask()方法内部的while循环时，它必定已经获取到了this锁，此时，线程执行while条件判断，如果条件成立（队列为空），线程将执行this.wait()，进入等待状态。\n这里的关键是：wait()方法必须在 当前获取的锁对象 上调用，这里获取的是this锁，因此调用this.wait()。\n调用wait()方法后，线程进入等待状态，wait()方法不会返回，直到将来某个时刻， 线程从等待状态被其他线程唤醒后 ，wait()方法才会返回，然后，继续执行下一条语句。\n有些仔细的童鞋会指出：即使线程在getTask()内部等待，其他线程如果拿不到this锁，照样无法执行addTask()，肿么办？\n这个问题的关键就在于wait()方法的执行机制非常复杂。首先，它不是一个普通的Java方法，而是定义在Object类的一个native方法，也就是由JVM的C代码实现的。其次，必须在synchronized块中才能调用wait()方法， 因为wait()方法调用时，会释放线程获得的锁 ，wait()方法返回时，线程又会重新试图获得锁。\n因此，只能在锁对象上调用wait()方法。因为在getTask()中，我们获得了this锁，因此，只能在this对象上调用wait()方法：\n1 2 3 4 5 6 7 8 public synchronized String getTask() { while (queue.isEmpty()) { // 释放this锁: this.wait(); // 重新获取this锁 } return queue.remove(); } 当一个线程在this.wait()等待时，它就会释放this锁，从而使得其他线程能够在addTask()方法获得this锁。\nnotify() 现在我们面临第二个问题：如何让等待的线程被 重新唤醒 ，然后从wait()方法返回？答案是在相同的锁对象上调用notify()方法。我们修改addTask()如下：\n1 2 3 4 public synchronized void addTask(String s) { this.queue.add(s); this.notify(); // 唤醒在this锁等待的线程 } 注意到在往队列中添加了任务后，线程立刻对this锁对象调用notify()方法，这个方法会唤醒一个正在this锁等待的线程（就是在getTask()中位于this.wait()的线程），从而使得等待线程从this.wait()方法返回。\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 55 56 import java.util.*; public class Main { public static void main(String[] args) throws InterruptedException { var q = new TaskQueue(); var ts = new ArrayList\u0026lt;Thread\u0026gt;(); for (int i=0; i\u0026lt;5; i++) { var t = new Thread() { public void run() { // 执行task: while (true) { try { String s = q.getTask(); System.out.println(\u0026#34;execute task: \u0026#34; + s); } catch (InterruptedException e) { return; } } } }; t.start(); ts.add(t); } var add = new Thread(() -\u0026gt; { for (int i=0; i\u0026lt;10; i++) { // 放入task: String s = \u0026#34;t-\u0026#34; + Math.random(); System.out.println(\u0026#34;add task: \u0026#34; + s); q.addTask(s); try { Thread.sleep(100); } catch(InterruptedException e) {} } }); add.start(); add.join(); Thread.sleep(100); for (var t : ts) { t.interrupt(); } } } class TaskQueue { Queue\u0026lt;String\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); public synchronized void addTask(String s) { this.queue.add(s); this.notifyAll(); } public synchronized String getTask() throws InterruptedException { while (queue.isEmpty()) { this.wait(); } return queue.remove(); } } 这个例子中，我们重点关注addTask()方法，内部调用了this.notifyAll()而不是this.notify()，使用notifyAll()将唤醒所有当前正在this锁等待的线程，而notify()只会 唤醒其中一个 （具体哪个依赖操作系统，有一定的 随机性）。这是因为可能有多个线程正在getTask()方法内部的wait()中等待，使用notifyAll()将 一次性全部唤醒 。通常来说，notifyAll()更安全。有些时候，如果我们的代码逻辑考虑不周，用notify()会导致只唤醒了一个线程，而其他线程可能永远等待下去醒不过来了。\n但是，注意到wait()方法返回时需要 重新 获得this锁。假设当前有3个线程被唤醒，唤醒后，首先要等待执行addTask()的线程结束此方法后，才能释放this锁，随后，这3个线程中只能有一个获取到this锁，剩下两个将继续等待。\n再注意到我们在while()循环中调用wait()，而不是if语句：\n1 2 3 4 5 6 public synchronized String getTask() throws InterruptedException { if (queue.isEmpty()) { this.wait(); } return queue.remove(); } 这种写法实际上是错误的，因为线程被唤醒时，需要再次获取this锁。多个线程被唤醒后，只有一个线程能获取this锁，此刻，该线程执行queue.remove()可以获取到队列的元素，然而，剩下的线程如果获取this锁后执行queue.remove()，此刻队列可能已经没有任何元素了，所以，要始终在while循环中wait()，并且每次被唤醒后拿到this锁就必须再次判断：\n1 2 3 while (queue.isEmpty()) { this.wait(); } 小结 wait和notify用于多线程协调运行：\n在synchronized内部可以调用wait()使线程进入等待状态； 必须在已获得的锁对象上调用wait()方法； 在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程； 必须在已获得的锁对象上调用notify()或notifyAll()方法； 已唤醒的线程还需要重新获得锁后才能继续执行。 生产者消费者模型 Java生产者消费者模式的实现和解析_哔哩哔哩_bilibili\n下面是从B站找来的简单的生产者消费者模型的示例，并不如上面线程通信中的示例以及下面的消息队列模型示例，这三个示例我想就能拿下该模型罢\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 public class Demo1 { /** * 交替执行两个线程 * 一个输出“1,2,3,...” * 一个输出“a,b,c,...” */ public static void main(String[] args) { Factory factory = new Factory(); final Thread t1 = new Thread(new Runnable() { @Override public void run() { for(int i = 1;i \u0026lt;= 26;i++){ factory.product(i); } } }); final Thread t2 = new Thread(new Runnable() { @Override public void run() { for(int i = \u0026#39;a\u0026#39;;i \u0026lt;= \u0026#39;z\u0026#39;;i++){ factory.consume((char) i); } } }); t1.start(); t2.start(); } } 1 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 public class Factory { /** * 0: 生产者正在生成，消费者正在等待，生产者结束生产后告知消费者进行消费 * 1: 消费者正在消费，生产者正在等待，消费者结束消费后高职生产者进行生产 */ private int sign = 0;\t//状态值 public synchronized void product(int n){ if(sign == 1){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(n); this.notify(); this.sign = 1; } public synchronized void consume(char c){ if(sign == 0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(c); this.notify(); this.sign = 0; } } 线程的运行有一定随机性，往往用户无法决定，但是生产者消费者模型，能实现两个线程的“交替”运行\n注释里的内容不再概述，我们来分析一下：\n假设线程 t1 先被调用，由于 sign = 0 ，所以打印字符 1 ， sign 变为1。下面有两种可能，调用线程 t1 或 t2\n调用 t1 :\nsign = 1 进入 try/catch 同步锁的对象 this。wait() 也就是进入 “等待” 状态 wait() 会 释放锁 ，线程 t2 执行，运行 consume() notfiy() 唤醒 this 中等待的线程 t1 sign 被赋值0，周而复始 调用 t2 :\n线程 t2 执行，运行 consume() notify 不唤醒任一线程(因为无线程处于等待状态) sign 被赋值0，周而复始 示例分析 下面是较复杂(贴切实际)的一种，思想和上面简单的例子差不多的\n关于下面示例中 lambda 表达式创建线程的方式，需要补充几点：\nnew Thread() - 创建新线程 () -\u0026gt; {...} - Lambda表达式定义线程任务 \u0026quot;生产者\u0026quot; + i - 线程命名 .start() - 启动线程 这里通过循环来创建线程，所以用循环的参数为其命名\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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 public static void main(String[] args) throws InterruptedException { MessageQueue queue = new MessageQueue(2); // 三个生产者向队列里存值 for (int i = 0; i \u0026lt; 3; i++) { int id = i; new Thread(() -\u0026gt; { queue.put(new Message(id, \u0026#34;值\u0026#34; + id)); }, \u0026#34;生产者\u0026#34; + i).start(); } Thread.sleep(1000); // 一个消费者不停的从队列里取值 new Thread(() -\u0026gt; { while (true) { queue.take(); } }, \u0026#34;消费者\u0026#34;).start(); } } // 消息队列被生产者和消费者持有 class MessageQueue { private LinkedList\u0026lt;Message\u0026gt; list = new LinkedList\u0026lt;\u0026gt;(); // 容量 private int capacity; public MessageQueue(int capacity) { this.capacity = capacity; } //生产者 public void put(Message message) { synchronized (list) { while (list.size() == capacity) { log.info(\u0026#34;队列已满，生产者等待\u0026#34;); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.addLast(message); log.info(\u0026#34;生产消息:{}\u0026#34;, message); // 生产后通知消费者 list.notifyAll(); } } //消费者 public Message take() { synchronized (list) { while (list.isEmpty()) { log.info(\u0026#34;队列已空，消费者等待\u0026#34;); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } Message message = list.removeFirst(); //从队列头部取出消息 log.info(\u0026#34;消费消息:{}\u0026#34;, message); // 消费后通知生产者 list.notifyAll(); return message; } } } // 消息 class Message { private int id; private Object value; } 主函数:\n创建了一个容量为2的消息队列MessageQueue 启动3个生产者线程，每个生产者向队列中放入一条消息 主线程休眠1秒，让生产者有足够时间开始工作 启动一个消费者线程，不断从队列中取出消息 生产者:\n使用synchronized块获取list对象的锁 检查队列是否已满（while循环防止虚假唤醒） 如果队列已满，调用wait()释放锁并等待 当队列有空闲时，添加消息到队列尾部 调用notifyAll()唤醒可能正在等待的消费者线程 消费者:\n使用synchronized块获取list对象的锁 检查队列是否为空（while循环防止虚假唤醒） 如果队列为空，调用wait()释放锁并等待 当队列有消息时，从队列头部取出消息 调用notifyAll()唤醒可能正在等待的生产者线程 返回取出的消息 小结 同步机制：使用synchronized保证对队列操作的原子性 等待/通知机制：使用wait()和notifyAll()实现线程间通信 循环检查条件：使用while而非if检查条件，防止虚假唤醒 容量限制：控制队列大小，防止内存耗尽 可重入锁 从Java 5开始，引入了一个高级的处理并发的java.util.concurrent包，它提供了大量更高级的并发功能，能大大简化多线程程序的编写。\n我们知道Java语言直接提供了synchronized关键字用于加锁，但这种锁一是很重，二是获取时必须一直等待，没有额外的尝试机制。\njava.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁，我们来看一下传统的synchronized代码：\n1 2 3 4 5 6 7 8 9 public class Counter { private int count; public void add(int n) { synchronized(this) { count += n; } } } 如果用ReentrantLock替代，可以把代码改造为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class Counter { private final Lock lock = new ReentrantLock(); private int count; public void add(int n) { lock.lock(); try { count += n; } finally { lock.unlock(); } } } 因为synchronized是Java语言层面提供的语法，所以我们不需要考虑异常，而ReentrantLock是Java代码实现的锁，我们就必须先获取锁，然后在finally中正确释放锁。\n顾名思义，ReentrantLock是可重入锁，它和synchronized一样，一个线程可以多次获取同一个锁。\n和synchronized不同的是，ReentrantLock可以尝试获取锁：\n1 2 3 4 5 6 7 if (lock.tryLock(1, TimeUnit.SECONDS)) { try { ... } finally { lock.unlock(); } } 上述代码在尝试获取锁的时候，最多等待1秒。如果1秒后仍未获取到锁，tryLock()返回false，程序就可以做一些额外处理，而不是无限等待下去。\n所以，使用ReentrantLock比直接使用synchronized更安全，线程在tryLock()失败的时候不会导致死锁。\n下面来介绍一下它的各种方法，以及一个较复杂的案例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 默认非公平锁，参数传true 表示未公平锁 ReentrantLock lock = new ReentrantLock(false); // 尝试获取锁 lock() // 释放锁 应放在finally块中 必须执行到 unlock() try { // 获取锁时可被打断,阻塞中的线程可被打断 LOCK.lockInterruptibly(); } catch (InterruptedException e) { return; } // 尝试获取锁 获取不到就返回false LOCK.tryLock() // 支持超时时间 一段时间没获取到就返回false tryLock(long timeout, TimeUnit unit) // 指定条件变量 休息室 一个锁可以创建多个休息室 Condition waitSet = ROOM.newCondition(); // 释放锁 进入waitSet等待 释放后其他线程可以抢锁 yanWaitSet.await() // 唤醒具体休息室的线程 唤醒后 重写竞争锁 yanWaitSet.signal() 1 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 55 56 57 58 59 60 61 62 63 64 65 66 67 public static void main(String[] args) { AwaitSignal awaitSignal = new AwaitSignal(5); // 构建三个条件变量 Condition a = awaitSignal.newCondition(); Condition b = awaitSignal.newCondition(); Condition c = awaitSignal.newCondition(); // 开启三个线程 new Thread(() -\u0026gt; { awaitSignal.print(\u0026#34;a\u0026#34;, a, b); }).start(); new Thread(() -\u0026gt; { awaitSignal.print(\u0026#34;b\u0026#34;, b, c); }).start(); new Thread(() -\u0026gt; { awaitSignal.print(\u0026#34;c\u0026#34;, c, a); }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } awaitSignal.lock(); try { // 先唤醒a a.signal(); } finally { awaitSignal.unlock(); } } } class AwaitSignal extends ReentrantLock { // 循环次数 private int loopNumber; public AwaitSignal(int loopNumber) { this.loopNumber = loopNumber; } /** * @param print 输出的字符 * @param current 当前条件变量 * @param next 下一个条件变量 */ public void print(String print, Condition current, Condition next) { for (int i = 0; i \u0026lt; loopNumber; i++) { lock(); try { try { // 获取锁之后等待 current.await(); System.out.print(print); } catch (InterruptedException e) { } next.signal(); } finally { unlock(); } } } 流程分析：\n初始化 ： 主线程创建了 AwaitSignal 对象，设置循环次数为 5。 创建了三个 Condition 对象：a、b、c，分别对应三个线程。 三个线程启动，分别调用 print(\u0026quot;a\u0026quot;, a, b) 、 print(\u0026quot;b\u0026quot;, b, c) 、 print(\u0026quot;c\u0026quot;, c, a) 。 主线程休眠 1 秒后，获取锁并通过 a.signal() 唤醒线程 A。 线程启动后 ： 每个线程进入 print 方法，执行 lock() 获取锁。由于 ReentrantLock 是互斥锁，同一时刻只有一个线程能持有锁。 假设线程 A 先获取锁，它调用 a.await() ，释放锁并进入等待状态（等待 Condition a 的信号）。 其他线程（B 和 C）尝试 lock() ，但锁被占用，它们会阻塞在 lock() 上。 主线程唤醒线程A ： 主线程在 try { Thread.sleep(1000); } 后执行 awaitSignal.lock() ，获取锁。 调用 a.signal() ，唤醒等待在 Condition a 上的线程 A。 主线程执行 unlock() ，释放锁。 线程A被唤醒后 ： 线程 A 从 a.await() 返回，但它需要重新获取锁才能继续执行。 因为主线程已经释放锁（ unlock() ），线程 A 成功重新获取锁。 线程 A 打印 \u0026ldquo;a\u0026rdquo;，然后调用 b.signal() 唤醒线程 B。 线程 A 执行 unlock() ，释放锁。 线程B被唤醒后 ： 线程 B 在 b.await() 上等待，收到 b.signal() 后被唤醒。 线程 B 尝试重新获取锁。由于线程 A 已释放锁，线程 B 获取锁成功。 线程 B 打印 \u0026ldquo;b\u0026rdquo;，调用 c.signal() 唤醒线程 C，然后释放锁。 小结 ReentrantLock可以替代synchronized进行同步；\nReentrantLock获取锁更安全；\n必须先获取到锁，再进入try {...}代码块，最后使用finally保证释放锁；\n可以使用tryLock()尝试获取锁。\n线程池 （线程池感觉都写的不是很明白）\nJava语言虽然内置了多线程支持，启动一个新线程非常方便，但是，创建线程需要操作系统资源（线程资源，栈空间等），频繁创建和销毁大量线程需要消耗大量时间。\n如果可以复用一组线程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 ┌─────┐ execute ┌──────────────────┐ │Task1│─────────▶│ThreadPool │ ├─────┤ │┌───────┐┌───────┐│ │Task2│ ││Thread1││Thread2││ ├─────┤ │└───────┘└───────┘│ │Task3│ │┌───────┐┌───────┐│ ├─────┤ ││Thread3││Thread4││ │Task4│ │└───────┘└───────┘│ ├─────┤ └──────────────────┘ │Task5│ ├─────┤ │Task6│ └─────┘ ... 那么我们就可以把很多小任务让一组线程来执行，而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。\n简单地说，线程池内部维护了若干个线程，没有任务的时候，这些线程都处于等待状态。如果有新任务，就分配一个空闲线程执行。如果所有线程都处于忙碌状态，新任务要么放入队列等待，要么增加一个新线程进行处理。\nJava标准库提供了ExecutorService接口表示线程池，它的典型用法如下：\n1 2 3 4 5 6 7 8 // 创建固定大小的线程池: ExecutorService executor = Executors.newFixedThreadPool(3); // 提交任务: executor.submit(task1); executor.submit(task2); executor.submit(task3); executor.submit(task4); executor.submit(task5); 因为ExecutorService只是接口，Java标准库提供的几个常用实现类有：\nFixedThreadPool：线程数固定的线程池； CachedThreadPool：线程数根据任务动态调整的线程池； SingleThreadExecutor：仅单线程执行的线程池。 创建这些线程池的方法都被封装到Executors这个类中。我们以FixedThreadPool为例，看看线程池的执行逻辑：\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 // thread-pool import java.util.concurrent.*; public class Main { public static void main(String[] args) { // 创建一个固定大小的线程池: ExecutorService es = Executors.newFixedThreadPool(4); for (int i = 0; i \u0026lt; 6; i++) { es.submit(new Task(\u0026#34;\u0026#34; + i)); } // 关闭线程池: es.shutdown(); } } class Task implements Runnable { private final String name; public Task(String name) { this.name = name; } @Override public void run() { System.out.println(\u0026#34;start task \u0026#34; + name); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println(\u0026#34;end task \u0026#34; + name); } } 我们观察执行结果，一次性放入6个任务，由于线程池只有固定的4个线程，因此，前4个任务会同时执行，等到有线程空闲后，才会执行后面的两个任务。\n线程池在程序结束的时候要关闭。使用shutdown()方法关闭线程池的时候，它会等待正在执行的任务先完成，然后再关闭。shutdownNow()会立刻停止正在执行的任务，awaitTermination()则会等待指定的时间让线程池关闭。\n如果我们把线程池改为CachedThreadPool，由于这个线程池的实现会根据任务数量动态调整线程池的大小，所以6个任务可一次性全部同时执行。\n如果我们想把线程池的大小限制在4～10个之间动态调整怎么办？我们查看Executors.newCachedThreadPool()方法的源码：\n1 2 3 4 5 6 public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor( 0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue\u0026lt;Runnable\u0026gt;()); } 因此，想创建指定动态范围的线程池，可以这么写：\n1 2 3 4 5 6 int min = 4; int max = 10; ExecutorService es = new ThreadPoolExecutor( min, max, 60L, TimeUnit.SECONDS, new SynchronousQueue\u0026lt;Runnable\u0026gt;()); ScheduledThreadPool 还有一种任务，需要定期反复执行，例如，每秒刷新证券价格。这种任务本身固定，需要反复执行的，可以使用ScheduledThreadPool。放入ScheduledThreadPool的任务可以定期反复执行。\n创建一个ScheduledThreadPool仍然是通过Executors类：\n1 ScheduledExecutorService ses = Executors.newScheduledThreadPool(4); 我们可以提交一次性任务，它会在指定延迟后只执行一次：\n1 2 // 1秒后执行一次性任务: ses.schedule(new Task(\u0026#34;one-time\u0026#34;), 1, TimeUnit.SECONDS); 如果任务以固定的每3秒执行，我们可以这样写：\n1 2 // 2秒后开始执行定时任务，每3秒执行: ses.scheduleAtFixedRate(new Task(\u0026#34;fixed-rate\u0026#34;), 2, 3, TimeUnit.SECONDS); 如果任务以固定的3秒为间隔执行，我们可以这样写：\n1 2 // 2秒后开始执行定时任务，以3秒为间隔执行: ses.scheduleWithFixedDelay(new Task(\u0026#34;fixed-delay\u0026#34;), 2, 3, TimeUnit.SECONDS); 注意FixedRate和FixedDelay的区别。FixedRate是指任务总是以固定时间间隔触发，不管任务执行多长时间：\n1 2 3 │░░░░ │░░░░░░ │░░░ │░░░░░ │░░░ ├───────┼───────┼───────┼───────┼────▶ │◀─────▶│◀─────▶│◀─────▶│◀─────▶│ 而FixedDelay是指，上一次任务执行完毕后，等待固定的时间间隔，再执行下一次任务：\n1 2 3 │░░░│ │░░░░░│ │░░│ │░ └───┼───────┼─────┼───────┼──┼───────┼──▶ │◀─────▶│ │◀─────▶│ │◀─────▶│ 因此，使用ScheduledThreadPool时，我们要根据需要选择执行一次、FixedRate执行还是FixedDelay执行。\n还可以思考下面的问题：\n在FixedRate模式下，假设每秒触发，如果某次任务执行时间超过1秒，后续任务会不会并发执行？ 如果任务抛出了异常，后续任务是否继续执行？ Java标准库还提供了一个java.util.Timer类，这个类也可以定期执行任务，但是，一个Timer会对应一个Thread，所以，一个Timer只能定期执行一个任务，多个定时任务必须启动多个Timer，而一个ScheduledThreadPool就可以调度多个定时任务，所以，我们完全可以用ScheduledThreadPool取代旧的Timer。\n小结 JDK提供了ExecutorService实现了线程池功能：\n线程池内部维护一组线程，可以高效执行大量小任务； Executors提供了静态方法创建不同类型的ExecutorService； 必须调用shutdown()关闭ExecutorService； ScheduledThreadPool可以定期调度多个任务。 ","date":"2025-03-21T14:50:07+08:00","permalink":"/p/java-%E5%A4%9A%E7%BA%BF%E7%A8%8B/","title":"Java 多线程"},{"content":"笔者近日配置好了物理服务器，便想利用起来建一个我的世界服务器，经熬夜研究后成功，将本攻略分享出来，希望能帮到大家，尤其是没有公网环境的Linux用户(像我一样)\n参考网站 在 RHEL 上安装并使用红帽构建的 OpenJDK 21 | Red Hat Product Documentation\nSakuraFrp 启动器安装 / 使用指南 | SakuraFrp 帮助文档\nCentOS | Docker Docs\nLinux终端开服教程★无面板★Minecraft_哔哩哔哩_bilibili\n来自B站的UP主翱翔大使，是全篇的主要思路来源 Java配置 运行我的世界需要，须对应版本的Java环境，笔者这里安装的是OpenJDK21\n1 2 sudo yum install java-21-openjdk java -version //验证是否成功安装 如果服务器有多个Java版本，可以用alternatives进行版本切换\n1 alternatives --config java 如图，我们输入2并回车，就切换到了需要的版本\n游戏部署 首先在下面这个网址下载Minecraft的服务器端，这里我下的是支持Fabric的Banner(1.20.1)\n[MohistMC](MohistMC - 主页)\n下载完成后是个类似banner-1.20.1-800-server.jar的文件，接下来打开SSH软件，在服务器上操作：\n1 2 3 cd /home/username //切换到个人文件夹或者想安装的位置 mkdir Minecraft //创建存放游戏的文件夹 cd Minecraft 我们用SSH软件中的SFTP功能(或其他文件传输功能)，将刚才的游戏文件banner-1.20.1-800-server.jar拷贝到新建的文件夹/home/username/Mineraft中\n接着我们来写一个服务器的启动脚本\n1 nano start.sh 其中内容如下填写，但注意各参数的作用\n-Xmx是最大分配内存，-Xms是最小分配内存，笔者有32GB内存，为游戏分配了6G(其实可以多分点) banner-1.20.1-800-server.jar是刚才下载的游戏文件名 1 2 java -Xmx6144M -Xms6144m -jar banner-1.20.1-800-server.jar stty echo 按Ctrl + O写入，Enter确认写入，Ctrl + X退出\n接着为start.sh赋权，避免无权限访问的情况\n1 chmod 777 start.sh 然后安装screen，简单来说，screen是帮用户创建独立会话，并可以随时恢复的工具\n1 yum install screen screen有如下几个常用命令\n1 2 3 screen -S [name] //新建名为\u0026#34;name\u0026#34;的screen screen -ls //列出所有运行中的screen的名称和端口 screen -r [port] //返回端口号为port的screen 接着，新建一个screen运行脚本\n1 screen -S Minecraft 在新出现的会话中，运行start.sh\n1 ./start.sh 然后一路顺畅，笔者在这里没有遇到报错，最后来到...EULA...让我们同意EULA协议，输入true后回车，等待一下，游戏服务器就在25565端口上成功运行了\n若想离开Minecraft的这个screen按下Ctrl+A+D即可\n关于游戏规则的更改(比如”是否允许非正版玩家加入“)，需要修改server.properties的内容\n关于连接，如果是云服务器，在管理界面映射一下端口，然后在客户端的Minecraft中连接域名:端口即可\n但是像笔者这样的物理服务器，或者说安装了Linux的设备，个人PC，在没有公网IP的情况下，就要继续内网穿透了\n内网穿透 笔者在这里使用我的世界领域中比较有名且良心的SakuraFrp进行内网穿透，其它工具也大同小异\nDocker Linux上的SakuraFrp是基于Docker运行的，所以下面我们先部署Docker，操作完全根据官方文档进行\n1 2 sudo dnf -y install dnf-plugins-core sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 笔者在下面这遇到了安装速度十分缓慢，和下载失败的问题，重新执行命令再执行一次便解决了\n1 sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 安装完毕后\n1 sudo systemctl enable --now docker 1 sudo docker run hello-world 上面的这个run hello-world测试极有可能失败，下面来解决这个问题，参考了下面两篇文章：\n【完全解决】Docker安装完成运行hello-world镜像失败：Unable to find image ‘hello-world:latest‘ locallylatest:_unable to find image \u0026lsquo;hello-world:latest\u0026rsquo; locally-CSDN博客\nDocker运行hello-world镜像失败或超时 - Paul7777 - 博客园\n综合上面二者，最终是能解决问题的，先来配置daemon文件\n1 nano /etc/docker/daemon.json 复制下面的内容进去\n在笔者测试的时间(2025/3/19)下面的镜像源还是可用的 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://h59pkpv6.mirror.aliyuncs.com\u0026#34;, \u0026#34;https://registry.docker-cn.com\u0026#34;, \u0026#34;https://docker.mirrors.ustc.edu.cn\u0026#34;, \u0026#34;https://hub-mirror.c.163.com\u0026#34;, \u0026#34;https://mirror.baidubce.com\u0026#34;, \u0026#34;https://do.nark.eu.org\u0026#34;, \u0026#34;https://dc.j8.work\u0026#34;, \u0026#34;https://docker.m.daocloud.io\u0026#34;, \u0026#34;https://dockerproxy.com\u0026#34;, \u0026#34;https://docker.nju.edu.cn\u0026#34; ] } 保存 + 退出，接下来重启docker，再执行一次测试\n1 2 3 sudo systemctl daemon-reload sudo systemctl restart docker docker run hello-world 笔者到这就已经成功安装好docker了，若是测试仍不通过，请检查daemon.json的内容，是否少了或者多了逗号和括号\nSakuraFrp SakuraFrp在Linux上的部署，官方文档给出了详细的方案\n首先在终端以管理员身份运行\n1 sudo bash -c \u0026#34;. \u0026lt;(curl -sSL https://doc.natfrp.com/launcher.sh)\u0026#34; 安装好后，应该是会自动输出日志，并需要填写访问密钥，这个(或者说接下来的操作)可以在SakuraFrp官网的管理面板找到\n登录好之后就能看到其日志文件，下面是常规的启动并查看日志的操作\n1 2 docker start natfrp-service docker logs natfrp-service 如图，接下来需要用物理方式操作下服务器\n打开浏览器(一般Linux自带Firefox)访问“使用”后面的网址打开WebUI\n然后看到“隧道”那什么都没有，只有一个加号，这时我们在打开SakuraFrp的管理面板，找到服务下的隧道列表，新建两个隧道，如图所示：\n第一个端口号为7102的是服务器上SakuraFrp的WebUI，以便远程管理\n第二个端口号为25565的是Minecraft的服务器端\n回到WebUI界面刷新一下就能看到刚刚创建好的两个隧道了，我们分别双击，然后回到终端的日志界面\n如图的红色字符的链接，就是WebUI和Minecraft的远程访问链接，将Minecraft对应的复制到游戏中即可连接上\n结语 大功告成！(笔者服务器出生地的截图 \u0026gt;w\u0026lt;)\n","date":"2025-03-19T18:48:59+08:00","permalink":"/p/%E5%9C%A8linuxcentos%E7%B3%BB%E7%BB%9F%E4%B8%8A%E9%83%A8%E7%BD%B2minecraft%E6%9C%8D%E5%8A%A1%E5%99%A8/","title":"在Linux(CentOS)系统上部署Minecraft服务器"},{"content":"Redis（Remote Dictionary Server）是一个高性能的开源内存数据结构存储系统，常被用作数据库、缓存和消息代理。\n参考网站 知乎 超强、超详细Redis入门教程\nCSDN【Redis二三事】一套超详细的Redis学习教程（步骤图片+实操）\u0026mdash;第一集\n详细，附有业务场景的实例\n数据结构 Redis包含五大数据类型：字符串(string), 列表(list), 哈希(hash), 集合(set), 集合(zset)\nstring Redis 最基本的数据类型，每个键对应一个值，值可以是文本、数字或二进制数据，最大存储 512MB。支持字符串拼接、截取、递增递减等操作，适用于缓存数据、计数器（如访问量统计）、分布式锁等场景。\n基础操作 1 2 3 set key value get key del key 添加修改多个数据 1 mset key1 value1 key2 value2... 获取多个数据 1 mget key1 key2... 获取数据的字符个数 1 2 3 4 5 strlen key //例如 set name1 nosql strlen name1 //输出为: 5 追加信息 1 2 3 4 5 append key value //例如 append name1 name get name1 /*输出为: nosqlname*/ 多数据操作与单数据操作 单指令执行 n 条指令需要 n次发送 + n次处理 + n次返回 多指令执行 n 条指令需要 1次发送 + n次处理 + 1次返回 数据量较大时，多指令消耗的时间远远少于单指令 扩展操作 设置数值数据增加/减少指定范围的值 1 2 incrby key increment decrby key increment 对字符串类型进行数值操作 1 2 3 4 5 6 7 8 127.0.0.1:6379\u0026gt; set mynum \u0026#34;2\u0026#34; OK 127.0.0.1:6379\u0026gt; get mynum \u0026#34;2\u0026#34; 127.0.0.1:6379\u0026gt; incr mynum (integer) 3 127.0.0.1:6379\u0026gt; get mynum \u0026#34;3\u0026#34; 遇到数值操作，redis会自动将字符串类型转换成数值\nstring类型数值操作的注意事项 数据操作不成功的反馈与数据正常操作之间的差异 表示运行结果是否成功 （integer）0-\u0026gt;false 失败 （integer）1-\u0026gt;true 成功 表示运行结果值 （integer）3-\u0026gt;3 3个 （integer）1-\u0026gt;1 1个 数据未获取到 （\u0026lsquo;\u0026rsquo;nil\u0026rsquo;\u0026rsquo;）等同于\u0026rsquo;\u0026rsquo;null\u0026rsquo;' 数据最大存储量 512MB 数值计算最大范围（\u0026lsquo;\u0026lsquo;java\u0026rsquo;\u0026lsquo;中的\u0026rsquo;\u0026rsquo;long\u0026rsquo;\u0026lsquo;的最大值） 9223372036854775807 hash 类似于小型的键值存储，适用于存储结构化数据，如用户信息（ID、姓名、邮箱等），相比 String 类型更节省内存，因为多个字段共享同一个键。可以对字段进行单独操作，避免整体读取修改，适用于存储对象、会话信息等。\n基础操作 1 2 3 4 hset key field value //添加/修改数据 hget key field //获取数据 hgetall key hdel key field1 [field2] 添加/修改多个数据 1 hmset key field1 value1 field2 value2 获取多个数据 1 hmget key field1 field2... 获取哈希表中字段的数量 1 hlen key 获取哈希表中是否存在指定的字段 1 hexists key field 扩展操作 获取哈希表中所有字段名或字段值 1 2 hkeys key hvals key 设置指定字段的数值数据增加指定范围的值 1 hincrby key field increment 综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //建立哈希，并赋值 127.0.0.1:6379\u0026gt; HMSET user:001 username antirez password P1pp0 age 34 OK //列出哈希的内容 127.0.0.1:6379\u0026gt; HGETALL user:001 1) \u0026#34;username\u0026#34; 2) \u0026#34;antirez\u0026#34; 3) \u0026#34;password\u0026#34; 4) \u0026#34;P1pp0\u0026#34; 5) \u0026#34;age\u0026#34; 6) \u0026#34;34\u0026#34; //更改哈希中的某一个值 127.0.0.1:6379\u0026gt; HSET user:001 password 12345 (integer) 0 //再次列出哈希的内容 127.0.0.1:6379\u0026gt; HGETALL user:001 1) \u0026#34;username\u0026#34; 2) \u0026#34;antirez\u0026#34; 3) \u0026#34;password\u0026#34; 4) \u0026#34;12345\u0026#34; 5) \u0026#34;age\u0026#34; 6) \u0026#34;34\u0026#34; hash类型数据操作的注意事项 1.hash类型下的value只能存储字符串，不允许存储其他数据类型，不存在嵌套现象。如果数据未获取到，对应的值为（nil） 2.每个hash可以存储2^23-1个键值对 3.hash类型十分贴近对象的数据存储形式，并且可以灵活添加对象属性。但hash设计初衷不是为了存储大量对象而设计的，切记不可滥用，更不可以将hash作为对象列表使用 4.hgetall操作可以获取全部属性，如果内部field过多，遍历整体数据效率就会很低，有可能成为数据访问瓶颈 list 基于双向链表实现，可以从头部（左侧）或尾部（右侧）快速插入和删除元素，同时支持指定范围的索引读取。适合实现消息队列、时间轴（如微博动态）、任务调度等应用，尤其适用于需要按照插入顺序处理数据的场景。\n基础操作 添加/修改数据 1 2 lpush key value1 [value2]... //从左边进 rpush key value1 [value2]... /*从右边进*/ 获取数据 关于lrange:\nlrange用来获取指定范围的元素 -1 代表倒数第一个元素 列表元素索引从位置0开始 1 2 3 lrange key start stop lindex key index lien key 获取并移除数据 1 2 lpop key rpop key 移除指定数据 关于lrem:\n参数count count \u0026gt; 0 → 从头（左侧）开始删除count个匹配的value count \u0026lt; 0 → 从尾（右侧）开始删除count个匹配的value count = 0 → 删除所有匹配的value（等价于删除列表中所有该值的元素） 如果key不存在 返回 0 常用于去除列表中的重复元素或清理数据 1 lrem key count value 扩展操作 在规定时间内获取并移除数据 关于blpop:\n参数key1 [key2...] 可以提供多个列表的键名，Redis 会按照顺序依次检查这些列表 参数timeout timeout \u0026gt; 0 → 如果列表为空，则最多等待 timeout 秒 timeout \u0026lt; 0 → 永远阻塞，直到有数据可用 适用于任务队列、生产者-消费者模型等场景 1 2 blpop key1 [key2] timeout brpop key1 [key2] timeout 示例\n1 2 3 4 5 RPUSH list1 \u0026#34;a\u0026#34; \u0026#34;b\u0026#34; \u0026#34;c\u0026#34; // 列表内容：[\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;] BLPOP list1 10 // 取出 \u0026#34;a\u0026#34;，返回 [\u0026#34;list1\u0026#34;, \u0026#34;a\u0026#34;] BLPOP list1 10 // 取出 \u0026#34;b\u0026#34;，返回 [\u0026#34;list1\u0026#34;, \u0026#34;b\u0026#34;] BLPOP list1 10 // 取出 \u0026#34;c\u0026#34;，返回 [\u0026#34;list1\u0026#34;, \u0026#34;c\u0026#34;] BLPOP list1 10 /* 列表为空，阻塞最多 10 秒，若无新元素，则返回 nil */ 综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //新建一个list叫做mylist，并在列表头部插入元素\u0026#34;1\u0026#34; 127.0.0.1:6379\u0026gt; lpush mylist \u0026#34;1\u0026#34; //返回当前mylist中的元素个数 (integer) 1 //在mylist右侧插入元素\u0026#34;2\u0026#34; 127.0.0.1:6379\u0026gt; rpush mylist \u0026#34;2\u0026#34; (integer) 2 //在mylist左侧插入元素\u0026#34;0\u0026#34; 127.0.0.1:6379\u0026gt; lpush mylist \u0026#34;0\u0026#34; (integer) 3 //列出mylist中从编号0到编号1的元素 127.0.0.1:6379\u0026gt; lrange mylist 0 1 1) \u0026#34;0\u0026#34; 2) \u0026#34;1\u0026#34; //列出mylist中从编号0到倒数第一个元素 127.0.0.1:6379\u0026gt; lrange mylist 0 -1 1) \u0026#34;0\u0026#34; 2) \u0026#34;1\u0026#34; 3) \u0026#34;2\u0026#34; list类型数据操作注意事项 1.list中保存的数据都是string类型的，数据总容量是有限的，最多2^32-1个元素 2.list具有\u0026rsquo;\u0026lsquo;索引\u0026rsquo;\u0026lsquo;的概念，但是操作数据时通常以\u0026rsquo;\u0026lsquo;队列\u0026rsquo;\u0026lsquo;的形式进行入队出队操作，或以\u0026rsquo;\u0026lsquo;栈\u0026rsquo;\u0026lsquo;的形式进行入栈出栈操作 3.获取全部数据操作结束索引设置为-1 4.list可以对数据进行分页操作，通常第1页的信息来自于list，第2页及更多的信息通过\u0026rsquo;\u0026lsquo;数据库\u0026rsquo;\u0026lsquo;的形式加载 set 由唯一无序的元素组成，支持 O(1) 时间复杂度的添加、删除和查找操作，并提供交集、并集、差集等集合运算，适用于去重、推荐系统中的共同关注、标签管理等应用。由于不允许重复元素，可以高效存储不重复的数据集合。\n基础操作 添加数据 1 sadd key member1 [member2] 获取全部数据 1 smembers key 删除数据 1 srem key member1 [member2] 获取集合数据总量 1 scard key 判断集合中是否包含指定数据 1 sismember key member 进阶操作 随机获取集合中指定数量的数据 1 srandmember key [count] 随机获取集合中的某个数据并将其移出集合 1 spop key 求两个集合的交、并、差集 1 2 3 sinter key1 [key2] sunion key1 [key2] sdiff key1 [key2] 求两个集合的交、并、差集 并存储到指定集合中 1 2 3 sinterstore destination key1 [key2] sunionstore destination key1 [key2] sdiffstore destination key1 [key2] 将指定数据从原始集合中移动到目标集合中 1 smove source destination member 综合示例 1 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 //向集合myset中加入一个新元素\u0026#34;one\u0026#34; 127.0.0.1:6379\u0026gt; sadd myset \u0026#34;one\u0026#34; (integer) 1 127.0.0.1:6379\u0026gt; sadd myset \u0026#34;two\u0026#34; (integer) 1 //列出集合myset中的所有元素 127.0.0.1:6379\u0026gt; smembers myset 1) \u0026#34;one\u0026#34; 2) \u0026#34;two\u0026#34; //判断元素1是否在集合myset中，返回1表示存在 127.0.0.1:6379\u0026gt; sismember myset \u0026#34;one\u0026#34; (integer) 1 //判断元素3是否在集合myset中，返回0表示不存在 127.0.0.1:6379\u0026gt; sismember myset \u0026#34;three\u0026#34; (integer) 0 //新建一个新的集合yourset 127.0.0.1:6379\u0026gt; sadd yourset \u0026#34;1\u0026#34; (integer) 1 127.0.0.1:6379\u0026gt; sadd yourset \u0026#34;2\u0026#34; (integer) 1 127.0.0.1:6379\u0026gt; smembers yourset 1) \u0026#34;1\u0026#34; 2) \u0026#34;2\u0026#34; //对两个集合求并集 127.0.0.1:6379\u0026gt; sunion myset yourset 1) \u0026#34;1\u0026#34; 2) \u0026#34;one\u0026#34; 3) \u0026#34;2\u0026#34; 4) \u0026#34;two\u0026#34; set类型数据操作的注意事项 set类型不允许数据重复，如果添加的数据在set中已经存在，将只保留一份 set虽然和hash的存储结构相同，但是无法启用hash中存储值的空间 zset 在 Set 的基础上增加了一个分数（score），并按分数排序，支持范围查询、按分数排名等操作。适用于排行榜（如游戏积分榜）、优先级队列（如定时任务）、时间排序数据存储（如文章阅读量排名）等需要按权重排序的场景。\n基础操作 添加数据 1 zadd key score1 member1 [score2 member2] 获取全部数据 1 2 zrange key start stop [WITHSCORES] //按从小到大的顺序展示 zrevrange key start stop [WITHSCORES] //按从大到小的顺序展示 删除数据 1 zrem key member [member...] 按条件获取数据 1 2 zrangebyscore key min max [Withscores][limit] zrevrangebyscore key max min [withscores] 按条件删除数据 1 2 zremrangebyrank key start stop //按索引删除 zremrangebyscore key min max //按范围删除 综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //新增一个有序集合myzset，并加入一个元素baidu.com，给它赋予的序号是1： 127.0.0.1:6379\u0026gt; zadd myzset 1 baidu.com (integer) 1 //向myzset中新增一个元素360.com，赋予它的序号是3 127.0.0.1:6379\u0026gt; zadd myzset 3 360.com (integer) 1 //向myzset中新增一个元素google.com，赋予它的序号是2 127.0.0.1:6379\u0026gt; zadd myzset 2 google.com (integer) 1 //列出myzset的所有元素，同时列出其序号，可以看出myzset已经是有序的了。 127.0.0.1:6379\u0026gt; zrange myzset 0 -1 with scores 1) \u0026#34;baidu.com\u0026#34; 2) \u0026#34;1\u0026#34; 3) \u0026#34;google.com\u0026#34; 4) \u0026#34;2\u0026#34; 5) \u0026#34;360.com\u0026#34; 6) \u0026#34;3\u0026#34; //只列出myzset的元素 127.0.0.1:6379\u0026gt; zrange myzset 0 -1 1) \u0026#34;baidu.com\u0026#34; 2) \u0026#34;google.com\u0026#34; 3) \u0026#34;360.com\u0026#34; 进阶操作 获取集合数据总量 1 2 zcard key zcount key min max 集合交、并集操作 1 2 zinterstore destination numkeys key [key …] zunionstore destination numkeys key [key …] sorted_set类型数据操作的注意事项 score保存的数据存储空间是64位，如果是整数范围是-9007199254740992~9007199254740992 score保存的数据也可以是一个双精度的double值，基于双精度浮点数的特征，可能会丢失精度，使用时候要慎重 sorted_set底层存储还是基于set结构的，因此数据不能重复，如果重复添加相同的数据，score值将被反复覆盖，保留最后一次修改的结果\n","date":"2025-03-18T15:41:00+08:00","permalink":"/p/redis/","title":"Redis"},{"content":"参考网站 自学SQL网(教程 视频 练习全套)\n学完知识就有题做\nMySQL 教程 | 菜鸟教程\nMySQL总结_sq连表-CSDN博客\n主键 - SQL教程 - 廖雪峰的官方网站\n观感最好的教程\n关系模型 引用自 关系模型 - SQL教程 - 廖雪峰的官方网站\n主键 在关系数据库中，一张表中的每一行数据被称为一条记录。一条记录就是由多个字段组成的。例如，students表的两行记录：\nid class id name gender score 1 1 小明 M 90 2 1 小红 F 95 对于关系表，有个很重要的约束，就是任意两条记录不能重复。不能重复不是指两条记录不完全相同，而是指能够通过某个字段唯一区分出不同的记录，这个字段被称为主键。\n例如，假设我们把name字段作为主键，那么通过名字小明或小红就能唯一确定一条记录。但是，这么设定，就没法存储同名的同学了，因为插入相同主键的两条记录是不被允许的。\n对主键的要求，最关键的一点是：记录一旦插入到表中，主键最好不要再修改，因为主键是用来唯一定位记录的，修改了主键，会造成一系列的影响。\n由于主键的作用十分重要，如何选取主键会对业务开发产生重要影响。如果我们以学生的身份证号作为主键，似乎能唯一定位记录。然而，身份证号也是一种业务场景，如果身份证号升位了，或者需要变更，作为主键，不得不修改的时候，就会对业务产生严重影响。\n所以，选取主键的一个基本原则是：不使用任何业务相关的字段作为主键。\n因此，身份证号、手机号、邮箱地址这些看上去可以唯一的字段，均不可用作主键。\n作为主键最好是完全业务无关的字段，我们一般把这个字段命名为id。常见的可作为id字段的类型有：\n自增整数类型：数据库会在插入数据时自动为每一条记录分配一个自增整数，这样我们就完全不用担心主键重复，也不用自己预先生成主键； 全局唯一GUID类型：也称UUID，使用一种全局唯一的字符串作为主键，类似8f55d96b-8acc-4636-8cb8-76bf8abc2f57。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的，大部分编程语言都内置了GUID算法，可以自己预算出主键。 对于大部分应用来说，通常自增类型的主键就能满足需求。我们在students表中定义的主键也是BIGINT NOT NULL AUTO_INCREMENT类型。\n如果使用INT自增类型，那么当一张表的记录数超过2147483647（约21亿）时，会达到上限而出错。使用BIGINT自增类型则可以最多约922亿亿条记录。\n小结 主键是关系表中记录的唯一标识。主键的选取非常重要：主键不要带有业务含义，而应该使用BIGINT自增或者GUID类型。主键也不应该允许NULL。\n可以使用多个列作为联合主键，但联合主键并不常用。\n外键 当我们用主键唯一标识记录时，我们就可以在students表中确定任意一个学生的记录：\nid name other columns\u0026hellip; 1 小明 \u0026hellip; 2 小红 \u0026hellip; 我们还可以在classes表中确定任意一个班级记录：\nid name other columns\u0026hellip; 1 一班 \u0026hellip; 2 二班 \u0026hellip; 但是我们如何确定students表的一条记录，例如，id=1的小明，属于哪个班级呢？\n由于一个班级可以有多个学生，在关系模型中，这两个表的关系可以称为“一对多”，即一个classes的记录可以对应多个students表的记录。\n为了表达这种一对多的关系，我们需要在students表中加入一列class_id，让它的值与classes表的某条记录相对应：\nid class_id name other columns\u0026hellip; 1 1 小明 \u0026hellip; 2 1 小红 \u0026hellip; 5 2 小白 \u0026hellip; 这样，我们就可以根据class_id这个列直接定位出一个students表的记录应该对应到classes的哪条记录。\n小明的class_id是1，因此，对应的classes表的记录是id=1的一班； 小红的class_id是1，因此，对应的classes表的记录是id=1的一班； 小白的class_id是2，因此，对应的classes表的记录是id=2的二班。 在students表中，通过class_id的字段，可以把数据与另一张表关联起来，这种列称为外键。\n外键并不是通过列名实现的，而是通过定义外键约束实现的：\n1 2 3 4 ALTER TABLE students ADD CONSTRAINT fk_class_id FOREIGN KEY (class_id) REFERENCES classes (id); 其中，外键约束的名称fk_class_id可以任意，FOREIGN KEY (class_id)指定了class_id作为外键，REFERENCES classes (id)指定了这个外键将关联到classes表的id列（即classes表的主键）。\n通过定义外键约束，关系数据库可以保证无法插入无效的数据。即如果classes表不存在id=99的记录，students表就无法插入class_id=99的记录。\n由于外键约束会降低数据库的性能，大部分互联网应用程序为了追求速度，并不设置外键约束，而是仅靠应用程序自身来保证逻辑的正确性。这种情况下，class_id仅仅是一个普通的列，只是它起到了外键的作用而已。\n要删除一个外键约束，也是通过ALTER TABLE实现的：\n1 2 ALTER TABLE students DROP FOREIGN KEY fk_class_id; 注意：删除外键约束并没有删除外键这一列。删除列是通过DROP COLUMN ...实现的。\n多对多 通过一个表的外键关联到另一个表，我们可以定义出一对多关系。有些时候，还需要定义“多对多”关系。例如，一个老师可以对应多个班级，一个班级也可以对应多个老师，因此，班级表和老师表存在多对多关系。\n多对多关系实际上是通过两个一对多关系实现的，即通过一个中间表，关联两个一对多关系，就形成了多对多关系：\nteachers表：\nid name 1 张老师 2 王老师 3 李老师 4 赵老师 classes表：\nid name 1 一班 2 二班 中间表teacher_class关联两个一对多关系：\nid teacher_id class_id 1 1 1 2 1 2 3 2 1 4 2 2 5 3 1 6 4 2 通过中间表teacher_class可知teachers到classes的关系：\nid=1的张老师对应id=1,2的一班和二班； id=2的王老师对应id=1,2的一班和二班； id=3的李老师对应id=1的一班； id=4的赵老师对应id=2的二班。 同理可知classes到teachers的关系：\nid=1的一班对应id=1,2,3的张老师、王老师和李老师； id=2的二班对应id=1,2,4的张老师、王老师和赵老师； 因此，通过中间表，我们就定义了一个“多对多”关系。\n一对一 一对一关系是指，一个表的记录对应到另一个表的唯一一个记录。\n例如，students表的每个学生可以有自己的联系方式，如果把联系方式存入另一个表contacts，我们就可以得到一个“一对一”关系：\nid student_id mobile 1 1 135xxxx6300 2 2 138xxxx2209 3 5 139xxxx8086 有细心的童鞋会问，既然是一对一关系，那为啥不给students表增加一个mobile列，这样就能合二为一了？\n如果业务允许，完全可以把两个表合为一个表。但是，有些时候，如果某个学生没有手机号，那么，contacts表就不存在对应的记录。实际上，一对一关系准确地说，是contacts表一对一对应students表。\n还有一些应用会把一个大表拆成两个一对一的表，目的是把经常读取和不经常读取的字段分开，以获得更高的性能。例如，把一个大的用户表分拆为用户基本信息表user_info和用户详细信息表user_profiles，大部分时候，只需要查询user_info表，并不需要查询user_profiles表，这样就提高了查询速度。\n小结 关系数据库通过外键可以实现一对多、多对多和一对一的关系。外键既可以通过数据库来约束，也可以不设置约束，仅依靠应用程序的逻辑来保证。\n索引 在关系数据库中，如果有上万甚至上亿条记录，在查找记录的时候，想要获得非常快的速度，就需要使用索引。\n索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引，可以让数据库系统不必扫描整个表，而是直接定位到符合条件的记录，这样就大大加快了查询速度。\n例如，对于students表：\nid class_id name gender score 1 1 小明 M 90 2 1 小红 F 95 3 1 小军 M 88 如果要经常根据score列进行查询，就可以对score列创建索引：\n1 2 ALTER TABLE students ADD INDEX idx_score (score); 使用ADD INDEX idx_score (score)就创建了一个名称为idx_score，使用列score的索引。索引名称是任意的，索引如果有多列，可以在括号里依次写上，例如：\n1 2 ALTER TABLE students ADD INDEX idx_name_score (name, score); 索引的效率取决于索引列的值是否散列，即该列的值如果越互不相同，那么索引效率越高。反过来，如果记录的列存在大量相同的值，例如gender列，大约一半的记录值是M，另一半是F，因此，对该列创建索引就没有意义。\n可以对一张表创建多个索引。索引的优点是提高了查询效率，缺点是在插入、更新和删除记录时，需要同时修改索引，因此，索引越多，插入、更新和删除记录的速度就越慢。\n对于主键，关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的，因为主键会保证绝对唯一。\n唯一索引 在设计关系数据表的时候，看上去唯一的列，例如身份证号、邮箱地址等，因为他们具有业务含义，因此不宜作为主键。\n但是，这些列根据业务要求，又具有唯一性约束：即不能出现两条记录存储了同一个身份证号。这个时候，就可以给该列添加一个唯一索引。例如，我们假设students表的name不能重复：\n1 2 ALTER TABLE students ADD UNIQUE INDEX uni_name (name); 通过UNIQUE关键字我们就添加了一个唯一索引。\n也可以只对某一列添加一个唯一约束而不创建唯一索引：\n1 2 ALTER TABLE students ADD CONSTRAINT uni_name UNIQUE (name); 这种情况下，name列没有索引，但仍然具有唯一性保证。\n无论是否创建索引，对于用户和应用程序来说，使用关系数据库不会有任何区别。这里的意思是说，当我们在数据库中查询时，如果有相应的索引可用，数据库系统就会自动使用索引来提高查询效率，如果没有索引，查询也能正常执行，只是速度会变慢。因此，索引可以在使用数据库的过程中逐步优化。\n小结 通过对数据库表创建索引，可以提高查询速度；\n通过创建唯一索引，可以保证某一列的值具有唯一性；\n数据库索引对于用户和应用程序来说都是透明的。\nSELECT 查询 1 2 3 4 5 6 7 8 SELECT column, another_column, … FROM mytable WHERE condition AND/OR another_condition AND/OR …; SELECT * FROM movies WHERE year\u0026gt;=2010 AND length_minutes\u0026lt;120; 筛选数字属性列 关键字 例 =, !=, \u0026lt; \u0026lt;=, \u0026gt;, ≥ col_name != 4 BETWEEN … AND … 在两个数之间 col_name BETWEEN 1.5 AND 10.5 NOT BETWEEN … AND … col_name NOT BETWEEN 1 AND 10 IN (…) 在一个列表 col_name IN (2, 4, 6) NOT IN (…) col_name NOT IN (1, 3, 5) 筛选字符串属性列 = 完全等于 != or \u0026lt;\u0026gt; 不等于 LIKE 没有用通配符等价于 = NOT LIKE 没有用通配符等价于 != % 通配符 col_name LIKE \u0026ldquo;%AT%” _(下划线) col_name LIKE \u0026ldquo;AN_” 1 2 3 4 5 6 7 /*通配符*/ col_name LIKE \u0026#34;%AT%\u0026#34;; /*\u0026#34;AT\u0026#34;\u0026#34;AT*...\u0026#34;\u0026#34;...*AT\u0026#34;\u0026#34;...*AT*...\u0026#34;均满足条件 \u0026#34;AT\u0026#34;前后可以有任意字符*/ col_name LIKE \u0026#34;AN_\u0026#34;; /*\u0026#34;AND\u0026#34;可以 \u0026#34;AN\u0026#34;\u0026#34;ANDD\u0026#34;均不行 与\u0026#39;%\u0026#39;相似 但只代表一个字符*/ 过滤/排序 1 2 3 4 /*用DISTINCT关键字来指定某个或某些属性列唯一返回*/ SELECT DISTINCT column, another_column, … FROM mytable WHERE condition(s); 1 2 3 4 5 6 7 8 9 10 11 /*让结果按一个或多个属性列做排序*/ SELECT column, another_column, … FROM mytable WHERE condition(s) /* ASC 升序或 DESC 降序*/ ORDER BY column ASC/DESC /*LIMIT来指定只返回多少行结果 用OFFSET来指定从哪一行开始返回*/ LIMIT num_limit OFFSET num_offset; /*关于OFFSET 若要输出第N行(及之后) 则OFFSET的参数须为N-1 */ 例题 SELECT复习题 在查询中使用表达式 实际上AS不仅用在表达式别名上，普通的属性列甚至是表（table）都可以取一个别名，这让SQL更容易理解\n1 2 3 4 5 --属性列和表取别名的例子 SELECT column AS better_column_name, … FROM a_long_widgets_table_name AS mywidgets INNER JOIN widget_sales ON mywidgets.id = widget_sales.widget_id; 1 2 3 4 5 --包含表达式的例子 SELECT particle_speed / 2.0 AS half_particle_speed --对结果做了一个除2 FROM physics_data WHERE ABS(particle_position) * 10.0 \u0026gt;500 --（条件要求这个属性绝对值乘以10大于500）; 在查询中进行统计 1 2 3 SELECT AGG_FUNC(column_or_expression) AS aggregate_description, … FROM mytable WHERE constraint_expression; 常用统计函数:\nFunction Description COUNT(*) COUNT(column) 计数！COUNT(*) 统计数据行数，COUNT(column) 统计column非NULL的行数 MIN(column) 找column最小的一行 MAX(column) 找column最大的一行 AVG(column) 对column所有行取平均值 SUM(column) 对column所有行求和 分组统计 GROUP BY 数据分组语法可以按某个col_name对数据进行分组，如：GROUP BY Year指对数据按年份分组， 相同年份的分到一个组里。如果把统计函数和GROUP BY结合，那统计结果就是对分组内的数据统计了 GROUP BY 分组结果的数据条数，就是分组数量，比如：GROUP BY Year，全部数据里有几年，就返回几条数据， 不管是否应用了统计函数\n1 2 3 4 5 --用分组的方式统计 SELECT AGG_FUNC(column_or_expression) AS aggregate_description, … FROM mytable WHERE constraint_expression GROUP BY column; 在 GROUP BY 分组语法中，我们知道数据库是先对数据做 WHERE ，然后对结果做分组，如果我们要对分组完的数据再筛选出几条如何办？ 一个不常用的语法 HAVING 语法将用来解决这个问题，他可以对分组之后的数据再做SELECT筛选\nHAVING 和 WHERE 语法一样，只不过作用的结果集不一样. 在我们例子数据表数据量小的情况下可能感觉 HAVING 没有什么用，但当你的数据量成千上万属性又很多时也许能帮上大忙\nJOIN 连接 数据库范式 数据库范式是数据表设计的规范，在范式规范下，数据库里每个表存储的重复数据降到最少(这有助于数据的一致性维护)同时在数据库范式下，表和表之间不再有很强的数据耦合，可以独立的增长(ie. 比如汽车引擎的增长和汽车的增长是完全独立的)\n1 2 3 4 5 6 7 8 SELECT column, another_table_column, … FROM mytable --主表 INNER JOIN another_table --要连接的表 ON mytable.id = another_table.id --想象一下刚才讲的主键连接，两个相同的连成1条 WHERE condition(s) ORDER BY column, … ASC/DESC LIMIT num_limit OFFSET num_offset; 本例中ON条件描述的关联关系:\nINNER JOIN (内)连接 先将两个表数据连接到一起，两个表中如果通过ID互相找不到的数据将会舍弃。此时，你可以将连表后的数据看作两个表的合并，SQL中的其他语句会在这个合并基础上 继续执行(想一下和之前的单表操作就一样了) 还有一个理解 INNER JOIN 的方式，就是把 INNER JOIN 想成两个集合的交集。\nOUTER JOIN外连接 1 2 3 4 5 6 7 8 --用LEFT/RIGHT/FULL JOINs 做多表查询 SELECT column, another_column, … FROM mytable INNER/LEFT/RIGHT/FULL JOIN another_table ON mytable.id = another_table.matching_id WHERE condition(s) ORDER BY column, … ASC/DESC LIMIT num_limit OFFSET num_offset; 在表A 连接 B， LEFT JOIN 保留A的所有行，不管有没有能匹配上B，反过来 RIGHT JOIN则保留所有B里的行。最后FULL JOIN 不管有没有匹配上，同时保留A和B里的所有行\n将两个表数据1-1连接，保留A或B的原有行，如果某一行在另一个表不存在，会用 NULL来填充结果数据。\n查询执行顺序 1 2 3 4 5 6 7 8 9 10 --这才是完整的SELECT查询 SELECT DISTINCT column, AGG_FUNC(column_or_expression), … FROM mytable JOIN another_table ON mytable.column = another_table.column WHERE constraint_expression GROUP BY column HAVING constraint_expression ORDER BY column ASC/DESC LIMIT count OFFSET COUNT; 1. FROM 和 JOIN FROM 或 JOIN 会第一个执行，确定一个整体的数据范围. 如果要JOIN不同表，可能会生成一个临时Table来用于 下面的过程。总之第一步可以简单理解为确定一个数据源表（含临时表）\n2. WHERE 我们确定了数据来源 WHERE 语句就将在这个数据源中按要求进行数据筛选，并丢弃不符合要求的数据行，所有的筛选col属性 只能来自 FROM 圈定的表. AS别名还不能在这个阶段使用，因为可能别名是一个还没执行的表达式\n3. GROUP BY 如果你用了 GROUP BY 分组，那 GROUP BY 将对之前的数据进行分组，统计等，并将是结果集缩小为分组数.这意味着 其他的数据在分组后丢弃.\n4. HAVING 如果你用了 GROUP BY 分组, HAVING 会在分组完成后对结果集再次筛选。AS别名也不能在这个阶段使用.\n5. SELECT 确定结果之后， SELECT 用来对结果col简单筛选或计算，决定输出什么数据.\n6. DISTINCT 如果数据行有重复 DISTINCT 将负责排重.\n7. ORDER BY 在结果集确定的情况下， ORDER BY 对结果做排序。因为 SELECT 中的表达式已经执行完了。此时可以用AS别名.\n8. LIMIT / OFFSET 最后 LIMIT 和 OFFSET 从排序的结果中截取部分数据.\n结论 不是每一个SQL语句都要用到所有的句法，但灵活运用以上的句法组合和深刻理解SQL执行原理将能在SQL层面更好的解决数据问题，而不用把问题 都抛给程序逻辑.\nNULL 如果某个字段你没有填写到数据库，很可能就会出现 NULL 。所有一个常见的方式就是为字段设置 默认值 ,比如 数字的默认值设置为0，字符串设置为 \u0026quot;\u0026quot; 字符串. 但是在一些 NULL 表示它本来含义的场景，需要注意是否设置默认值还是保持 NULL 。 (比如, 当你计算一些行的平均值的时候，如果是0会参与计算导致平均值差错，是 NULL 则不会参与计算).\n还有一些情况很难避免 NULL 的出现, 比如之前说的 outer-joining 多表连接，A和B有数据差异时，必须用 NULL 来填充。这种情况，可以用 IS NULL 和 IS NOT NULL 来选在某个字段是否等于 NULL.\n1 2 3 4 5 SELECT column, another_column, … FROM mytable WHERE column IS/IS NOT NULL AND/OR another_condition AND/OR …; 修改数据 部分引用自 修改数据 - SQL教程 - 廖雪峰的官方网站\n关系数据库的基本操作就是增删改查，即CRUD：Create、Retrieve、Update、Delete。其中，对于查询，我们已经详细讲述了SELECT语句的详细用法。\n而对于增、删、改，对应的SQL语句分别是：\nINSERT：插入新记录； UPDATE：更新已有记录； DELETE：删除已有记录。 我们将分别讨论这三种修改数据的语句的使用方法。\nINSERT 插入 引用于 MySQL 插入数据 | 菜鸟教程\n例如，我们向user表插入一条新记录，先列举出需要插入的字段名称，然后在VALUES子句中依次写出对应字段的值：\n1 2 3 4 5 INSERT INTO table_name (column1, column2, column3, ...) VALUES (value1, value2, value3, ...); -- INSERT INTO users (username, email, birthdate, is_active) VALUES (\u0026#39;test\u0026#39;, \u0026#39;test@runoob.com\u0026#39;, \u0026#39;1990-01-01\u0026#39;, true); 如果要插入所有列(即插入行)的数据，可以省略列名：\n1 2 INSERT INTO users VALUES (NULL,\u0026#39;test\u0026#39;, \u0026#39;test@runoob.com\u0026#39;, \u0026#39;1990-01-01\u0026#39;, true); 还可以一次性添加多条记录，只需要在VALUES子句中指定多个记录值，每个记录是由(...)包含的一组值，每组值用逗号,分隔：\n1 2 3 4 5 INSERT INTO users (username, email, birthdate, is_active) VALUES (\u0026#39;test1\u0026#39;, \u0026#39;test1@runoob.com\u0026#39;, \u0026#39;1985-07-10\u0026#39;, true), (\u0026#39;test2\u0026#39;, \u0026#39;test2@runoob.com\u0026#39;, \u0026#39;1988-11-25\u0026#39;, false), (\u0026#39;test3\u0026#39;, \u0026#39;test3@runoob.com\u0026#39;, \u0026#39;1993-05-03\u0026#39;, true); UPDATE 更新 MySQL UPDATE 更新 | 菜鸟教程\n1 2 3 UPDATE table_name SET column1 = value1, column2 = value2, ... WHERE condition; 参数说明：\ntable_name 是你要更新数据的表的名称。 column1, column2, \u0026hellip; 是你要更新的列的名称。 value1, value2, \u0026hellip; 是新的值，用于替换旧的值。 WHERE condition 是一个可选的子句，用于指定更新的行。如果省略 WHERE 子句，将更新表中的所有行。 更多说明：\n你可以同时更新一个或多个字段。 你可以在 WHERE 子句中指定任何条件。 你可以在一个单独表中同时更新数据。 当你需要更新数据表中指定行的数据时 WHERE 子句是非常有用的。\n1 2 3 4 --更新单个列的值 UPDATE employees SET salary = 60000 WHERE employee_id = 101; 1 2 3 4 --更新多个列的值 UPDATE orders SET status = \u0026#39;Shipped\u0026#39;, ship_date = \u0026#39;2023-03-01\u0026#39; WHERE order_id = 1001; 1 2 3 4 --使用表达式更新值 UPDATE products SET price = price * 1.1 WHERE category = \u0026#39;Electronics\u0026#39;; 1 2 3 4 5 6 7 8 --更新使用子查询的值 UPDATE customers SET total_purchases = ( SELECT SUM(amount) FROM orders WHERE orders.customer_id = customers.customer_id ) WHERE customer_type = \u0026#39;Premium\u0026#39;; 在使用MySQL这类真正的关系数据库时，UPDATE语句会返回更新的行数以及WHERE条件匹配的行数。\n例如，更新id=1的记录时：\n1 2 3 mysql\u0026gt; UPDATE students SET name=\u0026#39;大宝\u0026#39; WHERE id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 MySQL会返回1，可以从打印的结果Rows matched: 1 Changed: 1看到。\n当更新id=999的记录时：\n1 2 3 mysql\u0026gt; UPDATE students SET name=\u0026#39;大宝\u0026#39; WHERE id=999; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 MySQL会返回0，可以从打印的结果Rows matched: 0 Changed: 0看到。\nDELETE 删除 DELETE语句的基本语法是：\n1 DELETE FROM \u0026lt;表名\u0026gt; WHERE ...; 例如，我们想删除students表中id=1的记录，就需要这么写：\n1 2 3 4 -- 删除id=1的记录: DELETE FROM students WHERE id=1; -- 查询并观察结果: SELECT * FROM students; 注意到DELETE语句的WHERE条件也是用来筛选需要删除的行，因此和UPDATE类似，DELETE语句也可以一次删除多条记录：\n1 2 3 4 -- 删除id=5,6,7的记录: DELETE FROM students WHERE id\u0026gt;=5 AND id\u0026lt;=7; -- 查询并观察结果: SELECT * FROM students; 如果WHERE条件没有匹配到任何记录，DELETE语句不会报错，也不会有任何记录被删除。例如：\n1 2 3 4 -- 删除id=999的记录: DELETE FROM students WHERE id=999; -- 查询并观察结果: SELECT * FROM students; 最后，要特别小心的是，和UPDATE类似，不带WHERE条件的DELETE语句会删除整个表的数据：\n1 DELETE FROM students; 这时，整个表的所有记录都会被删除。所以，在执行DELETE语句时也要非常小心，最好先用SELECT语句来测试WHERE条件是否筛选出了期望的记录集，然后再用DELETE删除。\n在使用MySQL这类真正的关系数据库时，DELETE语句也会返回删除的行数以及WHERE条件匹配的行数。\n例如，分别执行删除id=1和id=999的记录：\n1 2 3 4 5 mysql\u0026gt; DELETE FROM students WHERE id=1; Query OK, 1 row affected (0.01 sec) mysql\u0026gt; DELETE FROM students WHERE id=999; Query OK, 0 rows affected (0.01 sec) CREATE 创建 MySQL 创建数据表 | 菜鸟教程\n1 2 3 4 5 6 7 8 --用户表实例 CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, birthdate DATE, is_active BOOLEAN DEFAULT TRUE ); 实例解析：\nid: 用户 id，整数类型，自增长，作为主键。 username: 用户名，变长字符串，不允许为空。 email: 用户邮箱，变长字符串，不允许为空。 birthdate: 用户的生日，日期类型。 is_active: 用户是否已经激活，布尔类型，默认值为 true。 以上只是一个简单的实例，用到了一些常见的数据类型包括INT, VARCHAR, DATE, BOOLEAN，可以根据实际需要选择不同的数据类型。\nAUTO_INCREMENT 关键字用于创建一个自增长的列，PRIMARY KEY 用于定义主键。\n如果希望在创建表时指定数据引擎，字符集和排序规则等，可以使用 CHARACTER SET 和 COLLATE 子句：\n1 2 3 4 CREATE TABLE mytable ( id INT PRIMARY KEY, name VARCHAR(50) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ","date":"2025-03-17T20:06:34+08:00","permalink":"/p/mysql/","title":"MySQL"}]