生产中的大语言模型(MEAP)(一)(2)https://developer.aliyun.com/article/1517049
2.2.5 嵌入
回想一下我们的语言特征,很容易理解为什么连续式语言建模是一次重大突破。嵌入接受我们创建的令牌化向量,这些向量不包含任何意义,并尝试根据可以从文本中得到的观察结果插入该意义,例如单词顺序和出现在相似上下文中的子词。尽管主要的意义模式是搭配(共同出现,即相邻的单词),但它们被证明是有用的,甚至显示出与人类编码的单词意义一些相似之处。
Word2Vec 中的典型例子之一是,将“king”的向量减去“man”的向量,加上“woman”的向量,然后找到其总和的最近邻居,得到的词向量就是“queen”的向量。这对我们来说是有意义的,因为它模拟了人类的语义。其中一个主要区别已经提到了几次,即语用学。人类使用语用背景来确定语义意义,理解仅仅因为你说了“我需要食物”并不意味着你实际上没有食物就会有危险。嵌入是纯粹使用情境之外的任何影响,这感觉就像是人类学习的方式,各方面都有很好的论点。唯一的问题是,如果我们可以以某种方式为模型提供更多的感知数据,那可能会为更有效的嵌入打开大门。
在第 2.5 节中,我们将深入探讨如何使用 pyplot 可视化嵌入。我们将在后面的章节中更深入地研究嵌入。这对于模型的可解释性以及在预训练步骤中进行验证都是有帮助的。如果您发现您的语义相似的嵌入在图上相对靠近彼此,那么您很可能朝着正确的方向前进了。
2.5 嵌入可视化
# After listing 2.4 is done and gradient descent has been executed words = [ "King", "Queen", "Lord", "Man", "Woman", "Prince", "Ophelia", "Rich", "Happy", ] embs = (W1.T + W2) / 2.0 idx = [word2Ind[word] for word in words] X = embs[idx, :] print(X.shape, idx) result = compute_pca(X, 2) pyplot.scatter(result[:, 0], result[:, 1]) for i, word in enumerate(words): pyplot.annotate(word, xy=(result[i, 0], result[i, 1])) pyplot.show()
图 2.4 一种用于词嵌入的可视化技术。可视化嵌入对于模型的可解释性非常重要。
如图 2.4 所示,这是我们从 CBoW 模型训练得到的一个成功但非常稀疏的嵌入表示。让这些语义表示(嵌入)更密集是我们可以看到这一领域改进的主要地方,尽管许多成功的实验已经运行过,其中更密集的语义含义已经通过指导和不同的思维链技术取代了更大的语用背景。我们稍后将讨论“思维链”(CoT)和其他技术。现在,让我们转而讨论为什么我们的连续嵌入技术甚至可能成功,鉴于基于频率的模型通常很难与现实相关联。所有这一切都始于半个多世纪前的多层感知器。
2.2.6 多层感知器
MLPs 体现了这样一种情感,“机器在做一件事情时真的很擅长,所以我希望我们可以使用一堆真的擅长做这一件事情的机器来制造一个擅长做很多事情的机器。” MLP 中的每个权重和偏置都擅长检测一个特征,所以我们将它们绑在一起以检测更大、更复杂的特征。MLPs 在大多数神经网络架构中充当主要的构建模块。架构之间的关键区别,如卷积神经网络和循环神经网络,主要来自数据加载方法和处理标记化和嵌入数据在模型层之间流动的方式,而不是个别层的功能,特别是全连接层。
在 2.6 中,我们提供了一个更动态的神经网络类,它可以根据您的任务需要拥有任意数量的层和参数。我们使用 pytorch 提供了一个更明确和明确的类,以便为您提供实现 MLP 的工具,无论您是从头开始还是在流行的框架中使用。
2.6 多层感知机 Pytorch 类实现
import torch import torch.nn as nn import torch.nn.functional as F class MultiLayerPerceptron(nn.Module): def __init__( self, input_size, hidden_size=2, output_size=3, num_hidden_layers=1, hidden_activation=nn.Sigmoid, ): """Initialize weights. Args: input_size (int): size of the input hidden_size (int): size of the hidden layers output_size (int): size of the output num_hidden_layers (int): number of hidden layers hidden_activation (torch.nn.*): the activation class """ super(MultiLayerPerceptron, self).__init__() self.module_list = nn.ModuleList() interim_input_size = input_size interim_output_size = hidden_size torch.device("cuda:0" if torch.cuda.is_available() else "cpu") for _ in range(num_hidden_layers): self.module_list.append( nn.Linear(interim_input_size, interim_output_size) ) self.module_list.append(hidden_activation()) interim_input_size = interim_output_size self.fc_final = nn.Linear(interim_input_size, output_size) self.last_forward_cache = [] def forward(self, x, apply_softmax=False): """The forward pass of the MLP Args: x_in (torch.Tensor): an input data tensor. x_in.shape should be (batch, input_dim) apply_softmax (bool): a flag for the softmax activation should be false if used with the Cross Entropy losses Returns: the resulting tensor. tensor.shape should be (batch, output_dim) """ for module in self.module_list: x = module(x) output = self.fc_final(x) if apply_softmax: output = F.softmax(output, dim=1) return output
从代码中我们可以看到,与具有静态两层的 CBoW 实现相反,直到实例化之前,这个 MLP 的大小是不固定的。如果您想给这个模型一百万层,您只需在实例化类时将 num_hidden_layers=1000000,尽管仅仅因为您可以给模型那么多参数,并不意味着它会立即变得更好。LLMs 不仅仅是很多层。像 RNNs 和 CNNs 一样,LLMs 的魔力在于数据如何进入并通过模型。为了说明这一点,让我们看一下 RNN 及其一个变体。
2.2.7 循环神经网络(RNNs)和长短期记忆网络(LSTMs)
循环神经网络(RNNs)是一类神经网络,设计用于分析序列,基于以前语言建模技术的弱点。其逻辑是,如果语言以序列的方式呈现,那么处理它的方式可能应该是按序列而不是逐个标记进行的。RNNs 通过使用我们以前看到的逻辑来实现这一点,在 MLPs 和马尔科夫链中都看到过,即在处理新输入时引用内部状态或记忆,并在检测到节点之间的连接有用时创建循环。
在完全递归网络中,如清单 2.7 中的网络,所有节点最初都连接到所有后续节点,但这些连接可以设置为零,以模拟它们被断开如果它们不是有用的。这解决了较早模型遇到的最大问题之一,即静态输入大小,并使得 RNN 及其变体能够处理可变长度的输入。不幸的是,较长的序列带来了一个新问题。因为网络中的每个神经元都与后续神经元连接,所以较长的序列对总和产生的改变较小,使得梯度变得较小,最终消失,即使有重要的词也是如此。
例如,让我们考虑具有任务情感分析的这些句子,“昨晚我喜欢看电影”,以及,“昨晚我去看的电影是我曾经期望看到的最好的。”即使这些句子不完全相同,它们也可以被认为在语义上相似。在通过 RNN 时,第一句中的每个单词都更有价值,其结果是第一句的积极评分比第二句高,仅仅因为第一句更短。反之亦然,爆炸梯度也是这种序列处理的结果,这使得训练深层 RNNs 变得困难。
要解决这个问题,长短期记忆(LSTMs)作为一种 RNN,使用记忆单元和门控机制,保持能够处理可变长度的序列,但没有较长和较短序列被理解为不同的问题。预见到多语言场景,并理解人们不只是单向思考语言,LSTMs 还可以通过将两个 RNNs 的输出连接起来进行双向处理,一个从左到右读取序列,另一个从右到左。这种双向性提高了结果,允许信息即使在经过数千个标记之后也能被看到和记住。
在清单 2.7 中,我们提供了 RNN 和 LSTM 的类。在与本书相关的存储库中的代码中,您可以看到训练 RNN 和 LSTM 的结果,结果是 LSTM 在训练集和验证集上的准确性都更好,而且仅需一半的时代(25 次与 RNN 的 50 次)。需要注意的一个创新是利用填充的打包嵌入,将所有可变长度序列扩展到最大长度,以便处理任何长度的输入,只要它比最大长度短即可。
清单 2.7 递归神经网络和长短期记忆 Pytorch 类实现
import torch from gensim.models import Word2Vec from sklearn.model_selection import train_test_split # Create our corpus for training with open("./chapters/chapter_2/hamlet.txt", "r", encoding="utf-8") as f: data = f.readlines() # Embeddings are needed to give semantic value to the inputs of an LSTM # embedding_weights = torch.Tensor(word_vectors.vectors) EMBEDDING_DIM = 100 model = Word2Vec(data, vector_size=EMBEDDING_DIM, window=3, min_count=3, workers=4) word_vectors = model.wv print(f"Vocabulary Length: {len(model.wv)}") del model padding_value = len(word_vectors.index_to_key) embedding_weights = torch.Tensor(word_vectors.vectors) class RNN(torch.nn.Module): def __init__( self, input_dim, embedding_dim, hidden_dim, output_dim, embedding_weights, ): super().__init__() self.embedding = torch.nn.Embedding.from_pretrained( embedding_weights ) self.rnn = torch.nn.RNN(embedding_dim, hidden_dim) self.fc = torch.nn.Linear(hidden_dim, output_dim) def forward(self, x, text_lengths): embedded = self.embedding(x) packed_embedded = torch.nn.utils.rnn.pack_padded_sequence( embedded, text_lengths ) packed_output, hidden = self.rnn(packed_embedded) output, output_lengths = torch.nn.utils.rnn.pad_packed_sequence( packed_output ) return self.fc(hidden.squeeze(0)) INPUT_DIM = 4764 EMBEDDING_DIM = 100 HIDDEN_DIM = 256 OUTPUT_DIM = 1 model = RNN( INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, embedding_weights ) optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) criterion = torch.nn.BCEWithLogitsLoss() device = torch.device("cuda" if torch.cuda.is_available() else "cpu") class LSTM(torch.nn.Module): def __init__( self, input_dim, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout, embedding_weights, ): super().__init__() self.embedding = torch.nn.Embedding.from_pretrained( embedding_weights ) self.rnn = torch.nn.LSTM( embedding_dim, hidden_dim, num_layers=n_layers, bidirectional=bidirectional, dropout=dropout, ) self.fc = torch.nn.Linear(hidden_dim * 2, output_dim) self.dropout = torch.nn.Dropout(dropout) def forward(self, x, text_lengths): embedded = self.embedding(x) packed_embedded = torch.nn.utils.rnn.pack_padded_sequence( embedded, text_lengths ) packed_output, (hidden, cell) = self.rnn(packed_embedded) hidden = self.dropout( torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1) ) return self.fc(hidden.squeeze(0)) INPUT_DIM = padding_value EMBEDDING_DIM = 100 HIDDEN_DIM = 256 OUTPUT_DIM = 1 N_LAYERS = 2 BIDIRECTIONAL = True DROPOUT = 0.5 model = LSTM( INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, N_LAYERS, BIDIRECTIONAL, DROPOUT, embedding_weights, ) optimizer = torch.optim.Adam(model.parameters()) criterion = torch.nn.BCEWithLogitsLoss() device = torch.device("cuda" if torch.cuda.is_available() else "cpu") def binary_accuracy(preds, y): rounded_preds = torch.round(torch.sigmoid(preds)) correct = (rounded_preds == y).float() acc = correct.sum()/len(correct) return acc def train(model, iterator, optimizer, criterion): epoch_loss = 0 epoch_acc = 0 model.train() for batch in iterator: optimizer.zero_grad() predictions = model(batch["text"], batch["length"]).squeeze(1) loss = criterion(predictions, batch["label"]) acc = binary_accuracy(predictions, batch["label"]) loss.backward() optimizer.step() epoch_loss += loss.item() epoch_acc += acc.item() return epoch_loss / len(iterator), epoch_acc / len(iterator) def evaluate(model, iterator, criterion): epoch_loss = 0 epoch_acc = 0 model.eval() with torch.no_grad(): for batch in iterator: predictions = model(batch["text"], batch["length"]).squeeze(1) loss = criterion(predictions, batch["label"]) acc = binary_accuracy(predictions, batch["label"]) epoch_loss += loss.item() epoch_acc += acc.item() return epoch_loss / len(iterator), epoch_acc / len(iterator) batch_size = 128 # Usually should be a power of 2 because it's the easiest for computer memory. def iterator(X, y): size = len(X) permutation = np.random.permutation(size) iterate = [] for i in range(0, size, batch_size): indices = permutation[i:i+batch_size] batch = {} batch['text'] = [X[i] for i in indices] batch['label'] = [y[i] for i in indices] batch['text'], batch['label'] = zip(*sorted(zip(batch['text'], batch['label']), key = lambda x: len(x[0]), reverse = True)) batch['length'] = [len(utt) for utt in batch['text']] batch['length'] = torch.IntTensor(batch['length']) batch['text'] = torch.nn.utils.rnn.pad_sequence(batch['text'], batch_first = True).t() batch['label'] = torch.Tensor(batch['label']) batch['label'] = batch['label'].to(device) batch['length'] = batch['length'].to(device) batch['text'] = batch['text'].to(device) iterate.append(batch) return iterate index_utt = word_vectors.key_to_index #You've got to determine some labels for whatever you're training on. X_train, X_test, y_train, y_test = train_test_split(index_utt, labels, test_size = 0.2) X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size = 0.2) train_iterator = iterator(X_train, y_train) validate_iterator = iterator(X_val, y_val) test_iterator = iterator(X_test, y_test) print(len(train_iterator), len(validate_iterator), len(test_iterator)) N_EPOCHS = 25 for epoch in range(N_EPOCHS): train_loss, train_acc = train( model, train_iterator, optimizer, criterion ) valid_loss, valid_acc = evaluate(model, validate_iterator, criterion) print( f"| Epoch: {epoch+1:02} | Train Loss: {train_loss: .3f} | Train Acc: {train_acc*100: .2f}% | Validation Loss: {valid_loss: .3f} | Validation Acc: {valid_acc*100: .2f}% |" ) #Training on our dataset | Epoch: 01 | Train Loss: 0.560 | Train Acc: 70.63% | Validation Loss: 0.574 | Validation Acc: 70.88% | | Epoch: 05 | Train Loss: 0.391 | Train Acc: 82.81% | Validation Loss: 0.368 | Validation Acc: 83.08% | | Epoch: 10 | Train Loss: 0.270 | Train Acc: 89.11% | Validation Loss: 0.315 | Validation Acc: 86.22% | | Epoch: 15 | Train Loss: 0.186 | Train Acc: 92.95% | Validation Loss: 0.381 | Validation Acc: 87.49% | | Epoch: 20 | Train Loss: 0.121 | Train Acc: 95.93% | Validation Loss: 0.444 | Validation Acc: 86.29% | | Epoch: 25 | Train Loss: 0.100 | Train Acc: 96.28% | Validation Loss: 0.451 | Validation Acc: 86.83% |
看看我们的类和实例化,你会发现 LSTM 与 RNN 并没有太大的区别。 init
输入变量中唯一的区别是 n_layers
(为了方便,你也可以用 RNN 指定它)、bidirectional
和 dropout
。双向性允许 LSTM 向前查看序列以帮助理解意义和上下文,但也极大地有助于多语言场景,因为像英语这样的从左到右的语言并不是正字法的唯一格式。丢失率是另一个巨大的创新,它改变了过拟合的范式,不再仅仅是数据相关,并通过在训练期间逐层关闭随机节点来帮助模型不过度拟合,强制所有节点不相互关联。在模型之外的唯一区别是,RNN 的最佳优化器是 SGD,就像我们的 CBoW 一样,而 LSTM 使用 Adam(可以使用任何优化器,包括 AdamW)。下面,我们定义了我们的训练循环并训练了 LSTM。将此训练循环与 gradient_descent
函数中的 Listing 2.4 中定义的训练循环进行比较。
本代码展示的一个惊人之处在于,与先前的模型迭代相比,LSTM 的学习速度要快得多,这要归功于双向性和丢失率。尽管先前的模型训练速度更快,但需要数百个周期才能达到与仅需 25 个周期的 LSTM 相同的性能。验证集上的性能,正如其名称所示,为架构增添了有效性,在训练期间对未经训练的示例进行推断,并保持准确性与训练集相当。
这些模型的问题并不太明显,主要表现为资源消耗极大,尤其是在应用于更长、更注重细节的问题时,如医疗保健和法律领域。尽管丢失率和双向处理具有令人难以置信的优势,但它们至少会将训练所需的处理能力增加一倍,因此,虽然推断最终只会比相同规模的 MLP 昂贵 2-3 倍,但训练则会变得 10-12 倍昂贵。它们很好地解决了爆炸梯度的问题,但却增加了训练所需的计算量。为了解决这个问题,设计并实施了一种快捷方式,使任何模型,包括 LSTM,在一个序列中找出哪些部分是最有影响力的,哪些部分可以安全地忽略,这就是注意力。
生产中的大语言模型(MEAP)(一)(4)https://developer.aliyun.com/article/1517052