常见音频的损失函数MSE均方误差损失$$ MSE=\frac{1}{n}\sum_{i=1}^n(y_i-\hat{y}_i)^2 $$公式n：表示样本的数量比如100张图片n就是100i：第i个样本$y_i$表示模型对滴i个样本的预测值$y_i-\hat y_i$是单个样本的预测误差$\sum$为求和1/n对总和求平均，因此也叫均方误差最小二乘损失最小二乘损失的核心思想是：通过最小化预测值与真实值之间差距的平方和，使得模型预测接近真实的情况。二乘指的是平方，我们不直接计算差距（残差），而是计算差距的平方。最小：我们的优化目标，让这个平方和达到最小。最小二乘法就是最小化平方和的方法，其对应的损失函数就是最小二乘损失函数最小二乘法是一种数学优化技术，它通过最小化误差的平方和，来找到一组数据的最佳拟合曲线（或直线）为什么二乘就是平方呢？想象一下，我们在三点图上画一条直线，试图让它尽可能地穿过靠近所有的数据点。每个数据点与直线在垂直方向上的距离，就是该点的误差（也就残差）。点在直线上方，误差为正，点在直线下方，误差为负。如果直接把所有误差相加，正负误差会相互抵消，这显然不合理。因此就将误差进行平方再相加。这个所有误差的平方之和，就是我们要最小化的目标。线性化回归就是最小二乘法的使用例子之一。对于单个样本，其损失是预测值$y_i$和真实值$\hat{y}_i$的平方$$ L(y_i,\hat{y_i})=(y_i-\hat{y_i})^2 $$数据集中，所有样本的损失的平均值，就是均方误差$$ L(y_i,\hat{y_i})=(y_i-\hat{y_i})^2 $$MAEMean absolute errorL1 loss这个名称来源于数学本质，即计算的是绝对差值的 L1范数。MAE这个名称更偏向于统计学意义，即平均绝对误差。他的目的是衡量预测值和真实值之间的误差。计算绝对值的平均值$$ \mathrm{MAE}=\frac{1}{n}\sum_{i=1}^n|y_i-\hat{y}_i| $$L1指的是L1范数，也称为曼哈顿距离。L1范数是计算向量各元素绝对值之和。由于使用的是绝对值而不是平方，当预测值与真实值相差巨大时（离群点），MAE 的惩罚增长是线性的，而 MSE 的惩罚是平方增长的。因此，MAE 比 MSE 对离群点更具鲁棒性。在反向传播计算梯度时，MAE 的导数是常数（±1），这有利于稳定的训练，但可能在接近最优解时收敛变慢。它的单位与预测目标单位一致，易于理解和解释。例如，预测房价（万元），MAE 是 5，就表示平均预测偏差了 5 万元。MSE损失的收敛目标是均值，而MAE的是中位数。STFT多分辨率损失多分辨率STFT loss通过在不同分辨率下计算短时傅里叶变换（STFT）佬捕捉音频信号的多尺度特征。核心思想是使用不同的STFT参数配置，在多个尺度上衡量生成音频与真实音频在频域上的差异。STFT将时域上的音频信号转化为时频上的表示，即频谱图幅度谱：表示了每个频率成分在特定时间点上的能量强弱。这是我们人耳感知音量的主要依据。相位谱：表示了每个频率在充分太热定时间点上的起始位置。它对于音频的清晰度和时序至关重要。I音频模型通常先生成目标的频谱图（尤其是幅度谱），然后再通过一个声码器（如Griffin-Lim或神经网络声码器）将频谱图转换回时域音频。因此，在频谱图层面设计良好的损失函数至关重要。频谱收敛损失：Magnitude-based Loss幅度损失是主要用于处理频谱图或其他时频表示，核心思想是比较生成音频与目标音频在幅度谱上的差异。在需要进度重建音频波形的任务中，它是核心的组成部分。但它不会单独使用$$ L_{sc}(x,\tilde{x})=\frac{\||STFT(x)|-|STFT(\tilde{x})|\|_F}{\||STFT(x)|\|_F} $$这个损失函数有一个更全局的视角，它关注的是生成频谱和目标频谱在形状上的一致性。SC损失将频谱图视为一个高维向量，并计算这两个向量之间的“夹角”或“方向”差异。它本质上计算的是归一化后的生成频谱与目标频谱之间的差异。你可以把它想象成：我们不直接比较两个向量的长度（那是幅度损失关心的），而是比较它们的方向是否一致。如果方向一致，说明频谱的“轮廓”或“相对能量分布”是正确的。对数幅度谱损失这个损失函数直接比较生成音频和目标音频的幅度谱。在幅度谱上计算生成结果与真实结果之间的差异。由于人耳对声音强度的感知是对数性质的（例如，声音强度增加一倍，我们感知到的响度变化不是线性的），所以通常会在对数尺度下进行比较，这更符合人耳的听觉特性。对数L1损失(更常用、更稳定):$$L_{log-stft-L1}=\mathbb{E}\left[\left\|\log(|S_{target}|+\epsilon)-\log(|S_{generated}|+\epsilon)\right\|_1\right]$$对数L2损失：$$L_{log-stft-L2}=\mathbb{E}\left[\left\|\log(|S_{target}|+\epsilon)-\log(|S_{generated}|+\epsilon)\right\|_2^2\right]$$其中$\|\cdot\|_{1\text{和}}\|\cdot\|_{2\text{分别表示 L1 范数和 L2 范数，}\mathbb{E}}$表示期望值(在批量数据上取平均)。通常这俩个函数几乎总是同时使用加权相加，构成一个复合函数。对数幅度谱复杂纹理，像一个像素级的损失。频率幅度谱负责轮廓，像一个全局结构损失，确保整个频谱大致形状是正确的。Feature Matching（FM）FM最早出现在论文 Improved GAN Training中后来被MelGAN和HIFI-GAN作为核心稳定器继承它的思想非常简单：不是值让判别器认为真和假。而是让生成器让fake的判别器中间层特征和real的判别器中间层特征尽量相同。判别器有多个子判别器，每个sub-D有多层，每层有feature map$D_k(x)$=判别器低k个字判别器的输出$f_{k,l}$=低k个子判别器的l层 feature mapFM的Loss定义为$$ L_{FM}(G, D) = \sum_k \sum_l \|f_{k,l}(x_{\text{real}}) - f_{k,l}(G(z))\|_1 $$也就是取多个判别器MPD+MSD对每个判别器每一层feature Map 做L1distance。最终加权求和。feature matching的直觉是：GAN的对抗损失值关心：生成器能不能骗过判别器判别器能不能区分真假但对抗损失不会告诉生成器怎么样更真实。Feature Matching 做的事情：判别器内部有层层抽象特征，例如：短期时域结构（similarity of waveform）周期性结构（pitch/周期）振幅动态（energy envelope）高频谐波多尺度卷积感受野捕捉的模式FM 要求“fake 的特征必须和 real 的相似”。**这本质上给生成器一个稳定的方向： 你要往真实特征靠近，而不是只要骗过判别器就行。**
分布式事务https://www.bilibili.com/video/BV1U341157BX事务ACID在分布式场景下ACID特性含义会被扩展。原子性（Atomicity）一个事务内涉及到的分布式多个节点的操作要么一起成功，要么一起失败一致性（Consistency）：跨节点场景下也不能出现中间状态。隔离性（Isolation）：一个分布式事务在一个节点哪怕完成了，在整个分布式事务完成之前，也不能被其他事务读取使用持久性（Durability）事务提交完成后的数据不仅要在当前节点保存，还应该在分布式场景下有备份高克yognxing早期分布式事务的三个方向分别是俩阶段模型，XA事务模型TCC事务模型Saga事务模型常见的方案俩阶段提交一个直接的思路就是三个系统都等到整个交易快照完成时再一起提交。根据这个思路就诞生了俩阶段提交的方法，阶段1协调器收到客户端commit命令，决定提交T。然后协调器向各节点发送prepare T消息，各节点决定该节点上T是提交还是中止并返回给协调器。第二阶段协调器收到各个节点的ready或don't commit响应，如果都是ready，那么就决定提交T，并向所有节点发送commit消息；如果收到来自一个节点或多个节点的don't commit T的消息，就决定丢弃T，并向所有节点发送abort的消息。XA事务就是把俩阶段提交标准化后形成指定的数据库接口，再由一个事务管理器作为协调者统一管理俩阶段提交，相当于标准化的俩阶段提交。很多单机数据库，如oracle、DB2、Mysql支持XA接口，应用程序实现的一个事务管理器，然后跳读下面数据库的接口实现俩阶段提交，从而实现分布式事务。XA事务允许各个系统选择不同类型的数据库，只要它们都支持XA接口。TCC事务TCC将整个业务逻辑分为三块Try、comfirm，和cancel三个操作Try就是尝试扣除用户月、尝试扣除库存；confirm就是Try的结果都成功，可以证实把订单状态状态改为已支付，通知仓库发货；而cancel则是try失败，那么就要取消教育，回复月，回复库存。TCC是业务逻辑补偿的应用层设计实现。TCC的一个关键点是，Try阶段如果完成，那么很多状态就已经保存起来，再出现故障也不会影响事务的晚餐，也就是说，哪怕confirm阶段或cancel阶段失败，也会通过不断重试完成交易，而不是回滚。TCC的过程类似于俩阶段提交，Try过程和俩阶段prepare比较相似。但TCC没有事务管理的抽象，Try、confirm、cancel和业务场景密切绑定，要考虑具体业务来实现。SATA事务模型Saga本来是为解决长事务而出现的分布式事务模型，长事务的是需要太长时间而不允许它们保持其他事务所需要的锁的事务。电商交易如果采用俩阶段提交，则订单系统的子系统要等到用户确认收货交易完成才能提交，而物流的时间一般比较长，这个时间可能需要几天。而订单系统事务可能会拖住一些资源，如用户的余额，这个锁维持几天的时间，会导致用户无法消费，这显然是不可行的。Saga是把分布式事务看做一个个单机事务的合集，对每一个单机事务A，Saga都有一个补偿事务$A^{-1}$。如果我们执行A，又执行$A^{-1}$，那么锁产生的数据库状态同A和$A^{-1}$都未执行前一样。同样都是需要业务逻辑接入，其中TCC适用于对一致性要求高、性能敏感的场景。SAGA的核心的回滚，像一个执行-如失败反向补偿的厂业务流程，适用于执行流程长，业务流程复杂的场景。体现在分布式整体上，只要我们能确认一个单机事务都执行成功，那么整个分布式事务就是成功的，可以提交。如果任意一个单机事务不成功，那么就要把它和它之前的单机事务回滚掉，但它之前的单机事务已经提交过了，无法回滚，这时候就需要执行它们的补偿事务。电商交易系统的例子可以通过设计补偿事务，把库存和用户余额加回，来实现分布式事务。分布式事务模型传奇基地交易 ：： ShardingSphere在单机数据库的基础上是心爱一个中间层（proxy），中间层基于单机事务加saga来实现分布式事务。高可用通过单机数据库的主备复制实现。如ShardingSphere-Proxy中，可以通过配置将分布式事务管理委托给Seata。当ShardingSphere-Proxy接收到一个跨多个数据库节点的事务请求时，它会将事务信息发送给Seata的全局事务协调器。全局事务协调器根据事务类型（如Base事务）和配置规则，对事务进行协调和管理。在事务执行过程中，ShardingSphere-Proxy会负责将事务操作路由到相应的数据库节点，并监控事务的执行状态。一旦事务执行完成，Seata会根据事务的执行结果，进行全局的提交或回滚操作，确保分布式事务的原子性和一致性。原生数据库：它们基于俩阶段提交实现分布式事务，不存在单机事务。比如蚂蚁的oceanbase和TIDB。SAGA生成补偿事务的思想是通用的，分布式数据库可以通过解析单机事务的SQL，从而生成逆SQL，从而生成补偿事务（比如AT模式）实现这一点要满足俩个条件要有一个标志把同一个分布式事务的所有单机事务联系起来，以便在需要执行补偿事务的时候能找到它们有的单机事务已提交，但它所在的分布式事务未提交，这时它提交的结果在分布式的角度仍处于脏数据，通常不允许其他事务对其进行读取。要单独进行处理。（通常使用分布式锁来实现）TCC的一致性比SAGA更强。SAGA是可以直接提交和直接回滚，但TCC要先try。前面介绍的俩阶段提交原型，可以看出，俩阶段提交对事物管理器的抽象天然适合封装在分布式数据库内部，这也是大部分原生数据库选择俩阶段提交的原因。俩阶段提交有一个显著的特点：事务只有在prepare阶段才会在数据行上锁，整个执行过程都是无锁的，这意味着并行的事务很有可能修改了同一行的数据。也就是说俩阶段提交是first commit win，是一种乐观事务模型（假设事务没有冲突发生）。目前一些商业的原生分布式数据库对俩阶段提交的加锁进行了改造，支持了悲观锁（即在事务执行阶段就加锁），这一步拓展原生数据库的支持能力。此外最新版本的俩阶段提交有严重的问题：协调及存在高可用问题，一旦宕机，在恢复或者新的协调器被选举出来之前，整个系统是不可用的。google的percolator事务模型对俩阶段提交问题做出了优化：把收到分布式事务请求的第一个节点作为俩阶段提交的协调者节点，一存储节点的高可用保证了协调节点的高可用，同时也不会出现些调整宕机了整个系统都不用的开启，因为任何一个节点都可以是协调者节点了。此外Percolator在事务开始时和事务提交时都会取一个时间戳并写入数据库中，这个时间戳可以很便捷的支持分布式MVCC的实现。数据库锁所是用来管理共享资源的访问的机制。在数据库中除了用户数据要上锁，还会对缓冲池中LRU列表、数据库元数据等其他资源上锁。不同数据库对锁实现差异很大。Oracle、Mysql大部分都是采用行锁设计。单机数据库的行锁会分为俩中：共享锁（S）和排它所（X），只有贡献锁之间才兼容。以mysql为例slect ... lock in share mode上的是S锁，不会阻塞后续的其他share mode。update/select... for update上的是X锁会阻塞读的操作，包括其他的share mode锁。事务中的锁还有很重要的概念，交俩阶段锁理论（2Phase lock）加锁阶段和解锁阶段是俩个独立的阶段，不能有重叠。俩阶段锁理论和事务的原子性以及隔离性关系，密切，是保证并发调度准确性的必要条件。加锁是执行阶段，解锁是提交阶段。在单机事务中大家很少考虑加锁和解锁的过程与代价。所资源默认堆所有事务是可见的，一行数据的加锁和解锁都可以看做原子操作。在分布式数据库中，数据行分布在多台机器上，因为跨节点网络传输的不可靠性，加锁和解锁不在是实时切原子的了。那么保证分布式上锁的正确性，需要在分布式事务中维持一个中心节点来记录所信息。加锁与解锁的成功和失败都以中心节点为准。在编排式SAGA中，有一个中央协调者（Saga协调器）负责管理和控制整个事务的执行流程。协调者按照预定义的顺序依次触发每个本地事务，并在事务失败后处理补偿事务。这种方式逻辑清晰、易于管理，但存在单点故障风险。而在协同式SAGA中，没有中央协调者，各个服务通过事件驱动的方式进行协调。每个服务自己决定何时开始下一个本地事务，通过订阅和发布事件来触发后续操作。这种方式去中心化、服务间耦合度低，但事务流程相对复杂，难以追踪。SAGA：独立中心节点俩阶段提交：协调者节点任何情况下出现了中心节点就要考虑高可用，通常使用共识算法来实现对于锁表和协调者的高可用性。Percolator直接把俩阶段提交的协调者放在了分布式事务的一个随机的数据节点，把协调者的高可用问题转换成了数据节点高可用问题。分布式锁通常只有X锁，因为实现分布式锁的代价本来就很大，如果还要实现分布式S锁就会增加非常多的维护和调度代价，并且S锁的使用本来就很少，因此一般没有实现分布式S锁的方案。对于select ... lock in share mode语句分布式数据库一般不支持（原生分布式数据库）或选择只在单机节点上S锁。隔离级别现在大部分关系型数据库都是基于MVCC的，要引入一个针对快照的新的隔离级别。SI（snapshot Isilation，SI）SI隔离级别会去读书屋开始时间点的数据快照，从而避免不可重复读，但又不像用锁实现的可重复读隔离级别那样要阻塞写。（如果始终读最新的快照，则相当于RC，但我们将SI，一般是事务开始的时候的快照）SI和RR的区别就是幻读和排序写。我们把点查询成为item查询，非点查询（范围聚合等），称为predicate查询因此我们可以把幻读定义为不可重复读的predicate版本。幻读和不可重复读都需要在一次事务中进行俩次读操作。不可重复读指的是俩次item查询到了不同的内容，而幻读指的是俩次predicate查询到了不同的结果。
X-Engine背景笔记来自 https://www.bilibili.com/video/BV1rS4y1N7YKX-Engine是阿里云数据库PolarDB的OLTP存储引擎（基于Mysql），主要针对电商 平台的工作负载开发，双11期间的工作负载有以下特征。活动开始时每秒交易量激增（TPS）称之为海啸问题（The tsunami problem）海啸问题：双十一零点时的TPS增加到原来的122倍，在2018年双十一的最高峰时间段，每秒钟同事进行491000笔交易，这意味着数据库同时在处理的事务达到了七千万，阿里团队希望这样的压力下依旧能维持稳定的响应时间。我们可以看到蓝色的线在红色的线夸张的涨幅下，仍旧维持了一个相对稳定的表现。缓存（Buffer）很快会被大量数据填满，我们称之为泄洪问题（the flood discharge problem）泄洪问题：随着写入量的迅速增加，存储引擎的内存缓存会被大量数据迅速填满。这时，我们就需要对新增的数据进行泄洪，也就是尽快把数据转移到持久化内存中。这也意味着我们需要一个写入能力很强的存储结构。随着促销活动的转变，数据的冷热会发生迅速的切换，称之为访问热点急速变化问题（The fast-moving current problem）数据访问局部性是缓存能够生效的关键，但是双十一中大量的整点秒杀活动，会让一些非常冷的数据变成热点数据。这时候一般数据库的存储引擎很难对这种快速的切换做出反应。从而造成缓存命中率的大幅度下降。X-Engine就是需要能够快速从持久化存储中取出新的热点数据，并且放到缓存中来解决这个问题。访问热点急速变化问题：双十一会有各种促销活动，比如整点秒杀（seckill）活动，会使当前数据的冷热急速变化。此时数据引擎需要对数据热点的变更做出反应，快速持久化存储中取出新的热点数据。LSM因为电商业务涉及到大量数据的插入和修改，所以X-Engine采用了有杰出性能的LSM-Tree。其写入的操作不做原地更新，而是以追加的方式写入内存。每一次写到一定程度，即冻结一层，写入持久化存储。一条数据->写入memtable->flush到持久化存储->经compaction操作进入持久化存储的更下层->覆盖同意key的老版本数据/同一key的新数据到来时丢弃。真正删除老数据是在后台的compaction（合并）过程中完成的。当多个SSTable文件合并时，系统和会保留最新版本的数据，删除旧版本的数据和墓碑标记，从而释放磁盘空间。compaction操作不断地把相邻的层次数据合并，并写入更低的层次。合并的过程实际上是吧相邻俩层数据读出，按key排序，想通的key之保留更新的版本。LSM-Tree的特点是写强而读弱，我们可以简单理解为LSM-Tree的写入能力比B+ Tree搞一个数量级，读能力弱一个数量级。这是因为LSM-Tree的数据可能存在多个版本。PceanBase和Tidb底层都是用的LSM-Tree。整体架构图中上面的数据方方框就是内存，下面的数据方框就是磁盘。内存中的数据都是红色的，表示热数据，橙色表示温数据，浅黄色表示冷数据。越热的数据会越往上层放。写入：写入中x-engine会和其他数据库一样做一个redo log落盘操作。然后把数据写在active memtable里面。这里是一个脏页的概念，一个active memtable写满后，他会变成一个Immutable memtable。这时候Memtable就不会做改变了，Immutable memtable 会等待一个Flash操作，进入磁盘中变成持久化存储。持久化存储越上面就是越昂贵的磁盘。比如最上面是NVMe，下层是SSD，下层是HDD。磁盘的价格是由贵到便宜，价格也是由高到底。下层之后就是依次往下合并变成一个冷数据。读取：首先我们要读取磁盘里面的active memtable和immutable memtable。X-Engine总体可以分为内存中的热数据层和持久化存储的 温、冷数据层。内存中有active memtable和多个Immutable memtable以及Read Cache和Index。硬盘中的数据分为多层，不同层的数据通过不同的存储设备存储，每一层数据以Extent为单位组织。X-Engine通过MVCC和2PL（二阶段锁）实现了SI（Snapshot Isolation 快照隔离）和RC隔离级别。SI的核心特点是：事务启动时获取一个数据库快照，该事物的所有操作都基于这个快照，不会看到其他事物的修改。提交时，只有当机遇该快照的任何并发修改与该事物的修改没有冲突时才会成功提交。SI避免了ANSI SQL-92标准定义的三种并发异常：脏读、不可重复读、幻读。读操作不会被阻塞，对于读多些少的应用场景性能表现优异。以mysql为例，有四个标准的隔离级别：读未提交，读已提交、可重复读、串行读。其中可重复读是默认隔离级别。SI是一种实现机制，mysql在可重复读隔离级别下，通过MVCC实现了快照隔离的特性，具体来说，事务启动时会创建一个一致性快照，事务内的所有读操作都基于这个快照，不会看到其他事务提交的修改，从而保证了可重复读。海啸问题面对海啸问题，一个常见的思路是增加服务器的数量，横向扩展服务器集群。但这个方案在双十一场景下有俩个问题：一是成本太高，双十一零点时TPS会激增122倍，服务器数量不可能也增加这么多倍。而是增加服务器的效果也不会很高，因为海啸问题面对的主要是写和读的混合压力，读压力比较容易扩展，写压力很难扩展，增加机器也不能很好的散开写压力。各个阶段还是要提高CPU、线程、内存、IO等资源的利用率，在数据引擎层面提供更高的事务并发和吞吐量，来加快流转效率，避免堆积。X-Engine主要采用了俩个措施来解决海啸问题，一个是交易的异步写入，第二个是多阶段生产线。交易异步写入我们把事务分为执行阶段和提交阶段。执行阶段：查看要更新的数据是否在buffer pool中如果不在的话，读取到buffer pool中生成redo log修改数据页提交阶段把redo log写到log buffer中把log buffer中的log落盘把数据也放入active memtable中，表示可查返回提交成功。这就是我们常说的write ahead log（WAL）操作，redo log落盘意味着事务完成了持久化。但是我们还需要做一个操作，把数据爷放到active memtable中，表示事务已经完成了可以被查到。为了实现简单，大部分存储引擎，比如innodb，都会采用一个事务一条线程的方法，这种方法写入日志IO存在长时间等待。X-Engine首先把任务分成了多种无锁写入队列。此后，大部分线程可以异步返回并处理其他交易，每个队列今流一个线程来参加多阶段流水线提交写入任务。为什么要这样节省线程？通常IO阶段需要线程等待较长的时间。因此我们可以执行到提交阶段后，线程就去干自己的事情。每个队列留一个线程执行提交写入任务就ok了。在抄告并发情况下，机器线程数也是一个限制。交易的一部写入避免了线程等待IO，可以增加线程的流转效率，从而减少甚至避免用户事务等待分配线程的情况。 redo log落盘的动作一定是要有序的。队列这种数据结构，先进先出，天然保证有序。但多个队列之间的顺序如何保证呢？在执行阶段或者说读写阶段，我们还是一个事务一条线程来执行的。但是到提交阶段就分成了多个写入者。然后再进入了多条无锁写入队列里面，等待去进行多阶段流水线的提交过程。那么多个写入队列的顺序如何保证呢，猜想分库或分用户，反正就是在业务层面确保写入的不相干。也可能是，一次只有一个队列写入，队列间流转。经过验证在32核的时候，每个队列分配8个线程是表现最优的。多阶段生产线提交阶段分为四个独立的更细的阶段X-Engine为了进一步提高事务处理的吞吐，在事务Group Commit的基础上，采用了一种流水线技术。通过合理配置这四个阶段的线程数，来减少或避免对资源（如CPU、内存、IO）的等待时间从而实现，最大吞吐量。也就是避免有的阶段IO利用率高，CPU利用率低，而有的阶段则相反。有些对多阶段生产线的解读是：事务到了提交阶段后，就可以自由选择执行流水线任意一个阶段。这样事务提交的四个阶段就可以并行起来。我认为这样是不正确的，因为这四个阶段顺序并不能颠倒X-Engine分别限制了每个阶段的线程数。因为前俩个阶段数据和罗技的强相关新，复用第一第二个线程可以提高内存和磁盘的利用率，统一时间之允许一个线程分别执行第一第二阶段time图中只有a线程执行1,2阶段.其他b,c,d线程争抢完成1,2阶段的任务。图中的x箭头可能是不合理的，因为按照文中的描述，不应该会有一个线程在执行完第三个阶段之后去执行第二个阶段。测试部分论文的测试主要针对mysql的三个存储引擎，Innodb，rocksdb，还有X-engine进行了横向对比。因为海啸问题主要关注存储引擎的巅峰吞吐量。所以说这个测试主要是对JV接口的一个吞吐量的测试。因为Innodb没有KV接口，所以没有列入比较，因为B+相对于X-engine写入量肯定是非常弱的。（右上角）左下角中测试交易异步写入的作用和配置多少线程数比较合适。其中紫色，粉色，橙色和绿色是异步写入，只有线程数的差别。下面重合的三条都是同步写入。可以看到哪怕同步写入的线程数配置的再多也影响不了吞吐量。而异步的。所以X-engine选择了每个队列八条线程的方式来进行异步写入。泄洪问题面对泄洪问题，大促的时候瞬时的写压力向洪水一样冲击内存，内存肯定是撑不住的，要快速的把内存中的数据向下层存储转移。这个问题在一般的系统中，DBA除了增大缓冲池，没有很好的解决方法。但是在X-Engine中，数据的向下转移表现为immutable memtable存储到硬盘和硬盘中LVM-Tree的操作。然而这里硬盘中的LSM-Tree的compaction操作依旧是一个硬伤。所以X-Engine主要通过优化compaction操作来解决泄洪问题。X-engine设计了多层存储结构。第0层的快速FlushImmutable memtable的数据会直接flush到L0层，所以L0层的数据是乱序的。那么在查找L0时，就要扫描整个L0层，这个代价是比较大的，会消耗很多IO。如果没有任何处理（所以L0层理的数据就是一个乱序的状态），那么L0层的数据会见到一定程度后直接和L1层合并。会造成大量的重复（冗余）的温数据进入更深层。尽管L0层可能只占珍格格持久化层的百分之一都不到。但他的数据是从刚刚immutable memtable中的flash下来的，仅仅比最近添加到memtable中的数据老一点点。根据电子商务的抢时间局部性原则，L0的数据是极有可能再次被使用到的。所以说X-Engine吧L0层定义为了一个温数据层。所以X-Engine引入了intra-L0 compaction，即只针对L0进行compaction。就是把L0层数据仅一个单独的合并操作，并排序去重，并且不写入下一层（原样写入L0层）。因为这部分数据很小，损耗也少，我们可以经常进行这个操作。这不操作可以保证L0的温数据不会向下转移的同时，高效率降低成本（因为L0的数据很少，所以这步操作的损耗很小。数据复用对于非L0层的compaction，X-engine主要做了三个方面的优化L0的数据达到阈值后，就会和L1层进行一个compaction操作向更下层去转移。data reuse：对于没有重叠的快，不进行merge 而只改变变meta。可以从三个粒度进行reuse，没有重叠的extent可以reuse，没有重叠的data block可以reuse，data block可以被拆分来促成reuse。extent就是X-Engine中的数据块，每个数据块是2MB，每一层都有许多元数据索引，指向数据块，然后他们一起再去构成一个metadata snapshot，一个元数据的快照。它指向所有的数据构成一个全局的索引。这个时候我们进行一个compaction操作，这是compaction操作后的结果。对于0层，即可用复用的extent，我们并不需要在磁盘上把它拷贝到level1层，而是可以直接修改meta index。然后生成一个全局的元数据快照即可。extent级别的复用并不需要在磁盘上覆盖extent，而是直接修改metadata index。而data block的复用只能拷贝，即便如此也可以节省大量CPU。绿色的块表示extent可以直接复用，紫红色表示extent需要进行分割，来支持更细力度的复用，黄色的块表示extent需要进行一个copy操作.图中L1层和L2层要进行compaction操作，compaction操作的结果被放到L2中。如果没有数据复用，compaction操作就会取出L1，L2的所有数据按key排序，key相同的则merge，即L1，L2的所有块都是红色。使用 data reuse的情况下，只有部分重叠的块会merge，Data Reuse会按三级粒度进行compaction。extent粒度：L1的[210,280]和L2的[50,70]，都是俩层没有发生重合的部分，可以直接复用。即只改变元数据索引即可data block粒度：L1的[1,35]和L2的[1,30]块重叠了。然而，重叠的仅仅是[1,25]这部分data block。所以[32,35]这部分 data block可以复用。拆分data block 粒度：L1的[80,200]和L2的多个extent重叠了。然而，这个data block的key是很稀疏的，可以看到没有在135和180之间的key。那么就可以将其拆分为[106,135][180,200]俩个data block，然后将他们和L2的[100,130][190,205]块合并，L2的[150,170]块则可以直接复用。Extent级别的复用不需要在磁盘上拷贝覆盖extent，而是直接修改meta index。而data block的复用只能拷贝，即便如此也可节省大量的CPU。对于数据复用的过程是在朱行迭代的过程中完成的，不过这种精细的数据复用带来的一个副作用，即数据的碎片化，所以在实际的操作的过程中也需要根据实际情况进行折中。这种精细的数据复用带来的弊端是，数据的碎片化有可能会消耗我们查询时候更多的计算资源。异步IOX-Engine将compaction分成Read-Merge-Write三个阶段，通过实现异步IO，来提升效率。在Extent层次，一个compaction操作包含三个独立的阶段从持久化存储中取出俩个extent合并把合并的extent写入持久化存储其中1,3俩步都是IO操作，2啧是计算铭感的操作。X-engine在1,3使用异步IO请求，2作为1的回调函数来执行。当多个compaction操作并发执行时，2会被其他步骤覆盖来隐藏IO。FPGA offloading将compaction offload到FPGA中执行，从而降低了Compaction对CPU的资源消耗。随着IO性能的不断提高，很多传统的一IO为瓶颈的设计的数据库算法主键转变为计算资源的限制；反之亦然。阿里团队发现LSM-Tree存储引擎中的compaction操作正是由IO瓶颈转换为计算瓶颈的例子。所有对compaction的操作都放在一个优先级队列里。一个具有频繁删除操作的案例中，对删除的compaction放在最高优先级，其他的compaction由上到下优先级依次递减。删除案例是案例的一个实例，每当要被删除的记录数达到阈值，就会触发一次针对删除这些记录的compaction操作。而且这个compaction操作是最高优先级的，为了防止垃圾数据对存储空间的浪费。（查看源码，实际上X-Engine定制化了一个专用于删除的compaction，作为一个参数传入的compaction函数。）实验解决泄洪问题主要是对compaction的优化。FPGA提升了27%的吞吐量并且大大提升了稳定性。电子上午的热点数据很集中，data reuse在记录重复率搞的情况下效果非常好。访问热点急速变化结构面对访问热点急速变化问题，通常DBA是没有任何优化方法的，数据突然变热到数据完全进入缓存的期间，必然出现响应时间的上涨。访问热点急速变化是一个纯粹的读方面问题，是LSM-Tree的弱项，因此要对整个读方法进行优化。X-Engine对数据块、索引、缓存进行了一系列定制化的设计，来保证读效率。对于LSM-Tree中影响非常大的compaction操作，X-Engine还涉及了增量缓存替换来减少其对缓存命中的影响。Extent和其他LST-Tree的SSD，sorted string table非常像。在电子商务中存在非常多更新DDL的操作，比如最常见的新增栏位。在有了schema中的这个version之后，我们可以在新增栏位的时候不需要去修改每个老extent中的schemadata block 以行的形式存储数据schema中存储的是各个column的属性index存储每个data block的偏移量scheme带有一个version，为了在更新scheme的时候不需要修改旧数据。每个extent 2MB，每个Data block约16KBextent越小，就越容易调度和复用，但管理成本也会随之上升。设为2MB，是综合考虑compation中数据复用和后面要介绍的增量缓存替换的最优解。X-Engine的每层都划分了固定大小的Extent，存放每层数据中连续片段（key Range）。为了快速定位extent，为每层建立了一套索引（Meta Index），所有这些索引加上所有的memory table（active immutable）一起组成了一个元数据树，root节点为metadata snapshotX-engine中除了当前正在写入的active memtable以外，其他结构都是只读的，不会被修改。X-engine的所有读都是以快照的结构为入口。优化缓存X-Engine针对但记录查询引入了Row Cache，在所有持久化的层次数据上做了一个缓存，在memory table中没有命中的单行查询，在Row cache中也会被捕获。Row Cache负责单行查询，Block Cache负责 Row cache的漏网之鱼。俩类缓存在不同的维度上面劲量的去保证数据的命中率，着重保证单行数据查询。多版本元数据索引X-Engine的meta Index用来记录LSM tree结构的变化。LSM-Tree中每个subtable都有meta index，它起始于一个根节点，并在每次更新后创建新的节点。如右图L0的extenti，它对应meta snapshotv，当extenti被compaction之后创建新的meta snapshotv+1，snapshotv+1只需要指向原来的extenti而不需要产生实际的I/O（extenti的位置也不需要改变），同时cache的数据也不需要变化。同时这样的copy on write结构使得事务可以访问任意版本的数据，避免了锁的开销。X-Engine中有GC机制类清理过期的snapshot。增量缓存替换X-Engine的增量缓存替换降低compaction对cache的影响。原LSM-Tree中，当SST发生compactio时，对应内存中的数据会失效，造成突然的Cache missX-engine的增量缓存替换会在compaction的时候检查一个block是否被cache，如果是，则将chache中的内容替换为merge之后的block，可以有效缓解compaction 对cache命中率带来的影响。多版本memtableX-Engine对memtable做了一点简单的优化。未优化时，如果多次更新同一个key，那么这个key会以<key,sequence_number>的形式存在memtable中存储，如果要查找最新的key，可能需要读取多个旧值。X-Engine中所有内存数据结构是一个跳表并且不进行整理，因此多次频繁更新同一个key的话，那么这个key会存在多个版本的数据，虽然查找内存中的数据是非常快的，但是存在多个版本数据的时候我们可能依旧要的是读到多个旧值。X-Engine的multi-version memtable会将同一个key的不同版本单独链起来。这样skiplist中就不会出现一个key的多个版本，同一个key的多个版本按照旧组织排序，最新的key可以被最快访问到。总结范围查找X-engine是要弱于Innodb的，所以选用LSM-Tree放弃范围查找的部分性能是X-Engine的可以接受的折中方案。
Python的 GIL背景一个进程可以有好几个线程。这些线程会共享进程内的所有内存。比如可以读写同样的变量。当一个进程有不止一个线程的时候，就会出现一个情况叫racing。因为这些进程中的线程有可能同时运行，也有可能交替运行。但不管是同时运行还是交替运行，都没有办法控制他们之间的相对顺序。a=1 if a>0: a-=1当俩个程序同时执行如上代码时就会出现a被减了俩次。发生了多线程竞争问题。python使用的垃圾收集算法是引用计数。如果一个进程有多个线程在运行的话就会有竞争问题。因为--操作实际上是进行了多个步骤。就可能会使得计数错误。就没有办法保证每一个python对象被正确释放了。最常用的解决方法是加锁，每次只有一个人能进入代码块运行。因此当时python的设计者们决定给python设计一个全局锁，也就是GIL。python通过这种机制保证每个程序在执行的时候都是拿到线程锁的。也就是说没有bytecode能被其他线程打断，因此在bytecode中运行的每一个c程序都是线程安全的。这种全局锁的好处是，简单，避免死锁，对于多线程的程序或者是没法并行多线程的程序这种全局锁的性能是非常优秀的。因为要锁是非常花时间的场景，全局锁就保证了每一个字节码运行的时候，最多只要一次锁，如果每个object都要访问很多的锁，那可能需要很多的拿锁时间。这也让给python写c扩展的时候方便了很多，因为可以确定在每一个bytecode运行的时候，没有线程竞争冒险的问题。这样在c代码中修改python对象的时候就不用管乱七八糟的锁。许多人尝试从python中拿走GIL，但是每次尝试都失败了，特别是先前兼容的问题。其实python比java还要大。是上个世纪90年代初的语言（早java4年）当时多核根本不存在，多线程存在的意义就是这些线程可以轮流执行，不会因为某个线程计算量过大，而卡住其他的事情。但是在21世纪后多核已经是电脑的标配。因此现在GIL出现了严重的水土不服。python的多线程在这种情况下没有办法利用多核来增加自己的计算速度。 python如今使用多进程来解决这个问题。虽然一个进程没有办法用多个cpu，但是我可以有很多个进程。from multiprocessing import Pool def f(x): return x * x if __name__ == '__main__': with Pool(5) as p: print(p.map(f, [1, 2, 3]))或者写c程序 在c语言中做多线程。当然理论上也可以尝试用一些python中没有GIL的解释器。jython就是没有GIL的。线程竞争import threading counter = 0 # 全局变量 def increment(): global counter for _ in range(100000): temp = counter # 1. 读取当前值 temp = temp + 1 # 2. 增加值 counter = temp # 3. 写回 # 创建两个线程 t1 = threading.Thread(target=increment) t2 = threading.Thread(target=increment) t1.start() t2.start() t1.join() t2.join() print(f"Final counter: {counter}") # 期望是 200000，但实际通常小于 200000在python3.11中运行代码，上述代码的值就是200w，但是如果用3.9来运行这个程序就会发现这个值不是200w，竞争冒险就出现了线程A执行完temp=counter后可能释放GIL（比如时间片到了）线程B获得GIL，执行完整的counter+=1线程 A 重新获得 GIL，继续从之前的状态执行，覆盖了线程 B 的更新GIL 只保证字节码执行的原子性，但不保证多行字节码操作的原子性！但是当前这个情况是一个行为而不是一个特性，所以任何人都不应该这样写代码。在未来的python版本中随时有可能会变化。要安全地访问共享的全局变量，你需要显式地使用线程锁：import threading counter = 0 lock = threading.Lock() # 创建一个同步锁 def increment(): global counter for _ in range(100000): with lock: # 获取锁 counter += 1 # 离开 with 块时自动释放锁 # 或者显式地： def increment_explicit(): global counter for _ in range(100000): lock.acquire() # 获取锁 try: counter += 1 finally: lock.release() # 确保释放锁同时执行一条字节码≠现成安全那么python的GIL真的一无是处吗？对于在执行过程中频繁需要等待的IO密集型任务，多线程还是有一定优势的。当线程碰到IO操作时，线程会自动释放锁，让其任务函数执行，避免CPU资源浪费。IO密集型任务就是要进行频繁等待的任务。如果是CPU密集型任务建议使用多进程，因为多进程不受全局解释器锁的影响。python未来python3.14取消了GIL，但是者是一个可选的特性。可以通过 --disable-gil编译标志来构建无GIL的Python解释器，新增了 sys.set_gil_enabled()函数，可以在运行时动态控制GIL的启用状态4核CPU上性能可达5倍提升，在CPU密集型任务中执行时间从27秒缩短到9秒。但是单线程性能有5-10%的损失，增加了百分之15%的内存占用。在自由线程模式下，Python 不再有 GIL 这把“万能大锁”来简单粗暴地保证线程安全，而是需要为许多内置对象（如字典、列表）引入更精细化的锁机制。每次访问或修改这些对象时，解释器都可能需要执行获取锁、检查锁状态、释放锁等操作。这些操作在GIL模式下是不需要的，因此带来了额外的开销python的最终走向应该是最终只会出现一个no-gil的build，不会出现永久的俩个build。cpython给出的时间是五年以上。FastAPIGIL 的存在意味着在单进程内，Python 解释器任一时刻只能执行一个线程的字节码。这对于 CPU 密集型（计算密集型）任务是一个瓶颈。FastAPI 通过以下策略应对：FASTAPI构建在Starlette之上，Starlette是一个高性能的ASGI框架通过异步处理，fastAPI可以在I/O操作（如数据库查询、外部API请求等）等待期间释放线程，从而处理其他请求，从而提高并发和响应速度。uvicorn 和fastapi是什么关系Uvicorn 和 FastAPI 是两个独立但高度互补的 Python 工具，它们的关系可以概括为：FastAPI 是一个基于 ASGI 标准构建的 Web 框架，而 Uvicorn 是一个支持 ASGI 的 ASGI 服务器，用于运行 FastAPI 应用。Async IOasync本质上还是python运行的单线程单进程程序，核心中是eventloop，在python中能执行的任务只有一个，不存在系统级的上下文切换。因此它有个好处是不存在竞争冒险的问题。import asyncio async def main(): print("hello") await asyncio.sleep(1) print("world") coro=main() asyncio.run(coro)corouting:这个async def main就是一个corouting function，所有async开头的都是corouting函数，一般函数调用的时候返回的是这个函数的返回值。而corouting函数调用的时候，返回的是corouting的object。为了运行我们要进行俩个事情。一个就是进入async模式。正常python的代码是在同步模式下运行的。eventloop建立之后就会去找哪个任务可以执行import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): print(f"started at {time.strftime('%X')}") await say_after(1, 'hello') await say_after(2, 'world') print(f"finished at {time.strftime('%X')}") asyncio.run(main())asyncio.sleep返回的也是corouting。这个corouting被包装成了task并且告诉eventloop。并且会告诉eventloop当前task无法执行了，可以先执行其他的task。这里会在一秒后输出hello 再一秒后输出world。await关键字必须在async定义的异步函数中使用所有控制器的返回都是显示的。eventloop没办法主动拿回控制权，必须task主动交出控制器。俩中交回方式：运行await会交回，函数运行完毕后会交回。如果一个task里面有死循环，那么整个eventloop就卡死了第二个world需要等第一个hello执行完毕了之后才能继续执行。为了这个问题，asyncio给我们提供了create_task函数。import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): task1 = asyncio.create_task( say_after(1, 'hello') ) task2 = asyncio.create_task( say_after(2, 'world') ) print(f"started at {time.strftime('%X')}") await task1 await task2 print(f"finished at {time.strftime('%X')}") # 运行主函数 if __name__ == "__main__": asyncio.run(main())其中create_task，这个函数的参数是一个corouting，它不会运行任何corouting中的代码。create_task会把这个corouting注册到异步编程里面。但是现在eventloop还没有办法执行这个task。因为task还在main的手里。当await后面是一个task而不是corouting的时候它就省略了把corouting变成task的步骤。因此async很适合解决网络通讯的问题，因为网络通讯很多时间是在等待上的，其他任务就可以干活。但是如果有10个task呢？我们可以使用一个叫asyncio.gather的方法。最终执行哪个部分永远是由eventloop来决定的，task没办法告诉eventloop去执行等待的某个特定的task。如果代码中并没有等待这件事情的话，协程对程序加速是没有帮助的。拿到corouting的返回值要用await。awaiteventloop在调度的时候最小单位是task。eventloop没办法执行一个coroutingimport asyncio async def main(): await asyncio.create_task( asyncio.sleep(0.1) ) asyncio.run(main())asyncio.run会建立一个task。代码中顶层的task是main
音频学习基础DAC传统的 的音频生成模型在压缩音频信号时存在以下问题音频伪影高频细节丢失模型通常针对特定音频类型，缺乏设计通用性https://github.com/descriptinc/descript-audio-codecDAC方案用AI对音频进行高倍率的压缩和解压缩，同时尽可能保证出色的音质。它可以将44.1kHz的音频压缩到8 kbps的极低比特率。这相当于约90倍的压缩率，意味着文件体积会变得非常小。VQ-VAE的目标是学习一个离散的潜空间。核心是编码器解码器结构，中间加入了向量量化步骤。传统VQ-VAE存在码本坍塌问题（部分码本未被使用）导致有效比特率降低论文中提出讲编码向量投影到低维空间（如8维）进行码本查找，再映射回高维。将欧氏距离转化为余弦相似度，提升训练稳定性。Diffusion TransformerDiffusion里面的骨干网络（网络架构），几乎用的都是U-net。也就是用U-Net去预测噪声。以往大多数论文都是研究如何改进diffusion本身的数学理论，很少关注它的骨干网络。那么能不能把这个架构换成Transformer呢？DiT模型基于潜空间扩散模型（latent diffusion LDM）框架，将图像编码为潜表示后，使用Transformer对其进行处理。音频中的常见操作STFTFT也就是傅里叶变换可以将信号从时域转换到频域。告诉我们这个信号是由哪些频率组成的。短时傅里叶变换。STFT通过对音频分段加窗 并对每段做Fourier Transformer，你会获得时间-频率（time-frequency）分布。现实中的信号很多都是非平稳的，它们的频率成分会随着时间的变换。比如一首曲子现有钢琴声，后由鼓声，频率分成在变化。既然整个信号不稳定，就假设信号在一小段时间内是稳定的。选择一个很短的信号，如汉宁窗，只分析窗内的这一段信号对窗内的这一小段信号进行傅里叶变换，得到这一小段时间内的频率分布。这相当于得到了信号开头时刻的“局部频谱”。将窗函数向右滑动一小段距离（这个距离称为步长或跳数）。重新对窗内信号进行傅里叶变换，得到下一个时间点的局部频谱图。最终，你会得到一系列按时间顺序排列的“局部频谱”。将这些频谱并排起来，就能同时看到频率如何随时间变化了。傅里叶变换和STFT本身是可逆的数学工具，它们不会丢失信息。（不可变通常发生在我们对变换后的数据特别是丢弃相位进行人为的修改后）Mel-SpectrogramSpectrogram ：从STFT的结果中获取幅度或功率得到可视化举证。普通频谱图 就像用一把均匀的尺子去测量声音，客观但不符合听觉。梅尔频谱图 就像用一把“人耳尺”去测量，这把尺子的刻度在低端很密，在高端很疏，测量结果更符合我们实际听到的感觉。人耳在低频（<1kHz）更敏感，在高频区的感知分辨率更低。Mel 频率尺度模拟了这种心理声学特性。对音频做 STFT → 得到 magnitude/power spectrum将频率轴通过 Mel filter-bank（Mel 滤波器组）进行加权得到 Mel 频率谱通常再做 log：log-Mel spectrogram在深度学习中梅尔频谱比普通频谱更常用与人耳特性更接近压缩维度减少模型负担更平滑，更适合 CNN、Transformer 等模型处理无论是普通频谱（通常指STFT的幅度谱）还是梅尔频谱，它们在生成过程中都主动丢弃了关键信息，这使得完美的逆向还原在数学上变得不可能。DCTDCT的核心思想是将一组信号（比如一段声音，一块图像像素）分解成一系列不同频率的余弦函数组合。它特别擅长将能量集中到少数几个系数上，这个特性让它称为视频（如JPEG，MPEG）领域的基石想象你在一个嘈杂的鸡尾酒会上，能听到各种声音的总和：人们的谈话声、杯碟碰撞声、以及背景音乐声。你的耳朵听到的：是一个复杂的、混合在一起的时域信号。这就像一张图片在空间域的表示，你看到的是千千万万个不同颜色和亮度的像素点混杂在一起。DCT所做的工作：就像一个超级敏锐的耳朵，它能把你听到的混合声音分解开来，并告诉你：“这个总的声音里，包含10%的100Hz低音（贝斯声）、5%的1000Hz中音（人声）、1%的10000Hz高音（杯碟碰撞声）……”在DCT之后，信号被表示成了不同频率成分的强度（系数）。这就是频域表示。对于自然图像，大部分能量都集中在其中低频的部分低频分量代表了图像大致的轮廓和缓变的色块。比如一个人脸的模糊轮廓这非常重要，包含了图像中的主要信息高频分量：代表了图像的细节和边缘。比如人脸这周头发丝和锐利的边界。这部分通常细节丰富，但人眼对其相对不敏感，且能量较弱。自然界的音频比如音乐，人声有个特点：其主要能量通常集中在少数几个频率上（通常是低频部分），而其他大量的频率 （系数很小或为0）DCT也是有损的压缩。MFCCMel-frequency cepstral coefficients 梅尔倒谱系数。计算流程STFTMel-Spectrogramlog压缩对每帧做DCT（离散余弦变换）->得到MFCC系数（用车前13维最常用）使用DCT将Mel频谱压缩成低维（类似PCA) 去掉相关性，更适合传统机器学习。但DCT去除了细节，对生成任务不利。HIFI-GANc5d736809766d46260d816d8dbc9eb44-Paper.pdfGAN有俩部分结构，一个是生成器一个是判别器。生成器生成器接受一个梅尔谱，输出是原始波形。把梅尔谱送进网络后，首先送进一个卷积核大小为7x1的卷积层。图中的h是通道数，输出结果送给keaky ReLU。然后送给一个转置卷积，这个转置卷积将会增强输入数据的长度 同时减小通道数。然后送给一个MRF模块，也就是多感受野融合模块。红色虚线框起来的部分，一共重复执行k次。因此这里的卷积核大小是根据不同的迭代次而决定的。然后输出过keaky RELU，然后卷积将通道数转换为1。然后使用Tanh激活，得到的输出结果就是原始波形。这张图展示了多感受野融合模块的展开。中间的部分就是一堆残差模块堆叠。这里每一个模块的感受野并不相同。右边是残差模块的进一步细分。右边部分红框有俩个循环。一层外一层内。一个残差模块内部执行一个循环。先leaky Relu之后送给一个卷积层，卷积核大小为k列表的第n个，单个残差模块内部的核大小相同。膨胀值也是一个列表。其实内循环就是一个膨胀卷积加一个普通卷积。不同的模型尺度设置的是不一样的。于是就得到了单个擦差模块的执行结果，然后继续执行其他残差模块，直到外层循环完成，然后得到输出结果。判别器多周期判别器（MPD）：存在多个不同划分周期的判别器。周期设定为2，3，5，7，11。我们首相将程度T的1D原始音频重塑为高度T/p和宽度p的2D数据，然后对他使用二维卷积。卷积核宽度轴设定为1。这是一个周期为3的模型判别图，将原始音频做成一个二维矩阵。程度为T/3宽度为3。卷积的时候，按照长度进行卷积，宽度设定为1，作者认为这种方式更能捕捉到音频的周期模式。作者设定了5个周期的判别器，让他们一起判别输入的原始音频多尺度判别器MSD：三个鉴别器（原始音频、x2平均池化音频、x4平均池化音频）多周期判别器卷积的时候是跳步卷积的，缺乏对连续时刻的处理，所以论文引入了多尺度判别器。论文这里就引入了三个判别器。第一个接受原生音频，第二个接受2单位平均池化的音频，第三个接受4单位平均池化的音频。三个判别器分别对他们进行判断。归一化是深度学习中的一项关键技术，其主要目的是通过调整数据的尺度或分布，提升模型训练的效率和稳定性。谱归一化和权重归一化是深度学习中俩个重要的参数归一化技术，通过不同的方式约束神经网络权重的范数。谱归一化：通常使用幂迭代法近似计算最大奇异值，计算成本相对较高。权重归一化：直接计算权重向量的L2范数，计算效率高。第一个判别器使用谱归一化；第二第三个判别器使用权重归一化。到目前位置我们一共有8个判别器，5个周期判别器，3个多尺度判别器。总而言之都是为了提高判别器的判别能力。让GAN提高生成质量。这是一个周期为p的周期判别器结构，上面是填充和reshape成周期形状。然后使用卷积核大小为5x1的判别结构。接着进行leakyrelu激活，这俩个步骤一共重复进行四次。之后再卷积再激活在卷积得到判断结果。这就是一个周期判别器的结构。损失函数接下来是损失函数第一个损失为对抗损失，本篇论文使用LSGAN的损失：$$ \begin{aligned} & \mathcal{L}_{Adv}(D;G)=\mathbb{E}_{(x,s)}\left[(D(x)-1)^2+(D(G(s)))^2\right] \\ & \mathcal{L}_{Adv}(G;D)=\mathbb{E}_s\left[(D(G(s))-1)^2\right] \end{aligned} $$这里的dx是对真实原始波形进行判断。$(D(x)-1)^2$就是当样本为真时，判别器概率越接近1越好。后面的一项是输入样本为假时的，也就是越接近0越好。生成网络则相反，也就是希望自己伪造的样本，判别网络判断越接近1越好。其中x表示真实音频，s表示输入条件，即真实音频的梅尔谱图。除了对抗损失作者还添加了梅尔谱损失。$$ \mathcal{L}_{Mel}(G)=\mathbb{E}_{(x,s)}\left[||\phi(x)-\phi(G(s))||_1\right] $$其实就是希望生成网络生成的原始波形，和原始波形的梅尔谱差距越小越好。我们都知道梅尔谱的特性，所以这个损失，就是希望生成网络可以生成高感知质量的音频。这就是梅尔谱重建损失。这里的$\phi$是将原始波形转化为梅尔谱的函数。这里的x是输入的原始波形。G(s)是还原回的原始波形。此外，还有一个特征匹配损失$$ \mathcal{L}_{FM}(G;D)=\mathbb{E}_{(x,s)}\left[\sum_{i=1}^T\frac{1}{N_i}||D^i(x)-D^i(G(s))||_1\right] $$希望各个层的特征具有相似性（只优化生成网络），是希望真实音频和伪造音频提取的特征也能越相似越好。这里的T是判别器中的层数，$D^i$和$N_i$分别表示判别器i-th层的特征和特征数量。$$ \begin{aligned} & \mathcal{L}_{G}=\mathcal{L}_{Adv}(G;D)+\lambda_{fm}\mathcal{L}_{FM}(G;D)+\lambda_{mel}\mathcal{L}_{Mel}(G) \\ & \mathcal{L}_{D}=\mathcal{L}_{Adv}(D;G) \end{aligned} $$$\lambda_{fm}$和$\lambda_{mel}$分别是2和45。语音合成俩阶段，比如使用Tacotron2从文本中生成梅尔谱。把得到的梅尔谱送给生成网络， 得到原始波形实验不语音合成。首先使用Tacotron2，可以将文字转成梅尔谱，把得到的梅尔谱送给生成网络，从而实现语音合成。CycleGAN[1703.10593] Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks论文中给出的模型的常用领域有把莫奈的化和真实图片相互转换。普通的马和斑马，夏天和冬天的风景照 ,根据线条具象化物体等等GAN的基本流程为假设有训练数据服从$P_{data}$分布，从训练数据中采样一张图像，送给判别网络D，让其判别真伪。与此同时从标准正态分布中采样一组数据，然后发给判别器，并把这个数据送给生成网络G，生成网络生成一张图像，假设服从概率分布$P_g$然后将伪造的图像送给判别网络。判别真伪，当模型收敛$P_{data}$和$P_g$分布是相同的，也就是说判别器有百分之50概率判别为真，50%概率判别为假。此时网络生成的图像相当于从$P_{data}$中生成出来的。要准备俩份训练数据，（不需要一一配对）左边是意义配对的图像数据，右边则没有配对。传统的图像域转换是需要有配对的关系的，否则无法训练，而cycleGAN不需要。要构造俩个生成网络跟判别网络，用于俩中风格化图像相互转换；生成网络记为G和F，判别器网络记为$D_x,D_y$。假设我们有俩分数据记为X（夏天）Y（冬天），并且服从概率分布记为$P_{data}(x),P_{data}$从训练数据中采样冬天的图像送给判别器，判断真伪。接着从夏天的图像中采样X送给生成网络G（注意这里不再是噪音了）。生成网络将其转化为服从概率分布$P_g(Y)$的图像，然后送给判别网络判断真伪。这个是由夏天转换为冬天的，还有从冬天转化为夏天的。 这来个相互转换流程，其实相当于俩个GAN模型，把夏天的图像X，送给生成网络G，得到冬天图像Y，让判别网络$D_Y$判断真伪。同时，也把冬天的图像Y送给生成网络F得到夏天的图像X，送给判别网络$D_X$判别真伪$$ \mathcal{L}_{GAN}(G,D_{y},X,Y)=\min_{G}\max_{D_{y}}\mathbb{E}_{y\sim P_{data}(Y)}\left[\log D_{y}(y)\right]+\mathbb{E}_{x\sim P_{data}(X)}\left[\log(1-D_{y}(G(x))\right]\quad(5)\\ \mathcal{L}_{GAN}(F,D_{x},Y,X)=\min_{F}\max_{D_{x}}\mathbb{E}_{x\sim P_{data}(X)}\left[\log D_{x}(x)\right]+\mathbb{E}_{y\sim P_{data}(Y)}\left[\log(1-D_{x}(F(y))\right]\quad(6) $$第一个式子是夏天转冬天，第二个是冬天转夏天。在CycleGAN中仅仅依靠这来个损失函数是不能实现季节的相互转化。因为GAN模型中当模型收敛，$P_{data}$和$P_{g}$的概率分布相等。但是夏天的图像有很多，为什么要和左边的图像长得像呢？比如图像变成了右边这样，这也是一种夏天的图像。此时转换后的图像已经不是简单的季节变换了，而是整体都变了。为了解决这个问题，作者提出了第三个损失函数。循环一致性损（Cycle Consistency Loss）当我们把x通过生成网络G转化为Y图像的时候，我们希望这张图像可以经过生成网络F可以转化回来。这将限制G在生成Y的时候保留结构信息，否则生成网络F将很难还原回来。右边部分是将Y转换成X然后还原的流程图。因此，我们就得到了下面的循环一致性损失。把转换为Y的X还原为X的时候，还原的值越接近越好。$$ \begin{aligned} & \mathcal{L}_{GAN}(G,D_{y},X,Y)=\min_{G}\max_{D_{y}}\mathbb{E}_{y\sim P_{\mathrm{dan}}(Y)}\left[\log D_{y}(y)\right]+\mathbb{E}_{x\sim P_{\mathrm{dan}}(X)}\left[\log(1-D_{y}(G(x)))\right]\quad(5) \\ & \mathcal{L}_{GAN}(F,D_{x},Y,X)=\min_{F}\max_{D_{x}}\mathbb{E}_{x\sim P_{\mathrm{dan}}(X)}\left[\log D_{x}(x)\right]+\mathbb{E}_{y\sim P_{\mathrm{dan}}(Y)}\left[\log(1-D_{x}(F(y)))\right]\quad(6) \\ & \mathcal{L}_{Cycle}(G,F)=\mathbb{E}_{x\sim P_{\mathrm{dan}}(X)}\left[\left[\left|F(G(x))-x\right|\right|_{1}\right]+\mathbb{E}_{y\sim P_{\mathrm{dan}}(Y)}\left[\left|\left|G(F(y))-y\right|\right|_{1}\right]\quad(7) \\ & \text{综合(5)、(6)、(7)可得最终损失函数} \\ & \mathcal{L}(G,F,D_{x},D_{y})=\mathcal{L}_{GAN}(G,D_{y},X,Y)+\mathcal{L}_{GAN}(F,D_{x},Y,X)+\lambda\mathcal{L}_{Gycle}(G,F)\quad(8) \\ & \text{其中}\lambda\text{是一个超参数,用于调节重要度的} \end{aligned} $$作者还提到了另一个损失函数，但这个损失是非必要的，作者在论文中提到，当模型做绘画转图像的任务时，加入这个鉴别损失能够有效提高转换质量。$$ \mathcal{L}_{\mathrm{identity}}(G,F)=\mathbb{E}_{y\sim P_{data}(Y)}\left[||G(y)-y||_1\right]+\mathbb{E}_{x\sim P_{data}(X)}\left[||F(x)-x||_1\right]\quad(9) $$这个损失函数是用来保留图像的颜色信息的，如果没有这个损失函数，生成器G和F可以在不需要的时候自由修改图像的色彩我。比如画像的白天转化为图片后，会变成黄昏。中间是不加这个损失函数，右侧是添加这个损失函数。
diffwave基础是语音合成任务使用的模型。原始音频通常是通过声波表达的。在语音中有三个最基本的要素，第一个是pitch（音高或音调）：frequency，它是通过波形图的频率来反映的，频率越高，音调越高我们可以观察钢琴和提琴的震动频率，在同样的时间点中钢琴震动了5次提琴震动了更多。如果我们在时间轴上进行平移我，钢琴的音调不会发生任何的变化timbre：shape是用波形的形状来表示的。这三个不同的语音波形的形状是不一样的。钢琴的波形随着时间的推移，虽然范围会发生变化，但是形状是几乎不变的。响度，loundness：range。这是通过波形的范围来表示的，范围越大响度越大，随着时间的推移，钢琴的范围是在不断的减小的，因此其响度在不断的下降。图像往往是通过三个维度来表示，对于大部分图像任务，图像的维度都是固定的，网络结构只需要做一种维度通道x宽x高而语音通常只有一个维度就是长度。声波的维度是变化的，这是因为声波每秒采用的频率是固定的，但是采样的长度是不固定的。有的时候想生成5秒的语音，有的时候想生成10秒的语音。对于一个256x256的图像下采样到128x128，其实是可以保留绝大部分信息的。只不过是分辨率稍微低了一点，模糊了一点。但是语音降采样会严重丢失信息。会从非常清晰的语音变成非常老式的电话。图像对于高频的噪音和小的缺陷并不致命，但是对于语音来说非常致命。比如数秒钟的语音出现了噪音，人耳听起来非常敏感，因此语音做这些任务会困难一些。一个22.05k时间步每秒的音频，持续九秒就等于（RGB）3x256x256 的图像的维度生成的维度是不固定的，人很难通过肉眼去理解波形图。语音需要非常高的质量。已经有的模型我们可以将任务分成几类。分类的语句是条件信息富足的程度。从富足到缺少条件信息分别可以分为Neural vocoding：声码器梅尔频谱本身并不能直接还原原始音频，因为会经过一些不可逆操作。因此单靠算法无法精确反演原始波形。声码器本质上是在做，将声学特征（mel）->近似真实的波形（audio waveform）这个映射不是传统的DSP的反向变换，而是学习出来的生成模型。波形中每个点的精确值（振幅）由频率和相位共同决定。梅尔频谱主要保留了频率和能量信息，但相位信息大量丢失。声码器的核心挑战之一就是如何高质量地估算出与梅尔频谱对应的、听起来自然的相位。拙劣的相位重建会导致语音听起来有杂音、金属感或嗡嗡声。这是条件信息非常强的任务，使用的是梅尔谱。梅尔谱是通过原始音频的STFFT得到的。包含了原始音频的绝大部分信息，声码器就是将这样的谱转化为音频，他是TTS系统里重要的组成部分。（另外一半就是通过文本生成这样的梅尔谱）在声码器领域有很多的工作。我们可以粗浅的分类为几个模型自回归模型比如wavenet就是一个维度一个维度的生成，这类模型质量非常好，但是生成的速度非常慢。是慢于实时的，生成1秒钟的音频需要5s的时长。实际应用非常困难。第二类模型是基于flow标准化流的模型。是通过likelihood训练一个可逆的神经网络。这类模型运行速度非常快，远远超过实时。但是缺点就是因为要让网络结构可逆需要的参数量会多很多，才能保证网络结构的容量VAE和GAN这俩个在图像领域流行的工作，稍微比上俩个模型要差一点点。Class-conditional和unconditional无条件生成：模型“自由发挥”，学习训练数据（如各种音乐、语音、环境声音）的整体分布，然后生成一个“典型”的、符合该分布但全新的音频样本。你无法控制它会生成什么具体内容。有条件生成：模型“按需创作”，生成过程受到一个或多个条件的约束。这个条件就像是一个指令或蓝图，告诉模型你希望生成什么样的音频。你可以控制生成结果的关键属性。条件上一个离散的类别标签然后进行生成，是条件非常弱的生成模型。unconditional：完全无监督的生成wave net也可以实现这个工作。wavenet可以生成听起来像单词的语音，但是质量会差一些。我们能不能设计一个普适的模型能够在刚才提到的不同的任务里都能达到非常好的效果。为了这个目标，提出了一个模型叫做diffusion probabilistic model这类模型不是自回归的，所以他的生成速度比wavenet要快很多。它的网络结构非常灵活，不像基于流的模型，需要维护一个可逆的网络。因此这类模型能实现更小的模型体积。这个模型只涉及训练单个网络。而GAN和VAE都需要训练俩个网络。训练俩个网络的时候，需要平衡俩个网络的训练进展。在降噪扩散概率模型方面 目前没有这类的问题。首先由x0这个数据分布。然后往干净的数据里加噪音（从右向左）,经过T步之后。Diffusion process q：add Gausian noise这个扩散模型是马尔科夫链，是固定的不科学系的，只有超参数，没有神经网络。Reverse process $p_\theta$: eliminate Gaussian noise.如果我们能把每一步加的噪音都准确找出来参数化的马尔科夫链是一个马尔科夫链，其状态转移概率不是固定不变的，我们可以从$X_t$中采样处随机噪音然后经过逆过程，就能最终得到很干净的数据。这些参数值发生变化时，整个马尔科夫链的动态行为也会随之改变。我们通过变分推断来进行训练，目标函数就是ELBO Evidence Lower Bound：变分下届$$ \mathbb{E}\parallel\epsilon-\epsilon_\theta\left(\sqrt{\bar{\alpha}_t}x_0+\sqrt{1-\bar{\alpha}_t}\epsilon,t\right)\parallel^2\quad\Leftrightarrow\quad\text{Denoising score matching} $$这里的$\epsilon$是一个随机噪音。$\epsilon_\theta$是一个神经网络，是用来预测这个随机噪音的。采样：$$ x_{t-1}=\mu_\theta(x_t,t)+\sigma_t\epsilon\quad\Leftrightarrow\quad\text{Langevin dynamics} $$Diffwavea diffusion probabilistic model for waveform synthesisa versatile model for various audio synthesis tasksa fast sampling algorithma compact neural architecture实现很小的模型体积，高质量，以及五倍于实时的生成。在基于类别和无条件生成中，Diffwave远远强于baseline。快速采样方法：标准的采样方法是要把很长的逆过程采样一遍，也就是神经网络要计算T次，这个T可能是200-1000.那能不能用非常短的逆过程来近似这个很长的逆过程呢？如果我们使用S步，这个S远远小于T。我们只需要在这个短的逆过程生成就可以了。我们可以把这个过程对齐起来。中间的怎么对齐呢？我们通过每个噪音的噪音水平来对其，这个噪音水平可以通过超参数计算出来，只需要做一个线性的差值来计算。这并不需要重新训练整个模型。只需要选取非常少的步数，比如6步就能实现高质量的语音的生成。理论上加速了三十多倍。网络结构在wavenet基础上进行了改进，使用了双向的dilated convolution把每个conditioner添加到Convolution layer之后。声码器重将梅尔谱进行上采样，通过几个conv加进去对于基于类别标签的生成任务，训练一个embedding向量 128维度，把它加进去。和网络一起训练。网络在不同的diffusion step t是不一样的但是我们不可能训练T个神经网络，所以我们要把步数t加到神经网络中去，让网络知道当前在第几步。首先将整数t编码为向量（有一套规则通过sin和cos计算的方式），然后通过几层全连接层加到每一个卷积层。就把diffusion step加到了网络中去。
Hubert笔记基础不同于语言已经是离散化的符号表示，语言是连续的波形大预言模型是自回归模式的，需要一个字一个字的输入，所以想让大预言模型认识语音，就需要将连续的语音表示为离散化的单元。那么语音中的单元是什么呢？怎么样的单元才是最好的呢？最重要的是学习合适的表示形式。一句话中有许多声音的单元语音中没有统一的词典语音单元没有显示的分割标志这篇论文给出的解决方案是：自监督的方式，让模型自己学习到语音的离散表示如果用文本类比的话，我们希望输入的文本就是输出的文本希望这个模型通过上下文预测出中间三个A. MFCC（梅尔频率倒谱系数）是什么？ MFCC 是一种能够很好地反映人耳听觉特性的音频特征。人耳对低频声音的变化更敏感，对高频声音的变化不那么敏感。MFCC 通过梅尔滤波器组来模拟这一特性。为什么用它？ 相比于原始的波形数据，MFCC 更能捕捉声音的“音色”或“质感”信息。例如，不同的人说同一个元音“a”，其音高（基频）可能不同，但他们的 MFCC 特征在整体形状上会非常相似。如何得到？ 对一小段音频（例如 20-40 毫秒的一“帧”）进行计算，会得到一个 MFCC 特征向量（通常包含 12-13 个系数）。一段完整的音频（如几秒钟）会被切分成成百上千个这样的帧，因此会得到一个 MFCC 特征向量的序列。总结：一段音频 → 一个 MFCC 向量序列（一个二维矩阵，行是帧，列是MFCC系数）。梅尔频谱是一个二维图像（时间 vs. 梅尔频率），它保留了非常丰富的信息，包括我们希望用于识别的音色信息，也包含了一些我们想过滤掉的细节（如个体发音的细微差异）。MFCC 是对梅尔频谱的进一步压缩和提炼。它通过离散余弦变换（DCT） 这个工具，从每一帧的梅尔频谱中提取出最能代表“声道形状”的倒谱系数。先通过CNN Encoder，先将连续的语音离散化（得到离散的向量）通过BERT结构，也就是双向transformer结构和Mask，学习更好的离散表示（如图遮蔽其中的x2，x3，x4） 计算输出的z2，z3，z4和原来的差距算loss网络学习的目标是什么？通过k-mean聚类的MFCC，还有第一轮迭代后的中间表示算法细节CNN-Encoder实现7个1d的卷积层，步长 [5,2,2,2,2,2,2] kernel-size[10,3,3,3,3,2,2]通道数为512最终的结果是1秒语音有16000个采样点，通过这个encoder之后，转换成50个特征，也就是50个向量，每个帧代表20ms的语音内容。这里经过了5个卷积层，以第一个卷积层为例，批大小为1，通道数为512，kernel-size为10，步长为5。经过第一个卷积层后由原来的[1,1,16000]变成[1,512,3200]如何mask？随机8%的比例选择开始位置，然后把连续的10个输入mask。网络loss如何计算？mask位置的loss与非mask位置的loss按一定比例哦混合，即在上图中x1和z1算的就是非mask部分，x2和z2就是mask部分。计算输出向量与目标向量的余弦相似度39维MFCC特征是在计算效率、信息完整性和模型性能之间取得的一个经典平衡点。它并非一个绝对的标准，而是一个在语音识别研究中被广泛验证且行之有效的经验值。MFCC最核心的部分是12维的MFCC系数。这12洗漱捕捉了语音信号的短时功率的网络形状，包络包含了绝大部分与发音内容相关的信息。原始的12维MFCC是静态特征，只描述了1帧内的频谱特性。但语音是动态的，发音时舌头、嘴唇等器官的运动会导致频谱随时间变化。这种变化率（速度）至关重要。一阶差分就是来描述这种变化的，它计算的是相邻帧之间的MFCC系数的差值。为了捕捉更精准的信息，比如加速度（变化本身的变化）我们引入二阶差分。二阶差分计算的是一阶差分岁时间的变化。除了频谱形状，帧与帧之间的能量（响度）变化也是一个非常重要的线索。因此我们把之前忽略的对数能量，以及它的一阶差分和二阶差分也加入向量特征 12+12+12+3=39维这39维特征已经能够捕捉到语音信号中与识别内容相关的大部分关键信息。更多的维度（如60维）带来的性能提升很小，但计算成本和存储开销会显著增加。目标标签如何获得？wav2vec 2.0wav2vec是这个方向重要的里程碑，启发了后续很多的模型包括hubertwavlmwhisper核心问题与LLM一样，能不能对语音进行预训练，得到一个通用的底层表示模型 ，然后只需要在不同下游任务上进行微调就能达到好的性能了。输入的波形是语音x，然后将cnn输入到cnn，得到z。将z进行量化得到Q，将Z输入到Transformer-encoder进行编码获得C，C与Q对比做lossMask是如何做的随机mask，以p=6.5%的概率选择是否mask，M=10表示如果决定要mask，那么就连续mask姐姐下来连续的10个表示。为什么是这样选择呢？因为这样设置的结果是有一半的语音输入会被mask。编码器是如何做的？特征编码器依然采用CNN结构，与HuBERT的特征编码器结构一直，结果就是1秒的语音会输出49个特征表示相当于20ms对应一个语音特征。采样率从16kHZ变成49HZ（也就是原来1秒钟的语音由16000个特征表示，编码后由49个特征表示）Loss是如何设定的？对比学习（contrastive learning）是无监督或自监督学习方法，核心思想是通过拉进相似样本的特征表示、推远不相似样本的特征表示，让模型学会从数据中提取有判别性的特征。针对图1中的灰色方块，要让灰色方块代表的输出与其对应的向量化后的向量（正样本）相似性最高，与其他量化后的向量（负样本）相似性不高。向量化表示是如何做的？同时存在多个码本，当输入一个原始向量时，从多个码本中找到一个量化后的向量，然后把他们前后拼接在一起，然后经过一个线性的变化，还原成原来的维度，得到量化后的向量。对比hubert整体思想和HuBERT类似只是在loss的设计上有不同，采用了量化和对比学习的方式构建了loss ，使得模型可以学习到更好的语音表示。出音频端到端，无需手工特征引入量化模块，把连续表示离散化结合对比学习，提高表示判训练后只需要少量的数据标注，也能微调出好模型。其实Hubert效果更好，训练更稳定，更强调语义一致性因为Hubert学习目标是离散的聚类ID，这些ID并不是唯一随机，而是通过k-means聚类获得的，他们包含了隐式的发音、因素、语义等信息。聚类标签是跨上下文的全局语义锚点，每一个聚类跨多个语音片段出现。而wav2vec是通过对比学习更强调判别性能。目标是把正样本拉进， 把负样本拉远，但是负样本可能是语义相近的token，这会引入噪声。但哇wav2vec启发了Hubert。WavLM之前介绍的wav2vec和hubert都是聚焦在语音识别任务上，并证明了有提升，那在其他语音任务上，也想有一种 统一的语音表示模型。WavLM可以有用于全栈的语音识别任务。wav2vec和hubert都是通过mask的方式，让预训练模型学习到了关于语音内容的统一表示。那非语音内容等语言信息可以通过什么方式来学习呢。（比如情感，或者说话人信息）这也是本文的创新点，一个基于降噪的预训练框架。整体的模型结构和Hubert是一样的，唯一的区别在于对输入数据的构造与Hubert的CNN encoder结构一样仍然采用K-means聚类方法聚类生成伪标签Z序列。20ms的语音片段核心的区别是带噪音的片段进行去噪。WAVLM的训练数据量更多，使用了9W小时语音，Hubert只用了6万小时。
C++基础笔记（写给有其他语言基础看的）基础暂停程序的执行system("pause");如果用CL可以不输出这个#include <iostream> using namespace std; int main() { //count：标准输出流对象，通常指控制台，count定义在<iosteam>头文件中，全称为std::count // <<流插入运算符，将右侧的数据发送到左侧的输出流对象。<<其实是运算符，c++支持运算符重载，本质上是函数链式调用 //endl 操纵器, 功能是插入一个换行符并刷新输出缓冲区，相当于std::flush // 整个表达式等价于一个函数调用：cout.operator<<(...) cout << "hello world" << endl; int a = 10; //这个函数返回本身，并且做了方法重载，因此可以接收各个不同的参数 cout << "a = " << a << endl; return 0;//默认缺省 返回0，return 0没有异常，1就是程序报错，-1特殊报错，取决于决定 }define宏常量 预处理阶段进行文本替换cost的编译阶段using namespace这会将指定命名空间中的所有名字“拉”到当前作用域，之后使用这些名字时就不需要再写命名空间前缀了。使用using声明更精确#include <iostream> using std::cout; // 只引入std命名空间中的cout using std::endl; // 只引入std命名空间中的endl int main() { cout << "Hello, world!" << endl; // 可以直接使用cout和endl // 但使用std命名空间的其他成员，如cin，仍然需要std::cin return 0; }虽然 using namespace很方便，但如果使用不当会带来问题，尤其是命名污染（namespace pollution）。请遵循以下最佳实践：避免在头文件中使用 using namespace这是最重要的原则。头文件通常会被多个源文件包含。如果在头文件里使用了 using namespace，那么这个命名空间就会被强制引入到所有包含了该头文件的源文件中，很容易引起难以排查的命名冲突尽量在局部作用域内使用即使在源文件(.cpp)中，也最好将 using namespace或 using声明限制在函数内部等尽可能小的作用域里，而不是放在文件开头全局使用，以减小其影响范围优先使用 using声明相比于引入整个命名空间的 using namespace指示，更推荐使用只引入特定成员的 using声明（如 using std::cout;）。这样做更安全，能最大程度地避免命名冲突using类似于java的import static，using namespace是import *数据类型int的取值范围通常是2^32次方，short2字节，int 4字节，long windows 4字节，linux 32位4字节，64位 8字节long long 16字节。具体按照平台定义和编译器实现。int main() { cout << sizeof(short) << endl;//win输出2 cout << sizeof(int) << endl;//win输出4 cout << sizeof(long) << endl;//win输出4 cout << sizeof(long long) << endl;//win输出8 return 0; }浮点型：float是固定的，double由系统决定。float 4字节 有效数字范围是7位 定义的时候要在数字结尾+f,如1'000'000.0fdouble 8字节 有效数字范围是7位默认情况下输出小数，会显式6位有效数字。字符类型'c'char在c++中只占用一个字节。字符串：由一个一个字符组成的。#include <iostream> //需要引入才能使用 #include<string> using namespace std; int main() { //c风格的字符串 char str[] = "hello world!"; //c++风格的字符串 string str2 = "asd"; std::cout << str2 << endl; return 0; } 数据输入#include <iostream> using namespace std; int main() { int a = 0; cout << "请给整形变量a赋值" << endl; cin >> a; cout << a << endl; } 数组和java不同C++，C++数组不记录长度。越界的数组访问可能产生未定义的行为。c++直接定义数组是在栈上分配的。int main() { int a[3] = {1, 2, 3}; for (int i = 0; i < 3; i++) { cout << a[i] << endl; } } 函数#include <iostream> using namespace std; //如果函数写在下面的化要进行声明 void fancyfunction(); int main() { fancyfunction(); } void fancyfunction() { cout << 1 << endl; }函数分页编写 我们需要创建.h后缀的头文件自己的文件用引号，库里的文件用角括号这是启动文件#include <iostream> #include "head.h" using namespace std; //如果函数写在下面的化要进行声明 int main() { fancyfunction(); } 这是head.h文件头文件一般约定俗成用来放函数声明void fancyfunction();这是被引入的文件，需要在cmake里定义#include <iostream> using namespace std; void fancyfunction() { std::cout << 1 << std::endl; }cmakecmake_minimum_required(VERSION 3.28) project(c__17) set(CMAKE_CXX_STANDARD 17) add_executable(c__17 main.cpp Main.cpp tool.cpp head.h )cmake用来编译的时候链接文件。cmake_minimum_required(VERSION 3.28)作用：指定构建本项目所需的 CMake 最低版本 为 3.28project(c__17)作用：定义项目的名称（这里为 c__17）add_executable(c__17 main.cpp Main.cpp tool.cpp head.h)作用：指示 CMake 从后面列出的源文件（main.cpp, Main.cpp, tool.cpp）生成一个名为 c__17的可执行文件文件include是预处理阶段的文本替换。#include <iostream>等效于把iostream里的代码全部复制过来using： 将特定命名空间中的名称引入当前作用域，使其无需限定即可使用using namespace std；告诉编译器，当遇到一个没有明确指定命名空间的标识符时，如果std命名空间中有同名的标识符，就使用std中的那个。using namespace是C++中的命名空间指令，主要功能是将指定命名空间中的所有标识符引入当前作用域，让我们可以直接使用这些标识符而无需添加命名空间前缀。指针*是指针变量名指针前加*代表解引用。int main() { int a = 10; int *p; p = &a; cout << "a地址是" << sizeof(p) << "值是" << *p; }a地址是8值是10（32位是4，64是8）#include <iostream> using namespace std; int main() { int *p = (int *) 0x1100; //访问野指针，只想非法的空间 int a = 10; int b = 10; //const修饰指针,指针指向可以修改，但指向的值不能修改 const int *p2 = &a; //*p2=20;//错误代码 p = &b; //正确 //指针常量 指针的指向不可以改，指针指向的值可以改 int *const p3 = &a; //都不可以改 const int *const p4 = &a; cout << "地址是" << sizeof(p) << "值是" << *p; } 函数指针int main() { int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; int *p = arr; //arr就是数组首地址 //输出数组的第一个 cout << *p << endl; *p = 10; cout << arr[0]<< endl; //用指针访问第二个元素 p += 10; //如果+10可能会访问到未知元素 cout << *p << endl; } c++中数组默认是指针传递（其实这是因为数组的本质就是指针，因此实际上还是值传递）#include <iostream> using namespace std; void func(int arr[]) { // 等价于 int* arr cout << "sizeof(arr) in function: " << sizeof(arr) << endl; // 输出指针大小 } int main() { int arr[5] = {1, 2, 3, 4, 5}; cout << "sizeof(arr) in main: " << sizeof(arr) << endl; // 输出数组总大小 func(arr); // 实际传递的是 &arr[0] return 0; }引用传递#include <iostream> using namespace std; void swap(int &a, int &b) noexcept { const int temp = a; a = b; b = temp; } int main() { int a = 1; int b = 2; swap(a, b); cout << a << endl; cout << b << endl; } 结构体结构体属于用户自定义的数据类型结构体也是值传递，结构体可以当做所有成员都是public的类。#include <iostream> using namespace std; //结构体继承 struct student { string name; int age; //结构体里可以定义结构体 struct teacher { string name; int age; }; int getAge() { //这里的this是只想自己的指针 return this->age; } } x2; //在创建结构体的时候，自动初始化变量，最好不要这样写 int main() { struct student st; st.name = "123"; cout << st.name << endl; //按顺序即可 struct student s2 = {"eee", 123}; //struct关键字可以省略，c20可以不按顺序写，只要声明了名字 student s3 = {.name = "eee", .age = 123}; //数组 student s4[10] = {{"", 123}, {"qaq", 333}}; //指针 student *p = &st; st.age = 10; //如果不给值随机输出，可能访问到奇怪的东西 std::cout << p->age << endl; //结构体里的方法 std::cout << st.getAge() << endl; } 继承#include <iostream> using namespace std; struct person { //可以有默认值 int age = 1; }; //go语言默认是public继承 struct student : public person { string NO = "qweqwc"; }; //在创建结构体的时候，自动初始化变量，最好不要这样写 int main() { student s = {1, ""}; cout << s.age << endl; } 程序地址全局区//全局变量 int a = 1; //函数名不能和变量名一样 void ca() { //普通变量前面加上static就是静态变量 //只创建一次 static也是全局的 static int a = 10; //第二次输出11 cout << a << endl; a = 11; } int main() { ca(); ca(); } 堆struct person { int age = 1; }; int main() { person *p = new person(); cout << p->age << endl; }new#include <iostream> using namespace std; struct person { int age = 1; }; int main() { person *p = new person(); cout << p->age << endl; //释放堆内存 delete p; }函数#include <iostream> using namespace std; int func(int a, int b = 20, int c = 30) { return a + b + c; } int func(int a) { return a; } //函数站位 int func(string) { return 1; } int main() { cout << func(20, 10) << endl; } 类对象class和struct的唯一区别就在于默认的访问权限不同。析构函数不可以有参数，不能重载，在销毁前自动调用析构函数。#include <iostream> using namespace std; class persion { public: int age = 10; persion() { cout << "我是人" << endl; this->age = 10; } persion(int age) { cout << "我是人" << endl; this->age = age; } void q() { cout << "hello world" << endl; } ~persion() { cout << "我被调用了" << endl; } }; class student { public: int age; }; int main() { //声明的时候自动调用构造函数 persion per; cout << per.age << endl; //new是把内存分配在堆上，和java有所不同 如果没有构造函数也可以这样直接赋值 persion p2{3}; persion *p = new persion(); p->q(); //delete自动调用析构函数 delete p; student stu; //直接访问到了内存中混乱的值 cout <<"学生的默认年龄" << stu.age << endl; } 构造函数#include <iostream> using namespace std; class student { public: int age; student(int age) { this->age = age; } //拷贝构造，剩下的构造都是普通构造。传递参数的时候会调用拷贝构造函数 student(const student &p) { cout << "拷贝构造被调用了" << endl; age = p.age; } }; void func(student s) { cout << s.age << endl; } int main() { //因为手动定义了构造函数因此就要传参了 student s(1); cout << s.age << endl; //这样也可以初始化 student s2{1}; cout << s2.age << endl; //值传递自动调用拷贝构造 func(s2); } 浅拷贝，编译器默认生成的拷贝构造函数和赋值运算符重载函数就是浅拷贝。浅拷贝的问题总结（双重危机）：悬空指针：多个对象共享同一块动态内存，其中一个对象被销毁并释放内存后，其他对象的指针就变成了悬空指针，使用它们会导致未定义行为。重复释放：当所有共享该内存的对象都销毁时，每个对象的析构函数都会尝试释放同一块内存，这会导致程序崩溃。深拷贝深拷贝解决了浅拷贝的所有问题。它的核心思想是：连“葫芦”带“瓢”一起复制。即，不仅复制指针本身，还为新的对象重新分配一块内存，并将原指针所指内存的内容完整地复制过来。#include <iostream> using namespace std; class student { public: int age; int *height; student(int age) { this->age = age; this->height = new int(1); } student(int age, int *height) { this->age = age; this->height = height; } student(const student &p) { cout << "拷贝构造被调用了" << endl; age = p.age; height = p.height; } //如果删掉 那么就是内存泄露了 ~student() { if (height != NULL) { delete height; } } }; int main() { int a = 2; student *s1 = new student(1, &a); //不能释放栈上的东西 delete s1; } int main() { student *s1 = new student(1); student *s2 = new student(*s1); delete s1; //内存被释放了俩次，会报错 delete s2; } 类中的函数一般被称为成员函数。静态成员就是在成员变量和成员函数之前加上static关键字，就称为静态成员。静态成员分为静态成员变量和静态成员函数。类一般放在hpp文件里，因为许多类要被用到多次。#include <iostream> using namespace std; class student { public: //这里不能初始化 static int a; static int pt() { cout << "hello world" << endl; return 0; } //这里要按顺序写 //c++17开始允许在内中使用inline声明+定义 inline static int b = student::pt(); }; //要在类外进行定义 int student::a = 1; int main() { cout << student::a << endl; student::pt(); cout << student::b << endl; }c++指针默认指向调用发起者，可以在调用的时候判断this是否会空。#include <iostream> using namespace std; class student { public: //这里不能初始化 static int a; static int pt() { cout << "hello world" << endl; return 0; } //这里要按顺序写 //c++17开始允许在内中使用inline声明+定义 inline static int b = student::pt(); int bc = 0; void func() { //为了防止空指针调用这个函数报错，可以先进行判断 if (this != nullptr) { //和java相似直接写bc等于this->bc cout << this->bc << endl; } else { cout << "你调用了一个空方法" << endl; } } }; //要在类外进行定义 int student::a = 1; int main() { cout << student::a << endl; student::pt(); cout << student::b << endl; auto s = new student(); //最好不要直接用NULL因为NULL是宏定义的0值 s = nullptr; //劲量在调用之前写 s->func(); } final和constc++中final用于继承控制// 禁止类被继承 class Base final { // ... }; // class Derived : public Base {}; // 错误：Base 是 final 的 // 禁止虚函数被重写 class Animal { public: virtual void speak() final { // 此函数不能被子类重写 cout << "Animal sound" << endl; } }; class Dog : public Animal { public: // void speak() override { } // 错误：speak 是 final 的 };const用于定义常量，表示"不可修改"。常量变量常量成员函数常量指针和引用函数参数和返回值的常量性// 常量变量 const int MAX_SIZE = 100; // MAX_SIZE = 200; // 错误：常量不可修改 // 常量成员函数 class MyClass { int value; public: int getValue() const { // 承诺不修改对象状态 return value; } }; // 常量指针 int x = 10; const int* ptr = &x; // 指向常量的指针 // *ptr = 20; // 错误：不能通过ptr修改x int* const ptr2 = &x; // 常量指针（指针本身不可修改）友元让特定的函数可以访问private的字段。同样的可以让特定类里的特定函数访问private字段。#include <iostream> using namespace std; class student { private: string primarySchool = "100"; public: friend void look(student &s); }; void look(student &s) { cout << s.primarySchool << endl; } int main() { student s; look(s); } 类友元class MyClass { private: int secret_data; // 声明 FriendClass 是 MyClass 的友元 friend class FriendClass; public: MyClass(int data) : secret_data(data) {} }; class FriendClass { public: void showSecret(MyClass& obj) { // FriendClass 可以直接访问 MyClass 的私有成员 secret_data std::cout << "The secret data is: " << obj.secret_data << std::endl; } void modifySecret(MyClass& obj, int new_value) { // 也可以修改私有成员 obj.secret_data = new_value; } }; // 一个非友元的类 class StrangerClass { public: void cannotShowSecret(MyClass& obj) { // 编译错误！secret_data 是 MyClass 的私有成员，无法访问。 // std::cout << obj.secret_data << std::endl; } }; int main() { MyClass my_obj(42); FriendClass friend_obj; StrangerClass stranger_obj; friend_obj.showSecret(my_obj); // 正确输出：The secret data is: 42 friend_obj.modifySecret(my_obj, 100); friend_obj.showSecret(my_obj); // 正确输出：The secret data is: 100 // stranger_obj.cannotShowSecret(my_obj); // 这行代码会导致编译错误 return 0; }运算符重载c++，c#，rust，python都支持，php，java，js，go等不支持#include <iostream> using namespace std; class person { public: int a = 10; int b = 10; person() { cout << "对象创建" << endl; } person operator+(person &p) { person temp; temp.a = this->a + p.a; temp.b = this->b + p.b; return temp; } }; person operator-(person &p1, person &p2) { person temp; temp.a = p1.a - p2.a; temp.b = p1.b - p2.b; return temp; } void t1() { person p1; person p2; //p3在这里也会创建一次 person p3 = p1 + p2; cout << p3.a << endl; cout << p3.b << endl; } int main() { //都是创建了三次对象 person p1; person p2; person p4 = p1 - p2; cout << p4.a << endl; cout << p4.b << endl; } 继承// 默认是私有继承 (private)，通常我们显式指定为 public class Derived : public Base { // 公有继承 // ... }; class Derived2 : Base { // 私有继承（默认，不推荐这样写） // ... };虚方法#include <iostream> using namespace std; class person { public: //纯虚函数，子类必须要重写这个方法 //virtual void func() =0; virtual void print() { cout << "hello world" << endl; } void print2() { cout << "hello world !" << endl; } }; class man : public person { public: void print() { person::print(); cout << "hi world" << endl; //调用其他方法 person::print2(); } }; int main() { man m; m.print(); //这里会默认调用a父方法,除非将调用的方法声明virtual person *a = new man(); a->print(); }class不声明的方法默认是private的#include <iostream> using namespace std; class A { public: void func() { cout << "A:hello world" << endl; } }; class B { public: void func() { cout << "B:hello world!" << endl; } void func2() { cout << "B:hi world!" << endl; } }; class C : public A, public B { public: //覆盖&重写 void func() { cout << "C:hello world" << endl; } }; int main() { C c; //直接调用func2没问题 c.func2(); //重写可以解决 c.func(); //直接调用具体的方法 c.B::func(); } C++中overwrite代表重写和覆盖 ，是c++11引入的class Base { public: virtual void show() { cout << "Base class" << endl; } virtual void display(int x) { cout << "Base display: " << x << endl; } }; class Derived : public Base { public: // 正确重写基类的虚函数 void show() override { cout << "Derived class" << endl; } // 使用override关键字，如果签名不匹配会报错 void display(int x) override { cout << "Derived display: " << x << endl; } };overwrite 通常指函数隐藏，当派生类定义了与基类同名的函数（但不是虚函数重写）时发生。类似java的overwrite注解class Base { public: void normalFunc() { cout << "Base normal function" << endl; } void func(int x) { cout << "Base func: " << x << endl; } }; class Derived : public Base { public: // 这隐藏了基类的normalFunc()，不是重写 void normalFunc() { cout << "Derived normal function" << endl; } // 隐藏了基类的func(int)，即使参数不同 void func(double x) { cout << "Derived func: " << x << endl; } }; int main() { Derived d; d.normalFunc(); // 调用Derived版本 d.func(5.0); // 调用Derived版本 // d.func(5); // 错误！基类的func(int)被隐藏 d.Base::func(5); // 正确，显式调用基类版本 }虚方法使用非常灵活，动态分派（Dynamic Dispatch）抽象类可以将虚函数写为纯虚函数。类中有了纯虚函数这个类也称为抽象类。内联class MyClass { public: // 1. 在类内部直接实现 - 默认是inline的 void func1() { // 函数体直接在类定义中 // 编译器会尝试将其作为inline函数处理 } // 2. 只是声明，实现在类外部 - 默认不是inline的 void func2(); }; // 在类外部实现 - 默认不是inline的 void MyClass::func2() { // 需要显式添加inline关键字才会成为inline函数 } // 如果想要func2也是inline的，需要显式指定 inline void MyClass::func2() { // 现在这个函数是inline的 }inline只是一个建议，编译器有权忽略复杂的函数模板函数模板c++提供俩种模板机制：函数模板和类模板 c11引入的建立一个通用函数，用一个虚拟的类型来代表。#include <iostream> //当写上template的时候，函数必须要用到这个T，或者调用声明的时候要用到这个T template<typename T> void swap(T &a, T &b) { T temp = a; a = b; b = temp; } int main() { // int a = 10; int b = 20; //编译器自动推导 //再编译时自动生成一个调用int的函数，然后调用哪个函数 swap(a, b); std::cout << a << std::endl; std::cout << b << std::endl; swap<int>(a, b); } 使用多个泛型template<typename T, typename U> void printPair(T first, U second) { std::cout << "First: " << first << ", Second: " << second << std::endl; } // 使用示例 printPair(10, "Hello"); // T=int, U=const char* printPair(3.14, true); // T=double, U=bool普通函数调用时可以发生自动类型转换。函数模板调用时，如果利用自动类型推导，不会发生隐式类型转换。如果利用显式指定类型的方式，可以发生隐式类型转换。普通函数和函数模板可以发生重载。但优先调用普通函数，函数模板页可以发生重载。数组不能直接赋值int arr1[5] = {1, 2, 3, 4, 5}; int arr2[5] = {6, 7, 8, 9, 10}; // 错误的！不能直接赋值 arr1 = arr2; // 编译错误！ // 错误的！数组名不能作为左值 arr1 = {10, 20, 30}; // 编译错误！因此函数模板处理数组可能出现错误。或者俩个对象可能是不能直接进行比较的。#include <iostream> class A { public: int a = 2; }; template<typename T> T add(T &a, T &b) { return a + b; } //这样就会走这段代码 template<> A add(A &a1, A &a2) { A temp; temp.a = a1.a + a2.a; return temp; } int main() { A a; A b; std::cout << add(a, b).a << std::endl; } 类模板#include <iostream> #include <string> //class和typename一样 template<class Name, class Age> class persion { public: persion(Name n, Age a) { this->name = n; this->age = a; } Name name; Age age; }; int main() { //类模板要显式指定类型 persion<std::string, int> p1("hh", 1); std::cout << p1.age << std::endl; std::cout << p1.name << std::endl; } 类模板没有自动类型推导，类模板在模板参数列表中可以有默认参数。类模板的成员函数创建实际是有区别的普通类中的成员函数一开始就可以创建类模板中的成员函数在调用时才创建类模板对象可以作为函数参数template<typename T> class MyArray { private: T m_array[100]; int m_size; public: MyArray() : m_size(0) {} void push_back(T value) { m_array[m_size++] = value; } int getSize() const { return m_size; } // ... 其他成员函数，如获取元素等 };// 函数参数是 MyArray<int>，非常具体 void printArray(MyArray<int>& arr) { for (int i = 0; i < arr.getSize(); ++i) { // 假设有获取元素的成员函数 // std::cout << arr.getElementAt(i) << " "; } std::cout << std::endl; } int main() { MyArray<int> intArray; intArray.push_back(10); intArray.push_back(20); MyArray<std::string> strArray; // 另一个类型的 MyArray 对象 printArray(intArray); // 正确！类型完全匹配 // printArray(strArray); // 错误！函数需要 MyArray<int>，但传入的是 MyArray<std::string> return 0; }子类继承父类 是一个类模板是，子类在声明的时候，要指定父类中T的类型 ，如果不指定，编译器无法给子类分配内存。如果想灵活指定父类中的T类型，子类变为类模板。STL长久以来软件界一直希望建立可重复利用的东西。大多数情况下，数据结构算法都未能有一套标准，导致大量的重复工作。为了建立数据结构和算法的一套标准诞生了STL。STL广义上分为：容器Container，算法algorithm，迭代器iteratorSTL分为六大组件，分别是：容器，算法，迭代器，仿函数，适配器，空间配置器。容器序列式容器和关联式容器：序列是容器：强调值的排序，序列是容器中每个元素均有固定的位置。关联式：二叉树结构，各个元素之间没有严格意义上的物理上的顺序关系vector#include <iostream> #include <string> #include <vector> #include<algorithm> using namespace std; void my_print(int a) { cout << a << endl; } int main() { vector<int> v; v.push_back(10); v.push_back(20); v.push_back(30); //迭代器 vector<int>::iterator itBegin = v.begin(); // 起始迭代器，指向第一个元素 vector<int>::iterator itEnd = v.end(); // 结束迭代器，指向容器中最后一个元素的下一个位置 //遍历 while (itBegin != itEnd) { cout << *itBegin << endl; itBegin++; } for (vector<int>::iterator it = v.begin(); it != v.end(); it++) { cout << *it << endl; } //函数回调 for_each(v.begin(), v.end(), my_print); //只读访问 for (const auto &element: v) { cout << element << endl; } //需要修改元素 for (auto &element: v) { element *= 2; } //使用下标索引,vector重载了下标 for (int i = 0; i < v.size(); i++) { std::cout << v[i] << endl; } } c++默认数组是随机初始值（上次未清理的值）int main() { int arr[10]; //随机初始 for (int i = 0; i < 10; i++) { cout << arr[i] << endl; } //c11开始变成固定初始值 int arr1[10]{}; for (int i = 0; i < 10; i++) { cout << arr1[i] << endl; } } 构造函数和初始化#include <vector> using namespace std; // 默认构造 vector<int> v1; // 指定大小和初始值 vector<int> v2(5, 10); // 5个元素，每个都是10 // 从数组初始化 int arr[] = {1, 2, 3, 4, 5}; vector<int> v3(arr, arr + 5); // 初始化列表（C++11） vector<int> v4 = {1, 2, 3, 4, 5}; // 拷贝构造 vector<int> v5(v4);元素访问vector<int> v = {10, 20, 30, 40, 50}; // 下标访问（不检查边界） cout << v[2]; // 30 // at() 方法（检查边界，越界抛出异常） cout << v.at(2); // 30 // 访问第一个和最后一个元素 cout << v.front(); // 10 cout << v.back(); // 50 // 数据指针（C++11） int* data = v.data();数组容量vector<int> v = {1, 2, 3}; cout << v.size(); // 当前元素个数：3 cout << v.capacity(); // 当前容量：可能大于size cout << v.empty(); // 是否为空：false v.reserve(100); // 预分配空间，避免多次重新分配 v.shrink_to_fit(); // 减少容量到刚好容纳元素在GCC和Clang中默认是0，扩容增量是2倍。也就是说·1,2,4,8,16·插入删除vector<int> v = {1, 2, 3}; // 末尾添加 v.push_back(4); // v: {1, 2, 3, 4} // 任意位置插入 v.insert(v.begin() + 1, 99); // v: {1, 99, 2, 3, 4} // 插入多个相同元素 v.insert(v.end(), 3, 100); // 末尾插入3个100 // C++11 原地构造 v.emplace_back(5); // 比push_back更高效 v.emplace(v.begin(), 0); vector<int> v = {1, 2, 3, 4, 5, 6}; v.pop_back(); // 删除末尾元素 v.erase(v.begin() + 1); // 删除第二个元素 v.erase(v.begin(), v.begin() + 2); // 删除前两个元素 v.clear(); // 清空所有元素string在大部分在绝大多数现代C++标准库实现中，std::string的底层确实使用了一个动态分配的字符数组（即 char*）来存储字符串数据。string的本质上是一个类。是一个char *的容器。#include <iostream> #include <string> #include <vector> #include<algorithm> using namespace std; int main() { //默认构造 string s1; cout << s1 << endl; const char *str = "hello world"; cout << str << endl; string s3(str); cout << s3 << endl; string s4(10, '1'); cout << s4 << endl; string a = "123"; cout << a << endl; }基本操作string s = "Hello"; // 获取长度 int len = s.length(); // 或 s.size() // 检查是否为空 bool isEmpty = s.empty(); // 访问字符 char c1 = s[0]; // 'H' char c2 = s.at(1); // 'e'，带边界检查 // 修改字符 s[0] = 'h'; // "hello"字符串拼接string s1 = "Hello", s2 = "World"; // + 运算符 string s3 = s1 + " " + s2; // "Hello World" // append() 方法 s1.append(" C++"); // "Hello C++" // += 运算符 s1 += "!"; // "Hello C++!"字符串比较string a = "apple", b = "banana"; if (a == b) cout << "相等"; else if (a < b) cout << "a小于b"; // 字典序比较 else cout << "a大于b"; // compare() 方法 int result = a.compare(b); // 返回负数、0或正数dequevector对于头部的插入删除效率低，数据量越大效率越低deque相对而言，对头部的插入删除速度比vector快。deque内部有个中空气维护每段缓冲区中的内容。deque在查找上相比于vector需要额外的地址计算，而且局部性较差。#include <iostream> #include <deque> int main() { std::deque<int> dq; // 尾部插入 dq.push_back(1); dq.push_back(2); // 头部插入 dq.push_front(0); dq.push_front(-1); // 遍历输出：-1 0 1 2 for (int num : dq) { std::cout << num << " "; } std::cout << std::endl; // 随机访问 std::cout << "第二个元素: " << dq[1] << std::endl; // 头部删除 dq.pop_front(); // 尾部删除 dq.pop_back(); return 0; }stack#include <iostream> #include <stack> using namespace std; int main() { stack<int> s; // 压入元素 s.push(1); s.push(2); s.push(3); cout << "栈顶元素: " << s.top() << endl; // 输出3 cout << "栈大小: " << s.size() << endl; // 输出3 // 遍历栈（遍历会清空栈） cout << "栈内容: "; while (!s.empty()) { cout << s.top() << " "; s.pop(); // 移除栈顶元素 } // 输出: 3 2 1 return 0; }Queue先进先出c++stack和Queue底层大部分是dequelistlist底层是链表#include <iostream> #include #include <string> // 自定义对象列表 struct Person { string name; int age; Person(string n, int a) : name(n), age(a) {} }; int main() { list<Person> people; // 添加元素 people.emplace_back("Alice", 25); people.emplace_back("Bob", 30); people.emplace_front("Charlie", 20); // 遍历 for(const auto& p : people) { cout << p.name << ": " << p.age << endl; } // 自定义排序 people.sort([](const Person& a, const Person& b) { return a.age < b.age; }); return 0; }set、multiset所有的元素在插入时会被自动排序。set不允许有重复元素。multiset允许有重复元素。创建#include <set> #include <iostream> using namespace std; int main() { // 声明一个空的 set set<int> s1; // 使用初始化列表 set<int> s2 = {1, 3, 5, 2, 4}; // 使用迭代器范围初始化 vector<int> vec = {1, 2, 3, 2, 1}; set<int> s3(vec.begin(), vec.end()); // 结果为 {1, 2, 3} return 0; }插入set<int> s; // 插入单个元素 s.insert(10); s.insert(20); s.insert(30); // 插入多个元素（C++11） s.insert({5, 15, 25}); // 使用 emplace（C++11），更高效 s.emplace(40); // 检查插入是否成功 auto result = s.insert(20); // 20已存在，插入失败 if (!result.second) { cout << "插入失败，元素已存在" << endl; }删除操作set<int> s = {1, 2, 3, 4, 5, 6}; // 通过值删除 s.erase(3); // 删除元素3 // 通过迭代器删除 auto it = s.find(4); if (it != s.end()) { s.erase(it); } // 删除一个范围 auto it1 = s.find(2); auto it2 = s.find(6); if (it1 != s.end() && it2 != s.end()) { s.erase(it1, it2); // 删除 [2, 6) 范围的元素 } // 删除所有元素 s.clear();查找set<int> s = {10, 20, 30, 40, 50}; // 使用 find auto it = s.find(30); if (it != s.end()) { cout << "找到元素: " << *it << endl; } // 使用 count（对于 set，只能是 0 或 1） if (s.count(25) > 0) { cout << "元素存在" << endl; } // 检查是否存在某个范围的元素 if (s.lower_bound(20) != s.upper_bound(40)) { cout << "存在 20-40 之间的元素" << endl; }Mapint main() { pair<string,int>p("tom",20); cout<<p.first<<endl; cout<<p.second<<endl; }map中所有元素都是pair，第一个元素是key，第二个元素是value。key起到了所有作用。#include <iostream> #include <string> #include using namespace std; int main() { map<int, int> m; m.insert(pair<int, int>(1, 10)); m.insert(pair<int, int>(2, 20)); m.insert(pair<int, int>(3, 30)); m.insert(pair<int, int>(4, 40)); for (const auto &pair: m) { std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl; }; std::map<int, std::string> myMap = { {1, "Apple"}, {2, "Banana"}, {3, "Cherry"} }; }#include <iostream> #include int main() { std::map<int, std::string> mp; mp[1] = "one"; mp.insert({2, "two"}); mp.emplace(3, "three"); for (const auto& pair : mp) { std::cout << pair.first << ":" << pair.second << " "; } // 输出：1:one 2:two 3:three return 0; }仿函数可以向函数一样被调用对象。通过重载函数调用运算符operator实现。（因为模板参数不能写函数指针，但是可以写类型）这个map是红黑树。unorderedmap才是hashmap
Linux socketsockethttps://www.bilibili.com/video/BV1VWtyeVEsVhttps://blog.valiantcheng.com/misc/socket-intro.html如果发送数据包要从HTTP到数据链路封包全部构建一遍，这样的操作成本无疑是非常高的。操作系统的构建者为我们提供了很好的支持，他们屏蔽了底层的细节，抽象出一个统一的网络操作接口。 来便于上层用户使用网络。这个统一的接口就是socket。socket直译是插槽，我们只需关心如何插入，而不需要关心内部的实现。socket是一套抽象接口体系，可以理解为一套使用协议或者是一套API。因为是操作系统聪明封装提供的机制，因此具体使用需要以系统调用的形式来实现。socket接口提供了非常多种类的socket，没种都有其使用场景。这是所有socket操作的第一步，create socket。getsocket和setsocket相对于查看获取和设置socket选项。很多和socket交互的细节都通过这个方法来进行控制。接下来是bind，绑定操作，相当于把电器插头插入插槽。也就是将机器上的某个IP地址和端口组合进行绑定。其他操作者无法使用这个组合，时一种声明网络所有权的操作类型。listen传输层语义的监听操作，被动等待某个其他的网络实体向自身（bind过的IP和端口组和）发起通讯（通常是connect操作）accept，传输层的确认操作：确认某个尝试建立连接的网络实体和自身之间的关系。一旦accept正式的传输就已经建立，可以开始后续的数据交互connect：传输层语义的连接操作，连接某个传输层实体（也就是协议+IP+端口号）协议可能是TCP、UDP或其他四层协议（对于TCP就是经典的三次握手过程）shutdown、close：传输层语义的关闭操作send、sendto：传输层语义的发送操作recvfrom：传输层语义的接收操作上述系统调用相当多都是传输层的，这说明socket很大一部分功能是提供四层接口、也就是传输层网络之上的接口。但是并非所有操作都是四层的。这些是常见的socket类型。socket逐渐从网络通信层面发展演变成了一个数据通信操作的标准接口体系。对大部分开发者来说有许多我们常用TCP的选项，如SOL_SOCKET，SO_REUSEADDR其他类型的socket也可以设置很多针对性的选项，比如广播和多播的可设置选项就非常多TCP常见场景机器正常下线哪怕是kill -9操作系统也会自动释放tcp连接。如果此时已经建立连接，正在写入数据，会收到EOF相关字样的报错如果还未成功收到连接会收到connection refused类报错连接被拒绝了。正常情况出现在对端机器正常，但对应的服务已经挂掉了，这种情况下收到了发向不可用服务短口的数据，这是宿主机就会为不可用的服务进程对应socket链接发送一个RST（reset）数据包通知对端本地服务不存在或终止了。机器异常下线如果机器网线被拉断了。由于对端服务所在的机器对我们来说已经不可用了。所以此时不会有任何响应发回。此时发送端会按照socket选项的默认配置，也就是keepAlive以及timeout相关的socket选项，进行发送的等待和重试。如果超过一定的发送次数和时间，会报错并中断连接。在 不设置socket timeout选项的情况下，发送后等待的默认重传次数一般是15次，时间大概是900秒左右。每次重试的中间间隔会依次按照指数退避算法进行而不是固定的重试频率。如果在日常遇到了这样的时间间隔，要足够敏感。如果想要尽快失败，可以调整内核参数tcp_retries2来修改这里的行为。这种情况下报错分为俩种情况，如果路由器没有替代应答本地会直接报timeout错误，如果路由器替代应答，会发回一个ICMP包，告知destination is unreachable，那么我们最终收到的是unreachable类的错误。如果在这个过程中机器又恢复了，恢复的机器后已经丢失了原先的信息（可能是被强制断电然后重启）因此会直接发送reset包。告知我们对端的机器拒绝接收当前发起或正在进行的连接。类似进程挂掉的情况。UDP对端不可用一般来讲默认的UDP通信在对端服务故障、不存在或丢包时是会无限阻塞住的。所以一般底层库都是会提供默认的超时值设置的。比如DNS解析是2S，如果没有，在编程时要注意自己控制超时。UDP发送的数据包目的端口被对端判断为无效（目标机器上没有这个端口对应的服务）或是其他不可达场景，对端通常会回复ICMP错误消息（不回复RST是因为UDP协议没有RST报文）。类似我们在上一部分提到的路由器代答错误。UDP本身可以不调用connect方法直接发送数据的，这一点和TCP有本质上的差异。所以机器上不会有连接信息。UDP sento判定标准是只要数据从缓冲区发送出去了，这个操作就是成功的，它不会管数据是否返回或者有报错信息。因此有ICMP返回的化，原来的UDP是接受不到的（这被称为一步ICMP错误问题）。因此要解决这类问题通常需要通过一个守护进程来代为接受ICMP包通知，或者是UDP发送数据前都确保了connect再进行后续其他操作。通过socket发送的数据被机器内部或外部设置的防火墙规则拦截了，如果本地网络有网络过滤处理，比如ipvs，iptable等可能会收到EPERM或Operation not permitted类报错。遇到这一类报错要注意很可能是本地协议栈中的的netfilter框架drop掉了我们的数据包。如果是外部防火墙，有俩种可能一种是直接丢包，这时我们感觉就像是对方机器挂了一样。还有一种是发送reset包拒绝我们的连接发起。
Docker与K8s笔记https://www.bilibili.com/video/BV13GmDY9EtVDocker问题在容器之类的技术出现前，用户直接将程序部署到物理机上运行。主要用ystemd类的守护程序管理业务程序。这种方式可以很方便的查看每个业务程序的运行效率，并进行手动自动化操作。但是很多时候程序可能伴随着各种依赖。比如动态链接库某个命令行工具第三方依赖特殊配置文件等在多台机器规模化的情况下，通过下发部署脚本来实现。然而业务程序的依赖是复杂容易变动的。同一个程序的多个版本的依赖可能是不同的。我们还要管理多个程序在同一个服务器上运行的情况，程序之间的依赖可能产生冲突。最简单的方式就是每个程序独占一个机器。当时虚拟机技术发展已经较为成熟了。这样做的本质就是把程序放在虚拟机中，但虚拟机过于庞大，启停速度过慢，虚拟化性能损耗大。解决方案镜像大小虚拟机之所以那么大，是因为存储了操作系统运行所需要的所有文件。对于同一系列操作系统的不同版本来说，往往存在大量相同的文件，比如动态链接库，内核等等。如果相同的文件只存一份就可以大大减少镜像大小。如果多个虚拟机同时修改文件怎么办，那么就拷贝一个文件副本给新的镜像。其实就是copy-on-write的思想。同样，物理机和虚拟机上也存在大量重复的文件。因此这个优化思路不仅限于虚拟机，也适用于物理机和部署在其上的镜像。该思想实现的具体技术就是UnionFS或UFS，在容器出现之前的2004年，这个技术就已经出现了。它的初衷是希望在多个文件系统上提供一个统一的文件系统视图。启动速度虚拟机启动慢因为把珍格格操作系统的流程都包含了进去。理论上如果我们只关心程序运行本身的化 ，启停是不需要的。因此就可以去掉操作系统的启停。因此必须要实现一个轻量化的虚拟机，把操作系统相关的启停开销去掉。Linux就是在这样的背景下，逐渐发展出了实现上述需求的技术。namespace和cgroup。要实现一个程序的隔离，主要是以下方面。文件系统进程，线程管理进程线程通信网络用户权限硬件资源使用如果一个运行的程序在以上几方面都有自己的转述隔离空间和配额。那么实际上就实现了我们所说的轻量化隔离目标。这也就可以称为容器了。我们把程序放在容器中运行，保持隔离的前提下，只运行程序本身。那么其启动速度显然会比虚拟机快很多，这就满足了我们对隔离后启停速度的要求。namespace和cgroup。在2008年基本发展成型。这早于docker的出现。Namespace和Cgroup是Linux内核提供的两项关键技术，它们共同构成了容器技术的基石。简单来说，Namespace主要负责提供隔离性，为进程创建一个独立的运行环境；而Cgroup主要负责提供限制性，对进程组可以使用的资源进行约束和管理这样我们解耦了操作系统本身冗余的部分，而只关注进程本身的启停。这样容器的速度就和进程本身没什么不同了。性能损耗虚拟机是一个完整的操作系统，例如linux有很多内核进程来协助内核做系统管理工作。如果有多个内核，每个内核都会有自己的一套内核管理进程运行。从整体的角度来看，这些重复工作的进程就是额外损耗。容器隔离相关的技术就解决了这个问题。因为本质上所有容器内部的进程都是直接使用共用内核的，不需要维护独立内核。此外，硬件损耗也是一部分，早期虚拟化技术对于硬件的损耗较大，不过随着虚拟化技术的发展，CPU，内存已经被降到很低了。但是GPU io设备的损耗依然存在。最后docker就是把上述方案都打包到一起，并提供了一个用户友好的使用界面的系统。Docker依赖容器基础技术，UFS，和轻量化隔离能力。在此之上，docker设计了容器镜像和运行的标准,提供了完整的易用工具。让程序和依赖进行绑定，作为独立单元来进行管理。docker的命名是集装箱，很符合其实现的功能。即软件的标准化交付，将程序本身和依赖打包为一个标准单元。外部的使用者不需要关心docker镜像中到底有什么。K8s简介在docker诞生和流行后，带来了相当大的技术红利。随着docker被大规模用于生产实践。单机上的容器可以使用docker很轻松的进行管理，但有一定规模的企业往往会有很多个应用，很多台机器。一旦应用机器的数量急剧增长。管理这些容器就变得非常麻烦了。如果没有任何手段，那么就只能编写运维脚本，然后批量下发到每台机器进行管理了。这种方案操作效率低：大规模重复的劳动，人为容易出错缺失全局记录信息：部署后无法感知多个机器的运行情况，需要自己建设一个系统来进行监视精细化控制成本高：比如实现一组应用程序受控分批逐步升降级、根据资源使用针对应用程序所申请的各类计算资源进行扩缩容等调整，一台机器宕机后对上面运行的应用程序进行恢复故障转移等。难以进行高效的资源管理使用，分配不高效。服务难以高效管理和治理：应用服务如何在大规模之间高效的互相感知协作。我们更多的将k8s叫做容器编排系统而不是管控系统。不管理实现细节。K8s是一个面向终态的系统。例如空调冰箱关注对象的温度达到指定值，否则会一直改变温度来尝试达到指定值。面向中台的关键就在于让控制逻辑感知到某种状态（现状），然后根据状态调整现状，达到目标状态。而目标状态也不是一成不变的。问题解决K8s要真正解决的问题是：大规容器化程序应用在大规模机器集群内的高效部署管理问题。其中部署挂历可以分为操作目标状态和最终目的三部分通过操作来改变目标状态实现目的。K8s将达成目标状态的操作自动化。声明式API是一种现代编程和系统设计范式，其核心思想是用户只需定义期望的系统目标状态，而不必关心实现该状态的具体步骤。系统会自动负责将当前状态调整到期望状态。这种模式与强调逐步执行命令的命令式API形成鲜明对比这种面向终态的思想有一个行业内的专业术语叫做 声明式API。k8s中使用的yml就是我们能直接看到的体现，我们把对yml写入终态，k8s就能帮我们自动实现，所以这里写yaml就好像变成了调API一样。我们只需要声明一下就可以达成效果。业内k8s工程师也经常被调侃为yml工程师。有了面向终态的思想带来了以下好处自动化、标准化操作流程：如重启升级等操作，基本操作流程都是不变的，会变的只是外部参数版本号。消除了人工介入的成本天然重试机制：面向终态带来了一个结果就是重试，比如进行某个操作时出现了偶然的出错，程序就可以重试来最终达成目标状态。全局状态记录：面向终态要时刻比较当下和目标状态的差异，因此对当下目标状态信息的记录成为了必要条件。所有的机器的信息都在管理机构中统一保存（ETCD）降低门槛：由于所有的操作都被自动化标准化了，运维人员的关注点从关注过程变成了关注结果。接下来就是剩下的俩个问题难以进行高效的资源管理使用，分配不高效。服务难以高效管理和治理：应用服务如何在大规模之间高效的互相感知协作。K8s解决资源分配调度问题进程线程调度，CDN网络流量，物流中多地仓库，内存管理或GC都可以试做调度问题：资源如何分配能达成某些条件下的最优效果。这个最优可能是全局最优或者局部最优。k8s面对的调度场景就是：如何将多个具备不同特点和规格的应用程序，安置到由多个不同特点和规格的机器所组成的集群内才能达成最高的资源利用率.一般来说资源录用率往往和服务质量（如实时性）是不可兼得的。一般都是在俩者之间做取舍。如果大部分调度对象对执行时间有较为严格的要求，调度侧重点就会变成平均时间开销，而不是资源录用率。k8s提供了一个通用的调度框架，帮使用者实现不同的调度需求目标。这里先介绍默认的调度策略。调度最重要的过程是过滤和打分过滤阶段调度器会将所有满足pod调度需求的节点筛选出来，过滤函数会检查所有候选节点的可用资源是否满足pod要求。在过滤之后得出一个节点列表，里面包含所有可调度的节点。通常情况下这个列表不止一个节点，列表为空则不可调度。在打分阶段调度器会根据当前启用的打分规则对调度列表中的每个节点进行打分。最终得到一个得分最高最合适的节点，如果存在多个得分一样的节点，调度器会随机选择一个。因此我们可以将调度理解为打分。打分结果是由多种策略加权求和得来的。K8s1.30默认策略包括（括号内是权重，以下分数为**加权求和后计算）Kubernetes调度器中“加权”的“权”主要来源于三个层面：Kubernetes项目内置的默认权重、管理员通过配置文件显式设置的权重，以及Pod配置中声明的权重。在 Kubernetes 中，污点是应用于节点（Node）上的一种特殊标记，它的核心作用是 “排斥” Pod。你可以把它想象成节点设立的一个“门禁”规则，明确告知调度器：“只有携带特定通行证（即容忍度）的 Pod，才能被我接纳。污点容忍情况（3）：节点打上了污点后，除非容器显式声明容忍，否则都不会调度到污点节点上。节点亲和性情况（2）：容器组声明的更倾向于希望所处的机器节点的条件可用资源情况（1）：节点上可供使用的计算资源的情况容器组拓扑分布情况（2）：相同的容器组希望或不希望出现在同一类机器节点上。比如希望在不同机房上分散开容器组亲和性情况（2）某些容器组希望或不希望处于同一或不同的节点节点资源分配情况（1）：节点当前已经分配出自愿的情况节点上存在镜像的情况（1）：机器是否有需要的容器镜像了（不需要重新下载拉取镜像）其中1,2,4,5可以分为一类，它们都是用户主动指定的调度偏好。即我想让这些应用运行在哪些地方，这里都给了相对较高的权重。因此K8s的默认策略是更照顾用户的主观需求的。3,6,7这三个条件则是客观资源条件。即吧应用放在哪里可以达成最好的资源录用率。这些权重是比前一种主观性要求低的。这些是默认策略，如果有具体需求，这些调度策略，权重，甚至是计算方法都是可以用户完全自定义的。例如一般的调度策略仅会将CPU和内存作为计算资源考量，事实上，除此之外的磁盘，网卡，GPU，各类设备硬件都可能成为调度对象。服务管理治理问题微服务在k8s火的时间也非常流行。俩者的理念模式存在非常多的相同点。微服务核心是把庞大的单体服务拆分成多个独立灵活小巧的微型服务。在拆分完服务之后，多个微服务的部署管理治理都变得更加复杂和棘手。而容器和k8s技术恰恰能够帮助解决这些问题。以下是微服务相关的技术栈配置中心k8s提供了俩类存在于集群中的资源ConfigMap和secret来实现配置的管理，前者用于保存配置数据，后者用于保存敏感数据。所有需要使用配置的容器化应用都可以通过挂在的方式，讲一个或多个configmap、secret放到与文件中，实现应用对配置的感知，同时多个实例也可以挂在同一个configmap、secret。用户对于configmap以及secret的修改也会实时的同步到挂载这些对象的应用实例上。这样k8s就实现了对服务配置的统一管理。注册中心、服务发现K8s可以将某个服务的一组实例抽象成一类集群中的资源。通过service来进行管理，service作为集群资源，会被保存到k8s的全局记录中，这样可以通过K8s API轻易获取集群中存在哪些可访问的应用。不同的服务间可以通过某个特定的Service，其具备固定的域名和虚拟IP，称为ClusterIP来实现互访，而无需感知实际要访问哪一个地址。负载均衡在上一层service上 k8s有一个名为kube-proxy的组件，通过os内核提供的（iptable和ipvs）实现了应用在发起端的负载均衡。这样当访问某个Service，流量基本能够均匀按照要求分布在服务的多个实例副本之间。实现负载均衡网关K8s抽象了Ingress ，getway等资源来一声明的方式提供网关实例。K8s自身没有强制绑定某一个网关的实现，而是定义il网管需要支持的基本语义规范（协议、接口）来提供给用户使用 。可以根据具体需求选择网管来进行使用。RPC由于RPC是一个和语言密切相关的操作，K8s没有提供具体的Rpc框架，K8s不强制绑定某个实现限流和熔断也是在网管和RPC层进行实现。总结K8s对应springcloud提供的能力，K8s均做了对应的实现。无法实现的提供了接口进行嵌入适配。k8s因为语言中立带来了普适性，以及面向协议，接口兼容的扩展性。应用程序应当且仅需管理应用程序自身。应用程序开发者应该更专注于业务程序本身 。