the LLM Twin 共12课的免费课程系列,这是第二课.
图片由 DALL-E 创作
这门课程为什么和其他的不同呢?通过完成“_《LLM双胞胎:构建你的AI副本》”免费的在线课程,你将学会如何设计、训练并部署一个利用LLM、向量数据库和LLM运维的最佳实践支持的生产就绪的LLM版本。
→ 不再担心孤立的脚本或笔记本!通过搭建并部署一个从头到尾的生产级LLM系统来学习如何在生产环境中应用ML
目标是? 构建并部署您的LLM孪生。
你的LLM双胞胎是谁? 它是一个智能角色,通过把你的风格、个性和语气加到AI模型里,写出像你一样的东西。
LLM双模架构
你打算怎么学?这门课程包含了10个动手实践的课程和你可以在GitHub上的访问,展示如何构建一个从头到尾的LLM系统。
此外,它还包括两个额外的课节,教你如何优化 RAG 系统。
基于专家建议,翻译如下: 基于上下文,"教训"可能更适合翻译为"课程"或"课",以更好地反映英文"Lessons"的意思,特别是在非正式语境中。根据建议: 课程更多关于该课程的详细信息,请查看它的GitHub仓库页面←
或
课这门课程分为12个课时。每篇Medium文章就是一节课的内容,
- 打造生产就绪的LLM系统端到端框架,通过构建您的LLM孪生
- 您的内容就是金:我将三年的博客文章转换成了LLM训练数据
- 我用50行CDC代码替换了1000行轮询代码
- SOTA Python流处理管道,用于实时微调LLM和RAG
- 您必须了解的4种高级RAG算法
- 将原始数据转化为微调数据集的过程
- 80亿参数、1个GPU,没问题:终极LLM微调管道
- 工程师的LLM&RAG评估框架指南
- 超越概念验证:构建可扩展的RAG系统的方法
- 终极提示监控管道方案
- [附赠] 用更少代码构建可扩展的RAG摄取管道
- [附赠] 构建多索引高级RAG应用程序
第二课:你的内容就是金矿:我将3年的博客贴变成了LLM训练的数据🔗 可以看看 GitHub 仓库 [1],并通过给我们一个 ⭐️ 来支持我们
我们在各处都有数据。领英、Medium、GitHub、Substack以及其他众多平台。
要构建你的数字孪生(注:即数字双胞胎),得要数据。
不是所有类型的数据,而是整理好的、干净、标准化的数据。
在第2课中,我们将学习如何思考并构建一个数据管道,通过聚合以下数据。
- 中等程度
- GitHub
- Substack
我们将呈现所有关于社交媒体数据采集管道的设计的架构设计决策,并解释为什么将原始(raw)数据和特征数据分离是至关重要的原因。
在第3课中,我们将介绍一种数据库架构——变更数据捕获(CDC)模式,以及数据管理系统的相关设计方法。
CDC的主要目的是识别并记录数据库数据中的更改,比如插入、更新或删除,我们将在第三课详细讲解这些。
数据流管道系统架构
内容目录- 什么是数据管道?任何AI项目的关键所在。
- 数据爬取。如何收集你的数据?
- 你是怎么存储数据的?
- 原始数据和特征数据
- 深入了解调度器和AWS Lambda这两个概念
- 运行所有内容并填充你的MongoDB数据仓库
🔗 可以看看 GitHub 仓库[1],并通过给它一个 ⭐️ 来表示支持,以星标表示支持我们的项目。
(注:⭐️ 表示星标)
1. 什么是数据流?它在任何AI项目中都至关重要。数据是任何成功的人工智能项目的命脉,而一个设计良好的数据管道是发挥其潜力的关键。
这个自动化系统充当引擎,将数据无缝地从一个阶段流转到另一个阶段,并将其从原始形态转换为可操作的见解。
但究竟什么是数据流水线,为什么它这么重要?
数据管道是由一系列自动步骤组成的,用来指导数据达到具体目标。
它从数据收集阶段开始,从各种渠道收集信息,例如 LinkedIn、Medium、Substack、Github 等平台。
管道接着对原始数据进行清洗和转换。
这一步删除了不一致之处和无关信息,并将数据转换成适合分析和ML模型的格式。
但为什么数据管道流程在AI项目中如此重要?以下是几点关键原因:
- 效率与自动化: 手动处理数据既慢又效率低下,容易出错。管道自动化了这一过程,确保了速度与准确性,特别是在处理海量数据时。
- 可扩展性: 人工智能项目往往在规模和复杂性上增长。一个设计良好的管道可以无缝扩展,适应增长,而不影响运行效率。
- 质量和一致性: 管道标准化了数据处理,确保在项目整个生命周期中保持数据的一致性和高质量,从而确保生成更可靠的AI模型。
- 灵活性和适应性: 人工智能领域不断变化,需求也在不断演变。一个健壮的数据管道可以灵活调整以应对需求变化,而无需完全重建,从而确保其长期价值。
数据是任何机器学习模型的动力。如果我们不重视数据,模型的输出可能会出乎意料。
数据的重要
但我们如何把原始数据转化为可操作的见解呢?
2. 数据爬取,如何获取数据?在本课中,我们将重点讨论四个数据来源,这是构建相关数据数据库的第一步,选择我们的数据来源。
- 领英
- Medium
- GitHub
- Substack
为什么我们选择四个数据源?我们需要复杂的和多样的数据来构建一个强大的LLM孪生体。为了获得这些特性,我们将专注于建立三个数据集合。
- 文章
- 社交媒体上的帖子
- 代码片段
对于数据爬取模块,我们将重点关注以下几个库:
- BeautifulSoup :一个用于解析(parse)HTML 和 XML 文档的 Python 库。它会创建解析树,帮助我们更快地提取数据,但 BeautifulSoup 需要先获取网页,因此我们需要与如
requests
或Selenium
这样的库一起使用,以便它们能为我们获取页面。 - Selenium :一个用于自动化网络浏览器的工具。在这里它被用来以编程方式来与网页互动(如登录 LinkedIn,浏览个人资料等)。Selenium 可以与多种浏览器配合使用,但代码配置了它与 Chrome 一起使用。我们创建了一个基础爬虫类来遵循最佳的软件工程规范。
在网页爬取的背景下,BaseAbstractCrawler
类在多个关键方面非常重要:
- 代码重用性和效率:它包含不同抓取器使用的标准方法和属性,减少代码重复,促进高效的开发流程。
- 简化和结构:这个基类抽象了复杂或重复的代码,使派生的抓取器类能够专注于特定任务。它确保不同抓取器之间的一致结构。
- 易于扩展:新的抓取器类型可以轻松扩展这个基类的功能,使系统更加灵活和可扩展,以满足未来的需求。
- 维护和测试:标准功能的更新或修复只需在基类中进行一次操作,简化了维护和测试。
导入 time 模块
从 abc 导入 ABC, abstractmethod
从 tempfile 导入 mkdtemp
从 core.db.documents 导入 BaseDocument
从 selenium 导入 webdriver
从 selenium.webdriver.chrome.options 导入 Options
class BaseCrawler(ABC):
model: type[BaseDocument]
@abstractmethod
def extract(self, link: str, **kwargs) -> None: ...
class BaseAbstractCrawler(BaseCrawler, ABC):
def __init__(self, scroll_limit: int = 5) -> None:
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
options.add_argument("--headless=new")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--log-level=3")
options.add_argument("--disable-popup-blocking")
options.add_argument("--disable-notifications")
options.add_argument("--disable-extensions")
options.add_argument("--disable-background-networking")
options.add_argument("--ignore-certificate-errors")
options.add_argument(f"--user-data-dir={mkdtemp()}")
options.add_argument(f"--data-path={mkdtemp()}")
options.add_argument(f"--disk-cache-dir={mkdtemp()}")
options.add_argument("--remote-debugging-port=9226")
self.set_extra_driver_options(options)
self.scroll_limit = scroll_limit
self.driver = webdriver.Chrome(
options=options,
)
def set_extra_driver_options(self, options: Options) -> None:
pass
def login(self) -> None:
pass
def scroll_page(self) -> None:
"""根据滚动限制在 LinkedIn 页面上滚动"""
current_scroll = 0
last_height = self.driver.execute_script("return document.body.scrollHeight")
while True:
self.driver.execute_script(
"window.scrollTo(0, document.body.scrollHeight);"
)
time.sleep(5)
new_height = self.driver.execute_script("return document.body.scrollHeight")
if new_height == last_height or (
self.scroll_limit and current_scroll >= self.scroll_limit
):
break
last_height = new_height
current_scroll += 1
这些基础类文件可以在该路径找到:data_crawling/crawlers/base.py
我们为每个类别(帖子、文章和项目仓库)创建了单独的爬虫程序,你可以在data_crawling/crawlers文件夹里找到这些爬虫程序。
每个爬虫都继承了 BaseCrawler 或 BaseAbstractCrawler 类,具体取决于其用途。
MediumCrawler 和 LinkedinCrawler 扩展自 BaseAbstractCrawler,它们依赖于登录功能和滚动功能。
下面就是 Medium 的 Crawler ↓
导入aws_lambda_powertools.Logger
从bs4 import BeautifulSoup
从core.db.documents import ArticleDocument
从selenium.webdriver.common.by import By
从crawlers.base import BaseAbstractCrawler
logger = Logger(service="llm-twin-course/crawler")
class MediumCrawler(BaseAbstractCrawler):
model = ArticleDocument
def set_extra_driver_options(self, options) -> None:
options.add_argument(r"--profile-directory=Profile 2")
def extract(self, link: str, **kwargs) -> None:
logger.info(f"开始抓取并处理Medium文章: {link}")
self.driver.get(link)
self.scroll_page()
soup = BeautifulSoup(self.driver.page_source, "html.parser")
title = soup.find_all("h1", class_="pw-post-title")
subtitle = soup.find_all("h2", class_="pw-subtitle-paragraph")
data = {
"Title": title[0].string if title else None,
"Subtitle": subtitle[0].string if subtitle else None,
"Content": soup.get_text(),
}
logger.info(f"成功抓取和保存了这篇文章: {link}")
self.driver.close()
instance = self.model(
platform="medium", content=data, link=link, author_id=kwargs.get("user")
)
instance.save()
def login(self):
"""使用Google账号登录到Medium"""
self.driver.get("https://medium.com/m/signin")
self.driver.find_element(By.TAG_NAME, "a").click()
例如,GitHub 爬虫是一个静态爬虫,不需要登录、_scrollpage 或浏览器驱动。它仅使用 git 命令。
The GithubCrawler 继承了这个 BaseCrawler 类,并利用 extract 方法获取所需的代码库。
import os
import shutil
import subprocess
import tempfile
from crawlers.base import BaseCrawler
from documents import RepositoryDocument
# GithubCrawler 类用于爬取 GitHub 代码仓库,并将代码树存入数据库中。
class GithubCrawler(BaseCrawler):
model = RepositoryDocument
def __init__(self, ignore=(".git", ".toml", ".lock", ".png")):
super().__init__()
self._ignore = ignore
def extract(self, link: str, **kwargs):
repo_name = link.rstrip("/").split("/")[-1]
local_temp = tempfile.mkdtemp()
try:
os.chdir(local_temp)
subprocess.run(["git", "clone", link])
repo_path = os.path.join(local_temp, os.listdir(local_temp)[0])
tree = {}
for root, dirs, files in os.walk(repo_path):
dir = root.replace(repo_path, "").lstrip("/")
if dir.startswith(self._ignore):
continue
for file in files:
if file.endswith(self._ignore):
continue
file_path = os.path.join(dir, file)
with open(os.path.join(root, file), "r", errors="ignore") as f:
tree[file_path] = f.read().replace(" ", "")
instance = self.model(
name=repo_name, link=link, content=tree, owner_id=kwargs.get("user")
)
instance.save()
except Exception:
raise
finally:
shutil.rmtree(local_temp)
3. 你是怎么存储数据的?ODM方法
对象文档映射(ODM)是一种将应用程序中的对象与文档库进行映射关系的技术。
通过模型类抽象数据库交互,数据存储和管理过程得以简化。这种方法特别适用于数据结构与对象导向编程模式相契合的应用。
这个documents.py
模块充当与MongoDB交互的基础框架。(链接:documents.py)
我们的数据模型专注于创建特定的类 — UserDocument,Repository文档,PostDocument 和 ArticleDocument — 这些类反映了我们 MongoDB 集合的结构特点。
这些类定义了我们存储的各种数据模式,比如用户的详细信息、仓库的相关信息、帖子的内容,以及文章的内容。
通过使用这些类,可以保证插入我们数据库的数据是一致的、有效的,并且易于检索以进行后续操作。
import uuid
from typing import List, Optional
from pydantic import UUID4, BaseModel, ConfigDict, Field
from pymongo import errors
import core.logger_utils as logger_utils
from core.db.mongo import connection
from core.errors import ImproperlyConfigured
_database = connection.get_database("twin")
logger = logger_utils.get_logger(__name__)
class BaseDocument(BaseModel):
id: UUID4 = Field(default_factory=uuid.uuid4)
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
@classmethod
def from_mongo(cls, data: dict):
"""将 '_id' 字段(字符串对象)转换为 'id' 属性(UUID对象)。"""
if not data:
return data
id = data.pop("_id", None)
return cls(**dict(data, id=id))
def to_mongo(self, **kwargs) -> dict:
"""将 'id' 属性(UUID对象)转换为 '_id' 字段(字符串对象)。"""
exclude_unset = kwargs.pop("exclude_unset", False)
by_alias = kwargs.pop("by_alias", True)
parsed = self.model_dump(
exclude_unset=exclude_unset, by_alias=by_alias, **kwargs
)
if "_id" not in parsed and "id" in parsed:
parsed["_id"] = str(parsed.pop("id"))
return parsed
def save(self, **kwargs):
# 该方法还未实现。
...
@classmethod
def get_or_create(cls, **filter_options) -> Optional[str]:
# 该方法还未实现。
...
@classmethod
def find(cls, **filter_options):
# 该方法还未实现。
...
@classmethod
def bulk_insert(cls, documents: List, **kwargs) -> Optional[List[str]]:
# .
# 该方法还未实现。
...
@classmethod
def _get_collection_name(cls):
if not hasattr(cls, "Settings") or not hasattr(cls.Settings, "name"):
raise ImproperlyConfigured(
"文档应该定义一个带有集合名称的设置配置类。"
)
return cls.Settings.name
class UserDocument(BaseDocument):
first_name: str = Field(alias="名字")
last_name: str = Field(alias="姓氏")
class Settings:
name = "users"
class RepositoryDocument(BaseDocument):
name: str
link: str
content: dict = Field(alias="内容")
owner_id: str = Field(alias="owner_id")
class Settings:
name = "repositories"
class PostDocument(BaseDocument):
platform: str
content: dict = Field(alias="内容")
author_id: str = Field(alias="author_id")
class Settings:
name = "posts"
class ArticleDocument(BaseDocument):
platform: str
link: str
content: dict = Field(alias="内容")
author_id: str = Field(alias="author_id")
class Settings:
name = "articles"
在我们适用于MongoDB的ODM方式中,集成了关键的CRUD功能:
- 转换:
to_mongo
方法将模型实例转换为 MongoDB 友好的格式。 - 插入:
save
方法使用 PyMongo 的insert_one
添加文档,并返回 MongoDB 的确认作为插入的 ID。 - 批量操作:
bulk_insert
使用insert_many
批量添加多个文档,并返回它们的 ID。 - 更新或插入:
get_or_create
或者获取现有文档,或者创建新的文档,确保数据更新的无缝。 - 验证和转换:使用 Pydantic 模型,每个类确保数据在进入数据库前被正确地结构化和验证。
4. 原始数据对比特征数据→ 完整代码在这里:core/db/documents.py
现在我们理解了数据管道在处理原始数据时的关键作用,让我们看看如何将这些数据转化为我们LLM孪生可用的格式。这时,我们就可以引入特征的概念了。
这些特性是用于微调您的LLM孪生模型的处理构建块。
想象你在教别人你的写作风格。你不会只是把你的所有社交媒体帖子给他们看吧!相反,你可能会指出你经常使用的特定关键词,你常写的话题类型,或者你文章中传达的整体情感。这些特征对于你的大语言模型(LLM)也是如此。
原始数据是指从各种来源收集的原始的未经处理的信息。社交媒体上的帖子可能包含可能包含的表情符号、无关链接或打字错误。这需要先进行清理和转换才能有效利用。
在我们这个数据流程中,未处理的原始数据首先被捕获并存储在MongoDB中,此时数据仍处于未处理状态。
然后,我们处理这些数据以创建特征——我们用来训练我们LLM副本的关键细节,并将这些特征保留在Qdrant中。我们这样做是为了保留原始数据的完整性,以便在需要时使用,而Qdrant则存储了用于高效机器学习的特征。
5. 探究调度器和AWS Lambda在这一部分,我们将重点介绍如何不断用最新的数据更新我们的数据库,这些数据来自于三个数据源。
在深入了解我们数据管道的基础设施之前,我想先展示一下如何在进入AWS的具体细节前,梳理整个流程。
做基础设施的第一步是画出各个组件的高层次概要图。
那么,我们数据流程的组件是:
- LinkedIn爬取器,Medium爬取器,GitHub爬取器,自定义文章爬取器,MongoDB数据存储
爬虫流量
每个爬虫都是一个.py文件。由于这个数据流必须不断更新,我们将设计一个基于lambda函数的系统架构,在该架构中,每个AWS Lambda函数实际上就是一个爬虫。
什么是AWS Lambda函数?它在AWS环境中如何工作?
AWS Lambda 是一种无服务器计算服务,允许您无需预置或管理服务器即可运行代码。它会在需要时运行您的代码,并且可以自动扩展规模,从每天几个请求到每秒数千个请求。
以下是Lambda在AWS环境中的位置及其特别强大的地方:
- 事件驱动特性: AWS Lambda 设计为使用事件作为触发器。这些事件可以是对 Amazon S3 存储桶中的数据的变更,对 DynamoDB 表的更新,通过 Amazon API Gateway 的 HTTP 请求,或通过其他应用程序的 SDK 进行直接调用。根据我提供的图,事件可能是 LinkedIn、Medium 或 GitHub 上的新内容或更新内容等。
- 高度可扩展: AWS Lambda 可根据传入事件的数量自动调整函数实例的数量。这可能意味着同时并行运行几十个甚至上百个函数实例。
- 托管的执行环境: AWS 负责管理底层基础设施的所有管理工作,包括服务器和操作系统的维护、容量配置和自动扩展、代码监控以及日志记录。这使您可以专注于编写代码,无需担心基础设施管理。
如何把媒介爬虫放到AWS Lambda函数?
我们需要一个能搞定这件事的人。
handler()
函数是AWS Lambda函数的入口函数。在AWS Lambda中,当事件触发Lambda函数时,会调用该handler()
函数。
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.typing import LambdaContext
from core import lib
from core.db.documents import UserDocument
from crawlers import CustomArticleCrawler, GithubCrawler, LinkedInCrawler
from dispatcher import CrawlerDispatcher
logger = Logger(service="llm-twin-course/crawler")
_dispatcher = CrawlerDispatcher()
_dispatcher.register("medium", CustomArticleCrawler)
_dispatcher.register("linkedin", LinkedInCrawler)
_dispatcher.register("github", GithubCrawler)
def handler(event, context: LambdaContext | None = None) -> dict[str, Any]:
first_name, last_name = lib.split_user_full_name(event.get("user"))
user_id = UserDocument.get_or_create(first_name=first_name, last_name=last_name)
link = event.get("link")
crawler = _dispatcher.get_crawler(link)
try:
crawler.extract(link=link, user=user_id)
return {"statusCode": 200, "body": "链接处理完毕"}
except Exception as e:
return {"statusCode": 500, "body": f"发生错误: {e}"}
→ 完整代码在这里:data_crawling/main.py
每个爬虫功能都根据其数据来源进行了定制,例如从LinkedIn抓取动态,从Medium抓取博文,从GitHub抓取仓库信息。
AWS 的高级架构图
我们创建了一个Python调度程序,负责管理特定域名的爬虫任务,以触发Lambda函数。
你可以为不同的域名注册各自的爬虫,然后通过 get_crawler()
方法为给定的 URL 获取合适的爬虫,如果没有为该域名注册爬虫,则默认使用 CustomArticleCrawler 爬虫。
import re
from aws_lambda_powertools import Logger
from crawlers.base import BaseCrawler
from crawlers.custom_article import CustomArticleCrawler
logger = Logger(service="llm-twin-course/crawler")
# 正则表达式,匹配以给定域名结尾的URL,支持www前缀
class CrawlerDispatcher:
def __init__(self) -> None:
self._crawlers = {}
def register(self, domain: str, crawler: type[BaseCrawler]) -> None:
# 注册爬虫,绑定域名与爬虫类
self._crawlers[r"https://(www\.)?{}.com/*".format(re.escape(domain))] = crawler
def get_crawler(self, url: str) -> BaseCrawler:
for pattern, crawler in self._crawlers.items():
if re.match(pattern, url):
return crawler()
否则:
logger.warning(
f"无法找到 {url} 的爬虫,将默认使用 CustomArticleCrawler。"
)
return CustomArticleCrawler() # 默认返回CustomArticleCrawler实例
爬虫抓取并处理其相应数据,然后发送到 MongoDB 数据仓库。
→ 完整代码如下:data_crawling/dispatcher.py
MongoDB组件作为统一的数据仓库,负责收集和管理由AWS Lambda函数收集的数据。
这个 系统 旨在从各种来源高效且易于扩展地 ETL 提取、转换和加载 数据到单一数据库。
6. 运行所有程序并填充你的 MongoDB 数据库中的数据首先,通过运行以下命令来启动您本地的基础设施:
本地启动
现在,你可以测试本地运行的Lambda函数爬虫,通过运行以下命令来抓取一篇测试的Medium文章:
make local-test-medium
另外,你可以用一个 GitHub URL 来测试吧:
local-test-github
请执行以下命令,将我们支持的所有网址填充进MongoDB数据库中:
本地数据导入
这会然后爬取文件中的所有链接。
请参阅我们安装与使用指南文档中的逐步安装和运行整个课程的指南。
最后在这门LLM Twin课程中,你学会了如何爬取这些数据源(如LinkedIn、GitHub、Medium等)的内容。
另外,你也学会了如何对数据进行标准化处理、清理,并将结果存入MongoDB。
通过使用调度器模式,我们有一个中心点,知道应该给哪个链接使用哪个爬虫。
最终,我们展示了如何将调度程序包装成AWS Lambda期望的接口,以便快速部署到AWS上。
在这节课里,我们讲解了如何构建数据流水线以及为什么它在机器学习项目中如此重要:
在第三课中,我们将深入探讨变更数据捕获(CDC)模式,并解释它是如何将数据工程与AI世界结合起来的。
🔗 可以看看浏览GitHub仓库 [1],并通过点个_⭐️_支持我们
我们发布的《LLM 工程师手册》启发了这个开源 LLM Twin 课程。
可以通过购买我们的书来支持我们的工作,以掌握一个完整的框架来构建和部署生产级的LLM和RAG系统——从数据到部署。
非常适合希望既想掌握理论又想具备实战经验的从业者,连接DE、研究、MLE和MLOps之间的点。
在亚马逊或 Packt 上购买《LLM 工程师手册》
《LLM 工程师手册》的封面 (https://www.amazon.com/LLM-Engineers-Handbook-engineering-production/dp/1836200072/)
喜欢这篇文章吗?加入解密ML获取实战内容,了解如何设计、编码和部署生产级别的AI大模型、推荐系统和MLOps系统。每周一个新项目↓
解码机器学习通讯 | 保罗·尤斯廷 | Substack加入获取实战验证的内容,关于设计、编码和部署生产级机器学习与MLOps系统的。每周一次。 参考 文学[1] 您的 LLM 孪生课程——GitHub 仓库 (大语言模型),Decoding ML GitHub 仓库 (2024 年)
图片除非另有说明,所有图片均由作者制作。
共同学习,写下你的评论
评论加载中...
作者其他优质文章