文本预处理(上)

适用读者:已阅读第一章"NLP概念"的零基础初学者。本章将进入实战阶段,你将动手完成NLP项目中的第一步也是最重要的一步:把原始文本变成计算机能理解的格式。

学习目标

  1. 理解文本预处理的完整流程和每一步的目的
  2. 掌握中文分词的核心概念和jieba分词工具的使用
  3. 理解One-Hot编码及其致命缺陷(理解为什么它被淘汰)
  4. 掌握Word2Vec的核心思想(这是理解所有现代NLP的基础)
  5. 理解FastText的改进思路和nn.Embedding的实际使用
  6. 学会使用TensorBoard可视化词嵌入

预计阅读时间:90-120分钟(包含动手练习和思考题时间)

环境准备:建议打开Jupyter Notebook或Python交互环境,边看边运行代码。


目录

  1. 零、热身:文本预处理的完整图景
  2. 一、为什么文本需要预处理
  3. 二、中文分词——把汉字序列变成词语列表
  4. 三、One-Hot编码——最朴素但最笨拙的文本向量化
  5. 四、Word2Vec——NLP历史上最重要的突破之一
  6. 五、FastText——在Word2Vec基础上的关键改进
  7. 六、nn.Embedding——词嵌入在深度学习中的实际使用
  8. 七、TensorBoard可视化——用眼睛"看"语义空间
  9. 八、补充提升内容
  10. 九、本章总结与自测题

热身:文本预处理的完整图景

一条文本的"进化"之路

在我们正式学习之前,先看一个"鸟瞰图"——一条原始文本要经历怎样的过程,才能最终被神经网络模型使用:

阶段1: 原始文本
  "我爱北京天安门,天安门上太阳升。"
        │
        ▼  【分词】
阶段2: 词语序列
  ["我", "爱", "北京", "天安门", ",", "天安门", "上", "太阳", "升", "。"]
        │
        ▼  【构建词汇表 + 映射ID】
阶段3: 数字序列
  [1, 5, 23, 45, 8, 45, 12, 67, 89, 3]
        │           ↑
        │     注意:两个"天安门"变成了相同的数字45,
        │     因为它们在词汇表中是同一个词。
        ▼
阶段4: 词向量序列(每个词变成一个固定长度的向量)
  [[0.1, 0.3, -0.2, ...], [0.5, -0.1, 0.8, ...], ...]
  每个向量通常是100-300维的实数向量
        │
        ▼  【序列填充/截断到统一长度】
阶段5: 固定长度的向量矩阵
  shape: [batch_size, max_sequence_length, embedding_dim]
  例如: [32, 50, 300] = 32个句子 × 每个句子50个词 × 每个词300维向量
        │
        ▼
阶段6: 输入神经网络模型(如LSTM、Transformer等)

关键洞察:预处理的核心就是完成阶段1到阶段5的转换。每一步都有明确的目的和对应的工具。

三大核心阶段

本章(上)聚焦于以下三大阶段:

阶段 核心问题 对应工具/方法
1. 文本向量化 如何把词变成数字向量? One-Hot → Word2Vec → nn.Embedding
2. 中文分词 如何正确切分中文文本? jieba分词
3. 可视化分析 如何直观观察词向量质量? TensorBoard

自检问题:看着上面的流程图,尝试用自己的话描述"分词"这一步要做什么。


为什么文本需要预处理?

从"非结构化"到"结构化"

核心问题:计算机只能处理数字,而文本是符号(字符、词语)。预处理就是"翻译官",把计算机不懂的文本"翻译"成计算机能计算的数字。

生活类比一:做菜前的备菜

文本预处理就像做菜前的备菜:

  • 原始文本 = 从菜市场买回来的食材(带泥土、没洗、没切)
  • 分词 = 清洗和分类(把蔬菜、肉类、调料分开)
  • 去停用词 = 丢掉不可食用的部分(菜根、烂叶)
  • 向量化 = 切块/切丝(变成统一规格,方便下锅)
  • 填充/截断 = 统一分量(每份菜定量)

如果不做预处理直接把原始文本扔给模型,就像把一棵没洗没切的整棵大白菜扔进锅里——火锅都没法这么吃。

生活类比二:跨国邮寄

你要把一件衣服寄给外国朋友:

  • 衣服本身 = 文本的含义
  • 打包成标准包裹 = 文本预处理
  • 包裹重量尺寸符合快递要求 = 序列填充到统一长度
  • 贴上快递单号 = 分配词ID
  • 如果没有标准包装(预处理),快递公司不收(模型不接受)

预处理对模型性能的影响有多大?

这不是一个可以忽略的边缘步骤。实际研究和工程经验表明:

真实案例

  • 情感分析任务中,仅优化分词策略(从默认jieba改为加入领域词典),F1提升3-5%
  • Twitter情感分析中,正确的文本清洗(处理emoji、@提及、URL)使准确率提升8%
  • 机器翻译中,使用BPE(Byte-Pair Encoding)子词分词替代传统分词,BLEU提升2-4个点

为什么预处理这么重要?

Garbage In, Garbage Out (GIGO) 原则:
┌──────────────┐     ┌─────────┐     ┌──────────────┐
│ 垃圾输入文本  │ ──→ │  模型   │ ──→ │ 垃圾输出结果  │
└──────────────┘     └─────────┘     └──────────────┘

无论模型多么强大,如果输入的文本已经被错误地切分或表示,
输出也不可能是正确的。

文本预处理面临的通用挑战

在开始之前,了解这些挑战有助于理解为什么某些步骤是必要的:

挑战 说明 例子
噪声数据 文本中包含无关信息 HTML标签、特殊符号、乱码
非标准写法 口语化、网络用语 “酱紫”、“yyds”、“2333”
多语言混合 中英文、数字混杂 “iPhone 15 Pro Max值得买吗?”
粒度不一致 不同来源文本的抽象层次不同 专业论文 vs 微博评论
标注质量 训练数据的标签可能不准确 人工标注的主观性差异

自检问题

  1. 用做菜的类比解释:如果不做文本预处理,相当于做菜时少了哪个环节?
  2. GIGO原则告诉我们什么?为什么它对NLP特别重要?

中文分词——把汉字序列变成词语列表

中文分词:一个专属于中文(及类似语言)的难题

为什么英文不需要分词?

英文有一个天然的优势:单词之间有空格

英文:"I love natural language processing"
      → 直接按空格切分 → ["I", "love", "natural", "language", "processing"]

中文:"我爱自然语言处理"
      → 没有空格!→ 需要分词工具 → ["我", "爱", "自然语言", "处理"]

类比:英文像一排已经切好的水果拼盘,每个水果块之间有空隙;中文像一根完整的甘蔗,你需要用刀把它切成一段段的。

中文分词为什么难?

中文分词不仅是因为"没有空格",还因为以下深层原因:

难题一:歧义切分

"乒乓球拍卖完了"

切分方案A:乒乓球 / 拍卖 / 完了  (在拍卖乒乓球)
切分方案B:乒乓球拍 / 卖完了     (球拍被卖完了)
切分方案C:乒乓 / 球拍 / 卖完了   (球拍被卖完了)

三种切分方案语法上都"合理",
但真实含义取决于上下文!

难题二:未登录词

“未登录词"不是"没登录QQ”,而是指不在词典里的词——这些词不能被简单的词典匹配方法识别。

常见未登录词类型:
- 新词/网络用语:"YYDS"、"绝绝子"、"躺平"、"内卷"
- 人名:"张伟强"(普通名字,不可能全部收录在词典里)
- 地名:"望京东路"、"回龙观西大街"
- 专业术语:"注意力机制"、"梯度下降"(在通用词典中不存在)
- 品牌/产品名:"赛博朋克2077"、"原神"

难题三:分词粒度不唯一

什么是一个"词"?不同任务有不同答案。

"自然语言处理"

粒度1(粗):["自然语言处理"]  ← 作为一个整体概念
粒度2(中):["自然语言", "处理"]  ← 拆分为两个部分
粒度3(细):["自然", "语言", "处理"]  ← 拆分到最小单元

哪种粒度是正确的?取决于你的任务:
- 情感分析:选粒度2("自然语言"和"处理"分别有独立含义)
- 搜索引擎:可同时保留多种粒度提高召回
- 关键词提取:选粒度3(每个字/词都可能是关键词)

jieba分词——中文分词的"瑞士军刀"

jieba("结巴"的拼音)是目前最流行的中文分词工具。它的名字很有趣——"结巴"是口吃的俗称,暗示着"把话切开"的意思。

jieba的三种分词模式

jieba提供三种分词模式,适用于不同场景。理解它们的区别非常重要:

模式 API 核心算法 特点 适用场景
精确模式 jieba.lcut(s, cut_all=False) 动态规划,找最优切分路径 最精确,无冗余 文本分析、情感分析
全模式 jieba.lcut(s, cut_all=True) 扫描所有成词的路径 速度快,有冗余 需要高召回的场景
搜索引擎模式 jieba.lcut_for_search(s) 精确模式基础上再切分长词 召回率高 搜索引擎索引构建
代码示例详解
# ============================================================
# 第1步:安装jieba
# ============================================================
# 在命令行执行:pip install jieba
# (如果使用Jupyter Notebook,在cell中执行:!pip install jieba)

import jieba

# ============================================================
# 第2步:定义测试句子
# ============================================================
sentence = "我爱北京天安门"

# ============================================================
# 第3步:精确模式(默认,推荐日常使用)
# ============================================================
# lcut = list + cut,直接返回列表,不返回生成器
# cut_all=False 表示精确模式
result_exact = jieba.lcut(sentence, cut_all=False)
print("精确模式:", result_exact)
# 输出:['我', '爱', '北京', '天安门']
#
# 解析:
# "我" —— 独立的代词
# "爱" —— 独立的动词
# "北京" —— 地名,作为一个完整的词
# "天安门" —— 景点名,作为一个完整的词
# 结果非常干净,没有冗余的切分

# ============================================================
# 第4步:全模式
# ============================================================
# cut_all=True 表示全模式
result_all = jieba.lcut(sentence, cut_all=True)
print("全模式:", result_all)
# 输出:['我', '爱', '北京', '天安', '天安门']
#
# 解析:
# 注意多出了"天安"!在全模式下,jieba会扫描所有可能的成词路径。
# "天安门"可以是"天安"+"门",也可以是"天安门"作为一个整体。
# 全模式把所有可能性都列出来了。
# 速度最快(不需要做复杂的决策),但有冗余。

# ============================================================
# 第5步:搜索引擎模式
# ============================================================
result_search = jieba.lcut_for_search(sentence)
print("搜索引擎模式:", result_search)
# 输出:['我', '爱', '北京', '天安', '天安门']
#
# 解析:
# 搜索引擎模式和全模式的结果在这个例子中相同。
# 它的逻辑是:先用精确模式切分,然后对长词进行二次切分。
# 比如"天安门"是长词,再切出"天安"。
# 这样既能精确匹配"天安门",也能匹配"天安"的搜索需求。

# ============================================================
# 第6步:更复杂的例子——三种模式对比
# ============================================================
complex_sentence = "我爱中华人民共和国"

print("精确模式:", jieba.lcut(complex_sentence, cut_all=False))
# 输出:['我', '爱', '中华人民共和国']

print("全模式:", jieba.lcut(complex_sentence, cut_all=True))
# 输出:['我', '爱', '中华', '中华人民', '中华人民共和国', '华人', '人民', '人民共和国', '共和', '共和国']
# 全模式把"中华人民共和国"中所有可能的子词组合都列出来了

print("搜索引擎模式:", jieba.lcut_for_search(complex_sentence))
# 输出:['我', '爱', '中华', '华人', '人民', '共和', '共和国', '中华人民共和国']
# 搜索引擎模式在精确模式基础上,对长词"中华人民共和国"做了合理分解
三种模式的选择指南
你想做什么?
    │
    ├── 文本分析/情感分析/分类
    │   → 精确模式(给你最准确的结果)
    │
    ├── 需要找出所有可能的词语
    │   → 全模式(给你最全的结果)
    │
    └── 构建搜索引擎/需要对长词进行召回
        → 搜索引擎模式(给你精确+细分的折中结果)

自定义词典——教jieba认识"新词"

jieba自带一个通用词典,但它不可能包含所有专业术语、人名和品牌名。这时你需要"教"jieba认识这些词。

为什么需要自定义词典?
# 没有自定义词典
jieba.lcut("文本预处理使用jieba分词工具")
# 输出:['文本', '预处理', '使用', 'jieba', '分词', '工具']
# "jieba"被正确识别,但假设我们有一个专业术语"文本预处理"
# jieba可能把它拆成"文本"和"预处理"

# 如果有领域专有名词需要保留完整,
# 就需要自定义词典
自定义词典格式

词典文件是一个纯文本文件,每行一个词,格式为:

词语 词频 词性
  • 词语:你要添加的词(必须)
  • 词频:这个词的常见程度,数字越大表示越常见(可选,默认很高)——设置得比默认词典高可以覆盖默认切分
  • 词性:这个词的词性,如n(名词)、v(动词)、nz(其他专名)(可选)
# ============================================================
# 自定义词典示例
# ============================================================

# 第1步:创建词典文件 userdict.txt
# 写入以下内容:
"""
自然语言处理 10 n
深度学习 10 n
注意力机制 10 n
预训练模型 8 n
"""

# 第2步:加载自定义词典
import jieba

# load_userdict() 加载用户自定义词典
# 参数是词典文件的路径(可以是相对路径或绝对路径)
jieba.load_userdict("./userdict.txt")

# 第3步:使用加载了自定义词典的jieba进行分词
sentence = "自然语言处理中的深度学习使用了注意力机制"
result = jieba.lcut(sentence)
print(result)
# 输出:['自然语言处理', '中', '的', '深度学习', '使用', '了', '注意力机制']
# 注意:专业术语都被正确地保留为完整词语了!

# ============================================================
# 动态添加词语(不需要文件)
# ============================================================
# 如果只是临时添加一两个词,可以使用 add_word()

jieba.add_word("文本向量化", freq=10, tag='n')
jieba.add_word("序列填充")
# freq 参数影响切分决策:freq越高,越倾向于将其作为一个整体保留

result = jieba.lcut("文本向量化是序列填充的前提")
print(result)
# 输出:['文本向量化', '是', '序列填充', '的', '前提']

# ============================================================
# 动态删除词语
# ============================================================
# 如果你想强制拆开某个词,可以删除它
jieba.del_word("天安门")
print(jieba.lcut("我爱北京天安门"))
# 输出:['我', '爱', '北京', '天', '安', '门']
# 注意"天安门"被拆成了三个字!
自定义词典的实用建议
  1. 不要贪多:只添加对你的任务真正重要的词。过多的自定义词可能导致意外切分
  2. 注意词频设置:如果发现一个专业术语仍然被错误切分,尝试提高其词频
  3. 迭代优化:先用默认词典分词→检查错误→逐批添加自定义词→重新分词→继续检查

词性标注——给每个词贴上"语法身份证"

词性标注是什么?

词性标注告诉计算机每个词在句子中扮演的语法角色:

"我 爱 自然语言 处理"
  │  │    │      │
  r  v    n     vn
  │  │    │      │
  代 动   名    动名
  词 词   词    词
为什么词性标注重要?

消除歧义的关键

"他在领导这个团队"  → 领导 = 动词(带领)
"他是我们的领导"   → 领导 = 名词(上级)

同一个词,不同词性 → 不同含义 → 不同翻译/NER结果/关系抽取结果
jieba词性标注代码详解
# ============================================================
# 使用jieba进行词性标注
# ============================================================

# 导入词性标注模块(posseg = Part-Of-Speech Segmentation)
import jieba.posseg as pseg

sentence = "我爱自然语言处理"

# pseg.lcut() 返回 (word, flag) 元组的列表
# word = 词语,flag = 词性标签
result = pseg.lcut(sentence)

print("词语\t词性\t含义")
print("-" * 30)
for word, flag in result:
    print(f"{word}\t{flag}\t{get_pos_name(flag)}")

# 输出:
# 词语    词性    含义
# ------------------------------
# 我      r      代词
# 爱      v      动词
# 自然语言  n      名词
# 处理     vn     动名词


# ============================================================
# 常用词性标签速查表(jieba使用北大词性标注集)
# ============================================================
def get_pos_name(tag):
    """将jieba词性标签转为人可读的名称"""
    pos_dict = {
        'n':  '名词 (noun)',
        'nr': '人名 (person name)',
        'ns': '地名 (place name)',
        'nt': '机构名 (organization)',
        'nz': '其他专名',
        'v':  '动词 (verb)',
        'vd': '副动词',
        'vn': '动名词 (verbal noun)',
        'a':  '形容词 (adjective)',
        'ad': '副形词',
        'd':  '副词 (adverb)',
        'r':  '代词 (pronoun)',
        'm':  '数词 (numeral)',
        'q':  '量词 (quantifier)',
        'p':  '介词 (preposition)',
        'c':  '连词 (conjunction)',
        'u':  '助词 (auxiliary)',
        'e':  '叹词 (exclamation)',
        'y':  '语气词 (modal particle)',
        'w':  '标点符号 (punctuation)',
    }
    return pos_dict.get(tag, f'未知({tag})')

# 测试一个更丰富的句子
sentence2 = "2024年小明在北京大学学习人工智能"
result2 = pseg.lcut(sentence2)
print(f"\n分词标注结果({sentence2}):")
for word, flag in result2:
    print(f"  {word:8s}{flag:4s} ({get_pos_name(flag)})")

# 预期输出:
#   2024     → m    (数词)
#   年       → q    (量词)
#   小明     → nr   (人名)
#   在       → p    (介词)
#   北京大学  → nt   (机构名)
#   学习     → v    (动词)
#   人工智能  → n    (名词)
词性标注在NLP流程中的位置
原始文本 → 分词 → 词性标注 → 下游任务
                          │
                          ├→ NER(人名通常是nr, 地名是ns)
                          ├→ 关键词提取(提取名词n和动词v)
                          ├→ 句法分析(基于词性构建语法树)
                          └→ 情感分析(形容词a和副词d提供情感信息)

命名实体识别(NER)概述

NER的概念在第一章已经详细介绍了。在文本预处理阶段,你需要知道的是:

  • NER标注格式:通常使用BIO或BIOES体系
  • 预处理中的NER:在对训练数据进行标注时需要使用BIO格式
  • jieba的NER能力:jieba本身不直接提供NER,但可以通过词性标注辅助(nr=人名,ns=地名,nt=机构名)
# 使用jieba的词性标注做简单的实体识别
import jieba.posseg as pseg

text = "乔布斯是苹果公司的创始人"
words = pseg.lcut(text)

entities = []
for word, flag in words:
    if flag == 'nr':   # 人名
        entities.append((word, 'PERSON'))
    elif flag == 'ns': # 地名
        entities.append((word, 'LOCATION'))
    elif flag == 'nt': # 机构名
        entities.append((word, 'ORGANIZATION'))

print(entities)
# 输出:[('乔布斯', 'PERSON'), ('苹果公司', 'ORGANIZATION')]

# 注意:这是一种近似方法,不够精确。
# 真正的NER需要专门的模型(如BERT+CRF),
# jieba的词性标注只能作为快速原型的方式。

自检问题

  1. 精确模式和全模式的核心区别是什么?各适用于什么场景?
  2. 什么时候需要自定义词典?给出一个具体的场景例子。
  3. 为什么NER在预处理阶段通常只用BIO格式标注,而不是自动识别?

One-Hot编码——最朴素但最笨拙的文本向量化

从"词"到"数字"的第一步尝试

在前面的内容中,我们说"计算机只认识数字"。那最容易想到的办法是什么?

最直白的方法:给每个词分配一个唯一的编号。

词汇表:
{ "我": 1, "爱": 2, "中国": 3, "北京": 4, "上海": 5, "深圳": 6 }

但这有一个严重的问题。让我们用一个类比来理解:

生活类比——学生的座位号

假设一个班级有40个学生,每个学生有一个座位号(1到40)。座位号能区分谁是谁,但它不能告诉你任何关于这个学生的信息——你不知道他的成绩好不好、性格内向还是外向、喜欢什么运动。

同样,给词编号只能区分两个词是否相同,但不能告诉我们"北京"和"上海"都是城市(它们应该"近"一些),而"北京"和"爱"没有关系(它们应该"远"一些)。

One-Hot编码的定义

One-Hot编码将每个词表示为一个长度为词汇表大小的向量,该向量中只有该词对应位置为1,其余位置全部为0。

公式

one_hot ( w i ) = [ 0 , 0 , . . . , 1 ⏟ 第 i 位 , . . . , 0 ] ∈ { 0 , 1 } ∣ V ∣ \text{one\_hot}(w_i) = [0, 0, ..., \underbrace{1}_{\text{第}i\text{位}}, ..., 0] \in \{0, 1\}^{|V|} one_hot(wi)=[0,0,...,i 1,...,0]{0,1}V

其中 ∣ V ∣ |V| V 为词汇表大小。

具体例子
假设词汇表只有6个词:{ "我", "爱", "中国", "北京", "上海", "深圳" }

词的One-Hot向量:
"我"   → [1, 0, 0, 0, 0, 0]  (第1位为1)
"爱"   → [0, 1, 0, 0, 0, 0]  (第2位为1)
"中国" → [0, 0, 1, 0, 0, 0]  (第3位为1)
"北京" → [0, 0, 0, 1, 0, 0]  (第4位为1)
"上海" → [0, 0, 0, 0, 1, 0]  (第5位为1)
"深圳" → [0, 0, 0, 0, 0, 1]  (第6位为1)

每个向量:
- 长度 = 词汇表大小 = 6
- 只有一个位置是1,其他全是0
- 不同词的向量完全正交(内积为0,即完全无关)

关键观察:任意两个不同的词,它们的One-Hot向量之间的余弦相似度都是0。这意味着:"北京"和"上海"的相似度,"北京"和"爱"的相似度——完全相同,都是0

这显然不符合直觉。两个中国城市(北京和上海)在语义上应该比"北京"和"爱"更相似,但One-Hot编码完全无法表达这种关系。

One-Hot编码的三大致命缺陷

缺陷一:维度灾难

真实NLP项目的词汇表通常有几万到几十万个词。

如果你的词汇表有100,000个词:
- 每个词的One-Hot向量长度 = 100,000
- 一个句子如果有50个词,需要 50 × 100,000 = 5,000,000 个数字来表示
- 其中只有50个位置是1,其余4,999,950个都是0

存储效率:99.999%的存储空间被浪费了!
缺陷二:语义信息为零
计算语义相似度(余弦相似度):

One-Hot("北京") · One-Hot("上海") = [0,0,0,1,0,0] · [0,0,0,0,1,0] = 0
One-Hot("北京") · One-Hot("爱")   = [0,0,0,1,0,0] · [0,1,0,0,0,0] = 0

两个结果都是0!模型无法从向量中得知"北京"和"上海"都是城市。

这就像一个只认识学号的人——他可以区分"001号学生"和"002号学生",但他不知道这两个学生中谁数学好、谁是同桌、谁和谁是好朋友。

缺陷三:无法处理未登录词(OOV,Out-of-Vocabulary)

如果测试时出现了一个训练时没见过的词,One-Hot编码完全不知道怎么办——因为词汇表里没有它。

训练时词汇表:{ "我", "爱", "中国" }
测试时句子:"我爱深度学习"

"深度学习"不在词汇表中 → 无法编码 → 处理链路中断

One-Hot编码代码实现

虽然One-Hot已经很少单独使用,但理解它的实现有助于理解后续的进阶方法。

# ============================================================
# One-Hot编码的实现
# ============================================================

from tensorflow.keras.preprocessing.text import Tokenizer
import joblib

# ============================================================
# 第1步:准备词汇表
# ============================================================
# vocabs 是一个包含所有可能词汇的集合(set)
# 在实际项目中,你需要从训练文本中提取所有出现过的词
vocabs = {"我", "爱", "中国", "北京", "上海", "深圳"}

# ============================================================
# 第2步:使用Tokenizer构建词→ID映射
# ============================================================
# Tokenizer是Keras提供的一个文本预处理工具
# 它的核心作用是:给每个词分配一个唯一的整数ID
tokenizer = Tokenizer()

# fit_on_texts() 学习词汇表,建立 word → index 的映射
# 注意:这里传入的是词汇的集合,每个词被视为一个"文本"
tokenizer.fit_on_texts(vocabs)

# 查看建立的映射关系
print("词 → 索引映射:")
for word, idx in tokenizer.word_index.items():
    print(f"  {word:6s}{idx}")
# 输出:
#   中国    → 1
#   我      → 2
#   上海    → 3
#   北京    → 4
#   爱      → 5
#   深圳    → 6
# 注意:索引从1开始(不是0),索引顺序取决于词的排序

# ============================================================
# 第3步:手动生成One-Hot向量
# ============================================================
# 创建一个全零向量(长度 = 词汇表大小)
vocab_size = len(vocabs)  # 6
zero_list = [0] * vocab_size  # [0, 0, 0, 0, 0, 0]

# 获取"中国"这个词的索引
# word_index 返回的是从1开始的索引,所以减1得到向量中的位置
idx = tokenizer.word_index["中国"] - 1  # 1 - 1 = 0(Python列表从0开始)

# 在对应位置设置为1
zero_list[idx] = 1
print(f"\n'中国'的One-Hot向量: {zero_list}")
# 输出:[1, 0, 0, 0, 0, 0]

# ============================================================
# 第4步:批量为多个词生成One-Hot向量
# ============================================================
def get_one_hot(word, tokenizer, vocab_size):
    """根据tokenizer为单个词生成One-Hot向量"""
    vec = [0] * vocab_size
    if word in tokenizer.word_index:
        idx = tokenizer.word_index[word] - 1
        vec[idx] = 1
    return vec

# 测试"北京"和"上海"
print(f"\n'北京'的One-Hot向量: {get_one_hot('北京', tokenizer, vocab_size)}")
print(f"'上海'的One-Hot向量: {get_one_hot('上海', tokenizer, vocab_size)}")
# 输出:
# '北京'的One-Hot向量: [0, 0, 0, 1, 0, 0]
# '上海'的One-Hot向量: [0, 0, 1, 0, 0, 0]

# ============================================================
# 第5步:使用texts_to_matrix生成One-Hot(Keras便捷方法)
# ============================================================
# Tokenizer提供了直接生成One-Hot矩阵的方法
# mode='binary' 表示生成One-Hot编码
one_hot_matrix = tokenizer.texts_to_matrix(
    ["北京", "上海"],   # 要编码的词列表
    mode='binary'      # 'binary' = One-Hot, 'count' = 词频, 'tfidf' = TF-IDF
)
print(f"\n批量One-Hot矩阵:\n{one_hot_matrix}")
# 输出:
# [[0. 1. 0. 0. 0. 0.]
#  [0. 0. 1. 0. 0. 0.]]
# 每一行是一个词的One-Hot向量


# ============================================================
# 第6步:持久化保存Tokenizer
# ============================================================
# 训练时建立的词→ID映射需要在推理时使用
# 因此需要保存tokenizer

# 保存(joblib是Python的序列化工具,比pickle更适合大对象)
joblib.dump(tokenizer, './mytokenizer')
print("\nTokenizer已保存到 ./mytokenizer")

# 加载
loaded_tokenizer = joblib.load('./mytokenizer')
print("Tokenizer已从 ./mytokenizer 加载")

# 验证加载的正确性
print(f"加载后的词表映射: {loaded_tokenizer.word_index}")
# 输出应与保存前一致

One-Hot编码总结

┌─────────────────────────────────────────────────────────────┐
│                   One-Hot编码                                │
│                                                             │
│  优点:                                                     │
│  ✓ 实现极其简单(几行代码)                                   │
│  ✓ 直观易懂(每个词有一个唯一的位置)                          │
│  ✓ 不需要训练(无需参数学习)                                 │
│                                                             │
│  缺点:                                                     │
│  ✗ 维度灾难(100,000词的词汇表 = 100,000维向量)              │
│  ✗ 语义为0(所有词对之间的相似度都是0)                       │
│  ✗ 极度稀疏(浪费存储和计算资源)                             │
│  ✗ 无法处理未登录词                                          │
│                                                             │
│  结论:One-Hot是NLP的"轮子"——                              │
│     虽然现在开汽车(Word2Vec/Embedding),                    │
│     但理解轮子才能理解汽车为什么能跑。                         │
└─────────────────────────────────────────────────────────────┘

自检问题

  1. Jon说"北京和上海的One-Hot向量余弦相似度是0,但直觉上它们应该相似。"这个说法对吗?为什么One-Hot产生这样的结果?
  2. 如果你的词汇表有500,000个词,一个100词的句子用One-Hot编码需要多少个数字存储?其中多少个是0?
  3. One-Hot编码有没有任何场景仍然有用?(提示:思考分类任务的标签编码)

Word2Vec——NLP历史上最重要的突破之一

问题的重新思考:如何让向量"有含义"?

回顾One-Hot的核心问题:语义相似的词在向量空间中关系为零。

Word2Vec解决这个问题的思路非常巧妙:

一个词的含义,由它周围的词决定。

这句话是Word2Vec(乃至所有词向量方法)的哲学基石。让我们先理解这个思想。

生活类比:通过朋友圈来了解一个人

你不知道"张三"是什么样的人,但你看到:
- 张三经常和 李四、王五 一起出现
- 张三很少和 赵六、孙七 一起出现

于是你推断:张三和李四、王五是"同一类人"。

同样:
- "北京"经常和"上海"、"城市"、"首都"一起出现
- "北京"很少和"奔跑"、"美丽"、"吃饭"一起出现

于是模型推断:"北京"和"上海"是"同一类词"——它们都是城市名。

CBOW与Skip-gram——两种互补的学习策略

Word2Vec有两种模型架构,它们是对同一个思想的两个方向:

CBOW(Continuous Bag of Words):
  用 上下文词 → 预测 中心词
  
  例子:用 ["我", "爱", "北京"] 预测 "天安门"
  输入:["我", "爱", "北京"]的One-Hot
  输出:"天安门"的概率

Skip-gram:
  用 中心词 → 预测 上下文词
  
  例子:用 "天安门" 预测 ["我", "爱", "北京"]
  输入:"天安门"的One-Hot
  输出:"我"、"爱"、"北京"各自的概率

生活类比

  • CBOW:看到3个朋友聚在一起,猜他们中间站着谁(根据周围人推断中间人)
  • Skip-gram:看到1个人,猜他最经常和哪些人在一起(根据一个人推断他的朋友圈)
CBOW(Continuous Bag of Words)详解

核心思想:用上下文词预测中心词。

公式

P ( w t ∣ w t − k , . . . , w t − 1 , w t + 1 , . . . , w t + k ) P(w_t \mid w_{t-k}, ..., w_{t-1}, w_{t+1}, ..., w_{t+k}) P(wtwtk,...,wt1,wt+1,...,wt+k)

理解这个公式

  • w t w_t wt 是中心词(我们要预测的词)
  • w t − k , . . . , w t − 1 w_{t-k}, ..., w_{t-1} wtk,...,wt1 是中心词左边的k个词
  • w t + 1 , . . . , w t + k w_{t+1}, ..., w_{t+k} wt+1,...,wt+k 是中心词右边的k个词
  • k 是窗口大小(通常为2-5)
窗口大小k=2 的示例:

句子:我  爱  北京  天安门  的  清晨
索引:1   2   3    4      5   6

当中心词是"北京"(索引3)时:
  上文:w[1]="我", w[2]="爱"  (左边2个词)
  下文:w[4]="天安门", w[5]="的"  (右边2个词)
  
  CBOW用 ["我", "爱", "天安门", "的"] 预测 "北京"

CBOW为什么叫"Continuous Bag of Words"?

  • Bag of Words:因为上下文词的顺序不重要("我爱天安门的"和"的天安门爱我"效果一样)
  • Continuous:因为输出是一个连续的概率分布(不是离散的one-hot)
Skip-gram详解

核心思想:用中心词预测上下文词。

公式

P ( w t + j ∣ w t ) , − k ≤ j ≤ k , j ≠ 0 P(w_{t+j} \mid w_t), \quad -k \leq j \leq k, \quad j \neq 0 P(wt+jwt),kjk,j=0

理解这个公式

  • w t w_t wt 是中心词(已知)
  • w t + j w_{t+j} wt+j 是上下文中的某个词(要预测的)
  • j 从-k到k(排除0),表示距离中心词的偏移量
  • 对于每个j,都单独计算一个预测
窗口大小k=2 的示例:

中心词是"北京"(索引3):
  Skip-gram用 "北京" 分别预测:
    j=-2: "我"
    j=-1: "爱"
    j=+1: "天安门"
    j=+2: "的"
  
  一共4个预测任务

Word2Vec的网络结构

Word2Vec本质上是一个浅层神经网络(只有一层隐藏层)。它的"副产品"——隐藏层的权重——就是我们要的词向量。

输入层                    隐藏层                      输出层
(V维)                    (N维)                      (V维)

  o ─┐                   ┌─ o                       ┌─ o
  o ─┤     权重矩阵W      ├─ o      权重矩阵W'        ├─ o
  o ─┤     [V × N]      ├─ o      [N × V]         ├─ o
  . ─┤                   ├─ .                       ├─ .
  . ─┤                   ├─ .                       ├─ .
  o ─┘                   └─ o                       └─ o

V: 词汇表大小(例如100,000)
N: 词向量维度(例如300)
W[V×N]: 输入→隐藏的权重矩阵(训练后取每一行 = 对应词的向量)
W'[N×V]: 隐藏→输出的权重矩阵(训练后通常丢弃)

架构关键点

  1. 输入是One-Hot向量(V维,只有一个位置是1)
  2. 隐藏层是线性激活(没有非线性函数,所以实际等价于矩阵乘法)
  3. 输出是Softmax(V维概率分布,表示每个词是目标词的概率)
  4. W矩阵的每一行对应输入词汇表中一个词的词向量

为什么取W而不是W’作为词向量?

W的每一行对应输入中的一个词。当我们用这个词的One-Hot向量乘以W时,实际效果就是"查表"——取出W中对应行。这一行就是该词的低维表示。

CBOW详细训练流程(Step by Step)

让我们用一个完整的例子走一遍CBOW的训练过程:

假设:
- 词汇表大小 V = 100,000
- 词向量维度 N = 300
- 窗口大小 k = 2
- 训练语料中的一句话:"the cat sits on the mat"

第1步:确定训练样本
  取中心词 = "sits"(位置3)
  上下文词 = ["the"(位置1), "cat"(位置2), "on"(位置4), "the"(位置5)]
  训练样本:(上下文=["the","cat","on","the"], 目标="sits")

第2步:上下文词的One-Hot编码
  One-Hot("the") = [0,0,0,1,0,0,...,0]  (V维)
  One-Hot("cat") = [0,0,0,0,0,1,...,0]  (V维)
  One-Hot("on")  = [1,0,0,0,0,0,...,0]  (V维)
  One-Hot("the") = [0,0,0,1,0,0,...,0]  (V维)

第3步:计算上下文词的隐藏表示
  每个One-Hot × W → 对应的W行向量
  h_the = W[the_idx]    (300维)
  h_cat = W[cat_idx]    (300维)
  h_on  = W[on_idx]     (300维)
  h_the2 = W[the_idx]   (300维,与第一个h_the相同)

第4步:求和/平均(这就是"Bag of Words"的意思)
  h_avg = (h_the + h_cat + h_on + h_the2) / 4
  h_avg 是一个300维向量
  注意:顺序被忽略了!"the cat on the"和"on the cat the"结果一样

第5步:从隐藏层到输出层
  output_scores = h_avg × W'  (1×300 × 300×100000 = 1×100000)
  得到每个词作为中心词的"得分"

第6步:Softmax得到概率分布
  P(word_i | context) = softmax(output_scores)[i]
  例如:P("sits" | "the cat on the") = 0.03
       P("cat"  | "the cat on the") = 0.001
       ...

第7步:计算损失(Loss)
  真实目标词的One-Hot: [0,...,0,1(sits位置),0,...,0]
  预测概率分布: [0.001, 0.03, 0.0001, ...]
  损失 = -log(P("sits" | context)) = -log(0.03) = 3.51

第8步:反向传播更新W和W'
  根据损失对W和W'求梯度,更新权重
  使得P("sits" | context)变得更高

训练的本质:通过不断调整W矩阵(词向量),使得语义相似的词在向量空间中更接近。

CBOW vs Skip-gram 详细对比

在理解了这两种架构后,我们来做一个全面的对比:

对比维度 CBOW Skip-gram 解释
核心操作 上下文→中心词 中心词→上下文 预测方向相反
训练速度 Skip-gram每个词对产生k×2个训练样本
每步训练样本数 1个 2k个 k=窗口大小,CBOW每次预测1个词,Skip-gram预测2k个
罕见词效果 Skip-gram给每个出现更多的训练机会
常见词效果 中等 CBOW对高频词有"平均"效果
大数据集 倾向于Skip-gram ✓ 推荐 Skip-gram能从更多数据中获益
小数据集 ✓ 推荐 容易过拟合 CBOW有更强的正则化效果
语义平滑 CBOW的"平均"操作减少了噪声
类比推理 稍弱 Skip-gram更好地保留了线性关系

选择建议

什么情况下...
  ├─ 数据集 < 1GB      → CBOW
  ├─ 数据集 > 1GB      → Skip-gram
  ├─ 很多罕见/专业词汇  → Skip-gram
  ├─ 追求训练速度       → CBOW
  └─ 追求最佳效果       → Skip-gram(如果数据和计算资源允许)

Word2Vec的革命性意义

从"编号"到"坐标"

Word2Vec之前,词是一个离散的符号(ID编号)。Word2Vec之后,词是一个连续空间中的坐标。

One-Hot时代:
  "国王" = ID 1532
  "王后" = ID 4821
  关系 = ???(无法计算)

Word2Vec时代:
  "国王" = [0.2, -0.5, 0.8, 0.1, ..., -0.3]  (300维)
  "王后" = [0.1, -0.4, 0.7, 0.2, ..., -0.2]  (300维)
  关系 = 可以计算余弦相似度!
词向量的"魔法"——语义运算

这是Word2Vec最令人惊叹的性质:

vec("国王") - vec("男人") + vec("女人") ≈ vec("王后")
vec("巴黎") - vec("法国") + vec("意大利") ≈ vec("罗马")
vec("游泳") - vec("游") + vec("跑") ≈ vec("跑步")

这意味着什么?

词向量不仅编码了词的含义,还编码了词之间的关系。上面的运算揭示了:

  • "国王"和"王后"之间的关系 ≈ "男人"和"女人"之间的关系(都是性别关系)
  • "巴黎"和"法国"之间的关系 ≈ "罗马"和"意大利"之间的关系(都是首都-国家关系)

生活类比

想象一个巨大的地图。每个城市在这个地图上有一个坐标。如果你知道"从北京到上海的向量方向",你就可以用这个方向去推理其他城市对的关系。

Word2Vec的语义空间就像一个"语义地图"——意思相近的词靠在一起,语义关系的方向在不同词对之间保持一致。

自检问题

  1. 用自己的话解释CBOW和Skip-gram的核心区别。
  2. 为什么Word2Vec训练完后,我们取的是W矩阵而不是W’矩阵作为词向量?
  3. 如果你有一个100MB的医学文本数据集(包含大量罕见医学术语),你会选CBOW还是Skip-gram?为什么?

FastText——在Word2Vec基础上的关键改进

Word2Vec的一个盲点:Out-of-Vocabulary (OOV)

让我们先思考一个问题:

训练语料中出现了"学习"和"机器",
但没有出现"机器学习"。

Word2Vec能处理"机器学习"吗?
→ 不能!因为"机器学习"不在词汇表中,无法编码。

但是,"机器学习"是由"机器"和"学习"组成的——
能不能利用这种子词信息?

这就是FastText的核心动机。

FastText的核心创新:子词(Subword)信息

FastText由Facebook AI Research于2016年开源。它最核心的改进是:将每个词分解为字符级n-gram的组合

什么是字符n-gram?
词:"自然语言"

如果我们使用3-gram(连续3个字符的组合):
加上特殊边界标记 < 和 >:
"<自然语言>"

提取所有3-gram:
<自, 自然, 然语, 语言, 言>

这个词最终被表示为:
"自然语言" = 词本身的向量 + 所有3-gram向量的和
这解决了什么问题?
训练语料中只有"计算机"和"网络",
但测试时出现了新词"计算机网络"。

Word2Vec:完全无法处理(OOV)

FastText:
  提取"计算机网络"的3-gram:
  <计, 计算, 算机, 机网, 网络, 络>
  
  这些3-gram中有一些在训练时见过!
  "计算"见过(来自"计算机")
  "网络"见过(来自"网络")
  
  因此FastText可以利用这些见过的子词信息
  来构造"计算机网络"的向量!

生活类比

Word2Vec就像一个人只认识完整的中文词语,遇到"人工智能"这个新词就懵了。

FastText就像一个人知道"人"、“工”、“智”、"能"这些——即使没见过"人工智能"这个完整的词,也能根据组成部分猜出一个大概的工作领域。

FastText安装与使用

# ============================================================
# 第1步:安装FastText
# ============================================================
# 在命令行执行以下命令之一:
# pip install fasttext
# 如果上述命令失败,可以尝试:
# pip install fasttext-wheel

# 如果安装仍然有问题(Windows环境常见),可以尝试:
# conda install -c conda-forge fasttext

import fasttext

# ============================================================
# 第2步:准备训练数据
# ============================================================
# FastText的无监督训练需要一个纯文本文件,
# 每行一个句子(或文档),词语之间用空格分隔

# 示例数据文件(data/my_corpus.txt)内容:
"""
我 爱 自然 语言 处理
深度 学习 改变 了 人工 智能
机器 学习 是 人工 智能 的 基础
自然 语言 处理 是 NLP 的 核心
"""

# ============================================================
# 第3步:训练FastText模型
# ============================================================
# train_unsupervised 用于无监督训练(学习词向量)
# 参数 'data/fil9' 是训练数据文件路径
model = fasttext.train_unsupervised('./data/fil9')

# 查看模型信息
print(f"词汇表大小: {len(model.words)}")
print(f"词向量维度: {model.get_dimension()}")


# ============================================================
# 第4步:保存和加载模型
# ============================================================
# 保存为二进制文件
model.save_model("./data/fil9.bin")

# 从二进制文件加载
model = fasttext.load_model('./data/fil9.bin')
# 加载后的模型功能与保存前完全一致

# ============================================================
# 第5步:获取词向量
# ============================================================
# get_word_vector() 返回一个numpy数组,shape为(维度,)
vector = model.get_word_vector('the')
print(f"'the'的词向量维度: {vector.shape}")  # (100,)
print(f"向量前10个值: {vector[:10]}")
# 输出示例:
# [-0.023, 0.045, -0.012, 0.089, -0.034, 0.067, -0.001, 0.023, -0.056, 0.078]

# ============================================================
# 第6步:查找近义词
# ============================================================
# get_nearest_neighbors() 返回与目标词最相似的词
# k 参数控制返回多少个结果(默认10)

# 查询sports的近义词
neighbors = model.get_nearest_neighbors('sports', k=5)
print("\n'sports'的近义词:")
for score, word in neighbors:
    print(f"  {word:20s} (相似度: {score:.4f})")
# 输出示例:
#   sportsnet            (相似度: 0.8412)
#   sport                (相似度: 0.8134)
#   sporting             (相似度: 0.7987)
#   athletics            (相似度: 0.7654)
#   espn                 (相似度: 0.7523)

# 查询dog的近义词
neighbors = model.get_nearest_neighbors('dog', k=5)
print("\n'dog'的近义词:")
for score, word in neighbors:
    print(f"  {word:20s} (相似度: {score:.4f})")
# 输出示例:
#   catdog               (相似度: 0.8451)
#   dogcow               (相似度: 0.7483)
#   dogs                 (相似度: 0.7321)
#   puppy                (相似度: 0.7198)
#   pet                  (相似度: 0.7056)

# 注意:前两个结果"catdog"和"dogcow"看起来很奇怪!
# 这是因为训练语料fil9中可能包含一些特殊的合成词。
# 这提醒我们:词向量的质量取决于训练数据的质量和规模。

# ============================================================
# 第7步:词类比(语义运算)
# ============================================================
# FastText也支持词类比查询
# 格式:get_analogies(A, B, C) → 寻找 X 使得 A:B = C:X
# 即:B - A + C 最接近的词

# 经典类比:国王 - 男人 + 女人 = 王后
analogies = model.get_analogies("king", "man", "woman", k=3)
print("\n类比: king - man + woman = ?")
for score, word in analogies:
    print(f"  {word:20s} (相似度: {score:.4f})")
# 预期:queen(王后)应该排在最前面

# ============================================================
# 第8步:处理OOV词(FastText的核心优势)
# ============================================================
# 即使一个词不在训练语料中,FastText也能生成它的向量
oov_word = "unbelievableperformance"  # 故意拼出的生造词
if oov_word in model.words:
    print(f"'{oov_word}' 在词汇表中")
else:
    print(f"'{oov_word}' 不在词汇表中,但可以生成向量")
    vec = model.get_word_vector(oov_word)
    print(f"向量维度: {vec.shape}")
    print(f"向量前5个值: {vec[:5]}")
# 这种能力是Word2Vec不具备的!

FastText训练参数详解

理解这些参数有助于你在实际项目中调优:

参数 默认值 说明 调优建议
model 'skipgram' 模型类型:‘cbow’ 或 ‘skipgram’ 小数据集用cbow,大数据集用skipgram
dim 100 词向量维度 通常100-300,更大数据集可以用更大维度
epoch 5 训练轮数 小数据增加到10-20,大数据5-10
lr 0.05 学习率 通常保持默认,收敛慢可增加到0.1
thread 12 训练使用的线程数 设为CPU核心数
minCount 5 忽略出现频率低于此值的词 小数据集可设为1-2
minn 3 字符n-gram的最小长度 英文适合3,中文适合1-2
maxn 6 字符n-gram的最大长度 英文适合6,中文适合3-4
bucket 2000000 子词哈希桶数量 通常不需要调整
ws 5 上下文窗口大小 更大的窗口捕捉更广泛的关系
# ============================================================
# 自定义参数训练示例
# ============================================================
model = fasttext.train_unsupervised(
    'data/fil9',       # 训练数据路径
    model="cbow",      # 使用CBOW架构(更快训练)
    dim=300,           # 300维词向量(比默认的100维更能表达语义)
    epoch=10,          # 训练10轮(更多轮训练=更好结果,但可能过拟合)
    lr=0.1,            # 学习率0.1(比默认0.05略高,加快收敛)
    thread=8,          # 使用8个线程并行训练
    minCount=3,        # 出现少于3次的词被忽略
    minn=2,            # 最短字符n-gram为2
    maxn=5             # 最长字符n-gram为5
)

# 查看训练结果的词汇表大小
print(f"训练后的词汇表大小: {len(model.words)}")
print(f"词向量维度: {model.get_dimension()}")

# 对于中文文本训练,建议设置:
# minn=1, maxn=3 来更好地捕捉汉字级别的信息

FastText vs Word2Vec 总结

┌────────────────────────────────────────────────────────────┐
│  FastText vs Word2Vec                                      │
│                                                            │
│  相同点:                                                  │
│  · 都基于CBOW/Skip-gram架构                               │
│  · 都使用浅层神经网络                                      │
│  · 都产生低维稠密词向量                                    │
│                                                            │
│  不同点:                                                  │
│  · FastText引入了子词(subword)信息                       │
│  · FastText可以处理OOV词                                  │
│  · FastText对罕见词效果更好                                │
│  · FastText训练速度更快(使用了层次Softmax优化)            │
│  · FastText同时支持文本分类(train_supervised)            │
│                                                            │
│  选择建议:                                                │
│  · 数据集有大量罕见词/新词 → FastText                      │
│  · 需要OOV处理能力 → FastText                             │
│  · 只需要常见词的词向量 → Word2Vec (gensim) 或 FastText    │
│  · 需要用词向量做文本分类 → FastText                       │
└────────────────────────────────────────────────────────────┘

自检问题

  1. FastText相对于Word2Vec的核心创新是什么?用你自己的话简要概括。
  2. 如果一个中文句子中包含一个从未见过的新词"元宇宙",Word2Vec和FastText分别会如何处理?
  3. 如果你要训练一个医学领域的词向量,应该把minn和maxn设大一些还是设小一些?为什么?

nn.Embedding——词嵌入在深度学习中的实际使用

从"预训练好的向量"到"可训练的查找表"

在前面的Word2Vec和FastText中,词向量是预训练好的——先用大规模语料训练,得到一个固定的词向量表,然后在各任务中使用。

但在深度学习中,尤其是在PyTorch这类框架中,词向量通常以另一种方式使用:作为神经网络的一部分,在整个任务训练过程中一起更新

nn.Embedding的本质

nn.Embedding 是PyTorch提供的一个模块。它的本质非常简单:一个可训练的查找表(Lookup Table)

nn.Embedding(num_embeddings=1000, embedding_dim=8)

等价于创建了一个形状为 [1000, 8] 的参数矩阵:

     dim0  dim1  dim2  dim3  dim4  dim5  dim6  dim7
词0  [0.12, 0.34, -0.5, 0.78, -0.2, 0.11, 0.45, -0.3]
词1  [0.01, -0.2, 0.33, -0.1, 0.56, 0.22, -0.4, 0.18]
词2  [0.88, -0.1, 0.01, 0.55, -0.3, 0.66, -0.1, 0.22]
...
词999 [-0.4, 0.55, -0.2, 0.09, 0.33, -0.5, 0.12, -0.1]

当你传入索引 [0, 2] 时:
Embedding 从矩阵中取出第0行和第2行作为这两个词的向量。

就是这么简单!没有复杂的计算,就是"查表"。

生活类比

nn.Embedding 就像一个智能通讯录

  • 你有999个联系人,每个联系人有一个编号
  • 每个联系人有一个"描述卡片"(一个8维向量)
  • 你输入编号5 → 自动取出第5个人的描述卡片
  • 不同之处:这个"描述卡片"是可以随着任务训练而自动修改的。如果任务发现某两个联系人应该更相似,就自动调整他们的描述卡片。

公式

Embedding ( x ) = lookup ( W e i g h t , x ) \text{Embedding}(x) = \text{lookup}(Weight, x) Embedding(x)=lookup(Weight,x)

其中 Weight 的形状为 [ v o c a b _ s i z e , e m b e d d i n g _ d i m ] [vocab\_size, embedding\_dim] [vocab_size,embedding_dim]

数学含义:将整数索引 x x x 映射为嵌入矩阵 W e i g h t Weight Weight 中第 x x x 行的向量。

代码示例详解

import torch
import torch.nn as nn

# ============================================================
# 第1步:创建嵌入层
# ============================================================
# num_embeddings: 词汇表大小(有多少个不同的词)
# embedding_dim:  每个词的向量维度(想用多少维来表示每个词)
#
# 这里:1000个词,每个词用8维向量表示
embed = nn.Embedding(num_embeddings=1000, embedding_dim=8)

# 查看嵌入层的形状
print(f"嵌入层参数矩阵的形状: {embed.weight.shape}")
# 输出:torch.Size([1000, 8])
# 解读:1000行(每个词一行),8列(每个维度一列)

# 查看初始化的值(训练前是随机初始化的)
print(f"\n前3个词的初始向量(前5维):")
print(embed.weight[:3, :5])
# 输出示例(随机初始化,每次运行不同):
# tensor([[-0.0082,  1.2451,  0.3578, -0.9876,  0.1234],
#         [ 0.7654, -0.4321, -1.2345,  0.0123, -0.8765],
#         [-0.5432,  0.9876,  0.0012, -0.3456,  0.9876]], ...)
# 注意:这些是随机初始化的值,训练过程中会不断更新


# ============================================================
# 第2步:查询单个词的嵌入向量
# ============================================================
# 用第5个词的索引(索引从0开始)查询它的向量
word_idx = torch.tensor([5])  # 形状: [1] —— 一个样本的索引
word_vec = embed(word_idx)     # 形状: [1, 8] —— 一个8维向量

print(f"\n第5个词的向量: {word_vec}")
print(f"向量的形状: {word_vec.shape}")
# 输出:torch.Size([1, 8])


# ============================================================
# 第3步:批量查询多个词的嵌入向量
# ============================================================
# 假设我们有2个句子,每个句子有4个词:
# 句子1: 词ID为 [1, 2, 3, 4]
# 句子2: 词ID为 [4, 5, 6, 20]
batch_idx = torch.tensor([[1, 2, 3, 4],
                           [4, 5, 6, 20]])  # 形状: [2, 4]

batch_vec = embed(batch_idx)  # 形状: [2, 4, 8]

print(f"\n批量词索引的形状: {batch_idx.shape}")   # [2, 4]
print(f"批量词向量的形状: {batch_vec.shape}")      # [2, 4, 8]
# 解读:
# 2 = batch size(句子数量)
# 4 = sequence length(每个句子的词数)
# 8 = embedding dim(每个词的向量维度)

# 查看句子1的第2个词(索引2,即"3")的向量
print(f"\n句子1的第2个词的向量: {batch_vec[0, 1]}")
# batch_vec[batch维度, 序列位置, 向量维度]


# ============================================================
# 第4步:理解nn.Embedding和Word2Vec的关系
# ============================================================
# 如果你已经用Word2Vec训练好了词向量,可以加载到nn.Embedding中:

# 假设有一个预训练好的词向量矩阵(通常用numpy或torch tensor存储)
# pretrained_vectors = torch.randn(1000, 8)  # 模拟预训练向量

# 创建嵌入层并用预训练向量初始化
# embed = nn.Embedding(num_embeddings=1000, embedding_dim=8)
# embed.weight.data.copy_(pretrained_vectors)  # 用预训练向量替换随机初始值

# 可以选择是否在后续训练中冻结(不更新)这些预训练向量
# embed.weight.requires_grad = False  # 冻结:不更新预训练向量
# embed.weight.requires_grad = True   # 微调:允许预训练向量随任务更新


# ============================================================
# 第5步:一个完整的词嵌入→下游模型的例子
# ============================================================
import torch.nn as nn

class SimpleTextClassifier(nn.Module):
    """
    一个简单的文本分类模型,演示nn.Embedding的使用
    """
    def __init__(self, vocab_size, embedding_dim, num_classes):
        super().__init__()
        # 嵌入层:将词索引转为词向量
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        # 全连接层:将词向量序列分类
        self.fc = nn.Linear(embedding_dim, num_classes)
    
    def forward(self, x):
        """
        x: 词索引序列,形状 [batch_size, seq_len]
        例如:[[1, 5, 3, 2], [4, 1, 6, 7]]
        """
        # 第1步:获取每个词的向量
        embedded = self.embedding(x)  # [batch_size, seq_len, embedding_dim]
        
        # 第2步:对序列中所有词的向量求平均(最简单的序列汇总方式)
        # dim=1 表示沿序列长度维度求平均
        pooled = embedded.mean(dim=1)  # [batch_size, embedding_dim]
        
        # 第3步:通过全连接层得到分类结果
        logits = self.fc(pooled)  # [batch_size, num_classes]
        
        return logits

# 创建模型实例
model = SimpleTextClassifier(
    vocab_size=1000,    # 词汇表1000个词
    embedding_dim=8,    # 每个词8维向量
    num_classes=2       # 二分类(正面/负面)
)

# 模拟一个批次的数据:2个句子,每个句子5个词
dummy_input = torch.randint(0, 1000, (2, 5))
print(f"\n模拟输入形状: {dummy_input.shape}")  # [2, 5]
print(f"模拟输入内容:\n{dummy_input}")

# 前向传播
output = model(dummy_input)
print(f"模型输出(logits)形状: {output.shape}")  # [2, 2]
print(f"模型输出值:\n{output}")

Word2Vec vs nn.Embedding:静态 vs 动态

这是初学者经常混淆的概念,下面做一个清晰的对比:

┌────────────────────────────────────────────────────────────┐
│  Word2Vec (静态词向量)                                      │
│  ┌─────────────┐                                           │
│  │ 大规模语料    │ ─→ 预训练 ─→ 固定词向量表                 │
│  │ (Wikipedia等) │     ↓                                   │
│  └─────────────┘    词向量不再改变                          │
│                                                             │
│  优点:                                                     │
│  · 利用了大量无标注数据学到丰富的语义知识                    │
│  · 作为固定特征输入,训练简单                               │
│  缺点:                                                     │
│  · 不能根据下游任务调整("学习"在教育和ML中指同一个向量)    │
└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐
│  nn.Embedding (动态词向量)                                  │
│  ┌─────────────┐                                           │
│  │ 随机初始化    │ ─→ 随下游任务 ─→ 任务自适应词向量          │
│  │ 词向量        │    一起训练                               │
│  └─────────────┘    词向量随任务优化                         │
│                                                             │
│  优点:                                                     │
│  · 词向量针对特定任务优化("学习"在文本分类和文本生成中不同) │
│  · 端到端训练,不需要中间存储                               │
│  缺点:                                                     │
│  · 需要大量标注数据                                         │
│  · 可能无法充分利用无标注数据                               │
└────────────────────────────────────────────────────────────┘

实际最佳实践结合两种方式

# 最佳实践:用Word2Vec/FastText预训练向量初始化nn.Embedding
# 然后在特定任务上微调

embed = nn.Embedding(vocab_size, embedding_dim)

# 用预训练好的词向量初始化
with torch.no_grad():
    embed.weight.copy_(pretrained_vectors)  # 或者使用 .from_pretrained()

# 允许在任务训练中微调(通常是好的策略)
embed.weight.requires_grad = True  # 微调
对比维度 Word2Vec nn.Embedding
向量来源 预训练得到 随机初始化或加载预训练
是否可训练 固定(离线使用) 可随任务微调
使用方式 先离线训练,再查表使用 融入模型端到端训练
对新词的适应性 无法处理OOV 无法处理(但配合子词方法可解决)
依赖数据 大规模无标注数据 任务相关标注数据
典型使用 特征工程阶段 深度学习模型的第一层

自检问题

  1. nn.Embedding的本质是什么?如果用一句话描述,你会怎么说?
  2. 为什么在实际项目中,我们经常用Word2Vec预训练的向量来初始化nn.Embedding,而不是随机初始化?
  3. batch_vec = embed(batch_idx) 中,batch_idx形状是[2, 4],batch_vec的形状是什么?为什么?

TensorBoard可视化——用眼睛"看"语义空间

为什么需要可视化?

词向量是高维的(通常100-300维),人眼无法直接"看"到一个300维的空间。可视化可以将高维空间压缩到2维或3维,让我们直观地观察:

  • 语义相似的词是否在空间中靠得近?
  • 不同类型的词是否形成了不同的"簇"?
  • 预训练词向量的质量如何?

生活类比:可视化词嵌入就像把一幅三维世界的地球仪投影到二维的平面地图上。虽然会损失一些精度,但能让你一眼看到大洲和国家之间的位置关系。

TensorBoard简介

TensorBoard是TensorFlow/PyTorch的可视化工具。它可以:

  • 可视化模型结构
  • 可视化训练过程中的损失和指标曲线
  • 可视化高维嵌入向量(我们的重点)

T-SNE / PCA 是常用的降维算法,在TensorBoard中会自动应用。

完整代码实现

# ============================================================
# 分词 + Tokenizer → ID → Embedding → TensorBoard
# 完整流程演示
# ============================================================

import torch
import torch.nn as nn
from torch.utils.tensorboard import SummaryWriter
from tensorflow.keras.preprocessing.text import Tokenizer
import jieba

# ============================================================
# 第1步:准备句子并分词
# ============================================================
# 定义两个示例句子
sentences = [
    "我爱北京天安门",
    "天安门上太阳升"
]

# 对每个句子进行jieba分词
# jieba.lcut(s) 将中文句子切分为词语列表
word_list = [jieba.lcut(s) for s in sentences]

print("分词结果:")
for s, w in zip(sentences, word_list):
    print(f"  {s:20s}{w}")

# 输出:
#   我爱北京天安门          → ['我', '爱', '北京', '天安门']
#   天安门上太阳升          → ['天安门', '上', '太阳', '升']


# ============================================================
# 第2步:使用Tokenizer将词转为ID
# ============================================================
# Tokenizer: Keras的文本预处理工具
# fit_on_texts: 学习所有文本中的词汇,建立 word → id 映射
tokenizer = Tokenizer()
tokenizer.fit_on_texts(word_list)

# 从tokenizer获取词列表(按ID排序)
# tokenizer.index_word 是一个字典:{id: word}
# 注意:id从1开始(0通常保留给padding)
my_token_list = list(tokenizer.index_word.values())

print("\n词汇表(词 → ID):")
for word, idx in tokenizer.word_index.items():
    print(f"  {word:8s}{idx}")
print(f"词汇总大小: {len(tokenizer.index_word)}")

# 预期输出:
#   我       → 1
#   爱       → 2
#   北京     → 3
#   天安门   → 4
#   上       → 5
#   太阳     → 6
#   升       → 7
# 词汇总大小: 7


# ============================================================
# 第3步:创建Embedding层
# ============================================================
# 嵌入层参数:
#   num_embeddings = 词汇表大小 + 1(因为ID从1开始,需要多一个位置给索引0)
#   embedding_dim  = 每个词的向量维度
embed = nn.Embedding(
    num_embeddings=len(tokenizer.index_word) + 1,  # 7+1=8
    embedding_dim=8  # 8维向量(实际项目中通常用100-300维)
)

print(f"\nEmbedding层参数形状: {embed.weight.shape}")
# 输出:torch.Size([8, 8])
# 解读:8个嵌入向量,每个8维

# 注意:这里embed.weight是随机初始化的,因此可视化结果没有实际意义。
# 在实际项目中,你应该:
# 1. 用预训练词向量初始化 embed.weight
# 2. 或者先训练模型再导出训练后的 embed.weight 进行可视化


# ============================================================
# 第4步:写入TensorBoard
# ============================================================
# SummaryWriter 将数据写入日志文件,TensorBoard读取这些文件
writer = SummaryWriter()
# 默认写入 ./runs/ 目录

# add_embedding 用于添加高维嵌入的可视化
# 参数:
#   mat: 嵌入矩阵,形状 [num_embeddings, embedding_dim]
#   metadata: 每个嵌入对应的标签(词本身)
#   metadata_header: 元数据的列名(可选)
writer.add_embedding(
    embed.weight.data,      # 嵌入矩阵(去掉梯度信息,只取数据)
    my_token_list,          # 词标签列表
    metadata_header=['word']  # 列名
)

# 关闭writer(确保数据写入磁盘)
writer.close()

print("\nTensorBoard数据已写入 ./runs/ 目录")


# ============================================================
# 第5步:启动TensorBoard
# ============================================================
# 在命令行(终端)中执行:
# tensorboard --logdir=runs --host 0.0.0.0
#
# 然后在浏览器中访问:
# http://127.0.0.1:6006
#
# 在TensorBoard界面中:
# 1. 点击顶部导航栏的 "PROJECTOR"
# 2. 在左侧可以选择降维方法(PCA、T-SNE、UMAP)
# 3. T-SNE通常给出最有信息量的可视化
# 4. 可以看到词在2D/3D空间中的分布


# ============================================================
# 第6步:完整的封装函数(生产环境使用)
# ============================================================
def visualize_embeddings(
    sentences,           # 句子列表
    embedding_dim=100,   # 嵌入维度
    log_dir='./runs'     # 日志目录
):
    """
    完整的文本→分词→向量化→TensorBoard可视化流程
    
    参数:
        sentences: 要可视化的句子列表(中文)
        embedding_dim: 词向量的维度
        log_dir: TensorBoard日志保存目录
    
    使用示例:
        visualize_embeddings(["我爱北京天安门", "天安门上太阳升"])
    """
    import jieba
    from tensorflow.keras.preprocessing.text import Tokenizer
    import torch.nn as nn
    from torch.utils.tensorboard import SummaryWriter
    
    # 第1步:分词
    word_list = [jieba.lcut(s) for s in sentences]
    print(f"已对 {len(sentences)} 个句子完成分词")
    
    # 第2步:构建词汇表
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(word_list)
    vocab = list(tokenizer.index_word.values())
    print(f"词汇表大小: {len(vocab)}")
    
    # 第3步:创建嵌入层
    embed = nn.Embedding(len(tokenizer.index_word) + 1, embedding_dim)
    print(f"嵌入矩阵形状: {embed.weight.shape}")
    
    # 第4步:写入TensorBoard
    writer = SummaryWriter(log_dir=log_dir)
    writer.add_embedding(embed.weight.data, vocab)
    writer.close()
    
    print(f"\n可视化数据已保存到 {log_dir}")
    print(f"启动命令: tensorboard --logdir={log_dir} --host 0.0.0.0")
    print(f"浏览器访问: http://127.0.0.1:6006")
    print(f"\n提示:如果词汇太少(< 10个),降维效果会不理想。")
    print(f"      建议至少使用50+个不同的词才能看到有意义的聚类。")

# 测试封装函数
visualize_embeddings(sentences, embedding_dim=8)

TensorBoard可视化解读指南

当你打开TensorBoard的PROJECTOR页面后,你会看到一个3D散点图,每个点代表一个词。以下是如何解读:

聚类模式                  含义
────────────────────  ─────────────────────────────────
同一"团"中的词          语义上相关(如所有的城市名在一个团中)
距离远的不同"团"        语义类别不同(城市名 vs 动词)
线性方向               可能对应某种语义关系
                         如"好→更好"的方向 ≈ "快→更快"的方向
离群点                 可能是不常见的词或分词错误

高质量词向量的特征

  • 同类词聚集在一起(所有地名靠近、所有人名靠近、所有动词靠近)
  • 相似的语义关系在小范围内有相似的方向
  • 没有不合理的"大杂烩"现象

低质量词向量的特征

  • 词随机散布,没有明显的聚类
  • 完全不相关的词靠得很近
  • 类别之间没有清晰的边界

自检问题

  1. 为什么要用T-SNE把高维词向量降到2维或3维来可视化?
  2. 如果你在TensorBoard中看到的词向量分布是随机散布的(没有任何聚类),可能是什么原因?

补充提升内容

中文分词难点深度剖析

歧义切分——中文分词最大的挑战

中文字与字之间没有分隔符,导致切分时有多种可能,需要依靠语义来决策。

经典歧义案例:

1. 交叉歧义(交集型歧义)
   "结合成分子"
   切分A: 结合 / 成 / 分子
   切分B: 结 / 合成 / 分子
   两个切分方案都合理,取决于上下文!

2. 组合歧义(覆盖型歧义)
   "马上"
   在"马上过来"中 → "马上"(立刻)
   在"他从马上下来" → "马" + "上"(在马背上)
   同一个字符序列在不同上下文中切分方式不同!

3. 真歧义
   "乒乓球拍卖完了"
   没有更多上下文时,人类也无法确定!
   必须依赖更广的上下文(前面的对话、场景等)。
未登录词识别

未登录词(OOV)是指不在系统词典中的词。主要有以下几类:

类型 例子 识别难点
新词 “躺平”、“内卷”、“emo” 词典更新跟不上语言发展
专有名词 “张伟强”、“望京东路” 组合空间无限,不可能全部收录
术语 “梯度消失”、“注意力头” 领域特定,通用词典不会收录
音译词 “乔布斯”、“特斯拉” 音译规则非唯一
数字混合 “3D打印”、“5G网络” 字符类型混合

其他分词工具横向对比

jieba不是唯一的选择,不同工具有不同的优势场景:

工具 开发方 核心算法 特点 适用场景 Python支持
jieba 个人开源 前缀词典 + HMM 轻量易用,社区活跃 通用场景,快速原型
HanLP 大快搜索 多模型切换 功能全面,精度高 专业NLP项目
LAC 百度 深度学习(Bi-GRU) 端到端,同时分词+词性+专名 垂直领域,需要高精度
PKUSeg 北京大学 CRF + 深度学习 多领域模型可选 不同领域的定制化分词
pynlpir 中科院 NLPIR/ICTCLAS 老牌系统,权威性高 学术研究
THULAC 清华大学 结构化感知机 快速,准确 大规模处理
SnowNLP 个人开源 自己实现的算法 自带情感分析 轻量中文NLP

选择建议

你的需求是什么?
  │
  ├─ 快速开始,先跑通流程
  │   → jieba(最简单)
  │
  ├─ 需要最高分词精度
  │   → HanLP 或 LAC
  │
  ├─ 需要同时分词+词性+NER
  │   → LAC(一站式完成)
  │
  ├─ 需要处理特定领域(如医疗)
  │   → PKUSeg(有多领域预训练模型)
  │
  └─ 需要做学术研究
      → pynlpir(中科院出品,方法经典)

词向量评估方法

训练完词向量后,如何评估它的质量?有两种评估方式:

内部评估(Intrinsic Evaluation)

直接评估词向量本身的语义质量,不需要下游任务:

方法一:词类比(Word Analogy)

问题格式:"A 之于 B,如同 C 之于 ___?"

例子:
  国王 : 王后  =  男人 : ?
  期待答案:女人
  计算方法:vec(王后) - vec(国王) + vec(男人) ≈ vec(女人)

  北京 : 中国  =  东京 : ?
  期待答案:日本
  计算方法:vec(中国) - vec(北京) + vec(东京) ≈ vec(日本)

方法二:词相似度(Word Similarity)

人工标注的词对相似度得分(如0-10分),与词向量计算的余弦相似度进行比较。常用的标注数据集:WordSim-353、SimLex-999。

外部评估(Extrinsic Evaluation)

将词向量作为下游NLP任务的输入特征,通过下游任务的表现来间接评估词向量的质量。

评估流程:
1. 训练词向量(方法A和方法B)
2. 用同样的下游任务(如情感分类)
3. 只替换词向量部分,其他完全相同
4. 比较下游任务的指标

指标越好 → 词向量质量越高

哪种更好?

评估方式 优点 缺点
内部评估 快速,不需要下游任务 不一定能反映实际使用效果
外部评估 直接反映实际使用价值 慢,受下游任务设计和训练影响

最佳实践:两种评估结合使用。内部评估用于快速筛选和迭代,外部评估用于最终确认。

自检问题

  1. 为什么"乒乓球拍卖完了"存在歧义切分?请列举至少两种合理的切分方案。
  2. 内部评估和外部评估的区别是什么?各自适合什么场景?

本章总结与自测题

核心概念回顾

概念 一句话总结 关键点
分词 将连续文本切分为词语序列 中文特有难题,歧义是核心挑战
One-Hot 每个词用一个"独热"向量表示 简单但致命缺陷:语义信息为零
Word2Vec 通过上下文学习语义的词向量方法 CBOW和Skip-gram两个方向
CBOW 用上下文预测中心词 快速,适合小数据集
Skip-gram 用中心词预测上下文 慢但效果好,支持罕见词
FastText 引入子词信息的Word2Vec改进 关键优势:可处理OOV词
nn.Embedding PyTorch的可训练词嵌入查找表 深度学习模型的标准输入层
TensorBoard 可视化工具,可观察词嵌入分布 降维后观察语义聚类

文本预处理流程全景图

原始中文文本
    │
    ▼
┌──────────────┐
│ 1. 文本清洗   │  ← 去除HTML标签、特殊符号、乱码等
└──────┬───────┘
       ▼
┌──────────────┐
│ 2. 分词       │  ← jieba: 精确模式/全模式/搜索引擎模式
└──────┬───────┘
       ▼
┌──────────────┐
│ 3. 构建词汇表  │  ← Tokenizer: 统计所有出现过的词,分配唯一ID
└──────┬───────┘
       ▼
┌──────────────┐
│ 4. 词向量化    │  ← 选择: One-Hot / Word2Vec / FastText / nn.Embedding
└──────┬───────┘
       ▼
┌──────────────┐
│ 5. 序列统一化  │  ← Padding/Truncating: 将所有句子统一到相同长度
└──────┬───────┘
       ▼
┌──────────────┐
│ 6. 输入模型    │  ← 形状统一的数字矩阵,可以被神经网络处理
└──────────────┘

本章自测题

基础题

  1. 概念辨析

    • 中文分词和英文分词有什么本质不同?
    • One-Hot编码和Word2Vec词向量有什么区别?
  2. 代码填空:补全以下jieba分词代码:

    import jieba
    s = "自然语言处理很有趣"
    # 精确模式分词
    result = jieba._____(s, cut_all=_____)
    print(result)
    
  3. 工具选择

    • 如果要在搜索引擎中建立索引,jieba的哪种模式最合适?为什么?
    • 如果你的数据集中有大量医学专业术语,应该怎么办?

进阶题

  1. 思路分析:解释为什么Word2Vec训练完后,权重矩阵W的一行就可以作为一个词的"语义向量"?这个向量中的每个数字大致代表什么?

  2. 对比分析:CBOW和Skip-gram在以下场景中,你分别会选择哪一个?

    • (a) 只有10MB的训练文本
    • (b) 文本中大量出现"梯度爆炸"、"反向传播"等专业术语
    • © 想要最快的训练速度
  3. 综合应用题:设计一个完整的中文情感分析预处理流程,从原始微博文本到你准备输入模型的数字矩阵。描述每一步做什么、用什么工具。

思考题

  1. 语义探索:Word2Vec的词向量可以做"vec(国王) - vec(男人) + vec(女人) ≈ vec(王后)"这样的语义运算。你认为这种"可加减性"是词向量的必然属性,还是仅仅是一个漂亮的巧合?为什么?

  2. 未来思考:近年来的大语言模型(如GPT、BERT)大多不再使用独立的Word2Vec来初始化词向量,而直接使用分词后的子词嵌入。你认为为什么?(提示:想想Word2Vec的局限性)


学习建议

  1. 动手运行本章所有代码示例,不要只读不看效果
  2. 尝试用你自己的一段文本(如一条朋友圈、一篇日记)走完整个预处理流程
  3. 在TensorBoard中观察词向量的分布,尝试解释你看到的聚类现象
  4. 本章的Word2Vec思想是理解现代NLP的基础——即使今天模型更复杂,核心的"语义由上下文定义"思想始终如一
  5. 下一章(文本预处理-下)将学习如何对预处理后的数据进行探索分析和特征工程
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐