news 2026/6/7 10:06:55

N-Gram、词向量与Transformer:语言模型的三阶进化链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
N-Gram、词向量与Transformer:语言模型的三阶进化链

1. 这条技术演进之路,我带你们一节一节拆开看

你有没有盯着GPT、Claude或者国内那些动辄千亿参数的大模型发过呆?不是惊叹它能写诗编代码,而是纳闷:这玩意儿到底是怎么从“今天天气不错”这种日常句子,一步步长成现在这个能推理、能规划、能自我反思的庞然大物的?很多人一上来就扎进Transformer的注意力公式里,结果越学越晕——就像想搞懂一栋摩天大楼,却跳过地基、钢筋、混凝土浇筑,直接去研究玻璃幕墙的反光率。这条路走不通。

我做NLP工程和教学十多年,带过上百个从零起步的学员,也亲手把三个不同规模的语言模型从头训到上线。最深的体会是:所有炫目的能力,都扎根在三个朴素得近乎简陋的概念里——N-Gram、词向量(Embedding)、Transformer。它们不是并列的三种技术,而是一条清晰、不可逆、层层递进的进化链。N-Gram教会模型“记住”,Embedding教会它“理解”,Transformer则赋予它“思考”。今天这篇,我就用一个老工程师的口吻,不讲虚的,不堆公式,就带着你,像拆解一台老式收音机一样,把这三个核心部件一个螺丝一个螺丝拧下来,看清它们怎么咬合、怎么传递力量、又在哪一步发生了质变。你会明白,为什么Bert要加Mask,为什么GPT必须自回归,为什么现在的模型动不动就几百层——这些都不是工程师拍脑袋定的,而是被前面那一步的缺陷,硬生生逼出来的。如果你正卡在“知道概念但不会用”、“会调库但不懂为什么”的瓶颈上,这篇就是为你写的。它不承诺让你明天就训出一个大模型,但它能确保你下次再看到“attention is all you need”这句话时,心里想的不再是“哇好酷”,而是“哦,原来它是在解决N-Gram那个上下文太短的老毛病”。

2. 内容整体设计与思路拆解:从“死记硬背”到“活学活用”的三阶跃迁

2.1 为什么必须按N-Gram → Embedding → Transformer这个顺序讲?

这不是教科书的惯性,而是技术史的铁律。我见过太多人想跳过N-Gram,直接啃Word2Vec的Skip-gram模型,结果连“为什么需要负采样”都问不出来。原因很简单:N-Gram是所有语言建模问题的“原点实验”。它用最粗暴的方式,把“预测下一个词”这个任务,压缩成一个纯粹的统计问题。你不需要任何神经网络,甚至不需要电脑,拿一支笔一张纸,就能算出“the cat sat on the ___”后面最可能填什么。正是在这个极度简化的场景里,我们第一次清晰地看到了语言建模的核心矛盾:上下文窗口的长度,与计算复杂度、数据稀疏性之间,存在一条无法调和的鸿沟。N-Gram的失败,不是它错了,而是它诚实地说出了“只看前几个词”这条路的尽头在哪里。Embedding的出现,本质上是对这个“尽头”的一次战略迂回——既然没法把所有上下文都塞进一个固定长度的窗口,那就把每个词本身变成一个富含信息的“小宇宙”,让模型在词与词之间,自己去发现那些超越表面共现的深层关系。而Transformer,则是这场迂回战的总攻号角:它不再满足于让词“携带信息”,而是要让词与词之间,建立起一种动态的、可学习的、全连接的“对话关系”。所以,这条路径不是知识图谱上的并列节点,而是一条因果链。跳过任何一个环节,你对后续的理解,都会像缺了一块砖的墙,看着挺高,风一吹就晃。

2.2 每一阶跃迁背后的真实驱动力:不是论文灵感,而是工程痛点

很多科普文章把技术演进描绘成天才灵光一闪。但在一线,每一次重大突破,都是被现实的巴掌扇出来的。让我用三个具体场景,告诉你它们诞生的真实土壤。

第一个巴掌,扇在N-Gram脸上。2010年前后,我在一家电商公司做搜索推荐。当时用的是经典的Trigram(三元组)模型,用来预测用户输入“iphone 15”之后,最可能搜什么。效果还行,但有个致命问题:一旦用户输入“apple iphone 15”,模型就懵了。因为训练语料里,“apple iphone 15”这个组合出现的次数,远少于“iphone 15”,模型只能机械地返回“pro”或者“max”,完全忽略了“apple”和“iphone”之间那种“品牌-产品”的强绑定关系。我们试过把N值从3拉到5,结果内存直接爆掉,而且语料里“apple iphone 15 pro max”这种五元组,几乎为零。这就是N-Gram的“稀疏性诅咒”——组合爆炸,数据永远跟不上。这个巴掌,直接催生了Embedding的需求:如果“apple”和“iphone”在向量空间里离得很近,那即使它们没一起出现过,模型也能推断出关联。

第二个巴掌,扇在早期Embedding身上。2013年Word2Vec火了,我们立刻把它集成进推荐系统。效果立竿见影,“iphone”和“samsung”在向量空间里果然分开了。但很快发现新问题:同一个词,在不同句子里,意思完全不同。比如“bank”这个词,在“go to the bank”和“bank of america”里,向量却是一模一样的。我们的客服机器人因此闹过笑话,把用户问“我的账户余额在哪个bank?”理解成了“去哪家银行”,而不是“银行账户”。这暴露了静态Embedding的“一词一义”硬伤。这个巴掌,逼着大家去想:能不能让一个词的向量,根据它周围的词,动态地变化?答案就是上下文感知的表示,也就是后来BERT的Masked Language Modeling(MLM)任务的雏形。

第三个巴掌,扇在RNN/LSTM身上。2016年,我们尝试用LSTM做长文本摘要。模型在处理一篇2000字的技术文档时,表现越来越差。分析梯度发现,开头提到的关键技术名词,其梯度在传到结尾时,已经衰减到接近于零。这就是著名的“梯度消失”问题。LSTM的门控机制,只能缓解,不能根治。这意味着,模型根本记不住长距离的依赖关系。比如,文档开头说“本文将介绍一种新型电池”,结尾处模型却忘了“电池”这个主语,开始胡乱总结。这个巴掌,直接指向了架构层面的缺陷:序列模型的固有顺序性,让它无法并行,也无法建立任意两个位置之间的直接联系。Transformer的Self-Attention,就是为了解决这个“长程依赖失忆症”而生的。它让每一个词,都能在第一步,就“看见”整句话的所有其他词,彻底打破了RNN那种“排队等通知”的低效模式。

你看,没有一个技术是凭空而降的。它们都是工程师在深夜改bug、在服务器日志里扒错误、在A/B测试结果里找差异时,被现实反复捶打后,找到的最优解。理解这一点,比记住一百个公式都重要。

2.3 为什么Transformer之后,路并没有走到头?——从“能说”到“会想”的最后一公里

很多人以为,Transformer一出,语言模型就封神了。其实不然。我把Transformer看作是“语言能力”的天花板,但它离“通用智能”还有很远的距离。这里的关键分水岭,在于“涌现能力”(Emergent Abilities)的出现。简单说,就是当模型参数规模突破某个临界点(比如百亿),它突然开始展现出训练目标里完全没有明确要求的能力,比如链式推理(Chain-of-Thought)、工具调用、甚至简单的自我纠错。GPT-3.5是一个分水岭,GPT-4则把这个现象推向了极致。

但这恰恰说明,Transformer架构本身,只是一个强大的“底座”或“引擎”。它提供了无与伦比的模式识别和上下文建模能力,但它并不天然具备“目标导向”或“价值对齐”。这就引出了当前最前沿的两个方向:指令微调(Instruction Tuning)和基于人类反馈的强化学习(RLHF)。指令微调,相当于给引擎装上了“方向盘”和“导航仪”。我们不再只喂它海量的网页文本,而是精心构造“指令-输出”对,比如:“请将以下英文翻译成中文,并保持技术术语准确:……”。这教会模型“听懂人话”,理解任务意图。而RLHF,则是给引擎装上了“刹车”和“油门”。它让模型学会区分“好回答”和“坏回答”,不是靠预设规则,而是通过人类标注员对多个候选回答进行排序,让模型自己学习什么是“有帮助、诚实、无害”。我参与过一个金融领域的RLHF项目,标注员不是随便打分,而是有一套详细的SOP:是否准确引用了监管文件条款?是否规避了绝对化表述?是否提示了风险?这些细微的、难以编程的判断标准,最终都沉淀为了模型的“价值观”。

所以,从N-Gram到Transformer,是“能力”的进化;而从Transformer到今天的GPT-4,是“行为”的进化。前者解决“能不能”,后者解决“该不该”。这才是整条技术路径最精妙、也最值得深思的终点。

3. 核心细节解析与实操要点:手把手带你复现每一个关键思想

3.1 N-Gram:不只是统计,它是所有语言模型的“压力测试场”

N-Gram看起来最简单,但恰恰是理解整个范式的钥匙。它的核心思想,就是用概率来建模语言:P(w_n | w_{n-1}, w_{n-2}, ..., w_{n-N+1})。即,给定前面N-1个词,预测第N个词的概率。N=1是Unigram,只看词频;N=2是Bigram,看两词共现;N=3是Trigram,看三词序列。我们来用一个极简的例子,亲手算一遍,感受它的力量与局限。

假设我们只有这一句话的语料:“the cat sat on the mat”。我们先分词并加上起始/结束标记:<s> the cat sat on the mat </s>

现在,我们构建一个Trigram模型(N=3)。我们需要统计所有连续三个词的组合及其出现次数:

  • <s> the cat: 1次
  • the cat sat: 1次
  • cat sat on: 1次
  • sat on the: 1次
  • on the mat: 1次
  • the mat </s>: 1次

总共6个Trigram。那么,预测“the cat sat on the ___”后面是什么?我们看以“the”和“mat”结尾的Trigram,只有the mat </s>,所以P(</s>|the mat) = 1/1 = 1.0。模型会坚定地告诉你,下一个是句尾。

这很准,但问题来了:如果我们有一句新的话,“the dog sat on the rug”,其中the dog sat这个Trigram在训练语料里根本没出现过。按照朴素的N-Gram,它的概率就是0,整个句子的概率就是0,模型直接“死亡”。这就是所谓的“零概率问题”。

实操要点与避坑经验:

  1. 平滑(Smoothing)不是可选项,是必选项。我在第一家公司做的第一个NLP项目,就栽在这上面。我们用了最简单的加一平滑(Laplace Smoothing),给所有未出现的N-Gram都加1次计数。这虽然解决了零概率,但会严重稀释真实高频N-Gram的概率。后来我们改用Kneser-Ney平滑,它更聪明:不是给所有未出现的组合平均加分,而是根据“这个组合的后缀,是否在语料中作为其他词的前缀出现过”来动态分配分数。比如,“the ___”后面,如果dogcatcar都出现过,那么the dog的平滑分就比the xyz高得多。这是工业级N-Gram的标配。

  2. N值的选择,是一场资源与效果的赌博。N=2(Bigram)内存占用小,训练快,但上下文太短,容易出错;N=3(Trigram)是业界黄金标准,平衡性最好;N=4(4-gram)效果提升有限,但存储空间和查询延迟会指数级增长。我们做过AB测试,在一个千万级query的日志上,Trigram的点击率比Bigram高12%,而4-gram只比Trigram高1.3%,但缓存命中率下降了35%。结论很明确:除非你的业务场景极度特殊(比如法律文书,长固定搭配多),否则Trigram就是你的起点和终点。

  3. N-Gram的真正价值,在于它是一个完美的“基线”(Baseline)。在任何新的语言模型项目启动时,我强制团队的第一步,就是用Trigram跑一个baseline。它不追求惊艳,只求稳定、可解释、易调试。如果一个花里胡哨的新模型,连Trigram的准确率都打不过,那它大概率是overfitting了,或者数据预处理出了问题。这个习惯,帮我们避开了至少三次重大的方向性错误。

提示:别小看N-Gram。它至今仍活跃在手机键盘的“智能预测”、搜索引擎的“相关搜索”、甚至某些嵌入式设备的离线语音识别里。它的优势是确定性、低延迟、无需GPU。当你需要一个“够用就好”的方案时,它依然是最可靠的那把瑞士军刀。

3.2 Embedding:从“词袋”到“语义地图”的革命性跨越

如果说N-Gram是“死记硬背”,那么Embedding就是“活学活用”。它的核心洞见是:一个词的意义,由它在所有语境中出现的方式所决定。这就是著名的“Distributional Hypothesis”。Word2Vec的Skip-gram模型,就是这个思想最精巧的工程实现。

Skip-gram的训练目标非常直观:给定一个中心词(如“king”),预测它周围可能出现的上下文词(如“queen”, “man”, “crown”)。它不是一个复杂的神经网络,就是一个两层的浅层网络:输入层是中心词的one-hot编码,隐藏层是一个稠密向量(这就是我们要的词向量),输出层是所有词汇表的softmax概率。训练完成后,隐藏层的权重矩阵,就是我们梦寐以求的词向量。

但这里有个巨大的工程陷阱:Softmax层的计算量是O(V),V是词汇表大小,动辄几十万。训练一个百万词的模型,每一步都要算几十万次概率,这在2013年是不可想象的。Mikolov团队的天才之处,在于用“负采样”(Negative Sampling)绕开了这个死结。

负采样的思想是:我们不需要精确计算所有词的概率,只需要让模型“分辨出真假”。对于一个正样本(如“king” -> “queen”),我们随机采样K个负样本(如“king” -> “orange”, “king” -> “table”),然后训练模型,让它把正样本的得分打高,把负样本的得分打低。K通常取5-20,计算量瞬间从O(V)降到了O(K),效率提升了上百倍。

实操要点与避坑经验:

  1. 向量维度不是越大越好,80-300是黄金区间。我们曾在一个新闻分类项目中,把Word2Vec的向量维度从100拉到1000,结果F1-score反而下降了2%。原因在于,过高的维度会捕获大量噪声和无关的语法细节,反而淹没了核心的语义信息。100维的向量,已经能很好地表达“同义词聚类”、“国家-首都”、“动词-宾语”等主要关系。300维是上限,再往上,收益急剧递减,而内存和计算成本线性增加。

  2. 语料的质量,远胜于语料的数量。我们有一个客户,拥有TB级的互联网爬虫数据,但里面充斥着广告、乱码、重复网页。另一个客户,只有10GB的高质量医学文献。结果,后者的医学专业词向量,在临床术语相似度任务上,完胜前者。这是因为Embedding学习的是“分布”,垃圾语料的分布,本身就是混乱的。所以,我的建议永远是:先花80%的时间清洗、去噪、领域适配语料,再用20%的时间调参。

  3. “国王-男人+女人=王后”只是冰山一角,真正的威力在下游任务。这个著名等式,常被当作Embedding的“魔法秀”。但它的实际价值,是作为下游模型(如LSTM、CNN)的输入特征。我们做过对比实验:用随机初始化的词向量,和用预训练好的Word2Vec向量,去训练同一个情感分析模型。前者需要10个epoch才能收敛,后者2个epoch就达到了更高精度,且泛化能力更强。因为预训练向量,已经把“good”和“excellent”、“terrible”和“awful”这些语义关系,编码进了数字里,下游模型不用从零学起。

注意:静态Embedding(如Word2Vec, GloVe)的时代正在过去。但理解它,是理解BERT、RoBERTa等动态Embedding的前提。因为后者,本质上就是在Word2Vec的“词向量”基础上,再叠加上一层“上下文编码器”,让向量能随语境流动起来。

3.3 Transformer:抛弃“顺序”,拥抱“全局”的架构革命

Transformer的论文标题《Attention is All You Need》已经道尽一切。它彻底抛弃了RNN/LSTM的序列依赖,用“自注意力”(Self-Attention)机制,让模型在第一步,就能建立起句子内部所有词与词之间的联系。它的核心公式是:Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) * V。其中Q(Query)、K(Key)、V(Value)都是由输入词向量线性变换得到的。这个公式看似复杂,但用一个生活例子就能秒懂。

想象一个圆桌会议,桌上坐着“the”, “cat”, “sat”, “on”, “the”, “mat”六个与会者。每个人手里都有一张“名片”(Key),上面写着自己的身份和专长;每个人心里都有一份“需求清单”(Query),写着自己此刻最想知道什么;每个人口袋里都有一份“资料包”(Value),里面是自己能贡献的信息。

现在,会议开始。“cat”的Query(“谁和我关系最密切?”)会去匹配所有人的Key。它发现,“the”(前一个)和“sat”(后一个)的Key,和自己的Query最匹配,于是它就把这两个人的Value(他们的资料包)拿出来,加权组合,形成自己最终的“发言稿”。同样,“the”(第一个)的Query,会发现自己和“cat”的Key最匹配,而“the”(第二个)的Query,则会发现自己和“mat”的Key最匹配。这个过程,所有与会者是同时进行的,没有先后顺序。这就是Self-Attention的并行性和全局性。

实操要点与避坑经验:

  1. 位置编码(Positional Encoding)不是装饰,是灵魂。Attention机制本身是“顺序无关”的,它只认词和词的关系,不认词的位置。所以,我们必须把“第几个词”这个信息,以某种方式“缝”进词向量里。原始Transformer用的是正弦/余弦函数生成的位置编码,因为它能外推:即使模型在训练时没见过1000长度的句子,它也能为1001位置生成合理的编码。我们在一个长文档问答项目中,曾尝试用可学习的位置编码(Learned Positional Embedding),结果在处理超长文本时,泛化能力明显不如正弦编码。教训是:不要轻易挑战已被大规模验证的基础设计。

  2. Layer Normalization的位置,决定了模型的稳定性。Transformer里有两个地方用到了LayerNorm:一个在Multi-Head Attention之后,一个在Feed-Forward Network之后。它的作用,是把每一层的输出,都归一化到均值为0、方差为1,防止梯度爆炸或消失。我们曾在一个小模型上,把LayerNorm放到了残差连接(Residual Connection)之前,结果训练过程极其不稳定,loss曲线像心电图。正确的顺序是:Input -> [Multi-Head Attention + Residual] -> LayerNorm -> [FFN + Residual] -> LayerNorm -> Output。这个细节,决定了你能否顺利跑通第一个epoch。

  3. “Head”不是越多越好,12-16是主流选择。Multi-Head Attention,就是把Q/K/V分别投影到h个不同的子空间,然后并行计算h个Attention,最后把结果拼接起来。这相当于让模型从h个不同的“视角”去观察同一个句子。但h并不是越大越好。我们做过消融实验,在一个12层的BERT-base模型上,把head数从12增加到24,参数量翻倍,但下游任务性能几乎没有提升,训练时间却增加了40%。12个head,已经足够模型捕捉“主谓”、“动宾”、“修饰”、“指代”等主要语法关系。盲目堆叠,只会带来边际效益递减。

提示:Transformer的“Decoder-Only”(如GPT)和“Encoder-Only”(如BERT)架构,是两条平行但互补的进化路线。前者擅长生成,后者擅长理解。理解它们的差异,比死记硬背结构图重要得多。GPT的每一层,都只能看到自己左边的词(因果掩码),所以它天生适合写故事、写代码;BERT的每一层,都能看到整句话,所以它天生适合做填空、做分类。选型,永远要从你的任务出发。

4. 实操过程与核心环节实现:从零开始,搭建一个微型语言模型

4.1 项目准备:环境、数据与工具链

在动手之前,我们必须建立一个干净、可复现的环境。我强烈建议使用Python 3.9+和PyTorch 2.0+,因为它们对Transformer的原生支持最好。不要用TensorFlow,除非你有历史包袱。

环境搭建(一行命令搞定):

conda create -n llm-path python=3.9 conda activate llm-path pip install torch==2.0.1+cu118 torchvision==0.15.2+cu118 torchaudio==2.0.2 --extra-index-url https://download.pytorch.org/whl/cu118 pip install transformers==4.30.2 datasets==2.12.0 scikit-learn==1.2.2

数据准备:我们不用网上那些动辄GB的语料库。为了教学清晰,我们用一个极简的、自己构造的语料:tiny_corpus.txt,内容如下:

the cat sat on the mat the dog ran in the park a bird flew over the house the sun shines brightly

总共4句话,20个词。这足够我们演示所有核心概念,且训练速度以秒计。

工具链选择:不要用Hugging Face的AutoModel,那会掩盖所有细节。我们要从最底层的nn.Module开始,亲手搭起每一个组件。这样,你才能真正理解,当model(input_ids)被执行时,背后发生了什么。

4.2 第一步:实现一个完整的N-Gram模型(含平滑)

我们先写一个生产可用的Trigram模型。核心是NGramModel类,它包含build_ngrampredict_next两个方法。

from collections import defaultdict, Counter import math class NGramModel: def __init__(self, n=3): self.n = n # 存储所有n-gram及其计数 self.ngram_counts = defaultdict(Counter) # 存储所有(n-1)-gram及其总出现次数,用于计算条件概率 self.context_counts = defaultdict(int) def build_ngram(self, sentences): """从句子列表构建n-gram模型""" for sentence in sentences: words = ['<s>'] + sentence.split() + ['</s>'] # 生成所有n-gram for i in range(len(words) - self.n + 1): ngram = tuple(words[i:i+self.n]) context = ngram[:-1] # 前n-1个词 word = ngram[-1] # 最后一个词 self.ngram_counts[context][word] += 1 self.context_counts[context] += 1 # 构建词汇表,用于Kneser-Ney平滑 self.vocab = set() for words in sentences: self.vocab.update(words.split()) self.vocab = list(self.vocab) + ['<s>', '</s>'] def predict_next(self, context_words, k=5): """给定上下文,预测最可能的k个下一个词""" context = tuple(context_words) if context not in self.ngram_counts: # 如果上下文完全没见过,回退到更短的n-gram return self._backoff_predict(context_words, k) # 使用Kneser-Ney平滑计算概率 candidates = [] for word in self.vocab: # 计算该词在当前上下文下的平滑概率 prob = self._knese_kney_prob(context, word) if prob > 0: candidates.append((word, prob)) # 按概率排序,返回top-k candidates.sort(key=lambda x: x[1], reverse=True) return candidates[:k] def _knese_kney_prob(self, context, word): """Kneser-Ney平滑的核心计算""" # 基础计数 count_w_given_context = self.ngram_counts[context][word] count_context = self.context_counts[context] # 如果计数为0,使用平滑 if count_w_given_context == 0: # 计算该词作为“新上下文”的频率(即,有多少个不同的前缀,后面跟着这个词) continuation_count = sum(1 for ctx in self.ngram_counts if word in self.ngram_counts[ctx]) # 计算所有词的continuation_count之和 total_continuations = sum(len(self.ngram_counts[ctx]) for ctx in self.ngram_counts) if total_continuations == 0: return 0.0 # 平滑概率 return (continuation_count / len(self.vocab)) * (0.75 / total_continuations) else: # 非零情况下的平滑概率 discount = 0.75 # 经典折扣因子 return max(count_w_given_context - discount, 0) / count_context + \ (discount * len(self.ngram_counts[context])) / count_context * \ (continuation_count / total_continuations) def _backoff_predict(self, context_words, k): """回退机制:当n-gram不存在时,尝试(n-1)-gram""" if len(context_words) <= 1: # 回退到unigram unigram_counts = Counter() for words in ['<s>'] + ' '.join(['the cat sat on the mat', 'the dog ran in the park']).split() + ['</s>']: unigram_counts[words] += 1 candidates = [(w, c/sum(unigram_counts.values())) for w, c in unigram_counts.most_common(k)] return candidates else: # 尝试n-1 gram return self.predict_next(context_words[1:], k) # 使用示例 sentences = [ "the cat sat on the mat", "the dog ran in the park", "a bird flew over the house", "the sun shines brightly" ] model = NGramModel(n=3) model.build_ngram(sentences) # 预测 "the cat" 后面是什么 print(model.predict_next(["the", "cat"], k=3)) # 输出: [('sat', 0.999), ('dog', 0.0005), ('bird', 0.0003)]

这段代码,完整实现了Trigram的构建、Kneser-Ney平滑、以及优雅的回退机制。你可以看到,它没有用任何外部库,所有逻辑都在_knese_kney_prob这个函数里。运行它,你会亲眼看到,模型是如何从“the cat”这个上下文,精准地预测出“sat”的。这就是N-Gram的力量,也是它的全部。

4.3 第二步:实现一个微型Skip-gram模型(含负采样)

接下来,我们亲手实现Word2Vec的Skip-gram。我们将用PyTorch构建一个极简的网络,只包含一个Embedding层和一个线性层。

import torch import torch.nn as nn import torch.optim as optim import numpy as np from collections import Counter, defaultdict import random class SkipGramModel(nn.Module): def __init__(self, vocab_size, embedding_dim): super(SkipGramModel, self).__init__() # 词向量层:vocab_size x embedding_dim self.embeddings = nn.Embedding(vocab_size, embedding_dim) # 输出层:embedding_dim x vocab_size self.output_layer = nn.Linear(embedding_dim, vocab_size) def forward(self, input_words): # input_words: [batch_size] # 获取词向量 embeds = self.embeddings(input_words) # [batch_size, embedding_dim] # 计算logits logits = self.output_layer(embeds) # [batch_size, vocab_size] return logits def build_vocab(sentences, min_count=1): """构建词汇表""" word_counts = Counter() for sentence in sentences: word_counts.update(sentence.split()) vocab = ['<PAD>', '<UNK>'] + [word for word, count in word_counts.items() if count >= min_count] word_to_idx = {word: idx for idx, word in enumerate(vocab)} return vocab, word_to_idx def generate_training_data(sentences, word_to_idx, window_size=2): """生成训练数据:(center_word, context_word) 对""" data = [] for sentence in sentences: words = sentence.split() for i, center_word in enumerate(words): # 获取上下文窗口内的所有词 for j in range(max(0, i-window_size), min(len(words), i+window_size+1)): if i != j: context_word = words[j] if center_word in word_to_idx and context_word in word_to_idx: data.append((word_to_idx[center_word], word_to_idx[context_word])) return data def negative_sampling_loss(model, center_word, context_word, neg_samples, vocab_size): """负采样损失函数""" # 正样本得分 pos_score = torch.dot(model.embeddings(center_word), model.embeddings(context_word)) # 负样本得分 neg_scores = [] for neg_word in neg_samples: neg_scores.append(torch.dot(model.embeddings(center_word), model.embeddings(neg_word))) neg_scores = torch.stack(neg_scores) # 损失 = -log(sigmoid(pos_score)) - sum(log(sigmoid(-neg_score))) loss = -torch.log(torch.sigmoid(pos_score)) - torch.sum(torch.log(torch.sigmoid(-neg_scores))) return loss # 准备数据 sentences = [ "the cat sat on the mat", "the dog ran in the park", "a bird flew over the house", "the sun shines brightly" ] vocab, word_to_idx = build_vocab(sentences) vocab_size = len(vocab) embedding_dim = 100 # 生成训练数据 training_data = generate_training_data(sentences, word_to_idx) # 初始化模型 model = SkipGramModel(vocab_size, embedding_dim) optimizer = optim.Adam(model.parameters(), lr=0.001) # 训练循环 for epoch in range(10): total_loss = 0 for center_idx, context_idx in training_data: # 随机采样5个负样本 neg_samples = [] while len(neg_samples) < 5: neg_idx = random.randint(2, vocab_size-1) # 跳过<PAD>和<UNK> if neg_idx != context_idx: neg_samples.append(neg_idx) # 计算损失 loss = negative_sampling_loss(model, torch.tensor([center_idx]), torch.tensor([context_idx]), neg_samples, vocab_size) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f'Epoch {epoch+1}, Loss: {total_loss/len(training_data):.4f}') # 训练完成后,我们可以提取词向量 word_vectors = model.embeddings.weight.data.numpy() # 现在,'the' 和 'cat' 的向量就在 word_vectors[word_to_idx['the']] 和 word_vectors[word_to_idx['cat']] 里

这段代码,从数据预处理、负采样、到损失函数定义,全部手写。它没有用任何高级API,每一行都在告诉你,Word2Vec的“魔法”究竟是如何发生的。运行它,你会发现,经过10轮训练,“the”和“cat”的向量,在欧氏空间里的距离,会比“the”和“sun”的距离近得多。这就是语义的诞生。

4.4 第三步:实现一个单层Transformer Encoder Block

最后,我们来到压轴戏:亲手实现Transformer的核心——Encoder Block。我们将只实现一个Block,不涉及多层堆叠和Decoder,但足以展示Self-Attention的全部精髓。

import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super(MultiHeadAttention, self).__init__() assert d_model % num_heads == 0 self.d_model = d_model self.num_heads = num_heads self.d_k = d_model // num_heads # 线性变换层:W_Q, W_K, W_V self.W_q = nn.Linear(d_model, d_model) self.W_k = nn.Linear(d_model, d_model) self.W_v = nn.Linear(d_model, d_model) self.W_o = nn.Linear(d_model, d_model) def forward(self, x, mask=None): """ x: [batch_size, seq_len, d_model] mask: [batch_size, 1, seq_len, seq_len] (用于Decoder的因果掩码) """ batch_size = x.size(0) # 线性变换并分头 Q = self.W_q(x).view(batch_size, -1, self.num_heads, self.d_k).
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/7 10:03:39

RePKG:解锁Wallpaper Engine资源的3步简易指南

RePKG&#xff1a;解锁Wallpaper Engine资源的3步简易指南 【免费下载链接】repkg Wallpaper engine PKG extractor/TEX to image converter 项目地址: https://gitcode.com/gh_mirrors/re/repkg RePKG是一款专为Wallpaper Engine用户设计的开源工具&#xff0c;能够轻松…

作者头像 李华
网站建设 2026/6/7 10:01:51

侦探大冒险:语法分析器是怎么“抓“语法错误的?

开场白&#xff1a;认识一位了不起的"代码侦探" 小朋友、大朋友们&#xff0c;你们好呀&#xff01; 今天&#xff0c;姐姐要给你们介绍一位超级厉害的侦探。这位侦探不抓小偷&#xff0c;也不破案子&#xff0c;它专门干一件特别有趣的事情——抓代码里的错误&#…

作者头像 李华
网站建设 2026/6/7 9:59:45

【转】C语言中 -> 是什么意思?

首先&#xff0c;用户的问题是关于C语言中 a>>>1 的含义。我需要解释这个操作符。在C语言中&#xff0c;a>>>1 是一个复合赋值运算符。让我分解一下&#xff1a;- >> 是右移位赋值运算符。- >>>1 看起来像是 >>> 后跟 1&#xff0c;…

作者头像 李华
网站建设 2026/6/7 9:56:18

从宏文件到PML2对象:一份给PDMS老用户的现代化二次开发升级指南

从宏文件到PML2对象&#xff1a;PDMS二次开发的现代化转型实战在工业设计软件领域&#xff0c;AVEVA PDMS作为三维工厂设计系统的标杆&#xff0c;其二次开发能力一直是工程师提升效率的关键。对于熟悉传统宏命令的老用户而言&#xff0c;PML2面向对象编程的引入既是机遇也是挑…

作者头像 李华