·请参考本系列目录:【英文文本分类实战】之一——实战项目总览
·下载本实战项目资源:神经网络实现英文文本分类.zip(pytorch)
这里涉及到文本分类任务中:词典、词向量两个概念。
首先明确我们做的是“英文文本分类”,所以是不需要像中文那样分词的,只用按照空格截取英文单词就行。
假设训练集train.csv
中有10w个文本,我们以空格为分隔符截取英文单词,一共截下来2w个单词,我们把这些单词从0到2w依次编号,称为“词典”。
【注】:这样的词典是不能接受的。从机器学习的角度看,词典中的某些词只出现一两次,对分类完全没有影响,反而徒增算法计算量(可以去加了解一下TF-IDF的思想)。同样,在神经网络模型中,也需要把每个词表示成一个n维的词向量,所以我们可能只去前1w个频率最高的词,放入“词典”。
要知道,我们只以空格为分隔符截取英文单词肯定不行,因为文本中不缺乏特殊字符、标点符号、开头单词大小写等等问题,所以我们要进行数据清洗。
那么我们如何评判清洗后的词典的优劣呢?
由于我们会使用预训练的词向量,可以以预训练词向量中的词典覆盖率作为参考。
【注】:使用预训练词向量是常用操作,需要仔细了解。
总结:由于文本中有特殊字符、标点符号、开头单词大小写等等问题,会干扰到提取的词典,所以需要清洗文本数据。我们计算提取的词典与预训练词向量中的词典的覆盖率作为提取的词典的好坏标准。
vocab字典,建立单词与其出现频次的映射,代码如下:
# ## 创建英文词典
def build_vocab(sentences, verbose=True):
vocab = {}
for sentence in tqdm(sentences, disable=(not verbose)):
for word in sentence:
try:
vocab[word] += 1
except KeyError:
vocab[word] = 1
return vocab
# ## 进度条初始化
tqdm.pandas()
# ## 加载数据集
df = pd.read_csv("../@_数据集/TLND/data/labelled_newscatcher_dataset.csv", encoding='utf-8', sep=';')
# ## 创建词典
sentences = df['title'].progress_apply(lambda x: x.split()).values
vocab = build_vocab(sentences)
我们来看一下建立好的vocab字典是什么样子,任取几个单词看一下:
{'Insulated': 3, 'Mergers': 9, 'Acquisitions': 9}
词向量是指用一组数值来表示一个汉字或者词语,这也是因为计算机只能进行数值计算。最简单的方法是one-hot,假如总的有一万个词,那词向量就一万维,词对应的那维为1,其他为0,但这样的表示维度太高也太稀疏了。
所以后来就开始用一个维度小的稠密向量来表示,词向量一般都50,100,200或者300维。预训练指提前训练好这种词向量,对应的是一些任务可以输入词id,然后在做具体的任务内部训练词向量,这样出来的词向量不具有通用性,而预训练的词向量,是在极大样本上训练的结果,有很好的通用性,无论什么任务都可以直接拿来用具体的训练方法。
从最开始的word2vec,elmo到现在的bert。
一些预训练词向量下载地址为:
glove
网址:https://nlp.stanford.edu/projects/glove/
GLOVE的工作原理类似于Word2Vec。上面可以看到Word2Vec是一个“预测”模型,它预测给定单词的上下文,GLOVE通过构造一个共现矩阵(words X context)来学习,该矩阵主要计算单词在上下文中出现的频率。因为它是一个巨大的矩阵,我们分解这个矩阵来得到一个低维的表示。有很多细节是相互配合的,但这只是粗略的想法。
网址:https://fasttext.cc/
FastText与上面的两个嵌入有很大的不同。Word2Vec和GLOVE将每个单词作为最小的训练单元,而FastText使用n-gram字符作为最小的单元。例如,单词vector,“apple”,可以分解为单词vector的不同单位,如“ap”,“app”,“ple”。使用FastText的最大好处是,它可以为罕见的单词,甚至是在训练过程中没有看到的单词生成更好的嵌入,因为n-gram字符向量与其他单词共享。这是Word2Vec和GLOVE无法做到的。
# ## 加载预训练词向量
def load_embed(file):
def get_coefs(word, *arr):
return word, np.asarray(arr, dtype='float32')
if file == '../@_词向量/fasttext/wiki-news-300d-1M.vec':
embeddings_index = dict(get_coefs(*o.split(" ")) for o in open(file,encoding='utf-8') if len(o) > 100)
else:
embeddings_index = dict(get_coefs(*o.split(" ")) for o in open(file, encoding='latin'))
return embeddings_index
# ## 加载词向量
glove = '../@_词向量/glove/glove.6B.50d.txt'
fasttext = '../@_词向量/fasttext/wiki-news-300d-1M.vec'
embed_glove = load_embed(glove)
embed_fasttext = load_embed(fasttext)
创建了词典后并且加载了预训练词向量后,我们编写代码来查看覆盖情况:
# ## 检查预训练embeddings和vocab的覆盖情况
def check_coverage(vocab, embeddings_index):
known_words = {} # 两者都有的单词
unknown_words = {} # embeddings不能覆盖的单词
nb_known_words = 0 # 对应的数量
nb_unknown_words = 0
# for word in vocab.keys():
for word in tqdm(vocab):
try:
known_words[word] = embeddings_index[word]
nb_known_words += vocab[word]
except:
unknown_words[word] = vocab[word]
nb_unknown_words += vocab[word]
pass
print('Found embeddings for {:.2%} of vocab'.format(len(known_words) / len(vocab))) # 覆盖单词的百分比
print('Found embeddings for {:.2%} of all text'.format(
nb_known_words / (nb_known_words + nb_unknown_words))) # 覆盖文本的百分比,与上一个指标的区别的原因在于单词在文本中是重复出现的。
unknown_words = sorted(unknown_words.items(), key=operator.itemgetter(1))[::-1]
print("unknown words : ", unknown_words[:30])
return unknown_words
oov_glove = check_coverage(vocab, embed_glove)
oov_fasttext = check_coverage(vocab, embed_fasttext)
查看输出:
100%|██████████| 108774/108774 [00:00<00:00, 343998.85it/s]
100%|██████████| 108774/108774 [00:00<00:00, 313406.97it/s]
100%|██████████| 123225/123225 [00:00<00:00, 882523.98it/s]
Found embeddings for 18.88% of vocab
Found embeddings for 51.99% of all text
unknown words : [('COVID-19', 6245), ('The', 5522), ('New', 3488), ('Market', 2932), ('Covid-19', 2621), ('–', 2521), ('To', 2458), ('US', 2184), ('Coronavirus', 2122), ('How', 2041), ('A', 2029), ('In', 1904), ('Global', 1786), ('Is', 1720), ('With', 1583), ('Of', 1460), ('For', 1450), ('Trump', 1440), ('And', 1361), ('Man', 1340), ('August', 1310), ('Apple', 1148), ('UK', 1132), ('On', 1122), ('What', 1113), ('Coronavirus:', 1077), ('Google', 1054), ('League', 1044), ('Why', 1032), ('China', 987)]
100%|██████████| 123225/123225 [00:00<00:00, 786827.09it/s]
Found embeddings for 50.42% of vocab
Found embeddings for 87.50% of all text
unknown words : [('COVID-19', 6245), ('Covid-19', 2621), ('Coronavirus:', 1077), ('Covid', 824), ('2020:', 736), ('COVID', 670), ('TikTok', 525), ('cases,', 470), ("Here's", 414), ('LIVE:', 345), ('Size,', 318), ('Share,', 312), ('COVID-19:', 297), ('news:', 280), ('Trends,', 270), ('TheHill', 263), ('coronavirus:', 259), ('Report:', 256), ('Analysis,', 247), ('Fortnite', 245), ("won't", 242), ('Growth,', 234), ("It's", 234), ('Covid-19:', 229), ("it's", 224), ("Trump's", 223), ('English.news.cn', 203), ('COVID-19,', 196), ('Here’s', 196), ('updates:', 177)]
分析上面的输出:
大写的问题很严重,我们先把数据集的文本中,大写字母转小写。
# ## 词典全部小写
print("=========转化小写后")
sentences = df['title'].apply(lambda x: x.lower())
sentences = sentences.progress_apply(lambda x: x.split()).values
vocab_low = build_vocab(sentences)
oov_glove = check_coverage(vocab_low, embed_glove)
oov_fasttext = check_coverage(vocab_low, embed_fasttext)
查看输出:
=========转化小写后
100%|██████████| 108774/108774 [00:00<00:00, 354067.79it/s]
100%|██████████| 108774/108774 [00:00<00:00, 317075.17it/s]
100%|██████████| 102482/102482 [00:00<00:00, 1007395.77it/s]
Found embeddings for 42.97% of vocab
Found embeddings for 86.47% of all text
unknown words : [('covid-19', 8897), ('–', 2521), ('covid', 1543), ('coronavirus:', 1338), ('2020:', 736), ('—', 626), ('tiktok', 529), ('covid-19:', 526), ("here's", 525), ('cases,', 502), ('live:', 464), ("it's", 459), ('news:', 363), ('size,', 326), ('updates:', 325), ('share,', 316), ("won't", 310), ("don't", 297), ('report:', 295), ("'the", 291), ('review:', 287), ('trends,', 280), ('covid-19,', 269), ('thehill', 263), ('update:', 260), ('analysis,', 253), ('here’s', 252), ("'i", 249), ('fortnite', 245), ('growth,', 244)]
100%|██████████| 102482/102482 [00:00<00:00, 778784.32it/s]
Found embeddings for 37.95% of vocab
Found embeddings for 85.17% of all text
unknown words : [('covid-19', 8897), ('covid', 1543), ('coronavirus:', 1338), ('2020:', 736), ('tiktok', 529), ('covid-19:', 526), ("here's", 525), ('cases,', 502), ('live:', 464), ("it's", 459), ('meghan', 401), ('news:', 363), ('size,', 326), ('updates:', 325), ('share,', 316), ("won't", 310), ("don't", 297), ('huawei', 297), ('report:', 295), ('sushant', 292), ('review:', 287), ('trends,', 280), ('covid-19,', 269), ('thehill', 263), ('update:', 260), ('analysis,', 253), ('here’s', 252), ('fortnite', 245), ('growth,', 244), ('xiaomi', 237)]
可以看到,转化为小写后,覆盖率有了极大的提升,但是85%左右的覆盖率还远远不够。我们尝试再去除特殊字符。
# ## 去除特殊字符
def clean_special_chars(text, punct, mapping):
for p in mapping:
text = text.replace(p, mapping[p])
for p in punct:
text = text.replace(p, f' {p} ')
specials = {'\u200b': ' ', '…': ' ... ', '\ufeff': '', 'करना': '', 'है': ''} # Other special characters that I have to deal with in last
for s in specials:
text = text.replace(s, specials[s])
return text
# ## 去除特殊字符
print("=========去除特殊字符后")
punct = "/-'?!.,#$%\'()*+-/:;<=>@[\\]^_`{|}~" + '""“”’' + '∞θ÷α•à−β∅³π‘₹´°£€\×™√²—–&'
punct_mapping = {"‘": "'", "₹": "e", "´": "'", "°": "", "€": "e", "™": "tm", "√": " sqrt ", "×": "x", "²": "2", "—": "-", "–": "-", "’": "'", "_": "-", "`": "'", '“': '"', '”': '"', '“': '"', "£": "e", '∞': 'infinity', 'θ': 'theta', '÷': '/', 'α': 'alpha', '•': '.', 'à': 'a', '−': '-', 'β': 'beta', '∅': '', '³': '3', 'π': 'pi', }
sentences = df['title'].apply(lambda x: clean_special_chars(x, punct, punct_mapping))
sentences = sentences.apply(lambda x: x.lower()).progress_apply(lambda x: x.split()).values
vocab_punct = build_vocab(sentences)
oov_glove = check_coverage(vocab_punct, embed_glove)
oov_fasttext = check_coverage(vocab_punct, embed_fasttext)
查看输出:
=========去除特殊字符后
100%|██████████| 108774/108774 [00:00<00:00, 359101.46it/s]
100%|██████████| 108774/108774 [00:00<00:00, 368426.97it/s]
100%|██████████| 55420/55420 [00:00<00:00, 1182299.35it/s]
Found embeddings for 80.79% of vocab
Found embeddings for 97.48% of all text
unknown words : [('covid', 11852), ('tiktok', 680), ('fortnite', 372), ('bbnaija', 288), ('thehill', 263), ('ps5', 260), ('oneplus', 212), ('jadon', 168), ('havertz', 144), ('wechat', 135), ('researchandmarkets', 132), ('redmi', 129), ('realme', 116), ('brexit', 108), ('xcloud', 94), ('valorant', 84), ('note20', 69), ('airpods', 68), ('nengi', 67), ('vaping', 64), ('fiancé', 62), ('selfie', 62), ('pokémon', 60), ('1000xm4', 59), ('sarri', 55), ('iqoo', 52), ('miui', 52), ('wassce', 51), ('beyoncé', 50), ('kiddwaya', 49)]
100%|██████████| 55420/55420 [00:00<00:00, 1066584.97it/s]
Found embeddings for 67.35% of vocab
Found embeddings for 95.61% of all text
unknown words : [('covid', 11852), ('tiktok', 680), ('meghan', 435), ('fortnite', 372), ('huawei', 364), ('sushant', 326), ('markle', 302), ('bbnaija', 288), ('xiaomi', 276), ('thehill', 263), ('ps5', 260), ('oneplus', 212), ('degeneres', 176), ('jadon', 168), ('buhari', 164), ('cagr', 162), ('solskjaer', 150), ('havertz', 144), ('perseid', 140), ('wechat', 135), ('researchandmarkets', 132), ('redmi', 129), ('fauci', 98), ('xcloud', 94), ('ardern', 93), ('valorant', 84), ('rtx', 83), ('akufo', 82), ('sadc', 82), ('kildare', 82)]
可以看到,去除特殊字符后,覆盖率达到了97.5%,已经够用了。观察生词,这些都是最近几年出的新词,如‘covid’、'tiktok’等,这是无法改变的。
【注】:因为预训练词向量是在大规模语料库上训练出的,我们使用了预训练词向量后,可以选择在反向传播时不去修改嵌入层的参数(固定下来),也可以选择在使用预训练词向量后,在反向传播时继续修改嵌入层的参数(这叫微调,微调随着Bert的提出越来越流行)。
【英文文本分类实战】之四——词典提取与词向量提取