一、chatGPT的发展史
阅读本篇文章前需要先了解一下chatGPT的一个发展历程,只有先从广义上有个大概框架认识,才能落地到每个重要的节点和知识概念。有句话说,“所有的新技术”都是基于某一种技术的革命和超越,所以在学习一个新的技术前必须先了解一下它的前世今生,可以不用全部都学,但是每个知识点需要至少用自己的方式去了解一下。
-
起源与词向量:
在谈论 chatGPT 的发展史时,我们需要先从词向量(Word Embeddings)的出现说起。词向量是将单词映射到连续向量空间中的表示形式,使得单词的语义信息能够被更好地捕捉和理解。其中,Word2Vec、GloVe、FastText 等模型为词向量的发展提供了重要基础。 -
循环神经网络(RNN)与序列模型:
随着对自然语言处理(NLP)任务的需求增加,循环神经网络(RNN)成为了一个重要的工具,尤其是在处理序列数据时。RNN 可以捕捉序列数据中的时间依赖关系,从而在自然语言理解任务中表现出色。 -
长短期记忆网络(LSTM)与门控循环单元(GRU):
随着对 RNN 的发展,人们意识到传统的 RNN 存在梯度消失和梯度爆炸等问题。为了解决这些问题,长短期记忆网络(LSTM)和门控循环单元(GRU)被提出。它们通过引入门控机制,有效地处理了长期依赖关系,成为了更为稳定和强大的模型。 -
注意力机制(Attention Mechanism):
注意力机制的提出使得模型能够更加灵活地关注输入序列中的不同部分,从而提高了模型在处理长序列和复杂序列数据时的性能。Transformer 模型的成功应用将注意力机制推向了前沿。 -
Transformer 模型:
Transformer 模型的问世标志着 NLP 领域的一次重大突破。它摒弃了传统的循环神经网络结构,采用了自注意力机制(Self-Attention)和多头注意力机制(Multi-Head Attention),极大地提升了模型的性能和并行计算能力。 -
预训练模型的兴起:
随着大规模预训练模型(如BERT、GPT)的兴起,自然语言处理领域取得了巨大的进展。这些模型通过在大规模文本数据上进行无监督的预训练,然后在特定任务上进行微调,取得了惊人的效果,成为了 NLP 领域的新标杆。 -
GPT 系列模型的发展:
GPT(Generative Pre-trained Transformer)系列模型由 OpenAI 提出,它们以 Transformer 模型为基础,采用了自回归语言模型(Autoregressive Language Model)的框架,通过无监督的预训练学习了大规模文本数据的语言表示,具备了强大的生成能力和语义理解能力。 -
GPT-3 的突破:
GPT-3 是 GPT 系列中规模最大的模型,拥有 1750 亿个参数。它展示了惊人的生成能力和泛化能力,能够在各种自然语言处理任务上取得令人瞩目的表现,引发了对于大规模预训练模型的广泛关注和研究。
二、GPT初始技术词向量原理与实践
词向量概念理论知识解析
词向量(Word Embeddings)是自然语言处理(NLP)中常用的一种表示方法,它将词汇表中的每个单词映射成一个定长的向量,这个向量通常存在于一个连续的高维空间中。词向量的主要目的是将自然语言中的离散符号转化为机器可以理解的数值形式,使得词与词之间的语义和语法关系可以通过向量间的数学运算得以体现。好的词向量模型能够捕捉到单词的上下文信息,使得语义相近的词在向量空间中的距离也较近。
那词向量与gpt的关系是什么呢?
词向量 想象一下,我们把每一个单词看作是一个独一无二的人,而有一个神秘的空间可以把每个人安置在一个特定的位置上。在这个空间里,不同位置代表不同的特征,比如身高、体重、性格等。同样地,在NLP领域,每个单词也可以被转换成一个“数值人”,放在一个高维空间里的一个点上,这就是词向量。
例如,“猫”、“狗”、“宠物”这三个词在词向量空间中,可能会彼此靠近,因为它们在语义上有相关性;而“猫”和“飞机”则可能距离较远,因为它们通常不共享相似的语境含义。这样一来,原本难以直接计算和比较的词语,现在可以通过向量的加减乘除等数学操作找到它们之间的某种联系。
GPT就像是一个非常聪明的故事讲述者,他不仅了解每个词汇的意思(通过词向量),还能根据上下文环境灵活调整对词汇的理解,并预测接下来可能的故事发展。
具体来说,当GPT开始处理一句话时,它首先会将句子中的每个单词都变换成对应的词向量,就像给每个单词穿上了一件数字化的衣服。然后,GPT内部有一套复杂的机制(Transformer结构),能够根据每个词向量在整个句子中的位置和其他词的关系,动态地更新每个词的含义。
比如在预训练阶段,GPT会学习大量文本数据,通过这种学习,它能理解“我最喜欢的宠物是猫”这句话中,“猫”和“宠物”之间有很强的关联性。而在后续的任务如生成新文本时,如果遇到类似上下文,GPT就很可能根据之前学到的规律,预测出下一个可能出现的是跟“猫”或“宠物”类似的词汇。
所以,词向量为GPT提供了理解和处理自然语言的基本元素,而GPT通过深度学习技术进一步挖掘并利用这些元素间的关系来进行文本生成等各种任务。
ngram语言模型
n-gram 模型就是一类最简单的语言模型,它基于马尔科夫假设(Markov assumption),即一个词出现的概率仅与其前面有限个词有关,而不依赖于更早之前的词。
在自然语言处理中,n-gram 是指一个连续出现的n个项目的序列,这里的项目通常指的是文本中的单个词汇、字符或音素。
想象一下,正在教一个小机器人如何学习人类说话的方式。n-gram就好比是小机器人学习的“短句规则”。
-
n-gram是什么? 假设你在教机器人一句简单的日常对话:“我爱你”。如果我们按照单个字来看,这是一个“一元gram”(unigram)的序列:“我”、“爱”、“你”。如果按照两个字一组来看,就是一个“二元gram”(bigram)的序列:“我爱你”可以拆分为“我爱”和“爱你”。如果看三个字一组,那就是“三元gram”(trigram):“我爱”、“爱你”。
-
n-gram与语言模型的关系 现在,我们要让机器人学会如何自己生成类似的对话。这就需要用到语言模型。我们可以建立一个基于n-gram的语言模型,告诉机器人:“在一般情况下,如果一个人说了'我'之后,很可能会跟着说'爱',然后是'你'。”
所以,n-gram语言模型就是这样一个“统计规律”的集合,它记录下在大量真实文本中各个词组(n-gram)出现的频率,然后根据这些频率来预测下一个词可能是什么。例如,模型发现“我爱”后面接“你”的情况非常多,于是当只看到“我爱”时,模型就会认为下一个词是“你”的概率很大。
然而,真实的语言并不总是如此简单线性,有时候我们需要考虑更复杂的上下文关系。传统的n-gram模型只能记住前几个词的影响,对于更长距离的上下文信息处理能力有限。而更先进的神经网络语言模型(如GPT)则像一个记忆力更强、思考更全面的机器人,它可以理解和记忆更长的上下文,从而生成更准确和流畅的文本。
Word2Vec神经网络模型
Word2Vec 是一个用于自然语言处理的神经网络模型,由 Google 的 Mikolov 等人在 2013 年提出,主要用于将自然语言中的单词映射为连续向量空间中的向量,这些向量称为词嵌入(word embeddings)。通过训练,Word2Vec 能够捕捉到单词之间的语义和语法关系,使得具有相似含义或经常出现在相似上下文中的单词在向量空间中彼此接近。
CBOW
CBOW (Continuous Bag-of-Words) 是 Word2Vec 提出的两种核心模型之一。它的基本思想是利用一个词的上下文(即周围的词)来预测该词本身。CBOW模型通过观察大量的上下文样本,学习到一种紧凑且有意义的词向量表示,使得模型能够根据给定的一组上下文词快速有效地预测出目标词,同时这些词向量在很多实际应用中展现出优秀的通用性和可解释性。
Skip-gram
Skip-gram模型通过大量语料库训练,使得语义上相似的词在向量空间中的距离更近,从而实现对词汇的高效且富有含义的数学表示。这些词嵌入不仅可用于诸如文本分类、情感分析等多种自然语言处理任务,还可以揭示潜在的语义结构,如通过计算词向量之间的余弦相似度发现同义词或类比关系。
Skip-gram和CBOW的区别
Skip-gram是用一个词去预测上下两个词,CBOW是用上下两个词去平均,然后预测下一个词。
-
训练目标与方向:
- CBOW:模型的目标是根据一个词的上下文(周围的词)来预测中心词。换句话说,CBOW是基于上下文来推测当前单词,模型的输入是上下文词向量的平均或加权平均,输出是中心词的预测概率分布。
- Skip-gram:模型则反过来,其目标是根据一个中心词来预测其周围的上下文词。也就是说,Skip-gram是利用当前单词来推测其前后出现的其他单词,对于每个中心词,模型会独立地对每个上下文词进行预测。
-
训练过程和效果:
- CBOW:由于它综合了上下文信息,因此在训练过程中,上下文词的信号会被合并在一起,对于高频词和低频词的区分可能不如Skip-gram明显,特别是在处理稀有词汇时,CBOW可能无法提供足够精确的词向量。
- Skip-gram:相比CBOW,Skip-gram更强调每个中心词与每个上下文词之间的独立关系,这使得模型在训练过程中需要做更多的预测任务,但同时也能够更细致地捕捉到单词之间的联系。因此,Skip-gram在某些情况下可能产生更好的词向量表示,特别是在处理低频词时,由于每个中心词都单独影响了上下文词的预测,因此对低频词的学习更有利。
-
计算和训练效率:
- CBOW:由于它直接使用上下文词的均值向量作为输入,所以在计算量上相对于Skip-gram较小,训练速度可能较快,特别在大数据集上表现得更为明显。
- Skip-gram:由于每次迭代都需要为每个中心词和多个上下文词组合执行多次预测,所以其计算量相对较大,训练时间可能较长,但它对单个词的学习更为细致,精度有时会更高。
CBOW和Skip-gram各有优势和适用场景,选择哪种模型通常取决于所需的应用特性和可用的数据资源。在实践中,二者都可以生成高质量的词嵌入,但具体表现会因任务需求的不同而有所差异。
Softmax函数
Softmax函数是机器学习和深度学习中用于多类别分类问题的一个重要激活函数。给定一组未归一化的分数(通常是神经网络最后一层的输出),softmax函数会将其转化为一个概率分布。具体公式如下:
这里的 i 表示第 i 个类别,输出结果是一个K维概率向量,每个元素都在 [0,1][0,1] 范围内且所有元素之和为1。在自然语言处理中的词嵌入模型(如Word2Vec的Skip-gram模型)中,softmax层用于计算给定中心词条件下所有词汇作为上下文词的概率分布。
Softmax树形优化
Softmax树形优化(通常称为层次Softmax或Hierarchical Softmax)是一种专门针对大规模多分类问题中Softmax函数计算瓶颈的优化技术,特别适用于像Word2Vec这样的词嵌入模型。在Word2Vec中,softmax层用于计算一个中心词与词汇表中所有词作为上下文词的概率分布。然而,当词汇表很大时,计算所有词汇的概率及相应的梯度代价高昂。
层次Softmax采用了树形数据结构(如霍夫曼树或二叉树)来替代传统的Softmax计算。在这个树结构中,每一个词汇都被映射为树的一个叶子节点。计算任一中心词与某个词汇作为上下文词的概率时,不需要遍历整个词汇表,而是沿着树从根节点到相应叶节点的路径来进行概率计算。
具体而言,每个内部节点代表一个二元决策,通过这个决策过程逐步确定最终到达哪个叶子节点(即词汇)。每条从根节点到叶子节点的路径对应一个概率累积过程,通过对路径上各内部节点的条件概率乘积即可得到最终词汇的概率。
通过这种方式,层次Softmax显著降低了计算复杂性和内存消耗,因为它只需对树的深度进行遍历,而非词汇表的大小。在实践中,层次Softmax极大地加快了训练速度,特别是对于拥有大量词汇的自然语言处理任务。
负采样优化
在训练词嵌入模型时,尤其是Word2Vec,传统的Softmax函数需要对整个词汇表计算概率分布,这在词汇表非常庞大的情况下计算成本非常高昂。为了解决这个问题,Mikolov等人提出了负采样(Negative Sampling)方法作为一种优化策略。
负采样不是直接计算所有词汇的概率,而是对每个正例(中心词与正确上下文词的配对)仅随机抽样一小部分负例(中心词与其他词汇的错误配对)。在每次迭代更新模型参数时,目标是最大化中心词与其正确的上下文词之间的共现概率,同时最小化中心词与负例词汇之间的共现概率。
这种方法大大减少了计算量,因为它仅需要优化少量(通常是5到20个)随机选取的负例,而不是整个词汇表。这样既保持了模型学习词向量的有效性,又极大地提升了训练速度和效率。因此,负采样成为了大规模自然语言处理任务中训练词嵌入模型时的一项重要优化技术。
Word2Vec代码实践
数据预处理
下载训练需要的数据
import io
import os
import sys
import requests
from collections import OrderedDict
import math
import random
import numpy as np
# 下载语料用来训练word2vec
def download_corpus():
# 使用百度云服务器上的开源数据集链接
corpus_url = "https://dataset.bj.bcebos.com/word2vec/text8.txt"
# 使用requests库下载数据集
response = requests.get(corpus_url)
# 将下载的内容写入本地的text8.txt文件
with open("./text8.txt", "wb") as f:
f.write(response.content)
# 调用函数下载语料
download_corpus()
#打开下载好的文本文件
def load_txt():
with open('./text8.txt', 'r') as f:
corpus = f.read().strip('\n')
f.close()
return corpus
corpus = load_txt()
#数据预处理,分词&大写转小写
def data_preprocess(corpus):
corpus = corpus.strip().lower()
corpus = corpus.split(' ')
return corpus
corpus = data_preprocess(corpus)
统计词频,并构建词频字典
# 统计词频,并构建词频dict
def build_dict(corpus):
# 初始化一个空字典用于存储单词频率
word_freq_dict = {}
# 遍历语料中的每个单词
for word in corpus:
# 如果单词不在字典中,则将其添加,并初始化频率为0
if word not in word_freq_dict:
word_freq_dict[word] = 0
# 更新单词频率
word_freq_dict[word] += 1
# 将字典按照值(频率)从大到小排序,并转换为列表
word_freq_dict = sorted(word_freq_dict.items(), key=lambda x: x[1], reverse=True)
# 初始化单词到ID的映射字典
word2id_dict = {}
# 初始化ID到单词的映射字典
id2word_dict = {}
# 初始化单词ID到频率的映射字典
word2id_freq = {}
# 遍历排好序的单词频率字典
for word, freq in word_freq_dict:
# 获取当前单词在字典中的ID
curr_id = len(word2id_dict)
# 将单词与对应的ID添加到单词到ID的映射字典中
word2id_dict[word] = curr_id
# 将单词ID与对应的频率添加到单词ID到频率的映射字典中
word2id_freq[curr_id] = freq
# 将ID与对应的单词添加到ID到单词的映射字典中
id2word_dict[curr_id] = word
# 返回单词ID到频率的映射字典、单词到ID的映射字典、ID到单词的映射字典
return word2id_freq, word2id_dict, id2word_dict
# 从语料构建词典
word2id_freq, word2id_dict, id2word_dict = build_dict(corpus)
vocab_size = len(word2id_dict)
# 输出前10个单词及其ID和频率
for _, (word, word_i