Delete happyllm-note directory

This commit is contained in:
xinala-781
2025-07-13 15:46:28 +08:00
committed by GitHub
parent fdc2e0cc85
commit 615abaab9f
10 changed files with 0 additions and 26725 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
unhappiness

File diff suppressed because it is too large Load Diff

View File

@@ -1,985 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "dc8f84d6",
"metadata": {},
"source": [
"# 第二章Transformer架构\n",
"\n",
"本章节将会结合正文当中的Transformer架构的知识为大家实际动手操作一下如何应用Transformer架构做一个中英翻译器并且最终保存为权重共大家进行推导集的实验\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "eedb3ae8",
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import torch.nn as nn\n",
"import torch.optim as optim\n",
"from torch.utils.data import Dataset, DataLoader\n",
"from torch.nn.utils.rnn import pad_sequence\n",
"import math\n",
"import torch.nn.functional as F\n",
"import jieba \n",
"import re \n",
"from collections import defaultdict, Counter\n",
"import os\n",
"import pandas as pd"
]
},
{
"cell_type": "markdown",
"id": "df9da891",
"metadata": {},
"source": [
"## 读取本地数据集\n",
"\n",
"数据集为english.en英文句子和chinese.zh对应中文翻译每行一条数据需保证两条文件的句子一一对应"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "dcec0d70",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"数据描述\n",
"数据集大小: 10000\n",
"数据集前5行:\n",
" chinese \\\n",
"0 1929年还是1989年? \n",
"1 巴黎-随着经济危机不断加深和蔓延,整个世界一直在寻找历史上的类似事件希望有助于我们了解目前正... \n",
"2 一开始很多人把这次危机比作1982年或1973年所发生的情况这样得类比是令人宽心的因为... \n",
"3 如今人们的心情却是沉重多了许多人开始把这次危机与1929年和1931年相比即使一些国家政... \n",
"4 目前的趋势是,要么是过度的克制(欧洲),要么是努力的扩展(美国)。 \n",
"\n",
" english \n",
"0 1929 or 1989? \n",
"1 PARIS As the economic crisis deepens and wid... \n",
"2 At the start of the crisis, many people likene... \n",
"3 Today, the mood is much grimmer, with referenc... \n",
"4 The tendency is either excessive restraint (Eu... \n"
]
}
],
"source": [
"def load_data(chinese_path, english_path, max_samples=10000):\n",
" \"\"\"加载中文和英文数据最多加载max_samples条\"\"\"\n",
" with open(chinese_path, 'r', encoding='utf-8') as f:\n",
" chinese_data = f.readlines() \n",
" with open(english_path, 'r', encoding='utf-8') as f:\n",
" english_data = f.readlines()\n",
" \n",
" # 确保两种语言数据量一致,并限制数量\n",
" min_len = min(len(chinese_data), len(english_data), max_samples)\n",
" chinese_data = chinese_data[:min_len]\n",
" english_data = english_data[:min_len]\n",
" \n",
" # 创建DataFrame\n",
" df = pd.DataFrame({\n",
" 'chinese': chinese_data,\n",
" 'english': english_data\n",
" })\n",
" \n",
" # 去除每行末尾的换行符\n",
" df['chinese'] = df['chinese'].str.strip()\n",
" df['english'] = df['english'].str.strip()\n",
" \n",
" return df\n",
"\n",
"chinese_path = 'chinese.zh'\n",
"english_path = 'english.en'\n",
"# 加载数据\n",
"print(\"数据描述\")\n",
"data = load_data(chinese_path, english_path)\n",
"print(f\"数据集大小: {len(data)}\")\n",
"print(f\"数据集前5行:\\n{data.head()}\")"
]
},
{
"cell_type": "markdown",
"id": "c5fa5fa7",
"metadata": {},
"source": [
"## 分词处理\n",
"\n",
"**1.** 中文分词用jieba实现对应文档 1.3.1 节 “中文分词”(解决中文无空格分隔问题)。\n",
"\n",
"**2.** 英文分词:用正则表达式分割单词和标点,对应文档 1.3.2 节 “子词切分”(将文本拆分为最小语义单位)。"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "edb4b49c",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Building prefix dict from the default dictionary ...\n",
"Loading model from cache C:\\Users\\xnlll\\AppData\\Local\\Temp\\jieba.cache\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"中文原句1929年还是1989年?\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Loading model cost 0.474 seconds.\n",
"Prefix dict has been built successfully.\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"中文分词:['1929', '年', '还是', '1989', '年', '?']\n",
"英文原句1929 or 1989?\n",
"英文分词:['1929', 'or', '1989', '?']\n"
]
}
],
"source": [
"def tokenize_zh(text):\n",
" \"\"\"中文分词保留标点返回token列表\"\"\"\n",
" return list(jieba.cut(text)) # jieba.cut返回生成器转为列表\n",
"\n",
"def tokenize_en(text):\n",
" \"\"\"英文分词:用正则分割单词和标点,转为小写\"\"\"\n",
" # 匹配字母、数字、 apostrophe如don't和标点\n",
" pattern = re.compile(r\"[a-zA-Z0-9']+|[^\\w\\s]\")\n",
" tokens = pattern.findall(text.lower()) # 小写统一格式\n",
" return tokens\n",
"\n",
"# 示例:测试分词效果\n",
"sample = data.iloc[0]\n",
"print(f\"中文原句:{sample['chinese']}\")\n",
"print(f\"中文分词:{tokenize_zh(sample['chinese'])}\")\n",
"print(f\"英文原句:{sample['english']}\")\n",
"print(f\"英文分词:{tokenize_en(sample['english'])}\")"
]
},
{
"cell_type": "markdown",
"id": "5ed65195",
"metadata": {},
"source": [
"## 构建词汇表\n",
"\n",
"对应文档中的“Embedding 层”内容\n",
"\n",
"词汇表是将 token 映射为索引的关键,用于 Embedding 层查找词向量。手动实现词汇表类"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "89eae687",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"中文词汇表大小9821英文词汇表大小9082\n",
"中文特殊符号索引:<bos>2<eos>3<pad>1\n"
]
}
],
"source": [
"class Vocab:\n",
" def __init__(self, tokens_list, min_freq=2, specials=['<unk>', '<pad>', '<bos>', '<eos>']):\n",
" \"\"\"\n",
" tokens_list: 所有句子的分词结果列表(如[[token1, token2], ...]\n",
" min_freq: 过滤低频词(出现次数<min_freq的token视为<unk>\n",
" specials: 特殊符号(未知词、填充、句首、句尾)\n",
" \"\"\"\n",
" # 统计token频率\n",
" token_counts = Counter([token for seq in tokens_list for token in seq])\n",
" # 筛选高频词保留出现次数≥min_freq的token\n",
" self.token_list = [token for token, cnt in token_counts.items() if cnt >= min_freq]\n",
" # 加入特殊符号(放在最前面,保证索引固定)\n",
" self.token_list = specials + self.token_list\n",
" # 构建token→索引映射\n",
" self.token2idx = {token: idx for idx, token in enumerate(self.token_list)}\n",
" # 构建索引→token映射\n",
" self.idx2token = {idx: token for token, idx in self.token2idx.items()}\n",
" \n",
" def __len__(self):\n",
" return len(self.token_list)\n",
" \n",
" def convert_tokens_to_ids(self, tokens):\n",
" \"\"\"将token列表转为索引列表未知token用<unk>的索引)\"\"\"\n",
" return [self.token2idx.get(token, self.token2idx['<unk>']) for token in tokens]\n",
" \n",
" def convert_ids_to_tokens(self, ids):\n",
" \"\"\"将索引列表转为token列表\"\"\"\n",
" return [self.idx2token[idx] for idx in ids]\n",
"\n",
"# 生成所有句子的分词结果,用于构建词汇表\n",
"zh_tokens_all = [tokenize_zh(text) for text in data['chinese']] # 所有中文句子的分词结果\n",
"en_tokens_all = [tokenize_en(text) for text in data['english']] # 所有英文句子的分词结果\n",
"\n",
"# 构建中、英文词汇表\n",
"zh_vocab = Vocab(zh_tokens_all, min_freq=2) # 中文词汇表\n",
"en_vocab = Vocab(en_tokens_all, min_freq=2) # 英文词汇表\n",
"\n",
"print(f\"中文词汇表大小:{len(zh_vocab)},英文词汇表大小:{len(en_vocab)}\")\n",
"print(f\"中文特殊符号索引:<bos>{zh_vocab.token2idx['<bos>']}<eos>{zh_vocab.token2idx['<eos>']}<pad>{zh_vocab.token2idx['<pad>']}\")"
]
},
{
"cell_type": "markdown",
"id": "104c397c",
"metadata": {},
"source": [
"## 构建数据集与数据加载器\n",
"\n",
"对应文档 “序列数据处理”\n",
"\n",
"将文本转为模型可输入的张量,添加<bos>(句首)和<eos>(句尾)符号,并对齐序列长度(用<pad>填充)。"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "f7ae7ffa",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"源语言批次形状torch.Size([32, 57])目标语言批次形状torch.Size([32, 53])\n"
]
}
],
"source": [
"class TranslationDataset(Dataset):\n",
" def __init__(self, df, src_vocab, tgt_vocab, src_col='english', tgt_col='chinese'):\n",
" \"\"\"\n",
" df: 包含源语言和目标语言的DataFrame\n",
" src_vocab: 源语言(英文)词汇表\n",
" tgt_vocab: 目标语言(中文)词汇表\n",
" src_col/tgt_col: DataFrame中源/目标语言列名\n",
" \"\"\"\n",
" self.df = df\n",
" self.src_vocab = src_vocab\n",
" self.tgt_vocab = tgt_vocab\n",
" self.src_col = src_col\n",
" self.tgt_col = tgt_col\n",
" \n",
" def __len__(self):\n",
" return len(self.df)\n",
" \n",
" def __getitem__(self, idx):\n",
" # 获取句子\n",
" src_text = self.df.iloc[idx][self.src_col]\n",
" tgt_text = self.df.iloc[idx][self.tgt_col]\n",
" \n",
" # 分词\n",
" src_tokens = tokenize_en(src_text) # 英文分词\n",
" tgt_tokens = tokenize_zh(tgt_text) # 中文分词\n",
" \n",
" # 添加句首(<bos>)和句尾(<eos>)符号\n",
" src_tokens = ['<bos>'] + src_tokens + ['<eos>']\n",
" tgt_tokens = ['<bos>'] + tgt_tokens + ['<eos>']\n",
" \n",
" # 转为索引\n",
" src_ids = self.src_vocab.convert_tokens_to_ids(src_tokens)\n",
" tgt_ids = self.tgt_vocab.convert_tokens_to_ids(tgt_tokens)\n",
" \n",
" return torch.tensor(src_ids, dtype=torch.long), torch.tensor(tgt_ids, dtype=torch.long)\n",
"\n",
"# 定义collate_fn将批次内序列填充至同一长度\n",
"def collate_fn(batch, pad_idx_src, pad_idx_tgt):\n",
" \"\"\"\n",
" batch: 数据集返回的(src_ids, tgt_ids)列表\n",
" pad_idx_src: 源语言<pad>的索引\n",
" pad_idx_tgt: 目标语言<pad>的索引\n",
" \"\"\"\n",
" src_batch, tgt_batch = zip(*batch)\n",
" # 填充源语言序列batch_first=True表示输出形状为[batch_size, seq_len]\n",
" src_padded = pad_sequence(src_batch, batch_first=True, padding_value=pad_idx_src)\n",
" # 填充目标语言序列\n",
" tgt_padded = pad_sequence(tgt_batch, batch_first=True, padding_value=pad_idx_tgt)\n",
" return src_padded, tgt_padded\n",
"\n",
"# 初始化数据集\n",
"dataset = TranslationDataset(\n",
" df=data,\n",
" src_vocab=en_vocab,\n",
" tgt_vocab=zh_vocab\n",
")\n",
"\n",
"# 初始化数据加载器\n",
"batch_size = 32\n",
"dataloader = DataLoader(\n",
" dataset,\n",
" batch_size=batch_size,\n",
" shuffle=True,\n",
" collate_fn=lambda x: collate_fn(\n",
" x,\n",
" pad_idx_src=en_vocab.token2idx['<pad>'],\n",
" pad_idx_tgt=zh_vocab.token2idx['<pad>']\n",
" )\n",
")\n",
"\n",
"# 测试数据加载器\n",
"src_sample, tgt_sample = next(iter(dataloader))\n",
"print(f\"源语言批次形状:{src_sample.shape},目标语言批次形状:{tgt_sample.shape}\")"
]
},
{
"cell_type": "markdown",
"id": "7bfbc8da",
"metadata": {},
"source": [
"## 构建 Transformer 模型(核心)\n",
"\n",
"### 1.位置编码\n",
"\n",
"注意力机制本身不包含位置信息,需通过正余弦函数注入序列顺序"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "0dcb59ba",
"metadata": {},
"outputs": [],
"source": [
"class PositionalEncoding(nn.Module):\n",
" def __init__(self, d_model, max_seq_len=5000, dropout=0.1):\n",
" super().__init__()\n",
" self.dropout = nn.Dropout(p=dropout)\n",
" # 初始化位置编码矩阵max_seq_len, d_model\n",
" pe = torch.zeros(max_seq_len, d_model)\n",
" # 位置索引0到max_seq_len-1\n",
" position = torch.arange(0, max_seq_len, dtype=torch.float).unsqueeze(1)\n",
" # 计算频率因子(避免数值过大)\n",
" div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))\n",
" # 偶数维度用正弦,奇数维度用余弦\n",
" pe[:, 0::2] = torch.sin(position * div_term)\n",
" pe[:, 1::2] = torch.cos(position * div_term)\n",
" # 增加batch维度1, max_seq_len, d_model\n",
" pe = pe.unsqueeze(0)\n",
" self.register_buffer('pe', pe) # 不参与训练的缓冲区\n",
" \n",
" def forward(self, x):\n",
" \"\"\"x: 输入序列嵌入,形状为[batch_size, seq_len, d_model]\"\"\"\n",
" x = x + self.pe[:, :x.size(1)] # 加位置编码自动广播batch维度\n",
" return self.dropout(x)"
]
},
{
"cell_type": "markdown",
"id": "ec04c1f3",
"metadata": {},
"source": [
"### 注意力机制\n",
"\n",
"实现核心公式 \n",
"\n",
"$$\n",
"attention(Q,K,V) = softmax(\\frac{QK^T}{\\sqrt{d_k}})V \n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "a2790ef3",
"metadata": {},
"outputs": [],
"source": [
"def attention(query, key, value, mask=None, dropout=None):\n",
" \"\"\"\n",
" query: [batch_size, n_heads, seq_len_q, d_k]\n",
" key: [batch_size, n_heads, seq_len_k, d_k]\n",
" value: [batch_size, n_heads, seq_len_v, d_v]\n",
" mask: [batch_size, 1, seq_len_q, seq_len_k]1表示可见0表示遮蔽\n",
" \"\"\"\n",
" d_k = query.size(-1)\n",
" # 计算相似度分数Q*K^T / sqrt(d_k)\n",
" scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)\n",
" # 应用掩码(遮蔽<pad>或未来信息)\n",
" if mask is not None:\n",
" scores = scores.masked_fill(mask == 0, -1e9) # 掩码位置设为负无穷\n",
" # softmax归一化得到注意力权重\n",
" attn_weights = F.softmax(scores, dim=-1)\n",
" # 应用dropout\n",
" if dropout is not None:\n",
" attn_weights = dropout(attn_weights)\n",
" # 加权求和得到输出\n",
" output = torch.matmul(attn_weights, value)\n",
" return output, attn_weights"
]
},
{
"cell_type": "markdown",
"id": "737776ee",
"metadata": {},
"source": [
"### 多头注意力\n",
"\n",
"将注意力拆分为多个头并行计算,捕捉不同语义关系"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "a25717cc",
"metadata": {},
"outputs": [],
"source": [
"class MultiHeadAttention(nn.Module):\n",
" def __init__(self, d_model, n_heads, dropout=0.1):\n",
" super().__init__()\n",
" assert d_model % n_heads == 0, \"d_model必须是n_heads的整数倍\"\n",
" self.d_k = d_model // n_heads # 每个头的维度\n",
" self.n_heads = n_heads\n",
" # Q/K/V的线性变换矩阵将d_model映射到d_model\n",
" self.w_q = nn.Linear(d_model, d_model)\n",
" self.w_k = nn.Linear(d_model, d_model)\n",
" self.w_v = nn.Linear(d_model, d_model)\n",
" # 输出线性变换矩阵将多头结果拼接后映射回d_model\n",
" self.w_o = nn.Linear(d_model, d_model)\n",
" self.dropout = nn.Dropout(dropout)\n",
" \n",
" def forward(self, query, key, value, mask=None):\n",
" \"\"\"\n",
" query/key/value: [batch_size, seq_len, d_model]\n",
" \"\"\"\n",
" batch_size = query.size(0)\n",
" # 线性变换并拆分多头:[batch_size, seq_len, d_model] → [batch_size, n_heads, seq_len, d_k]\n",
" q = self.w_q(query).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)\n",
" k = self.w_k(key).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)\n",
" v = self.w_v(value).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)\n",
" # 计算注意力\n",
" output, attn_weights = attention(q, k, v, mask, self.dropout)\n",
" # 拼接多头结果:[batch_size, n_heads, seq_len, d_k] → [batch_size, seq_len, d_model]\n",
" output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.n_heads * self.d_k)\n",
" # 输出线性变换\n",
" return self.w_o(output), attn_weights"
]
},
{
"cell_type": "markdown",
"id": "24c8f24b",
"metadata": {},
"source": [
"### 前馈网络与层归一化\n",
"\n",
"- 前馈网络FFN增强模型非线性能力对每个位置独立处理。\n",
"\n",
"- 层归一化 + 残差连接:稳定训练,缓解深层模型梯度问题。"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "61b8621f",
"metadata": {},
"outputs": [],
"source": [
"class PositionWiseFeedForward(nn.Module):\n",
" def __init__(self, d_model, d_ff, dropout=0.1):\n",
" super().__init__()\n",
" self.fc1 = nn.Linear(d_model, d_ff) # 升维\n",
" self.fc2 = nn.Linear(d_ff, d_model) # 降维\n",
" self.dropout = nn.Dropout(dropout)\n",
" self.activation = F.relu # 非线性激活\n",
" \n",
" def forward(self, x):\n",
" return self.fc2(self.dropout(self.activation(self.fc1(x))))\n",
"\n",
"class LayerNorm(nn.Module):\n",
" def __init__(self, features, eps=1e-6):\n",
" super().__init__()\n",
" self.gamma = nn.Parameter(torch.ones(features)) # 缩放参数\n",
" self.beta = nn.Parameter(torch.zeros(features)) # 偏移参数\n",
" self.eps = eps # 防止除零\n",
" \n",
" def forward(self, x):\n",
" mean = x.mean(-1, keepdim=True) # 最后一维求均值\n",
" std = x.std(-1, keepdim=True) # 最后一维求标准差\n",
" return self.gamma * (x - mean) / (std + self.eps) + self.beta"
]
},
{
"cell_type": "markdown",
"id": "b04f4044",
"metadata": {},
"source": [
"### Encoder 与 Decoder 层\n",
"\n",
"- Encoder 层:由 “多头自注意力 + 前馈网络” 组成,输出源语言编码。\n",
"\n",
"- Decoder 层:由 “掩码多头自注意力(遮蔽未来信息)+ 编码器 - 解码器注意力(关联源 - 目标语言)+ 前馈网络” 组成。"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "ddaddaba",
"metadata": {},
"outputs": [],
"source": [
"class EncoderLayer(nn.Module):\n",
" def __init__(self, d_model, n_heads, d_ff, dropout=0.1):\n",
" super().__init__()\n",
" self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) # 自注意力\n",
" self.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout) # 前馈网络\n",
" self.norm1 = LayerNorm(d_model) # 层归一化1\n",
" self.norm2 = LayerNorm(d_model) # 层归一化2\n",
" self.dropout1 = nn.Dropout(dropout) # dropout1\n",
" self.dropout2 = nn.Dropout(dropout) # dropout2\n",
" \n",
" def forward(self, x, mask):\n",
" # 自注意力 + 残差连接 + 层归一化\n",
" attn_output, _ = self.self_attn(x, x, x, mask)\n",
" x = self.norm1(x + self.dropout1(attn_output))\n",
" # 前馈网络 + 残差连接 + 层归一化\n",
" ff_output = self.feed_forward(x)\n",
" x = self.norm2(x + self.dropout2(ff_output))\n",
" return x\n",
"\n",
"class DecoderLayer(nn.Module):\n",
" def __init__(self, d_model, n_heads, d_ff, dropout=0.1):\n",
" super().__init__()\n",
" self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) # 掩码自注意力\n",
" self.cross_attn = MultiHeadAttention(d_model, n_heads, dropout) # 编码器-解码器注意力\n",
" self.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout) # 前馈网络\n",
" self.norm1 = LayerNorm(d_model) # 层归一化1\n",
" self.norm2 = LayerNorm(d_model) # 层归一化2\n",
" self.norm3 = LayerNorm(d_model) # 层归一化3\n",
" self.dropout1 = nn.Dropout(dropout) # dropout1\n",
" self.dropout2 = nn.Dropout(dropout) # dropout2\n",
" self.dropout3 = nn.Dropout(dropout) # dropout3\n",
" \n",
" def forward(self, x, enc_output, self_mask, cross_mask):\n",
" # 掩码自注意力(遮蔽未来信息)\n",
" attn_output, _ = self.self_attn(x, x, x, self_mask)\n",
" x = self.norm1(x + self.dropout1(attn_output))\n",
" # 编码器-解码器注意力(关联源语言和目标语言)\n",
" attn_output, _ = self.cross_attn(x, enc_output, enc_output, cross_mask)\n",
" x = self.norm2(x + self.dropout2(attn_output))\n",
" # 前馈网络\n",
" ff_output = self.feed_forward(x)\n",
" x = self.norm3(x + self.dropout3(ff_output))\n",
" return x"
]
},
{
"cell_type": "markdown",
"id": "4fd48e77",
"metadata": {},
"source": [
"### 组装成完整 Transformer 模型"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4c9c67ff",
"metadata": {},
"outputs": [],
"source": [
"class Transformer(nn.Module):\n",
" def __init__(self, src_vocab_size, tgt_vocab_size, d_model=512, n_layers=6, \n",
" n_heads=8, d_ff=2048, max_seq_len=5000, dropout=0.1):\n",
" super().__init__()\n",
" # 编码器\n",
" self.encoder_embedding = nn.Embedding(src_vocab_size, d_model) # 源语言Embedding\n",
" self.encoder_pos_encoding = PositionalEncoding(d_model, max_seq_len, dropout)\n",
" self.encoder_layers = nn.ModuleList([\n",
" EncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)\n",
" ])\n",
" # 解码器\n",
" self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model) # 目标语言Embedding\n",
" self.decoder_pos_encoding = PositionalEncoding(d_model, max_seq_len, dropout)\n",
" self.decoder_layers = nn.ModuleList([\n",
" DecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)\n",
" ])\n",
" # 输出层(映射到目标语言词汇表)\n",
" self.fc = nn.Linear(d_model, tgt_vocab_size)\n",
" self.d_model = d_model # 模型维度\n",
" \n",
" def generate_masks(self, src, tgt):\n",
" \"\"\"生成编码器和解码器掩码\"\"\"\n",
" batch_size, src_len = src.shape\n",
" batch_size, tgt_len = tgt.shape\n",
" \n",
" # 编码器掩码:遮蔽<pad>src中<pad>的位置设为0\n",
" src_mask = (src != en_vocab.token2idx['<pad>']).unsqueeze(1).unsqueeze(2) # [batch, 1, 1, src_len]\n",
" \n",
" # 解码器自注意力掩码:遮蔽<pad>和未来信息(下三角矩阵)\n",
" tgt_self_mask = (tgt != zh_vocab.token2idx['<pad>']).unsqueeze(1).unsqueeze(2) # [batch, 1, 1, tgt_len]\n",
" \n",
" # 确保下三角矩阵为布尔类型\n",
" tgt_tri_mask = torch.tril(torch.ones(tgt_len, tgt_len, device=tgt.device, dtype=torch.bool))\n",
" \n",
" # 确保两个掩码都是布尔类型再进行位运算\n",
" tgt_self_mask = tgt_self_mask & tgt_tri_mask # 结合两种掩码\n",
" \n",
" # 解码器交叉注意力掩码:与编码器掩码一致(确保对齐源语言)\n",
" tgt_cross_mask = src_mask\n",
" \n",
" return src_mask, tgt_self_mask, tgt_cross_mask\n",
" \n",
" def forward(self, src, tgt):\n",
" \"\"\"\n",
" src: 源语言序列(英文),形状[batch_size, src_len]\n",
" tgt: 目标语言序列(中文),形状[batch_size, tgt_len](训练时不含最后一个<eos>\n",
" \"\"\"\n",
" # 生成掩码\n",
" src_mask, tgt_self_mask, tgt_cross_mask = self.generate_masks(src, tgt)\n",
" \n",
" # 编码器前向\n",
" enc_emb = self.encoder_embedding(src) * math.sqrt(self.d_model) # Embedding缩放\n",
" enc_emb = self.encoder_pos_encoding(enc_emb) # 加位置编码\n",
" enc_output = enc_emb\n",
" for layer in self.encoder_layers:\n",
" enc_output = layer(enc_output, src_mask) \n",
" \n",
" # 解码器前向\n",
" dec_emb = self.decoder_embedding(tgt) * math.sqrt(self.d_model) # Embedding缩放\n",
" dec_emb = self.decoder_pos_encoding(dec_emb) # 加位置编码\n",
" dec_output = dec_emb\n",
" for layer in self.decoder_layers:\n",
" dec_output = layer(dec_output, enc_output, tgt_self_mask, tgt_cross_mask) # 经过所有解码器层\n",
" \n",
" # 输出层(映射到目标词汇表)\n",
" output = self.fc(dec_output)\n",
" return output"
]
},
{
"cell_type": "markdown",
"id": "f5062137",
"metadata": {},
"source": [
"## 模型训练与训练集保存"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "50d8c5e4",
"metadata": {},
"outputs": [],
"source": [
"# 超参数\n",
"d_model = 512 # 模型维度原论文512\n",
"n_layers = 6 # 编码器/解码器层数原论文6\n",
"n_heads = 8 # 注意力头数原论文8\n",
"d_ff = 2048 # 前馈网络隐藏层维度原论文2048\n",
"dropout = 0.1\n",
"epochs = 30 # 训练轮数\n",
"lr = 0.0001 # 学习率\n",
"\n",
"# 初始化模型\n",
"model = Transformer(\n",
" src_vocab_size=len(en_vocab),\n",
" tgt_vocab_size=len(zh_vocab),\n",
" d_model=d_model,\n",
" n_layers=n_layers,\n",
" n_heads=n_heads,\n",
" d_ff=d_ff,\n",
" dropout=dropout\n",
")\n",
"\n",
"# 损失函数(忽略<pad>的损失)\n",
"criterion = nn.CrossEntropyLoss(ignore_index=zh_vocab.token2idx['<pad>'])\n",
"\n",
"# 优化器Adam\n",
"optimizer = optim.Adam(model.parameters(), lr=lr, betas=(0.9, 0.98), eps=1e-9)"
]
},
{
"cell_type": "markdown",
"id": "8207b041",
"metadata": {},
"source": [
"准备完成之后开始训练我们自己的模型啦!"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "2626f91c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 1 平均损失: 6.5164\n",
"\n",
"Epoch 2 平均损失: 5.9108\n",
"\n",
"Epoch 3 平均损失: 5.5808\n",
"\n",
"Epoch 4 平均损失: 5.2947\n",
"\n",
"Epoch 5 平均损失: 5.0408\n",
"\n",
"Epoch 6 平均损失: 4.8072\n",
"\n",
"Epoch 7 平均损失: 4.5925\n",
"\n",
"Epoch 8 平均损失: 4.3832\n",
"\n",
"Epoch 9 平均损失: 4.1815\n",
"\n",
"Epoch 10 平均损失: 3.9823\n",
"\n",
"Epoch 11 平均损失: 3.7903\n",
"\n",
"Epoch 12 平均损失: 3.5957\n",
"\n",
"Epoch 13 平均损失: 3.4023\n",
"\n",
"Epoch 14 平均损失: 3.2126\n",
"\n",
"Epoch 15 平均损失: 3.0264\n",
"\n",
"Epoch 16 平均损失: 2.8420\n",
"\n",
"Epoch 17 平均损失: 2.6566\n",
"\n",
"Epoch 18 平均损失: 2.4752\n",
"\n",
"Epoch 19 平均损失: 2.2976\n",
"\n",
"Epoch 20 平均损失: 2.1292\n",
"\n",
"Epoch 21 平均损失: 1.9595\n",
"\n",
"Epoch 22 平均损失: 1.8008\n",
"\n",
"Epoch 23 平均损失: 1.6442\n",
"\n",
"Epoch 24 平均损失: 1.4992\n",
"\n",
"Epoch 25 平均损失: 1.3558\n",
"\n",
"Epoch 26 平均损失: 1.2265\n",
"\n",
"Epoch 27 平均损失: 1.1065\n",
"\n",
"Epoch 28 平均损失: 0.9931\n",
"\n",
"Epoch 29 平均损失: 0.8851\n",
"\n",
"Epoch 30 平均损失: 0.7962\n",
"\n",
"模型权重已保存至 'transformer_zh_en.pth'\n"
]
}
],
"source": [
"# 设备选择GPU优先\n",
"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
"model.to(device)\n",
"criterion.to(device)\n",
"\n",
"# 训练循环\n",
"for epoch in range(epochs):\n",
" model.train() # 训练模式\n",
" total_loss = 0.0\n",
" for batch_idx, (src, tgt) in enumerate(dataloader):\n",
" # 移动数据到设备\n",
" src = src.to(device) # [batch_size, src_len]\n",
" tgt = tgt.to(device) # [batch_size, tgt_len]\n",
" \n",
" # 解码器输入tgt[:, :-1](去除最后一个<eos>\n",
" # 解码器目标tgt[:, 1:](去除第一个<bos>\n",
" output = model(src, tgt[:, :-1]) # [batch_size, tgt_len-1, tgt_vocab_size]\n",
" \n",
" # 计算损失CrossEntropyLoss要求输入形状为[batch, vocab_size, seq_len]\n",
" loss = criterion(output.transpose(1, 2), tgt[:, 1:])\n",
" \n",
" # 反向传播\n",
" optimizer.zero_grad() # 清零梯度\n",
" loss.backward() # 计算梯度\n",
" optimizer.step() # 更新参数\n",
" \n",
" total_loss += loss.item()\n",
" \n",
" # 打印轮次平均损失\n",
" avg_loss = total_loss / len(dataloader)\n",
" print(f\"Epoch {epoch+1} 平均损失: {avg_loss:.4f}\\n\")\n",
"\n",
"# 保存模型权重\n",
"torch.save(model.state_dict(), 'transformer_zh_en.pth')\n",
"print(\"模型权重已保存至 'transformer_zh_en.pth'\")"
]
},
{
"cell_type": "markdown",
"id": "42d36bcc",
"metadata": {},
"source": [
"## 使用预训练集进行推导"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "29be2f75",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"===== 中英文翻译器 =====\n",
"提示:输入英文句子进行翻译,输入'q'退出\n",
"翻译结果: 巴黎-随着经济危机不断加深和蔓延,整个世界一直在寻找历史上的类似事件希望我们了解目前正在发生的情况。\n",
"翻译结果: 但如果这样的情况发生在危机前<unk>,那么有许多国家的人真正稳定自己的那么重要,那就是自己的自己的自己国家,就会有自己的自己的自己的吗?\n",
"程序已退出\n"
]
}
],
"source": [
"def preprocess_sentence(sentence, vocab, tokenize_fn):\n",
" \"\"\"预处理输入句子,转为模型可接受的格式\"\"\"\n",
" tokens = tokenize_fn(sentence) # 分词\n",
" tokens = ['<bos>'] + tokens + ['<eos>'] # 添加句首/句尾符号\n",
" indices = vocab.convert_tokens_to_ids(tokens) # 转为索引\n",
" tensor = torch.tensor(indices, dtype=torch.long).unsqueeze(0) # 添加batch维度\n",
" return tensor\n",
"\n",
"def translate_sentence(model, src_sentence, src_vocab, tgt_vocab, \n",
" src_tokenize_fn, tgt_tokenize_fn, device, max_len=50):\n",
" \"\"\"将英文句子翻译成中文\"\"\"\n",
" model.eval() # 评估模式关闭dropout等训练特有的层\n",
" with torch.no_grad(): # 关闭梯度计算,节省内存\n",
" # 预处理源语言句子\n",
" src_tensor = preprocess_sentence(src_sentence, src_vocab, src_tokenize_fn).to(device)\n",
" \n",
" # 编码器处理源语言\n",
" src_mask = (src_tensor != src_vocab.token2idx['<pad>']).unsqueeze(1).unsqueeze(2)\n",
" enc_emb = model.encoder_embedding(src_tensor) * math.sqrt(model.d_model)\n",
" enc_emb = model.encoder_pos_encoding(enc_emb)\n",
" enc_output = enc_emb\n",
" for layer in model.encoder_layers:\n",
" enc_output = layer(enc_output, src_mask)\n",
" \n",
" # 解码器自回归生成目标句子(从<bos>开始)\n",
" tgt_indices = [tgt_vocab.token2idx['<bos>']]\n",
" \n",
" for _ in range(max_len):\n",
" tgt_tensor = torch.tensor(tgt_indices, dtype=torch.long).unsqueeze(0).to(device)\n",
" \n",
" # 生成解码器掩码(遮蔽未来信息和<pad>\n",
" tgt_self_mask = (tgt_tensor != tgt_vocab.token2idx['<pad>']).unsqueeze(1).unsqueeze(2)\n",
" tgt_tri_mask = torch.tril(\n",
" torch.ones(tgt_tensor.size(1), tgt_tensor.size(1), device=device, dtype=torch.bool)\n",
" )\n",
" tgt_self_mask = tgt_self_mask & tgt_tri_mask\n",
" tgt_cross_mask = src_mask # 与编码器掩码一致\n",
" \n",
" # 解码器前向计算\n",
" dec_emb = model.decoder_embedding(tgt_tensor) * math.sqrt(model.d_model)\n",
" dec_emb = model.decoder_pos_encoding(dec_emb)\n",
" dec_output = dec_emb\n",
" for layer in model.decoder_layers:\n",
" dec_output = layer(dec_output, enc_output, tgt_self_mask, tgt_cross_mask)\n",
" output = model.fc(dec_output)\n",
"\n",
" # 取最后一个位置的预测结果\n",
" next_token_idx = output[:, -1, :].argmax(1).item()\n",
" tgt_indices.append(next_token_idx)\n",
" \n",
" # 遇到<eos>停止生成\n",
" if next_token_idx == tgt_vocab.token2idx['<eos>']:\n",
" break\n",
" \n",
" # 转换为中文句子(移除特殊符号)\n",
" tgt_tokens = tgt_vocab.convert_ids_to_tokens(tgt_indices)\n",
" translated_sentence = ''.join(tgt_tokens[1:-1]) # 跳过<bos>和<eos>\n",
" return translated_sentence\n",
"\n",
"# 加载模型(需与训练时的超参数一致)\n",
"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
"d_model = 512\n",
"n_layers = 6\n",
"n_heads = 8\n",
"d_ff = 2048\n",
"dropout = 0.1\n",
"\n",
"# 初始化模型并加载权重\n",
"model = Transformer(\n",
" src_vocab_size=len(en_vocab),\n",
" tgt_vocab_size=len(zh_vocab),\n",
" d_model=d_model,\n",
" n_layers=n_layers,\n",
" n_heads=n_heads,\n",
" d_ff=d_ff,\n",
" dropout=dropout\n",
").to(device)\n",
"model.load_state_dict(torch.load('transformer_zh_en.pth', map_location=device))\n",
"\n",
"# 交互翻译\n",
"print(\"===== 中英文翻译器 =====\")\n",
"print(\"提示:输入英文句子进行翻译,输入'q'退出\")\n",
"while True:\n",
" user_input = input(\"\\n请输入英文句子: \")\n",
" if user_input.lower() == 'q':\n",
" print(\"程序已退出\")\n",
" break\n",
" # 执行翻译\n",
" translation = translate_sentence(\n",
" model=model,\n",
" src_sentence=user_input,\n",
" src_vocab=en_vocab,\n",
" tgt_vocab=zh_vocab,\n",
" src_tokenize_fn=tokenize_en, # 英文分词函数\n",
" tgt_tokenize_fn=tokenize_zh, # 中文分词函数\n",
" device=device\n",
" )\n",
" print(f\"翻译结果: {translation}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "deeplearing",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.23"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,171 +0,0 @@
# 第四章课后习题
## 选择题
### 1. 一般认为,以下哪个模型是 LLM 的开端?(
A. BERT
B. GPT - 3
C. ChatGPT
D. LLaMA
答案B
### 2. LLM 具备的涌现能力类似于物理学中的哪种现象?(
A. 折射现象
B. 反射现象
C. 相变现象
D. 电磁感应现象
答案C
解析:可以类比到物理学中的相变现象,**涌现能力**的显现就像是模型性能随着规模增大而迅速提升,超过了随机水平,也就是我们常说的量变引起了质变
### 3. 上下文学习能力是由哪个模型首次引入的?(
A. BERT
B. GPT - 3
C. ChatGPT
D. T5
答案B
### 4. 以下哪种能力不是 LLM 区别于传统 PLM 的重要优势?(
A. 涌现能力
B. 上下文学习能力
C. 图像识别能力
D. 逐步推理能力
答案C
### 5. 训练 LLM 的预训练任务通常是( )。
A. 因果语言模型CLM
B. 掩码语言模型MLM
C. 序列到序列模型Seq2Seq
D. 卷积神经网络CNN
答案A
### 6. 以下哪个分布式训练框架使用面最广?(
A. Deepspeed
B. Megatron - LM
C. ColossalAI
D. TensorFlow
答案A
### 7. 在 SFT 阶段,指令数据集一般包含以下哪些键?(多选)(
A. instruction
B. input
C. output
D. label
答案ABC
### 8. RLHF 是指( )。
A. 随机梯度下降法
B. 人类反馈强化学习
C. 监督学习
D. 无监督学习
答案B
### 9. 以下哪个模型在中文环境上可能展现更优越的效果?(
A. GPT - 4
B. 文心一言
C. Google - Bard
D. Anthropic - Claude
答案B
### 10. LLM 的预训练数据处理一般不包括以下哪个流程?(
A. 文档准备
B. 语料过滤
C. 模型训练
D. 语料去重
答案C
## 简答题
1. **请简述大语言模型LLM的定义及与传统预训练语言模型PLM的核心差异。**
LLM 是一种相较传统语言模型参数量更多、在更大规模语料上进行预训练的语言模型。一般来说LLM 指包含数百亿(或更多)参数的语言模型,它们往往在数 T token 语料上通过多卡分布式集群进行预训练。与传统 PLM 的核心差异在于参数量更庞大、预训练语料更海量,从而展现出与传统预训练语言模型截然不同的能力,如具备涌现能力等。
2. **列举 LLM 的四种核心能力,并简要说明每种能力的含义。**
LLM 的四种核心能力分别为:
**1.** 涌现能力:同样的模型架构与预训练任务下,某些能力在小型模型中不明显,但在大型模型中特别突出,表现为模型性能随规模增大迅速提升,超过随机水平。
**2.**上下文学习:允许语言模型在提供自然语言指令或多个任务示例的情况下,通过理解上下文并生成相应输出的方式来执行任务,无需额外的训练或参数更新。
**3.**指令遵循:经过指令微调的 LLM 能够理解并遵循未见过的指令,并根据任务指令执行任务,无需事先见过具体示例,展示了强大的泛化能力。
**4.**逐步推理LLM 通过采用思维链推理策略,利用包含中间推理步骤的提示机制来解决涉及多个推理步骤的复杂任务,得出最终答案。
3. **训练一个完整的 LLM 需要经过哪三个阶段?并简要说明每个阶段的主要任务。**
答:训练完整的 LLM 需要经过 Pretrain、SFT 和 RLHF 三个阶段。
**1.** Pretrain预训练使用海量无监督文本对随机初始化的模型参数进行训练目前主流的 LLM 几乎都采用 Decoder - Only 的类 GPT 架构预训练任务为因果语言模型CLM此阶段核心在于庞大的参数量和海量的预训练语料。
**2.** SFT有监督微调通过指令微调的方式训练模型的 “通用指令遵循能力”,输入是各种类型的用户指令,让模型拟合希望的回复,使模型能够理解并回复用户的指令。
**3.** RLHF人类反馈强化学习引入强化学习技术通过实时的人类反馈令 LLM 能够给出更令人类满意的回复,让 LLM 和人类价值观对齐,达到安全、有用、无害的标准。
4. **预训练 LLM 时,数据并行和模型并行的核心思路分别是什么?**
答:数据并行的核心思路是,虽然训练模型的尺寸可以被单个 GPU 内存容纳,但增大训练的 batch_size 会增大显存开销,让模型实例在不同 GPU 和不同批数据上运行,每一次前向传递完成之后,收集所有实例的梯度并进行梯度更新。更新模型参数之后,再传递到所有实例当中,使得每张 GPU 上的模型参数保持一致,此时,训练的总批次大小就等于每张卡上的批次大小之和。
模型并行的核心思路是,当 LLM 扩大到上百亿参数,单张 GPU 内存无法存放完整的模型参数时,将模型拆分到多个 GPU 上,每个 GPU 上存放不同的层或不同的部分。
5. **在 SFT 阶段,如何构造多轮对话样本?哪种方式最合理,为什么?**
答:在 SFT 阶段,构造多轮对话样本一般有三种方式:
**1.** 直接将最后一次模型回复作为输出,前面所有历史对话作为输入,直接拟合最后一次回复。
**2.** 将 N 轮对话构造成 N 个样本。
**3.** 直接要求模型预测每一轮对话的输出。其中,第三种方式最合理。因为 LLM 本质还是进行的 CLM 任务,进行单向注意力计算,在预测时会从左到右依次进行拟合,前轮的输出预测不会影响后轮的预测,而第一种方式会丢失大量中间信息,第二种方式造成了大量重复计算。

View File

@@ -1,200 +0,0 @@
# 选择题
## 1. 在`ModelConfig`类中,`n_heads`参数表示什么?
A. 键值头的数量
B. 注意力机制的头数
C. Transformer的层数
D. 隐藏层维度
**答案**B
## 2. `RMSNorm`类中的`self.weight`参数的作用是什么?
A. 防止除以零
B. 可学习的缩放参数
C. 输入向量的维度数量
D. 归一化层的eps
**答案**B
**解析**:在`RMSNorm`的数学公式
$ \text{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{n}\sum_{i=1}^{n}x_i^2 + \epsilon}} \cdot \gamma $
中,$\gamma$ 是可学习的缩放参数,对应代码中的 `self.weight`。防止除以零的是`self.eps`,输入向量的维度数量在计算中使用,不是`self.weight`的作用归一化层的eps是`self.eps`
## 3. 在`repeat_kv`函数中,当`n_rep`为1时会发生什么
A. 对张量进行扩展和重塑操作
B. 直接返回原始张量
C. 抛出异常
D. 随机返回一个张量
**答案**B
**解析**:在`repeat_kv`函数中,会检查重复次数 `n_rep` 是否为1如果是1则说明不需要对键和值进行重复直接返回原始张量。
## 4. `precompute_freqs_cis`函数返回的两个矩阵分别表示什么?
A. 频率序列和时间序列
B. 旋转嵌入的实部和虚部
C. 输入张量的实部和虚部
D. 键和值的扩展矩阵
**答案**B
**解析**`precompute_freqs_cis`函数最终返回两个矩阵 `freqs_cos``freqs_sin`,分别表示旋转嵌入的实部和虚部,用于后续的旋转嵌入计算。
## 5. 在`Attention`类中,`self.flash`属性的作用是什么?
A. 控制是否使用Dropout
B. 控制是否使用Flash Attention
C. 控制是否使用旋转嵌入
D. 控制是否使用Grouped-Query Attention
**答案**B
**解析**:在`Attention`类中,`self.flash`属性用于检查是否使用Flash Attention需要PyTorch >= 2.0如果支持则使用Flash Attention进行计算否则使用手动实现的注意力机制。
## 6. 在`MLP`类中,如果没有指定`hidden_dim`,会如何处理?
A. 直接使用默认值0
B. 设置为输入维度的4倍然后减少到2/3最后确保是 `multiple_of` 的倍数
C. 设置为输入维度的2倍
D. 随机生成一个值
**答案**B
**解析**:在`MLP`类中,如果没有指定 `hidden_dim`会将其设置为输入维度的4倍然后减少到2/3最后确保它是 `multiple_of` 的倍数。
## 7. 在`DecoderLayer`类中,`forward`方法的主要步骤不包括以下哪一项?
A. 输入经过注意力归一化层
B. 输入经过前馈神经网络归一化层
C. 输入经过词嵌入层
D. 注意力计算结果与输入相加
**答案**C
**解析**:在`DecoderLayer`类的`forward`方法中,输入会经过注意力归一化层、进行注意力计算并与输入相加,然后经过前馈神经网络归一化层和前馈神经网络计算。词嵌入层在 `Transformer` 类的`forward`方法中处理,不在 `DecoderLayer``forward`方法中。
## 8. 在训练Tokenizer时我们选择使用哪种算法
A. Word-based Tokenizer
B. Character-based Tokenizer
C. Byte Pair Encoding (BPE)
D. WordPiece
**答案**C
**解析**文档中明确提到选择使用BPE算法来训练一个Subword Tokenizer因为BPE是一种简单而有效的分词方法能够处理未登录词和罕见词同时保持较小的词典大小。
## 9. Word-based Tokenizer的主要缺点不包括以下哪一项
A. 无法处理未登录词
B. 对复合词处理不够精细
C. 计算复杂度高
D. 处理不同语言时会遇到挑战
**答案**C
**解析**Word-based Tokenizer的主要缺点包括无法处理未登录词、对复合词和缩略词处理不够精细以及处理不同语言时会遇到挑战而计算复杂度高并不是其主要缺点它相对简单直观易于实现。
## 10. 在`Transformer`类的`generate`方法中,当 `temperature` 为 0.0 时会如何选择下一个token
A. 随机选择一个token
B. 选择最有可能的索引
C. 选择概率最小的索引
D. 选择中间概率的索引
**答案**B
**解析**:在`Transformer`类的`generate`方法中,当 `temperature` 为 0.0 时,会使用 `torch.topk` 函数选择最有可能的索引作为下一个token。
### 简答题
## 1. 简述`RMSNorm`的作用及数学原理。
**答**`RMSNorm`的作用是通过确保权重的规模不会变得过大或过小来稳定学习过程,在具有许多层的深度学习模型中特别有用。其数学原理用公式表示为
$$
\text{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{n}\sum_{i=1}^{n}x_i^2 + \epsilon}} \cdot \gamma
$$
其中 $x_i$ 是输入向量的第 $i$ 个元素,$\gamma$ 是可学习的缩放参数(对应代码中的 `self.weight`$n$ 是输入向量的维度数量,$\epsilon$ 是一个小常数,用于数值稳定性(以避免除以零的情况)。
## 2. 说明`Attention`类中`forward`方法的主要步骤。
**答**
1. 获取输入张量的批次大小和序列长度。
2. 计算查询Q、键K、值V并调整形状以适应头的维度。
3. 应用旋转位置嵌入RoPE到查询和键。
4. 对键和值进行扩展以适应重复次数。
5. 将头作为批次维度处理。
6. 根据是否支持Flash Attention选择相应的实现方式计算注意力。
7. 恢复时间维度并合并头。
8. 最终投影回残差流并应用dropout。
**解析**`Attention`类是LLaMA2模型中的关键部分`forward`方法定义了注意力机制的具体计算流程,理解这些步骤有助于深入理解模型的工作原理。
## 3. 比较不同类型TokenizerWord-based、Character-based、Subword的优缺点。
**答**
1. **Word-based Tokenizer**
- **优点**:简单直观,易于实现,与人类对语言的直觉相符。
- **缺点**无法处理未登录词OOV和罕见词对复合词和缩略词的处理不够精细处理不同语言时会遇到挑战。
2. **Character-based Tokenizer**
- **优点**:能非常精细地处理文本,适用于处理拼写错误、未登录词或新词,可捕捉细微的语言特征,适用于特定的生成式任务或处理大量未登录词的任务,能处理任何语言和字符集,具有极大的灵活性。
- **缺点**会导致token序列变得非常长增加模型的计算复杂度和训练时间字符级的分割可能会丢失一些词级别的语义信息使得模型难以理解上下文。
3. **Subword Tokenizer**
- **优点**:介于词和字符之间,能够更好地平衡分词的细粒度和处理未登录词的能力,能处理未知词,又能保持一定的语义信息。
- **缺点**不同的子词分词方法如BPE、WordPiece、Unigram有各自的实现复杂度和适用场景。
**解析**不同类型的Tokenizer适用于不同的自然语言处理任务了解它们的优缺点有助于根据具体任务选择合适的Tokenizer。
## 4. 解释`precompute_freqs_cis`函数的作用及实现步骤。
**答**`precompute_freqs_cis`函数的作用是构造获得旋转嵌入的实部和虚部。实现步骤如下:
1. 计算频率序列:使用 `torch.arange(0, dim, 2)[: (dim // 2)].float()` 生成一个从0开始步长为2的序列长度为`dim`的一半,每个元素除以`dim`后取`theta`的倒数,得到频率序列 `freqs`
2. 生成时间序列:使用 `torch.arange(end, device=freqs.device)` 生成一个从`0``end`的序列,长度为`end``end`通常是序列的最大长度。
3. 计算频率的外积:使用 `torch.outer(t, freqs).float()` 计算时间序列 `t` 和频率序列 `freqs` 的外积,得到一个二维矩阵 `freqs`
4. 计算实部和虚部:使用 `torch.cos(freqs)` 计算频率矩阵 `freqs` 的余弦值,得到旋转嵌入的实部;使用 `torch.sin(freqs)` 计算频率矩阵 `freqs` 的正弦值,得到旋转嵌入的虚部。
**解析**旋转嵌入是LLaMA2模型中的重要组件`precompute_freqs_cis`函数为旋转嵌入的计算提供了必要的实部和虚部,理解其作用和实现步骤有助于掌握旋转嵌入的原理。
## 5. 简述`Transformer`类中`generate`方法的主要逻辑。
**答**`generate`方法的主要逻辑是给定输入序列 `idx`通过多次生成新token来完成序列。具体步骤如下
1. 检查序列上下文长度,如果过长则截断到最大长度。
2. 进行前向传播获取序列中最后一个位置的logits。
3. 根据 `temperature` 的值选择不同的采样方式:
-`temperature` 为0.0时,选择最有可能的索引。
-`temperature` 不为0.0时缩放logits并应用softmax根据`top_k`的值进行截断,然后使用 `torch.multinomial` 进行采样。
4. 如果采样的索引等于 `stop_id`,则停止生成。
5. 将采样的索引添加到序列中并继续生成,直到达到 `max_new_tokens` 的数量。
6. 最后返回生成的token。

View File

@@ -1,157 +0,0 @@
# Chapter6
## 选择题
### 1. 在LoRA微调中对于预训练的权重参数矩阵 $ W_0 \in R^{d \times k} $ ,其更新表示为 $ W_0 + {\Delta}W = W_0 + BA $ ,其中 $ B \in R^{d \times r} $ $ A \in R^{r \times k} $,在训练过程中( )。
A. $W_0$、$A$、$B$都更新
B. $W_0$更新,$A$、$B$冻结
C. $W_0$冻结,$A$、$B$更新
D. $W_0$、$A$、$B$都冻结
答案C
### 2. 在Transformer结构中LoRA技术主要应用在注意力模块的四个权重矩阵消融实验发现同时调整哪两个矩阵会产生最佳结果 )。
A. $W_q$和$W_k$
B. $W_q$和$W_v$
C. $W_k$和$W_v$
D. $W_v$和$W_0$
答案B
### 3. 以下哪种高效微调方法不存在推理延迟问题( )。
A. Adapt Tuning
B. Prefix Tuning
C. LoRA
D. Ptuning
答案C
### 4. 在SFT阶段模型使用 )进行训练。
A. 海量无监督文本
B. 构建成对的指令对数据
C. 单一的文本数据
D. 随机生成的数据
答案B
### 5. 强化学习中,智能体通过不断与环境交互来优化策略,其交互步骤顺序正确的是( )。
### ①观察状态 ②选择动作 ③执行动作 ④接收奖励和新状态 ⑤更新策略
A. ①②③④⑤
B. ②①③④⑤
C. ①③②④⑤
D. ②③①④⑤
答案A
### 6. 在构建奖励模型的数据集时,不需要做的步骤是( )。
A. 收集初始回答
B. 自动生成标注
C. 人工标注与评估
D. 数据格式化与整理
答案B
### 7. 在LoRA微调中可训练参数个数 $ \Theta = 2 \times L_{LoRA} \times d_{model} \times r $ ,其中 $r$ 一般取( )。
A. 1、2、3
B. 4、8、16
C. 20、30、40
D. 50、100、200
答案B
### 8. Adapt Tuning方法在微调时 )。
A. 冻结原参数仅更新Adapter层
B. 更新全部参数
C. 冻结Adapter层更新原参数
D. 部分更新原参数和Adapter层
答案A
### 9. 在SFT过程中对于User的文本在targets中对应的文本内容使用 )进行遮蔽。
A. 0
B. 1
C. -100
D. 任意值
答案C
### 10. 以下关于强化学习目标的说法,正确的是( )。
A. 最小化总累计奖励
B. 使智能体随机选择动作
C. 通过在给定环境中反复试探和学习,使得智能体能够选择一系列动作从而最大化其总累计奖励
D. 只考虑短期奖励,不考虑长期奖励
答案C
## 简答题
1. **请简述LoRA微调的原理。**
LoRA假设权重更新的过程中有较低的本征秩对于预训练的权重参数矩阵 $ W_0 \in R^{d \times k} $ ,使用低秩分解来表示其更新,即 $ W_0 + {\Delta}W = W_0 + BA $ ,其中 $ B \in R^{d \times r} $ $ A \in R^{r \times k} $。在训练过程中,$W_0$ 冻结不更新,$A$、$B$ 包含可训练参数。其前向传递函数为 $ h = W_0 x + \Delta W x = W_0 x + B A x $ ,开始训练时,对 $A$ 使用随机高斯初始化,对 $B$ 使用零初始化然后使用Adam进行优化。在Transformer结构中LoRA技术主要应用在注意力模块的四个权重矩阵 $W_q$ 、 $W_k$ 、 $W_v$ 、 $W_0$ 而冻结MLP的权重矩阵消融实验发现同时调整 $W_q$ 和 $W_v$ 会产生最佳结果。
2. **对比模型预训练Pretrain和有监督微调SFT的核心差异。**
Pretrain和SFT均使用CLM建模其核心差异在于训练数据和loss计算方式。在Pretrain阶段会对海量无监督文本进行自监督建模来学习文本语义规则和文本中的世界知识对全部text进行loss计算要求模型对整个文本实现建模预测而在SFT阶段一般通过对Pretrain好的模型进行指令微调即训练模型根据用户指令完成对应任务从而使模型能够遵循用户指令根据用户指令进行规划、行动和输出使用构建成对的指令对数据仅对输出进行loss计算不计算指令部分的loss。
3. **简述强化学习的基本原理和目标。**
答:强化学习的基本原理涉及状态、动作、奖励、策略、价值函数和模型等元素。状态是系统在某一时刻的具体状况;动作是智能体在给定状态下可执行的操作;奖励是智能体执行动作后获得的反馈;策略是指导智能体选择动作的规则;价值函数是评估策略的工具,预测从当前状态出发长期能获得的总奖励;模型可帮助智能体预见动作结果。智能体通过观察状态、选择动作、执行动作、接收奖励和新状态、更新策略这几个步骤与环境进行交互,不断优化策略。其目标是通过在给定环境中反复试探和学习,使得智能体能够选择一系列动作从而最大化其总累计奖励,在数学上表示为训练一个策略$\pi$,使得在所有状态$s$下,智能体选择的动作能够使得回报$R(\tau)$的期望值最大化,通过梯度上升的方法不断更新策略参数$\theta$。
4. **请说明构建奖励模型数据集的步骤。**
构建奖励模型数据集可按以下步骤进行首先收集初始回答从一个已经过基本微调的“大模型”中为一组精心设计的提示生成多条回答然后进行人工标注与评估邀请专业标注人员或众包标注者对每条回答的质量进行评价基于一系列预先设计的评价标准如回答的准确性、完整性、上下文相关性、语言流畅度以及是否遵循道德与安全准则对不同回答进行比较与排序最后进行数据格式化与整理将标注完成的数据进行整理与格式化采用JSON、CSV等便于计算机处理的结构化数据格式明确标识每个问题、其对应的多个回答以及人类标注者对这些回答的选择如标记为 "chosen" 的最佳答案与 "rejected" 的较差答案。
5. **简述使用peft实现LoRA微调的步骤及注意事项。**
使用peft实现LoRA微调首先通过`get_peft_model`获取一个LoRA模型此处的`get_peft_model`底层操作有具体实现。然后使用transformers提供的Trainer进行训练示例代码如下
```python
trainer = Trainer(
model=model,
args=training_args,
train_dataset= IterableWrapper(train_dataset),
tokenizer=tokenizer
)
trainer.train()
```
如果应用在DPO、KTO上相同的加入LoRA参数并通过`get_peft_model`获取LoRA模型其他不需要修改。注意事项是LoRA微调能够大幅度降低显卡占用且在下游任务适配上能够取得较好的效果但如果是需要学习对应知识的任务LoRA由于只调整低秩矩阵难以实现知识的注入一般效果不佳因此不推荐使用LoRA进行模型预训练或后训练。

View File

@@ -1,167 +0,0 @@
# Chapter7
## 选择题
### 1. 以下哪个评测集用于评测模型在复杂工具使用任务中的表现?
A. MMLU
B. BFCL V2
C. GSM8K
D. ARC Challenge
答案B
### 2. Open LLM Leaderboard 是由哪个组织提供的开放式榜单?
A. Hugging Face
B. lmsys
C. 上海人工智能实验室
D. 南京大学
答案A
### 3. RAG 技术的核心原理是?
A. 单纯的内容生成
B. 单纯的信息检索
C. 将“检索”与“生成”结合
D. 利用历史数据生成内容
答案C
### 4. Tiny - RAG 不包含以下哪个模块?
A. 向量化模块
B. 图像识别模块
C. 文档加载和切分模块
D. 大模型模块
答案B
### 5. 以下哪个是特定领域的评测榜单?
A. Open LLM Leaderboard
B. Lmsys Chatbot Arena Leaderboard
C. 金融榜
D. OpenCompass
答案C
### 6. LLM Agent 能够不具备以下哪种能力?
A. 理解目标
B. 自主规划
C. 情感感知
D. 工具使用
答案C
### 7. 任务导向型 Agent 的特点不包括?
A. 专注于完成特定领域的任务
B. 有预设的流程
C. 能够自主学习新知识
D. 有可调用的特定工具集
答案C
### 8. 在 RAG 的流程中,“以检索到的上下文为条件,生成问题的回答”属于哪个步骤?
A. 索引
B. 检索
C. 生成
D. 向量化
答案C
### 9. 以下哪个评测集用于测试模型在更复杂的数学问题上的表现,包括代数和几何?
A. GSM8K
B. MATH
C. GPQA
D. HellaSwag
答案B
### 10. 多 Agent 系统的特点是?
A. 由多个具有不同角色或能力的 Agent 协同工作
B. 只能完成简单任务
C. 没有通信机制
D. 不具备规划能力
答案A
## 简答题
1. **请说明 LLM Agent 的几种类型及其特点。**
LLM Agent 大致可分为以下几类:
- 任务导向型 Agent专注于完成特定领域的、定义明确的任务通常有预设的流程和可调用的特定工具集LLM 主要负责理解用户意图、填充任务槽位、生成回应或调用合适的工具。
- 规划与推理型 Agent强调自主分解复杂任务、制定多步计划并根据环境反馈进行调整的能力常采用特定的思维框架如 ReActChain - of - Thought 等提示工程技术也是其推理的基础。
- 多 Agent 系统:由多个具有不同角色或能力的 Agent 协同工作共同完成一个更宏大的目标Agent 之间可以进行通信、协作、辩论甚至竞争。
- 探索与学习型 Agent这类 Agent 不仅执行任务,还能在与环境的交互中主动学习新知识、新技能或优化自身策略,可能包含更复杂的记忆和反思机制。
2. **搭建 Tiny - RAG 框架需要经过哪些步骤?**
搭建 Tiny - RAG 框架需要经过以下步骤:
- Step 1: RAG 流程介绍,了解 RAG 的基本结构,包括向量化模块、文档加载和切分模块、数据库、检索模块、大模型模块,以及其索引、检索、生成的流程。
- Step 2: 向量化,实现一个向量化的类,设置 `BaseEmbeddings` 基类,方便代码扩展,再继承该基类实现具体的嵌入类,如 `OpenAIEmbedding`
- Step 3: 文档加载和切分,实现一个文档加载和切分的类,根据文件扩展名选择读取方法,并按最大 Token 长度以句子为单位切分文档,保证片段之间有重叠内容。
- Step 4: 数据库与向量检索,设计一个向量数据库来存放文档片段和对应的向量表示,以及设计一个检索模块用于根据 Query 检索相关文档片段。
- Step 5: 大模型模块,实现一个基类 `BaseModel`,方便扩展其他模型,再实现具体的大模型类,如 `OpenAIChat`,并设计专用于 RAG 的大模型提示词。
- Step 6: Tiny - RAG Demo展示如何使用上述模块完成一个简单的 RAG 任务,可以选择保存数据库或从本地加载已处理好的数据库。
3. **简述 Agent 类在构造 Tiny - Agent 中的工作流程。**
Agent 类在构造 Tiny - Agent 中的工作流程如下:
1. 接收用户输入,将用户输入添加到消息列表中。
2. 调用大模型(如 Qwen并告知其可用的工具及其 Schema。
3. 检查模型是否调用了工具如果模型决定调用工具Agent 会解析请求,执行相应的 Python 函数,并将工具调用结果添加到消息列表中。
4. Agent 将工具的执行结果返回给模型。
5. 模型根据工具结果生成最终回复Agent 将最终回复添加到消息列表中并返回给用户。