首先配置好 vscodesettings.json

因为 uv 项目会在工程目录下新建一个 .venv 在这里面放置各种环境依赖,而 vscode 进行文件解析(如果把解释器错误的设置为平时用的路径,那么大概率会出现各种不能成功导入包,出现各种报错, 这种错误我曾经一笑而过,但是也确实在潜意识里让我觉得很难受,直到上 jyy 的课时我知道了 vscode 是 需要 也是 可以 去设置类似解释器,类似调试器路径,类似等等等操作,不过后来还是不太知道该怎么设置,不过让我知道了可以这么设置,而大模型就完全能够让我正确设置 settings.json

{
    "python.analysis.typeCheckingMode": "basic",
    "python.defaultInterpreterPath": "${workspaceFolder}/assignment1-basics/.venv/bin/python",      
}

对于大模型工具等声明

该声明是该 assignment 里的,强调了三点

  1. 最好不要用各种大模型自动补全工具
  2. 也没有大模型补全工具能够完成该作业
  3. 使用大模型补全工具会让使用者不能深层地理解该作业

Problem 1

为什么用 UTF-8 来训练我们的 tokenizer,而不是 UTF-16 等

(a) What are some reasons to prefer training our tokenizer on UTF-8 encoded bytes, rather than
UTF-16 or UTF-32? It may be helpful to compare the output of these encodings for various
input strings.

Deliverable: A one-to-two sentence response

根据维基百科对 UTF-8 的介绍

截止到 2019 年 11 月,在所有网页中,UTF-8 编码应用率高达 94.3%(其中一些仅是 ASCII 编码,因为它是 UTF-8 的子集),而在排名最高的 1000 个网页中占 96%。

用 UTF-8 是一种务实的表现(支持绝大部分网页),而且也可以产生更小的字符集和更有效率的 tokenizer

Training a tokenizer on UTF-8 bytes is preferable because UTF-8 is space-efficient for ASCII-heavy data (most text on the web), preserves compatibility with existing datasets, and avoids the fixed-width 2–4-byte overhead of UTF-16/UTF-32. Compared with UTF-16/32, UTF-8 produces shorter and more diverse byte sequences for common text, which leads to smaller vocabularies and more efficient tokenization.

考虑 UTF-8 的可变长度编码

UTF-8 的一些性质

  • 前缀码(huffman 编码),可以做到即时解码
  • 可变长度(1,2,3,4 字节都有)(自 RFC 3629 规定后最多四个字节)
  • US-ACII 仅使用一个字节。拉丁文等欧洲文字两个字节。汉语等三个字节。辅助平面的(比如 emoji)四个字节。
(b) Consider the following (incorrect) function, which is intended to decode a UTF-8 byte string into
a Unicode string. Why is this function incorrect? Provide an example of an input byte string
that yields incorrect results.
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".encode("utf-8"))
'hello'
 
Deliverable: An example input byte string for which decode_utf8_bytes_to_str_wrong pro-
duces incorrect output, with a one-sentence explanation of why the function is incorrect

UTF-8 是可变长度的,需要根据前缀码来解析

创建 的技巧

手动实现一个 nn. Linear 层中的权重初始化逻辑,具体使用了一个叫 truncated normal(截断正态分布) 的初始化方式。我们来逐步解释这段代码都做了什么、为什么这么做,以及它的意义。

一、代码逐行解释

定义可学习参数

Self.W = nn.Parameter (
    Torch.Empty (self.Out_features, self.In_features, device=self.Device, dtype=self.Dtype)
)

手动创建一个 torch.nn.Parameter,表示一个需要梯度的张量

明确控制初始化方式,这里是截断正态分布(Truncated Normal)

不使用 nn. Linear,可能需要自己写 forward (x @ self. W. T) 来完成前向传播

📌 优点:

高度可控(初始化方式、形状、设备、dtype 等)

适合写自定义的模块,如 Transformer、BERT 等论文级实现

📌 缺点:

更复杂、冗长

容易出错(需手动处理转置、广播等)

这行代码定义了一个参数张量 W,大小为 [输出维度, 输入维度],用来存放线性层的权重(即权重矩阵 W)。 • 用 torch.Empty () 只分配内存,不初始化值 • 用 nn.Parameter () 表示它是一个可训练的参数

self.w1 = nn.Linear(d_model,d_ff,bias=False)
  • 使用的是 PyTorch 提供的封装模块 nn. Linear

  • 自动创建权重参数 self.W1.Weight(形状为[d_ff, d_model])

  • 自动注册参数、添加到 model.Parameters()

  • 可选 bias 参数,设为 False 即不添加偏置

  • 默认使用 Kaiming Uniform 初始化权重


  1. 计算初始化标准差 std

这是 Xavier/Glorot 初始化中的一种变种,也叫 LeCun 初始化风格:

这个标准差值控制了初始值分布的“宽度”,有助于在网络初始化时避免梯度爆炸或消失。

  1. 使用截断正态分布初始化
Init.runc_normal_(self.W, std=std, a = -3 * std, b = 3 * std)

这表示: • 使用正态分布 𝒩(0, std²) 初始化权重 • 但只取落在区间 [] 内的值 • 超出范围的会重新采样,保证数值“集中”,避免极端值

这就是所谓的截断正态分布(truncated normal),相比直接 torch.Randn() 更安全。

二、为什么要用 trunc_normal_ 初始化?

优点解释 避免极端值限定在 ±3σ 内,保证初始梯度稳定 有良好均值和方差控制保证正态分布,但去掉尾部异常值 更适合深层网络避免深层传播中梯度消失或爆炸

这在 Transformer 模型、ResNet、ViT 等中都很常见,很多模型用它作为默认权重初始化。

补充:init.trunc_normal_ 是什么?

PyTorch 中:

torch.nn.init.trunc_normal_(tensor, mean=0.0, std=1.0, a=-2.0, b=2.0)

•	在区间 [a, b] 内从正态分布采样
•	mean 是均值,std 是标准差
•	初始化结果仍近似于 𝒩(mean, std²),但裁剪了尾部

当然有!在 PyTorch 中,有多种 参数初始化方式,可以用于 nn.Parameternn.Linear.weight 等张量。PyTorch 的初始化函数主要集中在 torch.nn.init 模块中。


🔧 常见初始化方法总览(torch.nn.init

初始化方法适用情况函数名分布特性
均匀初始化通用,简单uniform_均匀分布
正态初始化通用,简单normal_正态分布
截断正态初始化防止极端值(更稳定)trunc_normal_正态分布截断在
Xavier 均匀/正态初始化前后层激活值方差相同,常用于前馈层xavier_uniform_ / xavier_normal_方差基于输入+输出维度
Kaiming 均匀/正态初始化常用于 ReLU 激活前的权重kaiming_uniform_ / kaiming_normal_方差考虑非线性激活的影响
常数初始化用于特殊需求(例如测试、调试)constant_所有元素初始化为同一个值
单位初始化用于特殊结构如 RNN、残差连接等eye_对角为 1,其余为 0(单位矩阵)
稀疏初始化用于稀疏模型(如稀疏 Transformer)sparse_指定稀疏率
Zeros / Ones常用于偏置初始化zeros_, ones_所有元素为 0 或 1

🎯 实例:几种典型初始化 的方法

1. Xavier 初始化(适用于 Sigmoid/Tanh 激活)

init.xavier_uniform_(self.W)
# or
init.xavier_normal_(self.W)

2. Kaiming 初始化(适用于 ReLU)

init.kaiming_uniform_(self.W, a=0, mode='fan_in', nonlinearity='relu')
# or
init.kaiming_normal_(self.W, mode='fan_in', nonlinearity='relu')

3. 截断正态分布初始化(控制极端值)

std = 0.02
init.trunc_normal_(self.W, mean=0.0, std=std, a=-2*std, b=2*std)

GPT/BERT/Transformer 模型中,很多时候都使用 trunc_normal_,例如:

Init.trunc_normal_(self.W, std=0.02)

4. 纯手动初始化

self.W.data = torch.randn_like(self.W) * 0.01

🧠 如何选择初始化方式?

网络结构建议初始化方式
MLP + ReLUkaiming_uniform_
Transformer/BERTtrunc_normal_(std=0.02)
Sigmoid/Tanh 网络xavier_uniform_
RNN/LSTMorthogonal_, xavier_uniform_
测试/调试模型constant_, ones_, zeros_

初始化影响模型训练稳定性与收敛速度,权重初始化是深度学习中提升模型性能的“隐形手术刀”。

trick

根据这个 Transformer 作业的文档,我来解释为什么有些模块使用 class,有些使用 function:

使用 Class 的模块

需要可学习参数的组件:

  1. Linear - 有权重矩阵 W 参数
  2. Embedding - 有嵌入矩阵参数
  3. RMSNorm - 有可学习的增益参数 g
  4. PositionWiseFeedForward - 包含多个 Linear 层
  5. MultiHeadSelfAttention - 有 ,W_KW_VW_O$ 参数
  6. TransformerBlock - 组合多个有参数的子模块
  7. TransformerLM - 整个模型的顶层容器

这些都继承自 torch.nn.Module,因为它们:

  • 包含需要训练的参数(nn.Parameter
  • 需要参与梯度计算和反向传播
  • 需要状态管理(如设备、数据类型)
  • 需要序列化/反序列化功能

使用 Function 的模块

无可学习参数的操作:

  1. Softmax - 纯数学运算,无参数
  2. Scaled Dot-Product Attention - 计算注意力权重,无参数
  3. RoPE(可选) - 位置编码,使用预计算的 sin/cos 值

这些实现为 function 因为它们:

  • 不需要可学习参数
  • 是无状态的数学运算
  • 可以更高效地实现
  • 便于复用和测试

设计原则

# Class - 有参数
class Linear(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(out_features, in_features))
    
    def forward(self, x):
        return x @ self.weight.T
 
# Function - 无参数
def softmax(x, dim):
    x_max = torch.max(x, dim=dim, keepdim=True)[0]
    x_shifted = x - x_max
    return torch.exp(x_shifted) / torch.sum(torch.exp(x_shifted), dim=dim, keepdim=True)

这种设计遵循了 PyTorch 的最佳实践:有状态/参数的用 Module,无状态的用 Function