零基础吃透 RaggedTensor 文本特征提取示例(通俗版)
这份内容会从零开始拆解原示例,先讲核心思想,再逐步解释「要做什么、用什么API、API背后的原理」,全程用大白话,避免专业术语堆砌,确保你能理解每一步的目的和底层逻辑。
一、先搞懂:这个示例的核心思想(为什么要做这件事?)
1. 通俗目标
给长度不一样的句子(比如"Who is Dan Smith"4个词、"Pause"1个词、"Will it rain later today"5个词)做「特征提取」:
- 既要保留单个词的语义(比如"rain"代表下雨);
- 也要保留相邻词的搭配语义(比如"Will it"是疑问搭配);
- 最后把每个句子浓缩成一个固定长度的向量(4维),方便后续做文本分类、搜索、问答等任务。
2. 为什么用 RaggedTensor?
如果用普通张量处理长度不同的句子,必须把所有句子补0到最长长度(比如补到5个词),会产生冗余数据;而 RaggedTensor 能原生支持可变长度的列表,不用补0,直接处理,既省内存又保证计算准确。
3. 前置基础概念(先记牢,后续不懵)
| 概念 | 通俗解释 |
|---|---|
| 词嵌入(Embedding) | 把"单词"(字符串)变成"数字向量"(比如4维),让电脑能理解单词的语义(比如"rain"→[0.1, -0.2, 0.3, 0.4]) |
| 哈希桶(Hash Bucket) | 字符串不能直接查嵌入表,先通过"哈希算法"把字符串转成0~1023的整数(相当于给每个单词发一个编号) |
| 一元词嵌入 | 单个单词的嵌入向量(比如"Who"的4维向量) |
| 二元词嵌入 | 相邻两个单词拼接后的嵌入向量(比如"Who+is"的4维向量,捕捉词之间的搭配) |
| RaggedTensor | 专门存"长度不一样的嵌套列表"的张量(比如[[4词],[1词],[5词]]),TF原生支持,不用补0 |
二、完整示例(带逐行注释+原理)
步骤0:环境准备(先导入要用的工具)
python
# 导入TensorFlow(核心工具)
import tensorflow as tf
# 导入数学库(用于嵌入表初始化,计算标准差)
import math
# 打印TF版本,确保能正常运行(可选)
print(f"TensorFlow版本:{tf.__version__}")
步骤1:定义输入(可变长度的句子)
要做什么?
准备3个长度不同的句子,用 RaggedTensor 存储(核心:保留原始长度,不补0)。
用什么API?
tf.ragged.constant():把嵌套列表转成 RaggedTensor。
API原理?
自动识别嵌套列表的"行长度",生成"不规则张量",只记录有效元素,不存冗余的填充值。
代码+注释
python
# 定义3个长度不同的句子(4词、1词、5词)
queries = tf.ragged.constant([
['Who', 'is', 'Dan', 'Smith'], # 句子1:4个词
['Pause'], # 句子2:1个词
['Will', 'it', 'rain', 'later', 'today'] # 句子3:5个词
])
# 查看RaggedTensor的基本信息(验证是否正确)
print("输入的RaggedTensor:")
print(queries)
print("句子数量(行数):", queries.nrows()) # 输出3,对应3个句子
print("每个句子的长度:", queries.row_lengths()) # 输出[4,1,5],对应每个句子的词数
运行结果
输入的RaggedTensor:
<tf.RaggedTensor [[b'Who', b'is', b'Dan', b'Smith'], [b'Pause'], [b'Will', b'it', b'rain', b'later', b'today']]>
句子数量(行数): 3
每个句子的长度: tf.Tensor([4 1 5], shape=(3,), dtype=int64)
- 注意:字符串前的
b是TF默认把字符串转成字节型,不影响使用。
步骤2:创建嵌入表(单词→向量的映射表)
要做什么?
创建一个"字典":把每个"单词编号"映射到一个4维向量(让电脑能理解单词的语义)。
用什么API?
tf.Variable():创建可训练的变量(嵌入表后续可通过训练优化);tf.random.truncated_normal():生成"截断正态分布"的随机数(初始化嵌入表的值)。
API原理?
- 嵌入表形状是
[哈希桶数, 嵌入维度](这里[1024,4]):1024个"单词编号",每个编号对应1个4维向量; - 截断正态分布:避免生成过大的随机数,让初始值更稳定(
stddev=1/√嵌入维度是行业通用的初始化技巧)。
代码+注释
python
# 1. 定义超参数(可调整)
num_buckets = 1024 # 哈希桶数量:把单词映射到0~1023的整数(足够容纳常用词)
embedding_size = 4 # 嵌入向量维度:每个单词转成4维向量(维度越小越省内存,越大能表达的语义越丰富)
# 2. 创建嵌入表(可训练的变量)
embedding_table = tf.Variable(
# 生成随机数:形状[1024,4],截断正态分布,标准差=1/√4=0.5
tf.random.truncated_normal(
shape=[num_buckets, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)
)
)
# 查看嵌入表形状(验证是否正确)
print("\n嵌入表形状:", embedding_table.shape) # 输出(1024, 4)
步骤3:计算一元词嵌入(单个单词的向量)
要做什么?
把句子里的每个单词→转成编号→查嵌入表→得到单个单词的4维向量。
用什么API?
tf.strings.to_hash_bucket_fast():字符串→单词编号(哈希算法,速度快);tf.nn.embedding_lookup():根据单词编号查嵌入表,得到向量。
API原理?
- 哈希映射:不管输入什么字符串,都能快速转成0~1023的整数(解决"字符串无法直接计算"的问题);
- 嵌入查找:根据编号从嵌入表中"查字典",比如编号123→嵌入表第123行的4维向量。
代码+注释
python
# 1. 单词→编号(哈希映射)
word_buckets = tf.strings.to_hash_bucket_fast(queries, num_buckets)
print("\n单词对应的编号(RaggedTensor):")
print(word_buckets) # 形状和queries一致:[3, (4/1/5)]
# 2. 编号→向量(查嵌入表)
word_embeddings = tf.nn.embedding_lookup(embedding_table, word_buckets)
print("\n一元词嵌入的形状:", word_embeddings.shape) # 输出(3, None, 4):3个句子,每个句子N个词,每个词4维
# 解释:None代表可变长度(4/1/5),TF用None表示RaggedTensor的可变维度
关键说明
word_buckets形状和queries完全一致:句子1有4个编号,句子2有1个,句子3有5个;word_embeddings形状是[3, None, 4]:None对应可变的词数,每个词都是4维向量。
步骤4:给句子加首尾标记(为构造二元词做准备)
要做什么?
给每个句子的开头和结尾加#(比如"Pause"→"# Pause #"),确保能捕捉到"句首+第一个词""最后一个词+句尾"的搭配。
用什么API?
tf.fill():生成指定形状、填充固定值的张量;tf.concat():拼接张量(这里横向拼接:标记+句子+标记)。
API原理?
tf.fill([queries.nrows(), 1], '#'):生成[3,1]的张量(3个句子,每个句子1个#);tf.concat(axis=1):横向拼接(按"词"的维度拼接),RaggedTensor原生支持可变长度的拼接(普通张量做不到)。
代码+注释
python
# 1. 生成首尾标记:每个句子1个#,形状[3,1]
marker = tf.fill([queries.nrows(), 1], '#')
print("\n标记张量形状:", marker.shape) # 输出(3, 1)
# 2. 拼接:开头标记 + 原句子 + 结尾标记
padded = tf.concat([marker, queries, marker], axis=1)
print("\n加首尾标记后的句子:")
print(padded)
print("每个句子的长度:", padded.row_lengths()) # 输出[6,3,7](原长度+2)
运行结果
加首尾标记后的句子:
<tf.RaggedTensor [[b'#', b'Who', b'is', b'Dan', b'Smith', b'#'], [b'#', b'Pause', b'#'], [b'#', b'Will', b'it', b'rain', b'later', b'today', b'#']]>
每个句子的长度: tf.Tensor([6 3 7], shape=(3,), dtype=int64)
- 句子1长度从4→6(加了2个
#),句子2从1→3,句子3从5→7,符合预期。
步骤5:构造二元词(相邻词对)
要做什么?
把加标记后的句子里的"相邻两个词"拼接成一个词对(比如"#+Who""Who+is"),捕捉词之间的搭配语义。
用什么API?
- 切片
[:, :-1]和[:, 1:]:分别取"去掉最后一个词"和"去掉第一个词"的句子; tf.strings.join():把相邻的两个词拼接成一个字符串(分隔符+)。
API原理?
- 切片逻辑:比如句子1
[#, Who, is, Dan, Smith, #]→[:, :-1]是[#, Who, is, Dan, Smith],[:, 1:]是[Who, is, Dan, Smith, #]; - 拼接逻辑:把两个切片的对应位置拼接 →
#+Who、Who+is、is+Dan、Dan+Smith、Smith+#(共5个二元词)。
代码+注释
python
# 1. 取相邻词的切片
padded_left = padded[:, :-1] # 去掉每行最后一个元素
padded_right = padded[:, 1:] # 去掉每行第一个元素
# 2. 拼接成二元词(分隔符+)
bigrams = tf.strings.join([padded_left, padded_right], separator='+')
# 查看二元词(验证是否正确)
print("\n二元词:")
print(bigrams)
print("每个句子的二元词数量:", bigrams.row_lengths()) # 输出[5,2,6](加标记后的长度-1)
运行结果
二元词:
<tf.RaggedTensor [[b'#+Who', b'Who+is', b'is+Dan', b'Dan+Smith', b'Smith+#'], [b'#+Pause', b'Pause+#'], [b'#+Will', b'Will+it', b'it+rain', b'rain+later', b'later+today', b'today+#']]>
每个句子的二元词数量: tf.Tensor([5 2 6], shape=(3,), dtype=int64)
步骤6:计算二元词嵌入(词对的向量)
要做什么?
和"一元词嵌入"逻辑完全一致:二元词→转编号→查嵌入表→得到词对的4维向量。
用什么API?
和步骤3相同:tf.strings.to_hash_bucket_fast() + tf.nn.embedding_lookup()。
API原理?
二元词本质还是字符串,同样通过哈希转编号,再查嵌入表(嵌入表不区分"一元词"和"二元词",只认编号)。
代码+注释
python
# 1. 二元词→编号
bigram_buckets = tf.strings.to_hash_bucket_fast(bigrams, num_buckets)
# 2. 编号→向量
bigram_embeddings = tf.nn.embedding_lookup(embedding_table, bigram_buckets)
# 查看二元词嵌入形状
print("\n二元词嵌入形状:", bigram_embeddings.shape) # 输出(3, None, 4):3个句子,每个句子M个二元词,每个词对4维
步骤7:合并一元+二元嵌入(融合两种语义)
要做什么?
把每个句子的"一元词向量"和"二元词向量"拼在一起(比如句子1:4个一元词 + 5个二元词 = 9个向量)。
用什么API?
tf.concat(axis=1):横向拼接(按"词/词对"的维度拼接)。
API原理?
RaggedTensor的concat(axis=1)会自动匹配"行",只拼接同一行的元素,不会打乱句子的对应关系(普通张量需要补0才能拼接)。
代码+注释
python
# 合并一元+二元嵌入(axis=1:横向拼接)
all_embeddings = tf.concat([word_embeddings, bigram_embeddings], axis=1)
# 查看合并后的形状
print("\n合并后的嵌入形状:", all_embeddings.shape) # 输出(3, None, 4)
print("每个句子的总向量数:", all_embeddings.row_lengths()) # 输出[9,3,11](4+5, 1+2,5+6)
步骤8:计算每个句子的平均嵌入(浓缩成固定长度向量)
要做什么?
把每个句子的所有向量(一元+二元)求平均值→每个句子得到1个4维向量(固定长度,方便后续使用)。
用什么API?
tf.reduce_mean(axis=1):对每个句子的向量求均值(axis=1:按"词/词对"的维度求平均)。
API原理?
- 对可变长度的行求均值:只计算该行的有效元素(比如句子2的3个向量求平均),不会被填充值干扰;
- 结果是普通张量(不再是RaggedTensor):形状
[3,4],3个句子各1个4维向量(固定长度,可直接用于分类、搜索等任务)。
代码+注释
python
# 对每个句子的所有向量求均值
avg_embedding = tf.reduce_mean(all_embeddings, axis=1)
# 输出最终结果
print("\n每个句子的平均嵌入向量(最终结果):")
print(avg_embedding)
print("最终结果形状:", avg_embedding.shape) # 输出(3, 4)(普通张量,固定长度)
运行结果(和原示例一致,数值因随机初始化略有不同)
每个句子的平均嵌入向量(最终结果):
tf.Tensor(
[[ 0.02510674 -0.04737939 0.01799836 0.1717977 ]
[ 0.4977201 0.38424173 -0.40286708 -0.39982286]
[ 0.16051094 0.2062596 -0.05239218 0.10338864]], shape=(3, 4), dtype=float32)
最终结果形状: (3, 4)
三、核心总结(API+原理速查表)
| 步骤 | 核心API | API核心原理 |
|---|---|---|
| 定义输入 | tf.ragged.constant() | 把嵌套列表转成RaggedTensor,保留可变长度,不补0 |
| 创建嵌入表 | tf.Variable() + tf.random.truncated_normal() | 创建可训练的嵌入表,用截断正态分布初始化,避免值过大 |
| 单词→编号 | tf.strings.to_hash_bucket_fast() | 哈希算法把字符串转成0~num_buckets-1的整数,解决字符串无法计算的问题 |
| 编号→向量 | tf.nn.embedding_lookup() | 根据编号查嵌入表,得到单词/词对的向量 |
| 拼接张量 | tf.concat(axis=1) | 横向拼接RaggedTensor,自动匹配行,无需补0 |
| 构造二元词 | tf.strings.join() + 切片 | 切片取相邻词,拼接成词对,捕捉上下文语义 |
| 求均值 | tf.reduce_mean(axis=1) | 对每个句子的向量求均值,浓缩成固定长度向量,结果为普通张量 |
四、关键收获
- RaggedTensor的核心价值:处理可变长度数据,无需补0,保留原始结构;
- 词嵌入的核心逻辑:字符串→编号→向量(让电脑能理解文本语义);
- 二元词的作用:捕捉词之间的搭配关系(比单个词更能表达上下文);
- 最终目标:把长度不同的句子浓缩成固定长度的向量(方便后续机器学习任务)。
如果还有某个API或原理没懂,比如"哈希桶为什么是1024""嵌入维度为什么选4",可以告诉我,我再针对性解释。