自然语言处理实战第二版(MEAP)(三)(1)https://developer.aliyun.com/article/1517948
5.2.4 性别信息学
首先,导入 Pandas 并设置 max_rows 以仅显示您的 DataFrame 的几行。
>>> import pandas as pd >>> import numpy as np >>> pd.options.display.max_rows = 7
现在从 nlpia2 存储库下载原始数据并仅采样 10000 行,以使任何计算机都可以快速运行。
>>> np.random.seed(451) >>> df = pd.read_csv( # #1 ... 'https://proai.org/baby-names-us.csv.gz') >>> df.to_csv( # #2 ... 'baby-names-us.csv.gz', compression='gzip') >>> df = df.sample(10_000) # #3 >>> df.shape (10000, 6)
数据覆盖美国出生证书超过 100 年,但仅包括婴儿的名字:
| 地区 | 性别 | 年份 | 名字 | 数量 | 频率 | |
| 6139665 | WV | 女 | 1987 | Brittani | 10 | 0.000003 |
| 2565339 | MD | 女 | 1954 | Ida | 18 | 0.000005 |
| 22297 | AK | 男 | 1988 | Maxwell | 5 | 0.000001 |
| … | … | … | … | … | … | … |
| 4475894 | OK | 女 | 1950 | Leah | 9 | 0.000003 |
| 5744351 | VA | 女 | 2007 | Carley | 11 | 0.000003 |
| 5583882 | TX | 男 | 2019 | Kartier | 10 | 0.000003 |
您现在可以忽略地区和出生年份信息。您只需要使用自然语言名称就可以以合理的准确性预测性别。如果您对名字感到好奇,您可以将这些变量探索为特征或目标。您的目标变量将是性别(‘M’或’F’)。除男性和女性外,此数据集中没有提供其他性别分类。
您可能会喜欢探索数据集,以发现父母为他们的宝宝选择名字的频率。机器学习和 NLP 是消除成见和误解的好方法。
>>> df.groupby(['name', 'sex'])['count'].sum()[('Timothy',)] sex F 5 M 3538
这就是 NLP 和数据科学如此有趣的原因。它为我们提供了一个更广泛的世界视角,打破了我们生物大脑的有限视角。我从来没有见过一个叫“Timothy”的女人,但在美国出生证书上的至少 0.1%的婴儿名字为 Timothy 的是女性。
如果地区和年份不是名称的要素,不需要预测,可以在跨地区和年份聚合(组合)数据以加快模型训练。您可以使用 Pandas 的 DataFrame's .groupby() 方法来实现这一点。
>>> df = df.set_index(['name', 'sex']) >>> groups = df.groupby(['name', 'sex']) >>> counts = groups['count'].sum() >>> counts name sex Aaden M 51 Aahana F 26 Aahil M 5 .. Zvi M 5 Zya F 8 Zylah F 5
因为我们已经聚合了 “count” 列的数字数据,所以 counts 对象现在是 Pandas 的 Series 对象,而不是 DataFrame。它看起来有点奇怪,因为我们在名称和性别上创建了多级
现在数据集看起来像是训练逻辑回归的有效示例集。实际上,如果我们只想预测该数据库中的名称可能的性别,我们可以仅使用每个名称的最大计数(最常用法)。
但这是一本关于 NLP 和 NLU(自然语言理解)的书。你希望你的模型以某种方式理解姓名的文本。而且你希望它能够处理不在这个数据库中的奇怪姓名,比如"Carlana",一个由她的祖父母"Carl"和"Ana"组成的混合词,或者像"Cason"这样的独一无二的姓名。不在你的训练集或测试集中的示例被称为"分布外"。在现实世界中,你的模型几乎总是会遇到以前从未见过的词语和短语。当一个模型能够推广到这些分布外示例时,这被称为"泛化"。
但是你如何对一个单词像一个姓名进行标记化,以便你的模型可以泛化到完全新的虚构的从未见过的名字?你可以使用每个单词(或姓名)中的字符 n-gram 作为你的标记。你可以设置一个TfidfVectorizer来计算字符和字符 n-gram 而不是单词。你可以尝试更宽或更窄的ngram_range,但对于大多数基于 TF-IDF 的信息检索和 NLU 算法来说,3-gram 是一个不错的选择。例如,最先进的数据库 PostgreSQL 将其全文搜索索引默认设置为字符 3-gram。在后面的章节中,你甚至将使用词块和句子块标记化,它们可以选择最佳的字符序列作为你的标记。
>>> from sklearn.feature_extraction.text import TfidfVectorizer >>> vectorizer = TfidfVectorizer( ... use_idf=False, # #1 ... analyzer='char', ... ngram_range=(1, 3) # #2 ... ) >>> vectorizer TfidfVectorizer(analyzer='char', ngram_range=(1, 3), use_idf=False)
你应该按照文档频率来归一化标记计数,例如出生率。你将使用出生率来计算。对于姓名 TF-IDF 向量,你希望使用出生率或人口作为文档频率。这将帮助你的向量表示语料库之外的姓名频率。
现在你已经通过姓名和性别对我们的names系列进行了索引,跨州和年份聚合计数,你的Series中将会有更少的唯一行。在计算 TF-IDF 字符 n-gram 术语频率之前,你可以去重姓名。不要忘记跟踪出生证明的数量,以便将其用作文档频率。
>>> df = pd.DataFrame([list(tup) for tup in counts.index.values], ... columns=['name', 'sex']) >>> df['count'] = counts.values >>> df name sex counts 0 Aaden M 51 1 Aahana F 26 2 Aahil M 5 ... ... .. ... 4235 Zvi M 5 4236 Zya F 8 4237 Zylah F 5 [4238 rows x 3 columns]
你已经将 10,000 个姓名-性别对聚合成了只有 4238 个唯一的姓名-性别配对。现在你可以将数据分割成训练集和测试集了。
>>> df['istrain'] = np.random.rand(len(df)) < .9 >>> df name sex counts istrain 0 Aaden M 51 True 1 Aahana F 26 True 2 Aahil M 5 True ... ... .. ... ... 4235 Zvi M 5 True 4236 Zya F 8 True 4237 Zylah F 5 True
为了确保你不会意外地交换任何姓名的性别,重新创建name, sex的多索引:
>>> df.index = pd.MultiIndex.from_tuples( ... zip(df['name'], df['sex']), names=['name_', 'sex_']) >>> df name sex count istrain name_ sex_ Aaden M Aaden M 51 True Aahana F Aahana F 26 True Aahil M Aahil M 5 True ... ... .. ... ... Zvi M Zvi M 5 True Zya F Zya F 8 True Zylah F Zylah F 5 True
正如你之前看到的,这个数据集对许多姓名包含冲突的标签。在现实生活中,许多姓名都被用于男性和女性婴儿(或其他人类性别类别)。像所有的机器学习分类问题一样,数学将其视为回归问题。模型实际上是在预测一个连续值,而不是离散的二进制类别。线性代数和现实生活只适用于实值。在机器学习中,所有的二分法都是错误的。^([18]) 机器不会将单词和概念视为严格的类别,因此你也不应该这样做。
>>> df_most_common = {} # #1 >>> for name, group in df.groupby('name'): ... row_dict = group.iloc[group['count'].argmax()].to_dict() # #2 ... df_most_common[(name, row_dict['sex'])] = row_dict >>> df_most_common = pd.DataFrame(df_most_common).T # #3
由于重复,测试集标志可以从istrain的非运算中创建。
>>> df_most_common['istest'] = ~df_most_common['istrain'].astype(bool) >>> df_most_common name sex count istrain istest Aaden M Aaden M 51 True False Aahana F Aahana F 26 True False Aahil M Aahil M 5 True False ... ... .. ... ... ... Zvi M Zvi M 5 True False Zya F Zya F 8 True False Zylah F Zylah F 5 True False [4025 rows x 5 columns]
现在你可以将istest和istrain标志传递到原始数据框中,要小心,对于训练集和测试集,将NaNs填充为 False。
>>> df['istest'] = df_most_common['istest'] >>> df['istest'] = df['istest'].fillna(False) >>> df['istrain'] = ~df['istest'] >>> istrain = df['istrain'] >>> df['istrain'].sum() / len(df) 0.9091... # #1 >>> df['istest'].sum() / len(df) 0.0908... # #2 >>> (df['istrain'].sum() + df['istest'].sum()) / len(df) 1.0
现在你可以使用训练集来适应TfidfVectorizer,而不会因为重复的名称而使 n-gram 计数偏差。
>>> unique_names = df['name'][istrain].unique() >>> unique_names = df['name'][istrain].unique() >>> vectorizer.fit(unique_names) >>> vecs = vectorizer.transform(df['name']) >>> vecs <4238x2855 sparse matrix of type '<class 'numpy.float64'>' with 59959 stored elements in Compressed Sparse Row format>
当使用稀疏数据结构时,你需要小心。如果你用.todense()将它们转换为普通的稠密数组,可能会因为使用了所有的内存而导致计算机崩溃。但是这个稀疏矩阵只包含大约 1700 万个元素,所以它应该可以在大多数笔记本电脑上正常工作。你可以对稀疏矩阵使用toarray()来创建一个数据框,并为行和列提供有意义的标签。
>>> vecs = pd.DataFrame(vecs.toarray()) >>> vecs.columns = vectorizer.get_feature_names_out() >>> vecs.index = df.index >>> vecs.iloc[:,:7] a aa aac aad aah aak aal Aaden 0.175188 0.392152 0.0 0.537563 0.000000 0.0 0.0 Aahana 0.316862 0.354641 0.0 0.000000 0.462986 0.0 0.0 Aahil 0.162303 0.363309 0.0 0.000000 0.474303 0.0 0.0 ... ... ... ... ... ... ... ... Zvi 0.000000 0.000000 0.0 0.000000 0.000000 0.0 0.0 Zya 0.101476 0.000000 0.0 0.000000 0.000000 0.0 0.0 Zylah 0.078353 0.000000 0.0 0.000000 0.000000 0.0 0.0
注意到列标签(字符 n-grams)全部以小写字母开头。看起来TfidfVectorizer将大小写折叠了(将所有内容都转换为小写)。大写可能会帮助模型,所以让我们重新对名称进行向量化而不是转换为小写。
>>> vectorizer = TfidfVectorizer(analyzer='char', ... ngram_range=(1, 3), use_idf=False, lowercase=False) >>> vectorizer = vectorizer.fit(unique_names) >>> vecs = vectorizer.transform(df['name']) >>> vecs = pd.DataFrame(vecs.toarray()) >>> vecs.columns = vectorizer.get_feature_names_out() >>> vecs.index = df.index >>> vecs.iloc[:,:5] A Aa Aad Aah Aal name_ sex_ Aaden M 0.193989 0.393903 0.505031 0.000000 0.0 Aahana F 0.183496 0.372597 0.000000 0.454943 0.0 Aahil M 0.186079 0.377841 0.000000 0.461346 0.0 ... ... ... ... ... ... Zvi M 0.000000 0.000000 0.000000 0.000000 0.0 Zya F 0.000000 0.000000 0.000000 0.000000 0.0 Zylah F 0.000000 0.000000 0.000000 0.000000 0.0
好多了。这些字符 1、2 和 3 克就应该包含足够的信息来帮助神经网络猜测出这份出生证明数据库中姓名的性别。
选择神经网络框架
逻辑回归是任何高维特征向量(如 TF-IDF 向量)的完美机器学习模型。要将逻辑回归转换为神经元,你只需要一种方法将其连接到其他神经元。你需要一个能够学习预测其他神经元输出的神经元。并且你需要将学习分散开来,这样一个神经元就不会尝试做所有的工作。每当你的神经网络从数据集中获得一个显示正确答案的示例时,它就能计算出自己的错误程度,即损失或误差。但是如果有多个神经元共同贡献给该预测,它们每个都需要知道如何改变自己的权重,以使输出更接近正确答案。要知道这一点,你需要知道每个权重对输出的影响程度,即权重相对于误差的梯度(斜率)。计算梯度(斜率)并告诉所有神经元如何调整它们的权重以使损失降低的过程称为反向传播或反向传播。
像 PyTorch 这样的深度学习软件包可以自动处理所有这些。事实上,它可以处理您可以想象的任何计算图(网络)。PyTorch 可以处理任何数学操作之间的网络连接。正是因为这种灵活性,大多数研究人员使用它而不是 TensorFlow(Keras)来开发他们的突破性 NLP 算法。TensorFlow 设计了一种特定类型的计算图,可以在由大型技术公司制造的专用芯片上高效计算。深度学习对于大型技术公司来说是一个强大的赚钱方式,他们希望训练你的大脑只使用他们自己的工具来构建神经网络。我不知道大型技术公司会将 Keras 整合到 TensorFlow 中,否则我不会在第一版中推荐它。
Keras 的可移植性下降和 PyTorch 的快速增长的受欢迎程度是我们决定第二版书的原因。PyTorch 有什么好处呢?
维基百科上有一个公正和详细的所有深度学习框架的比较。而 Pandas 可以让您直接从网络加载它并放入一个DataFrame中:
>>> import pandas as pd >>> import re >>> dfs = pd.read_html('https://en.wikipedia.org/wiki/' ... + 'Comparison_of_deep-learning_software') >>> tabl = dfs[0]
这是如何使用基本的自然语言处理(NLP)通过维基百科的文章对前十个深度学习框架进行评分的方法,该文章列出了它们的优点和缺点。每当您想将半结构化自然语言转化为 NLP 管道中的数据时,您会发现这种代码非常有用。
>>> bincols = list(tabl.loc[:, 'OpenMP support':].columns) >>> bincols += ['Open source', 'Platform', 'Interface'] >>> dfd = {} >>> for i, row in tabl.iterrows(): ... rowd = row.fillna('No').to_dict() ... for c in bincols: ... text = str(rowd[c]).strip().lower() ... tokens = re.split(r'\W+', text) ... tokens += '\*' ... rowd[c] = 0 ... for kw, score in zip( ... 'yes via roadmap no linux android python \*'.split(), ... [1, .9, .2, 0, 2, 2, 2, .1]): ... if kw in tokens: ... rowd[c] = score ... break ... dfd[i] = rowd
现在维基百科的表格都整理好了,你可以为每个深度学习框架计算某种"总分"。
>>> tabl = pd.DataFrame(dfd).T >>> scores = tabl[bincols].T.sum() # #1 >>> tabl['Portability'] = scores >>> tabl = tabl.sort_values('Portability', ascending=False) >>> tabl = tabl.reset_index() >>> tabl[['Software', 'Portability']][:10] Software Portability 0 PyTorch 14.9 1 Apache MXNet 14.2 2 TensorFlow 13.2 3 Deeplearning4j 13.1 4 Keras 12.2 5 Caffe 11.2 6 PlaidML 11.2 7 Apache SINGA 11.2 8 Wolfram Mathematica 11.1 9 Chainer 11
PyTorch 几乎得到了满分,因为它支持 Linux、Android 和所有流行的深度学习应用程序。
另一个值得注意的是 ONNX。它实际上是一个元框架和一个允许在其他框架上进行网络转换的开放标准。ONNX 还具有一些优化和剪枝功能,可以使您的模型在非常有限的硬件上(例如便携设备)运行推理速度更快。
为了比较一下,使用 SciKit Learn 构建神经网络模型与使用 PyTorch 相比如何?
表 5.1 Scikit-Learn 与 PyTorch
| Scikit-Learn | PyTorch |
| 用于机器学习 | 用于深度学习 |
| 不适合 GPU | 适用于 GPU(并行处理) |
model.predict() |
model.forward() |
model.fit() |
通过自定义for循环训练 |
| 简单熟悉的 API | 灵活强大的 API |
足够讨论框架了,你在这里是为了学习神经元。PyTorch 正是你所需要的。而且还有很多东西等着你去探索,以熟悉你的新的 PyTorch 工具箱。
5.2.5 一个时尚性感的 PyTorch 神经元
最后,是时候使用 PyTorch 框架构建一个神经元了。让我们通过预测您在本章前面清理过的姓名的性别来将所有这些付诸实践中。
你可以通过使用 PyTorch 来实现一个具有逻辑激活函数的单个神经元来开始 - 就像你在本章开头用来学习玩具示例的那个一样。
>>> import torch >>> class LogisticRegressionNN(torch.nn.Module): ... def __init__(self, num_features, num_outputs=1): ... super().__init__() ... self.linear = torch.nn.Linear(num_features, num_outputs) ... def forward(self, X): ... return torch.sigmoid(self.linear(X)) >>> model = LogisticRegressionNN(num_features=vecs.shape[1], num_outputs=1) >>> model LogisticRegressionNN( (linear): Linear(in_features=3663, out_features=1, bias=True) )
让我们看看这里发生了什么。我们的模型是一个类,它扩展了用于定义神经网络的 PyTorch 类torch.nn.Module。与每个 Python 类一样,它有一个称为*init*的构造函数方法。构造函数是你可以定义神经网络的所有属性的地方 - 最重要的是,模型的层。在我们的情况下,我们有一个极其简单的架构 - 一个具有单个神经元的层,这意味着只会有一个输出。输入的数量,或特征,将等于您的 TF-IDF 向量的长度,即您的特征的维度。我们的名字数据集中有 3663 个唯一的 1 元组,2 元组和 3 元组,所以这就是你在这个单个神经元网络中将拥有的输入数量。
你需要为你的神经网络实现的第二个关键方法是forward()方法。这个方法定义了输入如何通过模型的层传播 - 前向传播。如果你在想反向传播(backprop)在哪里,你很快就会看到,但它不在构造函数中。我们决定使用逻辑或 S 型激活函数作为我们的神经元 - 所以我们的forward()方法将使用 PyTorch 的内置函数sigmoid。
这些就是训练我们的模型所需的全部吗?还不够。你的神经元还需要学习另外两个至关重要的部分。一个是损失函数,或者你在本章前面看到的成本函数。如果这是一个回归问题,那么你在第四章学到的均方误差(MSE)会是一个很好的候选作为错误度量标准。对于这个问题,你正在做的是二元分类,所以二元交叉熵是一个更常见的错误(损失)度量标准。
这就是单个分类概率p的二元交叉熵的样子:
方程式 5.2:二元交叉熵
BCE = -(_y_ log _p_ + (1 - _y_) log1 - _p_)
函数的对数性质使其能够惩罚"自信地错误"的示例,当你的模型以高概率预测某个名字的性别是男性时,而实际上它更常被标记为女性。我们可以通过使用我们可用的另一个信息片段来帮助它使惩罚更与现实相关 - 我们数据集中特定性别的名字的频率。
>>> loss_func_train = torch.nn.BCELoss( ... weight=torch.Tensor(df[['count']][istrain].values)) >>> loss_func_test = torch.nn.BCELoss( # #1 ... weight=torch.Tensor(df[['count']][~istrain].values)) >>> loss_func_train BCELoss()
我们需要选择的最后一件事是根据损失调整权重的方式 - 优化器算法。还记得我们关于沿着损失函数的梯度"滑行"的讨论吗?实现向下滑行的最常见方式称为随机梯度下降(SGD)。与您的 Python 感知器所做的一样,它不是考虑您的整个数据集,而是仅根据一个样本或者可能是一小批样本计算梯度。
你的优化器需要两个参数来知道沿着损失斜率滑行的速度有多快或如何滑行 - 学习速率和动量。学习速率决定了在出现错误时,你的权重发生多大的变化 - 可以将其视为你的“滑行速度”。增加它可以帮助你的模型更快地收敛到局部最小值,但如果太大,每次接近最小值时都可能超过。在 PyTorch 中使用的任何优化器都会有一个学习速率。
动量是我们梯度下降算法的一个属性,它可以在朝正确方向移动时进行加速,并在远离目标时减速。我们如何决定这两个属性的值?和本书中看到的其他超参数一样,你需要优化它们以找出对你的问题最有效的值。现在,你可以选择一些任意的值作为超参数momentum和lr(学习率)。
>>> from torch.optim import SGD >>> hyperparams = {'momentum': 0.001, 'lr': 0.02} # #1 >>> optimizer = SGD( ... model.parameters(), **hyperparams) # #2 >>> optimizer SGD ( Parameter Group 0 dampening: 0 differentiable: False foreach: None lr: 0.02 maximize: False momentum: 0.001 nesterov: False weight_decay: 0 )
在运行我们的模型训练之前,最后一步是将测试和训练数据集转换为 PyTorch 模型可以处理的格式。
>>> X = vecs.values >>> y = (df[['sex']] == 'F').values >>> X_train = torch.Tensor(X[istrain]) >>> X_test = torch.Tensor(X[~istrain]) >>> y_train = torch.Tensor(y[istrain]) >>> y_test = torch.Tensor(y[~istrain])
最后,你准备好了这一章最重要的部分——性别学习!让我们来看一看,了解每一步发生了什么。
>>> from tqdm import tqdm >>> num_epochs = 200 >>> pbar_epochs = tqdm(range(num_epochs), desc='Epoch:', total=num_epochs) >>> for epoch in pbar_epochs: ... optimizer.zero_grad() # #1 ... outputs = model(X_train) ... loss_train = loss_func_train(outputs, y_train) # #2 ... loss_train.backward() # #3 ... optimizer.step() # #4 ... Epoch:: 100%|█████████████████████████| 200/200 [00:02<00:00, 96.26it/s]
真快!训练这个单一神经元大约需要几秒钟,大约 200 个纪元和每个纪元数以千计的例子。
看起来很简单,对吧?我们尽可能将步骤简化,让你能清楚地看到。但我们甚至不知道我们的模型表现如何!让我们添加一些实用函数,帮助我们观察神经元是否随着时间的推移而改进。这被称为仪器化。当然,我们可以看损失,但也可以用更直观的分数来评估我们的模型表现,比如准确性。
首先,你需要一个函数将我们从模块中获得的 PyTorch 张量转换回numpy数组:
>>> def make_array(x): ... if hasattr(x, 'detach'): ... return torch.squeeze(x).detach().numpy() ... return x
现在你可以使用这个实用程序函数来测量每次迭代在输出(预测)的张量上的准确性:
>>> def measure_binary_accuracy(y_pred, y): ... y_pred = make_array(y_pred).round() ... y = make_array(y).round() ... num_correct = (y_pred == y).sum() ... return num_correct / len(y)
现在你可以使用这个实用程序函数重新运行训练,以查看模型在每个纪元中损失和准确度的进展:
for epoch in range(num_epochs): optimizer.zero_grad() # #1 outputs = model(X_train) loss_train = loss_func_train(outputs, y_train) loss_train.backward() epoch_loss_train = loss_train.item() optimizer.step() outputs_test = model(X_test) loss_test = loss_func_test(outputs_test, y_test).item() accuracy_test = measure_binary_accuracy(outputs_test, y_test) if epoch % 20 == 19: # #2 print(f'Epoch {epoch}:' f' loss_train/test: {loss_train.item():.4f}/{loss_test:.4f},' f' accuracy_test: {accuracy_test:.4f}')
Epoch 19: loss_train/test: 80.1816/75.3989, accuracy_test: 0.4275 Epoch 39: loss_train/test: 75.0748/74.4430, accuracy_test: 0.5933 Epoch 59: loss_train/test: 71.0529/73.7784, accuracy_test: 0.6503 Epoch 79: loss_train/test: 67.7637/73.2873, accuracy_test: 0.6839 Epoch 99: loss_train/test: 64.9957/72.9028, accuracy_test: 0.6891 Epoch 119: loss_train/test: 62.6145/72.5862, accuracy_test: 0.6995 Epoch 139: loss_train/test: 60.5302/72.3139, accuracy_test: 0.7073 Epoch 159: loss_train/test: 58.6803/72.0716, accuracy_test: 0.7073 Epoch 179: loss_train/test: 57.0198/71.8502, accuracy_test: 0.7202 Epoch 199: loss_train/test: 55.5152/71.6437, accuracy_test: 0.7280
只需使用一个神经元的一组权重,你的简单模型就能在我们混乱、不确定、真实世界的数据集上达到超过 70%的准确率。现在你可以添加一些真实世界中的有形人工智能的例子以及一些我们的贡献者。
>>> X = vectorizer.transform( ... ['John', 'Greg', 'Vishvesh', # #1 ... ... 'Ruby', 'Carlana', 'Sarah']) # #2 >>> model(torch.Tensor(X.todense())) tensor([[0.0196], [0.1808], [0.3729], [0.4964], [0.8062], [0.8199]], grad_fn=<SigmoidBackward0>)
早些时候,我们选择使用值 1 来表示“女性”,使用值 0 来表示“男性”。前三个例子的名字,“John”,“Greg”和“Vishvesh”,是男人的名字,他们慷慨地为对我重要的开源项目做出了贡献,包括本书中的代码。看起来 Vish 的名字在美国男婴的出生证明上出现的次数不如 John 或 Greg 多。对于“John”而言,模型对于“Vishvesh”的字符 n-gram 中的男性意味更加确定。
接下来三个名字,“Sarah”,“Carlana” 和 ‘Ruby’,是我在写这本书时脑海中首先想到的女性名字。^([19]) ^([20]) 名字“Ruby”在其字符 n-grams 中可能带有一些男性特征,因为一个类似的名字“Rudy”(通常用于男婴)与“Ruby”之间只有一个编辑距离。奇怪的是,“Carlana”这个名字中包含一个常见的男性名字“Carl”,被自信地预测为一个女性名字。
5.3 沿着误差斜坡滑行
在神经网络中训练的目标是通过找到最佳参数(权重)来最小化损失函数。在优化循环的每一步,你的算法都会找到最陡的下坡方式。请记住,这个误差斜率不是你的数据集中一个特定示例的误差。它是在一批数据中所有点的所有误差的平均值上最小化成本(损失)。
创建这一问题的可视化图表可以帮助建立你在调整网络权重时所做的事情的心理模型。
在第四章中,你学习了关于均方根误差(RMSE)的知识,它是回归问题中最常见的成本函数。如果你想象一下将误差作为可能的权重的函数绘制出来,给定特定的输入和特定的预期输出,存在一个使该函数最接近零的点;这就是你的最小值—你的模型误差最小的位置。
这个最小值将是给定训练示例的最佳输出的一组权重。你经常会看到这被表示为一个三维碗,其中两个轴是二维权重向量,第三个是误差(见图 5.8)。这个描述是一个非常简化的,但是在高维空间中概念是相同的(对于具有两个以上权重的情况)。
图 5.6 凸误差曲线
同样地,你可以将误差表面作为训练集中所有输入的所有可能权重的函数进行图表化。但是你需要稍微调整一下误差函数。你需要找到一些能够代表给定一组权重的所有输入的聚合误差的东西。在这个例子中,你将使用均方误差作为z轴。同样,在这里,你会找到一个误差表面上的位置,在该位置的坐标是最小化你的预测和训练集中的分类标签之间的平均误差的权重向量。这组权重将配置你的模型以尽可能地适合整个训练集。
5.3.1 离开缆车,进入斜坡——梯度下降和局部最小值
这个可视化代表了什么?在每个时期,算法都在进行梯度下降,试图最小化误差。每次你调整权重的方向都希望下次能减少你的误差。一个凸错误曲面会很棒。站在滑雪坡上,四处看看,找出哪个方向是下坡,然后朝那个方向走!
但是你并不总是有这样一个光滑的碗形;它可能有一些分散的凹陷和坑洞。这种情况被称为非凸误差曲线。而且,就像滑雪一样,如果这些坑洞足够大,它们会吸引你,你可能就无法到达斜坡底部了。
再次强调,这些图表代表了二维输入的权重。但是如果你有一个 10 维、50 维或 1000 维的输入,概念是一样的。在那些更高维的空间中,再也无法将其可视化,所以你要相信数学。一旦你开始使用神经网络,可视化错误曲面就变得不那么重要了。你可以通过观察(或绘制)训练时间内的错误或相关指标,看它是否趋向于零来获取相同的信息。这将告诉你你的网络是否在正确的轨道上。但是这些 3D 表示法对于创建过程的心理模型是一个有用的工具。
但是非凸错误空间呢?那些凹陷和坑洞是个问题吗?是的,是的。取决于你随机开始权重的位置,你可能会以截然不同的权重结束,训练会停止,因为从这个局部最小值往下走别无选择(见图 5.9)。
图 5.7 非凸误差曲线
随着你进入更高维的空间,局部最小值也会跟随你到那里。
5.3.2 改变方式:随机梯度下降
到目前为止,你已经在尽力快速地聚合所有训练样本的错误,并且尽可能快地滑向最陡峭的路线。但是一次处理整个训练集一个样本有点短视。这就像选择雪地公园的下坡路段,忽略了所有的跳跃。有时,一个好的滑雪跳台可以帮助你跳过一些崎岖的地形。
如果你试图一次性训练整个数据集,你可能会耗尽内存,导致你的训练在 SWAP 中陷入困境 —— 在 RAM 和你的更慢的持久性磁盘存储之间来回交换数据。而这个单一静态的错误曲面可能会有陷阱。因为你是从一个随机起点开始的(初始模型权重),你可能会盲目地滑下山坡进入一些局部最小值(凹陷、坑洞或洞穴)。你可能不知道存在更好的权重值选项。而且你的错误曲面是静态的。一旦你在错误曲面上达到一个局部最小值,就没有下坡的斜度来帮助你的模型滑出去,然后滑下山去。
因此,为了让事情更有变化,您希望向这个过程添加一些随机性。您希望周期性地对模型学习的训练样本的顺序进行洗牌。通常在每次通过训练数据集后重新洗牌训练样本的顺序。洗牌您的数据会改变模型考虑每个样本的预测误差的顺序。因此,它将改变其寻找全局最小值(该数据集的最小模型误差)的路径。这种洗牌是随机梯度下降的"随机"部分。
"梯度下降"方法的"梯度"估计部分仍然有改进的余地。您可以向优化器添加一些谦卑,这样它就不会过于自信,盲目地跟随每一个新的猜测,一直到它认为全局最小值应该在哪里。在您所在的滑雪道很少会直接指向山底滑雪小屋的直线方向。因此,你的模型沿着向下坡的方向(梯度)行进了一小段距离,而不是一直走到底。这样,每个独立样本的梯度就不会使您的模型偏离太远,您的模型也不会迷失在树林中。您可以调整 SGD 优化器(随机梯度下降)的学习率超参数,以控制您的模型对每个独立样本的梯度有多自信。
另一种训练方法是批量学习。一个批次是训练数据的一个子集,比如,0.1%、1%、10%或 20%的数据集。每个批次都会创建一个新的错误表面,让你在搜索未知的"全局"错误表面最小值时进行实验。你的训练数据只是真实世界中会发生的例子的样本。因此,您的模型不应假设"全局"真实世界的错误表面形状与训练数据的任何部分的错误表面相同。
这导致了大多数 NLP 问题的最佳策略:小批量学习。^([21]) Geoffrey Hinton 发现,对于大多数神经网络训练问题,大约 16 到 64 个样本的批次大小是最佳的。^([22]) 这是平衡了随机梯度下降的不稳定性和您希望朝向全局最小值正确方向取得显著进展的正确大小。当您朝着这个变化的局部最小值前进,并且使用正确的数据和正确的超参数时,您可以更容易地朝全局最小值迈进。小批量学习是完整批次学习和单个样本训练之间的一种折衷。小批量学习使您既能享受随机学习(随机徘徊)的好处,又能享受梯度下降学习(直接加速下坡)的好处。
尽管反向传播的工作细节非常吸引人 ^([23]), 但它们并不是微不足道的,我们不会在这里解释细节。一个可以帮助你训练模型的好的心理形象是,想象一下你问题的误差曲面就像某个外星行星上的未知地形。你的优化器只能看到你脚下地面的坡度。它利用这些信息向下走了几步,然后再次检查坡度(梯度)。用这种方式探索行星可能需要很长时间。但是一个好的优化算法可以帮助你的神经网络记住地图上的所有好位置,并用它们猜测地图上的一个新位置,以便在寻找全局最小值时探索。在地球上,这个行星表面上的最低点是南极地区丹曼冰川下面的峡谷底部,比海平面低 3.5 公里。^([24]) 一个好的小批量学习策略将帮助你找到最陡的滑雪道或冰川(如果你怕高不是一个愉快的形象)到全局最小值。希望你很快会发现自己在山脚下的滑雪小屋旁或丹曼冰川下的冰洞里的篝火旁。
看看你是否可以在本章中创建的感知器上添加额外的层。看看随着网络复杂性的增加,你所得到的结果是否会改善。对于小问题来说,更大不一定就更好。
5.4 自我测试
- 罗森布拉特的人工神经元无法解决的简单 AI 逻辑 “问题” 是什么?
- 对罗森布拉特架构进行了什么小改变,"修复"了感知器并结束了第一次 “AI 冬天”?
- PyTorch 的
model.forward()函数在 Scikit-Learn 模型中的等价物是什么? - 如果你跨年份和地区聚合名称,用于性别预测的
LogisticRegression模型的测试集准确率会是多少?不要忘记分层你的测试集以避免作弊。
5.5 总结
- 通过最小化成本函数,机器逐渐学习更多关于词语的知识。
- 反向传播算法是网络学习的手段。
- 权重对模型误差的贡献量与它需要更新的量直接相关。
- 神经网络本质上是优化引擎。
- 监控误差逐渐降低,注意训练过程中的陷阱(局部极小值)。
[1] 在这里查看 Dario Amodei 和 Danny Hernandez 的分析( openai.com/blog/ai-and-compute/)
[2] 查看第三章关于 “过拟合” 问题的词形还原 FAQ 聊天机器人示例失败的情况。
[3] 有关 Julie Beth Lovins 的维基百科文章:en.wikipedia.org/wiki/Julie_Beth_Lovins
[4] nlp.stanford.edu/IR-book/html/htmledition/stemming-and-lemmatization-1.html
[5] proai.org/middle-button-subreddit
[6] Robin Jia, Building Robust NLP Systems ( robinjia.GitHub.io/assets/pdf/robinjia_thesis.pdf)
[7] Not Even Wrong: The Failure of String Theory and the Search for Unity in Physical Law by Peter Woit
[8] Lex Fridman interview with Peter Woit ( lexfridman.com/peter-woit/)
[9] Rosenblatt, Frank (1957), The perceptron—a perceiving and recognizing automaton. Report 85-460-1, Cornell Aeronautical Laboratory.
[10] en.wikipedia.org/wiki/Universal_approximation_theorem
[11] The logistic activation function can be used to turn a linear regression into a logistic regression: ( scikit-learn.org/stable/auto_examples/linear_model/plot_logistic.html)
[12] scikit-learn.org/stable/modules/linear_model.html#logistic-regression
[13] McElreath, Richard, and Robert Boyd, Mathematical Models of Social Evolution: A guide for the perplexed, University of Chicago Press, 2008.
[14] USCDI (US Core Data Interoperability) ISA (Interoperability Standards Advisory) article on “Sex (Assigned at Birth)” ( www.healthit.gov/isa/uscdi-data/sex-assigned-birth)
[15] from “When I am pinned and wriggling on the wall” in “The Love Song of J. Alfred Prufrock” by T. S. Eliot ( www.poetryfoundation.org/poetrymagazine/poems/44212/the-love-song-of-j-alfred-prufrock)
[16] Overview of Coreference Resolution at The Stanford Natural Language Processing Group: ( nlp.stanford.edu/projects/coref.shtml)
[17] The Perifpheral by William Gibson on wikipedia ( en.wikipedia.org/wiki/The_Peripheral)
[18] False dichotomy article on wikipedia ( en.wikipedia.org/wiki/False_dilemma)
[19] Sarah Goode Wikipedia article ( en.wikipedia.org/wiki/Sarah_E._Goode)
[20] Ruby Bridges Wikipedia article ( en.wikipedia.org/wiki/Ruby_Bridges)
[21] “Faster SGD training by minibatch persistency”, by Fischetti et al ( arxiv.org/pdf/1806.07353.pdf)
[22] Neural Networks for Machine Learning - Overview of mini-batch gradient descent by Geoffrey Hinton ( www.cs.toronto.edu/~hinton/coursera/lecture6/lec6.pdf)
[23] Wikpedia, en.wikipedia.org/wiki/Backpropagation
[24] Wikipedia list of places below sea level ( en.wikipedia.org/wiki/List_of_places_on_land_with_elevations_below_sea_level)
第六章:使用词嵌入(词向量)进行推理的 6 个原因
本章涵盖内容
- 理解词嵌入或词向量
- 用向量表示含义
- 定制词嵌入以创建特定领域的向量
- 使用词嵌入进行推理
- 可视化单词的含义
词嵌入可能是你 NLP 工具箱中最易于接近和普遍有用的工具。它们可以让你的 NLP 流水线对单词有一个一般的理解。在本章中,你将学习如何将词嵌入应用到现实世界的应用中。同样重要的是,你将学会在哪里不要使用词嵌入。希望这些例子能帮助你在商业和个人生活中构想出新的有趣的应用。
你可以把单词向量想象成 Dota 2 英雄或角色扮演游戏(RPG)角色和怪物的属性列表。现在想象一下,这些角色表或简介上没有文字。你会希望保持所有数字的顺序一致,这样你就知道每个数字的含义。这就是词向量的工作方式。这些数字没有标记它们的含义。它们只是放在向量中的一个一致的位置或位置。这样,当你将两个单词向量相加、相减或相乘时,一个向量中的“力量”属性就与另一个向量中的力量属性相匹配。同样适用于 D&D(龙与地下城)中的“敏捷”、“智力”和阵营或哲学属性。
富有思想的角色扮演游戏经常鼓励对哲学和单词进行更深入的思考,例如"混乱善良"或"法律邪恶"等角色个性的微妙组合。我非常感激我的童年主持人开启了我的眼界,让我看到了像“善”和“恶”或“守法”和“混乱”这样的单词所暗示的错误二分法。[1] 在这里你将学习到的词向量有足够的空间来表达几乎任何文本和任何语言中的单词的每个可能的可量化属性。并且单词向量的属性或特征以复杂的方式相互交织在一起,可以轻松处理诸如“守法邪恶”,“善意的独裁者”和“利他的恶意”等概念。
学习词嵌入通常被归类为表示学习算法。[2] 任何词嵌入的目标都是构建一个单词“特征”的紧凑数值表示。这些数值表示使得机器能够以有意义的方式处理你的单词(或你的 Dota 2 角色表)。
6.1 这是你的单词大脑
词嵌入是我们用来表示含义的向量。而你的大脑是存储含义的地方。你的大脑受到单词的影响。就像化学物质影响大脑一样,单词也会影响大脑。“This is your brain on drugs”是 80 年代反毒品电视广告活动的一句流行口号,其中有一对鸡蛋在煎锅中煎炸。[3]
幸运的是,文字比化学物质更温和、更有益的影响者。图 6.1 中显示的文字在你大脑中的形象与鸡蛋在煎锅中滋滋作响有些不同。这张草图为你提供了一种想象的方式,当你阅读这些句子时,你的神经元会火花四溅,创造出你大脑内的思维。你的大脑通过向适当的邻近神经元发送信号将这些词的意义连接在一起。词嵌入是这些单词之间连接的矢量表示。因此,它们也是你大脑中神经元连接网络的节点嵌入的一种粗略表示。^([4])
图 6.1 你大脑中的词嵌入
当你思考一个单词时,你可以把词嵌入看作是你大脑中神经元触发模式的矢量表示。每当你想到一个词,这个思想就会在你的大脑中引发一波电荷和化学反应,在与该词或思想相关联的神经元开始。你的大脑内部的神经元像水池中扔下的圆形涟漪一样波动。但是,这些电信号只有从某些神经元流出,而不是其他神经元。
当你阅读这个句子中的词时,你会在你的神经元中引发一连串的活动,就像图 6.1 中的草图中那样。事实上,研究人员发现人工神经网络权重与词嵌入相似的模式和你思考词语时大脑内部活动的模式。^([5]) ^([6])
神经元中流出的电子就像学生在放学铃声响起时从学校门口跑出来一样。词语或思想就像学校的铃声。当然,你的思维和大脑中的电子比学生要快得多。你甚至不需要说出或听到这个词,就能在你的大脑中触发它的模式。你只需要想一想它。就像孩子们跑出去玩耍一样,电子永远不会沿着相同的路径流动。正如一个词的意义随着时间的推移而演变,你对一个词的嵌入也在不断演变。你的大脑是一个永不停止的语言学习者,与康奈尔大学的无止境语言学习者系统并没有太大的不同。^([7])
有些人对这个想法产生了过多的幻想,他们认为你可以用言辞实现一种形式的心灵控制。当我在 Reddit 上寻找关于 NLP 研究的信息时,我被 r/NLP 子论坛分散了注意力。这不是你想的那样。事实证明,一些励志演讲者在 Reddit 上为他们的 70 年代的"神经语言编程"赚钱计划占了" NLP"这个词。幸运的是,词向量能够很好地处理这种歧义和错误信息。
你甚至不需要告诉词向量算法你希望" NLP"这个词的含义是什么。它将根据你用于训练它的文本中它的用法找出这个缩写词最有用和最流行的含义。创建词向量的算法是一种自监督的机器学习算法。这意味着你不需要词典或同义词词典来喂养你的算法。你只需要很多文本。在本章的后面,你只需收集一堆维基百科文章来用作你的训练集。但任何语言的任何文本都可以,只要它包含了你感兴趣的很多单词。
还有一个需要考虑的"大脑上的文字"。文字不仅影响你的思维方式,还影响你的交流方式。而且你有点像是集体意识中的一个神经元,是社会的大脑。对我来说,“有点"这个词是一个特别强大的神经连接模式,因为我是从丹尼尔·丹尼特的《直觉泵》一书中学到了它的含义。它唤起了与复杂思想和词语相关联的联想,比如图灵用来解释 AI 和计算器背后机制完全相同的概念"渐进主义”。达尔文使用渐进主义这个概念来解释语言理解人类大脑如何通过简单机制从单细胞生物进化而来。
自然语言处理实战第二版(MEAP)(三)(3)https://developer.aliyun.com/article/1517956