- 示例工程
【免费下载链接】tfjs-examples
Examples built with TensorFlow.js
本指南围绕 tfjs-examples 仓库中的tsne-mnist-canvas示例,讲解如何借助 TensorFlow.js 生态下的 tfjs-tsne 库,在浏览器中完成 MNIST 手写数字数据集的 t-SNE 降维与实时可视化。读完本文,你将掌握 MNIST 数据的加载与张量预处理管线、tfjs-tsne 的 kNN 与迭代式嵌入 API、基于 d3 与 Canvas 2D 的坐标渲染方案,以及 WebGL 2 环境要求与关键参数的调优方向。
示例概览:在浏览器里跑通完整的 t-SNE 流程
正如 README.md 所描述的,本示例演示了使用tfjs-tsne库对 MNIST 数据集 做降维:从测试集中取出10000 张手写数字图片,将其缩放为10×10 像素,再用t-SNE 算法把它们组织到二维平面上,让相同数字在空间上聚成簇、不同数字彼此分离,从而直观地检验高维图像数据在低维空间中的结构。
演示页面的标题正是“GPU Accelerated T-SNE in the Browser”,整个计算过程依托 TensorFlow.js 与 WebGL 在 GPU 上完成,这也是本示例区别于传统 CPU 版 t-SNE 工具的核心亮点。
该示例的仓库结构十分精简,只有 6 个文件:
tsne-mnist-canvas/ ├── README.md # 示例说明与运行命令 ├── index.html # 页面结构、参数控件与样式 ├── index.js # 主逻辑:数据预处理、t-SNE 调用、渲染 ├── data.js # MnistData 类:MNIST 数据加载与切分 ├── package.json # 依赖与构建脚本 └── yarn.lock # 依赖锁定文件运行环境前提:必须支持 WebGL 2
README 给出了一个重要但容易被忽略的环境限制:
tfjs-tsne requires WebGL 2 support and thus will not work on certain devices, mobile devices especially. Currently it best works on desktop devices.
也就是说,tfjs-tsne依赖WebGL 2支持,因此:
- 移动设备上很可能无法运行,尤其是部分只支持 WebGL 1 的手机浏览器;
- 桌面端浏览器体验最佳,建议在较新的 Chrome / Firefox / Edge 等支持 WebGL 2 的桌面浏览器中运行。
从实现上可以推断,该限制源于 tfjs-tsne 将 kNN 近邻图构建与 t-SNE 优化等重计算全部放在 GPU 着色器(shader)中执行,以获得实时的迭代可视化体验;而 WebGL 2 提供了所需的更灵活的浮点纹理与计算能力。因此,如果运行页面时出现空白或报错,请先确认浏览器支持 WebGL 2。
快速运行:两条命令启动
README 给出的启动方式只有两条命令:
yarn yarn watchyarn安装依赖(依赖清单见 package.json:@tensorflow/tfjs@^1.3.2、@tensorflow/tfjs-tsne@0.2.0、d3@^5.2.0);yarn watch启动开发服务器,并在浏览器中自动打开页面(官方同时在 GCS 上托管了在线演示版本,可直接访问体验)。
package.json中完整定义了三个脚本,可满足开发与构建两种场景:
"scripts": { "watch": "NODE_ENV=development parcel index.html --no-hmr --open", "build": "NODE_ENV=production parcel build --no-scope-hoist index.html --public-url ./", "link-local": "yalc link" }yarn watch:基于Parcel的本地开发模式,--no-hmr关闭热模块替换(避免开发中频繁整页刷新),--open自动打开浏览器;yarn build:产出生产构建,--public-url ./保证资源使用相对路径,方便部署到任意静态目录;yarn link-local:配合 devDependencies 中的yalc,用于本地链接尚未发布的 tfjs-tsne 版本,属于库作者联调场景。
数据准备:从 MNIST 精灵图加载训练与测试集
数据层由 data.js 中的MnistData类负责,它直接从云存储加载两份 MNIST 原始资源:一张把全部 65000 张图片纵向拼接的精灵图(sprite),以及一份 uint8 格式的标签文件。
关键常量(见 data.js):
const IMAGE_SIZE = 784; // 28×28 = 784 像素 const NUM_CLASSES = 10; // 0~9 十个数字类别 const NUM_DATASET_ELEMENTS = 65000; // 全量样本数 const NUM_TRAIN_ELEMENTS = 55000; // 训练集 const NUM_TEST_ELEMENTS = 10000; // 测试集load()方法的核心加载流程值得注意:
- 用
Image对象加载精灵图,并设置crossOrigin = ''以跨域读取像素; - 以
chunkSize = 5000为步长分块绘制到临时 canvas 上,再通过getImageData取出像素——由于 MNIST 是灰度图,只需读取红色通道并除以 255 完成归一化(见 data.js); - 并行
fetch标签文件,解析为Uint8Array; - 用
tf.util.createShuffledIndices生成乱序索引,并将图像与标签按 55000 / 10000 切分为trainImages、testImages、trainLabels、testLabels(见 data.js)。
本示例只用到测试集,因此在 index.js 中仅需一行:
async function loadData() { data = new MnistData(); return data.load(); }预处理张量管线:28×28 图像降维到 10×10 特征
这是整个示例中最有工程价值的一段:在送入 t-SNE 之前,把高维输入压缩到可控维度。start函数(见 index.js)在tf.tidy作用域内完成全部张量操作:
const [reshaped, labels] = tf.tidy(() => { // 1. 标签:one-hot 转成数字数组 const labelsTensor = tf.tensor2d(data.testLabels, [NUM_IMAGES, NUM_CLASSES]); const labels = labelsTensor.argMax(1).dataSync(); // 2. 图像:构建 [10000, 28, 28, 1] 张量并截取前 numPoints 张 const images = tf.tensor4d(data.testImages, [ NUM_IMAGES, IMG_HEIGHT, IMG_WIDTH, 1 ]).slice([0], [numPoints]); // 3. 缩放:双线性插值到 10×10 const resized = images.resizeBilinear([NEW_HEIGHT, NEW_WIDTH]); // 4. 重塑:摊平为 tfjs-tsne 期望的 rank-2 输入 const reshaped = resized.reshape([numPoints, NEW_HEIGHT * NEW_WIDTH]); return [reshaped, labels]; });各步骤的作用:
- 标签转换:MNIST 标签是 one-hot 编码的
[10000, 10]张量,用argMax(1)取每行最大值下标,得到[10000]的数字标签数组,供后续按类别着色; - 切片:
slice([0], [numPoints])从测试集中取用户指定的样本数(默认 10000); - 双线性缩放:
resizeBilinear([10, 10])把每张 28×28 图像缩小到 10×10,将特征维度从 784 降到100,这既大幅降低了 t-SNE 的计算量,又不至于损失过多的数字结构信息; - 重塑:
reshape([numPoints, 100])得到 tfjs-tsne 要求的 rank-2 输入张量[样本数, 特征数]。
最后,tf.tidy自动回收中间张量,函数末尾还显式调用reshaped.dispose()释放输入张量,避免 GPU 显存泄漏——在动辄 10000 行样本的降维任务里,内存管理是保持页面流畅的必要细节。
t-SNE 计算流程:tfjs-tsne 的核心调用链
doEmbedding函数(见 index.js)完整展示了 tfjs-tsne 的异步 API 用法,整个流程分为两阶段:
async function doEmbedding( data, labels, numIterations, knnIter, perplexity, onIteration) { // 阶段一:创建 embedder 并预计算 kNN 近邻图 const embedder = tsne.tsne(data); await embedder.iterateKnn(knnIter); // 阶段二:逐轮迭代优化,并在每轮之间渲染当前坐标 for (let i = 0; i < numIterations; i++) { await embedder.iterate(1); const coordinates = await embedder.coordsArray(); onIteration(coordinates, labels); await tf.nextFrame(); } }逐行拆解:
tsne.tsne(data):传入[样本数, 100]的张量,创建一个 GPU 加速的 t-SNE embedder 实例;await embedder.iterateKnn(knnIter):正式优化前先执行若干轮 kNN(k 近邻)迭代,构建样本之间的近邻关系图,这是 t-SNE 计算相似度矩阵的基础;await embedder.iterate(1):执行一轮 t-SNE 梯度优化迭代。示例刻意每轮只迭代 1 次,代码注释明确说明“你也可以在两次取坐标渲染之间多迭代几次”——这样可以在迭代尚未收敛时就展示中间状态,形成渐进式动画;await embedder.coordsArray():从 GPU 取回当前所有样本的二维坐标数组,交给渲染回调;await tf.nextFrame():把控制权交还给浏览器渲染线程,避免长循环阻塞页面,是浏览器端异步计算中保持 UI 流畅的关键一步。
实时渲染:d3 与 Canvas 2D 画出手写数字散点
renderEmbedding(见 index.js)负责把坐标绘制到 800×800 的 canvas 上,采用d3 缩放 + Canvas 2D 逐点绘制的组合:
function renderEmbedding(coordinates, labels) { const x = d3.scaleLinear().range([0, width]).domain([0, 1]); const y = d3.scaleLinear().range([0, height]).domain([0, 1]); context.clearRect(0, 0, width, height); coordinates.forEach(function(d, i) { context.font = '10px sans'; context.fillStyle = colors(parseInt(labels[i], 10)); context.fillText(labels[i], x(d[0]), y(d[1])); }); }渲染细节:
- 坐标映射:
d3.scaleLinear把坐标从[0, 1]区间线性映射到画布像素范围——从 domain 取值可以推断,coordsArray()返回的坐标被归一化在[0, 1]内; - 逐点着色:
colors = d3.scaleOrdinal(d3.schemeCategory10)(见 index.js)为 10 个数字类别各分配一种颜色; - 直接绘制数字字符:每个样本用
fillText绘制它自身的标签数字(0~9),因此不依赖额外的点/图片资源,几万点也能在 10px 字号下保持清晰的散点分布; - 逐帧重绘:每轮 t-SNE 迭代都会
clearRect后重新绘制全部坐标,可视化出“点云逐步聚成 10 团”的动态过程。
canvas 本身由initVisualization在#vis容器内动态创建并获取 2d context(见 index.js),样式上通过 1px 黑色边框与页面布局区分可视化区域。
交互控件与参数调优
index.html 提供了一组滑块与单选按钮,initControls(见 index.js)在数据加载完成后将其启用。点击Start T-SNE按钮后读取全部参数并调用start(numPoints, tsneIter, knnIter, perplexity),按钮文字随之变为Restart TSNE,可反复调整参数重跑。
全部可调参数及默认值如下:
| 控件 | 含义 | 取值范围 | 默认值 |
|---|---|---|---|
| Perplexity | t-SNE 的核心超参数,控制每个点周围“有效近邻”的数量,用于平衡局部与全局结构 | 2 ~ 42,步长 1 | 30 |
| KNN Iterations | 正式优化前构建 kNN 近邻图的迭代轮数,决定相似度矩阵的质量 | 2 ~ 1500,步长 1 | 800 |
| T-SNE Iterations | 进入 t-SNE 优化阶段后的总迭代轮数,决定嵌入结果的收敛程度 | 1 ~ 1000,步长 1 | 500 |
| Number of Digits | 实际参与降维的样本数(对应numPoints参数) | 500 / 2000 / 10000(单选) | 10000 |
调参方向可以这样把握(以下为 t-SNE 通用经验,可作为调试参考):
- Perplexity:从算法原理看,值越大越侧重保留全局结构,值越小越侧重局部结构;对 500 个样本的小数据集,过大的 perplexity 会引入偏差,通常可相应调低;
- KNN Iterations:迭代越多,近邻图越准确,但耗时线性增长。若聚类结果明显杂乱,可优先增大该项;
- T-SNE Iterations:迭代越多收敛越好,但每轮都要在 GPU 上更新并回读坐标。本示例每轮只
iterate(1)并立即渲染,因此总迭代数也直接决定动画时长; - 样本数:10000 点最能展现 MNIST 的完整结构,但计算与渲染开销最大;调试阶段可用 500 或 2000 快速验证参数效果。
另外,控件区域的所有input[type=range]滑块都会在滑动时实时把当前值回显到其前的<span>标签中(见 index.js),方便观察所选数值。
小结:一条完整的浏览器端降维链路
tsne-mnist-canvas用约百行核心代码,串起了从数据加载、张量预处理、GPU 加速 t-SNE 到 Canvas 实时渲染的完整链路,是理解 tfjs-tsne 用法的理想起点。回顾本文要点:
- 环境:tfjs-tsne 依赖 WebGL 2,移动端兼容性有限,桌面浏览器体验最佳;
- 数据:data.js 展示了从精灵图异步加载 MNIST 并进行 train/test 切分的完整写法;
- 预处理:index.js 通过
slice → resizeBilinear → reshape把 28×28 图像压缩为 10×10(100 维)特征,并借助tf.tidy管理 GPU 内存; - 计算:
tsne.tsne → iterateKnn → iterate(1) → coordsArray → tf.nextFrame两阶段调用链兼顾了收敛质量与 UI 流畅度; - 渲染:d3 比例尺 + Canvas 逐点
fillText的方案支持数万点的实时动画。
如需深入,可继续阅读仓库中 data.js、index.js 与 index.html 的完整实现,并对照 package.json 了解依赖版本与构建脚本,即可在此基础上替换为自己的数据集,搭建浏览器端的通用高维数据探索工具。
- 示例工程
【免费下载链接】tfjs-examples
Examples built with TensorFlow.js
相关推荐
3步并行开发:Prime Agent Subagent 子智能体实战指南
3步并行开发:Prime Agent Subagent 子智能体实战指南 认证模块要改、测试要补、审查任务还堆着,主智能体只能一件件串行干。Prime Agen
人工智能AI Agent代码智能体工具调用交互助手MCP ClientsAgent 记忆2024年最受欢迎的流媒体代理工具:Threadfin核心功能全解析
2024年最受欢迎的流媒体代理工具:Threadfin核心功能全解析 Threadfin 是一款基于 xTeVe 开发的 M3U 代理工具,专为 Kernel、
后端音视频Plotly 可视化 t-SNE 与 UMAP 降维投影:从高维数据到 2D/3D 散点图实战(基于 plotly.py)
Plotly 可视化 t SNE 与 UMAP 降维投影:从高维数据到 2D/3D 散点图实战(基于 plotly.py) 本篇技术指南以 doc/python
数据可视化数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考