2024年2月

块设备的定义

块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备相比,块设备有以下几个特殊之处:

  1. 块设备可以从数据的任何位置进行访问
  2. 块数据总是以固定长度进行传输,即便请求的这是一个字节
  3. 对块设备的访问有大量的缓存。当进行读时,如果已经缓存了,就直接使用缓存中的数据,而不再读设备,对于写也通过缓存来进行延迟处理。

块设备读写是按块(所以叫块设备)进行的,使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。作为存储设备,块设备驱动的核心问题就是哪些page->segment->block->sector与哪些sector有数据交互,下图是Linux内核中块设备的驱动模型。

块设备驱动框架

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虚拟文件系统

虚拟文件系统(VFS)隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口。其基于不同的文件系统格式,比如EXT,FAT等。用户程序对设备的操作都通过VFS来完成,在VFS上面就是诸如open、close、write和read的函数API。

映射层

映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址。

通用块层

通用块层是 Linux 内核中的中间层,位于块设备驱动层和文件系统层之间。通用块层负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系。在通用块层中,通常用一个bio结构体来对应一个I/O请求。通用块层隐藏了底层硬件的细节,使上层的软件可以独立于具体的硬件实现。它还负责管理块设备的缓存、缓冲区和数据传输,以提高性能。

IO调度层

IO调度层是通用块层的一部分,它负责决定块设备上的IO请求的执行顺序。当多个IO请求同时到达块设备时,IO调度层使用调度算法来决定哪个IO请求应该首先执行。调度算法的目标是优化系统的性能和响应速度。常见的IO调度算法包括:

  • CFQ(完全公平队列):根据进程的IO请求历史和权重来调度IO请求。它试图公平地分配磁盘带宽给不同的进程。
  • Deadline(截止时间):使用截止时间来调度IO请求。它将IO请求分为读取和写入,并尽量在截止时间之前完成IO请求。
  • NOOP(无操作):简单地将IO请求按照先到先服务的方式进行调度,不做任何优化。

块设备驱动层

在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用request结构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交 I/O 请求, 调度程序将磁盘资源分配给系统中所有挂起的块 I/O 请求,其工作是管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。

页、段、块、扇区关系

  • 扇区(Sectors):
    任何块设备硬件对数据处理的基本单位。通常,1个扇区的大小为512byte。(对设备而言)
  • 块 (Blocks):
    由Linux制定对内核或文件系统等数据处理的基本单位。通常,1个块由1个或多个扇区组成。(对Linux操作系统而言)
  • 段(Segments):
    是一个内存页或内存页的一部分,它包含磁盘上物理相邻的几个数据块的内容,是块驱动的
    传送
    单位,大小不定(取决于通用块层,因为它把传送的数据下发给块驱动,而通用块还可以把几个段合并成物理段、硬件段(专门总线电路);同时也取决于用户所访问的大小。注意这里所指的段与内存中的段也有些联系,因为内存的段可能是一个对象/变量的大小;而用户程序也可能以变量/对象作为单位来访问设备。
  • 页(page):在这里仅仅是把4096B的连续数据称为一个页。也可以指内存的
    数据组织
    单位(有些体系还在使用段,但是多数已经将段屏蔽了,即内部使用,而外部“看不见"),大小通常为4096B。但内存本身的
    访问/传送
    单位并不是页,这是取决于内存的编址方法,现在内存通常是按字节编址的,而甚至部分可以按位编址,所以内存的访问单位可以更小。
  • 页、段、块、扇区之间的关系图如下:

块设备驱动-第 2 页.drawio

本文参考

https://zhuanlan.zhihu.com/p/507214979

https://zhuanlan.zhihu.com/p/504763396

https://zhuanlan.zhihu.com/p/87566255

https://www.cnblogs.com/lifexy/p/7651667.html

https://cloud.tencent.com/developer/article/2145712

之前介绍过
Python

Fire
库,一个用来生成命令行工具的的库。
请参考:
Python Fire:自动生成命令行接口

今天,针对命令行参数,补充两种更加灵活的设置方式。

1. *args 型参数

*args
型的参数可以接受任意长度的参数。
比如,模拟一个学校发送通知的功能:

import fire

def notions(school, *names):
    for name in names:
        print(f"[{school} 通知] hello {name}")


if __name__ == "__main__":
    fire.Fire(notions)

使用起来很灵活,

$  python.exe .\fire-sample.py NJ大学 小红 小李 小张 小华
[NJ大学 通知] hello 小红
[NJ大学 通知] hello 小李
[NJ大学 通知] hello 小张
[NJ大学 通知] hello 小华

$  python.exe .\fire-sample.py --school NJ大学 小红 小李 小张
[NJ大学 通知] hello 小红
[NJ大学 通知] hello 小李
[NJ大学 通知] hello 小张

$  python.exe .\fire-sample.py --school NJ大学 小红 小李 小张 小华
[NJ大学 通知] hello 小红
[NJ大学 通知] hello 小李
[NJ大学 通知] hello 小张
[NJ大学 通知] hello 小华

$  python.exe .\fire-sample.py 小红 小李 --school NJ大学 小张 小华
[NJ大学 通知] hello 小红
[NJ大学 通知] hello 小李
[NJ大学 通知] hello 小张
[NJ大学 通知] hello 小华

从上面使用的示例可看出,

  1. 可以不输入参数名称(比如第一个例子),按照顺序第一参数赋值给
    school
    ,其余的赋值给
    *names
  2. *names
    参数支持不定长度的值
  3. school
    参数指定参数名的话,可以放在任意的位置(比如上面第四个例子)。

2. **kwargs 型参数

**kwargs
型参数也是不定长度的,和
*args
型参数不同的地方在于,
使用
**kwargs
型参数时,需要指定
参数名称

比如,模拟一个显示学生成绩的功能:

import fire

def scores(cls, **students):
    for k, v in students.items():
        print(f"[{cls} 成绩] {k}: {v}")


if __name__ == "__main__":
    fire.Fire(scores)

使用示例如下:

$  python .\fire-sample.py 初三1班 --小红 98 --小李 89 --小王 100
[初三1班 成绩] 小红: 98
[初三1班 成绩] 小李: 89
[初三1班 成绩] 小王: 100

$  python .\fire-sample.py --小红 98 --小李 89 --小王 100  初三1班
[初三1班 成绩] 小红: 98
[初三1班 成绩] 小李: 89
[初三1班 成绩] 小王: 100

$  python .\fire-sample.py --小红 98 --小李 89 --小王 100 --cls 初三1班
[初三1班 成绩] 小红: 98
[初三1班 成绩] 小李: 89
[初三1班 成绩] 小王: 100

cls
参数可以不指定名称,也可以放在任意位置上。
**students
参数则必须指定参数名称,但参数名称不固定,参数的个数也不固定。

3. 总结

*args
型和
**kwargs
型参数可以有效的缓解命令行工具灵活性不足的问题。
开发命令行工具时,根据自己的场景,使用这两种类型的参数可让命令行工具的接口更加简洁灵活。

前言

这篇文章重点讲解docker compose的相关概念和命令。Compose是单机编排容器集群或者是分布式服务容器的应用工具。通过Compose,可以使用YAML文件来配置应用程序的服务。然后,使用一个命令,就可以从配置中创建并启动所有服务。

Docker-Compose是一个容器编排工具。通过一个.yml或.yaml文件,将所有的容器的部署方法、文件映射、容器端口映射等情况写在一个配置文件里,执行docker compose up命令就像执行脚本一样,一个一个的安装并部署容器。如下图所示,通过docker compose可以创建一个docker的集群。

1.使用Dockerfile创建镜像

我们可以使用Dockerfile文件,创建一个属于自己的镜像,首先是编写Dockerfile文件,然后是使用docker build命令。

可以使用nvidia的官方镜像,其中已经有了大部分的依赖包,下面是拉取pytorch镜像的命令:

官方地址:
https://catalog.ngc.nvidia.com/orgs/nvidia/containers/pytorch

docker pull nvcr.io/nvidia/pytorch:23.05-py3

下面是Dockerfile文件,在nvidia镜像基础之上,安装了一些包,同时使用了ENTRYPOINT命令在容器启动时创建了一个jupyter-lab服务:

FROM nvcr.io/nvidia/pytorch:23.05-py3
WORKDIR /workspace
USER root
EXPOSE 8888
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get upgrade -y && apt-get install git -y
# RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && pip install -r /workspace/requirements.txt
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
RUN pip install diffusers --upgrade
RUN pip3 install torch torchvision torchaudio
RUN pip install invisible_watermark transformers accelerate safetensors
RUN apt-get update
ENTRYPOINT ["jupyter-lab","--no-browser","--allow-root","--port=8888","--ip=0.0.0.0","--NotebookApp.token=123456"]

进入Dockerfile所在的目录执行build命令,并取个tag名称,注意后面有个'.'

docker build -t my-image:20240205 .

2.上传镜像到私人仓库

使用以下命令给镜像打标签:

docker tag my-image:20240205 registry.example.com/my-image:20240205

使用以下命令将镜像推送至registry.example.com私人仓库:

docker push registry.example.com/my-image:20240205

使用以下命令拉取私人仓库中的镜像:

docker pull registry.example.com/my-image:20240205

3.使用Docker Compose启动一个jupyter-lab服务

首先编写好yaml文件,也可以搭配docker-entrypoint.sh使用,因为镜像里起了jupyter-lab服务,所以下面的yaml注释了entrypoint,下面是docke-compose.yaml文件:

version: '3.9'
 
services:
  dl-train:
    image: registry.example.com/my-image:20240205
    ports:
      - "8811:8811"
    volumes:
      - "/home/lxj/workspace/models:/workspace/models"
      - "/home/lxj/workspace/src:/workspace/src"
    networks:
      - sdnet
    restart: always
    # entrypoint: ["sh","/workspace/src/jupyter-lab/docker-entrypoint.sh"]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ['0']
              capabilities: [gpu]
networks:
  sdnet:
    ipam:
      driver: default
      config:
        - subnet: 192.168.12.0/24

下面是docker-entrypoint.sh文件,可以在里面写一些pip语句安装一些包:

#!/bin/bash
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install --upgrade jupyterlab
# jupyter labextension install @jupyter-widgets/jupyterlab-manager
jupyter-lab --no-browser --allow-root --port=8811 --ip=0.0.0.0 --NotebookApp.token=123456

最后进入yaml所在的目录,运行启动命令即可:

docker compose up -d

4.Docker Compose常用命令

当我们起了一个容器后,需要对容器有一些操作,比如查看容器中服务的日志,停止容器重新启动,接下来讲一下docker compose中操作容器的常用命令:

# 运行容器
docker compose up

# 在后台运行容器
docker compose up -d

# 指定yaml在后台运行容器
docker compose -f docker-compose.prod.yaml up -d

# 查看日志
docker compose logs -f <service-name>

# 也可以直接使用容器ID查看容器日志
docker logs -f <container-id>

# 进入容器
docker attach <container-id> 不推荐,退出容器时,容器会停止
docker exec -it <container-id> /bin/bash 推荐使用

# 停止容器,并删除相应的配置,推荐使用,如果使用docker stop并不会删除容器的network和volume,使用下面的命令可以停止并删除由docker compose文件定义的容器、网络和卷
docker compose down

5.总结

以上就是docke compose单机编排容器集群的讲解,使用yaml文件可以直观明了的使用容器,不管是日常做深度学习实验起一个单容器,还是部署一整个线上服务集群,使用docker compose容器编排技术都是十分方便快捷的。

基于BiLSTM-CRF模型的分词、词性标注、信息抽取任务的详解,侧重模型推导细化以及LAC分词实践

1.GRU简介

GRU(Gate Recurrent Unit)门控循环单元,是[循环神经网络](RNN)的变种种,与 LSTM 类似通过门控单元解决 RNN 中不能长期记忆和反向传播中的梯度等问题。与 LSTM 相比,GRU 内部的网络架构较为简单。

  • GRU 内部结构

RU 网络内部包含两个门使用了更新门(update gate)与重置门(reset gate)。重置门决定了如何将新的输入信息与前面的记忆相结合,更新门定义了前面记忆保存到当前时间步的量。如果我们将重置门设置为 1,更新门设置为 0,那么我们将再次获得标准 [RNN]模型。这两个门控向量决定了哪些信息最终能作为门控循环单元的输出。这两个门控机制的特殊之处在于,它们能够保存长期序列中的信息,且不会随时间而清除或因为与预测不相关而移除。 GRU 门控结构如下图所示:

  • 更新门

更新门帮助模型决定到底要将多少过去的信息传递到未来,或到底前一时间步和当前时间步的信息有多少是需要继续传递的。

其中 Xt 为第 t 个时间步的输入向量,即输入序列 X 的第 t 个分量,它会经过一个线性变换(与权重矩阵 W(z) 相乘)。h(t-1) 保存的是前一个时间步 t-1 的信息,它同样也会经过一个线性变换。更新门将这两部分信息相加并投入到 Sigmoid 激活函数中,因此将激活结果压缩到 0 到 1 之间。以下是更新门的计算公式:

$$z_t=\sigma\left(W^{(z)} x_t+U^{(z)} h_{t-1}\right)$$

  • 重置门

重置门主要决定了到底有多少过去的信息需要遗忘。

h(t-1) 和 xt 先经过一个线性变换,再相加投入 Sigmoid 激活函数以输出激活值。

$$r_t=\sigma\left(W^{(r)} x_t+U^{(r)} h_{t-1}\right)$$

  • 当前记忆内容

在重置门的使用中,新的记忆内容将使用重置门储存过去相关的信息。

计算重置门 rt 与 Uh(t-1) 的 Hadamard 乘积,即 rt 与 Uh(t-1) 的对应元素乘积。因为前面计算的重置门是一个由 0 到 1 组成的向量,它会衡量门控开启的大小。例如某个元素对应的门控值为 0,那么它就代表这个元素的信息完全被遗忘掉。该 Hadamard 乘积将确定所要保留与遗忘的以前信息。

$$h_t^{\prime}=\tanh \left(W x_t+r_t \odot U h_{t-1}\right)$$

  • 当前时间步的最终记忆

最终记忆的计算过程中,需要使用更新门,它决定了当前记忆内容 ht' 和前一时间步 h(t-1) 中需要收集的信息。这一过程可以表示为zt 为更新门的激活结果,它同样以门控的形式控制了信息的流入。zt 与 h(t-1) 的 Hadamard乘积表示前一时间步保留到最终记忆的信息,该信息加上当前记忆保留至最终记忆的信息就等于最终门控循环单元输出的内容。

$$h_t=z_t \odot h_{t-1}+\left(1-z_t\right) \odot h_t^{\prime}$$

门控循环单元不会随时间而清除以前的信息,它会保留相关的信息并传递到下一个单元,因此它利用全部信息而避免了梯度消失问题。

1.1 基于双向BiLstm神经网络的中文分词讲解

双向LSTM(长短时记忆网络),双向LSTM是LSTM的改进版,LSTM是RNN的改进版。因此,首先需要理解RNN。

RNN的意思是,为了预测最后的结果,我先用第一个词预测,当然,只用第一个预测的预测结果肯定不精确,我把这个结果作为特征,跟第二词一起,来预测结果;接着,我用这个新的预测结果结合第三词,来作新的预测;然后重复这个过程;直到最后一个词。这样,如果输入有n个词,那么我们事实上对结果作了n次预测,给出了n个预测序列。整个过程中,模型共享一组参数。因此,RNN降低了模型的参数数目,防止了过拟合,同时,它生来就是为处理序列问题而设计的,因此,特别适合处理序列问题。

STM对RNN做了改进,使得能够捕捉更长距离的信息。但是不管是LSTM还是RNN,都有一个问题
,它是从左往右推进的,因此后面的词会比前面的词更重要,但是对于分词这个任务来说是不妥的
,因为句子各个字应该是平权的。因此出现了双向LSTM,它从左到右做一次LSTM,然后从右到左做一次LSTM,然后把两次结果组合起来。

在分词中,LSTM可以根据输入序列输出一个序列,这个序列考虑了上下文的联系,因此,可以给每个输出序列接一个softmax分类器,来预测每个标签的概率。基于这个序列到序列的思路,我们就可以直接预测句子的标签。假设每次输入$y_1-y_n$由下图所示每个输入所对应的标签为$x_1-x_n$。再抽象一点用$ x_{ij}$ 表示状态$x_i$的第j个可能值。

2. 基于字的 BiLSTM-CRF 模型

2.1 BiLSTM详解

使用基于字的 BiLSTM-CRF,主要参考的是文献 [4][5]。使用 Bakeoff-3 评测中所采用的的
BIO 标注集
,即 B-PER、I-PER 代表人名首字、人名非首字,B-LOC、I-LOC 代表地名首字、地名非首字,B-ORG、I-ORG 代表组织机构名首字、组织机构名非首字,O 代表该字不属于命名实体的一部分。如:

这里当然也可以采用更复杂的 BIOSE 标注集。

以句子为单位,将一个含有 $n$个字的句子(字的序列)记作 $x=(x_{1},x_{2},...,x_{n})$,

其中 $x_{i}$表示句子的第 $i$个字在字典中的 id,进而可以得到每个字的 one-hot 向量,维数是字典大小。

模型的第一层是 look-up 层,利用预训练或随机初始化的 embedding 矩阵将句子 $x_{i}$中的每个字由 one-hot 向量映射为低维稠密的字向量(character embedding) $x_{i}\epsilon \mathbb{R}^{d}$,$d$ 是 embedding 的维度。在输入下一层之前,设置 dropout 以缓解过拟合。

模型的第二层是双向 LSTM 层,自动提取句子特征。将一个句子的各个字的 char embedding 序列 $(x_{1},x_{2},...,x_{n})$作为双向 LSTM 各个时间步的输入,再将正向 LSTM 输出的隐状态序列 $\left ( \underset{h_{1}}{\rightarrow},\underset{h_{2}}{\rightarrow},...,\underset{h_{n}}{\rightarrow} \right )$与反向 LSTM 的 $\left ( \underset{h_{1}}{\leftarrow},\underset{h_{2}}{\leftarrow},...,\underset{h_{n}}{\leftarrow} \right )$ 在各个位置输出的隐状态进行按位置拼接 $ht = \left [ \underset{h_{t}}{\rightarrow};\underset{h_{t}}{\leftarrow} \right ]\epsilon \mathbb{R}^m$,得到完整的隐状态序列

$$({\boldsymbol h_1},{\boldsymbol h_2},...,{\boldsymbol h_n})\in\mathbb R^{n\times m}$$

在设置 dropout 后,接入一个线性层,将隐状态向量从 $m$ 维映射到 $k$维,$k$ 是标注集的标签数,从而得到自动提取的句子特征,记作矩阵 $P=({\boldsymbol p_1},{\boldsymbol p_2},...,{\boldsymbol p_n})\in\mathbb R^{n\times k}$。可以把 $\boldsymbol p_i\in\mathbb R^{k}$的每一维 $p_{ij}$都视作将字 $x_{i}$分类到第 $j$ 个标签的打分值,如果再对 $p$进行 Softmax 的话,就相当于对各个位置独立进行 $k$类分类。但是这样对各个位置进行标注时无法利用已经标注过的信息,所以接下来将接入一个 CRF 层来进行标注。

模型的第三层是 CRF 层,进行句子级的序列标注。CRF 层的参数是一个
$\mathbf{}$$\mathbf{(k+2) \times (k+2)}$
的矩阵 A ,$A_{ij}$表示的是从第 $i$个标签到第 $j$个标签的转移得分,进而在为一个位置进行标注的时候可以利用此前已经标注过的标签,之所以要加 2 是因为要为句子首部添加一个起始状态以及为句子尾部添加一个终止状态。如果记一个长度等于句子长度的标签序列 $y=(y_1,y_2,...,y_n)$,那么模型对于句子 $x$的标签等于 $y$的打分为

$$\mathbf{score(x,y)=\sum_{i=1}
{n}P_{i,y_{i}}+\sum_{i=1}
A_{y_{i-1},y_{i}}}$$

可以看出整个序列的打分等于各个位置的打分之和,而每个位置的打分由两部分得到,一部分是由 LSTM 输出的 $\boldsymbol p_i$决定,另一部分则由 CRF 的转移矩阵 $A$决定。进而可以利用 Softmax 得到归一化后的概率:

$$\mathbf{P(y|x)=\frac{\exp(score(x,y))}{\sum_{y'}\exp(score(x,y'))}}$$

模型训练时通过最大化对数似然函数,下式给出了对一个训练样本 $(x,y^{x})$ 的对数似然:

$$\log P(y
{x}|x)=score(x,y
)-\log(\sum_{y'}\exp(score(x,y')))$$

如果这个算法要自己实现的话,需要注意的是指数的和的对数要转换成 $\log\sum_i\exp(x_i) = a + \log\sum_i\exp(x_i - a)$, 在 CRF 中上式的第二项使用前向后向算法来高效计算。

模型在预测过程(解码)时使用动态规划的 Viterbi 算法来求解最优路径:$y^{*}=\arg\max_{y'}score(x,y')$

2.1 CRF 层详解

我们重点关注一下 CRF 层:

回顾一下整个模型,我们将 embedding 的字(或词或字和词的拼接)。(anyway,这些 embeddings 在训练的时候都会 fine-tuned 的),数据经过模型处理,得到所有词的 labels。

其中,数据在经过 biLSTM 后的形式如下图所示:

比如 $W_0$,BiLSTM 对应的输出是 1.5 (B-Person), 0.9 (I-Person), 0.1 (B-Organization), 0.08 (I-Organization) and 0.05 (O). 接着输入 CRF layer, .CRF layer 将选出最大分值的 labels 序列作为输出结果。

表面上,经过 BILSTM, 我们已经获得了各个词在不同 label 上的得分。比如 $w_{0}$对应 “B-Person”, 得分 (1.5), 我们仅需要选择每个词对应的最高得分的 label 最为输出就可以了,但这样得到的并非是好的结果。这也是为什么要接入 CRFlayer 的原因。

比如上图中,我们得到的结果: “I-Organization I-Person” 和 “B-Organization I-Person”. 显然是不合理的。

CRF 层可以为输出的预测标签添加一些约束以确保它们有效。 在训练过程中,CRF 层可以自动从训练数据集中学习这些约束。从而提升 labels 的准确性。

这些约束可能是:

  • 句子中第一个单词的标签应以 “B-” 或“O”开头,而不是“I-”
  • “B-label1 I-label2 I-label3 I- ...”,这样的输出中,label1,label2,label3 ...... 应该是相同的命名实体标签。 例如,“B-Person I-Person”是有效的,但 “B-Person I-Organization” 这样的预测无效。
  • “O I-label”是无效的。 一个命名实体的第一个标签应以 “B-” 而非 “I-” 开头,有效的输出模式应为“O B-label”
  • ...

CRF 层的 loss function 包含两个重要的 score,
Emission score 和 Transition score

Emission score

Emission score 通过 biLSTM 层的输出计算。

按照上图所示的输出逻辑,我们定义 label 对应的对应的 index 如下表:

定义 emission score 符号为 $\mathbf{x_{iy_j}}$ ,其中单词 $i$ 对应的输出 label 为 $y_j$ ,如上图所示,$x_{i=1,y_j=2} = x_{w_1,B-Organization} = 0.1$,表示 $w_1$的 emission score 为 0.1。

Transition score

定义 Transition score 符号为 $\mathbf{ t_{y_iy_j}}$, 表示 label 之间的转移关系,如上图所示,$t_{B-Person, I-Person} = 0.9$,表示 $B-Person \rightarrow I-Person$的 label transition 为 0.9. 我们建立
transition score matrix
来表示所有标签之间的转移关系。另外为了让
transition score matrix
是稳定的,我们额外加入 START 和 END 标签,他们分别表示句子的开始和结束。
transition score matrix 示例如下表:

从上表可以看出:

  • 句子中第一个单词的标签应以 “B-” 或“O”开头,而不是 “I-”(从“START” 到“I-Person 或 I-Organization”的转换分数非常低。)
  • “B-label1 I-label2 I-label3 I- ...”,的输出标签序列中,label1,label2,label3 ...... 应该是相同的命名实体标签。 例如,“B-Person I-Person”有效,若是 “B-Person I-Organization” 则不合理。 (从 “B-Organization” 到“I-Person”的得分仅为 0.0003,远低于其他值。)
  • “O I-label”也是无效的。 一个命名实体的第一个标签应该以 “B-” 而不是 “I-” 开头,换句话说,有效的输出模式应该是“O B-label”(我们可以看到 $t_{O,I-Person}$都非常小)
  • ....

transition score matrix 不是人为给的,它本身是模型的参数,一开始他是随机值,在模型的训练过程中,完成 fine-tuning.

CRF loss function

定义最优的 label 序列为
real path,
其得分 score 为
real path score
,其他可能的 label 序列为
possible paths。

损失函数由
real path score
和其他
possible paths
score
来构建。real path score 表示最优的 label 序列得分。该分数应该是所有 path 中的最高分。

Label Index
B-Person 0
I-Person 1
B-Organization 2
I-Organization 3
O 4
START 5
END 6

比如我们由 5 个 words 组成的 sentence,可能的路径:

    1. START B-Person B-Person B-Person B-Person B-Person END
    1. START B-Person I-Person B-Person B-Person B-Person END
  • 10) START B-Person I-Person O B-Organization O END
  • N) O O O O O O O

如果 $N$条
possible paths
分别有
score
$p_{i}$, 那么所有
paths
的所有
score
为 $P_{total} = P_1 + P_2 +...+ P_N = e^{S_1} + e^{S_2} +... + e^{S_N}$。假设训练集得出第 10 条
path

real path,$P
$_

应该在 所有 score ($P_1,P_2,...,P_N$)中占比最大。

因此损失函数可以定义为:
$$\mathbf{Loss Function = \frac{P_{RealPath}}{P_1 + P_2 +... + P_N}}$$

也许您会问,$P_{total} = P_1 + P_2 +...+ P_N = e^{S_1} + e^{S_2} +... + e^{S_N}$ 中,$s_N$是什么?换句话说,我们的
score 怎么求?

Real path score

我们之前的公式中,定义了 $e^{s_i}$表示第 $i$个
path

score
. 那么对于第 10 个 path(
the real path

“START B-Person I-Person O B-Organization O END”,
我们这样计算 $s_i$ :

原句子包含 7 个词:$w_0,w_1,w_2,w_3,w_4,w_5,w_6$。其中,$w_0,w_6$分别表示句子的开始标记
START
和结束标记
END

$S_i = EmissionScore + TransitionScore$

其中,$EmissionScore=x_{0,START}+x_{1,B-Person}+x_{2,I-Person}+x_{3,O}+x_{4,B-Organization}+x_{5,O}+x_{6,END}$。正如之前介绍的,EmissionScore 是根据 bilstm layer 的输出来统计的。例如,在我们的示意图中,$x_{1,B-Person} = 0.2$。我们可以设定 $x_{0,START}$和 $x_{6,END}$为 0。

$$TransitionScore=t_{START\rightarrow B-Person} + t_{B-Person\rightarrow I-Person} + t_{I-Person\rightarrow O} + t_{0\rightarrow B-Organization} + t_{B-Organization\rightarrow O} + t_{O\rightarrow END}$$正如之前介绍的,$t_{START\rightarrow B-Person}$ 即表示从 label $START$ 到  $B-Person$ 的 TransitionScore, 他们的值在模型训练的时候得到。

The total score of all the paths

对于一个 NER 识别任务,BILSTM 层输出的 label 有很多种,假设每一个 $w_i$ 输出的 $\mathbf{x_{iy_j}}$ 对应 $L$ 个 $y_i$。那么可能的 path 有 $L^{(i)}$个,这么多的可能,如果考虑累加来求所有
possible paths
的 $P_{total}$ 显然是不太实际的。

我们将损失函数变为 log loss function:$$LogLossFunction = \log \frac{P_{RealPath}}{P_1 + P_2 + ... + P_N}$$

因为我们的 train 目标是最小化 loss,所以我们加一个负号(目标变为最大化该函数):$$Log Loss Function = - \log \frac{P_{RealPath}}{P_1 + P_2 + ...+ P_N}$$

$$= - \log \frac{e
{S_{RealPath}}}{e
+ e^{S_2} + ... + e^{S_N}}$$

$$= - (\log(e^{S_{RealPath}}) - \log(e^{S_1} + e^{S_2} + ... + e^{S_N}))$$

$$= - (S_{RealPath} - \log(e^{S_1} + e^{S_2} + ... + e^{S_N}))$$

$$= - ( \sum_{i=1}^{N} x_{iy_i} + \sum_{i=1}^{N-1} t_{y_iy_{i+1}} - \log(e^{S_1} + e^{S_2} + ... + e^{S_N}))$$

上式中,前两项我们已经知道如何求得,我们要解决的是

$log(e^{S_1} + e^{S_2} + ... + e^{S_N})$

假设我们的训练数据是一个只有 3 个 word 的句子:$\mathbf{x} = [w_0, w_1, w_2]$,输出 2 个 label:$LabelSet = {l_1,l_2}$,Emission scores 对应如下表:

CRF Layer 中, Transition scores 对应如下表:

我们的计算过程应该是这样的,先计算到 $w_0$的所有 possible paths(也就是 $p_0$), 在此基础上计算 $w_0 \rightarrow w_1$的 possible paths,最后计算 $w_0 \rightarrow w_1\rightarrow w_2$ 的 possible paths。


previous
为上一步的结果 result,
obs
为当前 word 对应的的信息。我们开始计算 $$log(e^{S_1} + e^{S_2} + ... + e^{S_N})$$

$w_0$

$$obs = [x_{01}, x_{02}] \ \ previous = None$$

$w_0$ 前面没有产生 result,故
previous
为 None,$w_0$ 对应的观测值有 $x_{01}, x_{02}$,我们看到,每一个此时没有 Transition scores,只有 EmissionScore,因此 $w_0$ 的所有 possible paths score:

$$TotalScore(w_0)=\log (e^{s_{START\rightarrow 1}} + e^{s_{START\rightarrow 2}})=\log (e^{x_{01}} + e^{x_{02}})$$

$$w_0 \rightarrow w_1$$

$$obs = [x_{11}, x_{12}]\previous=[\log(e
{x_{01}}),\log(e
{x_{02}})] = [x_{01}, x_{02}]$$

  • 展开
    previous:

$previous = \begin{pmatrix} x
&x_{01}\ x_{02}&x_{02} \end{pmatrix}$_

  • 展开
    obs:

$obs = \begin{pmatrix} x
&x_{12}\ x_{11}&x_{12} \end{pmatrix}$_

  • 将前一步($w_0$)的 result 当前的 EmissionScore,TransitionScore 相加:

$$scores = \begin{pmatrix} x_{01}&x_{01}\ x_{02}&x_{02} \end{pmatrix} + \begin{pmatrix} x_{11}&x_{12}\ x_{11}&x_{12} \end{pmatrix} + \begin{pmatrix} t_{11}&t_{12}\ t_{21}&t_{22} \end{pmatrix}$$

$$= \begin{pmatrix} x_{01}+x_{11}+t_{11}&x_{01}+x_{12}+t_{12}\ x_{02}+x_{11}+t_{21}&x_{02}+x_{12}+t_{22} \end{pmatrix}$$

  • 构建下一步的 previous:

$$previous=[\log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}}), \log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}})]$$

  • 依照前计算 $w_0$的 totalScore,我们有:

$$TotalScore(w_0 \rightarrow w_1)=\log (e^{previous[0]} + e^{previous[1]})\=\log (e
{\log(e
+x_{11}+t_{11}} + e
{x_{02}+x_{11}+t_{21}})}+e
+x_{12}+t_{12}} + e
{x_{02}+x_{12}+t_{22}})}\=\log(e
+x_{11}+t_{11}}+e
{x_{02}+x_{11}+t_{21}}+e
+x_{12}+t_{12}}+e^{x_{02}+x_{12}+t_{22}})$$

我们可以看出上式中, S 与 path 的对应关系:

$$S_1 = x_{01}+x_{11}+t_{11} \ ($label_1\rightarrow label_1$)\ S_2 = x_{02}+x_{11}+t_{21} \ ($label_2\rightarrow label_1$)\ S_3 = x_{01}+x_{12}+t_{12} \ ($label_1\rightarrow label_2$)\ S_4 = x_{02}+x_{12}+t_{22} \ ($label_2\rightarrow label_2$)\$$

$$w_0 \rightarrow w_1\rightarrow w_2$$

$$obs = [x_{21}, x_{22}]\previous=[\log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}}), \log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}})]$$

  • 展开
    previous:

$$previous = \begin{pmatrix} \log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}})&\log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}})\ \log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}})&\log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}}) \end{pmatrix}$$

  • 展开
    obs:

$obs = \begin{pmatrix} x
&x_{22}\ x_{21}&x_{22} \end{pmatrix}$_

  • 将前一步的 result 当前的 EmissionScore,TransitionScore 相加:

$$scores = \begin{pmatrix} \log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}})&\log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}})\ \log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}})&\log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}}) \end{pmatrix} + \begin{pmatrix} x_{21}&x_{22}\ x_{21}&x_{22} \end{pmatrix} + \begin{pmatrix} t_{11}&t_{12}\ t_{21}&t_{22} \end{pmatrix}\= \begin{pmatrix} \log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}}) + x_{21} + t_{11} &\log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}}) + x_{22} + t_{12}\ \log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}}) + x_{21} + t_{21} &\log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}}) + x_{22} + t_{22} \end{pmatrix}$$

  • 构建下一步的 previous:

$$previous =[\ log( e^{\log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}}) + x_{21} + t_{11}} + e^{\log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}}) + x_{21} + t_{21}}),\\log( e^{\log (e^{x_{01}+x_{11}+t_{11}} + e^{x_{02}+x_{11}+t_{21}}) + x_{22} + t_{12}} +e^{\log (e^{x_{01}+x_{12}+t_{12}} + e^{x_{02}+x_{12}+t_{22}}) + x_{22} + t_{22}})]\ \=[\log((e^{x_{01}+x_{11}+t_{11}} + e
{x_{02}+x_{11}+t_{21}})e
+ t_{11}}+(e^{x_{01}+x_{12}+t_{12}} + e
{x_{02}+x_{12}+t_{22}})e
+ t_{21}}),\\log(e^{x_{01}+x_{11}+t_{11}} + e
{x_{02}+x_{11}+t_{21}})e
+ t_{12}}+(e^{x_{01}+x_{12}+t_{12}} + e
{x_{02}+x_{12}+t_{22}})e
+ t_{22}})]$$

$$TotalScore(w_0 \rightarrow w_1 rightarrow w_2)= \ \log (e^{previous[0]} + e^{previous[1]})= \ \log (e
{\log((e
+x_{11}+t_{11}} + e
{x_{02}+x_{11}+t_{21}})e
+ t_{11}}+(e^{x_{01}+x_{12}+t_{12}} + e
{x_{02}+x_{12}+t_{22}})e
+ t_{21}})}+e
{\log((e
+x_{11}+t_{11}} + e
{x_{02}+x_{11}+t_{21}})e
+ t_{12}}+(e^{x_{01}+x_{12}+t_{12}} + e
{x_{02}+x_{12}+t_{22}})e
+ t_{22}})}) \=\log (e^{x_{01}+x_{11}+t_{11}+x_{21}+t_{11}}+ \e^{x_{02}+x_{11}+t_{21}+x_{21}+t_{11}}+ \e^{x_{01}+x_{12}+t_{12}+x_{21}+t_{21}}+ \e^{x_{02}+x_{12}+t_{22}+x_{21}+t_{21}}+ \e^{x_{01}+x_{11}+t_{11}+x_{22}+t_{12}}+ \e^{x_{02}+x_{11}+t_{21}+x_{22}+t_{12}}+ \e^{x_{01}+x_{12}+t_{12}+x_{22}+t_{22}}+ \e^{x_{02}+x_{12}+t_{22}+x_{22}+t_{22}})$$

class My_CRF():
	def __init__():
		#[Initialization]
		'''
		Randomly initialize transition scores
	    '''
	def __call__(training_data_set):
		#[Loss Function]
		Total Cost = 0.0
		
		#Compute CRF Loss
		'''
		for sentence in training_data_set:
			1) The real path score of current sentence according the true labels
			2) The log total score of all the possbile paths of current sentence
			3) Compute the cost on this sentence using the results from 1) and 2)
			4) Total Cost += Cost of this sentence
		'''
		return Total Cost
	
	def argmax(new sentences):
		#[Prediction]
		'''
		Predict labels for new sentences
        '''

3.词法分析 LAC实战

词法分析任务的输入是一个字符串(我们后面使用『句子』来指代它),而输出是句子中的词边界和词性、实体类别。序列标注是词法分析的经典建模方式,我们使用基于 GRU 的网络结构学习特征,将学习到的特征接入 CRF 解码层完成序列标注。模型结构如下所示

  1. 输入采用 one-hot 方式表示,每个字以一个 id 表示
  2. one-hot 序列通过字表,转换为实向量表示的字向量序列;
  3. 字向量序列作为双向 GRU 的输入,学习输入序列的特征表示,得到新的特性表示序列,我们堆叠了两层双向 GRU 以增加学习能力;
  4. CRF 以 GRU 学习到的特征为输入,以标记序列为监督信号,实现序列标注。

3.1 数据准备

提供了少数样本用以示例输入数据格式。执行以下命令,下载并解压示例数据集:

python download.py --data_dir ./

训练使用的数据可以由用户根据实际的应用场景,自己组织数据。除了第一行是
text_a\tlabel
固定的开头,后面的每行数据都是由两列组成,以制表符分隔,第一列是 utf-8 编码的中文文本,以
\002
分割,第二列是对应每个字的标注,以
\002
分隔。我们采用 IOB2 标注体系,即以 X-B 作为类型为 X 的词的开始,以 X-I 作为类型为 X 的词的持续,以 O 表示不关注的字(实际上,在词性、专名联合标注中,不存在 O )。示例如下:

除\002了\002他\002续\002任\002十\002二\002届\002政\002协\002委\002员\002,\002马\002化\002腾\002,\002雷\002军\002,\002李\002彦\002宏\002也\002被\002推\002选\002为\002新\002一\002届\002全\002国\002人\002大\002代\002表\002或\002全\002国\002政\002协\002委\002员    p-B\002p-I\002r-B\002v-B\002v-I\002m-B\002m-I\002m-I\002ORG-B\002ORG-I\002n-B\002n-I\002w-B\002PER-B\002PER-I\002PER-I\002w-B\002PER-B\002PER-I\002w-B\002PER-B\002PER-I\002PER-I\002d-B\002p-B\002v-B\002v-I\002v-B\002a-B\002m-B\002m-I\002ORG-B\002ORG-I\002ORG-I\002ORG-I\002n-B\002n-I\002c-B\002n-B\002n-I\002ORG-B\002ORG-I\002n-B\002n-I

其中词性和专名类别标签集合如下表,包含词性标签 24 个(小写字母),专名类别标签 4 个(大写字母)。这里需要说明的是,人名、地名、机构名和时间四个类别,存在(PER / LOC / ORG / TIME 和 nr / ns / nt / t)两套标签,被标注为第二套标签的词,是模型判断为低置信度的人名、地名、机构名和时间词。开发者可以基于这两套标签,在四个类别的准确、召回之间做出自己的权衡。

标签 含义 标签 含义 标签 含义 标签 含义
n 普通名词 f 方位名词 s 处所名词 t 时间
nr 人名 ns 地名 nt 机构名 nw 作品名
nz 其他专名 v 普通动词 vd 动副词 vn 名动词
a 形容词 ad 副形词 an 名形词 d 副词
m 数量词 q 量词 r 代词 p 介词
c 连词 u 助词 xc 其他虚词 w 标点符号
PER 人名 LOC 地名 ORG 机构名 TIME 时间

3.2 模型训练

  • 单卡训练

启动方式如下:

python train.py \
        --data_dir ./lexical_analysis_dataset_tiny \
        --model_save_dir ./save_dir \
        --epochs 10 \
        --batch_size 32 \
        --device gpu \
        # --init_checkpoint ./save_dir/final

其中参数释义如下:

  • data_dir
    : 数据集所在文件夹路径.
  • model_save_dir
    : 训练期间模型保存路径。
  • epochs
    : 模型训练迭代轮数。
  • batch_size
    : 表示每次迭代
    每张卡
    上的样本数目。
  • device
    : 训练使用的设备, 'gpu'表示使用GPU, 'xpu'表示使用百度昆仑卡, 'cpu'表示使用CPU。
  • init_checkpoint
    : 模型加载路径,通过设置init_checkpoint可以启动增量训练。
  • 多卡训练

启动方式如下:

python -m paddle.distributed.launch --gpus "0,1"  train.py \
        --data_dir ./lexical_analysis_dataset_tiny \
        --model_save_dir ./save_dir \
        --epochs 10 \
        --batch_size 32 \
        --device gpu \
        # --init_checkpoint ./save_dir/final

3.3 模型评估

通过加载训练保存的模型,可以对测试集数据进行验证,启动方式如下:

python eval.py --data_dir ./lexical_analysis_dataset_tiny \
        --init_checkpoint ./save_dir/model_100.pdparams \
        --batch_size 32 \
        --device gpu

其中
./save_dir/model_100.pdparams
是训练过程中保存的参数文件,请更换为实际得到的训练保存路径。

3.4模型导出

使用动态图训练结束之后,还可以将动态图参数导出成静态图参数,具体代码见export_model.py。静态图参数保存在
output_path
指定路径中。

运行方式:

python export_model.py --data_dir=./lexical_analysis_dataset_tiny --params_path=./save_dir/model_100.pdparams --output_path=./infer_model/static_graph_params

其中
./save_dir/model_100.pdparams
是训练过程中保存的参数文件,请更换为实际得到的训练保存路径。

  • params_path
    是指动态图训练保存的参数路径
  • output_path
    是指静态图参数导出路径。

导出模型之后,可以用于部署,deploy/predict.py文件提供了python部署预测示例。运行方式:

python deploy/predict.py --model_file=infer_model/static_graph_params.pdmodel --params_file=infer_model/static_graph_params.pdiparams --data_dir lexical_analysis_dataset_tiny

3.5 模型预测

对无标签数据可以启动模型预测:

python predict.py --data_dir ./lexical_analysis_dataset_tiny \
        --init_checkpoint ./save_dir/model_100.pdparams \
        --batch_size 32 \
        --device gpu

得到类似以下输出:

(大学, n)(学籍, n)(证明, n)(怎么, r)(开, v)
(电车, n)(的, u)(英文, nz)
(什么, r)(是, v)(司法, n)(鉴定人, vn)
  • Taskflow一键预测
    可以使用PaddleNLP提供的Taskflow工具来对输入的文本进行一键分词,具体使用方法如下:
from paddlenlp import Taskflow

lac = Taskflow("lexical_analysis")
lac("LAC是个优秀的分词工具")
'''
[{'text': 'LAC是个优秀的分词工具', 'segs': ['LAC', '是', '个', '优秀', '的', '分词', '工具'], 'tags': ['nz', 'v', 'q', 'a', 'u', 'n', 'n']}]
'''

lac(["LAC是个优秀的分词工具", "三亚是一个美丽的城市"])
'''
[{'text': 'LAC是个优秀的分词工具', 'segs': ['LAC', '是', '个', '优秀', '的', '分词', '工具'], 'tags': ['nz', 'v', 'q', 'a', 'u', 'n', 'n']},
 {'text': '三亚是一个美丽的城市', 'segs': ['三亚', '是', '一个', '美丽', '的', '城市'], 'tags': ['LOC', 'v', 'm', 'a', 'u', 'n']}
]
'''

任务的默认路径为
$HOME/.paddlenlp/taskflow/lexical_analysis/lac/
,默认路径下包含了执行该任务需要的所有文件。

如果希望得到定制化的分词及标注结果,用户也可以通过Taskflow来加载自定义的词法分析模型并进行预测。

通过
task_path
指定用户自定义路径,自定义路径下的文件需要和默认路径的文件一致。

自定义路径包含如下文件(用户自己的模型权重、标签字典):

custom_task_path/
├── model.pdparams
├── word.dic
├── tag.dic
└── q2b.dic

使用Taskflow加载自定义模型进行一键预测:

from paddlenlp import Taskflow

my_lac = Taskflow("lexical_analysis", model_path="./custom_task_path/")

参考链接:

GRU:
https://blog.csdn.net/weixin_44750512/article/details/128856846

【NLP】命名实体识别(NER)的BiLSTM-CRF模型
https://blog.csdn.net/b285795298/article/details/100764066

从RNN到LSTM到GRU---crf---bilstm+crf如何命名实体识别的流程:
https://blog.csdn.net/qq_34243930/article/details/88778681

基于双向BiLstm神经网络的中文分词详解及:
https://www.cnblogs.com/vipyoumay/p/8608754.html

Lexical Analysis of Chinese

PaddleHub分词模型

[2] 向晓雯. 基于条件随机场的中文命名实体识别 [D]. , 2006.

[3] 张祝玉, 任飞亮, 朱靖波. 基于条件随机场的中文命名实体识别特征比较研究 [C]// 第 4 届全国信息检索与内容安全学术会议论文集. 2008.

[4] Huang Z, Xu W, Yu K. Bidirectional LSTM-CRF models for sequence tagging[J]. arXiv preprint arXiv:1508.01991, 2015.

[5] Lample G, Ballesteros M, Subramanian S, et al. Neural Architectures for Named Entity Recognition[C]//Proceedings of NAACL-HLT. 2016: 260-270.

[6] Collobert R, Weston J, Bottou L, et al. Natural language processing (almost) from scratch[J]. Journal of Machine Learning Research, 2011, 12(Aug): 2493-2537.

[7] Ma X, Hovy E. End-to-end sequence labeling via bi-directional lstm-cnns-crf[J]. arXiv preprint arXiv:1603.01354, 2016.

[8] Dong C, Zhang J, Zong C, et al. Character-Based LSTM-CRF with Radical-Level Features for Chinese Named Entity Recognition[C]//International Conference on Computer Processing of Oriental Languages. Springer International Publishing, 2016: 239-250.

本文全面介绍了云计算中的弹性计算服务(ECS),从基础概念、核心组件到选型考虑、实践案例和高级优化实践。文章不仅提供了丰富的技术细节和实际例证,还提出了独特的洞见,如数据作为核心、自动化的重要性、多维度的性能优化,以及高可用与灾备的不可或缺性。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

一、引言

file

在过去的十年里,云计算从一个前沿概念发展为企业和开发者的必备工具。传统的计算模型通常局限于单一的、物理的位置和有限的资源,而云计算则通过分布式的资源和服务,为计算能力带来了前所未有的"弹性"。

弹性:不仅仅是扩展性

很多人误以为弹性仅仅是可扩展性的同义词,但实际上它更加复杂。考虑一个电商网站在“黑色星期五”这一天的流量。流量不仅在这一天大幅增加,而且在不同的时段也有波动。弹性计算不仅能垂直或水平地扩展资源以应对高流量,还能在需求减少时相应地缩小,从而节省成本。AWS的自动缩放(Auto Scaling)就是一个很好的例子,它可以根据实时的需求自动增加或减少EC2实例。

技术与商业价值

弹性计算不仅在技术层面提供了便利,还在商业层面具有重要的价值。例如,Netflix使用弹性计算来应对每天晚上的流量高峰,这不仅提高了用户体验,也优化了资源使用,从而达到了成本效益的平衡。

通过这篇文章,您将能全面了解弹性计算服务的各个方面,包括但不限于其核心组件、架构选择、实践案例以及高级优化技术。


二、基础概念

file
在深入探讨弹性计算服务(ECS)的各个方面之前,了解其基础概念是非常重要的。本节将全面介绍弹性计算的核心概念,包括其定义、类型、优势与挑战。

什么是弹性计算?

弹性计算是一种计算模型,它允许系统根据需要动态地分配和回收计算资源。与传统的、固定的硬件资源不同,弹性计算能够迅速适应业务或应用的不断变化的需求。

CPU与内存的动态分配

考虑一个在线视频流平台,例如YouTube。在日常使用中,该平台可能需要大量的CPU资源来编解码视频,而在用户搜索或浏览内容时,则可能需要更多的内存资源。弹性计算允许这种类型的平台动态地调整CPU和内存资源,以优化服务性能。

与虚拟化的关系

虚拟化技术是实现弹性计算的一种关键手段。它允许多个虚拟机(VMs)在单个物理服务器上共享资源,而每个VM都可以根据需求动态地调整其资源配置。例如,VMware的vSphere平台提供了广泛的资源管理功能,包括CPU和内存的弹性分配。

类型

弹性计算不是单一的,而是包括多种类型和模式。

公有云与私有云

公有云服务,如AWS、Azure和Google Cloud Platform,提供了全面的弹性计算解决方案。然而,对于需要更高安全性或自定义性的场景,私有云也是一个可行的选项。例如,OpenStack提供了一套开源的软件平台,用于构建私有云环境。

虚拟机、容器与无服务器

虚拟机(VM)是最常见的弹性计算类型,但容器(如Docker)和无服务器(如AWS Lambda)也日渐流行。容器比虚拟机更轻量级,而无服务器则可以让开发者完全专注于代码,无需管理底层资源。

优势与挑战

弹性计算的优势和挑战是多维度和相互关联的。

优势

  1. 成本效益
    : 通过动态分配资源,企业可以避免购买和维护过多的硬件,从而节省成本。
  2. 灵活性
    : 弹性计算允许企业快速适应市场需求,无论是扩大规模还是收缩。

挑战

  1. 复杂性
    : 虽然弹性计算提供了高度的灵活性,但它也带来了更多的复杂性。例如,资源的动态分配可能会导致性能问题或安全风险。
  2. 成本控制
    : 在公有云环境中,未经妥善管理的弹性计算资源可能会导致意外的高额费用。

实例:Netflix的弹性计算

Netflix是一个广为人知的流媒体服务提供商,也是弹性计算的典型应用案例。通过使用AWS的弹性计算资源,Netflix能够在每天的不同时间自动调整其服务能力,以满足全球数百万用户的需求。这不仅提高了用户体验,也优化了资源利用率。

通过本节,您应该对弹性计算的基础概念有了全面的了解。接下来的章节将进一步深入探讨这一主题,包括其核心组件、选型考虑因素,以及实践案例等。


三、核心组件与架构

I
file
弹性计算服务(ECS)的成功实现不仅依赖于基础概念和类型,还需要一套精心设计的组件和架构。本节将详细介绍弹性计算的核心组件和推荐的架构模式。

核心组件

在弹性计算环境中,以下几个核心组件是不可或缺的。

计算节点(Compute Nodes)

计算节点是执行实际计算任务的服务器或虚拟机。这些节点通常具有多种配置选项,包括CPU、内存、存储和网络。

AWS EC2实例类型

AWS提供了多种EC2实例类型,从用于通用计算的
t2.micro
,到用于高性能计算(HPC)的
p3.16xlarge
,都可以根据应用需求灵活选择。

资源调度器(Resource Scheduler)

资源调度器负责在计算节点之间分配任务和资源。它需要考虑多个因素,包括但不限于任务优先级、资源需求和可用性。

Kubernetes调度器

Kubernetes是一个开源的容器编排平台,其内置的调度器可以根据预定义的规则和策略,自动地将容器分配到合适的节点上。

存储服务(Storage Services)

存储服务通常包括块存储、文件存储和对象存储等,以满足不同类型和规模的数据需求。

例子:Amazon S3和EBS

Amazon S3是一个对象存储服务,适用于存储大量的非结构化数据。EBS(Elastic Block Store)则提供了持久的块存储,适用于数据库或文件系统。

网络组件(Network Components)

网络组件包括负载均衡器、API网关、DNS等,它们共同确保数据能够在各个组件和服务之间有效地传输。

例子:AWS ELB和API Gateway

AWS的Elastic Load Balancer(ELB)可以自动分配进入的应用流量,以实现更高的可用性和容错能力。API Gateway则用于管理、维护和监控API接口。

推荐架构

在构建弹性计算环境时,有几种架构模式值得推荐。

微服务架构

微服务架构通过将应用拆分为多个独立的服务,使其更易于扩展和维护。这种架构模式非常适用于弹性计算环境。

事件驱动架构

事件驱动架构适用于高吞吐量、低延迟的应用场景。这种架构允许系统组件通过事件进行异步通信,从而提高了系统的响应能力和弹性。

多层架构

多层架构通常包括表现层、业务逻辑层和数据访问层。这种模式适用于传统的企业应用,但也可以通过合理的设计和优化,实现较高的弹性和可扩展性。

实例:Netflix的微服务架构

Netflix使用了基于微服务的架构,这使得它能够快速地发布新功能,并独立地扩展各个服务。通过AWS的多种弹性计算和存储服务,Netflix实现了高可用性和全球范围内的服务部署。

通过本节,您应该对弹性计算的核心组件和推荐的架构模式有了全面的了解。这将为您在实际应用中做出明智的技术选择和决策提供有力的支持。


四、选型与考虑因素

file
选型是构建弹性计算服务(ECS)方案的一个关键步骤。不同的业务和应用场景需要不同类型的计算、存储和网络资源。因此,选型的过程需细致地考虑多个因素。本节将探讨这些关键考虑因素,并通过实例加以说明。

计算需求

了解您的计算需求是选型过程中的第一步。这包括CPU、内存、GPU等硬件资源的需求。

数据分析任务

如果您的应用主要是进行数据分析,那么您可能需要更多的内存和高速的CPU。某些特定任务,如机器学习训练,还可能需要GPU支持。

存储需求

存储需求不仅包括数据的容量,还涉及数据的访问速度、持久性和安全性。

电子商务应用

一个电子商务网站可能需要高IOPS(输入/输出操作每秒)的存储来支持数据库操作,以及低成本的对象存储来存储产品图片和日志文件。

网络需求

网络需求包括带宽、延迟和连接数等。理解这些需求有助于选择合适的网络解决方案。

实时游戏服务

一个实时多人在线游戏可能需要低延迟和高吞吐量的网络,以确保游戏体验。

可用性与灾备

系统的可用性和灾备能力也是需要考虑的重要因素。这通常涉及多区域部署和数据备份策略。
金融交易平台

对于一个金融交易平台来说,高可用性是至关重要的。它可能需要在多个地理位置部署服务,并实施实时数据备份和快速故障恢复机制。

成本因素

最后,但同样重要的是成本因素。这包括硬件、软件、运维以及其他潜在的费用。

初创公司

对于初创公司来说,预算可能是一个关键的限制因素。因此,选择一种可以按需付费或具有免费层的弹性计算解决方案可能更为合适。

实例:Airbnb的选型策略

Airbnb是一个全球性的短租平台,它的服务需要能够处理大量的用户请求和数据交互。为了满足这些需求,Airbnb选择了一种混合云策略,结合了公有云和私有云的优点。这种策略允许它根据业务需要灵活地扩展计算和存储资源,同时也确保了数据的安全和合规性。

通过本节的讨论,您应该对如何进行弹性计算选型有了更全面和深入的理解。这将帮助您更准确地评估不同选项,并做出更合适的决策。


五、实践案例

理论知识和选型策略都是非常重要的,但真正的理解往往来源于实践。在本节中,我们将通过几个实践案例深入地探讨如何在不同的场景和需求下应用弹性计算服务(ECS)。

实时数据处理:Twitter

Twitter每天需要处理数十亿条推文和相关的用户互动,这对计算和存储资源提出了巨大的挑战。

架构与组件

Twitter使用了一种混合的微服务和事件驱动架构。其核心组件包括:

  • Storm
    :用于实时数据流处理。
  • Memcached
    :用于缓存频繁读取的数据。
  • Manhattan
    :Twitter自己开发的实时、多租户分布式数据库。

成功要素

  • 水平可扩展性
    :通过添加更多的计算节点来应对流量峰值。
  • 数据一致性
    :使用分布式事务确保数据的一致性。

电子商务:Amazon

Amazon是全球最大的电子商务平台之一,它需要处理复杂的商品目录、用户行为以及交易处理。

架构与组件

Amazon使用了微服务架构,主要包括以下几个部分:

  • DynamoDB
    :提供高可用性和扩展性的NoSQL数据库。
  • EC2
    :用于运行各种应用和服务的计算实例。
  • S3
    :用于存储商品图片和日志数据。

成功要素

  • 高可用性
    :通过多区域部署和负载均衡实现。
  • 数据分析
    :使用各种大数据工具进行用户行为分析和商品推荐。

移动应用:Pokemon Go

Pokemon Go是一款非常流行的移动游戏,其成功的关键在于能够处理大量的并发用户和实时交互。

架构与组件

  • Kubernetes
    :用于容器编排和资源管理。
  • Google Cloud Endpoints
    :用于处理API请求。
  • Bigtable
    :用于存储游戏状态和用户数据。

成功要素

  • 全球范围的部署
    :通过全球的数据中心实现低延迟和高可用性。
  • 动态资源分配
    :根据实时的用户需求动态调整资源。

通过以上实践案例,我们不仅可以看到弹性计算在不同场景下的应用,还可以了解到成功实施弹性计算所需考虑的多个方面。这些案例为如何在具体的业务场景中应用弹性计算提供了有价值的参考。


六、高级实践与优化

在实施弹性计算服务(ECS)后,下一步就是如何优化和进一步提升系统性能。这里,我们将涵盖高级实践和优化策略,通过实际例子展示这些高级技巧的效用。

性能调优

性能调优是任何计算环境中都不可或缺的一环。从操作系统到应用层,性能可以在多个维度进行优化。

例子:Netflix的性能调优

Netflix使用了多种策略来优化其流媒体服务的性能,其中包括:

  • 动态内容缓存
    :利用近源缓存来减少延迟。
  • 自适应比特率
    :根据用户的网络条件动态调整视频质量。

自动化与基础设施即代码

基础设施即代码(IAC)是一种将基础设施配置编码化的实践,可以显著提高自动化程度。

例子:Terraform与AWS

通过使用Terraform,可以用代码的形式描述AWS上的基础设施,从而实现一键部署和自动化管理。

数据分片与负载均衡

在大规模并发访问下,如何将数据和流量合理分布到各个节点是一项关键的技术挑战。

例子:YouTube的数据分片

YouTube使用了范围分片(range sharding)来分布视频数据,确保用户在任何地点都能快速访问到视频。

服务网格与微服务架构

服务网格是一种用于管理微服务交互的基础设施层。它可以处理服务之间的通信、监控、安全性等问题。

例子:Istio与Kubernetes

Istio是一个开源的服务网格,它与Kubernetes紧密集成,提供了丰富的微服务管理功能,包括流量路由、故障注入和安全策略。

灾备与高可用

构建高可用的系统并实施灾备计划是高级实践中非常关键的一环。

例子:金融系统的多活部署

一些高级金融交易系统使用多活(Multi-Active)部署模式,即在多个地理位置同时运行服务,确保即使某一地点出现问题,系统仍能保持运行。

通过深入探讨这些高级实践和优化策略,你不仅能够提升你的弹性计算系统的性能和可靠性,还可以更有效地管理和运维你的基础设施。这一切都将有助于你构建出更加健壮、高效和可扩展的解决方案。


七、总结

云计算和弹性计算服务(ECS)已经深刻地改变了我们构建和运行应用的方式。从基础概念、核心组件,到选型考虑、实践案例和高级优化实践,每一环节都有其独特的挑战和机会。但在这个多元复杂的技术领域中,一些共通的主题和洞见仍然显而易见。

数据作为核心

无论是实时数据处理、电子商务还是移动应用,数据始终是系统架构和优化的核心。这不仅限于数据存储,还包括数据的实时处理、分析和传输。

自动化的重要性

无论是基础设施即代码,还是服务网格在微服务架构中的应用,自动化是提高效率、减少错误和实现可扩展性的关键。而这一切都依赖于先进的软件工具和明智的架构选择。

高可用与灾备不可或缺

高级实践中,高可用和灾备是必不可少的。任何一个环节的失效都可能导致整个服务的不可用。因此,全面的灾备计划和高可用性设计是任何成功的弹性计算实践的基础。

多维度的性能优化

性能优化不仅是单一维度的工作。从底层的硬件配置、操作系统优化,到应用层的算法优化,甚至包括多地域、多云环境下的复杂优化,都需要综合多个因素来考虑。

在本文中,我们尝试通过多维度和深度来探讨弹性计算服务。希望这些内容能帮助你更全面、更深入地理解这一领域,从而更加明智地做出决策和实践。毕竟,弹性计算不仅仅是一项技术或工具,它更是一种思维方式,需要我们不断地学习、优化和创新。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。
如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。