私は100,000を超える画像を反復処理し、いくつかの画像機能をキャプチャして、結果のdataFrameをピクルスファイルとしてディスクに保存しようとしています。
残念ながらRAM=制約のため、画像を20,000のチャンクに分割し、結果をディスクに保存する前にそれらの操作を実行する必要があります。
以下のコードは、次の20,000画像を処理するループを開始する前に、20,000画像の結果のデータフレームを保存することになっています。
ただし、メモリがRAM=最初のforループの終わりに解放されないため、これは私の問題を解決していないようです
したがって、50,000番目のレコードの処理中に、メモリ不足エラーが原因でプログラムがクラッシュします。
オブジェクトをディスクに保存してガベージコレクターを起動した後、オブジェクトを削除しようとしましたが、RAMの使用率が低下していないようです。
何が欠けていますか?
#file_list_1 contains 100,000 images
file_list_chunks = list(divide_chunks(file_list_1,20000))
for count,f in enumerate(file_list_chunks):
# make the Pool of workers
pool = ThreadPool(64)
results = pool.map(get_image_features,f)
# close the pool and wait for the work to finish
list_a, list_b = Zip(*results)
df = pd.DataFrame({'filename':list_a,'image_features':list_b})
df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")
del list_a
del list_b
del df
gc.collect()
pool.close()
pool.join()
print("pool closed")
さて、5万分の1が非常に大きく、それがOOMの原因となっている可能性があるため、これをテストするために最初に試してみます。
file_list_chunks = list(divide_chunks(file_list_1,20000))[30000:]
10,000で失敗した場合、これは20kがチャンクサイズが大きすぎるかどうかを確認します。または、50,000で再び失敗した場合は、コードに問題があります...
さて、コードに...
まず、明示的なlist
コンストラクターは必要ありません。リスト全体をメモリに生成するよりも、pythonで反復する方がはるかに優れています。
file_list_chunks = list(divide_chunks(file_list_1,20000))
# becomes
file_list_chunks = divide_chunks(file_list_1,20000)
ここでThreadPoolを誤用している可能性があります。
これ以上のタスクがプールに送信されないようにします。すべてのタスクが完了すると、ワーカープロセスが終了します。
これはclose
のように読みますが、いくつかの考えがまだ実行されている可能性がありますが、これは安全だと思いますが、Pythonに少し違和感があるようです。ThreadPoolのコンテキストマネージャを使用する方が良いでしょう。
with ThreadPool(64) as pool:
results = pool.map(get_image_features,f)
# etc.
python 実際にメモリを解放することは保証されていません の明示的なdel
s。
あなたは後結合/後を収集する必要があります:
with ThreadPool(..):
...
pool.join()
gc.collect()
また、これを小さい部分に分割してみることもできます。 10,000以下!
pandas DataFramesと大きなリストを使用する代わりに、ここで行うことを検討します。SQLデータベースを使用することです。これは sqlite でローカルに実行できます。
import sqlite3
conn = sqlite3.connect(':memory:', check_same_thread=False) # or, use a file e.g. 'image-features.db'
そしてコンテキストマネージャを使用します:
with conn:
conn.execute('''CREATE TABLE images
(filename text, features text)''')
with conn:
# Insert a row of data
conn.execute("INSERT INTO images VALUES ('my-image.png','feature1,feature2')")
そうすれば、大きなリストオブジェクトやDataFrameを処理する必要がなくなります。
各スレッドに接続を渡すことができます...次のような少し奇妙なことをする必要があるかもしれません:
results = pool.map(get_image_features, Zip(itertools.repeat(conn), f))
次に、計算が完了したら、データベースからすべてを選択して、好きな形式に変換できます。例えば。 read_sql を使用します。
別のpython "Shell out"の同じインスタンスで実行するのではなく、ここでサブプロセスを使用します。
Startとendをsys.argsとしてpythonに渡すことができるため、これらをスライスできます。
# main.py
# a for loop to iterate over this
subprocess.check_call(["python", "chunk.py", "0", "20000"])
# chunk.py a b
for count,f in enumerate(file_list_chunks):
if count < int(sys.argv[1]) or count > int(sys.argv[2]):
pass
# do stuff
このようにして、サブプロセスは適切にクリーンアップしますpython(プロセスが終了するため、メモリリークが発生することはありません)。
私の賭けは、ハンマー1が進むべき道であり、大量のデータを結合し、pythonリストに不必要に読み込み、sqlite3(または他のデータベース)を使用しているように感じる)それを完全に回避します。
注:これは答えではなく、質問と提案のクイックリストです
ThreadPool()
_from multiprocessing.pool
_を使用していますか? (_python3
_に)十分に文書化されていないため、 ThreadPoolExecutor を使用したい( here も参照)sys.getsizeof()
に依存して、宣言されたすべてのglobals()
のリストと、それらのメモリフットプリントが返されます。del results
_を呼び出します(これはそれほど大きくないはずですが)あなたの問題は、マルチプロセッシングを使用する必要があるスレッドを使用していることです(CPUバインドvs IOバインド)。
私はあなたのコードを次のように少しリファクタリングします:
from multiprocessing import Pool
if __name__ == '__main__':
cpus = multiprocessing.cpu_count()
with Pool(cpus-1) as p:
p.map(get_image_features, file_list_1)
そして、私は関数を変更しますget_image_features
(これらのような2つの行を末尾に追加するなど)。これらの画像をどのように処理しているのか正確にはわかりませんが、各プロセス内ですべての画像を処理し、すぐにディスクに保存するという考え方です。
df = pd.DataFrame({'filename':list_a,'image_features':list_b})
df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")
したがって、データフレームは、終了した後ではなく、各プロセス内でピクル化されて保存されます。プロセスは終了するとすぐにメモリから消去されるため、これはメモリのフットプリントを低く保つために機能するはずです。
一部のLinuxビルドではpd.DataFrame(...)
がリークする可能性があるため(github issue and "workaround" を参照)、del df
でも役に立たない場合があります。
あなたの場合、githubからのソリューションはpd.DataFrame.__del__
のモンキーパッチなしで使用できます:
from ctypes import cdll, CDLL
try:
cdll.LoadLibrary("libc.so.6")
libc = CDLL("libc.so.6")
libc.malloc_trim(0)
except (OSError, AttributeError):
libc = None
if no libc:
print("Sorry, but pandas.DataFrame may leak over time even if it's instances are deleted...")
CHUNK_SIZE = 20000
#file_list_1 contains 100,000 images
with ThreadPool(64) as pool:
for count,f in enumerate(divide_chunks(file_list_1, CHUNK_SIZE)):
# make the Pool of workers
results = pool.map(get_image_features,f)
# close the pool and wait for the work to finish
list_a, list_b = Zip(*results)
df = pd.DataFrame({'filename':list_a,'image_features':list_b})
df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")
del df
# 2 new lines of code:
if libc: # Fix leaking of pd.DataFrame(...)
libc.malloc_trim(0)
print("pool closed")
追伸このソリューションは、単一のデータフレームが大きすぎる場合には役立ちません。これはCHUNK_SIZE
を減らすことによってのみ助けることができます
つまり、Pythonインタプリタでメモリを解放することはできません。各プロセスが独自にメモリを処理できるため、マルチプロセッシングを使用するのが最善の策です。
ガベージコレクターはメモリを「解放」しますが、期待するコンテキストではありません。ページとプールの処理は、CPythonソースで調べることができます。高レベルの記事もここにあります: https://realpython.com/python-memory-management/
List()を呼び出さないでください。これは、divide_chunks()から返されるもののメモリ内リストを作成します。あなたの記憶の問題がおそらく起こっているところです。
Memeoryにあるすべてのデータを一度に必要とするわけではありません。一度に1つずつファイル名を反復処理するだけで、すべてのデータが一度にメモリに存在するわけではありません。
スタックトレースを投稿して、詳細情報を入手してください
この種の問題に対する私の解決策は、並列処理ツールを使用することです。ローカルで作成された関数(「実装の詳細」であり、モジュール内でグローバルにしないことをお勧めします)でも並列化できるので、私は joblib を好みます。私の他のアドバイス:pythonではスレッド(およびスレッドプール)を使用せず、代わりにプロセス(およびプロセスプール)を使用してください。 joblibに少なくとも2つのプロセスのプールを作成してください。そうしないと、元のpythonプロセスですべてが実行されるため、最終的にRAMは解放されません。 joblibワーカープロセスが自動的に閉じられると、それらが割り当てたRAMはOSによって完全に解放されます。私のお気に入りの武器は joblib.Parallel です。ワーカーに大きなデータ(2GBより大きい)を転送する必要がある場合は、 joblib.dump (pythonオブジェクトをメインプロセスのファイルに書き込む)を使用し、- joblib.load (ワーカープロセスで読み取るため)。
_del object
_について:Pythonでは、コマンドは実際にはオブジェクトを削除しません。参照カウンターを減らすだけです。 import gc; gc.collect()
を実行すると、ガベージコレクターは解放するメモリと割り当てたままにするメモリを自分で決定します。可能な限りすべてのメモリを強制的に解放する方法は知りません。さらに悪いことに、一部のメモリが実際にpythonではなく、たとえば、一部の外部C/C++/Cython/etcコードで割り当てられ、コードがpythonを関連付けなかった場合メモリでカウンターを参照すると、上で書いたものを除いて、Pythonからそれを解放するためにできることはまったくありません。つまり、RAMを割り当てたpythonプロセスを終了することで、その場合はOSによって解放されることが保証されています。そのためPythonで一部のメモリを解放する唯一の100%信頼できる方法は、並列プロセスでそれを割り当てるコードを実行して、プロセスを終了することです。