自动分析句子成分的app 句子语法成分分析app

国学综合

自动分析句子成分的app 句子语法成分分析app

执掌命轮围观:℉更新时间:05-27 06:20

你现在阅读的是一篇关于自动分析句子成分的app的文章,里面有丰富多彩的内容,还有给你准备句子语法成分分析app和自动分析句子成分的app的精彩内容哦。

自动分析句子成分的app 句子语法成分分析app

自动分析句子成分的app 句子语法成分分析app

作者 | 赵志、曾庆隆、顾梦奇、王强、赵发

出品 | CSDN(ID:CSDNnews)

2023 年 3 月 25 日,苹果发布了 Swift 5.0 版本,宣布了 ABI 稳定,并且Swift runtime 和标准库已经植入系统中,而且苹果新出文档都用 Swift,Sample Code 也是 Swift,可以看出 Swift 是苹果扶持与研发的重点方向。

目前国内外各大公司都在相继试水,只要关注 Swift 在国内 iOS 生态圈现状,你就会发现,Swift 在国内 App 应用的比重逐渐升高。对于新 App 来说,可以直接用纯 Swift 进行开发,而对于老 App 来说,绝大部分以前都是用 OC 开发的,因此 Swift/OC 混编是一个必然面临的问题。

CSDN 付费下载自视觉中国

Swift 和 OC 混编开发

关于 Swift 和 OC 间如何混编,业内也已经有很多相关文章详细讲解,简单来说 OC/Swift 调用 Swift,最终通过 Swift Module 进行,而 Swift 调用 OC 时,则是通过 Clang Module,当然也可以通过 Clang Module 进行 OC 对 OC 的调用。58同城于 2023 年正式上线首个 Swift/OC(Objective-C,以下简称 OC)项目,与此同时,也在全公司范围内开展了一个多部门协作项目——混天项目,主要目标:

我们在 Module 化实践中发现,实际数据与苹果官方 Module 编译时间数据不一致,于是我们通过 Clang 源码和数据相结合的方式对 Clang Module进行了深入研究,找到了耗时的原因。由于 Swift/OC 混编下需要 Module 化的支持,同时借鉴业内 HeaderMap 方案让 OC 调用 OC 时避开 Module 化调用,将编译时间优化了约 35%,较好地解决了在 Module 化下的编译时间问题。

Clang Module 初探

Clang Module 在 2023 LLVM Developers Meeting 上第一次被提出,主要用来解决 C 语言预处理的各种问题。Modules 试图通过隔离特定库的接口并且编译一次生成高效的序列化文件来避免 C 预处理器重复解析 Header 的问题。在探究 Clang Module 之前,我们先了解一下预处理的前世今生。

一个源代码文件到经过编译输出为目标文件主要分为下面几个阶段:

源文件在经过 Clang 前端包含:词法分析(Lexical analysis) 、语法分析(Syntactic analysis) 、语义分析(Semantic analysis)。最后输出与平台无关的 IR(LLVM IR generator)进而交给后端进行优化生成汇编输出目标文件。

词法分析(Lexical analysis)作为前端的第一个步骤负责处理源代码的文本输入,具体步骤就是将语言结构拆分为一组单词和记号(token),跳过注释,空格等无意义的字符,并将一些保留关键字转义为定义好的类型。词法分析过程中遇到源代码 “#“ 的字符,且该字符在源代码行的起始位置,则认为它是一个预处理指令,会调用预处理器(Preprocessor)处理后续。在开发中引入外部文件的 include/import 指令,定义宏 define 等指令均是在预处理阶段交由预处理器进行处理。Clang Module 机制的引入带来的改变着重于解决常规预处理阶段的问题,那么跟随我们一起来重点探究一下其中的区别和实现原理吧!

2.1 普通 import 的机制

Clang Module 机制引入之前,在日常开发中,如果需要在源代码中引入外部的一些定义或者声明,常见的做法就是使用 #import 指令来使用外部的 API。那么这些使用的方式在预处理阶段是怎么处理的呢?

针对编译器遇到 #import<PodName/header.h> 或者 #import ”header.h” 这种导入方式时候,# 开头在词法分析阶段会触发预处理(Preprocessor)。而对于 Clang 的预处理器 import 与 include 指令都属于它的关键词。预处理器在处理 import Directive 时候主要工作为通过导入的 header 名称去查找文件的磁盘所在路径,然后进入该文件创建新的词法分析器对导入的头文件进行词法分析。

如下所示:编译器在遇到 #import 或者 #include 指令时,触发预处理机制查询头文件的路径,进入头文件对头文件的内容进行解析的流程。

以单个文件编译过程为维度举例:在针对一个文件编译输出目标文件的过程中,可能会引入多个外界的头文件,而被引入多个外界头文件也有可能存在引入外界头文件。这样的情况就导致虽然只是在编译单个文件,但是预处理器会对引入的头文件进行层层展开。这也是很多人称 #import 与 include 是一种特殊“复制”效果的原因。

那么在这种预处理器的机制在工程中编译中会存在什么问题呢?苹果官方在 2023 的 WWDC 视频上同样给了我们解答:Header Fragility (健壮性)和 Inherently Non-Scalable (不可扩展性)。

来看下面一段代码,在 PodBTestObj 类的文件中定义一个 ClassName 字符串的宏,然后在导入 PoBClass1.h 头文件,在 PoBClass1.h 的头文件中同样定义一个结构体名为 ClassName,这里与我们在 PodBTestObj 类中定义的宏同名。预处理的特殊的“复制”机制,在预处理阶段会发生下图所见的结果:

这样的问题相信在日常开发中并不罕见,而为了解决这种重名的问题,我们常规的手法只能通过增加前缀或者提前约定规则等方式来解决。

视频中同时指出这种机制在应对大型工程编译过程中的所带来的消耗问题。假设有 N 个源文件的工程,那么每个源文件引用 M 个头文件,由于预处理的这种机制,我们在针对处理每个源文件的编译过程中会对引入的 M 个头文件进行展开,经历一遍遍的词法分析-语法分析-语义分析的过程。那么你能想象一下针对系统头文件的引入在预处理阶段将会是一个多么庞大的开销!

那么针对 C 语言预处理器存在的问题,苹果有哪些方案可以优化这些存在的问题呢?

2.2 PCH (Precompiled Headers)

PCH(Precompile Prefix Header File)文件,也就是预编译头文件,其文件里的内容能被项目中的其他所有源文件访问。日常开发中,通常放一些通用的宏和头文件,方便编写代码,提高效率。

关于 PCH 的概述,苹果是这样定义的:

which uses a serialized representation of Clang’s internal data structures, encoded with the LLVM bitstream format.

(使用 Clang 内部数据结构序列化表示,采用的 LLVM 字节流表示)。

它的设计理念当项目中几乎每个源文件中都包含一组通用的头文件时,将该组头文件写入 PCH 文件中。在编译项目中的流程中,每个源文件的处理都会首先去加载 PCH 文件的内容,所以一旦 PCH 编译完成,后续源文件在处理引入的外部文件时候会复用 PCH 编译后的内容,从而加快编译速度。PCH 文件中存放我们所需要的外部头文件的信息(包括不局限于声明、定义等)。它以特殊二进制形式进行存储,而在每个源代码编译处理外部头文件信息时候,不需要每次进行头文件的展开和“复制”重复操作。而只需要“懒加载”预编译好的 PCH 内容即可。

存储内容方面它存放着序列化的 AST 文件。AST 文件本身包含 Clang 的抽象语法树和支持数据结构的序列化表示,它们使用与 LLVM’s bitcode file format. 相同的压缩位流进行存储。关于 AST File 文件的存储结构你可以在官方文档有详细的了解。

它作为苹果一种优化方案被提出,但是实际的工程中源代码的引用关系是很复杂的,所以找出一组几乎所有源文件都包含的头文件基本不可能,同时对于代码更新维护更是一个挑战。其次在被包含头文件改动下,因为 PCH 会被所有源文件引入,会带来代码“污染”的问题。同时一旦 PCH 文件发生改动,会导致大面积的源代码重编造成编译时间的浪费。

2.3 Modules

上述我们简单回顾了一些 C 语言预处理的机制以及为解决编译消耗引入 PCH 的方案,但是在一定程度上 PCH 方案也存在很大的缺陷。因此在 2023 LLVM Developer’s Meeting 首次提出了 Modules 的概念。

那么 Module 到底是什么呢?

Module 简单来说可以认为它是对一个组件的抽象描述,包含组件的接口和实现。Module 机制推出主要用来解决上述所阐述的预处理问题,想要探究 Clang Module 的实现,首先需要去开启 Module。那么针对 iOS 工程怎么开启 Module 呢只需要打开编译选项中:

对!你没看错,仅仅需要在 Xcode 的编译选项中修改配置即可。

而在代码的使用上几乎可以不用修改代码,开启 Module 之后,通过引用头文件的方式可以继续沿用 #import <PodName/Header.h> 方式。当然对于开发者也可以采用新的方式 @import ModuleName.SubModuleName,以及 @import ModuleName这几种方式。更为详细的信息和使用方法可以在苹果的官方文档中查看。

2.4 苹果对 Module 的解读

上文提到过基于 C 语言预处理器提供的 #include 机制提供的访问外界库 API 的方式存在的伸缩性和健壮性的问题。Modules 提供了更为健壮,更高效的语义模型来替换之前 textual preprocessor 改进对库的 API 访问方式。

苹果官方文档中针对 Module 的解读有以下几个优势:

我们翻阅了苹果 WWDC 2023 的 Advances in Objective-C 视频,视频中针对编译时间性能方面进行了 PCH 和 Module 编译速度的数据分析。苹果给出的结论是小项目中 Module 比 PCH 能提升 40% 的编译时间,并且随着工程规模的不断增大,如增大到 Xcode 级别,Module 的编译速度也会比 PCH 稍快。PCH 也是为了加速编译而存在的,由此也可以间接得出结论,Module的编译速度要比没有 PCH 的情况下,是更快的,如在 Mail 下,应该提升 40% 以上。

对 Clang Module 机制建立一定的认知上,我们着手进行了 Clang Module 在 58同城 App 上的 Module 化改造。

58同城初步实践

3.1 Module 化工程配置

在多 pod 的项目中,通过以下几种方式可以将各 pod 进行 Module 化:

  1. Podfile 中添加 use_modular_headers! 对所有的 pod 进行 Module 化;

  2. Podfile 中通过 modular_headers 对每个 pod 单独进行 Module 化,如对 PodC 进行 Module 化,pod 'PodC', :path => '../PodC',:modular_headers => true;

  3. 在 pod 所对应的 .podspecs 中的 xcconfig 中 sg 配置 DEFINES_MODULE,如 s.xcconfig = {'DEFINES_MODULE' => 'YES'}。

此外,为了能让其它组件能通过 module 方式引用 Module 化的组件,还需要设置它们之前的依赖关系。

在58同城中,维护了一个全局的依赖配置文件 dependency.json,这个文件通过自动化工具进行维护,各组件 pod 的 .podspecs 从 dependency.json 中动态读取自己依赖的其它组件,并生成相应的 dependency 关系。

3.2 Swift/OC 混编桥接文件

通常在 Swift/OC 混编工程中会自动或手动在当前pod添加加一个桥接文件,如 PodC-Bridging-Header.h,配置当前 pod 中 Swift 需要引用的 OC 文件,形式如下所示。

这样可以达到编译的目的,但是由于依赖的组件都是在桥接文件中统一配置,对于每个 Swift 文件依赖了哪些 pod 组件,实际上并不清楚,而且 Swift 中每次修改新增一个 OC 文件的引用,都需要在桥接文件中进行修改,并且如果是减少对某个 OC 文件的引用,也不好确定是否要在桥接文件中进行删除,因为还需要判断其它 Swift 文件中是否有引用。

Swift 文件中可以通过 module 的方式去引用 OC 文件,因此,如果所依赖 OC 文件的 pod 都 Module 化后,可以通过 import module 的方式进行引用,每个 Swift 文件各自维护对外部 pod 的依赖,从而将 XXX-Bridging-Header.h 文件删除,也减少了对桥接文件的维护成本。

3.3 同城的 Module 化编译数据

万事具备,只差编译!

结合苹果官方给出了性能数据,我们预测 Module 化后的编译速度是要比非 Module 情况更快,那不妨就编译试试,接下来在 58同城中分别在 module 和非 module 场景下进行编译。

通过编译数据,我们看到的结果发生了逆转,Module 化之后的时间竟然比非 Module 情况下长约 8%,这跟刚才我们看到的苹果官方数据不符,有点乱了。需要说明的是这份数据是 58同城全业务线在 M1 机器上运行出来的,并且把资源复制的环节从配置中删除了,即不包含资源复制时间,是纯代码编译时间,并且在非 M1 机器上也运行了进行对比,除了时间长些,结论基本也是 module 化之后时间长 10% 左右。

在面对实际测试结果 Module 化之后的编译耗时更长的情况下,我们从更深层次上进行对 Clang Module 原理进行了探究。

Clang Module 原理深究

Clang Module 机制的引入主要是为了解决预处理器的各种问题,那么工程在开启 Module 之后,工程上会有哪些变化呢?同时在编译过程中编译器工作流程与之前又有哪些不同呢?

4.1 ModuleMap 与 Umbrella

以基于 cocoapods 作为组件化管理工具为例,开启 Module 之后工程上带来最直观的改变是pod组件下 Support Files 目录新增几个文件:podxxx.moduleMap , podxxx-umbrella.h。

Clang 官方文档指出如果要支持 Module,必须提供一个 ModuleMap 文件用来描述从头文件到模块逻辑结构的映射关系。ModuleMap 文件的书写使用 Module Map Language。通过示例可以发现它定义了 Module 的名字,umbrella header 包含了其目录下的所有头文件。module * 该通配符的作用是为每个头文件创建一个 subModule。

简单来说,我们可以认为 ModuleMap 文件为编译器提供了构建 Clang Module 的一张地图。它描述了我们要构建的 Module 的名称以及 Module 结构中要暴露供外界访问的 API。为编译器构建 Module 提供必要条件。

除了上述开启 Module 的组件会新增 ModuleMap 与 Umbrella 文件之外。在使用开启 Module 的组件时候也有一些改变,使用 Module 组件的 target 中 BuildSetting 中 Other C Flag 中会增加 -fmodule-map-file 的参数。

苹果官方文章中对该参数的解释为:

Load the given module map file if a header from its directory or one of its subdirectories is loaded.

(当我们加载一个头文件属于 ModuleMap 的目录或者子目录则去加载 ModuleMap File)。

4.2 Module 的构建

了解完 ModuleMap 与 Umbrella 文件和新增的参数之后,我们决定深入去跟踪一下这些文件与参数的在编译期间的使用。

上文提到过在词法分析阶段以“#”开头的预处理指令,我们对针对 HeaderName 文件进行真实路径查找,并对要导入的文件进行同样的词法,语法,语义等操作。在开启 Module 化之后,头文件查找流程与之前有什么区别呢?在不修改代码的基础上编译器又是怎么识别为语义化模型导入(Module import)呢?

如下图所示:在初始化预处理之前,会针对 buildsetting 中设置的 Header Search path,Framework Search Path 等编译参数解析赋值给 SearchDirs。

在 Clang 的源码中 Header Search 类负责具体头文件的查找工作,Header Search 类中持有的 SearchDirs 存放着当前编译文件所需要的头文件搜索路径。其中对于一个头文件的搜索分三种情况:hmap, Header Search Path 以及 Frameworks search path。而 SearchDirs 的赋值发生在编译实体(CompilerInstance)初始化预处理器时,而这些参数的来源则是在 Xcode 工程 Buildsetting 中的相关编译参数。

编译器在查询头文件具体磁盘路径的过程中,会通过 Header.h 或者 PodName/Header.h 与 SearchDirs 集合中的路径拼接判断该路径下是否存在我们要查找的头文件。当前循环的 SearchDirs 对应的元素中根据类型:(Header Search Path,Frameworks,HeaderMap)进行相应的查询流程。

上文提到过针对开启 Module 的组件不需要额外的修改头文件导入的代码,编译器自动识别我们的头文件导入是否属于 Module,而判断 Header 导入是否属于 Module import 就发生在查找头文件路径中。上述代码我们会注意到针对 Framework 与常规的目录查找中,会透传一个参数 SuggestedModule。

我们进一步向下跟踪 SuggestModule 的赋值过程,在查找到头文件的磁盘路径之后,编译器会进行该文件目录或者父级目录路径作为 Key 去 UmbrellaDirs 查找该头文件的是否有对应的 Module 存在。如果能查询到则赋值 SuggestModule(ModuleMap::KnownHader(Module *,NormalHeader) )。下图为查询并赋值 SuggestModule 的流程。

相信你看到上面的源码,你又会出现新的疑惑。UmbrellaDirs 是什么?前面提到过使用开启 Module 组件的 Target 中会新增 -fmodule-map-file 的参数,编译器在解析编译参数时加载 MoudleMapFile,读取使用 Module Map Language 书写的 ModuleMap 文件,解析文件的内容。

编译器在编译工程源代码时候通过 -fmodule-map-file 参数读取我们要使用的 Module,并把 ModuleMap 文件所在的路径作为 key,我们要使用的 Module 作为 Value,赋值给 UmbrellaDirs。预处理器在解析外界引入的头文件时候,会判断头文件路径下或者头文件路径父级目录是否存在 ModuleMap 文件,如果存在则 SuggestModule 有值。头文件查找的流程至此结束。

SuggestModule 的值是编译器决定使用 Module Import 还是“文本导入” 的关键因素。预处理器处理头文件导入,会去查找头文件在磁盘上的绝对路径,如果 SuggestModule 有值,编译器会调用 ModuleLoader 加载需要的 Module,而不开启 Module 的组件头文件,编译器则会进入该文件进行新的词法分析等流程。

至此,相信读到这里大家对 ModuleMap、Umbrella 文件以及 -fmodule-map-path 有了一定的认知。而且我们也跟踪了为什么编译器可以做到不修改代码的“智能”的帮助代码在 # import 和 Module Import 之间切换。

与非 module 不同,我们来继续追踪一下 LoadModule 的后续发生了什么?ModuleLoader 进行指定的 Module 的加载,而这里的 LoadModule 正是 Module 机制的差异之处。

Module 的编译与加载是在第一次遇到 ModuleImport 类型的 importAction 时候进行缓存查找和加载,Module 的编译依赖 moduleMap 文件的存在,也是编译器编译 Module 的读取文件的入口,编译器在查找过程中命中不了缓存,则会在开启新的 compilerInstance,并具备新的预处理上下文,处理该 Module 下的头文件。产生抽象语法树然后以二进制形式持久化保存到后缀为 .pcm 的文件中(有关 pcm 文件后文有详细讲解),遇到需要 Module 导入的地方反序列化 PCM 文件中的 AST 内容,将需要的 Type 定义,声明等节点加载到当前的翻译单元中。

Module 持有对 Module 构建中每个头文件的引用,如果其中任何一个头文件发生变化,或者 Module 依赖的任何 Module 发生变化,则该 Module 会自动重新编译,该过程不需要开发人员干预。

4.3 Clang Module 复用机制

Clang Module 机制的引入,不仅仅从之前的“文本复制”到语义化模型导入的转变。它的设计理念同时也着重在复用机制,做到一次编译写入缓存 PCM 文件在此后其他的编译实体中复用缓存。关于 Module 都是编译和缓存探究的验证,我们可以在 build log 中通过 -fmodules-cache-path 来查看获取到 Module 缓存路径(eg:/Users/xxx/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/ )。当前如果你想自定义缓存路径可以通过添加 -fmodules-cache-path 指定缓存路径。

我们知道针对组件化工程,我们每个 pod 库都可能存在复杂的依赖关系,以某工程示例:

在多组件工程中,我们会发现不同的组件之间会存在相同的依赖情况。针对复杂的 Module 依赖的场景,通过 Clang源码发现,在编译 Module-lifeCirclePod(上述示例)时候,而 lifeCirclePod 依赖于 Module-UIKitPod。在编译 Module-lifeCirclePod 遇到需要 Module-UIKitPod 导入时,那么此时则会挂起该编译实体的线程,开辟新的线程进行 Module-UIKitPod 的编译。

当 Module-UIKitPod 编译完成时候才会恢复 lifeCirclePod 的任务。而开启 Module 之后每个组件都会作为一个 Module 编译并缓存,而当 MainPagePod 后续编译过程中遇到 Module-UIKitPodModule 的导入时,复用机制就可以触发。编译器可以通过读取 pcm 文件,反序列化 AST 文件直接使用。编译器不用每次重复的去解析外界头文件内容。

上述基本对 Module 的本质及其复用机制有一定的了解,是不是无脑开启 Moudle 就可以了呢?

其实不然!

我们在实践中发现(以基于 cocoapods 管理为例)在 fmodules-cache-path 的路径下存在很多份的 pcm 缓存文件,针对同一个工程就会发现存在多个下面的现象:

可以发现在工程的一次编译下,会出现多个目录出现同一个 module 的缓存情况(eg:lifeCirclePod-1EBT2E5N8K8FN.pcm)。之前讲过 Module 机制是一次编译后续复用的吗?实际情况好像与我们的理论冲突!这就要求我们去深入探究 Module 复用的机制。

追寻 Clang 的源码发现编译器进行预处理器 Preprocessor 的创建时,会根据自身工程的参数来设定 Module 缓存的路径。

我们将影响 Module 缓存的产生的 hash 目录的主要受编译参数分为下面几大类:

在实际的工程中,常常不同 pod 间的 build settting 不同,导致在编译过程中会生成不同的 hash 目录,从而缓存查找时候会出现查找不到 pcm 缓存而重复生成 Module 缓存的现象。这也解释了我们上面发现不同的缓存 hash 目录下会出现相同名字的 pcm 缓存。了解 Module 缓存的因素可以有助于在复杂的工程场景中,提高 Module 的复用率减少 Module Complier 的时间。

Tips:除了上述的缓存 hash 目录外,我们会发现在目录下存在以 ModuleName-hashxxxxxx.pcm 的命名,那么缓存文件的命名方式我们发现是 ModuleName+hash 值的方式,hash 值的生成来自 ModuleMap 文件的路径,所以保持工程路径的一致性也是 Module 复用的关键因素。

4.3 PCM

上文提到了一个很重要的文件 PCM,那么 PCM 文件作为 Module 的缓存存放,它的内容又是怎么样的呢?

提到 PCM 文件,我们第一时间很容易联想到 PCH。PCH 文件的应用大家应该都很熟悉,根据苹果在介绍 PCH 的官方文档中结构如下:

PCH 中存放着不同的模块,每个模块都包含 Clang 内部数据的序列化表示。采用 LLVM’s bitstream format 的方式存储。其中 Metadata 块主要用于验证 AST 文件的使用;SourceManager 块它是前端 SourceManager 类的序列化,它主要用来维护 SourceLocation 到源文件或者宏实例化的实际行/列的映射关系;Types: 包含 TranslationUnit 引用的所有类型的序列化数据,在 Clang 类型节点中,每个节点都有对应的类型;Declarations: 包含 TranslationUnit 引用的所有声明的序列化表示;Identifier Table: 它包含一个 hash Table,该表记录了 ASTfile 中每个标识符到标识符信息的序列化表示;Method Pool: 它与 Identifier Table 类似,也是 Hash Table,提供了 OC 中方法选择器和具体类方法和实例方方法的映射。Module 实现机制与 PCH 相同,也是序列化的 AST 文件,我们可以通过 llvm-bcanalyzer 把 pcm 文件的内容 dump 出来。

Module 的编译是在独立的线程,独立的编译实体过程,与我们输出目标文件对应的前端 action 不同,它所对应的FrontAction为GenerateModuleAction。Module 的机制思想主要是提供一种语义化的模块导入方式。所以 PCM 的缓存内容同样会经过词法,语法,语义分析的过程,PCM 文件中的 AST 模块的序列化保存是在发现在语义分析之后。

它利用了 Clang AST 基类中的 ASTConsumer 类,该类提供了若干可以 override 的方法,用来接收 AST 解析过程中的回调,当编译单元TranslationUnit的AST完整解析后,我们可以通过调用 HandleTranslationUnit 在获取到完整抽象语法树上的所有节点。PCM 文件的写入由 ASTWriter 类提供 API,这些具体的流程我们可以在 ASTWriter 类中具体跟踪。在该过程中主要分为 ControlBlock 信息的写入,该步骤包含 Metadata, InputFiles,Header search path 等信息的记录。这些 PCM 的具体内容 dump 出来如下图:

其中 Types,Declarations 等信息的写入流程发生在 ASTBlock 阶段。由于在处理处理 ModuleMap 文件的编译流程中会对 umbrella.h 中所暴露的头文件进行预处理,词法,语法,语义分析等流程。我们在使用 WriteAST 写入时,会将当前编译实体的 Sema 类(该类是 build AST 和语义分析的实现类)传递过来。Sema 持有当前的 ASTContext,ASTContext 则可以用于访问当前抽象语法树上的所有 Nodes(例如 types,decls)等信息。

如果所示:ASTWriter 将已经解析无误的 Module 信息,包括 AST 等内容写入 Module 的缓存文件 PCM 中。

我们在源码跟踪过程中可以发现会将AST节点信息等写入PCM中的ASTBlock中,我们可以通过打印获取到节点的类型和节点的名称:

通过上面源码等流程相信你掌握了以下:

同城编译时间数据分析

鉴于在58同城工程上实施的编译数据时间的加长的背景,我们在深入探究 Module 构建,复用等机制后,我们针对整个编译流程做了详细的编译阶段的插桩。

5.1 分析工具

Clang 9.0 合并了一个非常有用的功能 -ftime-trace,该功能允许以友好的格式生成时间跟踪分析数据,clang中预先插入了一些点标记,如每个文件的编译时间ExecuteCompiler、前端编译时间Frontend、module加载时间Module Load、后端处理时间Backend等。接下来通过-ftime-trace查看各编译阶段的打点时间。操作比较简单,只需要在Other C Flags中添加-ftime-trace即可。

编译完成后clang会在编译目录下,为每个源文件自动生成一个json文件,文件名和源码文件相同。

每个json文件中大概会有ExecuteCompiler、Frontend、Source、Module Load、Backend等打点数据,也有Total ExecuteCompiler、Total Frontend、Total Source、Total Module Load、Total Backend这样的数据,后者是前者的一个汇总,这是clang自带的,也可以在clang中去扩展。通过chrome://tracing/可以很方便查看单个json文件的耗时分布,如下。

-ftime-trace设置后主要时间段说明:

这些时间段都是Clang中已有的打点,从前面的chrome://tracing/图也能看出来是有一些包含关系的,如:

  1. ExecuteCompiler 包含Frontend和Backend;

  2. Frontend包含Source;

  3. Source中包含Module Load(前提是如当前.m中import了A/XX.h,而A没有module化,但XX.h中import了B/YY.h,B是Module化的,如果A是module化的,Module Load不包含在Source中);

  4. Module Load包含Module Compile。

5.2 时间段分析

先选取单个文件进行分析,将其拖到chrome://tracing/中,可看到如下数据。

从图上可看出,Total Frontend占总编译时间在都在70%以上,module编译中Total Frontend时间比非module明显要长,而Total Source占Total Frontend时间的70%左右,而Total Module Load是Total Source中最耗时的操作。结果中Total Module Load阶段,module明显是要比非module耗时更长。

上面是从单个文件进行分析,并不能代表整体项目的编译情况,因此,我们做了一个自动化工具,将所有.json文件中的对应时间进行统计汇总,得出整体各个时间段的汇总数据,如下。说明一下,我们统计的Total ExecuteCompiler指每个文件的编译时间总和,相当于在单核下编译时间,而前面显示的实际整体的编译时间少很多,是因为我们实际是在多核下编译。

从整体分析图上可看出,Total Frontend时间均占总编译时间Total ExecuteCompiler的80%以上,而Total Frontend中时间Total Source的总时间占80%以上,而在Total Source中Total Module Load时间占70%左右。总时间Total ExecuteCompiler和前端Total Frontend依然是module下更长,而在Total Frontend中Total Module Load的时长在module下明显比非module下长很多,跟上面单文件分析的结论基本一致。这里需要注意的是,Total ExecuteCompiler时间比前面统计的总时间长很多,是因为项目是在多核下编译,而Total ExecuteCompiler统计的是所有文件编译时间总和,而前面统计的时间是多文件并行编译下的时间,其它各段时间同理。

在Total Module Load中会执行Module的编译,但从上图我们可以看到其实Total Module Compile时间很短,都不超过50S,因此还需要进一步分析Total Module Load的耗时操作。为此我们根据clang中的处理流程,在clang中Module Load处理代码中扩展两个打点:

并在头文件查找扩展打点:

将Clang源码修改后编译生成自定义的Clang,替换XCode中的Clang分别在module和非module下再次进行编译,得出如下数据:

从图中可以看出,Module Load阶段中Module ReadAST时间占比近70%,此次编译module比非module下时间长约3%,而Module ReadAST段module比非module下时间长约2%,整个Module Load阶段module下比非module下长约4%。

因此,我们可以得出,相比非module,module化编译更为耗时,而主要耗时在验证Module缓存并反序列化操作。那么问题来了,有什么办法可以在module开启的情况下进行编译时间优化呢?

编译时间的优化

从上面的数据分析我们知道,如果底层组件进行 Module 化,并且上层组件通过module方式进行引用的话,会更耗时。但是为了支持 Swift/OC 混编,如 Swift 调用 OC,需要对组件进行 Module 化。因此,我们需要在 Module 化的基础上优化编译时间,如果上层组件不通过 Module 方式调用其它 Module 化的组件,而采用非 Module 化方式进行引用,理论上是能避免上述module化操作的耗时。

6.1 优化方案

为了进一步优化混编下的编译时间,我们参考苹果 WWDC 2023 的 header search path 中 headermap 查找方案,主要思路是通过 hmap 的方式来替换header search path 下的文件搜索,来减少编译耗时,为描述方便,我们称为hmap方案,目前业内美团对 hmap 有应用,并且有 50% 的优化效果。58同城也对 headermap 方案进行了研究并进行了落地,理想的实现方案就是做一个 cocoapods 插件,在插件中做了以下几件事:

  1. HooksManager注册cocoapods的post_install钩子;

  2. 通过header_mappings_by_file_accessor遍历所有头文件和header_dir,由header_dir/header.h和header.h为key,以头文件搜索路径为value,组装成一个Hash<key,value>,生成所有组件pod头文件的json文件,再通过hmap工具将json文件转成hmap文件。

  3. 再修改各pod中.xcconfig文件的HEADER_SEARCH_PATHS值,仅指向生成的hmap文件,删除原来添加的搜索目录;

  4. 修改各pod的USE_HEADERMAP值,关闭对默认的hmap文件的访问。

58对应的插件名为cocoapods-wbhmap,插件完成后,在Podfile中通过plugin 'cocoapods-wbhmap'接入。

6.2 优化数据

以下是58同城分别在非 Module、Module 化和优化后的 hmap 三种场景下编译时间数据,这里的 hmap 是在各组件 Module 化的基础上使用的。

首先说明一下,这里的整体编译时间数据上跟前面不一致,是因为重新编译了,每次编译时间略有不同,但不影响我们分析。从整体时间来看 Module 下的编译时间比非 Module 下略长,而 hmap 比非 Module 下优化了 32% 左右,比 Module 下优化了 33% 左右,可以看出 hmap 的优化效果是很显著的。

接下来分析一下编译各阶段的时间,是不跟我们预想的一致,我们预想的是 Total Lookup HeaderFile 和 hmap 在 Module Load 阶段加载的 Module基本是系统库,应当时间上差不多,而由于hmap节省了在众多目录下文件搜索的时间,应当在Total Lookup HeaderFile有较大差别。

从分段数据来看,三种编译方式的 Total ExecuteCompiler 跟上述整体时间比例接近,但是 Total Lookup HeaderFile 时间都较小,自然没多大差别,而 Total Module Load 差别较大,非 Module 和 Module 下比 hmap 大 61% 左右,跟我们预想的不一致。观察数据可以看到,Module Load 中大部分时间是在 Module ReadAST 阶段,因而我们继续研究 Module ReadAST 中的处理操作。

6.3 hmap 优化了什么?

针对 ReadAST 阶段再次细分打点计时,发现在 ReadAST 阶段去读取缓存时候,会对缓存 PCM 文件的 ControlBlock 块信息进行解析,该内容包含了当前 Module 缓存引用外界其他 ASTFile 的记录。而加载外界 ASTFile 的 PCM 缓存时候,会针对该 ModuleName 进行验证确保我们不会加载一个 non-Module 的 ASTFile 作为一个 Module。它通过查询是否存在 ModuleMap 文件来描述 Module 对应当前要查询的 ModuleName。

我们将重点聚焦在这个阶段,因为我们 hmap 方案最直接的优化之处在减少了 Header Search Path 的参数路径,将预处理期间的头文件查找转换为 key-value 查找,从而减少了在 Header Search Path 众多 pod 的目录中(如private、public)的搜索时间,源码中 SearchDirs 即为这些目录,Header Search Path 中目录越多,SearchDirs 中元素更多,要遍历的目录就更多,无用的搜索时间就越长,通过单个文件进行调试发现这里消耗的时间约有 70%,而系统库的查找在这里耗时较长,因为按照编译器搜索的顺序,系统库目录的是排在 Header Search Path 后的,经过一顿徒劳的搜索之后才到系统库目录搜索,效率较低。

我们猜想前面非 Module 和 hmap 在 Module Load 时间差较大的原因应当就在此,因此在 ReadAST 阶段的 HeaderSearch::lookupModule 方法内打个点 Lookup Module,即 Module ReadAST 包含 Lookup Module,重新编译进行数据统计如下:

这里只统计非 Module 和 hmap,整体编译时间如下:

从数据可以看出,再次编译 hmap 下的编译时间比非 Module 方式同样是优化了 35% 左右。再看分段数据,如下:

从占比分析,非 Module 方式下 Total Lookup Module 时间占 Total Module ReadAST 时间的 77%,并占 Total Module Load 时间的 72%,而在 hmap 方式中,Total Lookup Module 时间占 Total Module ReadAST 时间的 35%,并占 Total Module Load 时间的 27%,远小于非 Module 方式下的占比。

从数值分析,非 Module 方式下 Total Lookup Module 时间为 1422 秒,而 hmap 方式下时间仅为 182 秒,相差 7 倍多。

上面数据也进一步验证了我们对于 hmap 编译时间优化原因的猜想。到这里我们就从数据和原理上对 hmap 方案的编译优化做了一个完整的分析。

总结

由于 Swift/OC 混编项目的需要,58同城对组件进行了 Module 化,并且尝试让所有组件通过 Module 方式进行头文件引用。但我们发现编译时间却比非 Module 情况下更长,这也与苹果官方在 WWDC2023 中的 Module 性能分析结果不符。

然后在寻求编译时间的优化方案时,发现在 WWDC2023 中有提到 hmap 机制,并借鉴业内的一些宝贵经验,采用了 hmap 方案对编译时间进行优化。Module 方案虽无法降低编译耗时,但对比之前混编的桥接方式,可增强项目向 Swift 迁移过程中混编组件的可维护性。通过 hmap 方案对编译时间进行优化,同城最终编译时间比 Module 化之前优化了约 35%,对于其它 App 的 Module 化也是有较好的借鉴意义。

作者简介

参考文献

句子成分分析器(ACL)

ACL 2023 Long Papers

基于自关注编码器的成分句法分析

Constituency Parsing with a Self-Attentive Encoder

伯克利大学

University of California,Berkeley

本文是伯克利大学发表于ACL的论文,概述了Nikita Kitaev和Dan Klein的工作。全文主要贡献点在于使用自关注架构代替长短时记忆网络编码器,可以改善目前为止最好的成分句法分析器。作者的解析器在Penn Treebank上训练后获得了目前为止最好的效果,其中不使用外部数据时获得93.55的F1,使用其他预先训练的单词表示时获得95.13的F1。作者的解析器也优于SPMRL数据集中8种先前公布的最佳精度数据。

引言

RNNs在很大程度上取代了固定窗口大小的前馈网络,部分原因是它们捕获全局上下文的能力。然而,RNNs并不是唯一能够概括全局上下文的体系结构。文章中介绍了一种解析器,它结合使用这种自关注架构构建的编码器和自定义用于解析的解码器。如下图所示。

模型

树形记分和图表译码器

语法解析器对每个树分发了一个实值分数s(T)

其中s(I,j,l) 是一个关于成分的实值记分。

它位于句子中篱笆位置i和j之间,并具有标签l。为了处理一元链,标签集包括训练集中的每个一元链的折叠条目。该模型通过二值化和引入虚拟标签来处理n元树在二值化过程中创建的节点,具有以下性质:与虚拟标签相关联的分数总是零,确保对于n元树的所有可能的二进制化继续保持。

上下文感知词表示

编码器模型被分割为两个部分:基于文字的部分分配了上下文感知向量yt给每一个句子中的位置t。以及组合向量yt以产生跨度分数s(i;j;l)的图表部分。

编码器将一个字序列作为输入嵌入[w1; w2; :::; wT],第一个和第一个最后嵌入是特殊的开始和停止令牌。所有单词嵌入都是与模型的其他部分共同学习的。向量[z1; z2; : : : ; zT ]被转换成一堆8个相同的层,每层由两个堆叠的子层组成:多头关注机制和位置前馈子层。给定输入x的每个子层的输出是LayerNorm(x+SubLayer(x)),即,每个子层之后是残余连接和层规范化步骤。最后,所有子层输出,包括最终输出yt,大小都是dmodel。

8层中的每一层中的第一个子层是多头自我关注机制,这是信息可以在句子中的位置之间传播的唯一手段。自我关注机制的输入是一个T x dmodel矩阵X,其中每个行向量xt代表句子中的单词t。如下图中的每一个都有自己的可训练参数:这允许单词从句子中每个聚焦子层的最多8个远程位置收集信息。

位置智能前馈子层

由于在整个模型中使用了残余连接,输入和输出尺寸是相同的,但作者可以通过调整应用非线性的中间向量的大小来改变参数的数量。

来自前一节中描述的基于字的编码器部分的输出yt被组合以形成跨度分数s(i;j;),如下所示

其中LayerNorm代表层规范化,relu代表整流线性单位非线性。

合句子中相关位置的摘要向量。一个单词右侧的跨度端点可能需要与右端点不同信息的左端点。

内容与位置关注

在整个编码器中,信息传递的主要机制是自我关注,其中单词可以使用内容特征和位置信息相互联系。内容和位置信息交织在整个网络中。

将内容和位置信息混合在一个向量中,可能会导致一种关注力凌驾于另一种关注力之上,并损害网络在这两种关注力之间找到最佳平衡的能力。为此,作者提出了一个分解的模型版本,它显式地分离内容和位置信息。

如下图所示,作者的模型中的前馈子层同样被分成两个独立的部分,分别处理位置和内容信息。

或者,因子分解可以看作在整个模型中对参数矩阵实施块稀疏约束。

作者保持与之前相同的向量大小,这意味着因子分解严格地减少可训练参数的数量。为了简单起见,作者将每个向量分成包含位置和内容信息的相等的一半,将模型参数的数量大致减少一半。该分解方案能够达到开发集F1下的93.15分,比未分解模型提高了近0.5分。

这些结果表明,分解不同类型的信息会导致更好的解析器,但原则上存在一个混淆:也许通过使所有矩阵块稀疏,作者偶然发现了更好的超参数配置。例如,这些增益可能仅由于可训练参数的数量不同而导致。

模型分析

为了检查在作者的架构中基于内容和基于位置的关注的相对利用,作者在测试时通过选择性地将内容或位置组件对任何关注机制的贡献归零来干扰经过训练的模型。这可以在不同的层上独立完成。作者的模型学习使用两种关注类型的组合,其中基于位置的关注是最重要的。作者还看到,基于内容的关注在网络中较晚的层更有用,这与作者模型的初始层的行为类似于扩展的卷积网络,而上层在两种关注类型之间具有更大的平衡相一致。

作者还可以通过将窗口应用到注意机制来检查作者的模型对长距离上下文信息的使用。如下表所示,严格开窗的结果很差:即使是大小为40的窗口,与原始模型相比,也会导致解析精度的损失。

下表中还示出了在测试时间内轻松开窗的结果。

接下来,作者检查解析器对远程依赖项的使用是否对通过重新训练作者的模型以适应窗口化而实现任务至关重要。为了评估全局计算的作用,作者同时考虑严格窗口和松弛窗口。原则上,作者可以在训练时用全局计算的显式规定来代替松弛的窗口,但是为了分析的目的,作者选择最小化与原始架构的偏差。

下表所示的结果证明,对于使用作者的模型实现最大的解析精度,远程依赖仍然是必不可少的。严格和松弛窗口之间的旁侧比较表明,使用松弛方案中始终可用的指定位置来汇集全局信息的能力一致地转化为精度增益,但不足以补偿小窗口大小。这表明,原则上不仅必须能够得到来自远程令牌的信息信号,而且还有助于在不存在中间瓶颈的情况下直接访问该信息。

词汇模型

为了在不引入任何对外部系统的依赖的情况下恢复性能,作者探索将词汇特征直接结合到作者的模型中。本节中描述的不同方法的结果如下表所示。

考虑到自关注编码器在句子级上的有效性,将其看作子词结构同样具有美学吸引力。然而,它在经验上要慢得多,没有比字符级LSTM更好的并行化(因为单词往往很短),并且初始结果不如LSTM。一种解释是,在词汇模型中,人们只希望计算每个单词的单个向量,而自我关注的体系结构更适合于在序列中的多个位置生成上下文感知摘要。

作者的方法能够捕获子词信息和上下文线索:嵌入是由网络产生的,该网络以字符作为输入,然后使用LSTM捕获上下文信息,当为句子中的每个词生成向量表示时。

这些预先训练的单词表示是1024维的,而到目前为止作者所有的因式分解模型都有512维的内容表示;作者发现解决这种不匹配的最有效的方法是使用学习到的权重m将ELMo向量投影到所需的维度阿特里克斯随着语境化词语表达的增加,作者假设不再需要完整的8层自我关注。这在实践中证明是正确的:作者用四层编码器获得了95.21F1的最佳开发集结果。

实验分析

下表总结了前面部分中给出的解析器变体的开发集得分。性能最好的解析器在ELMo单词表示上使用了因子化的自关注编码器。

作者在测试集上评估模型的结果如下表所示。

作者通过九种语言上进行训练来测试作者的模型在语言之间通用的能力。除了学习率,作者在SPMRL任务中对一些较小的数据集进行了调整。结果如下表所示。

在9种语言的8种语言中,只在单系统条件下进行评估,作者的测试集结果超过了作者已知的任何系统中以前发布的最好的分数。

结论

在本文中,作者证明了编码器的选择对解析器的性能有很大的影响。特别地,作者使用一种基于因数自关注的新型编码器来演示解析结果。作者看到的益处不仅来自于合并更多的信息(例如子词特征或外部训练的词表示),还来自于结构化体系结构以将各种不同的信息彼此分离。作者的研究结果提示,进一步研究不同的编码方法可以导致对解析和其他自然语言处理任务的进一步改进。

句子成分分析器(PyTorch自然语言处理系列)

来源 | Natural Language Processing with PyTorch

作者 | Rao,McMahan

译者 | Liangchu

校对 | gongyouliu

编辑 | auroral-L

全文共4243字,预计阅读时间35分钟。

第二章 快速回顾传统 NLP 应用

1. 语料库,标记和类型

2. 一元组,二元组,三元组,... ,N元组

3. 词形和词干

4. 分类句子和文档

5. 分类单词:词性标注

6. 分类短语:分块和命名实体识别

7. 句子结构

8. 单词意义和情感

9. 总结

参考资料

自然语言处理(NLP)和计算语言学(computational linguistics,CL)是人类语言计算研究的两个领域。NLP 旨在研究解决涉及语言的实际问题的方法,如信息提取(information extraction)、自动语音识别(automatic speech recognition)、机器翻译(machine translation)、情感分析(sentiment analysis)、问答(question answering)和总结(summarization)。另一方面,CL 使用计算方法来理解人类语言特性。那么我们何以理解语言?我们何以产生语言?我们何以学习语言?不同语言之间又有什么关联呢?

在文献中,我们经常看到从 CL 到 NLP领域中研究方法和研究人员的交叉,反之亦然。来自CL关于语言的教训和经验可以用于作为NLP的先验内容,而来自NLP的统计和机器学习方法可以用来回答 CL 想要回答的问题。实际上,这些问题中的一部分已经扩展成它们自己的学科,如音位学(phonology)、形态学(morphology)、句法学(syntax)、语义学(semantics)和语用学(pragmatics)。

在本书中,我们只关注 NLP,但我们也经常会根据需要从 CL 中借鉴思想。在将自己完全投身于 NLP 的神经网络方法之前,还有必要回顾一下一些传统的 NLP 概念和方法,这就是本章的学习目标。

如果你有 NLP 相关的的背景知识则可以跳过这一章,但你也可以再看一遍回忆一下,也便于为未来建立一个共享的词汇表。

1. 语料库,标记和类型

所有的 NLP 方法,无论是经典的还是现代的,都以文本数据集开始,也称为语料库(corpora)。语料库通常包括原始文本(ASCII 或 UTF-8 格式)和与文本相关的所有元数据。原始文本是一个字符(字节)序列,但大多数情况下将字符分组成称为标记(token,也可以理解为词元 )的连续单元是有用的。在英语中,标记token对应于由空格字符或标点分隔的单词和数字序列。

元数据可以是任何与文本相关联的辅助信息,例如标识符,标签和时间戳。在机器学习术语中,文本及其元数据称为实例(instance)或数据点(data point)。下图(2-1)的语料库是一组实例,也称为一个数据集(dataset)。鉴于本书重点关注机器学习,我们可以自由地将语料库和数据集这两个术语交替使用。

将文本分解为标记token的过程称为分词(tokenization)。例如,世界语句子:Maria frapis la verda sor?istino有六个token。分词可能比简单地基于非字母数字字符拆分文本更加复杂,如下图(2-2)所示。对于像土耳其语这样的粘合语言来说,仅仅分隔空格和标点符号可能是不够的,因此可能需要更专业的技术。你将在第四章和第六章中看到,通过将文本表示为字节流,我们能够在某些神经网络模型中完全规避分词问题,这对于粘合语言来说是很重要的。

最后,考虑一下下面这条推文:

将推文进行分词的过程涉及到保存话题标签和@handle,以及将表情符号(如:-))和URL按单元分隔的问题。#MakeAMovieCold标签应该是 1 个还是 4 个token呢?虽然大多数论文并不太关注这一问题,但是实际上,有关分词的决策可能十分随意,然而这些决策在实践中对于准确性的影响要比公认的大得多。分词通常被认为是预处理的一项繁琐工作,大多数开源 NLP 包为分词提供了合理支持。下例(2-1)展示了来自 NLTK 和 SpaCy 的示例,它们都是用于文本处理的常用包。

示例 2-1:文本分词

Input[0]:import spacynlp = spacy.load('en')text = "Mary, don't slap the green witch"print([str(token) for token in nlp(text.lower())])Output[0]:['mary', ',', 'do', "n't", 'slap', 'the', 'green', 'witch', '.']Input[1]:from nltk.tokenize import TweetTokenizertweet=u"Snow White and the Seven Degrees #MakeAMovieCold@midnight:-)"tokenizer = TweetTokenizer()print(tokenizer.tokenize(tweet.lower()))Output[1]:['snow', 'white', 'and', 'the', 'seven', 'degrees', '#makeamoviecold', '@midnight', ':-)']

类型(type)是语料库中唯一的标记。语料库中所有类型的集合就是它的词汇表(vocabulary)或词典(lexicon)。词可以区分为内容词(content words)和停用词(stopwords)。像冠词和介词这样的限定词主要是为了语法正确性,就像承载着内容词的填充物一样。


特征工程

这种理解语言的语言学并将其应用于解决NLP问题的过程称为特征工程(feature engineering)。为了模型在不同语言之间的方便和可移植性,我们将其保持在最低限度。但是在构建和部署真实的生产系统时,特性工程是必不可少的,尽管最近也有说法与此相悖。一般来说,要了解特征工程,可以阅读 Zheng和Casari(2023)的书。


2. 一元组,二元组,三元组,...,N 元组

N 元组(N-grams)是文本中出现的固定长度(n)的连续token序列。二元组(bigram)有两个token,一元组(unigram)只有一个token。如下例(2-2)所示,从文本生成 N 元组非常容易,SpaCy 和 NLTK 等包提供了方便的方法。

示例 2-2:生成 N 元组

Input[0]:def n_grams(text, n): ''' takes tokens or text, returns a list of n grams ''' return [text[i:i+n] for i in range(len(text)-n+1)]cleaned = ['mary', ',', "n't", 'slap', green', 'witch', '.']print(n_grams(cleaned, 3)) Output[0]:[['mary', ',', "n't"], [',', "n't", 'slap'], ["n't", 'slap', 'green'], ['slap', 'green', 'witch'], ['green', 'witch', '.']]

对于子词(subword)信息本身携带有用信息的某些情况,可能需要生成字符 N 元组。例如,methanol中的后缀-ol表示它是一种醇,如果任务涉及对有机化合物名称的分类,那么你能知道 N 元组捕获的子词(subword)信息是很有用的。在这种情况下,你可以重用代码,将每个字符 N 元组视为token。

3. 词形和词干

词形(lemma)是单词的词根形式。考虑动词fly。它可以引申为许多不同单词——flow、flew、flies、flown、flowing等等——而fly是所有这些看似不同单词的词形。有时,为了保持向量表示的低维度,可以将token缩减为到它们的词形。这种简化称为词形还原(lemmatization),你可以在下例(2-3)中看到它的作用:

示例 2-3:词形还原

Input[0]import spacynlp = spacy.load('en')doc = nlp(u"he was running late")for token in doc: print('{} --> {}'.format(token, token.lemma_))Output[0]he --> hewas --> berunning --> runlate --> late

例如,SpaCy 使用一个预定义的字典 WordNet 来提取词形,但是词形还原可以构建为一个需要理解语言形态学的机器学习问题。

词干化(Stemming)是最常见的词形还原,它涉及使用人为制定的规则来去掉单词的结尾,从而将之简化为一种叫做词干的常见形式。通常在开源包中所实现的比较流行的词干分析器是 Porter 词干提取器和 Snowball 词干提取器。至于调用 SpaCy/NLTK API进行词干提取,就留给你自己去寻找和发现了。

4. 分类句子和文档

对文档进行归类或分类可能是 NLP 最早的应用之一。我们在第一章中描述的TF表示和TF-IDF表示对于对较长的文本块(如文档或句子)进行分类是非常有用的。主题标签的分配、评论情感的预测、垃圾邮件的过滤、语言识别和邮件分类等问题都可以被定义为受监督的文档分类问题。(对于半监督版本,其中只使用了一个小的标签数据集,非常有用,但不在本书涉及范围之内。)

5. 分类单词:词性标注

我们可以将标签的概念从文档扩展到单个单词或token。分类单词的一个常见示例是词性标注(categorizing words is part-of-speech,POS tagging),如下例(2-4)所示:

示例 2-4:词性标注

Input[0]import spacynlp = spacy.load('en')doc = nlp(u"Mary slapped the green witch.")for token in doc: print('{} - {}'.format(token, token.pos_))Output[0]Mary - PROPNslapped - VERBthe - DETgreen - ADJwitch - NOUN. - PUNCT

6. 分类短语:分块和命名实体识别

我们通常需要标记文本的范围,也即一个连续的多标记边界,例如句子Mary slapped the green witch.,我们可能需要识别其中的名词短语(noun phrase,NP)和动词短语(verb phrase,VP),如下所示:

[NP Mary] [VP slapped] [the green witch].

该操作称为分块(chunking)或浅层句法分析(shallow parsing)。浅层句法分析旨在推导出由名词、动词、形容词等语法原子组成的高阶单位。如果没有用于训练浅层句法分析模型的数据,可以在词性标注上编写正则表达式来近似浅层句法分析。幸运的是,对于英语等最广泛使用的语言来说,这样的数据和预训练模型是存在的。下例(2-5) 给出了一个使用 SpaCy 的浅层句法分析示例:

示例 2-5:NP分块

Input[0]: import spacy nlp = spacy.load('en') doc = nlp(u"Mary slapped the green witch.") for chunk in doc.noun_chunks: print '{} - {}'.format(chunk, chunk.label_) Output[0]: Mary - NP the green witch - NP

另一种有用的范围类型是命名实体(named entity)。命名实体是一个包括真实世界概念的字符串,如人员、位置、组织、药品名称等等。下面是例子:

7. 句子结构

浅层句法分析的作用是识别短语单元,而识别它们之间关系的任务称为解析(parsing)。你可能记得在上英语初级课程时有见过如下图(2-3)所示的图表来表示句子:

解析树(parse tree)表示句子中不同的语法单元在层次上是如何相关联的。上图(2-3)中的解析树显示了所谓的成分分析(constituent parse)。另一种可能更有用的用于显示关系的方法是使用依存句法分析(dependency parsing),如下图(2-4)所示:

要了解更多关于传统句子解析的信息,请参阅本章末尾的“参考资料”部分。

8. 单词意义和情感

单词是有意义的,而且通常不止一个意义。一个词的不同含义称为它的意义(senses)。WordNet 是一个长久运行的词汇资源项目,它来自普林斯顿大学,旨在对所有(绝大部分)英文单词的含义以及其他词汇关系进行分类。例如,考虑像plane这样的单词。下图(2-5)展示了plane一词的不同用法:

即使是在有现代流行方法的情况下,为像 WordNet 这样的项目付出数十年的努力也是值得的。本书后面的章节给出了在神经网络和深度学习方法的背景下使用现有语言资源的例子。

词的意义也可以从上下文中归纳总结出来——从文本中自动发现词义实际上是半监督学习在NLP中的第一个应用,尽管我们在本书中并不介绍相关内容,但我们还是鼓励你阅读 Jurasky and Martin(2023),第十七章,以及Manning and Schutze(1999),第七章。

9. 总结

在这一章中,我们回顾了 NLP 中一些基本的术语和思想,这些对于学习后续章节很有用。本章只涉及了有关传统 NLP 的部分内容,并忽略了传统NLP一些重要方面,因为我们想将本书的大部分笔墨用于 NLP 的深度学习。然而你得知道,其实存在大量不使用神经网络的 NLP 研究工作,并且仍然具有很大影响力(即,广泛用于构建生产系统)。在许多情况下,基于神经网络的方法应当被看作是传统方法的补充而非替代。有经验的实践者经常结合使用这两个方法的优点来构建最先进的系统。为了深入了解NLP传统方法,我们推荐以下参考资料。

?参考资料

1.Manning, Christopher D., and Hinrich Schütze. (1999). Foundations of Statistical Natural Language Processing. MIT press.

2.Bird, Steven, Ewan Klein, and Edward Loper. (2023). Natural Language Processing with Python: Analyzing Text with the Natural Language Toolkit. O'Reilly.

3.Smith, Noah A. (2023). Linguistic Structure prediction. Morgan and Clypool.

4.Jurafsky, Dan, and James H. Martin. (2023). Speech and Language Processing. Vol. 3. London: Pearson.

5.Russell, Stuart J., and Peter Norvig. (2023). Artificial Intelligence: A Modern Approach. Pearson.

6.Zheng, Alice, and Casari, Amanda. (2023). Feature Engineering for Machine Learning: Principles and Techniques for Data Scientists. O'Reilly.

儿女双全的赞美句子(2023年)

《青玉案·元夕》:众里寻他千百度,蓦然回首,那人却在灯火阑珊处。——辛弃疾

古有门当户对、三书六礼、明媒正娶,而21世纪的今天,门第甚至年龄不再是婚姻的绊脚石。

“忘年恋”、“跨国恋”层出不穷,仿佛只要彼此之间存于爱情的幻想,便能喜结良缘,白首相庄。

可究竟这样的婚姻能否走过“七年之痒”?

著名画家杨彦先生与他的非洲妻子爱达,两人相差将近三轮,又有着语言不通的障碍,可在世人眼里,他们就是恩爱夫妻的楷模。

但就是这样的“恩爱两不疑”,却使得杨彦在婚后第七年,选择遁入空门,而他的妻子也带着孩子逐渐淡出了大众的视线……

一眼万年的“相遇”

杨彦出生于1958年的青海西宁,天赋异禀的他,年仅6岁便能在舅舅马福伟的启蒙下临摹《芥子园画谱》。

家人深知他才华横溢,在杨彦20岁时,送至华拓门下。到了29岁那年,他又转至北戴河拜师于李可染。

十年间,他的画作接连出现在大众视野,《黄山图》、《海底系列》等作品如雨后春笋般涌现。

受南北方不同人文地理的影响,杨彦形成了自己独特的写意风格。

他不断地游历大江南北,以大自然的风光无限激发自己创作的灵感。

他对大自然那种与生俱来的热爱与关怀,也正是他艺术命根繁茂的重要原因之一。

“日出东方之野”、“才人出江山,泼墨生万象”等赞美之词皆是当时美术评论家对于杨彦画作的充分肯定,他的创作是大写意精神的弘扬,更是中国书画界的一支标杆。

而就是这样一位被誉为“当代张大千”的艺术家,在“四十一枝花”的年纪,却还未曾成家娶妻、儿女双全。

在世人眼中,艺术家本身便是浪漫的代名词,可在杨彦自己眼中,命定之人却还未曾出现。

他宁可孤身一人,也不愿听从家人的安排,他一边专心于自己的画作,一边等待着爱神丘比特的降临。

1998年这天,杨彦受邀到好友张二苗家中做客。

他这位挚友,不仅是一位同他一般的画家,更是一位收藏家,家中名师画作、奇珍异石、精美雕塑不胜其数。

而就在这里,杨彦遇到了他这一生无法忘怀的容颜。

那是一尊黝黑发亮的非洲少女雕塑,此少女有着非洲人特有的厚嘴唇,眼藏秋波、满含笑意。

这醉人的异域风情瞬间打动了杨彦,他甚至生出了想要娶这雕塑少女般的可人儿为妻的想法。

杨彦向好友询问,希望找到雕塑最初的源头,却没有得到想要的答案。一不做二不休,他便出国开始了漫漫的“寻妻”之路。

寻觅十余载,幸得一归人

杨彦流连了许多个国家,美国、意大利、非洲……,只要有黑人姑娘驻足的地方,就有他的影子。

可兜兜转转十余载,杨彦却还是没有遇到那个已经在他内心深印的“姑娘”。

可纵使这“寻妻”之路艰辛,杨彦还是不愿放弃心中的执念。

他一边以画作寄于国外风情,一边苦苦寻找着梦中佳人。

这时国内的一位友人听说了他的经历,托人将当时世界小姐非洲赛区的总冠军玛丽亚介绍给了杨彦。

玛丽亚

杨彦初见玛丽亚,她的年轻貌美皆符合他心中理想型的模样,当即把自己的想法告知了玛丽亚。

但玛丽亚只是倾慕中国文化与杨彦的画作,想要和他学习一二,并无他念,为了避开杨彦的热情攻势,玛丽亚甚至叫他“爸爸”。

杨彦见求爱不成,但因玛丽亚唤他一声“爸爸”。

他便转念一想,既然如此,何不通过玛丽亚为他介绍一个年轻貌美的非洲少女呢?

玛丽亚虽无心接受他的爱意,但乐于助他找到真正的爱情。

她告诉杨彦,她的家乡塞拉利昂是一个盛产美女的宝地,他不妨可以去那里试试看。

2023年,53岁的杨彦出发前往塞拉利昂,途中遇到了一个叫哈姆萨的本地人。

哈姆萨询问杨彦此行的目的,他轻快地回答道:“找老婆!”,哈姆萨热心,便自告奋勇当了他的翻译。

到达了塞拉利昂之后,杨彦发现这是一个拥有热带气候和美丽海滩的西非国家,人们黝黑的皮肤在太阳下闪着金光,冥冥中他感觉到他的爱人就在不远处呼唤着他。

果不其然,杨彦一眼便看到了那个他中意之人,那个少女站在人群中,诱人的厚嘴唇、眼含秋波、满脸笑意,这不正是他苦苦寻找的梦中情人!

杨彦激动得不知如何是好,他赶忙唤哈姆萨与这位少女沟通,哈姆萨将杨彦的爱意告诉了这位少女。

谁知少女一口便答应下来,她愿意和杨彦走,并告诉他,她的名字叫爱达。

原来爱达也早就注意到这个中国人,他的肤色、独特的气质都深深吸引着她,他是那么的神秘又熟悉,在一瞬间捕获了她的芳心。

两个人通过哈姆萨翻译基本了解了对方的情况后,他们做出了一个大胆的决定,那就是第二天便在当地举办婚礼。

可当时的爱达只有21岁,她还只是塞拉利昂大学校园里的一名学生,怎能抛下这里的学业还有亲友远嫁中国呢?

在做了一番思想斗争后,她决定带着杨彦返回家中,以征得父母的同意。

没想到,当两人回到爱达家中,父母十分热情地接待这位远道而来的客人。

虽然听到他们想要“闪婚”的消息,难掩内心的不舍,但还是给了两位新人最美好的祝福。

结发为夫妻,恩爱两不疑

就这样,两个肤色不同,有着语言差异,年龄还有30岁之差的人,在塞拉利昂的土地上举行了一个简单的结婚仪式。

之后,杨彦为爱达办理了护照一起回到了国内。

然而人们并不看好这对老夫少妻的异国版“艳遇”,更有甚者猜测这只是一场蓄谋已久的炒作,可杨彦和他的妻子爱达似乎并不畏惧众人的闲言碎语。

2023年10月,北京的枫叶开始片片飘落,而温水康熙行宫里奏乐笙歌,迎来了一对不平凡的夫妻。

爱达身着中国传统婚服,黝黑肤色与凤冠霞帔相衬竟别有一番风味,她坐于花轿之中,蒙着大红盖头,对面是等待她的夫君。

而杨彦为博妻子一笑,不惜花费重金包下专机自塞拉利昂运来大量的非洲食物与生活用品,用来招待妻子的娘家人与朋友。

这场婚礼,万众瞩目,并有不少知名人士共同参加,礼堂内缓缓升起两国国旗,象征着中非两国的友好,更象征着杨彦与妻子爱达的永恒爱情。

但争议之声却未曾消除半分,甚至愈演愈烈。锋芒从两人的相爱奇遇,转向了杨彦“土豪”式的“哗众取宠”。

有人认为这不过是一个画家自保名声的手段,也有人支持这一对“爷孙恋”,力宣自由婚姻。

但不论舆论更倾向于哪边,杨彦夫妇都当做未曾听过,仍旧恩爱如初。

婚后,杨彦倍加疼爱妻子爱达,并亲自教授她汉语与美术。

爱达自身聪颖,终也不负丈夫的谆谆教导,一手国画竟也能笔精墨妙,活色生香。

除此之外,爱达还对中国的多种传统文化十分感兴趣,如古筝、茶道、京剧。

只要有客人来家中做客,爱达总会穿一身端庄的旗袍,将长发盘起迎接,并且还能露上两手。

2023年,在丈夫的支持下,爱达举办了人生第一次画展,引得各界人士前去观瞻。

在画展采访上,杨彦骄傲地说道:“我的爱人就是中国画家,她只画中国国画。”

不仅如此,杨彦还与妻子多次合作作画,所出作品皆以义卖为由募捐。

久而久之,争议之声渐息,取而代之的是一片赞叹,甚至有人称他们为国画界的“明星夫妻”。

不久,杨彦夫妇有了爱情的结晶,十月怀胎过后,妻子爱达生下了一个可爱的小男孩。

杨彦与妻子给他取名为“杨和平”,而“和平”二字则是为表达中非两国的友好之谊长存。

空档之余,不少媒体都会来争相采访杨彦夫妇,邀请他们上节目,讲述他们传奇的爱情故事。

人们越来越羡慕这一对神仙夫妻,更是给同他们经历相同的人,追求自由婚姻的勇气。

杨彦在某次节目中说起自己的妻子,脸上洋溢着幸福喜悦的笑容。

他不仅收获了美满如梦的爱情与婚姻,也如愿帮助了非洲地区被埃博拉病痛折磨的大多数人。

随着杨彦夫妇的出镜率愈渐愈高,许多重要场合都能看见他们甜蜜地携手出席。

杨彦的画作也因此抬高了不少价值,有些人花以重金,只为求杨彦一画。

按说杨彦夫妇名利双收,婚姻也得到了所有人的祝福,他们本应该就这样白首相庄,相濡以沫。

可生活的柴米油盐使得二人开始争吵,由于语言不通,常常会曲解对方的意思。

杨彦本就比妻子年长,遇到一些争吵无果的事情,便会沉默不语,而妻子就会一气之下离家出走。

朱德庸曾经说过:“高难度的爱情是月色、诗歌、三十六万五千朵玫瑰,加上永恒;

高难度的婚姻,是账簿、证书、三十六万五千次争吵,加上忍耐;

而高难度的人生,是以上两者皆无。”

杨彦和妻子体验过了前者,收获了高难度的爱情,本以为能够永恒,可却在高难度的婚姻上不知所措,无能为力。

两个人就这样在争吵的道路上渐行渐远,最终杨彦做出了一个惊人的决定。

圣者法显,大觉愿行

2023年,终南山净业寺内,杨彦端坐在佛前,神色肃穆,等待着高僧为他剃度。

周围既有他昔时好友,也有各界媒体人士,却独不见他曾经的妻子爱达还有他们的儿子。

所有人都不理解为何杨彦会抛弃他的家庭,来到寺内剃度修行。

谁人家中没有二三烦心事,何必下这般狠心,又置他昔日忠贞的诺言于何地?

可当媒体们围着杨彦追问他如此决绝的缘由时,他只是闭目不语,手中犍槌“铛铛”作响,仿佛入定一般。

待媒体散尽,三两好友上前询问,杨彦也只淡淡一句:“只是为了智慧……”

简单的六个字,却让人生出了无数猜想……

有的人认为他已看破红尘,金钱功名与他而言已经成为前尘;有的人认为他不愿再碰触情爱,斩断白丝只为断情绝念。

可无论怎样的猜想,从此书画界再无那个挥金为博妻一笑的杨彦。

此后,净业寺内多了一位法号为“释大觉”的僧人,他不爱与人交流,常常一人独坐,仿佛要探穿这世间的奥秘。

大觉虽遁入空门,但仍醉心于绘画。前半生的杨彦作画龙飞凤舞,气势磅礴,而现在的大觉作画笔墨丹青之下疏密有致,令人身至其中。

俗话说:“吃大苦方得大自在。”大觉在摒弃红尘杂念后,开始游历各地,只为寻找他心中的智慧。

吐鲁番盆地、海南岛、西行之路乃至沙漠暴雪都有着他瘦小的身影。

他力行追宗法显衣钵,发愿在各地弘扬法显精神,清净人心、引路光明。

随着释大觉在世人眼中的出现,人们渐渐忘记了曾经那一家三口的其乐融融,忘记了那场轰动一时的婚礼,也忘记了荧幕中幸福的笑容。

取而代之的是大觉一幅又一幅惊世骇作的涌现,《法显西行图》、《演绎塔》等作品皆体现了这位僧人弘扬法显的决心。

或许已经没有人再记得那个自塞拉利昂跨国而来的非洲女孩,也没有多少人再去关心她和孩子们的去向。

“婚姻是一座围城,城外的人想进去,城里的人想出来。”

一段婚姻里,有的人着迷于柴米油盐酱醋茶的烟火气息,有的人却贪恋罗曼蒂克式的天马行空,可阴晴闹和,才是婚姻的情趣所在。

我们都不清楚杨彦是否后悔过抛弃他的家庭,选择遁入空门;也不清楚那个笑靥如花的非洲少女,是否后悔过远嫁中国,只为一眼万年的爱情。

但只要爱过,便不留遗憾。

而“放手”或许是他们另一种关于幸福的抉择,就像为了“智慧”皈依佛门的杨彦,虽曾有过相爱相知,但他的余生将会为了弘扬法显,继续一人远行。

参考资料

[1]《北京客》,第20230411期,《画家杨彦千里寻他非洲媳妇》

[2]《我家有明星》,第20231211期,《当非洲媳妇爱达遇到中国老公杨彦》

[3]《大家健康》,2023年第08期,《国画大师与非洲妻子:爱情是神的指引》

关于心理描写的句子(好词好句积累)

好词好句积累:描写心理活动的句子与成语,形象生动的刻画出来!

作文素材

描写人物心理的成语

安心乐意 指心情安定,满意,很愿意如此

黯然神伤 黯然:心情抑郁沮丧的样子。情绪低沉,心神忧伤

茶饭无心 没有心思喝茶吃饭。形容心情焦虑不安

夺眶而出 眶:眼的四周。指眼泪无法控制地从眼里流出,形容心情非常激动

归心似箭 想回家的心情像射出的箭一样急。形容回家心切

柳泣花啼 形容风雨中暗淡的心情

满脸春风 形容心情喜悦,满脸笑容

满面红光 满面:整个面部。形容心情舒畅,精神健旺的样子

百感交集 感:感想;交:同时发生。各种感触交织在一起。形容感触很多,心情复杂。

悲欢离合 悲伤、欢乐、离散、聚会。泛指生活中经历的各种境遇和由此产生的各种心情。

悲喜交集 悲伤和喜悦的心情交织在一起。

碧海青天 原是形容嫦娥在广寒宫夜夜看着空阔的碧海青天,心情孤寂凄凉。后比喻女子对爱情的坚贞。

愁多夜长 因心情愁闷而夜不成寐,感到时光悠长难遣。

春风得意 旧时形容考中进士后的兴奋心情。后形容职位升迁顺利。

春晖寸草 春晖:春天的阳光;比喻父母对儿女的慈爱抚养。寸草:一寸长的小草;比喻子女对父母的养育之恩的无限感戴心情。

表示人物心理的成语

莼羹鲈脍 莼:莼菜;脍:切得很细的肉。比喻怀念故乡的心情。

莼鲈之思 比喻怀念故乡的心情。

澹泊寡欲 澹泊:恬淡;寡:少;欲:欲望。形容心情恬淡,不图名利。

澹泊明志,宁静致远 澹泊:不追求名利;宁静:心情平静沉着。不追求名利,生活简朴以表现自己高尚的情趣;心情平稳沉着,专心致志,才可有所作为。

低回不已 低回:徘徊留恋;不已:不停止。不停地徘徊,留恋忘返。形容伤感难忘的心情。

槁木死灰 枯干的树木和火灭后的冷灰。比喻心情极端消沉,对一切事情无动于衷。

归心如箭 想回家的心情象射出的箭一样快。形容回家心切。

观望不前 不前:不敢上前。事情尚难确定时,怀着犹豫不定的心情,观察事物的发展,暂不前进。

寒毛卓竖 汗毛都竖立起来。形容非常恐怖,或心情特别紧张害怕。

近乡情怯 指远离家乡多年,不通音信,一旦返回,离家乡越近,心情越不平静,惟恐家乡发生了什么不幸的事。用以形容游子归乡时的复杂心情。

开怀畅饮 开怀:心情无所拘束,十分畅快。比喻敞开胸怀,尽情饮酒。

襟怀洒落 襟怀:胸怀;洒落:洒脱。心情坦率,光明正大。

惊魂未定 指受惊后心情还没有平静下来。

久旱逢甘雨 干旱了很久,忽然遇到一场好雨。形容盼望已久终于如愿的欣喜心情。

闷闷不乐 闷闷:心情不舒畅,心烦。形容心事放不下,心里不快活。

捏一把汗 因担心而手上出汗。形容非常紧张的心情。

灭此朝食 让我先把敌人消灭掉再吃早饭。形容急于消灭敌人的心情和必胜的信心。

平心静气 心情平和,态度冷静。

屏气敛息 指因心情紧张或注意力集中,暂止住了呼吸。

平心而论 平心:心情平和,不动感情;论:评论。平心静气地给予客观评价。

人莫予毒 莫:没有;予:我;毒:分割,危害。再也没有人怨恨我、伤害我了。形容劲敌被消灭后高兴的心情。

迫不及待 近:紧急。急迫得不能等待。形容心情急切。

如释重负 释:放下;重负:重担子。象放下重担那样轻松。形容紧张心情过去以后的的轻松愉快。

人琴俱亡 俱:全,都;亡:死去,不存在。形容看到遗物,怀念死者的悲伤心情。

赏心悦目 悦目:看了舒服。指看到美好的景色而心情愉快。

赏心乐事 赏心:心情欢畅。欢畅的心情,快乐的事情。

体贴入微 体贴:细心体谅别人的心情和处境,给予关心和照顾;入微:达到细微的程度。形容对人照顾或关怀非常细心、周到。

陶情适性 陶:喜,快乐;适:舒适,畅快。使心情愉快。

肃然起敬 肃然:恭敬的样子;起敬:产生敬佩的心情。形容产生严肃敬仰的感情。

痛定思痛 指悲痛的心情平静以后,再追想当时所受的痛苦。常含有警惕未来之意。

无可奈何花落去 对春花的凋落感到没有办法。形容留恋春景而又无法挽留的心情。后来泛指怀念已经消逝了的事物的惆怅心情。

关于心理描写的句子

1) 当四十分钟跑过时,我的心跳似乎更加快了。我的心一直悬着。直到下午第四节课的时候,我的心才稍微安定了一点。

2) 当我拖着沉重的双脚走向台上时,才回过神来,我马上就要开唱了。站在最后一排,也是最高的,下面的一切的一切都尽收眼底。我发现每一个人都在盯着我,我的心跳加快了,脸上火辣辣的,好像被谁抽了耳光似的;手心里也时不时的渗透着冷汗。我再也不敢往下看了,只是把眼睛微微地闭上,等待着、等待着…

3) 她屏住呼吸,一动也不敢动。只听到自己的心怦怦地剧烈地跳动。似乎要碎裂了般的疼痛。她紧紧地闭住眼睛。

4) 今天,我们要进行期中考试。天气很阴冷,可是我的手心竟然出了汗,应该是要考试的缘故吧。我紧张的走进考场,心“怦怦”直跳,不知道这次题目有多难。我坐在座位上,焦急地等着老师发试卷。

5) 看到同学们都在紧张的答题,我也赶紧收回了目光,继续写我的卷子。写着写着,后面的题目渐渐难了起来,我想了半天才答出一道题,我也有些紧张了,我手心上出满了汗,额头也冒汗了。

6) 人到齐了,我们跟着两个老师走向赛场。我无比紧张的走近赛场,心中好像有一个顽皮的小精灵在不停地跳跃着,而且越跳越快。好想在跟我说:“小笨蛋,千万别紧张,别紧张!”

7) 时间似乎故意和我作对——走得慢极了,烦躁、焦急一起涌上心来,我不停地看表,盯着那慢慢移动的秒针。

8) 手脚钻心的冷,刚吃过的东西似乎已经消化掉了,在心脏有节奏的跳动下,随着肠道慢慢下滑。如果时间允许,我想去一下很重要的地方,可是……

9) 台下观众的欢呼声、加油声、呐喊声响成一片,那可真是一浪盖一浪。这欢呼声却让我有种莫名的紧张,我深呼吸,调整一下心态。

10) 我感觉自己的心像要跳出来一般,徘徊、流浪却找不到出口,只知道自己将面临着一项艰巨却又不得不为的重担,心突然间好累……

心理描写的优美句子

1) 我是一个很普通的人,既然很普通,那么我也跟一般人一样,只要一紧张,汗水就会顺着脸颊慢慢地流下来,止也止不住。不一会,汗水就会浸透我的衣衫,好难受,本来宽宽的衣服变成了紧身的衣服,你们说难不难受?

2) 曾经的事历历在目。你就我如何再去走完剩下的路。

3) 顿时,我好像掉进了冰窖里,从心顶凉到了脚尖。

4) 风吹过,落叶与我的好心情一起飘零,这深秋的花败正好衬托出我内心的苍凉,放眼望去,已不见绿色,到处是令人丧气的枯萎,难道,老天也感知了我的沮丧么?

5) 回家的路上路灯坏了好几盏,没有了灯光我看不清,没有发言权却落寞的影子。

6) 回家的路上我哭了,眼泪再一次崩溃孓.无能为力这样走着,再也不敢骄傲奢求了。我还能够说些什么,我还能够做些什么?我好希望你会听见, 因为爱你我让你走了。

7) 睛重得抬不起来,长长的的睫羽上挂着来源不明沉重的几滴珠水,眨了几次,晃悠悠跌落下来,视线迷迷蒙蒙的,透过依稀水气,映出一张表情恍恍惚惚的脸。

8) 沮丧时总会明显感到孤独的力量,多渴望懂得的人给些温暖借个肩膀,很高兴一路上有你们相伴.最初的梦想紧握在手上,最想要去的地方,怎敢能在半路返航?

9) 脸上的快乐,别人看得到,心里的痛又有谁能感觉到。

10) 淋过雨的空气, 疲倦了的伤心,我记忆里的童话已经慢慢的融化。

11) 漫漫悠长的人生道路,我尝试了所有!爱过,哭过,笑过,沮丧过,悲伤过,痛心过,付出过,被抛弃过,虚伪过,孤独过,寂寞过,折磨过自己!到最后什么都没有得到,开心幸福我没

12) 有得到过,伤心难过孤独寂寞我却得到的比任何人都多!我所剩的只有些埋藏在心底的那模糊不清的回忆。伤感的句子

13) 木头对火说:“抱我”! 火拥抱了木头,木头微笑着化为灰烬! 火哭了!泪水熄灭了自己,当木头爱上烈火注定会被烧伤。

14) 那件事比三伏天穿棉袄还难受,

15) 内疚、懊悔敲击着我的心,翻来覆去睡不着。

以上内容是关于自动分析句子成分的app和句子语法成分分析app的内容,小编幸苦为你编辑整理,喜欢的请点赞收藏把。

标签:句子语法成分分析app

标题:自动分析句子成分的app 句子语法成分分析app

链接:http://m.zhaichaow.cn/z/1633198.html