CS336 作业1(上):构建 Transformer 语言模型

参考的知乎专栏文章:https://zhuanlan.zhihu.com/p/1935209292715241716

1 作业概述

本次作业是Stanford CS336 的作业1,要求 从零实现 训练标准 Transformer 语言模型所需的全部组件,并完成模型训练。需要实现的内容:

(第 2 节)字节对编码(BPE)分词器
(第 3 节)Transformer 语言模型
(第 4 节)交叉熵损失函数 与 AdamW 优化器
(第 5 节)训练循环,支持模型与优化器状态的序列化保存和加载

2 BPE 分词器

本章节你需要训练并实现 字节级BPE 分词器。
思路:将任意 Unicode 字符串转为字节序列,再基于字节序列训练 BPE 分词器。
最终使用该分词器将原始文本编码为整数 ID 序列,供语言模型使用。

2.1 Unicode 标准

Unicode 字符编码标准
Unicode 字符编码标准,将字符映射为整数码点。
该标准的码点范围是0x0, 0x1, …, 0x10FFFF,总容量 1,114,112(0x110000)。
Unicode 17.0 总码点空间已分配的有30多万,大部分还未分配。

Python 内置的转换函数
以下是 Unicode 字符-整数码点转换的接口,两者互为反向操作。

  • ord(字符):字符转成数字。
  • chr(整数):数字转成字符。
>>> ord('牛')
29275
>>> chr(29275)
'牛'

控制字符
字符编码标准有部分是控制字符(Control Characters),比如0x00 ~ 0x1F,这些字符不可见,具有换行、响铃等用途。

下面的 '\t'、'\x00' 等是转义字符。

>>> ord('\t')
9
>>> chr(9)
'\t'

2.1 (a)

问, chr(0) 返回结果是什么?(a) chr(0) returns '\x00'

>>> ord('\x00')
0
>>> chr(0)
'\x00'

__repr()__ 和 打印输出

简单理解, repr() 用途是调试 / 还原,追求无歧义,对不可见字符往往用转义字符表示。不直接渲染视觉效果。相关官方文档:

  • repr() 返回一个字符串,内容是 object 的可打印表示形式(printable representation)。返回内容由类的 __repr__() 方法控制,对许多类型而言,满足 eval(repr(obj)) == obj

IDLE Shellrepr()用于表达式值的交互式回显。它返回字符串时,其中的控制代码等码位将被替换为转义字符。

  • str() 是 str 类的构造方法,往往返回 object 的字符串版本(如果不传入 object 则返回空字符串)。

print() 将 objects 打印输出至文本流。把所有非关键字参数转换为字符串,就像是执行了 str() 一样,这些字符串被写入到流,以 sep 分隔并在末尾加上 end(默认sep=' ', end='\n')。

2.1 (b)

问,chr(0)__repr__() 和直接打印输出有什么区别?(b) chr(0) is invisible when printed but its __repr__() shows it as the escape sequence '\x00'.

>>> chr(0)
'\x00'
>>> print(chr(0))
 

2.1 (c)

问,文本中包含chr(0)会怎样?建议在 Python 解释器中运行以下代码来验证你的猜想。 (c) 文本中的chr(0)在被打印输出时是一个空格,在 __repr__() 中显示为 \x00。

>>> "this is a test" + chr(0) + "string"
'this is a test\x00string'
>>> print("this is a test" + chr(0) + "string")
this is a test string

打印时,chr(0)不是不存在,而是一个空格。
为了确认这一点,尝试发现与“”拼接等效于没拼,倒是与" "拼接的结果和chr(0)的一样。

>>> print("this is a test" + "string")
this is a teststring

>>> print("this is a test" + "" + "string")
this is a teststring

>>> print("this is a test" + " " + "string")
this is a test string

2.2 Unicode 编码

UTF 是Unicode编码的实现

现实中,Unicode 的字符要是直接用编号对应的二进制来存浪费很大(15 万个字符统统3字节),因此另外设计实现方案。Unicode编码的实现被称为 Unicode 转换格式(Unicode Transformation Format, UTF)。参考来源:https://zhuanlan.zhihu.com/p/195493519

UTF-8

Unicode 有多种实现方式,其中 UTF-8 最为主流。UTF-8 是一种变长编码,一个 Unicode 的字符被编码成 1 至 4 个字节,符号不同字节个数也不同。

article-16-1
截图自:bilibili.com/video/BV1xP4y1J7CS

UTF-8 的字节有三类,互不重叠,靠高位比特区分:

  1. 单字节字符的字节:最高位为 0。固定格式:0xxxxxxx
  2. 多字节字符的首字节(Leading byte):开头1的个数等于当前字符总字节数。固定格式:110xxxxx(2 字节)、1110xxxxx(3 字节)、11110xxx(4 字节)。
  3. 多字节字符的后续字节(Continuation byte):最高两位为 10。固定格式:10xxxxxx。

Python 相关接口
字符串.encode("utf-8"):将 Unicode 字符串转为 UTF-8 字节对象(bytes 类型)。
list(字节对象):提取每个字节对应的整数(取值范围 0~255)。
字节对象.decode("utf-8"):将 UTF-8 字节序列还原为 Unicode 字符串。

2.2 (a)

问,相比于 UTF-16、UTF-32,选择 UTF-8 训练分词器的原因有哪些?可对比不同编码对同一文本的输出结果分析。(a) UTF-16 和 UTF-32 字节对象包括许多冗余的 \x00 空字节,而 UTF-8 字节紧凑;编码相同文本,UTF-8 字节总数往往更少;UTF-8 跨平台兼容性更好,98%以上的网页文本用的是 UTF-8。

>>> test_string = "hello! こんにちは!"
>>> utf8_encoded = test_string.encode("utf-8")
>>> utf16_encoded = test_string.encode("utf-16")
>>> utf32_encoded = test_string.encode("utf-32")
>>> print(utf8_encoded)
b'hello! \xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf!'
>>> print(utf16_encoded)
b'\xff\xfeh\x00e\x00l\x00l\x00o\x00!\x00 \x00S0\x930k0a0o0!\x00'
>>> print(utf32_encoded)
b'\xff\xfe\x00\x00h\x00\x00\x00e\x00\x00\x00l\x00\x00\x00l\x00\x00\x00o\x00\x00\x00!\x00\x00\x00 \x00\x00\x00S0\x00\x00\x930\x00\x00k0\x00\x00a0\x00\x00o0\x00\x00!\x00\x00\x00'
>>> list(utf8_encoded)
[104, 101, 108, 108, 111, 33, 32]
>>> list(utf16_encoded)
[255, 254, 104, 0, 101, 0, 108, 0, 108, 0, 111, 0, 33, 0, 32, 0]
>>> list(utf32_encoded)
[255, 254, 0, 0, 104, 0, 0, 0, 101, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 111, 0, 0, 0, 33, 0, 0, 0, 32, 0, 0, 0]

Python 打印字节对象时,超出 127 的二进制值使用相应的转义序列形式加入 bytes 字面值。而对于可打印 ASCII 字符(字母、数字、空格、标点等)时,直接显示本身。并且连续的 \x00 只会显示成数字 0,不再重复写 \x。

2.2 (b)

问,以下函数本意是将 UTF-8 字节序列解码为字符串,但函数实现是错的。请说明错误原因,并给出一组会产生错误输出的字节序列示例。(b) 例子:"牛,こんにちは!"。被编码成多字节的 Unicode 的字符,其中的单字节不构成完整 Unicode 字符,逐字节解码会报“数据意外截断”的错误。

>>> def decode_utf8_bytes_to_str_wrong(bytestring: bytes):
    return "".join([bytes([b]).decode("utf-8") for b in bytestring])

>>> decode_utf8_bytes_to_str_wrong("hello world. python program".encode("utf-8"))
'hello world. python program'
>>> decode_utf8_bytes_to_str_wrong("牛,こんにちは!".encode("utf-8"))
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    decode_utf8_bytes_to_str_wrong("牛,こんにちは!".encode("utf-8"))
  File "<pyshell#2>", line 2, in decode_utf8_bytes_to_str_wrong
    return "".join([bytes([b]).decode("utf-8") for b in bytestring])
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe7 in position 0: unexpected end of data

UTF-8 不全是单字节字符,多字节字符对应的字节序列得完整处理才能正确解码。题目 (b) 说明 UTF-8 不能拆分成单字节逐个解码。

2.2 (c)

问,给出一组无法解码为合法 Unicode 字符的双字节序列,并附一句话解释。(c) 例子:来自题目 (a) 的 "\xe3\x81"。日语字符是三字节字符,日语字节序列的两字节片段无法解码为合法字符。

>>> b"\xe3\x81".decode("utf-8")
Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    b"\xe3\x81".decode("utf-8")
UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 0-1: unexpected end of data

2.2 (a) 有现成的日语编码字节,其中任意两字节片段都符合要求。

posted @ 2026-06-11 03:41  修子xiu1zi3  阅读(12)  评论(0)    收藏  举报