☰
基于 tfjs-tsne 的浏览器端 GPU 加速 t-SNE 实战:MNIST 手写数字二维降维可视化
2026/10/15 2:05:24 网站建设 项目流程
  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

项目地址:https://gitcode.com/gh_mirrors/tf/tfjs-examples
点击查看免费下载

本指南围绕 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 watch
  • yarn安装依赖(依赖清单见 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()方法的核心加载流程值得注意:

  1. 用Image对象加载精灵图,并设置crossOrigin = ''以跨域读取像素;
  2. 以chunkSize = 5000为步长分块绘制到临时 canvas 上,再通过getImageData取出像素——由于 MNIST 是灰度图,只需读取红色通道并除以 255 完成归一化(见 data.js);
  3. 并行fetch标签文件,解析为Uint8Array;
  4. 用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,可反复调整参数重跑。

全部可调参数及默认值如下:

控件含义取值范围默认值
Perplexityt-SNE 的核心超参数,控制每个点周围“有效近邻”的数量,用于平衡局部与全局结构2 ~ 42,步长 130
KNN Iterations正式优化前构建 kNN 近邻图的迭代轮数,决定相似度矩阵的质量2 ~ 1500,步长 1800
T-SNE Iterations进入 t-SNE 优化阶段后的总迭代轮数,决定嵌入结果的收敛程度1 ~ 1000,步长 1500
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 用法的理想起点。回顾本文要点:

  1. 环境:tfjs-tsne 依赖 WebGL 2,移动端兼容性有限,桌面浏览器体验最佳;
  2. 数据:data.js 展示了从精灵图异步加载 MNIST 并进行 train/test 切分的完整写法;
  3. 预处理:index.js 通过slice → resizeBilinear → reshape把 28×28 图像压缩为 10×10(100 维)特征,并借助tf.tidy管理 GPU 内存;
  4. 计算:tsne.tsne → iterateKnn → iterate(1) → coordsArray → tf.nextFrame两阶段调用链兼顾了收敛质量与 UI 流畅度;
  5. 渲染:d3 比例尺 + Canvas 逐点fillText的方案支持数万点的实时动画。

如需深入,可继续阅读仓库中 data.js、index.js 与 index.html 的完整实现,并对照 package.json 了解依赖版本与构建脚本,即可在此基础上替换为自己的数据集,搭建浏览器端的通用高维数据探索工具。

  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

项目地址:https://gitcode.com/gh_mirrors/tf/tfjs-examples
点击查看免费下载

相关推荐

上一篇:AlphaFold 3:革命性蛋白质结构预测的7大关键技术突破与实战指南
下一篇:gh_mirrors/de/developer组件设计:构建可复用UI

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询