赞
踩
以一个人名为输入,使用模型帮助判断最有可能是来自哪一个国家的人名,在某些国际化公司的业务中具有重要意义,例如在用户注册过程中,会根据用户填写的名字直接分配可能的国家或地区,并限制其手机号码位数等。
整个案例实现可以分为五个步骤:
from io import open # 帮助使用正则表达式进行子目录的查询 import glob import os # 用于获得常见字母及字符规范化 import string import unicodedata # 导入随机工具random import random # 导入时间和数学工具包 import time import math # 导入torch工具 import torch # 导入nn准备构建模型 import torch.nn as nn # 导入制图工具包 import matplotlib.pyplot as plt
all_letters = string.ascii_letters + " .,;\'"
# 获取常用字符数量
n_letters = len(all_letters)
print("n_letter: ", n_letters)
n_letter: 57
# 暂时不考虑编码问题
# 我们认为这个函数的作用就是去除掉一些语言中的重音标记
# 如Ślusàrski ---> Slusarski
def unicodeToAscii(s):
return ''.join(
c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn'
and c in all_letters
)
s = 'Ślusàrski'
print(unicodeToAscii(s))
Slusarski
data_path = './data/names/'
def readLines(filename):
# 打开指定文件并读取所有内容,使用strip()去掉两侧空白符,然后以'\n'进行切割
lines = open(filename, encoding='utf-8').read().strip().split('\n')
# 对应每个lines列表中的名字进行Ascii转换,使其规范化,最后返回一个名字列表
return [unicodeToAscii(line) for line in lines]
# filename是数据集中某个具体文件
filename = data_path + "Chinese.txt"
lines = readLines(filename)
print(lines)
['Ang', 'AuYong', 'Bai', 'Ban', 'Bao', 'Bei', 'Bian', 'Bui', 'Cai', 'Cao', 'Cen', 'Chai', 'Chaim', 'Chan', 'Chang', ..., 'Zhang', 'Zhi', 'Zhuan', 'Zhui']
# 构建的category_lines形如:{'English':['Lily', "Susan"], "Chinese":["Zhang"]} category_lines = {} # all_categories形如:['English', ..., 'Chinese'] all_categories = [] # 读取指定路径下的txt文件,使用glob,path中可以使用正则表达式 for filename in glob.glob(data_path + '*.txt'): # 获取每个文件的文件名, 就是对应的名字类别 category = os.path.splitext(os.path.basename(filename))[0] # 将其装到all_categories all_categories.append(category) # 然后读取每个文件内容,形成名字列表 lines = readLines(filename) # 按照对应类别,将名字列表写入category_lines字典中 category_lines[category] = lines n_categories = len(all_categories) print('n_categories', n_categories) print(category_lines['Italian'][:10]) n_categories 18 ['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni', 'Abatescianni', 'Abba', 'Abbadelli', 'Abbascia', 'Abbatangelo']
def lineToTensor(line): # 首先初始化一个0张量,它的形状(len(line), 1, n_letters) # 代表人名中每一个字母用一个1乘n_letters张量表示 tensor = torch.zeros(len(line), 1, n_letters) # 遍历这个人名中每个字符索引和字符 for li, letter in enumerate(line): # 使用字符串方法找到每个字符在all_letters中索引 # 它也是我们生成onehot张量中1的索引位置 tensor[li][0][all_letters.find(letter)] = 1 # 返回结果 return tensor line = 'Bai' line_tensor = lineToTensor(line) print("line_tensor: ", line_tensor) line_tensor: tensor([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], [[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], [[0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]])
# 使用nn.RNN构建完成传统RNN使用类 class RNN(nn.Module): def __init__(self, input_size, hidden_size, output_size, num_layers = 1): ''' Parameters ---------- input_size : 输入最后一维尺寸. hidden_size : 隐层最后一维尺寸. output_size : TYPE 输出的维度. num_layers : TYPE, optional RNN层数. The default is 1. Returns ------- None. ''' super(RNN, self).__init__() # 将参数传入 self.input_size = input_size self.output_size = output_size self.hidden_size = hidden_size self.num_layers = num_layers # 实例化定义的nn.RNN,它的三个参数分别是input_size, hidden_size, num_layers self.rnn = nn.RNN(input_size, hidden_size, num_layers) # 实例化nn.Linear,这个线性层用于将nn.RNN的输出维度转化为指定输出维度 self.linear = nn.Linear(hidden_size, output_size) # 实例化nn中预定的Softmax层,用于从输出层获得类别结果 self.softmax = nn.LogSoftmax(dim=-1) def forward(self, input, hidden): ''' 完成RNN中的主要逻辑 Parameters ---------- input : 输入张量,形状是1×n_letters. hidden : 隐层张量,它的形状是self.num_layers × 1 × self.hidden_size. Returns ------- self.softmax(self.linear(rr)), hn. ''' # 因为预定义的nn.RNN要求输入维度一定是三维张量,所以扩展一个维度 input = input.unsqueeze(0) # 将input和hidden输入到传统RNN实例化对象中,如果num_layers=1,rr恒等于hn rr, hn = self.rnn(input, hidden) # 将从RNN中获得的结果通过线性变换和softmanx返回,同时返回hn作为后续RNN的输入 return self.softmax(self.linear(rr)), hn def initHidden(self): ''' 初始化隐层张量 Returns ------- torch.zeros(self.num_layers, 1, self.hidden_size). ''' # 初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量 return torch.zeros(self.num_layers, 1, self.hidden_size)
# 使用nn.LSTM构建完成LSTM使用类 class LSTM(nn.Module): def __init__(self, input_size, hidden_size, output_size, num_layers = 1): ''' 初始化函数参数与RNN相同 Parameters ---------- input_size : 输入最后一维尺寸. hidden_size : 隐层最后一维尺寸. output_size : TYPE 输出的维度. num_layers : TYPE, optional RNN层数. The default is 1. Returns ------- None. ''' super(LSTM, self).__init__() # 将参数传入 self.input_size = input_size self.output_size = output_size self.hidden_size = hidden_size self.num_layers = num_layers # 实例化定义的nn.LSTM,它的三个参数分别是input_size, hidden_size, num_layers self.lstm = nn.LSTM(input_size, hidden_size, num_layers) # 实例化nn.Linear,这个线性层用于将nn.RNN的输出维度转化为指定输出维度 self.linear = nn.Linear(hidden_size, output_size) # 实例化nn中预定的Softmax层,用于从输出层获得类别结果 self.softmax = nn.LogSoftmax(dim=-1) def forward(self, input, hidden, c): ''' 完成LSTM中的主要逻辑 Parameters ---------- input : 输入张量,形状是1×n_letters. hidden : 隐层张量,它的形状是self.num_layers × 1 × self.hidden_size. c : LSTM中的细胞状态张量. Returns ------- self.softmax(self.linear(rr)), hn, c. ''' # 因为预定义的nn.RNN要求输入维度一定是三维张量,所以扩展一个维度 input = input.unsqueeze(0) # 将input和hidden以及初始化的c输入到lstm中 rr, (hn, c) = self.lstm(input, (hidden, c)) # 将从RNN中获得的结果通过线性变换和softmanx返回,同时返回hn作为后续RNN的输入并更新细胞状态 return self.softmax(self.linear(rr)), hn, c def initHiddenAndC(self): ''' 初始化隐层张量 Returns ------- hidden, c. ''' # 初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量以及c,他们形状相同 c = hidden = torch.zeros(self.num_layers, 1, self.hidden_size) return hidden, c
# 使用nn.GRU构建完成GRU使用类 # GRU与传统RNN的外部形式相同,都是只传递隐层张量,因此只需要更改预定义层的名字 class GRU(nn.Module): def __init__(self, input_size, hidden_size, output_size, num_layers = 1): ''' Parameters ---------- input_size : 输入最后一维尺寸. hidden_size : 隐层最后一维尺寸. output_size : TYPE 输出的维度. num_layers : TYPE, optional RNN层数. The default is 1. Returns ------- None. ''' super(GRU, self).__init__() # 将参数传入 self.input_size = input_size self.output_size = output_size self.hidden_size = hidden_size self.num_layers = num_layers # 实例化定义的nn.GRU,它的三个参数分别是input_size, hidden_size, num_layers self.gru = nn.GRU(input_size, hidden_size, num_layers) # 实例化nn.Linear,这个线性层用于将nn.RNN的输出维度转化为指定输出维度 self.linear = nn.Linear(hidden_size, output_size) # 实例化nn中预定的Softmax层,用于从输出层获得类别结果 self.softmax = nn.LogSoftmax(dim=-1) def forward(self, input, hidden): ''' 完成RNN中的主要逻辑 Parameters ---------- input : 输入张量,形状是1×n_letters. hidden : 隐层张量,它的形状是self.num_layers × 1 × self.hidden_size. Returns ------- self.softmax(self.linear(rr)), hn. ''' # 因为预定义的nn.GRU要求输入维度一定是三维张量,所以扩展一个维度 input = input.unsqueeze(0) # 将input和hidden输入到传统GRU实例化对象中,如果num_layers=1,rr恒等于hn rr, hn = self.gru(input, hidden) # 将从RNN中获得的结果通过线性变换和softmanx返回,同时返回hn作为后续GRU的输入 return self.softmax(self.linear(rr)), hn def initHidden(self): ''' 初始化隐层张量 Returns ------- torch.zeros(self.num_layers, 1, self.hidden_size). ''' # 初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量 return torch.zeros(self.num_layers, 1, self.hidden_size)
# 因为是onehot编码,输入张量最后一维尺寸就是n_letters input_size = n_letters # 定义隐层的最后一维尺寸大小 n_hidden = 128 # 输出尺寸为语言类别总数n_categories output_size = n_categories # num_layer使用默认值1 # 假如以字母B作为RNN首次输入,通过lineToTensor转为张量 # 因为lineToTensor输出是三维张量,而RNN类需要二维张量 # 因此需要使用squeeze(0)降低一个维度 input = lineToTensor('B').squeeze(0) # 初始化一个三维的隐层0张量,也就是初始细胞状态张量 hidden = c = torch.zeros(1, 1, n_hidden) # 调用 rnn = RNN(n_letters, n_hidden, n_categories) lstm = LSTM(n_letters, n_hidden, n_categories) gru = GRU(n_letters, n_hidden, n_categories) rnn_output, next_hidden = rnn(input, hidden) print('rnn:', rnn_output) lstm_output, next_hidden, c = lstm(input, hidden, c) print('lstm:', lstm_output) gru_output, next_hidden = gru(input, hidden) print('gru:', gru_output) rnn: tensor([[[-2.8331, -2.9564, -2.7444, -2.8873, -3.0144, -2.8921, -2.9993, -2.8616, -2.8584, -2.9508, -2.8577, -2.9920, -2.9023, -2.8029, -2.7954, -2.9758, -2.9047, -2.8477]]], grad_fn=<LogSoftmaxBackward>) lstm: tensor([[[-2.8993, -2.9749, -2.8188, -2.8973, -2.8329, -2.8795, -2.9100, -2.9440, -2.9793, -2.8052, -2.8427, -2.9326, -2.8233, -2.8597, -2.9480, -2.8460, -2.8935, -2.9671]]], grad_fn=<LogSoftmaxBackward>) gru: tensor([[[-2.8107, -2.9070, -2.8710, -2.8599, -2.9110, -3.0012, -3.0116, -2.8663, -2.8228, -2.8659, -2.8145, -2.8629, -2.9072, -2.8821, -2.9657, -2.9611, -2.8662, -2.8690]]], grad_fn=<LogSoftmaxBackward>)
def categoryFromOutput(output): ''' 从输出结果中获取指定类别,参数为输出张量output Parameters ---------- output : 输出张量output. Returns ------- 语言类别和索引值. ''' # 从输出张量中返回最大的值和索引对象,这里主要需要这个索引 top_n, top_i = output.topk(1) # top_i对象中取出索引的值 category_i = top_i[0].item() # 根据索引值获得对应语言类别,返回语言类别和索引值 return all_categories[category_i], category_i category, category_i = categoryFromOutput(gru_output) print('category:', category) print('category_i:', category_i) category: Japanese category_i: 10
def randomTrainingExample(): ''' 该函数用于随机产生训练数据 Returns ------- category, line, category_tensor, line_tensor. ''' # 首先使用random的choice方法从all_categories随机选择一个类别 category = random.choice(all_categories) # 然后通过category.lines字典取该类对应的名字列表 # 之后再从列表中随机取一个名字 line = random.choice(category_lines[category]) # 接着将这个类别在所有类别列表中的索引封装成tensor,得到张量类别category_tensor category_tensor = torch.tensor([all_categories.index(category)], dtype=torch.long) # 最后将随即取到的名字通过函数lineToTensor转化为onehot张量表示 line_tensor = lineToTensor(line) return category, line, category_tensor, line_tensor
# 定义损失函数nn.NLLLoss,因为RNN的最后一层是nn.LogSoftmax,两者内部计算逻辑吻合 criterion = nn.NLLLoss() # 设置学习率 learning_rate = 0.05 def trainRNN(category_tensor, line_tensor): ''' 定义训练函数 Parameters ---------- category_tensor : 训练数据的标签(张量). line_tensor : 训练数据(张量). Returns ------- 结果和损失值. ''' # 在函数中,首先通过实例化对象rnn初始化隐层张量 hidden = rnn.initHidden() # 然后将模型结构中的梯度归零 rnn.zero_grad() # 下面开始训练,将训练数据line_tensor每个字符 for i in range(line_tensor.size()[0]): output, hidden = rnn(line_tensor[i], hidden) # 因为对象由nn.RNN实例化得到,最终输出结果是三维张量,为满足category_tensor # 进行对比计算损失,需要减少第一个维度,使用squeeze()方法 loss = criterion(output.squeeze(0), category_tensor) # 损失进行反向传播 loss.backward() # 更新模型中所有参数 for p in rnn.parameters(): # 将参数张量表示与参数的梯度乘以学习率的结果相加以此来更新参数 p.data.add_(-learning_rate, p.grad.data) # 返回结果和损失的值 return output, loss.item()
# 与传统RNN相比多出了细胞状态 def trainLSTM(category_tensor, line_tensor): hidden, c = lstm.initHiddenAndC() lstm.zero_grid() for i in range(line_tensor.size()[0]): output, hidden, c = lstm(line_tensor[i], hidden, c) loss = criterion(output.squeeze(0), category_tensor) loss.backward() for p in lstm.parameters(): p.data.add_(-learning_rate, p.grad.data) return output, loss.item() # 代码层面与RNN完全相同 def trainGRU(category_tensor, line_tensor): hidden = gru.initHidden() gru.zero_grad() for i in range(line_tensor.size()[0]): output, hidden = gru(line_tensor[i], hidden) loss = criterion(output.squeeze(0), category_tensor) loss.backward() for p in gru.parameters(): p.data.add_(-learning_rate, p.grad.data) return output, loss.item()
def timeSince(since): ''' 获得每次打印的训练耗时 Parameters ---------- since : 训练开始的时间. Returns ------- 指定格式的耗时. ''' now = time.time() s = now - since m = math.floor(s / 60) # 向下取整 s -= m*60 return '%dmins %dsecs' % (m, s)
# 设置训练迭代次数 n_iters = 1000 # 设置结果的打印间隔 print_every = 50 # 设置绘制损失曲线上的制图间隔 plot_every = 10 def train(train_type_fn): ''' 训练过程的日志打印函数 Parameters ---------- train_type_fn : 选择哪种模型训练函数,例如trainRNN. Returns ------- None. ''' # 每个制图间隔损失保存列表 all_losses = [] # 获得训练开始时间戳 start = time.time() # 设置初始间隔损失为0 current_loss = 0 # 从1开始进行训练迭代,共n_iters次 for iter in range(1, n_iters+1): # 通过randomTrainingExample函数随机获取一组训练数据和对应类别 category, line, category_tensor, line_tensor = randomTrainingExample() # 将训练数据和对应类别张量表示传入到train函数中 output, loss = train_type_fn(category_tensor, line_tensor) # 计算制图间隔中的总损失 current_loss += loss # 如果迭代数能够打印间隔 if iter % print_every ==0: # 取该迭代步上的output通过categoryFromOutput函数获得对应类别和类别索引 guess, guess_i = categoryFromOutput(output) # 和真实类别做比较 correct = 'True' if guess == category else 'False(%s)' % category # 打印迭代步,迭代步百分比,当前训练耗时,损失,该步预测名字,是否正确 print('%d %d%% (%s) %.4f %s / %s %s' % (iter, iter/n_iters*100, timeSince(start), loss, line, guess, correct)) # 绘图间隔 if iter % plot_every ==0: # 将保存该间隔的平均损失到all_losses列表中 all_losses.append(current_loss / plot_every) # 间隔损失重置为0 current_loss = 0 # 返回对应总损失列表和训练耗时 return all_losses, int(time.time() - start)
# 设置训练迭代次数 n_iters = 1000 # 设置结果的打印间隔 print_every = 50 # 设置绘制损失曲线上的制图间隔 plot_every = 10 def train(train_type_fn): ''' 训练过程的日志打印函数 Parameters ---------- train_type_fn : 选择哪种模型训练函数,例如trainRNN. Returns ------- None. ''' # 每个制图间隔损失保存列表 all_losses = [] # 获得训练开始时间戳 start = time.time() # 设置初始间隔损失为0 current_loss = 0 # 从1开始进行训练迭代,共n_iters次 for iter in range(1, n_iters+1): # 通过randomTrainingExample函数随机获取一组训练数据和对应类别 category, line, category_tensor, line_tensor = randomTrainingExample() # 将训练数据和对应类别张量表示传入到train函数中 output, loss = train_type_fn(category_tensor, line_tensor) # 计算制图间隔中的总损失 current_loss += loss # 如果迭代数能够打印间隔 if iter % print_every ==0: # 取该迭代步上的output通过categoryFromOutput函数获得对应类别和类别索引 guess, guess_i = categoryFromOutput(output) # 和真实类别做比较 correct = 'True' if guess == category else 'False(%s)' % category # 打印迭代步,迭代步百分比,当前训练耗时,损失,该步预测名字,是否正确 print('%s: %d %d%% (%s) %.4f %s / %s %s' % (train_type_fn.__name__, iter, iter/n_iters*100, timeSince(start), loss, line, guess, correct)) with open(train_type_fn.__name__ + '.txt', 'a') as file0: print('%d %d%% (%s) %.4f %s / %s %s' % (iter, iter/n_iters*100, timeSince(start), loss, line, guess, correct), file=file0) # 绘图间隔 if iter % plot_every ==0: # 将保存该间隔的平均损失到all_losses列表中 all_losses.append(current_loss / plot_every) # 间隔损失重置为0 current_loss = 0 # 返回对应总损失列表和训练耗时 return all_losses, int(time.time() - start)
200 20% (0m 1s) 2.8526 Bellamy / English False(French)
400 40% (0m 2s) 1.8337 Antonopoulos / Greek True
800 80% (0m 7s) 3.2786 Woo / Czech False(Chinese)
可以发现不管是哪一种模型,False都占了大多数。这样的结果主要原因是训练次数太少,输出结果并不理想。
下面是运行十万次后的图象:
损失对比曲线分析:模型训练的损失降低快慢代表模型收敛程度,由图可知,传统RNN模型的收敛情况最好,然后是GRU,最后是LSTM。这是因为:我们当前处理的文本数据是人名,长度有限,且长距离字母间基本没有特定关联,因此无法发挥改进模型LSTM和GRU的长距离捕捉语义关联的优势,所以在以后的模型选用时,要通过对任务的分析以及实验对比,选择最合适的模型。
训练耗时对比图分析:模型训练的耗时长短代表模型的计算复杂度。我们可以根据图发现,传统RNN的复杂度很低,几乎是后两者的一半,然后是GRU,最后是LSTM。
def evaluateRNN(line_tensor): ''' 评估函数,逻辑和训练函数相同 Parameters ---------- line_tensor : 名字的张量表示. Returns ------- None. ''' # 初始化隐层张量 hidden = rnn.initHidden() # 将评估数据line_tensor每个字符逐个传入rnn中 for i in range(line_tensor.size()[0]): output, hidden = rnn(line_tensor[i], hidden) # 获得输出结果 return output.squeeze(0) def evaluateLSTM(line_tensor): hidden, c = lstm.initHiddenAndC() for i in range(line_tensor.size()[0]): output, hidden, c = lstm(line_tensor[i], hidden, c) return output.squeeze(0) def evaluateGRU(line_tensor): hidden = gru.initHidden() for i in range(line_tensor.size()[0]): output, hidden = gru(line_tensor[i], hidden) return output.squeeze(0) #line = 'Zhang' #line_tensor = lineToTensor(line) #rnn_output = evaluateRNN(line_tensor) #lstm_output = evaluateLSTM(line_tensor) #gru_output = evaluateGRU(line_tensor) #print(rnn_output) #print(lstm_output) #print(gru_output) tensor([[-3.5773, -2.0988, -5.1781, -3.1590, -3.2170, -4.0893, -4.3061, -4.2271, -3.2636, -3.1062, -3.9020, -0.7948, -5.3064, -3.8007, -4.0158, -4.0093, -4.5094, -2.4414]], grad_fn=<SqueezeBackward1>) tensor([[-2.6393, -2.8724, -3.0469, -2.9063, -2.9979, -2.7807, -2.8535, -2.8309, -2.8880, -3.0629, -3.0885, -2.7007, -2.8724, -2.9484, -2.8637, -2.7832, -2.8150, -3.2597]], grad_fn=<SqueezeBackward1>) tensor([[-2.7454, -2.5936, -2.8496, -2.5222, -3.2314, -3.2224, -2.8892, -2.9692, -2.6086, -3.1576, -3.1461, -2.8338, -3.1694, -2.8322, -2.7507, -2.9711, -3.1019, -2.8547]], grad_fn=<SqueezeBackward1>)
def predict(input_line, evaluate_fn, n_predictions=3): ''' 预测函数 Parameters ---------- input_line : 输入的名字. evaluate_fn : 评估的模型函数,RNN,LSTM,GRU n_predictions : 要娶最有可能的多少个. The default is 3. Returns ------- None. ''' print('\n> %s' % input_line) # 不进行求梯度 with torch.no_grad(): # 使输入名字转为张量表示,并使用evaluate_fn函数获得预测输出 output = evaluate_fn(lineToTensor(input_line)) # 取最大的值和索引 topv, topi = output.topk(n_predictions, 1, True) # 创建承装结果列表 predictions = [] # 遍历n_predictions for i in range(n_predictions): # 从topv中取出output值 value = topv[0][i].item() # 取出索引并找到对应类别 category_index = topi[0][i].item() # 打印output值,和对应类别 print('(%.2f) %s' % (value, all_categories[category_index])) # 将结果装进predictions中 predictions.append([value, all_categories[category_index]]) return predictions # 简单调用验证 for evaluate_fn in [evaluateRNN, evaluateLSTM, evaluateGRU]: print('%s:\n', evaluate_fn.__name__) predict('Dovesky', evaluate_fn) predict('Jackson', evaluate_fn) predict('Satoshi', evaluate_fn) RNN > Dovesky (-0.08) Russian (-3.15) English (-3.84) Czech > Jackson (-0.28) Scottish (-1.77) Russian (-2.69) English > Satoshi (-0.65) Arabic (-1.84) Japanese (-1.97) Italian LSTM > Dovesky (-1.18) Russian (-1.36) Polish (-1.56) Czech > Jackson (-0.41) Scottish (-2.17) Polish (-2.50) English > Satoshi (-0.32) Japanese (-1.93) Arabic (-2.54) Polish GRU > Dovesky (-0.85) Czech (-1.19) Russian (-2.45) English > Jackson (-0.30) Scottish (-2.20) English (-3.18) Polish > Satoshi (-0.80) Japanese (-0.96) Arabic (-2.70) Polish
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。