import numpy as np class Tensor(object): def __init__(self, data, autograd=False, creators=None, creation_op=None, id=None): self.data = np.array(data) self.autograd = autograd self.grad = None if (id is None): self.id = np.random.randint(0, 100000) else: self.id = id self.creators = creators self.creation_op = creation_op self.children = {} if (creators is not None): for c in creators: if (self.id not in c.children): c.children[self.id] = 1 else: c.children[self.id] += 1 def all_children_grads_accounted_for(self): for id, cnt in self.children.items(): if (cnt != 0): return False return True def backward(self, grad=None, grad_origin=None): if (self.autograd): if (grad is None): grad = Tensor(np.ones_like(self.data)) if (grad_origin is not None): if (self.children[grad_origin.id] == 0): raise Exception("cannot backprop more than once") else: self.children[grad_origin.id] -= 1 if (self.grad is None): self.grad = grad else: self.grad += grad # grads must not have grads of their own assert grad.autograd == False # only continue backpropping if there's something to # backprop into and if all gradients (from children) # are accounted for override waiting for children if # "backprop" was called on this variable directly if (self.creators is not None and (self.all_children_grads_accounted_for() or grad_origin is None)): if (self.creation_op == "add"): self.creators[0].backward(self.grad, self) self.creators[1].backward(self.grad, self) if (self.creation_op == "sub"): self.creators[0].backward(Tensor(self.grad.data), self) self.creators[1].backward(Tensor(self.grad.__neg__().data), self) if (self.creation_op == "mul"): new = self.grad * self.creators[1] self.creators[0].backward(new, self) new = self.grad * self.creators[0] self.creators[1].backward(new, self) if (self.creation_op == "mm"): c0 = self.creators[0] c1 = self.creators[1] new = self.grad.mm(c1.transpose()) c0.backward(new) new = self.grad.transpose().mm(c0).transpose() c1.backward(new) if (self.creation_op == "transpose"): self.creators[0].backward(self.grad.transpose()) if ("sum" in self.creation_op): dim = int(self.creation_op.split("_")[1]) self.creators[0].backward(self.grad.expand(dim, self.creators[0].data.shape[dim])) if ("expand" in self.creation_op): dim = int(self.creation_op.split("_")[1]) self.creators[0].backward(self.grad.sum(dim)) if (self.creation_op == "neg"): self.creators[0].backward(self.grad.__neg__()) if (self.creation_op == "sigmoid"): ones = Tensor(np.ones_like(self.grad.data)) self.creators[0].backward(self.grad * (self * (ones - self))) if (self.creation_op == "tanh"): ones = Tensor(np.ones_like(self.grad.data)) self.creators[0].backward(self.grad * (ones - (self * self))) if (self.creation_op == "index_select"): new_grad = np.zeros_like(self.creators[0].data) indices_ = self.index_select_indices.data.flatten() grad_ = grad.data.reshape(len(indices_), -1) for i in range(len(indices_)): new_grad[indices_[i]] += grad_[i] self.creators[0].backward(Tensor(new_grad)) if (self.creation_op == "cross_entropy"): dx = self.softmax_output - self.target_dist self.creators[0].backward(Tensor(dx)) def __add__(self, other): if (self.autograd and other.autograd): return Tensor(self.data + other.data, autograd=True, creators=[self, other], creation_op="add") return Tensor(self.data + other.data) def __neg__(self): if (self.autograd): return Tensor(self.data * -1, autograd=True, creators=[self], creation_op="neg") return Tensor(self.data * -1) def __sub__(self, other): if (self.autograd and other.autograd): return Tensor(self.data - other.data, autograd=True, creators=[self, other], creation_op="sub") return Tensor(self.data - other.data) def __mul__(self, other): if (self.autograd and other.autograd): return Tensor(self.data * other.data, autograd=True, creators=[self, other], creation_op="mul") return Tensor(self.data * other.data) def sum(self, dim): if (self.autograd): return Tensor(self.data.sum(dim), autograd=True, creators=[self], creation_op="sum_" + str(dim)) return Tensor(self.data.sum(dim)) def expand(self, dim, copies): trans_cmd = list(range(0, len(self.data.shape))) trans_cmd.insert(dim, len(self.data.shape)) new_data = self.data.repeat(copies).reshape(list(self.data.shape) + [copies]).transpose(trans_cmd) if (self.autograd): return Tensor(new_data, autograd=True, creators=[self], creation_op="expand_" + str(dim)) return Tensor(new_data) def transpose(self): if (self.autograd): return Tensor(self.data.transpose(), autograd=True, creators=[self], creation_op="transpose") return Tensor(self.data.transpose()) def mm(self, x): if (self.autograd): return Tensor(self.data.dot(x.data), autograd=True, creators=[self, x], creation_op="mm") return Tensor(self.data.dot(x.data)) def sigmoid(self): if (self.autograd): return Tensor(1 / (1 + np.exp(-self.data)), autograd=True, creators=[self], creation_op="sigmoid") return Tensor(1 / (1 + np.exp(-self.data))) def tanh(self): if (self.autograd): return Tensor(np.tanh(self.data), autograd=True, creators=[self], creation_op="tanh") return Tensor(np.tanh(self.data)) def index_select(self, indices): if (self.autograd): new = Tensor(self.data[indices.data], autograd=True, creators=[self], creation_op="index_select") new.index_select_indices = indices return new return Tensor(self.data[indices.data]) def cross_entropy(self, target_indices): temp = np.exp(self.data) softmax_output = temp / np.sum(temp, axis=len(self.data.shape) - 1, keepdims=True) t = target_indices.data.flatten() p = softmax_output.reshape(len(t), -1) target_dist = np.eye(p.shape[1])[t] loss = -(np.log(p) * (target_dist)).sum(1).mean() if (self.autograd): out = Tensor(loss, autograd=True, creators=[self], creation_op="cross_entropy") out.softmax_output = softmax_output out.target_dist = target_dist return out return Tensor(loss) def softmax(self): temp = np.exp(self.data) softmax_output = temp / np.sum(temp, axis=len(self.data.shape) - 1, keepdims=True) return softmax_output def __repr__(self): return str(self.data.__repr__()) def __str__(self): return str(self.data.__str__()) class Layer(object): def __init__(self): self.parameters = list() def get_parameters(self): return self.parameters class Tanh(Layer): def __init__(self): super().__init__() def forward(self, input): return input.tanh() class Sigmoid(Layer): def __init__(self): super().__init__() def forward(self, input): return input.sigmoid() ''' 损失函数(CrossEntropyLoss) 计算交叉熵损失。 功能:定义一个交叉熵损失函数。 作用:计算模型预测值与真实标签之间的交叉熵损失。 ''' class CrossEntropyLoss(object): def __init__(self): # 功能:调用父类的初始化方法。 super().__init__() # 作用:确保 CrossEntropyLoss 类继承了父类的属性和方法。 ''' input:模型的输出,通常是一个 Tensor 对象,表示预测的概率分布。 target:真实标签,通常是一个 Tensor 对象,表示真实的类别索引。 调用 cross_entropy 方法 input.cross_entropy(target): 调用 input 的 cross_entropy 方法,计算交叉熵损失。 这里的 cross_entropy 方法需要在 Tensor 类中实现。 返回值 功能:返回交叉熵损失值。 作用:用于衡量模型预测值与真实标签之间的差异。 ''' def forward(self, input, target): return input.cross_entropy(target) class Sequential(Layer): def __init__(self, layers=list()): super().__init__() self.layers = layers def add(self, layer): self.layers.append(layer) def forward(self, input): for layer in self.layers: input = layer.forward(input) return input def get_parameters(self): params = list() for l in self.layers: params += l.get_parameters() return params ''' (2.1) 嵌入层(Embedding) 将词的索引映射为固定维度的向量。 index_select 方法根据输入索引选择对应的嵌入向量。 说明: Embedding 类是深度学习中的一个重要组件,用于将离散的符号(如单词、类别等)映射到连续的向量空间。 这种映射通常被称为词嵌入(Word Embedding),是自然语言处理(NLP)任务中的基础操作。 下面我会详细讲解 Embedding 类的实现和功能。 ''' class Embedding(Layer): # 1. 类的定义 功能:定义一个嵌入层,继承自 Layer 类。 作用:将离散的符号(如单词索引)映射到连续的向量空间。 def __init__(self, vocab_size, dim): # 2. 初始化方法:__init__ super().__init__() # (1) super().__init__() 功能:调用父类 Layer 的初始化方法。 作用:确保 Embedding 类继承了 Layer 类的属性和方法。 self.vocab_size = vocab_size # (2)self.vocab_size = vocab_size #功能:存储词汇表的大小。 #作用:词汇表的大小决定了嵌入矩阵的行数。 self.dim = dim # (3) self.dim = dim # 功能:初始化嵌入矩阵。 # 细节: # np.random.rand(vocab_size, dim):生成一个形状为 (vocab_size, dim) 的随机矩阵,元素值在 [0, 1) 之间。 # - 0.5:将元素值调整到 [-0.5, 0.5) 之间。 # / dim:将元素值缩放到 [-0.5/dim, 0.5/dim) 之间,这是一种常见的初始化方法。 # Tensor(..., autograd=True):将矩阵封装为 Tensor 对象,并启用自动求导。 # this random initialiation style is just a convention from word2vec self.weight = Tensor((np.random.rand(vocab_size, dim) - 0.5) / dim, autograd=True) # 功能:将嵌入矩阵添加到模型的参数列表中。 # 作用:在训练过程中,优化器会更新这些参数。 self.parameters.append(self.weight) # 3. 前向传播方法:forward # (1) 输入参数 input # 功能:接受输入数据。 # 类型:input 是一个 Tensor 对象,通常包含词的索引。 def forward(self, input): return self.weight.index_select(input) ''' self.weight.index_select(input) 功能:根据输入索引从嵌入矩阵中选择对应的向量。 细节: self.weight 是嵌入矩阵,形状为 (vocab_size, dim)。 input 是词的索引,形状为 (batch_size, sequence_length) 或 (batch_size,)。 index_select 方法根据索引从嵌入矩阵中选择对应的行(即词向量) (3) 返回值 功能:返回选择的词向量。 作用:这些词向量可以作为后续层的输入。 ''' class Linear(Layer): def __init__(self, n_inputs, n_outputs): super().__init__() W = np.random.randn(n_inputs, n_outputs) * np.sqrt(2.0 / (n_inputs)) self.weight = Tensor(W, autograd=True) self.bias = Tensor(np.zeros(n_outputs), autograd=True) self.parameters.append(self.weight) self.parameters.append(self.bias) def forward(self, input): return input.mm(self.weight) + self.bias.expand(0, len(input.data)) class MSELoss(Layer): def __init__(self): super().__init__() def forward(self, pred, target): return ((pred - target) * (pred - target)).sum(0) ''' 优化器(SGD) 实现随机梯度下降(SGD)优化器。 ''' class SGD(object): def __init__(self, parameters, alpha=0.1): self.parameters = parameters self.alpha = alpha def zero(self): for p in self.parameters: p.grad.data *= 0 def step(self, zero=True): for p in self.parameters: p.data -= p.grad.data * self.alpha if (zero): p.grad.data *= 0 ''' (2.2) RNN 单元(RNNCell) 实现了一个简单的 RNN 单元。 forward 方法计算当前时间步的输出和隐藏状态。 init_hidden 方法初始化隐藏状态。 ''' class RNNCell(Layer): # 1. 类的定义 #功能:定义一个 RNN 单元,继承自 Layer 类。 #作用:处理序列数据,维护隐藏状态,并输出当前时间步的结果。 def __init__(self, n_inputs, n_hidden, n_output, activation='sigmoid'): # 初始化方法:__init__ super().__init__() # 功能:调用父类 Layer 的初始化方法。 作用:确保 RNNCell 类继承了 Layer 类的属性和方法。 self.n_inputs = n_inputs # n_inputs:输入数据的维度。 self.n_hidden = n_hidden # n_hidden:隐藏状态的维度。 self.n_output = n_output # n_output:输出数据的维度。 ''' activation:激活函数类型(支持 sigmoid 和 tanh)。 self.activation:根据 activation 参数选择激活函数。 Sigmoid():Sigmoid 激活函数。 Tanh():Tanh 激活函数。 ''' if (activation == 'sigmoid'): self.activation = Sigmoid() elif (activation == 'tanh'): self.activation == Tanh() else: raise Exception("Non-linearity not found") self.w_ih = Linear(n_inputs, n_hidden) # self.w_ih:输入到隐藏状态的线性变换层,形状为 (n_inputs, n_hidden)。 self.w_hh = Linear(n_hidden, n_hidden) # self.w_hh:隐藏状态到隐藏状态的线性变换层,形状为 (n_hidden, n_hidden)。 self.w_ho = Linear(n_hidden, n_output) # self.w_ho:隐藏状态到输出的线性变换层,形状为 (n_hidden, n_output)。 ''' self.parameters:将 w_ih、w_hh 和 w_ho 的参数添加到模型的参数列表中。 作用:在训练过程中,优化器会更新这些参数。 ''' self.parameters += self.w_ih.get_parameters() # self.parameters += self.w_hh.get_parameters() # self.parameters += self.w_ho.get_parameters() # ''' 输入参数: input:当前时间步的输入数据,形状为 (batch_size, n_inputs)。 hidden:上一个时间步的隐藏状态,形状为 (batch_size, n_hidden)。 ''' def forward(self, input, hidden): ''' 计算隐藏状态 ''' from_prev_hidden = self.w_hh.forward(hidden) # 将上一个时间步的隐藏状态 hidden 通过线性变换 w_hh 映射到当前时间步的隐藏状态空间。 combined = self.w_ih.forward( input) + from_prev_hidden # 将当前时间步的输入 input 通过线性变换 w_ih 映射到隐藏状态空间,并与 from_prev_hidden 相加。 new_hidden = self.activation.forward(combined) # 对相加后的结果应用激活函数,得到当前时间步的隐藏状态 new_hidden output = self.w_ho.forward(new_hidden) # 将当前时间步的隐藏状态 new_hidden 通过线性变换 w_ho 映射到输出空间。 return output, new_hidden # output:当前时间步的输出,形状为 (batch_size, n_output)。 new_hidden:当前时间步的隐藏状态,形状为 (batch_size, n_hidden)。 # 初始化隐藏状态方法:init_hidden # 作用:初始化隐藏状态。 # 返回值:一个全零的 Tensor,形状为 (batch_size, n_hidden)。 def init_hidden(self, batch_size=1): # batch_size:批量大小,默认为 1 return Tensor(np.zeros((batch_size, self.n_hidden)), autograd=True) import sys, random, math from collections import Counter import numpy as np import sys np.random.seed(0) f = open('shakesper.txt', 'r') raw = f.read() f.close() vocab = list(set(raw)) print("vocab") print(vocab) print("len(vocab)") print(len(vocab)) word2index = {} for i,word in enumerate(vocab): word2index[word]=i print("word2index[word]") print(word2index[word]) indices = np.array(list(map(lambda x:word2index[x],raw))) embed = Embedding(vocab_size=len(vocab), dim=512) model = RNNCell(n_inputs=512, n_hidden=512, n_output=len(vocab)) criterion = CrossEntropyLoss() optim = SGD(parameters=model.get_parameters() + embed.get_parameters(), alpha=0.05) print(raw[0:5]) print(indices[0:5]) ''' 假设你的数据集有 1000 个样本,每个样本是一个长度为 64 的字符序列。批次大小为 32,BPTT 步长为 16。那么: 批次大小(Batch Size): 每次训练迭代处理 32 个样本。 总共需要1000/32 =3 2 次迭代。 序列长度(Sequence Length): 每个样本是一个长度为 64 的序列。 BPTT 步长(Truncated Backpropagation Through Time): 每次反向传播只处理 16 个时间步。 ''' batch_size = 32 #总共分多少次迭代 bptt = 16 #每个序列反向传播的步长 n_batches = int((indices.shape[0]/(batch_size))) #每次迭代包含的序列个数。 ''' 截取索引数组以确保其长度是批次大小的整数倍。 将索引数组重新排列为一个二维数组,形状为 (n_batches, batch_size)。 转置数组以方便后续处理。 ''' trimmed_indices = indices[:n_batches*batch_size] batched_indices = trimmed_indices.reshape(batch_size,n_batches) batched_indices = batched_indices.transpose() ''' 将输入数据和目标数据分开,输入数据是所有字符的前一个字符,目标数据是对应的下一个字符 ''' input_batched_indices = batched_indices[0:-1] target_batched_indices = batched_indices[1:] ''' 计算可以进行 BPTT 的批次数量。 将输入和目标数据重新排列为三维数组,形状为 (n_bptt, bptt, batch_size)。 ''' n_bptt= int(((n_batches-1)/bptt)) input_batches = input_batched_indices[:n_bptt*bptt] input_batches = input_batches.reshape(n_bptt,bptt,batch_size) target_batches = target_batched_indices[:n_bptt*bptt] target_batches = target_batches.reshape(n_bptt,bptt,batch_size) print("batched_indices[0:5]") print(batched_indices[0:5]) print("input_batches[0][0:5]") print(input_batches[0][0:5]) print("target_batches[0][0:5]") print(target_batches[0][0:5]) ''' 初始化隐藏状态和输入字符。 使用模型生成文本,每次生成一个字符,直到生成指定数量的字符。 使用 Softmax 分布随机选择下一个字符。 这段代码实现了一个文本生成函数 generate_sample,它使用训练好的 RNN 模型生成指定长度的文本 n:生成文本的长度(默认为 30 个字符)。 init_char:初始字符(默认为空格 ' '),用于启动文本生成。 ''' def generate_sample(n =30, init_char=' '): ''' 初始化变量 s:用于存储生成的文本。 hidden:初始化隐藏状态。model.init_hidden(batch_size=1) 返回一个初始的隐藏状态,用于单个样本(batch_size=1)。 input:将初始字符转换为索引,并包装为 Tensor 对象。word2index[init_char] 将字符映射为索引。 ''' s = "" hidden = model.init_hidden(batch_size=1) input = Tensor(np.array([word2index[init_char]])) ''' 循环 n 次,每次生成一个字符,直到生成指定长度的文本。 ''' for i in range(n): ''' embed.forward(input): 将输入字符的索引通过嵌入层(embed)转换为嵌入向量。 嵌入层的作用是将离散的字符索引映射到连续的向量空间。 ''' rnn_input = embed.forward(input) ''' model.forward(input=rnn_input, hidden=hidden): 将嵌入向量和当前隐藏状态输入到 RNN 模型中。 模型返回输出向量 output 和新的隐藏状态 hidden。 ''' output,hidden = model.forward(input = rnn_input,hidden=hidden) ''' output.data *= 10: 将输出向量的值放大 10 倍。这一步是为了调整 Softmax 分布的“锐度”,使得生成的字符更加确定性(减少随机性)。 ''' output.data *=10 ''' 对输出向量应用 Softmax 函数,将其转换为概率分布。 ''' temp_dist = output.softmax() ''' 将 Softmax 分布归一化,确保概率和为 1。 ''' temp_dist /= temp_dist.sum() ''' 生成一个随机数(np.random.rand()),并与 Softmax 分布进行比较。 选择第一个大于随机数的索引 m。这种方法称为“按概率随机选择”,可以生成更自然的文本。 ''' m = (temp_dist > np.random.rand()).argmax() ''' 根据索引 m 获取对应的字符 c。 temp_dist > np.random.rand():生成一个布尔数组,表示每个字符的概率是否大于随机数。 .argmax():选择第一个大于随机数的索引 m。 vocab[m]:根据索引 m 获取对应的字符。 为什么引入随机性? 增加多样性: 如果总是选择概率最大的词,生成的文本可能会非常单调,尤其是在训练数据中某些模式非常频繁的情况下。引入随机性可以生成更多样化的文本。 避免局部最优: 在训练数据中,某些字符组合可能非常频繁,直接选择概率最大的词可能会导致模型陷入局部最优,生成重复的模式。随机选择可以避免这种情况。 模拟人类语言的不确定性: 人类语言本身具有一定的不确定性。引入随机性可以生成更接近人类语言的文本。 ''' c = vocab[m] ''' 将新生成的字符索引包装为 Tensor 对象,作为下一次输入。 ''' input = Tensor(np.array([m])) ''' 将新生成的字符添加到生成的文本中。 ''' s += c return s #print(generate_sample(n=2000,init_char='\n')) ''' 这段代码实现了一个简单的 RNN 训练循环,用于训练字符级语言模型 功能:定义训练函数 train,参数 iterations 指定训练的总迭代次数(即训练的轮数)。 ''' def train(iterations=100): for iter in range(iterations): ''' 功能:在每次迭代开始时,初始化变量: total_loss:用于累计当前迭代的总损失。 n_loss:未使用的变量,可能是为后续扩展预留的。 ''' total_loss = 0 n_loss = 0 ''' 功能:初始化 RNN 的隐藏状态。model.init_hidden(batch_size=batch_size) 会返回一个初始的隐藏状态,其大小与批次大小一致。隐藏状态用于存储序列的历史信息。 ''' hidden = model.init_hidden(batch_size=batch_size) ''' 功能:遍历所有输入批次。input_batches 是一个三维数组,形状为 (n_bptt, bptt, batch_size),表示每个批次的输入数据。 ''' for batch_i in range(len(input_batches)): ''' 功能:将隐藏状态包装为 Tensor 对象,并启用自动微分(autograd=True),以便在反向传播时计算梯度。 ''' hidden = Tensor(hidden.data,autograd=True) ''' loss:用于存储当前批次的累积损失。 losses:用于存储每个时间步的损失值。 ''' loss = None losses =list() ''' 内层循环:遍历每个时间步 ''' for t in range(bptt): ''' 将当前时间步的输入数据包装为 Tensor 对象,并启用自动微分。 ''' input = Tensor(input_batches[batch_i][t],autograd = True) ''' 将输入字符的索引通过嵌入层转换为词向量。 ''' rnn_input = embed.forward(input=input) ''' 将词向量和当前隐藏状态输入到 RNN 模型中,得到输出和新的隐藏状态。 ''' output,hidden = model.forward(input=rnn_input,hidden=hidden) ''' 将目标数据包装为 Tensor 对象,并启用自动微分。 ''' target = Tensor(target_batches[batch_i][t],autograd=True) ''' 计算当前时间步的损失值(例如交叉熵损失)。 ''' batch_loss = criterion.forward(output,target) ''' 将当前时间步的损失值存储到 losses 列表中。 ''' losses.append(batch_loss) ''' 累积当前批次的总损失。 ''' if(t ==0): loss = batch_loss else: loss = loss + batch_loss for loss in losses: "" ''' loss.backward():从累积的损失开始反向传播,计算所有参数的梯度。 ''' loss.backward() ''' optim.step():使用优化器(如 SGD)更新模型参数。 ''' optim.step() ''' 功能:将当前批次的总损失累加到 total_loss 中。 ''' total_loss += loss.data log = "" log += "\r Iter:" + str(iter) log += " - Batch " + str(batch_i+1) + "/" + str(len(input_batches)) #计算困惑度(Perplexity),用于评估模型性能。 log += " - Loss: " + str(np.exp(total_loss / (batch_i + 1))) ''' 在每个迭代的第一个批次生成一段样本文本,展示模型的生成能力。 ''' if(batch_i == 0): log += " - " + generate_sample(70,'\n').replace("\n"," ") if(batch_i % 10 ==0 or batch_i - 1 == len(input_batches)): #将日志输出到控制台。 sys.stdout.write(log) #功能:在每个迭代结束时,将优化器的学习率乘以 0.99,实现学习率衰减,有助于模型收敛。 optim.alpha *= 0.99 print() train() print(generate_sample(n=2000,init_char='\n')) ''' 训练30次后,按照文章的风格,写的一段2000单词的文本。可以用百度翻译进行翻译了。说明至少单词没有太大的错误。 语句还算通顺。有人想复现可以考虑使用100次训练试试,把损失值更加减少试试,看看能不能写出更好的文章。 实际看自己电脑的算力,我的电脑算了半天才能看着正常点。 Iter:0 - Batch 2171/2178 - Loss: 15.757976440237602 Iter:1 - Batch 2171/2178 - Loss: 9.206335793419358 Iter:2 - Batch 2171/2178 - Loss: 8.131693753138245 Iter:3 - Batch 2171/2178 - Loss: 7.497620743339918 Iter:4 - Batch 2171/2178 - Loss: 7.052746677924271 Iter:5 - Batch 2171/2178 - Loss: 6.710357936801665 Iter:6 - Batch 2171/2178 - Loss: 6.430444691181043 Iter:7 - Batch 2171/2178 - Loss: 6.199097439061244 Iter:8 - Batch 2171/2178 - Loss: 6.002201815930014 Iter:9 - Batch 2171/2178 - Loss: 5.83009425667503 Iter:10 - Batch 2171/2178 - Loss: 5.676567964264448 Iter:11 - Batch 2171/2178 - Loss: 5.540036337751631 Iter:12 - Batch 2171/2178 - Loss: 5.417842874574542 Iter:13 - Batch 2171/2178 - Loss: 5.308576411114698 Iter:14 - Batch 2171/2178 - Loss: 5.205805793937915 Iter:15 - Batch 2171/2178 - Loss: 5.106609738326981 Iter:16 - Batch 2171/2178 - Loss: 5.0101791402776845 Iter:17 - Batch 2171/2178 - Loss: 4.917905528329477 Iter:18 - Batch 2171/2178 - Loss: 4.831088050694672 Iter:19 - Batch 2171/2178 - Loss: 4.749952210206796 Iter:20 - Batch 2171/2178 - Loss: 4.669741603857014 Iter:21 - Batch 2171/2178 - Loss: 4.587301296098097 Iter:22 - Batch 2171/2178 - Loss: 4.513284317262715 Iter:23 - Batch 2171/2178 - Loss: 4.440020752749533 Iter:24 - Batch 2171/2178 - Loss: 4.375834052022757 Iter:25 - Batch 2171/2178 - Loss: 4.3029276423764244 Iter:26 - Batch 2171/2178 - Loss: 4.235140186831388 Iter:27 - Batch 2171/2178 - Loss: 4.162141675322302 Iter:28 - Batch 2171/2178 - Loss: 4.103686668096435 Iter:29 - Batch 2171/2178 - Loss: 4.041437694098596 As hour eyest the noble so bear the wordsh we have Warwick, and bear the wordsh we have Warwick, and Warwick, and Warwick, and the wordsh we have Warwick, and the Warwick, and Warwick, and the wordsh we have Warwick, and the wordsh we have bear the wordsh we have bear the wordsh we have Warwick, and the words, and beWarding be not all the plorWed WaWen the Warwick, and the wordsW the wordsh we have Warwick, and the wordsW the Warwick, and bear the Warwick, and Warwick, and the Warwick, and the wordsh we have Warwick, and the Warwick, and the Warwick, and Warwick, and beWarding beWave the noble so bear the wordsh we have bear the noble so bear the Warwick, and the noble so beWarding be Warwick, and the wordsh we have Warwick, and the wordsh we have Warwick, and Warwick, and the wordsh we have Warwick, and the wordsh we have bear the wordsh we have bear the Warwick, and the noble so bear the Warwick, and the wordsh we have Warwick, and the noble so beWarding beWave and Warwick, and the wordsh we have bear the wordsh we have Warwick, and the wordsh Warwick, and the Warwick, and the wordsh we have Warwick, and Warwick, and Warwick, and Warwick, and Warwick, and the wordsh we have Warwick, and Warwick, and the Warwick, and Warwick, and the wordsh we have bear the wordsh the noble so bear the wordsh we have bear the Warwick, and the noble so bear the wordsh we have Warwick, and the Warwick, and the Warwick, and the wordsh we have bear the wordsh we have Warwick, and beWave Warwick, and the wordsh we have bear the wordsW the wordsh we have bear the wordsh we have bear the wordsh we have Warwick, and Warwick, and Warwick, and Warwick, and the noble so beWarding beWWave Warwick, and the wordsh we have bear the Warwick, and Warwick, and the wordsh we have bear the wordsh we have bear the wordsh we have Warwick, and the wordsh we have Warwick, and the wordsh we have Warwick, and beWaWn the noble so bear the wordsW the Warwick, and the noble so bear the wordsh we have bear the '''