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 聊天 群聊 论坛 公告 新闻区 建议区 自我介绍 主菜单

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

把自己的 上传到 ,让任何人都能下载。用 export.py 导出 ,写好,然后 push。

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

发布我的模型

是上传和下载的网站,有点像专门放模型的 。一个模型就是一个 。把文件、设置、、运行和()一起上传,别人就能下载同一个模型,原样运行。

一共三步。export.py 把检查点里的数字导出成 文件,你来写模型卡,再用 git 把所有文件 push 到你的 Hugging Face 仓库。 不用改。下载的人用 export.py --restore 还原检查点,再用同样的 generate.py、runner.py、xchat.py 运行。

为什么要发布——分享模型的地方 Hugging Face

Hugging Face 是上传和下载模型、集和演示的公开。从大公司到学生,谁都在上面上传模型,概述里看到的 SmolLM2-135M 也是从这里下载的。

  • 仓库:一个模型就是一个 git 仓库。地址是 huggingface.co/用户名/模型名。
  • :每次 push 都会留下一个提交,所以可以回到以前的权重。
  • 模型卡:仓库里的 README.md 就是模型的首页。
  • 公开范围:Public 所有人都能看到;Private 只有你和你允许的人能看到。

发布以后,别人可以把你的结果原样重新跑一遍。这叫作。在真实的人工智能开发中,人们每天都在下载公开模型、做,再重新上传。

safetensors——存放权重的安全文件

safetensors 是 Hugging Face 制定的权重文件格式。它只有三个部分。

  • 开头 8 :后面 头的长度(无符号整数,)。
  • JSON 头:每个层名对应它的 (F32)、shape(行, 列)和 data_offsets(在正文里的起始和结束字节)。
  • 正文:所有数字按 4 字节 紧紧相连的字节。

以前常用的 .bin 文件是 pickle,打开文件的一瞬间,里面藏着的代码就可能被执行。safetensors 里只有数字和 JSON,打开也不会执行任何代码。所以下载别人上传的模型时更安全。

在 Python 里的数字是 8 字节(float64),写进文件时缩成 4 字节(float32)。会出现很小的差异,比如 0.1 变成 0.10000000149011612,但用默认数据的模型生成的文字完全一样。1,248 个 × 4 字节 = 4,992 字节,再加上头部,文件大约 5.7 。

动手验证 lab/safetensors.py
import struct
from pathlib import Path
from export import write_safetensors, read_safetensors

path = Path('lab/tiny.safetensors')
write_safetensors(path, {'w': [[0.1, 0.5], [-2.0, 3.0]]})
raw = path.read_bytes()
size = struct.unpack('<Q', raw[:8])[0]
print('file bytes:', len(raw), 'header bytes:', size)
print('header:', raw[8:8 + size].decode().strip())
print('numbers:', raw[8 + size:].hex(' ', 4))
print('read back:', read_safetensors(path)['w'])
cd ~/xslm
mkdir -p lab
micro lab/safetensors.py
python3 -m lab.safetensors

运行结果

file bytes: 128 header bytes: 104
header: {"__metadata__": {"format": "xslm"}, "w": {"dtype": "F32", "shape": [2, 2], "data_offsets": [0, 16]}}
numbers: cdcccc3d 0000003f 000000c0 00004040
read back: [[0.10000000149011612, 0.5], [-2.0, 3.0]]

一起上传的文件——只有权重是跑不起来的

model.safetensors 只是一张数字表。要再次使用这些数字,下载的人还需要三样东西。

  • config.json:模型大小(width、)、大小、生成的默认值和数。
  • tokenizer.json:把文字变成编号的词表和。只要有一个编号不同,模型看到的就是错误的字符。
  • 运行代码:engine.py、model.py、tokenizer.py、generate.py、runner.py、xchat.py 和 export.py。同样的数字,只有用同样的计算来运行,才会得到同样的回答。

export.py 把这些文件全部收集到 hub 文件夹。(data)和 train.py 不上传。使用模型时用不到它们,而且数据的权利需要另外确认。大模型也一样。Hugging Face 上的模型仓库通常同时有 config.json、tokenizer.json 和 *.safetensors。

模型卡——写在 README.md 里的模型说明书

模型卡就是仓库里的 README.md。它分为上下两个部分。

  • 头(两行 --- 之间):(许可证)、language(语言)、tags(用来搜索的词)和 pipeline_tag(任务类型,这里是 text-generation)。Hugging Face 读取它,用于模型的筛选和标签。
  • 正文:给人看的 说明。

正文至少要写五件事:它是什么、有哪些文件、怎么运行、用什么数据训练、做不到什么(局限)。老实写出局限很重要。如果写明它只有 1,248 个,所以回答几乎是随机的,就没有人会用错。

许可证和数据——发布前要检查

Public 仓库全世界任何人都能看到、都能下载。上传前请检查三件事。

  • 许可证:决定别人可以怎样使用你的模型。MIT 是一个很短的许可证,只要保留版权声明,几乎可以自由使用。如果想用别的,就用 Hugging Face 列表里的名字,比如 或 CC-BY-4.0。
  • 训练数据的权利:如果没有经过允许就用别人的文章训练,那么这个模型也不要发布。默认数据是专门为本课程写的例句。
  • 个人信息:如果数据里放了姓名、电话号码或地址,模型可能会一字不差地说出来。先删掉,重新训练,再上传。

可以先以 Private 上传确认,满意后再到仓库的 Settings 里改成 Public。

大文件和 git-lfs——数字文件单独上传

git 是为记录有行的文字(比如源代码)中改动的行而设计的。权重这样的没有行,哪怕只改一点,也会把整个文件再存一遍。所以 Hugging Face 会拒绝把二进制文件直接放进 git 的 push,就算文件很小也一样。

  • :把文件内容单独上传,git 里只放一个指向它的短指针(大小和 )。
  • .gitattributes:写明哪些文件交给 LFS 的列表。Hugging Face 创建仓库时会预先加入 *.safetensors。
  • git-xet:Hugging Face 更新、更快的方式,但只能在 64bit 电脑上用。这个是 ,所以用 git-lfs。用 git-lfs 上传的文件,Hugging Face 也会照样接收并保存。

git lfs install 是把 LFS 接到 git 上的设置,只需要做一次。之后像平常一样 git add、、push 就行。用 git lfs ls-files 可以查看哪些文件交给了 LFS。

访问令牌——代替密码的钥匙

Hugging Face 的 git push 不接受账号密码。要在填密码的地方放入在 Settings → Access Tokens 创建的令牌(以 hf_ 开头的一长串字符)。

  • :Read 令牌只能下载;Write 令牌还能上传。push 需要 Write。
  • 保管:令牌就等于密码。不要把它放进、聊天、代码或仓库地址(https://ID:TOKEN@…)里。
  • credential.helper :只在里记住你输入的令牌 15 分钟,让 git 和 git-lfs 不用问两次。不会保存到文件。
  • 删除:全部上传完后,可以在 Access Tokens 里删除这个令牌。仓库和模型都会原样保留。

如果觉得令牌泄露了,马上删除,重新创建一个。被删除的令牌什么也做不了。

下载后再运行——证明发布成功

发布的模型要能在别人的电脑上运行才有意义。下载的人这样做。

  • 用 git clone https://huggingface.co/YOUR_ID/xslm-tiny 获取仓库。没有 git-lfs 的话,得到的不是 model.safetensors,而只是一段短短的指针文字,这时请在网页的 Files 标签里下载这个文件。
  • 用 export.py --restore . 把 safetensors、config 和 tokenizer 还原成 checkpoints/model.json。
  • 用 python3 generate.py 生成文字,或用 runner.py、xchat.py 对话。不需要安装任何包。

seed 和设置相同,就会得到和这个终端完全一样的回答。这就是复现。大模型也是同样的方式:接收的一方读取 safetensors,在自己的运行环境(PyTorch、、 等)中运行。

动手输入并实验

先在 huggingface.co 注册,然后在 huggingface.co/new 创建一个名为 xslm-tiny 的 Public 模型仓库。再到 Settings → Access Tokens 创建一个有 Write 权限的令牌。接着按顺序输入下面的。把 YOUR_ID 换成你的 Hugging Face 用户名。git push 询问 Username 时输入用户名,询问 Password 时不要输入密码,而是粘贴令牌。令牌不会显示在屏幕上。

cd ~/xslm
micro export.py
sudo apk add git-lfs
git lfs install
git clone https://huggingface.co/YOUR_ID/xslm-tiny hub
python3 export.py
nano hub/README.md
cd hub
git config user.name "YOUR_ID"
git config user.email "you@example.com"
git config credential.helper cache
git add .
git commit -m "My first XSLM"
git push
在终端中练习
export.py需要亲手编写的完整源码

write_safetensors 把数字按 4 字节 float32 一个接一个写入,并在前面加上 JSON 头。read_safetensors 正好相反。export 把权重、设置、分词器和运行代码收集到 hub 文件夹,再读回来显示差异。--restore 把下载的文件还原成 checkpoints/model.json。

import argparse
import json
import shutil
import struct
from pathlib import Path

SOURCES = ['engine.py', 'model.py', 'tokenizer.py', 'generate.py',
           'runner.py', 'xchat.py', 'export.py']


def write_safetensors(path, weights):
    header, body = {'__metadata__': {'format': 'xslm'}}, b''
    for name, matrix in weights.items():
        rows, cols = len(matrix), len(matrix[0])
        data = struct.pack('<%df' % (rows * cols),
                           *[v for row in matrix for v in row])
        header[name] = {'dtype': 'F32', 'shape': [rows, cols],
                        'data_offsets': [len(body), len(body) + len(data)]}
        body += data
    head = json.dumps(header).encode('utf-8')
    head += b' ' * (-len(head) % 8)
    path.write_bytes(struct.pack('<Q', len(head)) + head + body)


def read_safetensors(path):
    raw = path.read_bytes()
    size = struct.unpack('<Q', raw[:8])[0]
    header = json.loads(raw[8:8 + size])
    body, weights = raw[8 + size:], {}
    for name, info in header.items():
        if name == '__metadata__':
            continue
        assert info['dtype'] == 'F32', 'only float32'
        rows, cols = info['shape']
        start, end = info['data_offsets']
        flat = struct.unpack('<%df' % (rows * cols), body[start:end])
        weights[name] = [list(flat[r * cols:(r + 1) * cols])
                         for r in range(rows)]
    return weights


def export(folder):
    saved = json.loads(Path('checkpoints/model.json').read_text())
    folder.mkdir(exist_ok=True)
    write_safetensors(folder / 'model.safetensors', saved['weights'])
    config = {'model_type': 'xslm', 'vocab_size': len(saved['tokenizer']['vocab']),
              'step': saved['step'], 'settings': saved['settings']}
    (folder / 'config.json').write_text(json.dumps(config, indent=2))
    (folder / 'tokenizer.json').write_text(
        json.dumps(saved['tokenizer'], ensure_ascii=False), encoding='utf-8')
    for name in SOURCES:
        shutil.copy(name, folder / name)
    loaded = read_safetensors(folder / 'model.safetensors')
    gap = max(abs(a - b) for name in loaded
              for row, old in zip(loaded[name], saved['weights'][name])
              for a, b in zip(row, old))
    count = sum(len(row) for matrix in loaded.values() for row in matrix)
    print('parameters:', count, 'bytes:', (folder / 'model.safetensors').stat().st_size)
    print('largest float32 gap:', '%.1e' % gap)
    print('exported:', folder)


def restore(folder):
    config = json.loads((folder / 'config.json').read_text())
    state = {'settings': config['settings'],
             'tokenizer': json.loads((folder / 'tokenizer.json').read_text(encoding='utf-8')),
             'weights': read_safetensors(folder / 'model.safetensors')}
    Path('checkpoints').mkdir(exist_ok=True)
    Path('checkpoints/model.json').write_text(
        json.dumps(state, ensure_ascii=False), encoding='utf-8')
    print('restored: checkpoints/model.json')


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--to', default='hub')
    parser.add_argument('--restore', metavar='FOLDER')
    args = parser.parse_args()
    if args.restore:
        restore(Path(args.restore))
    else:
        export(Path(args.to))
hub/README.md需要亲手编写的完整源码

这是模型卡。最上面两行 --- 之间的 YAML 是 Hugging Face 读取的信息(许可证、语言、标签、任务类型),下面是给人看的说明。把 YOUR_ID 换成你的用户名;如果改过数据,也要把参数数量和训练数据的说明改成你自己的。

---
license: mit
language:
- en
tags:
- xslm
- tiny
- educational
pipeline_tag: text-generation
---

# XSLM tiny

A 1,248-parameter GPT that I typed from scratch and trained
inside my web browser, following https://sonub.com/ai/build

## Files

- model.safetensors: trained weights (float32)
- config.json: size and sampling settings
- tokenizer.json: character BPE vocabulary and merge rules
- engine.py, model.py, tokenizer.py, generate.py: the model code
- runner.py, xchat.py: HTTP Runner and terminal chat
- export.py: writes these files, and turns them back into a checkpoint

## How to run

Python 3 only. Nothing to install.

    git clone https://huggingface.co/YOUR_ID/xslm-tiny
    cd xslm-tiny
    python3 export.py --restore .
    python3 generate.py

## Training data

15 short English Q/A lines written for the course
(12 for training, 3 for validation).

## Limitations

For learning only. With 1,248 parameters and a few training
steps, the answers are mostly random. Do not use it for real work.

逐行拆解代码

export.py SOURCES——要一起上传的运行代码

import argparse
import json
import shutil
import struct
from pathlib import Path

SOURCES = ['engine.py', 'model.py', 'tokenizer.py', 'generate.py',
           'runner.py', 'xchat.py', 'export.py']

struct 是把数字变成字节、再变回来的,shutil 用来复制文件。SOURCES 是运行模型所需的源文件列表。只在训练时用的 prepare.py 和 train.py 不放进去。

export.py write_safetensors——把数字写进文件

def write_safetensors(path, weights):
    header, body = {'__metadata__': {'format': 'xslm'}}, b''
    for name, matrix in weights.items():
        rows, cols = len(matrix), len(matrix[0])
        data = struct.pack('<%df' % (rows * cols),
                           *[v for row in matrix for v in row])
        header[name] = {'dtype': 'F32', 'shape': [rows, cols],
                        'data_offsets': [len(body), len(body) + len(data)]}
        body += data
    head = json.dumps(header).encode('utf-8')
    head += b' ' * (-len(head) % 8)
    path.write_bytes(struct.pack('<Q', len(head)) + head + body)

把每一层的展开成一行,用 struct.pack 变成字节。在格式 <%df 里,< 表示 little-endian,f 是 4 字节 float32,%d 的位置填数字的个数。头部记下这一层的 dtype、shape,以及它在正文里的起始和结束位置。为了让 JSON 头的长度是 8 的倍数,会补上空格,再在最前面用 8 字节(<Q)写入这个长度。

export.py read_safetensors——把文件读成数字

def read_safetensors(path):
    raw = path.read_bytes()
    size = struct.unpack('<Q', raw[:8])[0]
    header = json.loads(raw[8:8 + size])
    body, weights = raw[8 + size:], {}
    for name, info in header.items():
        if name == '__metadata__':
            continue
        assert info['dtype'] == 'F32', 'only float32'
        rows, cols = info['shape']
        start, end = info['data_offsets']
        flat = struct.unpack('<%df' % (rows * cols), body[start:end])
        weights[name] = [list(flat[r * cols:(r + 1) * cols])
                         for r in range(rows)]
    return weights

和写入正好相反。先用开头 8 字节读出头部长度,再把这么多字节当作 JSON 读取。__metadata__ 不是层,所以跳过。用 data_offsets 从正文里只切出这一层的字节,用 struct.unpack 取出数字,再每 cols 个切开,还原成矩阵。

export.py export——制作 hub 文件夹

def export(folder):
    saved = json.loads(Path('checkpoints/model.json').read_text())
    folder.mkdir(exist_ok=True)
    write_safetensors(folder / 'model.safetensors', saved['weights'])
    config = {'model_type': 'xslm', 'vocab_size': len(saved['tokenizer']['vocab']),
              'step': saved['step'], 'settings': saved['settings']}
    (folder / 'config.json').write_text(json.dumps(config, indent=2))
    (folder / 'tokenizer.json').write_text(
        json.dumps(saved['tokenizer'], ensure_ascii=False), encoding='utf-8')
    for name in SOURCES:
        shutil.copy(name, folder / name)

读取训练保存的 checkpoints/model.json。权重写进 safetensors,大小、设置和训练步数写进 config.json,词表和合并规则写进 tokenizer.json。因为用了 ensure_ascii=False,中文这样的字符也会原样保存。最后把 SOURCES 复制到 hub。

export.py 检查——读回来量一量差异

    loaded = read_safetensors(folder / 'model.safetensors')
    gap = max(abs(a - b) for name in loaded
              for row, old in zip(loaded[name], saved['weights'][name])
              for a, b in zip(row, old))
    count = sum(len(row) for matrix in loaded.values() for row in matrix)
    print('parameters:', count, 'bytes:', (folder / 'model.safetensors').stat().st_size)
    print('largest float32 gap:', '%.1e' % gap)
    print('exported:', folder)

用 read_safetensors 把刚写的文件再读一遍,找出和原来数字相差最大的值。这个差异来自缩成 float32,所以在 1e-8 左右是正常的。还会输出参数数量和文件大小,确认 hub 做得对不对。

export.py restore——把下载的文件变回检查点

def restore(folder):
    config = json.loads((folder / 'config.json').read_text())
    state = {'settings': config['settings'],
             'tokenizer': json.loads((folder / 'tokenizer.json').read_text(encoding='utf-8')),
             'weights': read_safetensors(folder / 'model.safetensors')}
    Path('checkpoints').mkdir(exist_ok=True)
    Path('checkpoints/model.json').write_text(
        json.dumps(state, ensure_ascii=False), encoding='utf-8')
    print('restored: checkpoints/model.json')

把 config.json 里的设置、tokenizer.json 和 safetensors 里的权重收集起来,按 generate.py 的 load 读取的样子原封不动地写出 checkpoints/model.json。所以 generate.py、runner.py、xchat.py 一行都不用改,就能运行下载的模型。

export.py 命令行——导出与还原

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--to', default='hub')
    parser.add_argument('--restore', metavar='FOLDER')
    args = parser.parse_args()
    if args.restore:
        restore(Path(args.restore))
    else:
        export(Path(args.to))

不带参数时导出到 hub,用 --to 可以指定别的文件夹。给 --restore 一个文件夹,它就用那里的文件还原检查点。在下载的仓库里面,要像 python3 export.py --restore . 这样给一个点()。

检查理解

打开 huggingface.co/YOUR_ID/xslm-tiny 看看。第一屏有没有显示模型卡?Files 标签里的 model.safetensors 有没有 LFS 标记?把它 git clone 到另一个文件夹,运行 python3 export.py --restore . 和 python3 generate.py,如果得到和这个终端一样的回答,模型就发布成功了。

遇到问题时

如果 git push 因为 binary files 被拒绝,说明你漏掉了 git lfs install。用 hub 删除文件夹,从 git lfs install 重新开始。出现 failed,是因为你输入了密码,或者用了只有 Read 权限的令牌。刷新页面会让 hub 里的 .git 消失,这时也要先 rm -rf hub,再重新运行 git clone 和 python3 export.py。全部上传完后,可以在 Access Tokens 里删除令牌。

常见问题

可以把仓库设成不公开(Private)吗?

可以。Private 仓库只有你和你允许的人能看到。上传方法完全一样,唯一的区别是下载时也需要令牌。确认好以后,可以在仓库的 Settings 里改成 Public。

怎样修改或删除已上传的模型?

要修改,就重新训练,运行 python3 export.py,然后在 hub 里 git add、commit、push。会新增一个提交,以前的版本也留在历史记录里。要删除,就点仓库 Settings 最下面的删除(Delete)按钮。删除的仓库无法恢复。

能用 transformers 库加载吗?

直接不行。 只能读取 、 这类已登记的结构,而 XSLM 的层结构不同,分词器也是按字符的。所以我们把运行代码一起上传,用 export.py --restore 在自己的代码里运行。safetensors 文件本身是标准格式,可以用 safetensors 打开,读成 数组。

学习指南

人工智能

智能体工具

Vibe 编程

AI 应用

AI证书

AI课堂

IT 基础

聊天

论坛

创作

社区

更多

术语词典

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

还没有匹配的术语。

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