前言

折腾搜索相关的功能,有感记录,想让程序判断"爬虫"和"蜘蛛"意思相近、"爬虫"和"火锅"不相干,这种事对人 trivial 到不值一提,对程序却是要命的原生问题。翻了些资料,发现这个问题的答案就是NLP的第一块基石:词向量。

从一个尴尬说起

计算机表示一个词最原始的办法是one-hot编码:词表里有五万个词,就造一个五万维的向量,这个词所在的位置填1,其他全是0。这样做有两个要命的问题:

  • 维度爆炸,词表越大向量越长,而且绝大多数位置都是浪费的0
  • 所有词两两之间距离都一样,"电脑"和"计算机"的距离,与"电脑"和"螃蟹"的距离分毫不差,任何语义信息都没有

要让程序理解相似性,就得把词放进一个低维的、有意义的数学空间里,让意思相近的词在这个空间里挨着。这样的向量就叫词向量,也叫词的分布式表示(distributed representation)。

凭什么意思相近的词会挨着

理论基础是语言学家Firth那句著名的话:You shall know a word by the company it keeps(观其伴而知其义)。一个词的意思,是由它周围经常出现的词决定的。"爬虫"周围总是出现"网页""抓取""HTTP","蜘蛛"周围也总出现这些,那它俩的向量自然就该靠近。

把这个思想工程化的就是2013年Google Mikolov那篇著名的Word2Vec论文,两种结构:

  • CBOW:拿上下文词去预测中间那个词
  • Skip-gram:反过来,拿中间的词去预测上下文里都出现了谁

训练过程说白了就是拿一个滑动窗口在语料上扫,窗口里词共现就是正样本。但有个计算上的坑:按原始定义,预测词表里五万个词谁该出场,分母要把五万个词都遍历一遍,这个开销扛不住。工程解法叫负采样(negative sampling):正样本之外,随机从词表里抽几个词当反面教材,把五万分类问题变成几个二分类问题,训练开销直接砍到能跑。就这么一个看起来糙的近似,效果却出奇的好。

跑一下感受感受

gensim几行代码就能在自己语料上训练:

from gensim.models import Word2Vec

# 语料:分好词的句子列表,中文需要先分词
corpus = [
    ["网络", "爬虫", "抓取", "网页", "数据"],
    ["蜘蛛", "也是", "爬虫", "的", "一种", "叫法"],
    ["火锅", "配", "毛肚", "最佳"],
    # ......真实场景下这里应该是几万条句子
]

# sg=1用Skip-gram,negative=5负采样,vector_size向量维度
model = Word2Vec(sentences=corpus, vector_size=100, window=5,
                 min_count=1, sg=1, negative=5, workers=4)

print(model.wv.most_similar("爬虫"))  # 语义最近的邻居

词向量最出圈的玩法是算术:

model.wv.most_similar(positive=["国王", "女人"], negative=["男人"])
# 期望出来"女王",国王-男人+女人≈女王

向量空间里居然隐含着性别、时态、首都国家这类关系,2013年这一点震惊了整个圈子里外:

$$ \text{国王} - \text{男人} + \text{女人} \approx \text{女王} $$

语料太小的时候上面这些效果出不来,真实项目里一般要么拿自己领域的大语料训练,要么直接下载别人用海量新闻训练好的向量用。

它都用在哪些地方

  • 搜索召回与关键词扩展:query和文档都变成向量,算相似度
  • 推荐系统的内容理解
  • 文本分类、情感分析的底层特征
  • 机器翻译早期的双语词向量对齐

最后

词向量把NLP从"处理字符串"带到了"计算语义"的时代,是后来一切预训练模型的起点。但它也有个天生缺陷:一个词只有一副面孔,"苹果手机"和"吃苹果"里的苹果在它眼里是同一个向量。这个坑怎么补上的,放下篇说。

标签: none

评论已关闭