---
type: slides
---

# 模型的训练和更新

Notes: 欢迎来到最后一章，我们来学习现代自然语言处理最激动人心的部分：训练我们自
己的模型！

本节课中，我们要学习训练和更新 spaCy 的流程组件和其中的神经网络模型，以及其相应
的数据，我们主要专注于命名实体识别器这一部分。

---

# 为什么要更新模型？

- 对特定领域有更好的结果
- 对特定问题学习特定的分类类别
- 对文本分类非常必要
- 对命名实体识别非常有用
- 对词性标注和依存关系识别不是很关键

Notes: 我们介绍 _如何_ 更新模型之前先花一点时间问问我们自己：为什么我们想要用我
们自己的例子来更新模型？我们是不是只用预训练好的流程就可以了？

统计模型可以基于相应的训练数据来做预测。

给模型更多我们自己特定领域的数据通常可以让模型预测更准确。

我们经常需要预测一些和我们特定问题相关的类别，所以模型也需要学习。

更新模型对文本分类十分必要，对实体识别也非常有用，但对词性标注和依存句法分析就没
有那么关键了。

---

# 如何训练(1)

1. **初始化**模型权重使之变为随机值
2. **预测**几个例子，看看当前权重的表现
3. **比较**预测结果和真实标注的标签
4. **计算**如何调整权重来改善预测结果
5. **微调**模型权重
6. 重复步骤 2。

Notes: spaCy 支持用更多的例子来更新现有模型训练新模型。如果我们不是用一个预训练
模型做初始模型的话，我们首先需要随机化所有的权重。

然后 spaCy 调用`nlp.update`，这个方法使用当前权重来预测一批次的例子。

模型然后把预测结果和正确答案做比较，决定下一步应该如何改变模型权重来使得下一次的
预测结果表现更好。

最后我们对当前权重做出微调，然后在下一个批次的例子上做预测。

spaCy 然后对数据中的每一批例子调用`nlp.update`。在训练过程中，我们通常要多次遍历
数据进行训练，直到模型不再变得更好。

---

# 如何训练(2)

<img src="/training_zh.png" alt="训练流程图表" />

- **训练数据：**例子及其标注
- **文本：**输入文本，模型会在这些文本上做标签预测。
- **标签：**模型需要预测的标签
- **梯度：**权重如何微调

Notes: 我们来看看模型训练的过程。

训练数据是我们想要用来更新模型的输入例子。

文本应该是一个句子、一个段落或者更长的文档。要得到最好的结果，这些文本应该尽可能
与模型将要在生产环境中预测的文本格式相似。

标签是我们想让模型预测的结果。标签可以是一个文本类别，或者是一个实体的跨度 span
及其类别。

梯度决定了我们应该如何微调模型来减少当前的错误。我们通过比较模型预测标签和真实标
签来计算梯度。

训练结束后，我们就可以保存更新过的模型，在实际应用中使用它了。

---

# 举例：训练实体识别器

- 实体识别器可以对词语和短语做基于语境的实体识别
- 每一个词符只能是某一个实体的一部分
- 训练例子需要带有语境

```python
doc = nlp("iPhone X就要来了")
doc.ents = [Span(doc, 0, 8, label="GADGET")]
```

- 并非实体的文本部分也非常重要

```python
doc = nlp("我急需一部新手机，给点建议吧！")
doc.ents = []
```

- **目标：**让模型学会泛化

Notes: 我们来看一个例子，专注于一个特定的组件：实体识别器。

实体识别器读入一个文档，预测其中的短语及其 _语境中_ 的标签。这意味着训练数据需要
有文本、包含的实体以及实体的标签。

实体之间不能重叠，所以一个词符只能属于一个实体。

最简单的方法就是给模型输入一段文本和一个字符位置的列表。spaCy 可以通过标准的含有
标注为`doc.ents`的实体`Doc`对象来进行更新。比如"iPhone X"是一个电子产品，是从字
符 0 开始到字符 8 结束的。

让模型知道哪些词 _并不是_ 实体也是非常重要的。

这种情况下，span 标注的列表会是空的。

我们的目标是教会模型在相似语境中识别出新的实体，就算这些实体并没有在训练数据中出
现过。

---

# 训练数据

- 一系列我们希望模型在语境中做出预测的例子
- 更新**现有模型**：需要几百到几千个例子
- 训练**新的类别**：需要几千乃至几百万个例子
  - spaCy 的中文模型是在 200 万个词语的语料规模上训练的
- 通常需要由标注师人工标注数据
- 也可以是半自动的，比如用 spaCy 的模板匹配器`Matcher`！

Notes: 训练数据告诉模型我们想要预测什么。预测目标可能是想要识别的文本和命名实体
，或者是词符及其正确的词性标签，或是任何模型可以预测的结果。

要更新一个现有的模型，我们可以先试试几百到几千数据量的例子。

要训练一个新的类别我们可能需要百万级别的训练数据。

spaCy 的训练好的中文流程是在 200 万个词汇的语料上训练的，这些文本都已经标注了词
性标签、依存关系和命名实体。

训练数据通常是由训练师手动标注文本创建的。

数据标注是一项繁重的任务，但也可以半自动化进行，比如用 spaCy 的模板匹配
器`Matcher`。

---

# 训练数据 vs 测试数据

- **训练数据：**用来更新模型
- **测试数据：**
  - 模型在训练过程中未见到的数据
  - 用来计算模型的准确度
  - 代表了模型在生产环境中会遇到的真实数据

Notes: 训练模型中的重要一环是了解模型的表现，模型学习的方向是否正确。方法是让模
型在一些训练中 _尚未_ 见过的数据上进行预测，然后对比我们已经知道的正确答案。因此
在训练数据之外，我们还需要测试数据，也被称作为开发数据。

测试数据用来计算模型的准确度。举个例子，一个准确度为 90%的模型意味着模型在测试数
据上的预测结果中 90%都是正确的。

这意味着测试数据需要能够代表模型在生产环境中会遇到的数据，否则准确度也将失去意义
，因为这不能告诉我们模型 _真正_ 表现得有多好。

---

# 生成训练语料(1)

```python
import spacy
nlp = spacy.blank("zh")
# 创建一个含有实体span的Doc
doc1 = nlp("iPhone X就要来了")
doc1.ents = [Span(doc1, 0, 8, label="GADGET")]
# 创建另一个没有实体span的Doc
doc2 = nlp("我急需一部新手机，给点建议吧！")
docs = [doc1, doc2]  # 以此类推...
```

Notes: spaCy 可以用与其创建的`Doc`对象相同格式的数据来更新。我们已经在第二章中学
到了很多如何创建`Doc`和`Span`对象的知识。

在这个例子中，我们为语料创建了两个`Doc`对象：一个中含有一个实体而另一个不含有实
体。要为`Doc`设置实体，我们需要把`Span`加入到`doc.ents`中。

当然了，我们还需要更多的训练数据来有效地训练出一个可以泛化和在语境中预测类似实体
的模型。根据任务的不同，我们通常希望有至少几百到上千个有代表性的数据。

---

# 生成训练语料(2)

- 将数据分割成两份:
  - **训练数据：**用来更新模型
  - **开发数据：**用来测试模型

```python
random.shuffle(docs)
train_docs = docs[:len(docs) // 2]
dev_docs = docs[len(docs) // 2:]
```

Notes: 如前面所提到的，我们不仅需要数据来训练模型，还需要在模型训练中未见过的数
据上面测试模型准确度。我们通常对数据随机排序，然后把数据分成两份：一份作为训练数
据，一份作为测试数据。这里我们只是简单的 50/50 对半分。

---

# 生成训练语料(3)

- `DocBin`：用来有效存储`Doc`对象的容器
- 可以保存为二进制文件
- 二进制文件可以用来训练模型

```python
# 创建和保存一系列的训练文档
train_docbin = DocBin(docs=train_docs)
train_docbin.to_disk("./train.spacy")
# 创建和保存一系列的测试文档
dev_docbin = DocBin(docs=dev_docs)
dev_docbin.to_disk("./dev.spacy")
```

Notes: 我们一般希望可以将训练和测试数据保存为硬盘上的文件，这样我们就可以读入到
spaCy 的训练流程中。

`DocBin`是用来有效存储和序列化`Doc`对象的容器。我们可以用一个`Doc`对象的列表来初
始化它，然后调用 `to_disk`方法将其存储为一个二进制文件。这些文件我们一般使
用`.spacy`作为后缀。

相比起其它如`pickle`的二进制序列化规制，我们的`DocBin`会更加快，生成的文件更小，
因为其仅对于共享的词汇表仅存储一次。关于`DocBin`运作的更多相关信息可以查
询[文档](https://spacy.io/api/docbin).

---

# 小经验：数据转换

- `spacy convert`可以将语料转换为常见的格式
- 支持`.conll`, `.conllu`, `.iob`以及 spaCy 之前的 JSON 格式。

```bash
$ python -m spacy convert ./train.gold.conll ./corpus
```

Notes: 有时候我们的训练和测试数据可能已经是常见的格式 - 比如 CoNLL 或者 IOB.
spaCy 的`convert`命令可以自动将这些文件转换为 spaCy 的二进制格式。它也可以转换
spaCy v2 中使用旧格式的 JSON 文件。

---

# 上手练习吧！

Notes: 是时候开始准备一些训练语料了。我们来看看几个例子，为一个新的实体类别创建
一个小的训练数据集。
