开始编辑:2024/2/16;最后编辑2024/2/16
教程出自:https://learn.microsoft.com/en-sg/training/modules/intro-natural-language-processing-pytorch/
第二部分:https://blog.csdn.net/qq_33345365/article/details/136142152
本博客旨在探讨处理自然语言文本的不同神经网络架构。近年来,自然语言处理(Natural Language Processing,NLP)经历了快速发展,主要是由于语言模型能够在大型文本语料库上使用无监督训练时更快地准确“理解”人类语言。例如,使用GPT-3或BERT等预训练文本模型生成句子,简化了许多NLP任务,并显著提高了性能。
本博客将重点关注在PyTorch中将NLP表示为张量的基本方面,以及经典的NLP架构,如使用词袋(BoW)、词嵌入、循环神经网络和生成网络。
以下是一些常见的自然语言处理 (NLP) 任务:
本博客将主要关注**文本分类**任务。使用新闻标题中的文本来分类它们属于四个类别中的哪一个:World, Sports, Business, Sci/Tech。此外还介绍生成模型,它可以自我生成类似人类的文本序列。
huggingface transformers的auto class分类,包含了目前大部分机器学习分类,详见https://blog.csdn.net/qq_33345365/article/details/136126773?spm=1001.2014.3001.5501
如果想用神经网络解决自然语言处理(NLP)任务,则需要一些方法将文本表示为张量。计算机已经将文本字符表示为数字,这些数字使用ASCII或UTF-8等编码映射到屏幕上的字体。程序员知道每个字母代表什么,以及所有的字符如何组合成一个句子的单词。然而,计算机本身并没有这样的理解,神经网络必须在训练过程中学习其含义。
因此,在表示文本时,可以使用不同的方法:
token: 为了统一这些方法,我们通常将原子文本片段称为一个token。在某些情况下,token可以是字母,在其他情况下,可以是单词或单词的一部分。
例如,我们可以选择将“indivisible”标记为“
in
-divis
-ible
”,其中‘-’符号表示该标记是前一个单词的延续。这将允许"divis"始终由一个token表示,对应于一个核心含义。
序列化tokenization:将文本转换为标记序列的过程。
向量化vectorization:为了可以将标记输入神经网络,为序列化的每个标记分配一个数字的过程称为vectorization,通常通过构建token词汇表来完成。
版本一览,如果无法运行,请参考如下版本与类进行安装:
pip install -r https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/nlp-pytorch/requirements.txt
# 如果打不开,那么将下面内容复制到新文件“requirements.txt”,然后pip install -r 新文件.txt即可
gensim==3.8.3
huggingface==0.0.1
matplotlib
nltk==3.5
numpy==1.18.5
opencv-python==4.5.1.48
Pillow==7.1.2
scikit-learn
scipy
torch==1.8.1
torchaudio==0.8.1
torchinfo==0.0.8
torchtext==0.9.1
torchvision==0.9.1
transformers==4.3.3
在本模块中,我们将从基于AG_NEWS样本数据集的简单文本分类任务开始,即将新闻标题分为四类:World, Sports, Business and Sci/Tech。这个数据集是由PyTorch的torchtext模块构建的。
import torch
import torchtext
import os
import collections
# import portalocker
os.makedirs('./data',exist_ok=True)
train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')
classes = ['World', 'Sports', 'Business', 'Sci/Tech']
在这里,’ train_dataset ‘和’ test_dataset '包含分别返回标签(类的数量)和文本对的迭代器,例如:
a = next(train_dataset)
print(a)
# 输出为:
# (3, "Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\band of ultra-cynics, are seeing green again.")
打印数据集中的前5个新标题:
for i,x in zip(range(5),train_dataset):
# x[0]存放标题种类,可能是0,1,2,3,x[1]是标题
print(f"**{classes[x[0]]}** -> {x[1]}\n")
#输出为:
# **Sci/Tech** -> Carlyle Looks Toward Commercial Aerospace (Reuters) Reuters - Private investment firm Carlyle Group,\which has a reputation for making well-timed and occasionally\controversial plays in the defense industry, has quietly placed\its bets on another part of the market.
# **Sci/Tech** -> Oil and Economy Cloud Stocks' Outlook (Reuters) Reuters - Soaring crude prices plus worries\about the economy and the outlook for earnings are expected to\hang over the stock market next week during the depth of the\summer doldrums.
# **Sci/Tech** -> Iraq Halts Oil Exports from Main Southern Pipeline (Reuters) Reuters - Authorities have halted oil export\flows from the main pipeline in southern Iraq after\intelligence showed a rebel militia could strike\infrastructure, an oil official said on Saturday.
# **Sci/Tech** -> Oil prices soar to all-time record, posing new menace to US economy (AFP) AFP - Tearaway world oil prices, toppling records and straining wallets, present a new economic menace barely three months before the US presidential elections.
# **Sci/Tech** -> Stocks End Up, But Near Year Lows (Reuters) Reuters - Stocks ended slightly higher on Friday\but stayed near lows for the year as oil prices surged past #36;46\a barrel, offsetting a positive outlook from computer maker\Dell Inc. (DELL.O)
因为数据集是迭代器,如果我们想多次使用数据,我们需要将其转换为列表list:
train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')
train_dataset = list(train_dataset)
test_dataset = list(test_dataset)
现在我们需要将文本转换为可以表示为张量的数字,并将其输入神经网络。第一步是序列化:将文本转换为标记。如果我们使用word-level表示,每个词将由它自己的token表示。我们将使用torchtext模块的内置标记器:
tokenizer = torchtext.data.utils.get_tokenizer('basic_english')
PyTorch的tokenizer被用来拆分前两篇新闻文章中的单词和空格。本例使用basic_english作为tokenizer来理解语言结构。这将返回文本和字符的字符串列表。
basic_english
分词器是一种简单分词器,它将文本分割成空格分隔的单词。它将所有单词转换为小写,并删除所有标点符号。
first_sentence = train_dataset[0][1]
second_sentence = train_dataset[1][1]
f_tokens = tokenizer(first_sentence) # 使用tokenizer进行序列化
s_tokens = tokenizer(second_sentence)
print(f'\nfirst token list:\n{f_tokens}') # 标题被序列化后的结果
print(f'\nsecond token list:\n{s_tokens}')
# 输出
# first token list:
# ['wall', 'st', '.', 'bears', 'claw', 'back', 'into', 'the', 'black', '(', 'reuters', ')', 'reuters', '-', 'short-sellers', ',', 'wall', 'street', "'", 's', 'dwindling\\band', 'of', 'ultra-cynics', ',', 'are', 'seeing', 'green', 'again', '.']
# second token list:
# ['carlyle', 'looks', 'toward', 'commercial', 'aerospace', '(', 'reuters', ')', 'reuters', '-', 'private', 'investment', 'firm', 'carlyle', 'group', ',', '\\which', 'has', 'a', 'reputation', 'for', 'making', 'well-timed', 'and', 'occasionally\\controversial', 'plays', 'in', 'the', 'defense', 'industry', ',', 'has', 'quietly', 'placed\\its', 'bets', 'on', 'another', 'part', 'of', 'the', 'market', '.']
接下来,要将文本转换为数字,需要构建包含所有标记的词汇表。首先使用Counter对象构建字典,然后创建一个Vocab对象来帮助我们处理向量化:
counter = collections.Counter() # 初始化字典
for (label, line) in train_dataset:
counter.update(tokenizer(line)) # 构建字典
vocab = torchtext.vocab.Vocab(counter, min_freq=1) # 用来处理向量化
为了查看每个单词如何映射到词汇表,需要遍历列表中的每个单词以查找它在vocab中的索引号。每个单词或字符都显示其相应的索引。例如,单词“the”在两个句子中都出现了几次,它在词汇表中的唯一索引是数字3。
此处使用了python的列表推导式,可以参考https://blog.csdn.net/qq_33345365/article/details/136102620?spm=1001.2014.3001.5501了解相关知识
word_lookup = [list((vocab[w], w)) for w in f_tokens]
print(f'\nIndex lockup in 1st sentence:\n{word_lookup}')
word_lookup = [list((vocab[w], w)) for w in s_tokens]
print(f'\nIndex lockup in 2nd sentence:\n{word_lookup}')
# 输出
# Index lockup in 1st sentence:
# [[432, 'wall'], [426, 'st'], [2, '.'], [1606, 'bears'], [14839, 'claw'], [114, 'back'], [67, 'into'], [3, 'the'], [849, 'black'], [14, '('], [28, 'reuters'], [15, ')'], [28, 'reuters'], [16, '-'], [50726, 'short-sellers'], [4, ','], [432, 'wall'], [375, 'street'], [17, "'"], [10, 's'], [67508, 'dwindling\\band'], [7, 'of'], [52259, 'ultra-cynics'], [4, ','], [43, 'are'], [4010, 'seeing'], [784, 'green'], [326, 'again'], [2, '.']]
# Index lockup in 2nd sentence:
# [[15875, 'carlyle'], [1073, 'looks'], [855, 'toward'], [1311, 'commercial'], [4251, 'aerospace'], [14, '('], [28, 'reuters'], [15, ')'], [28, 'reuters'], [16, '-'], [930, 'private'], [798, 'investment'], [321, 'firm'], [15875, 'carlyle'], [99, 'group'], [4, ','], [27658, '\\which'], [29, 'has'], [6, 'a'], [4460, 'reputation'], [12, 'for'], [565, 'making'], [52791, 'well-timed'], [9, 'and'], [80618, 'occasionally\\controversial'], [2126, 'plays'], [8, 'in'], [3, 'the'], [526, 'defense'], [242, 'industry'], [4, ','], [29, 'has'], [3891, 'quietly'], [82815, 'placed\\its'], [6575, 'bets'], [11, 'on'], [207, 'another'], [360, 'part'], [7, 'of'], [3, 'the'], [127, 'market'], [2, '.']]
使用词汇表,可以很容易地将标记字符串编码为一组数字。以第一篇新闻文章为例:
vocab_size = len(vocab)
print(f"Vocab size if {vocab_size}")
def encode(x):
return [vocab.stoi[s] for s in tokenizer(x)] # 列表推导式
vec = encode(first_sentence)
print(vec)
# 输出
# Vocab size if 95812
# [432, 426, 2, 1606, 14839, 114, 67, 3, 849, 14, 28, 15, 28, 16, 50726, 4, 432, 375, 17, 10, 67508, 7, 52259, 4, 43, 4010, 784, 326, 2]
在这个代码中,torchtext的vocab.stoi
字典允许我们将字符串表示转换为数字(stoi代表“从字符串到整数”)。要将文本从数字表示转换回文本,我们可以使用词汇表。它的字典执行反向查找:
def decode(x):
return [vocab.itos[i] for i in x]
print(decode(vec))
# ['wall', 'st', '.', 'bears', 'claw', 'back', 'into', 'the', 'black', '(', 'reuters', ')', 'reuters', '-', 'short-sellers', ',', 'wall', 'street', "'", 's', 'dwindling\\band', 'of', 'ultra-cynics', ',', 'are', 'seeing', 'green', 'again', '.']
单词标记化的一个限制是有些单词是多单词表达的一部分,例如,单词“hot dog”与其他上下文中的单词 “hot” 和 “dog”具有完全不同的含义。如果总是用相同的向量来表示单词“hot”和“dog”,就会混淆模型。为了解决这个问题,有时在文档分类中使用N-gram表示,其中每个单词、双单词或三单词的频率是训练分类器的有用特征。
为了获得n-gram表示,可以使用ngrams_iterator
函数,该函数将token序列转换为n-gram序列。在下面的代码中,我们将从我们的新闻数据集中构建bigram词汇表:
from torchtext.data.utils import ngrams_iterator
bi_counter = collections.Counter()
for (label, line) in train_dataset:
bi_counter.update(ngrams_iterator(tokenizer(line),ngrams=2))
bi_vocab = torchtext.vocab.Vocab(bi_counter, min_freq=2)
print(f"Bigram vocab size = {len(bi_vocab)}")
# 输出为:
# Bigram vocab size = 481971 for min_freq=2
# Bigram vocab size = 294491 for min_freq=3
def encode(x):
return [bi_vocab.stoi[s] for s in tokenizer(x)]
print(encode(first_sentence))
# 输出为:
# [572, 564, 2, 2326, 49106, 150, 88, 3, 1143, 14, 32, 15, 32, 16, 443749, 4, 572, 499, 17, 10, 0, 7, 468770, 4, 52, 7019, 1050, 442, 2]
N-gram方法的主要缺点是词汇表的大小会以极快的速度增长。这里为Vocab构造函数指定min_freq标志,以避免那些只在文本中出现一次的标记。还可以进一步增加min_freq,因为不经常出现的单词/短语通常对分类的准确性影响很小。
尝试将set min_freq参数设置为更大的值,观察词汇表长度的变化。
在实践中,n-gram词汇表的大小仍然太高,无法将单词表示为单热向量,因此我们需要将这种表示与一些降维技术结合起来,例如嵌入,我们将在后面的单元中讨论。
本博客讲述了如何用数字来表示每个单词。现在,为了建立文本分类模型,需要将整句(或整篇新闻文章)输入神经网络。这里的问题是每篇文章/句子都有不同的长度;但是全连接或卷积神经网络只能处理固定的输入大小。有两种方法可以解决这个问题:
整体代码如下所示:
import torch
import torchtext
import os
import collections
os.makedirs('./data', exist_ok=True)
train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')
classes = ['World', 'Sports', 'Business', 'Sci/Tech']
a = next(train_dataset)
print(a)
for i, x in zip(range(5), train_dataset):
print(f"**{classes[x[0]]}** -> {x[1]}")
train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')
train_dataset = list(train_dataset)
test_dataset = list(test_dataset)
tokenizer = torchtext.data.utils.get_tokenizer('basic_english')
first_sentence = train_dataset[0][1]
second_sentence = train_dataset[1][1]
f_tokens = tokenizer(first_sentence)
s_tokens = tokenizer(second_sentence)
print(f'\nfirst token list:\n{f_tokens}')
print(f'\nsecond token list:\n{s_tokens}')
counter = collections.Counter() # 初始化字典
for (label, line) in train_dataset:
counter.update(tokenizer(line)) # 构建字典
vocab = torchtext.vocab.Vocab(counter, min_freq=1) # 用来处理向量化
word_lookup = [list((vocab[w], w)) for w in f_tokens]
print(f'\nIndex lockup in 1st sentence:\n{word_lookup}')
word_lookup = [list((vocab[w], w)) for w in s_tokens]
print(f'\nIndex lockup in 2nd sentence:\n{word_lookup}')
vocab_size = len(vocab)
print(f"Vocab size if {vocab_size}")
def encode(x):
return [vocab.stoi[s] for s in tokenizer(x)]
vec = encode(first_sentence)
print(vec)
def decode(x):
return [vocab.itos[i] for i in x]
print(decode(vec))
from torchtext.data.utils import ngrams_iterator
bi_counter = collections.Counter()
for (label, line) in train_dataset:
bi_counter.update(ngrams_iterator(tokenizer(line), ngrams=2))
bi_vocab = torchtext.vocab.Vocab(bi_counter, min_freq=3)
print(f"Bigram vocab size = {len(bi_vocab)}")
def encode(x):
return [bi_vocab.stoi[s] for s in tokenizer(x)]
print(encode(first_sentence))