Word2Vec + LSTM on API Sequence

Viewed 963

I am trying to apply word2Vec and LSTM on a dataset that contains files' API trace log including API function calls and their parameters for a binary classification.

The data looks like:

File_ID,    Label,   API Trace log
 1,           M,      kernel32 LoadLibraryA kernel32.dll
                      kernel32 GetProcAddress MZ\x90 ExitProcess
                      ...

 2,           V,     kernel32 GetModuleHandleA RPCRT4.dll
                     kernel32 GetCurrentThreadId d\x8B\x0D0 POINTER POINTER
                     ...

The API trace including: module name, API function name, parameters (that separated by blank space)

Take first API trace of file 1 as example, kernel32 is the module name, LoadLibraryA is function name, kernel32.dll is parameter. Each API trace is separated by \n so that each line represents a API sequence information sequentially.

Firstly I trained a word2vec model based on the line sentence of all API trace log. There are about 5k API function calls, e.g. LoadLibraryA, GetProcAddress. However, because parameter value could be vary, the model becomes quite big (with 300,000 vocabulary) after including those parameters.

After that, I trained a LSTM by applying word2vec's embedding_wrights, the model structure looks like:

model = Sequential() 
model.add(Embedding(output_dim=vocab_dim, input_dim=n_symbols, \
                mask_zero=False, weights=[embedding_weights], \
                trainable=False))
model.add(LSTM(dense_dim,kernel_initializer='he_normal', dropout=0.15, 
recurrent_dropout=0.15, implementation=2))
model.add(Dropout(0.3))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=batch_size, callbacks=[early_stopping, parallel_check_cb])

The way I get embedding_weights is to create a matrix, for each vocabulary in word2vec model, map the index of the word in the model, to it's vector

def create_embedding_weights(model, max_index=0):
    # dimensionality of your word vectors
    num_features = len(model[model.vocab.keys()[0]])
    n_symbols = len(model.vocab) + 1  # adding 1 to account for 0th index (for masking)
    # Only word2vec feature set
    embedding_weights = np.zeros((max(n_symbols + 1, max_index + 1), num_features))
    for word, value in model.vocab.items():
        embedding_weights[value.index, :] = model[word]

    return embedding_weights

For training data, what I did is that for each word in API call, convert the actual word to the index of word2vec model so that it's consistent to the index in embedding_weights above. e.g. kernel32 -> 0, LoadLibraryA -> 1, kernel32.dll -> 2. GetProcAddress -> 4, MZ\x90 -> 5, ExitProcess ->6

So the train data for file 1 looks like [0, 1, 2, 3, 4, 5, 6]. Noted, I didn't do line split for each API trace. As a result, the model may not know where is the start and end of API trace? And the training accuracy of the model is pretty bad - accuracy is 50% :(

My question is that, when prepare the training and validation dataset, should I also split the line when mapping the actual words to their index? then The above training data would be changed to following, each API trace is separated by a line, and maybe padd the missing value to -1 which doesn't exist in word2vec's indexes.

[[0, 1, 2, -1]
 [3, 4, 5, 6]]

Meanwhile I am using a very simple structure for training, while word2vec model is quite big, any suggestion on structure would also be appreciated.

1 Answers

I would at least split the trace lines in three:

  • Module (make a dictionary and an embedding)
  • Function (make a dictionary and an embedding)
  • Parameters (make a dictionary and an embedding - see details later)

Since this is a very specific application, I believe it would be best to keep the embeddings trainable (the whole point of the embeddings is to create meaningful vectors, and the meanings depend a lot on the model that is going to use them. Question: how did you create the word2vec model? From what data does it learn?).

This model would have more inputs. All of them as integers from zero to max dictionary index. Consider using mask_zero=True and padding all files to maxFileLines.

moduleInput = Input(maxFileLines,) 
functionInput = Input(maxFileLines,)    

For the parameters, I'd probably make a subsequence as if the list of parameters were a sentence. (Again, mask_zero=True, and pad up to maxNumberOfParameters)

parametersInput = Input(maxFileLines, maxNumberOfParameters)

Function and module embeddings:

moduleEmb = Embedding(.....mask_zero=True,)(moduleInput)    
functionEmb = Embedding(.....mask_zero=True)(functionInput)

Now, for the parameters, I though of creating a sequence of sequences (maybe this is too much). For that, I first transfer the lines dimension to the batch dimension and work with only length = maxNumberOfParameters:

paramEmb = Lambda(lambda x: K.reshape(x,(-1,maxNumberOfParameters)))(parametersInput)
paramEmb = Embedding(....,mask_zero=True)(paramEmb)
paramEmb = Lambda(lambda x: K.reshape(x,(-1,maxFileLines,embeddingSize)))(paramEmb)

Now we concatenate all of them in the last dimension and we're ready to get into the LSTMs:

joinedEmbeddings = Concatenate()([moduleEmb,functoinEmb,paramEmb])
out = LSTM(...)(joinedEmbeddings)
out = ......

model = Model([moduleInput,functionInput,parametersInput], out)

How to prepare the inputs

With this model, you need three separate inputs. One for the module, one for the function and one for the parameters.

These inputs will contain only indices (no vectors). And they don't need a previous word2vec model. Embeddings are word2vec transformers.

So, get the file lines and split. First we split by commas, then we split the API calls by spaces:

import numpy as np

#read the file
loadedFile = open(fileName,'r')
allLines = [l.strip() for l in loadedFile.readlines()] 
loadedFile.close()

#split by commas
splitLines = []
for l in allLines[1:]: #use 1 here only if you have headers in the file
    splitLines.append (l.split(','))
splitLines = np.array(splitLines)

#get the split values and separate ids, targets and calls
ids = splitLines[:,0]
targets = splitLines[:,1]
calls = splitLines[:,2]

#split the calls by space, adding dummy parameters (spaces) to the max length
splitCalls = []
for c in calls:
    splitC = c.strip().split(' ')

    #pad the parameters (space for dummy params)
    for i in range(len(splitC),maxParams+2):
        splitC.append(' ') 

    splitCalls.append(splitC)

splitCalls = np.array(splitCalls)

modules = splitCalls[:,0]
functions = splitCalls[:,1]
parameters = splitCalls[:,2:] #notice the parameters have an extra dimension

Now lets make the indices:

modIndices, modCounts = np.unique(modules,return_counts=True)
funcIndices, funcCounts = np.unique(functions,return_counts=True)

#for de parameters, let's flatten the array first (because we have 2 dimensions)
flatParams = parameters.reshape((parameters.shape[0]*parameters.shape[1],))
paramIndices, paramCounts = np.unique(flatParams,return_counts=True)

These will create a list of unique words and get their counts. Here you can customize which words you're going to group in "another word" class. (Maybe based on the counts, if the count is too little, make it an "another word").

Let's then make the dictionaries:

def createDic(uniqueWords):
    dic = {}
    for i,word in enumerate(uniqueWords):
         dic[word] = i + 1 # +1 because we want to reserve the zeros for padding     
    return dic

Just take care with the parameters, because we used a dummy space there:

moduleDic = createDic(modIndices)
funcDic = createDic(funcIndices)
paramDic = createDic(paramIndices[1:]) #make sure the space got the first position here    
paramDic[' '] = 0

Well, now we just replace the original values:

moduleData = [moduleDic[word] for word in modules]
funcData = [funcDic[word] for word in functions]
paramData = [[paramDic[word] for word in paramLine] for paramLine in parameters]

Pad them:

for i in range(len(moduleData),maxFileLines):
    moduleData.append(0)
    funcData.append(0)
    paramData.append([0] * maxParams)

Do this for every file, and store in a list of files:

moduleTrainData = []  
functionTrainData = []
paramTrainData = []
for each file do the above and:
    moduleTrainData.append(moduleData)
    functionTrainData.append(funcData)
    paramTrainData.append(paramData)

moduleTrainData = np.asarray(moduleTrainData)
functionTrainData = np.asarray(functionTrainData)
paramTrainData = np.asarray(paramTrainData)

That's all for the inputs.

model.fit([moduleTrainData,functionTrainData,paramTrainData],outputLabels,...)
Related