【Bug已解决】How to convert a list of strings into a tensor in pytorch? 解决方案
2026/9/13 1:36:36 网站建设 项目流程

【Bug已解决】How to convert a list of strings into a tensor in pytorch? 解决方案

问题描述

在自然语言处理(NLP)任务中,我们经常需要将文本数据(字符串列表)转换为 PyTorch 张量用于模型训练。然而,PyTorch 张量本质上是数值型数据的容器,不能直接存储字符串。很多开发者在尝试将字符串列表转为张量时,会遇到各种错误。

常见的错误表现:

import torch # 尝试直接转换 strings = ["hello", "world", "pytorch"] try: tensor = torch.tensor(strings) except Exception as e: print(f"错误: {e}") # ValueError: failed to convert to tensor # 或 TypeError: not a valid type

这个问题的核心在于:PyTorch 张量只能存储数值类型(int, float, bool 等),不支持字符串类型。要将文本数据转为张量,需要先进行编码(encoding),将字符串映射为数值表示。

文本到张量的转换涉及多个层次:

  1. 词汇表映射:将每个单词/子词映射为整数索引
  2. 字符编码:将每个字符映射为 ASCII/Unicode 码
  3. 嵌入表示:将索引进一步映射为稠密向量
  4. 序列填充:处理不同长度的文本序列

错误复现

以下代码演示了各种尝试将字符串列表转为张量时遇到的错误:

import torch # ===== 错误1:直接转换字符串列表 ===== strings = ["hello", "world", "pytorch"] try: tensor = torch.tensor(strings) except Exception as e: print(f"错误1: {type(e).__name__}: {e}") # ValueError: failed to convert # ===== 错误2:转换混合类型 ===== mixed = ["hello", 42, "world"] try: tensor = torch.tensor(mixed) except Exception as e: print(f"错误2: {type(e).__name__}: {e}") # ===== 错误3:使用 LongTensor ===== try: tensor = torch.LongTensor(strings) except Exception as e: print(f"错误3: {type(e).__name__}: {e}") # ===== 错误4:使用 from_numpy ===== import numpy as np try: np_array = np.array(strings) tensor = torch.from_numpy(np_array) print(f"错误4: 成功创建,但类型为: {tensor.dtype}") # 这会创建一个字符串类型的张量,但无法用于模型训练 except Exception as e: print(f"错误4: {type(e).__name__}: {e}") # ===== 错误5:不同长度的字符串 ===== var_strings = ["hi", "hello", "greetings"] try: # 尝试将每个字符转为数字 tensor = torch.tensor([[ord(c) for c in s] for s in var_strings]) print(f"错误5: 成功,但形状不统一: {tensor.shape}") # 这会失败,因为不同长度的列表无法构成规则张量 except Exception as e: print(f"错误5: {type(e).__name__}: {e}") # ValueError: expected sequence of length 2 at dim 1 (got 5)

根因分析

1. PyTorch 张量的类型限制

PyTorch 张量(torch.Tensor)是基于数值计算的,支持的数据类型包括:

  • 浮点型:float16,float32,float64
  • 整型:int8,int16,int32,int64
  • 布尔型:bool
  • 复数型:complex64,complex128

PyTorch不支持字符串类型的张量。这是因为 GPU 计算需要固定大小的数值数据,而字符串是变长的、非数值的。

2. 文本数据的变长特性

文本数据天然是变长的——不同的句子有不同的单词数量,不同的单词有不同的字符数量。PyTorch 张量要求每个维度的大小是固定的,因此需要通过填充(padding)或截断(truncation)来统一长度。

3. 编码方案的多样性

将文本转为数值有多种方案,每种方案适用于不同的场景:

  • 词级别编码:每个单词映射为一个整数索引
  • 字符级别编码:每个字符映射为其 ASCII/Unicode 码
  • 子词编码:使用 BPE(Byte Pair Encoding)等算法
  • 预训练编码:使用 BERT、GPT 等模型的 tokenizer

4. 批处理的需求

在模型训练中,通常需要将多个文本组成一个批次(batch)。不同长度的文本需要通过填充(padding)对齐,并使用注意力掩码(attention mask)标记有效位置。

解决方案

方案一:使用词汇表进行词级别编码

import torch from collections import Counter class Vocabulary: """词汇表管理器""" def __init__(self, min_freq=1, max_size=None): self.min_freq = min_freq self.max_size = max_size self.word2idx = {} self.idx2word = {} # 特殊标记 self.pad_token = '<PAD>' self.unk_token = '<UNK>' self.sos_token = '<SOS>' # Start of sequence self.eos_token = '<EOS>' # End of sequence def build_vocab(self, texts): """从文本列表构建词汇表""" # 统计词频 counter = Counter() for text in texts: words = text.lower().split() counter.update(words) # 按频率排序 sorted_words = sorted( counter.items(), key=lambda x: x[1], reverse=True ) # 过滤低频词 if self.min_freq > 1: sorted_words = [(w, f) for w, f in sorted_words if f >= self.min_freq] # 限制词汇表大小 if self.max_size: sorted_words = sorted_words[:self.max_size] # 构建映射 self.word2idx = { self.pad_token: 0, self.unk_token: 1, self.sos_token: 2, self.eos_token: 3, } self.idx2word = {v: k for k, v in self.word2idx.items()} for word, freq in sorted_words: idx = len(self.word2idx) self.word2idx[word] = idx self.idx2word[idx] = word print(f"词汇表大小: {len(self.word2idx)}") return self def encode(self, text, max_len=None): """将文本编码为索引列表""" words = text.lower().split() indices = [self.word2idx.get(w, self.word2idx[self.unk_token]) for w in words] if max_len: if len(indices) > max_len: indices = indices[:max_len] else: indices = indices + [self.word2idx[self.pad_token]] * (max_len - len(indices)) return indices def decode(self, indices): """将索引列表解码为文本""" words = [] for idx in indices: if idx in [self.word2idx[self.pad_token], self.word2idx[self.eos_token]]: break words.append(self.idx2word.get(idx, self.unk_token)) return ' '.join(words) # 使用示例 texts = [ "hello world this is pytorch", "pytorch is a deep learning framework", "deep learning is fun and powerful", "hello from the world of ai", ] vocab = Vocabulary(min_freq=1) vocab.build_vocab(texts) # 编码单个文本 encoded = vocab.encode("hello world", max_len=10) tensor = torch.tensor(encoded, dtype=torch.long) print(f"编码: {encoded}") print(f"张量: {tensor}") print(f"解码: {vocab.decode(encoded)}")

方案二:使用字符级别编码

import torch import string class CharEncoder: """字符级别编码器""" def __init__(self): # 包含所有可打印字符 self.chars = string.printable # 0-9, a-z, A-Z, 标点, 空格等 self.char2idx = {c: i + 1 for i, c in enumerate(self.chars)} self.idx2char = {i + 1: c for i, c in enumerate(self.chars)} self.pad_idx = 0 self.unk_idx = len(self.chars) + 1 self.char2idx['<PAD>'] = self.pad_idx self.char2idx['<UNK>'] = self.unk_idx self.idx2char[self.pad_idx] = '<PAD>' self.idx2char[self.unk_idx] = '<UNK>' def encode(self, text, max_len=None): """将文本编码为字符索引""" indices = [self.char2idx.get(c, self.unk_idx) for c in text] if max_len: if len(indices) > max_len: indices = indices[:max_len] else: indices = indices + [self.pad_idx] * (max_len - len(indices)) return indices def decode(self, indices): """将索引解码为文本""" chars = [] for idx in indices: if idx == self.pad_idx: break chars.append(self.idx2char.get(idx, '<UNK>')) return ''.join(chars) def encode_batch(self, texts, max_len=None): """批量编码文本""" if max_len is None: max_len = max(len(t) for t in texts) batch = [self.encode(t, max_len) for t in texts] return torch.tensor(batch, dtype=torch.long) # 使用示例 encoder = CharEncoder() texts = ["hello", "world", "pytorch"] batch_tensor = encoder.encode_batch(texts, max_len=10) print(f"批量编码形状: {batch_tensor.shape}") # torch.Size([3, 10]) print(f"批量编码:\n{batch_tensor}") # 解码 for i in range(len(texts)): decoded = encoder.decode(batch_tensor[i].tolist()) print(f"解码 {i}: {decoded}")

方案三:使用 PyTorch 内置的 collate_fn 进行批处理

import torch from torch.utils.data import Dataset, DataLoader class TextDataset(Dataset): """文本数据集""" def __init__(self, texts, labels, vocab): self.texts = texts self.labels = labels self.vocab = vocab def __len__(self): return len(self.texts) def __getitem__(self, idx): encoded = self.vocab.encode(self.texts[idx]) return torch.tensor(encoded, dtype=torch.long), self.labels[idx] def collate_fn(batch, pad_idx=0): """ 自定义批处理函数。 将不同长度的序列填充到批次内最大长度。 """ # 分离数据和标签 sequences, labels = zip(*batch) # 获取批次内最大长度 max_len = max(len(seq) for seq in sequences) # 填充序列 padded_sequences = [] attention_masks = [] for seq in sequences: padding_length = max_len - len(seq) padded = torch.cat([ seq, torch.full((padding_length,), pad_idx, dtype=seq.dtype) ]) mask = torch.cat([ torch.ones(len(seq), dtype=torch.long), torch.zeros(padding_length, dtype=torch.long) ]) padded_sequences.append(padded) attention_masks.append(mask) # 堆叠为张量 sequences_tensor = torch.stack(padded_sequences) masks_tensor = torch.stack(attention_masks) labels_tensor = torch.tensor(labels, dtype=torch.long) return sequences_tensor, masks_tensor, labels_tensor ![配图](https://i-blog.csdnimg.cn/img_convert/2242c3b84ea993382eb7ffd3ebda54e9.png) # 使用示例 texts = [ "hello world", "this is a longer sentence with more words", "short text", "medium length text here", ] labels = [0, 1, 0, 1] # 构建词汇表 vocab = Vocabulary(min_freq=1) vocab.build_vocab(texts) # 创建数据集和数据加载器 dataset = TextDataset(texts, labels, vocab) dataloader = DataLoader( dataset, batch_size=2, shuffle=True, collate_fn=lambda batch: collate_fn(batch, pad_idx=vocab.word2idx['<PAD>']), ) # 迭代数据 for batch_seqs, batch_masks, batch_labels in dataloader: print(f"序列形状: {batch_seqs.shape}") print(f"掩码形状: {batch_masks.shape}") print(f"标签: {batch_labels}") print(f"序列:\n{batch_seqs}") print(f"掩码:\n{batch_masks}") print("-" * 40)

方案四:使用 HuggingFace Tokenizer

# pip install transformers from transformers import AutoTokenizer import torch # 使用预训练模型的 tokenizer tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased') texts = [ "Hello, how are you?", "I am learning PyTorch for NLP.", "This is a great framework!", ] # 批量编码 encoded = tokenizer( texts, padding=True, # 填充到批次内最大长度 truncation=True, # 截断超长文本 max_length=20, # 最大长度 return_tensors='pt', # 返回 PyTorch 张量 ) print(f"input_ids 形状: {encoded['input_ids'].shape}") print(f"attention_mask 形状: {encoded['attention_mask'].shape}") print(f"input_ids:\n{encoded['input_ids']}") print(f"attention_mask:\n{encoded['attention_mask']}") # 解码 for i in range(len(texts)): decoded = tokenizer.decode(encoded['input_ids'][i], skip_special_tokens=True) print(f"解码 {i}: {decoded}")

完整修复代码

以下是一个完整的文本到张量转换工具,支持多种编码方案:

import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from collections import Counter from typing import List, Dict, Tuple, Optional import string class TextToTensorConverter: """ 完整的文本到张量转换工具。 支持词级别、字符级别和子词级别的编码。 """ # 特殊标记 PAD_TOKEN = '<PAD>' UNK_TOKEN = '<UNK>' SOS_TOKEN = '<SOS>' EOS_TOKEN = '<EOS>' def __init__(self, level='word', min_freq=1, max_vocab_size=None): """ Args: level: 编码级别 ('word', 'char') min_freq: 最小词频 max_vocab_size: 最大词汇表大小 """ self.level = level self.min_freq = min_freq self.max_vocab_size = max_vocab_size self.token2idx: Dict[str, int] = {} self.idx2token: Dict[int, str] = {} # 初始化特殊标记 self.pad_idx = 0 self.unk_idx = 1 self.sos_idx = 2 self.eos_idx = 3 self._built = False def _tokenize(self, text: str) -> List[str]: """分词""" if self.level == 'word': return text.lower().split() elif self.level == 'char': return list(text) else: raise ValueError(f"不支持的级别: {self.level}") def build_vocab(self, texts: List[str]): """构建词汇表""" counter = Counter() for text in texts: tokens = self._tokenize(text) counter.update(tokens) # 过滤低频 if self.min_freq > 1: items = [(t, f) for t, f in counter.items() if f >= self.min_freq] else: items = list(counter.items()) # 按频率排序 items.sort(key=lambda x: x[1], reverse=True) # 限制大小 if self.max_vocab_size: items = items[:self.max_vocab_size - 4] # 留出特殊标记 # 构建映射 self.token2idx = { self.PAD_TOKEN: self.pad_idx, self.UNK_TOKEN: self.unk_idx, self.SOS_TOKEN: self.sos_idx, self.EOS_TOKEN: self.eos_idx, } for token, freq in items: self.token2idx[token] = len(self.token2idx) self.idx2token = {v: k for k, v in self.token2idx.items()} self._built = True print(f"词汇表构建完成: {len(self.token2idx)} 个 token (level={self.level})") return self def encode(self, text: str, max_len: Optional[int] = None, add_special: bool = False) -> torch.Tensor: """ 将单个文本编码为张量。 Args: text: 输入文本 max_len: 最大长度(None 表示不限制) add_special: 是否添加 SOS/EOS 标记 Returns: 编码后的张量 (LongTensor) """ tokens = self._tokenize(text) if add_special: tokens = [self.SOS_TOKEN] + tokens + [self.EOS_TOKEN] indices = [self.token2idx.get(t, self.unk_idx) for t in tokens] if max_len: if len(indices) > max_len: indices = indices[:max_len] else: indices = indices + [self.pad_idx] * (max_len - len(indices)) return torch.tensor(indices, dtype=torch.long) def encode_batch(self, texts: List[str], max_len: Optional[int] = None, add_special: bool = False) -> Tuple[torch.Tensor, torch.Tensor]: """ 批量编码文本。 Returns: (input_ids, attention_mask) input_ids: 编码后的张量 (batch_size, max_len) attention_mask: 注意力掩码 (batch_size, max_len) """ # 编码所有文本 encoded_list = [self.encode(t, add_special=add_special) for t in texts] # 计算最大长度 if max_len is None: max_len = max(len(e) for e in encoded_list) # 填充 padded = [] masks = [] for encoded in encoded_list: length = len(encoded) if length > max_len: padded.append(encoded[:max_len]) masks.append(torch.ones(max_len, dtype=torch.long)) else: pad_length = max_len - length padded.append(torch.cat([ encoded, torch.full((pad_length,), self.pad_idx, dtype=torch.long) ])) masks.append(torch.cat([ torch.ones(length, dtype=torch.long), torch.zeros(pad_length, dtype=torch.long) ])) input_ids = torch.stack(padded) attention_mask = torch.stack(masks) return input_ids, attention_mask def decode(self, indices: torch.Tensor, skip_special: bool = True) -> str: """将索引张量解码为文本""" if isinstance(indices, torch.Tensor): indices = indices.tolist() tokens = [] for idx in indices: if skip_special and idx in [self.pad_idx, self.sos_idx, self.eos_idx]: if idx == self.eos_idx: break continue tokens.append(self.idx2token.get(idx, self.UNK_TOKEN)) if self.level == 'word': return ' '.join(tokens) else: return ''.join(tokens) def get_embedding_layer(self, embedding_dim: int = 128) -> nn.Embedding: """获取与词汇表匹配的嵌入层""" return nn.Embedding( num_embeddings=len(self.token2idx), embedding_dim=embedding_dim, padding_idx=self.pad_idx, ) class NLPDataset(Dataset): """用于 NLP 任务的 PyTorch 数据集""" def __init__(self, texts, labels, converter, max_len=None): self.texts = texts self.labels = labels self.converter = converter self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoded = self.converter.encode(self.texts[idx], max_len=self.max_len) return encoded, self.labels[idx] def collate_batch(batch, pad_idx=0): """用于 DataLoader 的 collate_fn""" sequences, labels = zip(*batch) max_len = max(len(seq) for seq in sequences) padded = [] for seq in sequences: if len(seq) < max_len: padding = torch.full((max_len - len(seq),), pad_idx, dtype=seq.dtype) padded.append(torch.cat([seq, padding])) else: padded.append(seq[:max_len]) return torch.stack(padded), torch.tensor(labels) # ===== 完整使用示例 ===== def demo_word_level(): """词级别编码示例""" print("=" * 60) print("词级别编码示例") print("=" * 60) texts = [ "the quick brown fox jumps over the lazy dog", "machine learning is a subset of artificial intelligence", "pytorch is an open source deep learning framework", "natural language processing is fascinating", "transformers have revolutionized nlp tasks", ] labels = [0, 1, 1, 0, 1] # 构建转换器 converter = TextToTensorConverter(level='word', min_freq=1) converter.build_vocab(texts) # 单个编码 text = "the lazy fox" tensor = converter.encode(text, max_len=10) print(f"\n文本: '{text}'") print(f"编码: {tensor}") print(f"解码: '{converter.decode(tensor)}'") # 批量编码 batch_texts = texts[:3] input_ids, attention_mask = converter.encode_batch(batch_texts, max_len=10) print(f"\n批量编码:") print(f" input_ids 形状: {input_ids.shape}") print(f" attention_mask 形状: {attention_mask.shape}") print(f" input_ids:\n{input_ids}") print(f" attention_mask:\n{attention_mask}") # 创建 DataLoader dataset = NLPDataset(texts, labels, converter, max_len=10) dataloader = DataLoader( dataset, batch_size=2, shuffle=True, collate_fn=lambda b: collate_batch(b, converter.pad_idx) ) print(f"\nDataLoader 迭代:") for batch_x, batch_y in dataloader: print(f" Batch shape: {batch_x.shape}, Labels: {batch_y}") # 嵌入层 embedding = converter.get_embedding_layer(embedding_dim=64) embedded = embedding(input_ids) print(f"\n嵌入后形状: {embedded.shape}") # (batch, seq_len, embed_dim) def demo_char_level(): """字符级别编码示例""" print("\n" + "=" * 60) print("字符级别编码示例") print("=" * 60) texts = ["hello", "world", "pytorch"] converter = TextToTensorConverter(level='char') converter.build_vocab(texts) # 编码 input_ids, masks = converter.encode_batch(texts, max_len=10) print(f"字符级编码形状: {input_ids.shape}") print(f"编码:\n{input_ids}") # 解码 for i, text in enumerate(texts): decoded = converter.decode(input_ids[i]) print(f" '{text}' -> {input_ids[i].tolist()} -> '{decoded}'") def demo_with_model(): """与模型集成的示例""" print("\n" + "=" * 60) print("与模型集成示例") print("=" * 60) texts = [ "good movie I loved it", "terrible film waste of time", "amazing story great acting", "boring and predictable plot", ] labels = [1, 0, 1, 0] # 1=正面, 0=负面 # 构建转换器 converter = TextToTensorConverter(level='word') converter.build_vocab(texts) # 创建数据集 dataset = NLPDataset(texts, labels, converter, max_len=10) dataloader = DataLoader( dataset, batch_size=2, shuffle=True, collate_fn=lambda b: collate_batch(b, converter.pad_idx) ) # 创建简单的文本分类模型 class TextClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes, pad_idx=0): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=pad_idx) self.fc = nn.Sequential( nn.Linear(embed_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_classes), ) def forward(self, x): # x: (batch, seq_len) embedded = self.embedding(x) # (batch, seq_len, embed_dim) # 平均池化 pooled = embedded.mean(dim=1) # (batch, embed_dim) return self.fc(pooled) # 初始化模型 model = TextClassifier( vocab_size=len(converter.token2idx), embed_dim=64, hidden_dim=32, num_classes=2, pad_idx=converter.pad_idx, ) # 训练一个 epoch optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() model.train() for batch_x, batch_y in dataloader: optimizer.zero_grad() output = model(batch_x) loss = criterion(output, batch_y) loss.backward() optimizer.step() print(f" Loss: {loss.item():.4f}") # 推理 model.eval() test_text = "great movie loved it" test_encoded = converter.encode(test_text, max_len=10).unsqueeze(0) with torch.no_grad(): pred = model(test_encoded) pred_label = pred.argmax(dim=1).item() print(f"\n推理: '{test_text}' -> 预测类别: {pred_label} ({'正面' if pred_label == 1 else '负面'})") if __name__ == "__main__": demo_word_level() demo_char_level() demo_with_model() print("\n" + "=" * 60) print("所有示例运行完成!") print("=" * 60)

常见陷阱与注意事项

1. 填充值的选择

填充值(PAD index)通常设为 0。在nn.Embedding中设置padding_idx=0可以确保填充位置的嵌入向量始终为零向量,不参与梯度更新。

2. 注意力掩码的使用

填充位置不应参与注意力计算。使用attention_mask标记有效位置,在注意力计算时将填充位置的注意力权重设为负无穷(softmax 后为 0)。

3. 词汇表覆盖问题

测试集中可能出现训练集中没有的词(OOV 词)。使用<UNK>标记处理未知词,但过多的 OOV 词会影响模型性能。考虑使用子词编码(如 BPE)减少 OOV。

4. 编码一致性

确保训练和推理使用相同的词汇表和编码方式。保存模型时,应同时保存词汇表(token2idxidx2token)。

5. 中文文本处理

中文没有天然的空格分词,需要使用专门的分词工具(如 jieba)或字符级别编码:

import jieba tokens = list(jieba.cut("自然语言处理很有趣"))

6. 大规模词汇表的内存问题

词汇表过大时,nn.Embedding层会占用大量内存。可以使用哈希技巧(hashing trick)或自适应嵌入(adaptive softmax)来减少内存。

总结

将字符串列表转换为 PyTorch 张量需要经过编码步骤,因为 PyTorch 张量只支持数值类型。核心要点如下:

  1. 选择编码级别:词级别适合大多数 NLP 任务,字符级别适合处理拼写变化或未知词较多的场景。
  2. 构建词汇表:统计词频,建立 token 到索引的映射,处理特殊标记(PAD, UNK, SOS, EOS)。
  3. 处理变长序列:使用填充(padding)和注意力掩码(attention mask)统一批次内序列长度。
  4. 使用嵌入层nn.Embedding将离散的索引映射为稠密向量,是文本到张量转换后的下一步。
  5. 使用预训练 Tokenizer:对于生产级应用,推荐使用 HuggingFace 的 tokenizer,它处理了 BPE、特殊标记等复杂逻辑。
  6. 保存词汇表:与模型一起保存词汇表,确保推理时编码一致。

通过系统性地处理文本到张量的转换,可以构建高效、可扩展的 NLP 数据处理流水线。

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

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

立即咨询