这篇用
hfCLI 把一个多语言情感模型按固定 revision 只下载需要的文件,再对照 Hub 上记录的 LFS sha256 校验权重是否完整。 之后完全离线(HF_HUB_OFFLINE=1)对一批商品评价做情感分类,置信度不够的标记为“需人工复核”。
先看清成品(box 上真实运行,CPU)
- 只下载了 6 个文件,共 521 MB,跳过了 ONNX、TF、
pytorch_model.bin等重复格式。model.safetensors的 sha256 与 Hub 记录一致(0ab095b690339440…)。- 6 条评价 3.4 秒分完,“颜色暗一点但可以接受”被标记为需要复核。
- pytest 3 项通过。
合规提醒:Hub 上每个模型和数据集都有自己的许可证,下载前先看卡片。本文用的模型
lxyuan/distilbert-base-multilingual-cased-sentiments-student是 Apache-2.0,但它的卡片写明是在tyqiangz/multilingual-sentiments上蒸馏的,而这个数据集的中文部分来自amazon_reviews_multi。后者的原始条款是“仅限非商业研究”,并且已经被数据提供方下架。所以商用前请自行评估训练数据的风险,本文只做本地学习演示。Token 不要写进代码或仓库。本文不列 Hugging Face 付费方案的价格,以官网为准。
运行说明:全部在 box 上真实运行,没有登录 Hugging Face(匿名下载公开模型)。没有用 GPU,也没有测试 ONNX 推理。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
download_model.sh |
固定 revision、按模式筛选文件的下载命令 |
verify_model.py |
对照 Hub API 返回的 LFS sha256,校验本地权重 |
classify.py |
离线批量分类,输出 reviews_labeled.csv |
reviews.csv |
6 条示例评价(自己写的) |
test_classify.py |
3 项测试 |
需要准备哪些环境?
| 项目 | 要求 |
|---|---|
| Python | 3.10 以上(box 上是 3.13) |
| 依赖 | pip install torch --index-url https://download.pytorch.org/whl/cpu,然后 pip install transformers huggingface_hub pytest(box 上的版本:huggingface_hub 2.2.0、transformers 5.20.2、torch 2.14.1+cpu) |
| 磁盘 | 约 600 MB |
为什么要固定 revision?
main 分支随时可能被作者更新,今天能跑的代码,明天可能因为权重或分词器变了而结果不同。用 commit sha 下载可以保证每次拿到的都是同一份文件。sha 可以从 https://huggingface.co/api/models/<repo> 返回的 sha 字段读到。
文件结构
56-huggingface-A-src/
├── download_model.sh
├── verify_model.py
├── classify.py
├── reviews.csv
├── test_classify.py
└── models/sentiment/ # 下载后生成,未随源码附带
第 1 步:下载
#!/usr/bin/env bash
# 固定 revision 下载;每个 --include 只跟一个模式(见调试示例)
set -euo pipefail
REPO=lxyuan/distilbert-base-multilingual-cased-sentiments-student
REV=cf991100d706c13c0a080c097134c05b7f436c45
hf download "$REPO" --revision "$REV" \
--include "*.json" --include "model.safetensors" --include "vocab.txt" \
--exclude "onnx/*" --local-dir models/sentiment
第 2 步:校验
"""对照 Hub 上固定 revision 的 LFS sha256,校验本地权重没有损坏或被替换。"""
import hashlib, json, pathlib, sys, urllib.request
REPO = "lxyuan/distilbert-base-multilingual-cased-sentiments-student"
REV = "cf991100d706c13c0a080c097134c05b7f436c45"
LOCAL = pathlib.Path("models/sentiment")
def sha256(p):
h = hashlib.sha256()
with open(p, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def main():
url = f"https://huggingface.co/api/models/{REPO}/revision/{REV}?blobs=true"
meta = json.load(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "binbho-tutorial/1.0"}), timeout=30))
bad = 0
for s in meta["siblings"]:
p = LOCAL / s["rfilename"]
if not p.exists() or "lfs" not in s:
continue # 只校验已下载的 LFS 大文件
ok = sha256(p) == s["lfs"]["sha256"]
bad += not ok
print(("✓" if ok else "✗"), s["rfilename"], s["lfs"]["sha256"][:16])
sys.exit(1 if bad else 0)
if __name__ == "__main__":
main()
第 3 步:离线分类
"""离线批量情感分类:读 reviews.csv,写 reviews_labeled.csv。不联网(HF_HUB_OFFLINE=1)。"""
import csv, os, sys
os.environ.setdefault("HF_HUB_OFFLINE", "1")
from transformers import pipeline
MODEL_DIR = "models/sentiment"
NEUTRAL_MARGIN = 0.15 # 最高分与次高分差距小于它,就标记为需要人工复核
def load(model_dir=MODEL_DIR):
return pipeline("text-classification", model=model_dir, tokenizer=model_dir, top_k=None, device=-1)
def label_rows(clf, texts, batch_size=16):
out = []
for scores in clf(texts, batch_size=batch_size, truncation=True, max_length=256):
s = sorted(scores, key=lambda x: -x["score"])
out.append({"label": s[0]["label"], "score": round(s[0]["score"], 4),
"review": s[0]["score"] - s[1]["score"] < NEUTRAL_MARGIN})
return out
def main(src="reviews.csv", dst="reviews_labeled.csv"):
rows = list(csv.DictReader(open(src, encoding="utf-8")))
res = label_rows(load(), [r["text"] for r in rows])
with open(dst, "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=["id", "text", "label", "score", "needs_review"])
w.writeheader()
for r, x in zip(rows, res):
w.writerow({"id": r["id"], "text": r["text"], "label": x["label"], "score": x["score"], "needs_review": x["review"]})
print(f"{r['id']:>3} {x['label']:<8} {x['score']:.3f} {'复核' if x['review'] else ' '} {r['text']}")
if __name__ == "__main__":
main(*sys.argv[1:])
id,text
1,物流很快,包装完好,东西和描述一致,很满意。
2,用了三天就坏了,客服也不回消息,太失望了。
3,还行吧,价格一般,质量一般。
4,This charger works great with my laptop.
5,颜色比图片暗一点,不过可以接受。
6,第二次买了,一如既往的好。
import os, pathlib, pytest
import classify
pytestmark = pytest.mark.skipif(not pathlib.Path("models/sentiment/model.safetensors").exists(), reason="先运行 download_model.sh")
@pytest.fixture(scope="module")
def clf():
return classify.load()
def test_clear_cases(clf):
pos, neg = classify.label_rows(clf, ["第二次买了,一如既往的好。", "用了三天就坏了,客服也不回消息,太失望了。"])
assert pos["label"] == "positive" and neg["label"] == "negative"
def test_output_shape(clf):
(r,) = classify.label_rows(clf, ["还行吧"])
assert r["label"] in {"positive", "neutral", "negative"} and 0 <= r["score"] <= 1 and isinstance(r["review"], bool)
def test_offline_flag_set():
assert os.environ["HF_HUB_OFFLINE"] == "1"
第 4 步:运行
$ ./download_model.sh
path=/tmp/p/56A/models/sentiment
$ find models -type f -not -path '*/.cache/*'
models/sentiment/config.json
models/sentiment/model.safetensors
models/sentiment/special_tokens_map.json
models/sentiment/tokenizer.json
models/sentiment/tokenizer_config.json
models/sentiment/vocab.txt
$ python verify_model.py
✓ model.safetensors 0ab095b690339440
$ python classify.py
1 positive 0.996 物流很快,包装完好,东西和描述一致,很满意。
2 negative 0.883 用了三天就坏了,客服也不回消息,太失望了。
3 neutral 0.778 还行吧,价格一般,质量一般。
4 positive 0.966 This charger works great with my laptop.
5 positive 0.474 复核 颜色比图片暗一点,不过可以接受。
6 positive 0.978 第二次买了,一如既往的好。
$ python -m pytest -q
3 passed in 2.87s
检查点:断网后再运行 classify.py,结果应该一样。如果报错提示要联网,说明代码里还在用仓库名加载模型,而不是本地目录。
调试示例
--include 被忽略,配置文件一个都没下载下来。 第一次我把多个模式写在同一个 --include 后面:
$ hf download lxyuan/distilbert-base-multilingual-cased-sentiments-student --revision cf99110... --include "*.json" "model.safetensors" "vocab.txt" --local-dir models/sentiment
UserWarning: Ignoring `--include` since filenames have been explicitly set.
$ ls models/sentiment
model.safetensors vocab.txt
后面两个参数被当成了“要下载的具体文件名”。一旦显式指定了文件名,--include 就整个被忽略了,所以 config.json 和分词器文件都没下载。改成每个模式单独写一个 --include 之后,6 个文件全部下载到位。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
提示 You are sending unauthenticated requests |
没有设置 Token | 公开模型可以忽略;下载量大时用 hf auth login 登录,提高速率限制 |
| 401/403,或提示要同意条款 | gated 模型 | 先在网页上同意条款,再用自己的 Token 下载 |
| 同一个模型下载了好几份格式 | 没有筛选文件 | 用 --include 和 --exclude 筛选 |
| 离线时报错 | 用了仓库名加载,或者缺少文件 | 用本地目录加载,并设置 HF_HUB_OFFLINE=1 |
| 校验不一致 | 下载中断,或者文件被替换 | 删掉文件重新下载,不要使用来源不明的权重 |
模板规则
- 下载时用 commit sha 固定版本,用
--include只下载需要的文件,优先选 safetensors 格式。 - 下载完先校验 sha256 再加载。
- 线上推理离线运行,不在启动时临时从 Hub 拉文件。
项目资源
固定版本、校验文件、离线运行,模型的行为就可以复现,也能经得起审计。