一、环境配置
python 3.7
torch==2.1.0
二、数据预处理
姓氏数据集,它收集了来自18个不同国家的10,000个姓氏,这些姓氏是作者从互联网上不同的姓名来源收集的。该数据集具有一些使其有趣的性质。第一个性质是它是相当不平衡的。排名前三的国籍占数据的60%以上:27%是英语,21%是俄语,14%是阿拉伯语。剩下的15个民族的频率也在下降——这也是语言特有的特性。第二个特点是,在国籍和姓氏正字法(拼写)之间有一种有效和直观的关系。有些拼写变体与原籍国联系非常紧密(比如“O ‘Neill”、“Antonopoulos”、“Nagasawa”或“Zhu”)。本文中提到的姓氏分类是指将姓氏分类至其原有国籍类别中。
2.1 关于MLP的数据预处理
class SurnameDataset(Dataset):
def __init__(self, surname_df, vectorizer):
"""
参数:
surname_df (pandas.DataFrame): 数据集
vectorizer (SurnameVectorizer): 从数据集实例化的向量化器
"""
self.surname_df = surname_df
self._vectorizer = vectorizer
self.train_df = self.surname_df[self.surname_df.split=='train'] # 训练集
self.train_size = len(self.train_df) # 训练集大小
self.val_df = self.surname_df[self.surname_df.split=='val'] # 验证集
self.validation_size = len(self.val_df) # 验证集大小
self.test_df = self.surname_df[self.surname_df.split=='test'] # 测试集
self.test_size = len(self.test_df) # 测试集大小
self._lookup_dict = {
'train': (self.train_df, self.train_size), # 查找字典
self.set_split('train') # 默认设置为训练集
# 类权重
class_counts = surname_df.nationality.value_counts().to_dict() # 统计每个类别的数量
def sort_key(item):
return self._vectorizer.nationality_vocab.lookup_token(item[0])
sorted_counts = sorted(class_counts.items(), key=sort_key) # 根据类别排序
frequencies = [count for _, count in sorted_counts]
self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32) # 计算类权重
@classmethod
def load_dataset_and_make_vectorizer(cls, surname_csv):
"""加载数据集并从头开始创建一个新的向量化器
参数:
surname_csv (str): 数据集的位置
返回:
SurnameDataset 的一个实例
"""
surname_df = pd.read_csv(surname_csv) # 读取CSV文件
train_surname_df = surname_df[surname_df.split=='train'] # 提取训练集数据
return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df)) # 创建并返回实例
@classmethod
def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
"""加载数据集和相应的向量化器。
在向量化器已被缓存以便重用的情况下使用
参数:
surname_csv (str): 数据集的位置
vectorizer_filepath (str): 保存向量化器的位置
返回:
SurnameDataset 的一个实例
"""
surname_df = pd.read_csv(surname_csv) # 读取CSV文件
vectorizer = cls.load_vectorizer_only(vectorizer_filepath) # 加载向量化器
return cls(surname_df, vectorizer) # 创建并返回实例
@staticmethod
def load_vectorizer_only(vectorizer_filepath):
"""一个静态方法,从文件中加载向量化器
参数:
vectorizer_filepath (str): 序列化向量化器的位置
返回:
SurnameVectorizer 的一个实例
"""
with open(vectorizer_filepath) as fp:
return SurnameVectorizer.from_serializable(json.load(fp)) # 反序列化并返回向量化器
def save_vectorizer(self, vectorizer_filepath):
"""使用json将向量化器保存到磁盘
参数:
vectorizer_filepath (str): 保存向量化器的位置
"""
with open(vectorizer_filepath, "w") as fp:
json.dump(self._vectorizer.to_serializable(), fp) # 序列化并保存向量化器
def get_vectorizer(self):
""" 返回向量化器 """
return self._vectorizer
def set_split(self, split="train"):
""" 使用数据框中的列选择数据集的分割 """
self._target_split = split
self._target_df, self._target_size = self._lookup_dict[split] # 设置目标数据集及其大小
def __len__(self):
return self._target_size # 返回当前数据集的大小
def __getitem__(self, index):
"""PyTorch数据集的主要入口方法
参数:
index (int): 数据点的索引
返回:
包含数据点的字典:
特征 (x_surname)
标签 (y_nationality)
"""
row = self._target_df.iloc[index] # 获取指定索引处的数据行
surname_vector = self._vectorizer.vectorize(row.surname) # 向量化姓氏
nationality_index = self._vectorizer.nationality_vocab.lookup_token(row.nationality) # 获取国籍的索引
return {'x_surname': surname_vector, 'y_nationality': nationality_index} # 返回包含特征和标签的字典
def get_num_batches(self, batch_size):
"""给定批次大小,返回数据集中的批次数量
参数:
batch_size (int)
返回:
数据集中的批次数量
"""
return len(self) // batch_size # 计算批次数量
def generate_batches(dataset, batch_size, shuffle=True, drop_last=True, device="cpu"):
"""
一个生成器函数,包装了PyTorch的DataLoader。
它将确保每个张量在正确的设备位置上。
"""
dataloader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last) # 创建DataLoader
for data_dict in dataloader:
out_data_dict = {}
for name, tensor in data_dict.items():
out_data_dict[name] = data_dict[name].to(device) # 将张量移动到指定设备
yield out_data_dict # 生成批次数据
我们定义了一个姓氏数据集类和一个批次生成器函数,用于处理和加载姓氏数据,以便在机器学习模型中使用。
class Vocabulary(object):
"""处理文本并提取用于映射的词汇的类"""
def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
"""
参数:
token_to_idx (dict): 预先存在的词汇到索引的映射
add_unk (bool): 是否添加UNK(未知)标记的标志
unk_token (str): 要添加到词汇中的UNK标记
"""
if token_to_idx is None:
token_to_idx = {}
self._token_to_idx = token_to_idx
self._idx_to_token = {idx: token
for token, idx in self._token_to_idx.items()}
self._add_unk = add_unk
self._unk_token = unk_token
self.unk_index = -1
if add_unk:
self.unk_index = self.add_token(unk_token) # 添加UNK标记并获取其索引
def to_serializable(self):
""" 返回可以序列化的字典 """
return {'token_to_idx': self._token_to_idx,
'add_unk': self._add_unk,
'unk_token': self._unk_token}
@classmethod
def from_serializable(cls, contents):
""" 从序列化字典实例化词汇表 """
return cls(**contents)
def add_token(self, token):
"""根据标记更新映射字典。
参数:
token (str): 要添加到词汇表中的项
返回:
index (int): 对应于该标记的整数索引
"""
try:
index = self._token_to_idx[token]
except KeyError:
index = len(self._token_to_idx)
self._token_to_idx[token] = index
self._idx_to_token[index] = token
return index
def add_many(self, tokens):
"""将标记列表添加到词汇表中
参数:
tokens (list): 字符串标记的列表
返回:
indices (list): 对应于这些标记的索引列表
"""
return [self.add_token(token) for token in tokens]
def lookup_token(self, token):
"""检索与标记关联的索引,如果标记不存在则返回UNK索引。
参数:
token (str): 要查找的标记
返回:
index (int): 对应于该标记的索引
注意:
`unk_index`需要 >=0(已添加到词汇表中)以实现UNK功能
"""
if self.unk_index >= 0:
return self._token_to_idx.get(token, self.unk_index)
else:
return self._token_to_idx[token]
def lookup_index(self, index):
"""返回与索引关联的标记
参数:
index