自然语言处理实战第二版(MEAP)(三)(3)https://developer.aliyun.com/article/1517956
6.4.3 学习没有字典的含义
对于这个 Word2Vec 训练示例,您不需要使用字典,比如 wiktionary.org 来明确定义单词的含义。相反,您可以只让 Word2Vec 读取包含有意义的句子的文本。您将使用 PyTorch 中 torchtext 包中提供的 WikiText2 语料库。
>>> import torchtext >>> dsets = torchtext.datasets.WikiText2() >>> num_texts = 10000 >>> filepath = DATA_DIR / f'WikiText2-{num_texts}.txt' >>> with open(filepath, 'wt') as fout: ... fout.writelines(list(dsets[0])[:num_texts])
为了让它更不神秘,您可以查看您刚刚从WikiText2数据集中创建的包含约 10,000 个段落的文本文件:
>>> !tail -n 3 ~/nessvec-data/WikiText2-10000.txt When Marge leaves Dr. Zweig 's office , she says , " Whenever the wind whistles through the leaves , I 'll think , Lowenstein , Lowenstein … " . This is a reference to The Prince of Tides ; the <unk> is Dr. Lowenstein . = = Reception = =
第 99,998 段碰巧包含缩写"Dr.“。在这种情况下,缩写是为了单词"doctor”。您可以利用这个来练习您的"妈妈是一名医生"直觉泵。因此,您很快就会发现 Word2Vec 是否能学会什么是医生。或者它可能会因为使用"Dr."表示"drive"的街道地址而感到困惑。
方便的是,WikiText2 数据集已经将文本分词成单词。单词之间用单个空格(" ")字符分隔。因此,您的管道不必决定"Dr.“是否是句子的结尾。如果文本没有被分词,您的 NLP 管道将需要删除所有句子末尾的句号。甚至标题分隔符文本"=="也已经被拆分为两个独立的标记"="和"="。段落由换行("\n")字符分隔。对于维基百科标题如"== 接待 ==",将创建许多"段落”,同时保留段落之间的所有空行。
您可以利用像 SpaCy 这样的句子边界检测器或句子分割器将段落分割成句子。这将防止您的 Word2Vec 训练对从一个句子溢出到另一个句子。尊重句子边界的 Word2Vec 可以提高词嵌入的准确性。但是我们将把这个决定留给您,看您是否需要额外的准确性提升。
这里的管道可以处理的一个关键基础设施是大型语料库的内存管理。如果您正在对数百万段落进行词嵌入的训练,您将需要使用一个管理磁盘上文本的数据集对象,只加载需要的部分到 RAM 或 GPU 中。Hugging Face Hub 的datasets包可以为您处理这个问题:
>>> import datasets >>> dset = datasets.load_dataset('text', data_files=str(filepath)) >>> dset DatasetDict({ train: Dataset({ features: ['text'], num_rows: 10000 }) })
但是,您仍然需要告诉 Word2Vec 什么是单词。这是您需要担心的唯一"监督"Word2Vec 数据集。您可以使用第二章中最简单的分词器来实现良好的结果。对于这种空格分词的文本,您只需使用str.split()方法。您可以使用str.lower()进行大小写折叠,将您的词汇表大小减半。令人惊讶的是,这已经足够让 Word2Vec 学会单词的含义和内涵,以至于能够解决类似 SAT 测试中可能会看到的类比问题,并且甚至能够推理现实世界的对象和人。
def tokenize_row(row): row['all_tokens'] = row['text'].lower().split() return row
现在,您可以在包含数据行迭代序列的 torchtext 数据集上使用您的分词器,每行数据都有一个用于 WikiText2 数据的"text"键。
>>> dset = dset.map(tokenize_row) >>> dset DatasetDict({ train: Dataset({ features: ['text', 'tokens'], num_rows: 10000 }) })
您需要为数据集计算词汇表,以处理神经网络的一热编码和解码。
>>> vocab = list(set( ... [tok for row in dset['train']['tokens'] for tok in row])) >>> vocab[:4] ['cast', 'kaifeng', 'recovered', 'doctorate'] >>> id2tok = dict(enumerate(vocab)) >>> list(id2tok.items())[:4] [(0, 'cast'), (1, 'kaifeng'), (2, 'recovered'), (3, 'doctorate')] >>> tok2id = {tok: i for (i, tok) in id2tok.items()} >>> list(tok2id.items())[:4] [('cast', 0), ('kaifeng', 1), ('recovered', 2), ('doctorate', 3)]
唯一剩下的特征工程步骤是通过对令牌序列进行窗口化,然后在这些窗口内配对跳字来创建跳字对。
WINDOW_WIDTH = 10 >>> def windowizer(row, wsize=WINDOW_WIDTH): """ Compute sentence (str) to sliding-window of skip-gram pairs. """ ... doc = row['tokens'] ... out = [] ... for i, wd in enumerate(doc): ... target = tok2id[wd] ... window = [ ... i + j for j in range(-wsize, wsize + 1, 1) ... if (i + j >= 0) & (i + j < len(doc)) & (j != 0) ... ] ... out += [(target, tok2id[doc[w]]) for w in window] ... row['moving_window'] = out ... return row
一旦你将 windowizer 应用于你的数据集,它将有一个 ‘window’ 键,其中将存储标记的窗口。
>>> dset = dset.map(windowizer) >>> dset DatasetDict({ train: Dataset({ features: ['text', 'tokens', 'window'], num_rows: 10000 }) })
这是你的 skip_gram 生成函数:
>>> def skip_grams(tokens, window_width=WINDOW_WIDTH): ... pairs = [] ... for i, wd in enumerate(tokens): ... target = tok2id[wd] ... window = [ ... i + j for j in ... range(-window_width, window_width + 1, 1) ... if (i + j >= 0) ... & (i + j < len(tokens)) ... & (j != 0) ... ] ... pairs.extend([(target, tok2id[tokens[w]]) for w in window]) # huggingface datasets are dictionaries for every text element ... return pairs
你的神经网络只需要窗口化数据中的跳字对:
>>> from torch.utils.data import Dataset >>> class Word2VecDataset(Dataset): ... def __init__(self, dataset, vocab_size, wsize=WINDOW_WIDTH): ... self.dataset = dataset ... self.vocab_size = vocab_size ... self.data = [i for s in dataset['moving_window'] for i in s] ... ... def __len__(self): ... return len(self.data) ... ... def __getitem__(self, idx): ... return self.data[idx]
而且你的 DataLoader 会为你处理内存管理。这将确保你的管道可重用于几乎任何大小的语料库,甚至是整个维基百科。
from torch.utils.data import DataLoader dataloader = {} for k in dset.keys(): dataloader = { k: DataLoader( Word2VecDataset( dset[k], vocab_size=len(vocab)), batch_size=BATCH_SIZE, shuffle=True, num_workers=CPU_CORES - 1) }
你需要一个独热编码器将你的词对转换成独热向量对:
def one_hot_encode(input_id, size): vec = torch.zeros(size).float() vec[input_id] = 1.0 return vec
为了揭示你之前看到的示例的一些魔力,你将从头开始训练网络,就像你在第五章中所做的一样。你可以看到,Word2Vec 神经网络几乎与你之前章节中的单层神经网络相同。
from torch import nn EMBED_DIM = 100 # #1 class Word2Vec(nn.Module): def __init__(self, vocab_size=len(vocab), embedding_size=EMBED_DIM): super().__init__() self.embed = nn.Embedding(vocab_size, embedding_size) # #2 self.expand = nn.Linear(embedding_size, vocab_size, bias=False) def forward(self, input): hidden = self.embed(input) # #3 logits = self.expand(hidden) # #4 return logits
一旦实例化你的 Word2Vec 模型,你就可以为你词汇表中的 20000 多个词创建 100 维嵌入:
>>> model = Word2Vec() >>> model Word2Vec( (embed): Embedding(20641, 100) (expand): Linear(in_features=100, out_features=20641, bias=False) )
如果你有 GPU,你可以将模型发送到 GPU 来加快训练速度:
>>> import torch >>> if torch.cuda.is_available(): ... device = torch.device('cuda') >>> else: ... device = torch.device('cpu') >>> device device(type='cpu')
如果你没有 GPU,不用担心。在大多数现代 CPU 上,这个 Word2Vec 模型将在不到 15 分钟内训练完毕。
>>> model.to(device) Word2Vec( (embed): Embedding(20641, 100) (expand): Linear(in_features=100, out_features=20641, bias=False) )
现在是有趣的部分!你可以看到 Word2Vec 快速地学习了“Dr.”等成千上万个标记的含义,只是通过阅读大量的文本。你可以去泡杯茶或吃些巧克力,或者只是冥想 10 分钟,思考生命的意义,而你的笔记本电脑则在思考单词的意义。首先,让我们定义一些训练参数
>>> from tqdm import tqdm # noqa >>> EPOCHS = 10 >>> LEARNING_RATE = 5e-4 EPOCHS = 10 loss_fn = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=LEARNING_RATE)
running_loss = [] pbar = tqdm(range(EPOCHS * len(dataloader['train']))) for epoch in range(EPOCHS): epoch_loss = 0 for sample_num, (center, context) in enumerate(dataloader['train']): if sample_num % len(dataloader['train']) == 2: print(center, context) # center: tensor([ 229, 0, 2379, ..., 402, 553, 521]) # context: tensor([ 112, 1734, 802, ..., 28, 852, 363]) center, context = center.to(device), context.to(device) optimizer.zero_grad() logits = model(input=context) loss = loss_fn(logits, center) if not sample_num % 10000: # print(center, context) pbar.set_description(f'loss[{sample_num}] = {loss.item()}') epoch_loss += loss.item() loss.backward() optimizer.step() pbar.update(1) epoch_loss /= len(dataloader['train']) running_loss.append(epoch_loss) save_model(model, loss)
6.4.4 Word2Vec 的计算技巧
在最初的出版之后,通过各种计算技巧提高了 word2vec 模型的性能。在本节中,我们重点介绍了三个关键改进,这些改进有助于词嵌入在更少的计算资源或训练数据下实现更高的准确性:
- 将频繁的二元组添加到词汇表中
- 欠采样(子采样)频繁的标记
- 负例的欠采样
频繁的二元组
一些词经常与其他词组合在一起形成复合词。例如,“Aaron”经常跟在“Swartz”后面,“AI”经常跟在“Ethics”后面。由于单词“Swartz”跟在单词“Aaron”后面的概率高于平均水平,你可能想为“Aaron Swartz”创建一个单一的复合专有名词的单词向量。为了提高 Word2Vec 嵌入在涉及专有名词和复合词的应用中的准确性,Mikolov 的团队在他们的 Word2Vec 词汇表中包括了一些二元组和三元组。团队使用共现频率来识别应该被视为单个术语的二元组和三元组,使用以下评分函数:
方程 6.5 大二元组评分函数
Bigram 分数是两个单词一起出现的次数除以它们分别出现的次数。当单词在一起出现的次数足够多时,它们将作为一对标记包含在 Word2Vec 词汇中,通过用下划线替换空格,比如 "ice_cream"。你会注意到许多词嵌入模型的词汇,比如 Word2vec,包含诸如 “New_York” 或 “San_Francisco” 的术语。这样,这些术语将被表示为单个向量,而不是两个单独的向量,比如 “San” 和 “Francisco”。
单词对的另一个影响是,单词组合通常表示的意义与单个单词的向量之和不同。例如,MLS 足球队 “Portland Timbers” 与单词 “Portland” 或 “Timbers” 的单词具有不同的含义。但是通过将经常出现的二元组添加到 Word2vec 模型中,它们的嵌入可以包含在您的模型中使用的嵌入词汇中,而无需您为文本中的二元组训练自定义嵌入。
可视化词嵌入的一种好方法是使用诸如主成分分析(PCA)之类的算法将它们的维度降至二维。这样你就可以得到一个地图,展示词嵌入之间的关系。当你要绘制的词是地名,比如美国城市时,这可以揭示词语的有趣地理维度。词嵌入为你提供了词语的 “北部性”、“南部性”、“东部性” 和 “西部性” 特性。甚至在词语中还有一点高度可以通过词嵌入来衡量。
图 6.8 美国城市词图
如果你在 2-D 图中绘制诸如 “New” 和 “York” 这样的词语,如 6.8,它们不会出现在 “New York” 一词的嵌入附近。
对频繁标记进行欠采样
对原始算法的另一个准确性改进是对频繁词语进行欠采样(子采样)。这也被称为“欠采样多数类”,以平衡类别权重。常见词语,如“the”和“a”,通常不包含大多数自然语言处理问题相关的信息和含义,因此被称为停用词。Mikolov 和其他人经常选择对这些词语进行子采样。子采样意味着在连续 skip-gram 或 CBOW 的语料抽样过程中随机忽略它们。许多博主会把这一做法发挥到极致,在预处理过程中完全删除它们。虽然进行子采样或过滤停用词可能有助于让您的词向量算法训练得更快,但有时可能产生反效果。而且,在现代计算机和应用中,训练时间提高 1% 不太可能抵消词向量精度的损失。而且停用词与语料库中其他“词语”的共现可能会导致词向量表示中出现含糊不清的词语之间的较不有意义的连接,从而通过错误的语义相似性训练来混淆 Word2Vec 表示。
重要提示
所有词语都有意义,包括停用词。因此,在训练词向量或组成词汇表时,不应完全忽略或跳过停用词。此外,由于词向量常用于生成模型(例如 Cole 在本书中用于组合句子的模型),停用词和其他常见词语必须包含在词汇表中,并允许其影响其相邻词语的词向量。
为了减少像停用词这样频繁出现的词语的强调,训练过程中对词语进行抽样,抽样概率与其频率成反比。这种影响类似于 IDF 对 TF-IDF 向量的影响。频繁出现的词语对向量的影响要小于罕见的词语。Tomas Mikolov 使用以下方程确定抽样给定词语的概率。该概率决定了在训练期间是否包含特定词语在特定 skip-gram 中:
方程式 6.6 中 Mikolov 的 Word2Vec 论文中的子采样概率
word2vec 的 C++ 实现使用了与论文中提到的略有不同的抽样概率,但效果相同:
方程式 6.7 中 Mikolov 的 word2vec 代码中的子采样概率
在上述方程中,f(w[i]) 表示语料库中词语的频率,t 表示希望在其上应用子采样概率的频率阈值。阈值取决于语料库大小、平均文档长度以及这些文档中使用的词语种类。文献中通常使用 10^(-5) 和 10^(-6) 之间的值。
如果一个词在整个语料库中出现了 10 次,而你的语料库有一百万个不同的单词,并且你将子采样阈值设为 10^(-6),那么在任何特定的 n-gram 中保留该单词的概率为 68%。在分词过程中,你将在 32% 的时间内跳过它们。
Mikolov 表明,子采样提高了词向量的准确性,例如回答类比问题。
负采样
Mikolov 提出的最后一个技巧是负采样的概念。如果向网络提供了一对词的单个训练示例,它将导致网络的所有权重被更新。这会改变词汇表中所有单词的所有向量的值。但是如果你的词汇表包含数千个或数百万个单词,更新大型的独热向量的所有权重是低效的。为了加速词向量模型的训练,Mikolov 使用了负采样。
Mikolov 提出,与其更新未包含在词窗口中的所有单词权重,不如只对几个负样本(在输出向量中)进行采样以更新它们的权重。不是更新所有权重,而是选择 n 个负例词对(不匹配该示例的目标输出的单词)并更新导致其特定输出的权重。这样,计算量可以大大减少,并且训练网络的性能不会显著下降。
注意
如果你用小语料库训练你的词模型,你可能想使用 5 到 20 个样本的负采样率。对于更大的语料库和词汇表,你可以将负采样率降低到两到五个样本,根据 Mikolov 及其团队的说法。
使用 gensim.word2vec 模块
如果前面的部分听起来太复杂,别担心。各种公司提供了预训练的词向量模型,而不同编程语言的流行 NLP 库允许你高效地使用这些预训练模型。在下一节中,我们将看看如何利用词向量的魔力。对于词向量,你将使用流行的 gensim 库,这是你在第四章中首次看到的。
如果你已经安装了 nlpia 包,你可以使用以下命令下载预训练的 word2vec 模型:
>>> from nlpia.data.loaders import get_data >>> word_vectors = get_data('word2vec')
如果这对你不起作用,或者你喜欢自己动手,你可以搜索 word2vec 在 Google News 文档上预训练的模型。在找到并下载了 Google 的原始二进制格式模型并将其放在本地路径后,你可以像这样使用 gensim 包加载它:
>>> from gensim.models.keyedvectors import KeyedVectors >>> word_vectors = KeyedVectors.load_word2vec_format(\ ... '/path/to/GoogleNews-vectors-negative300.bin.gz', binary=True)
使用单词向量可能会消耗大量内存。如果您的可用内存有限,或者如果您不想等待几分钟才能加载单词向量模型,您可以通过传递limit关键字参数来减少加载到内存中的单词数量。在以下示例中,您将从 Google 新闻语料库中加载前 20 万个最常见的单词:
>>> from gensim.models.keyedvectors import KeyedVectors >>> from nlpia.loaders import get_data >>> word_vectors = get_data('w2v', limit=200000) # #1
但请记住,具有有限词汇量的单词向量模型会导致您的 NLP 流水线性能较低,如果您的文档包含尚未加载单词向量的单词。因此,在开发阶段,您可能只想限制单词向量模型的大小。对于本章中的其余示例,如果您想获得我们在此处展示的相同结果,则应使用完整的 Word2Vec 模型。
gensim.KeyedVectors.most_similar()方法提供了一种有效的方式来找到任何给定词向量的最近邻居。关键字参数positive接受一个要相加的向量列表,类似于本章开头的足球队示例。类似地,您可以使用negative参数进行减法操作并排除不相关的术语。参数topn确定应作为返回值提供多少相关术语。
与传统的同义词词典不同,Word2Vec 的同义词(相似性)是一个连续的分数,一个距离。这是因为 Word2Vec 本身是一个连续的向量空间模型。Word2Vec 高维度和每个维度的连续值使其能够捕捉任何给定单词的完整含义范围。这就是为什么类比甚至是 zeugma,同一个词内多个意义的奇怪的并列,都不成问题。处理类比和 zeugma 是一件很重要的事情。理解类比和 zeugma 需要对世界的人类水平的理解,包括常识知识和推理[⁴²]。词嵌入足以让机器至少能够对您可能在 SAT 测验中看到的类比有一定了解。
>>> word_vectors.most_similar(positive=['cooking', 'potatoes'], topn=5) [('cook', 0.6973530650138855), ('oven_roasting', 0.6754530668258667), ('Slow_cooker', 0.6742032170295715), ('sweet_potatoes', 0.6600279808044434), ('stir_fry_vegetables', 0.6548759341239929)] >>> word_vectors.most_similar(positive=['germany', 'france'], topn=1) [('europe', 0.7222039699554443)]
单词向量模型还允许您确定不相关的术语。gensim库提供了一个名为doesnt_match的方法:
>>> word_vectors.doesnt_match("potatoes milk cake computer".split()) 'computer'
为了确定列表中最不相关的术语,该方法返回与所有其他列表术语的距离最大的术语。
如果您想执行计算(例如著名的例子king + woman - man = queen,这是最初引起 Mikolov 和他的顾问兴奋的例子),您可以通过向most_similar方法调用添加negative参数来实现:
>>> word_vectors.most_similar(positive=['king', 'woman'], ... negative=['man'], topn=2) [('queen', 0.7118192315101624), ('monarch', 0.6189674139022827)]
gensim库还允许您计算两个术语之间的相似度。如果您想比较两个单词并确定它们的余弦相似度,请使用方法.similarity():
>>> word_vectors.similarity('princess', 'queen') 0.70705315983704509
如果您想要开发自己的函数并使用原始单词向量进行工作,您可以通过 Python 的方括号语法([])或KeyedVector实例上的get()方法访问它们。您可以将加载的模型对象视为字典,其中您感兴趣的单词是字典键。返回的数组中的每个浮点数代表一个向量维度。在谷歌的词模型中,您的 numpy 数组的形状将为 1x300。
>>> word_vectors['phone'] array([-0.01446533, -0.12792969, -0.11572266, -0.22167969, -0.07373047, -0.05981445, -0.10009766, -0.06884766, 0.14941406, 0.10107422, -0.03076172, -0.03271484, -0.03125 , -0.10791016, 0.12158203, 0.16015625, 0.19335938, 0.0065918 , -0.15429688, 0.03710938, ...
如果您想知道所有这些数字意味着什么,您可以找到答案。但这需要大量的工作。您需要检查一些同义词,并查看它们在数组中共享的 300 个数字中的哪些。或者,您可以找到这些数字的线性组合,构成像“位置”和“女性”之类的维度,就像您在本章的开头所做的那样。
6.4.6 生成自己的词向量表示
在某些情况下,您可能希望创建自己的特定领域的单词向量模型。这样做可以提高您的模型准确性,如果您的 NLP 管道正在处理使用词汇方式与 Google News 中 2006 年以前 Mikolov 训练的参考word2vec模型不同的文档,则会更加如此。请记住,您需要大量的文档来做到这一点,就像 Google 和 Mikolov 一样。但是,如果您的词在 Google News 上特别罕见,或者您的文本在受限领域内以独特的方式使用它们,比如医学文本或转录文本,则特定于领域的单词模型可能会提高您的模型准确性。在接下来的部分中,我们将向您展示如何训练您自己的word2vec模型。
为了训练一个特定于领域的word2vec模型,您将再次转向gensim,但在您开始训练模型之前,您需要使用第二章中发现的工具对语料库进行预处理。
预处理步骤
首先,您需要将文档分成句子,然后将句子分成标记。gensim的word2vec模型期望得到一个句子列表,其中每个句子被分成标记。这可以防止单词向量学习邻近句子中的无关单词出现。您的训练输入应该类似于以下结构:
>>> token_list [ ['to', 'provide', 'early', 'intervention/early', 'childhood', 'special', 'education', 'services', 'to', 'eligible', 'children', 'and', 'their', 'families'], ['essential', 'job', 'functions'], ['participate', 'as', 'a', 'transdisciplinary', 'team', 'member', 'to', 'complete', 'educational', 'assessments', 'for'] ... ]
要将句子分割成标记,然后将句子转换为标记,您可以应用第二章中学到的各种策略。让我们再增加一个:Detector Morse 是一个句子分段器,它改进了 NLTK 和gensim中提供的准确性分段器的某些应用场景。[43] 它已经在《华尔街日报》的多年文本中进行了预训练。因此,如果您的语料库包含与《华尔街日报》类似的语言,那么 Detector Morse 很可能会为您提供目前可能的最高准确性。如果您拥有来自您领域的大量句子集合,还可以在自己的数据集上重新训练 Detector Morse。一旦您将文档转换为标记列表(每个句子一个列表),您就可以开始进行word2vec训练了。
训练您的领域特定的word2vec模型
通过加载word2vec模块开始:
>>> from gensim.models.word2vec import Word2Vec
训练需要一些设置细节。
列表 6.2 控制 word2vec 模型训练的参数
>>> num_features = 300 # #1 >>> min_word_count = 3 # #2 >>> num_workers = 2 # #3 >>> window_size = 6 # #4 >>> subsampling = 1e-3 # #5
现在,您可以开始培训了。
列表 6.3 实例化 word2vec 模型
>>> model = Word2Vec( ... token_list, ... workers=num_workers, ... size=num_features, ... min_count=min_word_count, ... window=window_size, ... sample=subsampling)
根据您的语料库大小和 CPU 性能,训练将需要相当长的时间。对于较小的语料库,训练可以在几分钟内完成。但是对于一个全面的词模型,语料库将包含数百万句子。您需要有关语料库中所有不同单词的所有不同用法的几个示例。如果开始处理较大的语料库,例如维基百科语料库,预期训练时间会更长,并且内存消耗量会更大。
另外,Word2Vec 模型可能会消耗大量内存。但请记住,只有隐藏层的权重矩阵才感兴趣。一旦训练了您的词模型,如果您冻结模型并丢弃不必要的信息,您可以将内存占用减少约一半。以下命令将丢弃神经网络的不需要的输出权重:
>>> model.init_sims(replace=True)
init_sims方法将冻结模型,存储隐藏层的权重并丢弃预测单词共现的输出权重。输出权重不是大多数 Word2Vec 应用程序所使用的向量的一部分。但是一旦丢弃了输出层的权重,模型就无法再进一步训练。
您可以使用以下命令保存已训练的模型,并将其保留以供以后使用:
>>> model_name = "my_domain_specific_word2vec_model" >>> model.save(model_name)
如果要测试您新训练的模型,可以使用与前一节学到的相同方法。
列表 6.4 加载已保存的word2vec模型
>>> from gensim.models.word2vec import Word2Vec >>> model_name = "my_domain_specific_word2vec_model" >>> model = Word2Vec.load(model_name) >>> model.most_similar('radiology')
自然语言处理实战第二版(MEAP)(三)(5)https://developer.aliyun.com/article/1517966