UniTouch实战5分钟构建你的触觉-视觉跨模态分类器最近在实验室里折腾机器人抓取项目我们遇到了一个挺有意思的难题想让机器人通过“触摸”来识别不同材质的物体。传统的视觉模型在光线变化、遮挡面前常常“抓瞎”而触觉传感器采集的数据又五花八门格式不一标注成本高得吓人。就在我们纠结要不要花几个月时间从头收集数据、训练模型时耶鲁和密歇根大学联合发布的UniTouch模型进入了视野。这个模型最吸引人的地方在于它声称能直接利用预训练好的视觉-语言模型的知识让触觉数据“蹭”上视觉的便车实现零样本的跨模态分类。听起来很美好但实际用起来会不会又是一堆坑抱着试试看的心态我花了一个下午的时间把官方代码跑了一遍并针对我们自己的触觉传感器数据做了适配。结果出乎意料——只用了几行代码一个能分辨布料、金属、木材、塑料的触觉分类器就搭起来了而且完全不需要我们准备任何标注数据。整个过程的核心其实就是理解UniTouch如何将不同传感器的触觉数据“翻译”成视觉模型能懂的语言然后利用CLIP这类强大模型的零样本能力。这篇文章我就把自己趟过的路、踩过的坑以及最终跑通的完整流程毫无保留地分享给你。1. 环境准备与模型初探在开始敲代码之前我们得先搞清楚UniTouch到底做了什么。简单来说它解决了一个核心矛盾触觉数据难获取、难标注但视觉和语言数据却海量且已有强大的预训练模型如CLIP。UniTouch的聪明之处在于它没有试图从头训练一个触觉模型而是设计了一个巧妙的“对齐”过程。它训练了一个专门的触觉编码器Tactile Encoder这个编码器的目标是让它输出的触觉特征向量和CLIP模型中的图像特征向量在同一个语义空间里“挨得很近”。你可以把它想象成学习一门新的方言触觉编码器学习用CLIP能理解的“视觉语言”来描述触觉感受。一旦这个对齐完成了所有CLIP模型能做的事情——比如根据文本描述进行零样本分类——触觉数据就也都能做了。1.1 快速搭建PyTorch环境为了确保复现过程顺利我强烈建议使用Conda来管理环境避免包版本冲突。下面是我使用的环境配置它平衡了稳定性和对新硬件的支持。# 创建并激活一个新的conda环境 conda create -n unitouch python3.9 -y conda activate unitouch # 安装PyTorch请根据你的CUDA版本选择这里以CUDA 11.8为例 pip install torch2.0.1 torchvision0.15.2 torchaudio2.0.2 --index-url https://download.pytorch.org/whl/cu118 # 安装其他核心依赖 pip install transformers4.35.0 pip install opencv-python-headless pillow pip install matplotlib tqdm pip install scikit-learn # 用于后续的评估如果你的GPU内存比较紧张比如只有8GB或更少在安装transformers时可以加上[accelerate]选项来启用一些内存优化技术pip install transformers[accelerate]4.35.01.2 理解UniTouch的核心组件在下载代码和模型之前我们先看看项目结构。从官方仓库克隆下来的代码核心部分通常包含以下几个模块models/: 存放触觉编码器tactile_encoder.py和传感器特定标记sensor_tokens.py的定义。configs/: 模型配置和训练参数。datasets/: 数据加载和处理逻辑通常会支持多个公开的触觉数据集。utils/: 工具函数包括特征对齐、零样本分类的脚本。对于我们这种只想快速用起来的开发者来说最需要关注的是触觉编码器和预训练权重。编码器通常基于Vision Transformer (ViT)架构但输入的是触觉图像一种特殊的、由触觉传感器生成的“压力分布图”或“纹理图”。模型权重文件一个.pth或.pt文件包含了编码器对齐后的参数。注意由于网络访问问题直接从Hugging Face或官方链接下载模型权重有时会比较慢。一个实用的技巧是先尝试用命令行工具如wget或curl直接下载如果失败可以尝试在开源社区如GitHub的Release页面、国内镜像站寻找搬运的权重文件。务必核对文件的MD5或SHA256校验和确保文件完整无误。2. 数据预处理将你的触觉数据“喂”给模型这是实战中最关键、也最容易出错的一步。UniTouch在训练时使用了多种基于视觉的触觉传感器数据比如GelSight、TacTip等。这些传感器输出的虽然是“图像”但和普通的RGB图像在像素值分布、通道意义上可能有很大不同。我们的目标是把任意来源的触觉数据处理成模型期望的格式。2.1 触觉数据的常见格式与转换触觉数据来源多样可能是单通道的压力值矩阵、三通道的彩色形变图甚至是多通道的深度信息。UniTouch的预训练模型通常期望输入是三通道、尺寸固定的图像例如224x224。下面是一个通用的预处理函数你可以根据自己的数据格式进行修改import cv2 import numpy as np from PIL import Image import torch from torchvision import transforms def preprocess_tactile_image(raw_data, sensor_typecustom): 将原始触觉数据转换为模型可用的张量。 Args: raw_data: numpy数组原始触觉数据。形状可能是 (H, W), (H, W, 1), (H, W, 3) 等。 sensor_type: 传感器类型用于选择特定的归一化策略。 Returns: torch.Tensor: 形状为 (1, 3, 224, 224) 的预处理后张量。 # 1. 确保数据是numpy数组并处理维度 if isinstance(raw_data, Image.Image): raw_data np.array(raw_data) elif torch.is_tensor(raw_data): raw_data raw_data.numpy() # 2. 通道处理目标是变成3通道 if len(raw_data.shape) 2: # 单通道 (H, W) # 假设是压力或深度图归一化到[0, 255]并复制为三通道 # 这里使用min-max归一化你也可以用其他方法如根据传感器量程 data_min, data_max raw_data.min(), raw_data.max() if data_max - data_min 1e-6: normalized ((raw_data - data_min) / (data_max - data_min) * 255).astype(np.uint8) else: normalized np.zeros_like(raw_data, dtypenp.uint8) tactile_img np.stack([normalized]*3, axis-1) # (H, W, 3) elif len(raw_data.shape) 3 and raw_data.shape[2] 1: # (H, W, 1) channel raw_data[:, :, 0] # 同样的归一化逻辑 data_min, data_max channel.min(), channel.max() if data_max - data_min 1e-6: normalized ((channel - data_min) / (data_max - data_min) * 255).astype(np.uint8) else: normalized np.zeros_like(channel, dtypenp.uint8) tactile_img np.stack([normalized]*3, axis-1) elif len(raw_data.shape) 3 and raw_data.shape[2] 3: # (H, W, 3) 例如RGB触觉图 # 假设已经是8-bit图像直接使用。如果不是可能需要先转换。 if raw_data.dtype ! np.uint8: # 非uint8需要根据实际情况归一化这里假设值域在[0,1]或[0,255] if raw_data.max() 1.0: tactile_img (raw_data * 255).astype(np.uint8) else: tactile_img raw_data.astype(np.uint8) else: tactile_img raw_data else: raise ValueError(f不支持的输入形状: {raw_data.shape}。期望 (H,W), (H,W,1), 或 (H,W,3)。) # 3. 调整大小到模型输入尺寸 (默认224x224) tactile_img cv2.resize(tactile_img, (224, 224), interpolationcv2.INTER_LINEAR) # 4. 转换为PIL Image然后应用标准的ImageNet归一化 # 这是关键因为触觉编码器是与CLIP的图像编码器对齐的所以使用相同的预处理流程。 preprocess transforms.Compose([ transforms.ToTensor(), # 将PIL Image或numpy数组转换为张量并缩放到[0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), # ImageNet统计量 ]) # 注意cv2.resize后是BGRPIL期望RGB。如果触觉图是RGB语义需要转换。 # 但很多触觉图并非真实颜色所以顺序可能不重要。为保险起见转为RGB。 tactile_img_rgb cv2.cvtColor(tactile_img, cv2.COLOR_BGR2RGB) input_tensor preprocess(Image.fromarray(tactile_img_rgb)) # 增加批次维度 input_tensor input_tensor.unsqueeze(0) # (1, 3, 224, 224) return input_tensor # 示例假设你有一个单通道的压力矩阵 pressure_data np.random.randn(100, 80) * 10 50 # 模拟压力数据 tensor_for_model preprocess_tactile_image(pressure_data) print(f预处理后的张量形状: {tensor_for_model.shape}) print(f值范围: [{tensor_for_model.min():.3f}, {tensor_for_model.max():.3f}])为什么使用ImageNet的均值和标准差这是一个需要理解的关键点。UniTouch的触觉编码器在训练时其对比学习的目标是让触觉特征靠近CLIP的图像特征。而CLIP的图像编码器通常是ViT在预训练时就是用ImageNet的统计量进行归一化的。因此为了保持特征空间的一致性我们在推理时也必须对触觉“图像”进行同样的归一化即使它看起来不像自然图像。2.2 处理多传感器与传感器特定标记如果你的项目涉及多种触觉传感器比如同时有GelSight和TacTipUniTouch还有一个“杀手锏”可学习的传感器特定标记Sensor-Specific Tokens。这个设计非常巧妙它允许模型用一个共享的编码器主干同时处理来自不同传感器的数据。在推理时模型需要知道当前输入的触觉图像来自哪种传感器以便选择对应的标记。官方代码通常会提供一个传感器检索机制计算输入图像与训练集中各传感器原型prototype的距离选择最接近的那个。对于我们快速上手如果你只有一种传感器或者不确定可以直接使用模型默认的或第一个传感器的标记。在加载预训练模型时通常会包含这些标记的权重。# 伪代码展示传感器标记的概念 class UniTouchWithSensorTokens(nn.Module): def __init__(self, tactile_encoder, sensor_tokens): super().__init__() self.tactile_encoder tactile_encoder # 共享的ViT编码器 self.sensor_tokens nn.Parameter(sensor_tokens) # 形状: (K, L, D) # K: 传感器数量 L: 每个传感器的标记数 D: 特征维度 def forward(self, tactile_img, sensor_idNone): # 提取图像块特征 patch_embeddings self.tactile_encoder.patch_embed(tactile_img) # 获取当前传感器对应的可学习标记 if sensor_id is not None: token self.sensor_tokens[sensor_id] # (L, D) else: # 如果未指定可以使用一个默认的例如平均标记或第一个 token self.sensor_tokens.mean(dim0) # 或 self.sensor_tokens[0] # 将标记作为前缀与图像块特征拼接 combined_features torch.cat([token, patch_embeddings], dim1) # 通过Transformer编码器 output self.tactile_encoder.transformer(combined_features) return output在实际调用中我们通常不需要手动处理这些细节加载好的模型已经封装好了这些逻辑。我们只需要确保输入数据的格式正确并在必要时指定sensor_type参数。3. 加载模型与执行零样本分类环境准备好了数据也处理好了现在就是最激动人心的部分加载模型并让它告诉我们触觉图像是什么材料。这里我们会用到Hugging Face的transformers库来方便地加载CLIP的文本编码器并结合我们下载的UniTouch触觉编码器。3.1 整合CLIP与UniTouch编码器零样本分类的原理是计算触觉特征与一系列文本特征每个对应一个类别的余弦相似度选择相似度最高的那个类别。因此我们需要两个编码器一个用于触觉图像UniTouch一个用于文本CLIP。import torch import torch.nn.functional as F from transformers import CLIPProcessor, CLIPModel import requests from io import BytesIO # 假设你已经有了UniTouch的触觉编码器类 TactileEncoder from models.tactile_encoder import TactileEncoder class UniTouchZeroShotClassifier: def __init__(self, tactile_ckpt_path, clip_model_nameopenai/clip-vit-base-patch32, deviceNone): 初始化分类器。 Args: tactile_ckpt_path: UniTouch触觉编码器权重文件路径。 clip_model_name: Hugging Face上的CLIP模型名称。 device: 运行设备 (cuda 或 cpu)。 self.device device if device else (cuda if torch.cuda.is_available() else cpu) print(f使用设备: {self.device}) # 1. 加载CLIP模型和处理器用于文本 print(加载CLIP文本模型...) self.clip_model CLIPModel.from_pretrained(clip_model_name).to(self.device) self.clip_processor CLIPProcessor.from_pretrained(clip_model_name) # 我们只需要文本编码器但CLIPModel是一个整体没关系。 self.clip_model.eval() # 2. 加载UniTouch触觉编码器 print(加载UniTouch触觉编码器...) # 首先实例化模型结构需要与训练时配置一致。通常可以从checkpoint中加载配置。 checkpoint torch.load(tactile_ckpt_path, map_locationcpu) config checkpoint[config] # 假设checkpoint保存了配置 self.tactile_encoder TactileEncoder(config).to(self.device) # 加载权重 self.tactile_encoder.load_state_dict(checkpoint[model_state_dict]) self.tactile_encoder.eval() print(模型加载完毕。) def get_text_features(self, class_names, prompt_templatea photo of a {} material.): 为给定的类别名称列表生成文本特征。 Args: class_names: 列表如 [fabric, metal, wood, plastic]。 prompt_template: 用于包装类别名的提示模板。UniTouch论文中使用了类似“这感觉像{}”的模板 但用CLIP的标准视觉提示“a photo of a {}”通常也有效。 Returns: torch.Tensor: 文本特征形状为 (num_classes, feature_dim)。 # 使用提示模板构造文本输入 texts [prompt_template.format(cls) for cls in class_names] # 使用CLIP的tokenizer处理文本 inputs self.clip_processor(texttexts, return_tensorspt, paddingTrue).to(self.device) with torch.no_grad(): text_features self.clip_model.get_text_features(**inputs) # 归一化特征向量以便计算余弦相似度 text_features F.normalize(text_features, dim-1) return text_features def get_tactile_features(self, tactile_image_tensor, sensor_typeNone): 从预处理后的触觉图像张量中提取特征。 Args: tactile_image_tensor: 形状为 (B, C, H, W) 的张量已经过归一化。 sensor_type: 可选传感器类型标识用于选择特定的传感器标记。 Returns: torch.Tensor: 触觉特征形状为 (B, feature_dim)。 with torch.no_grad(): # 将数据移到设备上 tactile_image_tensor tactile_image_tensor.to(self.device) # 前向传播。具体参数取决于你的TactileEncoder实现。 # 假设forward方法接受图像和可选的sensor_id if sensor_type is not None: tactile_features self.tactile_encoder(tactile_image_tensor, sensor_idsensor_type) else: tactile_features self.tactile_encoder(tactile_image_tensor) # 同样进行归一化 tactile_features F.normalize(tactile_features, dim-1) return tactile_features def predict(self, tactile_image_tensor, class_names, prompt_templatea photo of a {} material.): 执行零样本预测。 Args: tactile_image_tensor: 预处理后的触觉图像张量 (1, C, H, W)。 class_names: 候选类别列表。 prompt_template: 提示模板。 Returns: dict: 包含预测类别、置信度相似度和所有类别得分的字典。 # 1. 提取触觉特征 tactile_feat self.get_tactile_features(tactile_image_tensor) # (1, D) # 2. 提取文本特征 text_feats self.get_text_features(class_names, prompt_template) # (C, D) # 3. 计算余弦相似度 (矩阵乘法) # tactile_feat: (1, D), text_feats: (C, D) - similarity: (1, C) similarity (tactile_feat text_feats.T).squeeze(0) # 变成 (C,) # 4. 应用softmax得到概率分布可选相似度本身已可排序 probs F.softmax(similarity * 100, dim-1) # 温度系数 scaling (CLIP常用100) # 5. 获取预测结果 pred_idx similarity.argmax().item() pred_class class_names[pred_idx] confidence probs[pred_idx].item() results { predicted_class: pred_class, confidence: confidence, similarities: {cls: sim.item() for cls, sim in zip(class_names, similarity)}, probabilities: {cls: prob.item() for cls, prob in zip(class_names, probs)} } return results # 使用示例 if __name__ __main__: # 初始化分类器 classifier UniTouchZeroShotClassifier( tactile_ckpt_path./pretrained/unitouch_vit_best.pth, clip_model_nameopenai/clip-vit-base-patch32 ) # 模拟一个触觉图像张量 (假设已经用preprocess_tactile_image处理好了) # 这里我们用随机数据代替实际中应替换为你的真实数据 dummy_tactile_input torch.randn(1, 3, 224, 224).clamp(0, 1) # 模拟归一化后的图像 dummy_tactile_input transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])(dummy_tactile_input) # 定义你想要分类的材质类别 material_classes [cotton fabric, stainless steel, oak wood, smooth plastic, rough sandpaper] # 进行预测 result classifier.predict(dummy_tactile_input, material_classes, prompt_templateThis feels like {}.) print(f预测结果: {result[predicted_class]} (置信度: {result[confidence]:.3f})) print(所有类别相似度:) for cls, sim in result[similarities].items(): print(f {cls}: {sim:.4f})这段代码构建了一个完整的零样本分类流水线。UniTouchZeroShotClassifier类封装了所有步骤你只需要提供触觉图像和类别名称列表。prompt_template参数很重要论文中发现针对触觉任务设计特定的提示词如“This feels like {}”能提升性能你可以多尝试几种。3.2 处理常见错误与调试技巧第一次运行很可能不会一帆风顺。下面是我遇到过的几个典型问题及解决方法错误:KeyError: model_state_dict这意味着你的checkpoint文件格式与代码期望的不符。用print(checkpoint.keys())查看里面到底有什么。权重可能直接保存在checkpoint字典的根目录下或者叫state_dict。你需要相应地修改加载权重的代码行。错误:RuntimeError: size mismatch或Unexpected key(s) in state_dict模型结构不匹配。确保你实例化的TactileEncoder与保存权重时的模型结构完全一致相同的配置参数。最稳妥的方法是使用官方提供的模型定义文件和配置。性能不佳所有类别得分都很低或很接近检查数据预处理确保你的触觉图像归一化方式正确使用了ImageNet的均值和标准差。可以可视化一下预处理后的张量看看是否还保留有原始数据的结构信息。尝试不同的提示模板CLIP和UniTouch对提示词敏感。除了“This feels like {}”还可以试试“a texture of {}”、“a tactile image of {} material”、“The material is {}”等。检查特征维度确保触觉编码器和CLIP文本编码器输出的特征维度相同通常是512或768。如果不一致可能需要一个投影层但预训练的UniTouch应该已经处理好了。确认传感器类型如果你使用的是模型训练时未见过的传感器性能可能会下降。可以尝试不指定sensor_type让模型使用平均标记或者用代码计算与哪个训练传感器最接近。CUDA内存不足如果遇到GPU内存错误尝试减小批次大小batch size。使用torch.no_grad()和model.eval()。将模型和数据转移到CPU上运行速度会慢很多。4. 超越分类探索UniTouch的其他能力零样本材料分类只是UniTouch能力的冰山一角。由于它将触觉嵌入了与视觉、语言共享的语义空间我们可以玩出更多花样。下面我分享两个扩展应用的思路和代码片段。4.1 触觉-图像检索为触觉感受找到对应的图片既然触觉和视觉特征在同一个空间那么给定一个触觉样本我们可以在一个图像数据库中检索出视觉上最相似的图片。这在机器人操作中很有用比如通过触摸猜测物体的大致外观。def tactile_to_image_retrieval(tactile_feature, image_features, image_paths, top_k5): 根据触觉特征检索最相似的图像。 Args: tactile_feature: 触觉特征向量形状 (1, D)。 image_features: 预计算好的图像特征矩阵形状 (N, D)。 image_paths: 对应图像路径列表长度N。 top_k: 返回前K个结果。 Returns: list: 包含(top_k个图像路径, 相似度分数)的列表。 # 归一化触觉特征如果尚未归一化 tactile_feature F.normalize(tactile_feature, dim-1) # 计算与所有图像特征的余弦相似度 similarities (tactile_feature image_features.T).squeeze(0) # (N,) # 获取top-k索引 top_indices similarities.topk(top_k).indices.cpu().numpy() top_scores similarities[top_indices].cpu().numpy() # 组装结果 results [(image_paths[i], top_scores[idx]) for idx, i in enumerate(top_indices)] return results # 使用示例假设你有一个图像文件夹并已用CLIP的图像编码器提取了所有图像特征 # image_features 和 image_paths 是预先计算好的 # tactile_feat 是上一节中提取的触觉特征 # top_images tactile_to_image_retrieval(tactile_feat, image_features, image_paths, top_k3) # for img_path, score in top_images: # print(f相似度 {score:.3f}: {img_path})要实现这个功能你需要预先用一个图像编码器比如CLIP的视觉编码器处理你的图像库提取并保存所有图像的特征。这是一个离线预处理步骤之后检索速度会非常快。4.2 结合大语言模型LLM进行触觉问答这是最令人兴奋的方向之一。UniTouch的触觉特征可以作为一个“插件”接入现有的视觉-语言大模型VLM如BLIP-2、LLaVA构建一个能“理解”触觉的对话系统。思路是用UniTouch编码器替换VLM中的视觉编码器或者将触觉特征与视觉特征拼接后输入给VLM。# 这是一个概念性示例展示如何将触觉特征输入给一个简单的LLM from transformers import AutoModelForCausalLM, AutoTokenizer class TactileQA: def __init__(self, tactile_encoder, llm_namemicrosoft/phi-2, deviceNone): self.device device or (cuda if torch.cuda.is_available() else cpu) self.tactile_encoder tactile_encoder.to(self.device).eval() self.llm_tokenizer AutoTokenizer.from_pretrained(llm_name) self.llm_model AutoModelForCausalLM.from_pretrained(llm_name).to(self.device) # 可能需要一个投影层将触觉特征维度匹配到LLM的嵌入维度 self.projection nn.Linear(tactile_encoder.output_dim, self.llm_model.config.hidden_size).to(self.device) def encode_tactile(self, tactile_img): with torch.no_grad(): feat self.tactile_encoder(tactile_img.to(self.device)) projected_feat self.projection(feat) # (1, D_llm) return projected_feat def ask(self, tactile_img, question): # 1. 编码触觉图像 tactile_embedding self.encode_tactile(tactile_img) # (1, D_llm) # 2. 编码问题文本 input_text fGiven this tactile sensation: {question} inputs self.llm_tokenizer(input_text, return_tensorspt).to(self.device) # 3. 将触觉特征作为特殊的“视觉token”插入到输入中 # 这里需要根据具体LLM的输入格式进行调整是一个复杂步骤 # 一种简单方法将触觉特征作为前缀加到输入嵌入中 input_embeds self.llm_model.get_input_embeddings()(inputs[input_ids]) # 假设我们在开头添加触觉特征 combined_embeds torch.cat([tactile_embedding, input_embeds], dim1) # 需要相应地调整attention_mask等 # 4. 生成回答 with torch.no_grad(): outputs self.llm_model.generate(inputs_embedscombined_embeds, max_new_tokens50) answer self.llm_tokenizer.decode(outputs[0], skip_special_tokensTrue) return answer # 注意这只是一个高度简化的框架。实际集成需要深入理解LLM的架构 # 并可能需要对LLM进行微调以学会处理这种新的多模态输入。更可行的方案利用像LLaVA这样的开源VLM它们通常设计了用于融合视觉特征的连接器connector。我们可以训练一个轻量级的适配器adapter将UniTouch的特征映射到LLaVA视觉编码器的特征空间然后直接使用LLaVA的预训练权重进行推理或微调。这比从头训练一个触觉LLM要现实得多。5. 项目实战构建一个实时材质分类Demo理论讲得再多不如动手做一个能跑起来的Demo。这个部分我将带你搭建一个简单的Streamlit网页应用允许用户上传触觉图像或模拟数据并实时看到模型的分类结果。5.1 使用Streamlit搭建交互界面Streamlit能让数据科学项目快速变成交互式应用。首先安装Streamlitpip install streamlit。创建一个名为app.py的文件import streamlit as st import torch from PIL import Image import numpy as np import cv2 from torchvision import transforms import plotly.graph_objects as go # 导入之前定义的分类器类 from unitouch_classifier import UniTouchZeroShotClassifier # 设置页面 st.set_page_config(page_titleUniTouch 触觉材质分类器, layoutwide) st.title( UniTouch 触觉材质零样本分类演示) st.markdown(上传一张触觉传感器图像或使用示例模型将预测其材质。) # 侧边栏模型和参数设置 with st.sidebar: st.header(⚙️ 设置) model_path st.text_input(UniTouch权重路径, value./pretrained/unitouch.pth) clip_model st.selectbox(CLIP模型, [openai/clip-vit-base-patch32, openai/clip-vit-large-patch14]) prompt st.text_input(提示词模板, valueThis feels like {} material.) classes_input st.text_area(分类类别每行一个, valuecotton\nmetal\nwood\nplastic\npaper\nceramic) class_list [c.strip() for c in classes_input.split(\n) if c.strip()] # 初始化模型使用缓存避免重复加载 st.cache_resource def load_classifier(model_path, clip_model): try: classifier UniTouchZeroShotClassifier(model_path, clip_model) return classifier, None except Exception as e: return None, str(e) classifier, error load_classifier(model_path, clip_model) if error: st.sidebar.error(f模型加载失败: {error}) else: st.sidebar.success(模型加载成功) # 主界面 col1, col2 st.columns(2) with col1: st.subheader( 输入触觉图像) input_method st.radio(选择输入方式, [上传图片, 使用示例数据]) tactile_img None if input_method 上传图片: uploaded_file st.file_uploader(选择一张图片..., type[png, jpg, jpeg, tiff]) if uploaded_file is not None: image Image.open(uploaded_file).convert(RGB) st.image(image, caption上传的触觉图像, use_column_widthTrue) # 转换为numpy数组进行预处理假设上传的是触觉“图像” tactile_np np.array(image) # 这里需要你的预处理函数 from preprocessing import preprocess_tactile_image tactile_tensor preprocess_tactile_image(tactile_np, sensor_typecustom) tactile_img tactile_tensor else: # 使用示例数据这里生成一个模拟的灰度压力图 st.write(使用模拟的触觉压力图...) # 创建一个简单的模拟压力图中心压力高 h, w 100, 80 y, x np.ogrid[-h//2:h//2, -w//2:w//2] mask x*x y*y (min(h,w)//4)**2 pressure_data np.zeros((h, w)) pressure_data[mask] 80 np.random.randn(*pressure_data[mask].shape)*10 pressure_data[~mask] 20 np.random.randn(*pressure_data[~mask].shape)*5 # 可视化 fig go.Figure(datago.Heatmap(zpressure_data, colorscaleViridis)) fig.update_layout(title模拟触觉压力分布图, xaxis_titleWidth, yaxis_titleHeight) st.plotly_chart(fig, use_container_widthTrue) # 预处理 from preprocessing import preprocess_tactile_image tactile_tensor preprocess_tactile_image(pressure_data, sensor_typecustom) tactile_img tactile_tensor with col2: st.subheader( 预测结果) if tactile_img is not None and classifier is not None: if st.button(开始分类, typeprimary): with st.spinner(模型推理中...): result classifier.predict(tactile_img, class_list, prompt_templateprompt) st.success(f**预测材质:** {result[predicted_class]}) st.metric(label置信度, valuef{result[confidence]:.3f}) # 绘制相似度条形图 classes list(result[similarities].keys()) sim_values list(result[similarities].values()) fig go.Figure(data[go.Bar(xclasses, ysim_values, marker_color[green if cresult[predicted_class] else lightblue for c in classes])]) fig.update_layout(title与各类别的余弦相似度, xaxis_title材质类别, yaxis_title相似度) st.plotly_chart(fig, use_container_widthTrue) # 显示详细数据 with st.expander(查看详细分数): for cls, prob in result[probabilities].items(): st.write(f- **{cls}**: {prob:.4f}) elif tactile_img is None: st.info(请在上传或生成触觉图像后点击“开始分类”。) else: st.warning(模型未正确加载请检查侧边栏设置。) # 底部信息 st.markdown(---) st.caption( **技术说明:** 本演示使用UniTouch模型将触觉图像特征与CLIP文本特征在共享语义空间中对齐实现零样本分类。 预处理步骤将原始触觉数据转换为符合ImageNet归一化的三通道图像。 )5.2 部署与优化建议在本地运行这个Demo很简单在终端执行streamlit run app.py。浏览器会自动打开一个本地页面。如果你想把它部署到网上例如用Hugging Face Spaces或Streamlit Cloud需要注意模型文件大小预训练模型可能很大几百MB到1GB。在Hugging Face Spaces上你可以将模型权重放在一个公开的仓库然后在应用启动时下载。使用torch.hub.load_state_dict_from_url或wget。依赖管理创建一个requirements.txt文件精确列出所有依赖包及其版本。缓存利用Streamlit的st.cache_resource装饰器缓存加载的模型避免每次交互都重新加载。错误处理在Web应用中健壮的错误处理至关重要。用try...except包裹模型加载和预测代码给用户友好的错误提示。性能优化对于实时应用考虑使用更小的CLIP模型变体如clip-vit-base-patch16。将文本特征预计算并缓存因为类别列表通常是固定的。如果处理视频流可以使用异步操作或队列来避免界面卡顿。这个Demo只是一个起点。你可以在此基础上增加更多功能比如支持摄像头实时采集模拟触觉、批量处理、保存结果、或者集成第二部分提到的图像检索和问答功能。UniTouch为我们打开了一扇新的大门让机器能真正地“感受”世界而不仅仅是“看见”。从快速分类到高级的触觉-视觉-语言推理其中的可能性正等待我们去探索。我在实际项目中最深的体会是跨模态对齐的思想比某个具体模型更重要。理解如何将一种稀缺模态触觉的数据映射到一个由丰富数据视觉、文本预训练好的语义空间中这个思路可以应用到声音、气味、温度等多种传感器数据上。下次当你遇到数据匮乏的感知任务时不妨想想有没有一个数据丰富的相关模态可以让我“借力”呢