没有安装pytorch如何使用训练好的RNN模型

3
我已经用pytorch训练了一个RNN模型。由于某些奇怪的依赖关系问题,我无法在某个环境中安装pytorch,但我可以安装numpy、scipy和其他库。因此,我想在没有pytorch的情况下使用训练好的模型和网络定义进行预测。
我保存了模型的权重和状态字典,方式符合标准方式,但我也可以使用json/pickle文件或类似的方式进行保存。
我还有网络定义文件,但它在很多方面都依赖于pytorch。这是我的RNN网络定义。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import random

torch.manual_seed(1)
random.seed(1)
device = torch.device('cpu')

class RNN(nn.Module):
  def __init__(self, input_size, hidden_size, output_size,num_layers, matching_in_out=False, batch_size=1):
    super(RNN, self).__init__()
    self.input_size = input_size
    self.hidden_size = hidden_size
    self.output_size = output_size
    self.num_layers = num_layers
    self.batch_size = batch_size
    self.matching_in_out = matching_in_out #length of input vector matches the length of output vector 
    self.lstm = nn.LSTM(input_size, hidden_size,num_layers)
    self.hidden2out = nn.Linear(hidden_size, output_size)
    self.hidden = self.init_hidden()
  def forward(self, feature_list):
    feature_list=torch.tensor(feature_list)
    
    if self.matching_in_out:
      lstm_out, _ = self.lstm( feature_list.view(len( feature_list), 1, -1))
      output_space = self.hidden2out(lstm_out.view(len( feature_list), -1))
      output_scores = torch.sigmoid(output_space) #we'll need to check if we need this sigmoid
      return output_scores #output_scores
    else:
      for i in range(len(feature_list)):
        cur_ft_tensor=feature_list[i]#.view([1,1,self.input_size])
        cur_ft_tensor=cur_ft_tensor.view([1,1,self.input_size])
        lstm_out, self.hidden = self.lstm(cur_ft_tensor, self.hidden)
        outs=self.hidden2out(lstm_out)
      return outs
  def init_hidden(self):
    #return torch.rand(self.num_layers, self.batch_size, self.hidden_size)
    return (torch.rand(self.num_layers, self.batch_size, self.hidden_size).to(device),
            torch.rand(self.num_layers, self.batch_size, self.hidden_size).to(device))

我知道这个问题,但我愿意尽可能地降低级别。我可以使用numpy数组而不是张量,并使用reshape而不是view,而且我不需要设备设置。
基于上面的类定义,我所需要从torch中获取的组件只有以下内容才能从前向函数中获得输出:
  • nn.LSTM
  • nn.Linear
  • torch.sigmoid
我认为使用numpy可以很容易地实现Sigmoid函数。但是,我能否使用不涉及pytorch的东西来实现nn.LSTM和nn.Linear?另外,我如何将state dict中的权重用于新类?
因此,问题是,如何将这个RNN定义“翻译”成一个不需要pytorch的类,以及如何为它使用state dict权重? 或者,是否有一个“轻量级”的pytorch版本,我可以使用它来运行模型并产生结果?
编辑
我认为将nn.LSTM和nn.linear的numpy/scipy等效项包括进来可能会有用。这将帮助我们比较相同代码的numpy输出与torch输出,并为我们提供一些可重复使用的模块化代码/函数。具体来说,以下内容的numpy等效项将非常有用:
rnn = nn.LSTM(10, 20, 2)
input = torch.randn(5, 3, 10)
h0 = torch.randn(2, 3, 20)
c0 = torch.randn(2, 3, 20)
output, (hn, cn) = rnn(input, (h0, c0))

而且也适用于线性:

m = nn.Linear(20, 30)
input = torch.randn(128, 20)
output = m(input)

你好,您遇到的 glibc 依赖问题是什么? - Ollie Graham
@OllieGraham 在我的主机上,glibc版本是2.12,但pytorch需要2.14,我尝试了很多方法来安装它,但由于没有root访问权限,我无法安装。 - hmghaly
感谢@hmghaly - 我知道这并没有回答你的问题,但如果你的托管平台允许的话,我建议使用容器(例如Docker)部署模型,这样你就可以根据需要配置环境并安装所有相关依赖项。 - Ollie Graham
感谢@OllieGraham,但不幸的是,该托管服务有很多限制,包括安装软件、内存和文件数量等方面。 - hmghaly
2个回答

4

您应该尝试使用torch.onnx导出模型。该页面提供了一个示例,供您入手。

另一种选择是使用TorchScript,但这需要torch库。

这两者都可以在无需Python的情况下运行。 您可以在C++应用程序中加载torchscripthttps://pytorch.org/tutorials/advanced/cpp_export.html

ONNX更具可移植性,您可以在C#、Java或Javascript等语言中使用https://onnxruntime.ai/(甚至在浏览器上)

一个运行的示例

只需稍微修改您的示例即可解决我发现的错误

请注意,通过跟踪任何if / elif / else,for,while将被展开

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import random

torch.manual_seed(1)
random.seed(1)
device = torch.device('cpu')

class RNN(nn.Module):
  def __init__(self, input_size, hidden_size, output_size,num_layers, matching_in_out=False, batch_size=1):
    super(RNN, self).__init__()
    self.input_size = input_size
    self.hidden_size = hidden_size
    self.output_size = output_size
    self.num_layers = num_layers
    self.batch_size = batch_size
    self.matching_in_out = matching_in_out #length of input vector matches the length of output vector 
    self.lstm = nn.LSTM(input_size, hidden_size,num_layers)
    self.hidden2out = nn.Linear(hidden_size, output_size)
  def forward(self, x, h0, c0):
    lstm_out, (hidden_a, hidden_b) = self.lstm(x, (h0, c0))
    outs=self.hidden2out(lstm_out)
    return outs, (hidden_a, hidden_b)
  def init_hidden(self):
    #return torch.rand(self.num_layers, self.batch_size, self.hidden_size)
    return (torch.rand(self.num_layers, self.batch_size, self.hidden_size).to(device).detach(),
            torch.rand(self.num_layers, self.batch_size, self.hidden_size).to(device).detach())

# convert the arguments passed during onnx.export call
class MWrapper(nn.Module):
    def __init__(self, model):
        super(MWrapper, self).__init__()
        self.model = model;
    def forward(self, kwargs):
        return self.model(**kwargs)

运行一个示例。
rnn = RNN(10, 10, 10, 3)
X = torch.randn(3,1,10)
h0,c0  = rnn.init_hidden()
print(rnn(X, h0, c0)[0])

使用相同的输入来跟踪模型并导出ONNX文件。


torch.onnx.export(MWrapper(rnn), {'x':X,'h0':h0,'c0':c0}, 'rnn.onnx', 
                  dynamic_axes={'x':{1:'N'},
                               'c0':{1: 'N'},
                               'h0':{1: 'N'}
                               },
                  input_names=['x', 'h0', 'c0'],
                  output_names=['y', 'hn', 'cn']
                 )

请注意,您可以为某些输入的某些轴的维度使用符号值。未指定的维度将使用来自跟踪输入的值进行修复。默认情况下,LSTM 将使用第 1 维作为批处理。

接下来,我们加载 ONNX 模型并传递相同的输入。

import onnxruntime
ort_model = onnxruntime.InferenceSession('rnn.onnx')
print(ort_model.run(['y'], {'x':X.numpy(), 'c0':c0.numpy(), 'h0':h0.numpy()}))

谢谢您的建议。看起来很有前途,但由于某些原因它非常慢,我已经加载了示例中的模型大约10分钟,最终被杀掉了。也许是因为示例中的模型相当大,我需要想办法用我的较小模型来尝试它。 - hmghaly
将您的torch模型导出为ONNX,这也是我在没有访问pytorch的情况下会做的事情。通常,ONNX模型比Pytorch模型更快,但在导出过程中仍可能遇到问题。如果推理时间是一个重要问题,并且您有nvidia gpu,可以尝试使用TensorRT。 - Maxime D.

1
基本上,使用numpy实现并从你的pytorch模型中复制权重即可解决问题。 对于你的用例,你只需要进行前向传递,所以我们只需要实现这个。
#Set Parameters for a small LSTM network
input_size  = 2 # size of one 'event', or sample, in our batch of data
hidden_dim  = 3 # 3 cells in the LSTM layer
output_size = 1 # desired model output

num_layers=3
torch_lstm = RNN( input_size, 
                 hidden_dim ,
                 output_size,
                 num_layers,
                 matching_in_out=True
                 )

state = torch_lstm.state_dict() # state will capture the weights of your model

现在在使用numpy中的LSTM时,将使用以下函数: 从此链接中获取下面的代码:https://towardsdatascience.com/the-lstm-reference-card-6163ca98ae87
### NOT MY CODE
import numpy as np 
from scipy.special import expit as sigmoid

def forget_gate(x, h, Weights_hf, Bias_hf, Weights_xf, Bias_xf, prev_cell_state):
    forget_hidden  = np.dot(Weights_hf, h) + Bias_hf
    forget_eventx  = np.dot(Weights_xf, x) + Bias_xf
    return np.multiply( sigmoid(forget_hidden + forget_eventx), prev_cell_state )

def input_gate(x, h, Weights_hi, Bias_hi, Weights_xi, Bias_xi, Weights_hl, Bias_hl, Weights_xl, Bias_xl):
    ignore_hidden  = np.dot(Weights_hi, h) + Bias_hi
    ignore_eventx  = np.dot(Weights_xi, x) + Bias_xi
    learn_hidden   = np.dot(Weights_hl, h) + Bias_hl
    learn_eventx   = np.dot(Weights_xl, x) + Bias_xl
    return np.multiply( sigmoid(ignore_eventx + ignore_hidden), np.tanh(learn_eventx + learn_hidden) )


def cell_state(forget_gate_output, input_gate_output):
    return forget_gate_output + input_gate_output

  
def output_gate(x, h, Weights_ho, Bias_ho, Weights_xo, Bias_xo, cell_state):
    out_hidden = np.dot(Weights_ho, h) + Bias_ho
    out_eventx = np.dot(Weights_xo, x) + Bias_xo
    return np.multiply( sigmoid(out_eventx + out_hidden), np.tanh(cell_state) )


我们还需要Sigmoid函数。
def sigmoid(x):
    return 1/(1 + np.exp(-x))

因为PyTorch以堆叠方式存储权重,所以我们需要将其拆分,因此我们需要以下函数。
def get_slices(hidden_dim):
    slices=[]
    breaker=(hidden_dim*4)
    slices=[[i,i+3] for i in range(0, breaker, breaker//4)]
    return slices

现在我们已经准备好了用于LSTM的函数,接下来我们创建一个LSTM类,从PyTorch类中复制权重并获取输出。
class numpy_lstm:
    def __init__( self, layer_num=0, hidden_dim=1, matching_in_out=False):
        self.matching_in_out=matching_in_out
        self.layer_num=layer_num
        self.hidden_dim=hidden_dim
        
    def init_weights_from_pytorch(self, state):
        slices=get_slices(self.hidden_dim)
        print (slices)

        #Event (x) Weights and Biases for all gates
        
        lstm_weight_ih='lstm.weight_ih_l'+str(self.layer_num)
        self.Weights_xi = state[lstm_weight_ih][slices[0][0]:slices[0][1]].numpy()  # shape  [h, x]
        self.Weights_xf = state[lstm_weight_ih][slices[1][0]:slices[1][1]].numpy()  # shape  [h, x]
        self.Weights_xl = state[lstm_weight_ih][slices[2][0]:slices[2][1]].numpy()  # shape  [h, x]
        self.Weights_xo = state[lstm_weight_ih][slices[3][0]:slices[3][1]].numpy() # shape  [h, x]

        
        lstm_bias_ih='lstm.bias_ih_l'+str(self.layer_num)
        self.Bias_xi = state[lstm_bias_ih][slices[0][0]:slices[0][1]].numpy()  #shape is [h, 1]
        self.Bias_xf = state[lstm_bias_ih][slices[1][0]:slices[1][1]].numpy()  #shape is [h, 1]
        self.Bias_xl = state[lstm_bias_ih][slices[2][0]:slices[2][1]].numpy()  #shape is [h, 1]
        self.Bias_xo = state[lstm_bias_ih][slices[3][0]:slices[3][1]].numpy() #shape is [h, 1]
        
        
        lstm_weight_hh='lstm.weight_hh_l'+str(self.layer_num)

        #Hidden state (h) Weights and Biases for all gates
        self.Weights_hi = state[lstm_weight_hh][slices[0][0]:slices[0][1]].numpy()  #shape is [h, h]
        self.Weights_hf = state[lstm_weight_hh][slices[1][0]:slices[1][1]].numpy()  #shape is [h, h]
        self.Weights_hl = state[lstm_weight_hh][slices[2][0]:slices[2][1]].numpy()  #shape is [h, h]
        self.Weights_ho = state[lstm_weight_hh][slices[3][0]:slices[3][1]].numpy() #shape is [h, h]
        
        
        lstm_bias_hh='lstm.bias_hh_l'+str(self.layer_num)

        self.Bias_hi = state[lstm_bias_hh][slices[0][0]:slices[0][1]].numpy()  #shape is [h, 1]
        self.Bias_hf = state[lstm_bias_hh][slices[1][0]:slices[1][1]].numpy()  #shape is [h, 1]
        self.Bias_hl = state[lstm_bias_hh][slices[2][0]:slices[2][1]].numpy()  #shape is [h, 1]
        self.Bias_ho = state[lstm_bias_hh][slices[3][0]:slices[3][1]].numpy() #shape is [h, 1]
    def forward_lstm_pass(self,input_data):
        h = np.zeros(self.hidden_dim)
        c = np.zeros(self.hidden_dim)
        
        output_list=[]
        for eventx in input_data:
            f = forget_gate(eventx, h, self.Weights_hf, self.Bias_hf, self.Weights_xf, self.Bias_xf, c)
            i =  input_gate(eventx, h, self.Weights_hi, self.Bias_hi, self.Weights_xi, self.Bias_xi, 
                        self.Weights_hl, self.Bias_hl, self.Weights_xl, self.Bias_xl)
            c = cell_state(f,i)
            h = output_gate(eventx, h, self.Weights_ho, self.Bias_ho, self.Weights_xo, self.Bias_xo, c)
            if self.matching_in_out: # doesnt make sense but it was as it was in main code :(
                output_list.append(h)
        if self.matching_in_out:
            return output_list
        else:
            return h


同样地,对于全连接层,
    
    
class fully_connected_layer:
    def __init__(self,state, dict_name='fc', ):
        self.fc_Weight = state[dict_name+'.weight'][0].numpy()
        self.fc_Bias = state[dict_name+'.bias'][0].numpy() #shape is [,output_size]
        
    def forward(self,lstm_output, is_sigmoid=True):
        res=np.dot(self.fc_Weight, lstm_output)+self.fc_Bias
        print (res)
        if is_sigmoid:
            return sigmoid(res)
        else:
            return res
        

现在我们需要一个类将它们全部调用起来,并针对多层进行泛化。如果您需要更多的全连接层或想为sigmoid设置false条件,可以修改下面的类。
        
class RNN_model_Numpy:
    def __init__(self, state, input_size, hidden_dim, output_size, num_layers, matching_in_out=True):
        self.lstm_layers=[]
        for i in range(0, num_layers):
            lstm_layer_obj=numpy_lstm(layer_num=i, hidden_dim=hidden_dim, matching_in_out=True)
            lstm_layer_obj.init_weights_from_pytorch(state) 
            self.lstm_layers.append(lstm_layer_obj)
        
        self.hidden2out=fully_connected_layer(state, dict_name='hidden2out')
        
    def forward(self, feature_list):
        for x in self.lstm_layers:
            lstm_output=x.forward_lstm_pass(feature_list)
            feature_list=lstm_output
            
        return self.hidden2out.forward(feature_list, is_sigmoid=False)

对numpy变量进行健全性检查:
data = np.array(
           [[1,1],
            [2,2],
            [3,3]])



check=RNN_model_Numpy(state, input_size, hidden_dim, output_size, num_layers)
check.forward(data)

解释: 由于我们只需要前向传递,因此我们需要一些在LSTM中所需的函数,因此我们有遗忘门、输入门、单元门和输出门。它们只是在您提供的输入上执行的一些操作。
对于get_slices函数,它用于分解我们从pytorch状态字典中获得的权重矩阵(状态字典是包含我们网络中所有层的权重的字典)。特别是对于LSTM,我们将其按顺序排列为ignore、forget、learn、output。因此,我们需要将其分解为不同的LSTM单元。
对于numpy_lstm类,我们有一个必须调用的init_weights_from_pytorch函数,它将从我们之前从pytorch模型对象中获取的状态字典中提取权重,然后使用pytorch权重填充numpy数组权重。您可以先训练您的模型,然后通过pickle保存状态字典,然后再使用它。
全连接层类只实现了hidden2out神经网络。
最后,我们的rnn_model_numpy类是为了确保如果您有多个层,则能够将一个lstm层的输出发送到另一个lstm层。
最后,对数据变量进行了小的合理性检查。
重要提示:请注意,由于PyTorch处理输入的方式完全不同,因此您可能会遇到尺寸错误,请确保您的输入numpy具有与数据变量相似的形状。
重要参考文献: https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html

https://christinakouridi.blog/2019/06/19/backpropagation-lstm/


非常有用,谢谢。但是在代码中有一些我无法轻松重用的点。是否可能提供等效于以下代码的代码: rnn = nn.LSTM(10, 20, 2) input = torch.randn(5, 3, 10) h0 = torch.randn(2, 3, 20) c0 = torch.randn(2, 3, 20) output, (hn, cn) = rnn(input, (h0, c0)) 以及等效于以下代码的代码: m = nn.Linear(20, 30) input = torch.randn(128, 20) output = m(input)这样我们就可以比较numpy等效于pytorch的输出,并且也可以拥有LSTM和线性模块的等效版本,再次感谢! - hmghaly
在上面的评论中没有清楚地显示,我在问题的末尾包含了一个更新,如果您能帮忙解决这个问题,我将不胜感激。 - hmghaly
解决方案是针对pytorch权重制定的。所以你只需要简单地定义输入大小、输出大小等,创建你在问题中展示的RNN类,其中也包括线性神经网络层。然后你需要调用numpy,并像我最后一步所做的那样给出状态字典和形状输入。我的代码只是复制了pytorch的权重,并根据权重数量轻松地知道了神经网络的形状。 - ibadia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接