LSTST:用语言支架让大模型理解时间序列分类
1. 项目概述与核心挑战
时间序列分类,说白了就是从一串按时间先后顺序记录的数据里,找出它属于哪个类别。这事儿听起来简单,但在工业预测、医疗诊断、金融风控这些领域,可是实打实的硬骨头。传统的路子,无论是依赖循环记忆的RNN、擅长捕捉局部特征的CNN,还是近年来大火的Transformer,核心目标都是一个:想方设法从数据里“抠”出那些随时间变化的模式和依赖关系。这些模型各有千秋,也都在各自的战场上立下了汗马功劳。
但最近两年,情况有点不一样了。大语言模型(LLM)的横空出世,让大家看到了新的可能性。LLM在理解长文本、进行复杂语义推理方面展现出的惊人能力,让人不禁遐想:能不能让这位“语言大师”也来搞搞时间序列分析?毕竟,序列建模是它的看家本领。这个想法很诱人,因为如果成功,我们或许能借助LLM在预训练阶段学到的、关于世界的丰富“常识”和逻辑推理能力,让时间序列分类模型变得更聪明、更通用,甚至能处理那些让传统模型头疼的“不规则”数据——比如医疗记录里采样时间点乱七八糟的生理信号。
理想很丰满,现实却给了我们一记闷棍。最直接的想法是把时间序列数据“翻译”成文字描述,比如“在t1时刻,传感器A的读数是x1;在t2时刻,读数是x2...”,然后扔给LLM去理解。但这条路很快就被堵死了:一个稍微长点的多变量时间序列,转换成文本后,其token数量轻松就能突破LLM的上下文长度限制(比如4096或8192)。你不可能把一整年的股票分时数据或一个病人所有的监护仪读数都塞进提示词里。于是,一些研究走了另一个极端:干脆扔掉LLM的“语言接口”。他们把时间序列数据通过一个卷积层之类的编码器,压缩成一组固定长度的向量(称为patch embedding),然后直接喂给LLM的中间层(Transformer块),只利用其强大的序列建模能力,而完全放弃了其语义理解的部分。这就好比请来一位语言学教授,却只让他做单词拼写检查,实在是暴殄天物。
更棘手的是“不规则时间序列”问题。现实世界的数据很少是规规矩矩、等间隔采样的。病人做检查的时间间隔可能不同,工厂传感器可能在某些时段故障导致数据缺失。传统方法处理这种数据,要么靠复杂的插值算法把数据“掰”整齐,要么设计特殊的网络结构(如基于图神经网络或连续时间注意力机制)。而前面提到的“嵌入直喂”法,由于要求输入固定数量的嵌入向量,在面对长度不一、采样点错位的序列时,要么截断丢失信息,要么填充引入噪声,其内置的、基于顺序编号的位置编码也无法正确反映真实的时间间隔,效果大打折扣。
所以,我们面临的是一道双重难题:既要充分利用LLM的语义知识和推理能力,又要突破其输入长度的限制,同时还得优雅地处理不规则时间序列。这就像要在螺蛳壳里做道场,还得把场子做得既漂亮又实用。接下来要介绍的LSTST,就是我们针对这道难题交出的一份答卷。
2. LSTST核心设计思路:语言支架的巧思
LSTST的全称是Language-Scaffolded Time Series Transformer,翻译过来就是“语言支架时序Transformer”。这个名字精准地概括了它的核心思想:为时间序列数据搭建一个“语言支架”,让LLM能够站在自己熟悉的语言平台上,去观察和理解陌生的时间序列世界。
这个设计的灵感,来源于我们如何使用LLM解决知识问答任务。当你问LLM“《红楼梦》的作者是谁?”时,它之所以能回答,是因为它在训练时“读”过相关的文本。那么,我们能不能把时间序列分类也变成一个类似的“上下文问答”任务呢?这就是LSTST架构的起点。
2.1 将分类重构为上下文问答
想象一下这个场景。你是一位医生,LLM是一位拥有海量医学知识但不懂看心电图的专家。现在有一份心电图(时间序列数据)和病人的年龄、性别等静态信息。你会怎么向这位专家咨询诊断意见?
你大概率不会直接把心电图数据列给他看。你会这样说:“这里有一位65岁男性患者的心电图数据。根据这份数据,请判断他是否患有心律失常?选项是:0. 正常,1. 房颤,2. 室性早搏...”
LSTST所做的,就是自动化这个过程。它构建了一个结构化的“语言支架”提示词,其核心是一个问题模板。例如,对于一个有C个类别的分类任务,支架的结尾部分(称为gq)会是:“What is the label? Please choose in [0, 1, ..., C-1]. A: ”这个模板将分类任务明确地框定为一个选择题,引导LLM在给定的选项范围内生成答案。
2.2 时序嵌入与语言支架的融合
关键问题来了:心电图数据本身(那些起伏的波形)无法被直接“说”出来。LLM的词汇表里没有“电压值0.5mV”这样的词。这就是“嵌入直喂”法选择抛弃语言接口的原因。但LSTST找到了一个更巧妙的融合点。
它没有抛弃语言接口,而是选择在“语言”和“数据”之间架一座桥。具体步骤如下:
- 生成时间序列嵌入:首先,使用一个一维卷积层(Conv1D)处理原始时间序列数据。这个卷积层的作用类似于一个“感知器”,将一长串原始数据(比如L个时间点)转换成J个更紧凑的、富含信息的特征向量(Patch Embeddings)。这解决了原始序列过长的问题。
- 在语言支架中预留“插槽”:在构建完整的输入提示时,除了最终的问答模板,我们还会在描述数据的部分插入一些特殊的标记(Token)。例如,支架的前半部分(称为
gp)可能是:“Also, time series data exists with patch1 is <|patch|>, patch2 is <|patch|>, ...”。这里的<|patch|>就是一个我们自定义的特殊标记,它像一个占位符,告诉LLM:“这里将会插入一个非文本的、代表数据片段的嵌入向量”。 - 嵌入插入:现在,我们将整个提示词(包含静态信息文本、数据描述和问题)进行常规的Token化,并通过LLM的词嵌入层转换为向量序列。接着,我们找到所有
<|patch|>标记对应的向量位置,将它们“挖空”(掩码掉)。最后,把第一步生成的时间序列嵌入向量,精准地“插入”到这些被挖空的位置上。
这个过程就是“支架”(Scaffolding)一词的生动体现:语言提示构成了一个结构化的框架,而时间序列数据作为核心内容被填充到这个框架的特定位置。LLM在处理这个混合序列时,既能理解“问题是什么”(通过语言部分),又能“看到”具体的数据(通过插入的嵌入向量),从而调用其深层的语义推理能力来建立数据与答案之间的联系。
注意:这里有一个非常重要的工程细节。
<|patch|>这个特殊标记需要被添加到LLM的Tokenizer词汇表中,并为其分配一个随机的词嵌入初始化。在训练过程中,这个标记的嵌入向量会被学习,但它主要起定位作用。真正承载信息的是我们插入的时间序列嵌入。
2.3 处理不规则时序的关键:实时位置编码
对于规则采样的数据,每个Patch对应的时间间隔是固定的。我们可以直接用它在序列中的顺序位置(第1个、第2个...)来生成位置编码,这能告诉模型各个Patch的先后顺序。
但不规则数据就麻烦了。假设第一个Patch覆盖了第0到第5分钟的数据,第二个Patch可能覆盖了第5到第15分钟的数据。如果我们还用“位置2”来编码第二个Patch,模型会误以为这两个Patch是等间隔的,从而扭曲了真实的时间关系。
LSTST的解决方案既直观又有效:使用真实时间位置编码。具体来说,对于每个由卷积生成的Patch,我们计算这个Patch所覆盖的所有原始时间点的中位数时间戳。然后用这个真实的时间戳(或将其归一化后)来计算正弦/余弦位置编码。公式可以简化为:f_real(j) = f( median( time_points_in_patch_j ) )其中f是传统Transformer的位置编码函数。
这样一来,即使两个Patch在序列顺序上相邻,但如果它们实际覆盖的时间段相隔很远,其位置编码的差异也会很大。这相当于给了模型一张“真实时间地图”,让它能正确理解数据点之间的实际时间关系,从而从容应对采样不规则、数据点缺失等现实情况。
3. 模型架构与实操要点详解
理解了核心思想,我们深入到LSTST的模型架构内部,看看各个组件是如何协同工作的,以及在工程实现上需要注意哪些坑。
3.1 整体工作流程拆解
LSTST的完整前向传播流程可以清晰地分为以下几个步骤,我结合一个具体的例子(比如使用Pytorch框架)来说明:
输入预处理与嵌入生成:
# 假设输入: X (batch_size, seq_len, num_features), D (batch_size, num_static_features), C (num_classes) # 1. 时间序列嵌入 # 使用一个Conv1d层将长序列映射为J个patch embeddings # kernel_size, stride, padding 是关键超参数,决定了patch的划分方式 conv = nn.Conv1d(in_channels=num_features, out_channels=d_model, kernel_size=k, stride=s, padding=p) P = conv(X.transpose(1, 2)).transpose(1, 2) # 形状: (batch_size, J, d_model) # 2. 计算实时位置编码并相加 # 假设我们有一个数组 real_times 记录了每个原始时间点的时间戳 # 需要根据卷积参数计算每个patch对应的真实时间戳中位数 patch_real_times = calculate_median_time_for_each_patch(real_times, k, s, p) real_pos_enc = real_time_positional_encoding(patch_real_times, d_model) P = P + real_pos_enc # 将位置信息注入嵌入构建语言支架并Token化:
# 3. 构建文本提示。例如,对于静态特征D(如年龄、性别),将其文本化。 # “Patient age is 65. Gender is male. Also, time series data exists with patch1 is <|patch|>, patch2 is <|patch|>... What is the label? Please choose in [0,1,2]. A: ” # 注意:这里需要根据batch中的每个样本动态生成提示,因为静态特征值不同。 prompts = [] for i in range(batch_size): static_text = f"Patient age is {D[i,0]}. Gender is {D[i,1]}." patch_placeholders = " ".join([f"patch{j} is <|patch|>," for j in range(J)]) question = f"What is the label? Please choose in {list(range(C))}. A: " full_prompt = static_text + " Also, time series data exists with " + patch_placeholders + " " + question prompts.append(full_prompt) # 4. Token化提示词 input_ids = tokenizer(prompts, return_tensors='pt', padding=True).input_ids # 形状: (batch_size, prompt_len) # 获取 <|patch|> 标记在所有序列中的位置索引 patch_token_id = tokenizer.convert_tokens_to_ids('<|patch|>') patch_positions = (input_ids == patch_token_id) # 布尔掩码矩阵嵌入融合与模型前向:
# 5. 通过LLM的词嵌入层获取提示词的初始嵌入 E_text = llm_model.get_input_embeddings()(input_ids) # 形状: (batch_size, prompt_len, d_model) # 6. 将时间序列嵌入插入到预留位置 # 首先,将文本嵌入中对应<patch>的位置置零 E_text[patch_positions] = 0 # 然后,将时间序列嵌入P加到对应的零位置上。 # 这里需要精巧的索引操作,因为每个样本的patch数量J可能小于提示词中<patch>的数量(如果J可变)。 # 假设我们已对齐,简化表示: E = E_text.scatter_add(dim=1, index=patch_positions.nonzero().unsqueeze(-1).expand(-1,-1,d_model), src=P) # 7. 将融合后的嵌入E输入LLM的Transformer主体 # 通常我们会冻结LLM的大部分参数,只训练嵌入层、部分LayerNorm和分类头 outputs = llm_model(inputs_embeds=E, attention_mask=attention_mask, output_hidden_states=True) last_hidden_state = outputs.last_hidden_state # 形状: (batch_size, prompt_len, d_model)上下文感知的分类头:
# 8. 获取最后一个Token(即答案提示符“A:”之后的位置)的隐藏状态 # 我们需要找到每个序列中“A:”对应的位置索引。假设它在提示词中是固定的。 answer_token_index = find_index_of_answer_token(input_ids) # 例如,可能是 prompt_len - 1 last_token_hidden = last_hidden_state[:, answer_token_index, :] # 形状: (batch_size, d_model) # 9. 通过分类头得到logits # 关键技巧:分类头的权重初始化 classifier = nn.Linear(d_model, C) # 从LLM的语言模型头(lm_head)中,取出对应类别标签Token(如“0”,“1”,“2”)的权重来初始化分类头。 # 这赋予了分类头初始的“语义理解”能力。 with torch.no_grad(): label_token_ids = tokenizer.convert_tokens_to_ids(['0', '1', '2']) # 示例 classifier.weight.data = llm_model.lm_head.weight[label_token_ids, :].clone() classifier.bias.data = llm_model.lm_head.bias[label_token_ids].clone() logits = classifier(last_token_hidden) # 形状: (batch_size, C)
3.2 关键组件与超参数选择
- 一维卷积层 (Conv1D Embedding):这是将原始时序数据转换为模型可处理嵌入的第一关。
kernel_size、stride和padding的选择至关重要,它们共同决定了J(patch数量)以及每个patch覆盖的时间范围。较大的kernel_size和stride能产生更少的patch,有利于处理超长序列,但可能会损失细粒度的时间信息。实践中,需要根据数据集的平均长度和计算资源进行权衡。论文中通常会在{8, 16, 32}等值中进行搜索。 - 实时位置编码计算:计算每个patch的“真实时间”中位数是处理不规则数据的核心。你需要访问原始数据的时间戳数组。对于规则数据,这一步可以退化为常规的顺序位置编码。实现时要特别注意边界条件,确保卷积窗口和时间戳数组的正确对应。
- 语言支架模板设计:模板的措辞会影响LLM的理解。论文中使用的模板(“What is label? Please choose in ... A:”)是有效的,但你也可以尝试更符合任务描述的模板,例如在医疗任务中使用“Based on the following vital signs, the patient's condition is most likely: [选项]”。一个重要的原则是保持一致性,在整个数据集中使用相同的模板结构。
- LLM主干的选择与参数冻结:通常选择中等规模、开源且性能良好的LLM作为主干,如GPT-2、GPT-J或LLaMA的某些版本。为了高效微调并防止灾难性遗忘,标准的做法是冻结LLM主干的所有Transformer层参数,只训练以下部分:
- 新添加的一维卷积嵌入层。
- LLM主干中的层归一化(LayerNorm)参数(经验表明解冻它们能带来性能提升)。
- 最后的分类头。
- 可选:词嵌入层(特别是我们添加的
<|patch|>标记的嵌入)。
3.3 训练策略与技巧
- 损失函数:标准的交叉熵损失(CrossEntropyLoss)足矣。
- 优化器:AdamW是默认且可靠的选择。学习率需要调优,由于大部分参数被冻结,学习率可以设置得相对较高(例如
1e-4到5e-4),但针对新添加的卷积层和分类头,有时需要更大的学习率(例如1e-3)。 - 批次构建:由于每个样本经过提示模板生成后,其输入长度(文本Token数 + Patch数)可能不同。需要使用动态填充(Dynamic Padding)和对应的注意力掩码(Attention Mask)。务必确保注意力掩码能正确覆盖所有真实的Token和插入的Patch嵌入,将填充部分屏蔽掉。
- 梯度检查:在第一次运行训练时,建议检查梯度流。确保卷积层和分类头的参数在更新,而被冻结的LLM主干参数梯度为零(或极小)。这可以验证你的冻结设置是否正确。
实操心得:在调试阶段,一个非常有用的小技巧是“可视化嵌入”。你可以将
<|patch|>位置插入前后的嵌入向量E_text和E取出来,计算它们的差异,确保时间序列嵌入被正确地加到了指定的位置,而不是其他地方。这能避免很多因索引错误导致的诡异bug。
4. 实验复现与结果分析
纸上得来终觉浅,绝知此事要躬行。要真正理解LSTST的威力,最好的办法就是复现其论文中的实验,并解读其结果。这里我们聚焦于其在规则时间序列和不规则时间序列分类任务上的表现。
4.1 规则时间序列分类实验
数据集:实验在UEA多元时间序列分类档案库的10个经典数据集上进行,涵盖了手势识别(UWaveGestureLibrary)、交通流量(PEMS-SF)、心电图(Heartbeat)、语音(SpokenArabicDigits)等多个领域。这些数据都是等间隔采样的规则序列。
对比基线:作者设置了很高的对比标准,包括了四大类19个基线模型:
- 距离模型:欧氏距离、动态时间规整(DTW)等传统但强大的方法。
- 深度学习模型:涵盖了RNN系(LSTNet)、CNN系(Rocket, TimesNet, ModernTCN)、MLP系(DLinear)以及Transformer系(PatchTST, FEDformer)的SOTA模型。
- LLM-based方法:主要是GPT4TS和TEST,这两个是直接利用LLM做时间序列分析的先驱工作。
核心结果:LSTST在10个数据集上的平均准确率达到了76.2%,超越了之前最好的LLM方法GPT4TS(74.0%)和最好的CNN方法ModernTCN(74.2%)。这是一个显著的提升。具体来看:
- 在Heartbeat(HB)和PEMS-SF(PS)数据集上,LSTST取得了所有模型中的最佳性能。
- 在类别数较多的Handwriting(HW)数据集上,LSTST也表现优异,这说明其利用LLM语义能力处理复杂分类任务的优势。
结果解读:这个实验有力地证明了LSTST框架的有效性。它不仅仅是一个“能用LLM”的模型,更是一个“能比专门设计的SOTA模型用得更好”的模型。其成功的关键在于,它没有牺牲LLM的语义理解能力。相比之下,GPT4TS等方法丢弃了文本接口,相当于自废武功,只用了LLM的“骨架”(序列建模能力),而LSTST通过语言支架,成功调动了LLM的“大脑”(预训练知识),从而实现了性能突破。
4.2 不规则时间序列分类实验
这才是真正体现LSTST设计精妙之处的地方。模型本身没有为不规则数据做任何特殊结构调整,却能在不规则数据集上取得极具竞争力的结果。
数据集:
- PAM:日常生活活动识别数据集,传感器数据,采样不规则。
- P19, P12:来自PhysioNet的重症监护医疗数据集,包含生命体征数据,采样极不规则且缺失值多,是典型的、具有挑战性的真实世界数据。
对比基线:这里对比的模型都是专门为不规则时间序列设计的“特种部队”,例如:
- GRU-D:使用衰减机制处理缺失值。
- SeFT, mTAND:将不规则序列视为集合,使用集合函数或连续时间注意力。
- Raindrop, ViTST:当前SOTA,分别使用图神经网络和视觉Transformer的变体。
核心结果:
- 在PAM数据集上,LSTST在准确率、精确率、召回率和F1分数上全面超越了之前的SOTA模型(Raindrop),提升约1个百分点。
- 在类别不平衡严重的P12和P19医疗数据集上(使用AUROC和AUPRC评估),LSTST取得了第二好的性能,与SOTA模型(ViTST)的差距非常小(AUROC差距在0.1%-1.9%)。
结果解读:这个结果非常令人惊讶。一个没有为不规则性做显式设计的通用框架,竟然能媲美甚至超越那些精心设计的专用模型。这充分证明了实时位置编码和动态上下文长度设计的威力。实时位置编码让模型理解了真实的时间流逝,而语言支架允许输入可变数量的Patch嵌入,自然适应了不同长度的序列。这说明了LSTST框架强大的泛化能力和处理现实世界复杂数据的能力。
4.3 消融实验的启示
论文中的消融实验像手术刀一样,精准地剖析了各个组件的作用:
支架结构(Ablation on Scaffold Structure):
- 移除位置提示词(Type2):在描述patch时,不用“patch1 is, patch2 is...”这样的语言描述,性能下降。这说明简单的语言提示能帮助LLM更好地组织对多个数据片段的认知。
- 改变问题模板(Type3):将“What is label? Please choose...”换成简单的“the label is”,性能也下降。这说明将任务明确框定为“选择题”的问答形式,能更有效地激发LLM的推理模式。
- 移除静态特征(Type4):不提供年龄、性别等文本描述的静态信息,性能下降。这直接验证了LSTST能够有效融合并利用文本形式的辅助信息。
权重初始化(Output Scaffolding):
- 比较了用LLM语言头对应标签词(如“0”,“1”)的权重初始化分类头,和随机初始化分类头。使用预训练权重初始化的方式明显更优。这证明了“输出支架”的有效性——它让分类器从一开始就“认识”这些选项,继承了LLM对这些符号的语义理解。
实时位置编码(Real-Time PE):
- 在极不规则的P12数据集上,用传统的顺序位置编码替换实时位置编码,性能显著下降。这铁证如山地说明了,对于不规则数据,注入真实时间信息是至关重要的,而不是简单的顺序编号。
5. 工程实践:从理论到代码的挑战与解决方案
把LSTST从论文搬到你的代码编辑器里,会遇到一些教科书上不会写的坑。这里我结合自己的实现经验,分享几个关键问题的解决思路。
5.1 动态提示生成与批次处理
这是实现中最繁琐但必须处理好的部分。每个样本的静态特征(如年龄、性别)值不同,导致其文本提示不同,Token化后的长度也可能不同。此外,不同样本的时间序列长度可能不同,经过卷积后得到的Patch数量J也可能不同。
解决方案:
- 预计算与缓存:对于静态特征文本部分,可以预先为所有可能的特征值组合生成文本片段。但更通用的做法是在数据加载器(DataLoader)中动态生成。
- 自定义整理函数(Collate_fn):这是PyTorch DataLoader的核心技巧。你需要编写一个自定义的
collate_fn函数,它接收一个batch的数据样本(每个样本包含原始时序X、静态特征D、标签y,可能还有时间戳times)。- 在这个函数内部,为batch中的每个样本生成完整的提示文本。
- 使用tokenizer对这批文本进行填充(padding),得到统一的
input_ids和attention_mask。 - 关键步骤:在生成提示文本时,必须同步记录每个样本的
<|patch|>标记在填充后的input_ids序列中的精确位置索引。这个索引信息将用于后续的嵌入插入操作。 - 同时,对时间序列数据
X进行填充(通常填充0),并记录有效长度,以便卷积层能正确处理(或使用masked convolution)。
- 嵌入插入的索引对齐:在模型前向函数中,你需要根据
collate_fn传过来的patch_positions(一个形状为[batch_size, max_prompt_len]的布尔掩码张量),将形状为[batch_size, J, d_model]的时间序列嵌入P,精确地加到E_text的对应位置上。这里通常需要使用torch.scatter_add_或高级索引操作,需要仔细处理维度对齐。
5.2 处理可变长度的Patch数量
如果每个样本的J不同(由于原始序列长度不同),那么P就是一个“锯齿状”张量,无法直接堆叠成batch。有两种主流处理方式:
- 统一到最大J(填充):将所有样本的
J通过填充(在时间维度末尾补零)统一到batch中的最大值。这是最简单的方法,但会引入无效计算。在卷积后,这些填充部分产生的Patch嵌入可能没有意义,需要在插入时通过掩码忽略。 - 使用Pack/Pad序列:更高效的方式是使用PyTorch的
nn.utils.rnn.pack_padded_sequence功能,但需要确保你的卷积层能够处理这种打包格式。或者,你可以放弃批次卷积,采用循环遍历样本的方式,但这样会牺牲GPU并行效率。论文中的实现很可能采用了填充策略,因为现代Transformer对填充有成熟的注意力掩码机制来处理。
避坑指南:强烈建议在开发初期,先固定
J(例如通过调整卷积参数或对输入序列长度进行裁剪/填充),让模型在固定长度的设置下跑通。待核心流程稳定后,再引入可变J的复杂性。同时,在插入嵌入时,务必打印和检查patch_positions掩码和P的形状,确保每个样本的J个嵌入都被正确地对位插入,没有错位或遗漏。
5.3 内存优化与量化
即使冻结了LLM主干,一个大模型(如GPT-J 6B)的前向传播仍然需要可观的GPU内存。处理长序列时间序列时,Patch数量J可能不小,导致输入序列总长度(文本Token + J)较长,这会显著增加注意力计算的开销。
优化策略:
- 梯度检查点:使用
torch.utils.checkpoint可以在训练时用时间换空间,大幅降低内存消耗。 - 4/8比特量化:如论文所述,对预训练的LLM主干进行4比特量化(例如使用
bitsandbytes库),可以将其内存占用减少到原来的1/4到1/2,而性能损失通常很小。这是让大模型在消费级显卡上运行的关键技术。 - 注意力优化:考虑使用Flash Attention(如果模型架构支持)来加速长序列的注意力计算。
- 批次大小:从较小的批次大小(如4或8)开始,根据内存情况逐步调整。
5.4 分类头初始化的陷阱
“用语言模型头对应标签词的权重来初始化分类头”这个技巧听起来简单,但有一个隐藏的坑:标签词可能不在基础词汇表中。例如,如果你的类别是“正常”、“异常”,对应的标签词是“normal”和“abnormal”,这没问题。但如果你的类别是数字[0, 1, 2],而你的LLM(特别是某些中文或代码预训练模型)的tokenizer可能会把“0”拆分成子词(subword),比如"0"本身就是一个token,但"10"可能被拆成"1"和"0"。
解决方案:
- 在初始化之前,务必用你的tokenizer检查每个类别标签对应的token id。使用
tokenizer(“0”, add_special_tokens=False).input_ids来获取。 - 确保获取到的id是单个token。如果是多个token,你需要决定是取第一个token的嵌入,还是将多个token的嵌入进行平均。论文中处理数字类别,通常数字本身是独立token,所以问题不大。
- 将这些id对应的权重从
lm_head.weight中提取出来,用来初始化你的分类头classifier.weight。
6. 超越分类:LSTST框架的扩展思考
LSTST的成功不仅仅在于它在时间序列分类任务上取得了SOTA,更在于它提供了一种将非文本模态数据与LLM深度结合的新范式。这个“语言支架”的思想具有很强的扩展性。
1. 扩展到其他时序任务:
- 时间序列预测:只需将语言支架中的问题模板改为“Given the past values, the next value is”。模型需要预测的是连续的数值,可以将输出头改为回归头,并从LLM的隐藏状态解码出预测值。这比分类任务更具挑战性,因为需要模型学习生成数字。
- 时间序列异常检测:可以构建为二分类(正常/异常)问题,或者重构为“这段序列是否异常?”的问答形式。甚至可以利用LLM的生成能力,让模型描述异常的类型和可能原因。
- 时序-文本跨模态任务:例如,根据心电图生成诊断报告。这需要更复杂的支架设计,可能将时序嵌入作为上下文,然后以“Generate a diagnostic report:”为提示,让LLM进行自由生成。
2. 处理更复杂的数据结构:
- 多模态时间序列:除了数值传感器数据,可能还有同步的视频、音频片段。LSTST的框架可以扩展:为视频使用视觉编码器(如ViT)生成视觉patch嵌入,为音频使用音频编码器生成音频patch嵌入,然后在语言支架中为每种模态预留不同的特殊标记(如
<|vision_patch|>,<|audio_patch|>),将它们一起插入。LLM则扮演多模态信息融合与推理的中心角色。 - 图结构时间序列:许多现实世界数据(如交通网络、社交网络)本质是图。可以先用图神经网络(GNN)处理每个时间片的图数据,得到图级别的嵌入,再将这一系列图嵌入作为时间序列patch输入LSTST。
3. 与提示工程和微调策略结合:
- 动态提示(Dynamic Prompting):根据输入数据的特性(如某个特征异常),动态调整语言支架中的描述文本,给LLM更精确的指令。
- 参数高效微调:除了全文微调(Full Fine-tuning)和冻结大部分参数的微调,可以引入LoRA、Adapter等参数高效微调方法,只训练注入到LLM中的少量适配器参数,从而在保持LLM知识不被破坏的前提下,更快地适配下游时序任务。
LSTST打开了一扇门,它告诉我们,LLM不仅仅是文本生成器,它可以成为一个强大的、可编程的“通用序列理解与推理引擎”。通过精心设计的“支架”,我们可以将各种形态的、复杂的、非结构化的数据“嫁接”到LLM这棵大树上,让它结出我们想要的果实。当然,这条路上还有计算成本、可解释性等挑战,但LSTST无疑指出了一个充满希望的方向。
