SONUB
ZH English 한국어 日本語 中文 हिन्दी বাংলা اردو Español Português Français Deutsch Русский العربية Bahasa Indonesia Tiếng Việt ไทย Türkçe

如何开启通知

此浏览器或网址无法使用通知。请使用支持通知的浏览器,通过安全连接访问SONUB。

如果通知被屏蔽,请在浏览器设置中允许通知。

  1. 打开地址栏旁的站点设置或浏览器的站点设置,将此网站的通知设为允许。
  2. 在设备设置中允许浏览器发送通知,并检查勿扰模式或专注模式。
  3. 返回SONUB并刷新页面。通知开启后,铃铛将不再显示。

在iPhone或iPad上,将SONUB添加到主屏幕,从主屏幕打开后允许通知。

登录 退出登录 显示主题 默认主题 深色主题 粉色主题 聊天 1:1 聊天 群聊 论坛 公告 新闻区 建议区 自我介绍 主菜单

现在,让我们打造属于你自己的人工智能吧。

把下一个的分数变成实际生成的文字。写好 generate.py,逐一改变 、 和 seed,比较结果。

一排词语积木的最后是一个空格。空格上方有三块候选积木,各带一根高低不同的柱子;其中一块被选中,接到这一排的末尾,一个箭头又绕回去,再选下一个。
7

文本生成

每次输入最近并添加一个。为0选最高分,正数调整随机;为0保留所有候选。

seed帮助实验。遇或长度上限停止,只返回新生成文字。保持时Q:/A:格式。

生成循环——逐个接上词元(自回归)

generate 按设定的词元数重复这个顺序。

  • 从目前为止的词元中只取最后 context(12)个,放入 forward。
  • 返回的分数中只用最后一个位置的分数 [-1]。那就是下一个词元的分数。
  • 用设定的方法(、温度、top_k)选出一个词元。
  • 把选出的词元接到 ids 后面。下一圈会看到包含这个词元的文字。
  • 选出的词元是换行就停止。

把自己的输出再作为输入,所以叫(autoregressive)生成。选过的词元不会撤回,前面出错,后面就会跟着这个错误走。

生成只读取。但 的 forward 和训练时做同样的 Value 计算,连用不上的也会生成。 在 torch.no_grad() 中生成,不建图,并用 model.eval() 关闭 。

贪心选择——temperature 0

temperature 设为 0,就总是选分数最高的词元(greedy, )。相同输入总是得到相同回答。

优点是可预测。它如实展示认为什么最合理,适合用来确认训练是否有效。缺点是容易重复同一个短语。像 "is warn is warn is warn" 那样一旦进入循环,每次都是同一个词元最高,出不来。所以对话中会混入一点随机性。

温度——让概率分布变尖或变平

temperature(T)在 之前把分数除以 T。概率与 exp(分数 ÷ T) 成正比。

  • T < 1:分数差距变大,分布变尖。高分词元被抽中得更频繁。
  • T = 1:就是模型给出的概率。
  • T > 1:分数差距变小,分布变平。低分词元也常被抽中,结果变得离谱。

下面的实验对分数 warm 2.0、hot 1.0、blue 0.2、run −1.0 改变温度。warm 的概率在 T 0.5 时是 0.858,1.0 时是 0.632,2.0 时是 0.447;run 从 0.002 升到 0.1。

generate.py 也做同样的计算。从候选分数中减去最大值(数值稳定),除以 T 后取 exp,把这个值作为权重交给 random.choices。权重总和不必为 1,choices 会按比例抽取。通常多用 0.7~0.9,0.1~0.5 偏保守,1.0 以上容易变得离谱。

动手验证 lab/temperature.py
import math

logits = {'warm': 2.0, 'hot': 1.0, 'blue': 0.2, 'run': -1.0}
for t in [0.5, 1.0, 2.0]:
    weights = {w: math.exp(s / t) for w, s in logits.items()}
    total = sum(weights.values())
    print('T =', t, {w: round(v / total, 3) for w, v in weights.items()})
print('top_k=2 keeps', sorted(logits, key=logits.get, reverse=True)[:2])
cd ~/xslm
mkdir -p lab
micro lab/temperature.py
python3 -m lab.temperature

运行结果

T = 0.5 {'warm': 0.858, 'hot': 0.116, 'blue': 0.023, 'run': 0.002}
T = 1.0 {'warm': 0.632, 'hot': 0.232, 'blue': 0.104, 'run': 0.031}
T = 2.0 {'warm': 0.447, 'hot': 0.271, 'blue': 0.182, 'run': 0.1}
top_k=2 keeps ['warm', 'hot']

top-k 与 top-p——减少候选

即使用了温度,非常低的词元合起来也可能有相当大的概率。有 5 万个时,每个虽小,加起来也不能忽视。所以抽取前先减少候选。

  • top-k:只保留分数前 k 个,其余丢掉。XSLM 的 top_k 8 在词表 40 个中只留 8 个候选。为 0 则不减少。实验中的 top_k=2 只留下 warm、hot。
  • top-p(nucleus):按概率从高到低相加,只保留到总和达到 p(例如 0.9)为止。模型有把握时候选只有几个,犹豫时候选变多,会随情况变化。XSLM 没有,但真实服务中常用。

词表有几万个的模型常用 top_k 40~60。(温度 0.7, top_k 30)求准确,(0.9, 50)求平衡,(1.1, 100)求创意,是常见的组合。在词表 40 个的 XSLM 中,8 左右是相近的比例。

seed——复现随机选择

temperature 大于 0 时,random.choices 随机抽取词元。generate 每次都用 random.Random(seed) 新建生成器,所以检查点、、温度、top_k、seed 都相同,总会得到相同的文字。

下面的实验按权重 5:3:2 抽取 a、b、c 共 12 次。seed 7 两次都得到相同的一行,seed 8 得到不同的一行。权重大的 a 出现最多,但顺序由 seed 决定。

seed 是复现结果的编号,不是保证好回答的编号。比较设置时固定 seed、只改一项;想看结果是不是运气,就换几个 seed 试试。

动手验证 lab/seed.py
import random

for seed in [7, 7, 8]:
    rng = random.Random(seed)
    print(seed, ''.join(rng.choices('abc', weights=[5, 3, 2])[0] for _ in range(12)))
cd ~/xslm
mkdir -p lab
micro lab/seed.py
python3 -m lab.seed

运行结果

7 aababaabaaaa
7 aababaabaaaa
8 acabaacabaaa

停止条件——换行与最大词元数

生成在两种情况下停止。

  • 抽到换行词元时:训练资料中一行答案以换行结束,所以这表示模型判断"答案结束了"。换行不放进结果。
  • 达到请求的词元数(默认 24,最多 64)时:即使模型没能结束,也防止它无休止地写下去。

结果中去掉提示词,只把新生成的词元 后返回。第一个词元就抽到换行,就会得到空回答。

真实模型在 特殊词元或设定的停止标记(例如 [/ANSWER])处停止,并截掉后面的部分。每生成一个词元就立刻发到屏幕的输出也很常见。

提示词格式——与训练的形状一样,连空格都相同

模型只擅长接续训练资料中见过的形状后面的文字。所以生成时放入的文字(提示词)必须和训练格式一模一样。XSLM 的默认提示词是 "Q: hi" 的下一行放 "A: "。

一个空格也很重要。训练资料中的 "A: " 是一个词元(30 号),去掉末尾空格写成 "A:",就变成 "A"(5 号)和 ":"(4 号)两个词元。这两个词元这样出现的情况在训练资料中一次也没有,所以模型在从未见过的情况下开始回答。在桌面上用 0.1 训练 3,000 次的模型询问 sun,以 "A: " 结尾时会给出 "te sun is warn is warn…" 这样和训练句子相似的文字,以 "A:" 结尾时只给出 "ore" 这样的碎片。

格式较长时,连答案的开头也放进去。例如在 [ANSWER] 后面放上 "总结:" 和 "-",模型就会直接接着写总结列表。

回答奇怪时——原因与实验

按常见顺序看小模型回答奇怪的原因。

  • 训练不足:默认 20 次是用来确认的。在 3.7 附近,模型几乎还什么都不懂。
  • 资料不足:12 对只能模仿格式,无法回答没见过的问题。
  • 格式不一致:提示词和训练格式不同,或混入了未知字符(�)。
  • 上下文不足:超过 context 12 个词元的前面部分看不到。
  • 设置:出现重复就把温度、top_k 稍微调高,离谱就调低。

提高随机性解决不了训练不足。值得做的实验是这样:用新检查点把学习率改成 0.1,把 --steps 分成每次几百次来训练,用 temperature 0 问同一个问题,记录回答怎样变化。上面看到的 "te sun is warn…" 是开始模仿训练句子 "the sun is warm."、但还没背下来的阶段。

动手输入并实验

写generate.py,用同一检查点逐个改变温度、top_k和seed。

cd ~/xslm
micro generate.py
python3 generate.py --temperature 0 --top-k 1
python3 generate.py --temperature 0.8 --top-k 8 --seed 7
python3 generate.py --temperature 1.2 --top-k 0 --seed 8
在终端中练习
generate.py需要亲手编写的完整源码

恢复结构和数字,截取上下文并反复采样。

import argparse
import json
import math
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE


def load():
    saved = json.loads(Path('checkpoints/model.json').read_text())
    cfg = saved['settings']
    tokenizer = BPE(**saved['tokenizer'])
    model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
    model.load(saved['weights'])
    return model, tokenizer, cfg


def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
    if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
        raise ValueError('invalid sampling parameters')
    ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
    for _ in range(tokens):
        logits = model.forward(ids[-model.context:])[-1]
        ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
        if temperature == 0:
            token = ranked[0]
        else:
            candidates = ranked[:top_k] if top_k else ranked
            peak = logits[candidates[0]].data
            weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
            token = rng.choices(candidates, weights=weights)[0]
        ids.append(token)
        if tokenizer.vocab[token] == '\n':
            break
        result.append(token)
    return tokenizer.decode(result)


if __name__ == '__main__':
    model, tokenizer, cfg = load()
    parser = argparse.ArgumentParser()
    parser.add_argument('--prompt', default='Q: hi\nA: ')
    parser.add_argument('--tokens', type=int, default=cfg['tokens'])
    parser.add_argument('--temperature', type=float, default=0.8)
    parser.add_argument('--top-k', type=int, default=8)
    parser.add_argument('--seed', type=int, default=cfg['seed'])
    args = parser.parse_args()
    assert 1 <= args.tokens <= cfg['max_tokens']
    print(generate(model, tokenizer, args.prompt, args.tokens,
                   args.temperature, args.top_k, args.seed))

逐行拆解代码

generate.py load——用检查点恢复模型

def load():
    saved = json.loads(Path('checkpoints/model.json').read_text())
    cfg = saved['settings']
    tokenizer = BPE(**saved['tokenizer'])
    model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
    model.load(saved['weights'])
    return model, tokenizer, cfg

从一个 model.json 连设置和都读出来,所以即使 settings.json 或 tokenizer.json 之后改变,也能建出和训练时相同的模型。建立同样大小的 (初始值马上会被覆盖),再用 load 放入训练好的数字。

generate.py 输入检查与准备

def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
    if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
        raise ValueError('invalid sampling parameters')
    ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)

空提示词、小于 1 的词元数、负数/无穷大/ 的温度、负数 top_k,都用 拒绝。math.isfinite 遇到 inf、nan 时为假。把提示词变成编号,再建立收集新词元的 result 和只用于这次生成的随机数生成器 rng。

generate.py 求下一个词元的分数

    for _ in range(tokens):
        logits = model.forward(ids[-model.context:])[-1]
        ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)

用 ids[-model.context:] 只放入最后 12 个词元,只用 forward 结果的最后一行 [-1]。ranked 是把词表编号按分数从高到低排序的列表,ranked[0] 是最高的词元。

generate.py 选出一个词元

        if temperature == 0:
            token = ranked[0]
        else:
            candidates = ranked[:top_k] if top_k else ranked
            peak = logits[candidates[0]].data
            weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
            token = rng.choices(candidates, weights=weights)[0]

temperature 为 0 就选最高的词元。否则保留 top_k 个(为 0 则全部)作为候选,减去最大分数 peak,再除以 temperature 取 exp,以此为权重,用 rng.choices 抽一个。choices 返回列表,所以用 [0] 取出。

generate.py 接上、停止、返回

        ids.append(token)
        if tokenizer.vocab[token] == '\n':
            break
        result.append(token)
    return tokenizer.decode(result)

把选出的词元接到 ids 上,放进下一圈的输入。这个词元是换行就不放进结果并停止,否则收集到 result。结束后只把 result decode 后返回,所以结果中不包含提示词。

generate.py 命令行运行

if __name__ == '__main__':
    model, tokenizer, cfg = load()
    parser = argparse.ArgumentParser()
    parser.add_argument('--prompt', default='Q: hi\nA: ')
    parser.add_argument('--tokens', type=int, default=cfg['tokens'])
    parser.add_argument('--temperature', type=float, default=0.8)
    parser.add_argument('--top-k', type=int, default=8)
    parser.add_argument('--seed', type=int, default=cfg['seed'])
    args = parser.parse_args()
    assert 1 <= args.tokens <= cfg['max_tokens']
    print(generate(model, tokenizer, args.prompt, args.tokens,
                   args.temperature, args.top_k, args.seed))

直接运行时读取检查点,接收 --prompt、--tokens、--temperature、--top-k、--seed 选项。argparse 把 --top-k 变成 args.top_k(- 变成 _)。用 确认词元数在 1~max_tokens(64) 之间后,输出生成的文字。在 中要给出含换行的提示词,就像 --prompt "$( 'Q: cat\nA: ')" 这样用 printf。

检查理解

相同输入、权重和设置应复现。首个词元为换行时可能得到空答案。

遇到问题时

小模型短训后乱码正常。先检查格式、未知字和损失,增加随机性不能替代训练。

常见问题

为什么同一个问题每次回答都一样?

因为 seed 相同。generate 每次请求都用同一个 seed 新建生成器。想看不同的回答,就改 --seed,或在 请求 中把 "seed" 设成别的整数。temperature 为 0 时,不管 seed 是多少总是相同。

温度调高会变聪明吗?

不会。温度只改变从已算好的概率中选择的方法。模型给正确答案的概率很低,温度怎么调正确答案也不会常出现。聪明程度由训练(资料、规模、次数)决定。

出现了空回答。

这是第一个词元就抽到换行而立刻停止的情况。训练资料中 "A: " 后面从不会直接换行,所以是训练不足或提示词格式不同时才会发生。检查提示词是否以 "A: " 结尾,再多训练,或换 seed、temperature 试试。

学习指南

人工智能

智能体工具

Vibe 编程

AI 应用

AI证书

AI课堂

IT 基础

聊天

论坛

创作

社区

更多

术语词典

选一个你不认识的词吧,可以看到简单的解释和相关术语。

还没有匹配的术语。

选择一个术语即可查看解释。