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 Shell 把
repr()用于表达式值的交互式回显。它返回字符串时,其中的控制代码等码位将被替换为转义字符。
- 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 个字节,符号不同字节个数也不同。

截图自:bilibili.com/video/BV1xP4y1J7CS
UTF-8 的字节有三类,互不重叠,靠高位比特区分:
- 单字节字符的字节:最高位为 0。固定格式:0xxxxxxx
- 多字节字符的首字节(Leading byte):开头1的个数等于当前字符总字节数。固定格式:110xxxxx(2 字节)、1110xxxxx(3 字节)、11110xxx(4 字节)。
- 多字节字符的后续字节(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) 有现成的日语编码字节,其中任意两字节片段都符合要求。

浙公网安备 33010602011771号