面试里我爱问的几个小问题(一):词向量

面试里,我偶尔会扔几个「看起来很基础」的小问题。不是为了为难人,而是想快速判断:对方有没有把概念真正内化,还是只停在名词层面。

这篇是系列第一篇,从现代 NLP 的基础 – 词向量说起。

核心性质可以压成一句话:语义或概念相近的词,在向量空间中的距离也更近。比如苹果、香蕉、西瓜之类的水果在一起,北京、上海、杭州之类的地点在一起。那么,请问这种性质的来源是什么?是碰巧还是有原因的呢?

说说我的理解:这个问题的答案与语言学规律和算法设计都有关系,缺一不可。

当我自己在学 word-to-vec 的时候,总是会听到一个名次叫 distributional hypothesis,中文翻译是分布式假设,我觉得这个词翻译的十分差,以至于有好多年我都没体会到这个概念是啥含义。直到有一天,我觉得他是想 表示语言学的分布规律,即词语的出现和他所在内容的上下文有关系,句子和词不是随便出现的;譬如“我要吃”的后面总是跟着一个食物的概念,而不会出现一个地点或者什么别的东西。

我让 Claude 给做了一个 demo 来展示中心词和上下文之间的关系:

互动 — 分布规律

一个窗口扫过语料,语义就自己冒出来了

下面十三行是一小段语料。一个宽度固定的窗口从第一个词开始,逐个词往后挪; 每停一次,就把落在窗口里的词记成中心词的「上下文」。规则只有这一条,没有模型,没有向量。 盯着水果那三行和城市那三行——扫完之后,它们各自收集到的上下文词几乎是同一批。

这个互动需要 JavaScript。它的内容是:窗口扫完这段语料后,「苹果」收集到的上下文是 吃、了、这个、真、甜,「香蕉」「西瓜」收集到的几乎一模一样;而「北京」收集到的是 要、去、的、房价。同类词共享的上下文词有五个左右,不同类之间接近于零。 这个差别完全来自语料本身,不需要任何人去标注「这是水果、那是城市」。

这是一段为了看清机制而写的玩具语料,中文已经预先分好词。真实语料要脏得多, 但窗口的规则就是这么简单——word2vec 取训练样本用的正是这一条。 窗口半径可以调:调小则上下文更看重紧邻的搭配,调大则更偏向主题相关。

第二,和算法有关,否则为什么语言模型或者其他的词向量模型,不强调这个性质呢?skip-gram/cbow 的方法和上面语言学规律的表述是一致的,所以二者结合才有这样的性质。

说是「和算法有关」,其实 skip-gram 做的事情就是把上面那个窗口接到梯度下降上:窗口每停一次吐出来的几对词,就是它的训练样本。而「缺一不可」可以直接做个对照实验——算法完全不动,只把语料的词序打乱,看看还剩下什么。

互动 — 算法那一半

同一个窗口,接上 skip-gram

还是刚才那个窗口,只是这次它每停一下,就把「中心词 → 每个上下文词」交给梯度下降: 让中心词的向量更容易预测出这几个词。这就是 skip-gram 的全部输入,也是它做的全部事情。 下面六张卡把每个词收到的题目一直留在页面上,卡底那条是它和「苹果」的余弦—— 左右对着看,语料给的题目和训练给的几何是怎么对上的。看完再切到对照组:算法一个字不改,只把语料的词序打乱。

这个互动需要 JavaScript。它在页面里真的训练一个 skip-gram:窗口每停一次就产出几条 「中心词 → 上下文词」样本,用负采样和随机梯度下降把模型对这几对的把握往 1 推。 因为「苹果」和「香蕉」一次次被要求预测同一批词(吃、了、这个、真、甜), 它们的向量只能越长越像:语料过八十轮之后 cos(苹果, 香蕉) 是 1.00,cos(苹果, 北京) 是 0.12。 对照组把语料词序打乱后重训,同样的算法、同样的轮数,两个数字分别是 0.42 和 0.28——分不出高低。 语言学规律决定了哪些词要做同一道题,训练目标把「同一道题」变成「同一个方向」,两者缺一不可。

向量是四维的:真实模型有几百维,这里取到够用又能算得飞快。 上下文词下方的数字是模型此刻认为中心词会带出这个词的把握;因为用了负采样,它收敛到的不是 1, 看趋势就好。想快点得到结果就按「直接训完」,想看清样本怎么产生就用「走一停」和「跳到下一个目标词」。 随机数种子固定,每次刷新过程都一样。值得留意的是,对照组一样收敛得动——拟合得动,不等于学到的表示有语义。