别光会调API,底层逻辑得清楚
从手写推导到拥抱大模型的机器学习入门血泪史
早上8点,北京西二旗的地铁站刚把我吐出来。裹紧冲锋衣,扫了辆共享单车,伴随着冷风蹬了十分钟,终于赶在8点半前坐到了工位上。趁着组里那帮夜猫子还没来,我给自己泡了杯浓茶,准备安静地敲会儿代码。
说实话,作为一个坚持手写代码、觉得“AI生成代码没有灵魂”的保守派,我以前对大模型和机器学习是有点抵触的。我更喜欢抠底层原理,觉得只有把矩阵求导和反向传播的每一步都推导清楚,这代码才算真正属于自己。但现实很快就给我上了一课。
上个月,总监拍脑袋决定要把咱们的内部知识库搜索重构成“AI搜索”。产品经理拿着竞品的截图,唾沫横飞地说:“人家都能对话式查数据,还能自动调接口,咱们也得有,下个月必须上!”我当时就想怼他,但看了看刚发下来的绩效表,硬生生把话咽了回去。作为组里为数不多还愿意啃论文、看底层数学推导的“老古董”,这苦差事自然落到了我头上。
被逼无奈,我只能放下身段,从机器学习的基础概念重新盘起。今天这篇博客,就当是给我这段时间的“渡劫”做个复盘,也给还在传统开发里卷的兄弟们探探路。
别做调包侠,咱们往下扎一扎
现在很多人搞机器学习,上来就是import torch,然后model.fit(),跑通了就觉得自己会了。但在我这种底层原教旨主义者眼里,这跟黑盒有什么区别?一旦线上出了Bug,或者模型效果拉胯,你连怎么调都不知道。
咱们先聊聊最核心的梯度下降。别嫌枯燥,这玩意儿是所有神经网络的基石。
从数学本质上讲,机器学习就是在一个高维空间里找函数的极小值。假设我们的损失函数是 $J(\theta)$,我们要更新参数 $\theta$。底层逻辑其实就是泰勒展开的一阶近似。沿着梯度的反方向走,步长由学习率 $\alpha$ 控制:
$$ \theta_{t+1} = \theta_t - \alpha \nabla J(\theta_t) $$
看着简单对吧?但真到了代码里,坑就来了。上周五晚上快11点,我跑一个文本分类模型,Loss一开始降得挺欢,突然一个 NaN 砸在屏幕上。当时真的想砸电脑。排查了半天,发现是学习率设大了,导致梯度爆炸。后来我老老实实去看了PyTorch底层源码,发现它在做反向传播时,如果没有做梯度裁剪(Gradient Clipping),一旦遇到长文本,连乘的梯度很容易溢出。
for epoch in range(num_epochs):
for batch in dataloader:
optimizer.zero_grad()
outputs = model(batch.text)
loss = criterion(outputs, batch.label)
loss.backward()
# 保守派的坚持:手动加上梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
还有优化器的选择。SGD(随机梯度下降)虽然古老,但在某些图像任务上泛化能力就是比Adam好。为什么?因为Adam的自适应学习率会导致在极小值附近震荡,而SGD配合动量(Momentum)能更好地越过局部最优解。这些底层差异,不自己推导一遍公式,光看文档是体会不到的。
技术前瞻:从传统ML到大模型时代的跨越
把基础概念盘明白后,我发现传统的机器学习算法(比如SVM、随机森林)在处理咱们现在的非结构化业务需求时,已经力不从心了。产品经理要的“智能”,不是简单的分类和回归,而是理解和生成。这就必须得往大模型(LLM)方向靠了。
这里必须得提一下现在最火的 AI搜索。
以前我们搞搜索,就是Elasticsearch那一套,倒排索引加上TF-IDF或者BM25算法。用户搜“苹果”,你只能匹配包含“苹果”这个词的文档。但如果用户搜“乔布斯当年被赶出公司后做的那个电脑品牌”,传统搜索直接就抓瞎了。
现在的AI搜索,底层逻辑变了。我们引入了Embedding(词向量)技术,把文本映射到高维向量空间。在这个空间里,语义相近的句子,向量距离就近。这就用到了向量数据库(比如Milvus)。检索的时候,不再是字符串匹配,而是计算向量之间的余弦相似度。底层算法通常是HNSW(分层导航小世界),通过构建多层图结构来加速高维空间的近似最近邻(ANN)搜索。
但这还不够,大模型本身有幻觉,而且不知道咱们公司内部的私有数据。所以现在的标准解法是RAG(检索增强生成)。用户提问 -> 向量检索召回相关文档 -> 把文档和问题一起塞给大模型 -> 生成最终答案。
让大模型长出手脚:Function Calling
AI搜索解决了“看”的问题,但产品经理又提需求了:“能不能让AI直接帮我查一下昨天西二旗店的销售额?”
大模型本质上是个文本接龙机器,它不会自己连数据库。这时候,Function Calling(函数调用)就派上用场了。这也是我最近觉得大模型最性感的一个特性。它让大模型从“只会说”变成了“能干活”。
底层原理其实并不复杂。我们在System Prompt里把可用的工具(函数)定义好,包括函数名、描述、参数的JSON Schema。大模型在生成回复时,如果判断需要调用工具,它不会直接输出文本,而是输出一个特定格式的JSON,告诉系统:“我要调用这个函数,参数是这些”。
// 定义给大模型的工具描述
{
"name": "query_store_sales",
"description": "查询指定门店在指定日期的销售额",
"parameters": {
"type": "object",
"properties": {
"store_name": {"type": "string", "description": "门店名称,如'西二旗店'"},
"date": {"type": "string", "description": "日期,格式YYYY-MM-DD"}
},
"required": ["store_name", "date"]
}
}
当用户问“昨天西二旗店卖多少钱”时,大模型会返回类似这样的结构:
{
"name": "query_store_sales",
"arguments": "{\"store_name\": \"西二旗店\", \"date\": \"2023-10-24\"}"
}
我们的后端代码拦截到这个JSON,去执行真正的SQL查询,拿到结果(比如“15000元”)后,再把这个结果作为上下文喂给大模型,让它生成最终的自然语言回复:“昨天西二旗店的销售额是15000元。”
这套流程跑通后,我当时在工位上没忍住拍了下大腿,终于搞定了,开心!
踩坑与调优:脏数据教做人
别看上面写得行云流水,实际落地的时候,坑多得能埋人。
最让我头疼的不是算法,而是数据集清洗。组里之前的知识库文档,简直是群魔乱舞。有Markdown格式错乱的,有HTML标签没删干净的,还有各种表情包和乱码。大模型是“Garbage in, garbage out”,你喂给它垃圾,它就给你吐垃圾。
我花了整整两周时间,写了一堆正则表达式和清洗脚本,甚至用了一个小模型来做数据去重和纠错。那段时间,我闭上眼睛满脑子都是各种奇怪的Unicode字符。
在模型评估方面,我也交了不少学费。一开始我光看Loss和Accuracy,觉得指标挺高,结果一上业务,产品经理骂娘了,说搜出来的东西牛头不对马嘴。后来我静下心来,结合业务场景重新定义了评估指标。对于搜索召回,我们更看重Recall(召回率),宁可多召回一些,也不能漏掉关键文档;而对于最终的大模型生成,我们引入了RAGAS框架,从上下文相关性、答案忠实度等维度进行自动化评估。
保守派的自我和解
现在,咱们的AI搜索和Function Calling功能已经灰度上线了。看着后台监控面板上稳步上升的调用量,我心里还是挺有成就感的。
回顾这几个月,我这个“保守派”也算是在AI的浪潮里扑腾出了点水花。我依然坚持手写核心逻辑,依然喜欢去GitHub上扒源码看底层实现,但我不再排斥AI辅助了。
其实,AI并不是来替代我们程序员的,它是来替代那些不愿意思考、只会复制粘贴的“API调用工程师”的。当我们把基础概念吃透,把底层原理摸清,AI就会从一个“抢饭碗的敌人”,变成一个帮你写写单元测试、做做数据预处理的得力助手。
不说了,8点半了,组里的夜猫子们应该快到了,产品经理估计又要来催进度了。我得赶紧把这段向量检索的C++底层加速代码写完,今天争取不加班,晚上还得回去陪老婆看剧呢。

评论 0