简介:《大语言模型入门的基础知识》是一份面向零基础学习者、转型开发者与技术分享者的PDF讲义,围绕语言模型的核心概念、发展脉络与评估指标展开,帮助读者建立从统计语言模型到神经语言模型、预训练模型及大语言模型的系统认知。压缩包内仅含1个PDF文件,大小约3.24MB,篇幅集中、目录清晰,便于按章节阅读,适合用作课程预习、团队内部分享提纲或面试前的知识梳理材料。内容从LLM背景切入,依次讲解语言模型定义、句子概率计算、N-gram的马尔科夫假设与数据稀疏问题,并延伸到神经网络语言模型、Transformer与预训练范式,配合bigram计数、下一词预测等例子说明原理与局限。已有129人学习,适合希望用较短时间搭建LLM基础知识框架、补齐关键术语与模型演进脉络的入门读者。
1. 先破除一个误解:大语言模型不是「什么都懂的搜索框」
刚接触大语言模型的人,多半会问同一个问题:它是不是把整个互联网都存进去了,我问什么它就从里面翻出答案?这个理解一旦立住,后面全是坑。它更像一个读了海量文字、专门练过「接下一句」的人,你给它上半句,它按见过的语言习惯往下接。它能做文本分类、问答、翻译、对话这些自然语言任务,靠的是从大量文本里学到的语言规律,而不是背下来的资料库。这篇入门从「语言模型到底在算什么」讲起,一路走到第一个能跑通的小例子,零基础也能跟上。
2. 语言模型到底在算什么:一句话像不像「人话」的概率
2.1 用生活化的说法:给句子打一个「顺不顺口」的分
先记住一个最朴素的解释:语言模型就是用来计算一个句子出现概率的模型,换句话说,它判断一句话「像不像人话」。标准的写法是这样的:给定一个句子序列 S = {W1, W2, W3, …, Wn},语言模型算的是这个序列发生的概率 P(S)。如果给定的词序列符合语用习惯,就给出高概率;不符合,就给低概率。到这里为止,没有任何深度学习的影子,就是一个概率问题。
拿一个只有五个词的词典举例,假设 V = {黑马、程序、员、来、学习},每个词都属于这个词典。那么「黑马程序员来学习」这句话,就是把这五个词按顺序排成一个序列,语言模型要做的就是给这个序列算出一个 P(S),并且这个概率必须大于等于 0。你可能会问,算这个有什么用?用处有两个,而且都很实在:一是判断一句话合不合理,比如拼音输入法给你几个候选,你希望它先给最像人话的那个;二是往下接词,给一段前文,预测下一个词最可能是什么。这两个用途,撑起了后面所有的技术演进。
这里要提前说清一件事,很多人会混淆:语言模型是一个大类,大语言模型只是这个大类里参数特别多的那一批。先有语言模型,才有大语言模型,顺序不能反。入门的时候先把「算概率」这件事吃透,后面看到参数量、预训练、对齐这些词,才不会觉得是天上掉下来的。
2.2 第一次动手:用二十行代码算出「我想去打篮球」的概率
最直觉的做法就是数数。假设手上有一个语料库,里面有 N 个句子,我们统计一下某个句子在语料里出现了多少次,记作 n,那么这个句子的概率就是 n 除以 N。这个方法简单到不像话,但它能跑,而且能让你第一次看到「概率」是怎么从数据里冒出来的。
from collections import Counter # 一个极小的语料库,每个句子已经分好词 corpus = [ ["我", "想", "去", "打", "篮球"], ["我", "想", "去", "学习"], ["我", "想", "去", "打", "晚饭"], ["他", "想", "去", "学习"], ] # 统计每个句子出现的次数 sentence_count = Counter(tuple(s) for s in corpus) N = len(corpus) # 语料库里的句子总数 target = ("我", "想", "去", "打", "篮球") print(sentence_count[target] / N) # 0.25,出现过 1 次 # 换一个没在语料里出现过的句子 other = ("我", "想", "去", "打", "羽毛球") print(sentence_count[other] / N) # 0.0,一次都没出现过这段代码里,Counter负责把每个句子当整体去计数,tuple(s)是把列表转成元组,因为列表不能当字典的键。N是语料里的句子总数,除一下就是频率,用频率近似概率。跑完你会看到两个结果:0.25 和 0.0。
关键就在这个 0.0 上。任何一个没在语料库里原样出现过的句子,概率都是 0。而人类语言是无穷无尽的组合,你几乎不可能把所有合法句子都提前塞进语料库。所以这种「整句计数」的办法,实际预测能力几乎为零。这不是代码写得不好,是思路本身走不通。
2.3 链式法则能救场,但带来两个新麻烦
既然整句统计不行,那就换个思路:把整句的概率拆成一个个词的条件概率连乘。这就是概率论里的链式法则,一句话的概率可以写成 P(W1) 乘以 P(W2|W1),再乘以 P(W3|W1,W2),一路乘到最后一个词。这么拆完,只要我们能算出「已知前面所有词,下一个词是什么」的概率,整句的概率就出来了。
这个拆法也顺带给了语言模型第二个定义,而且这个定义更贴近今天的用法:给定一个短语,语言模型可以生成或者说预测接下来的一个词。你在手机上打字时跳出来的那几个候选词,背后就是这个思路在跑。
但链式法则有两个绕不过去的坎。第一个是参数空间太大:条件概率 P(Wn|W1,W2,…,Wn-1) 的取值可能性太多,前文每多一个词,要估的组合就翻着倍涨,根本估不完,而且很多组合估出来也没什么用。第二个是数据稀疏:大量词对组合在语料库里压根没出现过,用最大似然估计算出来的概率直接是 0,一乘就全归零了。这两个问题,是后面所有技术演进的出发点,记住它们,你看 N-gram、看神经网络模型,就知道每一代在解决哪一个。
提示:初学阶段最容易犯的错是急着背公式。把这段的两句话记住就够了——语言模型算的是句子的概率,算不出来是因为参数太多加数据太少。
3. 四代语言模型怎么走过来的:从查表到神经网络
3.1 N-gram 的核心假设:只看前面一两个词
要解决「条件概率估不完」的问题,最直接的办法是砍掉一部分条件。N-gram 模型引入了一个假设,叫马尔科夫假设:一个词出现的概率,只和它前面有限的几个词有关,再往前的就不管了。按看几个词来分,主要有这么几种:
- unigram,也就是一元模型,假设每个词和其他词独立,完全不看前文;
- bigram,二元模型,当前词只依赖前面一个词;
- trigram,三元模型,当前词只依赖前面两个词。
推广一下,N 元模型就是假设当前词只和它前面的 N-1 个词有关。这里的 N 要你自己选,实践中用得最多的是 bigram 和 trigram,因为 N 再往上,参数量涨得飞快,而效果提升有限。这些概率参数都可以通过大规模语料库统计出来,三元的话就是统计「前两个词加当前词」出现了多少次。
这个假设粗糙吗?粗糙。但它把「估不完」变成了「估得起」,这是从零到一的一步。
3.2 手把手跑一个 bigram:从计数到预测下一个词
理论说完,直接跑。bigram 的参数就是 P(Wi|Wi-1),计算方法很朴素:先数一下词对 (Wi-1, Wi) 在语料里出现了多少次,记作 C(Wi-1, Wi),再数一下 Wi-1 单独出现了多少次,记作 C(Wi-1),两个一除就是概率。
from collections import Counter import math corpus = [ ["我", "想", "去", "打", "篮球"], ["我", "想", "去", "学习"], ["我", "想", "打", "篮球"], ["他", "想", "去", "打", "篮球"], ["我", "不", "想", "去"], ] uni = Counter() # 单个词的计数 C(Wi-1) bi = Counter() # 相邻词对的计数 C(Wi-1, Wi) for s in corpus: for w in s: uni[w] += 1 for a, b in zip(s, s[1:]): bi[(a, b)] += 1 vocab_size = len(uni) # 词表大小,用于平滑 def prob(word, prev, k=0.1): """计算 P(word | prev),k 是平滑系数,防止没见过的组合概率为 0""" return (bi[(prev, word)] + k) / (uni[prev] + k * vocab_size) print(round(prob("想", "我"), 3)) # 看「我」后面接「想」的概率 print(round(prob("去", "想"), 3)) # 看「想」后面接「去」的概率zip(s, s[1:])这一步是拿相邻的两个词组成词对,等价于在句子上一格一格滑动。k是平滑系数,作用是给每个可能组合都垫一个极小的底数,这样即使某个词对在语料里从没出现,概率也不会是 0。没有这个 k,模型只要碰到生词组合就直接崩溃,这是新手最常翻车的地方之一。
有了参数,就能算整句概率了。因为连乘很容易数值下溢变成 0,实践里都是先取对数、求和、再取指数还原:
def sentence_prob(seq, k=0.1): """按 bigram 计算一个词序列的概率,内部用对数相加避免下溢""" logp = 0.0 for prev, w in zip(seq, seq[1:]): logp += math.log(prob(w, prev, k)) return math.exp(logp) print(sentence_prob(["我", "想", "去", "打", "篮球"])) print(sentence_prob(["我", "想", "去", "打", "晚饭"]))两句的概率不会一样,因为「打」后面接「篮球」和接「晚饭」在语料里出现的次数不同。这就是模型在做判断:它认为语料里更常见的搭配,概率更高。
再往前一步,用模型预测下一个词,也就是填空:
def next_words(prev, topn=3): """给定前一个词,返回概率最高的几个候选""" cands = [(prob(w, prev), w) for w in uni] cands.sort(reverse=True) return [(round(p, 3), w) for p, w in cands[:topn]] print(next_words("打")) # 看看「打」后面最可能接什么cands里遍历的是整个词表,所以词表越大这个函数越慢,真实场景里会做各种剪枝。但对入门来说,这个几十行的脚本已经是一个完整的语言模型了:有训练(统计计数)、有参数(条件概率)、有推理(算句子概率、预测下一个词)。
3.3 N-gram 的优点很实在,缺点也很实在
它的优点值得认真说。第一,用极大似然估计,参数容易训练,本质上就是数数,不涉及任何复杂的优化过程;第二,它完整包含了前 N-1 个词的全部信息,在假设范围内没有丢东西;第三,可解释性强,你能明确指着某个词对说「它之所以得分高,是因为在语料里出现了 8 次」。这种透明度在今天反而成了稀缺品。
缺点同样清楚:只能建模到前 N-1 个词,第 N 个词之前的信息全部丢掉;随着 N 增大,参数空间呈指数增长,词表大一点就撑不住;数据稀疏问题无法根治,没见过的组合就是没见过,平滑只是缓解不是治愈;最要命的是泛化能力差——模型不理解「篮球」和「足球」是同类东西,它只认识字面上的搭配。
这几条缺点,正好是下一代模型要解决的问题清单。
4. 从神经网络到预训练:模型开始「理解」而不只是「数数」
4.1 神经网络语言模型:把词变成一串数字
N-gram 最大的问题是把每个词当成孤立的符号。神经网络语言模型换了个做法:给每个词配一个向量,也就是词向量,让意思相近的词在数值上也接近。这样一来,即使「打篮球」没在语料里出现过,「打足球」出现过,模型也能迁移一部分知识。这就是泛化能力的来源。
具体结构不复杂,按层看就清楚了。输入层拿前 n-1 个词,把它们对应的词向量首尾拼接成一个长向量;中间接一个全连接层,过一遍 tanh 激活函数,做非线性变换;输出层有 V 个节点,V 是词表大小,每个节点对应一个词,输出的是未归一化的打分。最后用 softmax 把这些打分变成概率:
import math def softmax(scores): """把一组任意实数打分变成和为 1 的概率""" m = max(scores) # 减去最大值,防止 exp 溢出 exps = [math.exp(s - m) for s in scores] total = sum(exps) return [e / total for e in exps] scores = [2.0, 0.5, -1.0] # 词表里三个候选词的原始打分 print([round(x, 3) for x in softmax(scores)]) # [0.786, 0.175, 0.039]scores是模型最后一层吐出来的三个数,没有任何约束,可以是负数也可以很大。max(scores)那一行是减掉最大值再取指数,纯粹为了数值稳定,不改变最终的比例关系。跑出来的三个概率加起来正好是 1,最大的那个 0.786 就是模型给出的预测结果。
相比 N-gram,它的泛化能力明显更好,数据稀疏的痛点被缓解了不少。但新的问题也来了:对长序列的建模能力依然有限,而且层数一深就容易出现梯度消失,训不动。
4.2 预训练加微调:一次训练,到处复用
Transformer 架构出现之后,玩法彻底变了。这一代模型的核心思路是两段式:先预训练,再微调。
预训练是在大规模通用文本上先把模型训一遍,让它学到通用的语言表示和知识,这一步非常贵,但只需要做一次。微调是拿预训练好的模型,在具体下游任务的数据上再训一小段,把通用能力迁移到你的任务上,这一步很便宜。这个范式的好处是显而易见的:中小团队不用从零开始烧算力,拿一个预训练好的底座改一改就能用,泛化能力、语义表示、抗过拟合都比前几代强得多。
代价也要说清楚:预训练阶段计算资源需求极大,而且模型内部到底学到了什么,可解释性很差,很多时候它给你一个答案,你没法像看 bigram 那样指着计数说「因为这里出现了 8 次」。这个「黑匣子」的特质,是后面所有使用注意事项的根源。
4.3 大语言模型「大」在哪:10B 这条线和它带来的变化
到了这一代,事情的走向有点反直觉:预训练模型的参数规模指数级往上堆的时候,语言模型的性能大致会跟着线性上升。目前业界比较常用的一个界定是,参数量超过 10B(一百亿)的模型才算大语言模型,而头部模型的参数量早就到了千亿甚至更多。参数是在大量文本数据上训出来的,规模越大,模型见过的语言现象就越丰富。
规模上来之后,最明显的变化不是分数涨了多少,而是用法变了。早期模型你要用它,得针对任务改权重、做微调;而参数量足够大的模型,可以在完全不调整权重的情况下,只靠你给的任务示例就把事情做完。这个变化对新手特别友好——你不需要显卡集群,也能先用起来。
它像人一样能对话、能按指令完成任务,甚至能接上外部工具去检索信息,这些都是优点。缺点同样要提前知道:参数量大意味着算力要求高、训练时间长,而且它仍然可能生成有害的、带偏见的内容。这不是模型没训好,是「按概率生成」这件事本身决定的。
4.4 怎么判断它好不好:入门必知的几个评估指标
模型训完或者用起来之后,总得有个东西衡量好坏。入门阶段知道四个词就够了。
困惑度(perplexity)是最核心的一个,它衡量模型对一段文本有多「困惑」,数值越低说明模型越不意外,预测得越准。计算方式是取每个词概率的对数、求平均、再取指数:
import math # 假设模型对一句话里 5 个词分别给出的概率 probs = [0.2, 0.1, 0.05, 0.4, 0.25] # 困惑度 = exp(-平均对数概率) ppl = math.exp(-sum(math.log(p) for p in probs) / len(probs)) print(round(ppl, 2)) # 6.31这段代码里,math.log(p)把概率转成对数,sum(...) / len(probs)求平均,前面加负号,最后math.exp还原。可以粗略理解成:困惑度是 6.31,差不多等于模型在每个位置上要在 6 个多候选词里犹豫。概率给得越高,困惑度越低,模型越好。
另外三个是:准确率、召回率这类分类指标,用在下游任务上;BLEU 这类文本生成指标,用来比对生成文本和参考文本的重合程度;以及人工评估,让真人来打分,因为自动指标经常和人的判断对不上。入门阶段别纠结公式,先记住「困惑度看语言模型本身,BLEU 和准确率看具体任务,人工评估兜底」这个分工就行。
5. 新手入门最容易踩的 5 个坑:现象、原因和补救
5.1 坑一:模型说得很肯定,就以为它真的知道
现象:你问一个事实性问题,模型语气笃定地给了一个答案,一查是错的,而且错得很自然。
原因:它本质上还是在算「下一个词接什么概率最高」,不是在数据库里检索。训练数据里没有、或者有冲突的内容,它照样能顺着语言习惯编出一段通顺的话,这在业内叫幻觉。概率高不等于事实对。
解决:把模型输出当成「草稿」而不是「结论」,凡是涉及具体数字、时间、引用来源的,一律自己核一遍。做工程时在流程里加一道校验,比如让模型只输出结构化字段,再拿字段去查真实数据源。
5.2 坑二:换个说法问,模型就答不上来了
现象:同一件事,你用训练语料里的说法问,它答得挺好;换个同义表达,效果断崖式下跌。
原因:这是数据稀疏的老毛病在新架构上的残留。词表里没出现过的组合,模型没有直接经验,只能靠泛化能力硬猜,猜不准很正常。N-gram 时代这个问题表现为概率直接归零,现在表现为「答得似是而非」。
解决:问法上做归一化,把口语化的表达先转成更常见的说法;或者一次给几个不同角度的问法,取一致的答案。做训练数据时,同义改写是一条性价比很高的扩充手段。
5.3 坑三:以为参数越大效果越好,忽略了数据和任务
现象:换了个参数更大的模型,结果在自己的任务上反而更差,或者提升微乎其微。
原因:参数量只是三个变量里的一个,另外两个是训练数据的质量和数量、以及和人类意图的对齐程度。一个参数中等但数据干净、对齐到位的模型,在具体任务上完全可能赢过参数更大但没调好的模型。
解决:选型时不要只看参数量,先拿自己的真实数据跑一个小规模对比,固定评测集,看实际分数。评测集一定不能拿去调参,否则分数虚高,上线就露馅。
5.4 坑四:一上来就想训一个自己的模型
现象:刚学两天就想着准备数据、买算力、从头训一个模型,折腾两周卡在环境上,热情耗尽。
原因:训练一个大模型是个系统工程,涉及数据清洗、分布式训练、显存优化、评测一整套东西,前置知识比想象的多得多。新手在这个阶段投入产出比极低。
解决:先用现成模型把「用」这一环跑通,理解输入输出、理解提示词怎么写、理解评估怎么做,再去碰训练。我第一次学的时候就是先写了一个几十行的 bigram 脚本跑通,才真正明白「语言模型」四个字在说什么,这个顺序别跳。
5.5 坑五:拿测试集反复调,分数很好看但没用
现象:本地评测分数一路涨到很高,一换真实场景就崩。
原因:你在测试集上反复试错、反复选参数,等于把测试集的信息泄露进了模型,这叫数据泄露。分数涨的是「对这个测试集的拟合度」,不是真实能力。
解决:数据一开始就切成三份,训练、验证、测试。调参只看验证集,测试集只在最后跑一次,跑完就封存。这个习惯早点养成,能省下大量「上线才发现不行」的后悔药。
6. 下一步学什么:按这条顺序走,两周就能有手感
入门阶段最怕的是漫无目的地刷资料。给一条能落地的顺序,按周排:
| 阶段 | 时间 | 具体做什么 | 检验标准 |
|---|---|---|---|
| 第一步 | 第 1~2 天 | 把第 3 章的 bigram 脚本抄一遍跑通,改语料、改 k 值,观察概率怎么变 | 能解释为什么加平滑 |
| 第二步 | 第 3~5 天 | 把统计对象从 bigram 换成 trigram,对比两种模型对同一句话的打分 | 能说出 N 变大后的代价 |
| 第三步 | 第 6~9 天 | 用现成的大模型接口做三个小任务:分类、抽取、改写 | 每个任务能写出稳定的提示词 |
| 第四步 | 第 10~14 天 | 搭一个自己的小评测集,几十条就够,把上面三个任务量化打分 | 能说清每个任务好在哪里差在哪里 |
第一、二步的意义在于建立手感。你在纸上算不明白的东西,写两行代码跑一下立刻就懂了,比如平滑系数从 0.1 改成 1,生僻组合的概率会明显变高,这就是参数在起作用。第三步开始接触真正的大模型,重点不是调得多花哨,而是理解「同一个任务,提示词差一句话,结果差一个档」。第四步是分水岭,有没有自己的评测集,决定了你后面是在盲调还是在做实验。
学的过程里如果只记一件事,就记这个:任何一次改动,都要能在一组固定的题目上量出变化。做技术的血泪经验大多来自「感觉变好了」,而不是「量出来变好了」。
说个我自己的教训。早期我做模型对比,全凭肉眼扫几个例子就下结论,来回折腾了好几轮,最后发现方向从一开始就是错的,因为我看的那几个例子恰好都在模型的舒适区里。从那以后我每次动模型之前,都强制先把评测集固定下来、跑一遍基线、留一份结果,再去改。这个动作花不了十分钟,但省下来的时间是以天计的。
照着上面的顺序走,不用追求一次看懂所有公式,先把第一个例子跑出来。这份入门笔记可以当成一条主线,遇到不懂的概念再回头翻对应的那一节。希望帮到你。
本文还有配套的精品资源,点击获取