Compare commits

43 Commits
PDF ... main

Author SHA1 Message Date
KMnO4-zx
0d2471d3ee docs(chapter7): update content 2025-06-28 10:43:44 +08:00
不要葱姜蒜
a5e7622e1f Merge pull request #55 from gift-is-coding/patch-1
Update 前言.md
2025-06-27 12:28:24 +08:00
Tiff Wu
ebc0f077d3 Update 前言.md
Typo of Language
2025-06-27 11:24:30 +07:00
KMnO4-zx
643226e252 docs(chapter5): 更新tokenizer训练数据加载说明
使用出门问问序列猴子开源数据集替代wikitext数据集,并添加JSONL文件读取方法
2025-06-26 11:02:10 +08:00
KMnO4-zx
d8150c8e7b docs: 更新项目star历史图表并移除多余的语言切换链接
- 用新的star历史图表(2025624)替换旧的(2025612)
- 从docs/README.md中移除中英文切换链接
2025-06-24 16:21:03 +08:00
Logan Zou
edb73c7aeb Update 第二章 Transformer架构.md 2025-06-24 10:54:02 +08:00
Logan Zou
71f8d48290 Update 第二章 Transformer架构.md 2025-06-23 11:09:04 +08:00
Logan Zou
98a122e323 Update 第二章 Transformer架构.md
add pre-norm
2025-06-23 11:02:23 +08:00
Logan Zou
5f2ccc44bf Update 第二章 Transformer架构.md
fix arg bug
2025-06-23 10:53:45 +08:00
Logan Zou
3950b06a5f Update transformer.py
fix arg bug
2025-06-23 10:53:25 +08:00
Logan Zou
185a212acd Update 第二章 Transformer架构.md 2025-06-23 10:50:16 +08:00
Logan Zou
bd3fb6cf48 Update transformer.py
fix dim bug
2025-06-23 10:48:56 +08:00
KMnO4-zx
3b24a9fd1e docs: 更新README和文档内容,添加模型下载链接
- 在README中新增模型下载章节,包含ModelScope链接
- 更新模型示例代码中的默认检查点路径
- 优化训练脚本的注释和参数说明
- 添加中文文档的模型下载和体验地址
- 修复文档中的训练时长和设备信息
2025-06-22 10:05:36 +08:00
KMnO4-zx
b421894dcc docs(chapter3): 修正T5模型中RMSNorm公式的描述和参数说明 2025-06-21 13:36:42 +08:00
KMnO4-zx
fc19776feb docs(chapter4): 修正章节编号错误 2025-06-21 13:35:09 +08:00
KMnO4-zx
30f3f01619 refactor(dataset): 使用tokenizer动态生成a_sequence并替换硬编码值
fix(ddp_sft_full): 修正参数默认值和优化器类型
docs(ddp_pretrain): 添加详细注释和优化参数描述
2025-06-21 11:39:40 +08:00
KMnO4-zx
21bc50882a docs: 更新README文件中的徽章样式和链接
- 将徽章样式从for-the-badge改为flat
- 添加GitHub Project和SwanLab项目链接
- 更新第六章状态为进行中
2025-06-21 11:37:30 +08:00
KMnO4-zx
4fcb1924dd docs: 更新第六章状态为进行中 2025-06-20 23:02:40 +08:00
KMnO4-zx
fe07d0ede1 feat(RAG): 更新RAG模块代码和文档
refactor: 简化Embeddings和LLM类实现,移除不必要依赖
docs: 更新文档内容,添加硅基流动API使用说明
chore: 更新requirements.txt依赖版本
2025-06-20 22:53:23 +08:00
KMnO4-zx
0eea57b11f docs: 修复章节2中Embedding层的拼写错误 2025-06-20 15:04:23 +08:00
KMnO4-zx
dcdf98df22 docs(chapter7): 修正图7.10的标题描述 2025-06-20 12:17:39 +08:00
KMnO4-zx
2b9b53a383 docs: 调整文档中图片位置并删除重复内容 2025-06-20 12:15:19 +08:00
KMnO4-zx
28636a0f9b feat(Agent): 新增维基百科搜索和温度查询工具并实现web界面
- 添加search_wikipedia和get_current_temperature工具函数
- 实现基于Streamlit的web交互界面
- 更新requirements.txt添加相关依赖
- 修复PROMPT_TEMPLATE变量名拼写错误
- 移除不再使用的工具函数
- 添加web界面截图到文档
2025-06-20 12:14:19 +08:00
不要葱姜蒜
cdf10fea16 Merge pull request #43 from MengYue-MK2000/main
更新Windows下载Datasets的方法
2025-06-20 00:40:12 +08:00
MengYue-MK2000
b1ac936d36 created windows_download_dataset.sh, deleted original changes in download_dataset.sh 2025-06-19 17:52:24 +08:00
Reagan Zhang
18ff1a73a8 Update download_dataset.sh
Update Mac installation for modelscope
2025-06-19 16:09:59 +08:00
Reagan Zhang
56fb0c34d4 Update download_dataset.sh 2025-06-19 16:06:05 +08:00
KMnO4-zx
2601c45444 docs(chapter5): 修复LLaMA2 Attention结构图中图片链接格式 2025-06-18 16:33:43 +08:00
KMnO4-zx
2fca30c239 docs(chapter5): 更新LLaMA2注意力机制图示 2025-06-18 16:32:07 +08:00
KMnO4-zx
ce535629ca docs(chapter5): 更新模型文档并添加数据处理脚本
- 更新LLaMA2模型文档,修正图片引用和编号
- 添加Attention结构示意图
- 新增数据处理脚本download_dataset.sh和deal_dataset.py
- 优化文档中的代码示例说明
2025-06-18 16:26:33 +08:00
KMnO4-zx
ada2e0c44f fix(download.py): 修复解压命令未指定目标目录的问题 2025-06-18 12:34:52 +08:00
KMnO4-zx
5d25cb0992 docs: 更新README中图片路径引用 2025-06-17 17:18:34 +08:00
KMnO4-zx
20a4bde837 docs(chapter1): 在NLP基础概述中添加词汇表说明 2025-06-17 17:10:45 +08:00
KMnO4-zx
1f46fc1dd5 docs: 更新文档中的图片链接为绝对路径
将所有文档中的相对图片路径替换为完整的GitHub raw链接,确保图片在文档中能够正确显示
2025-06-17 17:07:09 +08:00
KMnO4-zx
6dd4815b1e docs(chapter4): 修正大语言模型章节中的公式格式和空格
统一公式前后空格格式,提高文档可读性
2025-06-17 12:21:31 +08:00
KMnO4-zx
d49819cd9d docs(chapter4): 修正奖励模型公式中的数学符号和格式错误
修复公式中的数学符号显示问题,包括 KL 散度计算和奖励函数中的点乘符号
2025-06-17 12:16:06 +08:00
KMnO4-zx
08ee8ef753 docs(chapter2): 修正自注意力机制文档中的错别字 2025-06-15 09:45:06 +08:00
KMnO4-zx
a866753911 docs: 修正文档链接路径
更新README.md和docs/README.md中的前言链接路径,从`./docs/README.md`改为`./前言.md`以保持一致性
2025-06-13 21:49:24 +08:00
KMnO4-zx
5e8f26544a docs: 更新star-history 2025-06-12 16:58:18 +08:00
KMnO4-zx
5713a54960 fix(docs): 修正文档中torch拼写错误 2025-06-12 09:10:18 +08:00
KMnO4-zx
6a47afc997 fix: 修正 5.1.2 中输出张量形状的错误 2025-06-12 09:08:38 +08:00
KMnO4-zx
74908262f1 docs: 更新README中的PDF下载链接格式 2025-06-10 16:47:56 +08:00
KMnO4-zx
1516bb487d docs: 添加在线阅读链接和PDF下载说明 2025-06-10 16:40:20 +08:00
38 changed files with 1394 additions and 552 deletions

162
.gitignore vendored Normal file
View File

@@ -0,0 +1,162 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.idea/
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
.history
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.DS_Store
.obsidian

View File

@@ -4,9 +4,11 @@
</div> </div>
<div align="center"> <div align="center">
<img src="https://img.shields.io/github/stars/datawhalechina/happy-llm?style=for-the-badge&logo=github" alt="GitHub stars"/> <img src="https://img.shields.io/github/stars/datawhalechina/happy-llm?style=flat&logo=github" alt="GitHub stars"/>
<img src="https://img.shields.io/github/forks/datawhalechina/happy-llm?style=for-the-badge&logo=github" alt="GitHub forks"/> <img src="https://img.shields.io/github/forks/datawhalechina/happy-llm?style=flat&logo=github" alt="GitHub forks"/>
<img src="https://img.shields.io/badge/language-Chinese-brightgreen?style=for-the-badge" alt="Language"/> <img src="https://img.shields.io/badge/language-Chinese-brightgreen?style=flat" alt="Language"/>
<a href="https://github.com/datawhalechina/happy-llm"><img src="https://img.shields.io/badge/GitHub-Project-blue?style=flat&logo=github" alt="GitHub Project"></a>
<a href="https://swanlab.cn/@kmno4/Happy-LLM/overview"><img src="https://raw.githubusercontent.com/SwanHubX/assets/main/badge1.svg" alt="SwanLab"></a>
</div> </div>
<div align="center"> <div align="center">
@@ -16,6 +18,7 @@
</div> </div>
<div align="center"> <div align="center">
<p><a href="https://datawhalechina.github.io/happy-llm/">📚 在线阅读地址</a></p>
<h3>📚 从零开始的大语言模型原理与实践教程</h3> <h3>📚 从零开始的大语言模型原理与实践教程</h3>
<p><em>深入理解 LLM 核心原理,动手实现你的第一个大模型</em></p> <p><em>深入理解 LLM 核心原理,动手实现你的第一个大模型</em></p>
</div> </div>
@@ -42,15 +45,32 @@
| 章节 | 关键内容 | 状态 | | 章节 | 关键内容 | 状态 |
| --- | --- | --- | | --- | --- | --- |
| [前言](./docs/README.md) | 本项目的缘起、背景及读者建议 | ✅ | | [前言](./docs/前言.md) | 本项目的缘起、背景及读者建议 | ✅ |
| [第一章 NLP 基础概念](./docs/chapter1/第一章%20NLP基础概念.md) | 什么是 NLP、发展历程、任务分类、文本表示演进 | ✅ | | [第一章 NLP 基础概念](./docs/chapter1/第一章%20NLP基础概念.md) | 什么是 NLP、发展历程、任务分类、文本表示演进 | ✅ |
| [第二章 Transformer 架构](./docs/chapter2/第二章%20Transformer架构.md) | 注意力机制、Encoder-Decoder、手把手搭建 Transformer | ✅ | | [第二章 Transformer 架构](./docs/chapter2/第二章%20Transformer架构.md) | 注意力机制、Encoder-Decoder、手把手搭建 Transformer | ✅ |
| [第三章 预训练语言模型](./docs/chapter3/第三章%20预训练语言模型.md) | Encoder-only、Encoder-Decoder、Decoder-Only 模型对比 | ✅ | | [第三章 预训练语言模型](./docs/chapter3/第三章%20预训练语言模型.md) | Encoder-only、Encoder-Decoder、Decoder-Only 模型对比 | ✅ |
| [第四章 大语言模型](./docs/chapter4/第四章%20大语言模型.md) | LLM 定义、训练策略、涌现能力分析 | ✅ | | [第四章 大语言模型](./docs/chapter4/第四章%20大语言模型.md) | LLM 定义、训练策略、涌现能力分析 | ✅ |
| [第五章 动手搭建大模型](./docs/chapter5/第五章%20动手搭建大模型.md) | 实现 LLaMA2、训练 Tokenizer、预训练小型 LLM | ✅ | | [第五章 动手搭建大模型](./docs/chapter5/第五章%20动手搭建大模型.md) | 实现 LLaMA2、训练 Tokenizer、预训练小型 LLM | ✅ |
| [第六章 大模型训练实践](./docs/chapter6/第六章%20大模型训练流程实践.md) | 预训练、有监督微调、LoRA/QLoRA 高效微调 | | | [第六章 大模型训练实践](./docs/chapter6/第六章%20大模型训练流程实践.md) | 预训练、有监督微调、LoRA/QLoRA 高效微调 | 🚧 |
| [第七章 大模型应用](./docs/chapter7/第七章%20大模型应用.md) | 模型评测、RAG 检索增强、Agent 智能体 | ✅ | | [第七章 大模型应用](./docs/chapter7/第七章%20大模型应用.md) | 模型评测、RAG 检索增强、Agent 智能体 | ✅ |
### 模型下载
| 模型名称 | 下载地址 |
| --- | --- |
| Happy-LLM-Chapter5-Base-215M | [🤖 ModelScope](https://www.modelscope.cn/models/kmno4zx/happy-llm-215M-base) |
| Happy-LLM-Chapter5-SFT-215M | [🤖 ModelScope](https://www.modelscope.cn/models/kmno4zx/happy-llm-215M-sft) |
> *ModelScope 创空间体验地址:[🤖 创空间](https://www.modelscope.cn/studios/kmno4zx/happy_llm_215M_sft)*
### PDF 版本下载
&emsp;&emsp;***本 Happy-LLM PDF 教程完全开源免费。为防止各类营销号加水印后贩卖给大模型初学者,我们特地在 PDF 文件中预先添加了不影响阅读的 Datawhale 开源标志水印,敬请谅解~***
> *Happy-LLM PDF : https://github.com/datawhalechina/happy-llm/releases/tag/PDF*
> *Happy-LLM PDF 国内下载地址 : https://www.datawhale.cn/learn/summary/179*
## 💡 如何学习 ## 💡 如何学习
&emsp;&emsp;本项目适合大学生、研究人员、LLM 爱好者。在学习本项目之前,建议具备一定的编程经验,尤其是要对 Python 编程语言有一定的了解。最好具备深度学习的相关知识,并了解 NLP 领域的相关概念和术语,以便更轻松地学习本项目。 &emsp;&emsp;本项目适合大学生、研究人员、LLM 爱好者。在学习本项目之前,建议具备一定的编程经验,尤其是要对 Python 编程语言有一定的了解。最好具备深度学习的相关知识,并了解 NLP 领域的相关概念和术语,以便更轻松地学习本项目。
@@ -90,7 +110,7 @@
## Star History ## Star History
<div align='center'> <div align='center'>
<img src="./images/star-history-202566.png" alt="Datawhale" width="90%"> <img src="./images/star-history-2025624.png" alt="Datawhale" width="90%">
</div> </div>
<div align="center"> <div align="center">

View File

@@ -4,11 +4,12 @@
</div> </div>
<div align="center"> <div align="center">
<img src="https://img.shields.io/github/stars/datawhalechina/happy-llm?style=for-the-badge&logo=github" alt="GitHub stars"/> <img src="https://img.shields.io/github/stars/datawhalechina/happy-llm?style=flat&logo=github" alt="GitHub stars"/>
<img src="https://img.shields.io/github/forks/datawhalechina/happy-llm?style=for-the-badge&logo=github" alt="GitHub forks"/> <img src="https://img.shields.io/github/forks/datawhalechina/happy-llm?style=flat&logo=github" alt="GitHub forks"/>
<img src="https://img.shields.io/badge/language-English-brightgreen?style=for-the-badge" alt="Language"/> <img src="https://img.shields.io/badge/language-Chinese-brightgreen?style=flat" alt="Language"/>
<a href="https://github.com/datawhalechina/happy-llm"><img src="https://img.shields.io/badge/GitHub-Project-blue?style=flat&logo=github" alt="GitHub Project"></a>
<a href="https://swanlab.cn/@kmno4/Happy-LLM/overview"><img src="https://raw.githubusercontent.com/SwanHubX/assets/main/badge1.svg" alt="SwanLab"></a>
</div> </div>
<div align="center"> <div align="center">
[中文](./README.md) | [English](./README_en.md) [中文](./README.md) | [English](./README_en.md)
@@ -16,6 +17,7 @@
</div> </div>
<div align="center"> <div align="center">
<p><a href="https://datawhalechina.github.io/happy-llm/">📚 Online Reading</a></p>
<h3>📚 A Comprehensive Tutorial on Large Language Model Principles and Practice from Scratch</h3> <h3>📚 A Comprehensive Tutorial on Large Language Model Principles and Practice from Scratch</h3>
<p><em>Deep understanding of LLM core principles, hands-on implementation of your first large model</em></p> <p><em>Deep understanding of LLM core principles, hands-on implementation of your first large model</em></p>
</div> </div>
@@ -48,9 +50,26 @@
| [Chapter 3: Pre-trained Language Models](./docs/chapter3/第三章%20预训练语言模型.md) | Comparison of Encoder-only, Encoder-Decoder, Decoder-Only models | ✅ | | [Chapter 3: Pre-trained Language Models](./docs/chapter3/第三章%20预训练语言模型.md) | Comparison of Encoder-only, Encoder-Decoder, Decoder-Only models | ✅ |
| [Chapter 4: Large Language Models](./docs/chapter4/第四章%20大语言模型.md) | LLM definition, training strategies, emergent ability analysis | ✅ | | [Chapter 4: Large Language Models](./docs/chapter4/第四章%20大语言模型.md) | LLM definition, training strategies, emergent ability analysis | ✅ |
| [Chapter 5: Building Large Models from Scratch](./docs/chapter5/第五章%20动手搭建大模型.md) | Implementing LLaMA2, training Tokenizer, pre-training small LLM | ✅ | | [Chapter 5: Building Large Models from Scratch](./docs/chapter5/第五章%20动手搭建大模型.md) | Implementing LLaMA2, training Tokenizer, pre-training small LLM | ✅ |
| [Chapter 6: Large Model Training Practice](./docs/chapter6/第六章%20大模型训练流程实践.md) | Pre-training, supervised fine-tuning, LoRA/QLoRA efficient fine-tuning | | | [Chapter 6: Large Model Training Practice](./docs/chapter6/第六章%20大模型训练流程实践.md) | Pre-training, supervised fine-tuning, LoRA/QLoRA efficient fine-tuning | 🚧 |
| [Chapter 7: Large Model Applications](./docs/chapter7/第七章%20大模型应用.md) | Model evaluation, RAG retrieval enhancement, Agent intelligent agents | ✅ | | [Chapter 7: Large Model Applications](./docs/chapter7/第七章%20大模型应用.md) | Model evaluation, RAG retrieval enhancement, Agent intelligent agents | ✅ |
### Model Downloads
| Model Name | Download Link |
| --- | --- |
| Happy-LLM-Chapter5-Base-215M | [🤖 ModelScope](https://www.modelscope.cn/models/kmno4zx/happy-llm-215M-base) |
| Happy-LLM-Chapter5-SFT-215M | [🤖 ModelScope](https://www.modelscope.cn/models/kmno4zx/happy-llm-215M-sft) |
> *ModelScope Studio Experience: [🤖 Studio](https://www.modelscope.cn/studios/kmno4zx/happy_llm_215M_sft)*
### PDF Version Download
&emsp;&emsp;***This Happy-LLM PDF tutorial is completely open source and free. To prevent various marketing accounts from adding watermarks and selling to LLM beginners, we have pre-added Datawhale open source logo watermarks that do not affect reading in the PDF files. Please understand~***
> *Happy-LLM PDF : https://github.com/datawhalechina/happy-llm/releases/tag/PDF*
> *Happy-LLM PDF Domestic Download: https://www.datawhale.cn/learn/summary/179*
## 💡 How to Learn ## 💡 How to Learn
&emsp;&emsp;This project is suitable for university students, researchers, and LLM enthusiasts. Before learning this project, it is recommended to have some programming experience, especially familiarity with the Python programming language. It's best to have knowledge of deep learning and understand concepts and terminology in the NLP field to learn this project more easily. &emsp;&emsp;This project is suitable for university students, researchers, and LLM enthusiasts. Before learning this project, it is recommended to have some programming experience, especially familiarity with the Python programming language. It's best to have knowledge of deep learning and understand concepts and terminology in the NLP field to learn this project more easily.
@@ -90,7 +109,7 @@ We welcome any form of contribution!
## Star History ## Star History
<div align='center'> <div align='center'>
<img src="./images/star-history-202566.png" alt="Datawhale" width="90%"> <img src="./images/star-history-2025624.png" alt="Datawhale" width="90%">
</div> </div>
<div align="center"> <div align="center">
@@ -101,7 +120,7 @@ We welcome any form of contribution!
<div align='center'> <div align='center'>
<img src="./images/datawhale.png" alt="Datawhale" width="30%"> <img src="./images/datawhale.png" alt="Datawhale" width="30%">
<p>Scan the QR code to follow Datawhale WeChat Official Account for more quality open-source content</p> <p>Scan the QR code to follow Datawhale WeChat Official Account for more quality open source content</p>
</div> </div>
--- ---
@@ -109,3 +128,9 @@ We welcome any form of contribution!
## 📜 Open Source License ## 📜 Open Source License
This work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/). This work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/).
---
## 📜 Open Source License
This work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/).

BIN
docs/.DS_Store vendored

Binary file not shown.

View File

@@ -4,15 +4,11 @@
</div> </div>
<div align="center"> <div align="center">
<img src="https://img.shields.io/github/stars/datawhalechina/happy-llm?style=for-the-badge&logo=github" alt="GitHub stars"/> <img src="https://img.shields.io/github/stars/datawhalechina/happy-llm?style=flat&logo=github" alt="GitHub stars"/>
<img src="https://img.shields.io/github/forks/datawhalechina/happy-llm?style=for-the-badge&logo=github" alt="GitHub forks"/> <img src="https://img.shields.io/github/forks/datawhalechina/happy-llm?style=flat&logo=github" alt="GitHub forks"/>
<img src="https://img.shields.io/badge/language-Chinese-brightgreen?style=for-the-badge" alt="Language"/> <img src="https://img.shields.io/badge/language-Chinese-brightgreen?style=flat" alt="Language"/>
</div> <a href="https://github.com/datawhalechina/happy-llm"><img src="https://img.shields.io/badge/GitHub-Project-blue?style=flat&logo=github" alt="GitHub Project"></a>
<a href="https://swanlab.cn/@kmno4/Happy-LLM/overview"><img src="https://raw.githubusercontent.com/SwanHubX/assets/main/badge1.svg" alt="SwanLab"></a>
<div align="center">
[中文](./README.md) | [English](./README_en.md)
</div> </div>
<div align="center"> <div align="center">
@@ -42,14 +38,30 @@
| 章节 | 关键内容 | 状态 | | 章节 | 关键内容 | 状态 |
| --- | --- | --- | | --- | --- | --- |
| [前言](./docs/README.md) | 本项目的缘起、背景及读者建议 | ✅ | | [前言](./前言.md) | 本项目的缘起、背景及读者建议 | ✅ |
| [第一章 NLP 基础概念](./docs/chapter1/第一章%20NLP基础概念.md) | 什么是 NLP、发展历程、任务分类、文本表示演进 | ✅ | | [第一章 NLP 基础概念](./chapter1/第一章%20NLP基础概念.md) | 什么是 NLP、发展历程、任务分类、文本表示演进 | ✅ |
| [第二章 Transformer 架构](./docs/chapter2/第二章%20Transformer架构.md) | 注意力机制、Encoder-Decoder、手把手搭建 Transformer | ✅ | | [第二章 Transformer 架构](./chapter2/第二章%20Transformer架构.md) | 注意力机制、Encoder-Decoder、手把手搭建 Transformer | ✅ |
| [第三章 预训练语言模型](./docs/chapter3/第三章%20预训练语言模型.md) | Encoder-only、Encoder-Decoder、Decoder-Only 模型对比 | ✅ | | [第三章 预训练语言模型](./chapter3/第三章%20预训练语言模型.md) | Encoder-only、Encoder-Decoder、Decoder-Only 模型对比 | ✅ |
| [第四章 大语言模型](./docs/chapter4/第四章%20大语言模型.md) | LLM 定义、训练策略、涌现能力分析 | ✅ | | [第四章 大语言模型](./chapter4/第四章%20大语言模型.md) | LLM 定义、训练策略、涌现能力分析 | ✅ |
| [第五章 动手搭建大模型](./docs/chapter5/第五章%20动手搭建大模型.md) | 实现 LLaMA2、训练 Tokenizer、预训练小型 LLM | ✅ | | [第五章 动手搭建大模型](./chapter5/第五章%20动手搭建大模型.md) | 实现 LLaMA2、训练 Tokenizer、预训练小型 LLM | ✅ |
| [第六章 大模型训练实践](./docs/chapter6/第六章%20大模型训练流程实践.md) | 预训练、有监督微调、LoRA/QLoRA 高效微调 | | | [第六章 大模型训练实践](./chapter6/第六章%20大模型训练流程实践.md) | 预训练、有监督微调、LoRA/QLoRA 高效微调 | 🚧 |
| [第七章 大模型应用](./docs/chapter7/第七章%20大模型应用.md) | 模型评测、RAG 检索增强、Agent 智能体 | ✅ | | [第七章 大模型应用](./chapter7/第七章%20大模型应用.md) | 模型评测、RAG 检索增强、Agent 智能体 | ✅ |
### 模型下载
| 模型名称 | 下载地址 |
| --- | --- |
| Happy-LLM-Chapter5-Base-215M | [🤖 ModelScope](https://www.modelscope.cn/models/kmno4zx/happy-llm-215M-base) |
| Happy-LLM-Chapter5-SFT-215M | [🤖 ModelScope](https://www.modelscope.cn/models/kmno4zx/happy-llm-215M-sft) |
> *ModelScope 创空间体验地址:[🤖 创空间](https://www.modelscope.cn/studios/kmno4zx/happy_llm_215M_sft)*
### PDF 版本下载
&emsp;&emsp;***本 Happy-LLM PDF 教程完全开源免费。为防止各类营销号加水印后贩卖给大模型初学者,我们特地在 PDF 文件中预先添加了不影响阅读的 Datawhale 开源标志水印,敬请谅解~***
> *Happy-LLM PDF : https://github.com/datawhalechina/happy-llm/releases/tag/PDF*
> *Happy-LLM PDF 国内下载地址 : https://www.datawhale.cn/learn/summary/179*
## 💡 如何学习 ## 💡 如何学习
@@ -90,7 +102,7 @@
## Star History ## Star History
<div align='center'> <div align='center'>
<img src="./images/star-history-202566.png" alt="Datawhale" width="90%"> <img src="./images/star-history-2025624.png" alt="Datawhale" width="90%">
</div> </div>
<div align="center"> <div align="center">

View File

@@ -219,6 +219,7 @@ vector = [0, 0, ..., 1, 0, ..., 1, 0, ..., 1, 0, ..., 1, 0, ..., 1, 0, ...]
# 实际有效维度仅5维非零维度 # 实际有效维度仅5维非零维度
# 稀疏率:(16384-5)/16384 ≈ 99.97% # 稀疏率:(16384-5)/16384 ≈ 99.97%
``` ```
> 词汇表是一个包含所有可能出现的词语的集合。在向量空间模型中,每个词对应词汇表中的一个位置,通过这种方式可以将词语转换为向量表示。例如,如果词汇表大小为 16384 ,那么每个词都会被表示为一个 16384 维的向量,其中只有该词对应的位置为 1其他位置都为 0。
为了解决这些问题,研究者们对向量空间模型的研究主要集中在两个方面:一是改进特征表示方法,如借助图方法、主题方法等进行关键词抽取;二是改进和优化特征项权重的计算方法,可以在现有方法的基础上进行融合计算或提出新的计算方法. 为了解决这些问题,研究者们对向量空间模型的研究主要集中在两个方面:一是改进特征表示方法,如借助图方法、主题方法等进行关键词抽取;二是改进和优化特征项权重的计算方法,可以在现有方法的基础上进行融合计算或提出新的计算方法.

View File

@@ -25,7 +25,7 @@ class MultiHeadAttention(nn.Module):
# args: 配置对象 # args: 配置对象
super().__init__() super().__init__()
# 隐藏层维度必须是头数的整数倍,因为后面我们会将输入拆成头数个矩阵 # 隐藏层维度必须是头数的整数倍,因为后面我们会将输入拆成头数个矩阵
assert args.n_embd % args.n_heads == 0 assert args.dim % args.n_heads == 0
# 模型并行处理大小默认为1。 # 模型并行处理大小默认为1。
model_parallel_size = 1 model_parallel_size = 1
# 本地计算头数,等于总头数除以模型并行处理大小。 # 本地计算头数,等于总头数除以模型并行处理大小。
@@ -39,7 +39,7 @@ class MultiHeadAttention(nn.Module):
self.wq = nn.Linear(args.n_embd, args.n_heads * self.head_dim, bias=False) self.wq = nn.Linear(args.n_embd, args.n_heads * self.head_dim, bias=False)
self.wk = nn.Linear(args.n_embd, args.n_heads * self.head_dim, bias=False) self.wk = nn.Linear(args.n_embd, args.n_heads * self.head_dim, bias=False)
self.wv = nn.Linear(args.n_embd, args.n_heads * self.head_dim, bias=False) self.wv = nn.Linear(args.n_embd, args.n_heads * self.head_dim, bias=False)
# 输出权重矩阵,维度为 n_embd x n_embdhead_dim = n_embeds / n_heads # 输出权重矩阵,维度为 dim x n_embdhead_dim = n_embeds / n_heads
self.wo = nn.Linear(args.n_heads * self.head_dim, args.dim, bias=False) self.wo = nn.Linear(args.n_heads * self.head_dim, args.dim, bias=False)
# 注意力的 dropout # 注意力的 dropout
self.attn_dropout = nn.Dropout(args.dropout) self.attn_dropout = nn.Dropout(args.dropout)

View File

@@ -9,21 +9,21 @@
- 前馈神经网络Feedforward Neural NetworkFNN即每一层的神经元都和上下两层的每一个神经元完全连接如图2.1所示: - 前馈神经网络Feedforward Neural NetworkFNN即每一层的神经元都和上下两层的每一个神经元完全连接如图2.1所示:
<div align="center"> <div align="center">
<img src="../images/2-figures/1-0.png" alt="图片描述" width="90%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/1-0.png" alt="图片描述" width="90%"/>
<p>图2.1 前馈神经网络</p> <p>图2.1 前馈神经网络</p>
</div> </div>
- 卷积神经网络Convolutional Neural NetworkCNN即训练参数量远小于前馈神经网络的卷积层来进行特征提取和学习如图2.2所示: - 卷积神经网络Convolutional Neural NetworkCNN即训练参数量远小于前馈神经网络的卷积层来进行特征提取和学习如图2.2所示:
<div align="center"> <div align="center">
<img src="../images/2-figures/1-1.png" alt="图片描述" width="90%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/1-1.png" alt="图片描述" width="90%"/>
<p>图2.2 卷积神经网络</p> <p>图2.2 卷积神经网络</p>
</div> </div>
- 循环神经网络Recurrent Neural NetworkRNN能够使用历史信息作为输入、包含环和自重复的网络如图2.3所示: - 循环神经网络Recurrent Neural NetworkRNN能够使用历史信息作为输入、包含环和自重复的网络如图2.3所示:
<div align="center"> <div align="center">
<img src="../images/2-figures/1-2.png" alt="图片描述" width="90%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/1-2.png" alt="图片描述" width="90%"/>
<p>图2.3 循环神经网络</p> <p>图2.3 循环神经网络</p>
</div> </div>
@@ -155,7 +155,7 @@ def attention(query, key, value, dropout=None):
但是,在我们的实际应用中,我们往往只需要计算 Query 和 Key 之间的注意力结果,很少存在额外的真值 Value。也就是说我们其实只需要拟合两个文本序列。在经典的 注意力机制中Q 往往来自于一个序列K 与 V 来自于另一个序列,都通过参数矩阵计算得到,从而可以拟合这两个序列之间的关系。例如在 Transformer 的 Decoder 结构中Q 来自于 Decoder 的输入K 与 V 来自于 Encoder 的输出,从而拟合了编码信息与历史信息之间的关系,便于综合这两种信息实现未来的预测。 但是,在我们的实际应用中,我们往往只需要计算 Query 和 Key 之间的注意力结果,很少存在额外的真值 Value。也就是说我们其实只需要拟合两个文本序列。在经典的 注意力机制中Q 往往来自于一个序列K 与 V 来自于另一个序列,都通过参数矩阵计算得到,从而可以拟合这两个序列之间的关系。例如在 Transformer 的 Decoder 结构中Q 来自于 Decoder 的输入K 与 V 来自于 Encoder 的输出,从而拟合了编码信息与历史信息之间的关系,便于综合这两种信息实现未来的预测。
​但在 Transformer 的 Encoder 结构中,使用的是 注意力机制的变种 —— 自注意力self-attention自注意力机制。所谓自注意力即是计算本身序列中每个元素其他元素的注意力分布即在计算过程中Q、K、V 都由同一个输入通过不同的参数矩阵计算得到。在 Encoder 中Q、K、V 分别是输入对参数矩阵 $W_q、W_k、W_v$ 做积得到,从而拟合输入语句中每一个 token 对其他所有 token 的关系。 ​但在 Transformer 的 Encoder 结构中,使用的是 注意力机制的变种 —— 自注意力self-attention自注意力机制。所谓自注意力即是计算本身序列中每个元素其他元素的注意力分布即在计算过程中Q、K、V 都由同一个输入通过不同的参数矩阵计算得到。在 Encoder 中Q、K、V 分别是输入对参数矩阵 $W_q、W_k、W_v$ 做积得到,从而拟合输入语句中每一个 token 对其他所有 token 的关系。
通过自注意力机制,我们可以找到一段文本中每一个 token 与其他所有 token 的相关关系大小从而建模文本之间的依赖关系。在代码中的实现self-attention 机制其实是通过给 Q、K、V 的输入传入同一个参数实现的: 通过自注意力机制,我们可以找到一段文本中每一个 token 与其他所有 token 的相关关系大小从而建模文本之间的依赖关系。在代码中的实现self-attention 机制其实是通过给 Q、K、V 的输入传入同一个参数实现的:
@@ -222,7 +222,7 @@ scores = F.softmax(scores.float(), dim=-1).type_as(xq)
在原论文中作者也通过实验证实多头注意力计算中每个不同的注意力头能够拟合语句中的不同信息如图2.4所示: 在原论文中作者也通过实验证实多头注意力计算中每个不同的注意力头能够拟合语句中的不同信息如图2.4所示:
<div align="center"> <div align="center">
<img src="../images/2-figures/1-3.jpeg" alt="图片描述" width="90%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/1-3.jpeg" alt="图片描述" width="90%"/>
<p>图2.4 多头注意力机制</p> <p>图2.4 多头注意力机制</p>
</div> </div>
@@ -252,7 +252,7 @@ class MultiHeadAttention(nn.Module):
# args: 配置对象 # args: 配置对象
super().__init__() super().__init__()
# 隐藏层维度必须是头数的整数倍,因为后面我们会将输入拆成头数个矩阵 # 隐藏层维度必须是头数的整数倍,因为后面我们会将输入拆成头数个矩阵
assert args.n_embd % args.n_heads == 0 assert args.dim % args.n_heads == 0
# 模型并行处理大小默认为1。 # 模型并行处理大小默认为1。
model_parallel_size = 1 model_parallel_size = 1
# 本地计算头数,等于总头数除以模型并行处理大小。 # 本地计算头数,等于总头数除以模型并行处理大小。
@@ -266,7 +266,7 @@ class MultiHeadAttention(nn.Module):
self.wq = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False) self.wq = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False)
self.wk = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False) self.wk = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False)
self.wv = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False) self.wv = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False)
# 输出权重矩阵,维度为 n_embd x n_embdhead_dim = n_embeds / n_heads # 输出权重矩阵,维度为 dim x n_embdhead_dim = n_embeds / n_heads
self.wo = nn.Linear(args.n_heads * self.head_dim, args.dim, bias=False) self.wo = nn.Linear(args.n_heads * self.head_dim, args.dim, bias=False)
# 注意力的 dropout # 注意力的 dropout
self.attn_dropout = nn.Dropout(args.dropout) self.attn_dropout = nn.Dropout(args.dropout)
@@ -346,7 +346,7 @@ Seq2Seq即序列到序列是一种经典 NLP 任务。具体而言,是
Transformer 中的 Encoder就是用于上述的编码过程Decoder 则用于上述的解码过程。Transformer 结构如图2.5所示: Transformer 中的 Encoder就是用于上述的编码过程Decoder 则用于上述的解码过程。Transformer 结构如图2.5所示:
<div align="center"> <div align="center">
<img src="../images/2-figures/2-0.jpg" alt="图片描述" width="90%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/2-0.jpg" alt="图片描述" width="90%"/>
<p>图2.5 编码器-解码器结构</p> <p>图2.5 编码器-解码器结构</p>
</div> </div>
@@ -540,7 +540,7 @@ class DecoderLayer(nn.Module):
norm_x = self.attention_norm_2(x) norm_x = self.attention_norm_2(x)
h = x + self.attention.forward(norm_x, enc_out, enc_out) h = x + self.attention.forward(norm_x, enc_out, enc_out)
# 经过前馈神经网络 # 经过前馈神经网络
out = h + self.feed_forward.forward(self.fnn_norm(h)) out = h + self.feed_forward.forward(self.ffn_norm(h))
return out return out
``` ```
@@ -568,7 +568,7 @@ class Decoder(nn.Module):
在前两章,我们分别深入剖析了 Attention 机制和 Transformer 的核心——Encoder、Decoder 结构,接下来,我们就可以基于上一章实现的组件,搭建起一个完整的 Transformer 模型。 在前两章,我们分别深入剖析了 Attention 机制和 Transformer 的核心——Encoder、Decoder 结构,接下来,我们就可以基于上一章实现的组件,搭建起一个完整的 Transformer 模型。
### 2.3.1 Embeddng 层 ### 2.3.1 Embedding 层
正如我们在第一章所讲过的,在 NLP 任务中,我们往往需要将自然语言的输入转化为机器可以处理的向量。在深度学习中,承担这个任务的组件就是 Embedding 层。 正如我们在第一章所讲过的,在 NLP 任务中,我们往往需要将自然语言的输入转化为机器可以处理的向量。在深度学习中,承担这个任务的组件就是 Embedding 层。
@@ -590,7 +590,7 @@ output: 2
因此Embedding 层的输入往往是一个形状为 batch_sizeseq_len1的矩阵第一个维度是一次批处理的数量第二个维度是自然语言序列的长度第三个维度则是 token 经过 tokenizer 转化成的 index 值。例如对上述输入Embedding 层的输入会是: 因此Embedding 层的输入往往是一个形状为 batch_sizeseq_len1的矩阵第一个维度是一次批处理的数量第二个维度是自然语言序列的长度第三个维度则是 token 经过 tokenizer 转化成的 index 值。例如对上述输入Embedding 层的输入会是:
``` ```
[[0,1,2]] [[[0],[1],[2]]]
``` ```
其 batch_size 为1seq_len 为3转化出来的 index 如上。 其 batch_size 为1seq_len 为3转化出来的 index 如上。
@@ -732,7 +732,7 @@ $$
上述编码结果如图2.6所示: 上述编码结果如图2.6所示:
<div align="center"> <div align="center">
<img src="../images/2-figures/3-0.png" alt="图片描述" width="90%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/3-0.png" alt="图片描述" width="90%"/>
<p>图2.6 编码结果</p> <p>图2.6 编码结果</p>
</div> </div>
@@ -773,10 +773,12 @@ class PositionalEncoding(nn.Module):
上述所有组件,再按照下图的 Tranfromer 结构拼接起来就是一个完整的 Transformer 模型了如图2.7所示: 上述所有组件,再按照下图的 Tranfromer 结构拼接起来就是一个完整的 Transformer 模型了如图2.7所示:
<div align="center"> <div align="center">
<img src="../images/2-figures/3-1.png" alt="图片描述" width="80%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/2-figures/3-1.png" alt="图片描述" width="80%"/>
<p>图2.7 Transformer 模型结构</p> <p>图2.7 Transformer 模型结构</p>
</div> </div>
但需要注意的是上图是原论文《Attention is all you need》配图LayerNorm 层放在了 Attention 层后面也就是“Post-Norm”结构但在其发布的源代码中LayerNorm 层是放在 Attention 层前面的也就是“Pre Norm”结构。考虑到目前 LLM 一般采用“Pre-Norm”结构可以使 loss 更稳定本文在实现时采用“Pre-Norm”结构。
如图,经过 tokenizer 映射后的输出先经过 Embedding 层和 Positional Embedding 层编码,然后进入上一节讲过的 N 个 Encoder 和 N 个 Decoder在 Transformer 原模型中N 取为6最后经过一个线性层和一个 Softmax 层就得到了最终输出。 如图,经过 tokenizer 映射后的输出先经过 Embedding 层和 Positional Embedding 层编码,然后进入上一节讲过的 N 个 Encoder 和 N 个 Decoder在 Transformer 原模型中N 取为6最后经过一个线性层和一个 Softmax 层就得到了最终输出。
基于之前所实现过的组件,我们实现完整的 Transformer 模型: 基于之前所实现过的组件,我们实现完整的 Transformer 模型:

View File

@@ -26,7 +26,7 @@ BERT 是一个统一了多种思想的预训练模型。其所沿承的核心思
BERT 的模型架构是取了 Transformer 的 Encoder 部分堆叠而成其主要结构如图3.1所示: BERT 的模型架构是取了 Transformer 的 Encoder 部分堆叠而成其主要结构如图3.1所示:
<div align="center"> <div align="center">
<img src="../images/3-figures/1-0.png" alt="图片描述" width="100%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-0.png" alt="图片描述" width="100%"/>
<p>图3.1 BERT 模型结构</p> <p>图3.1 BERT 模型结构</p>
</div> </div>
@@ -35,7 +35,7 @@ BERT 是针对于 NLU 任务打造的预训练模型,其输入一般是文本
模型整体既是由 Embedding、Encoder 加上 prediction_heads 组成: 模型整体既是由 Embedding、Encoder 加上 prediction_heads 组成:
<div align="center"> <div align="center">
<img src="../images/3-figures/1-1.png" alt="图片描述" width="70%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-1.png" alt="图片描述" width="70%"/>
<p>图3.2 BERT 模型简略结构</p> <p>图3.2 BERT 模型简略结构</p>
</div> </div>
@@ -44,21 +44,21 @@ BERT 是针对于 NLU 任务打造的预训练模型,其输入一般是文本
prediction_heads 其实就是线性层加上激活函数一般而言最后一个线性层的输出维度和任务的类别数相等如图3.3所示: prediction_heads 其实就是线性层加上激活函数一般而言最后一个线性层的输出维度和任务的类别数相等如图3.3所示:
<div align="center"> <div align="center">
<img src="../images/3-figures/1-5.png" alt="图片描述" width="20%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-5.png" alt="图片描述" width="20%"/>
<p>图3.3 prediction_heads 结构</p> <p>图3.3 prediction_heads 结构</p>
</div> </div>
而每一层 Encoder Layer 都是和 Transformer 中的 Encoder Layer 结构类似的层如图3.4所示: 而每一层 Encoder Layer 都是和 Transformer 中的 Encoder Layer 结构类似的层如图3.4所示:
<div align="center"> <div align="center">
<img src="../images/3-figures/1-2.png" alt="图片描述" width="40%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-2.png" alt="图片描述" width="40%"/>
<p>图3.4 Encoder Layer 结构</p> <p>图3.4 Encoder Layer 结构</p>
</div> </div>
如图3.5所示,已经通过 Embedding 层映射的 hidden_states 进入核心的 attention 机制,然后通过残差连接的机制和原输入相加,再经过一层 Intermediate 层得到最终输出。Intermediate 层是 BERT 的特殊称呼,其实就是一个线性层加上激活函数: 如图3.5所示,已经通过 Embedding 层映射的 hidden_states 进入核心的 attention 机制,然后通过残差连接的机制和原输入相加,再经过一层 Intermediate 层得到最终输出。Intermediate 层是 BERT 的特殊称呼,其实就是一个线性层加上激活函数:
<div align="center"> <div align="center">
<img src="../images/3-figures/1-3.png" alt="图片描述" width="40%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-3.png" alt="图片描述" width="40%"/>
<p>图3.5 Intermediate 结构</p> <p>图3.5 Intermediate 结构</p>
</div> </div>
@@ -71,7 +71,7 @@ GELU 的核心思路为将随机正则的思想引入激活函数,通过输入
BERT 的 注意力机制和 Transformer 中 Encoder 的 自注意力机制几乎完全一致,但是 BERT 将相对位置编码融合在了注意力机制中将相对位置编码同样视为可训练的权重参数如图3.6所示: BERT 的 注意力机制和 Transformer 中 Encoder 的 自注意力机制几乎完全一致,但是 BERT 将相对位置编码融合在了注意力机制中将相对位置编码同样视为可训练的权重参数如图3.6所示:
<div align="center"> <div align="center">
<img src="../images/3-figures/1-4.png" alt="图片描述" width="40%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/1-4.png" alt="图片描述" width="40%"/>
<p>图3.6 BERT 注意力机制结构</p> <p>图3.6 BERT 注意力机制结构</p>
</div> </div>
@@ -230,14 +230,14 @@ T5 的大一统思想将不同的 NLP 任务如文本分类、问答、翻译等
BERT 采用了 Encoder-Only 结构,只包含编码器部分;而 GPT 采用了 Decoder-Only 结构只包含解码器部分。T5 则采用了 Encoder-Decoder 结构,其中编码器和解码器都是基于 Transformer 架构设计。编码器用于处理输入文本解码器用于生成输出文本。编码器和解码器之间通过注意力机制进行信息交互从而实现输入文本到输出文本的转换。其主要结构如图3.7所示: BERT 采用了 Encoder-Only 结构,只包含编码器部分;而 GPT 采用了 Decoder-Only 结构只包含解码器部分。T5 则采用了 Encoder-Decoder 结构,其中编码器和解码器都是基于 Transformer 架构设计。编码器用于处理输入文本解码器用于生成输出文本。编码器和解码器之间通过注意力机制进行信息交互从而实现输入文本到输出文本的转换。其主要结构如图3.7所示:
<div align="center"> <div align="center">
<img src="../images/3-figures/2-1.png" alt="图片描述" width="100%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/2-1.png" alt="图片描述" width="100%"/>
<p>图3.7 T5 模型详细结构</p> <p>图3.7 T5 模型详细结构</p>
</div> </div>
如图3.8所示,从整体来看 T5 的模型结构包括 Tokenizer 部分和 Transformer 部分。Tokenizer 部分主要负责将输入文本转换为模型可接受的输入格式包括分词、编码等操作。Transformer 部分又分为 EncoderLayers 和 DecoderLayers 两部分,他们分别由一个个小的 Block组成每个 Block 包含了多头注意力机制、前馈神经网络和 Norm 层。Block 的设计可以使模型更加灵活,像乐高一样可以根据任务的复杂程度和数据集的大小来调整 Block 的数量和层数。 如图3.8所示,从整体来看 T5 的模型结构包括 Tokenizer 部分和 Transformer 部分。Tokenizer 部分主要负责将输入文本转换为模型可接受的输入格式包括分词、编码等操作。Transformer 部分又分为 EncoderLayers 和 DecoderLayers 两部分,他们分别由一个个小的 Block组成每个 Block 包含了多头注意力机制、前馈神经网络和 Norm 层。Block 的设计可以使模型更加灵活,像乐高一样可以根据任务的复杂程度和数据集的大小来调整 Block 的数量和层数。
<div align="center"> <div align="center">
<img src="../images/3-figures/2-2.png" alt="图片描述" width="70%"/> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/2-2.png" alt="图片描述" width="70%"/>
<p>图3.8 T5 模型整体结构</p> <p>图3.8 T5 模型整体结构</p>
</div> </div>
@@ -246,28 +246,28 @@ T5 模型的 Encoder 和 Decoder 部分都是基于 Transformer 架构设计的
和 Encoder 不一样的是,在 Decoder 中还包含了 Encoder-Decoder Attention 结构,用于捕捉输入和输出序列之间的依赖关系。这两种 Attention 结构几乎完全一致,只有在位置编码和 Mask 机制上有所不同。如图3.9所示Encoder 和 Decoder 的结构如下: 和 Encoder 不一样的是,在 Decoder 中还包含了 Encoder-Decoder Attention 结构,用于捕捉输入和输出序列之间的依赖关系。这两种 Attention 结构几乎完全一致,只有在位置编码和 Mask 机制上有所不同。如图3.9所示Encoder 和 Decoder 的结构如下:
<div align='center'> <div align='center'>
<img src="../images/3-figures/2-3.png" alt="alt text" width="50%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/2-3.png" alt="alt text" width="50%">
<p>图3.9 Encoder 和 Decoder</p> <p>图3.9 Encoder 和 Decoder</p>
</div> </div>
T5 的 Self-Attention 机制和 BERT 的 Attention 机制是一样的,都是基于 Self-Attention 机制设计的。Self-Attention 机制是一种全局依赖关系建模方法,通过计算 Query、Key 和 Value 之间的相似度来捕捉输入序列中的全局依赖关系。Encoder-Decoder Attention 仅仅在位置编码和 Mask 机制上有所不同主要是为了区分输入和输出序列。如图3.10所示Self-Attention 结构如下: T5 的 Self-Attention 机制和 BERT 的 Attention 机制是一样的,都是基于 Self-Attention 机制设计的。Self-Attention 机制是一种全局依赖关系建模方法,通过计算 Query、Key 和 Value 之间的相似度来捕捉输入序列中的全局依赖关系。Encoder-Decoder Attention 仅仅在位置编码和 Mask 机制上有所不同主要是为了区分输入和输出序列。如图3.10所示Self-Attention 结构如下:
<div align='center'> <div align='center'>
<img src="../images/3-figures/2-4.png" alt="alt text" width="50%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/2-4.png" alt="alt text" width="50%">
</p>图3.10 Self-Attention 结构</p> </p>图3.10 Self-Attention 结构</p>
</div> </div>
与原始 Transformer 模型不同T5 模型的LayerNorm 采用了 RMSNorm通过计算每个神经元的均方根Root Mean Square来归一化每个隐藏层的激活值。RMSNorm 的参数设置与Layer Normalization 相比更简单只有一个可调参数可以更好地适应不同的任务和数据集。RMSNorm函数可以用以下数学公式表示 与原始 Transformer 模型不同T5 模型的LayerNorm 采用了 RMSNorm通过计算每个神经元的均方根Root Mean Square来归一化每个隐藏层的激活值。RMSNorm 的参数设置与Layer Normalization 相比更简单只有一个可调参数可以更好地适应不同的任务和数据集。RMSNorm函数可以用以下数学公式表示
$$ $$
\text{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{n}\sum_{i=1}^{n}w_i^2 + \epsilon}} \text{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{n}\sum_{i=1}^{n}x_i^2 + \epsilon}} \cdot \gamma
$$ $$
其中: 其中:
- \( $x$ \) 是层的输入。 - $x_i$ 是输入向量的第 $i$ 个元素
- \( $w_i$ \) 代表层的权重。 - $\gamma$ 是可学习的缩放参数
- \( $n$ \) 是权重的数量 - $n$ 是输入向量的维度数量
- \( $\epsilon$ \) 是一个小常数,用于数值稳定性(以避免除以零的情况) - $\epsilon$ 是一个小常数,用于数值稳定性(以避免除以零的情况)
这种归一化有助于通过确保权重的规模不会变得过大或过小来稳定学习过程,这在具有许多层的深度学习模型中特别有用。 这种归一化有助于通过确保权重的规模不会变得过大或过小来稳定学习过程,这在具有许多层的深度学习模型中特别有用。
@@ -298,7 +298,7 @@ T5通过大规模的文本数据进行预训练然后在具体任务上进行
我们可以通过图3.11,更加直观地理解 T5 的大一统思想: 我们可以通过图3.11,更加直观地理解 T5 的大一统思想:
<div align='center'> <div align='center'>
<img src="../images/3-figures/2-0.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/2-0.png" alt="alt text" width="90%">
<p>图3.11 T5 的大一统思想</p> <p>图3.11 T5 的大一统思想</p>
</div> </div>
@@ -323,7 +323,7 @@ GPT即 Generative Pre-Training Language Model是由 OpenAI 团队于 2018
#### 1 模型架构——Decoder Only #### 1 模型架构——Decoder Only
<div align='center'> <div align='center'>
<img src="../images/3-figures/3-0.png" alt="alt text" width="100%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/3-0.png" alt="alt text" width="100%">
<p>图3.12 GPT 模型结构</p> <p>图3.12 GPT 模型结构</p>
</div> </div>
@@ -394,7 +394,7 @@ LLaMA模型是由Meta前Facebook开发的一系列大型预训练语言模
与GPT系列模型一样LLaMA模型也是基于Decoder-Only架构的预训练语言模型。LLaMA模型的整体结构与GPT系列模型类似只是在模型规模和预训练数据集上有所不同。如图3.13是LLaMA模型的架构示意图 与GPT系列模型一样LLaMA模型也是基于Decoder-Only架构的预训练语言模型。LLaMA模型的整体结构与GPT系列模型类似只是在模型规模和预训练数据集上有所不同。如图3.13是LLaMA模型的架构示意图
<div align='center'> <div align='center'>
<img src="../images/3-figures/3-1.png" alt="alt text" width="100%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/3-1.png" alt="alt text" width="100%">
<p>图3.13 LLaMA-3 模型结构</p> <p>图3.13 LLaMA-3 模型结构</p>
</div> </div>
@@ -460,7 +460,7 @@ GLM 通过优化一个自回归空白填充任务来实现 MLM 与 CLM 思想的
通过将 MLM 与 CLM 思想相结合,既适配逐个 token 生成的生成类任务,也迫使模型从前后两个方向学习输入文本的隐含关系从而适配了理解类任务。使用 GLM 预训练任务产出的 GLM 模型,在一定程度上展现了其超出同体量 BERT 系模型的优越性能: 通过将 MLM 与 CLM 思想相结合,既适配逐个 token 生成的生成类任务,也迫使模型从前后两个方向学习输入文本的隐含关系从而适配了理解类任务。使用 GLM 预训练任务产出的 GLM 模型,在一定程度上展现了其超出同体量 BERT 系模型的优越性能:
<div align='center'> <div align='center'>
<img src="../images/3-figures/3-2.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/3-2.png" alt="alt text" width="90%">
<p>图3.14 alt text</p> <p>图3.14 alt text</p>
</div> </div>
@@ -479,7 +479,7 @@ ChatGLM3-6B 发布于 23年 10月相对于二代在语义、数学、推理
图3.15展示了 GLM 系列模型在基准集上的表现演进: 图3.15展示了 GLM 系列模型在基准集上的表现演进:
<div align='center'> <div align='center'>
<img src="../images/3-figures/3-3.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/3-figures/3-3.png" alt="alt text" width="90%">
<p>图3.15 alt text</p> <p>图3.15 alt text</p>
</div> </div>

View File

@@ -96,13 +96,13 @@ LLM 的强大能力也为其带来了跨模态的强大表现。随着 LLM 的
在上一节,我们分析了 LLM 的定义及其特有的强大能力,通过更大规模的参数和海量的训练语料获得远超传统预训练模型的涌现能力,展现出强大的上下文学习、指令遵循及逐步推理能力,带来 NLP 领域的全新变革。那么,通过什么样的步骤,我们才可以训练出一个具有涌现能力的 LLM 呢?训练一个 LLM与训练传统的预训练模型又有什么区别 在上一节,我们分析了 LLM 的定义及其特有的强大能力,通过更大规模的参数和海量的训练语料获得远超传统预训练模型的涌现能力,展现出强大的上下文学习、指令遵循及逐步推理能力,带来 NLP 领域的全新变革。那么,通过什么样的步骤,我们才可以训练出一个具有涌现能力的 LLM 呢?训练一个 LLM与训练传统的预训练模型又有什么区别
<div align='center'> <div align='center'>
<img src="../images/4-figures/2-0.jpg" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-0.jpg" alt="alt text" width="90%">
<p>图4.1 训练 LLM 的三个阶段</p> <p>图4.1 训练 LLM 的三个阶段</p>
</div> </div>
一般而言,训练一个完整的 LLM 需要经过图1中的三个阶段——Pretrain、SFT 和 RLHF。在这一节我们将详细论述训练 LLM 的三个阶段,并分析每一个阶段的过程及其核心难点、注意事项,帮助读者们从理论上了解要训练一个 LLM需要经过哪些步骤。 一般而言,训练一个完整的 LLM 需要经过图1中的三个阶段——Pretrain、SFT 和 RLHF。在这一节我们将详细论述训练 LLM 的三个阶段,并分析每一个阶段的过程及其核心难点、注意事项,帮助读者们从理论上了解要训练一个 LLM需要经过哪些步骤。
### 4.2.2 Pretrain ### 4.2.1 Pretrain
Pretrain即预训练是训练 LLM 最核心也是工程量最大的第一步。LLM 的预训练和传统预训练模型非常类似,同样是使用海量无监督文本对随机初始化的模型参数进行训练。正如我们在第三章中所见,目前主流的 LLM 几乎都采用了 Decoder-Only 的类 GPT 架构LLaMA 架构),它们的预训练任务也都沿承了 GPT 模型的经典预训练任务——因果语言模型Causal Language ModelCLM Pretrain即预训练是训练 LLM 最核心也是工程量最大的第一步。LLM 的预训练和传统预训练模型非常类似,同样是使用海量无监督文本对随机初始化的模型参数进行训练。正如我们在第三章中所见,目前主流的 LLM 几乎都采用了 Decoder-Only 的类 GPT 架构LLaMA 架构),它们的预训练任务也都沿承了 GPT 模型的经典预训练任务——因果语言模型Causal Language ModelCLM
@@ -128,7 +128,7 @@ GPT-3|96|12288|96|175B|300B
也正因如此,分布式训练框架也成为 LLM 训练必不可少的组成部分。分布式训练框架的核心思路是数据并行和模型并行。所谓数据并行,是指训练模型的尺寸可以被单个 GPU 内存容纳,但是由于增大训练的 batch_size 会增大显存开销,无法使用较大的 batch_size 进行训练;同时,训练数据量非常大,使用单张 GPU 训练时长难以接受。 也正因如此,分布式训练框架也成为 LLM 训练必不可少的组成部分。分布式训练框架的核心思路是数据并行和模型并行。所谓数据并行,是指训练模型的尺寸可以被单个 GPU 内存容纳,但是由于增大训练的 batch_size 会增大显存开销,无法使用较大的 batch_size 进行训练;同时,训练数据量非常大,使用单张 GPU 训练时长难以接受。
<div align='center'> <div align='center'>
<img src="../images/4-figures/2-1.jpg" alt="alt text" width="60%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-1.jpg" alt="alt text" width="60%">
<p>图4.2 模型、数据并行</p> <p>图4.2 模型、数据并行</p>
</div> </div>
@@ -137,7 +137,7 @@ GPT-3|96|12288|96|175B|300B
但是,当 LLM 扩大到上百亿参数,单张 GPU 内存往往就无法存放完整的模型参数。如图4.3所示,在这种情况下,可以将模型拆分到多个 GPU 上,每个 GPU 上存放不同的层或不同的部分,从而实现模型并行。 但是,当 LLM 扩大到上百亿参数,单张 GPU 内存往往就无法存放完整的模型参数。如图4.3所示,在这种情况下,可以将模型拆分到多个 GPU 上,每个 GPU 上存放不同的层或不同的部分,从而实现模型并行。
<div align='center'> <div align='center'>
<img src="../images/4-figures/2-2.jpg" alt="alt text" width="30%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-2.jpg" alt="alt text" width="30%">
<p>图4.3 模型并行</p> <p>图4.3 模型并行</p>
</div> </div>
@@ -179,7 +179,7 @@ StackExchange|2.0%|78 GB
目前,已有很多经过处理的高质量预训练语料和专用于预训练数据处理的框架。例如,有基于 LLaMA 思路收集、清洗的预训练数据集[RedPajama-1T](https://huggingface.co/datasets/togethercomputer/RedPajama-Data-1T),以及在 RedPajama 基础上进行筛选去重的[SlimPajama-627B](https://huggingface.co/datasets/cerebras/SlimPajama-627B/tree/main/train)数据集,实验证明高质量的 627B Slimpajama 数据集能够获得比 1T 的 RedPajama 数据集更好的效果。 目前,已有很多经过处理的高质量预训练语料和专用于预训练数据处理的框架。例如,有基于 LLaMA 思路收集、清洗的预训练数据集[RedPajama-1T](https://huggingface.co/datasets/togethercomputer/RedPajama-Data-1T),以及在 RedPajama 基础上进行筛选去重的[SlimPajama-627B](https://huggingface.co/datasets/cerebras/SlimPajama-627B/tree/main/train)数据集,实验证明高质量的 627B Slimpajama 数据集能够获得比 1T 的 RedPajama 数据集更好的效果。
### 4.2.3 SFT ### 4.2.2 SFT
预训练是 LLM 强大能力的根本来源事实上LLM 所覆盖的海量知识基本都是源于预训练语料。LLM 的性能本身,核心也在于预训练的工作。但是,预训练赋予了 LLM 能力,却还需要第二步将其激发出来。经过预训练的 LLM 好像一个博览群书但又不求甚解的书生对什么样的偏怪问题都可以流畅地接出下文但他偏偏又不知道问题本身的含义只会“死板背书”。这一现象的本质是因为LLM 的预训练任务就是经典的 CLM也就是训练其预测下一个 token 的能力,在没有进一步微调之前,其无法与其他下游任务或是用户指令适配。 预训练是 LLM 强大能力的根本来源事实上LLM 所覆盖的海量知识基本都是源于预训练语料。LLM 的性能本身,核心也在于预训练的工作。但是,预训练赋予了 LLM 能力,却还需要第二步将其激发出来。经过预训练的 LLM 好像一个博览群书但又不求甚解的书生对什么样的偏怪问题都可以流畅地接出下文但他偏偏又不知道问题本身的含义只会“死板背书”。这一现象的本质是因为LLM 的预训练任务就是经典的 CLM也就是训练其预测下一个 token 的能力,在没有进一步微调之前,其无法与其他下游任务或是用户指令适配。
@@ -290,14 +290,14 @@ StackExchange|2.0%|78 GB
显然可知,第一种方式会丢失大量中间信息,第二种方式造成了大量重复计算,只有第三种方式是最合理的多轮对话构造。我们之所以可以以第三种方式来构造多轮对话样本,是因为 LLM 本质还是进行的 CLM 任务,进行单向注意力计算,因此在预测时会从左到右依次进行拟合,前轮的输出预测不会影响后轮的预测。目前,绝大部分 LLM 均使用了多轮对话的形式来进行 SFT。 显然可知,第一种方式会丢失大量中间信息,第二种方式造成了大量重复计算,只有第三种方式是最合理的多轮对话构造。我们之所以可以以第三种方式来构造多轮对话样本,是因为 LLM 本质还是进行的 CLM 任务,进行单向注意力计算,因此在预测时会从左到右依次进行拟合,前轮的输出预测不会影响后轮的预测。目前,绝大部分 LLM 均使用了多轮对话的形式来进行 SFT。
## 4.2.4 RLHF ## 4.2.3 RLHF
RLHF全称是 Reinforcement Learning from Human Feedback即人类反馈强化学习是利用强化学习来训练 LLM 的关键步骤。相较于在 GPT-3 就已经初见雏形的 SFTRLHF 往往被认为是 ChatGPT 相较于 GPT-3 的最核心突破。事实上,从功能上出发,我们可以将 LLM 的训练过程分成预训练与对齐alignment两个阶段。预训练的核心作用是赋予模型海量的知识而所谓对齐其实就是让模型与人类价值观一致从而输出人类希望其输出的内容。在这个过程中SFT 是让 LLM 和人类的指令对齐,从而具有指令遵循能力;而 RLHF 则是从更深层次令 LLM 和人类价值观对齐,令其达到安全、有用、无害的核心标准。 RLHF全称是 Reinforcement Learning from Human Feedback即人类反馈强化学习是利用强化学习来训练 LLM 的关键步骤。相较于在 GPT-3 就已经初见雏形的 SFTRLHF 往往被认为是 ChatGPT 相较于 GPT-3 的最核心突破。事实上,从功能上出发,我们可以将 LLM 的训练过程分成预训练与对齐alignment两个阶段。预训练的核心作用是赋予模型海量的知识而所谓对齐其实就是让模型与人类价值观一致从而输出人类希望其输出的内容。在这个过程中SFT 是让 LLM 和人类的指令对齐,从而具有指令遵循能力;而 RLHF 则是从更深层次令 LLM 和人类价值观对齐,令其达到安全、有用、无害的核心标准。
如图4.4所示ChatGPT 在技术报告中将对齐分成三个阶段,后面两个阶段训练 RM 和 PPO 训练,就是 RLHF 的步骤: 如图4.4所示ChatGPT 在技术报告中将对齐分成三个阶段,后面两个阶段训练 RM 和 PPO 训练,就是 RLHF 的步骤:
<div align='center'> <div align='center'>
<img src="../images/4-figures/2-3.png" alt="alt text" width="100%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-3.png" alt="alt text" width="100%">
<p>图4.4 ChatGPT 训练三个的阶段</p> <p>图4.4 ChatGPT 训练三个的阶段</p>
</div> </div>
@@ -331,7 +331,7 @@ RMReward Model即奖励模型。RM 是用于拟合人类偏好,来给 LL
在具体 PPO 训练过程中会存在四个模型。如图4.5所示,两个 LLM 和两个 RM。两个 LLM 分别是进行微调、参数更新的 actor model 和不进行参数更新的 ref model均是从 SFT 之后的 LLM 初始化的。两个 RM 分别是进行参数更新的 critic model 和不进行参数更新的 reward model均是从上一步训练的 RM 初始化的。 在具体 PPO 训练过程中会存在四个模型。如图4.5所示,两个 LLM 和两个 RM。两个 LLM 分别是进行微调、参数更新的 actor model 和不进行参数更新的 ref model均是从 SFT 之后的 LLM 初始化的。两个 RM 分别是进行参数更新的 critic model 和不进行参数更新的 reward model均是从上一步训练的 RM 初始化的。
<div align='center'> <div align='center'>
<img src="../images/4-figures/2-4.jpg" alt="alt text" width="100%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/4-figures/2-4.jpg" alt="alt text" width="100%">
<p>图4.5 PPO 训练流程</p> <p>图4.5 PPO 训练流程</p>
</div> </div>
@@ -339,9 +339,9 @@ RMReward Model即奖励模型。RM 是用于拟合人类偏好,来给 LL
1. 从 SFT 之后的 LLM 初始化两个模型分别作为 Actor Model 和 Ref Model从训练的 RM 初始化两个模型分别作为 Reward Model 和 Critic Model 1. 从 SFT 之后的 LLM 初始化两个模型分别作为 Actor Model 和 Ref Model从训练的 RM 初始化两个模型分别作为 Reward Model 和 Critic Model
2. 输入一个 PromptActor Model 和 Ref Model 分别就 Prompt 生成回复; 2. 输入一个 PromptActor Model 和 Ref Model 分别就 Prompt 生成回复;
3. Actor Response 和 Ref Response 计算 KL 散度:$r_{KL} = -\theta_{KL}D_{KL}(\pi_{PPO}(y|x)||\pi_{base}(y|x))$其中,$\pi_{PPO}(y|x)$即为 Actor Model 的输出,而 $\pi_{base}(y|x)$即为 Ref Model 的输出,$theta_{KL}D_{KL}$即是计算 KL 散度的方法; 3. Actor Response 和 Ref Response 计算 KL 散度: $r_{KL} = -\theta_{KL}D_{KL}(\pi_{PPO}(y|x)||\pi_{base}(y|x))$ 其中, $\pi_{PPO}(y|x)$ 即为 Actor Model 的输出,而 $\pi_{base}(y|x)$ 即为 Ref Model 的输出, $\theta_{KL}D_{KL}$ 即是计算 KL 散度的方法;
4. Actor Response 分别输入到 Reward Model 和 Critic Model 进行打分其中Reward Model 输出的是回复对应的标量奖励Critic Model 还会输出累加奖励即从i位置到最后的累积奖励 4. Actor Response 分别输入到 Reward Model 和 Critic Model 进行打分其中Reward Model 输出的是回复对应的标量奖励Critic Model 还会输出累加奖励即从i位置到最后的累积奖励
5. 计算的 KL 散度、两个模型的打分均输入到奖励函数中,计算奖励:$loss = -(kl_{ctl}*r_{KL} + \gamma * V_{t+1} - V_{t})logP(A_t|V_t)$,这里的 $kl_{ctl}是控制 KL 散度对结果影响的权重参数,$\gamma$ 是控制下一个时间(也就是样本)打分对结果影响的权重参数,$V_t$ 是 Critic Model 的打分输出,$A_t$ 则是 Reward Model 的打分输出; 5. 计算的 KL 散度、两个模型的打分均输入到奖励函数中,计算奖励: $loss = -(kl_{ctl} \cdot r_{KL} + \gamma \cdot V_{t+1} - V_{t}) \log P(A_t|V_t)$ ,这里的 $kl_{ctl}$ 是控制 KL 散度对结果影响的权重参数, $\gamma$ 是控制下一个时间(也就是样本)打分对结果影响的权重参数, $V_t$ 是 Critic Model 的打分输出, $A_t$ 则是 Reward Model 的打分输出;
6. 根据奖励函数分别计算出的 actor loss 和 critic loss更新 Actor Model 的参数和 Critic Model 的参数注意Actor Model 和 Critic Model 的参数更新方法是不同的,此处就不再一一赘述了,感兴趣的读者可以深入研究强化学习的相关理论。 6. 根据奖励函数分别计算出的 actor loss 和 critic loss更新 Actor Model 的参数和 Critic Model 的参数注意Actor Model 和 Critic Model 的参数更新方法是不同的,此处就不再一一赘述了,感兴趣的读者可以深入研究强化学习的相关理论。
在上述过程中,因为要使用到四个模型,显存占用会数倍于 SFT。例如如果我们 RM 和 LLM 都是用 7B 的体量PPO 过程中大概需要 240G4张 80G A100每张卡占用 60G显存来进行模型加载。那么为什么我们需要足足四个模型呢Actor Model 和 Critic Model 较为容易理解,而之所以我们还需要保持原参数不更新的 Ref Model 和 Reward Model是为了限制模型的更新不要过于偏离原模型以至于丢失了 Pretrain 和 SFT 赋予的能力。 在上述过程中,因为要使用到四个模型,显存占用会数倍于 SFT。例如如果我们 RM 和 LLM 都是用 7B 的体量PPO 过程中大概需要 240G4张 80G A100每张卡占用 60G显存来进行模型加载。那么为什么我们需要足足四个模型呢Actor Model 和 Critic Model 较为容易理解,而之所以我们还需要保持原参数不更新的 Ref Model 和 Reward Model是为了限制模型的更新不要过于偏离原模型以至于丢失了 Pretrain 和 SFT 赋予的能力。

View File

@@ -6,7 +6,6 @@ import pandas as pd
import numpy as np import numpy as np
from torch.utils.data import Dataset, DataLoader from torch.utils.data import Dataset, DataLoader
import torch import torch
from sklearn.model_selection import train_test_split
import os import os
class PretrainDataset(Dataset): class PretrainDataset(Dataset):
@@ -56,7 +55,7 @@ class SFTDataset(Dataset):
def generate_loss_mask(self, input_ids): def generate_loss_mask(self, input_ids):
# 生成 loss mask, 0 表示不计算损失, 1 表示计算损失 # 生成 loss mask, 0 表示不计算损失, 1 表示计算损失
mask = [0] * len(input_ids) mask = [0] * len(input_ids)
a_sequence = [3, 1074, 537, 500, 203] # <|im_start|>assistant\n a_sequence = self.tokenizer("<|im_start|>assistant\n")['input_ids'] # <|im_start|>assistant\n
a_length = len(a_sequence) a_length = len(a_sequence)
n = len(input_ids) n = len(input_ids)
i = 0 i = 0
@@ -69,10 +68,10 @@ class SFTDataset(Dataset):
match = False match = False
break break
if match: if match:
# 从子序列结束的位置开始查找第一个4 # 从子序列结束的位置开始查找第一个 4 (eos_token_id)
j = None j = None
for idx in range(i + a_length, n): for idx in range(i + a_length, n):
if input_ids[idx] == 4: if input_ids[idx] == self.tokenizer.eos_token_id:
j = idx j = idx
break break
if j is not None: if j is not None:

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os import os
import platform import platform
import argparse import argparse
@@ -17,176 +18,307 @@ from dataset import PretrainDataset
import swanlab import swanlab
# 忽略警告信息
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
def Logger(content): def Logger(content):
"""
简单的日志记录函数
Args:
content (str): 要打印的内容
"""
print(content) print(content)
def get_lr(it, all): def get_lr(it, all):
warmup_iters = args.warmup_iters """
lr_decay_iters = all 计算当前迭代的学习率,使用余弦退火调度策略
min_lr = args.learning_rate / 10
学习率调度策略:
1. Warmup阶段学习率从0线性增长到目标学习率
2. 余弦退火阶段:学习率按余弦函数衰减到最小学习率
3. 超出训练步数后:保持最小学习率
Args:
it (int): 当前迭代步数
all (int): 总迭代步数
Returns:
float: 当前步数对应的学习率
"""
warmup_iters = args.warmup_iters # 预热迭代次数
lr_decay_iters = all # 学习率衰减的总迭代次数
min_lr = args.learning_rate / 10 # 最小学习率为初始学习率的1/10
# Warmup阶段线性增长
if it < warmup_iters: if it < warmup_iters:
return args.learning_rate * it / warmup_iters return args.learning_rate * it / warmup_iters
# 超出训练步数:保持最小学习率
if it > lr_decay_iters: if it > lr_decay_iters:
return min_lr return min_lr
# 余弦退火阶段
decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters) decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
assert 0 <= decay_ratio <= 1 assert 0 <= decay_ratio <= 1
coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) # 余弦系数
return min_lr + coeff * (args.learning_rate - min_lr) return min_lr + coeff * (args.learning_rate - min_lr)
def train_epoch(epoch): def train_epoch(epoch):
start_time = time.time() """
for step, (X, Y, loss_mask) in enumerate(train_loader): 训练一个epoch的函数
X = X.to(args.device)
Y = Y.to(args.device)
loss_mask = loss_mask.to(args.device)
实现了完整的训练循环,包括:
1. 数据加载和设备转移
2. 动态学习率调整
3. 前向传播和损失计算
4. 梯度累积和反向传播
5. 梯度裁剪和优化器更新
6. 日志记录和模型保存
Args:
epoch (int): 当前epoch编号
"""
start_time = time.time() # 记录开始时间
# 遍历数据加载器中的每个batch
for step, (X, Y, loss_mask) in enumerate(train_loader):
# 将数据转移到指定设备GPU/CPU
X = X.to(args.device) # 输入序列
Y = Y.to(args.device) # 目标序列
loss_mask = loss_mask.to(args.device) # 损失掩码用于忽略padding token
# 计算当前步骤的学习率
lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch) lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch)
# 更新优化器中所有参数组的学习率
for param_group in optimizer.param_groups: for param_group in optimizer.param_groups:
param_group['lr'] = lr param_group['lr'] = lr
# 使用混合精度训练上下文
with ctx: with ctx:
# 前向传播
out = model(X, Y) out = model(X, Y)
# 计算损失并除以累积步数(用于梯度累积)
loss = out.last_loss / args.accumulation_steps loss = out.last_loss / args.accumulation_steps
# 将loss_mask展平为一维
loss_mask = loss_mask.view(-1) loss_mask = loss_mask.view(-1)
# 应用掩码计算有效损失忽略padding位置
loss = torch.sum(loss * loss_mask) / loss_mask.sum() loss = torch.sum(loss * loss_mask) / loss_mask.sum()
# 使用scaler进行混合精度的反向传播
scaler.scale(loss).backward() scaler.scale(loss).backward()
# 每accumulation_steps步执行一次优化器更新
if (step + 1) % args.accumulation_steps == 0: if (step + 1) % args.accumulation_steps == 0:
# 取消梯度缩放,准备梯度裁剪
scaler.unscale_(optimizer) scaler.unscale_(optimizer)
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
# 执行优化器步骤
scaler.step(optimizer) scaler.step(optimizer)
# 更新scaler的缩放因子
scaler.update() scaler.update()
# 清零梯度set_to_none=True可以节省内存
optimizer.zero_grad(set_to_none=True) optimizer.zero_grad(set_to_none=True)
# 每log_interval步记录一次日志
if step % args.log_interval == 0: if step % args.log_interval == 0:
spend_time = time.time() - start_time spend_time = time.time() - start_time
# 打印训练进度信息
Logger( Logger(
'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.7f} epoch_Time:{}min:'.format( 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.7f} epoch_Time:{}min;'.format(
epoch + 1, epoch + 1,
args.epochs, args.epochs,
step, step,
iter_per_epoch, iter_per_epoch,
loss.item() * args.accumulation_steps, loss.item() * args.accumulation_steps, # 恢复真实的loss值
optimizer.param_groups[-1]['lr'], optimizer.param_groups[-1]['lr'],
spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60)) spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))
# 如果启用SwanLab记录训练指标
if args.use_swanlab: if args.use_swanlab:
swanlab.log({ swanlab.log({
"loss": loss.item() * args.accumulation_steps, "loss": loss.item() * args.accumulation_steps,
"lr": optimizer.param_groups[-1]['lr'] "lr": optimizer.param_groups[-1]['lr']
}) })
# 每save_interval步保存一次模型
if (step + 1) % args.save_interval == 0: if (step + 1) % args.save_interval == 0:
model.eval() model.eval() # 切换到评估模式
# 构建检查点文件名
ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}.pth' ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}.pth'
# 处理多卡保存 # 处理多卡保存如果是DataParallel模型需要访问.module属性
state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict() state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
torch.save(state_dict, ckp) torch.save(state_dict, ckp)
model.train() model.train() # 切换回训练模式
# 每20000步保存一个带步数标记的检查点
if (step + 1) % 20000 == 0: if (step + 1) % 20000 == 0:
model.eval() model.eval()
# 构建带步数的检查点文件名
ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step+1}.pth' ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step+1}.pth'
# 保存模型状态字典
state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict() state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
torch.save(state_dict, ckp) torch.save(state_dict, ckp)
model.train() model.train()
def init_model(): def init_model():
"""
初始化模型和分词器
功能包括:
1. 加载预训练的分词器
2. 创建Transformer模型
3. 设置多GPU并行训练如果可用
4. 将模型移动到指定设备
5. 统计并打印模型参数量
Returns:
tuple: (model, tokenizer) 初始化后的模型和分词器
"""
def count_parameters(model): def count_parameters(model):
"""
统计模型中可训练参数的数量
Args:
model: PyTorch模型
Returns:
int: 可训练参数总数
"""
return sum(p.numel() for p in model.parameters() if p.requires_grad) return sum(p.numel() for p in model.parameters() if p.requires_grad)
# 从本地路径加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/') tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/')
# 根据配置创建Transformer模型
model = Transformer(lm_config) model = Transformer(lm_config)
# 多卡初始化 # 多卡初始化检查可用GPU数量并设置DataParallel
num_gpus = torch.cuda.device_count() num_gpus = torch.cuda.device_count()
if num_gpus > 1: if num_gpus > 1:
Logger(f"Using {num_gpus} GPUs with DataParallel!") Logger(f"Using {num_gpus} GPUs with DataParallel!")
# 使用DataParallel包装模型以支持多GPU训练
model = torch.nn.DataParallel(model) model = torch.nn.DataParallel(model)
# 将模型移动到指定设备GPU或CPU
model = model.to(args.device) model = model.to(args.device)
# 计算并打印模型参数量(以百万为单位)
Logger(f'LLM总参数量{count_parameters(model) / 1e6:.3f} 百万') Logger(f'LLM总参数量{count_parameters(model) / 1e6:.3f} 百万')
return model, tokenizer return model, tokenizer
if __name__ == "__main__": if __name__ == "__main__":
# ==================== 命令行参数解析 ====================
parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining") parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining")
parser.add_argument("--out_dir", type=str, default="base_monkey_215M", help="Output directory")
parser.add_argument("--epochs", type=int, default=1, help="Number of epochs") # 基础训练参数
parser.add_argument("--batch_size", type=int, default=64, help="Batch size") parser.add_argument("--out_dir", type=str, default="base_model_215M", help="模型输出目录")
parser.add_argument("--learning_rate", type=float, default=2e-4, help="Learning rate") parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="Device to use") parser.add_argument("--batch_size", type=int, default=64, help="批次大小")
parser.add_argument("--dtype", type=str, default="bfloat16", help="Data type") parser.add_argument("--learning_rate", type=float, default=2e-4, help="学习率")
parser.add_argument("--use_swanlab", type=bool, default=True, help="Use Weights & Biases") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
parser.add_argument("--num_workers", type=int, default=8, help="Number of workers for data loading") parser.add_argument("--dtype", type=str, default="bfloat16", help="数据类型")
parser.add_argument("--data_path", type=str, default="", help="Path to training data")
parser.add_argument("--accumulation_steps", type=int, default=8, help="Gradient accumulation steps") # 实验跟踪和数据加载参数
parser.add_argument("--grad_clip", type=float, default=1.0, help="Gradient clipping threshold") parser.add_argument("--use_swanlab", action="store_true", help="是否使用SwanLab进行实验跟踪")
parser.add_argument("--warmup_iters", type=int, default=0, help="Number of warmup iterations") parser.add_argument("--num_workers", type=int, default=8, help="数据加载的工作进程数")
parser.add_argument("--log_interval", type=int, default=100, help="Logging interval") parser.add_argument("--data_path", type=str, default="./seq_monkey_datawhale.jsonl", help="训练数据路径")
parser.add_argument("--save_interval", type=int, default=1000, help="Model saving interval")
# 添加多卡参数 # 训练优化参数
parser.add_argument("--gpus", type=str, default='0,1', help="Comma-separated GPU IDs (e.g. '0,1,2')") parser.add_argument("--accumulation_steps", type=int, default=8, help="梯度累积步数")
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
parser.add_argument("--warmup_iters", type=int, default=0, help="学习率预热迭代次数")
# 日志和保存参数
parser.add_argument("--log_interval", type=int, default=100, help="日志记录间隔")
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
# 多GPU训练参数
parser.add_argument("--gpus", type=str, default='0,1,2,3,4,5,6,7', help="使用的GPU ID用逗号分隔 (例如: '0,1,2')")
args = parser.parse_args() args = parser.parse_args()
# 设置可见GPU # ==================== GPU环境设置 ====================
# 设置可见的GPU设备
if args.gpus is not None: if args.gpus is not None:
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpus os.environ["CUDA_VISIBLE_DEVICES"] = args.gpus
# 自动设置主设备为第一个GPU # 自动设置主设备为第一个可用GPU
if torch.cuda.is_available(): if torch.cuda.is_available():
args.device = "cuda:0" args.device = "cuda:0"
else: else:
args.device = "cpu" args.device = "cpu"
# ==================== 实验跟踪初始化 ====================
if args.use_swanlab: if args.use_swanlab:
swanlab.login(api_key='your key') # 注意:使用前需要先登录 swanlab.login(api_key='your key')
run = swanlab.init( run = swanlab.init(
project="Tiny-LLM", project="Happy-LLM", # 项目名称
experiment_name="Pretrain-215M", experiment_name="Pretrain-215M", # 实验名称
config=args, config=args, # 保存所有超参数
) )
# ==================== 模型配置 ====================
# 定义语言模型的配置参数
lm_config = ModelConfig( lm_config = ModelConfig(
dim=1024, dim=1024, # 模型维度
n_layers=18, n_layers=18, # Transformer层数
) )
max_seq_len = lm_config.max_seq_len
args.save_dir = os.path.join(args.out_dir) # ==================== 训练环境设置 ====================
max_seq_len = lm_config.max_seq_len # 最大序列长度
args.save_dir = os.path.join(args.out_dir) # 模型保存目录
# 创建必要的目录
os.makedirs(args.save_dir, exist_ok=True) os.makedirs(args.save_dir, exist_ok=True)
os.makedirs(args.out_dir, exist_ok=True) os.makedirs(args.out_dir, exist_ok=True)
# 设置随机种子以确保结果可复现
torch.manual_seed(42) torch.manual_seed(42)
# 确定设备类型(用于选择合适的上下文管理器)
device_type = "cuda" if "cuda" in args.device else "cpu" device_type = "cuda" if "cuda" in args.device else "cpu"
# 设置混合精度训练的上下文管理器
# CPU训练时使用nullcontextGPU训练时使用autocast
ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast() ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast()
# ==================== 模型和数据初始化 ====================
# 初始化模型和分词器
model, tokenizer = init_model() model, tokenizer = init_model()
# 创建训练数据集
train_ds = PretrainDataset(args.data_path, tokenizer, max_length=max_seq_len) train_ds = PretrainDataset(args.data_path, tokenizer, max_length=max_seq_len)
# 创建数据加载器
train_loader = DataLoader( train_loader = DataLoader(
train_ds, train_ds,
batch_size=args.batch_size, batch_size=args.batch_size, # 批次大小
pin_memory=True, pin_memory=True, # 将数据加载到固定内存中加速GPU传输
drop_last=False, drop_last=False, # 不丢弃最后一个不完整的批次
shuffle=True, shuffle=True, # 随机打乱数据
num_workers=args.num_workers num_workers=args.num_workers # 数据加载的并行工作进程数
) )
# ==================== 优化器和训练组件初始化 ====================
# 初始化混合精度训练的梯度缩放器
# 只有在使用float16或bfloat16时才启用
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16'])) scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))
# 初始化Adam优化器
optimizer = optim.Adam(model.parameters(), lr=args.learning_rate) optimizer = optim.Adam(model.parameters(), lr=args.learning_rate)
# ==================== 开始训练 ====================
# 计算每个epoch的迭代次数
iter_per_epoch = len(train_loader) iter_per_epoch = len(train_loader)
# 开始训练循环
for epoch in range(args.epochs): for epoch in range(args.epochs):
train_epoch(epoch) train_epoch(epoch)

View File

@@ -17,13 +17,18 @@ from dataset import SFTDataset
import swanlab import swanlab
# 忽略警告
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
def Logger(content): def Logger(content):
"""日志记录器"""
print(content) print(content)
def get_lr(it, all): def get_lr(it, all):
"""获取学习率"""
# 1) linear warmup for warmup_iters steps
# 1) 预热迭代的线性预热
warmup_iters = args.warmup_iters warmup_iters = args.warmup_iters
lr_decay_iters = all lr_decay_iters = all
min_lr = args.learning_rate / 10 min_lr = args.learning_rate / 10
@@ -31,33 +36,42 @@ def get_lr(it, all):
if it < warmup_iters: if it < warmup_iters:
return args.learning_rate * it / warmup_iters return args.learning_rate * it / warmup_iters
# 2) if it > lr_decay_iters, return min learning rate
# 2) 如果迭代次数超过学习率衰减迭代次数,则返回最小学习率
if it > lr_decay_iters: if it > lr_decay_iters:
return min_lr return min_lr
# 3) in between, use cosine decay down to min learning rate
# 3) 在两者之间,使用余弦衰减至最小学习率
decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters) decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
assert 0 <= decay_ratio <= 1 assert 0 <= decay_ratio <= 1
coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
return min_lr + coeff * (args.learning_rate - min_lr) return min_lr + coeff * (args.learning_rate - min_lr)
def train_epoch(epoch): def train_epoch(epoch):
"""训练一个epoch"""
start_time = time.time() start_time = time.time()
for step, (X, Y, loss_mask) in enumerate(train_loader): for step, (X, Y, loss_mask) in enumerate(train_loader):
X = X.to(args.device) X = X.to(args.device)
Y = Y.to(args.device) Y = Y.to(args.device)
loss_mask = loss_mask.to(args.device) loss_mask = loss_mask.to(args.device)
# 获取学习率并更新优化器
lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch) lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch)
for param_group in optimizer.param_groups: for param_group in optimizer.param_groups:
param_group['lr'] = lr param_group['lr'] = lr
# 前向传播
with ctx: with ctx:
out = model(X, Y) out = model(X, Y)
loss = out.last_loss / args.accumulation_steps loss = out.last_loss / args.accumulation_steps
loss_mask = loss_mask.view(-1) loss_mask = loss_mask.view(-1)
loss = torch.sum(loss * loss_mask) / loss_mask.sum() loss = torch.sum(loss * loss_mask) / loss_mask.sum()
# 反向传播
scaler.scale(loss).backward() scaler.scale(loss).backward()
# 更新权重
if (step + 1) % args.accumulation_steps == 0: if (step + 1) % args.accumulation_steps == 0:
scaler.unscale_(optimizer) scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
@@ -67,6 +81,7 @@ def train_epoch(epoch):
optimizer.zero_grad(set_to_none=True) optimizer.zero_grad(set_to_none=True)
# 打印日志
if step % args.log_interval == 0: if step % args.log_interval == 0:
spend_time = time.time() - start_time spend_time = time.time() - start_time
Logger( Logger(
@@ -84,6 +99,7 @@ def train_epoch(epoch):
"lr": optimizer.param_groups[-1]['lr'] "lr": optimizer.param_groups[-1]['lr']
}) })
# 保存模型
if (step + 1) % args.save_interval == 0: if (step + 1) % args.save_interval == 0:
model.eval() model.eval()
ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}.pth' ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}.pth'
@@ -93,6 +109,7 @@ def train_epoch(epoch):
torch.save(state_dict, ckp) torch.save(state_dict, ckp)
model.train() model.train()
# 定期保存模型
if (step + 1) % 20000 == 0: if (step + 1) % 20000 == 0:
model.eval() model.eval()
ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}_step{step+1}.pth' ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}_step{step+1}.pth'
@@ -103,14 +120,19 @@ def train_epoch(epoch):
def init_model(): def init_model():
"""初始化模型"""
def count_parameters(model): def count_parameters(model):
"""计算模型参数量"""
return sum(p.numel() for p in model.parameters() if p.requires_grad) return sum(p.numel() for p in model.parameters() if p.requires_grad)
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/') tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/')
# 初始化模型
model = Transformer(lm_config) model = Transformer(lm_config)
ckp = './base_monkey_215M/pretrain_1024_18_6144.pth' # 加载预训练权重
ckp = './base_model_215M/pretrain_1024_18_6144.pth'
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
unwanted_prefix = '_orig_mod.' unwanted_prefix = '_orig_mod.'
for k, v in list(state_dict.items()): for k, v in list(state_dict.items()):
@@ -131,22 +153,22 @@ def init_model():
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining") parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining")
parser.add_argument("--out_dir", type=str, default="BeelGroup_sft_model_215M", help="Output directory") parser.add_argument("--out_dir", type=str, default="sft_model_215M", help="输出目录")
parser.add_argument("--epochs", type=int, default=1, help="Number of epochs") parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
parser.add_argument("--batch_size", type=int, default=64, help="Batch size") parser.add_argument("--batch_size", type=int, default=64, help="批处理大小")
parser.add_argument("--learning_rate", type=float, default=2e-4, help="Learning rate") parser.add_argument("--learning_rate", type=float, default=2e-4, help="学习率")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="Device to use") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="使用的设备")
parser.add_argument("--dtype", type=str, default="bfloat16", help="Data type") parser.add_argument("--dtype", type=str, default="bfloat16", help="数据类型")
parser.add_argument("--use_swanlab", type=bool, default=True, help="Use Weights & Biases") parser.add_argument("--use_swanlab", action="store_true", help="是否使用SwanLab进行实验跟踪")
parser.add_argument("--num_workers", type=int, default=4, help="Number of workers for data loading") parser.add_argument("--num_workers", type=int, default=8, help="数据加载的工作进程数")
parser.add_argument("--data_path", type=str, default="", help="Path to training data") parser.add_argument("--data_path", type=str, default="./BelleGroup_sft.jsonl", help="训练数据路径")
parser.add_argument("--accumulation_steps", type=int, default=4, help="Gradient accumulation steps") parser.add_argument("--accumulation_steps", type=int, default=8, help="梯度累积步数")
parser.add_argument("--grad_clip", type=float, default=1.0, help="Gradient clipping threshold") parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
parser.add_argument("--warmup_iters", type=int, default=0, help="Number of warmup iterations") parser.add_argument("--warmup_iters", type=int, default=0, help="预热迭代次数")
parser.add_argument("--log_interval", type=int, default=100, help="Logging interval") parser.add_argument("--log_interval", type=int, default=100, help="日志记录间隔")
parser.add_argument("--save_interval", type=int, default=1000, help="Model saving interval") parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
# 添加多卡参数 # 添加多卡参数
parser.add_argument("--gpus", type=str, default='0,1', help="Comma-separated GPU IDs (e.g. '0,1,2')") parser.add_argument("--gpus", type=str, default='0,1,2,3,4,5,6,7', help="逗号分隔的GPU ID (例如 '0,1,2')")
args = parser.parse_args() args = parser.parse_args()
@@ -159,14 +181,15 @@ if __name__ == "__main__":
else: else:
args.device = "cpu" args.device = "cpu"
# 初始化swanlab
if args.use_swanlab: if args.use_swanlab:
swanlab.login(api_key='your key')
run = swanlab.init( run = swanlab.init(
project="Tiny-LLM", project="Happy-LLM",
experiment_name="BelleGropu-sft-215M", experiment_name="SFT-215M",
config=args, config=args,
) )
# 模型配置
lm_config = ModelConfig( lm_config = ModelConfig(
dim=1024, dim=1024,
n_layers=18, n_layers=18,
@@ -178,10 +201,13 @@ if __name__ == "__main__":
torch.manual_seed(42) torch.manual_seed(42)
device_type = "cuda" if "cuda" in args.device else "cpu" device_type = "cuda" if "cuda" in args.device else "cpu"
# 上下文管理器
ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast() ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast()
# 初始化模型和分词器
model, tokenizer = init_model() model, tokenizer = init_model()
# 创建数据集和数据加载器
train_ds = SFTDataset(args.data_path, tokenizer, max_length=max_seq_len) train_ds = SFTDataset(args.data_path, tokenizer, max_length=max_seq_len)
train_loader = DataLoader( train_loader = DataLoader(
train_ds, train_ds,
@@ -192,9 +218,11 @@ if __name__ == "__main__":
num_workers=args.num_workers num_workers=args.num_workers
) )
# 缩放器和优化器
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16'])) scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))
optimizer = optim.Adam(model.parameters(), lr=args.learning_rate) optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
# 开始训练
iter_per_epoch = len(train_loader) iter_per_epoch = len(train_loader)
for epoch in range(args.epochs): for epoch in range(args.epochs):
train_epoch(epoch) train_epoch(epoch)

View File

@@ -1,32 +1,24 @@
import os import os
from tqdm import tqdm
import json import json
from tqdm import tqdm
# 设置环境变量 # pretrain_data 为运行download_dataset.sh时下载的pretrain_data本地路径
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' pretrain_data = 'your local pretrain_data'
output_pretrain_data = 'seq_monkey_datawhale.jsonl'
# 下载预训练数据集
os.system("modelscope download --dataset ddzhu123/seq-monkey mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2 --local_dir your_local_dir")
# 解压预训练数据集
os.system("tar -xvf your_local_dir/mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2")
# 下载SFT数据集
os.system(f'huggingface-cli download --repo-type dataset --resume-download BelleGroup/train_3.5M_CN --local-dir BelleGroup')
# sft_data 为运行download_dataset.sh时下载的sft_data本地路径
sft_data = 'your local sft_data'
output_sft_data = 'BelleGroup_sft.jsonl'
# 1 处理预训练数据 # 1 处理预训练数据
def split_text(text, chunk_size=512): def split_text(text, chunk_size=512):
"""将文本按指定长度切分成块""" """将文本按指定长度切分成块"""
return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)] return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
input_file = 'mobvoi_seq_monkey_general_open_corpus.jsonl' with open(output_pretrain_data, 'a', encoding='utf-8') as pretrain:
with open(pretrain_data, 'r', encoding='utf-8') as f:
with open('seq_monkey_datawhale.jsonl', 'a', encoding='utf-8') as pretrain:
with open(input_file, 'r', encoding='utf-8') as f:
data = f.readlines() data = f.readlines()
for line in tqdm(data, desc=f"Processing lines in {input_file}", leave=False): # 添加行级别的进度条 for line in tqdm(data, desc=f"Processing lines in {pretrain_data}", leave=False): # 添加行级别的进度条
line = json.loads(line) line = json.loads(line)
text = line['text'] text = line['text']
chunks = split_text(text) chunks = split_text(text)
@@ -34,7 +26,6 @@ with open('seq_monkey_datawhale.jsonl', 'a', encoding='utf-8') as pretrain:
pretrain.write(json.dumps({'text': chunk}, ensure_ascii=False) + '\n') pretrain.write(json.dumps({'text': chunk}, ensure_ascii=False) + '\n')
# 2 处理SFT数据 # 2 处理SFT数据
def convert_message(data): def convert_message(data):
""" """
将原始数据转换为标准格式 将原始数据转换为标准格式
@@ -49,8 +40,8 @@ def convert_message(data):
message.append({'role': 'assistant', 'content': item['value']}) message.append({'role': 'assistant', 'content': item['value']})
return message return message
with open('BelleGroup_sft.jsonl', 'a', encoding='utf-8') as sft: with open(output_sft_data, 'a', encoding='utf-8') as sft:
with open('BelleGroup/train_3.5M_CN.json', 'r') as f: with open(sft_data, 'r') as f:
data = f.readlines() data = f.readlines()
for item in tqdm(data, desc="Processing", unit="lines"): for item in tqdm(data, desc="Processing", unit="lines"):
item = json.loads(item) item = json.loads(item)

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# 设置环境变量
export HF_ENDPOINT=https://hf-mirror.com
# dataset dir 下载到本地目录
dataset_dir="your local dataset dir"
# 下载预训练数据集, 需要预先安装modelscope使用pip3 install modelscope安装
modelscope download --dataset ddzhu123/seq-monkey mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2 --local_dir ${dataset_dir}
# 解压预训练数据集
tar -xvf "${dataset_dir}/mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2" -C "${dataset_dir}"
# 下载SFT数据集
huggingface-cli download \
--repo-type dataset \
--resume-download \
BelleGroup/train_3.5M_CN \
--local-dir "${dataset_dir}/BelleGroup"

View File

@@ -8,7 +8,7 @@ import argparse
class TextGenerator: class TextGenerator:
def __init__(self, def __init__(self,
checkpoint='out/SkyWork_pretrain_768_12_6144.pth', # 模型检查点路径 checkpoint='./base_model_215M/pretrain_1024_18_6144.pth', # 模型检查点路径
tokenizer_model_path='./tokenizer_k/', # 分词器模型路径 tokenizer_model_path='./tokenizer_k/', # 分词器模型路径
seed=42, # 随机种子,确保可重复性 seed=42, # 随机种子,确保可重复性
device=None, # 设备,优先使用 CUDA如果没有可用的 CUDA则使用 CPU device=None, # 设备,优先使用 CUDA如果没有可用的 CUDA则使用 CPU
@@ -55,7 +55,7 @@ class TextGenerator:
def chat_template(self, prompt): def chat_template(self, prompt):
message = [ message = [
{"role": "system", "content": "你是一个AI助手。"}, {"role": "system", "content": "你是一个AI助手,你的名字叫小明"},
{"role": "user", "content": prompt} {"role": "user", "content": prompt}
] ]
return self.tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=True) return self.tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=True)
@@ -126,18 +126,6 @@ class TextGenerator:
return generated_texts # 返回生成的文本样本 return generated_texts # 返回生成的文本样本
if __name__ == "__main__": if __name__ == "__main__":
print("\n ------------------- SFT Sample ------------------- \n")
sft_prompt_datas = [
'你好呀',
"中国的首都是哪里?",
"1+1等于多少",
]
generator = TextGenerator(checkpoint='./BeelGroup_sft_model_215M/sft_dim1024_layers18_vocab_size6144.pth') # 初始化生成器
for i in range(len(sft_prompt_datas)):
samples = generator.sft_sample(start=sft_prompt_datas[i], num_samples=1, max_new_tokens=512, temperature=0.75)
print(f"\nSample {i+1}:\nQuestion: {sft_prompt_datas[i]} \nAI answer: {samples[0]}\n{'-'*20}") # 打印生成的样本并用分隔线分割
print("------------------- Pretrain Sample ------------------- \n") print("------------------- Pretrain Sample ------------------- \n")
pretrain_prompt_datas = [ pretrain_prompt_datas = [
@@ -145,7 +133,22 @@ if __name__ == "__main__":
'<|im_start|>中国矿业大学(北京)地球科学与测绘工程学院', '<|im_start|>中国矿业大学(北京)地球科学与测绘工程学院',
] ]
generator = TextGenerator(checkpoint='./base_monkey_215M/pretrain_1024_18_6144.pth') # 初始化生成器 generator = TextGenerator(checkpoint='./base_model_215M/pretrain_1024_18_6144.pth') # 初始化生成器
for i in range(len(pretrain_prompt_datas)): for i in range(len(pretrain_prompt_datas)):
samples = generator.pretrain_sample(start=pretrain_prompt_datas[i], num_samples=1, max_new_tokens=120, temperature=1.0) samples = generator.pretrain_sample(start=pretrain_prompt_datas[i], num_samples=1, max_new_tokens=120, temperature=0.75)
print(f"\nSample {i+1}:\n{pretrain_prompt_datas[i]}{samples[0]}\n{'-'*20}") # 打印生成的样本并用分隔线分割 print(f"\nSample {i+1}:\n{pretrain_prompt_datas[i]}{samples[0]}\n{'-'*20}") # 打印生成的样本并用分隔线分割
print("\n ------------------- SFT Sample ------------------- \n")
sft_prompt_datas = [
'你好呀',
"中国的首都是哪里?",
"1+12等于多少",
"你是谁?"
]
generator = TextGenerator(checkpoint='./sft_model_215M/sft_dim1024_layers18_vocab_size6144.pth') # 初始化生成器
for i in range(len(sft_prompt_datas)):
samples = generator.sft_sample(start=sft_prompt_datas[i], num_samples=1, max_new_tokens=128, temperature=0.6)
print(f"\nSample {i+1}:\nQuestion: {sft_prompt_datas[i]} \nAI answer: {samples[0]}\n{'-'*20}") # 打印生成的样本并用分隔线分割

View File

@@ -0,0 +1,35 @@
# Windows下载方式
# 使用PowerShell下载
# 暂时为当前PowerShell界面设置环境关闭Powershell环境自动消失
$env:HF_ENDPOINT = "https://hf-mirror.com"
# 将\path\to\your\dataset替换成想要下载dataset目录地址
$dataset_dir = "\path\to\your\dataset"
# 需要预先安装modelscope使用pip install modelscope安装
modelscope download --dataset ddzhu123/seq-monkey mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2 --local_dir "$dataset_dir"
tar -xvf "$dataset_dir\mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2" -C "$dataset_dir"
huggingface-cli download `
--repo-type dataset `
--resume-download `
BelleGroup/train_3.5M_CN `
--local-dir "$dataset_dir\BelleGroup"
# 使用CMD下载
# 暂时为当前CMD界面设置环境关闭CMD环境自动消失
set HF_ENDPOINT=https://hf-mirror.com
# 将\path\to\your\dataset替换成想要下载dataset目录地址
set dataset_dir=\path\to\your\dataset
modelscope download --dataset ddzhu123/seq-monkey mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2 --local_dir %dataset_dir%
tar -xvf "%dataset_dir%\mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2" -C "%dataset_dir%"
huggingface-cli download ^
--repo-type dataset ^
--resume-download ^
BelleGroup/train_3.5M_CN ^
--local-dir "%dataset_dir%\BelleGroup"

View File

@@ -4,11 +4,11 @@
Meta原Facebook于2023年2月发布第一款基于Transformer结构的大型语言模型LLaMA并于同年7月发布同系列模型LLaMA2。我们在第四章已经学习了解的了LLM记忆如何训练LLM等等。那本小节我们就来学习如何动手写一个LLaMA2模型。 Meta原Facebook于2023年2月发布第一款基于Transformer结构的大型语言模型LLaMA并于同年7月发布同系列模型LLaMA2。我们在第四章已经学习了解的了LLM记忆如何训练LLM等等。那本小节我们就来学习如何动手写一个LLaMA2模型。
LLaMA2 模型结构如下图5.0所示: LLaMA2 模型结构如下图5.1所示:
<div align='center'> <div align='center'>
<img src="../images/5-images/LLama2.png" alt="alt text" width="100%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/5-images/LLama2.png" alt="alt text" width="100%">
<p>图 5.0 LLaMA2结构</p> <p>图 5.1 LLaMA2结构</p>
</div> </div>
### 5.1.1 定义超参数 ### 5.1.1 定义超参数
@@ -51,6 +51,8 @@ class ModelConfig(PretrainedConfig):
super().__init__(**kwargs) super().__init__(**kwargs)
``` ```
> 在以下代码中出现 `args` 时,即默认为以上 `ModelConfig` 参数配置。
我们来看一下其中的一些超参数的含义,比如`dim`是模型维度,`n_layers`是Transformer的层数`n_heads`是注意力机制的头数,`vocab_size`是词汇表大小,`max_seq_len`是输入的最大序列长度等等。上面的代码中也对每一个参数做了详细的注释,在后面的代码中我们会根据这些超参数来构建我们的模型。 我们来看一下其中的一些超参数的含义,比如`dim`是模型维度,`n_layers`是Transformer的层数`n_heads`是注意力机制的头数,`vocab_size`是词汇表大小,`max_seq_len`是输入的最大序列长度等等。上面的代码中也对每一个参数做了详细的注释,在后面的代码中我们会根据这些超参数来构建我们的模型。
### 5.1.2 构建 RMSNorm ### 5.1.2 构建 RMSNorm
@@ -104,13 +106,18 @@ output = norm(x)
print(output.shape) print(output.shape)
out: out:
orch.Size([1, 50, 288]) torch.Size([1, 50, 768])
``` ```
### 5.1.3 构建 LLaMA2 Attention ### 5.1.3 构建 LLaMA2 Attention
在 LLaMA2 模型中,虽然只有 LLaMA2-70B模型使用了分组查询注意力机制Grouped-Query AttentionGQA但我们依然选择使用 GQA 来构建我们的 LLaMA Attention 模块,它可以提高模型的效率,并节省一些显存占用。 在 LLaMA2 模型中,虽然只有 LLaMA2-70B模型使用了分组查询注意力机制Grouped-Query AttentionGQA但我们依然选择使用 GQA 来构建我们的 LLaMA Attention 模块,它可以提高模型的效率,并节省一些显存占用。
<div align='center'>
<img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/5-images/llama2-attention.png" alt="alt text" width="50%">
<p>图 5.2 LLaMA2 Attention 结构</p>
</div>
#### 5.1.3.1 repeat_kv #### 5.1.3.1 repeat_kv
在 LLaMA2 模型中,我们需要将键和值的维度扩展到和查询的维度一样,这样才能进行注意力计算。我们可以通过如下代码实现`repeat_kv` 在 LLaMA2 模型中,我们需要将键和值的维度扩展到和查询的维度一样,这样才能进行注意力计算。我们可以通过如下代码实现`repeat_kv`
@@ -759,34 +766,26 @@ from typing import Generator
#### Step 2: 加载训练数据 #### Step 2: 加载训练数据
我们使用 `datasets.load_dataset()` 库加载一个英文文本数据集,用于训练 BPE Tokenizer。这里我们使用 `wikitext` 数据集,包含了维基百科的文章文本 这里我们使用与预训练相同的数据集出门问问序列猴子开源数据集训练tokenizer可使用`code/download_dataset.sh``code/deal_dataset.py` 下载和预处理数据集
> 注:由于数据集过大,可能会导致在训练过程中内存不足。因为本项目为学习目的,建议学习者手动分割小部分数据集用于训练验证,笔者也在 Github 仓库中存放了训练好的 tokenizer可以直接使用。
```python ```python
dataset = load_dataset("wikitext", "wikitext-103-v1", split="train") def read_texts_from_jsonl(file_path: str) -> Generator[str, None, None]:
"""读取JSONL文件并安全提取文本数据"""
# 准备训练数据 with open(file_path, 'r', encoding='utf-8') as f:
def batch_iterator(batch_size=1000): for line_num, line in enumerate(f, 1):
for i in range(0, len(dataset), batch_size): try:
yield dataset[i:i + batch_size]["text"] data = json.loads(line)
``` if 'text' not in data:
raise KeyError(f"Missing 'text' field in line {line_num}")
如果你使用本地的文本数据集,可以将数据加载到一个列表中,然后传入 `batch_iterator()` 函数中。如下所示: yield data['text']
except json.JSONDecodeError:
```python print(f"Error decoding JSON in line {line_num}")
def load_text_from_files(path_list): continue
text_data = [] except KeyError as e:
for file_path in path_list: print(e)
with open(file_path, 'r', encoding='utf-8') as file: continue
text_data.extend(file.readlines())
return text_data
def batch_iterator(text_data, batch_size=1000):
for i in range(0, len(text_data), batch_size):
yield text_data[i:i + batch_size]
# 假设你的文件路径列表是
path_list = ['text_data1.txt', 'text_data2.txt', 'text_data3.txt']
text_data = load_text_from_files(path_list)
``` ```
#### Step 3: 创建配置文件 #### Step 3: 创建配置文件
@@ -1330,11 +1329,11 @@ class PretrainDataset(Dataset):
return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(loss_mask) return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(loss_mask)
``` ```
在以上代码和图5.1可以看出,`Pretrain Dataset` 主要是将 `text` 通过 `tokenizer` 转换成 `input_id`,然后将 `input_id` 拆分成 `X``Y`,其中 `X``input_id` 的前 n-1 个元素,`Y``input_id` 的后 n-1 `个元素。loss_mask` 主要是用来标记哪些位置需要计算损失,哪些位置不需要计算损失。 在以上代码和图5.3可以看出,`Pretrain Dataset` 主要是将 `text` 通过 `tokenizer` 转换成 `input_id`,然后将 `input_id` 拆分成 `X``Y`,其中 `X``input_id` 的前 n-1 个元素,`Y``input_id` 的后 n-1 `个元素。loss_mask` 主要是用来标记哪些位置需要计算损失,哪些位置不需要计算损失。
<div align='center'> <div align='center'>
<img src="../images/5-images/pretrain_dataset.png" alt="alt text" width="100%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/5-images/pretrain_dataset.png" alt="alt text" width="100%">
<p>图5.1 预训练损失函数计算</p> <p>图5.3 预训练损失函数计算</p>
</div> </div>
图中示例展示了当`max_length=9`时的处理过程: 图中示例展示了当`max_length=9`时的处理过程:
@@ -1417,11 +1416,11 @@ class SFTDataset(Dataset):
return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(loss_mask) return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(loss_mask)
``` ```
在 SFT 阶段,这里使用的是多轮对话数据集,所以就需要区分哪些位置需要计算损失,哪些位置不需要计算损失。在上面的代码中,我使用了一个 `generate_loss_mask` 函数来生成 `loss_mask`。这个函数主要是用来生成 `loss_mask`,其中 `loss_mask` 的生成规则是:当遇到 `|<im_start|>assistant\n` 时,就开始计算损失,直到遇到 `|<im_end|>` 为止。这样就可以保证我们的模型在 SFT 阶段只计算当前轮的对话内容如图5.2所示。 在 SFT 阶段,这里使用的是多轮对话数据集,所以就需要区分哪些位置需要计算损失,哪些位置不需要计算损失。在上面的代码中,我使用了一个 `generate_loss_mask` 函数来生成 `loss_mask`。这个函数主要是用来生成 `loss_mask`,其中 `loss_mask` 的生成规则是:当遇到 `|<im_start|>assistant\n` 时,就开始计算损失,直到遇到 `|<im_end|>` 为止。这样就可以保证我们的模型在 SFT 阶段只计算当前轮的对话内容如图5.4所示。
<div align='center'> <div align='center'>
<img src="../images/5-images/sftdataset.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/5-images/sftdataset.png" alt="alt text" width="90%">
<p>图5.2 SFT 损失函数计算</p> <p>图5.4 SFT 损失函数计算</p>
</div> </div>
可以看到,其实 SFT Dataset 和 Pretrain Dataset 的 `X``Y` 是一样的,只是在 SFT Dataset 中我们需要生成一个 `loss_mask` 来标记哪些位置需要计算损失,哪些位置不需要计算损失。 图中 `Input ids` 中的蓝色小方格就是AI的回答所以是需要模型学习的地方。所以在 `loss_mask` 中,蓝色小方格对应的位置是黄色,其他位置是灰色。在代码 `loss_mask` 中的 1 对应的位置计算损失0 对应的位置不计算损失。 可以看到,其实 SFT Dataset 和 Pretrain Dataset 的 `X``Y` 是一样的,只是在 SFT Dataset 中我们需要生成一个 `loss_mask` 来标记哪些位置需要计算损失,哪些位置不需要计算损失。 图中 `Input ids` 中的蓝色小方格就是AI的回答所以是需要模型学习的地方。所以在 `loss_mask` 中,蓝色小方格对应的位置是黄色,其他位置是灰色。在代码 `loss_mask` 中的 1 对应的位置计算损失0 对应的位置不计算损失。
@@ -1480,169 +1479,294 @@ class SFTDataset(Dataset):
```python ```python
def get_lr(it, all): def get_lr(it, all):
warmup_iters = args.warmup_iters """
lr_decay_iters = all 计算当前迭代的学习率,使用余弦退火调度策略
min_lr = args.learning_rate / 10
学习率调度策略:
1. Warmup阶段学习率从0线性增长到目标学习率
2. 余弦退火阶段:学习率按余弦函数衰减到最小学习率
3. 超出训练步数后:保持最小学习率
Args:
it (int): 当前迭代步数
all (int): 总迭代步数
Returns:
float: 当前步数对应的学习率
"""
warmup_iters = args.warmup_iters # 预热迭代次数
lr_decay_iters = all # 学习率衰减的总迭代次数
min_lr = args.learning_rate / 10 # 最小学习率为初始学习率的1/10
# Warmup阶段线性增长
if it < warmup_iters: if it < warmup_iters:
return args.learning_rate * it / warmup_iters return args.learning_rate * it / warmup_iters
# 超出训练步数:保持最小学习率
if it > lr_decay_iters: if it > lr_decay_iters:
return min_lr return min_lr
# 余弦退火阶段
decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters) decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
assert 0 <= decay_ratio <= 1 assert 0 <= decay_ratio <= 1
coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) # 余弦系数
return min_lr + coeff * (args.learning_rate - min_lr) return min_lr + coeff * (args.learning_rate - min_lr)
def train_epoch(epoch): def train_epoch(epoch):
start_time = time.time() """
for step, (X, Y, loss_mask) in enumerate(train_loader): 训练一个epoch的函数
X = X.to(args.device)
Y = Y.to(args.device)
loss_mask = loss_mask.to(args.device)
实现了完整的训练循环,包括:
1. 数据加载和设备转移
2. 动态学习率调整
3. 前向传播和损失计算
4. 梯度累积和反向传播
5. 梯度裁剪和优化器更新
6. 日志记录和模型保存
Args:
epoch (int): 当前epoch编号
"""
start_time = time.time() # 记录开始时间
# 遍历数据加载器中的每个batch
for step, (X, Y, loss_mask) in enumerate(train_loader):
# 将数据转移到指定设备GPU/CPU
X = X.to(args.device) # 输入序列
Y = Y.to(args.device) # 目标序列
loss_mask = loss_mask.to(args.device) # 损失掩码用于忽略padding token
# 计算当前步骤的学习率
lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch) lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch)
# 更新优化器中所有参数组的学习率
for param_group in optimizer.param_groups: for param_group in optimizer.param_groups:
param_group['lr'] = lr param_group['lr'] = lr
# 使用混合精度训练上下文
with ctx: with ctx:
# 前向传播
out = model(X, Y) out = model(X, Y)
# 计算损失并除以累积步数(用于梯度累积)
loss = out.last_loss / args.accumulation_steps loss = out.last_loss / args.accumulation_steps
# 将loss_mask展平为一维
loss_mask = loss_mask.view(-1) loss_mask = loss_mask.view(-1)
# 应用掩码计算有效损失忽略padding位置
loss = torch.sum(loss * loss_mask) / loss_mask.sum() loss = torch.sum(loss * loss_mask) / loss_mask.sum()
# 使用scaler进行混合精度的反向传播
scaler.scale(loss).backward() scaler.scale(loss).backward()
# 每accumulation_steps步执行一次优化器更新
if (step + 1) % args.accumulation_steps == 0: if (step + 1) % args.accumulation_steps == 0:
# 取消梯度缩放,准备梯度裁剪
scaler.unscale_(optimizer) scaler.unscale_(optimizer)
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
# 执行优化器步骤
scaler.step(optimizer) scaler.step(optimizer)
# 更新scaler的缩放因子
scaler.update() scaler.update()
# 清零梯度set_to_none=True可以节省内存
optimizer.zero_grad(set_to_none=True) optimizer.zero_grad(set_to_none=True)
# 每log_interval步记录一次日志
if step % args.log_interval == 0: if step % args.log_interval == 0:
spend_time = time.time() - start_time spend_time = time.time() - start_time
# 打印训练进度信息
Logger( Logger(
'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.7f} epoch_Time:{}min:'.format( 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.7f} epoch_Time:{}min;'.format(
epoch + 1, epoch + 1,
args.epochs, args.epochs,
step, step,
iter_per_epoch, iter_per_epoch,
loss.item() * args.accumulation_steps, loss.item() * args.accumulation_steps, # 恢复真实的loss值
optimizer.param_groups[-1]['lr'], optimizer.param_groups[-1]['lr'],
spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60)) spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))
# 如果启用SwanLab记录训练指标
if args.use_swanlab: if args.use_swanlab:
swanlab.log({ swanlab.log({
"loss": loss.item() * args.accumulation_steps, "loss": loss.item() * args.accumulation_steps,
"lr": optimizer.param_groups[-1]['lr'] "lr": optimizer.param_groups[-1]['lr']
}) })
# 每save_interval步保存一次模型
if (step + 1) % args.save_interval == 0: if (step + 1) % args.save_interval == 0:
model.eval() model.eval() # 切换到评估模式
# 构建检查点文件名
ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}.pth' ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}.pth'
# 处理多卡保存 # 处理多卡保存如果是DataParallel模型需要访问.module属性
state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict() state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
torch.save(state_dict, ckp) torch.save(state_dict, ckp)
model.train() model.train() # 切换回训练模式
# 每20000步保存一个带步数标记的检查点
if (step + 1) % 20000 == 0: if (step + 1) % 20000 == 0:
model.eval() model.eval()
# 构建带步数的检查点文件名
ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step+1}.pth' ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step+1}.pth'
# 保存模型状态字典
state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict() state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
torch.save(state_dict, ckp) torch.save(state_dict, ckp)
model.train() model.train()
def init_model(): def init_model():
"""
初始化模型和分词器
功能包括:
1. 加载预训练的分词器
2. 创建Transformer模型
3. 设置多GPU并行训练如果可用
4. 将模型移动到指定设备
5. 统计并打印模型参数量
Returns:
tuple: (model, tokenizer) 初始化后的模型和分词器
"""
def count_parameters(model): def count_parameters(model):
"""
统计模型中可训练参数的数量
Args:
model: PyTorch模型
Returns:
int: 可训练参数总数
"""
return sum(p.numel() for p in model.parameters() if p.requires_grad) return sum(p.numel() for p in model.parameters() if p.requires_grad)
# 从本地路径加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/') tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/')
# 根据配置创建Transformer模型
model = Transformer(lm_config) model = Transformer(lm_config)
# 多卡初始化 # 多卡初始化检查可用GPU数量并设置DataParallel
num_gpus = torch.cuda.device_count() num_gpus = torch.cuda.device_count()
if num_gpus > 1: if num_gpus > 1:
Logger(f"Using {num_gpus} GPUs with DataParallel!") Logger(f"Using {num_gpus} GPUs with DataParallel!")
# 使用DataParallel包装模型以支持多GPU训练
model = torch.nn.DataParallel(model) model = torch.nn.DataParallel(model)
# 将模型移动到指定设备GPU或CPU
model = model.to(args.device) model = model.to(args.device)
# 计算并打印模型参数量(以百万为单位)
Logger(f'LLM总参数量{count_parameters(model) / 1e6:.3f} 百万') Logger(f'LLM总参数量{count_parameters(model) / 1e6:.3f} 百万')
return model, tokenizer return model, tokenizer
if __name__ == "__main__": if __name__ == "__main__":
# ==================== 命令行参数解析 ====================
parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining") parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining")
parser.add_argument("--out_dir", type=str, default="output", help="Output directory")
parser.add_argument("--epochs", type=int, default=1, help="Number of epochs") # 基础训练参数
parser.add_argument("--batch_size", type=int, default=64, help="Batch size") parser.add_argument("--out_dir", type=str, default="base_model_215M", help="模型输出目录")
parser.add_argument("--learning_rate", type=float, default=2e-4, help="Learning rate") parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="Device to use") parser.add_argument("--batch_size", type=int, default=64, help="批次大小")
parser.add_argument("--dtype", type=str, default="bfloat16", help="Data type") parser.add_argument("--learning_rate", type=float, default=2e-4, help="学习率")
parser.add_argument("--use_swanlab", type=bool, default=True, help="Use Weights & Biases") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
parser.add_argument("--num_workers", type=int, default=8, help="Number of workers for data loading") parser.add_argument("--dtype", type=str, default="bfloat16", help="数据类型")
parser.add_argument("--data_path", type=str, default="", help="Path to training data")
parser.add_argument("--accumulation_steps", type=int, default=8, help="Gradient accumulation steps") # 实验跟踪和数据加载参数
parser.add_argument("--grad_clip", type=float, default=1.0, help="Gradient clipping threshold") parser.add_argument("--use_swanlab", action="store_true", help="是否使用SwanLab进行实验跟踪")
parser.add_argument("--warmup_iters", type=int, default=0, help="Number of warmup iterations") parser.add_argument("--num_workers", type=int, default=8, help="数据加载的工作进程数")
parser.add_argument("--log_interval", type=int, default=100, help="Logging interval") parser.add_argument("--data_path", type=str, default="./seq_monkey_datawhale.jsonl", help="训练数据路径")
parser.add_argument("--save_interval", type=int, default=1000, help="Model saving interval")
# 添加多卡参数 # 训练优化参数
parser.add_argument("--gpus", type=str, default='0,1', help="Comma-separated GPU IDs (e.g. '0,1,2')") parser.add_argument("--accumulation_steps", type=int, default=8, help="梯度累积步数")
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
parser.add_argument("--warmup_iters", type=int, default=0, help="学习率预热迭代次数")
# 日志和保存参数
parser.add_argument("--log_interval", type=int, default=100, help="日志记录间隔")
parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
# 多GPU训练参数
parser.add_argument("--gpus", type=str, default='0,1,2,3,4,5,6,7', help="使用的GPU ID用逗号分隔 (例如: '0,1,2')")
args = parser.parse_args() args = parser.parse_args()
# 设置可见GPU # ==================== GPU环境设置 ====================
# 设置可见的GPU设备
if args.gpus is not None: if args.gpus is not None:
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpus os.environ["CUDA_VISIBLE_DEVICES"] = args.gpus
# 自动设置主设备为第一个GPU # 自动设置主设备为第一个可用GPU
if torch.cuda.is_available(): if torch.cuda.is_available():
args.device = "cuda:0" args.device = "cuda:0"
else: else:
args.device = "cpu" args.device = "cpu"
# ==================== 实验跟踪初始化 ====================
if args.use_swanlab: if args.use_swanlab:
swanlab.login(api_key='your key') # 注意:使用前需要先登录 swanlab.login(api_key='your key')
run = swanlab.init( run = swanlab.init(
project="Tiny-LLM", project="Happy-LLM", # 项目名称
experiment_name="Pretrain-215M", experiment_name="Pretrain-215M", # 实验名称
config=args, config=args, # 保存所有超参数
) )
# ==================== 模型配置 ====================
# 定义语言模型的配置参数
lm_config = ModelConfig( lm_config = ModelConfig(
dim=1024, dim=1024, # 模型维度
n_layers=18, n_layers=18, # Transformer层数
) )
max_seq_len = lm_config.max_seq_len
args.save_dir = os.path.join(args.out_dir) # ==================== 训练环境设置 ====================
max_seq_len = lm_config.max_seq_len # 最大序列长度
args.save_dir = os.path.join(args.out_dir) # 模型保存目录
# 创建必要的目录
os.makedirs(args.save_dir, exist_ok=True) os.makedirs(args.save_dir, exist_ok=True)
os.makedirs(args.out_dir, exist_ok=True) os.makedirs(args.out_dir, exist_ok=True)
# 设置随机种子以确保结果可复现
torch.manual_seed(42) torch.manual_seed(42)
# 确定设备类型(用于选择合适的上下文管理器)
device_type = "cuda" if "cuda" in args.device else "cpu" device_type = "cuda" if "cuda" in args.device else "cpu"
# 设置混合精度训练的上下文管理器
# CPU训练时使用nullcontextGPU训练时使用autocast
ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast() ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast()
# ==================== 模型和数据初始化 ====================
# 初始化模型和分词器
model, tokenizer = init_model() model, tokenizer = init_model()
# 创建训练数据集
train_ds = PretrainDataset(args.data_path, tokenizer, max_length=max_seq_len) train_ds = PretrainDataset(args.data_path, tokenizer, max_length=max_seq_len)
# 创建数据加载器
train_loader = DataLoader( train_loader = DataLoader(
train_ds, train_ds,
batch_size=args.batch_size, batch_size=args.batch_size, # 批次大小
pin_memory=True, pin_memory=True, # 将数据加载到固定内存中加速GPU传输
drop_last=False, drop_last=False, # 不丢弃最后一个不完整的批次
shuffle=True, shuffle=True, # 随机打乱数据
num_workers=args.num_workers num_workers=args.num_workers # 数据加载的并行工作进程数
) )
# ==================== 优化器和训练组件初始化 ====================
# 初始化混合精度训练的梯度缩放器
# 只有在使用float16或bfloat16时才启用
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16'])) scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))
# 初始化Adam优化器
optimizer = optim.Adam(model.parameters(), lr=args.learning_rate) optimizer = optim.Adam(model.parameters(), lr=args.learning_rate)
# ==================== 开始训练 ====================
# 计算每个epoch的迭代次数
iter_per_epoch = len(train_loader) iter_per_epoch = len(train_loader)
# 开始训练循环
for epoch in range(args.epochs): for epoch in range(args.epochs):
train_epoch(epoch) train_epoch(epoch)
``` ```
@@ -1671,13 +1795,18 @@ from dataset import SFTDataset
import swanlab import swanlab
# 忽略警告
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
def Logger(content): def Logger(content):
"""日志记录器"""
print(content) print(content)
def get_lr(it, all): def get_lr(it, all):
"""获取学习率"""
# 1) linear warmup for warmup_iters steps
# 1) 预热迭代的线性预热
warmup_iters = args.warmup_iters warmup_iters = args.warmup_iters
lr_decay_iters = all lr_decay_iters = all
min_lr = args.learning_rate / 10 min_lr = args.learning_rate / 10
@@ -1685,33 +1814,42 @@ def get_lr(it, all):
if it < warmup_iters: if it < warmup_iters:
return args.learning_rate * it / warmup_iters return args.learning_rate * it / warmup_iters
# 2) if it > lr_decay_iters, return min learning rate
# 2) 如果迭代次数超过学习率衰减迭代次数,则返回最小学习率
if it > lr_decay_iters: if it > lr_decay_iters:
return min_lr return min_lr
# 3) in between, use cosine decay down to min learning rate
# 3) 在两者之间,使用余弦衰减至最小学习率
decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters) decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
assert 0 <= decay_ratio <= 1 assert 0 <= decay_ratio <= 1
coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
return min_lr + coeff * (args.learning_rate - min_lr) return min_lr + coeff * (args.learning_rate - min_lr)
def train_epoch(epoch): def train_epoch(epoch):
"""训练一个epoch"""
start_time = time.time() start_time = time.time()
for step, (X, Y, loss_mask) in enumerate(train_loader): for step, (X, Y, loss_mask) in enumerate(train_loader):
X = X.to(args.device) X = X.to(args.device)
Y = Y.to(args.device) Y = Y.to(args.device)
loss_mask = loss_mask.to(args.device) loss_mask = loss_mask.to(args.device)
# 获取学习率并更新优化器
lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch) lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch)
for param_group in optimizer.param_groups: for param_group in optimizer.param_groups:
param_group['lr'] = lr param_group['lr'] = lr
# 前向传播
with ctx: with ctx:
out = model(X, Y) out = model(X, Y)
loss = out.last_loss / args.accumulation_steps loss = out.last_loss / args.accumulation_steps
loss_mask = loss_mask.view(-1) loss_mask = loss_mask.view(-1)
loss = torch.sum(loss * loss_mask) / loss_mask.sum() loss = torch.sum(loss * loss_mask) / loss_mask.sum()
# 反向传播
scaler.scale(loss).backward() scaler.scale(loss).backward()
# 更新权重
if (step + 1) % args.accumulation_steps == 0: if (step + 1) % args.accumulation_steps == 0:
scaler.unscale_(optimizer) scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
@@ -1721,6 +1859,7 @@ def train_epoch(epoch):
optimizer.zero_grad(set_to_none=True) optimizer.zero_grad(set_to_none=True)
# 打印日志
if step % args.log_interval == 0: if step % args.log_interval == 0:
spend_time = time.time() - start_time spend_time = time.time() - start_time
Logger( Logger(
@@ -1738,6 +1877,7 @@ def train_epoch(epoch):
"lr": optimizer.param_groups[-1]['lr'] "lr": optimizer.param_groups[-1]['lr']
}) })
# 保存模型
if (step + 1) % args.save_interval == 0: if (step + 1) % args.save_interval == 0:
model.eval() model.eval()
ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}.pth' ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}.pth'
@@ -1747,6 +1887,7 @@ def train_epoch(epoch):
torch.save(state_dict, ckp) torch.save(state_dict, ckp)
model.train() model.train()
# 定期保存模型
if (step + 1) % 20000 == 0: if (step + 1) % 20000 == 0:
model.eval() model.eval()
ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}_step{step+1}.pth' ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}_step{step+1}.pth'
@@ -1757,14 +1898,19 @@ def train_epoch(epoch):
def init_model(): def init_model():
"""初始化模型"""
def count_parameters(model): def count_parameters(model):
"""计算模型参数量"""
return sum(p.numel() for p in model.parameters() if p.requires_grad) return sum(p.numel() for p in model.parameters() if p.requires_grad)
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/') tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/')
# 初始化模型
model = Transformer(lm_config) model = Transformer(lm_config)
ckp = './base_monkey_215M/pretrain_1024_18_6144.pth' # 加载预训练权重
ckp = './base_model_215M/pretrain_1024_18_6144.pth'
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
unwanted_prefix = '_orig_mod.' unwanted_prefix = '_orig_mod.'
for k, v in list(state_dict.items()): for k, v in list(state_dict.items()):
@@ -1785,22 +1931,22 @@ def init_model():
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining") parser = argparse.ArgumentParser(description="Tiny-LLM Pretraining")
parser.add_argument("--out_dir", type=str, default="output", help="Output directory") parser.add_argument("--out_dir", type=str, default="sft_model_215M", help="输出目录")
parser.add_argument("--epochs", type=int, default=1, help="Number of epochs") parser.add_argument("--epochs", type=int, default=1, help="训练轮数")
parser.add_argument("--batch_size", type=int, default=64, help="Batch size") parser.add_argument("--batch_size", type=int, default=64, help="批处理大小")
parser.add_argument("--learning_rate", type=float, default=2e-4, help="Learning rate") parser.add_argument("--learning_rate", type=float, default=2e-4, help="学习率")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="Device to use") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="使用的设备")
parser.add_argument("--dtype", type=str, default="bfloat16", help="Data type") parser.add_argument("--dtype", type=str, default="bfloat16", help="数据类型")
parser.add_argument("--use_swanlab", type=bool, default=True, help="Use Weights & Biases") parser.add_argument("--use_swanlab", action="store_true", help="是否使用SwanLab进行实验跟踪")
parser.add_argument("--num_workers", type=int, default=4, help="Number of workers for data loading") parser.add_argument("--num_workers", type=int, default=8, help="数据加载的工作进程数")
parser.add_argument("--data_path", type=str, default="", help="Path to training data") parser.add_argument("--data_path", type=str, default="./BelleGroup_sft.jsonl", help="训练数据路径")
parser.add_argument("--accumulation_steps", type=int, default=4, help="Gradient accumulation steps") parser.add_argument("--accumulation_steps", type=int, default=8, help="梯度累积步数")
parser.add_argument("--grad_clip", type=float, default=1.0, help="Gradient clipping threshold") parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
parser.add_argument("--warmup_iters", type=int, default=0, help="Number of warmup iterations") parser.add_argument("--warmup_iters", type=int, default=0, help="预热迭代次数")
parser.add_argument("--log_interval", type=int, default=100, help="Logging interval") parser.add_argument("--log_interval", type=int, default=100, help="日志记录间隔")
parser.add_argument("--save_interval", type=int, default=1000, help="Model saving interval") parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")
# 添加多卡参数 # 添加多卡参数
parser.add_argument("--gpus", type=str, default='0,1', help="Comma-separated GPU IDs (e.g. '0,1,2')") parser.add_argument("--gpus", type=str, default='0,1,2,3,4,5,6,7', help="逗号分隔的GPU ID (例如 '0,1,2')")
args = parser.parse_args() args = parser.parse_args()
@@ -1813,14 +1959,15 @@ if __name__ == "__main__":
else: else:
args.device = "cpu" args.device = "cpu"
# 初始化swanlab
if args.use_swanlab: if args.use_swanlab:
swanlab.login(api_key='your key')
run = swanlab.init( run = swanlab.init(
project="Tiny-LLM", project="Happy-LLM",
experiment_name="BelleGropu-sft-215M", experiment_name="SFT-215M",
config=args, config=args,
) )
# 模型配置
lm_config = ModelConfig( lm_config = ModelConfig(
dim=1024, dim=1024,
n_layers=18, n_layers=18,
@@ -1832,10 +1979,13 @@ if __name__ == "__main__":
torch.manual_seed(42) torch.manual_seed(42)
device_type = "cuda" if "cuda" in args.device else "cpu" device_type = "cuda" if "cuda" in args.device else "cpu"
# 上下文管理器
ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast() ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast()
# 初始化模型和分词器
model, tokenizer = init_model() model, tokenizer = init_model()
# 创建数据集和数据加载器
train_ds = SFTDataset(args.data_path, tokenizer, max_length=max_seq_len) train_ds = SFTDataset(args.data_path, tokenizer, max_length=max_seq_len)
train_loader = DataLoader( train_loader = DataLoader(
train_ds, train_ds,
@@ -1846,9 +1996,11 @@ if __name__ == "__main__":
num_workers=args.num_workers num_workers=args.num_workers
) )
# 缩放器和优化器
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16'])) scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))
optimizer = optim.Adam(model.parameters(), lr=args.learning_rate) optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
# 开始训练
iter_per_epoch = len(train_loader) iter_per_epoch = len(train_loader)
for epoch in range(args.epochs): for epoch in range(args.epochs):
train_epoch(epoch) train_epoch(epoch)
@@ -1866,9 +2018,17 @@ python model_sample.py
我们来看下`model_sample.py`文件中的代码,这个文件中定义了一个`TextGenerator`类,用于生成文本。 我们来看下`model_sample.py`文件中的代码,这个文件中定义了一个`TextGenerator`类,用于生成文本。
```python ```python
import os
import pickle
from contextlib import nullcontext
import torch
from k_model import ModelConfig, Transformer
from transformers import AutoTokenizer, AutoModelForCausalLM
import argparse
class TextGenerator: class TextGenerator:
def __init__(self, def __init__(self,
checkpoint='out/SkyWork_pretrain_768_12_6144.pth', # 模型检查点路径 checkpoint='./base_model_215M/pretrain_1024_18_6144.pth', # 模型检查点路径
tokenizer_model_path='./tokenizer_k/', # 分词器模型路径 tokenizer_model_path='./tokenizer_k/', # 分词器模型路径
seed=42, # 随机种子,确保可重复性 seed=42, # 随机种子,确保可重复性
device=None, # 设备,优先使用 CUDA如果没有可用的 CUDA则使用 CPU device=None, # 设备,优先使用 CUDA如果没有可用的 CUDA则使用 CPU
@@ -1915,7 +2075,7 @@ class TextGenerator:
def chat_template(self, prompt): def chat_template(self, prompt):
message = [ message = [
{"role": "system", "content": "你是一个AI助手。"}, {"role": "system", "content": "你是一个AI助手,你的名字叫小明"},
{"role": "user", "content": prompt} {"role": "user", "content": prompt}
] ]
return self.tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=True) return self.tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=True)
@@ -1984,6 +2144,33 @@ class TextGenerator:
generated_texts.append(self.tokenizer.decode(y[0].tolist())) # 解码生成的 token 序列为可读文本 generated_texts.append(self.tokenizer.decode(y[0].tolist())) # 解码生成的 token 序列为可读文本
return generated_texts # 返回生成的文本样本 return generated_texts # 返回生成的文本样本
if __name__ == "__main__":
print("------------------- Pretrain Sample ------------------- \n")
pretrain_prompt_datas = [
'<|im_start|>北京大学是',
'<|im_start|>中国矿业大学(北京)地球科学与测绘工程学院',
]
generator = TextGenerator(checkpoint='./base_model_215M/pretrain_1024_18_6144.pth') # 初始化生成器
for i in range(len(pretrain_prompt_datas)):
samples = generator.pretrain_sample(start=pretrain_prompt_datas[i], num_samples=1, max_new_tokens=120, temperature=0.75)
print(f"\nSample {i+1}:\n{pretrain_prompt_datas[i]}{samples[0]}\n{'-'*20}") # 打印生成的样本并用分隔线分割
print("\n ------------------- SFT Sample ------------------- \n")
sft_prompt_datas = [
'你好呀',
"中国的首都是哪里?",
"1+12等于多少",
"你是谁?"
]
generator = TextGenerator(checkpoint='./sft_model_215M/sft_dim1024_layers18_vocab_size6144.pth') # 初始化生成器
for i in range(len(sft_prompt_datas)):
samples = generator.sft_sample(start=sft_prompt_datas[i], num_samples=1, max_new_tokens=128, temperature=0.6)
print(f"\nSample {i+1}:\nQuestion: {sft_prompt_datas[i]} \nAI answer: {samples[0]}\n{'-'*20}") # 打印生成的样本并用分隔线分割
``` ```
最后我们来看一下模型输出的结果: 最后我们来看一下模型输出的结果:
@@ -2025,7 +2212,13 @@ Sample 2:
到这里,我们的模型就训绽完成了,恭喜你训练了一个属于你自己的大模型。 到这里,我们的模型就训绽完成了,恭喜你训练了一个属于你自己的大模型。
> 大家在训练的时候可以将 batch 调的低一些,这样可以减少显存的占用,避免显存不足的问题。当然这样会增加训练时间,可以根据自己的显卡显存大小来调整 batch 的大小。实测 Pretrain batch 为 4 的情况下只需要 7G 显存,训练时长预计 533 小时。作者是在 4卡A100上进行训练的,预训练一共耗时26小时SFT 阶段在 BelleGroup 350万条中文指令训练 4 小时。 > 大家在训练的时候可以将 batch 调的低一些,这样可以减少显存的占用,避免显存不足的问题。当然这样会增加训练时间,可以根据自己的显卡显存大小来调整 batch 的大小。实测 Pretrain batch 为 4 的情况下只需要 7G 显存,训练时长预计 533 小时。作者是在 8卡4090 上进行训练的,预训练一共耗时 46 小时SFT 阶段在 BelleGroup 350万条中文指令训练 24 小时。
作者也在魔搭平台上传了本章节训来的模型,如果大家的设备不足以训练大模型,大家也可以在魔搭平台下载模型进行调试和模型体验。模型下载地址如下:
> *ModelScope 模型下载地址:[🤖 ModelScope](https://www.modelscope.cn/collections/Happy-LLM-e98b91b10b684a)*
> *ModelScope 创空间体验地址:[🤖 创空间](https://www.modelscope.cn/studios/kmno4zx/happy_llm_215M_sft)*
**参考资料** **参考资料**

View File

@@ -15,7 +15,7 @@
Transformers 是由 Hugging Face 开发的 NLP 框架,通过模块化设计实现了对 BERT、GPT、LLaMA、T5、ViT 等上百种主流模型架构的统一支持。通过使用 Transformers开发者无需重复实现基础网络结构通过 AutoModel 类即可一键加载任意预训练图6.1 为 Hugging Face Transformers 课程首页: Transformers 是由 Hugging Face 开发的 NLP 框架,通过模块化设计实现了对 BERT、GPT、LLaMA、T5、ViT 等上百种主流模型架构的统一支持。通过使用 Transformers开发者无需重复实现基础网络结构通过 AutoModel 类即可一键加载任意预训练图6.1 为 Hugging Face Transformers 课程首页:
<div align='center'> <div align='center'>
<img src="../images/6-images/1-1.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/1-1.png" alt="alt text" width="90%">
<p>图6.1 Hugging Face Transformers</p> <p>图6.1 Hugging Face Transformers</p>
</div> </div>
@@ -24,7 +24,7 @@ Transformers 是由 Hugging Face 开发的 NLP 框架,通过模块化设计实
对 LLM 时代的 NLP 研究者更为重要的是HuggingFace 基于 Transformers 框架搭建了其庞大的 AI 社区开放了数亿个预训练模型参数、25万+不同类型数据集,通过 Transformers、Dataset、Evaluate 等多个框架实现对预训练模型、数据集及评估函数的集成,从而帮助开发者可以便捷地使用任一预训练模型,在开源模型及数据集的基础上便捷地实现个人模型的开发与应用。 对 LLM 时代的 NLP 研究者更为重要的是HuggingFace 基于 Transformers 框架搭建了其庞大的 AI 社区开放了数亿个预训练模型参数、25万+不同类型数据集,通过 Transformers、Dataset、Evaluate 等多个框架实现对预训练模型、数据集及评估函数的集成,从而帮助开发者可以便捷地使用任一预训练模型,在开源模型及数据集的基础上便捷地实现个人模型的开发与应用。
<div align='center'> <div align='center'>
<img src="../images/6-images/1-2.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/1-2.png" alt="alt text" width="90%">
<p>图6.2 Hugging Face Transformers 模型社区</p> <p>图6.2 Hugging Face Transformers 模型社区</p>
</div> </div>
@@ -35,14 +35,14 @@ Transformers 是由 Hugging Face 开发的 NLP 框架,通过模块化设计实
我们可以使用 transformers 的 AutoModel 类来直接初始化已经实现好的模型。对于任意预训练模型,其参数中都包含有模型的配置信息。如果是想要从头训练一个 LLM可以使用一个已有的模型架构来直接初始化。这里我们以 [Qwen-2.5-1.5B](https://huggingface.co/Qwen/Qwen2.5-1.5B/tree/main)的模型架构为例: 我们可以使用 transformers 的 AutoModel 类来直接初始化已经实现好的模型。对于任意预训练模型,其参数中都包含有模型的配置信息。如果是想要从头训练一个 LLM可以使用一个已有的模型架构来直接初始化。这里我们以 [Qwen-2.5-1.5B](https://huggingface.co/Qwen/Qwen2.5-1.5B/tree/main)的模型架构为例:
<div align='center'> <div align='center'>
<img src="../images/6-images/1-3.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/1-3.png" alt="alt text" width="90%">
<p>图6.3 Qwen-2.5-1.5B</p> <p>图6.3 Qwen-2.5-1.5B</p>
</div> </div>
该界面即为 HuggingFace 社区中的 Qwen-2.5-1.5B 模型参数,其中的 `config.json` 文件即是模型的配置信息包括了模型的架构、隐藏层大小、模型层数等如图6.4所示: 该界面即为 HuggingFace 社区中的 Qwen-2.5-1.5B 模型参数,其中的 `config.json` 文件即是模型的配置信息包括了模型的架构、隐藏层大小、模型层数等如图6.4所示:
<div align='center'> <div align='center'>
<img src="../images/6-images/1-4.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/1-4.png" alt="alt text" width="90%">
<p>图6.4 Qwen-2.5-1.5B config.json 文件</p> <p>图6.4 Qwen-2.5-1.5B config.json 文件</p>
</div> </div>
@@ -59,7 +59,7 @@ os.system('huggingface-cli download --resume-download Qwen/Qwen2.5-1.5B --local-
如图6.5,此处的 “Qwen/Qwen2.5-1.5B”即为要下载模型的标识符,对于其他模型,可以直接复制 HuggingFace 上的模型名即可: 如图6.5,此处的 “Qwen/Qwen2.5-1.5B”即为要下载模型的标识符,对于其他模型,可以直接复制 HuggingFace 上的模型名即可:
<div align='center'> <div align='center'>
<img src="../images/6-images/1-5.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/1-5.png" alt="alt text" width="90%">
<p>图6.5 模型下载标识</p> <p>图6.5 模型下载标识</p>
</div> </div>
@@ -87,7 +87,7 @@ model = AutoModelForCausalLM.from_config(config,trust_remote_code=True)
由于 LLM 一般都是 CausalLM 架构,此处使用了 AutoModelForCausalLM 类进行加载。如果是用于分类任务训练,可使用 AutoModelForSequenceClassification 类来加载。查看该 model图6.6可以看到其架构和定义的配置文件相同: 由于 LLM 一般都是 CausalLM 架构,此处使用了 AutoModelForCausalLM 类进行加载。如果是用于分类任务训练,可使用 AutoModelForSequenceClassification 类来加载。查看该 model图6.6可以看到其架构和定义的配置文件相同:
<div align='center'> <div align='center'>
<img src="../images/6-images/1-6.png" alt="alt text" width="70%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/1-6.png" alt="alt text" width="70%">
<p>图6.6 模型结构输出结果</p> <p>图6.6 模型结构输出结果</p>
</div> </div>
@@ -130,7 +130,7 @@ ds["train"][0]
``` ```
<div align='center'> <div align='center'>
<img src="../images/6-images/1-7.png" alt="alt text" width="100%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/1-7.png" alt="alt text" width="100%">
<p>图6.7 数据集展示</p> <p>图6.7 数据集展示</p>
</div> </div>
@@ -788,7 +788,7 @@ trainer.save_model()
具体而言,其在预训练模型每层中插入用于下游任务的参数,即 Adapter 模块在微调时冻结模型主体仅训练特定于任务的参数如图6.8所示。 具体而言,其在预训练模型每层中插入用于下游任务的参数,即 Adapter 模块在微调时冻结模型主体仅训练特定于任务的参数如图6.8所示。
<div align='center'> <div align='center'>
<img src="../images/6-images/3-1.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/3-1.png" alt="alt text" width="90%">
<p>图6.8 Adapt Tuning</p> <p>图6.8 Adapt Tuning</p>
</div> </div>
@@ -840,7 +840,7 @@ $$h = W_0 x + \Delta W x = W_0 x + B A x$$
训练思路如图6.9所示: 训练思路如图6.9所示:
<div align='center'> <div align='center'>
<img src="../images/6-images/3-2.jpg" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/6-images/3-2.jpg" alt="alt text" width="90%">
<p>图6.9 LoRA</p> <p>图6.9 LoRA</p>
</div> </div>

View File

@@ -1,5 +1,5 @@
from src.core import Agent from src.core import Agent
from src.tools import add, count_letter_in_string, compare, get_current_datetime from src.tools import add, count_letter_in_string, compare, get_current_datetime, search_wikipedia, get_current_temperature
from openai import OpenAI from openai import OpenAI
@@ -13,7 +13,7 @@ if __name__ == "__main__":
agent = Agent( agent = Agent(
client=client, client=client,
model="Qwen/Qwen2.5-32B-Instruct", model="Qwen/Qwen2.5-32B-Instruct",
tools=[get_current_datetime, add, compare, count_letter_in_string], tools=[get_current_datetime, search_wikipedia, get_current_temperature],
) )
while True: while True:

View File

@@ -1,4 +1,55 @@
json altair==5.5.0
openai annotated-types==0.7.0
datetime anyio==4.9.0
pprint attrs==25.3.0
beautifulsoup4==4.13.4
blinker==1.9.0
cachetools==6.1.0
certifi==2025.6.15
charset-normalizer==3.4.2
click==8.2.1
datetime==5.5
distro==1.9.0
gitdb==4.0.12
gitpython==3.1.44
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.10
jinja2==3.1.6
jiter==0.10.0
jsonschema==4.24.0
jsonschema-specifications==2025.4.1
markupsafe==3.0.2
narwhals==1.43.1
numpy==2.3.0
openai==1.88.0
packaging==25.0
pandas==2.3.0
pillow==11.2.1
protobuf==6.31.1
pyarrow==20.0.0
pydantic==2.11.7
pydantic-core==2.33.2
pydeck==0.9.1
python-dateutil==2.9.0.post0
pytz==2025.2
referencing==0.36.2
requests==2.32.4
rpds-py==0.25.1
setuptools==80.9.0
six==1.17.0
smmap==5.0.2
sniffio==1.3.1
soupsieve==2.7
streamlit==1.46.0
tenacity==9.1.2
toml==0.10.2
tornado==6.5.1
tqdm==4.67.1
typing-extensions==4.14.0
typing-inspection==0.4.1
tzdata==2025.2
urllib3==2.5.0
wikipedia==1.4.0
zope-interface==7.2

View File

@@ -2,11 +2,11 @@ from openai import OpenAI
import json import json
from typing import List, Dict, Any from typing import List, Dict, Any
from src.utils import function_to_json from src.utils import function_to_json
from src.tools import get_current_datetime, add, compare, count_letter_in_string from src.tools import get_current_datetime, add, compare, count_letter_in_string, search_wikipedia, get_current_temperature
import pprint import pprint
SYSREM_PROMPT = """ SYSTEM_PROMPT = """
你是一个叫不要葱姜蒜的人工智能助手。你的输出应该与用户的语言保持一致。 你是一个叫不要葱姜蒜的人工智能助手。你的输出应该与用户的语言保持一致。
当用户的问题需要调用工具时,你可以从提供的工具列表中调用适当的工具函数。 当用户的问题需要调用工具时,你可以从提供的工具列表中调用适当的工具函数。
""" """
@@ -17,7 +17,7 @@ class Agent:
self.tools = tools self.tools = tools
self.model = model self.model = model
self.messages = [ self.messages = [
{"role": "system", "content": SYSREM_PROMPT}, {"role": "system", "content": SYSTEM_PROMPT},
] ]
self.verbose = verbose self.verbose = verbose

View File

@@ -1,12 +1,14 @@
from datetime import datetime import datetime
import wikipedia
import requests
# 获取当前日期和时间 # 获取当前日期和时间
def get_current_datetime() -> str: def get_current_datetime() -> str:
""" """
获取当前日期和时间。 获取真实的当前日期和时间。
:return: 当前日期和时间的字符串表示。 :return: 当前日期和时间的字符串表示。
""" """
current_datetime = datetime.now() current_datetime = datetime.datetime.now()
formatted_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S") formatted_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
return formatted_datetime return formatted_datetime
@@ -54,3 +56,76 @@ def count_letter_in_string(a: str, b: str):
count = string.count(letter) count = string.count(letter)
return(f"The letter '{letter}' appears {count} times in the string.") return(f"The letter '{letter}' appears {count} times in the string.")
def search_wikipedia(query: str) -> str:
"""
在维基百科中搜索指定查询的前三个页面摘要。
:param query: 要搜索的查询字符串。
:return: 包含前三个页面摘要的字符串。
"""
page_titles = wikipedia.search(query)
summaries = []
for page_title in page_titles[: 3]: # 取前三个页面标题
try:
# 使用 wikipedia 模块的 page 函数,获取指定标题的维基百科页面对象。
wiki_page = wikipedia.page(title=page_title, auto_suggest=False)
# 获取页面摘要
summaries.append(f"页面: {page_title}\n摘要: {wiki_page.summary}")
except (
wikipedia.exceptions.PageError,
wikipedia.exceptions.DisambiguationError,
):
pass
if not summaries:
return "维基百科没有搜索到合适的结果"
return "\n\n".join(summaries)
def get_current_temperature(latitude: float, longitude: float) -> str:
"""
获取指定经纬度位置的当前温度。
:param latitude: 纬度坐标。
:param longitude: 经度坐标。
:return: 当前温度的字符串表示。
"""
# Open Meteo API 的URL
open_meteo_url = "https://api.open-meteo.com/v1/forecast"
# 请求参数
params = {
'latitude': latitude,
'longitude': longitude,
'hourly': 'temperature_2m',
'forecast_days': 1,
}
# 发送 API 请求
response = requests.get(open_meteo_url, params=params)
# 检查响应状态码
if response.status_code == 200:
# 解析 JSON 响应
results = response.json()
else:
# 处理请求失败的情况
raise Exception(f"API Request failed with status code: {response.status_code}")
# 获取当前 UTC 时间
current_utc_time = datetime.datetime.now(datetime.UTC)
# 将时间字符串转换为 datetime 对象
time_list = [datetime.datetime.fromisoformat(time_str).replace(tzinfo=datetime.timezone.utc) for time_str in
results['hourly']['time']]
# 获取温度列表
temperature_list = results['hourly']['temperature_2m']
# 找到最接近当前时间的索引
closest_time_index = min(range(len(time_list)), key=lambda i: abs(time_list[i] - current_utc_time))
# 获取当前温度
current_temperature = temperature_list[closest_time_index]
# 返回当前温度的字符串形式
return f'现在温度是 {current_temperature}°C'

View File

@@ -0,0 +1,62 @@
import streamlit as st
from src.core import Agent
from src.tools import add, count_letter_in_string, compare, get_current_datetime, search_wikipedia, get_current_temperature
from openai import OpenAI
# --- 页面配置 ---
st.set_page_config(
page_title="Tiny Agent Demo", # 页面标题
page_icon="🤖", # 页面图标
layout="centered", # 页面布局
initial_sidebar_state="auto", # 侧边栏初始状态
)
# --- OpenAI客户端初始化 ---
client = OpenAI(
api_key="sk-quovvfgjdmmrvwiljusggiwvxfiekzicwjgtdvpfqhpmbpqu",
base_url="https://api.siliconflow.cn/v1",
)
# --- Agent初始化 ---
@st.cache_resource
def load_agent():
"""创建并缓存Agent实例。"""
return Agent(
client=client,
model="Qwen/Qwen2.5-32B-Instruct", # 使用的模型
tools=[get_current_datetime, search_wikipedia, get_current_temperature], # Agent可以使用的工具
)
agent = load_agent() # 加载Agent
# --- UI组件 ---
st.title("🤖 Happy-LLM Tiny Agent") # 设置页面标题
st.markdown("""欢迎来到 Tiny Agent web 界面!
在下方输入您的提示,查看 Agent 的实际操作。
""") # 显示Markdown格式的欢迎信息
# 初始化聊天记录
if "messages" not in st.session_state:
st.session_state.messages = []
# 在应用重新运行时显示历史聊天记录
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# 响应用户输入
if prompt := st.chat_input("我能为您做些什么?"):
# 在聊天消息容器中显示用户消息
st.chat_message("user").markdown(prompt)
# 将用户消息添加到聊天记录中
st.session_state.messages.append({"role": "user", "content": prompt})
with st.spinner('思考中...'):
response = agent.get_completion(prompt) # 获取Agent的响应
# 在聊天消息容器中显示助手响应
with st.chat_message("assistant"):
st.markdown(response)
# 将助手响应添加到聊天记录中
st.session_state.messages.append({"role": "assistant", "content": response})

View File

@@ -0,0 +1,4 @@
# 此处默认使用国内可访问的轨迹流动平台 https://cloud.siliconflow.cn/
OPENAI_API_KEY='your api key'
OPENAI_BASE_URL='https://api.siliconflow.cn/v1'

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
@File : Embeddings.py @File : Embedding.py
@Time : 2024/02/10 21:55:39 @Time : 2025/06/20 13:50:47
@Author : 不要葱姜蒜 @Author : 不要葱姜蒜
@Version : 1.0 @Version : 1.1
@Desc : None @Desc : None
''' '''
@@ -12,8 +12,8 @@ import os
from copy import copy from copy import copy
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
import numpy as np import numpy as np
from openai import OpenAI
os.environ['CURL_CA_BUNDLE'] = ''
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) _ = load_dotenv(find_dotenv())
@@ -23,21 +23,59 @@ class BaseEmbeddings:
Base class for embeddings Base class for embeddings
""" """
def __init__(self, path: str, is_api: bool) -> None: def __init__(self, path: str, is_api: bool) -> None:
"""
初始化嵌入基类
Args:
path (str): 模型或数据的路径
is_api (bool): 是否使用API方式。True表示使用在线API服务False表示使用本地模型
"""
self.path = path self.path = path
self.is_api = is_api self.is_api = is_api
def get_embedding(self, text: str, model: str) -> List[float]: def get_embedding(self, text: str, model: str) -> List[float]:
"""
获取文本的嵌入向量表示
Args:
text (str): 输入文本
model (str): 使用的模型名称
Returns:
List[float]: 文本的嵌入向量
Raises:
NotImplementedError: 该方法需要在子类中实现
"""
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def cosine_similarity(cls, vector1: List[float], vector2: List[float]) -> float: def cosine_similarity(cls, vector1: List[float], vector2: List[float]) -> float:
""" """
calculate cosine similarity between two vectors 计算两个向量之间的余弦相似度
Args:
vector1 (List[float]): 第一个向量
vector2 (List[float]): 第二个向量
Returns:
float: 两个向量的余弦相似度,范围在[-1,1]之间
""" """
dot_product = np.dot(vector1, vector2) # 将输入列表转换为numpy数组并指定数据类型为float32
magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2) v1 = np.array(vector1, dtype=np.float32)
if not magnitude: v2 = np.array(vector2, dtype=np.float32)
return 0
# 检查向量中是否包含无穷大或NaN值
if not np.all(np.isfinite(v1)) or not np.all(np.isfinite(v2)):
return 0.0
# 计算向量的点积
dot_product = np.dot(v1, v2)
# 计算向量的范数(长度)
norm_v1 = np.linalg.norm(v1)
norm_v2 = np.linalg.norm(v2)
# 计算分母(两个向量范数的乘积)
magnitude = norm_v1 * norm_v2
# 处理分母为0的特殊情况
if magnitude == 0:
return 0.0
# 返回余弦相似度
return dot_product / magnitude return dot_product / magnitude
@@ -48,70 +86,18 @@ class OpenAIEmbedding(BaseEmbeddings):
def __init__(self, path: str = '', is_api: bool = True) -> None: def __init__(self, path: str = '', is_api: bool = True) -> None:
super().__init__(path, is_api) super().__init__(path, is_api)
if self.is_api: if self.is_api:
from openai import OpenAI
self.client = OpenAI() self.client = OpenAI()
# 从环境变量中获取 硅基流动 密钥
self.client.api_key = os.getenv("OPENAI_API_KEY") self.client.api_key = os.getenv("OPENAI_API_KEY")
# 从环境变量中获取 硅基流动 的基础URL
self.client.base_url = os.getenv("OPENAI_BASE_URL") self.client.base_url = os.getenv("OPENAI_BASE_URL")
def get_embedding(self, text: str, model: str = "text-embedding-3-large") -> List[float]: def get_embedding(self, text: str, model: str = "BAAI/bge-m3") -> List[float]:
"""
此处默认使用轨迹流动的免费嵌入模型 BAAI/bge-m3
"""
if self.is_api: if self.is_api:
text = text.replace("\n", " ") text = text.replace("\n", " ")
return self.client.embeddings.create(input=[text], model=model).data[0].embedding return self.client.embeddings.create(input=[text], model=model).data[0].embedding
else: else:
raise NotImplementedError raise NotImplementedError
class JinaEmbedding(BaseEmbeddings):
"""
class for Jina embeddings
"""
def __init__(self, path: str = 'jinaai/jina-embeddings-v2-base-zh', is_api: bool = False) -> None:
super().__init__(path, is_api)
self._model = self.load_model()
def get_embedding(self, text: str) -> List[float]:
return self._model.encode([text])[0].tolist()
def load_model(self):
import torch
from transformers import AutoModel
if torch.cuda.is_available():
device = torch.device("cuda")
else:
device = torch.device("cpu")
model = AutoModel.from_pretrained(self.path, trust_remote_code=True).to(device)
return model
class ZhipuEmbedding(BaseEmbeddings):
"""
class for Zhipu embeddings
"""
def __init__(self, path: str = '', is_api: bool = True) -> None:
super().__init__(path, is_api)
if self.is_api:
from zhipuai import ZhipuAI
self.client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY"))
def get_embedding(self, text: str) -> List[float]:
response = self.client.embeddings.create(
model="embedding-2",
input=text,
)
return response.data[0].embedding
class DashscopeEmbedding(BaseEmbeddings):
"""
class for Dashscope embeddings
"""
def __init__(self, path: str = '', is_api: bool = True) -> None:
super().__init__(path, is_api)
if self.is_api:
import dashscope
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
self.client = dashscope.TextEmbedding
def get_embedding(self, text: str, model: str='text-embedding-v1') -> List[float]:
response = self.client.call(
model=model,
input=text
)
return response.output['embeddings'][0]['embedding']

View File

@@ -2,37 +2,33 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
@File : LLM.py @File : LLM.py
@Time : 2024/02/12 13:50:47 @Time : 2025/06/20 13:50:47
@Author : 不要葱姜蒜 @Author : 不要葱姜蒜
@Version : 1.0 @Version : 1.1
@Desc : None @Desc : None
''' '''
import os import os
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
from openai import OpenAI
PROMPT_TEMPLATE = dict( from dotenv import load_dotenv, find_dotenv
RAG_PROMPT_TEMPALTE="""使用以上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。 _ = load_dotenv(find_dotenv())
问题: {question}
可参考的上下文: RAG_PROMPT_TEMPLATE="""
··· 使用以上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。
{context} 问题: {question}
··· 可参考的上下文:
如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。 ···
有用的回答:""", {context}
InternLM_PROMPT_TEMPALTE="""先对上下文进行内容总结,再使用上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。 ···
问题: {question} 如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。
可参考的上下文: 有用的回答:
··· """
{context}
···
如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。
有用的回答:"""
)
class BaseModel: class BaseModel:
def __init__(self, path: str = '') -> None: def __init__(self, model) -> None:
self.path = path self.model = model
def chat(self, prompt: str, history: List[dict], content: str) -> str: def chat(self, prompt: str, history: List[dict], content: str) -> str:
pass pass
@@ -41,73 +37,18 @@ class BaseModel:
pass pass
class OpenAIChat(BaseModel): class OpenAIChat(BaseModel):
def __init__(self, path: str = '', model: str = "gpt-3.5-turbo-1106") -> None: def __init__(self, model: str = "Qwen/Qwen2.5-32B-Instruct") -> None:
super().__init__(path)
self.model = model self.model = model
def chat(self, prompt: str, history: List[dict], content: str) -> str: def chat(self, prompt: str, history: List[dict], content: str) -> str:
from openai import OpenAI
client = OpenAI() client = OpenAI()
client.api_key = os.getenv("OPENAI_API_KEY") client.api_key = os.getenv("OPENAI_API_KEY")
client.base_url = os.getenv("OPENAI_BASE_URL") client.base_url = os.getenv("OPENAI_BASE_URL")
history.append({'role': 'user', 'content': PROMPT_TEMPLATE['RAG_PROMPT_TEMPALTE'].format(question=prompt, context=content)}) history.append({'role': 'user', 'content': RAG_PROMPT_TEMPLATE.format(question=prompt, context=content)})
response = client.chat.completions.create( response = client.chat.completions.create(
model=self.model, model=self.model,
messages=history, messages=history,
max_tokens=150, max_tokens=2048,
temperature=0.1 temperature=0.1
) )
return response.choices[0].message.content return response.choices[0].message.content
class InternLMChat(BaseModel):
def __init__(self, path: str = '') -> None:
super().__init__(path)
self.load_model()
def chat(self, prompt: str, history: List = [], content: str='') -> str:
prompt = PROMPT_TEMPLATE['InternLM_PROMPT_TEMPALTE'].format(question=prompt, context=content)
response, history = self.model.chat(self.tokenizer, prompt, history)
return response
def load_model(self):
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
self.tokenizer = AutoTokenizer.from_pretrained(self.path, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(self.path, torch_dtype=torch.float16, trust_remote_code=True).cuda()
class DashscopeChat(BaseModel):
def __init__(self, path: str = '', model: str = "qwen-turbo") -> None:
super().__init__(path)
self.model = model
def chat(self, prompt: str, history: List[Dict], content: str) -> str:
import dashscope
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
history.append({'role': 'user', 'content': PROMPT_TEMPLATE['RAG_PROMPT_TEMPALTE'].format(question=prompt, context=content)})
response = dashscope.Generation.call(
model=self.model,
messages=history,
result_format='message',
max_tokens=150,
temperature=0.1
)
return response.output.choices[0].message.content
class ZhipuChat(BaseModel):
def __init__(self, path: str = '', model: str = "glm-4") -> None:
super().__init__(path)
from zhipuai import ZhipuAI
self.client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY"))
self.model = model
def chat(self, prompt: str, history: List[Dict], content: str) -> str:
history.append({'role': 'user', 'content': PROMPT_TEMPLATE['RAG_PROMPT_TEMPALTE'].format(question=prompt, context=content)})
response = self.client.chat.completions.create(
model=self.model,
messages=history,
max_tokens=150,
temperature=0.1
)
return response.choices[0].message

View File

@@ -2,16 +2,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
@File : VectorBase.py @File : VectorBase.py
@Time : 2024/02/12 10:11:13 @Time : 2025/06/20 10:11:13
@Author : 不要葱姜蒜 @Author : 不要葱姜蒜
@Version : 1.0 @Version : 1.1
@Desc : None @Desc : None
''' '''
import os import os
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
import json import json
from RAG.Embeddings import BaseEmbeddings, OpenAIEmbedding, JinaEmbedding, ZhipuEmbedding from Embeddings import BaseEmbeddings, OpenAIEmbedding
import numpy as np import numpy as np
from tqdm import tqdm from tqdm import tqdm

19
docs/chapter7/RAG/demo.py Normal file
View File

@@ -0,0 +1,19 @@
from VectorBase import VectorStore
from utils import ReadFiles
from LLM import OpenAIChat
from Embeddings import OpenAIEmbedding
# 没有保存数据库
docs = ReadFiles('./data').get_content(max_token_len=600, cover_content=150) # 获得data目录下的所有文件内容并分割
vector = VectorStore(docs)
embedding = OpenAIEmbedding() # 创建EmbeddingModel
vector.get_vector(EmbeddingModel=embedding)
vector.persist(path='storage') # 将向量和文档内容保存到storage目录下下次再用就可以直接加载本地的数据库
# vector.load_vector('./storage') # 加载本地的数据库
question = 'RAG的原理是什么'
content = vector.query(question, EmbeddingModel=embedding, k=1)[0]
chat = OpenAIChat(model='Qwen/Qwen2.5-32B-Instruct')
print(chat.chat(question, [], content))

View File

@@ -0,0 +1,28 @@
annotated-types==0.7.0
anyio==4.9.0
beautifulsoup4==4.13.4
bs4==0.0.2
certifi==2025.6.15
charset-normalizer==3.4.2
distro==1.9.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.10
jiter==0.10.0
markdown==3.8.2
numpy==2.3.0
openai==1.88.0
pydantic==2.11.7
pydantic-core==2.33.2
pypdf2==3.0.1
python-dotenv==1.1.0
regex==2024.11.6
requests==2.32.4
sniffio==1.3.1
soupsieve==2.7
tiktoken==0.9.0
tqdm==4.67.1
typing-extensions==4.14.0
typing-inspection==0.4.1
urllib3==2.5.0

View File

@@ -2,9 +2,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
@File : utils.py @File : utils.py
@Time : 2024/02/11 09:52:26 @Time : 2025/06/20 13:50:47
@Author : 不要葱姜蒜 @Author : 不要葱姜蒜
@Version : 1.0 @Version : 1.1
@Desc : None @Desc : None
''' '''
@@ -13,7 +13,6 @@ from typing import Dict, List, Optional, Tuple, Union
import PyPDF2 import PyPDF2
import markdown import markdown
import html2text
import json import json
from tqdm import tqdm from tqdm import tqdm
import tiktoken import tiktoken

View File

@@ -17,7 +17,6 @@
2. **工具使用评测集** 2. **工具使用评测集**
- **BFCL V2**:用于评测模型在复杂工具使用任务中的表现,特别是在执行多步骤操作时的正确性和效率。这些任务通常涉及与数据库交互或执行特定指令,以模拟实际工具使用场景。 - **BFCL V2**:用于评测模型在复杂工具使用任务中的表现,特别是在执行多步骤操作时的正确性和效率。这些任务通常涉及与数据库交互或执行特定指令,以模拟实际工具使用场景。
- **Nexus**:用于测试模型在多步骤操作中的工具使用能力,主要评估其在多任务操作中的协调性和任务管理能力,如进行文件操作、数据整合等复杂流程。
3. **数学评测集** 3. **数学评测集**
- **GSM8K**GSM8K是一个包含小学数学问题的数据集用于测试模型的数学推理和逻辑分析能力。具体任务包括算术运算、简单方程求解、数字推理等。GSM8K中的问题虽然看似简单但模型需要理解问题语义并进行正确的数学运算体现了逻辑推理和语言理解的双重挑战。 - **GSM8K**GSM8K是一个包含小学数学问题的数据集用于测试模型的数学推理和逻辑分析能力。具体任务包括算术运算、简单方程求解、数字推理等。GSM8K中的问题虽然看似简单但模型需要理解问题语义并进行正确的数学运算体现了逻辑推理和语言理解的双重挑战。
@@ -47,7 +46,7 @@
Open LLM Leaderboard 为由 Hugging Face 提供的开放式榜单汇集了多个开源大模型的评测结果帮助用户了解不同模型在各种任务上的表现。该榜单通过多个标准化测试集来评估模型的性能并通过持续更新的方式反映最新的技术进展为研究者和开发者提供了高价值的对比参考如图7.1所示。 Open LLM Leaderboard 为由 Hugging Face 提供的开放式榜单汇集了多个开源大模型的评测结果帮助用户了解不同模型在各种任务上的表现。该榜单通过多个标准化测试集来评估模型的性能并通过持续更新的方式反映最新的技术进展为研究者和开发者提供了高价值的对比参考如图7.1所示。
<div align='center'> <div align='center'>
<img src="../images/7-images/7-1-Open%20LLM%20Leaderboard.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-1-Open%20LLM%20Leaderboard.png" alt="alt text" width="90%">
<p>图 7.1 Open LLM Leaderboard</p> <p>图 7.1 Open LLM Leaderboard</p>
</div> </div>
@@ -56,7 +55,7 @@ Open LLM Leaderboard 为由 Hugging Face 提供的开放式榜单,汇集了多
由lmsys提供的聊天机器人评测榜单通过多维度的评估展示各类大模型在对话任务中的能力。该榜单采用真实用户与模型交互的方式来评测对话质量重点考察模型的自然语言生成能力、上下文理解能力以及用户满意度是当前评估聊天机器人性能的重要工具如图7.2所示。 由lmsys提供的聊天机器人评测榜单通过多维度的评估展示各类大模型在对话任务中的能力。该榜单采用真实用户与模型交互的方式来评测对话质量重点考察模型的自然语言生成能力、上下文理解能力以及用户满意度是当前评估聊天机器人性能的重要工具如图7.2所示。
<div align='center'> <div align='center'>
<img src="../images/7-images/7-1-lmsys%20Chatbot%20Arena%20Leaderboard.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-1-lmsys%20Chatbot%20Arena%20Leaderboard.png" alt="alt text" width="90%">
<p>图7.2 Lmsys Chatbot Arena Leaderboard</p> <p>图7.2 Lmsys Chatbot Arena Leaderboard</p>
</div> </div>
@@ -65,7 +64,7 @@ Open LLM Leaderboard 为由 Hugging Face 提供的开放式榜单,汇集了多
OpenCompass 是国内的评测榜单,针对大模型在多种语言和任务上的表现进行评估,提供了中国市场特定应用的参考。该榜单结合了中文语言理解和多语言能力的测试,以适应本地化需求,并特别关注大模型在中文语境下的准确性、鲁棒性和适应性,为国内企业和研究者选择合适的模型提供了重要参考。 OpenCompass 是国内的评测榜单,针对大模型在多种语言和任务上的表现进行评估,提供了中国市场特定应用的参考。该榜单结合了中文语言理解和多语言能力的测试,以适应本地化需求,并特别关注大模型在中文语境下的准确性、鲁棒性和适应性,为国内企业和研究者选择合适的模型提供了重要参考。
<div align='center'> <div align='center'>
<img src="../images/7-images/7-1-opencompass.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-1-opencompass.png" alt="alt text" width="90%">
<p>图7.3 OpenCompass</p> <p>图7.3 OpenCompass</p>
</div> </div>
@@ -84,7 +83,7 @@ OpenCompass 是国内的评测榜单,针对大模型在多种语言和任务
- 医疗榜基于MedBench评测集评估大语言模型在医学知识问答、安全伦理理解等方面的表现。由上海人工智能实验室提供。 - 医疗榜基于MedBench评测集评估大语言模型在医学知识问答、安全伦理理解等方面的表现。由上海人工智能实验室提供。
<div align='center'> <div align='center'>
<img src="../images/7-images/7-1-垂直领域榜单.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-1-垂直领域榜单.png" alt="alt text" width="90%">
<p>图7.4 垂直领域榜单</p> <p>图7.4 垂直领域榜单</p>
</div> </div>
@@ -118,7 +117,7 @@ RAG的基本结构有哪些呢
上述也就是 TinyRAG 的所有模块内容如图7.5所示。 上述也就是 TinyRAG 的所有模块内容如图7.5所示。
<div align='center'> <div align='center'>
<img src="../images/7-images/7-2-tinyrag.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-2-tinyrag.png" alt="alt text" width="90%">
<p>图7.5 TinyRAG 项目结构</p> <p>图7.5 TinyRAG 项目结构</p>
</div> </div>
@@ -131,7 +130,7 @@ RAG的基本结构有哪些呢
如下图7.6所示的流程图,图片出处 ***[Retrieval-Augmented Generation for Large Language Models: A Survey](https://arxiv.org/pdf/2312.10997.pdf)*** 如下图7.6所示的流程图,图片出处 ***[Retrieval-Augmented Generation for Large Language Models: A Survey](https://arxiv.org/pdf/2312.10997.pdf)***
<div align='center'> <div align='center'>
<img src="../images/7-images/7-2-rag.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-2-rag.png" alt="alt text" width="90%">
<p>图7.6 RAG 流程图</p> <p>图7.6 RAG 流程图</p>
</div> </div>
@@ -147,21 +146,59 @@ class BaseEmbeddings:
Base class for embeddings Base class for embeddings
""" """
def __init__(self, path: str, is_api: bool) -> None: def __init__(self, path: str, is_api: bool) -> None:
"""
初始化嵌入基类
Args:
path (str): 模型或数据的路径
is_api (bool): 是否使用API方式。True表示使用在线API服务False表示使用本地模型
"""
self.path = path self.path = path
self.is_api = is_api self.is_api = is_api
def get_embedding(self, text: str, model: str) -> List[float]: def get_embedding(self, text: str, model: str) -> List[float]:
"""
获取文本的嵌入向量表示
Args:
text (str): 输入文本
model (str): 使用的模型名称
Returns:
List[float]: 文本的嵌入向量
Raises:
NotImplementedError: 该方法需要在子类中实现
"""
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def cosine_similarity(cls, vector1: List[float], vector2: List[float]) -> float: def cosine_similarity(cls, vector1: List[float], vector2: List[float]) -> float:
""" """
calculate cosine similarity between two vectors 计算两个向量之间的余弦相似度
Args:
vector1 (List[float]): 第一个向量
vector2 (List[float]): 第二个向量
Returns:
float: 两个向量的余弦相似度,范围在[-1,1]之间
""" """
dot_product = np.dot(vector1, vector2) # 将输入列表转换为numpy数组并指定数据类型为float32
magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2) v1 = np.array(vector1, dtype=np.float32)
if not magnitude: v2 = np.array(vector2, dtype=np.float32)
return 0
# 检查向量中是否包含无穷大或NaN值
if not np.all(np.isfinite(v1)) or not np.all(np.isfinite(v2)):
return 0.0
# 计算向量的点积
dot_product = np.dot(v1, v2)
# 计算向量的范数(长度)
norm_v1 = np.linalg.norm(v1)
norm_v2 = np.linalg.norm(v2)
# 计算分母(两个向量范数的乘积)
magnitude = norm_v1 * norm_v2
# 处理分母为0的特殊情况
if magnitude == 0:
return 0.0
# 返回余弦相似度
return dot_product / magnitude return dot_product / magnitude
``` ```
@@ -177,12 +214,16 @@ class OpenAIEmbedding(BaseEmbeddings):
def __init__(self, path: str = '', is_api: bool = True) -> None: def __init__(self, path: str = '', is_api: bool = True) -> None:
super().__init__(path, is_api) super().__init__(path, is_api)
if self.is_api: if self.is_api:
from openai import OpenAI
self.client = OpenAI() self.client = OpenAI()
# 从环境变量中获取 硅基流动 密钥
self.client.api_key = os.getenv("OPENAI_API_KEY") self.client.api_key = os.getenv("OPENAI_API_KEY")
# 从环境变量中获取 硅基流动 的基础URL
self.client.base_url = os.getenv("OPENAI_BASE_URL") self.client.base_url = os.getenv("OPENAI_BASE_URL")
def get_embedding(self, text: str, model: str = "text-embedding-3-large") -> List[float]: def get_embedding(self, text: str, model: str = "BAAI/bge-m3") -> List[float]:
"""
此处默认使用轨迹流动的免费嵌入模型 BAAI/bge-m3
"""
if self.is_api: if self.is_api:
text = text.replace("\n", " ") text = text.replace("\n", " ")
return self.client.embeddings.create(input=[text], model=model).data[0].embedding return self.client.embeddings.create(input=[text], model=model).data[0].embedding
@@ -190,6 +231,8 @@ class OpenAIEmbedding(BaseEmbeddings):
raise NotImplementedError raise NotImplementedError
``` ```
> 注:此处我们默认使用国内用户可访问的[硅基流动大模型API服务平台](https://cloud.siliconflow.cn/i/ybUFvmqK)。
#### Step 3: 文档加载和切分 #### Step 3: 文档加载和切分
接下来我们来实现一个文档加载和切分的类,这个类主要用于加载文档并将其切分成文档片段。 接下来我们来实现一个文档加载和切分的类,这个类主要用于加载文档并将其切分成文档片段。
@@ -252,7 +295,7 @@ def get_chunk(cls, text: str, max_token_len: int = 600, cover_content: int = 150
- `get_vector`:获取文档的向量表示。 - `get_vector`:获取文档的向量表示。
- `query`:根据问题检索相关文档片段。 - `query`:根据问题检索相关文档片段。
完整代码可以在 ***[RAG/VectorBase.py](RAG/VectorBase.py)*** 文件中找到。 完整代码可以在 ***[/VectorBase.py](./RAG/VectorBase.py)*** 文件中找到。
```python ```python
class VectorStore: class VectorStore:
@@ -303,41 +346,43 @@ class BaseModel:
pass pass
``` ```
`BaseModel` 包含两个方法:`chat``load_model`。对于本地化运行的开源模型需要实现`load_model`而API模型则不需要。 `BaseModel` 包含两个方法:`chat``load_model`。对于本地化运行的开源模型需要实现`load_model`而API模型则不需要。在此处我们还是使用国内用户可访问的硅基流动大模型API服务平台使用API服务的好处就是用户不需要本地的计算资源可以大大降低学习者的学习门槛。
下面以 ***[InternLM2-chat-7B](https://huggingface.co/internlm/internlm2-chat-7b)*** 模型为例:
```python ```python
class InternLMChat(BaseModel): from openai import OpenAI
def __init__(self, path: str = '') -> None:
super().__init__(path)
self.load_model()
def chat(self, prompt: str, history: List = [], content: str='') -> str: class OpenAIChat(BaseModel):
prompt = PROMPT_TEMPLATE['InternLM_PROMPT_TEMPLATE'].format(question=prompt, context=content) def __init__(self, model: str = "Qwen/Qwen2.5-32B-Instruct") -> None:
response, history = self.model.chat(self.tokenizer, prompt, history) self.model = model
return response
def chat(self, prompt: str, history: List[dict], content: str) -> str:
client = OpenAI()
client.api_key = os.getenv("OPENAI_API_KEY")
client.base_url = os.getenv("OPENAI_BASE_URL")
history.append({'role': 'user', 'content': RAG_PROMPT_TEMPLATE.format(question=prompt, context=content)})
response = client.chat.completions.create(
model=self.model,
messages=history,
max_tokens=2048,
temperature=0.1
)
return response.choices[0].message.content
def load_model(self):
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
self.tokenizer = AutoTokenizer.from_pretrained(self.path, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(self.path, torch_dtype=torch.float16, trust_remote_code=True).cuda()
``` ```
可以用一个字典来保存所有的prompt方便维护 设计一个专用于RAG的大模型提示词如下
```python ```python
PROMPT_TEMPLATE = dict( RAG_PROMPT_TEMPLATE="""
InternLM_PROMPT_TEMPLATE="""先对上下文进行内容总结,再使用上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。 使用上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。
问题: {question} 问题: {question}
可参考的上下文: 可参考的上下文:
··· ···
{context} {context}
··· ···
如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。 如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。
有用的回答:""" 有用的回答:
) """
``` ```
这样我们就可以利用InternLM2模型来做RAG啦 这样我们就可以利用InternLM2模型来做RAG啦
@@ -347,47 +392,51 @@ PROMPT_TEMPLATE = dict(
接下来我们来看看Tiny-RAG的Demo吧 接下来我们来看看Tiny-RAG的Demo吧
```python ```python
from RAG.VectorBase import VectorStore from VectorBase import VectorStore
from RAG.utils import ReadFiles from utils import ReadFiles
from RAG.LLM import OpenAIChat, InternLMChat from LLM import OpenAIChat
from RAG.Embeddings import JinaEmbedding, ZhipuEmbedding from Embeddings import OpenAIEmbedding
# 没有保存数据库 # 没有保存数据库
docs = ReadFiles('./data').get_content(max_token_len=600, cover_content=150) # 获data目录下的所有文件内容并分割 docs = ReadFiles('./data').get_content(max_token_len=600, cover_content=150) # 获data目录下的所有文件内容并分割
vector = VectorStore(docs) vector = VectorStore(docs)
embedding = ZhipuEmbedding() # 创建EmbeddingModel embedding = OpenAIEmbedding() # 创建EmbeddingModel
vector.get_vector(EmbeddingModel=embedding) vector.get_vector(EmbeddingModel=embedding)
vector.persist(path='storage') # 将向量和文档内容保存到storage目录下次再用可以直接加载本地数据库 vector.persist(path='storage') # 将向量和文档内容保存到storage目录,下次再用可以直接加载本地数据库
question = 'git的原理是什么' # vector.load_vector('./storage') # 加载本地的数据库
content = vector.query(question, model='zhipu', k=1)[0] question = 'RAG的原理是什么'
chat = InternLMChat(path='model_path')
content = vector.query(question, EmbeddingModel=embedding, k=1)[0]
chat = OpenAIChat(model='Qwen/Qwen2.5-32B-Instruct')
print(chat.chat(question, [], content)) print(chat.chat(question, [], content))
``` ```
也可以从本地加载已处理好的数据库: 也可以从本地加载已处理好的数据库:
```python ```python
from RAG.VectorBase import VectorStore from VectorBase import VectorStore
from RAG.utils import ReadFiles from utils import ReadFiles
from RAG.LLM import OpenAIChat, InternLMChat from LLM import OpenAIChat
from RAG.Embeddings import JinaEmbedding, ZhipuEmbedding from Embeddings import OpenAIEmbedding
# 保存数据库之后 # 保存数据库之后
vector = VectorStore() vector = VectorStore()
vector.load_vector('./storage') # 加载本地数据库 vector.load_vector('./storage') # 加载本地数据库
question = 'git的原理是什么?' question = 'RAG的原理是什么?'
embedding = ZhipuEmbedding() # 创建EmbeddingModel embedding = ZhipuEmbedding() # 创建EmbeddingModel
content = vector.query(question, EmbeddingModel=embedding, k=1)[0] content = vector.query(question, EmbeddingModel=embedding, k=1)[0]
chat = InternLMChat(path='model_path') chat = OpenAIChat(model='Qwen/Qwen2.5-32B-Instruct')
print(chat.chat(question, [], content)) print(chat.chat(question, [], content))
``` ```
> 7.2 章节的所有代码均可在 [Happy-LLM Chapter7 RAG](https://github.com/datawhalechina/happy-llm/tree/main/docs/chapter7/RAG) 中找到。
## 7.3 Agent ## 7.3 Agent
### 7.3.1 什么是 LLM Agent ### 7.3.1 什么是 LLM Agent
@@ -403,7 +452,7 @@ print(chat.chat(question, [], content))
传统的LLM像一个知识渊博但只能纸上谈兵的图书馆员而 LLM Agent 则更像一个全能的私人助理,不仅懂得多,还能跑腿办事,甚至能主动思考最优方案。 传统的LLM像一个知识渊博但只能纸上谈兵的图书馆员而 LLM Agent 则更像一个全能的私人助理,不仅懂得多,还能跑腿办事,甚至能主动思考最优方案。
<div align='center'> <div align='center'>
<img src="../images/7-images/7-3-Agent工作原理.png" alt="alt text" width="90%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-3-Agent工作原理.png" alt="alt text" width="90%">
<p>图7.7 Agent 工作原理</p> <p>图7.7 Agent 工作原理</p>
</div> </div>
@@ -440,7 +489,7 @@ LLM Agent 通过将大型语言模型的强大语言理解和生成能力与规
最终的实现效果如图7.8所示: 最终的实现效果如图7.8所示:
<div align='center'> <div align='center'>
<img src="../images/7-images/7-3-tinyagent-example.png" style="width: 100%;"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-3-tinyagent-example.png" style="width: 100%;">
<p>图7.8 效果示意图</p> <p>图7.8 效果示意图</p>
</div> </div>
@@ -481,29 +530,6 @@ def get_current_datetime() -> str:
formatted_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S") formatted_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
return formatted_datetime return formatted_datetime
def add(a: float, b: float):
"""
计算两个浮点数的和。
:param a: 第一个浮点数。
:param b: 第二个浮点数。
:return: 两个浮点数的和。
"""
return str(a + b)
def compare(a: float, b: float):
"""
比较两个浮点数的大小。
:param a: 第一个浮点数。
:param b: 第二个浮点数。
:return: 比较结果的字符串表示。
"""
if a > b:
return f'{a} is greater than {b}'
elif a < b:
return f'{b} is greater than {a}'
else:
return f'{a} is equal to {b}'
def count_letter_in_string(a: str, b: str): def count_letter_in_string(a: str, b: str):
""" """
统计字符串中某个字母的出现次数。 统计字符串中某个字母的出现次数。
@@ -513,6 +539,28 @@ def count_letter_in_string(a: str, b: str):
""" """
return str(a.count(b)) return str(a.count(b))
def search_wikipedia(query: str) -> str:
"""
在维基百科中搜索指定查询的前三个页面摘要。
:param query: 要搜索的查询字符串。
:return: 包含前三个页面摘要的字符串。
"""
page_titles = wikipedia.search(query)
summaries = []
for page_title in page_titles[: 3]: # 取前三个页面标题
try:
# 使用 wikipedia 模块的 page 函数,获取指定标题的维基百科页面对象。
wiki_page = wikipedia.page(title=page_title, auto_suggest=False)
# 获取页面摘要
summaries.append(f"页面: {page_title}\n摘要: {wiki_page.summary}")
except (
wikipedia.exceptions.PageError,
wikipedia.exceptions.DisambiguationError,
):
pass
if not summaries:
return "维基百科没有搜索到合适的结果"
return "\n\n".join(summaries)
# ... (可能还有其他工具函数) # ... (可能还有其他工具函数)
``` ```
@@ -552,7 +600,7 @@ from utils import function_to_json
# 导入定义好的工具函数 # 导入定义好的工具函数
from tools import get_current_datetime, add, compare, count_letter_in_string from tools import get_current_datetime, add, compare, count_letter_in_string
SYSREM_PROMPT = """ SYSTEM_PROMPT = """
你是一个叫不要葱姜蒜的人工智能助手。你的输出应该与用户的语言保持一致。 你是一个叫不要葱姜蒜的人工智能助手。你的输出应该与用户的语言保持一致。
当用户的问题需要调用工具时,你可以从提供的工具列表中调用适当的工具函数。 当用户的问题需要调用工具时,你可以从提供的工具列表中调用适当的工具函数。
""" """
@@ -633,7 +681,7 @@ Agent 的工作流程如下:
如图7.9所示Agent 调用工具流程: 如图7.9所示Agent 调用工具流程:
<div align='center'> <div align='center'>
<img src="../images/7-images/7-3-Tiny_Agent.jpg" alt="alt text" width="80%"> <img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-3-Tiny_Agent.jpg" alt="alt text" width="80%">
<p>图7.9 Agent 工作流程</p> <p>图7.9 Agent 工作流程</p>
</div> </div>
@@ -667,8 +715,6 @@ if __name__ == "__main__":
print("\033[92mAssistant: \033[0m", response) # 绿色显示AI助手回答 print("\033[92mAssistant: \033[0m", response) # 绿色显示AI助手回答
``` ```
运行 `python demo.py`你可以开始提问。如果问题需要调用工具Agent 会自动处理。
**示例交互:** **示例交互:**
```bash ```bash
@@ -691,6 +737,14 @@ Assistant: 当前的时间是2025年4月26日17:01:33。不过我注意到
User: exit User: exit
``` ```
另外,我们也准备了一份可以展示的 Streamlit 应用,可以运行在本地,展示 Agent 的功能。`streamlit run web_demo.py` 来运行,以下为 Agent 运行效果。
<div align='center'>
<img src="https://raw.githubusercontent.com/datawhalechina/happy-llm/main/docs/images/7-images/7-3-streamlit-demo.png" alt="alt text" width="80%">
<p>图 7.10 Streamlit Demo</p>
</div>
**参考文献** **参考文献**
[1] Hugging Face. (2023). *Open LLM Leaderboard: 开源大语言模型基准测试平台*. https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard [1] Hugging Face. (2023). *Open LLM Leaderboard: 开源大语言模型基准测试平台*. https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -2,7 +2,7 @@
2022年底ChatGPT 的横空出世改变了人们对人工智能的认知也给自然语言处理Natural Language ProcessNLP领域带来了阶段性的变革以 GPT 系列模型为代表的大语言模型Large Language ModelLLM成为 NLP 乃至人工智能领域的研究主流。自 2023年至今LLM 始终是人工智能领域的核心话题,引发了一轮又一轮的科技浪潮。 2022年底ChatGPT 的横空出世改变了人们对人工智能的认知也给自然语言处理Natural Language ProcessNLP领域带来了阶段性的变革以 GPT 系列模型为代表的大语言模型Large Language ModelLLM成为 NLP 乃至人工智能领域的研究主流。自 2023年至今LLM 始终是人工智能领域的核心话题,引发了一轮又一轮的科技浪潮。
LLM 其实是 NLP 领域经典研究方法预训练语言模型Pretrain Langguage ModelPLM的一种衍生成果。NLP 领域聚焦于人类书写的自然语言文本的处理、理解和生成,从诞生至今经历了符号主义阶段、统计学习阶段、深度学习阶段、预训练模型阶段到而今大模型阶段的多次变革。以 GPT、BERT 为代表的 PLM 是上一阶段 NLP 领域的核心研究成果,以注意力机制为模型架构,通过预训练-微调的阶段思想通过在海量无监督文本上进行自监督预训练,实现了强大的自然语言理解能力。但是,传统的 PLM 仍然依赖于一定量有监督数据进行下游任务微调且在自然语言生成任务上性能还不尽如人意NLP 系统的性能距离人们所期待的通用人工智能还有不小的差距。 LLM 其实是 NLP 领域经典研究方法预训练语言模型Pretrain Language ModelPLM的一种衍生成果。NLP 领域聚焦于人类书写的自然语言文本的处理、理解和生成,从诞生至今经历了符号主义阶段、统计学习阶段、深度学习阶段、预训练模型阶段到而今大模型阶段的多次变革。以 GPT、BERT 为代表的 PLM 是上一阶段 NLP 领域的核心研究成果,以注意力机制为模型架构,通过预训练-微调的阶段思想通过在海量无监督文本上进行自监督预训练,实现了强大的自然语言理解能力。但是,传统的 PLM 仍然依赖于一定量有监督数据进行下游任务微调且在自然语言生成任务上性能还不尽如人意NLP 系统的性能距离人们所期待的通用人工智能还有不小的差距。
LLM 是在 PLM 的基础上,通过大量扩大模型参数、预训练数据规模,并引入指令微调、人类反馈强化学习等手段实现的突破性成果。相较于传统 PLMLLM 具备涌现能力具有强大的上下文学习能力、指令理解能力和文本生成能力。在大模型阶段NLP 研究者可以一定程度抛弃大量的监督数据标注工作通过提供少量监督示例LLM 即能在指定下游任务上达到媲美大规模微调 PLM 的性能。同时,强大的指令理解能力与文本生成能力使 LLM 能够直接、高效、准确地响应用户指令,从而真正向通用人工智能的目标逼近。 LLM 是在 PLM 的基础上,通过大量扩大模型参数、预训练数据规模,并引入指令微调、人类反馈强化学习等手段实现的突破性成果。相较于传统 PLMLLM 具备涌现能力具有强大的上下文学习能力、指令理解能力和文本生成能力。在大模型阶段NLP 研究者可以一定程度抛弃大量的监督数据标注工作通过提供少量监督示例LLM 即能在指定下游任务上达到媲美大规模微调 PLM 的性能。同时,强大的指令理解能力与文本生成能力使 LLM 能够直接、高效、准确地响应用户指令,从而真正向通用人工智能的目标逼近。

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB