TextGrocery-Shop 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- textgrocery_shop-0.2.0.dist-info/METADATA +733 -0
- textgrocery_shop-0.2.0.dist-info/RECORD +11 -0
- textgrocery_shop-0.2.0.dist-info/WHEEL +5 -0
- textgrocery_shop-0.2.0.dist-info/licenses/LICENSE.txt +29 -0
- textgrocery_shop-0.2.0.dist-info/top_level.txt +1 -0
- tgrocery/__init__.py +346 -0
- tgrocery/base.py +281 -0
- tgrocery/classifier.py +225 -0
- tgrocery/converter.py +632 -0
- tgrocery/learner/__init__.py +16 -0
- tgrocery/learner/linear.py +458 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: TextGrocery_Shop
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: TextGrocery_Shop: a simple, cross-platform short-text classification tool (Python 3.10+)
|
|
5
|
+
Author-email: ranvane <ranvane@gmail.com>
|
|
6
|
+
Maintainer-email: ranvane <ranvane@gmail.com>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://github.com/ranvane/TextGrocery_Shop
|
|
9
|
+
Project-URL: Repository, https://github.com/ranvane/TextGrocery_Shop
|
|
10
|
+
Project-URL: PyPI, https://pypi.org/project/textgrocery-shop/
|
|
11
|
+
Keywords: text classification,short text,tf-idf,logistic regression,jieba
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Natural Language :: Chinese (Simplified)
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE.txt
|
|
25
|
+
Requires-Dist: jieba>=0.42.1
|
|
26
|
+
Requires-Dist: numpy>=1.23
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# TextGrocery_Shop
|
|
32
|
+
|
|
33
|
+
一个简单、跨平台的中文短文本分类工具。支持 **Python 3.10+**,安装时**不需要任何 C 编译器**,
|
|
34
|
+
并完整保留旧版 `Grocery` 的调用方式。
|
|
35
|
+
|
|
36
|
+
内置 [结巴分词](https://github.com/fxsjy/jieba),在词特征之外增加字符 n-gram、TF-IDF 加权与
|
|
37
|
+
L2 正则逻辑回归,对新闻标题、商品标题、工单摘要这类超短文本更友好。
|
|
38
|
+
|
|
39
|
+
其他语言:[English](README_EN.md)
|
|
40
|
+
|
|
41
|
+
## 项目来历
|
|
42
|
+
|
|
43
|
+
**原版 TextGrocery**(作者 [2shou](https://github.com/2shou/TextGrocery))诞生于 Python 2 时代,
|
|
44
|
+
是中文短文本分类里常用的轻量基线。它的技术路线是:
|
|
45
|
+
|
|
46
|
+
- 用 jieba 做中文分词;
|
|
47
|
+
- 用词 unigram / bigram 计数构造特征,再做二值化和 L2 归一化;
|
|
48
|
+
- 通过 `ctypes` 调用编译好的 **LibShortText / LibLinear** C++ 动态库完成训练与预测;
|
|
49
|
+
- 模型用 `pickle` 保存。
|
|
50
|
+
|
|
51
|
+
这套方案在当时非常简单直接,但也积累了几个问题:
|
|
52
|
+
|
|
53
|
+
1. **Python 2 已停止维护**,原代码无法在 Python 3.10+ 上运行;
|
|
54
|
+
2. **安装依赖 C 编译器**,Windows 上需要 MSVC、Linux/macOS 上需要 `make + gcc`,
|
|
55
|
+
而且编译出的 `.so` 不能跨平台复用;
|
|
56
|
+
3. **模型不可移植**:`pickle` 与 Python 版本、NumPy 版本绑定,换环境经常直接失败;
|
|
57
|
+
4. **超短文本效果有限**:十几字的标题分词后只剩几个词,未登录词直接丢失,特征向量接近全零;
|
|
58
|
+
5. **只有决策值,没有概率**,也不方便扩展特征。
|
|
59
|
+
|
|
60
|
+
**0.2.x 的目标**是保留“简单好用”的 API,同时把上述问题一次性解决:用纯 Python + NumPy
|
|
61
|
+
重写训练器,去掉编译依赖;加入字符 n-gram 与 TF-IDF;模型改用 JSON + NumPy 数组;
|
|
62
|
+
并补上概率输出、分词模式、用户词典等实用选项。旧版的设计思路仍然值得借鉴,
|
|
63
|
+
感谢原作者与 LibShortText / LibLinear 的工作。
|
|
64
|
+
|
|
65
|
+
## 新版特性
|
|
66
|
+
|
|
67
|
+
- **零编译安装**:纯 Python + NumPy,`pip install` 在 Windows / Linux / macOS 行为一致。
|
|
68
|
+
- **API 不变**:`Grocery(name)` → `train()` → `save()` → `load()` → `predict()` / `test()`。
|
|
69
|
+
- **更适合短文本**:字符 2–3gram + sublinear TF + 平滑 IDF + L2 归一化。
|
|
70
|
+
- **输出概率**:`result.probabilities`,同时保留原始决策值 `result.dec_values`。
|
|
71
|
+
- **模型可搬运**:`metadata.json` + `vocab.json` + `model.npz`,不使用 `pickle`。
|
|
72
|
+
- **分词可配置**:全模式 / 精确模式 / 搜索引擎模式,支持用户词典、停用词、NFKC 归一化。
|
|
73
|
+
- **速度**:10 类新闻标题模型上约 **80–85 µs/条**,训练全程向量化。
|
|
74
|
+
|
|
75
|
+
## 安装
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install textgrocery-shop
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
PyPI 发行名为 `TextGrocery_Shop`(会被规范化为 `textgrocery-shop`);导入名仍是
|
|
82
|
+
`tgrocery`,老代码不需要改。
|
|
83
|
+
|
|
84
|
+
从源码安装:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
依赖:`jieba`、`numpy`;Python ≥ 3.10。
|
|
91
|
+
|
|
92
|
+
## 快速开始
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from tgrocery import Grocery
|
|
96
|
+
|
|
97
|
+
# 创建一个杂货铺,名字同时作为模型目录名
|
|
98
|
+
grocery = Grocery("sample")
|
|
99
|
+
|
|
100
|
+
# 用列表训练:[(标签, 文本), ...]
|
|
101
|
+
train_src = [
|
|
102
|
+
("education", "名师指导托福语法技巧:名词的复数形式"),
|
|
103
|
+
("education", "中国高考成绩海外认可 是狼来了吗"),
|
|
104
|
+
("sports", "图文:法网孟菲尔斯苦战进16强 孟菲尔斯怒吼"),
|
|
105
|
+
("sports", "四川丹棱举行全国长距登山挑战赛 近万人参与"),
|
|
106
|
+
]
|
|
107
|
+
grocery.train(train_src)
|
|
108
|
+
|
|
109
|
+
# 也可以从文件训练:标签<TAB>文本(UTF-8)
|
|
110
|
+
# grocery.train("train_ch.txt")
|
|
111
|
+
|
|
112
|
+
grocery.save()
|
|
113
|
+
|
|
114
|
+
# 加载模型(名字与保存时一致)
|
|
115
|
+
new_grocery = Grocery("sample")
|
|
116
|
+
new_grocery.load()
|
|
117
|
+
|
|
118
|
+
# 预测
|
|
119
|
+
result = new_grocery.predict("考生必读:新托福写作考试评分标准")
|
|
120
|
+
print(result) # education(与旧版一样直接打印标签)
|
|
121
|
+
print(result.dec_values) # {'education': 2.13, 'sports': -2.05, ...}
|
|
122
|
+
print(result.probabilities) # {'education': 0.91, 'sports': 0.06, ...}
|
|
123
|
+
|
|
124
|
+
# 批量测试
|
|
125
|
+
test_src = [
|
|
126
|
+
("education", "福建春季公务员考试报名18日截止 2月6日考试"),
|
|
127
|
+
("sports", "意甲首轮补赛交战记录:米兰客场8战不败国米10年连胜"),
|
|
128
|
+
]
|
|
129
|
+
test_result = new_grocery.test(test_src)
|
|
130
|
+
print(test_result.accuracy_overall)
|
|
131
|
+
test_result.show_result() # 逐标签精确率 / 召回率表格
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
更多示例见 [sample/](sample/)。
|
|
135
|
+
|
|
136
|
+
## 为什么对短文本更有效
|
|
137
|
+
|
|
138
|
+
原版的特征是“词计数 → 二值化 → L2”。标题再短一些时,分词后往往只剩 3–5 个词,
|
|
139
|
+
大量词不在词表里,最终输入向量接近全零。新版从三个层面缓解:
|
|
140
|
+
|
|
141
|
+
### 1. 字符 n-gram 兜底
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
"中国高考" → c:中国, c:国高, c:高考, c:中国高, c:国高考
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
即使“高考生”“考研”是未登录词,只要共享“高考”两个字符,就能命中 `c:高考`。
|
|
148
|
+
词特征和字符特征分别加 `w:` / `c:` 前缀,互不覆盖。
|
|
149
|
+
|
|
150
|
+
### 2. TF-IDF 加权
|
|
151
|
+
|
|
152
|
+
- **sublinear TF**:`1 + log(tf)`,避免一个词重复出现就主导整条文本;
|
|
153
|
+
- **平滑 IDF**:`log((1 + N) / (1 + df)) + 1`,降低“的、是”等高频词权重,且不会出现 0/负值;
|
|
154
|
+
- **L2 行归一化**:让长短文本处于同一尺度。
|
|
155
|
+
|
|
156
|
+
### 3. 分类器与噪声控制
|
|
157
|
+
|
|
158
|
+
- OvR 逻辑回归 + L2 正则,用 **L-BFGS**(Armijo 回溯线搜索)优化;
|
|
159
|
+
- `min_df` 自动策略:语料 ≥ 1000 篇时过滤只出现一次的特征,小语料则全部保留;
|
|
160
|
+
- `class_weight="balanced"` 处理类别不平衡;
|
|
161
|
+
- 稀疏矩阵乘法用 `numpy.bincount`,计算仍发生在 C 层。
|
|
162
|
+
|
|
163
|
+
目标函数:
|
|
164
|
+
|
|
165
|
+
$$
|
|
166
|
+
\min_{w_k,b_k}\;\frac{1}{2}\lVert w_k\rVert^2
|
|
167
|
+
+ C\sum_{i=1}^{n}s_i\log\!\left(1+\exp\!\left(-y_{ik}(w_k^\top x_i+b_k)\right)\right)
|
|
168
|
+
$$
|
|
169
|
+
|
|
170
|
+
预测时取 `argmax_k (w_k^T x + b_k)`,再经 softmax 得到概率。
|
|
171
|
+
|
|
172
|
+
## 分词与文本预处理
|
|
173
|
+
|
|
174
|
+
默认分词器仍是 jieba 全模式(`cut_all=True`),这是原版行为,也是当前数据上效果最好的模式。
|
|
175
|
+
新增选项都通过**仅关键字参数**提供,不传时行为不变。
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
grocery = Grocery(
|
|
179
|
+
"sample",
|
|
180
|
+
tokenizer="full", # full | precise | search
|
|
181
|
+
normalize="none", # none | nfkc
|
|
182
|
+
char_ngram_mode="global", # global | word_boundary
|
|
183
|
+
mask_patterns=False, # URL / 邮箱 / 数字占位符
|
|
184
|
+
stopwords=["的", "了"], # 也可传文件路径
|
|
185
|
+
user_dict="domain_words.txt", # 每行一个领域词
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 分词模式
|
|
190
|
+
|
|
191
|
+
| 模式 | 说明 | 实测准确率 |
|
|
192
|
+
|---|---|---:|
|
|
193
|
+
| `full`(默认) | 全模式,输出所有可能成词的片段,召回高 | **90.22%** |
|
|
194
|
+
| `precise` | 精确模式,只输出一条最可能的切分 | 89.28% |
|
|
195
|
+
| `search` | 搜索引擎模式,精确模式 + 长词子词 | 89.40% |
|
|
196
|
+
|
|
197
|
+
同一份 THUCNews 30k/5k 数据、相同特征与超参数下,全模式仍最好。精确模式特征更少
|
|
198
|
+
(149k vs 164k)、训练略快,适合对内存/延迟更敏感的场景;领域不同可以自行 A/B。
|
|
199
|
+
|
|
200
|
+
### 用户词典
|
|
201
|
+
|
|
202
|
+
领域实体、产品名、缩写在通用词典里经常被切碎。把词表文件传给 `user_dict`,
|
|
203
|
+
初始化时会注册进 jieba,并**随模型一起写入 metadata**,换机器加载也能复现:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
grocery = Grocery("sample", user_dict="domain_words.txt")
|
|
207
|
+
# 或直接传列表
|
|
208
|
+
grocery = Grocery("sample", user_dict=["烯晶算力网络", "星云一体机"])
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
当前 `user_dict` 只接受 **UTF-8 文本、每行一个词**;词内可以包含空格,但不能带频次、
|
|
212
|
+
拼音或词性列。搜狗导出的 `.scel` 是二进制细胞词库,**不能直接传给 `user_dict`**,
|
|
213
|
+
需要先转换成纯文本。下面是一套完整的转换与清洗流程。
|
|
214
|
+
|
|
215
|
+
#### 1. 把 `.scel` 转成文本
|
|
216
|
+
|
|
217
|
+
使用[深蓝词库转换](https://github.com/studyzy/imewlconverter)(ImeWlConverter)、`scel2txt` 等工具,把 `.scel` 导出为 `.txt`。
|
|
218
|
+
不同工具的输出列不完全一样,常见形式有:
|
|
219
|
+
|
|
220
|
+
```text
|
|
221
|
+
烯晶算力网络
|
|
222
|
+
烯晶算力网络 10
|
|
223
|
+
烯晶算力网络 xijing suanli wangluo 10
|
|
224
|
+
烯晶算力网络 10 n
|
|
225
|
+
烯晶算力网络,10,名
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
前四种分别对应“纯词”“词 + 频次”“词 + 拼音 + 频次”“jieba 风格词 + 频次 + 词性”。
|
|
229
|
+
我们的加载器只认第一种,其他格式需要清洗。**建议在转换工具里把输出列配置为
|
|
230
|
+
TAB 分隔**(`词<TAB>频次` 或 `词<TAB>拼音<TAB>频次`);如果工具只能输出空格分隔的
|
|
231
|
+
`词 拼音 频次`,请先改成 TAB/CSV 再清洗,否则拼音会被当成词的一部分。
|
|
232
|
+
|
|
233
|
+
#### 2. 清洗成“每行一个词”
|
|
234
|
+
|
|
235
|
+
把下面的脚本保存为 `sogou_to_userdict.py`,按实际情况修改输入输出路径后运行:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from pathlib import Path
|
|
239
|
+
import re
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def sogou_to_userdict(src: str, dst: str) -> None:
|
|
243
|
+
"""把搜狗转换后的词表清洗成 TextGrocery_Shop 的 user_dict 格式。
|
|
244
|
+
|
|
245
|
+
规则:
|
|
246
|
+
1. 优先按 TAB / 逗号取第一列(词),丢掉频次、拼音、词性;
|
|
247
|
+
2. 没有分隔符时,去掉行尾的“频次 [词性]”,兼容 "词 10 n";
|
|
248
|
+
3. 跳过空行与 # 注释,按原顺序去重;
|
|
249
|
+
4. 统一输出 UTF-8(无 BOM),每行一个词。
|
|
250
|
+
"""
|
|
251
|
+
words: list[str] = []
|
|
252
|
+
seen: set[str] = set()
|
|
253
|
+
|
|
254
|
+
for raw in Path(src).read_text(encoding="utf-8-sig").splitlines():
|
|
255
|
+
line = raw.strip()
|
|
256
|
+
if not line or line.startswith("#"):
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# TSV / CSV:第一列是词,其余列是频次、拼音或词性
|
|
260
|
+
first = re.split(r"[\t,]", line, maxsplit=1)[0].strip()
|
|
261
|
+
|
|
262
|
+
# 空格分隔:只处理 "词 频次 [词性]";
|
|
263
|
+
# 若是 "词 拼音 频次",请在转换工具中改用 TAB/CSV 输出。
|
|
264
|
+
# 注意:没有 TAB/逗号时,"iPhone 15" 这类词尾数字会被误删;
|
|
265
|
+
# 如果词表含此类产品名,请优先使用 TAB 分隔的导出格式。
|
|
266
|
+
word = re.sub(r"\s+\d+(?:\s+[a-z]+)?$", "", first).strip()
|
|
267
|
+
|
|
268
|
+
if not word or word in seen:
|
|
269
|
+
continue
|
|
270
|
+
seen.add(word)
|
|
271
|
+
words.append(word)
|
|
272
|
+
|
|
273
|
+
Path(dst).write_text("\n".join(words) + "\n", encoding="utf-8")
|
|
274
|
+
print(f"写入 {len(words)} 个词 -> {dst}")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
sogou_to_userdict("sogou_export.txt", "domain_words.txt")
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
运行后检查一下结果:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
python -c "from pathlib import Path; w=Path('domain_words.txt').read_text(encoding='utf-8').splitlines(); print(len(w)); print(w[:20])"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### 3. 交给 `user_dict`
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from tgrocery import Grocery
|
|
291
|
+
|
|
292
|
+
grocery = Grocery(
|
|
293
|
+
"sample",
|
|
294
|
+
tokenizer="precise", # 领域词较多时,精确模式通常更稳
|
|
295
|
+
user_dict="domain_words.txt",
|
|
296
|
+
)
|
|
297
|
+
print(len(grocery.feature_config.user_words)) # 已加载的词数
|
|
298
|
+
|
|
299
|
+
grocery.train(train_src)
|
|
300
|
+
|
|
301
|
+
# 验证领域词确实进入了词表(词特征带 w: 前缀)
|
|
302
|
+
print("w:烯晶算力网络" in grocery.model.text_converter.feature2idx)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
保存后,词表会写入 `metadata.json` 的 `features.user_words`:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"features": {
|
|
310
|
+
"tokenizer": "precise",
|
|
311
|
+
"user_words": ["烯晶算力网络", "星云一体机"]
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
之后 `Grocery("sample").load()` 会从 metadata 重新注册这些词,**换机器加载模型时
|
|
317
|
+
不需要再带原始词典文件**。
|
|
318
|
+
|
|
319
|
+
#### 4. 注意事项
|
|
320
|
+
|
|
321
|
+
- **不要直接导入全量搜狗词库**。通用词对分类帮助很小,却会让 `metadata.json`
|
|
322
|
+
膨胀、`load()` 变慢。建议只保留领域实体、产品名、型号、缩写、人名地名,
|
|
323
|
+
规模控制在几千到一两万词。
|
|
324
|
+
- 搜狗词库通常带频次。如果转换工具保留了频次,可以先按频次阈值过滤,再做上面的清洗。
|
|
325
|
+
- 开启 `lowercase=True`(默认)时,英文词会被转成小写后写入 metadata;
|
|
326
|
+
如果产品名大小写有意义,请设置 `lowercase=False`。
|
|
327
|
+
- 同一进程内 jieba 的词典是全局状态:多个 `Grocery` 使用不同 `user_dict` 时会互相影响。
|
|
328
|
+
需要严格隔离时,把不同模型放到不同进程中使用。
|
|
329
|
+
- 不同 jieba 版本的基础词典可能略有差异;对分词结果要求完全可复现时,请固定 jieba 版本。
|
|
330
|
+
- 搜狗词库有各自的许可条款;把词表写入模型 metadata 意味着它会随模型一起分发,
|
|
331
|
+
请确认授权范围。
|
|
332
|
+
- `user_dict` 只影响 jieba 分词;使用 `custom_tokenize` 时不会生效。
|
|
333
|
+
|
|
334
|
+
### 停用词
|
|
335
|
+
|
|
336
|
+
`stopwords` 支持文件路径或可迭代对象,只作用于词特征与 `word_boundary` 字符特征。
|
|
337
|
+
`global` 字符特征不会删除停用词,否则会破坏相邻字符 n-gram。
|
|
338
|
+
|
|
339
|
+
### 归一化与占位符
|
|
340
|
+
|
|
341
|
+
- `normalize="nfkc"`:全角字母/数字/标点转半角,统一 `ABC` 与 `ABC`;
|
|
342
|
+
- `mask_patterns=True`:把 URL、邮箱、数字替换为 `__url__` / `__email__` / `__num__`,
|
|
343
|
+
适合链接、价格、日期很多 UGC 文本,避免特征爆炸。
|
|
344
|
+
|
|
345
|
+
### 字符 n-gram 模式
|
|
346
|
+
|
|
347
|
+
- `global`(默认):去掉空白后整串滑窗,中文标题效果最好;
|
|
348
|
+
- `word_boundary`:只在词内部滑窗并补边界空格(类似 sklearn 的 `char_wb`),
|
|
349
|
+
避免英文/中英混排生成跨词噪声。分词更重时开销会上升,建议搭配 `tokenizer="precise"`。
|
|
350
|
+
|
|
351
|
+
### 效率
|
|
352
|
+
|
|
353
|
+
默认路径(全模式 + 全局字符 n-gram + 不做 NFKC/占位符替换)与之前完全一致:
|
|
354
|
+
30k 训练约 **26.7 s**,5k 预测约 **0.42 s**。新增的归一化、停用词、用户词典
|
|
355
|
+
只在开启时产生额外开销;`word_boundary` 是唯一可能明显变慢的选项。
|
|
356
|
+
|
|
357
|
+
## 实测效果
|
|
358
|
+
|
|
359
|
+
数据:THUCNews 10 类新闻标题(训练 30,000 / 测试 5,000,标题中位数 19 字)。
|
|
360
|
+
三组配置使用相同的分类器与超参数(`C=8`、`max_iter=50`、`min_df=2`),只改变特征:
|
|
361
|
+
|
|
362
|
+
| 配置 | 完整标题 | 截断到 10 字 | 训练耗时 |
|
|
363
|
+
|---|---:|---:|---:|
|
|
364
|
+
| `legacy`(词 1–2gram、二值、无 IDF) | 88.82% | 82.86% | 11.0 s |
|
|
365
|
+
| `word_tfidf`(词 1–2gram + TF-IDF) | 89.20% | 83.74% | 10.8 s |
|
|
366
|
+
| **`word_char`(词 + 字符 2–3gram + TF-IDF)** | **90.22%** | **84.66%** | 26.7 s |
|
|
367
|
+
|
|
368
|
+
预测:5,000 条约 0.42 秒(约 84 微秒/条)。
|
|
369
|
+
|
|
370
|
+
> 该数据与旧版 README 中的 48k/16k、32 类基准不是同一份数据,绝对数值不可直接比较。
|
|
371
|
+
> 请运行 `benchmark.py` 在你自己的数据上验证。
|
|
372
|
+
|
|
373
|
+
## 基准测试
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
# 对比 legacy / word_tfidf / word_char
|
|
377
|
+
python benchmark.py --train cnews.train.txt --test cnews.test.txt
|
|
378
|
+
|
|
379
|
+
# 模拟超短文本(每条截断到前 10 个字符)
|
|
380
|
+
python benchmark.py --train cnews.train.txt --test cnews.test.txt --truncate 10
|
|
381
|
+
|
|
382
|
+
# 限制样本量,快速试验
|
|
383
|
+
python benchmark.py --train cnews.train.txt --test cnews.test.txt \
|
|
384
|
+
--train-size 30000 --test-size 5000
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
数据格式默认是 `文本<TAB>标签`;若是 `标签<TAB>文本`,加 `--label-column first`。
|
|
388
|
+
|
|
389
|
+
## 参数说明
|
|
390
|
+
|
|
391
|
+
所有新增参数都是**仅关键字参数**,旧代码无需修改。
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
Grocery(
|
|
395
|
+
"name",
|
|
396
|
+
custom_tokenize=None, # 自定义分词函数,优先级高于 tokenizer
|
|
397
|
+
C=8.0, # L2 正则强度的倒数
|
|
398
|
+
class_weight=None, # None | "balanced" | {"标签": 权重}
|
|
399
|
+
word_ngrams=(1, 2), # 词级 n-gram
|
|
400
|
+
char_ngrams=(2, 3), # 字符级 n-gram;() 表示关闭
|
|
401
|
+
lowercase=True,
|
|
402
|
+
min_df=None, # None = 自动(<1000 篇取 1,否则取 2)
|
|
403
|
+
max_features=None,
|
|
404
|
+
binary=False, # True 时退化为旧版二值特征
|
|
405
|
+
sublinear_tf=True,
|
|
406
|
+
use_idf=True,
|
|
407
|
+
smooth_idf=True,
|
|
408
|
+
norm="l2",
|
|
409
|
+
tokenizer="full", # full | precise | search
|
|
410
|
+
normalize="none", # none | nfkc
|
|
411
|
+
char_ngram_mode="global", # global | word_boundary
|
|
412
|
+
mask_patterns=False,
|
|
413
|
+
stopwords=None,
|
|
414
|
+
user_dict=None,
|
|
415
|
+
max_iter=50,
|
|
416
|
+
tol=1e-4,
|
|
417
|
+
verbose=False,
|
|
418
|
+
)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
调参建议:
|
|
422
|
+
|
|
423
|
+
- 数据量大、内存吃紧:`max_features=300000` 或提高 `min_df`;
|
|
424
|
+
- 类别不平衡:`class_weight="balanced"`;
|
|
425
|
+
- 追求速度:`char_ngrams=()`,只用词特征 + TF-IDF;
|
|
426
|
+
- 英文/中英混排较多:`tokenizer="precise", char_ngram_mode="word_boundary"`;
|
|
427
|
+
- 全角字符或 UGC 文本:`normalize="nfkc"`,必要时加 `mask_patterns=True`;
|
|
428
|
+
- 完全复现旧版特征:`word_ngrams=(1, 2), char_ngrams=(), binary=True,
|
|
429
|
+
sublinear_tf=False, use_idf=False, normalize="none"`。
|
|
430
|
+
|
|
431
|
+
## 模型目录格式
|
|
432
|
+
|
|
433
|
+
`save()` 生成一个自包含、可移植的目录:
|
|
434
|
+
|
|
435
|
+
```text
|
|
436
|
+
name/
|
|
437
|
+
id # 模型哈希
|
|
438
|
+
metadata.json # 标签、特征配置(含分词/停用词/用户词典)、分类器参数
|
|
439
|
+
vocab.json # 特征词表(UTF-8)
|
|
440
|
+
vectorizer.npz # IDF 向量
|
|
441
|
+
model.npz # 权重矩阵 + 偏置
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
不使用 `pickle`,模型可在不同 Python 版本、不同操作系统之间搬运;
|
|
445
|
+
出问题时也可以直接打开 `metadata.json` / `vocab.json` 检查。
|
|
446
|
+
|
|
447
|
+
## 兼容性与迁移
|
|
448
|
+
|
|
449
|
+
- **调用方式不变**:`Grocery(name)`、`train()`、`save()`、`load()`、`predict()`、
|
|
450
|
+
`test()`,以及 `GroceryPredictResult` / `GroceryTestResult` 的字段与旧版一致。
|
|
451
|
+
- `predict()` 返回对象可以 `print` 出标签;新增 `probabilities`。
|
|
452
|
+
- 旧的 C 后端(LibShortText / LibLinear)已移除,当前只保留纯 NumPy 实现。
|
|
453
|
+
- **0.1.x 旧模型不可加载**:旧模型依赖 pickle 与 C 后端,请用新版重新训练一次。
|
|
454
|
+
- 0.2.0 早期模型仍可加载:缺失的新配置会自动回退到旧行为。
|
|
455
|
+
|
|
456
|
+
## 打包与发布
|
|
457
|
+
|
|
458
|
+
本节面向维护者,说明如何构建 wheel/sdist、上传 TestPyPI 和 PyPI。项目是纯 Python 包,
|
|
459
|
+
wheel 为 `py3-none-any`,构建过程不需要 C 编译器。
|
|
460
|
+
|
|
461
|
+
### 1. 发行名
|
|
462
|
+
|
|
463
|
+
发行名是 `TextGrocery_Shop`。PyPI 与 pip 会把它规范化为 `textgrocery-shop`,
|
|
464
|
+
所以安装命令是:
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
pip install textgrocery-shop
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Python 导入名仍然是 `tgrocery`,只有安装命令与原版不同。撰写本文时该名称在
|
|
471
|
+
PyPI 上可用,首次上传前请再确认 <https://pypi.org/project/textgrocery-shop/>。
|
|
472
|
+
如果已被占用,修改 `pyproject.toml` 的 `[project].name`,并同步更新两份 README
|
|
473
|
+
里所有 `pip install` 命令。
|
|
474
|
+
|
|
475
|
+
### 2. 同步版本号
|
|
476
|
+
|
|
477
|
+
版本号出现在两个地方,发布前必须保持一致:
|
|
478
|
+
|
|
479
|
+
- `pyproject.toml` 的 `[project].version`
|
|
480
|
+
- `tgrocery/__init__.py` 的 `__version__`
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
# 查看 pyproject.toml 中的版本号(兼容 Python 3.10)
|
|
484
|
+
python -c "import pathlib; print([line for line in pathlib.Path('pyproject.toml').read_text('utf-8').splitlines() if line.startswith('version =')][0])"
|
|
485
|
+
python -c "import tgrocery; print(tgrocery.__version__)"
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
PyPI 上的版本号不可覆盖,发布后也不能删除重传同一版本;每次发布都要提升版本号。
|
|
489
|
+
|
|
490
|
+
### 3. 安装构建工具并清理旧产物
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
# 安装构建工具
|
|
494
|
+
python -m pip install --upgrade build twine
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
# Linux / macOS / Git Bash
|
|
499
|
+
rm -rf build dist *.egg-info
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
```powershell
|
|
503
|
+
# PowerShell
|
|
504
|
+
Remove-Item -Recurse -Force build, dist -ErrorAction SilentlyContinue
|
|
505
|
+
Get-ChildItem -Filter *.egg-info -Recurse | Remove-Item -Recurse -Force
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### 4. 运行质量检查
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
ruff check .
|
|
512
|
+
ruff format --check .
|
|
513
|
+
pytest
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
三项都通过再打包,避免把坏版本发到 PyPI。
|
|
517
|
+
|
|
518
|
+
### 5. 构建 wheel 与 sdist
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
python -m build
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
产物在 `dist/`:
|
|
525
|
+
|
|
526
|
+
```text
|
|
527
|
+
dist/
|
|
528
|
+
textgrocery_shop-0.2.0.tar.gz # 源码包
|
|
529
|
+
textgrocery_shop-0.2.0-py3-none-any.whl # 纯 Python wheel
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
只构建 wheel 用 `python -m build --wheel`;只构建源码包用
|
|
533
|
+
`python -m build --sdist`。`python -m build` 会创建隔离的构建环境,
|
|
534
|
+
需要联网安装 `setuptools`。
|
|
535
|
+
|
|
536
|
+
### 6. 检查产物
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
python -m zipfile -l dist/textgrocery_shop-0.2.0-py3-none-any.whl
|
|
540
|
+
python -m twine check dist/*
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
PowerShell 不会像 bash 那样展开 `dist/*`,可以改用:
|
|
544
|
+
|
|
545
|
+
```powershell
|
|
546
|
+
python -m twine check (Get-ChildItem dist | ForEach-Object FullName)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
wheel 里应只包含运行时代码和元数据,例如:
|
|
550
|
+
|
|
551
|
+
```text
|
|
552
|
+
tgrocery/*.py
|
|
553
|
+
tgrocery/learner/*.py
|
|
554
|
+
textgrocery_shop-0.2.0.dist-info/METADATA
|
|
555
|
+
textgrocery_shop-0.2.0.dist-info/licenses/LICENSE.txt
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
不应出现 `tests/`、`sample/`、`__pycache__/`、`build/` 或模型目录。源码包由
|
|
559
|
+
`MANIFEST.in` 控制,会额外包含 README、测试和示例;两个产物都应由
|
|
560
|
+
`twine check` 输出 `PASSED`。
|
|
561
|
+
|
|
562
|
+
### 7. 本地安装 wheel 验证
|
|
563
|
+
|
|
564
|
+
在干净环境中安装刚构建的 wheel:
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
python -m pip install --force-reinstall --no-deps dist/textgrocery_shop-0.2.0-py3-none-any.whl
|
|
568
|
+
python -c "import tgrocery; print(tgrocery.__version__)"
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
再跑一个最小训练/预测:
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
python -c "from tgrocery import Grocery; g=Grocery('smoke'); g.train([('a','上涨 突破 新高'),('b','下跌 亏损 风险')]); print(g.predict('上涨 突破'))"
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
该命令会在当前目录生成 `smoke/` 模型目录,验证后可以删除。如果当前目录是源码目录,
|
|
578
|
+
Python 可能优先导入本地 `tgrocery/`;请换到其他目录执行,或确认 `pip show textgrocery-shop`
|
|
579
|
+
的 `Location` 指向安装目录。验证完后如继续开发,再执行 `pip install -e .[test]`。
|
|
580
|
+
|
|
581
|
+
### 8.(可选)上传到 TestPyPI 彩排
|
|
582
|
+
|
|
583
|
+
正式发布推荐用第 10 节的 Trusted Publishing;这一节只在需要本地彩排时使用。
|
|
584
|
+
|
|
585
|
+
先在 [test.pypi.org](https://test.pypi.org/) 注册账号并创建 API token。
|
|
586
|
+
建议用环境变量传递 token,不要把 token 写进仓库:
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
export TWINE_USERNAME=__token__
|
|
590
|
+
export TWINE_PASSWORD=pypi-你的TestPyPI令牌
|
|
591
|
+
python -m twine upload --repository testpypi dist/*
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
PowerShell:
|
|
595
|
+
|
|
596
|
+
```powershell
|
|
597
|
+
$env:TWINE_USERNAME="__token__"
|
|
598
|
+
$env:TWINE_PASSWORD="pypi-你的TestPyPI令牌"
|
|
599
|
+
python -m twine upload --repository testpypi (Get-ChildItem dist | ForEach-Object FullName)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
从 TestPyPI 安装验证:
|
|
603
|
+
|
|
604
|
+
```bash
|
|
605
|
+
pip install --index-url https://test.pypi.org/simple/ \
|
|
606
|
+
--extra-index-url https://pypi.org/simple \
|
|
607
|
+
textgrocery-shop==0.2.0
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`--extra-index-url` 用于从正式 PyPI 获取 `jieba`、`numpy` 等依赖,因为 TestPyPI
|
|
611
|
+
不保证同步所有依赖。TestPyPI 适合彩排,确认无误后再发正式 PyPI。
|
|
612
|
+
|
|
613
|
+
### 9.(备用)手动上传到 PyPI
|
|
614
|
+
|
|
615
|
+
推荐用第 10 节的 GitHub Actions + Trusted Publishing。只有需要绕过 CI 或排查上传
|
|
616
|
+
问题时,才使用下面的本地 `twine` 流程。
|
|
617
|
+
|
|
618
|
+
在 [pypi.org](https://pypi.org/) 创建 API token;首次发布通常需要账号级 token,
|
|
619
|
+
项目创建后可改用仅限该项目的 token。账号需开启 2FA。
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
export TWINE_USERNAME=__token__
|
|
623
|
+
export TWINE_PASSWORD=pypi-你的PyPI令牌
|
|
624
|
+
python -m twine upload dist/*
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
PowerShell:
|
|
628
|
+
|
|
629
|
+
```powershell
|
|
630
|
+
$env:TWINE_USERNAME="__token__"
|
|
631
|
+
$env:TWINE_PASSWORD="pypi-你的PyPI令牌"
|
|
632
|
+
python -m twine upload (Get-ChildItem dist | ForEach-Object FullName)
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
说明:
|
|
636
|
+
|
|
637
|
+
- `twine upload` 默认上传到正式 PyPI;显式指定可用 `--repository pypi`。
|
|
638
|
+
- 上传过程中断时,**不要修改代码后覆盖同一版本**。先确认哪些文件已上传,
|
|
639
|
+
再决定重试同一批文件还是提升版本号。
|
|
640
|
+
- 版本号或文件名一旦被 PyPI 接收,即使删除也不能复用;元数据写错也要发新版本。
|
|
641
|
+
- 不要把 `dist/`、`.pypirc` 或 token 提交到 Git;`.pypirc` 已在 `.gitignore` 中。
|
|
642
|
+
|
|
643
|
+
发布后验证:
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
pip index versions textgrocery-shop
|
|
647
|
+
pip install --no-cache-dir textgrocery-shop==0.2.0
|
|
648
|
+
python -c "import tgrocery; print(tgrocery.__version__)"
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### 10. 用 GitHub Actions 自动发布(推荐)
|
|
652
|
+
|
|
653
|
+
仓库已包含 `.github/workflows/pypi_publish.yml`。在 GitHub **发布 Release** 时,
|
|
654
|
+
工作流会自动构建 wheel/sdist 并上传 PyPI。它使用 PyPI Trusted Publishing
|
|
655
|
+
(OIDC 短期凭证),仓库中不需要保存 `TWINE_PASSWORD` 或 API token。
|
|
656
|
+
|
|
657
|
+
工作流关键点:
|
|
658
|
+
|
|
659
|
+
- 触发条件:`release: types: [published]`
|
|
660
|
+
- 构建 job `release-build`:`python -m build`,artifact 名 `release-dists`
|
|
661
|
+
- 发布 job `pypi-publish`:`environment: pypi` + `permissions: id-token: write`
|
|
662
|
+
- 上传动作:`pypa/gh-action-pypi-publish@release/v1`
|
|
663
|
+
|
|
664
|
+
一次性配置:
|
|
665
|
+
|
|
666
|
+
1. 在 PyPI 的 **Publishing** 页面添加 GitHub publisher:
|
|
667
|
+
owner 填 `ranvane`、repository 填 `TextGrocery_Shop`、workflow 填
|
|
668
|
+
`pypi_publish.yml`、environment 填 `pypi`。项目尚不存在时使用
|
|
669
|
+
**pending publisher**。
|
|
670
|
+
2. 在 GitHub 仓库 **Settings → Environments** 创建 `pypi` 环境,建议添加
|
|
671
|
+
required reviewers。
|
|
672
|
+
3. 确认 `pyproject.toml` 的版本号与 `tgrocery/__init__.py` 的 `__version__`
|
|
673
|
+
一致。
|
|
674
|
+
|
|
675
|
+
发布流程:
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
# 1. 确保代码已提交、CI 通过,然后打 tag
|
|
679
|
+
git tag v0.2.0
|
|
680
|
+
git push origin v0.2.0
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
2. 打开 GitHub 仓库的 **Releases → Draft a new release**,选择 tag `v0.2.0`,
|
|
684
|
+
标题填 `v0.2.0`,点击 **Publish release**。
|
|
685
|
+
3. 在 **Actions** 页面确认 `Upload Python Package` 工作流执行成功。
|
|
686
|
+
4. 验证正式包:
|
|
687
|
+
|
|
688
|
+
```bash
|
|
689
|
+
pip install --no-cache-dir textgrocery-shop==0.2.0
|
|
690
|
+
python -c "import tgrocery; print(tgrocery.__version__)"
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
`permissions: id-token: write` 必须保留,否则 OIDC 无法换取 PyPI 凭证。
|
|
694
|
+
若要先发 TestPyPI,在 workflow 的上传步骤加
|
|
695
|
+
`with: repository-url: https://test.pypi.org/legacy/`,并在 TestPyPI 的
|
|
696
|
+
Publishing 页面单独配置对应的 trusted publisher。
|
|
697
|
+
|
|
698
|
+
### 11. 发布检查清单
|
|
699
|
+
|
|
700
|
+
- [ ] PyPI 发行名可用,或当前账号拥有该项目
|
|
701
|
+
- [ ] `pyproject.toml` 与 `tgrocery/__init__.py` 的版本号一致
|
|
702
|
+
- [ ] README 中的安装命令与实际发行名一致
|
|
703
|
+
- [ ] `ruff check .`、`ruff format --check .`、`pytest` 全部通过
|
|
704
|
+
- [ ] `python -m build` 生成 wheel 和 sdist
|
|
705
|
+
- [ ] `twine check` 全部 `PASSED`
|
|
706
|
+
- [ ] wheel 内没有测试、示例、缓存和模型目录
|
|
707
|
+
- [ ] 在干净环境安装 wheel 并能训练/预测
|
|
708
|
+
- [ ] (可选)在 TestPyPI 安装验证通过
|
|
709
|
+
- [ ] 打 tag(如 `v0.2.0`)并推送
|
|
710
|
+
- [ ] 创建并发布 GitHub Release,触发 `Upload Python Package`
|
|
711
|
+
- [ ] GitHub Actions 发布 job 成功,正式 PyPI 可用 `pip install` 安装
|
|
712
|
+
- [ ] 未提交 `dist/`、`.pypirc`、token 或本地模型
|
|
713
|
+
|
|
714
|
+
## 开发
|
|
715
|
+
|
|
716
|
+
```bash
|
|
717
|
+
pip install -e .[test]
|
|
718
|
+
pytest
|
|
719
|
+
ruff check .
|
|
720
|
+
ruff format --check .
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## 更新日志
|
|
724
|
+
|
|
725
|
+
- **2026-10-24**:PyPI 发行名改为 `TextGrocery_Shop`
|
|
726
|
+
(`pip install textgrocery-shop`);导入名仍为 `tgrocery`。
|
|
727
|
+
许可证元数据与 `LICENSE.txt` 统一为 BSD-3-Clause。
|
|
728
|
+
- **2026-10-24**:发布流程改为 GitHub Release + Trusted Publishing,工作流
|
|
729
|
+
`.github/workflows/pypi_publish.yml` 自动上传 PyPI。
|
|
730
|
+
|
|
731
|
+
## 许可证
|
|
732
|
+
|
|
733
|
+
BSD,见 [LICENSE.txt](LICENSE.txt)。
|