簡體   English   中英

多處理日志記錄 - 如何將 loguru 與 joblib Parallel 一起使用

[英]Multiprocessing Logging - How to use loguru with joblib Parallel

我有一堆 Python 腳本來運行一些數據科學模型。 這需要相當長的時間,加快速度的唯一方法是使用多處理。 為此,我使用了joblib庫,它運行良好。 然而,不幸的是,這會擾亂日志記錄,並且控制台 output 也會出現亂碼(然而,這是預期的),因為所有進程同時轉儲各自的輸出。

我是使用logging庫的新手,並按照其他一些 SO 答案嘗試讓它工作。 我正在使用 8 個核心進行處理。 使用 SO 上的答案,我寫出日志文件,並預計每次迭代都會有 8 個新文件。 但是,它在第一次迭代中創建了 8 個文件,並且在每個循環中僅寫入/附加到這 8 個文件。 這有點不方便,所以我進一步探索並找到logurulogzero 雖然它們都涵蓋了使用multiprocessing的示例,但它們都沒有展示如何將其與joblib一起使用。 這是我到目前為止所擁有的:

運行模型.py

import math
import multiprocessing
import time
from datetime import datetime
from loguru import logger

import pandas as pd
import psutil
from joblib import Parallel, delayed

import helper
import log
import prep_data
import stock_subscriber_data
import train_model


def get_pred(cust_df, stock_id, logger):

    logger.info('--------------------------------Stock loop {}-------------------------------'.format(stock_id))

    cust_stockid_df = stock_subscriber_data.get_stockid_data(cust_df, stock_id)
    weekly_timeseries, last_date, abn_df = prep_data.prep(cust_stockid_df, logger)  
    single_row_df = stock_subscriber_data.get_single_row(cust_df, stock_id)

    stock_subscriber_data.write_data(abn_df, 't1')
    test_y, prd = train_model.read_train_write(cust_df, stock_id, weekly_timeseries, last_date, logger)

    return True


def main():

    cust_df = stock_subscriber_data.get_data()
    cust_df = helper.clean_data(cust_df)
    stock_list = cust_df['intStockID'].unique()

    max_proc = max(math.ceil(((psutil.virtual_memory().total >> 30) - 100) / 50), 1)
    num_cores = min(multiprocessing.cpu_count(), max_proc)

    logger.add("test_loguru.log", format="{time} {level}: ({file}:{module} - {line}) >> {message}", level="INFO", enqueue=True)

    Parallel(n_jobs=num_cores)(delayed(get_pred)(cust_df, s, logger) for s in stock_list)


if __name__ == "__main__":
    main()

訓練模型.py

import math
from datetime import datetime
from itertools import product
from math import sqrt

import pandas as pd
from keras import backend
from keras.layers import Dense
from keras.layers import LSTM
from keras.models import Sequential
from numpy import array
from numpy import mean
from pandas import DataFrame
from pandas import concat
from sklearn.metrics import mean_squared_error

import helper
import stock_subscriber_data

# bunch of functions here that don't need logging...

# walk-forward validation for univariate data
def walk_forward_validation(logger, data, n_test, cfg):
    #... do stuff here ...
    #... and here ...
    logger.info('{0:.3f}'.format(error))
    return error, model


# score a model, return None on failure
def repeat_evaluate(logger, data, config, n_test, n_repeats=10):
    #... do stuff here ...
    #... and here ...
    logger.info('> Model{0} {1:.3f}'.format(key, result))
    return key, result, best_model



def read_train_write(data_df, stock_id, series, last_date, logger):
    #... do stuff here ...
    #... and here ...
    logger.info('done')

    #... do stuff here ...
    #... and here ...

    # bunch of logger.info() statements here... 
    #
    #
    #
    #

    #... do stuff here ...
    #... and here ...

    return test_y, prd

當一次只有一個進程時,這很有效。 但是,我得到一個_pickle.PicklingError: Could not pickle the task to send it to the workers. 在多進程模式下運行時出錯。 我究竟做錯了什么? 我該如何解決這個問題? 我不介意切換到logurulogzero以外的其他東西,只要我可以創建一個包含連貫日志的文件,甚至是n文件,每個文件都包含joblib每次迭代的日志。

我通過修改我的run_models.py讓它工作。 現在,我每個循環都有一個日志文件。 這會創建大量日志文件,但它們都與每個循環相關,而不是混亂或任何東西。 我猜,一次一個步驟。 這是我所做的:

運行模型.py

import math
import multiprocessing
import time
from datetime import datetime
from loguru import logger

import pandas as pd
import psutil
from joblib import Parallel, delayed

import helper
import log
import prep_data
import stock_subscriber_data
import train_model


def get_pred(cust_df, stock_id):

    log_file_name = "log_file_{}".format(stock_id)

    logger.add(log_file_name, format="{time} {level}: ({file}:{module} - {line}) >> {message}", level="INFO", enqueue=True)

    logger.info('--------------------------------Stock loop {}-------------------------------'.format(stock_id))

    cust_stockid_df = stock_subscriber_data.get_stockid_data(cust_df, stock_id)
    weekly_timeseries, last_date, abn_df = prep_data.prep(cust_stockid_df, logger)  
    single_row_df = stock_subscriber_data.get_single_row(cust_df, stock_id)

    stock_subscriber_data.write_data(abn_df, 't1')
    test_y, prd = train_model.read_train_write(cust_df, stock_id, weekly_timeseries, last_date, logger)

    return True


def main():

    cust_df = stock_subscriber_data.get_data()
    cust_df = helper.clean_data(cust_df)
    stock_list = cust_df['intStockID'].unique()

    max_proc = max(math.ceil(((psutil.virtual_memory().total >> 30) - 100) / 50), 1)
    num_cores = min(multiprocessing.cpu_count(), max_proc)

    Parallel(n_jobs=num_cores)(delayed(get_pred)(cust_df, s) for s in stock_list)


if __name__ == "__main__":
    main()

因此,將 loguru 與 joblib 結合使用的正確方法是將后端更改為多處理。

from loguru import logger
from joblib import Parallel, delayed
from tqdm.autonotebook import tqdm 

logger.remove()
logger.add(sys.stdout, level = 'INFO', enqueue=True)

logger.info('test')
logger.debug('should not appear')

def do_thing(i):
    logger.info('item %i' %i)
    logger.debug('should not appaear')
    return None


Parallel(n_jobs=4, backend='multiprocessing')(
    delayed(do_thing)(i)
    for i in tqdm(range(10))
)



Parallel(n_jobs=4)(
    delayed(do_thing)(i)
    for i in tqdm(range(10))
)

第一個並行調用有效。 第二個得到你之前提到的舊問題

核心概念:主進程和每個子進程都需要調用logger.add() 您可以使用相同的文件名將 pipe 所有日志保存到同一個文件中。

# Pseudo-code to get the idea
def main():
    logfile = 'execution.log'
    # Use enqueue to ensure works properly with multiprocessing
    logger.add(logfile, enqueue=True)
    ...
    # Add logfile to the params passed to get_pred
    Parallel(n_jobs=num_cores)(delayed(get_pred)(cust_df, s, logfile) for s in stock_list)

# Add logfile as param to get_pred
def get_pred(cust_df, stock_id, logfile):
    # Add the *same* logfile each time the child process is called!
    logger.add(logfile, enqueue=True)
    # Add identifiers to log messages to distinguish them
    logger.info(f'{stock_id} - more info')
    # ...

@CodinginCircles answer中,在get_pred() function 中,他們每次都使用唯一的日志文件名稱調用logger.add() 這會創建許多不同的日志文件。

相反,您可以每次使用相同的名稱調用logger.add() ,所有日志都將 go 到同一個日志文件。 設置enqueue=True將有助於確保它與多處理一起正常工作。

要知道哪個日志對應於哪個事物(在我們的例子中是股票),只需將股票名稱添加到日志消息中,例如logger.info(f'{stock_id} - more info')

此外,我發現在logger.add()中添加backtrace=True, diagnose=True作為參數在使用 Loguru 時特別有用。

最后注意:您還可以將LOGFILE = 'execution.log'定義為 function 定義之外的常量,這樣您就不需要將其作為參數傳遞給get_pred() 上面概述的方法可以讓您對日志文件名做更多的事情,就像給它一個獨特的時間簽名一樣。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM