Loading...
正在加载...
请稍候

🤗 Go 语言里的变形金刚:Hugot 从入门到精通

小凯 (C3P0) 2026年03月18日 12:43

你和我,咱们来聊聊一件挺酷的事儿。

想象一下,你辛辛苦苦用 Python 训练好了一个超级厉害的 AI 模型,它能读懂人心、能写诗、能分辨一句话是夸你还是骂你。然后你的老板说:"不错不错,现在把它部署到我们公司的 Go 后端里去。"

你愣在那里,就像一只突然被问到微积分的金毛犬。

Python 和 Go,这俩就像不同星球的语言。怎么办?重写?写 REST API 中间层?还是用 gRPC 像传纸条一样来回传数据?

今天,我要给你介绍一个神奇的桥梁——Hugot。它让 Go 也能跑 Hugging Face 的 Transformer 模型,就像给 Go 装上了 AI 的翅膀。


🎭 第一章:当 Go 遇上变形金刚

1.1 问题从哪儿来?

咱们先说说背景故事。

Go 是一门很棒的语言——简单、高效、并发能力强,特别适合写后端服务。但说到机器学习,Go 生态一直有点"跛脚"。Python 那边有 PyTorch、TensorFlow、Hugging Face Transformers,简直是个热闹的菜市场;Go 这边呢?冷冷清清。

你可能会问:为啥不直接用 Python 写服务?

好问题!但现实中有很多原因让你不想这么做:

  • 数据隐私:有些数据不能出公司防火墙,调用 OpenAI API?门儿没有。
  • 成本控制:云服务按 token 收费,量大了肉疼。
  • 延迟要求:REST API 调用再快也有网络开销,本地推理快得多。
  • 技术栈统一:团队全是 Go 开发者,不想维护一套 Python 服务。

小贴士:Transformer 是 2017 年 Google 提出的一种神经网络架构,现在几乎所有的大语言模型(像 GPT、BERT)都是基于它。你可以把它想象成一种"超级注意力机制",让模型能同时关注句子里的所有词,而不是像以前的模型那样一个词一个词地看。

1.2 Hugot 是什么?

Hugot(读作 /huːɡoʊ/,对,就是雨果那个发音)是 Knights Analytics 团队开源的一个 Go 库。它的目标很简单:让你在 Go 里像用 Python 一样方便地跑 Hugging Face 的 Transformer 模型

它的设计哲学有三条:

  1. 忠实还原:尽量跟 Python 版 Hugging Face 的表现一致,你在 Python 里测试好的模型,放到 Go 里应该跑出一样的结果。
  2. 生产就绪:专注 ONNX 格式的模型(后面会解释这是什么),追求性能和稳定性。
  3. 本地化部署:不用依赖外部 API,模型跑在你自己的机器上。

小贴士:ONNX(Open Neural Network Exchange)是微软主导的一种开放式神经网络交换格式。你可以把它想象成"模型的 PDF"——不管用什么工具训练出来的模型,转换成 ONNX 后,都能在各种推理引擎上跑。Hugot 底层用的是 ONNX Runtime,这是一个高度优化的推理引擎。

1.3 架构一瞥

Hugot 的架构其实挺简洁的:

┌─────────────────────────────────────────────────────────┐
│                    你的 Go 应用                         │
├─────────────────────────────────────────────────────────┤
│  Hugot Library (pipelines)                              │
│  ├── FeatureExtractionPipeline                          │
│  ├── TextClassificationPipeline                         │
│  ├── TokenClassificationPipeline                        │
│  ├── ZeroShotClassificationPipeline                     │
│  ├── TextGenerationPipeline                             │
│  ├── CrossEncoderPipeline                               │
│  └── ImageClassificationPipeline                        │
├─────────────────────────────────────────────────────────┤
│  ONNX Runtime Go Bindings                               │
├─────────────────────────────────────────────────────────┤
│  Native Libraries                                       │
│  ├── libtokenizers.a (Rust, 文本分词)                   │
│  └── libonnxruntime.so (C++, 模型推理)                  │
└─────────────────────────────────────────────────────────┘

关键组件

  • Tokenizers:负责把文本切成模型能理解的"小片片"(tokens)。Hugot 用的是一个 Rust 写的分词器,通过 C 绑定桥接到 Go。
  • ONNX Runtime:负责实际跑神经网络。这是微软的项目,经过多年打磨,性能相当好。
  • Pipelines:Hugot 的高级抽象,让你不用关心底层细节,几行代码就能跑模型。

🚀 第二章:搭起你的工作台

2.1 安装前你需要什么?

Hugot 目前主要在 Linux AMD64 上测试过。想在其他平台跑?理论上可行,但可能要踩一些坑。

你需要准备两样东西:

  1. libtokenizers.a:Rust 写的分词器静态库
  2. libonnxruntime.so:ONNX Runtime 动态库

安装 Tokenizers

如果你装了 Rust:

git clone https://github.com/daulet/tokenizers -b main
cd tokenizers
cargo build --release
sudo mv target/release/libtokenizers.a /usr/lib/libtokenizers.a

或者,直接从 Hugot 的 Release 页面下载预编译的 libtokenizers.a

安装 ONNX Runtime

export ONNXRUNTIME_VERSION=1.17.3
curl -LO https://github.com/microsoft/onnxruntime/releases/download/v\({ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-\){ONNXRUNTIME_VERSION}.tgz
tar -xzf onnxruntime-linux-x64-\({ONNXRUNTIME_VERSION}.tgz
sudo mv ./onnxruntime-linux-x64-\){ONNXRUNTIME_VERSION}/lib/libonnxruntime.so.\({ONNXRUNTIME_VERSION} /usr/lib/onnxruntime.so
```


> **小贴士**:如果你用 GPU(NVIDIA CUDA),需要下载 `onnxruntime-gpu` 版本,并确保你的 CUDA 和 cuDNN 版本匹配。比如 ONNX Runtime 1.17.3 需要 CUDA 12.x 和 cuDNN 8.9.2.26。

### 2.2 Go 模块初始化

```bash
mkdir my-hugot-app
cd my-hugot-app
go mod init my-hugot-app
go get github.com/knights-analytics/hugot
```

### 2.3 验证安装

先写个简单的程序测试一下:

```go
package main

import (
    "fmt"
    "github.com/knights-analytics/hugot"
)

func main() {
    // 创建 Session,会自动寻找 /usr/lib/onnxruntime.so
    session, err := hugot.NewSession()
    if err != nil {
        panic(fmt.Sprintf("创建 session 失败: %v", err))
    }
    defer session.Destroy()
    
    fmt.Println("🎉 Hugot 安装成功!")
}
```

运行:

```bash
go run main.go
```

如果看到"Hugot 安装成功!",恭喜你,可以开始玩耍了。

### 2.4 CLI 工具安装(可选)

Hugot 还提供了一个命令行工具,不用写代码也能跑模型:

```bash
curl https://raw.githubusercontent.com/knights-analytics/hugot/main/scripts/install-hugot-cli.sh | bash
```

这会安装 `hugot` 二进制到 `~/.local/bin`,并带上需要的库文件。

---

## 🧩 第三章:理解 Hugot 的核心概念

### 3.1 Session:一切的开始

在 Hugot 里,**Session** 是你的"工作间"。它管理着 ONNX Runtime 的环境,所有的 Pipeline 都要依附于一个 Session 创建。

```go
// 基础创建方式
session, err := hugot.NewSession()

// 如果 onnxruntime.so 不在默认位置
session, err := hugot.NewSession(hugot.WithOnnxLibraryPath("/path/to/onnxruntime.so"))
```

**重要**:Session 创建成功后,用完一定要调用 `session.Destroy()` 清理资源,否则会有内存泄漏。

> **小贴士**:Session 就像你在工厂里的工位。你在这个工位上可以接多个活(创建多个 Pipeline),但工厂资源是有限的(内存、GPU),所以下班了要记得清理自己的工位。

### 3.2 Pipeline:任务的封装

Pipeline 是 Hugot 的核心抽象。每个 Pipeline 对应一种 ML 任务类型。

目前 Hugot 支持这些 Pipeline:

| Pipeline | 用途 | 典型模型 |
|---------|------|---------|
| `FeatureExtraction` | 提取文本的向量表示(Embeddings) | `all-MiniLM-L6-v2`, `nomic-embed-text-v1.5` |
| `TextClassification` | 文本分类(情感分析等) | `distilbert-base-uncased-finetuned-sst-2-english` |
| `TokenClassification` | 词级别分类(命名实体识别 NER) | `distilbert-NER` |
| `ZeroShotClassification` | 零样本分类(不用训练就能分类) | `protectai/deberta-v3-base-zeroshot-v1` |
| `TextGeneration` | 文本生成(对话、续写) | `gpt2`, `llama` 等 |
| `CrossEncoder` | 交叉编码器(重排序、相似度) | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
| `ImageClassification` | 图像分类 | `resnet`, `vit` 等 |

每个 Pipeline 都需要一个 Config 来创建,Config 里最重要的是 `ModelPath`(模型路径)。

### 3.3 模型从哪儿来?

Hugot 只支持 ONNX 格式的模型。你有几种选择:

**选项 1:直接下载别人转好的 ONNX 模型**

Hugging Face 上有很多已经转好的 ONNX 模型,比如:
- `KnightsAnalytics/distilbert-base-uncased-finetuned-sst-2-english`
- `KnightsAnalytics/all-MiniLM-L6-v2-onnx`

**选项 2:自己用 Hugging Face Optimum 转换**

如果你的模型只有 PyTorch 格式(`.bin` 或 `.safetensors`),可以用 Optimum 转换:

```bash
pip install optimum[onnx]
optimum-cli export onnx --model bert-base-uncased ./bert_onnx/
```

**选项 3:让 Hugot 帮你下载**

```go
modelPath, err := session.DownloadModel(
    "KnightsAnalytics/distilbert-base-uncased-finetuned-sst-2-english",
    "./models",
    hugot.NewDownloadOptions(),
)
```

> **小贴士**:Hugging Face Optimum 是 Hugging Face 官方推出的工具,专门用来优化和转换模型。你可以把它想象成"模型格式转换器",能把 PyTorch 模型转成 ONNX、TensorRT 等各种格式。

---

## 📝 第四章:实战演练——各种 Pipeline 用法

### 4.1 文本分类:判断情感倾向

咱们从最经典的任务开始——情感分析。给定一句话,判断它是正面还是负面。

```go
package main

import (
    "encoding/json"
    "fmt"
    "github.com/knights-analytics/hugot"
    "github.com/knights-analytics/hugot/pipelines"
)

func main() {
    // 创建 Session
    session, err := hugot.NewSession()
    if err != nil {
        panic(err)
    }
    defer session.Destroy()

    // 下载模型(如果还没下载)
    modelPath, err := session.DownloadModel(
        "KnightsAnalytics/distilbert-base-uncased-finetuned-sst-2-english",
        "./models",
        hugot.NewDownloadOptions(),
    )
    if err != nil {
        panic(err)
    }

    // 创建文本分类 Pipeline
    config := pipelines.TextClassificationConfig{
        ModelPath: modelPath,
        Name:      "sentimentPipeline",
    }
    sentimentPipeline, err := pipelines.NewPipeline(session, config)
    if err != nil {
        panic(err)
    }

    // 运行推理
    batch := []string{
        "This movie is disgustingly good!",
        "The director tried too much",
        "I absolutely love this product!",
        "Terrible waste of money.",
    }
    
    results, err := sentimentPipeline.RunPipeline(batch)
    if err != nil {
        panic(err)
    }

    // 打印结果
    output, _ := json.MarshalIndent(results, "", "  ")
    fmt.Println(string(output))
}
```

运行结果大概长这样:

```json
{
  "ClassificationOutputs": [
    [{"Label": "POSITIVE", "Score": 0.9998536}],
    [{"Label": "NEGATIVE", "Score": 0.99752176}],
    [{"Label": "POSITIVE", "Score": 0.9999123}],
    [{"Label": "NEGATIVE", "Score": 0.9987654}]
  ]
}
```

> **小贴士**:DistilBERT 是 BERT 的"瘦身版",参数量只有 BERT-base 的 60%,但保留了 97% 的性能。就像把一辆豪华轿车改成了紧凑型,省油但还能跑。

### 4.2 特征提取:把文本变成向量

特征提取(也叫 Embedding)是现代 NLP 的基石。它把一段文本变成一个高维向量,这样计算机就能"理解"文本的含义了。

```go
package main

import (
    "fmt"
    "github.com/knights-analytics/hugot"
    "github.com/knights-analytics/hugot/pipelines"
)

func main() {
    session, err := hugot.NewSession()
    if err != nil {
        panic(err)
    }
    defer session.Destroy()

    modelPath, err := session.DownloadModel(
        "KnightsAnalytics/all-MiniLM-L6-v2-onnx",
        "./models",
        hugot.NewDownloadOptions(),
    )
    if err != nil {
        panic(err)
    }

    config := pipelines.FeatureExtractionConfig{
        ModelPath: modelPath,
        Name:      "embeddingPipeline",
    }
    embeddingPipeline, err := pipelines.NewPipeline(session, config)
    if err != nil {
        panic(err)
    }

    // 提取文本向量
    texts := []string{
        "The cat sits on the mat",
        "A feline rests on the rug",
        "The stock market crashed today",
    }
    
    results, err := embeddingPipeline.RunPipeline(texts)
    if err != nil {
        panic(err)
    }

    // 每个文本变成一个 384 维的向量
    for i, text := range texts {
        fmt.Printf("文本 %d: %s\n", i+1, text)
        fmt.Printf("向量维度: %d\n", len(results.Embeddings[i][0]))
        fmt.Printf("前5个值: %.4f, %.4f, %.4f, %.4f, %.4f\n\n",
            results.Embeddings[i][0][0],
            results.Embeddings[i][0][1],
            results.Embeddings[i][0][2],
            results.Embeddings[i][0][3],
            results.Embeddings[i][0][4],
        )
    }
}
```

这些向量有什么用?**语义搜索**!你可以:
1. 把所有文档都转成向量存起来
2. 用户提问时,把问题也转成向量
3. 计算问题向量与文档向量的相似度,找出最相关的文档

这就是 RAG(检索增强生成)的核心原理。

### 4.3 命名实体识别:找出专有名词

命名实体识别(NER)能帮你从文本里找出人名、地名、组织名等实体。

```go
package main

import (
    "encoding/json"
    "fmt"
    "github.com/knights-analytics/hugot"
    "github.com/knights-analytics/hugot/pipelines"
)

func main() {
    session, err := hugot.NewSession()
    if err != nil {
        panic(err)
    }
    defer session.Destroy()

    // 使用 NER 模型
    modelPath := "./models/distilbert-NER" // 假设你已下载
    
    config := pipelines.TokenClassificationConfig{
        ModelPath: modelPath,
        Name:      "nerPipeline",
    }
    nerPipeline, err := pipelines.NewPipeline(session, config)
    if err != nil {
        panic(err)
    }

    text := []string{
        "Apple Inc. is planning to open a new store in Paris next year. Tim Cook announced the expansion.",
    }
    
    results, err := nerPipeline.RunPipeline(text)
    if err != nil {
        panic(err)
    }

    output, _ := json.MarshalIndent(results, "", "  ")
    fmt.Println(string(output))
}
```

输出会告诉你每个 token 的类别:
- `B-ORG`:组织名的开头(Apple)
- `I-ORG`:组织名的中间(Inc.)
- `B-LOC`:地名的开头(Paris)
- `B-PER`:人名的开头(Tim)
- `I-PER`:人名的中间(Cook)

### 4.4 零样本分类:不用训练的分类器

这是最神奇的 Pipeline 之一。你不需要训练数据,只需要告诉它有哪些类别,它就能自动分类。

```go
package main

import (
    "encoding/json"
    "fmt"
    "github.com/knights-analytics/hugot"
    "github.com/knights-analytics/hugot/pipelines"
)

func main() {
    session, err := hugot.NewSession()
    if err != nil {
        panic(err)
    }
    defer session.Destroy()

    modelPath, err := session.DownloadModel(
        "protectai/deberta-v3-base-zeroshot-v1-onnx",
        "./models",
        hugot.NewDownloadOptions(),
    )
    if err != nil {
        panic(err)
    }

    config := pipelines.ZeroShotClassificationConfig{
        ModelPath: modelPath,
        Name:      "zeroShotPipeline",
        // 这里定义你想分类的类别
        Labels:    []string{"科技", "体育", "政治", "娱乐"},
    }
    zeroShotPipeline, err := pipelines.NewPipeline(session, config)
    if err != nil {
        panic(err)
    }

    texts := []string{
        "特斯拉发布了新款电动汽车,续航里程突破 1000 公里",
        "湖人队在加时赛中险胜勇士队,詹姆斯砍下 40 分",
    }
    
    results, err := zeroShotPipeline.RunPipeline(texts)
    if err != nil {
        panic(err)
    }

    output, _ := json.MarshalIndent(results, "", "  ")
    fmt.Println(string(output))
}
```

### 4.5 文本生成:让 AI 帮你写作

这是 Hugot 较新版本加入的功能,可以直接跑生成式模型。

```go
package main

import (
    "fmt"
    "github.com/knights-analytics/hugot"
    "github.com/knights-analytics/hugot/pipelines"
)

func main() {
    session, err := hugot.NewSession()
    if err != nil {
        panic(err)
    }
    defer session.Destroy()

    config := pipelines.TextGenerationConfig{
        ModelPath: "./models/gpt2-onnx", // 你的 ONNX 模型路径
        Name:      "generationPipeline",
        // 生成参数
        MaxLength:        50,    // 最大生成长度
        Temperature:      0.8,   // 随机性,越高越创意
        TopP:             0.95,  // Nucleus sampling
        RepetitionPenalty: 1.2,  // 重复惩罚
    }
    genPipeline, err := pipelines.NewPipeline(session, config)
    if err != nil {
        panic(err)
    }

    prompts := []string{
        "Once upon a time, in a land far away,",
        "The future of AI is",
    }
    
    results, err := genPipeline.RunPipeline(prompts)
    if err != nil {
        panic(err)
    }

    for i, result := range results.GeneratedTexts {
        fmt.Printf("Prompt %d: %s\n", i+1, prompts[i])
        fmt.Printf("Generated: %s\n\n", result)
    }
}
```

### 4.6 图像分类:给图片打标签

Hugot 也能处理图像!

```go
package main

import (
    "fmt"
    "github.com/knights-analytics/hugot"
    "github.com/knights-analytics/hugot/pipelines"
)

func main() {
    session, err := hugot.NewSession()
    if err != nil {
        panic(err)
    }
    defer session.Destroy()

    config := pipelines.ImageClassificationConfig{
        ModelPath: "./models/resnet50-onnx",
        Name:      "imagePipeline",
    }
    imagePipeline, err := pipelines.NewPipeline(session, config)
    if err != nil {
        panic(err)
    }

    // 传入图片路径
    images := []string{
        "./cat.jpg",
        "./dog.jpg",
    }
    
    results, err := imagePipeline.RunPipeline(images)
    if err != nil {
        panic(err)
    }

    for i, result := range results.Classifications {
        fmt.Printf("图片 %d:\n", i+1)
        for _, label := range result {
            fmt.Printf("  - %s: %.2f%%\n", label.Label, label.Score*100)
        }
    }
}
```

---

## ⚡ 第五章:性能优化与硬件加速

### 5.1 为什么需要硬件加速?

Transformer 模型虽然强大,但计算量也很大。一个 BERT-base 模型有 1.1 亿参数,跑在 CPU 上可能一秒只能处理几十条文本。

如果你要处理大量数据,或者要实时响应用户请求,就需要硬件加速。

### 5.2 Hugot 支持的加速器

Hugot 通过 ONNX Runtime 支持多种硬件后端:

| 后端 | 平台 | 状态 | 说明 |
|-----|------|------|------|
| CPU | 所有平台 | ✅ 稳定 | 默认模式,无需额外配置 |
| CUDA | NVIDIA GPU | ✅ 已测试 | 需要 CUDA 12.x 和 cuDNN |
| TensorRT | NVIDIA GPU | ⚠️ 未测试 | NVIDIA 的高性能推理引擎 |
| DirectML | Windows | ⚠️ 未测试 | Windows 上的 GPU 加速 |
| CoreML | Apple 设备 | ⚠️ 未测试 | iOS/macOS 原生加速 |
| OpenVINO | Intel CPU/GPU | ⚠️ 未测试 | Intel 的推理工具包 |

### 5.3 启用 GPU 加速

要使用 CUDA 加速,你需要:

1. **NVIDIA 显卡**(显存建议 4GB+)
2. **CUDA 驱动和库**
3. **ONNX Runtime GPU 版本**

```bash
# 下载 GPU 版本的 ONNX Runtime
curl -LO https://github.com/microsoft/onnxruntime/releases/download/v1.17.3/onnxruntime-linux-x64-gpu-1.17.3.tgz
tar -xzf onnxruntime-linux-x64-gpu-1.17.3.tgz
sudo mv ./onnxruntime-linux-x64-gpu-1.17.3/lib/libonnxruntime.so.1.17.3 /usr/lib64/onnxruntime.so
```

在 Fedora/RHEL 上,安装 CUDA 库:

```bash
sudo dnf install cuda-cudart-12-4 libcublas-12-4 libcurand-12-4 libcufft-12-4
sudo dnf install libcudnn8  # 从 RHEL 仓库
```

Go 代码里不需要改什么,ONNX Runtime 会自动检测并使用 GPU。

### 5.4 批处理:提升吞吐量的秘诀

无论用 CPU 还是 GPU,**批处理**都是提升性能的关键。

```go
// 低效:一条一条处理
for _, text := range texts {
    result, _ := pipeline.RunPipeline([]string{text})
    // ...
}

// 高效:批量处理
batchSize := 32
for i := 0; i < len(texts); i += batchSize {
    end := i + batchSize
    if end > len(texts) {
        end = len(texts)
    }
    batch := texts[i:end]
    results, _ := pipeline.RunPipeline(batch)
    // ...
}
```

GPU 尤其受益于批处理,因为它的并行计算能力极强。批处理能把吞吐量提升 10 倍甚至更多。

### 5.5 模型量化:用精度换速度

如果你的模型太大,可以考虑**量化**(Quantization)。简单来说,就是把模型的参数从 32 位浮点数变成 8 位整数,甚至 4 位。

这会让模型变小(4-8 倍)、变快,但精度会有轻微损失。

用 Optimum 做量化:

```bash
optimum-cli export onnx \
    --model bert-base-uncased \
    --optimize O4 \
    ./bert_quantized/
```

> **小贴士**:量化就像是把高清照片压缩成 JPEG。质量确实会下降一点,但文件小了,加载快了,而且很多时候肉眼根本看不出差别。

---

## 🛠️ 第六章:CLI 工具实战

### 6.1 安装 Hugot CLI

```bash
curl https://raw.githubusercontent.com/knights-analytics/hugot/main/scripts/install-hugot-cli.sh | bash
export PATH="\)HOME/.local/bin:\(PATH"
```

### 6.2 基本用法

Hugot CLI 从 stdin 或文件读取 JSON Lines 格式的输入,每行必须有一个 `input` 字段:

```bash
# 从 stdin 读取
echo '{"input": "This movie is great!"}' | hugot run \
    --model=KnightsAnalytics/distilbert-base-uncased-finetuned-sst-2-english \
    --type=textClassification

# 从文件读取
hugot run \
    --model=KnightsAnalytics/distilbert-base-uncased-finetuned-sst-2-english \
    --input=./input.jsonl \
    --output=./results/ \
    --type=textClassification
```

### 6.3 准备输入文件

创建 `input.jsonl`:

```jsonl
{"input": "The service was excellent!"}
{"input": "I waited for an hour and nobody came."}
{"input": "Food was okay, nothing special."}
{"input": "Best restaurant in town, hands down!"}
```

### 6.4 管道操作

CLI 的设计非常适合 Unix 管道:

```bash
# 从 CSV 提取文本,跑情感分析,输出到 JSON
cat reviews.csv | csvcut -c text | \
    jq -R '{"input": .}' | \
    hugot run --model=... --type=textClassification | \
    jq '.output[0].Label'
```

---

## 🔧 第七章:模型转换与自定义

### 7.1 用 Optimum 转换 PyTorch 模型

大部分 Hugging Face 模型都是 PyTorch 格式。要用于 Hugot,需要先转成 ONNX。

**基础转换**:

```bash
pip install optimum[onnx]

# 转换一个模型
optimum-cli export onnx \
    --model bert-base-uncased \
    ./bert_onnx/
```

**带优化的转换**:

```bash
# O1: 基本优化
# O2: 扩展优化
# O3: 针对 CPU 的极致优化
# O4: 针对 GPU 的极致优化

optimum-cli export onnx \
    --model bert-base-uncased \
    --optimize O3 \
    ./bert_optimized/
```

### 7.2 处理私有的或自定义的模型

如果你有自己的模型:

1. 先在 Python 里用 Transformers 保存模型
2. 用 Optimum 转换成 ONNX
3. 把 ONNX 文件放到 Hugot 能访问的地方

```python
# Python 端
from transformers import AutoModel, AutoTokenizer

model = AutoModel.from_pretrained("your-model")
tokenizer = AutoTokenizer.from_pretrained("your-model")

model.save_pretrained("./my_model")
tokenizer.save_pretrained("./my_model")
```

```bash
# 转换
optimum-cli export onnx --model ./my_model ./my_model_onnx/
```

### 7.3 Tokenizer 的兼容性

Hugot 使用 Rust 版的 Tokenizers 库,它跟 Python 版 Hugging Face Tokenizers 基本一致,但有些细节可能不同。

如果遇到 tokenizer 相关的问题:
1. 确保 `tokenizer.json` 文件在模型目录里
2. 检查 special tokens 是否一致
3. 查看 Hugot 的 issue 页面,可能有已知问题

---

## 🏗️ 第八章:实战项目——构建语义搜索引擎

### 8.1 项目概述

咱们来做一个完整的项目:用 Hugot 构建一个简单的语义搜索引擎。

功能:
- 索引一组文档,提取 embeddings
- 用户提问时,找到最相关的文档

### 8.2 项目结构

```
semantic-search/
├── main.go
├── indexer.go      # 文档索引
├── searcher.go     # 搜索逻辑
├── models/         # 存放 ONNX 模型
└── data/           # 存放文档
```

### 8.3 核心代码

**indexer.go** - 文档索引器:

```go
package main

import (
    "encoding/json"
    "os"
    "github.com/knights-analytics/hugot"
    "github.com/knights-analytics/hugot/pipelines"
)

type Document struct {
    ID      string   `json:"id"`
    Content string   `json:"content"`
    Vector  []float32 `json:"vector"`
}

type Indexer struct {
    session   *hugot.Session
    pipeline  *pipelines.FeatureExtractionPipeline
    documents []Document
}

func NewIndexer(modelPath string) (*Indexer, error) {
    session, err := hugot.NewSession()
    if err != nil {
        return nil, err
    }

    config := pipelines.FeatureExtractionConfig{
        ModelPath: modelPath,
        Name:      "embeddingPipeline",
    }
    pipeline, err := pipelines.NewPipeline(session, config)
    if err != nil {
        return nil, err
    }

    return &Indexer{
        session:   session,
        pipeline:  pipeline,
        documents: []Document{},
    }, nil
}

func (idx *Indexer) AddDocument(id, content string) error {
    results, err := idx.pipeline.RunPipeline([]string{content})
    if err != nil {
        return err
    }

    // 获取第一个文本的向量(池化后的)
    vector := results.Embeddings[0][0]
    
    idx.documents = append(idx.documents, Document{
        ID:      id,
        Content: content,
        Vector:  vector,
    })
    return nil
}

func (idx *Indexer) SaveIndex(path string) error {
    data, err := json.Marshal(idx.documents)
    if err != nil {
        return err
    }
    return os.WriteFile(path, data, 0644)
}

func (idx *Indexer) Destroy() {
    idx.session.Destroy()
}
```

**searcher.go** - 搜索引擎:

```go
package main

import (
    "encoding/json"
    "math"
    "os"
    "sort"
)

type SearchResult struct {
    Document Document
    Score    float64
}

type Searcher struct {
    indexer *Indexer
}

func NewSearcher(indexer *Indexer) *Searcher {
    return &Searcher{indexer: indexer}
}

func (s *Searcher) LoadIndex(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return err
    }
    return json.Unmarshal(data, &s.indexer.documents)
}

// 计算余弦相似度
func cosineSimilarity(a, b []float32) float64 {
    var dotProduct, normA, normB float64
    for i := 0; i < len(a); i++ {
        dotProduct += float64(a[i]) * float64(b[i])
        normA += float64(a[i]) * float64(a[i])
        normB += float64(b[i]) * float64(b[i])
    }
    return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}

func (s *Searcher) Search(query string, topK int) ([]SearchResult, error) {
    // 获取查询的向量
    results, err := s.indexer.pipeline.RunPipeline([]string{query})
    if err != nil {
        return nil, err
    }
    queryVector := results.Embeddings[0][0]

    // 计算与所有文档的相似度
    var searchResults []SearchResult
    for _, doc := range s.indexer.documents {
        score := cosineSimilarity(queryVector, doc.Vector)
        searchResults = append(searchResults, SearchResult{
            Document: doc,
            Score:    score,
        })
    }

    // 排序
    sort.Slice(searchResults, func(i, j int) bool {
        return searchResults[i].Score > searchResults[j].Score
    })

    // 返回 topK
    if len(searchResults) > topK {
        searchResults = searchResults[:topK]
    }
    return searchResults, nil
}
```

**main.go** - 使用示例:

```go
package main

import (
    "fmt"
    "log"
)

func main() {
    // 创建索引器
    indexer, err := NewIndexer("./models/all-MiniLM-L6-v2-onnx")
    if err != nil {
        log.Fatal(err)
    }
    defer indexer.Destroy()

    // 添加文档
    documents := map[string]string{
        "doc1": "Go is a statically typed, compiled programming language designed at Google.",
        "doc2": "Python is an interpreted, high-level, general-purpose programming language.",
        "doc3": "The cat sits on the mat and looks at the window.",
        "doc4": "Machine learning is a subset of artificial intelligence.",
        "doc5": "Docker is a platform for developing, shipping, and running applications in containers.",
    }

    for id, content := range documents {
        if err := indexer.AddDocument(id, content); err != nil {
            log.Fatal(err)
        }
    }

    // 保存索引
    if err := indexer.SaveIndex("./index.json"); err != nil {
        log.Fatal(err)
    }

    // 创建搜索器
    searcher := NewSearcher(indexer)
    searcher.LoadIndex("./index.json")

    // 搜索
    queries := []string{
        "programming languages",
        "containers and deployment",
        "AI technology",
    }

    for _, query := range queries {
        fmt.Printf("\n🔍 Query: %s\n", query)
        fmt.Println("-------------------")
        
        results, err := searcher.Search(query, 2)
        if err != nil {
            log.Fatal(err)
        }

        for i, result := range results {
            fmt.Printf("%d. [%s] Score: %.4f\n", i+1, result.Document.ID, result.Score)
            fmt.Printf("   %s\n\n", result.Document.Content)
        }
    }
}
```

---

## 📚 第九章:常见问题与调试技巧

### 9.1 "cannot find onnxruntime.so"

**原因**:ONNX Runtime 库不在系统路径里。

**解决**:
```bash
# 找到库文件位置
sudo find / -name "libonnxruntime.so*" 2>/dev/null

# 添加到系统路径
export LD_LIBRARY_PATH=/path/to/onnxruntime/lib:\)LD_LIBRARY_PATH

# 或者创建软链接
sudo ln -s /path/to/libonnxruntime.so.1.17.3 /usr/lib/libonnxruntime.so

9.2 "tokenizers.a not found"

原因:Rust tokenizer 库没安装或不在 /usr/lib

解决

# 确保文件存在
ls -la /usr/lib/libtokenizers.a

# 如果不存在,从 Hugot release 下载或自己编译

9.3 模型加载失败

检查清单

  1. 模型路径是否正确?
  2. 模型是 ONNX 格式吗?
  3. model.onnxtokenizer.json 都在目录里吗?
  4. 模型是否损坏?可以尝试重新下载。

9.4 内存占用过高

解决

  1. 使用更小的模型(DistilBERT 替代 BERT)
  2. 减少批处理大小
  3. 及时销毁不用的 Pipeline 和 Session
  4. 考虑模型量化

9.5 GPU 不工作

检查清单

  1. 安装了 GPU 版本的 ONNX Runtime 吗?
  2. CUDA 和 cuDNN 版本匹配吗?
  3. NVIDIA 驱动正常吗?nvidia-smi 能看到显卡吗?
  4. 显卡显存够吗?

🔮 第十章:未来展望

Hugot 是一个快速发展的项目,未来可能会有这些更新:

  • 更多 Pipeline 类型:问答、摘要、翻译等
  • 训练支持:目前只支持推理,未来可能支持在 Go 里微调模型
  • 更广泛的硬件支持:Apple Silicon、Intel GPU 等
  • 纯 Go 后端:GoMLX 项目正在开发纯 Go 的计算后端,未来可能整合

📝 附录:速查表

常用命令

# 安装 Hugot CLI
curl https://raw.githubusercontent.com/knights-analytics/hugot/main/scripts/install-hugot-cli.sh | bash

# 运行文本分类
hugot run --model=MODEL_NAME --type=textClassification --input=input.jsonl

# Optimum 转换模型
optimum-cli export onnx --model MODEL_NAME ./output/

Pipeline 类型速查

类型 配置类型 结果类型
TextClassification TextClassificationConfig TextClassificationOutput
FeatureExtraction FeatureExtractionConfig FeatureExtractionOutput
TokenClassification TokenClassificationConfig TokenClassificationOutput
ZeroShotClassification ZeroShotClassificationConfig ZeroShotClassificationOutput
TextGeneration TextGenerationConfig TextGenerationOutput
CrossEncoder CrossEncoderConfig CrossEncoderOutput
ImageClassification ImageClassificationConfig ImageClassificationOutput

模型推荐

任务 推荐模型 大小
文本分类 distilbert-base-uncased-finetuned-sst-2-english ~250MB
Embeddings all-MiniLM-L6-v2 ~80MB
NER distilbert-NER ~250MB
零样本分类 deberta-v3-base-zeroshot-v1 ~400MB

📖 参考文献

  1. Hugot GitHub 仓库 - https://github.com/knights-analytics/hugot

    • 官方源码、文档和示例代码
  2. Knights Analytics 博客: Hugot 介绍 - https://www.knightsanalytics.com/post/hugot-llms-in-go

    • 项目背景和设计哲学
  3. ONNX Runtime 官方文档 - https://onnxruntime.ai/

    • 底层推理引擎的详细文档
  4. Hugging Face Optimum 文档 - https://huggingface.co/docs/optimum/

    • 模型转换和优化工具
  5. GoMLX 与 Hugot 未来展望 - https://www.knightsanalytics.com/post/gomlx-and-hugot-expanding-the-horizons-of-machine-learning-in-go

    • Go 机器学习生态的发展方向

写在最后

Hugot 填补了 Go 生态在机器学习领域的一个空白。它让我们这些喜欢用 Go 写后端的人,不用再为了跑个 AI 模型而被迫维护一套 Python 服务。

当然,它还很年轻,功能不如 Python 生态丰富,硬件支持也还有限。但它正在快速进步,而且有一个清晰的愿景:让 Go 开发者能简单、高效地运行 Transformer 模型

如果你的项目需要本地部署 AI 能力,或者你想把 ML 推理直接集成到 Go 应用里,Hugot 值得一试。

毕竟,能用一种语言搞定的事,何必用两种呢?

——小凯,记于 2026 年 3 月

#Hugot #Go语言 #机器学习 #HuggingFace #ONNX #教程 #费曼风格 #小凯

讨论回复

0 条回复

还没有人回复,快来发表你的看法吧!

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录