Dingxingdi: 请你仔细阅读/workspace/ChatTS
里面的文件,理解他们的含义逻辑和结构。现在我需要你告诉我,ChatTS在处理一段既包含文本又包含时间序列的输入的时候,是怎么精确地分别哪些是文本哪些是时序,从而对两者分别进行编码的?
GitHub Copilot: 我先在 ChatTS 代码库里快速检索和阅读与输入解析和编码相关的模块(如 encoding_utils、llm_utils、vllm 相关脚本),以确认它如何区分文本与时序并分别编码。预期结果是找到明确的分割标记、数据结构或预处理函数,以及相应的编码流程。
搜索与 **/*.py
匹配的文件,124 匹配项
我将阅读 ChatTS 中与编码最相关的模块(尤其是 encoding_utils.py、llm_utils.py、以及 vLLM 推理脚本)来确认它如何在混合输入中区分文本与时序并分别编码,预期找出明确的分隔标记、正则或数据结构及编码流程。
读取 [](file:///workspace/ChatTS/chatts/utils/encoding_utils.py)
我会继续阅读 llm_utils.py、inference_tsmllm_vllm.py 和 vllm_stream_qa.py 以找到混合输入的解析与推理流程,确认
读取 [](file:///workspace/ChatTS/chatts/utils/llm_utils.py)
我将继续查看 vLLM 侧的时间序列支持 chatts_vllm.py,以及推理脚本 inference_tsmllm_vllm.py 和 vllm_stream_qa.py,以确认如何将
读取 [](file:///workspace/ChatTS/chatts/vllm/chatts_vllm.py#1-1),行 1 到 400
读取 [](file:///workspace/ChatTS/chatts/vllm/chatts_vllm.py#400-400),行 400 到 612
我将查看 ChatTS 推理工具 inference_tsmllm_vllm.py 和 vllm_stream_qa.py,了解如何把 <ts><ts/>
。
读取 [](file:///workspace/ChatTS/chatts/utils/inference_tsmllm_vllm.py)
读取 [](file:///workspace/ChatTS/chatts/utils/vllm_stream_qa.py)
搜索 <ts>
(**/ChatTS/**
)、20 结果的文本
读取 [](file:///workspace/ChatTS/chatts/utils/inference_tsmllm_deepspeed.py)
读取 [](file:///workspace/ChatTS/README.md#1-1),行 1 到 260
我已梳理 ChatTS 里与“混合输入(文本+时间序列)解析与编码”相关的模块,下面结合关键文件讲清它是如何精确区分文本与时序并分别编码、再对齐融合的。
结论速览
- 文本与时序的“分界”靠两层机制:
- 文本里使用专用占位标记对时序插槽定位:
<ts><ts/>
(以及可选的内嵌数据形式<ts>...[JSON 数组]...<ts/>
)。 - 同时将真实的时间序列以结构化字段传入:
multi_modal_data.timeseries=[np.ndarray,...]
(或 HF 的processor(..., timeseries=...)
)。
- 文本里使用专用占位标记对时序插槽定位:
- 编码时:
- 文本走普通 tokenizer/chat template 流程,生成 token 序列。
- 时间序列走“值保留”的数值归一化与补齐(sp/no 等),再按 patch 切分,经 MLP 编成 patch 级别的嵌入。
- 最后用占位 token(配置里的
ts_token_start_index
)把“时序 patch 嵌入”插到文本 token 嵌入中指定的位置,形成统一的输入。
ChatTS 如何识别文本 vs 时间序列
- 占位标记与计数
- 任何要在提示词中插入时序的位置,用成对标记写成:
<ts><ts/>
。
例如:"TS1 长度 256: <ts><ts/>; TS2 长度 128: <ts><ts/>"
。 - 这串标记不会包含数值本身,只负责“数量与位置”的定位。编码与对齐流程会用这些标记确定每段时序的注入点和需要的占位长度。
- 相关代码:
- encoding_utils.py: 生成含占位的 prompt(如
no_encoding/sp_encoding
返回<ts><ts/>
)。 - chatts_vllm.py:
Qwen2TSDummyInputsBuilder.get_dummy_text()
用<ts><ts/>
为每个时序生成“虚拟占位文本”,说明这种标记是 vLLM/Processor 侧约定的“插槽”。
- encoding_utils.py: 生成含占位的 prompt(如
- 结构化传参
- 真正的时间序列以数组形式单独传入推理引擎,而不是放在文本里:
- vLLM 路径:在请求里传
{ "prompt": "...", "multi_modal_data": {"timeseries": [ts1, ts2, ...]} }
,见 llm_utils.py 的LLMClient.llm_batch_generate()
。 - Transformers 路径:用
AutoProcessor
直接传processor(text=[...], timeseries=[...])
,见 inference_tsmllm_deepspeed.py。
- vLLM 路径:在请求里传
- 若用户把数值直接写进文本(
<ts>...[JSON 数组]...<ts/>
),会先解析并“挖出”数组,然后把文本替换成占位标记<ts><ts/>
,再把数组放进multi_modal_data
:chatts/utils/vllm_stream_qa.py: extract_and_remove_ts()
用正则抓取<ts>...<ts/>
之间的 JSON,抽出为数组,并把文本替换为<ts><ts/>
,确保“文本只留占位,数组进入结构化通道”。
这两层机制保证了:
- 文本里只保留占位信息(位置、数量),不混入原始数据;
- 时序数据通过专用字段传给处理器/模型编码器。
编码与对齐的完整流程
- 文本侧
- 走普通 tokenizer/chat template:
chatts/utils/llm_utils.py: LLMClient._apply_chat_template()
;- vLLM 和 HF 两条路径都会得到包含占位 token 的 input_ids。
- 时间序列侧(值保留编码 + 补齐)
- 归一化与遮罩(Mask):
- 训练/示例中常用的“sp”编码,会为每个时间步生成“值+mask(=1)”的对,便于变长对齐与有效长度计算;见 README.md“About sp Encoding”和
encoding_utils.sp_encoding()
的思路说明(推理时实际由 AutoProcessor 完成该标准化与打包)。
- 训练/示例中常用的“sp”编码,会为每个时间步生成“值+mask(=1)”的对,便于变长对齐与有效长度计算;见 README.md“About sp Encoding”和
- 打包到 HF/vLLM 期望的结构:
chatts/vllm/chatts_vllm.py: Qwen2TSMultiModalProcessor._call_hf_processor()
把timeseries
交给 HF Processor,产出(ts_tokens, encoded_ts_arrays)
:encoded_ts_arrays
是带有值与 mask 的张量集合(代码注释:“每个元素是 (value, mask)”)。ts_tokens
是与时序插槽对齐的已分词占位序列。
- patch 切分与 MLP 编码:
TimeSeriesEmbedding.forward()
将每段时序按patch_size
切块,支持位置编码或位置索引(可选),再过多层 MLP 得到每个 patch 的向量表示;有效长度由最后一维的mask
求和得到(mask = x[:, :, -1]
),自动忽略 padding。- 返回每条时序的 patch 嵌入以及每条的 patch 数
patch_cnt
。
- 与文本对齐并融合
- 占位 token 与 patch 数的匹配:
Qwen2TSMultiModalProcessor._get_prompt_updates()
基于encoded_ts_arrays
推出每条时序的patch_cnt
,如果文本中<ts><ts/>
转成的占位 token 数不足,就自动“扩充”占位,确保“占位数量 == patch 数量”。- 这里用到配置里的占位 token id:
config.ts_token_start_index
(即“时序起始占位 token”)。
- 嵌入合并:
Qwen2TSForCausalLM.get_input_embeddings()
中,通过merge_multimodal_embeddings(input_ids, text_embeds, ts_embeds, ts_token_start_index)
将“时序 patch 嵌入”按占位 token 的位置插入到文本嵌入序列中,得到统一的输入。- 这一步是“区分编码、精确对齐”的落点:文本与时序先独立编码,再通过占位 token 的位置进行无歧义的拼接。
关键代码指引(可快速定位)
- 标记与提示拼装:
- encoding_utils.py(
<ts><ts/>
的生产与替换、可选值内嵌<ts>...<ts/>
) chatts/utils/vllm_stream_qa.py: extract_and_remove_ts()
(从文本里提取数值并替换为占位)
- encoding_utils.py(
- vLLM 多模态接入与处理:
- chatts_vllm.py
- Processor 与 Parser:
Qwen2TSMultiModalProcessor
/Qwen2TSDataParser
- Prompt 占位扩展与替换:
_get_prompt_updates()
- 时序编码器:
TimeSeriesEmbedding
(patchify + MLP) - 融合:
Qwen2TSForCausalLM.get_input_embeddings()
调merge_multimodal_embeddings
- Processor 与 Parser:
- chatts_vllm.py
- 推理封装:
- vLLM 批量:
chatts/utils/llm_utils.py: LLMClient.llm_batch_generate()
(构造multi_modal_data.timeseries
) - HF+DeepSpeed:inference_tsmllm_deepspeed.py(
AutoProcessor(..., timeseries=...)
)
- vLLM 批量:
小结
- ChatTS 用
<ts><ts/>
在文本里精确标注“时序插槽”,而真实时序通过结构化字段传入;如果时序被写进文本(<ts>...[JSON]...<ts/>
),会先提取出来再把文本改回占位。 - 编码层面,文本和时序各走各的编码器:文本走 tokenizer;时序走值保留编码+patch MLP。最后以占位 token 为锚,把时序 patch 嵌入插入到文本嵌入序列中,保证两者既独立编码又能严格对齐。这样既不混淆模态边界,又能在一个自回归序列里共同推理。
Dingxingdi: 所以用户的输入一定要是某种符合格式的输入吗?也就是时间序列在用户输入里面一定要符合某种格式,否则无法识别?
GitHub Copilot: 好问题。我来直接说结论,并给出可用的输入格式选项。
- 是的:要让 ChatTS 把“某一段输入”当作时间序列模态来处理,输入需要满足它约定的标记与结构化形式;否则这段内容只会被当作普通文本,模型不会把它当作时间序列去编码与对齐。
- 不是必须把数值写进文本里,但必须在文本中放“时序占位符”,并通过结构化字段把数值提供给模型。
以下是三种被支持的方式(任选其一,推荐前两种):
- 推荐:文本里放占位,时序用结构化字段传
- 文本中为每条时序放一个占位对:
<ts><ts/>
(一个时间序列就是一对)。 - 同时把真实时序数组通过结构化字段传给推理端:
- vLLM 路径:传入
{"prompt": "...", "multi_modal_data": {"timeseries": [ts1, ts2, ...]}}
,见 llm_utils.py。 - Transformers 路径:
AutoProcessor(...). __call__(text=[...], timeseries=[...])
,见 inference_tsmllm_deepspeed.py。
- vLLM 路径:传入
- 要点:占位对的个数必须与传入的时间序列个数一致;每条时序只需要一对
<ts><ts/>
,内部会按 patch 自动扩展对齐。
- 互动场景(仅限 vllm_stream_qa.py 脚本):文本内内嵌 JSON 数组
- 你可以在输入里直接写:
<ts>[1.2, 0.7, ...]<ts/>
或二维/带 mask 的列表。 - vllm_stream_qa.py 的
extract_and_remove_ts()
会自动:- 用正则提取
<ts>...<ts/>
之间的 JSON 数组并转成 numpy; - 把文本中这段替换成占位对
<ts><ts/>
; - 以结构化的
multi_modal_data.timeseries
传入模型。
- 用正则提取
- 这让“把数值写进文本里”的用法也能被识别,但仅在该脚本路径下生效。
- 仅文本(无占位、无结构化时序)
- 这时模型只把它当纯文本 LLM 使用,不会加载时序编码器,也无法进行“基于数值序列”的分析。
补充说明与常见陷阱
- 必需的“格式”是什么?
- 至少要有“每条时序一对”的占位符:
<ts><ts/>
(文本侧定位与对齐锚点)。 - 加上结构化的时序数组(API/processor 侧,以 list 或 numpy 数组传入)。这两者需一一对应。
- 至少要有“每条时序一对”的占位符:
- 不能只给占位不传数组,也不能只传数组不放占位。前者会没有可插入的编码,后者则没有插槽可对齐。
- 多条时序:文本里写多对
<ts><ts/>
,结构化里给等量数组;长度不同也支持(依靠 mask 补齐与 patch 化)。 - 限制:一次输入的时序条数受
limit_mm_per_prompt
限制(代码示例里常见 15 或 50);过短序列(如 <64)虽能解析,但效果在 README 里提示可能较弱。 - 你不需要在文本里手写归一化或 mask,“值保留编码”会由 Processor/模型内的时序编码器自动完成(见 chatts_vllm.py 的
TimeSeriesEmbedding
和Qwen2TSMultiModalProcessor
)。
简单示例(推荐方式)
- 文本:
"我有两段序列。TS1: <ts><ts/>;TS2: <ts><ts/>。请比较它们的峰值时间差别。"
- 结构化:
timeseries=[np.array([...]), np.array([...])]
- 推理时 vLLM/Transformers 会:
- 把文本 token 化;
- 把时序做值保留编码 + patch 化;
- 按占位符把时序嵌入插进对应位置,一起送入模型。
结论
- 要让 ChatTS 识别“这是时序,不是普通文本”,你需要遵守它的“占位符 + 结构化时序”约定;或者在特定脚本(vllm_stream_qa.py)里用
<ts>JSON 数组<ts/>
,它会自动转成标准形式。否则它只会把你输入当普通文本处理。