自然语言处理


经典方法

TF—IDF

  • TF(Term Frequency):在文章中反复出现的词重要;统计各个词的词频;
  • IDF(Inverse Document Frequency):在各个文章中都出现的词不重要,统计一个词在多少文章中出现n,总共的文章数N;$IDF=log\frac{N}{n+1}$,为词的逆文档权重;
  • 将词的TF与IDF相乘得到词的TF—IDF权重

词嵌入

引入

  • 将序列向量化:

    学习上下文信息,表示学习,不仅仅可以用于文本,还可以拓展到各种学习上下文信息的场景中;
  • Look-up Table:

    • 通过输入词的编号从表中查找词的向量:

    • 低频、不重要的词,可以统一用一个$UNK$符号表示:

  • 词的含义很大程度上可以从它所使用的场景得到:

    • 一个没有见过的新词,可以通过有相近用法的词来推测含义
    • 词的含义与它所在的上下文有非常强的关系

one-hot编码缺点

  • 占空间
  • 相互独立,无法计算相似度
  • 编码没有任何泛化能力,无法看出词之间的关联

基于计数的方法

正点互信息

Co-Occurence Counts:计算一个词为中心词,其它词出现的数量

PMI(Pointwise Mutual Information)点互信息:这一指标用来衡量两个事物之间的相关性。

Positive Pointwise Mutual Information (PPMI):为了避免PMI中为负的情况,设计了PPMI。

  • 互信息MI为0代表独立没有关系,负值同样是有关系;
  • 但是语言中负关系往往不明显:
    • 即一个词和少数词的互信息是正的;
    • 和绝大多数词的互信息都是很微小的负值;
  • 因此可以对这种负值截断。

词向量可以看成是通过神经网络方法对PMI进行一个隐式的因式分解

Word2vec

思路

通过预测的方式来拉近中心词和共线背景词的联合概率或条件概率

目标函数

似然:
$$
\mbox{Likelihood} = L(\theta)=\prod\limits_{t=1}^T\prod\limits_{-m\le j \le m, j\neq 0}P(w_{t+j}|{w_t}, \theta)
$$
损失函数:

给每个词准备两个向量,分别在词作为中心词和背景词时使用

可得:

具体其中一对词的损失函数为:

通过给每个词两个向量,可以使得每个向量的计算都是线性的,更加容易计算和优化

负采样

常规负采样

前面计算概率需要计算中心词或背景词与所有词的概率,计算开销大,负采样是从非背景词中选择一部分进行计算:
$$
J_{t,j}(\theta)=
-\log\sigma(u_{cute}^Tv_{cat}) -
\sum\limits_{w\in {w_{i_1},\dots, w_{i_K}}}\log\sigma({-u_w^Tv_{cat}}) \\
-\log\sigma(u_{cute}^Tv_{cat}) -
\sum\limits_{w\in {w_{i_1},\dots, w_{i_K}}}\log(1-\sigma({u_w^Tv_{cat}}\color{black}))
$$

Word2Vec按修正的词频$U^{3/4}(w)$进行采样

层序softmax

通过二叉树来组合生成条件概率,避免了分母概率的计算。
可以看成是对概率进行了因式分解。

每个词对应二叉树的一个叶节点,此时有:
$$
P(w_3 \mid w_c) = \sigma(u_{n(w_3, 1)}^\top v_c) \cdot \sigma(-u_{n(w_3, 2)}^\top v_c) \cdot \sigma(u_{n(w_3, 3)}^\top v_c)
$$
因为:$\sigma(x)+\sigma(-x) = 1$,所以:
$$
\sum_{w \in \mathcal{V}} P(w \mid w_c) = 1
$$
计算复杂度下降到了对数级。

NCE loss

  • 原来优化的是条件概率$P(w_o|w_c)$,是预测、重构的方式,现在优化的是互信息$\frac{P(w_o|w_c)}{P(w_o)}$,对比式。
  • 非归一化
  • 二分类

变种类型

skip-gram

  • 跳字模型
  • 在给定中心词时,计算窗口内背景词的联合条件概率:$\prod_{t=1}^TP(w^{t-m},…,w^{t-1},w^{t+1},…,w^{t+m}|w^t)$;
  • 联合条件概率加入朴素假设:背景词概率条件独立,最大化这个概率。最后的概率表示为:$\prod_{t=1}^T\prod_{-m\leq j \leq m,j\neq 0}P(w^{t+j}|w^{t})$
  • 通过加入对数得到损失函数:$-\frac{1}{T}\sum_{t=1}^T\sum_{-m\leq j \leq m,j\neq 0}log P(w^{t+j}|w^t)$
  • 每个词有两组向量,分别是作为中心词和作为背景词时的表示。用$v$表示作为中心词时的表示,用$u$表示作为背景词时的表示,可得:$P(w_o|w_c)=\frac{e^{u^T_ov_c}}{\sum_{i\in V}e^{u^T_iv_c}}$,其中$V$是所有节点。
  • 带负采样的跳字模型其实是在隐式的估算了一个带偏移的点互信息的因式分解。

CBOW

  • 连续词袋模型
    在已知背景词时,计算中心词的条件概率,背景词的向量求和与中心词向量求內积,对应的有:
    • 联合条件概率:$\prod_{t=1}^TP(w^t|w^{t-m},…,w^{t-1},w^{t+1},…,w^{t+m})$
    • 损失函数:$-\frac{1}{T}\sum_{t=1}^TlogP(w^t|w^{t-m},…,w^{t-1},w^{t+1},…,w^{t+m})$
    • $P(w_c|w_{O1},…,w_{O2m})=\frac{e^{u^T_c(v_{o1}+…+v_{o2m})/(2m)}}{\sum_{i\in V}e^{u^T_i(v_{o1}+…+v_{o2m})/(2m)}}$

其他讨论

超参数

超参数的选择取决于具体任务,常见的选择:

窗口长度

更长的窗口长度使得词中更多包含主题相似
而更短的窗口长度使得词包含更多词法上的相似
还可以尝试给更近的共现对更高的权重

词的重要性

词频越高的词,提供的信息量越少,如介词
因此还可以使用基于词频的采样,以概率:$P(w_i)=1 - \sqrt{\frac{thr}{f(w_i)}}$在训练中忽略。
其中$f(w_i)$是词频,$thr$是选择概率,原论文中$thr=10^{-5}$,词频大于$thr$的词汇会以概率$P(w_i)$被忽略。
可以训练的更快、效果更好

拓展

任何有上下文关系的离散数据都可以用Word2vec学习表示
学习段落/文章表示

  • 让一部分隐向量随段落/文章变化,这部分就可以表示段落文章
    • Distributed representations of sentences and documents

网络Embedding

  • 将网络中的节点类比文本中的词,学习节点的表示
    • LINE: Large-scale Information Network Embedding

关键词/网页

  • 在查询关键词和用户点击的网页之间建立上下文关系,使得Word2Vec模型可以学习到查询关键词以及网页的隐含向量。
    • Context and Content-aware Embeddings for Query Rewriting in Sponsored Search

GloVe

Global Vectors for Word Representation是基于计数方法和基于预测方法的结合:

信息来源使用基于计数的方法得到全局信息,向量优化采用梯度下降。

具体的损失函数如下:

后者代表优化目标:

  • 即共线频数越高的组合,向量内积越大;同时学习偏置值;
  • 前者代表组合权重,降低低频组合的权重,同时不过度提高高频组合的权重;

评估

直观评估

通过词向量查看相似词,从语义上直观判断词向量效果,如:

  • 相似度高的词,语义应该相似
  • 相似度高的词,类型应该相似
  • 好的Embedding可以表示类比关系,如:国王-男人+女人$\approx$王后;
  • 好的Embedding还可以表示不同语言之间的映射关系 相似有多种角度,如直接计算内积、距离范数、可视化等

任务评估

通过对比实验比较不同词向量方法下实际任务的效果,来判断词向量的好坏

FastText

将一个词的n-gram词都作为这个词的词向量,可以优化效果和速度

文本分类

引入

常见类型

文本分类常常作为很多业务的前置环节
常见类型:

数据集

常见的分类数据集:

任务流程

一般包括特征提取和分类两个环节

模型类型

分类模型可以是生成的,也可以是判别的

经典方法

朴素贝叶斯

原理

因为概率乘积一般不稳定,且最终只考虑大小,因此可以通过对数进行优化:
$$
\log P(x, y=k)=\log P(y=k) + \sum\limits_{t=1}^n\log P(x_t|y=k)
$$

参数估计

其中类别的先验概率容易确定。
而具体类别下的文档概率为:
$$
P(x| y=k)=P(x_1, \dots, x_n|y=k)
$$

而朴素贝叶斯假设是:

  • Bag of Words假设:词序无关紧要,
  • 条件独立假设:特征(词)在给定类的情况下是独立的。

进而有:
$$
P(x| y=k)=P(x_1, \dots, x_n|y=k)=\prod\limits_{t=1}^nP(x_t|y=k)
$$
同时可以通过计数的方式得到标签下每个词的概率:
$$
P(x_i|y=k)=\frac{N(x_i, y=k)}{\sum\limits_{t=1}^{|V|}N(x_t, y=k)}
$$

为了避免词没有出现的情况,可以加上一个较小的值来实现拉普拉斯平滑:
$$
P(x_i|y=k)=\frac{\color{red}{\delta} +\color{black} N(x_i, y=k)
}{\sum\limits_{t=1}^{|V|}(\color{red}{\delta} +\color{black}N(x_t, y=k))} =
\frac{\color{red}{\delta} +\color{black} N(x_i, y=k)
}{\color{red}{\delta\cdot |V|}\color{black} + \sum\limits_{t=1}^{|V|}\color{black}N(x_t, y=k)}
$$

预测

在计算以上概率后,便可以进行预测:

总结

在朴素贝叶斯方法中:

  • 通过one-hot对词进行表示;
  • 进而通过词袋(Bag of Words)来表示文本;
  • 最终通过朴素贝叶斯的推导得到预测结果

改进

  1. 由于朴素的假设,可能使得词汇组成完全相同的文本意思完全不同:
  • 可以尝试将一些频繁的N-Grams作为单词使用:
  1. 不使用不重要的词:

逻辑回归

原理

逻辑回归,又称最大熵分类,则是一种判别模型,直接关系$P(y=k|x)$,而不在意联合概率分布。
具体的:

  • 将文本转化为特征表示;
  • 特征表示跟权重矩阵或向量计算各个类别的概率。

$$
P(class=k|{h})=\frac{\exp(w^{(k)}{h})}{\sum\limits_{i=1}^K\exp(w^{(i)}{h})}
$$

训练

基本原则是对数最大似然:
$$
w^{\ast}=\arg \max\limits_{w}\sum\limits_{i=1}^N\log P(y=y^i|x^i)
$$

等价为最小化交叉熵损失函数:

比较

SVM

神经网络方法

原理

主要特点是通过神经网络来获取文本的特征表示

最后的分类部分属于逻辑回归

优化

神经网络的模型和逻辑回归没有本质不同,因此优化过程也是由最大似然得到的交叉熵损失函数。

模型类型

经典方式

词袋嵌入(Bag of Embeddings)通过将输入文本所有词的独热编码求和,或加权求和得到特征表示:

加权的权重可以使用tf-idf

循环神经网络

  • 多层:

  • 双向:

卷积网络

一般流程:

  • 通过卷积网络对输入文本的嵌入进行特征提取,每个卷积核提取一种特征;
  • 对不同位置上的卷积结果进行全局池化,得到一个固定长度的向量;
  • 不同卷积核得到的全局池化结果可以拼接起来,得到最终的特征表示。
卷积网络也可以使用多层。
  • 卷积的作用类似于ngram,一个卷积核处理一类ngram家族;
  • 池化可以起到阈值过滤的作用,保留最有代表性的特征,而忽略低于阈值的特征。

多标签分类

与单标签相比,只有在输出的时候需要做一些调整:

  1. 将输出激活函数由Softmax改为Element-wise Sigmoid,使得每个类别的概率独立。

  2. 将损失函数由单个交叉熵函数改为多个二元交叉熵函数的和。

技巧

词嵌入

词嵌入有多种获得方式:

  1. 重新训练;
  2. 通过Word2Vec、GloVe等提前获得并固定;
  3. 通过Word2Vec、GloVe等提前获得并继续训练。
  • 由于文本分类的数据集相对较小,因此第一种方式往往信息不充足;
  • 而后两种方式相当于是通过其它的海量预料进行了预训练,可以利用外部信息;
  • 第3中方式通过微调训练,一般要优于第二种:
    • 因为词嵌入主要学习共现关系;
    • 很多时候反义词如good和bad共现概率大,因此嵌入向量接近
    • 这不利于文本分类;
    • 通过微调可以缓解这个问题。

数据增广

word dropout

随机将一些文本标记为UNK或随机词,使得模型不要过度依赖某些词。

同义词替换

通过外部词库,替换文本中的同义词:

跨语言翻译

通过成熟的翻译模型将文本翻译成其它语言,再翻译回来:

语言建模

引入

  • 词的概率可以通过在语料中出现的频率计算;
  • 但句子不行,没有语料可以包含所有句子;
  • 所以即使很合理的句子也会在频率计算出0概率;

语言建模对文本概况进行条件概率分解,每次计算在给定文本下,下一个词的概率:
$$
P(y_1, y_2, \dots, y_n)=P(y_1)\cdot P(y_2|y_1)\cdot P(y_3|y_1, y_2)\cdot\dots\cdot P(y_n|y_1, \dots, y_{n-1})=
\prod \limits_{t=1}^n P(y_t|y_{\mbox{<}t})
$$
在得到语言模型后,就可以逐个预测下一个文本。

N-gram语言模型

原理

N-gram是在频率计数法$P(y_t|y_1, \dots, y_{t-1}) = \frac{N(y_1, \dots, y_{t-1}, y_t)}{N(y_1, \dots, y_{t-1})}$的基础上增加N-gram的限制,假设语言模型具有马尔科夫性$P(y_t|y_1, \dots, y_{t-1}) = \frac{N(y_1, \dots, y_{t-1}, y_t)}{N(y_1, \dots, y_{t-1})}$。

平滑

为了避免分子分母为0的情况,需要做一些处理

Backoff

当N-gram分母为0时,可以尝试N-1-gram,持续迭代

线性插值

给各个概率加权平均

拉普拉斯平滑

当分子为0时,给每种情况一个小概率:

总结

  • 因为N-gram的马尔可夫性只使用了短文本,没有考虑长文本,因此N-gram生成的文本往往不流畅。

神经网络模型

流程

通过神经网络对所有过去的文本进行特征提取,得到向量表示,并预测下一个词。

与文本分类的流程非常相似,只是这里的类别数非常高,与词表大小相同。

语言模型的输出层也可以看成是词嵌入:

$$
p(y_t| y_{\mbox{<}t}) = \frac{exp({h_t^T}{e_{y_t}})}{\sum\limits_{w\in V}exp({h_t^T}{e_{w}})}
$$
因此也可以通过让输入词嵌入和输出层参数共享,来降低参数量,提升模型的泛化能力。

损失函数

让输出概率分布等于设定独热分布:

因为交叉熵等价于KL散度$D_{KL}(p^{\ast}|| p)$:
$$
Loss(p^{\ast}, p^{})= - p^{\ast} \log(p) = -\sum\limits_{i=1}^{|V|}p_i^{\ast} \log(p_i) \\
= -\log(p_{y_t})=-\log(p(y_t| y_{\mbox{<}t}))
$$

建模模型

RNN

卷积神经网络

与文本分类不同,在语言模型中:

  • CNN一般不用池化层;

  • 并在开头用padding:

  • 要保证前面的预测不会用到后面的信息;

  • 堆叠多层来获得足够的感受野;

  • 还可以通过残差连接来提升训练效果

生成策略

贪婪的生成策略往往不能保证语言的连贯性和多样性,需要在生成过程中加入一些处理。

贪婪解码

不做任何处理

标准采样

不一定采用概率最大的值,而是依据生成的分布采样

带温度的采样

在softmax前除以一个温度值,可以调整输出的分布:

  • 温度越高时,分布越平均,随机性和多样性越高,越发散没意义
  • 温度越低时,分布越极端,更加确定和重复,

Top-K采样

只在概率值Top-K的词中采样

  • 可以去掉非常不可能的词
  • 但是固定的K不一定好

Top-p采样

只在概率和大于p%的词中采样,可采样数是动态的

除了在单步的词选择上进行优化外,还可以在多步之间进行优化
单步贪婪选择时存在一下问题:

即贪心解并不一定是最优解

Beam Search在每个时刻会保存概率最高的N个序列,可以更大概率的取到更优解

beam size 一般设为4-10

评估方式

交叉熵

反应模型对于文本是否流畅的认可度

困惑度

困惑度(Perplexity):$Perplexity(y_{1:M})=2^{-\frac{1}{M}L(y_{1:M})}$

是损失函数的一种变化:

  • 最佳的困惑度是1,也就是模型总是以1的概率正确预测词
  • 最坏的困惑度是$|V|$
    • 表面上困惑度的值还可以增加到正无穷,但其实这种情况是学习到了某种反向的相关性
      $$
      Perplexity(y_{1:M})=2^{-\frac{1}{M}L(y_{1:M})} = 2^{-\frac{1}{M}\sum\limits_{t=1}^M\log_2 p(y_t|y_{1:t-1})}=2^{-\frac{1}{M}\cdot M \cdot \log_2\frac{1}{|V|}}=2^{\log_2 |V|} =|V|
      $$

ChatGPT

ChatGPT的原理

ChatGPT没有联网,不是在网上搜索答案,也不是从一个数据库中查找
GPT-1、GPT-2、GPT-3是比较单纯地预训练模型,基于自监督方法,学习大量语料。这种方式让模型有了大量的信息量,但是在回答问题方面效果不佳,给出的回答往往非常随意。
ChatGPT是一个大规模的预训练语言模型,在GPT的基础上加入了强化学习。人类只需要给出回答的打分,就可以引导模型的方向;再进一步的,将人类的打分训练成一个评估模型,用评估模型可以更加大量地给ChatGPT打分。

强化学习环节

使用强化学习训练ChatGPT的示例:

import tensorflow as tf
import numpy as np

# 构建强化学习环境
class Environment:
    def __init__(self, agent):
        self.agent = agent
        self.history = []

    def reset(self):
        self.history = []

    def step(self, action):
        reply = self.agent.generate_reply(self.history, action)
        reward = self.compute_reward(reply)
        self.history.append(reply)
        return reward

    def compute_reward(self, reply):
        # 计算奖励,可以根据任务需求自定义奖励函数
        return ...

# 构建强化学习代理
class Agent:
    def __init__(self, model):
        self.model = model

    def generate_reply(self, history, action):
        # 输入当前状态和动作,输出回复文本
        state = self.encode_state(history, action)
        reply_logits = self.model(state)
        reply = self.decode_reply(reply_logits)
        return reply

    def encode_state(self, history, action):
        # 编码当前状态
        state = ...
        return state

    def decode_reply(self, reply_logits):
        # 解码模型输出的回复文本
        reply = ...
        return reply

# 训练模型
def train(model, data, env, agent, optimizer, num_epochs=10):
    for epoch in range(num_epochs):
        epoch_reward = 0
        for inputs, targets in data:
            # 在强化学习环境中运行模型
            env.reset()
            for i in range(len(inputs)):
                action = targets[i]  # 动作为目标回复
                reward = env.step(action)
                epoch_reward += reward
            # 更新模型参数
            loss = -tf.reduce_sum(tf.math.log(agent.model(inputs)) * targets)
            gradients = tape.gradient(loss, model.trainable_variables)
            optimizer.apply_gradients(zip(gradients, model.trainable_variables))
        print("Epoch %d reward: %.2f" % (epoch, epoch_reward))

# 使用强化学习训练模型
model = ...
data = ...
env = Environment(agent)
agent = Agent(model)
optimizer = tf.keras.optimizers.Adam()
train(model, data, env, agent, optimizer, num_epochs=10)

具体来说,训练过程中会构建一个强化学习环境,其中状态由之前的对话历史和当前的输入组成,动作由模型输出的回复文本组成,奖励由用户的反馈和其他任务相关的指标组成。然后,通过不断迭代优化模型参数,使模型能够在给定的任务上获得更好的性能。

奖励设计

计算奖励值的方法可以根据具体的应用场景和任务需求来设计。以对话生成任务为例,可以通过以下几种方式计算奖励值:

基于人类评估的奖励值:将对话生成模型的输出提交给人类评估员进行评估,评估员根据对话的质量和流畅度打分,然后将得分作为奖励值。这种方法需要投入大量的人力资源,但可以更好地反映出真实用户对话的质量。

基于自动评估的奖励值:使用一些自动化的评估指标来计算奖励值,例如BLEU、Perplexity、F1值等。这种方法可以自动化地评估对话的质量,但是评估结果可能与真实用户对话的质量存在一定的偏差。

基于任务目标的奖励值:根据对话生成任务的具体目标来设计奖励函数,例如生成有用的回答、回答问题的准确性、提供有趣的信息等。这种方法需要根据具体的任务需求进行设计,但可以更好地引导模型生成符合任务目标的对话。

基于自动评估指标

下面是一个基于自动评估指标BLEU的奖励计算代码示例:

from nltk.translate.bleu_score import sentence_bleu

# 定义目标回答
target_response = "I am doing well, thank you. How about you?"

def calculate_reward(response):
    # 计算生成回答和目标回答的BLEU值
    bleu_score = sentence_bleu([target_response.split()], response.split())
    # 根据BLEU值计算奖励值
    if bleu_score > 0.5:
        reward = 1
    else:
        reward = -1
    return reward

在这个示例中,我们使用NLTK库中的sentence_bleu函数计算生成回答和目标回答之间的BLEU值,然后根据阈值0.5来判断奖励值的正负。如果BLEU值大于0.5,则认为生成的回答比较接近目标回答,奖励值为1;否则奖励值为-1。实际应用中可以根据具体需求来调整阈值和奖励函数的设计。

基于任务目标

下面以另一个对话生成任务为例,介绍一种基于任务目标的奖励计算方法,这个任务是生成关于天气的回答,任务目标是回答的准确性和完整性。

奖励计算的基本原理是,对于用户提出的关于天气的问题,我们首先需要解析出问题的关键信息,例如地点、时间、天气类型等。然后,根据这些关键信息,我们可以查询天气数据,并将查询到的天气信息和用户提出的问题进行比对。如果生成的回答和实际的天气信息比较接近,并且回答的内容准确和完整,就给予正向的奖励值;否则给予负向的奖励值。

下面是一个简单的奖励计算代码示例:

import requests

def calculate_reward(response, intent):
    # 解析用户提出的问题,获取关键信息
    location = intent['location']
    date = intent['date']
    weather_type = intent['weather_type']
    
    # 查询天气数据
    url = f"https://api.weather.com/v1/geocode/{location}/forecast/daily/{date}.json"
    response = requests.get(url)
    weather_data = response.json()
    
    # 比较生成回答和实际天气信息的差异
    if response.lower() in weather_data['daily']['narrative'].lower() and weather_type in weather_data['daily']['narrative'].lower():
        # 回答准确和完整,给予正向奖励
        reward = 1
    else:
        # 回答不准确或不完整,给予负向奖励
        reward = -1
    return reward

在这个示例中,使用了一个公共的天气查询API,根据用户提出的问题获取对应的天气数据。然后,将生成的回答和实际天气信息进行比较,如果回答准确和完整,就给予正向奖励值;否则给予负向奖励值。实际应用中需要注意,由于外部API的不确定性,查询结果可能存在一定的误差,因此需要针对具体应用场景进行调整和优化。

基于人类评估的奖励值

下面以一个任务为例,介绍一种基于人类评估的奖励计算方法。这个任务是回答有关于美食的问题,任务目标是回答的相关度和语言流畅度。

基于人类评估的奖励计算方法是通过人类评估模型,对模型生成的回答进行打分,根据打分给予相应的奖励值。在这个例子中,我们可以请专业人员或者普通用户对模型生成的回答进行打分。评分可以是一个连续值,也可以是一个离散值。例如,评分为5分代表非常相关、流畅,评分为1分代表不相关、不流畅。

下面是一个简单的基于人类评估的奖励计算代码示例:

def calculate_reward(response):
    # 模型生成回答
    generated_response = model.generate_response()
    
    # 将生成的回答输出给用户进行评分
    score = human_evaluate(generated_response)
    
    # 根据评分给予奖励值
    if score >= 4:
        # 回答相关度和语言流畅度非常好,给予较高的奖励
        reward = 10
    elif score >= 2:
        # 回答相关度和语言流畅度一般,给予较低的奖励
        reward = 5
    else:
        # 回答相关度和语言流畅度差,给予负向奖励
        reward = -5
    return reward

在这个示例中,首先调用模型生成回答,然后将生成的回答输出给人类评估模型进行评分。根据评分的结果,如果回答相关度和语言流畅度非常好,就给予较高的奖励值;如果回答相关度和语言流畅度一般,就给予较低的奖励值;如果回答相关度和语言流畅度差,就给予负向奖励值。实际应用中需要注意,人类评估的结果会受到个体主观因素的影响,因此需要统计多个评分结果进行平均,降低评分误差对奖励计算的影响。同时,还需要定期更新评分标准,以保持奖励计算的准确性。

基于人类评估的奖励模型

使用少量的人类评估数据训练一个奖励模型,该模型的输入是生成的文本,输出是与人类评估数据尽可能接近的奖励分数。然后使用该奖励模型来为生成的文本打分,以指导训练。通常情况下不会直接使用人类评估数据,而是对其进行一些预处理,例如对分数进行归一化或对其进行平滑处理,以减少模型对噪声的敏感度。

需要注意的是,这种方法也存在一些限制和缺点。例如,奖励模型的性能取决于人类评估数据的质量和数量,如果数据太少或质量不好,则模型可能会产生误导性的分数。此外,奖励模型的训练需要额外的计算资源和时间,因为需要在训练生成模型的同时训练奖励模型。

以下是一个简单的代码示例,展示了如何使用人类评估数据训练奖励模型,以指导生成模型的训练。

首先,我们需要准备一些人类评估数据。这些数据可以来自人类评估或人工标注,我们可以将其存储为一个包含文本和对应评分的数据集。假设我们的数据集中有100个样本,每个样本包含一个文本和一个0-10之间的评分。

human_data = [
    ("This is a good example.", 8),
    ("I like this book very much.", 9),
    # ... 其它98个样本
]

接下来,我们可以使用这些数据来训练一个奖励模型。为了简单起见,这里使用一个基于多层感知器(MLP)的模型。我们将文本编码为词向量,并将其输入到MLP中,以预测与人类评分最接近的奖励分数。

import tensorflow as tf
from tensorflow.keras.layers import Input, Dense, Embedding, Flatten
from tensorflow.keras.models import Model

# 定义模型输入和输出
inputs = Input(shape=(None,))
x = Embedding(vocab_size, embedding_dim)(inputs)
x = Flatten()(x)
x = Dense(64, activation="relu")(x)
outputs = Dense(1, activation="linear")(x)

# 编译模型
model = Model(inputs=inputs, outputs=outputs)
model.compile(optimizer="adam", loss="mse")

# 准备训练数据
texts = [text for text, _ in human_data]
scores = [score for _, score in human_data]

# 将文本编码为词向量
text_sequences = tokenizer.texts_to_sequences(texts)
text_sequences = tf.keras.preprocessing.sequence.pad_sequences(
    text_sequences, maxlen=max_sequence_length, padding="post"
)

# 训练模型
model.fit(text_sequences, scores, epochs=10, batch_size=32)

一旦我们训练好了奖励模型,我们可以使用它来为生成的文本打分,以指导生成模型的训练。在这个例子中,我们使用了基于策略梯度的强化学习方法来训练生成模型。具体地,我们定义了一个损失函数,其中奖励分数由奖励模型给出,以指导生成模型的优化。

import numpy as np

# 定义损失函数
def policy_gradient_loss(y_true, y_pred):
    # 计算奖励分数
    reward = model.predict(y_pred)
    # 对奖励分数进行归一化处理
    reward = (reward - np.mean(reward)) / (np.std(reward) + 1e-9)
    # 计算损失函数
    loss = -tf.reduce_mean(reward * tf.math.log(y_pred))
    return loss

# 训练生成模型
generator_model.compile(optimizer="adam", loss=policy_gradient_loss)
generator_model.fit(dataset, epochs=10)

ChatGPT如何学会那么多种语言:语言模型可以在不同语言之间泛化,在多种语言上做预训练后,只要在某一个语言上训练一个任务,就会自动学会其它语言的同样任务

seq2seq

总览

从一个序列到另一个序列的任务,一般包含编码器和解码器两个组件:

最常见的seq2seq任务是机器翻译

seq2seq任务可以看成是条件语言模型


当然,条件语言模型中的条件$x$并不一定要是文本,也可以是图像等其它形式

损失函数生成推理的解码问题与语言模型一致

模型结构

RNN

最经典的结构

RNN编码器将输入的条件句转化为一个向量表示;
通过可视化可以发现,有相似意思的输入句子,在向量特征空间中比较接近:

注意力

流程

RNN编码器将输入句子的所有信息都压缩为一个向量;
这个过程会存在损失信息,且不相关的信息难以压缩的问题。
因此编码器与解码器之间的特征向量成了瓶颈。
注意力机制用待解码的特征向量与所有输入位置的特征向量计算出一个注意力分数:

这个注意力分数进一步归一化得到权重,与所有输入位置的特征向量加权求和得到最终的待解码特征向量:

注意力计算

注意力分数有多种方式可以计算,包括:

  • 点积;
  • 双线性bilinear函数,又称Luong attention,原论文中用的单向RNN;
  • 多层感知机multi-layer perceptron,又称Bahdanau attention,原论文中用了双向RNN。

注意力对齐

通过观察注意力分数,可以提供可解释性
例如在翻译任务中可以知道是什么翻译为了什么

transformer

总览

只使用注意力,不使用RNN
  • 其中编码器部分包含自注意力
  • 解码器包含自注意力和注意力

相比RNN:

  • 并行度高,更快的速度;
  • 可以捕捉长时间依赖

计算复杂度:

Self-Attention

Masked Self-Attention

在解码器中,各个时间步也可以并行;
但是需要通过Masked阻止模型看到未来的信息。

Multi-Head Attention

让模型同时关注不同的角度
$$
\mbox{head}_i=\mbox{Attention}(QW_Q^i, KW_K^i, VW_V^i) \\
\mbox{MultiHead}(Q, K, V) = \mbox{Concat}(\mbox{head}_1, \dots, \mbox{head}_n)W_o
$$

随着训练的进行,还可以去掉一些没有学到东西的头,以降低复杂度

前馈块

$$
FFN(x) = ReLU(xW_1+b_1)W_2+b_2
$$

残差连接&层标准化

残差连接 层标准化

位置编码

transformer不能得到词在序列中的位置,而这是十分重要的
位置编码主动记录这种信息
$$
PE_{pos, 2i}=\sin(pos/10000^{2i/d_{model}}) \\
PE_{pos, 2i+1}=\cos(pos/10000^{2i/d_{model}})
$$

迁移学习

基础

引入

很多场景下数据量较小,难以训练出较好地模型,可以考虑将其它场景下训练好的包好丰富信息的模型迁移到目标场景。

类别

词嵌入

如前文提到的,将词嵌入应用到文本分类等场景中:可以利用大量无标签文本对词嵌入进行预训练,然后将词嵌入应用到文本分类任务中微调,可以同时使用两个场景的数据信息。

基于语言模型的词嵌入

词嵌入方法得到的词表示仅仅包含的词的使用信息,而通过语言模型得到的词表示可以包含词所在的上下文信息。

因此基于语言模型的词嵌入:

  • 不是简单地训练并迁移词的嵌入向量;
  • 而是训练并迁移一个语言模型,使得新场景可以直接得到上下文的表示。

CoVe

CoVe通过翻译的预训练任务来得到上下文词向量,具体的:

  • 设计一个机器翻译的任务:

  • 将训练好的模型的编码器部分拿出来用在下游任务中,具体的将编码器的输出和词嵌入结合,其中包含了丰富的信息:

ELMo

在CoVe的基础上:

  • 将翻译任务改为了语言模型;
  • 不是直接查表得到词的嵌入而是使用一个字符级别的网络学习,可以扩展到不认识的词。

最后将各个层的表示加权级联:

基于模型的迁移

预训练环境不是只提供词表示,而是提供整个模型。
下游任务不需要设计各自的网络,只需要处理输入输出即可微调。

GPT

生成式预训练语言模型,相当于transformer的解码器部分,在预训练后可以用于各种任务:

图中的start等是特殊符号
预训练阶段的损失函数为语言模型的损失函数:$L_{xent}=-\sum\limits_{t=1}^n\log(p(y_t| y_{\mbox{<}t}))$
微调阶段的损失函数为语言模型的损失函数加上任务的损失函数:$L = L_{xent} + \lambda \cdot L_{task}$

BERT

来自transformer的预训练双向编码器表示,相当于transformer的编码器部分。特点:

  • transformer的encode;
  • 预训练+微调;
  • 双向;
  • 不同任务上只需要改最后一层;

BERT有两种预训练任务:NSP、MLM

NSP

分类任务,输入两个句子的连接,判断两个句子是否是前后句。
输入由句子的嵌入、位置嵌入、段落嵌入组成
正例和负例的比例各占一半

MLM

随机将一部分词改为mask、其他词或不变,在输出预测这些词

即:完形填空

Adapter

为了减少微调的参数量,适配器在预训练模型中插入一些参数量小的模块,并只训练这些模块:

聊天机器人

开放聊天

规则型

正则化匹配人工智能标记语言(AIML)是一种基于XML标准的声明式语言,它规定了可以在机器人中使用的编程构想和数据结构。
可以基于人工智能标记语言(AIML)来定义聊天机器人的模式和回复,构建模式匹配聊天机器人

有答案就直接返回;
没有答案可以通过一定的规则进行泛化匹配;

主题知识问答

包括:

  • 主题知识整理;
  • 主题实体词识别;
  • 主题知识点匹配;
  • 主题无答案个性化回复;
中文命名实体识别(Named Entity Recognition,NER)是指识别中文文本中实体的边界和类别。命名实体识别是文本处理中的基础技术,广泛应用在自然语言处理、推荐系统、知识图谱等领域,比如推荐系统中的基于实体的用户画像、基于实体召回等。

图谱问答

无歧义实体答案合成:当问题不直接匹配时,可以根据图谱找到合适的节点 歧义实体答案合成:当问题不直接匹配且实体存在多个可能选项时,可以反问用户

生成式问答

在seq2seq的基础上加入合适的信息,如主题、情绪、性别等等,得到基于情绪的回答:

在输入上加入风格特征,得到基于风格化的回答:

机器阅读

阅读材料,根据提问给出答案


文章作者: 万川
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 万川 !
  目录